rounds 5-7: real tree textures, terrain detail, audio pass + F12 capture tool#8
Conversation
Visuals (rounds 5-6): - Trees rebuilt around external CC0 texture packs (SOURCES.md in assets/textures/external): ShareTextures Platanus 2K / Robinia 4K branch atlases stamped whole via atlasCard, ambientCG LeafSet004 scanned-leaf compositor as fallback, Poly Haven pine bark embedded as JPEG. Three species variants (prop_tree, prop_tree2, prop_tree3); fallback chain atlas -> leaf compositor -> procedural keeps fresh checkouts building without the packs. - tools/png.ts grows a full 8-bit PNG decoder (color types 0/2/3/4/6) for the compositors; build-props gains the atlas + compositor paths. - Terrain: macro moisture patches (~45 m), riverbed mud blend gated on the carved channel + waterline, straw/lush sweeps; grass blades get the squared root->tip gradient; house.glb rebuilt. Audio (round 7): - Weapon-specific fire sounds, positional alien vocals (attack/pain/ three death variants per kind, from Unvanquished's own GPL assets - see assets/sounds/SOURCES.md), flesh-impact hit confirm, ricochets, player pain/death, wading splashes on a cooldown. - Audio listener follows the camera every frame so playSound3D pans and attenuates correctly. Requires the engine round8 branch (Bloom-Engine/engine#86) for the material per-view wiring, multi-primitive skinning, cached rotated draws, and per-cascade shadow uniforms these assets were tuned against.
takeScreenshot is broken on Windows and geisterhand /key ignores F-keys; posting VK_F12 WM_KEYDOWN/UP to the game window triggers the engine screenshot handler and yields true 4K captures in redirected batch runs. Snapshots the existing screenshot set first and reports only the new file - never deletes.
📝 WalkthroughWalkthroughThe PR updates terrain, water, and grass shaders; expands procedural asset generation; adds tree variants and external texture support; revises forest rendering; introduces positional audio and river testing; and adds asset provenance and screenshot tooling. ChangesRendering, assets, and gameplay feedback
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Player
participant GameLoop
participant AudioSystem
participant WaterSystem
participant WaterShader
participant ReflectionProbe
Player->>GameLoop: Move, fire, or hit an enemy
GameLoop->>AudioSystem: Play positional weapon, impact, enemy, or splash sound
GameLoop->>WaterSystem: Submit cooldown-gated river impulse
WaterSystem->>WaterShader: Provide impulse and water parameters
ReflectionProbe-->>WaterShader: Provide planar reflection
WaterShader->>WaterShader: Compose refraction, absorption, Fresnel, and reflection
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tools/png.ts (1)
84-169: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
decodePngaddsthrow new Error(...)— confirm the no-throw rule doesn't extend to build tooling.The coding guideline for
{src/**/*.ts,tools/**/*.ts}says to return{ ok, error }rather than throwing.decodePngintroduces four newthrowsites (invalid signature, unsupported bit depth/interlace/colour type). These are build-time Bun tools (not Perry FFI runtime), andencodePng/resolveTexturealready throw here, so this may be intentionally out of scope — but the path pattern matches, so please confirm.Decoder logic itself (signature, chunk walk, inflate, Sub/Up/Avg/Paeth unfilter, palette/tRNS handling) is correct.
As per coding guidelines: "Return
{ ok: boolean, error: string }instead of throwing exceptions."🤖 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 `@tools/png.ts` around lines 84 - 169, Confirm the no-throw guideline for tools/**/*.ts and align decodePng with the project’s error-handling convention: replace its four exception paths for invalid PNGs and unsupported formats with a typed { ok, error } result, then update callers to check and propagate failures; if build tooling is explicitly exempt, document or verify that exception and leave decodePng consistent with encodePng and resolveTexture.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.
Inline comments:
In `@src/main.ts`:
- Around line 2162-2176: Update the pain-bark condition in the enemy-hit
handling block to use the shot counter rather than the fixed enemy slot: replace
the `(i & 3) === 0` gate with an equivalent check based on `shotsFired`,
preserving the existing non-lethal-hit logic and pain sound call.
---
Nitpick comments:
In `@tools/png.ts`:
- Around line 84-169: Confirm the no-throw guideline for tools/**/*.ts and align
decodePng with the project’s error-handling convention: replace its four
exception paths for invalid PNGs and unsupported formats with a typed { ok,
error } result, then update callers to check and propagate failures; if build
tooling is explicitly exempt, document or verify that exception and leave
decodePng consistent with encodePng and resolveTexture.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 9a169ad9-7c43-47cd-aee9-595f1da2a4ba
⛔ Files ignored due to path filters (62)
assets/sounds/alien0_attack.wavis excluded by!**/*.wavassets/sounds/alien0_die1.wavis excluded by!**/*.wavassets/sounds/alien0_die2.wavis excluded by!**/*.wavassets/sounds/alien0_die3.wavis excluded by!**/*.wavassets/sounds/alien0_pain.wavis excluded by!**/*.wavassets/sounds/alien1_attack.wavis excluded by!**/*.wavassets/sounds/alien1_die1.wavis excluded by!**/*.wavassets/sounds/alien1_die2.wavis excluded by!**/*.wavassets/sounds/alien1_die3.wavis excluded by!**/*.wavassets/sounds/alien1_pain.wavis excluded by!**/*.wavassets/sounds/alien2_attack.wavis excluded by!**/*.wavassets/sounds/alien2_die1.wavis excluded by!**/*.wavassets/sounds/alien2_die2.wavis excluded by!**/*.wavassets/sounds/alien2_die3.wavis excluded by!**/*.wavassets/sounds/alien2_pain.wavis excluded by!**/*.wavassets/sounds/alien3_attack.wavis excluded by!**/*.wavassets/sounds/alien3_die1.wavis excluded by!**/*.wavassets/sounds/alien3_die2.wavis excluded by!**/*.wavassets/sounds/alien3_die3.wavis excluded by!**/*.wavassets/sounds/alien3_pain.wavis excluded by!**/*.wavassets/sounds/alien4_attack.wavis excluded by!**/*.wavassets/sounds/alien4_die1.wavis excluded by!**/*.wavassets/sounds/alien4_die2.wavis excluded by!**/*.wavassets/sounds/alien4_die3.wavis excluded by!**/*.wavassets/sounds/alien4_pain.wavis excluded by!**/*.wavassets/sounds/blaster_fire.wavis excluded by!**/*.wavassets/sounds/impact_flesh.wavis excluded by!**/*.wavassets/sounds/player_die1.wavis excluded by!**/*.wavassets/sounds/player_die2.wavis excluded by!**/*.wavassets/sounds/player_pain1.wavis excluded by!**/*.wavassets/sounds/player_pain2.wavis excluded by!**/*.wavassets/sounds/ricochet1.wavis excluded by!**/*.wavassets/sounds/ricochet2.wavis excluded by!**/*.wavassets/sounds/rifle_fire2.wavis excluded by!**/*.wavassets/sounds/splash1.wavis excluded by!**/*.wavassets/textures/external/forrest_ground_01/forrest_ground_01_diff_2k.jpgis excluded by!**/*.jpgassets/textures/external/forrest_ground_01/forrest_ground_01_nor_gl_2k.jpgis excluded by!**/*.jpgassets/textures/external/forrest_ground_01/forrest_ground_01_rough_2k.jpgis excluded by!**/*.jpgassets/textures/external/leafset/LeafSet004/LeafSet004_2K-PNG_Color.pngis excluded by!**/*.pngassets/textures/external/leafset/LeafSet004/LeafSet004_2K-PNG_Opacity.pngis excluded by!**/*.pngassets/textures/external/pine_bark/pine_bark_diff_1k.jpgis excluded by!**/*.jpgassets/textures/external/pine_bark/pine_bark_diff_2k.jpgis excluded by!**/*.jpgassets/textures/external/pine_bark/pine_bark_nor_gl_1k.jpgis excluded by!**/*.jpgassets/textures/external/pine_bark/pine_bark_nor_gl_2k.jpgis excluded by!**/*.jpgassets/textures/external/pine_bark/pine_bark_rough_1k.jpgis excluded by!**/*.jpgassets/textures/external/pine_bark/pine_bark_rough_2k.jpgis excluded by!**/*.jpgassets/textures/external/platanus/PlatanusOccidentalis_1-2K/PlatanusOccidentalis_1_ambientocclusion-2K.pngis excluded by!**/*.pngassets/textures/external/platanus/PlatanusOccidentalis_1-2K/PlatanusOccidentalis_1_basecolor-2K.pngis excluded by!**/*.pngassets/textures/external/platanus/PlatanusOccidentalis_1-2K/PlatanusOccidentalis_1_height-2K.pngis excluded by!**/*.pngassets/textures/external/platanus/PlatanusOccidentalis_1-2K/PlatanusOccidentalis_1_metallic-2K.pngis excluded by!**/*.pngassets/textures/external/platanus/PlatanusOccidentalis_1-2K/PlatanusOccidentalis_1_normal-2K.pngis excluded by!**/*.pngassets/textures/external/platanus/PlatanusOccidentalis_1-2K/PlatanusOccidentalis_1_opacity-2K.pngis excluded by!**/*.pngassets/textures/external/platanus/PlatanusOccidentalis_1-2K/PlatanusOccidentalis_1_roughness-2K.pngis excluded by!**/*.pngassets/textures/external/platanus/PlatanusOccidentalis_1-2K/PlatanusOccidentalis_1_translucency-2K.pngis excluded by!**/*.pngassets/textures/external/robinia/RobiniaViscosa_2-4K/RobiniaViscosa_2_ambientocclusion-4K.pngis excluded by!**/*.pngassets/textures/external/robinia/RobiniaViscosa_2-4K/RobiniaViscosa_2_basecolor-4K.pngis excluded by!**/*.pngassets/textures/external/robinia/RobiniaViscosa_2-4K/RobiniaViscosa_2_height-4K.pngis excluded by!**/*.pngassets/textures/external/robinia/RobiniaViscosa_2-4K/RobiniaViscosa_2_metallic-4K.pngis excluded by!**/*.pngassets/textures/external/robinia/RobiniaViscosa_2-4K/RobiniaViscosa_2_normal-4K.pngis excluded by!**/*.pngassets/textures/external/robinia/RobiniaViscosa_2-4K/RobiniaViscosa_2_opaciy-4K.pngis excluded by!**/*.pngassets/textures/external/robinia/RobiniaViscosa_2-4K/RobiniaViscosa_2_roughness-4K.pngis excluded by!**/*.pngassets/textures/external/robinia/RobiniaViscosa_2-4K/RobiniaViscosa_2_translucency-4K.pngis excluded by!**/*.png
📒 Files selected for processing (13)
assets/materials/grass_instanced.wgslassets/materials/terrain.wgslassets/materials/water.wgslassets/models/house.glbassets/models/prop_tree.glbassets/models/prop_tree2.glbassets/models/prop_tree3.glbassets/sounds/SOURCES.mdassets/textures/external/SOURCES.mdsrc/main.tstools/build-props.tstools/f12-shot.ps1tools/png.ts
👮 Files not reviewed due to content moderation or server errors (1)
- assets/models/house.glb
| // Per-kind death screech, positional, variant by slot. | ||
| playSound3D(sfxAlienDie[enKind[i] * 3 + (i % 3)], | ||
| enX[i], enY[i] + 1, enZ[i]); | ||
| } else if ((i & 3) === 0) { | ||
| // Occasional pain bark so sustained fire isn't monotone. | ||
| playSound3D(sfxAlienPain[enKind[i]], enX[i], enY[i] + 1, enZ[i]); | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| if (!struckEnemy) { | ||
| // World hit — ricochet ping (alternating variants). | ||
| playSound3D(sfxRicochet[shotsFired & 1], | ||
| hit.point.x, hit.point.y, hit.point.z); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Pain-bark gate is deterministic per-enemy, not "occasional" across shots.
(i & 3) === 0 gates on the enemy's fixed array slot i, not on the shot/frame. This means enemies occupying slots 0,4,8,... will bark on every non-lethal hit, while all others will never bark — the comment's intent ("Occasional pain bark so sustained fire isn't monotone") isn't achieved; the outcome is fixed per-slot rather than varied per-hit.
🎯 Proposed fix: gate on shot count instead of enemy index
- } else if ((i & 3) === 0) {
+ } else if ((shotsFired & 3) === 0) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Per-kind death screech, positional, variant by slot. | |
| playSound3D(sfxAlienDie[enKind[i] * 3 + (i % 3)], | |
| enX[i], enY[i] + 1, enZ[i]); | |
| } else if ((i & 3) === 0) { | |
| // Occasional pain bark so sustained fire isn't monotone. | |
| playSound3D(sfxAlienPain[enKind[i]], enX[i], enY[i] + 1, enZ[i]); | |
| } | |
| break; | |
| } | |
| } | |
| if (!struckEnemy) { | |
| // World hit — ricochet ping (alternating variants). | |
| playSound3D(sfxRicochet[shotsFired & 1], | |
| hit.point.x, hit.point.y, hit.point.z); | |
| } | |
| // Per-kind death screech, positional, variant by slot. | |
| playSound3D(sfxAlienDie[enKind[i] * 3 + (i % 3)], | |
| enX[i], enY[i] + 1, enZ[i]); | |
| } else if ((shotsFired & 3) === 0) { | |
| // Occasional pain bark so sustained fire isn't monotone. | |
| playSound3D(sfxAlienPain[enKind[i]], enX[i], enY[i] + 1, enZ[i]); | |
| } | |
| break; | |
| } | |
| } | |
| if (!struckEnemy) { | |
| // World hit — ricochet ping (alternating variants). | |
| playSound3D(sfxRicochet[shotsFired & 1], | |
| hit.point.x, hit.point.y, hit.point.z); | |
| } |
🤖 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/main.ts` around lines 2162 - 2176, Update the pain-bark condition in the
enemy-hit handling block to use the shot counter rather than the fixed enemy
slot: replace the `(i & 3) === 0` gate with an equivalent check based on
`shotsFired`, preserving the existing non-lethal-hit logic and pain sound call.
…thorable)
`terrain.wgsl` computed its four layer weights procedurally — slope for rock, a
moisture FBM for the dry/lush grass split, distance to a hardcoded river line for
dirt. Good defaults, and no authoring story at all: `TerrainLayer.weights` was
written by nothing and read by nothing.
The editor now paints those weights. They arrive as one RGBA8 texel per terrain
cell and are mixed OVER the procedural blend by COVERAGE (the sum of the four
weights):
- a cell nobody painted has zero coverage and keeps the procedural look exactly,
which is why neither shipped arena changed by a pixel;
- erasing lowers coverage rather than zeroing the ground, so paint fades back
into the procedural blend instead of leaving a bald patch.
Layer order is the ABI and it is the WORLD FILE'S, not ours: layer i's textureRef
becomes array slice i and its weights become splat channel i (normal found by the
_albedo.png -> _normal.png convention). No layers = the built-in four.
The splat needed an array slot and all three were taken, so SH-010's detail normal
moved to slice 4 of the normal array — all five normal maps are 512² and a texture
array wants a uniform extent anyway. Transport is engine EN-049.
Verified in the game, not just the editor: tools/paint-test-world.ts writes what
the brush writes, and the shooter renders it.
Two bugs found on the way, both mine-adjacent and both silent:
- engine EN-050: Perry miscompiled `clamp`, so every splat weight quantised to 0
and painted terrain loaded unpainted (docs/perry-quirks.md #8);
- the splat_uv params landed at float 24 instead of 28 — four per-layer tints are
16 floats, not 12 — which overwrote the detail-normal scales and left the UV
transform zero. That reads as "the splat map is empty", not as an off-by-one.
perry-quirks #8 also records the five probes that gave confidently WRONG answers
before one gave a right one — constant folding, shadowing, calls nested in log
concatenations, a bogus `as` cast, and worst of all stale builds: Perry does not
rebuild when only an imported module changed.
Claude-Session: https://claude.ai/code/session_01V3MihNxaRwMR8GjMPvMkRb
Summary
Lands the rounds 5–7 content pass and the new batch-capture tool. The game-side counterpart of Bloom-Engine/engine#86 — the two headline fixes from that PR (scene-wide missing shadows; the wave-2 "distorted dragon") are engine-side, so this PR carries no gameplay-code fixes for them, only the content that was tuned against that engine state.
Visuals (rounds 5–6)
assets/textures/external/SOURCES.md): ShareTextures Platanus 2K / Robinia 4K branch atlases, ambientCG LeafSet004 scanned-leaf compositor fallback, Poly Haven pine bark embedded as JPEG. Three species variants; fallback chain atlas → compositor → procedural keeps fresh checkouts building without the packs.tools/png.ts: full 8-bit PNG decoder (color types 0/2/3/4/6) powering the compositors;tools/build-props.tsgains the atlas + compositor paths.Audio (round 7)
assets/sounds/SOURCES.md), hit confirms, ricochets, player pain/death, wading splashes; listener follows the camera.Tools
tools/f12-shot.ps1:takeScreenshotis broken on Windows and geisterhand's key endpoint ignores F-keys — postingVK_F12to the game window triggers the engine's own screenshot handler for true 4K captures in batch runs.Verification
Batch F12 captures at title + gameplay (all five alien kinds forced into wave 1 via a temporary, reverted hack): posed shadows under player and every alien, tree/building shadows across the terrain, marauder/tyrant/dragoon correctly shaped and scaled. Run against the engine branch in Bloom-Engine/engine#86 — merge that first (or rebuild the engine lib from it) before relinking.
Summary by CodeRabbit
New Features
Documentation
Bug Fixes