Devlog — We should've been writing for months, but didn't
This is a catch-up post. We’ve been building this game for a while and haven’t documented much of it, so this is us going back through several months of work and writing it up properly before we forget what any of it actually does.
The game is a fast first-person arena shooter in Godot 4. You traverse procedurally generated corridor modules, fight squads of enemies, take an elevator to the next floor, repeat. The movement system is the spine of it — wall running, grappling, sliding, air dashing, momentum melee. The intent is that playing well means playing fast, and the scoring system enforces that.
Here’s what got built.
Movement
The old movement had a few specific problems that had been sitting there for a while.
The slide was the most broken one. On any upward incline it acted as an absolute brake — you’d hit a ramp mid-slide and lose everything instantly. The fix was replacing the engine’s is_on_floor() check with a ShapeCast3D that reads the actual surface normal. Uphill now adds proportional friction rather than stopping you dead. Steeper angle means more resistance, but it’s a gradient, not a wall. Flat surfaces also got a grace window before friction kicks in, so the transition from a downhill run onto a level section doesn’t immediately kill your speed. The floor snap length is managed dynamically too — it drops to zero when you’re moving upward, otherwise the engine’s own snap system drags you back down slopes you’re trying to climb.
The wallrun was clipping into surfaces and felt sticky. The clip fix is a constant outward push applied every frame — the physics body literally can’t sink into the wall because it’s always being nudged away from it. Stickiness was the normal validation: the surface has to actually be beside you, not in front or at an angle, and the side locks at entry so you can’t flicker between two surfaces mid-run. Vertical movement was also missing entirely. It’s in now — looking up or down while on a wall adds a vertical component proportional to your horizontal speed. You can climb by looking into the wall, or cut downward to build speed before a jump. Wall jump chaining got a ramping multiplier: each consecutive wall jump increases the outward kick and the upward force, up to three times the base values. The camera picks this up too, so jump six in a chain feels noticeably different from jump one.
The grapple was fully rebuilt. The original was pendulum physics — fire the hook, wait, the swing happens to you. Playtesters called it rope-like, which was accurate and not a compliment. The new version is driven by where you look. A force pushes perpendicular to the hook direction based on the gap between your aim and the anchor, so swinging is something you steer rather than something that carries you. On top of that there’s a directional boost that fires for half a second on hook to get you moving immediately — no slack, no jerk. The rope length acts as a soft ceiling enforced by a spring rather than the primary locomotion driver, so you still feel the tether but it’s not running the movement. An arm IK system swaps between left and right arm based on which one is facing the hook point, blending over 0.2 seconds if it crosses center. Both can be active during the blend.
The air dash is also in. One per jump, sprint to activate. Below a speed threshold it’s a flat boost. If you’re already fast, it redirects your speed in the new direction rather than slowing you down. High-speed dashes are course corrections, not resets.
Enemy AI
The old AI was: if in range, shoot. If too far, walk closer. If too close, back up. No squad behaviour, no coordination, no spatial awareness. It worked as a placeholder.
The current system has a proper state machine per enemy (idle, repositioning, attacking, stunned, dead) plus an alert state that fires when the squad gets hit before spotting you. Coordination runs through a token system — a shared autoload called SquadCommand manages a pool of four attack tokens and two suppression tokens. Every enemy that wants to shoot at close-to-medium range has to hold an attack token. Without one it repositions or suppresses instead. This means you never get the whole squad unloading simultaneously, which was the main failure mode of the old approach.
There’s a tactical mode on top of that. SquadCommand tracks time-to-contact — how long before the fastest enemy reaches the player. Below eight seconds that’s urgency mode, and roles collapse: everyone near the player goes to attack regardless of formation. Below three seconds it’s scramble mode and formation is abandoned entirely. Outside of that, composed mode spaces enemies apart by role (flankers push to the sides, cover stays further back) and prevents clustering.
Perception isn’t just range checks. There’s a vision cone at 60 degrees and a hearing system that extends range based on the player’s speed — running loudly makes you audible further away. The hearing radius caps at 120 metres. Predictive aiming leads the player based on velocity with distance-based noise added on top, so it doesn’t feel like an aimbot but it does track movement intelligently. There’s also a punch response state — getting hit causes the enemy to briefly stop shooting and react, which makes melee feel like it has actual impact on the encounter.
Cover selection is scored rather than just nearest-cover. The formula weighs occlusion from the player, distance from a poison zone around the current anchor (to prevent just ducking behind the same wall forever), and a flank bias that pushes enemies toward advantageous angles. High ground has a separate query that looks for elevated surfaces above the player using downward raycasts.
There’s a sniper variant that’s exempt from the token system entirely. It has its own BEAM and RETREAT states, 500m vision, and lives at range. When it loses line of sight it holds for a grace period before repositioning, rather than immediately moving like a standard enemy would.
Level Generation
The old version generated ten rooms in a straight line from handmade modules that were too small and didn’t connect properly. The new system grows a proper dungeon layout using BFS from the elevator exit, and every piece of geometry inside every module is built at runtime from code. There’s no pre-authored mesh anywhere in the level.
The BFS grows outward until it hits the configured room count. The first exit is always forced so there’s always a forward path. Subsequent rooms branch at a configurable probability. After the tree is built there’s a loop injection pass — it finds leaf nodes where a connection back to an earlier room would create a meaningful shortcut, then adds those as extra exits so the dungeon has circuits instead of only dead ends. The depth difference requirement of at least three floors between the two rooms exists to prevent loops that are so short they’re basically just fat junctions with no navigational payoff.
Getting loops to work correctly was probably the most involved thing in the generator. The BFS produces a tree, so every room knows its parent and children but nothing else. Adding a loop means reaching sideways to a room that isn’t a child, and that creates several problems at once.
The first is timing. Room types come from exit count — one exit forward is a straight corridor, lateral exit is a bend, two exits is a junction. If you inject a loop after type assignment, the module placed there won’t have the right opening. So the pass has to run after the full tree exists but before any type is assigned.
The second problem is direction. Every room’s exits are in its own local space. A room entered from the south thinks forward is north. A room entered from the east thinks forward is west. When you try to connect two rooms from different branches, you have to translate the connecting direction into local space for both rooms independently, or you open the wrong wall.
The third problem is slot conflicts. If the neighbour you want to loop into already has a child or an existing loop exit on that wall, you can’t add a second corridor there. The check has to look at both lists before committing.
Room types are assigned purely from exit topology after all of that resolves. Types determine which module pool gets used for that slot. The end elevator is placed at the farthest leaf by Manhattan distance.
The seeding is fully deterministic. Floor seed mixes in a large prime to the run seed multiplied by floor index. Module seed adds grid position multiplied by different large primes. Same run seed always produces the same dungeon on the same floor. Enemies are seeded per module too so density and placement are reproducible.
Module placement runs in two passes with a physics frame gap between them because Jolt Physics needs newly placed colliders to register with the server before any module script raycasts against them. NavMesh bakes after all modules finish. Enemies spawn after that.
The modules themselves are 300 metres long and 60 wide. That’s a deliberate decision — the old modules were too small to actually use wall runs or grapples before hitting a wall. At current movement speeds you need that length.
All module geometry is built at runtime via MultiMesh batching. Each surface type gets its own bucket — floor, walls, obstacles, neon strips, ceiling, frame — and all transforms accumulate before committing in a single flush. The module scripts are @tool, so the same generation code runs in the editor when tweaking parameters and at runtime during a run.
Each floor gets a unique colour palette derived from a golden ratio hue rotation. The palette is passed into every module and also pushed to the VHS post-process shader, so the global colour grade matches the floor’s tone. The neon strips running through every module respond to player health in real time — they lerp from cyan to red as health drops, updating all active modules simultaneously.
There’s a minimap. It rebuilds every frame as a vector drawing. Rooms appear as you visit them. Each room type is drawn with the correct geometry — straight lines for corridors, arc curves for bends, branching lines for junctions. A ghost trail traces the last twenty positions with fading alpha. When health drops below 30% the whole map jitters.
OptiRoute / Overdrive
The movement system was working but passive. You could go fast, but the level didn’t react to it. OptiRoute is the attempt to make the environment respond to how you move through it.
Every module tags its traversal surfaces at generation time — wall-run walls, ramps, ceilings for grappling, jump edges — with an invisible Area3D on a dedicated collision layer. The player’s forward cast queries that layer while above 40 m/s and selects whichever registered surface is most directly in front with clear line of sight. That surface gets a hologram overlay: a box sized to the surface, color-coded by type, with a scanline and noise shader on it. It slides along the surface as you approach so it stays ahead of you. A leader line in the HUD connects to it from the screen edge, labeled with the surface type.
Actually hitting that surface — wall running it, grappling off a tagged ceiling, launching from a tagged edge — triggers an overdrive specific to that action. Wall run overdrive lifts the speed cap from 70 to 120 m/s for two seconds. Slide overdrive boosts speed and refills air dashes. Jump overdrive pushes velocity upward and forward. Grapple overdrive adds speed in your look direction on hook connect, but only if the hook landed close enough to the registered surface center — it won’t fire if you just graze nearby. A shared six-second cooldown prevents stacking multiple triggers back to back.
While any overdrive is running, friction and drag in the relevant movement state collapse to near zero. The states don’t change their logic — they just stop fighting the speed the overdrive gave them.
There’s also a kill route layer. The game tracks which enemy you’re moving toward based on velocity direction. Kill that enemy while on the route and you get a momentum boost plus an overdrive activation. The intent is that charging directly at enemies scores better than playing carefully at range.
When an overdrive fires, the VHS colour grade flashes to electric white-blue in about a frame and recovers over half a second regardless of the floor’s palette. At very high speeds the map geometry shader starts glitching — UV offsets and scan tearing on the module surfaces. The world visually destabilizes at the speeds the system is designed to push you toward.
Combat
The old version had one punch. It was basically what the air lunge is now — a big hit at the end of a charge. Everything else is new.
There are four punch variants distinguished by state and charge time. Jab is under 0.15 seconds — quick hit, small boost, you stay mobile. Heavy charge up to a second gives a ground sweep that knocks enemies into a pinball trajectory or a slide uppercut that launches you upward off the target. Air lunge during a long charge in the air drives you into the target at 30 to 80 m/s depending on how long you held it.
Damage is momentum-based: 20 plus relative impact speed times 2.5. A fast enemy or a fast player makes each hit harder. The hitbox scales with charge and speed from 1x to 4x, so a fully charged air lunge at speed has a large contact window.
Hit-stop fires on every clean hit — time scale drops to 0.25 for 0.15 seconds. At 100+ relative speed it cuts off early (rip-through) so fast kills don’t feel like they’re snagging. On overkill (180+ damage) the game fully freezes: Engine.time_scale to zero, the enemy’s emissive material fires white, a flash light pulses, and the camera triggers an overkill shake with a tween that ignores time scale so it still lands while everything is stopped.
The pinball mode on ground sweep sends enemies bouncing off walls up to six times at 90% speed retention per bounce. Damage is three times the base formula per bounce. Between bounces the trajectory steers 80% toward the nearest living enemy within 30 metres — the body finds its own targets. Hitting a floor flattens all the bones 90 degrees via direct physics server transforms (the pancake), which then pops back on the next wall hit.
Ragdoll replaced the old approach of just disabling enemy collision on death. The new system uses full skeleton physics with per-bone damping — arms are stiff, torso is loose. A sled phase runs immediately after death: the bones are driven to match the agent’s velocity via direct physics server state writes before releasing to free simulation. That means an enemy killed mid-sprint keeps travelling at speed instead of just crumpling in place where it was standing.
Scoring
Runtime score updates live on a 7-segment display on the player’s left wrist. It’s a SubViewport rendered onto a 3D mesh on the arm, drawn entirely with _draw() calls — no Godot UI controls. Inactive segments show as dim ghosts at 7% alpha so it reads like an actual hardware display. Score sources: +500 per overdrive activation, plus kill score based on impact velocity.
At the end of each floor, a screen on the exit elevator shows a full breakdown before the transition. Lines print one at a time with a typewriter delay. The score formula multiplies the raw runtime total by average movement speed, clearance pace against a par time, and a depth bonus that scales with how far into the run you are. Time spent standing on the ground cuts into the multiplier. Playing fast and aggressive scores exponentially better than playing slow and careful.
UI
The previous version had no main menu and a pause screen that didn’t work.
The main menu has an animated background and some decorative terminal text. The actual work is the run configuration panel: seed, room count, enemy density, enemies per module, branching probability. Same seed always produces the same dungeon.
The pause menu is diegetic. Pressing Escape slows the game to a stop — time_scale ramps down with a cubic ease, the camera greyscales, and the player’s left arm raises into view via IK. A holographic arc of menu items appears above the wrist in world space. The hand’s IK target follows whichever item is hovered, so the finger points at the selection. Closing runs the sequence in reverse.
If the player is mid-punch when Escape is pressed, the menu waits up to 0.8 seconds for the punch to finish before opening.
Camera & Feel
Camera had gotten feedback that it was nauseating so it got a full pass. The core change was switching from blunt position offsets to a trauma system — trauma accumulates and decays, shake magnitude is trauma squared. Small bumps barely register, a full overkill hit actually rattles the screen. The noise switched from random per-frame to smooth simplex noise so it feels like physical motion rather than static. Different events contribute different amounts: overkill hits hardest at 1.0, punches at 0.6, wall jumps scale with chain count.
FOV has multiple independent sources layered on top. Speed pushes it up continuously up to +40. Every action — punching, dashing, wall jumping, grappling — fires a one-shot impulse that decays independently. Wall jump FOV uses two easing curves back to back: a fast cubic push when you leave the wall, then a long exponential tail over a second and a half. It makes the wall jump feel like it had weight.
There’s a persistent VHS post-process layer — chromatic aberration, vignette, film grain, scanlines, colour grade. On top of that a speed-reactive red grain that fades in above 60 m/s. Wind particles stream past the camera above 30 m/s and scale up from there.
The adaptive music system was built from scratch. Tracks are split into three stems — bass, drums, synth — and all three players start in the same frame to stay in phase. An audio momentum value tracks speed but falls off very slowly, so brief slowdowns don’t immediately strip the music back. At rest only the bass plays, heavily low-pass filtered. Above 20 m/s the drums come in. Above 40 the synth. Hard landings briefly muffle the whole music bus. The bass gets an extra filter pass while airborne.
That’s most of it. The things still missing are a proper respawn system, tutorial, gamepad support, settings persistence, and more SFX detail. The OR cooldown has no visual feedback yet — you just have to know it’s running.
The bigger additions planned are roguelike progression and boss fights. Two layers of perks: permanent unlocks that carry between runs, and temporary perks picked up during a run that expire when you die. Boss encounters are on the list — the current loop of corridor modules into elevator doesn’t have any punctuation to it. New enemy types are coming. Level loading also needs a pass on larger room counts.
And we have some other ideas. Ones that would require more than one person to really appreciate. We’ll leave it at that for now.
By the speed junkies, for the speed junkies.