✨ Försterkreislauf: Setzlinge beim Fällen, Försterhaus, Förster-Job
- Gefällter Baum → 1–2 tree_seed im Stockpile (zufällig) - Neues Gebäude forester_hut (50 wood): Log-Hütten-Grafik, Klick öffnet Info-Panel - Zonenmarkierung: Edit-Zone-Tool, Radius 5 Tiles, halbtransparente Overlay-Anzeige - Neuer JobType 'forester': Nisse pflanzen Setzlinge auf markierten Zonen-Tiles - Chop-Priorisierung: Zonen-Bäume werden vor natürlichen Bäumen gefällt - Nisse-Panel & Info-Panel zeigen forester-Priorität-Button Closes #25 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
194
src/systems/ForesterZoneSystem.ts
Normal file
194
src/systems/ForesterZoneSystem.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import Phaser from 'phaser'
|
||||
import { TILE_SIZE, FORESTER_ZONE_RADIUS } from '../config'
|
||||
import { PLANTABLE_TILES } from '../types'
|
||||
import type { TileType } from '../types'
|
||||
import { stateManager } from '../StateManager'
|
||||
import type { LocalAdapter } from '../NetworkAdapter'
|
||||
|
||||
/** Colors used for zone rendering. */
|
||||
const COLOR_IN_RADIUS = 0x44aa44 // unselected tile within radius (edit mode only)
|
||||
const COLOR_ZONE_TILE = 0x00ff44 // tile marked as part of the zone
|
||||
const ALPHA_VIEW = 0.18 // always-on zone overlay
|
||||
const ALPHA_RADIUS = 0.12 // in-radius tiles while editing
|
||||
const ALPHA_ZONE_EDIT = 0.45 // zone tiles while editing
|
||||
|
||||
export class ForesterZoneSystem {
|
||||
private scene: Phaser.Scene
|
||||
private adapter: LocalAdapter
|
||||
|
||||
/** Graphics layer for the always-visible zone overlay. */
|
||||
private zoneGraphics!: Phaser.GameObjects.Graphics
|
||||
/** Graphics layer for the edit-mode radius/tile overlay. */
|
||||
private editGraphics!: Phaser.GameObjects.Graphics
|
||||
|
||||
/** Building ID currently being edited, or null when not in edit mode. */
|
||||
private editBuildingId: string | null = null
|
||||
|
||||
/**
|
||||
* Callback invoked after a tile toggle so callers can react (e.g. refresh the panel).
|
||||
* Receives the updated zone tiles array.
|
||||
*/
|
||||
onZoneChanged?: (buildingId: string, tiles: string[]) => void
|
||||
|
||||
/**
|
||||
* Callback invoked when the user exits edit mode (right-click or programmatic close).
|
||||
* UIScene listens to this to close the zone edit indicator.
|
||||
*/
|
||||
onEditEnded?: () => void
|
||||
|
||||
/**
|
||||
* @param scene - The Phaser scene this system belongs to
|
||||
* @param adapter - Network adapter for dispatching state actions
|
||||
*/
|
||||
constructor(scene: Phaser.Scene, adapter: LocalAdapter) {
|
||||
this.scene = scene
|
||||
this.adapter = adapter
|
||||
}
|
||||
|
||||
/** Creates the graphics layers and registers the pointer listener. */
|
||||
create(): void {
|
||||
this.zoneGraphics = this.scene.add.graphics().setDepth(3)
|
||||
this.editGraphics = this.scene.add.graphics().setDepth(4)
|
||||
|
||||
this.scene.input.on('pointerdown', (ptr: Phaser.Input.Pointer) => {
|
||||
if (!this.editBuildingId) return
|
||||
if (ptr.rightButtonDown()) {
|
||||
this.exitEditMode()
|
||||
return
|
||||
}
|
||||
this.handleTileClick(ptr.worldX, ptr.worldY)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Redraws all zone overlays for every forester hut in the current state.
|
||||
* Should be called whenever the zone data changes.
|
||||
*/
|
||||
refreshOverlay(): void {
|
||||
this.zoneGraphics.clear()
|
||||
const state = stateManager.getState()
|
||||
for (const zone of Object.values(state.world.foresterZones)) {
|
||||
for (const key of zone.tiles) {
|
||||
const [tx, ty] = key.split(',').map(Number)
|
||||
this.zoneGraphics.fillStyle(COLOR_ZONE_TILE, ALPHA_VIEW)
|
||||
this.zoneGraphics.fillRect(tx * TILE_SIZE, ty * TILE_SIZE, TILE_SIZE, TILE_SIZE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates zone-editing mode for the given forester hut.
|
||||
* Draws the radius indicator and zone tiles in edit colors.
|
||||
* @param buildingId - ID of the forester_hut building to edit
|
||||
*/
|
||||
startEditMode(buildingId: string): void {
|
||||
this.editBuildingId = buildingId
|
||||
this.drawEditOverlay()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivates zone-editing mode and clears the edit overlay.
|
||||
* Triggers the onEditEnded callback.
|
||||
*/
|
||||
exitEditMode(): void {
|
||||
if (!this.editBuildingId) return
|
||||
this.editBuildingId = null
|
||||
this.editGraphics.clear()
|
||||
this.onEditEnded?.()
|
||||
}
|
||||
|
||||
/** Returns true when the zone editor is currently active. */
|
||||
isEditing(): boolean {
|
||||
return this.editBuildingId !== null
|
||||
}
|
||||
|
||||
/** Destroys all graphics objects. */
|
||||
destroy(): void {
|
||||
this.zoneGraphics.destroy()
|
||||
this.editGraphics.destroy()
|
||||
}
|
||||
|
||||
// ─── Private helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Handles a left-click during edit mode.
|
||||
* Toggles the clicked tile in the zone if it is within radius and plantable.
|
||||
* @param worldX - World pixel X of the pointer
|
||||
* @param worldY - World pixel Y of the pointer
|
||||
*/
|
||||
private handleTileClick(worldX: number, worldY: number): void {
|
||||
const id = this.editBuildingId
|
||||
if (!id) return
|
||||
|
||||
const state = stateManager.getState()
|
||||
const building = state.world.buildings[id]
|
||||
if (!building) { this.exitEditMode(); return }
|
||||
|
||||
const tileX = Math.floor(worldX / TILE_SIZE)
|
||||
const tileY = Math.floor(worldY / TILE_SIZE)
|
||||
|
||||
// Chebyshev distance — must be within radius
|
||||
const dx = Math.abs(tileX - building.tileX)
|
||||
const dy = Math.abs(tileY - building.tileY)
|
||||
if (Math.max(dx, dy) > FORESTER_ZONE_RADIUS) return
|
||||
|
||||
const zone = state.world.foresterZones[id]
|
||||
if (!zone) return
|
||||
|
||||
const key = `${tileX},${tileY}`
|
||||
const idx = zone.tiles.indexOf(key)
|
||||
const tiles = idx >= 0
|
||||
? zone.tiles.filter(t => t !== key) // remove
|
||||
: [...zone.tiles, key] // add
|
||||
|
||||
this.adapter.send({ type: 'FORESTER_ZONE_UPDATE', buildingId: id, tiles })
|
||||
this.refreshOverlay()
|
||||
this.drawEditOverlay()
|
||||
this.onZoneChanged?.(id, tiles)
|
||||
}
|
||||
|
||||
/**
|
||||
* Redraws the edit-mode overlay showing the valid radius and current zone tiles.
|
||||
* Only called while editBuildingId is set.
|
||||
*/
|
||||
private drawEditOverlay(): void {
|
||||
this.editGraphics.clear()
|
||||
const id = this.editBuildingId
|
||||
if (!id) return
|
||||
|
||||
const state = stateManager.getState()
|
||||
const building = state.world.buildings[id]
|
||||
if (!building) return
|
||||
|
||||
const zone = state.world.foresterZones[id]
|
||||
const zoneSet = new Set(zone?.tiles ?? [])
|
||||
const r = FORESTER_ZONE_RADIUS
|
||||
|
||||
for (let dy = -r; dy <= r; dy++) {
|
||||
for (let dx = -r; dx <= r; dx++) {
|
||||
const tx = building.tileX + dx
|
||||
const ty = building.tileY + dy
|
||||
const key = `${tx},${ty}`
|
||||
|
||||
// Only draw on plantable terrain
|
||||
const tileType = state.world.tiles[ty * 512 + tx] as TileType
|
||||
if (!PLANTABLE_TILES.has(tileType)) continue
|
||||
|
||||
if (zoneSet.has(key)) {
|
||||
this.editGraphics.fillStyle(COLOR_ZONE_TILE, ALPHA_ZONE_EDIT)
|
||||
this.editGraphics.strokeRect(tx * TILE_SIZE, ty * TILE_SIZE, TILE_SIZE, TILE_SIZE)
|
||||
} else {
|
||||
this.editGraphics.fillStyle(COLOR_IN_RADIUS, ALPHA_RADIUS)
|
||||
}
|
||||
this.editGraphics.fillRect(tx * TILE_SIZE, ty * TILE_SIZE, TILE_SIZE, TILE_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a subtle border around the entire radius square
|
||||
const bx = (building.tileX - r) * TILE_SIZE
|
||||
const by = (building.tileY - r) * TILE_SIZE
|
||||
const bw = (2 * r + 1) * TILE_SIZE
|
||||
this.editGraphics.lineStyle(1, COLOR_ZONE_TILE, 0.4)
|
||||
this.editGraphics.strokeRect(bx, by, bw, bw)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user