- core/: Pure TS hex engine (axial coords, hex grid, terrain types, edge connectivity with constraint solver, HexMap state model) - src/map/: Leaflet L.CRS.Simple map init, Canvas-based hex overlay layer (L.GridLayer), click/edge interaction detection - src/ui/: Sidebar with toolbar (Select/Paint/Feature modes), terrain picker, hex inspector, map settings (hex size, grid, opacity) - pipeline/: Tile pyramid generator (sharp, from source image) - tests/: 32 passing tests for coords, hex-grid, edge-connectivity - Uses Kiepenkerl tiles (symlinked) for development Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
130 lines
3.5 KiB
TypeScript
130 lines
3.5 KiB
TypeScript
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<string, HexTerrain>();
|
|
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): void {
|
|
const terrain = this.getTerrain(coord);
|
|
const existing = terrain.features.find(f => f.terrainId === terrainId);
|
|
if (existing) {
|
|
existing.edgeMask = edgeMask;
|
|
} else {
|
|
terrain.features.push({ terrainId, edgeMask });
|
|
}
|
|
// 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;
|
|
}
|
|
}
|