diff --git a/src/game/GameStore.ts b/src/game/GameStore.ts
index f1297bd..7af2cb6 100644
--- a/src/game/GameStore.ts
+++ b/src/game/GameStore.ts
@@ -8,10 +8,10 @@ import {
LOG_FALL_ARC_HEIGHT,
LOG_FALL_DURATION,
LOGS_PER_TREE,
- TREE_COUNT,
TREE_EDGE_MARGIN,
TREE_MIN_DISTANCE_BETWEEN,
TREE_MIN_DISTANCE_FROM_CENTER,
+ treesForLevel,
} from './constants';
import { loadSave, writeSave } from './persistence';
import { costForNextLevel, UPGRADES, type UpgradeId } from './upgrades';
@@ -24,6 +24,7 @@ function randomId(prefix: string): string {
}
export class GameStore {
+ level = 1;
character: Vec2 = { x: 0, y: 0 };
trees: TreeState[] = [];
logDrops: LogDrop[] = [];
@@ -44,6 +45,8 @@ export class GameStore {
private saveProgress = 0;
private lastTimestamp: number | null = null;
private rafId: number | null = null;
+ /** Map dimensions from the last initialize(), reused to spawn each new level's trees. */
+ private mapSize: { width: number; height: number } | null = null;
constructor() {
// autoBind so methods stay correctly bound when passed as bare references (event handlers,
@@ -100,6 +103,11 @@ export class GameStore {
return 1 / logsPerSecond;
}
+ /** Whether every tree on the current level has been chopped down and every log collected. */
+ get canAdvanceLevel(): boolean {
+ return this.trees.length === 0 && this.logDrops.length === 0;
+ }
+
/** Buys the next level of the given upgrade if there are enough coins. Returns whether it bought. */
buyUpgrade(id: UpgradeId): boolean {
const level = this.upgradeLevels[id];
@@ -116,6 +124,7 @@ export class GameStore {
* logs/progress) if one exists; otherwise places the character in the center and scatters
* fresh trees. */
initialize(width: number, height: number) {
+ this.mapSize = { width, height };
this.heldTreeId = null;
this.moveTarget = null;
this.chopProgress = 0;
@@ -123,6 +132,8 @@ export class GameStore {
this.saveProgress = 0;
const save = loadSave();
+ this.level = save?.level ?? 1;
+
if (save?.character && save.trees && save.logDrops) {
this.character = { ...save.character };
this.trees = save.trees.map((tree) => ({ ...tree }));
@@ -138,6 +149,21 @@ export class GameStore {
this.upgradeLevels = { ...this.upgradeLevels, ...save.upgradeLevels };
}
+ /** Advances to the next level once every tree is chopped and every log collected: respawns a
+ * fresh batch of trees (level + 4 of them) and clears the ground of logs. */
+ nextLevel() {
+ if (!this.canAdvanceLevel || !this.mapSize)
+ return;
+
+ this.level += 1;
+ this.heldTreeId = null;
+ this.moveTarget = null;
+ this.chopProgress = 0;
+ this.trees = this.spawnTrees(this.mapSize.width, this.mapSize.height);
+ this.logDrops = [];
+ this.save();
+ }
+
/** Persists the full game (world state + progress) to localStorage right away. */
save() {
writeSave({
@@ -147,6 +173,7 @@ export class GameStore {
character: { ...this.character },
trees: this.trees.map((tree) => ({ ...tree })),
logDrops: this.logDrops.map((log) => ({ ...log, flight: log.flight ? { ...log.flight } : undefined })),
+ level: this.level,
});
}
@@ -155,8 +182,9 @@ export class GameStore {
const centerY = height / 2;
const trees: TreeState[] = [];
let attempts = 0;
+ const treeCount = treesForLevel(this.level);
- while (trees.length < TREE_COUNT && attempts < 1000) {
+ while (trees.length < treeCount && attempts < 1000) {
attempts++;
const x = TREE_EDGE_MARGIN + Math.random() * Math.max(0, width - TREE_EDGE_MARGIN * 2);
const y = TREE_EDGE_MARGIN + Math.random() * Math.max(0, height - TREE_EDGE_MARGIN * 2);
diff --git a/src/game/constants.ts b/src/game/constants.ts
index cd66a79..ddb0223 100644
--- a/src/game/constants.ts
+++ b/src/game/constants.ts
@@ -1,4 +1,8 @@
-export const TREE_COUNT = 5;
+/** Number of trees spawned on a given level. */
+export function treesForLevel(level: number): number {
+ return level + 4;
+}
+
export const LOGS_PER_TREE = 5;
/** How close the character must get to a tree's center to start chopping it. */
diff --git a/src/game/persistence.ts b/src/game/persistence.ts
index 68f2ead..5d22d45 100644
--- a/src/game/persistence.ts
+++ b/src/game/persistence.ts
@@ -12,6 +12,8 @@ export interface SaveData {
character?: Vec2;
trees?: TreeState[];
logDrops?: LogDrop[];
+ /** Absent on older saves, which are treated as level 1. */
+ level?: number;
}
function isValidSave(value: unknown): value is SaveData {