No description
  • Rust 92.8%
  • JavaScript 6%
  • WGSL 1.2%
Find a file
HunterGooD 63513aae10 Let the player see which weapon a bot picked up
Escalation changed what the opposition fires and nothing about what it looks
like: a bot that had picked up a shotgun closed to four metres and took half a
health bar off while visibly holding the same rifle as everybody else. The
difficulty went up for reasons the player could only work out by dying to
them, which is the one way a shooter must never explain itself.

Seven tagged parts on the drawn soldier instead of four, three of them hidden
unless the kit calls for them — a scope on the marksman rifle, a belt box
under the machine gun, a magazine tube under the shotgun — and the shared four
scale per kit. `ai::weapon_shape` is the table; `dress_weapon` applies it when
`soldier::DrawnKit` disagrees with `Enemy::kit`, which is once per body per
weapon change rather than per frame. `Changed<Enemy>` cannot do that job:
`think` writes the component every fixed step, so everything is always
changed.

Transforms, not geometry. A corpse is recycled every five seconds and a
rebuilt weapon would be four despawns and four spawns each time, for a
silhouette that is four boxes at the range these fights happen at.

The model viewer now shows all four, one per exhibit, which is what makes this
inspectable at all: `--model` staged four poses of the same rifle before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 21:35:40 +04:00
docs/multiplayer Sixth rejection: making health authoritative let a corpse keep shooting 2026-08-02 23:36:16 +04:00
meta MP-O1: a meta service that the game cannot accidentally depend on 2026-08-02 02:48:29 +04:00
src Let the player see which weapon a bot picked up 2026-08-13 21:35:40 +04:00
tools Claude of Duty 2026-07-25 15:32:09 -04:00
.gitignore Plan the multiplayer port, and write down how to work on this repo 2026-08-01 22:30:49 +04:00
ARCHITECTURE.md Let the player see which weapon a bot picked up 2026-08-13 21:35:40 +04:00
Cargo.lock Measure the frame before optimising it, and find a knob wired to nothing 2026-08-13 13:57:51 +04:00
Cargo.toml Measure the frame before optimising it, and find a knob wired to nothing 2026-08-13 13:57:51 +04:00
CLAUDE.md Ask what the player is carrying, and let the enemy answer back 2026-08-13 21:27:02 +04:00
index.html Claude of Duty 2026-07-25 15:32:09 -04:00
LICENSE Claude of Duty 2026-07-25 15:32:09 -04:00
package-lock.json Claude of Duty 2026-07-25 15:32:09 -04:00
package.json Claude of Duty 2026-07-25 15:32:09 -04:00
prompt.md Claude of Duty 2026-07-25 15:32:09 -04:00
README.md Ask what the player is carrying, and let the enemy answer back 2026-08-13 21:27:02 +04:00
vite.config.js Claude of Duty 2026-07-25 15:32:09 -04:00

Claude of Duty

A first-person shooter in Rust on Bevy 0.19. Roughly 28k lines across twelve subsystems.

Originally written in Three.js and WebGL2 by a fleet of AI agents; this tree is the port of that game to Bevy, also written by agents. 107 of the 109 JavaScript files are gone. The two that remain are the two features this engine cannot currently carry, kept as the spec for whoever fixes that; ARCHITECTURE.md names them and says why.

There are no art assets. Every texture, mesh, animation and sound is generated from code at boot. No models, no HDRIs, no image files, no audio files. The only dependencies are bevy and bevy_rapier3d.

cargo run --release
move WASD
aim / fire mouse, LMB
aim down sights RMB
sprint / crouch / prone Shift / Ctrl or C / Z
jump Space
lean Q / E
reload R
grenade G
swap weapon 1 / 2 / Tab
scope magnification mouse wheel, or click the wheel (4x / 8x, marksman kit)
melee / use / torch V / F / T
pause Esc
performance panel F3 (full → off → one line)
graphics settings Esc — the panel on the right of the pause menu
change kit Esc — LOADOUT

Graphics settings apply live, including while paused: the frame limit (vsync, 60/120/144, or unlimited), shadows on/off, shadow map size, shadow distance, cascade count, shadow filter, render scale, and each post effect. Every one of them is also a flag, so a measured comparison is a second run rather than a session of clicking:

cargo run --release -- --quality=ultra --shadows=off
cargo run --release -- --shadow-map=2048 --cascades=2 --fps=120
cargo run --release -- --window=1280x720   # logical; a 2x display doubles it

Two flags exist for testing the settings screen rather than playing: --cycle-quality=SECONDS changes one setting at a time while the game runs, and --cycle-exhaustive makes it walk all 256 on/off combinations in Gray-code order — one row per step, which is the shape of change that crashed the renderer twice. --cycle-seed=N picks a different walk.

Other flags: --quality=low|medium|high|ultra; --capture, which fixes every random stream and disables the texture cache so two runs produce identical frames; and --no-metrics / --metrics-log=PATH for the profiling log, which is otherwise written to metric.log beside the executable. Any unknown flag prints the whole list.

What's in it

