# Architecture ## The shape ``` ┌──────────────────────────────────────────┐ │ src/sim/ -- plain RefCounted objects │ authoritative │ SimWorld, SimPlayer, SimEnemy, SimBoss, │ replica (server) │ BulletPool, emitters │ (client) │ no nodes · no physics · no rendering │ └───────────────┬──────────────────────────┘ │ ┌────────────────────────┼────────────────────────┐ │ │ │ ┌───────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐ │ src/instances/ │ │ src/net/ │ │ src/view/ ui/ │ │ lobby, dungeon │ │ codec, server & │ │ read-only draw │ │ progression │ │ client runtimes │ │ │ └────────────────┘ └────────┬────────┘ └─────────────────┘ │ ┌────────▼────────┐ │ src/autoload/ │ │ net.gd (RPC) │ └─────────────────┘ ``` Dependencies point inwards only. `src/sim/` imports nothing from `net`, `view`, `ui` or `instances`, and never touches the `Net` autoload. That is what makes the simulation runnable in a unit test, in a headless server and inside a client without a single conditional. ## Why the simulation is not made of nodes Godot's instinct is `CharacterBody2D` + `Area2D` + `MultiplayerSynchronizer`. This project deliberately does not, for four reasons: 1. **Bullet count.** Hundreds of live bullets. A node and an `Area2D` each would cost more than the entire simulation does. `BulletPool` is parallel packed arrays; a tick over ~350 bullets costs 0.24 ms including AI and hit checks. 2. **Headless cost.** The dedicated server allocates no nodes and touches no physics server. 3. **Testability.** A `SimWorld` is `SimWorld.new()`. The suite drives thousands of ticks in ~2 s with no SceneTree, no awaits and no frame timing. 4. **Replication control.** `MultiplayerSynchronizer` is excellent for prototyping and wrong for this shape: it would replicate per-bullet state and saturate the link. See [NETCODE.md](NETCODE.md). The trade is that there is no physics engine — no arbitrary collision shapes, no tilemap collision. The arena is a rectangle and every hit is circle-vs-circle. For a bullet hell that is not a limitation; hitboxes in the genre are circles by convention anyway. A dungeon with real geometry would need a static collision representation added to `SimWorld`, not a switch back to nodes. ## Tick Everything runs on Godot's physics tick, pinned to 60 Hz in `src/main.gd` (asserted in code, because `project.godot` drops settings equal to the engine default). `SimConfig` holds every constant both sides must agree on; per-machine options live in `GameOpts` and must never affect the simulation. ## Content pipeline ``` BulletEmitter (Resource, abstract) ├── RingEmitter count, arc, spin per shot -> rings and spirals ├── AimedSpreadEmitter fan at nearest player -> punishes standing still ├── WallGapEmitter curtain with a sliding gap -> forces a committed dodge └── ArcSweepEmitter rotating arms -> forces tracking EnemyDef = stats + movement enum + emitters BossPhase = hp threshold + telegraph + looping timeline of emitters BossDef = stats + ordered phases ``` `SimWorld._run_emitters()` is shared by enemies and bosses, so any pattern can be dropped on either. A boss is four phases layering one idea at a time; a new boss is a new function in `src/content/content.gd` and zero simulation changes. ## Entry point One executable, one `src/main.tscn`. `GameOpts.parse()` reads the command line after `--`: - `--server` → `ServerRuntime`, no view, `max_fps` pinned to the tick rate. - `--join` / `--bot` → connect immediately, skipping the menu. - otherwise → the connect menu, which can also start a listen server. ## File map | File | Role | | --- | --- | | `src/core/sim_config.gd` | Every constant server and client must agree on. | | `src/core/movement.gd` | Pure movement + overlap helpers. Shared by prediction. | | `src/core/game_log.gd`, `game_opts.gd` | Static; usable from tools and tests. | | `src/sim/sim_world.gd` | The simulation. Authority flag decides what runs. | | `src/sim/bullet_pool.gd` | Struct-of-arrays bullet storage and integration. | | `src/sim/input_frame.gd` | The only thing a client may assert about itself. | | `src/net/net_codec.gd` | Snapshot / event / input binary codecs. | | `src/net/server_runtime.gd` | Instances, ticking, transfers, broadcast. | | `src/net/client_runtime.gd` | Prediction, reconciliation, interpolation, bot input. | | `src/instances/instance.gd` | Lobby hub and dungeon progression. | | `src/autoload/net.gd` | ENet lifecycle, RPCs, local loopback for listen servers. | | `src/view/bullet_renderer.gd` | The whole bullet field in one MultiMesh draw call. |