Files
nissefolk/src/scenes/UIScene.ts

523 lines
24 KiB
TypeScript
Raw Normal View History

2026-03-20 08:11:31 +00:00
import Phaser from 'phaser'
import type { BuildingType, JobPriorities } from '../types'
import type { FarmingTool } from '../systems/FarmingSystem'
import type { DebugData } from '../systems/DebugSystem'
2026-03-20 08:11:31 +00:00
import { stateManager } from '../StateManager'
const ITEM_ICONS: Record<string, string> = {
wood: '🪵', stone: '🪨', wheat_seed: '🌱', carrot_seed: '🥕',
wheat: '🌾', carrot: '🧡',
}
export class UIScene extends Phaser.Scene {
private stockpileTexts: Map<string, Phaser.GameObjects.Text> = new Map()
private stockpilePanel!: Phaser.GameObjects.Rectangle
private hintText!: Phaser.GameObjects.Text
private toastText!: Phaser.GameObjects.Text
private toastTimer = 0
private buildMenuGroup!: Phaser.GameObjects.Group
private buildMenuVisible = false
private villagerPanelGroup!: Phaser.GameObjects.Group
private villagerPanelVisible = false
private buildModeText!: Phaser.GameObjects.Text
private farmToolText!: Phaser.GameObjects.Text
private coordsText!: Phaser.GameObjects.Text
private controlsHintText!: Phaser.GameObjects.Text
2026-03-20 08:11:31 +00:00
private popText!: Phaser.GameObjects.Text
private stockpileTitleText!: Phaser.GameObjects.Text
private contextMenuGroup!: Phaser.GameObjects.Group
private contextMenuVisible = false
private inBuildMode = false
private inFarmMode = false
private debugPanelText!: Phaser.GameObjects.Text
private debugActive = false
2026-03-20 08:11:31 +00:00
constructor() { super({ key: 'UI' }) }
/**
* Creates all HUD elements, wires up game scene events, and registers
* keyboard shortcuts (B, V, F3, ESC).
*/
2026-03-20 08:11:31 +00:00
create(): void {
this.createStockpilePanel()
this.createHintText()
this.createToast()
this.createBuildMenu()
this.createBuildModeIndicator()
this.createFarmToolIndicator()
this.createCoordsDisplay()
this.createDebugPanel()
2026-03-20 08:11:31 +00:00
const gameScene = this.scene.get('Game')
gameScene.events.on('buildModeChanged', (a: boolean, b: BuildingType) => this.onBuildModeChanged(a, b))
gameScene.events.on('farmToolChanged', (t: FarmingTool, l: string) => this.onFarmToolChanged(t, l))
gameScene.events.on('toast', (m: string) => this.showToast(m))
gameScene.events.on('openBuildMenu', () => this.toggleBuildMenu())
gameScene.events.on('cameraMoved', (pos: { tileX: number; tileY: number }) => this.onCameraMoved(pos))
this.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.B)
.on('down', () => gameScene.events.emit('uiRequestBuildMenu'))
this.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.V)
.on('down', () => this.toggleVillagerPanel())
this.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.F3)
.on('down', () => this.toggleDebugPanel())
2026-03-20 08:11:31 +00:00
this.scale.on('resize', () => this.repositionUI())
this.input.mouse!.disableContextMenu()
this.contextMenuGroup = this.add.group()
this.input.on('pointerdown', (ptr: Phaser.Input.Pointer) => {
if (ptr.rightButtonDown()) {
if (!this.inBuildMode && !this.inFarmMode && !this.buildMenuVisible && !this.villagerPanelVisible) {
this.showContextMenu(ptr.x, ptr.y)
}
} else if (this.contextMenuVisible) {
this.hideContextMenu()
}
})
this.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.ESC)
.on('down', () => this.hideContextMenu())
2026-03-20 08:11:31 +00:00
}
/**
* Updates the stockpile display, toast fade timer, population count,
* and the debug panel each frame.
* @param _t - Total elapsed time (unused)
* @param delta - Frame delta in milliseconds
*/
2026-03-20 08:11:31 +00:00
update(_t: number, delta: number): void {
this.updateStockpile()
this.updateToast(delta)
this.updatePopText()
if (this.debugActive) this.updateDebugPanel()
2026-03-20 08:11:31 +00:00
}
// ─── Stockpile ────────────────────────────────────────────────────────────
/** Creates the stockpile panel in the top-right corner with item rows and population count. */
2026-03-20 08:11:31 +00:00
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.stockpileTitleText = this.add.text(x + 10, y + 7, '⚡ STOCKPILE', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101)
2026-03-20 08:11:31 +00:00
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, '👥 Nisse: 0 / 0', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(101)
2026-03-20 08:11:31 +00:00
}
/** Refreshes all item quantities and colors in the stockpile panel. */
2026-03-20 08:11:31 +00:00
private updateStockpile(): void {
const sp = stateManager.getState().world.stockpile
for (const [item, t] of this.stockpileTexts) {
const qty = sp[item as keyof typeof sp] ?? 0
t.setStyle({ color: qty > 0 ? '#88dd88' : '#444444' })
t.setText(`${ITEM_ICONS[item]} ${item}: ${qty}`)
}
}
/** Updates the Nisse population / bed capacity counter. */
2026-03-20 08:11:31 +00:00
private updatePopText(): void {
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(`👥 Nisse: ${current} / ${beds} [V]`)
2026-03-20 08:11:31 +00:00
}
// ─── Hint ─────────────────────────────────────────────────────────────────
/** Creates the centered hint text element near the bottom of the screen. */
2026-03-20 08:11:31 +00:00
private createHintText(): void {
this.hintText = this.add.text(this.scale.width / 2, this.scale.height - 40, '', {
fontSize: '14px', color: '#ffff88', fontFamily: 'monospace',
backgroundColor: '#00000099', padding: { x: 10, y: 5 },
}).setOrigin(0.5).setScrollFactor(0).setDepth(100).setVisible(false)
}
// ─── Toast ────────────────────────────────────────────────────────────────
/** Creates the toast notification text element (top center, initially hidden). */
2026-03-20 08:11:31 +00:00
private createToast(): void {
this.toastText = this.add.text(this.scale.width / 2, 60, '', {
fontSize: '15px', color: '#88ff88', fontFamily: 'monospace',
backgroundColor: '#00000099', padding: { x: 12, y: 6 },
}).setOrigin(0.5, 0).setScrollFactor(0).setDepth(102).setAlpha(0)
}
/**
* Displays a toast message for 2.2 seconds then fades it out.
* @param msg - Message to display
*/
2026-03-20 08:11:31 +00:00
showToast(msg: string): void { this.toastText.setText(msg).setAlpha(1); this.toastTimer = 2200 }
/**
* Counts down the toast timer and triggers the fade-out tween when it expires.
* @param delta - Frame delta in milliseconds
*/
2026-03-20 08:11:31 +00:00
private updateToast(delta: number): void {
if (this.toastTimer <= 0) return
this.toastTimer -= delta
if (this.toastTimer <= 0) this.tweens.add({ targets: this.toastText, alpha: 0, duration: 400 })
}
// ─── Build Menu ───────────────────────────────────────────────────────────
/** Creates and hides the build menu with buttons for each available building type. */
2026-03-20 08:11:31 +00:00
private createBuildMenu(): void {
this.buildMenuGroup = this.add.group()
const buildings: { kind: BuildingType; label: string; cost: string }[] = [
{ kind: 'floor', label: 'Floor', cost: '2 wood' },
{ kind: 'wall', label: 'Wall', cost: '3 wood + 1 stone' },
{ kind: 'chest', label: 'Chest', cost: '5 wood + 2 stone' },
{ kind: 'bed', label: '🛏 Bed', cost: '6 wood (+1 villager)' },
{ kind: 'stockpile_zone', label: '📦 Stockpile', cost: 'free (workers deliver here)' },
]
const menuX = this.scale.width / 2 - 150, menuY = this.scale.height / 2 - 140
const bg = this.add.rectangle(menuX, menuY, 300, 280, 0x000000, 0.88).setOrigin(0,0).setScrollFactor(0).setDepth(200)
this.buildMenuGroup.add(bg)
this.buildMenuGroup.add(this.add.text(menuX + 150, menuY + 14, 'BUILD MENU [B/ESC]', { fontSize: '11px', color: '#aaaaaa', fontFamily: 'monospace' }).setOrigin(0.5,0).setScrollFactor(0).setDepth(201))
buildings.forEach((b, i) => {
const btnY = menuY + 38 + i * 46
const btn = this.add.rectangle(menuX + 14, btnY, 272, 38, 0x1a3a1a, 0.9).setOrigin(0,0).setScrollFactor(0).setDepth(201).setInteractive()
btn.on('pointerover', () => btn.setFillStyle(0x2d6a4f, 0.9))
btn.on('pointerout', () => btn.setFillStyle(0x1a3a1a, 0.9))
btn.on('pointerdown', () => { this.closeBuildMenu(); this.scene.get('Game').events.emit('selectBuilding', b.kind) })
this.buildMenuGroup.add(btn)
this.buildMenuGroup.add(this.add.text(menuX + 24, btnY + 5, b.label, { fontSize: '13px', color: '#ffffff', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(202))
this.buildMenuGroup.add(this.add.text(menuX + 24, btnY + 22, `Cost: ${b.cost}`, { fontSize: '10px', color: '#888888', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(202))
})
this.buildMenuGroup.setVisible(false)
}
/** Toggles the build menu open or closed. */
2026-03-20 08:11:31 +00:00
private toggleBuildMenu(): void { this.buildMenuVisible ? this.closeBuildMenu() : this.openBuildMenu() }
/** Opens the build menu and notifies GameScene that a menu is active. */
2026-03-20 08:11:31 +00:00
private openBuildMenu(): void { this.buildMenuVisible = true; this.buildMenuGroup.setVisible(true); this.scene.get('Game').events.emit('uiMenuOpen') }
/** Closes the build menu and notifies GameScene that no menu is active. */
2026-03-20 08:11:31 +00:00
private closeBuildMenu(): void { this.buildMenuVisible = false; this.buildMenuGroup.setVisible(false); this.scene.get('Game').events.emit('uiMenuClose') }
// ─── Villager Panel (V key) ───────────────────────────────────────────────
/** Toggles the Nisse management panel open or closed. */
2026-03-20 08:11:31 +00:00
private toggleVillagerPanel(): void {
if (this.villagerPanelVisible) {
this.closeVillagerPanel()
} else {
this.openVillagerPanel()
}
}
/** Opens the Nisse panel, builds its contents, and notifies GameScene. */
2026-03-20 08:11:31 +00:00
private openVillagerPanel(): void {
this.villagerPanelVisible = true
this.buildVillagerPanel()
this.scene.get('Game').events.emit('uiMenuOpen')
}
/** Closes and destroys the Nisse panel and notifies GameScene. */
2026-03-20 08:11:31 +00:00
private closeVillagerPanel(): void {
this.villagerPanelVisible = false
this.villagerPanelGroup?.destroy(true)
this.scene.get('Game').events.emit('uiMenuClose')
}
/**
* Destroys and rebuilds the Nisse panel from current state.
* Shows name, status, energy bar, and job priority buttons per Nisse.
*/
2026-03-20 08:11:31 +00:00
private buildVillagerPanel(): void {
if (this.villagerPanelGroup) this.villagerPanelGroup.destroy(true)
this.villagerPanelGroup = this.add.group()
const state = stateManager.getState()
const villagers = Object.values(state.world.villagers)
const panelW = 420
const rowH = 60
const panelH = Math.max(100, villagers.length * rowH + 50)
const px = this.scale.width / 2 - panelW / 2
const py = this.scale.height / 2 - panelH / 2
const bg = this.add.rectangle(px, py, panelW, panelH, 0x0a0a0a, 0.92).setOrigin(0,0).setScrollFactor(0).setDepth(210)
this.villagerPanelGroup.add(bg)
this.villagerPanelGroup.add(
this.add.text(px + panelW/2, py + 12, '👥 NISSE [V] close', { fontSize: '12px', color: '#aaaaaa', fontFamily: 'monospace' })
2026-03-20 08:11:31 +00:00
.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 Nisse yet.\nBuild a 🛏 Bed first!', {
2026-03-20 08:11:31 +00:00
fontSize: '13px', color: '#666666', fontFamily: 'monospace', align: 'center'
}).setOrigin(0.5).setScrollFactor(0).setDepth(211)
)
}
villagers.forEach((v, i) => {
const ry = py + 38 + i * rowH
const gameScene = this.scene.get('Game') as any
// Name + status
const statusText = gameScene.villagerSystem?.getStatusText(v.id) ?? '—'
this.villagerPanelGroup.add(
this.add.text(px + 12, ry, `${v.name}`, { fontSize: '13px', color: '#ffffff', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(211)
)
this.villagerPanelGroup.add(
this.add.text(px + 12, ry + 16, statusText, { fontSize: '10px', color: '#888888', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(211)
)
// Energy bar
const eg = this.add.graphics().setScrollFactor(0).setDepth(211)
eg.fillStyle(0x333333); eg.fillRect(px + 12, ry + 30, 80, 6)
const col = v.energy > 60 ? 0x4CAF50 : v.energy > 30 ? 0xFF9800 : 0xF44336
eg.fillStyle(col); eg.fillRect(px + 12, ry + 30, 80 * v.energy / 100, 6)
this.villagerPanelGroup.add(eg)
// Job priority buttons: chop / mine / farm
const jobs: Array<{ key: keyof JobPriorities; label: string }> = [
{ key: 'chop', label: '🪓' }, { key: 'mine', label: '⛏' }, { key: 'farm', label: '🌾' }
]
jobs.forEach((job, ji) => {
const bx = px + 110 + ji * 100
const pri = v.priorities[job.key]
const label = pri === 0 ? `${job.label} OFF` : `${job.label} P${pri}`
const btn = this.add.text(bx, ry + 6, label, {
fontSize: '11px', color: pri === 0 ? '#555555' : '#ffffff',
fontFamily: 'monospace', backgroundColor: pri === 0 ? '#1a1a1a' : '#1a4a1a',
padding: { x: 6, y: 4 }
}).setScrollFactor(0).setDepth(212).setInteractive()
btn.on('pointerover', () => btn.setStyle({ backgroundColor: '#2d6a4f' }))
btn.on('pointerout', () => btn.setStyle({ backgroundColor: pri === 0 ? '#1a1a1a' : '#1a4a1a' }))
btn.on('pointerdown', () => {
const newPri = ((v.priorities[job.key] + 1) % 5) // 0→1→2→3→4→0
const newPriorities: JobPriorities = { ...v.priorities, [job.key]: newPri }
this.scene.get('Game').events.emit('updatePriorities', v.id, newPriorities)
this.closeVillagerPanel()
this.openVillagerPanel() // Rebuild to reflect change
})
this.villagerPanelGroup.add(btn)
})
})
}
// ─── Build mode indicator ─────────────────────────────────────────────────
/** Creates the build-mode indicator text in the top-left corner (initially hidden). */
2026-03-20 08:11:31 +00:00
private createBuildModeIndicator(): void {
this.buildModeText = this.add.text(10, 10, '', { fontSize: '13px', color: '#ffff00', fontFamily: 'monospace', backgroundColor: '#00000099', padding: { x: 8, y: 4 } }).setScrollFactor(0).setDepth(100).setVisible(false)
}
/**
* Shows or hides the build-mode indicator based on whether build mode is active.
* @param active - Whether build mode is currently active
* @param building - The selected building type
*/
2026-03-20 08:11:31 +00:00
private onBuildModeChanged(active: boolean, building: BuildingType): void {
this.inBuildMode = active
2026-03-20 08:11:31 +00:00
this.buildModeText.setText(active ? `🏗 BUILD: ${building.toUpperCase()} [RMB/ESC cancel]` : '').setVisible(active)
}
// ─── Farm tool indicator ──────────────────────────────────────────────────
/** Creates the farm-tool indicator text below the build-mode indicator (initially hidden). */
2026-03-20 08:11:31 +00:00
private createFarmToolIndicator(): void {
this.farmToolText = this.add.text(10, 44, '', { fontSize: '13px', color: '#aaffaa', fontFamily: 'monospace', backgroundColor: '#00000099', padding: { x: 8, y: 4 } }).setScrollFactor(0).setDepth(100).setVisible(false)
}
/**
* Shows or hides the farm-tool indicator and updates the active tool label.
* @param tool - Currently selected farm tool
* @param label - Human-readable label for the tool
*/
2026-03-20 08:11:31 +00:00
private onFarmToolChanged(tool: FarmingTool, label: string): void {
this.inFarmMode = tool !== 'none'
2026-03-20 08:11:31 +00:00
this.farmToolText.setText(tool === 'none' ? '' : `[F] Farm: ${label} [RMB cancel]`).setVisible(tool !== 'none')
}
// ─── Coords + controls ────────────────────────────────────────────────────
/** Creates the tile-coordinate display and controls hint at the bottom-left. */
2026-03-20 08:11:31 +00:00
private createCoordsDisplay(): void {
this.coordsText = this.add.text(10, this.scale.height - 24, '', { fontSize: '11px', color: '#666666', fontFamily: 'monospace' }).setScrollFactor(0).setDepth(100)
this.controlsHintText = this.add.text(10, this.scale.height - 42, '[WASD] Pan [Scroll] Zoom [F] Farm [B] Build [V] Nisse [F3] Debug', {
2026-03-20 08:11:31 +00:00
fontSize: '10px', color: '#444444', fontFamily: 'monospace', backgroundColor: '#00000066', padding: { x: 4, y: 2 }
}).setScrollFactor(0).setDepth(100)
}
/**
* Updates the tile-coordinate display when the camera moves.
* @param pos - Tile position of the camera center
*/
2026-03-20 08:11:31 +00:00
private onCameraMoved(pos: { tileX: number; tileY: number }): void {
this.coordsText.setText(`Tile: ${pos.tileX}, ${pos.tileY}`)
}
// ─── Debug Panel (F3) ─────────────────────────────────────────────────────
/** Creates the debug panel text object (initially hidden). */
private createDebugPanel(): void {
this.debugPanelText = this.add.text(10, 80, '', {
fontSize: '12px',
color: '#cccccc',
backgroundColor: '#000000cc',
padding: { x: 8, y: 6 },
lineSpacing: 2,
fontFamily: 'monospace',
}).setScrollFactor(0).setDepth(150).setVisible(false)
}
/** Toggles the debug panel and notifies GameScene to toggle the pathfinding overlay. */
private toggleDebugPanel(): void {
this.debugActive = !this.debugActive
this.debugPanelText.setVisible(this.debugActive)
this.scene.get('Game').events.emit('debugToggle')
}
/**
* Reads current debug data from DebugSystem and updates the panel text.
* Called every frame while debug mode is active.
*/
private updateDebugPanel(): void {
const gameScene = this.scene.get('Game') as any
const debugSystem = gameScene.debugSystem
if (!debugSystem?.isActive()) return
const ptr = this.input.activePointer
const data = debugSystem.getDebugData(ptr) as DebugData
const resLine = data.resourcesOnTile.length > 0
? data.resourcesOnTile.map(r => `${r.kind} (hp:${r.hp})`).join(', ')
: '—'
const bldLine = data.buildingsOnTile.length > 0 ? data.buildingsOnTile.join(', ') : '—'
const cropLine = data.cropsOnTile.length > 0
? data.cropsOnTile.map(c => `${c.kind} (${c.stage}/${c.maxStage})`).join(', ')
: '—'
const { idle, walking, working, sleeping } = data.nisseByState
const { chop, mine, farm } = data.jobsByType
this.debugPanelText.setText([
'── F3 DEBUG ──────────────────',
`FPS: ${data.fps}`,
'',
`Mouse world: ${data.mouseWorld.x.toFixed(1)}, ${data.mouseWorld.y.toFixed(1)}`,
`Mouse tile: ${data.mouseTile.tileX}, ${data.mouseTile.tileY}`,
`Tile type: ${data.tileType}`,
`Resources: ${resLine}`,
`Buildings: ${bldLine}`,
`Crops: ${cropLine}`,
'',
`Nisse: ${data.nisseTotal} total`,
` idle: ${idle} walking: ${walking} working: ${working} sleeping: ${sleeping}`,
'',
`Jobs active:`,
` chop: ${chop} mine: ${mine} farm: ${farm}`,
'',
`Paths: ${data.activePaths} (cyan lines in world)`,
'',
'[F3] close',
])
}
// ─── Context Menu ─────────────────────────────────────────────────────────
/**
* Shows the right-click context menu at the given screen coordinates.
* Any previously open context menu is closed first.
* @param x - Screen x position of the pointer
* @param y - Screen y position of the pointer
*/
private showContextMenu(x: number, y: number): void {
this.hideContextMenu()
const menuW = 150
const btnH = 32
const menuH = 8 + 2 * (btnH + 6) - 6 + 8
const mx = Math.min(x, this.scale.width - menuW - 4)
const my = Math.min(y, this.scale.height - menuH - 4)
const bg = this.add.rectangle(mx, my, menuW, menuH, 0x000000, 0.88)
.setOrigin(0, 0).setScrollFactor(0).setDepth(300)
this.contextMenuGroup.add(bg)
const entries: { label: string; action: () => void }[] = [
{
label: '🏗 Build',
action: () => { this.hideContextMenu(); this.scene.get('Game').events.emit('uiRequestBuildMenu') },
},
{
label: '👥 Nisse',
action: () => { this.hideContextMenu(); this.toggleVillagerPanel() },
},
]
entries.forEach((entry, i) => {
const by = my + 8 + i * (btnH + 6)
const btn = this.add.rectangle(mx + 8, by, menuW - 16, btnH, 0x1a3a1a, 0.9)
.setOrigin(0, 0).setScrollFactor(0).setDepth(301).setInteractive()
btn.on('pointerover', () => btn.setFillStyle(0x2d6a4f, 0.9))
btn.on('pointerout', () => btn.setFillStyle(0x1a3a1a, 0.9))
btn.on('pointerdown', entry.action)
this.contextMenuGroup.add(btn)
this.contextMenuGroup.add(
this.add.text(mx + 16, by + btnH / 2, entry.label, {
fontSize: '13px', color: '#ffffff', fontFamily: 'monospace',
}).setOrigin(0, 0.5).setScrollFactor(0).setDepth(302)
)
})
this.contextMenuVisible = true
this.scene.get('Game').events.emit('uiMenuOpen')
}
/**
* Closes and destroys the context menu if it is currently visible.
*/
private hideContextMenu(): void {
if (!this.contextMenuVisible) return
this.contextMenuGroup.destroy(true)
this.contextMenuGroup = this.add.group()
this.contextMenuVisible = false
this.scene.get('Game').events.emit('uiMenuClose')
}
2026-03-20 08:11:31 +00:00
// ─── Resize ───────────────────────────────────────────────────────────────
/**
* Repositions all fixed UI elements after a canvas resize.
* Open overlay panels are closed so they reopen correctly centered.
*/
2026-03-20 08:11:31 +00:00
private repositionUI(): void {
const { width, height } = this.scale
// 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)
2026-03-20 08:11:31 +00:00
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()
2026-03-20 08:11:31 +00:00
}
}