diff --git a/index.html b/index.html index 92cf312..4ab2367 100644 --- a/index.html +++ b/index.html @@ -45,6 +45,24 @@ width: 65px; } + .reset-btn { + width: calc(100% - 16px); + margin: 12px 8px; + padding: 6px; + background: #333; + color: #aaa; + border: 1px solid #555; + border-radius: 3px; + font-family: monospace; + font-size: 12px; + cursor: pointer; + } + + .reset-btn:hover { + background: #444; + color: #ddd; + } + #gui-container { flex: 1; } diff --git a/src/Config.ts b/src/Config.ts index 141539a..bfbc4ee 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -1,4 +1,6 @@ -export default { +const STORAGE_KEY = "ants-simulation-config"; + +const defaults = { worldSize: 1024, antsCount: 12, simulationStepsPerSecond: 60, @@ -17,3 +19,41 @@ export default { repellentMaxPerCell: 10, repellentThreshold: 0.01, }; + +type ConfigType = typeof defaults; + +function loadSaved(): Partial { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) return JSON.parse(raw); + } catch { + // corrupted data, ignore + } + return {}; +} + +const saved = loadSaved(); +const Config: ConfigType = { ...defaults, ...saved }; + +export function saveConfig(): void { + const toSave: Partial = {}; + for (const key of Object.keys(defaults) as (keyof ConfigType)[]) { + if (Config[key] !== defaults[key]) { + // biome-ignore lint/suspicious/noExplicitAny: generic key/value copy + (toSave as any)[key] = Config[key]; + } + } + if (Object.keys(toSave).length > 0) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave)); + } else { + localStorage.removeItem(STORAGE_KEY); + } +} + +export function resetConfig(): void { + Object.assign(Config, defaults); + localStorage.clear(); +} + +export { defaults }; +export default Config; diff --git a/src/GUI.ts b/src/GUI.ts index 89b0942..90e296b 100644 --- a/src/GUI.ts +++ b/src/GUI.ts @@ -1,5 +1,5 @@ import GUI from "lil-gui"; -import Config from "./Config"; +import Config, { resetConfig, saveConfig } from "./Config"; type EventHandler = () => void; @@ -16,33 +16,50 @@ class GUIController { .add(Config, "worldSize", 256, 4096) .name("World size") .step(1) - .onChange(() => this.emit("reset")); + .onChange(() => this.saveAndEmit("reset")); simFolder .add(Config, "antsCount", 0, 22) .name("Ants count 2^") .step(1) - .onChange(() => this.emit("reset")); + .onChange(() => this.saveAndEmit("reset")); simFolder .add(Config, "scentFadeOutFactor", 0, 0.01) .name("Pheromone evaporation factor") .step(0.0001) - .onChange(() => this.emit("reset")); + .onChange(() => this.saveAndEmit("reset")); simFolder .add(Config, "scentBlurRadius", 0, 0.5) .name("Pheromone diffusion factor") .step(0.01) - .onChange(() => this.emit("reset")); + .onChange(() => this.saveAndEmit("reset")); simFolder .add(Config, "simulationStepsPerSecond", 1, 500) .name("Simulation steps per second") - .step(1); + .step(1) + .onChange(() => saveConfig()); const controlsFolder = this.gui.addFolder("Controls"); - controlsFolder.add(Config, "brushRadius", 1, 100).name("Brush radius"); + controlsFolder + .add(Config, "brushRadius", 1, 100) + .name("Brush radius") + .onChange(() => saveConfig()); simFolder.open(); controlsFolder.open(); + + const resetBtn = document.createElement("button"); + resetBtn.textContent = "Reset to defaults"; + resetBtn.className = "reset-btn"; + resetBtn.addEventListener("click", () => { + resetConfig(); + for (const c of this.gui.controllersRecursive()) { + c.updateDisplay(); + } + this.emit("reset"); + }); + // biome-ignore lint/style/noNonNullAssertion: gui-container exists in index.html + document.getElementById("gui-container")!.appendChild(resetBtn); } on(event: string, handler: EventHandler): void { @@ -53,6 +70,11 @@ class GUIController { this.listeners.get(event)!.push(handler); } + private saveAndEmit(event: string): void { + saveConfig(); + this.emit(event); + } + private emit(event: string): void { const handlers = this.listeners.get(event); if (handlers) {