mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:20:47 +00:00
5be72db060
## Problem The debug render GUI's **"Reset to Defaults"** restored bare `createRenderSettings()` defaults, wiping the user's graphics overrides (colorblind theme, ocean color, lighting, name scaling, etc.) from the live render settings. The per-prop right-click "reset to default" and the modified-indicators had the same flaw — their captured defaults were raw, ignoring overrides. ## Fix Thread the existing `resolveRenderSettings` (`createRenderSettings()` + `applyGraphicsOverrides()`) into the debug GUI as the defaults provider, so reset restores the same settings the renderer was actually built with. - **`debug/index.ts`** — added a `resolveDefaults` param (defaults to `createRenderSettings` to keep the module decoupled). The captured `defaults` now include overrides, fixing the per-prop reset and modified indicators too. - **`debug/Wiring.ts`** — `wireActions` takes `resolveDefaults`; the reset handler `deepAssign`s `resolveDefaults()` instead of `createRenderSettings()`. - **`ClientGameRunner.ts`** — passes `resolveRenderSettings` into `createDebugGui`, and extracts a `refreshDerivedGraphics` helper (terrain rebuild + re-theme/palette) from `onGraphicsChanged`, wired as the GUI's `onSettingsChanged` so the reapplied terrain/colorblind overrides become *visible* after reset (they're baked into GPU textures and aren't picked up per-frame). Side benefit: editing terrain/theme settings in the debug GUI now refreshes those textures live too (that callback was previously never wired). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
217 lines
6.1 KiB
TypeScript
217 lines
6.1 KiB
TypeScript
import GUI, { FunctionController } from "lil-gui";
|
|
import type { RenderSettings } from "../RenderSettings";
|
|
import { dumpSettings } from "../RenderSettings";
|
|
import { deepAssign } from "../SettingsUtils";
|
|
import type { ConfigProp } from "./ConfigProp";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Draggable title bar
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function makeDraggable(gui: GUI): void {
|
|
const titleBar = gui.domElement.querySelector(
|
|
".title, .lil-title",
|
|
) as HTMLElement | null;
|
|
if (!titleBar) return;
|
|
|
|
titleBar.style.cursor = "grab";
|
|
let dragging = false;
|
|
let didDrag = false;
|
|
let startX = 0,
|
|
startY = 0,
|
|
startLeft = 0,
|
|
startTop = 0;
|
|
|
|
titleBar.addEventListener("mousedown", (e) => {
|
|
dragging = true;
|
|
didDrag = false;
|
|
titleBar.style.cursor = "grabbing";
|
|
const rect = gui.domElement.getBoundingClientRect();
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
startLeft = rect.left;
|
|
startTop = rect.top;
|
|
gui.domElement.style.left = rect.left + "px";
|
|
gui.domElement.style.right = "auto";
|
|
e.preventDefault();
|
|
});
|
|
|
|
window.addEventListener("mousemove", (e) => {
|
|
if (!dragging) return;
|
|
didDrag = true;
|
|
gui.domElement.style.left = startLeft + e.clientX - startX + "px";
|
|
gui.domElement.style.top = startTop + e.clientY - startY + "px";
|
|
});
|
|
|
|
window.addEventListener("mouseup", () => {
|
|
if (!dragging) return;
|
|
dragging = false;
|
|
titleBar.style.cursor = "grab";
|
|
});
|
|
|
|
titleBar.addEventListener(
|
|
"click",
|
|
(e) => {
|
|
if (didDrag) e.stopPropagation();
|
|
},
|
|
{ capture: true },
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Actions: Download JSON, Load JSON, Reset to Defaults
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function wireActions(
|
|
gui: GUI,
|
|
settings: RenderSettings,
|
|
props: ConfigProp[],
|
|
resolveDefaults: () => RenderSettings,
|
|
onSettingsChanged?: () => void,
|
|
): void {
|
|
gui.add({ dump: () => dumpSettings(settings) }, "dump").name("Download JSON");
|
|
|
|
const fileInput = document.createElement("input");
|
|
fileInput.type = "file";
|
|
fileInput.accept = ".json";
|
|
fileInput.style.display = "none";
|
|
document.body.appendChild(fileInput);
|
|
|
|
fileInput.addEventListener("change", () => {
|
|
const file = fileInput.files?.[0];
|
|
if (!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
try {
|
|
deepAssign(settings, JSON.parse(reader.result as string));
|
|
props.forEach((p) => p.updateDisplay());
|
|
onSettingsChanged?.();
|
|
} catch (e) {
|
|
console.error("Failed to load render settings:", e);
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
fileInput.value = "";
|
|
});
|
|
|
|
gui.add({ load: () => fileInput.click() }, "load").name("Load JSON");
|
|
|
|
gui
|
|
.add(
|
|
{
|
|
reset: () => {
|
|
deepAssign(settings, resolveDefaults());
|
|
props.forEach((p) => p.resetToDefault());
|
|
onSettingsChanged?.();
|
|
},
|
|
},
|
|
"reset",
|
|
)
|
|
.name("Reset to Defaults");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Modified indicators: blue label + right-click reset context menu
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const MODIFIED_CLASS = "lil-modified";
|
|
|
|
let stylesInjected = false;
|
|
function injectModifiedStyles(): void {
|
|
if (stylesInjected) return;
|
|
stylesInjected = true;
|
|
const style = document.createElement("style");
|
|
style.textContent = `
|
|
.${MODIFIED_CLASS} .lil-name { color: #5ba8d6; }
|
|
.lil-reset-menu {
|
|
position: fixed;
|
|
z-index: 10000;
|
|
background: #1a1a2e;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
padding: 4px 0;
|
|
font: 12px sans-serif;
|
|
color: #ccc;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
|
|
}
|
|
.lil-reset-menu div {
|
|
padding: 4px 16px;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
}
|
|
.lil-reset-menu div:hover {
|
|
background: #2a2a4e;
|
|
color: #fff;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
function createContextMenu(): HTMLDivElement {
|
|
const menu = document.createElement("div");
|
|
menu.className = "lil-reset-menu";
|
|
menu.style.display = "none";
|
|
document.body.appendChild(menu);
|
|
document.addEventListener("mousedown", (e) => {
|
|
if (!menu.contains(e.target as Node)) menu.style.display = "none";
|
|
});
|
|
return menu;
|
|
}
|
|
|
|
export function wireModifiedIndicators(
|
|
gui: GUI,
|
|
props: ConfigProp[],
|
|
onSettingsChanged?: () => void,
|
|
): void {
|
|
injectModifiedStyles();
|
|
const contextMenu = createContextMenu();
|
|
|
|
// Map each lil-gui Controller back to its ConfigProp
|
|
const allControllers = gui.controllersRecursive();
|
|
// Props were pushed in walk order, controllers are in the same order (minus FunctionControllers)
|
|
const propControllers = allControllers.filter(
|
|
(c) => !(c instanceof FunctionController),
|
|
);
|
|
|
|
propControllers.forEach((ctrl, i) => {
|
|
const prop = props[i];
|
|
|
|
const updateClass = () =>
|
|
ctrl.domElement.classList.toggle(MODIFIED_CLASS, prop.isModified());
|
|
|
|
updateClass();
|
|
|
|
const prev = ctrl._onChange;
|
|
ctrl.onChange(function (...args: unknown[]) {
|
|
prev?.apply(ctrl, args as any);
|
|
updateClass();
|
|
});
|
|
|
|
ctrl.$name.addEventListener("contextmenu", (e) => {
|
|
if (!prop.isModified()) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
contextMenu.innerHTML = "";
|
|
const item = document.createElement("div");
|
|
item.textContent = "Reset to default";
|
|
item.addEventListener("mousedown", (ev) => {
|
|
ev.stopPropagation();
|
|
prop.resetToDefault();
|
|
updateClass();
|
|
onSettingsChanged?.();
|
|
contextMenu.style.display = "none";
|
|
});
|
|
contextMenu.appendChild(item);
|
|
contextMenu.style.left = e.clientX + "px";
|
|
contextMenu.style.top = e.clientY + "px";
|
|
contextMenu.style.display = "";
|
|
});
|
|
});
|
|
|
|
// Wire onFinishChange for persistence
|
|
if (onSettingsChanged) {
|
|
allControllers.forEach((c) => c.onFinishChange(onSettingsChanged));
|
|
}
|
|
}
|