Compare commits
6 Commits
feature/ri
...
2c949cc19e
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c949cc19e | |||
| 6385872dd1 | |||
| c9c8e45b0c | |||
| 787ada7cb4 | |||
| 8ed67313a8 | |||
| 5828f40497 |
14
CHANGELOG.md
14
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`,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +93,8 @@ export class BuildingSystem {
|
||||
|
||||
// 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 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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user