anaytics
This commit is contained in:
+35
-2
@@ -20,6 +20,7 @@ import {
|
||||
import { pickSpawnPositions } from './spawning';
|
||||
import type { Inventory, ResourceType, Vec2 } from './types';
|
||||
import { costForNextLevel, UPGRADES, type UpgradeId } from './upgrades';
|
||||
import type { Analytics } from '../analytics';
|
||||
|
||||
/** How often (seconds of played time) the game autosaves while running. */
|
||||
const AUTOSAVE_INTERVAL = 2;
|
||||
@@ -77,7 +78,15 @@ export class GameStore {
|
||||
/** Cumulative coins earned from converting resources — unlike inventory.coins, this never goes
|
||||
* down when coins are spent on upgrades. */
|
||||
totalCoinsEarned = 0;
|
||||
/** Real seconds elapsed this life (ticked deltaTime, not in-game clock time) — stops
|
||||
* accumulating once dead, same as the other life stats above (tick() returns early then). */
|
||||
playTimeSeconds = 0;
|
||||
|
||||
/** playTimeSeconds' value at the moment the current day/level started — not persisted, so
|
||||
* continuing a saved game restarts this day's timer from the moment of resuming rather than
|
||||
* from when that day originally began (a minor approximation, not worth the extra save fields
|
||||
* to avoid). Used to compute completeLevel()'s elapsed-seconds argument at the next dawn. */
|
||||
private levelStartPlayTime = 0;
|
||||
private coinProgress = 0;
|
||||
private saveProgress = 0;
|
||||
private lastTimestamp: number | null = null;
|
||||
@@ -88,7 +97,10 @@ export class GameStore {
|
||||
/** Map dimensions from the last initialize(), reused to grow each new day's nodes. */
|
||||
private mapSize: { width: number; height: number } | null = null;
|
||||
|
||||
constructor() {
|
||||
private readonly analytics: Analytics;
|
||||
|
||||
constructor(analytics: Analytics) {
|
||||
this.analytics = analytics;
|
||||
// autoBind so methods stay correctly bound when passed as bare references (event handlers,
|
||||
// e.g. `onPointerUp={store.release}` or `window.addEventListener('blur', store.release)`).
|
||||
// Deliberately no side effects (e.g. global event listener registration) here beyond
|
||||
@@ -96,7 +108,11 @@ export class GameStore {
|
||||
// initializer in dev, constructing (and discarding) an extra instance — any listener
|
||||
// registered in the constructor would leak and fire for that discarded instance too. Global
|
||||
// listeners like beforeunload belong in a useEffect instead (see GameMap), which StrictMode
|
||||
// handles correctly via its mount/cleanup/remount cycle.
|
||||
// handles correctly via its mount/cleanup/remount cycle. The same reasoning is why the caller
|
||||
// must pass an already-built AnalyticsProvider (a module-level singleton — see App.tsx)
|
||||
// rather than GameStore constructing a GameAnalyticsProvider itself: that class registers its
|
||||
// own listeners and starts a session in its constructor, so double-invocation would leak a
|
||||
// listener set and open two sessions.
|
||||
makeAutoObservable(this, {}, { autoBind: true });
|
||||
}
|
||||
|
||||
@@ -238,6 +254,7 @@ export class GameStore {
|
||||
|
||||
this.inventory.coins -= cost;
|
||||
this.upgradeLevels[id] = level + 1;
|
||||
this.analytics.buyUpgrade(id, level + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -281,6 +298,8 @@ export class GameStore {
|
||||
this.bushesChoppedCount = save?.bushesChoppedCount ?? 0;
|
||||
this.rabbitsCaughtCount = save?.rabbitsCaughtCount ?? 0;
|
||||
this.totalCoinsEarned = save?.totalCoinsEarned ?? 0;
|
||||
this.playTimeSeconds = save?.playTimeSeconds ?? 0;
|
||||
this.levelStartPlayTime = this.playTimeSeconds;
|
||||
|
||||
// Rabbits are restored/spawned independently of the world-state branch above so a save from
|
||||
// before rabbits existed still loads fine — it just gets a freshly spawned batch of them.
|
||||
@@ -333,6 +352,7 @@ export class GameStore {
|
||||
bushesChoppedCount: this.bushesChoppedCount,
|
||||
rabbitsCaughtCount: this.rabbitsCaughtCount,
|
||||
totalCoinsEarned: this.totalCoinsEarned,
|
||||
playTimeSeconds: this.playTimeSeconds,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -461,6 +481,7 @@ export class GameStore {
|
||||
if (this.isDead)
|
||||
return;
|
||||
|
||||
this.playTimeSeconds += deltaTime;
|
||||
this.updateClock(deltaTime);
|
||||
this.updateBloodMoon(deltaTime);
|
||||
this.updateMovementAndChopping(deltaTime);
|
||||
@@ -510,6 +531,7 @@ export class GameStore {
|
||||
this.characterHitFlashTimer = HIT_FLASH_DURATION;
|
||||
if (this.character.hp <= 0) {
|
||||
this.isDead = true;
|
||||
this.analytics.death('killed_by_rabbit');
|
||||
clearSave();
|
||||
}
|
||||
}
|
||||
@@ -518,6 +540,7 @@ export class GameStore {
|
||||
* (upgradable) day speed otherwise — rolling over into a new day, which grows fresh plants and
|
||||
* rabbits up to the new day's caps, whenever it passes midnight. */
|
||||
private updateClock(deltaTime: number) {
|
||||
const wasNight = this.isNight;
|
||||
const speed = this.isNight ? NIGHT_SPEED : this.daySpeedHoursPerSecond;
|
||||
this.hour += speed * deltaTime;
|
||||
while (this.hour >= 24) {
|
||||
@@ -526,6 +549,16 @@ export class GameStore {
|
||||
this.growNewNodesForDay();
|
||||
this.growRabbitsForDay();
|
||||
}
|
||||
// Edge-triggered on the night->day flip rather than tracking a "last announced day", so it
|
||||
// fires exactly once per dawn regardless of day-rollover timing — and never for day 1's own
|
||||
// dawn, since a fresh game starts at noon (no night->day transition has happened yet).
|
||||
if (wasNight && !this.isNight) {
|
||||
const completedDay = this.day - 1;
|
||||
const elapsedSeconds = this.playTimeSeconds - this.levelStartPlayTime;
|
||||
this.analytics.completeLevel(completedDay, elapsedSeconds);
|
||||
this.levelStartPlayTime = this.playTimeSeconds;
|
||||
this.analytics.startLevel(this.day);
|
||||
}
|
||||
}
|
||||
|
||||
/** Each rabbit independently cycles: pause (deciding its next action) → jump or strike
|
||||
|
||||
@@ -29,6 +29,9 @@ export interface SaveData {
|
||||
/** Cumulative coins earned from converting resources — unlike inventory.coins, this never goes
|
||||
* down when coins are spent on upgrades. */
|
||||
totalCoinsEarned?: number;
|
||||
/** Real seconds elapsed this life (ticked deltaTime, not in-game clock time) — stops
|
||||
* accumulating once dead, same as the other life stats above. */
|
||||
playTimeSeconds?: number;
|
||||
}
|
||||
|
||||
function isValidSave(value: unknown): value is SaveData {
|
||||
|
||||
Reference in New Issue
Block a user