Add nuke fallout color graphics option (#4355)

## What

Adds a **Nuke fallout color** option to the in-game graphics settings
modal (Effects section), letting players recolor the fallout tint left
on territory after a nuke.

![modal](https://github.com/user-attachments/assets/placeholder)

## How

Mirrors the existing **Ocean color** override pattern:

- `GraphicsOverrides.ts` — adds `staleNukeColor` (hex string) to the
`mapOverlay` override schema.
- `RenderOverrides.ts` — `applyGraphicsOverrides` parses the hex and
writes the renderer's `staleNukeR/G/B` 0–1 float channels (`hexToRgb`
yields 0–255, so it divides by 255).
- `GraphicsSettingsModal.ts` — new hex-text + native color-picker row,
default computed from `render-settings.json`.
- `en.json` — `nuke_color_label` / `nuke_color_desc`.

The value persists via `UserSettings.graphicsOverrides()` and is cleared
by the modal's existing "Reset to defaults".

The render debug GUI already exposes the same setting as **Stale Nuke
Color** (Map Overlay), so no change was needed there.

## Testing

- `tsc --noEmit` clean.
- Verified in a headless solo game: the row renders with the green
default (`#0d8c12`), changing it persists `mapOverlay.staleNukeColor`,
and `applyGraphicsOverrides("#ff0000")` produces `staleNukeR=1, G=0,
B=0`.

🤖 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-19 20:28:11 -07:00
committed by evanpelle
parent cb0d79ed6d
commit 4ee68b4ea7
4 changed files with 73 additions and 0 deletions
+2
View File
@@ -547,6 +547,8 @@
"name_cull_desc": "Hide names smaller than this size",
"name_cull_label": "Minimum name size",
"name_scale_label": "Name Scale",
"nuke_color_desc": "Color of the fallout tint left on territory after a nuke.",
"nuke_color_label": "Nuke fallout color",
"ocean_color_desc": "Base color of ocean.",
"ocean_color_label": "Ocean color",
"rail_distance_desc": "How far zoomed out train tracks remain visible",
@@ -115,6 +115,22 @@ function falloffToUnitGlowSlider(falloff: number): number {
const HEX_COLOR_RE = /^#?([0-9a-fA-F]{6})$/;
// The stale-nuke (fallout ground tint) color is stored in render-settings.json
// as three 0-1 floats; the color picker wants a "#rrggbb" hex string.
function rgbFloatsToHex(r: number, g: number, b: number): string {
const ch = (v: number) =>
Math.round(v * 255)
.toString(16)
.padStart(2, "0");
return `#${ch(r)}${ch(g)}${ch(b)}`;
}
const NUKE_COLOR_DEFAULT = rgbFloatsToHex(
renderDefaults.mapOverlay.staleNukeR,
renderDefaults.mapOverlay.staleNukeG,
renderDefaults.mapOverlay.staleNukeB,
);
export class ShowGraphicsSettingsModalEvent {
constructor(
public readonly isVisible: boolean = true,
@@ -356,6 +372,20 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
this.patchMapOverlay({ coordinateGridOpacity: value });
}
private currentNukeColor(): string {
return (
this.userSettings.graphicsOverrides().mapOverlay?.staleNukeColor ??
NUKE_COLOR_DEFAULT
);
}
private onNukeColorChange(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.patchMapOverlay({ staleNukeColor: `#${match[1].toLowerCase()}` });
}
private onRailDrawDistanceChange(event: Event) {
const drawDistance = parseFloat((event.target as HTMLInputElement).value);
// Invert: higher draw distance => tracks visible when more zoomed out.
@@ -572,6 +602,7 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
const railDrawDistance = RAIL_ZOOM_MAX - this.currentRailMinZoom();
const railThickness = this.currentRailThickness();
const oceanColor = this.currentOceanColor();
const nukeColor = this.currentNukeColor();
const ambientLevel = this.currentAmbientLevel();
const unitGlow = this.currentUnitGlow();
const colorblind = this.currentColorblind();
@@ -1123,6 +1154,33 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
/>
</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.nuke_color_label")}
</div>
<div class="text-sm text-slate-400">
${translateText("graphics_setting.nuke_color_desc")}
</div>
</div>
<input
type="text"
.value=${nukeColor}
placeholder=${NUKE_COLOR_DEFAULT}
spellcheck="false"
@change=${this.onNukeColorChange}
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=${nukeColor}
@input=${this.onNukeColorChange}
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"
>
@@ -27,6 +27,9 @@ export const GraphicsOverridesSchema = z
territorySaturation: z.number(),
territoryAlpha: z.number(),
coordinateGridOpacity: z.number(),
// "#rrggbb" hex string; overrides the lingering fallout ground tint
// left on territory after a nuke.
staleNukeColor: z.string(),
})
.partial(),
railroad: z
+10
View File
@@ -1,5 +1,6 @@
import type { GraphicsOverrides } from "./GraphicsOverrides";
import { createThemeSettings, type RenderSettings } from "./RenderSettings";
import { hexToRgb } from "./utils/ColorUtils";
/**
* Apply the user's graphics overrides onto a RenderSettings in place: name
@@ -64,6 +65,15 @@ export function applyGraphicsOverrides(
settings.mapOverlay.coordinateGridOpacity =
overrides.mapOverlay.coordinateGridOpacity;
}
if (overrides.mapOverlay?.staleNukeColor !== undefined) {
// hexToRgb yields 0-255 channels; the stale-nuke uniforms are 0-1 floats.
const rgb = hexToRgb(overrides.mapOverlay.staleNukeColor);
if (rgb !== null) {
settings.mapOverlay.staleNukeR = rgb[0] / 255;
settings.mapOverlay.staleNukeG = rgb[1] / 255;
settings.mapOverlay.staleNukeB = rgb[2] / 255;
}
}
if (overrides.railroad?.railMinZoom !== undefined) {
settings.railroad.railMinZoom = overrides.railroad.railMinZoom;
}