- Rust 92.8%
- JavaScript 6%
- WGSL 1.2%
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> |
||
|---|---|---|
| docs/multiplayer | ||
| meta | ||
| src | ||
| tools | ||
| .gitignore | ||
| ARCHITECTURE.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| index.html | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| prompt.md | ||
| README.md | ||
| vite.config.js | ||
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.