The camera was stuck inside a tractor
329 commits in one day produced a game that mostly did not run, and the browser build we used to test it found the player spawned inside the tractor.

The first tech demo looked finished on paper and was almost empty in play. A browser build with telemetry showed why the camera misbehaved: the player had spawned inside the tractor.
On 24 May I reported that the camera was behaving randomly and that seven shots had all missed. The browser build sent a small telemetry snapshot a few times per second, so we had a trace to read.
The camera had spent 92 percent of the session at its minimum distance, 30 centimetres from the player. The player had spawned at the exact spot where the tractor was.
Why the camera sat at 30 centimetres
The camera hangs on a spring arm. When something sits between the camera and the player, the arm shortens so the view is not blocked. With the player inside the tractor, there was an obstacle from the very first frame, and the arm stayed at its shortest the whole time.
That explains why the camera felt random: it was not following my movement, it was pinned against the obstacle.
The tractor had already caused trouble two days earlier. It was an ordinary rigid body, so every contact pushed the tiny player and shook the camera. We changed it to a sensor, which has no contact response. The game does its own distance check and applies the damage separately, so the tractor no longer solves a collision by throwing the player away.
329 commits and an almost empty game
The tractor ended up on the spawn point because of how the demo was made. The original roadmap allowed ten or eleven weeks for a tech demo. On 19 May, 329 commits landed on the development line in one day.
Different agents took successive slices of the roadmap. Most of the 329 were small issue and review commits, not 329 separate features. By the end of the day the repository held eleven weapons, cartoon gore, fatalities, the picker, tractor and monkey, enemy state machines and behaviour trees, a wave director, a HUD, saves and a day/night cycle.
Then the five reviewers played it. Many systems compiled but were not connected to the app. Nine AI systems had never been registered with Bevy, so the enemies stood still. Their line-of-fire check always returned false, so they could not shoot. Weapon fire rates were in the data files and ignored at runtime.
The review found these faults because it tested behaviour instead of counting finished files. Ninety-seven of the 110 findings were fixed that night.
What changed after that review
A compiling game and a plausible test suite do not prove that a system is registered, that a camera is controllable or that an enemy can land a shot. From then on every playable slice needed a run, a capture, a measurement or a focused test matching the claim being made.
Developing without the final assets
The main agent machine has no discrete GPU and does not run Blender, so gameplay could not wait for finished meshes. The first versions therefore use plain shapes with known sizes: a capsule for the banana, boxes for crates and the tractor, cylinders for trees. That is why they look so bare, and it lets gameplay tests run without a window or any model files.
The later player and Brute models serve the same purpose at a more advanced stage: they let us measure foot contact, weapon sockets, hit zones and animation timing. GattixGGX’s final characters will replace them once those contracts are stable.
The browser build
The first playtest loop ran in a browser: we compiled the game to WebAssembly and uploaded each build for me to try. By the end of May the browser version was hiding more problems than it showed, so native builds became the reference and the web target was later removed from the game repository.
Under the hood: placeholders, headless tests, the tractor sensor and the browser build
Placeholders. The decision is ADR-0016, from 20 May. The PlaceholderAssets resource in sd-level holds a mesh and material handle for each entity role. The banana is a Capsule3d of 0.3 m, a palm trunk a 12 m cylinder, a wooden crate a 1 m cuboid, the picker a 1.8 m cuboid and the tractor a 4x2x2 m cuboid. A system checks whether the real .glb exists on disk. If it does not, it logs a warning and spawns the primitive. Spawn code checks whether the asset handle has loaded and falls back to the placeholder if not.
Headless tests. Because spawn code does not need a render context, tests build an App with MinimalPlugins, add the plugin under test and step the schedule. The workflow note (docs/testing-without-assets.md) also lists LLVMpipe on the agent machine, which is software rendering and only good for checking that the game starts. Since the web target died, visual checks happen on a separate test machine with a GPU.
The tractor. The tractor is RigidBody::Kinematic with a Sensor collider. A sensor generates collision events but no contact forces, so it cannot push the player. The damage check is manual, a distance test inside the tractor system. It stays a valid hurtbox for projectiles through a marker component:
RigidBody::Kinematic,
Collider::cuboid(1.5, 0.5, 1.0),
CollisionLayers::from_bits(hazard_layers.memberships, hazard_layers.filters),
// Sensor: a trigger volume, it does not push the player.
Sensor,
ProjectileHurtbox,
The spring arm. The arm length shrinks when something sits between target and camera. A spawn point inside a collider therefore makes the obstacle permanent, and the arm never gets a frame in which to extend. The trace we read reported the camera distance over time, which is how the 92 percent figure came out.
The browser build. The game was compiled to WebAssembly, uploaded and labelled with a build number. Build 14, made at two in the morning on 21 May, was called “FINAL”. Ten builds later, build 24 was the first one described simply as “working”. The failures were ordinary: balance files were not packaged, so they had to be embedded; WebGL2 could not use the 16-bit terrain textures we had exported; some rendering plugins had to be disabled, and the first attempt to disable them was wrong. WebGL2 also has lower visual limits than the native renderer, and audio needed browser-specific handling. The wasm release profile used lto = "fat" (ADR-0014). That decision and the wasm asset-check rule (ADR-0013) went away with the web target.
Sources
- Avian 3D 0.7 API documentation, the reference for the physics schedule, collision layers and rigid-body behaviour used throughout the project
Next time: a 30 centimetre player breaks the camera and the ground collision, and the level generator loses to an editor.