Skip to content

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
mainfrom
spike/cod-walking-skeleton
Open

feat(game): harvest the Claude-of-Duty procedural game toolkit (physics · materials · sky · audio · fx · springs · locomotion · packaged @/lib/cod)#591
TortoiseWolfe wants to merge 9 commits into
mainfrom
spike/cod-walking-skeleton

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

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/3d island pattern:

  1. physicsStaticWorld (BVH) + CharacterController (swept-capsule collide-and-slide, no tunnelling)
  2. materials — the floor baked by the procedural PBR MaterialSystem forge
  3. whole-scene PBR — per-surface materials + tiling (brick wall, wood crate, concrete step, dirt floor)
  4. <ProceduralSky> — atmospheric sky + clouds + PMREM IBL env map (no HDRI)
  5. audio — surface-keyed procedural footsteps (Web Audio, useFootsteps)
  6. fx — surface-tinted footstep dust (GPU ParticleLayer, useFootstepDust)
  7. camera feel — head-bob + landing punch (useCameraFeel, CoD springs)
  8. stances & gaits — crouch (C) / sprint (Shift) / prone + army-crawl (X); a low overhang to crouch under
  9. Phase 2a — packaged toolkit — core gems (EventBus + QUALITY_PRESETS/useQuality), a typed public barrel (@/lib/cod + .d.ts for the physics classes), a README, and the gems wired into the demo

Paradigm — 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 under src/lib/cod/ and adapt each to R3F (bake off-screen, tick in useFrame, subscribe in useEffect); the OVERWATCH ctx becomes 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.ts the 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 hooks
  • validate:structure: passes the (updated #538) 5-file gate — none of the new components flagged
  • Playwright on the live route (real WebGL): physics smoke 15/15 on r184; materials/sky/dust render; footsteps fire (117 voices); the stance state machine steps stand→crouch→prone→…; and the wired gems verified — ?q=low renders at ratio 0.72 vs ?q=ultra 1.0, and the stance HUD updates via the event bus across the <Canvas> boundary

What's next — Phase 2b (designed, not in this PR)

docs/prp-docs/cod-game-toolkit-prp.md designs the gauntlet-loop game-demo generator skill that will scaffold a playable demo from a short spec, targeting this @/lib/cod toolkit. Feature brief: features/enhancements/051-cod-game-toolkit/.

Notes for review

  • Large, additive branch: nearly all new files under src/lib/cod/**, src/components/game/{CodSkeleton,ProceduralSky}/, src/app/game/cod-skeleton/, docs/, and features/enhancements/051-*. The only edit to a pre-existing file is one additive section in features/IMPLEMENTATION_ORDER.md. Rebased cleanly onto current main.
  • Constraints honored: static-export (ssr:false dynamic route), Docker-first, 5-file component CI gate (plop-shaped components), canvas-a11y carve-out (FallbackPanel + WebGL probe).
  • MIT attribution: all vendored code retains its LICENSE / per-subdir NOTICE.md.
  • The lone console line Invalid or unexpected token predates this branch (a dev-only artifact) and does not affect rendering.

🤖 Generated with Claude Code

TortoiseWolfe and others added 9 commits August 6, 2026 10:15
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 047 Three.js Game

1 participant