DungeonCrawl: Action-Roguelike + RTS Hybrid
87,000-line Godot 4.6 engine and game ("DungeonCrawl"). Data-oriented horde combat with a 2,700-line Rust GDExtension that mutates SoA arrays in place using rayon + wide SIMD, chunked streaming overworld, procedurally generated dungeons, authoritative co-op, and custom GDShaders. Ships on Windows, macOS, Linux, and Android.
A solo action-roguelike that grew into an engine. The core loop wanted horde-scale unit counts, seamless open-world traversal, and a deep RPG progression stack all in one build. Everything from the Rust GDExtension for the per-frame hot loops to the World Baker EditorPlugin was written for this game.
Combat runs through HordeSystem (5,222 LoC in scripts/combat/horde_system.gd), a Structure-of-Arrays unit engine with ~30 parallel packed arrays (position, velocity, HP, defense, morale, charge timers, veteran rank, burn, scatter). No per-unit GameObject allocation, so scaling unit count doesn't touch the object allocator. Broad-phase neighbor queries run through a spatial hash grid (SPATIAL_CELL_SIZE = 48.0 at horde_system.gd:1795 — tightened from the original 80 for higher unit counts, where smaller cells mean fewer neighbors per bucket) that turns O(N) proximity scans into O(k) localized lookups. Most per-frame hot loops move into a Rust GDExtension (rust_extensions/src/lib.rs, 2,714 LoC) that reads and writes Godot's PackedFloat32Array in place via as_mut_slice() — zero copies, direct writes into the shared buffer.
The GDExtension exposes 20+ #[func] methods to GDScript covering the per-frame hot loops: rebuild_spatial (once per frame), resolve_collisions, batch_refresh_targets, tick_movement, tick_bullets, tick_per_unit_state, tick_aux, spread_player_aggro, push_around_obstacles, get_hostiles_in_radius, get_units_in_radius, compact_slots, and micro-benchmarks (bench_add_loop, bench_collision) for regression tracking. A persistent SpatialHash struct (lib.rs:24) is built once per rebuild_spatial call and reused across all subsequent Rust calls in the same tick — replacing the earlier design where collision, target-refresh, and bullet-hit each rebuilt their own hash from scratch. Parallelism uses rayon over spatial-hash rows (lib.rs:15), with SendPtr/SendPtrConst wrappers (lib.rs:42-48) letting closures share mutable slot access safely as long as each thread only writes its own indices. SIMD uses wide::f32x4 (lib.rs:18) for f32-quad batched distance-squared in the collision inner loop. Release profile compiles with LTO fat and codegen-units=1 (Cargo.toml). An optional dhat feature flag ships the Rust heap profiler for allocation-site tracing.
Damage in HordeSystem is directional and formation-aware. Attacker-to-victim dot product classifies front / flank / rear hits: attacker.dot(victim.forward) <= -0.3 is a rear hit and applies the REAR = 2.0× multiplier (horde_system.gd:662). Flank bonus is 1.6× (line 872). Cavalry charge damage scales with mass and speed: base_mult = 1.6 for heavy_cavalry, 1.3 for lighter (line 737). Morale drives late-game breaks — a morale threshold check at line 593 gates knockback based on ratio × 1.5 × morale_f × (0.5 + 0.5·hp_ratio), clamped to [0.3, 12.0] push pixels. Doctrine AI (FRONTAL_ASSAULT, HAMMER_ANVIL, BAIT_AND_FLANK, KITING_HARASS, DEFENSIVE_LINE) is documented as a design in docs/mermaid/battle_rts/22_DOCTRINE_PLAYBOOK_LAYER.md but not yet implemented in source — the file describes the intent, the code currently ships without the doctrine layer.
12,544 LoC live in scripts/dungeon. floor_generator.gd (888 LoC) runs a three-phase branching random walk: main path first, branches next, fill last, with BFS-distance stair placement and forced critical-path transitions. Room shapes come from 8 archetypes: rectangles, circles, L-shapes, plus cellular-automata caves using the B5678/S45678 rule with 4–5 smoothing iterations, flood-fill island culling, and guaranteed center connectivity. Voronoi-style elliptical mines carve out through Manhattan-distance candidate regions with density-biased frontier expansion. A blueprint decorator (room_blueprints.gd, 438 LoC) drops 10 themed templates into rooms — THRONE_ROOM, CHAPEL, BLACKSMITH, PRISON_BLOCK, ALCHEMIST_LAB, EXECUTION_PIT, WAR_ROOM, TREASURE_VAULT, BARRACKS, TAVERN — with per-zone deduplication.
The overworld (5,149 LoC in overworld.gd, plus supporting systems) uses 64-tile chunks (CHUNK_SIZE = 64 at overworld.gd:100, ≈2048 px per side at 32-px tiles). Load radius is 1 chunk out (LOAD_RADIUS = 1 at :101 — 3×3 = 9 chunks visible, tightened from 3 which loaded 49 chunks and caused stalls, per inline comment). Prefetch is 2 chunks out (PREFETCH_RADIUS = 2 at :116 — 5×5 = 25 chunks total, tightened from 5 which pre-loaded 121). The tile painter is frame-budgeted at 300 TileMap writes per frame (TILES_PER_FRAME = 300 at :104, tightened down from 2000 to spread chunk paint across more frames without stalling). Chunks stream through ResourceLoader.load_threaded_request (overworld.gd:348). On top of the flat tilemap sits a 2.5D height system (height_system.gd, 1,162 LoC) with 7 levels (-3 to +3): per-level Y-offset rendering, cliff face and edge shading, auto-generated ramp collision, and integrated line-of-sight blocking.
CoopSession (244 LoC) sets up an ENet transport with automatic port-fallback (coop_session.gd:100 tries port through port+5, 6 attempts on EADDRINUSE), a 32-peer cap (clampi at :93), and optional UPnP discovery + port mapping (host_with_optional_upnp at :122). CoopRuntime (284 LoC) runs a host-authoritative tick model with split broadcast rates: player state at 30 Hz (PLAYER_BROADCAST_INTERVAL_SEC := 0.033 at coop_runtime.gd:10), enemy snapshots at 20 Hz (ENEMY_BROADCAST_INTERVAL_SEC := 0.05 at :11), all over Godot RPCs. Bullet sync uses a two-flavor split: real damaging enemy bullets vs. zero-damage visual player-bullet ghosts with collision_mask = 16 (walls-only, coop_runtime.gd:255 — comment: 'so it despawns on walls naturally') to keep remote peers from double-counting damage. Remote players render as RemotePlayer ghost nodes (coop_runtime.gd:122) with client-side lerp interpolation smoothing 30 Hz snapshots.
liquid_surface.gdshader (95 LoC) does world-vertex-coord dual-layer sine displacement with a hash21 Perlin lattice noise (function at line 10), per-tile phase coherence via hash21(floor(world_pos / tile_size)), and a Gaussian wake field around the player (line 50: exp(-pow((player_dist - 15.0) / 10.0, 2.0))) plus a wake_core term at :51 and a directional wake_offset at :56. Four liquid types (water, blue, poison, lava) toggle through a uniform. warp_portal.gdshader (60 LoC) is a polar-coordinate spiral vortex with multi-octave hash crackle and tweenable uniforms for radial aperture progression. projectile_fx.gdshader (44 LoC) does directional vector-field glow with front-flash and tail-trail falloff plus sine pulsation. LightingManager (778 LoC) code-generates 128×128 radial-falloff GradientTexture2Ds for torches and lanterns at runtime (lighting_manager.gd:88-92, :106-110), configures an HDR WorldEnvironment with glow_blend_mode = GLOW_BLEND_MODE_SOFTLIGHT (:183), glow_hdr_threshold = 0.8 (:184), and glow_intensity = 0.3 (:180), and blends per-biome mood profiles (torches, night, etc.) that reset glow_intensity dynamically at :299.
perf_telemetry.gd (688 LoC) samples in two tiers: fast metrics at 4 Hz, deep metrics at 1 Hz, snapshots at 0.5 Hz. Every gameplay counter — active enemies, bullets, hazards, traps, fire patches, poison clouds — registers as a custom monitor against Godot's Performance API, alongside frame ms, physics ms, draw calls, static/message-buffer/video/texture memory, and object/resource/node/orphan counts. A spike detector fires on 34 ms frame or 12 ms physics with a 5s cooldown, dumping a snapshot to disk with a baseline-delta suspect report for offline analysis.
27 stats across 6 categories with dual-track flat/percent modifier stacking, computed at read time from 5 sources: base → level allocations → perks → equipment → gems. 74 perk resources across 5 perk trees with a prerequisite graph and class aliasing (archer ↔ ranger). 604 item resources. 48 weapon patterns subclass a base WeaponPattern that returns [direction, delay, offset, speed_mult] fire data — some patterns (SpiralPattern, RailgunPattern, HelixPattern, MeteorPattern) maintain integrator state coupled to attack speed. 12 stackable bullet mutations: life-steal, exploding, chain-lightning 3-hop, homing, pierce, and more. Class weapon-proficiency curves discourage cross-class spam without hard-locking anything (0.75 → 1.0 native, 0.0 → 1.0 off-class).
world_baker EditorPlugin adds a "🍞 Bake World" toolbar button that spawns a headless Godot subprocess running bake_chunks.gd to serialize the live overworld into pre-baked chunk .tres resources, then triggers a filesystem rescan. A Python codebase-to-Obsidian generator (tools/obsidian/generate_code_notes.py) categorizes every script into domain hubs so I can navigate the 87k lines as a knowledge graph rather than a file tree.
Being honest, because I audited every claim against source: the public README describes an older arena-battle system and cites some values that the code has since been tuned past. What's actually in the repo today: combat is integrated into the overworld through HordeSystem rather than in a separate arena; the Rust GDExtension handles most per-frame hot loops (targeting, collision, movement, bullets), not only collisions; the spatial hash cell size was tightened from 80 to 48 for higher unit counts; the overworld streaming budget was tightened from 2000 tile writes per frame down to 300 to avoid stalls; load radius was reduced from 3 to 1 and prefetch from 5 to 2. The five-doctrine AI described in the README (FRONTAL_ASSAULT, HAMMER_ANVIL, etc.) is documented as a design in docs/mermaid/battle_rts/ but is not yet implemented in source. Almost every direction of change was toward tighter, more measured, more parallelized code, but the README hadn't caught up.