subsystem what it does
core Fixed-step schedule, the RNG (bit-identical to the JS stream), input, config and the quality presets
render HDR forward pipeline: cascaded shadows, GTAO, TAA, motion blur, bloom, contact shadows, volumetrics, auto-exposure and aim-driven depth of field, all gated by preset
materials 19 procedural surfaces baked on the CPU at boot and cached on disk, plus a WGSL extension for the runtime half — triplanar projection, curvature-driven edge wear
sky Bevy's physical atmosphere, a sun on a day arc, a drifting cloud deck and a star layer that fades in after sunset
world A 92 × 140 m market street: modular buildings with real wall thickness, glazed and shuttered window openings, four enterable furnished rooms, a compound wall, barricades, wrecks, stalls, palms and cables across the street
physics bevy_rapier3d, stepped inside the game's own 120 Hz loop: level colliders, ray casts, the kinematic character controller, dynamic grenades and articulated ragdolls — plus an analytic height field for the sand, which has no collider
player Movement state machine, stances, slide, mantle and vault, validated lean, health with regeneration, and the camera rig
weapons Eight weapons in four kits — carbine, marksman rifle with a 4-8x scope, shotgun that fires eight pellets, machine gun, and four sidearms — each with its own recoil pattern and pose, plus fire modes, swapping, stance-scaled spread, damage falloff, phased reloads, and grenades that cook and bounce
ai Perception, state machine, posting to cover, burst fire from one of four kits that the player's own kill count unlocks, flinch, grenade avoidance, grid pathfinding, Verlet ragdolls, and a director that caps how many enemies fight at once
fx Muzzle flash, tracers, brass, impact debris, bullet-hole decals, dust and smoke, and always-on atmosphere
ui Crosshair tracking the real cone, ammo, hitmarkers, health, damage arcs, killfeed, compass, minimap, objective marker, a pause menu that stops the simulation, and the live graphics settings
audio Synthesised at boot to PCM — weapons, impacts, reloads, footsteps, explosions — played spatially off the camera
boot A progress bar while the textures bake — 1.8 s cold on low, 6.2 s on ultra, and one frozen black frame before it existed
metrics The frame split into phases, the counters that explain it, a panel under the minimap and a JSONL profile at metric.log

ARCHITECTURE.md is the contract: subsystem interfaces, directory ownership, the cross-subsystem event vocabulary, the hard rules, and the quality bar.

Running it

cargo run --release              # the game
cargo run --release -- --model   # the enemy model viewer
cargo test                       # 366 tests

--model puts six soldiers in a row — standing, aiming, crouching, walking, reloading, dying — on a floor with scale marks, under an orbiting camera, driven by the real animator. It exists because the level is a street canyon fought at fifteen to twenty-five metres in its own shadow, and a 180-pixel silhouette in a firefight is not a way to find out whether a boot is on the road.

Measured on an Apple M5, ultra:

boot, warm 0.40 s
boot, cold texture cache 5.61 s
level build 265k triangles, 3165 collision boxes, 0.16 s
navigation grid 126 × 190 cells, 26 % blocked, 31 µs

Frame rate has not been profiled. The JS build had a whole harness for it and none of it survived the port; there is no equivalent yet and no number here worth quoting.

What the port changed, and why

Not line-for-line. Where Bevy already had the thing, the port is the wiring. Where the JavaScript solved a problem this engine does not have, it was rebuilt from the level's own data rather than translated:

  • The minimap is drawn from the layout table the buildings are generated from — crisp rectangles, no bake — instead of an orthographic depth render read back into a bitmap.
  • Navigation is a grid over the collision list, baked in 31 µs, instead of a navmesh decomposed into convex polygons. Every obstacle here is already an axis-aligned box.
  • The enemy is a 23-bone rigid-part figure rather than a skinned mesh. Joints crease instead of deforming; at this range that reads the same and costs a tenth of the code. If skinning ever lands, the bone table does not change.
  • Weapon viewmodels are chamfered blockouts rather than lofted geometry.

Two things are deliberately absent, and ARCHITECTURE.md says so where they would have gone: heat shimmer needs a refraction pass this forward pipeline cannot give it, and a use-prompt has nothing to prompt for because nothing in the level can be used.

How it was built

The method that produced everything above: measure, fix, measure again. Not inspection, not screenshots — resolve the world coordinates and read the number.

The enemy went through seven rounds of it. What that found, none of which was visible in the code and some of which the test suite was actively hiding:

  • The gait's stance and swing halves were swapped, so the planted boot crossed the road at 3.5× body speed.
  • Cadence was 5.64 footfalls a second — a walk's amplitude driven at a sprint's rate. Fixing it meant inverting the leg's own geometry so the foot travels at constant speed rather than the thigh angle.
  • Crouch-walking moved the feet 0.168 m while the body moved 0.550. Enemies now stand up to reposition.
  • Sixty-nine millimetres of a crouched soldier's head hung outside its own hitbox, and 28 % of the rounds into its visible chest scored limb.

Contact slip is now 0.7 mm per step against a 1.06 m stride, and nothing on a corpse goes below the road at any point in its fall.

Three times the test was the thing that was wrong, and each one now says so where it sits. One asserted twice the correct foot travel while never moving the body, so nothing in it could see sliding at all. One reimplemented the posing maths in its helper, dropped the lean, and stayed green while every walking soldier drove its sole 51 mm through the asphalt. One measured triangles by an index count that the shape library never fills in.

src/wiring.rs exists because three features shipped doing nothing while every module test passed — the seams between modules are what a module test cannot see.

Honest assessment

The goal was to match a modern Call of Duty. It does not, and the gap is mostly where it was before the port: procedural surfaces read as noise rather than as photographed reality at close range, enemies read as well-proportioned mannequins, and indirect light is a uniform ambient term rather than GI.

Known and unfixed, with numbers:

  • The pelvis rises and falls 167 mm over a gait cycle against a human's 60–120. A rigid leg with no toe roll cannot do better; the fix is costed and measured (plantarflexion at 0.80 rad brings it to 96 mm) and not applied.
  • A crouched soldier's rifle points 0.37 m low at fifteen metres. The rig never elevates to the target.
  • The head hitbox is narrower than the helmet, so 3.7 % of visible-head rays hit nothing.
  • SSR is disabled. Bevy's needs a deferred prepass and this pipeline is forward.
  • No generated IBL, so there is no specular ambient at all.