Compare commits
12 Commits
986c2ea9eb
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 26c3807481 | |||
| d02ed33435 | |||
| c7cf971e54 | |||
| 08dffa135f | |||
| 4f2e9f73b6 | |||
| 84b6e51746 | |||
| 5f646d54ca | |||
| 94b2f7f457 | |||
| cd171c859c | |||
| d9ef57c6b0 | |||
| 87f69b4774 | |||
| 8d2c58cb5f |
@@ -7,6 +7,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Y-based depth sorting** (Issue #31): trees, rocks, seedlings and buildings now use `tileY + 5` as depth instead of fixed values — objects lower on screen always render in front of objects above them, regardless of spawn order; build ghost moved to depth 1000
|
||||||
|
- **Nisse always visible** (Issue #33): Nisse sprites fixed at depth 900, always rendered above world objects
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Försterkreislauf** (Issue #25):
|
- **Försterkreislauf** (Issue #25):
|
||||||
- **Setzlinge beim Fällen**: Jeder gefällte Baum gibt 1–2 `tree_seed` in den Stockpile
|
- **Setzlinge beim Fällen**: Jeder gefällte Baum gibt 1–2 `tree_seed` in den Stockpile
|
||||||
|
|||||||
@@ -179,18 +179,19 @@ export class GameScene extends Phaser.Scene {
|
|||||||
const name = `bobj_${building.id}`
|
const name = `bobj_${building.id}`
|
||||||
if (this.children.getByName(name)) continue
|
if (this.children.getByName(name)) continue
|
||||||
|
|
||||||
|
const worldDepth = building.tileY + 5
|
||||||
if (building.kind === 'chest') {
|
if (building.kind === 'chest') {
|
||||||
const g = this.add.graphics().setName(name).setDepth(8)
|
const g = this.add.graphics().setName(name).setDepth(worldDepth)
|
||||||
g.fillStyle(0x8B4513); g.fillRect(wx - 10, wy - 7, 20, 14)
|
g.fillStyle(0x8B4513); g.fillRect(wx - 10, wy - 7, 20, 14)
|
||||||
g.fillStyle(0xCD853F); g.fillRect(wx - 9, wy - 6, 18, 6)
|
g.fillStyle(0xCD853F); g.fillRect(wx - 9, wy - 6, 18, 6)
|
||||||
g.lineStyle(1, 0x5C3317); g.strokeRect(wx - 10, wy - 7, 20, 14)
|
g.lineStyle(1, 0x5C3317); g.strokeRect(wx - 10, wy - 7, 20, 14)
|
||||||
} else if (building.kind === 'bed') {
|
} else if (building.kind === 'bed') {
|
||||||
this.add.image(wx, wy, 'bed_obj').setName(name).setDepth(8)
|
this.add.image(wx, wy, 'bed_obj').setName(name).setDepth(worldDepth)
|
||||||
} else if (building.kind === 'stockpile_zone') {
|
} else if (building.kind === 'stockpile_zone') {
|
||||||
this.add.image(wx, wy, 'stockpile_obj').setName(name).setDepth(4).setAlpha(0.8)
|
this.add.image(wx, wy, 'stockpile_obj').setName(name).setDepth(4).setAlpha(0.8)
|
||||||
} else if (building.kind === 'forester_hut') {
|
} else if (building.kind === 'forester_hut') {
|
||||||
// Draw a simple log-cabin silhouette for the forester hut
|
// Draw a simple log-cabin silhouette for the forester hut
|
||||||
const g = this.add.graphics().setName(name).setDepth(8)
|
const g = this.add.graphics().setName(name).setDepth(worldDepth)
|
||||||
// Body
|
// Body
|
||||||
g.fillStyle(0x6B3F16); g.fillRect(wx - 12, wy - 9, 24, 18)
|
g.fillStyle(0x6B3F16); g.fillRect(wx - 12, wy - 9, 24, 18)
|
||||||
// Roof
|
// Roof
|
||||||
|
|||||||
@@ -59,6 +59,18 @@ export class UIScene extends Phaser.Scene {
|
|||||||
/** True while the zone-edit tool is active (shown in ESC priority stack). */
|
/** True while the zone-edit tool is active (shown in ESC priority stack). */
|
||||||
private inForesterZoneEdit = false
|
private inForesterZoneEdit = false
|
||||||
|
|
||||||
|
// ── Action Bar ────────────────────────────────────────────────────────────
|
||||||
|
private static readonly BAR_H = 48
|
||||||
|
private static readonly TRAY_H = 68
|
||||||
|
private actionBarBg!: Phaser.GameObjects.Rectangle
|
||||||
|
private actionBuildBtn!: Phaser.GameObjects.Rectangle
|
||||||
|
private actionBuildLabel!: Phaser.GameObjects.Text
|
||||||
|
private actionNisseBtn!: Phaser.GameObjects.Rectangle
|
||||||
|
private actionNisseLabel!: Phaser.GameObjects.Text
|
||||||
|
private actionTrayGroup!: Phaser.GameObjects.Group
|
||||||
|
private actionTrayVisible = false
|
||||||
|
private activeCategory: 'build' | 'nisse' | null = null
|
||||||
|
|
||||||
constructor() { super({ key: 'UI' }) }
|
constructor() { super({ key: 'UI' }) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,6 +86,7 @@ export class UIScene extends Phaser.Scene {
|
|||||||
this.createBuildModeIndicator()
|
this.createBuildModeIndicator()
|
||||||
this.createFarmToolIndicator()
|
this.createFarmToolIndicator()
|
||||||
this.createDebugPanel()
|
this.createDebugPanel()
|
||||||
|
this.createActionBar()
|
||||||
|
|
||||||
const gameScene = this.scene.get('Game')
|
const gameScene = this.scene.get('Game')
|
||||||
gameScene.events.on('buildModeChanged', (a: boolean, b: BuildingType) => this.onBuildModeChanged(a, b))
|
gameScene.events.on('buildModeChanged', (a: boolean, b: BuildingType) => this.onBuildModeChanged(a, b))
|
||||||
@@ -99,6 +112,7 @@ export class UIScene extends Phaser.Scene {
|
|||||||
this.nisseInfoGroup = this.add.group()
|
this.nisseInfoGroup = this.add.group()
|
||||||
this.settingsGroup = this.add.group()
|
this.settingsGroup = this.add.group()
|
||||||
this.foresterPanelGroup = this.add.group()
|
this.foresterPanelGroup = this.add.group()
|
||||||
|
this.actionTrayGroup = this.add.group()
|
||||||
|
|
||||||
gameScene.events.on('foresterHutClicked', (id: string) => this.openForesterPanel(id))
|
gameScene.events.on('foresterHutClicked', (id: string) => this.openForesterPanel(id))
|
||||||
gameScene.events.on('foresterZoneEditEnded', () => this.onForesterEditEnded())
|
gameScene.events.on('foresterZoneEditEnded', () => this.onForesterEditEnded())
|
||||||
@@ -171,7 +185,7 @@ export class UIScene extends Phaser.Scene {
|
|||||||
|
|
||||||
/** Creates the centered hint text element near the bottom of the screen. */
|
/** Creates the centered hint text element near the bottom of the screen. */
|
||||||
private createHintText(): void {
|
private createHintText(): void {
|
||||||
this.hintText = this.add.text(this.scale.width / 2, this.scale.height - 40, '', {
|
this.hintText = this.add.text(this.scale.width / 2, this.scale.height - UIScene.BAR_H - 24, '', {
|
||||||
fontSize: '14px', color: '#ffff88', fontFamily: 'monospace',
|
fontSize: '14px', color: '#ffff88', fontFamily: 'monospace',
|
||||||
backgroundColor: '#00000099', padding: { x: 10, y: 5 },
|
backgroundColor: '#00000099', padding: { x: 10, y: 5 },
|
||||||
}).setOrigin(0.5).setScrollFactor(0).setDepth(100).setVisible(false)
|
}).setOrigin(0.5).setScrollFactor(0).setDepth(100).setVisible(false)
|
||||||
@@ -266,6 +280,10 @@ export class UIScene extends Phaser.Scene {
|
|||||||
this.villagerPanelVisible = false
|
this.villagerPanelVisible = false
|
||||||
this.villagerPanelGroup?.destroy(true)
|
this.villagerPanelGroup?.destroy(true)
|
||||||
this.scene.get('Game').events.emit('uiMenuClose')
|
this.scene.get('Game').events.emit('uiMenuClose')
|
||||||
|
if (this.activeCategory === 'nisse') {
|
||||||
|
this.activeCategory = null
|
||||||
|
this.updateCategoryHighlights()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -525,6 +543,7 @@ export class UIScene extends Phaser.Scene {
|
|||||||
if (this.foresterPanelVisible) { this.closeForesterPanel(); return }
|
if (this.foresterPanelVisible) { this.closeForesterPanel(); return }
|
||||||
if (this.contextMenuVisible) { this.hideContextMenu(); return }
|
if (this.contextMenuVisible) { this.hideContextMenu(); return }
|
||||||
if (this.buildMenuVisible) { this.closeBuildMenu(); return }
|
if (this.buildMenuVisible) { this.closeBuildMenu(); return }
|
||||||
|
if (this.actionTrayVisible) { this.closeActionTray(); return }
|
||||||
if (this.villagerPanelVisible) { this.closeVillagerPanel(); return }
|
if (this.villagerPanelVisible) { this.closeVillagerPanel(); return }
|
||||||
if (this.nisseInfoVisible) { this.closeNisseInfoPanel(); return }
|
if (this.nisseInfoVisible) { this.closeNisseInfoPanel(); return }
|
||||||
if (this.settingsVisible) { this.closeSettings(); return }
|
if (this.settingsVisible) { this.closeSettings(); return }
|
||||||
@@ -1137,6 +1156,152 @@ export class UIScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Action Bar ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the persistent bottom action bar with Build and Nisse category buttons.
|
||||||
|
* The bar is always visible; individual button highlights change with the active category.
|
||||||
|
*/
|
||||||
|
private createActionBar(): void {
|
||||||
|
const { width, height } = this.scale
|
||||||
|
const barY = height - UIScene.BAR_H
|
||||||
|
|
||||||
|
this.actionBarBg = this.add.rectangle(0, barY, width, UIScene.BAR_H, 0x080808, 0.92)
|
||||||
|
.setOrigin(0, 0).setScrollFactor(0).setDepth(300)
|
||||||
|
|
||||||
|
this.actionBuildBtn = this.add.rectangle(8, barY + 8, 88, 32, 0x1a3a1a, 0.9)
|
||||||
|
.setOrigin(0, 0).setScrollFactor(0).setDepth(301).setInteractive()
|
||||||
|
this.actionBuildBtn.on('pointerover', () => {
|
||||||
|
if (this.activeCategory !== 'build') this.actionBuildBtn.setFillStyle(0x2a5a2a, 0.9)
|
||||||
|
})
|
||||||
|
this.actionBuildBtn.on('pointerout', () => {
|
||||||
|
if (this.activeCategory !== 'build') this.actionBuildBtn.setFillStyle(0x1a3a1a, 0.9)
|
||||||
|
})
|
||||||
|
this.actionBuildBtn.on('pointerdown', () => this.toggleCategory('build'))
|
||||||
|
|
||||||
|
this.actionBuildLabel = this.add.text(52, barY + UIScene.BAR_H / 2, '🔨 Build', {
|
||||||
|
fontSize: '12px', color: '#cccccc', fontFamily: 'monospace',
|
||||||
|
}).setOrigin(0.5, 0.5).setScrollFactor(0).setDepth(302)
|
||||||
|
|
||||||
|
this.actionNisseBtn = this.add.rectangle(104, barY + 8, 88, 32, 0x1a1a3a, 0.9)
|
||||||
|
.setOrigin(0, 0).setScrollFactor(0).setDepth(301).setInteractive()
|
||||||
|
this.actionNisseBtn.on('pointerover', () => {
|
||||||
|
if (this.activeCategory !== 'nisse') this.actionNisseBtn.setFillStyle(0x2a2a5a, 0.9)
|
||||||
|
})
|
||||||
|
this.actionNisseBtn.on('pointerout', () => {
|
||||||
|
if (this.activeCategory !== 'nisse') this.actionNisseBtn.setFillStyle(0x1a1a3a, 0.9)
|
||||||
|
})
|
||||||
|
this.actionNisseBtn.on('pointerdown', () => this.toggleCategory('nisse'))
|
||||||
|
|
||||||
|
this.actionNisseLabel = this.add.text(148, barY + UIScene.BAR_H / 2, '👥 Nisse', {
|
||||||
|
fontSize: '12px', color: '#cccccc', fontFamily: 'monospace',
|
||||||
|
}).setOrigin(0.5, 0.5).setScrollFactor(0).setDepth(302)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the given action bar category on or off.
|
||||||
|
* Selecting the active category deselects it; selecting a new one closes the previous.
|
||||||
|
* @param cat - The category to toggle ('build' or 'nisse')
|
||||||
|
*/
|
||||||
|
private toggleCategory(cat: 'build' | 'nisse'): void {
|
||||||
|
if (this.activeCategory === cat) {
|
||||||
|
this.deactivateCategory()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Close whatever was open before
|
||||||
|
if (this.activeCategory === 'build') this.closeActionTray()
|
||||||
|
if (this.activeCategory === 'nisse' && this.villagerPanelVisible) this.closeVillagerPanel()
|
||||||
|
|
||||||
|
this.activeCategory = cat
|
||||||
|
this.updateCategoryHighlights()
|
||||||
|
|
||||||
|
if (cat === 'build') {
|
||||||
|
this.openActionTray()
|
||||||
|
} else {
|
||||||
|
this.openVillagerPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivates the currently active category, closing its associated panel or tray.
|
||||||
|
*/
|
||||||
|
private deactivateCategory(): void {
|
||||||
|
if (this.activeCategory === 'build') this.closeActionTray()
|
||||||
|
if (this.activeCategory === 'nisse' && this.villagerPanelVisible) this.closeVillagerPanel()
|
||||||
|
this.activeCategory = null
|
||||||
|
this.updateCategoryHighlights()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the visual highlight of the Build and Nisse buttons
|
||||||
|
* to reflect the current active category.
|
||||||
|
*/
|
||||||
|
private updateCategoryHighlights(): void {
|
||||||
|
this.actionBuildBtn.setFillStyle(this.activeCategory === 'build' ? 0x3d7a3d : 0x1a3a1a, 0.9)
|
||||||
|
this.actionNisseBtn.setFillStyle(this.activeCategory === 'nisse' ? 0x3d3d7a : 0x1a1a3a, 0.9)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and shows the building tool tray above the action bar.
|
||||||
|
* Each building is shown as a clickable tile with emoji and name.
|
||||||
|
*/
|
||||||
|
private openActionTray(): void {
|
||||||
|
if (this.actionTrayVisible) return
|
||||||
|
this.actionTrayVisible = true
|
||||||
|
this.actionTrayGroup.destroy(true)
|
||||||
|
this.actionTrayGroup = this.add.group()
|
||||||
|
|
||||||
|
const { width, height } = this.scale
|
||||||
|
const trayY = height - UIScene.BAR_H - UIScene.TRAY_H
|
||||||
|
|
||||||
|
const bg = this.add.rectangle(0, trayY, width, UIScene.TRAY_H, 0x0d0d0d, 0.88)
|
||||||
|
.setOrigin(0, 0).setScrollFactor(0).setDepth(300)
|
||||||
|
this.actionTrayGroup.add(bg)
|
||||||
|
|
||||||
|
const buildings: { kind: BuildingType; emoji: string; label: string }[] = [
|
||||||
|
{ kind: 'floor', emoji: '🪵', label: 'Floor' },
|
||||||
|
{ kind: 'wall', emoji: '🧱', label: 'Wall' },
|
||||||
|
{ kind: 'chest', emoji: '📦', label: 'Chest' },
|
||||||
|
{ kind: 'bed', emoji: '🛏', label: 'Bed' },
|
||||||
|
{ kind: 'stockpile_zone', emoji: '📦', label: 'Stockpile' },
|
||||||
|
{ kind: 'forester_hut', emoji: '🌲', label: 'Forester' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const itemW = 84
|
||||||
|
buildings.forEach((b, i) => {
|
||||||
|
const bx = 8 + i * (itemW + 4)
|
||||||
|
const btn = this.add.rectangle(bx, trayY + 4, itemW, UIScene.TRAY_H - 8, 0x1a2a1a, 0.9)
|
||||||
|
.setOrigin(0, 0).setScrollFactor(0).setDepth(301).setInteractive()
|
||||||
|
btn.on('pointerover', () => btn.setFillStyle(0x2d4a2d, 0.9))
|
||||||
|
btn.on('pointerout', () => btn.setFillStyle(0x1a2a1a, 0.9))
|
||||||
|
btn.on('pointerdown', () => {
|
||||||
|
this.closeActionTray()
|
||||||
|
this.deactivateCategory()
|
||||||
|
this.scene.get('Game').events.emit('selectBuilding', b.kind)
|
||||||
|
})
|
||||||
|
this.actionTrayGroup.add(btn)
|
||||||
|
this.actionTrayGroup.add(
|
||||||
|
this.add.text(bx + itemW / 2, trayY + 18, b.emoji, { fontSize: '18px' })
|
||||||
|
.setOrigin(0.5, 0.5).setScrollFactor(0).setDepth(302)
|
||||||
|
)
|
||||||
|
this.actionTrayGroup.add(
|
||||||
|
this.add.text(bx + itemW / 2, trayY + 44, b.label, {
|
||||||
|
fontSize: '10px', color: '#cccccc', fontFamily: 'monospace',
|
||||||
|
}).setOrigin(0.5, 0.5).setScrollFactor(0).setDepth(302)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides and destroys the building tool tray.
|
||||||
|
*/
|
||||||
|
private closeActionTray(): void {
|
||||||
|
if (!this.actionTrayVisible) return
|
||||||
|
this.actionTrayVisible = false
|
||||||
|
this.actionTrayGroup.destroy(true)
|
||||||
|
this.actionTrayGroup = this.add.group()
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Resize ───────────────────────────────────────────────────────────────
|
// ─── Resize ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1157,8 +1322,16 @@ export class UIScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bottom elements
|
// Bottom elements
|
||||||
this.hintText.setPosition(width / 2, height - 40)
|
this.hintText.setPosition(width / 2, height - UIScene.BAR_H - 24)
|
||||||
this.toastText.setPosition(width / 2, 60)
|
this.toastText.setPosition(width / 2, 60)
|
||||||
|
|
||||||
|
// Action bar — reposition persistent elements
|
||||||
|
this.actionBarBg.setPosition(0, height - UIScene.BAR_H).setSize(width, UIScene.BAR_H)
|
||||||
|
this.actionBuildBtn.setPosition(8, height - UIScene.BAR_H + 8)
|
||||||
|
this.actionBuildLabel.setPosition(48, height - UIScene.BAR_H + UIScene.BAR_H / 2)
|
||||||
|
this.actionNisseBtn.setPosition(104, height - UIScene.BAR_H + 8)
|
||||||
|
this.actionNisseLabel.setPosition(144, height - UIScene.BAR_H + UIScene.BAR_H / 2)
|
||||||
|
if (this.actionTrayVisible) this.closeActionTray()
|
||||||
// Close centered panels — their position is calculated on open, so they
|
// Close centered panels — their position is calculated on open, so they
|
||||||
// would be off-center if left open during a resize
|
// would be off-center if left open during a resize
|
||||||
if (this.buildMenuVisible) this.closeBuildMenu()
|
if (this.buildMenuVisible) this.closeBuildMenu()
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export class BuildingSystem {
|
|||||||
|
|
||||||
create(): void {
|
create(): void {
|
||||||
this.ghost = this.scene.add.rectangle(0, 0, TILE_SIZE, TILE_SIZE, 0x00FF00, 0.35)
|
this.ghost = this.scene.add.rectangle(0, 0, TILE_SIZE, TILE_SIZE, 0x00FF00, 0.35)
|
||||||
this.ghost.setDepth(20)
|
this.ghost.setDepth(1000)
|
||||||
this.ghost.setVisible(false)
|
this.ghost.setVisible(false)
|
||||||
this.ghost.setStrokeStyle(2, 0x00FF00, 0.8)
|
this.ghost.setStrokeStyle(2, 0x00FF00, 0.8)
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ export class BuildingSystem {
|
|||||||
fontSize: '10px', color: '#ffffff', fontFamily: 'monospace',
|
fontSize: '10px', color: '#ffffff', fontFamily: 'monospace',
|
||||||
backgroundColor: '#000000aa', padding: { x: 3, y: 2 }
|
backgroundColor: '#000000aa', padding: { x: 3, y: 2 }
|
||||||
})
|
})
|
||||||
this.ghostLabel.setDepth(21)
|
this.ghostLabel.setDepth(1001)
|
||||||
this.ghostLabel.setVisible(false)
|
this.ghostLabel.setVisible(false)
|
||||||
this.ghostLabel.setOrigin(0.5, 1)
|
this.ghostLabel.setOrigin(0.5, 1)
|
||||||
|
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ export class ResourceSystem {
|
|||||||
sprite.setOrigin(0.5, 0.75)
|
sprite.setOrigin(0.5, 0.75)
|
||||||
}
|
}
|
||||||
|
|
||||||
sprite.setDepth(5)
|
sprite.setDepth(node.tileY + 5)
|
||||||
|
|
||||||
const healthBar = this.scene.add.graphics()
|
const healthBar = this.scene.add.graphics()
|
||||||
healthBar.setDepth(6)
|
healthBar.setDepth(node.tileY + 6)
|
||||||
healthBar.setVisible(false)
|
healthBar.setVisible(false)
|
||||||
|
|
||||||
this.sprites.set(node.id, { sprite, node, healthBar })
|
this.sprites.set(node.id, { sprite, node, healthBar })
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export class TreeSeedlingSystem {
|
|||||||
const key = `seedling_${Math.min(s.stage, 2)}`
|
const key = `seedling_${Math.min(s.stage, 2)}`
|
||||||
const sprite = this.scene.add.image(x, y, key)
|
const sprite = this.scene.add.image(x, y, key)
|
||||||
.setOrigin(0.5, 0.85)
|
.setOrigin(0.5, 0.85)
|
||||||
.setDepth(5)
|
.setDepth(s.tileY + 5)
|
||||||
this.sprites.set(s.id, sprite)
|
this.sprites.set(s.id, sprite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ const ARRIVAL_PX = 3
|
|||||||
|
|
||||||
const WORK_LOG_MAX = 20
|
const WORK_LOG_MAX = 20
|
||||||
|
|
||||||
|
/** Job-type → display icon mapping; defined once at module level to avoid per-frame allocation. */
|
||||||
|
const JOB_ICONS: Record<string, string> = { chop: '🪓', mine: '⛏', farm: '🌾', forester: '🌲', '': '' }
|
||||||
|
|
||||||
interface VillagerRuntime {
|
interface VillagerRuntime {
|
||||||
sprite: Phaser.GameObjects.Image
|
sprite: Phaser.GameObjects.Image
|
||||||
nameLabel: Phaser.GameObjects.Text
|
nameLabel: Phaser.GameObjects.Text
|
||||||
@@ -118,15 +121,15 @@ export class VillagerSystem {
|
|||||||
case 'sleeping':this.tickSleeping(v, rt, delta); break
|
case 'sleeping':this.tickSleeping(v, rt, delta); break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync sprite to state position
|
// Nisse always render above world objects
|
||||||
rt.sprite.setPosition(v.x, v.y)
|
rt.sprite.setPosition(v.x, v.y)
|
||||||
|
|
||||||
rt.nameLabel.setPosition(v.x, v.y - 22)
|
rt.nameLabel.setPosition(v.x, v.y - 22)
|
||||||
rt.energyBar.setPosition(0, 0)
|
rt.energyBar.setPosition(0, 0)
|
||||||
this.drawEnergyBar(rt.energyBar, v.x, v.y, v.energy)
|
this.drawEnergyBar(rt.energyBar, v.x, v.y, v.energy)
|
||||||
|
|
||||||
// Job icon
|
// Job icon
|
||||||
const icons: Record<string, string> = { chop: '🪓', mine: '⛏', farm: '🌾', forester: '🌲', '': '' }
|
const jobType = v.aiState === 'sleeping' ? '💤' : (v.job ? (JOB_ICONS[v.job.type] ?? '') : '')
|
||||||
const jobType = v.aiState === 'sleeping' ? '💤' : (v.job ? (icons[v.job.type] ?? '') : '')
|
|
||||||
rt.jobIcon.setText(jobType).setPosition(v.x + 10, v.y - 18)
|
rt.jobIcon.setText(jobType).setPosition(v.x + 10, v.y - 18)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,20 +379,27 @@ export class VillagerSystem {
|
|||||||
const vTY = Math.floor(v.y / TILE_SIZE)
|
const vTY = Math.floor(v.y / TILE_SIZE)
|
||||||
const dist = (tx: number, ty: number) => Math.abs(tx - vTX) + Math.abs(ty - vTY)
|
const dist = (tx: number, ty: number) => Math.abs(tx - vTX) + Math.abs(ty - vTY)
|
||||||
|
|
||||||
|
// Extract state collections once — avoids repeated Object.values() allocation per branch/loop.
|
||||||
|
const resources = Object.values(state.world.resources)
|
||||||
|
const buildings = Object.values(state.world.buildings)
|
||||||
|
const crops = Object.values(state.world.crops)
|
||||||
|
const seedlings = Object.values(state.world.treeSeedlings)
|
||||||
|
const zones = Object.values(state.world.foresterZones)
|
||||||
|
|
||||||
type C = { type: JobType; targetId: string; tileX: number; tileY: number; dist: number; pri: number }
|
type C = { type: JobType; targetId: string; tileX: number; tileY: number; dist: number; pri: number }
|
||||||
const candidates: C[] = []
|
const candidates: C[] = []
|
||||||
|
|
||||||
if (p.chop > 0) {
|
if (p.chop > 0) {
|
||||||
// Build the set of all tiles belonging to forester zones for chop priority
|
// Build the set of all tiles belonging to forester zones for chop priority
|
||||||
const zoneTiles = new Set<string>()
|
const zoneTiles = new Set<string>()
|
||||||
for (const zone of Object.values(state.world.foresterZones)) {
|
for (const zone of zones) {
|
||||||
for (const key of zone.tiles) zoneTiles.add(key)
|
for (const key of zone.tiles) zoneTiles.add(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
const zoneChop: C[] = []
|
const zoneChop: C[] = []
|
||||||
const naturalChop: C[] = []
|
const naturalChop: C[] = []
|
||||||
|
|
||||||
for (const res of Object.values(state.world.resources)) {
|
for (const res of resources) {
|
||||||
if (res.kind !== 'tree' || this.claimed.has(res.id)) continue
|
if (res.kind !== 'tree' || this.claimed.has(res.id)) continue
|
||||||
// Skip trees with no reachable neighbour — A* cannot reach them.
|
// Skip trees with no reachable neighbour — A* cannot reach them.
|
||||||
if (!this.hasAdjacentPassable(res.tileX, res.tileY)) continue
|
if (!this.hasAdjacentPassable(res.tileX, res.tileY)) continue
|
||||||
@@ -405,7 +415,7 @@ export class VillagerSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (p.mine > 0) {
|
if (p.mine > 0) {
|
||||||
for (const res of Object.values(state.world.resources)) {
|
for (const res of resources) {
|
||||||
if (res.kind !== 'rock' || this.claimed.has(res.id)) continue
|
if (res.kind !== 'rock' || this.claimed.has(res.id)) continue
|
||||||
// Same reachability guard for rock tiles.
|
// Same reachability guard for rock tiles.
|
||||||
if (!this.hasAdjacentPassable(res.tileX, res.tileY)) continue
|
if (!this.hasAdjacentPassable(res.tileX, res.tileY)) continue
|
||||||
@@ -414,7 +424,7 @@ export class VillagerSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (p.farm > 0) {
|
if (p.farm > 0) {
|
||||||
for (const crop of Object.values(state.world.crops)) {
|
for (const crop of crops) {
|
||||||
if (crop.stage < crop.maxStage || this.claimed.has(crop.id)) continue
|
if (crop.stage < crop.maxStage || this.claimed.has(crop.id)) continue
|
||||||
candidates.push({ type: 'farm', targetId: crop.id, tileX: crop.tileX, tileY: crop.tileY, dist: dist(crop.tileX, crop.tileY), pri: p.farm })
|
candidates.push({ type: 'farm', targetId: crop.id, tileX: crop.tileX, tileY: crop.tileY, dist: dist(crop.tileX, crop.tileY), pri: p.farm })
|
||||||
}
|
}
|
||||||
@@ -422,7 +432,7 @@ export class VillagerSystem {
|
|||||||
|
|
||||||
if (p.forester > 0 && (state.world.stockpile.tree_seed ?? 0) > 0) {
|
if (p.forester > 0 && (state.world.stockpile.tree_seed ?? 0) > 0) {
|
||||||
// Find empty plantable zone tiles to seed
|
// Find empty plantable zone tiles to seed
|
||||||
for (const zone of Object.values(state.world.foresterZones)) {
|
for (const zone of zones) {
|
||||||
for (const key of zone.tiles) {
|
for (const key of zone.tiles) {
|
||||||
const [tx, ty] = key.split(',').map(Number)
|
const [tx, ty] = key.split(',').map(Number)
|
||||||
const targetId = `forester_tile_${tx}_${ty}`
|
const targetId = `forester_tile_${tx}_${ty}`
|
||||||
@@ -430,12 +440,12 @@ export class VillagerSystem {
|
|||||||
// Skip if tile is not plantable
|
// Skip if tile is not plantable
|
||||||
const tileType = state.world.tiles[ty * WORLD_TILES + tx] as TileType
|
const tileType = state.world.tiles[ty * WORLD_TILES + tx] as TileType
|
||||||
if (!PLANTABLE_TILES.has(tileType)) continue
|
if (!PLANTABLE_TILES.has(tileType)) continue
|
||||||
// Skip if something occupies this tile
|
// Skip if something occupies this tile — reuse already-extracted arrays
|
||||||
const occupied =
|
const occupied =
|
||||||
Object.values(state.world.resources).some(r => r.tileX === tx && r.tileY === ty) ||
|
resources.some(r => r.tileX === tx && r.tileY === ty) ||
|
||||||
Object.values(state.world.buildings).some(b => b.tileX === tx && b.tileY === ty) ||
|
buildings.some(b => b.tileX === tx && b.tileY === ty) ||
|
||||||
Object.values(state.world.crops).some(c => c.tileX === tx && c.tileY === ty) ||
|
crops.some(c => c.tileX === tx && c.tileY === ty) ||
|
||||||
Object.values(state.world.treeSeedlings).some(s => s.tileX === tx && s.tileY === ty)
|
seedlings.some(s => s.tileX === tx && s.tileY === ty)
|
||||||
if (occupied) continue
|
if (occupied) continue
|
||||||
candidates.push({ type: 'forester', targetId, tileX: tx, tileY: ty, dist: dist(tx, ty), pri: p.forester })
|
candidates.push({ type: 'forester', targetId, tileX: tx, tileY: ty, dist: dist(tx, ty), pri: p.forester })
|
||||||
}
|
}
|
||||||
@@ -444,8 +454,9 @@ export class VillagerSystem {
|
|||||||
|
|
||||||
if (candidates.length === 0) return null
|
if (candidates.length === 0) return null
|
||||||
|
|
||||||
// Lowest priority number wins; ties broken by distance
|
// Lowest priority number wins; ties broken by distance — avoid spread+map allocation
|
||||||
const bestPri = Math.min(...candidates.map(c => c.pri))
|
let bestPri = candidates[0].pri
|
||||||
|
for (let i = 1; i < candidates.length; i++) if (candidates[i].pri < bestPri) bestPri = candidates[i].pri
|
||||||
return candidates
|
return candidates
|
||||||
.filter(c => c.pri === bestPri)
|
.filter(c => c.pri === bestPri)
|
||||||
.sort((a, b) => a.dist - b.dist)[0] ?? null
|
.sort((a, b) => a.dist - b.dist)[0] ?? null
|
||||||
@@ -569,16 +580,23 @@ export class VillagerSystem {
|
|||||||
* for a newly added Nisse.
|
* for a newly added Nisse.
|
||||||
* @param v - Villager state to create sprites for
|
* @param v - Villager state to create sprites for
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Creates and registers all runtime objects (sprite, outline, label, energy bar, icon)
|
||||||
|
* for a newly added Nisse.
|
||||||
|
* @param v - Villager state to create sprites for
|
||||||
|
*/
|
||||||
private spawnSprite(v: VillagerState): void {
|
private spawnSprite(v: VillagerState): void {
|
||||||
const sprite = this.scene.add.image(v.x, v.y, 'villager').setDepth(11)
|
// Nisse always render above trees, buildings and other world objects.
|
||||||
|
const sprite = this.scene.add.image(v.x, v.y, 'villager')
|
||||||
|
.setDepth(900)
|
||||||
|
|
||||||
const nameLabel = this.scene.add.text(v.x, v.y - 22, v.name, {
|
const nameLabel = this.scene.add.text(v.x, v.y - 22, v.name, {
|
||||||
fontSize: '8px', color: '#ffffff', fontFamily: 'monospace',
|
fontSize: '8px', color: '#ffffff', fontFamily: 'monospace',
|
||||||
backgroundColor: '#00000088', padding: { x: 2, y: 1 },
|
backgroundColor: '#00000088', padding: { x: 2, y: 1 },
|
||||||
}).setOrigin(0.5, 1).setDepth(12)
|
}).setOrigin(0.5, 1).setDepth(901)
|
||||||
|
|
||||||
const energyBar = this.scene.add.graphics().setDepth(12)
|
const energyBar = this.scene.add.graphics().setDepth(901)
|
||||||
const jobIcon = this.scene.add.text(v.x, v.y - 18, '', { fontSize: '10px' }).setDepth(13)
|
const jobIcon = this.scene.add.text(v.x, v.y - 18, '', { fontSize: '10px' }).setDepth(902)
|
||||||
|
|
||||||
sprite.setInteractive()
|
sprite.setInteractive()
|
||||||
sprite.on('pointerdown', () => this.onNisseClick?.(v.id))
|
sprite.on('pointerdown', () => this.onNisseClick?.(v.id))
|
||||||
@@ -667,6 +685,14 @@ export class VillagerSystem {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys all Nisse sprites and clears the runtime map.
|
||||||
|
* Should be called when the scene shuts down.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Destroys all Nisse sprites and clears the runtime map.
|
||||||
|
* Should be called when the scene shuts down.
|
||||||
|
*/
|
||||||
/**
|
/**
|
||||||
* Destroys all Nisse sprites and clears the runtime map.
|
* Destroys all Nisse sprites and clears the runtime map.
|
||||||
* Should be called when the scene shuts down.
|
* Should be called when the scene shuts down.
|
||||||
|
|||||||
@@ -172,6 +172,16 @@ export class WorldSystem {
|
|||||||
this.resourceTiles.delete(tileY * WORLD_TILES + tileX)
|
this.resourceTiles.delete(tileY * WORLD_TILES + tileX)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a resource (tree or rock) occupies the given tile.
|
||||||
|
* Uses the O(1) resourceTiles index.
|
||||||
|
* @param tileX - Tile column
|
||||||
|
* @param tileY - Tile row
|
||||||
|
*/
|
||||||
|
hasResourceAt(tileX: number, tileY: number): boolean {
|
||||||
|
return this.resourceTiles.has(tileY * WORLD_TILES + tileX)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts world pixel coordinates to tile coordinates.
|
* Converts world pixel coordinates to tile coordinates.
|
||||||
* @param worldX - World X in pixels
|
* @param worldX - World X in pixels
|
||||||
|
|||||||
Reference in New Issue
Block a user