Alex CoulombePresents
← Tank Commander

Devlog

Building Tank Commander

Made for Ani 🧡 — a one-man turret you sit inside and actually operate: flip the battery master, hold the starter until the engine catches, grab the twin tillers, cycle the breech lever, arm the rocket console behind its safety cover. Every texture, sound, voice line, and piece of geometry — cockpit included — is generated at runtime in pure GDScript. Nothing is imported.

That commitment to building everything from scratch is also where the worst bug of the project's life came from — and it took someone actually inside the headset to catch it, not a single automated check.

01 · Origins

A cockpit first, a game around it second

The design brief was the cockpit, not the battlefield: sit inside a real armored-vehicle crew station and physically operate every control — a battery master switch, a starter you have to hold until the engine catches, twin tillers for the tracks, a turret joystick, a breech lever you cycle by hand to reload, a rocket console behind a flip-up safety cover. The battlefields, enemy waves, and the eventual jeep, plane, biplane, helicopter, and on-foot runner mode all got built around that one physically- operated seat, not the other way around.

Nothing in the game is an imported asset — every mesh comes out of a custom MeshKit builder at runtime, every texture is procedurally generated, every sound and voice line is synthesized. That constraint is also why the biggest bug below was even possible: with no imported reference geometry to compare against, the entire pipeline — generator and validator alike — could quietly agree on the same wrong convention for a long time before anyone noticed.

02 · Feel

Growing past one seat

The tank cockpit went through several real-front-view passes before it actually read correctly in stereo — the turret's exterior armor box never had an opening cut into it at all, so no amount of widening the interior slit could ever have shown terrain through it. The fix cut an actual driver's window through the exterior plate, shrank and repositioned the breech so it stopped intruding on the driver's space (the rear face had been sitting five centimeters from the eye), and moved the rocket pods from directly over the side hatches to a rear mount clear of the sightlines — verified with a seven-angle seated render sweep instead of a single forward-only shot.

A drivable jeep followed, mirror-imaged from the tank's own thumbstick conventions — left stick drives and steers, right stick swings a full-360 rear-mounted gun — with a steering wheel grip, throttle lever, and a red cannon button standing in for the tank's breech ritual at jeep speed. Later still, on-foot mode got real traversal: terrain collision bodies became climbable across every cliff and rim, and the grapple's attach mask widened from a set of world-object layers that had never actually been populated to any solid surface in the game — cliffs, buildings, vehicles, all of it.

Some of that traversal work caught its own bug in the same pass: a new render-check tool built to verify the dressed-up soldier avatars found that the arm-IK solver mixed world-space shoulder positions with avatar-local hand targets, so arms solved toward the map origin instead of the avatar — invisible in an origin-staged test scene, blatant (pole-arms pointing at nothing) for an NPC gunner, a remote co-op player, or your own body in third person.

03 · Postmortems

Bugs found the hard way

One bug here is the whole reason this section exists: it took a human with two eyes in a headset to catch what every automated tool in the project had already agreed was fine.

'I have depth perception' — 70% of the game's normals were invertedrendering

Alex flagged it from inside the headset, against every automated check saying otherwise: roughly 70% of the game's geometry read as inside-out. The reason no tool had caught it is the reason it was bad — mesh_audit.gd, the project's own automated winding validator, had been written with the same counter-clockwise/right-hand-rule convention as the mesh generator it was checking. It didn't just miss the bug — it actively blessed backwards meshes and flagged the one correctly-imported .glb in the project as the broken one.

The empirical proof, once someone went looking: a single right-hand-rule-wound triangle with back-face culling on renders only from behind its own normal. Godot's front faces are clockwise — the OpenGL habit most engines share is backwards here. That one fact meant fixing MeshKit.box() (emitting the wrong order on every box in the game — same silhouette on a flat screenshot, blatant in VR stereo), MeshKit.cyl() (an earlier “fix,” guided by the same wrong audit tool, had flipped cylinders from correct to wrong — restoring the pre-fix order was the actual repair), and MeshKit.prism() (the one shape that was already correctly wound — its stored lighting normals were pointing inward and needed negating instead). A commit marked as a breaking change, a new verification stack (a one-triangle ground-truth render, a per-triangle winding math table, a zero-AI whole-project facing sweep script), and Alex's own eyes as the real acceptance test — not the tool that had been wrong the whole time.

