111 lines
4.1 KiB
TypeScript
111 lines
4.1 KiB
TypeScript
import '../App.css';
|
|
import { Analytics, makeAnalyticsGameInfo } from '../analytics';
|
|
import type { AnalyticsTransport } from '../analytics/AnalyticsTransport';
|
|
import { GameAnalyticsTransport } from '../analytics/GameAnalyticsTransport';
|
|
import { NullAnalyticsTransport } from '../analytics/NullAnalyticsTransport';
|
|
import { GameStore } from '../game/GameStore';
|
|
import { clearSave, hasSave } from '../game/persistence';
|
|
import { GameScreen } from './GameScreen';
|
|
import { createMainMenu } from './MainMenu';
|
|
import { createSettingsScreen } from './SettingsScreen';
|
|
|
|
/** Top-level screen controller (menu <-> game) — the plain-TS replacement for App.tsx. Owns the
|
|
* GameStore/Analytics singletons for the page's whole lifetime, same as the original module-level
|
|
* singletons (no React StrictMode double-invocation to guard against here, since nothing
|
|
* constructs this more than once — see main.ts). */
|
|
export class App {
|
|
private readonly root: HTMLElement;
|
|
private readonly store: GameStore;
|
|
private readonly analytics: Analytics;
|
|
private gameScreen: GameScreen | null = null;
|
|
|
|
constructor(root: HTMLElement) {
|
|
this.root = root;
|
|
|
|
const gaGameKey = import.meta.env.VITE_GAMEANALYTICS_GAME_KEY;
|
|
const gaGameSecret = import.meta.env.VITE_GAMEANALYTICS_GAME_SECRET;
|
|
let transport: AnalyticsTransport;
|
|
if (!gaGameKey || !gaGameSecret) {
|
|
console.warn('VITE_GAMEANALYTICS_GAME_KEY/VITE_GAMEANALYTICS_GAME_SECRET not set — analytics disabled (see .env.example).');
|
|
transport = new NullAnalyticsTransport();
|
|
} else {
|
|
transport = new GameAnalyticsTransport(gaGameKey, gaGameSecret);
|
|
}
|
|
|
|
this.analytics = new Analytics(transport, () => makeAnalyticsGameInfo(this.store));
|
|
this.analytics.start();
|
|
this.store = new GameStore(this.analytics);
|
|
|
|
// Always starts on the menu, regardless of whether a save exists — loading straight into a
|
|
// resumed game on every page (re)load isn't what was asked for.
|
|
this.showMenu();
|
|
}
|
|
|
|
private showMenu(): void {
|
|
this.teardownGameScreen();
|
|
const menu = createMainMenu(
|
|
this.analytics,
|
|
() => this.handleNewGame(),
|
|
() => this.handleContinue(),
|
|
() => this.showSettings(),
|
|
);
|
|
this.root.replaceChildren(menu);
|
|
}
|
|
|
|
private showSettings(): void {
|
|
const settings = createSettingsScreen(
|
|
this.store.seed,
|
|
() => this.showMenu(),
|
|
(seed) => this.handleNewGame(seed),
|
|
);
|
|
this.root.replaceChildren(settings);
|
|
}
|
|
|
|
private showGame(): void {
|
|
this.gameScreen = new GameScreen(
|
|
this.store,
|
|
() => this.handleExitToMenu(),
|
|
() => this.handleDeath(),
|
|
);
|
|
this.root.replaceChildren(this.gameScreen.el);
|
|
// Only now is `el` actually attached to the document — GameScreen's canvas sizes itself from
|
|
// the map div's clientWidth/Height, which reads 0 on a still-detached element.
|
|
this.gameScreen.mount();
|
|
}
|
|
|
|
/** Starts a fresh game — a freshly random seed by default, or an explicit one when called from
|
|
* Settings' seed field (which also acts as "restart the game with this seed"). */
|
|
private handleNewGame(seed?: number): void {
|
|
// A resumable save existing means there's a live (not-dead) game being abandoned by starting
|
|
// fresh instead of continuing it — record that as the last level's failure. A death already
|
|
// records its own failure at the moment it happens (see GameStore.landRabbitStrike) and clears
|
|
// the save there too, so this naturally doesn't double-fire right after a death.
|
|
if (hasSave())
|
|
this.analytics.death('restarted');
|
|
clearSave();
|
|
if (seed !== undefined)
|
|
this.store.setNextSeed(seed);
|
|
this.showGame();
|
|
}
|
|
|
|
private handleContinue(): void {
|
|
this.showGame();
|
|
}
|
|
|
|
private handleExitToMenu(): void {
|
|
this.store.save();
|
|
this.showMenu();
|
|
}
|
|
|
|
// Called after a death overlay is acknowledged: the store already wiped the save itself the
|
|
// moment the character was caught, so this just returns to the menu without saving over it.
|
|
private handleDeath(): void {
|
|
this.showMenu();
|
|
}
|
|
|
|
private teardownGameScreen(): void {
|
|
this.gameScreen?.destroy();
|
|
this.gameScreen = null;
|
|
}
|
|
}
|