Map Layers 🗺️ - #4703
Conversation
WalkthroughThis PR adds validated map-layer metadata and PNG assets, loads and renders layers between terrain and territory, persists layer visibility settings, and propagates nuke blast-radius tiles so nukeable layers show destroyed tiles. ChangesMap layers
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant NukeExecution
participant GameImpl
participant GameRunner
participant Worker
participant GameView
participant WebGLFrameBuilder
participant MapRenderer
NukeExecution->>GameImpl: queueNukeImpact(tile)
GameRunner->>GameImpl: drainNukeImpacts()
GameRunner->>Worker: send packedNukeImpacts
Worker->>GameView: deliver update batch
GameView->>WebGLFrameBuilder: expose recentlyNukedTiles()
WebGLFrameBuilder->>MapRenderer: mark nukeable layer tiles destroyed
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (3)
src/client/WebGLFrameBuilder.ts (1)
294-310: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winDouble loop drives many individual per-tile GPU uploads for large nuke blasts.
syncNukeImpactscallsmarkLayerTileDestroyedonce per(nukeable layer, nuked tile)pair. Each call rebinds the destroyed-mask texture and re-uploads a single texel (seeMapLayerPass.markTileDestroyed), so a big blast radius across several nukeable layers turns into a large burst of individual WebGL calls on one frame.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/WebGLFrameBuilder.ts` around lines 294 - 310, Update syncNukeImpacts and the underlying MapLayerPass.markTileDestroyed path to batch destroyed-mask updates per nukeable layer, avoiding one texture rebind and single-texel upload for every nuked tile. Preserve the existing full-blast-radius behavior while issuing consolidated GPU uploads for each layer.src/client/render/gl/Renderer.ts (1)
1406-1411: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
findIndexscan in the nuke-destruction hot path.
markLayerTileDestroyedre-scansstoredLayerswithfindIndexon every call;WebGLFrameBuilder.syncNukeImpactscalls this once per(layer, tile)pair for the whole blast radius. Shares one root cause withMapLayerPass.markTileDestroyed's per-call texture rebind — see consolidated comment.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/render/gl/Renderer.ts` around lines 1406 - 1411, Optimize markLayerTileDestroyed by avoiding the storedLayers.findIndex scan on every destruction call. Reuse an existing layer-to-pass index mapping or maintain one for direct lookup from layerId, while preserving the current bounds check and markTileDestroyed(tileIndex) behavior.src/client/render/gl/passes/MapLayerPass.ts (1)
120-139: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winPer-tile bind + pixelStorei on every nuke-destruction call.
markTileDestroyedrebindsdestroyedTexand re-setspixelStoreion every invocation, with no batch entry point for multiple tiles. This is the root cause of a shared perf finding — see consolidated comment coveringWebGLFrameBuilder.tsandRenderer.ts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/render/gl/passes/MapLayerPass.ts` around lines 120 - 139, The per-tile update path in markTileDestroyed should avoid rebinding destroyedTex and resetting UNPACK_ALIGNMENT for every destroyed tile. Add or reuse a batch update flow that binds/configures the texture once, uploads all pending tile bytes, and expose the necessary coordination through the relevant rendering path while preserving the existing bounds and duplicate-destruction checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/client/hud/layers/GraphicsSettingsModal.ts`:
- Around line 356-365: Update onToggleLayer and every graphics-override write
path, including reset, preset import, and preset application, to use one shared
helper that synchronizes mapLayerVisibility with the WebGL layers. Keep
visibility-only updates out of the generic graphics-change refresh so they do
not rebuild terrain or refresh player colors, while preserving the existing
direct-toggle callback and UI update behavior.
In `@src/client/render/gl/passes/MapLayerPass.ts`:
- Line 1: Batch nuke-destruction updates per layer rather than issuing one GPU
update per tile. Add a batch method beside MapLayerPass.markTileDestroyed that
binds destroyedTex and configures pixelStorei once, then uploads every provided
index; update Renderer.markLayerTileDestroyed to resolve the layer pass once and
forward the full index list; and change WebGLFrameBuilder.syncNukeImpacts to
collect matching tile indices per nukeable layer before making one batch call.
- Around line 174-201: Cache the uniform locations for uLayerTex, uTerrainBytes,
and uDestroyedMask in the MapLayerPass constructor alongside uCamera,
uPlacement, uNukeable, and uVisible, storing them on the pass instance. Update
draw() to reuse those cached locations and remove the per-frame
gl.getUniformLocation calls.
In `@src/client/render/gl/Renderer.ts`:
- Around line 1364-1422: Replace the parallel mapLayerPasses array with a Map
keyed by layer id, updating setMapLayers, setLayerVisible,
markLayerTileDestroyed, and setLayerDestroyedMask to retrieve passes directly by
id and safely no-op when absent. Update draw and dispose to iterate
mapLayerPasses.values(), and clear the map after disposal so missing layer
images cannot shift pass associations.
In `@src/core/execution/NukeExecution.ts`:
- Around line 372-374: Add tests covering nuke-impact propagation for both land
and water nukes, using the repository’s setup() helper from tests/util/Setup.ts.
Verify drainNukeImpacts() returns all queued tiles after each impact and returns
an empty result on the subsequent drain, exercising the queueNukeImpact call in
NukeExecution.
In `@src/core/game/TerrainMapLoader.ts`:
- Around line 134-150: Update the layer-loading flow in TerrainMapLoader to
downsample each layer PNG to manifest.map4x dimensions when mapSize is
GameMapSize.Compact before uploading it. Pass the resulting image dimensions
into MapLayerPass and its shader so texture sampling uses the scaled texel space
while preserving full-resolution behavior for other map sizes.
In `@tests/MapLayers.test.ts`:
- Around line 61-95: Replace the locally defined VALID_ID_RE, reservedIds, and
validPlacements assertions in “Layer validation rules” with tests that invoke
the production layer validator or map generator. Assert the actual validation
results or errors for valid IDs, invalid characters, empty IDs, reserved
“image”, and invalid placement “air”, rather than testing duplicated local
rules.
---
Nitpick comments:
In `@src/client/render/gl/passes/MapLayerPass.ts`:
- Around line 120-139: The per-tile update path in markTileDestroyed should
avoid rebinding destroyedTex and resetting UNPACK_ALIGNMENT for every destroyed
tile. Add or reuse a batch update flow that binds/configures the texture once,
uploads all pending tile bytes, and expose the necessary coordination through
the relevant rendering path while preserving the existing bounds and
duplicate-destruction checks.
In `@src/client/render/gl/Renderer.ts`:
- Around line 1406-1411: Optimize markLayerTileDestroyed by avoiding the
storedLayers.findIndex scan on every destruction call. Reuse an existing
layer-to-pass index mapping or maintain one for direct lookup from layerId,
while preserving the current bounds check and markTileDestroyed(tileIndex)
behavior.
In `@src/client/WebGLFrameBuilder.ts`:
- Around line 294-310: Update syncNukeImpacts and the underlying
MapLayerPass.markTileDestroyed path to batch destroyed-mask updates per nukeable
layer, avoiding one texture rebind and single-texel upload for every nuked tile.
Preserve the existing full-blast-radius behavior while issuing consolidated GPU
uploads for each layer.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: dde52b81-a615-4305-8c3b-10cb4af7010f
⛔ Files ignored due to path filters (2)
src/client/render/gl/shaders/map-layer/layer.frag.glslis excluded by!**/*.glslsrc/client/render/gl/shaders/map-layer/layer.vert.glslis excluded by!**/*.glsl
📒 Files selected for processing (26)
map-generator/codegen.gomap-generator/main.goresources/lang/en.jsonsrc/client/ClientGameRunner.tssrc/client/WebGLFrameBuilder.tssrc/client/hud/GameRenderer.tssrc/client/hud/layers/GraphicsSettingsModal.tssrc/client/render/gl/GraphicsOverrides.tssrc/client/render/gl/MapRenderer.tssrc/client/render/gl/Renderer.tssrc/client/render/gl/passes/MapLayerPass.tssrc/client/view/GameView.tssrc/core/GameRunner.tssrc/core/execution/NukeExecution.tssrc/core/game/BinaryLoaderGameMapLoader.tssrc/core/game/FetchGameMapLoader.tssrc/core/game/Game.tssrc/core/game/GameImpl.tssrc/core/game/GameMapLoader.tssrc/core/game/GameUpdates.tssrc/core/game/Maps.gen.tssrc/core/game/TerrainMapLoader.tssrc/core/worker/Worker.worker.tstests/MapConsistency.test.tstests/MapLayers.test.tstests/perf/fullgame/NodeGameMapLoader.ts
…p for layer passes
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tests/MapConsistency.test.ts (1)
595-613: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winFail when the
map_layerssection is absent.At Line 600, the early return lets defined layers pass without any translation entries. Use an empty fallback object so Lines 612-618 report every missing layer name.
Proposed fix
- if (!mapLayersSection) { - // No layers defined anywhere — that's fine. - return; - } + const mapLayers = mapLayersSection ?? {}; ... - if (mapLayersSection[layer.id] === undefined) { + if (mapLayers[layer.id] === undefined) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/MapConsistency.test.ts` around lines 595 - 613, Update the mapLayersSection initialization in the “Layer names exist in en.json map_layers section” test to use an empty object when enContent.map_layers is absent, and remove the early return. Preserve the existing layer iteration and missing-entry error reporting so defined layers are still validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/core/executions/NukeExecution.test.ts`:
- Around line 265-272: Strengthen the assertions around game.drainNukeImpacts()
to verify the complete expected blast set or count, rather than only checking
impacts.length is positive. Include the target tile and all queued impact tiles
in the expected result while preserving the existing validity checks for each
returned reference.
In `@tests/util/layerValidation.ts`:
- Around line 28-42: Update the layer validation logic in the layer-validation
routine to validate raw field types before applying ID checks: require layer.id
to be a string before testing emptiness, the reserved value, or VALID_ID_RE, and
reject non-string IDs without allowing regex coercion. When nukeable is present,
require it to be boolean and report invalid values consistently with the
existing errors collection.
---
Outside diff comments:
In `@tests/MapConsistency.test.ts`:
- Around line 595-613: Update the mapLayersSection initialization in the “Layer
names exist in en.json map_layers section” test to use an empty object when
enContent.map_layers is absent, and remove the early return. Preserve the
existing layer iteration and missing-entry error reporting so defined layers are
still validated.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 968c50b1-9d7e-43d3-8742-ecb757cec9fd
📒 Files selected for processing (10)
src/client/WebGLFrameBuilder.tssrc/client/hud/layers/GraphicsSettingsModal.tssrc/client/render/gl/MapRenderer.tssrc/client/render/gl/Renderer.tssrc/client/render/gl/passes/MapLayerPass.tssrc/core/game/TerrainMapLoader.tstests/MapConsistency.test.tstests/MapLayers.test.tstests/core/executions/NukeExecution.test.tstests/util/layerValidation.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- tests/MapLayers.test.ts
- src/client/WebGLFrameBuilder.ts
- src/client/render/gl/MapRenderer.ts
- src/client/render/gl/passes/MapLayerPass.ts
- src/client/hud/layers/GraphicsSettingsModal.ts
- src/core/game/TerrainMapLoader.ts
- src/client/render/gl/Renderer.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/core/executions/NukeExecution.test.ts`:
- Around line 346-355: Strengthen the assertions in the water-nuke test around
drainNukeImpacts by verifying the deterministic full impact set or count, rather
than only checking that impacts are non-empty and contain the land target. Add
an assertion for a known water tile within the blast radius, while preserving
the existing ref validity checks and empty follow-up drain assertion.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: ecc260cb-7a15-48e2-900d-dbd41cb50435
📒 Files selected for processing (1)
tests/core/executions/NukeExecution.test.ts
evanpelle
left a comment
There was a problem hiding this comment.
From Claude:
Correctness & Risk
- Full-map terrain re-upload becomes ~3.4M GL calls (high)
Renderer.applyTerrainDelta now does a 1×1 texSubImage2D plus a fresh new Uint8Array([...]) allocation per ref (Renderer.ts:955-980). That's fine for a nuke burst, but ClientGameRunner.ts:473 calls it on context restore with every tile on the map:
const allRefs = new Array(mapSize); // 2000×1700 = 3.4M for a typical map
view.applyTerrainDelta(allRefs, allTerrain);
TerrainPass has the same per-ref loop (pre-existing), but at least reuses this.pixelScratch. This PR adds a second full pass over it plus 3.4M short-lived allocations — and it runs on every map, including the ones with no layers, since terrainBytesTex is always created.
Suggested fix:
if (this.mapLayerPasses.size === 0) return; // nothing consumes it
if (refs.length === this.mapW * this.mapH) { // full re-upload fast path
// single texSubImage2D of terrainBytes
} else { /* per-ref, reusing a scratch Uint8Array(1) */ }
- Layer PNGs are also loaded in the Web Worker (high)
loadTerrainMap is called from both ClientGameRunner (main thread, needs the bitmaps) and GameRunner.createGameRunner (GameRunner.ts:42, runs in Worker.worker.ts, never uses them). The worker will therefore fetch, decode and permanently retain an ImageBitmap per layer — ~13.6 MB for a 2000×1700 layer, held forever in the module-level loadedMaps cache — and block worker startup on it.
Gate image loading behind a parameter (loadLayerImages = false by default) or split it into a separate loadMapLayerImages() that only the client calls.
- Layer loading blocks game start (medium)
The await Promise.all(layers.map(...)) sits inside loadTerrainMap before the map data is returned, so multi-MB full-size transparent PNGs are on the critical path for every player on a layered map. Since the renderer already tolerates a missing layer (console.warn + skip), consider loading them off the critical path and calling setMapLayers when they arrive.
- Context restore silently drops layer state (medium)
MapRenderer.handleContextRestored re-creates passes via setMapLayers(...), which builds fresh MapLayerPass objects with a zeroed destroyedData and _visible = true. After a context loss:
- every nuke-destroyed layer tile reappears, and
- layers the player hid become visible again.
MapLayerPass.restoreFrom() and Renderer/MapRenderer.setLayerDestroyedMask() were clearly written for this but are never called (confirmed by grep across src + tests). Either wire them up (keep the masks + visibility map on MapRenderer, re-apply after restore) or delete them.
…loading, context-restore state, nuke batch upload - Renderer/RailroadPass/TerrainPass: full-map fast path (single texSubImage2D) + scratch buffer reuse, eliminating ~3.4M per-tile GL calls on context restore - TerrainMapLoader: add loadImages param (default true); worker passes false to skip layer ImageBitmap loading - ClientGameRunner: load layer images off the critical path via loadLayerImages(); setMapLayers when ready - MapRenderer: preserve layer visibility and destroyed masks across context loss; re-apply on restore - MapLayerPass.markTilesDestroyed: bounding-box batch sub-region upload instead of per-tile 1x1 calls - WebGLFrameBuilder.syncNukeImpacts: filter nuked tiles by layer placement (water layer skips land tiles)
|
I have added a big performance boost to nukeable layers, and addressed the claude review: Issue 1: Full-map terrain re-upload — ~3.4M GL calls ✅ FixedRoot cause: Changes:
Issue 2: Layer PNGs loaded in Web Worker ✅ FixedRoot cause: Changes:
Issue 3: Layer loading blocks game start ✅ FixedRoot cause: Changes:
Issue 4: Context restore silently drops layer state ✅ FixedRoot cause: |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/client/render/gl/passes/RailroadPass.ts (1)
251-280: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRequire canonical refs before bulk-uploading
bytes.
bytesis documented as parallel torefs, but this path ignoresrefsand treatsbytes[0]as tile(0,0).refs.length === mapW * mapHdoes not prove refs are unique and ordered, so an unordered full-sized delta corrupts bridge water detection. Only use this path when everyrefs[i] === i; otherwise retain per-tile uploads.Proposed fix
- if (refs.length === this.mapW * this.mapH) { + if ( + refs.length === this.mapW * this.mapH && + refs.every((ref, index) => ref === index) + ) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/render/gl/passes/RailroadPass.ts` around lines 251 - 280, Update the full-map fast path in the terrain upload logic to require canonical ordering, not just refs.length === this.mapW * this.mapH: verify every refs[i] equals i before bulk-uploading bytes. Fall back to the existing per-tile texSubImage2D loop for unordered or duplicate refs, preserving the current fast path only for canonical refs.src/core/game/TerrainMapLoader.ts (2)
71-73: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftDo not cache incompatible
loadImagesresults.The cache key ignores
loadImages. A firstloadTerrainMap(..., false)cacheslayerImages: undefined, so a later default call returns without loading images; the reverse also defeats the caller’s request not to retain images. Cache the image-free terrain base separately and construct image-bearing results per mode.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/core/game/TerrainMapLoader.ts` around lines 71 - 73, Update the cache flow around loadedMaps and loadTerrainMap so cache entries never mix loadImages modes: store and reuse only the image-free terrain base, then construct image-bearing results when requested and image-free results when not requested. Ensure a prior false-mode load does not bypass image loading for a later true/default call, and a true-mode request does not force images into a false-mode result.
14-14: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy liftMove
ImageBitmapand PNG loading out ofTerrainMapLoader.
loadTerrainMap()/loadLayerImages()andImageBitmapnow live insrc/core/game; this binds core map loading to the browser image APIs. Keep map metadata/simulation in core, and pass layer image data through an owned typed loader in the client instead ofImageBitmap.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/core/game/TerrainMapLoader.ts` at line 14, Remove ImageBitmap and PNG loading responsibilities from TerrainMapLoader and its loadTerrainMap()/loadLayerImages() flow. Keep TerrainMapLoader focused on core terrain metadata and simulation, and introduce or reuse a client-owned typed loader to provide layer image data without exposing browser ImageBitmap types in core.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/client/render/gl/passes/RailroadPass.ts`:
- Around line 251-280: Update the full-map fast path in the terrain upload logic
to require canonical ordering, not just refs.length === this.mapW * this.mapH:
verify every refs[i] equals i before bulk-uploading bytes. Fall back to the
existing per-tile texSubImage2D loop for unordered or duplicate refs, preserving
the current fast path only for canonical refs.
In `@src/core/game/TerrainMapLoader.ts`:
- Around line 71-73: Update the cache flow around loadedMaps and loadTerrainMap
so cache entries never mix loadImages modes: store and reuse only the image-free
terrain base, then construct image-bearing results when requested and image-free
results when not requested. Ensure a prior false-mode load does not bypass image
loading for a later true/default call, and a true-mode request does not force
images into a false-mode result.
- Line 14: Remove ImageBitmap and PNG loading responsibilities from
TerrainMapLoader and its loadTerrainMap()/loadLayerImages() flow. Keep
TerrainMapLoader focused on core terrain metadata and simulation, and introduce
or reuse a client-owned typed loader to provide layer image data without
exposing browser ImageBitmap types in core.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 334c705a-31f3-492d-9619-e3d0ff115d1b
📒 Files selected for processing (9)
src/client/ClientGameRunner.tssrc/client/WebGLFrameBuilder.tssrc/client/render/gl/MapRenderer.tssrc/client/render/gl/Renderer.tssrc/client/render/gl/passes/MapLayerPass.tssrc/client/render/gl/passes/RailroadPass.tssrc/client/render/gl/passes/TerrainPass.tssrc/core/GameRunner.tssrc/core/game/TerrainMapLoader.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- src/core/GameRunner.ts
- src/client/WebGLFrameBuilder.ts
- src/client/ClientGameRunner.ts
- src/client/render/gl/MapRenderer.ts
- src/client/render/gl/Renderer.ts
- src/client/render/gl/passes/MapLayerPass.ts
Description:
Adds a map layer system that lets mappers overlay PNG images (forests, little decorations, labels, deserts, huts, springs, waterfalls, ice fields, etc.) between the terrain and territory rendering passes. Each layer is a full-size PNG with transparency, placed on either land or water tiles. Layers can optionally be marked as nukeable, meaning they are permanently erased in the blast radius of any nuke. Also works with water nukes and compact maps!
How it works
A map defines layers in its info.json:
Each layer needs a matching PNG file (e.g. forests.png) in the map-generator assets folder. The PNG must be the same dimensions as the map's image.png. The placement field controls whether the layer appears on land tiles or water tiles. When nukeable is true, the layer is destroyed whenever a nuke detonates within its radius.
Players can toggle individual layers on/off in the Graphics Settings modal under a new "Map Layers" section. Visibility preferences are saved in graphics overrides and persist across sessions.
The map generator validates layer definitions at build time (id format, placement values, duplicate ids, PNG dimensions) and copies layer PNGs to the output alongside image.png. The gen-maps script also populates the map_layers section in en.json so layer names can be translated via Crowdin.
Comprehensive tests cover layer validation rules in info.json, PNG existence in both map-generator and resources directories, manifest/info.json consistency, and en.json coverage.
Media
Example: Water nukes cause tiles to use different layers after impact
2026-07-24.14-45-29.mp4
Example: Nukeable water layer
2026-07-24.14-46-35.mp4
Graphic settings: Toggle layers
2026-07-24.15-00-06.mp4
Please complete the following:
Please put your Discord username so you can be contacted if a bug or regression is found:
FloPinguin