A stopgap shipped first while the real per-shape fixes were still being chased down: a global cull-mode override on every material entering the scene tree, so ~40 flipped-normal sites stopped being visibly broken while the actual winding-order bugs got fixed one at a time behind it.

Every physical cockpit control, unit-tested and never once wired upinput

The battery switch, starter, tillers, breech lever, rocket cover — every physical control in the cockpit was built and unit-tested from day one, and for most of the project's life, none of them actually worked in the headset. The grab/poke proximity code checked membership in a Godot group that, per a full search of the project's own git history, no control had ever actually joined — add_to_group("vrcontrols") was simply never called. One missing line had been quietly gating the hatch lever (and with it, entry into on-foot mode) for the entire life of the feature.

A second, unrelated bug was compounding the same symptom from underneath: Godot's OpenXRInterface silently renames pose actions to its own internal engine names — grip_pose becomes grip, aim_pose becomes aim — and the project's controller nodes had been requesting the pre-rename action names, matching nothing. That one is the real root cause behind an invisible controller-mode hand glove across four separate playtesting rounds, on top of the dead hatch-lever grabs and a frozen on-foot movement bug that all looked, from the outside, like the same single problem.

Aim pose vs. hand tracking: a fight over who owns the menu rayQuest 3S

Quest 3S runs hand tracking concurrently with physical controllers, which the menu-ray code hadn't accounted for — its hand-tracking aim branch kept hijacking the ray even while the player was holding controllers, so the pointer aimed up at the ceiling instead of out at the menu and nothing could be selected. The fix gives controller aim pose absolute priority the instant a controller is actually tracked, rather than letting whichever branch runs second in a frame win by accident.

04 · Multiplayer

A persistent host, and logs that don't need a cable

LAN co-op already existed — one headset drives and mans the machine guns, the other runs the turret, both rendered to each other as fully procedural avatars — but every session needed a local host actually on the network to find. The fix ported Swing City's Cloudflare Worker + Durable Object relay pattern almost directly: no server-side physics or game state, every client trusted to report its own position and effects, the WebSocket Hibernation API so an idle room of open sockets never pins the Durable Object in memory. What's different here is the relay is deliberately generic — instead of hardcoding Tank Commander's own message types into the Worker the way Swing City's joust/ poke/knock protocol is baked into its relay, this one re-broadcasts any message type it doesn't specially recognize, stamped with the sender's id. Round timers, scoring, host-granted map/mode/bot control, seat-switching — all of that can be added client-side without ever touching or redeploying the Worker itself.

The same Worker took on a second, unrelated job: a plain POST /logs endpoint backed by a KV namespace, so crash logs and session data can come in from a headset that isn't even on the same network as the machine debugging it — no adb cable, no requirement that every test headset physically be in the room. Wireless adb over the LAN covers the rest: whichever headsets are on-site get paired once, then pulled from without a cable ever again.

05 · Appendix

Sample document: the overnight routing manifest

After a full playtesting pass in July 2026 — headsets full of crash logs, a page of gameplay notes ranging from “the jeep needs a steering wheel” to “host should feel like a god” — the follow-up list got routed by who should actually build each piece: a Claude tier by difficulty, Gemini for research that shouldn't cost a token on the main run, or the local model fleet for the free, mechanical grunt work. This is the real routing document that came out of that pass, reproduced close to as-generated — an internal task-division artifact, not a feature list, kept here the same way Swing City's equivalent appendix is: a concrete example of what that delegation process actually looks like on a real project, run unattended overnight with a token budget in mind.

Fleet — local, free (Ollama)Gemini — research, no repo writesSonnet — orchestrator + bulk workOpus — hardest reasoning onlyFable — fast content & polish
01Multiplayer join-crash — one player connecting crashes the otherOpus · genuine root-cause dig

Symptom-only reports so far (“in most cases, regardless of vehicle choice, in multiplayer you start with Tank” is a strong hint the peer-spawn path and the crash share a cause). Needs a real dig through the net layer's peer-spawn sequence, not a guess-and-patch pass — staying on Opus.

02Round timer + scoring, host god-mode, shared vehicle seatsOpus · netcode design

