diff --git a/CHANGELOG.md b/CHANGELOG.md index c16b2da..f0f8f53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/config.ts b/src/config.ts index 463c80e..7b23952 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,8 +20,18 @@ export const BUILDING_COSTS: Record> = { 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 diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index 9a4bd1e..210086e 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -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() 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['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() diff --git a/src/scenes/UIScene.ts b/src/scenes/UIScene.ts index 7078eff..bf87497 100644 --- a/src/scenes/UIScene.ts +++ b/src/scenes/UIScene.ts @@ -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 diff --git a/src/systems/BuildingSystem.ts b/src/systems/BuildingSystem.ts index 2d7d7d4..0065dc5 100644 --- a/src/systems/BuildingSystem.ts +++ b/src/systems/BuildingSystem.ts @@ -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}!`) } diff --git a/src/systems/VillagerSystem.ts b/src/systems/VillagerSystem.ts index dfd8eb2..50ffdd2 100644 --- a/src/systems/VillagerSystem.ts +++ b/src/systems/VillagerSystem.ts @@ -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() - private claimed = new Set() // target IDs currently claimed by a villager + private claimed = new Set() // target IDs currently claimed (resources, crops, etc.) + private mineClaimsMap = new Map() // 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> } + 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 diff --git a/src/types.ts b/src/types.ts index fe7b134..d90bc17 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,7 +32,7 @@ export const PLANTABLE_TILES = new Set([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'