миграция на pixi

This commit is contained in:
azykov@mail.ru
2026-09-09 13:45:30 +03:00
parent a3f2f4eeef
commit 4e30c585d1
41 changed files with 1303 additions and 899 deletions
+95
View File
@@ -0,0 +1,95 @@
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';
/** 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.root.replaceChildren(menu);
}
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();
}
private handleNewGame(): 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();
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;
}
}