feat(game): harvest the Claude-of-Duty procedural game toolkit (physics · materials · sky · audio · fx · springs · locomotion · packaged @/lib/cod) - #591
Open
TortoiseWolfe wants to merge 9 commits into
Conversation
Harvest the framework-agnostic procedural physics from the MIT Claude-of-Duty (github.com/mshumer/Claude-of-Duty) into ScriptHammer and drive it from React Three Fiber — the first slice of the "harvest, not embed" extraction (epic #576). Vendored under src/lib/cod/ (MIT LICENSE + NOTICE retained): - math.js / surfaces.js — scalar/geometry kernel + surface vocabulary - bvh.js — StaticWorld: binned-SAH BVH over triangle soup + swept-capsule and overlap queries - character.js — swept-capsule collide-and-slide character controller - springs.js — camera-feel / recoil springs (for a later slice) Integration (harvest, not embed): R3F owns the <Canvas> render loop; the controller runs a fixed 120 Hz tick inside useFrame and writes onto the camera. New /game/cod-skeleton route (dynamic ssr:false + Loader, mirrors the closed #48 /game/3d island). First-person WASD + pointer-lock mouselook, collide-and-slide over a small procedural level (floor, 0.40 m step-up, wall, crate) skinned with zero-asset procedural DataTextures. WebGL probe -> FallbackPanel seam mirrors Scene (FR-008). Proves in one slice: r180 -> r184 port, static-export ssr:false, framework-agnostic extraction, and the physics ⭐ piece. Verification (all run outside the app container): - scripts/cod-physics-smoke.mjs: 15/15 on three@0.184 (drop-settle, walk, 0.40 m step-up, 300 m/s no-tunnel) - tsc --noEmit: 0 errors in the new files - vitest: 7/7 (5 unit + 2 a11y, axe clean) - validate:structure: 113/113 (CodSkeleton is the 5-file +1) Pending (need a browser/container): Playwright real-GL + static build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second extraction slice on top of the physics walking skeleton: replace
the floor's placeholder DataTexture with a real Claude-of-Duty procedural
PBR surface (the ⭐ "single most valuable extract" in the map).
Vendored the minimal 10-file forge under src/lib/cod/materials/ (MIT
NOTICE retained): index/generator/library/shader/masks.js +
glsl/{noise,surfaces-arch,-ground,-metal,-organic}.js. `three`-only, no
core/. The OVERWATCH ctx is fully bypassed by `new MaterialSystem({
renderer })` + `init({})` — same harvest-not-embed seam as physics.
Integration (CodSkeleton.tsx): the FLOOR mesh bakes a `dirt` surface via
`getTextureSet` (albedo/normal/ORM rendered on the GPU at load, zero
assets), off-screen in useMemo with the renderer's target + autoClear
saved/restored so no R3F frame is corrupted; the forge is disposed on
unmount (it owns the render targets). Uses the "plain" MeshStandardMaterial
path (map/normalMap/roughnessMap+metalnessMap=ORM) — no onBeforeCompile,
so no r184 shader-chunk risk. Smaller boxes keep the DataTexture stand-in.
Guarded on `gl`, so the mocked-Canvas unit test falls back to the stand-in.
Proves the materials ⭐ extract ports r180 -> r184: the forge bakes and
renders a correctly-lit cracked-dirt floor under real WebGL.
Verification:
- tsc --noEmit: 0 errors in the new files
- vitest (in-container): 7/7 (5 unit + 2 a11y)
- Playwright against the live container: forge bakes (ReadPixels passes
run), floor renders as real PBR dirt on r184, no bake exceptions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the CoD materials forge from the floor to every mesh. Add an optional `material` id to BoxSpec (distinct per box so the forge's per-id cache gives each mesh its own texture set + independent tiling): floor=dirt, step=concrete, wall=brick, crate=wood. Physics `surface` tags unchanged (brick→concrete via guessSurface, collision unaffected). The materials useMemo now bakes all boxes via the existing MaterialSystem (same save/restore-getRenderTarget/autoClear off-screen bake), with per-box `tileRepeat()` (~2 m tiles from the two largest dims). DataTexture stand-in remains the per-box fallback when gl is absent or a bake throws. Verification (Docker-native, in-container): - tsc --noEmit: 0 errors in the file - vitest: 7/7 (mocked path still falls back — no gl) - Playwright: wall renders as brick, crate as wood, step as concrete, floor as dirt — whole scene is real procedural PBR, no bake exceptions Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Third extraction slice: harvest Claude-of-Duty's procedural sky into R3F so the scene gets a real sky AND the PBR materials get image-based lighting (scene.environment) for real specular — no HDRI, zero assets. Vendored the minimal 8-file sky set under src/lib/cod/sky/ (MIT NOTICE): fullscreen/atmosphere/noise/stars/clouds/luts/celestial/dome.js — three-only, raw GLSL3 (no shader chunks / onBeforeCompile → low r184 risk). volumetrics.js dropped (post-chain coupled); index.js NOT vendored — its OVERWATCH ctx wiring is replaced by driver.js (faithful ports of buildSharedUniforms + a trimmed updateCelestial, lights/exposure/events/fog stripped). New 5-file component src/components/game/ProceduralSky/: child of <Canvas>, guarded on gl+scene. At mount, in dependency order (shared uniforms → SkyLuts adds LUT textures → celestial solve → bakeStatic/bakeSkyView → equirect blit → PMREM.fromEquirectangular), it sets scene.environment (IBL) and adds the sky-dome mesh (renderOrder −10000, self-tracks the camera). Render target saved/restored; every RT/material/PMREM disposed on unmount. Wired into CodSkeleton (replaces the flat <color>/<fog>). Proves the sky ⭐ Tier-2 extract ports r180 → r184: atmosphere + procedural clouds render, and the env map lights the materials — no fallback needed. Verification (Docker-native, in-container): - tsc --noEmit: 0 errors in new files - vitest: 10/10 (CodSkeleton + ProceduralSky unit + a11y) - validate:structure: 114/114 (ProceduralSky is the +1) - Playwright: procedural sky + clouds render on r184, no bake exceptions Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fourth extraction slice: harvest Claude-of-Duty's procedural audio so the
character makes footstep sounds keyed to whatever surface it's standing on —
pure Web Audio, zero assets, no r184 involvement (three-free).
Vendored the minimal 3-file set under src/lib/cod/audio/ (MIT NOTICE):
rng.js (seedable PRNG, from src/core/rng.js), dsp.js (NoiseBank + synth
primitives), foley.js (footstep() + STEP table, imports only ./dsp.js). No
mixer/spatial/ir/ambience/index. foley's STEP surface keys are byte-identical
to the 12 physics SURFACE_NAMES, so CharacterController.groundSurfaceName
passes straight through (fallback: concrete) — no mapping.
New hook src/lib/cod/audio/useFootsteps.ts (+ test): owns the AudioContext +
master gain + NoiseBank (built once), returns { resume, step }. `resume()`
rides the existing pointer-lock CLICK to satisfy the browser autoplay policy
(context starts suspended). `step(distance, grounded, surface)` is driven
imperatively from the controller's move loop — every ~2.2 m it fires one
footstep() voice and prunes finished voices in-frame (no per-frame alloc, no
timers). SSR/jsdom-safe (no AudioContext until a gesture); context closed on
unmount. Wired into FirstPersonWorld: the movement useFrame now accumulates
move()'s returned distance and calls step() with cc.groundSurfaceName.
This sidesteps the bug the workflow's adversarial reviewer caught (the naive
recipe read a non-existent c.lastMoveDistance → zero footsteps): the movement
loop captures move()'s return directly.
Verification:
- tsc --noEmit: 0 errors in new files
- vitest (in-container): 12/12 (incl. the hook's jsdom no-op contract)
- validate:structure: 114/114 (audio is a lib hook, not a component)
- Playwright on the live container: trusted click resumes the context
(state 'running', 44.1 kHz), walking fires footstep voices
(117 buffer-source voices over one walk); clean load has no audio errors
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fifth extraction slice: harvest Claude-of-Duty's GPU particle system so each
footstep kicks up a surface-tinted dust puff at the feet — the visible payoff
that pairs with the footstep audio (same cadence).
Vendored ONE file, src/lib/cod/fx/particles.js (MIT NOTICE): ParticleLayer —
a deterministic GPU particle system (instanced quads, per-particle sim in the
vertex shader from a uTime clock, one instanced draw). three-only, ctx-free,
renderer-free, and r184-SAFE: standalone THREE.ShaderMaterial (GLSL3), NO
onBeforeCompile / no shader-chunk patching. The sprite atlas (947 LOC) is not
needed — a hand-rolled 64² round-alpha DataTexture works.
New hook src/lib/cod/fx/useFootstepDust.ts (+ test): used inside <Canvas>,
returns { emit, tick } (imperatively driven, like useFootsteps). useMemo builds
the ParticleLayer + sprite (guarded on gl → no-op in SSR/jsdom), useEffect adds
layer.mesh to the scene + disposes on unmount. emit(x,y,z,surface) fires ~8
particles in an upward radial cone (gravity/drag/sizeCurve mirror impacts.js
dust) with a static per-surface TINT for the 12 physics SURFACE_NAMES; tick(dt)
advances now + flush(). Reuses the SP spawn singleton (no per-frame alloc).
useFootsteps.step() now returns a boolean (did a footstep fire), so ONE cadence
drives both sound and dust. FirstPersonWorld's move loop: didStep = stepAudio();
if (didStep) emitDust(feet, groundSurfaceName); tickDust(delta).
Verification:
- tsc --noEmit: 0 errors in new/edited files
- vitest (in-container): 14/14 (incl. the dust hook's jsdom no-op contract)
- validate:structure: 114/114 (fx is a lib hook, not a component)
- Playwright on the live container: instanced draws climb 0 → 21 with up to
104 live particles while walking (dust emits + renders on the GPU); no
particle shader-compile error on r184
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sixth extraction slice: put the already-vendored CoD springs (src/lib/cod/
springs.js, unused until now) to work, adding tactile weight to the
first-person camera.
New hook src/lib/cod/player/useCameraFeel.ts (+ test): returns { apply },
driven imperatively from the movement loop (like the audio/dust hooks). Uses
a damped Spring for a LANDING PUNCH (instant dip scaled by the controller's
landingSpeed on the airborne→grounded frame, then springs back with a slight
under-damped overshoot) and a distance-keyed HEAD-BOB (sinusoid synced to the
footstep cadence, eased in/out with movement so a stopped camera has no static
offset, plus a subtle lateral sway along the camera's right vector). Pure math
on the camera transform — no GPU, no DOM — so it's SSR-safe AND runs for real
in jsdom.
FirstPersonWorld applies it after the base camera transform each frame:
applyCameraFeel(camera, cc, moved, delta, yaw). No new vendoring.
Verification:
- tsc --noEmit: 0 errors in new/edited files
- vitest (in-container): 17/17 — incl. 3 REAL behavior tests (not no-op
contracts): head-bob oscillates camera.y above+below base while walking,
settles to ~0 when stopped, and a landing transition dips y below base then
recovers
- validate:structure: 114/114 (player is a lib hook, not a component)
- Playwright on the live container: walk + jump + land runs with no runtime
errors (regression); motion is felt, not screenshotted
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seventh slice: a compact locomotion state machine on top of the harvested primitives — no new vendoring, it just wires them together. - C toggles stand↔crouch, X toggles prone↔stand, Shift = sprint (held, only when standing + moving forward). Edge-triggered off keydown (ignores repeat). - Stances change the physics capsule via cc.setHeight(); a raise blocked by a low ceiling (canFit → setHeight returns false) leaves you in the lower stance. - Per-stance config drives speed (stand 4.5 / crouch 2.2 / prone 1.1 army-crawl / sprint 7.0), footstep gait (walk/crouch/sprint → foley), head-bob scale (sprint 1.4× … prone 0.2×), and dust intensity. The camera eye-height glides to the stance target (1.55/0.85/0.35). Jump only from standing. - Added a low METAL OVERHANG beam to the level (underside ~1.05 m) — you must crouch/prone to pass; standing up under it is blocked by canFit. - HUD: a live stance badge (STAND/CROUCH/PRONE) lifted to the wrapper via an onStance callback, and an updated controls hint. Backward-compatible param additions (defaults preserve old behavior): useFootsteps.step(..., gait) + STRIDE-by-gait; useCameraFeel.apply(..., bobScale); useFootstepDust.emit(..., intensity). Verification (in-container): - tsc --noEmit: 0 errors in new/edited files - vitest: 18/18 (incl. a new bobScale behavior assertion) - validate:structure: 114/114 - Playwright on the live container: the stance badge steps stand→crouch→prone→stand→crouch→stand under C/X (state machine proven), scene renders in-stance with the overhang beam, no runtime errors Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ic API + docs)
Turn the harvested Claude-of-Duty primitives into a clean, typed, documented
reusable game toolkit — the substrate the gauntlet-loop generator (Phase 2b)
will target. Foundation-first, done the repo's way.
Core gems (the last unharvested bit), ported to typed TS under src/lib/cod/core/:
- event-bus.ts — EventBus<Events> (from CoD registry.js:86-122): on()→unsubscribe,
once/off/emit/clear; synchronous, error-isolated, insertion-order. + a shared
`bus` singleton + a typed GameEvents map.
- quality.ts — QUALITY_PRESETS (from config.js:21; renderer-generic fields only,
CoD post-chain flags dropped) + a useSyncExternalStore useQuality() honoring ?q=.
Public API:
- src/lib/cod/index.ts — the barrel (@/lib/cod): hooks + gems + physics classes +
materials/particles/sky/springs + surface vocab.
- character.d.ts + bvh.d.ts — hand-written types for the two classes the app/
generator drive directly (CharacterController, StaticWorld); the app conforms.
- README.md — the toolkit's public API + "harvest, not embed" paradigm + MIT NOTICEs.
Gems wired into the demo (real, not dead code):
- useQuality → Canvas dpr (renderScale), texture anisotropy, dust particle pool
(particleBudget); a HUD <select> + ?q= param. Proven: ?q=low renders at ratio
0.72, ?q=ultra at 1.0.
- bus carries player:stance from inside the <Canvas> to the outer HUD badge
(replacing the onStance prop-drill). Proven: stand→C→crouch via the bus.
- useFootstepDust gained a `capacity` param; CodSkeleton imports via the barrel.
Docs (the repo's way):
- docs/prp-docs/cod-game-toolkit-prp.md — the toolkit PRP AND the full design of the
Phase 2b gauntlet-loop `game-demo` generator skill (fan-out builders + blind
critics → plop-scaffolded ssr:false demo, loop-until-wowed; static-export/Docker/
5-file/canvas-a11y constraints).
- features/enhancements/051-cod-game-toolkit/{feature,research,quickstart}.md +
IMPLEMENTATION_ORDER.md entry.
Verification (Docker-native, in-container): tsc 0 errors; vitest 30/30 (12 new gem
behavior tests + updated dpr assertion); validate:structure 114/114; Playwright:
quality tiers + event bus proven live, console clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Harvests Matt Shumer's MIT Claude-of-Duty procedural primitives into a reusable, asset-free React-Three-Fiber game toolkit (
@/lib/cod), and ships a full first-person reference demo at/game/cod-skeleton. 100% client-side procedural Three.js (r184) — zero art/audio assets, zero Supabase.Nine commits, each an independently-verified slice, built in a git worktree off the closed #48
/game/3disland pattern:StaticWorld(BVH) +CharacterController(swept-capsule collide-and-slide, no tunnelling)MaterialSystemforge<ProceduralSky>— atmospheric sky + clouds + PMREM IBL env map (no HDRI)useFootsteps)ParticleLayer,useFootstepDust)useCameraFeel, CoD springs)EventBus+QUALITY_PRESETS/useQuality), a typed public barrel (@/lib/cod+.d.tsfor the physics classes), a README, and the gems wired into the demoParadigm — harvest, not embed
CoD runs its own imperative loop + service-locator kernel; R3F owns the
<Canvas>renderer + loop. We vendor only the framework-agnostic primitives undersrc/lib/cod/and adapt each to R3F (bake off-screen, tick inuseFrame, subscribe inuseEffect); the OVERWATCHctxbecomes plain modules (event bus, quality store, injected renderers). The kernel (engine/registry/prewarm/main) is never vendored.Verification (per slice, Docker-native)
tsc --noEmit: 0 errors in the new files (incl. hand-written.d.tsthe app conforms to)vitest: 30/30 — real behavior tests for the physics port,EventBus, quality store, and camera-feel springs; jsdom no-op contracts for the GL/audio hooksvalidate:structure: passes the (updated#538) 5-file gate — none of the new components flaggedstand→crouch→prone→…; and the wired gems verified —?q=lowrenders at ratio 0.72 vs?q=ultra1.0, and the stance HUD updates via the event bus across the<Canvas>boundaryWhat's next — Phase 2b (designed, not in this PR)
docs/prp-docs/cod-game-toolkit-prp.mddesigns the gauntlet-loopgame-demogenerator skill that will scaffold a playable demo from a short spec, targeting this@/lib/codtoolkit. Feature brief:features/enhancements/051-cod-game-toolkit/.Notes for review
src/lib/cod/**,src/components/game/{CodSkeleton,ProceduralSky}/,src/app/game/cod-skeleton/,docs/, andfeatures/enhancements/051-*. The only edit to a pre-existing file is one additive section infeatures/IMPLEMENTATION_ORDER.md. Rebased cleanly onto currentmain.ssr:falsedynamic route), Docker-first, 5-file component CI gate (plop-shaped components), canvas-a11y carve-out (FallbackPanel+ WebGL probe).LICENSE/ per-subdirNOTICE.md.Invalid or unexpected tokenpredates this branch (a dev-only artifact) and does not affect rendering.🤖 Generated with Claude Code