Compare commits
4 Commits
fix/rock-t
...
feature/mi
| Author | SHA1 | Date | |
|---|---|---|---|
| fcf805d4d2 | |||
| ccc224e2b9 | |||
| 202ff435f7 | |||
| a6c2aa5309 |
@@ -7,6 +7,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Mine Building** (Issue #42): 3×2 building placeable only on resource-free ROCK tiles (costs: 200 wood + 50 stone); Nisse with mine priority walk to the entrance, disappear inside for 15 s, then reappear carrying 2 stone; up to 3 Nisse work simultaneously; ⛏ X/3 status label shown directly on the building in world space; surface rock harvesting remains functional alongside the building
|
||||
|
||||
### Fixed
|
||||
- **Debug-Panel überlagert Nisse-Info-Panel** (Issue #41): F3-Debug-Panel weicht dynamisch aus — wenn das Nisse-Info-Panel offen ist, erscheint das Debug-Panel unterhalb davon statt darüber
|
||||
- **Stockpile-Overlay Transparenz** (Issue #39): `updateStaticPanelOpacity()` verwendete `setAlpha()` statt `setFillStyle()` — dadurch wurde die Opacity quadratisch statt linear angewendet; bei 100 % blieb das Panel sichtbar transparent
|
||||
|
||||
@@ -20,8 +20,18 @@ export const BUILDING_COSTS: Record<BuildingType, Record<string, number>> = {
|
||||
bed: { wood: 6 },
|
||||
stockpile_zone:{ wood: 0 },
|
||||
forester_hut: { wood: 50 },
|
||||
mine: { stone: 50, wood: 200 },
|
||||
}
|
||||
|
||||
/** Maximum number of Nisse that can work inside a mine simultaneously. */
|
||||
export const MINE_CAPACITY = 3
|
||||
|
||||
/** Milliseconds a Nisse spends inside a mine per visit. */
|
||||
export const MINE_WORK_MS = 15_000
|
||||
|
||||
/** Stone yielded per mine visit. */
|
||||
export const MINE_STONE_YIELD = 2
|
||||
|
||||
/** Max Chebyshev radius (in tiles) that a forester hut's zone can extend. */
|
||||
export const FORESTER_ZONE_RADIUS = 5
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Phaser from 'phaser'
|
||||
import { AUTOSAVE_INTERVAL, TILE_SIZE } from '../config'
|
||||
import { AUTOSAVE_INTERVAL, TILE_SIZE, MINE_CAPACITY } from '../config'
|
||||
import { TileType } from '../types'
|
||||
import type { BuildingType } from '../types'
|
||||
import { stateManager } from '../StateManager'
|
||||
@@ -27,6 +27,8 @@ export class GameScene extends Phaser.Scene {
|
||||
foresterZoneSystem!: ForesterZoneSystem
|
||||
private autosaveTimer = 0
|
||||
private menuOpen = false
|
||||
/** World-space status labels for mine buildings, keyed by building ID. */
|
||||
private mineStatusTexts = new Map<string, Phaser.GameObjects.Text>()
|
||||
|
||||
constructor() { super({ key: 'Game' }) }
|
||||
|
||||
@@ -165,6 +167,7 @@ export class GameScene extends Phaser.Scene {
|
||||
|
||||
this.events.emit('cameraMoved', this.cameraSystem.getCenterTile())
|
||||
this.buildingSystem.update()
|
||||
this.updateMineStatusLabels()
|
||||
|
||||
this.autosaveTimer -= delta
|
||||
if (this.autosaveTimer <= 0) {
|
||||
@@ -203,10 +206,105 @@ export class GameScene extends Phaser.Scene {
|
||||
g.fillStyle(0x2a1500); g.fillRect(wx - 4, wy + 1, 8, 8)
|
||||
// Tree symbol on the roof
|
||||
g.fillStyle(0x228B22); g.fillTriangle(wx - 6, wy - 11, wx + 6, wy - 11, wx, wy - 20)
|
||||
} else if (building.kind === 'mine') {
|
||||
this.renderMineBuilding(building)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a mine building (3×2 tiles) at the given building position.
|
||||
* Blocks the 5 non-entrance tiles in the WorldSystem passability index and
|
||||
* creates a world-space status label showing current / max workers.
|
||||
* @param building - The mine building state to render
|
||||
*/
|
||||
private renderMineBuilding(building: ReturnType<typeof stateManager.getState>['world']['buildings'][string]): void {
|
||||
const name = `bobj_${building.id}`
|
||||
const left = building.tileX * TILE_SIZE
|
||||
const top = building.tileY * TILE_SIZE
|
||||
const W = TILE_SIZE * 3 // 96 px
|
||||
const H = TILE_SIZE * 2 // 64 px
|
||||
|
||||
const g = this.add.graphics().setName(name).setDepth(building.tileY + 6)
|
||||
|
||||
// Rocky stone face
|
||||
g.fillStyle(0x424242); g.fillRect(left, top, W, H)
|
||||
|
||||
// Stone texture highlights — top row
|
||||
g.fillStyle(0x5a5a5a, 0.7)
|
||||
g.fillRect(left + 4, top + 3, 20, 10)
|
||||
g.fillRect(left + 28, top + 5, 18, 9)
|
||||
g.fillRect(left + 52, top + 3, 22, 11)
|
||||
g.fillRect(left + 76, top + 5, 14, 10)
|
||||
|
||||
// Stone highlights — bottom row sides (left of entrance, right of entrance)
|
||||
g.fillRect(left + 4, top + 36, 16, 10)
|
||||
g.fillRect(left + 70, top + 37, 18, 10)
|
||||
g.fillStyle(0x3a3a3a, 0.5)
|
||||
g.fillRect(left + 6, top + 50, 18, 8)
|
||||
g.fillRect(left + 68, top + 51, 16, 8)
|
||||
|
||||
// Wooden support posts flanking entrance
|
||||
g.fillStyle(0x8B4513)
|
||||
g.fillRect(left + 30, top + 22, 8, H - 22) // left post
|
||||
g.fillRect(left + 58, top + 22, 8, H - 22) // right post
|
||||
|
||||
// Lintel beam across top of entrance
|
||||
g.fillStyle(0x6B3311)
|
||||
g.fillRect(left + 28, top + 20, 40, 10)
|
||||
|
||||
// Horizontal wood grain lines on posts
|
||||
g.lineStyle(1, 0x5C2A00, 0.4)
|
||||
for (let yy = top + 28; yy < top + H; yy += 7) {
|
||||
g.lineBetween(left + 30, yy, left + 38, yy)
|
||||
g.lineBetween(left + 58, yy, left + 66, yy)
|
||||
}
|
||||
|
||||
// Mine shaft (dark entrance opening)
|
||||
g.fillStyle(0x0d0d0d); g.fillRect(left + 38, top + 30, 20, H - 30)
|
||||
g.fillStyle(0x000000, 0.5); g.fillRect(left + 38, top + 30, 4, H - 30) // left shadow
|
||||
|
||||
// Rail track at entrance floor
|
||||
g.fillStyle(0x888888); g.fillRect(left + 42, top + H - 5, 12, 2)
|
||||
g.fillStyle(0x777777)
|
||||
g.fillRect(left + 44, top + H - 8, 2, 6)
|
||||
g.fillRect(left + 50, top + H - 8, 2, 6)
|
||||
|
||||
// Block the 5 non-entrance tiles in the passability index.
|
||||
// Entrance = (tileX+1, tileY+1) — stays passable.
|
||||
for (let dy = 0; dy < 2; dy++) {
|
||||
for (let dx = 0; dx < 3; dx++) {
|
||||
if (dx === 1 && dy === 1) continue // entrance tile: skip
|
||||
this.worldSystem.addResourceTile(building.tileX + dx, building.tileY + dy)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the ⛏ X/3 status label above the building (only once)
|
||||
if (!this.mineStatusTexts.has(building.id)) {
|
||||
const cx = left + W / 2
|
||||
const st = this.add.text(cx, top - 4, `⛏ 0/${MINE_CAPACITY}`, {
|
||||
fontSize: '10px', color: '#ffdd88', fontFamily: 'monospace',
|
||||
backgroundColor: '#00000088', padding: { x: 2, y: 1 },
|
||||
}).setOrigin(0.5, 1).setDepth(building.tileY + 7)
|
||||
this.mineStatusTexts.set(building.id, st)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ⛏ X/3 status label for every mine building each frame.
|
||||
* Counts Nisse currently working (inside) the mine from the game state.
|
||||
*/
|
||||
private updateMineStatusLabels(): void {
|
||||
const state = stateManager.getState()
|
||||
const villagers = Object.values(state.world.villagers)
|
||||
for (const [buildingId, text] of this.mineStatusTexts) {
|
||||
const count = villagers.filter(
|
||||
v => v.job?.targetId === buildingId && v.aiState === 'working'
|
||||
).length
|
||||
text.setText(`⛏ ${count}/${MINE_CAPACITY}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Saves game state and destroys all systems cleanly on scene shutdown. */
|
||||
shutdown(): void {
|
||||
stateManager.save()
|
||||
|
||||
@@ -229,9 +229,10 @@ export class UIScene extends Phaser.Scene {
|
||||
{ kind: 'bed', label: '🛏 Bed', cost: '6 wood (+1 villager)' },
|
||||
{ kind: 'stockpile_zone', label: '📦 Stockpile', cost: 'free (workers deliver here)' },
|
||||
{ kind: 'forester_hut', label: '🌲 Forester Hut', cost: '50 wood' },
|
||||
{ kind: 'mine', label: '⛏ Mine', cost: '200 wood + 50 stone (place on ROCK)' },
|
||||
]
|
||||
const menuX = this.scale.width / 2 - 150, menuY = this.scale.height / 2 - 168
|
||||
const bg = this.add.rectangle(menuX, menuY, 300, 326, 0x000000, this.uiOpacity).setOrigin(0,0).setScrollFactor(0).setDepth(200)
|
||||
const menuX = this.scale.width / 2 - 150, menuY = this.scale.height / 2 - 186
|
||||
const bg = this.add.rectangle(menuX, menuY, 300, 372, 0x000000, this.uiOpacity).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))
|
||||
|
||||
@@ -1284,6 +1285,7 @@ export class UIScene extends Phaser.Scene {
|
||||
{ kind: 'bed', emoji: '🛏', label: 'Bed' },
|
||||
{ kind: 'stockpile_zone', emoji: '📦', label: 'Stockpile' },
|
||||
{ kind: 'forester_hut', emoji: '🌲', label: 'Forester' },
|
||||
{ kind: 'mine', emoji: '⛏', label: 'Mine' },
|
||||
]
|
||||
|
||||
const itemW = 84
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Phaser from 'phaser'
|
||||
import { TILE_SIZE, BUILDING_COSTS } from '../config'
|
||||
import { TileType, IMPASSABLE } from '../types'
|
||||
import type { BuildingType } from '../types'
|
||||
import type { BuildingType, BuildingState } from '../types'
|
||||
import { stateManager } from '../StateManager'
|
||||
import type { LocalAdapter } from '../NetworkAdapter'
|
||||
|
||||
@@ -58,9 +58,38 @@ export class BuildingSystem {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tile footprint dimensions for the given building type.
|
||||
* @param kind - The building type to query
|
||||
* @returns Width and height in tiles
|
||||
*/
|
||||
private getFootprint(kind: BuildingType): { w: number; h: number } {
|
||||
if (kind === 'mine') return { w: 3, h: 2 }
|
||||
return { w: 1, h: 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all tile positions occupied by the given building,
|
||||
* expanding multi-tile buildings (e.g. mine: 3×2) to their full footprint.
|
||||
* @param b - The building state to expand
|
||||
* @returns Array of tile positions in the building's footprint
|
||||
*/
|
||||
private getBuildingFootprintTiles(b: BuildingState): Array<{ tileX: number; tileY: number }> {
|
||||
if (b.kind === 'mine') {
|
||||
const result: Array<{ tileX: number; tileY: number }> = []
|
||||
for (let dy = 0; dy < 2; dy++)
|
||||
for (let dx = 0; dx < 3; dx++)
|
||||
result.push({ tileX: b.tileX + dx, tileY: b.tileY + dy })
|
||||
return result
|
||||
}
|
||||
return [{ tileX: b.tileX, tileY: b.tileY }]
|
||||
}
|
||||
|
||||
/** Select a building type and activate build mode */
|
||||
selectBuilding(kind: BuildingType): void {
|
||||
this.selectedBuilding = kind
|
||||
const { w, h } = this.getFootprint(kind)
|
||||
this.ghost.setSize(w * TILE_SIZE, h * TILE_SIZE)
|
||||
this.activate()
|
||||
}
|
||||
|
||||
@@ -97,11 +126,12 @@ export class BuildingSystem {
|
||||
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
|
||||
const snapY = tileY * TILE_SIZE + TILE_SIZE / 2
|
||||
const { w, h } = this.getFootprint(this.selectedBuilding)
|
||||
const snapX = tileX * TILE_SIZE + (w * TILE_SIZE) / 2
|
||||
const snapY = tileY * TILE_SIZE + (h * TILE_SIZE) / 2
|
||||
|
||||
this.ghost.setPosition(snapX, snapY)
|
||||
this.ghostLabel.setPosition(snapX, snapY - TILE_SIZE / 2 - 2)
|
||||
this.ghostLabel.setPosition(snapX, snapY - (h * TILE_SIZE) / 2 - 2)
|
||||
|
||||
// Color ghost based on can-build
|
||||
const canBuild = this.canBuildAt(tileX, tileY)
|
||||
@@ -116,6 +146,11 @@ export class BuildingSystem {
|
||||
|
||||
private canBuildAt(tileX: number, tileY: number): boolean {
|
||||
const state = stateManager.getState()
|
||||
|
||||
if (this.selectedBuilding === 'mine') {
|
||||
return this.canPlaceMineAt(tileX, tileY)
|
||||
}
|
||||
|
||||
const tile = state.world.tiles[tileY * 512 + tileX] as TileType // 512 = WORLD_TILES
|
||||
|
||||
// Can only build on passable ground tiles (not water, not existing buildings)
|
||||
@@ -141,6 +176,40 @@ export class BuildingSystem {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a 3×2 mine can be placed with top-left at (tileX, tileY).
|
||||
* All 6 tiles must be ROCK with no resources and no overlapping buildings.
|
||||
* @param tileX - Top-left tile column
|
||||
* @param tileY - Top-left tile row
|
||||
*/
|
||||
private canPlaceMineAt(tileX: number, tileY: number): boolean {
|
||||
const state = stateManager.getState()
|
||||
|
||||
// Check costs
|
||||
const costs = BUILDING_COSTS.mine
|
||||
for (const [item, qty] of Object.entries(costs)) {
|
||||
const have = state.world.stockpile[item as keyof typeof state.world.stockpile] ?? 0
|
||||
if (have < qty) return false
|
||||
}
|
||||
|
||||
const resources = Object.values(state.world.resources)
|
||||
const buildings = Object.values(state.world.buildings)
|
||||
|
||||
for (let dy = 0; dy < 2; dy++) {
|
||||
for (let dx = 0; dx < 3; dx++) {
|
||||
const tx = tileX + dx, ty = tileY + dy
|
||||
// Must be ROCK
|
||||
if ((state.world.tiles[ty * 512 + tx] as TileType) !== TileType.ROCK) return false
|
||||
// No resource on this tile
|
||||
if (resources.some(r => r.tileX === tx && r.tileY === ty)) return false
|
||||
// No existing building footprint overlapping this tile
|
||||
if (buildings.some(b => this.getBuildingFootprintTiles(b).some(t => t.tileX === tx && t.tileY === ty))) return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private tryPlace(ptr: Phaser.Input.Pointer): void {
|
||||
const worldX = ptr.worldX
|
||||
const worldY = ptr.worldY
|
||||
@@ -162,15 +231,13 @@ export class BuildingSystem {
|
||||
}
|
||||
|
||||
this.adapter.send({ type: 'PLACE_BUILDING', building, costs })
|
||||
// Only change the tile type for buildings that have a floor/wall tile mapping
|
||||
const tileMapped = BUILDING_TILE[this.selectedBuilding]
|
||||
if (tileMapped !== undefined) {
|
||||
this.adapter.send({
|
||||
type: 'CHANGE_TILE',
|
||||
tileX,
|
||||
tileY,
|
||||
tile: tileMapped,
|
||||
})
|
||||
// Mine keeps its ROCK tile type; footprint blocking is handled in renderPersistentObjects.
|
||||
// Other buildings change tile type where applicable.
|
||||
if (this.selectedBuilding !== 'mine') {
|
||||
const tileMapped = BUILDING_TILE[this.selectedBuilding]
|
||||
if (tileMapped !== undefined) {
|
||||
this.adapter.send({ type: 'CHANGE_TILE', tileX, tileY, tile: tileMapped })
|
||||
}
|
||||
}
|
||||
this.onPlaced?.(`Placed ${this.selectedBuilding}!`)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Phaser from 'phaser'
|
||||
import { TILE_SIZE, VILLAGER_SPEED, VILLAGER_SPAWN_INTERVAL, VILLAGER_WORK_TIMES, VILLAGER_NAMES, WORLD_TILES } from '../config'
|
||||
import { TILE_SIZE, VILLAGER_SPEED, VILLAGER_SPAWN_INTERVAL, VILLAGER_WORK_TIMES, VILLAGER_NAMES, WORLD_TILES, MINE_CAPACITY, MINE_WORK_MS, MINE_STONE_YIELD } from '../config'
|
||||
import { TileType, PLANTABLE_TILES } from '../types'
|
||||
import type { VillagerState, VillagerJob, JobType, AIState, ItemId } from '../types'
|
||||
import { stateManager } from '../StateManager'
|
||||
@@ -37,7 +37,8 @@ export class VillagerSystem {
|
||||
private farmingSystem!: FarmingSystem
|
||||
|
||||
private runtime = new Map<string, VillagerRuntime>()
|
||||
private claimed = new Set<string>() // target IDs currently claimed by a villager
|
||||
private claimed = new Set<string>() // target IDs currently claimed (resources, crops, etc.)
|
||||
private mineClaimsMap = new Map<string, number>() // mine building ID → number of claimed slots
|
||||
private spawnTimer = 0
|
||||
private nameIndex = 0
|
||||
|
||||
@@ -72,6 +73,48 @@ export class VillagerSystem {
|
||||
this.farmingSystem = farmingSystem
|
||||
}
|
||||
|
||||
// ─── Claim helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Claims a job target. Mine buildings use a capacity counter (up to MINE_CAPACITY);
|
||||
* all other targets use the exclusive claimed Set.
|
||||
* @param targetId - The resource, crop, or building ID being claimed
|
||||
*/
|
||||
private claimTarget(targetId: string): void {
|
||||
if (stateManager.getState().world.buildings[targetId]?.kind === 'mine') {
|
||||
this.mineClaimsMap.set(targetId, (this.mineClaimsMap.get(targetId) ?? 0) + 1)
|
||||
} else {
|
||||
this.claimed.add(targetId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases a job target claim.
|
||||
* @param targetId - The previously claimed target ID
|
||||
*/
|
||||
private releaseTarget(targetId: string): void {
|
||||
if (stateManager.getState().world.buildings[targetId]?.kind === 'mine') {
|
||||
const n = this.mineClaimsMap.get(targetId) ?? 0
|
||||
if (n <= 1) this.mineClaimsMap.delete(targetId)
|
||||
else this.mineClaimsMap.set(targetId, n - 1)
|
||||
} else {
|
||||
this.claimed.delete(targetId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given target is fully claimed.
|
||||
* Mine buildings are claimed when their worker count reaches MINE_CAPACITY.
|
||||
* All other targets are claimed exclusively.
|
||||
* @param targetId - The target ID to check
|
||||
*/
|
||||
private isTargetClaimed(targetId: string): boolean {
|
||||
if (stateManager.getState().world.buildings[targetId]?.kind === 'mine') {
|
||||
return (this.mineClaimsMap.get(targetId) ?? 0) >= MINE_CAPACITY
|
||||
}
|
||||
return this.claimed.has(targetId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawns sprites for all Nisse that exist in the saved state
|
||||
* and re-claims any active job targets.
|
||||
@@ -81,7 +124,7 @@ export class VillagerSystem {
|
||||
for (const v of Object.values(state.world.villagers)) {
|
||||
this.spawnSprite(v)
|
||||
// Re-claim any active job targets
|
||||
if (v.job) this.claimed.add(v.job.targetId)
|
||||
if (v.job) this.claimTarget(v.job.targetId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +216,7 @@ export class VillagerSystem {
|
||||
// Find a job
|
||||
const job = this.pickJob(v)
|
||||
if (job) {
|
||||
this.claimed.add(job.targetId)
|
||||
this.claimTarget(job.targetId)
|
||||
this.adapter.send({
|
||||
type: 'VILLAGER_SET_JOB', villagerId: v.id,
|
||||
job: { type: job.type, targetId: job.targetId, tileX: job.tileX, tileY: job.tileY, carrying: {} },
|
||||
@@ -232,10 +275,20 @@ export class VillagerSystem {
|
||||
*/
|
||||
private onArrived(v: VillagerState, rt: VillagerRuntime): void {
|
||||
switch (rt.destination) {
|
||||
case 'job':
|
||||
rt.workTimer = VILLAGER_WORK_TIMES[v.job?.type ?? 'chop'] ?? 3000
|
||||
case 'job': {
|
||||
// Mine buildings take longer than surface-rock mining and hide the Nisse sprite.
|
||||
const isMineBuilding = v.job?.type === 'mine' &&
|
||||
stateManager.getState().world.buildings[v.job.targetId]?.kind === 'mine'
|
||||
rt.workTimer = isMineBuilding ? MINE_WORK_MS : (VILLAGER_WORK_TIMES[v.job?.type ?? 'chop'] ?? 3000)
|
||||
if (isMineBuilding) {
|
||||
rt.sprite.setVisible(false)
|
||||
rt.nameLabel.setVisible(false)
|
||||
rt.energyBar.setVisible(false)
|
||||
rt.jobIcon.setVisible(false)
|
||||
}
|
||||
this.adapter.send({ type: 'VILLAGER_SET_AI', villagerId: v.id, aiState: 'working' })
|
||||
break
|
||||
}
|
||||
|
||||
case 'stockpile':
|
||||
this.adapter.send({ type: 'VILLAGER_DEPOSIT', villagerId: v.id })
|
||||
@@ -276,7 +329,7 @@ export class VillagerSystem {
|
||||
const job = v.job
|
||||
if (!job) { this.adapter.send({ type: 'VILLAGER_SET_AI', villagerId: v.id, aiState: 'idle' }); return }
|
||||
|
||||
this.claimed.delete(job.targetId)
|
||||
this.releaseTarget(job.targetId)
|
||||
const state = stateManager.getState()
|
||||
|
||||
if (job.type === 'chop') {
|
||||
@@ -293,14 +346,27 @@ export class VillagerSystem {
|
||||
this.addLog(v.id, `✓ Chopped tree (+2 wood, +${seeds} tree seed)`)
|
||||
}
|
||||
} else if (job.type === 'mine') {
|
||||
const res = state.world.resources[job.targetId]
|
||||
if (res) {
|
||||
this.adapter.send({ type: 'VILLAGER_HARVEST_RESOURCE', villagerId: v.id, resourceId: job.targetId })
|
||||
// ROCK tile stays ROCK after mining — empty rocky ground remains passable
|
||||
// and valid for mine building placement.
|
||||
this.worldSystem.removeResourceTile(res.tileX, res.tileY)
|
||||
this.resourceSystem.removeResource(job.targetId)
|
||||
this.addLog(v.id, '✓ Mined rock (+2 stone)')
|
||||
const building = state.world.buildings[job.targetId]
|
||||
if (building?.kind === 'mine') {
|
||||
// Mine building: yield stone directly into carrying, then show the Nisse again
|
||||
const mutableJob = v.job as { carrying: Partial<Record<'stone', number>> }
|
||||
mutableJob.carrying.stone = (mutableJob.carrying.stone ?? 0) + MINE_STONE_YIELD
|
||||
rt.sprite.setVisible(true)
|
||||
rt.nameLabel.setVisible(true)
|
||||
rt.energyBar.setVisible(true)
|
||||
rt.jobIcon.setVisible(true)
|
||||
this.addLog(v.id, `✓ Mined (+${MINE_STONE_YIELD} stone) at mine`)
|
||||
} else {
|
||||
// Surface rock: ROCK tile stays ROCK after mining
|
||||
const res = state.world.resources[job.targetId]
|
||||
if (res) {
|
||||
this.adapter.send({ type: 'VILLAGER_HARVEST_RESOURCE', villagerId: v.id, resourceId: job.targetId })
|
||||
// ROCK tile stays ROCK after mining — empty rocky ground remains passable
|
||||
// and valid for mine building placement.
|
||||
this.worldSystem.removeResourceTile(res.tileX, res.tileY)
|
||||
this.resourceSystem.removeResource(job.targetId)
|
||||
this.addLog(v.id, '✓ Mined rock (+2 stone)')
|
||||
}
|
||||
}
|
||||
} else if (job.type === 'farm') {
|
||||
const crop = state.world.crops[job.targetId]
|
||||
@@ -401,7 +467,7 @@ export class VillagerSystem {
|
||||
const naturalChop: C[] = []
|
||||
|
||||
for (const res of resources) {
|
||||
if (res.kind !== 'tree' || this.claimed.has(res.id)) continue
|
||||
if (res.kind !== 'tree' || this.isTargetClaimed(res.id)) continue
|
||||
// Skip trees with no reachable neighbour — A* cannot reach them.
|
||||
if (!this.hasAdjacentPassable(res.tileX, res.tileY)) continue
|
||||
const c: C = { type: 'chop', targetId: res.id, tileX: res.tileX, tileY: res.tileY, dist: dist(res.tileX, res.tileY), pri: p.chop }
|
||||
@@ -416,8 +482,15 @@ export class VillagerSystem {
|
||||
}
|
||||
|
||||
if (p.mine > 0) {
|
||||
// Mine buildings: walk to entrance tile (tileX+1, tileY+1) and work inside
|
||||
for (const b of buildings) {
|
||||
if (b.kind !== 'mine' || this.isTargetClaimed(b.id)) continue
|
||||
const eTileX = b.tileX + 1, eTileY = b.tileY + 1
|
||||
candidates.push({ type: 'mine', targetId: b.id, tileX: eTileX, tileY: eTileY, dist: dist(eTileX, eTileY), pri: p.mine })
|
||||
}
|
||||
// Surface rocks (still valid without a mine building)
|
||||
for (const res of resources) {
|
||||
if (res.kind !== 'rock' || this.claimed.has(res.id)) continue
|
||||
if (res.kind !== 'rock' || this.isTargetClaimed(res.id)) continue
|
||||
// Same reachability guard for rock tiles.
|
||||
if (!this.hasAdjacentPassable(res.tileX, res.tileY)) continue
|
||||
candidates.push({ type: 'mine', targetId: res.id, tileX: res.tileX, tileY: res.tileY, dist: dist(res.tileX, res.tileY), pri: p.mine })
|
||||
@@ -426,7 +499,7 @@ export class VillagerSystem {
|
||||
|
||||
if (p.farm > 0) {
|
||||
for (const crop of crops) {
|
||||
if (crop.stage < crop.maxStage || this.claimed.has(crop.id)) continue
|
||||
if (crop.stage < crop.maxStage || this.isTargetClaimed(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 })
|
||||
}
|
||||
}
|
||||
@@ -437,7 +510,7 @@ export class VillagerSystem {
|
||||
for (const key of zone.tiles) {
|
||||
const [tx, ty] = key.split(',').map(Number)
|
||||
const targetId = `forester_tile_${tx}_${ty}`
|
||||
if (this.claimed.has(targetId)) continue
|
||||
if (this.isTargetClaimed(targetId)) continue
|
||||
// Skip if tile is not plantable
|
||||
const tileType = state.world.tiles[ty * WORLD_TILES + tx] as TileType
|
||||
if (!PLANTABLE_TILES.has(tileType)) continue
|
||||
@@ -481,7 +554,7 @@ export class VillagerSystem {
|
||||
|
||||
if (!path) {
|
||||
if (v.job) {
|
||||
this.claimed.delete(v.job.targetId)
|
||||
this.releaseTarget(v.job.targetId)
|
||||
this.adapter.send({ type: 'VILLAGER_SET_JOB', villagerId: v.id, job: null })
|
||||
}
|
||||
rt.idleScanTimer = 4000 // longer delay after failed pathfind to avoid tight retry loops
|
||||
|
||||
@@ -32,7 +32,7 @@ export const PLANTABLE_TILES = new Set<TileType>([TileType.GRASS, TileType.DARK_
|
||||
|
||||
export type ItemId = 'wood' | 'stone' | 'wheat_seed' | 'carrot_seed' | 'wheat' | 'carrot' | 'tree_seed'
|
||||
|
||||
export type BuildingType = 'floor' | 'wall' | 'chest' | 'bed' | 'stockpile_zone' | 'forester_hut'
|
||||
export type BuildingType = 'floor' | 'wall' | 'chest' | 'bed' | 'stockpile_zone' | 'forester_hut' | 'mine'
|
||||
|
||||
export type CropKind = 'wheat' | 'carrot'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user