Add ocean color override to graphics settings (#4269)

## What

Adds a **Terrain** section to the graphics settings modal with a color
picker and a hex-code text field (paste a `#rrggbb` code) for the
**ocean** (deep water) color.

## Details

- The picked color sets the *shallow-water base*; the existing per-depth
brightness gradient is preserved (deeper water still darkens).
- Only deep water is affected — shoreline water and land are untouched.
- Follows the same override pattern as every other graphics setting: the
default lives in `render-settings.json` (`terrain.oceanColor`), the
override is a field in `GraphicsOverrides`, and `applyGraphicsOverrides`
copies it into the live `RenderSettings`.
- Rebased on #4271 (settings resolved before renderer construction): the
terrain texture **bakes the resolved ocean color at construction**, so a
saved override shows on load with no special-casing. Terrain is baked
into a GPU texture rather than read per-frame, so a *live* change still
triggers an explicit `view.rebuildTerrain()`.
- Resetting graphics overrides clears it back to the default ocean
color.

## Testing
Verified live in a headless singleplayer game:
- A **saved** ocean override renders green deep-water on load, baked at
construction with no settings-change event fired.
- A mid-game color change recolors the deep ocean instantly, gradient
preserved, shoreline/land untouched.

`tsc` and ESLint clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Evan
2026-06-13 21:13:46 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 54a7042303
commit 8b9bda1c8b
11 changed files with 172 additions and 15 deletions
@@ -70,6 +70,8 @@ const RAIL_THICKNESS_MIN = 0.5;
const RAIL_THICKNESS_MAX = 3;
const RAIL_THICKNESS_STEP = 0.1;
const HEX_COLOR_RE = /^#?([0-9a-fA-F]{6})$/;
export class ShowGraphicsSettingsModalEvent {
constructor(
public readonly isVisible: boolean = true,
@@ -334,6 +336,29 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
this.patchStructure({ iconSize: value });
}
private patchTerrain(patch: Partial<GraphicsOverrides["terrain"]>) {
const current = this.userSettings.graphicsOverrides();
this.userSettings.setGraphicsOverrides({
...current,
terrain: { ...current.terrain, ...patch },
});
this.requestUpdate();
}
private currentOceanColor(): string {
return (
this.userSettings.graphicsOverrides().terrain?.oceanColor ??
renderDefaults.terrain.oceanColor
);
}
private onOceanColorChange(event: Event) {
const value = (event.target as HTMLInputElement).value.trim();
const match = HEX_COLOR_RE.exec(value);
if (!match) return; // ignore partial/invalid hex while typing
this.patchTerrain({ oceanColor: `#${match[1].toLowerCase()}` });
}
private currentClassicIcons(): boolean {
return (
this.userSettings.graphicsOverrides().structure?.classicIcons ?? true
@@ -459,6 +484,7 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
const coordinateGridOpacity = this.currentCoordinateGridOpacity();
const railDrawDistance = RAIL_ZOOM_MAX - this.currentRailMinZoom();
const railThickness = this.currentRailThickness();
const oceanColor = this.currentOceanColor();
const colorblind = this.currentColorblind();
return html`
@@ -919,6 +945,39 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
</div>
</div>
<div
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider mt-2"
>
${translateText("graphics_setting.section_terrain")}
</div>
<div
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
>
<div class="flex-1">
<div class="font-medium">
${translateText("graphics_setting.ocean_color_label")}
</div>
<div class="text-sm text-slate-400">
${translateText("graphics_setting.ocean_color_desc")}
</div>
</div>
<input
type="text"
.value=${oceanColor}
placeholder=${renderDefaults.terrain.oceanColor}
spellcheck="false"
@change=${this.onOceanColorChange}
class="w-24 px-2 py-1 bg-slate-900 border border-slate-500 rounded-sm text-sm text-white font-mono"
/>
<input
type="color"
.value=${oceanColor}
@input=${this.onOceanColorChange}
class="w-10 h-8 bg-transparent border border-slate-500 rounded-sm cursor-pointer"
/>
</div>
<div
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider mt-2"
>