diff --git a/CHANGELOG.md b/CHANGELOG.md index 276024b..b1eee5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,20 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Fixed +- Nisse now clear the FOREST/ROCK tile after harvesting, opening paths to deeper resources +- Nisse no longer get stuck idle after depositing items at the stockpile +- Working Nisse now reset to idle on game load (like walking ones), preventing stale AI state +- Stale jobs with empty carry are now cleared after work completes, avoiding a false "haul to stockpile" loop +- UI elements (stockpile panel, controls hint) now reposition correctly after window resize +- Centered overlay panels (build menu, villager panel) close on resize so they reopen at the correct position +- Mouse world coordinates now use `ptr.worldX`/`ptr.worldY` in BuildingSystem and FarmingSystem, fixing misalignment after resize or zoom + +### Changed +- Villagers are now called **Nisse** throughout the UI (panel, controls hint, stockpile display, context menu, spawn message) + ### Added -- Right-click context menu: suppresses browser default, shows Build and Folks actions in the game world +- Right-click context menu: suppresses browser default, shows Build and Nisse actions in the game world - Initial project setup: Phaser 3 + TypeScript + Vite - Core scenes: `BootScene`, `GameScene`, `UIScene` - Systems: `BuildingSystem`, `CameraSystem`, `FarmingSystem`, `PlayerSystem`, diff --git a/src/StateManager.ts b/src/StateManager.ts index 35211c3..f1fc7e6 100644 --- a/src/StateManager.ts +++ b/src/StateManager.ts @@ -176,9 +176,9 @@ class StateManager { if (!p.world.crops) p.world.crops = {} if (!p.world.villagers) p.world.villagers = {} if (!p.world.stockpile) p.world.stockpile = {} - // Reset walking villagers to idle on load + // Reset in-flight AI states to idle on load so runtime timers start fresh for (const v of Object.values(p.world.villagers)) { - if (v.aiState === 'walking') v.aiState = 'idle' + if (v.aiState === 'walking' || v.aiState === 'working') v.aiState = 'idle' } return p } catch (_) { return null } diff --git a/src/scenes/UIScene.ts b/src/scenes/UIScene.ts index b34f428..fa94dd0 100644 --- a/src/scenes/UIScene.ts +++ b/src/scenes/UIScene.ts @@ -21,7 +21,9 @@ export class UIScene extends Phaser.Scene { private buildModeText!: Phaser.GameObjects.Text private farmToolText!: Phaser.GameObjects.Text private coordsText!: Phaser.GameObjects.Text + private controlsHintText!: Phaser.GameObjects.Text private popText!: Phaser.GameObjects.Text + private stockpileTitleText!: Phaser.GameObjects.Text private contextMenuGroup!: Phaser.GameObjects.Group private contextMenuVisible = false private inBuildMode = false @@ -80,13 +82,13 @@ export class UIScene extends Phaser.Scene { private createStockpilePanel(): void { const x = this.scale.width - 178, y = 10 this.stockpilePanel = this.add.rectangle(x, y, 168, 165, 0x000000, 0.72).setOrigin(0, 0).setScrollFactor(0).setDepth(100) - this.add.text(x + 10, y + 7, '⚡ STOCKPILE', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101) + this.stockpileTitleText = this.add.text(x + 10, y + 7, '⚡ STOCKPILE', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101) const items = ['wood','stone','wheat_seed','carrot_seed','wheat','carrot'] as const items.forEach((item, i) => { const t = this.add.text(x + 10, y + 26 + i * 22, `${ITEM_ICONS[item]} ${item}: 0`, { fontSize: '13px', color: '#88dd88', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101) this.stockpileTexts.set(item, t) }) - this.popText = this.add.text(x + 10, y + 145, '👥 Pop: 0 / 0', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101) + this.popText = this.add.text(x + 10, y + 145, '👥 Nisse: 0 / 0', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101) } private updateStockpile(): void { @@ -102,7 +104,7 @@ export class UIScene extends Phaser.Scene { const state = stateManager.getState() const beds = Object.values(state.world.buildings).filter(b => b.kind === 'bed').length const current = Object.keys(state.world.villagers).length - this.popText?.setText(`👥 Pop: ${current} / ${beds} [V] manage`) + this.popText?.setText(`👥 Nisse: ${current} / ${beds} [V]`) } // ─── Hint ───────────────────────────────────────────────────────────────── @@ -202,13 +204,13 @@ export class UIScene extends Phaser.Scene { this.villagerPanelGroup.add(bg) this.villagerPanelGroup.add( - this.add.text(px + panelW/2, py + 12, '👥 VILLAGERS [V] close', { fontSize: '12px', color: '#aaaaaa', fontFamily: 'monospace' }) + this.add.text(px + panelW/2, py + 12, '👥 NISSE [V] close', { fontSize: '12px', color: '#aaaaaa', fontFamily: 'monospace' }) .setOrigin(0.5, 0).setScrollFactor(0).setDepth(211) ) if (villagers.length === 0) { this.villagerPanelGroup.add( - this.add.text(px + panelW/2, py + panelH/2, 'No villagers yet.\nBuild a 🛏 Bed first!', { + this.add.text(px + panelW/2, py + panelH/2, 'No Nisse yet.\nBuild a 🛏 Bed first!', { fontSize: '13px', color: '#666666', fontFamily: 'monospace', align: 'center' }).setOrigin(0.5).setScrollFactor(0).setDepth(211) ) @@ -286,7 +288,7 @@ export class UIScene extends Phaser.Scene { private createCoordsDisplay(): void { this.coordsText = this.add.text(10, this.scale.height - 24, '', { fontSize: '11px', color: '#666666', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(100) - this.add.text(10, this.scale.height - 42, '[WASD] Pan [Scroll] Zoom [F] Farm [B] Build [V] Villagers', { + this.controlsHintText = this.add.text(10, this.scale.height - 42, '[WASD] Pan [Scroll] Zoom [F] Farm [B] Build [V] Nisse', { fontSize: '10px', color: '#444444', fontFamily: 'monospace', backgroundColor: '#00000066', padding: { x: 4, y: 2 } }).setScrollFactor(0).setDepth(100) } @@ -322,7 +324,7 @@ export class UIScene extends Phaser.Scene { action: () => { this.hideContextMenu(); this.scene.get('Game').events.emit('uiRequestBuildMenu') }, }, { - label: '👥 Folks', + label: '👥 Nisse', action: () => { this.hideContextMenu(); this.toggleVillagerPanel() }, }, ] @@ -359,10 +361,33 @@ export class UIScene extends Phaser.Scene { // ─── Resize ─────────────────────────────────────────────────────────────── + /** + * Repositions all fixed UI elements after a canvas resize. + * Open overlay panels are closed so they reopen correctly centered. + */ private repositionUI(): void { const { width, height } = this.scale - this.hintText.setPosition(width/2, height - 40) - this.toastText.setPosition(width/2, 60) + + // Stockpile panel — anchored to top-right; move all elements by the delta + const newPanelX = width - 178 + const deltaX = newPanelX - this.stockpilePanel.x + if (deltaX !== 0) { + this.stockpilePanel.setX(newPanelX) + this.stockpileTitleText.setX(this.stockpileTitleText.x + deltaX) + this.stockpileTexts.forEach(t => t.setX(t.x + deltaX)) + this.popText.setX(this.popText.x + deltaX) + } + + // Bottom elements + this.hintText.setPosition(width / 2, height - 40) + this.toastText.setPosition(width / 2, 60) this.coordsText.setPosition(10, height - 24) + this.controlsHintText.setPosition(10, height - 42) + + // Close centered panels — their position is calculated on open, so they + // would be off-center if left open during a resize + if (this.buildMenuVisible) this.closeBuildMenu() + if (this.villagerPanelVisible) this.closeVillagerPanel() + if (this.contextMenuVisible) this.hideContextMenu() } } diff --git a/src/systems/BuildingSystem.ts b/src/systems/BuildingSystem.ts index df1b25e..3e52d8a 100644 --- a/src/systems/BuildingSystem.ts +++ b/src/systems/BuildingSystem.ts @@ -92,9 +92,9 @@ export class BuildingSystem { if (!this.active) return // Update ghost to follow mouse (snapped to tile grid) - const ptr = this.scene.input.activePointer - const worldX = this.scene.cameras.main.scrollX + ptr.x - const worldY = this.scene.cameras.main.scrollY + ptr.y + const ptr = this.scene.input.activePointer + const worldX = ptr.worldX + const worldY = ptr.worldY const tileX = Math.floor(worldX / TILE_SIZE) const tileY = Math.floor(worldY / TILE_SIZE) const snapX = tileX * TILE_SIZE + TILE_SIZE / 2 @@ -142,8 +142,8 @@ export class BuildingSystem { } private tryPlace(ptr: Phaser.Input.Pointer): void { - const worldX = this.scene.cameras.main.scrollX + ptr.x - const worldY = this.scene.cameras.main.scrollY + ptr.y + const worldX = ptr.worldX + const worldY = ptr.worldY const tileX = Math.floor(worldX / TILE_SIZE) const tileY = Math.floor(worldY / TILE_SIZE) diff --git a/src/systems/FarmingSystem.ts b/src/systems/FarmingSystem.ts index 79ece7b..ea30a1a 100644 --- a/src/systems/FarmingSystem.ts +++ b/src/systems/FarmingSystem.ts @@ -80,9 +80,8 @@ export class FarmingSystem { // ─── Tool actions ───────────────────────────────────────────────────────── private useToolAt(ptr: Phaser.Input.Pointer): void { - const cam = this.scene.cameras.main - const worldX = cam.scrollX + ptr.x - const worldY = cam.scrollY + ptr.y + const worldX = ptr.worldX + const worldY = ptr.worldY const tileX = Math.floor(worldX / TILE_SIZE) const tileY = Math.floor(worldY / TILE_SIZE) const state = stateManager.getState() diff --git a/src/systems/VillagerSystem.ts b/src/systems/VillagerSystem.ts index 4c3ebfd..68efa98 100644 --- a/src/systems/VillagerSystem.ts +++ b/src/systems/VillagerSystem.ts @@ -1,5 +1,6 @@ import Phaser from 'phaser' import { TILE_SIZE, VILLAGER_SPEED, VILLAGER_SPAWN_INTERVAL, VILLAGER_WORK_TIMES, VILLAGER_NAMES } from '../config' +import { TileType } from '../types' import type { VillagerState, VillagerJob, JobType, AIState, ItemId } from '../types' import { stateManager } from '../StateManager' import { findPath } from '../utils/pathfinding' @@ -170,6 +171,7 @@ export class VillagerSystem { case 'stockpile': this.adapter.send({ type: 'VILLAGER_DEPOSIT', villagerId: v.id }) this.adapter.send({ type: 'VILLAGER_SET_AI', villagerId: v.id, aiState: 'idle' }) + rt.idleScanTimer = 0 // scan for a new job immediately after deposit break case 'bed': @@ -199,13 +201,19 @@ export class VillagerSystem { const state = stateManager.getState() if (job.type === 'chop') { - if (state.world.resources[job.targetId]) { + const res = state.world.resources[job.targetId] + if (res) { this.adapter.send({ type: 'VILLAGER_HARVEST_RESOURCE', villagerId: v.id, resourceId: job.targetId }) + // Clear the FOREST tile so the area becomes passable for future pathfinding + this.adapter.send({ type: 'CHANGE_TILE', tileX: res.tileX, tileY: res.tileY, tile: TileType.DARK_GRASS }) this.resourceSystem.removeResource(job.targetId) } } else if (job.type === 'mine') { - if (state.world.resources[job.targetId]) { + const res = state.world.resources[job.targetId] + if (res) { this.adapter.send({ type: 'VILLAGER_HARVEST_RESOURCE', villagerId: v.id, resourceId: job.targetId }) + // Clear the ROCK tile so the area becomes passable for future pathfinding + this.adapter.send({ type: 'CHANGE_TILE', tileX: res.tileX, tileY: res.tileY, tile: TileType.GRASS }) this.resourceSystem.removeResource(job.targetId) } } else if (job.type === 'farm') { @@ -217,7 +225,13 @@ export class VillagerSystem { } } - // Back to idle so decideAction handles depositing + // If the harvest produced nothing (resource already gone), clear the stale job + // so tickIdle does not try to walk to a stockpile with nothing to deposit. + if (!v.job?.carrying || !Object.values(v.job.carrying).some(n => (n ?? 0) > 0)) { + this.adapter.send({ type: 'VILLAGER_SET_JOB', villagerId: v.id, job: null }) + } + + // Back to idle — tickIdle will handle hauling to stockpile if carrying items this.adapter.send({ type: 'VILLAGER_SET_AI', villagerId: v.id, aiState: 'idle' }) } @@ -342,7 +356,7 @@ export class VillagerSystem { this.adapter.send({ type: 'SPAWN_VILLAGER', villager }) this.spawnSprite(villager) - this.onMessage?.(`${name} has joined the settlement! 🏘`) + this.onMessage?.(`${name} the Nisse has arrived! 🏘`) } // ─── Sprite management ────────────────────────────────────────────────────