feat: New features (research, invest, gambling, new game+, trophies)

This commit is contained in:
gauvainboiche
2026-10-08 14:03:00 +02:00
parent 1caa4d92b9
commit fc0d5e3ba4
56 changed files with 4332 additions and 249 deletions
+85 -11
View File
@@ -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);