Three genuinely interconnected systems on top of the same relay: a Rec-Room-paintball- style round clock with live scoring, a host that can change map/mode/difficulty and spawn bots at any time, and co-op seating flexible enough that one player can run both halves of a vehicle solo, or two players can swap seats on a hotkey. All three ride the same generic relay message convention the Worker already supports — the design work is in the message shapes and host-authority rules, not the transport.

03Persistent-host fallback + wireless log gatheringOpus · deployed live mid-session

Ported from Swing City's relay pattern doc directly — this is the piece described in the Multiplayer section above. Unusually, this one didn't stay purely delegated: the Cloudflare account login and first deploy needed a human in the loop for OAuth, so it got stood up live, smoke-tested end to end (relay fan-out, log upload, token-gated retrieval), and handed to the rest of the run as a working endpoint rather than a task on the list.

04Spider-Man powers gated behind a pickup, not always-onOpus · rework of existing system

The grapple-anywhere and world-climbing traversal already exists and already attaches to cliffs, buildings, and vehicles — see the Feel section above. What's missing is the gate: the ability should only turn on once a specific pickup is found, not be available from the start. Touches the same interaction and inventory code closely enough to want one careful pass rather than a bolt-on flag.

05Bulk playtest punch list — vehicles, weather, UI, onboardingSonnet · ~20 items

Everything well-specified enough to implement directly: consistent enter/exit across every vehicle, right-trigger-forward on all of them, a jeep steering wheel, fixed plane spawn facing, snap/smooth turn and sprint options for on-foot mode, a fog weather state, flowing and occasionally-erupting volcano lava with a matching no-grass palette, an attackable (and eventually killable) baby-room target, clearer avatar silhouettes with per-team coloring, a team mode alongside free-for-all, names floating over every avatar and vehicle, a pause-menu roster and score display, a single-player exemption from the wifi-required gate, broken lobby map previews, and a first-run splash screen.

06Energy drink crash + drink feedback, coffee effectSonnet · lead already found

The energy drink crashing the game has a specific suspect: its action handler plays a sound and reaches into the on-foot body's sprint-boost method in the same call where it frees itself — a same-frame free-while-still-executing-your-own-signal-handler shape. Coffee, meanwhile, has no effect and no feedback at all — not a crash, just dead weight that needs an actual boost and a visible tell that it worked.

07More weapons, drink/coffee FX, cubemap fix, splash artFable · content, not systems

Net-new weapons follow an existing pickable pattern closely enough to hand off directly. The energy drink and coffee need particles, sound, and (for the energy drink) a crushed- can mesh once it's actually consumed. The store page's six-sided cubemap has two wrong faces (ground and sky are fine) that just need regenerating. None of this touches game systems — it's art and juice layered on top of mechanics that already exist or are being built in parallel.

08Research: Quest username API, splash-screen patternsGemini · no repo writes

Whether an in-headset build can actually read local and remote players' Quest usernames is a platform-API feasibility question, not an implementation one — worth answering before any pause-menu roster work assumes it's possible. Splash-screen conventions and a review of the Cloudflare relay integration plan against its own pattern doc are the same shape: research that shouldn't spend a token of the main run's budget.

09Log triage, crash-pattern scan, changelog draftFleet · free, parallel

Every headset log and crash dump collected this run gets parsed into a structured bug ledger by a local model, in parallel with everything else, at zero token cost. A second pass scans every script in the project for the shape of bug that already bit this project once — freed-node access, null dereference, a signal handler freeing the node it's still executing on.

Priority order for the run

Crashes first, then the netcode core, then everything else

  1. 1Crash fixes — the join-crash and the energy-drink crash
  2. 2Multiplayer core — rounds, scoring, host god-mode, shared seats
  3. 3Vehicle and on-foot UX — enter/exit, steering wheel, plane facing, movement options
  4. 4Weather and content — fog, volcano lava, weapons, drink/coffee effects
  5. 5Polish — cubemap fix, splash screen, names and team colors

Run unattended overnight, in batches — build, commit, version-bump per landed feature rather than per edit — with a hard rule to stop cleanly and flag a blocker rather than loop on it. The goal was never to burn the token budget proving a point; it was to have as much of the punch list actually done, and every headset's logs actually collected, by morning.