From b8e0d013590ae6305d5f766e8659085228c1f928 Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Fri, 4 Sep 2026 16:20:32 +0300 Subject: [PATCH] =?UTF-8?q?=D1=83=D1=80=D0=BE=D0=B2=D0=BD=D0=B8=20=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.css | 33 +++++++++++++++++++++++++++++++++ src/components/GameMap.tsx | 11 +++++++++++ src/components/Topbar.tsx | 3 +++ src/game/GameStore.ts | 32 ++++++++++++++++++++++++++++++-- src/game/constants.ts | 6 +++++- src/game/persistence.ts | 2 ++ 6 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/App.css b/src/App.css index 7bc8431..c878bf8 100644 --- a/src/App.css +++ b/src/App.css @@ -192,6 +192,39 @@ animation: walk-bob 0.3s infinite steps(2); } +.next-level-btn { + position: absolute; + left: 50%; + top: 40%; + transform: translate(-50%, -50%); + padding: 16px 24px; + background: var(--panel-bg); + border: 4px solid var(--accent); + color: var(--accent); + font-family: inherit; + font-size: 14px; + text-shadow: 2px 2px 0 var(--panel-border); + box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.35); + cursor: pointer; + animation: next-level-pop 0.6s ease-out; +} + +.next-level-btn:hover { + box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.35); + margin: -4px 0px 0px -4px; +} + +@keyframes next-level-pop { + 0% { + transform: translate(-50%, -50%) scale(0.85); + opacity: 0; + } + 100% { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + } +} + @keyframes chop-shake { 0%, 100% { transform: translate(0, 0) rotate(0deg); diff --git a/src/components/GameMap.tsx b/src/components/GameMap.tsx index b3d2b75..e89d594 100644 --- a/src/components/GameMap.tsx +++ b/src/components/GameMap.tsx @@ -150,6 +150,17 @@ function GameMap({ store }: Props) { ))} + + {store.canAdvanceLevel && ( + + )} )} diff --git a/src/components/Topbar.tsx b/src/components/Topbar.tsx index b8a476f..e22b25b 100644 --- a/src/components/Topbar.tsx +++ b/src/components/Topbar.tsx @@ -11,6 +11,9 @@ function Topbar({ store }: Props) { return (

Forest

+
+ Уровень {store.level} +
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 {