feat: New features (research, invest, gambling, new game+, trophies)
This commit is contained in:
+85
-11
@@ -1,5 +1,6 @@
|
||||
// Entry point: wires the engine, the services (cookies, i18n, themes) and the UI.
|
||||
|
||||
import { abandonContract, acceptContract, claimEvent } from './engine/contracts.js';
|
||||
import {
|
||||
advance,
|
||||
buyGenerator,
|
||||
@@ -11,6 +12,10 @@ import {
|
||||
setPrice,
|
||||
simulateOffline,
|
||||
} from './engine/engine.js';
|
||||
import { buyChip, buyReliability, gamble } from './engine/gamble.js';
|
||||
import { buyShares, sellShares } from './engine/investments.js';
|
||||
import { buyPerk, prestige } from './engine/legacy.js';
|
||||
import { buyLab, buyResearch } from './engine/research.js';
|
||||
import { createState, sanitizeState, serializeState } from './engine/state.js';
|
||||
import { fetchLocales, fetchThemes, loadGameContext } from './services/data.js';
|
||||
import { createFormatter } from './services/format.js';
|
||||
@@ -26,8 +31,16 @@ import {
|
||||
takeLegacySaves,
|
||||
} from './services/storage.js';
|
||||
import { applyThemeStyles } from './services/themes.js';
|
||||
import { createAchievementsView } from './ui/achievementsView.js';
|
||||
import { createContractsView } from './ui/contractsView.js';
|
||||
import { $, refreshStaticIcons, setThemeAssets } from './ui/dom.js';
|
||||
import { createGambleView } from './ui/gambleView.js';
|
||||
import { createInvestView } from './ui/investView.js';
|
||||
import { createLegacyView } from './ui/legacyView.js';
|
||||
import { createResearchView } from './ui/researchView.js';
|
||||
import { createSettings } from './ui/settings.js';
|
||||
import { createTabs } from './ui/tabs.js';
|
||||
import { formatDuration, moneyFormatter, rewardText } from './ui/text.js';
|
||||
import { showToast } from './ui/toast.js';
|
||||
import { createView } from './ui/view.js';
|
||||
|
||||
@@ -42,6 +55,7 @@ let themeId;
|
||||
let locale;
|
||||
let state;
|
||||
let fmt;
|
||||
let tabs;
|
||||
let income = 0;
|
||||
let lastSaveError = 0;
|
||||
const themeNames = new Map();
|
||||
@@ -115,7 +129,7 @@ async function fillSelects() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Reload texts, assets and the lists for the current theme + language. */
|
||||
/** Reload texts, assets and every screen for the current theme + language. */
|
||||
async function refreshTexts() {
|
||||
await loadDictionary(locale, themeId);
|
||||
fmt = createFormatter(locale);
|
||||
@@ -123,6 +137,9 @@ async function refreshTexts() {
|
||||
applyTranslations();
|
||||
document.title = t('theme.title');
|
||||
view.rebuild(ctx, fmt);
|
||||
for (const screen of Object.values(screens)) screen.rebuild(ctx, fmt);
|
||||
tabs.rebuild();
|
||||
refreshStaticIcons();
|
||||
await fillSelects();
|
||||
}
|
||||
|
||||
@@ -137,11 +154,26 @@ async function setTheme(id) {
|
||||
|
||||
// ------------------------------------------------------------------ the loop
|
||||
|
||||
function formatDuration(seconds) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 1) return t('time.seconds', { value: Math.round(seconds) });
|
||||
if (minutes < 60) return t('time.minutes', { value: minutes });
|
||||
return t('time.hoursMinutes', { hours: Math.floor(minutes / 60), minutes: minutes % 60 });
|
||||
/** Tell the player about what just happened (see `notices` in engine.advance). */
|
||||
function announce(notice) {
|
||||
switch (notice.kind) {
|
||||
case 'contract_done':
|
||||
showToast(t('toast.contractDone', { reward: rewardText(notice.reward, fmt, notice.seconds) }));
|
||||
break;
|
||||
case 'contract_failed':
|
||||
showToast(t('toast.contractFailed'), { error: true });
|
||||
break;
|
||||
case 'event_opportunity':
|
||||
showToast(t('toast.eventOpportunity', { title: t(`event.${notice.type}.title`) }));
|
||||
break;
|
||||
case 'event_incident':
|
||||
showToast(t('toast.eventIncident', { title: t(`event.${notice.type}.title`) }), { error: true });
|
||||
break;
|
||||
case 'achievement':
|
||||
showToast(t('toast.achievement', { title: t(`ach.title.${notice.id}`) }));
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply progress made while the game was closed or the device asleep. */
|
||||
@@ -152,7 +184,7 @@ function catchUp(elapsedSeconds) {
|
||||
t('toast.welcomeBack', {
|
||||
duration: formatDuration(report.seconds),
|
||||
produced: fmt.number(Math.floor(report.produced)),
|
||||
funds: t('theme.fmt.funds', { value: fmt.money(report.revenue) }),
|
||||
funds: moneyFormatter(fmt)(report.revenue),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -168,14 +200,19 @@ function frame() {
|
||||
if (dt >= ctx.rules.offline.minSeconds) {
|
||||
catchUp(dt); // tab was frozen or the device slept
|
||||
} else {
|
||||
const { revenue } = advance(state, ctx, dt);
|
||||
income += (revenue / dt - income) * Math.min(1, dt / INCOME_SMOOTHING_SECONDS);
|
||||
const totals = advance(state, ctx, dt);
|
||||
income += (totals.revenue / dt - income) * Math.min(1, dt / INCOME_SMOOTHING_SECONDS);
|
||||
totals.notices.forEach(announce);
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
/** The resource bar and the unlocked tabs always; the open section only. */
|
||||
function render() {
|
||||
view.update(state, derive(state, ctx), { income });
|
||||
const d = derive(state, ctx);
|
||||
view.update(state, d, { income });
|
||||
tabs.update(state);
|
||||
if (tabs.active !== 'main') screens[tabs.active]?.update(state, d);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- actions
|
||||
@@ -195,6 +232,41 @@ const view = createView({
|
||||
nudgePrice: (delta) => act(setPrice)(state.price + delta),
|
||||
});
|
||||
|
||||
function restart() {
|
||||
if (!confirm(t('legacy.confirm'))) return;
|
||||
const gain = prestige(state, ctx);
|
||||
if (!gain) return;
|
||||
income = 0;
|
||||
persist();
|
||||
tabs.select('main');
|
||||
showToast(t('toast.restarted', { gain: fmt.number(gain) }));
|
||||
}
|
||||
|
||||
/** One screen per section; each is built and refreshed by its own module. */
|
||||
const screens = {
|
||||
contracts: createContractsView($('#view-contracts'), {
|
||||
accept: act(acceptContract),
|
||||
abandon: act(abandonContract),
|
||||
claimEvent: () => {
|
||||
claimEvent(state, ctx, derive(state, ctx));
|
||||
render();
|
||||
},
|
||||
}),
|
||||
invest: createInvestView($('#view-invest'), { buy: act(buyShares), sell: act(sellShares) }),
|
||||
research: createResearchView($('#view-research'), { buyLab: act(buyLab), buyNode: act(buyResearch) }),
|
||||
gamble: createGambleView($('#view-gamble'), {
|
||||
buyChip: act(buyChip),
|
||||
buyReliability: act(buyReliability),
|
||||
run(stake) {
|
||||
const result = gamble(state, ctx, stake, derive(state, ctx).mods);
|
||||
render();
|
||||
return result;
|
||||
},
|
||||
}),
|
||||
legacy: createLegacyView($('#view-legacy'), { buyPerk: act(buyPerk), restart }),
|
||||
achievements: createAchievementsView($('#view-achievements')),
|
||||
};
|
||||
|
||||
createSettings({
|
||||
getExport: () => ({
|
||||
text: buildExport(state),
|
||||
@@ -211,6 +283,7 @@ createSettings({
|
||||
state = createState(ctx);
|
||||
income = 0;
|
||||
persist();
|
||||
tabs.select('main');
|
||||
render();
|
||||
},
|
||||
});
|
||||
@@ -226,6 +299,8 @@ async function boot() {
|
||||
[ctx, themes, locales] = await Promise.all([loadGameContext(), fetchThemes(), fetchLocales()]);
|
||||
if (themes.length === 0) throw new Error('No theme installed in public/themes/');
|
||||
|
||||
tabs = createTabs(ctx.tabs, () => state && render());
|
||||
|
||||
const prefs = loadPrefs();
|
||||
locale = pickLocale(prefs.lang);
|
||||
const initialTheme = themes.find((th) => th.id === prefs.theme) ?? themes[0];
|
||||
@@ -240,7 +315,6 @@ async function boot() {
|
||||
});
|
||||
|
||||
await setTheme(initialTheme.id);
|
||||
refreshStaticIcons();
|
||||
|
||||
state = loadSavedState() ?? createState(ctx);
|
||||
catchUp((Date.now() - state.savedAt) / 1000);
|
||||
|
||||
Reference in New Issue
Block a user