Initial commit: Transcience MVP

Top-down twin-stick bullet-hell, Godot 4.7, server-authoritative dedicated
server with client-side prediction. Clients send input only; the server
resolves every hit for both players and enemies (no PvP).

- SimWorld: whole simulation as plain RefCounted objects (no nodes, no
  physics server), ~0.24ms/tick at peak load -- runs headless for free and
  drives 78 tests in under a second
- BulletPool: struct-of-arrays bullet storage, replicated as spawn/despawn
  events rather than per-tick state
- Emitter framework (Ring/AimedSpread/WallGap/ArcSweep) shared by trash
  enemies and bosses -- a new boss is data in src/content/content.gd, no
  simulation changes
- The Warden of the Fold: stationary 4-phase boss built entirely on that
  format
- Lobby hub with a portal into on-demand dungeon instances; one process
  hosts the hub plus every concurrent dungeon
- Emergency escape: 3s server-owned channel, cancelled by damage
- tools/check.sh, test.sh (GUT), smoke.sh (real server + bot clients over
  ENet), bench.gd; git hooks wired to the same scripts
- docs/ARCHITECTURE.md, NETCODE.md, WORKFLOW.md, ROADMAP.md
This commit is contained in:
2026-09-03 16:03:57 +02:00
commit c4beeae38f
385 changed files with 28725 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
# Agent-assisted Godot development
Research notes and the resulting setup for this repo. The short version: Godot
is an editor-centric engine and a coding agent is a text-centric tool, so the
whole workflow is built around making the game fully runnable, inspectable and
verifiable from the command line — with the editor as an optional viewer rather
than a required step.
## The core problem
An agent editing a Godot project cannot see the editor. It cannot click play,
cannot look at the scene dock, and cannot tell whether a `.tscn` it wrote is
valid. Left alone it will produce code that looks right and does not run.
Everything below exists to close that gap. Three properties matter:
1. **A fast, honest failure signal.** Parse errors must surface in seconds, from
the CLI, with the engine's own words.
2. **A way to actually play the game without a human.** Not "the tests pass" —
the real binary, the real socket, the real loop.
3. **Source of truth in text.** Binary scenes and inspector-tuned resources are
invisible to review and produce meaningless diffs.
## Godot's CLI, as an agent uses it
| Command | Use |
| --- | --- |
| `godot --headless --path . --import` | Rebuild the import + **global class cache**. Required after adding any `class_name`. ~2s. |
| `godot --headless --path . res://scene.tscn` | Run a scene headless with autoloads alive. |
| `godot --headless --path . --script tools/x.gd` | Run a `SceneTree` script. **No autoloads.** |
| `godot --headless --check-only -s file.gd` | Parse one file. |
| `godot --headless --path . -s addons/gut/gut_cmdln.gd -gdir=res://tests -gexit` | Run the test suite. |
| `godot --headless --path . --export-release <preset> <out>` | Build. |
Four things cost real time to discover, so they are worth stating plainly:
- **The global class cache is not built by running the game.** It lives in
`.godot/global_script_class_cache.cfg` and is written by the editor or by
`--import`. Add a `class_name`, run your code, and every reference to it fails
with `Identifier "Foo" not declared in the current scope` — a message that
looks exactly like a typo and is not one. `tools/check.sh` runs `--import`
first for this reason.
- **Autoload singletons do not exist under `--script`.** `--script` replaces the
main loop, so autoload names are unresolved identifiers at compile time. Any
tool that needs an autoload has to run as a scene instead. The practical
consequence for design: keep autoloads to the minimum that genuinely needs to
be a node. This repo has exactly one (`Net`, because RPC routing is by node
path); logging and CLI options are static classes, which makes them usable
from tests and tools alike.
- **`ResourceLoader.load()` on a script with a parse error can return a non-null
object.** Detecting failures by null-checking the return value silently passes.
Read the engine's stderr instead — that is what `tools/check.sh` does.
- **Never call `Script.reload()` on the script currently executing.** It hangs
the engine with no output.
## What this repo does about it
### `tools/check.sh` — the inner loop
Refreshes the class cache, loads every `.gd` in the project, greps the engine's
stderr for `Parse Error` / `SCRIPT ERROR` / `Failed to load script`, and exits
non-zero on any hit. About five seconds. This is the command to run after every
edit; it catches the entire class of "looks right, does not compile" mistakes
that an agent produces most often.
### `tools/test.sh` — GUT, headless
[GUT 9.7.1](https://github.com/bitwes/Gut) is vendored into `addons/gut/`.
Chosen over [gdUnit4](https://github.com/godot-gdunit-labs/gdUnit4) because this
project is GDScript-only, where GUT is the lighter and simpler of the two;
gdUnit4 is the better pick when C# is in play or when you want its scene-runner
utilities. Either runs headless in CI.
The suite runs in about two seconds because of an architectural choice, not a
testing trick: **the entire simulation is plain `RefCounted` objects** — no
nodes, no physics server, no rendering. A test constructs a `SimWorld`, drives
a thousand ticks, and asserts. Nothing to await, nothing to instantiate, no
frame timing. If the simulation had been built out of `CharacterBody2D` and
`Area2D`, every one of these tests would need a live SceneTree and would be
slower and flakier by an order of magnitude.
That is the single highest-leverage decision in this repo for agent-assisted
work, and it is worth stating as a general rule: **keep game logic out of nodes.**
Nodes are for presentation and input. Logic in plain objects is testable,
diffable, reviewable, and runs on a headless server for free.
### `tools/smoke.sh` — the thing tests cannot do
Boots the real dedicated server, connects two scripted bot clients over a real
ENet socket, and asserts on the server and client logs that the whole loop
happened: handshake, lobby, portal into a dungeon, emergency escape back out.
Unit tests structurally cannot cover RPC wiring, codec round-trips over the
wire, instance transfers or client reconciliation. This does, in ~35 seconds,
with no display. The bot input lives in `ClientRuntime._bot_input()` behind
`--bot`, so the "player" driving it is the same code path a human uses.
This is the pattern to reach for whenever an agent needs to verify something
interactive: give the program a scripted-input mode and a `--autoquit`, run it
headless, and assert on structured log lines. Log markers like `SERVER_READY`
exist specifically to be grepped.
### `tools/bench.gd` — performance as a number
Runs the simulation with no transport and no rendering and prints milliseconds
per tick. Current numbers, 4 players, 60s of play:
```
boss fight 0.237 ms/tick peak 352 bullets headroom x70
trash wave 0.179 ms/tick peak 145 bullets headroom x93
```
A 60 Hz tick has a 16.6 ms budget, so one instance uses ~1.4% of one core. That
is the measurement that says a single server process can host dozens of
concurrent dungeons, and it took ten seconds to get because the simulation has
no engine dependencies.
## Content as code, not as `.tres`
Enemies and bosses are built by GDScript functions in `src/content/content.gd`,
not authored as `.tres` files. For agent-assisted work this is the right default:
- A boss is a readable diff. A `.tres` full of `SubResource` ids is not.
- No resource UID churn in version control.
- A test can build content inline without touching the filesystem.
- The agent can write a boss without an editor.
`tools/export_content.gd` writes `.tres` copies into `resources/` for anyone who
wants to tune numbers in the inspector, with the direction of truth documented:
code wins, port inspector changes back.
The same reasoning applies to scenes. This project has three `.tscn` files, each
a handful of nodes. Anything dynamic — the HUD, the menu — is built in code,
because a scene file for it would only be one more thing to keep in sync.
## Godot MCP servers
An MCP server lets an agent talk to a live Godot editor: inspect the scene tree,
read node properties, create nodes, run the project and read its output. The
most established option is
[Coding-Solo/godot-mcp](https://github.com/Coding-Solo/godot-mcp) (~5.5k stars,
Node ≥18):
```bash
claude mcp add godot -- npx @coding-solo/godot-mcp
```
It exposes `launch_editor`, `run_project`, `stop_project`, `get_debug_output`,
`get_godot_version`, `get_project_info`, `create_scene`, `add_node`,
`load_sprite`, `save_scene`, `export_mesh_library`, and UID helpers for 4.4+.
Set `GODOT_PATH` if `godot` is not on `PATH`.
**Where it earns its place:** projects with large hand-authored scene trees,
where the agent otherwise has to guess node names and paths. Being able to read
the real tree instead of inferring it is the biggest single quality jump in that
situation.
**Where it does not:** this repo. `run_project` and `get_debug_output` are what
`tools/server.sh` and `tools/smoke.sh` already do, more precisely and with
assertions attached; and the scene-authoring tools matter little when there are
three small scenes. It also adds a live-editor dependency, and editor
auto-reload racing against agent file writes is a known source of stale state.
The honest recommendation: **start with the headless CLI loop, add the MCP when
scene inspection becomes the bottleneck.** It is one command to add later.
## CI
`.github/workflows/ci.yml` runs check → test → smoke on a container image with
Godot preinstalled. All three are headless, so nothing extra is needed.
## Sources
- [Coding-Solo/godot-mcp](https://github.com/Coding-Solo/godot-mcp)
- [GUT](https://github.com/bitwes/Gut) · [gdUnit4](https://github.com/godot-gdunit-labs/gdUnit4)
- [Godot: high-level multiplayer](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html)
- [Godot: command line tutorial](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html)