import type { AxialCoord, HexTerrain, HexFeature, EdgeMask } from './types.js'; import { coordKey, parseCoordKey } from './coords.js'; import { DEFAULT_BASE_TERRAIN } from './terrain.js'; /** Serialized hex map format (for save/load) */ export interface SerializedHexMap { hexes: Array<{ q: number; r: number; base: string; features: HexFeature[]; }>; } /** * In-memory hex map state. * Sparse storage: only hexes that differ from default are stored. */ export class HexMap { private data = new Map(); private _dirty = false; /** Get terrain for a hex. Returns default if not explicitly set. */ getTerrain(coord: AxialCoord): HexTerrain { const key = coordKey(coord); const existing = this.data.get(key); if (existing) return existing; return { base: DEFAULT_BASE_TERRAIN, features: [] }; } /** Set terrain for a hex */ setTerrain(coord: AxialCoord, terrain: HexTerrain): void { this.data.set(coordKey(coord), terrain); this._dirty = true; } /** Set only the base terrain, preserving features */ setBase(coord: AxialCoord, base: string): void { const terrain = this.getTerrain(coord); terrain.base = base; this.setTerrain(coord, terrain); } /** Add or update a linear feature on a hex */ setFeature(coord: AxialCoord, terrainId: string, edgeMask: EdgeMask, waterSide?: 1 | -1): void { const terrain = this.getTerrain(coord); const existing = terrain.features.find(f => f.terrainId === terrainId); if (existing) { existing.edgeMask = edgeMask; if (waterSide !== undefined) existing.waterSide = waterSide; } else { terrain.features.push({ terrainId, edgeMask, ...(waterSide !== undefined ? { waterSide } : {}) }); } // Remove features with empty mask terrain.features = terrain.features.filter(f => f.edgeMask !== 0); this.setTerrain(coord, terrain); } /** Remove a feature entirely from a hex */ removeFeature(coord: AxialCoord, terrainId: string): void { const terrain = this.getTerrain(coord); terrain.features = terrain.features.filter(f => f.terrainId !== terrainId); this.setTerrain(coord, terrain); } /** Reset a hex to default state */ clearHex(coord: AxialCoord): void { this.data.delete(coordKey(coord)); this._dirty = true; } /** Check if a hex has been explicitly set */ hasHex(coord: AxialCoord): boolean { return this.data.has(coordKey(coord)); } /** Get all explicitly set hexes */ getAllHexes(): Array<{ coord: AxialCoord; terrain: HexTerrain }> { const result: Array<{ coord: AxialCoord; terrain: HexTerrain }> = []; for (const [key, terrain] of this.data) { result.push({ coord: parseCoordKey(key), terrain }); } return result; } /** Number of explicitly set hexes */ get size(): number { return this.data.size; } /** Whether the map has unsaved changes */ get dirty(): boolean { return this._dirty; } markClean(): void { this._dirty = false; } /** Serialize for persistence */ serialize(): SerializedHexMap { return { hexes: this.getAllHexes().map(({ coord, terrain }) => ({ q: coord.q, r: coord.r, base: terrain.base, features: terrain.features, })), }; } /** Load from serialized data */ static deserialize(data: SerializedHexMap): HexMap { const map = new HexMap(); for (const hex of data.hexes) { map.setTerrain({ q: hex.q, r: hex.r }, { base: hex.base, features: hex.features, }); } map._dirty = false; return map; } /** Clear all data */ clear(): void { this.data.clear(); this._dirty = true; } }