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) } }