Fix aiming under a scrolling camera; add status and decisions docs
ci / verify (push) Successful in 46s

The aim bug was collateral from the camera work. "Mouse relative to the centre
of the screen" WAS the cursor's world position while the world was drawn fixed
at the origin, so subtracting the player position gave the right vector. Once
the camera scrolled, that expression became the aim vector itself, and
subtracting the player position again made the ship aim at a fixed world
location -- walking around swung the crosshair with the mouse held still.

Fixed by inverting the transform the view actually draws with (world = screen -
world_view.position, published by the game scene each frame) rather than
assuming the player is centred, so it still holds if the camera later clamps at
map edges or gets shake or look-ahead. tests/unit/test_aim.gd pins it, including
the regression directly: moving the player must not move the crosshair.

Documentation, for other sessions picking this up cold:
- docs/ROADMAP.md rewritten as the status map -- every feature in the brief
  against its state and the file implementing it, the known gaps called out
  (actor interest management is the notable one), and the ten design questions
  that are genuinely unspecified and should not be guessed at.
- docs/DECISIONS.md, new: settled decisions with their reasoning, so a session
  does not re-litigate or re-ask. Several are not the obvious default -- no
  i-frames, no contact damage, non-interruptible escape, and never sending the
  map seed.
- CLAUDE.md and README point at both.

137 tests; check.sh, test.sh and smoke.sh pass.
This commit is contained in:
2026-09-03 20:58:03 +02:00
parent 7af439341d
commit de48afcbd9
8 changed files with 342 additions and 54 deletions
+128
View File
@@ -0,0 +1,128 @@
# Settled decisions
Design choices the user has already made, with the reasoning. **Check here
before asking** — re-litigating a settled decision wastes a round trip, and
several of these look like defaults you would otherwise pick differently.
Ordered newest last.
---
## Simulation shape
**Game logic lives in plain `RefCounted` objects, not nodes.** No
`CharacterBody2D`, no `Area2D`, no physics server. Bullet counts make per-bullet
nodes unaffordable, the dedicated server allocates nothing, and the whole suite
runs with no `SceneTree`. Full reasoning in [ARCHITECTURE.md](ARCHITECTURE.md).
**Content is GDScript, not `.tres`.** A boss is a readable diff, there are no
resource UIDs churning in version control, and a test can build content inline.
`tools/export_content.gd` writes `.tres` copies for inspector tuning, but code
is the source of truth — port inspector edits back.
---
## Netcode
**The client sends input and nothing else.** No message exists for position,
hits, damage, or a finished escape. Having no code path is strictly stronger
than validating one.
**Bullets replicate as spawn events, not state.** ~36 bytes once per bullet;
both sides run the identical integration. Only early deaths (a hit, or a wall)
need announcing. Pinned by `tests/integration/test_replica_parity.gd`.
**No lag compensation.** Rewinding the world to a shooter's view would mean a
player who dodged on their own screen still takes the hit. If it becomes a
complaint, lag-compensate *player bullets against enemies only* — never enemy
bullets against players.
**`INPUT_MAX_LEAD` must stay well above `INPUT_LEAD_MAX`.** The server's input
acceptance window has to be wider than the band in which the client re-syncs its
own numbering. When it was not, drifting clocks landed in a dead zone where the
server silently rejected every input and the client never noticed. Pinned by
`tests/unit/test_input_lead.gd`.
---
## Combat feel
**Hitbox is smaller than the sprite** (`PLAYER_RADIUS` 6 vs
`PLAYER_VISUAL_RADIUS` 13), and `PLAYER_MUZZLE_OFFSET` derives from the visual
radius. A visible near-miss reads as fair; an invisible hit does not.
**No invulnerability frames.** Every bullet that touches you lands. I-frames
make dense patterns *safer* than sparse ones, which inverts the genre. Measured
cost: ~13.6s stationary in the Warden's opening phase. `spawn_grace` on entering
a dungeon is the sole exception, and it is a transition, not a combat mechanic.
**No contact damage.** Every threat is a bullet you can see and dodge. The
Stalker carries a point-blank shotgun rather than damaging you by touch.
`tests/unit/test_content.gd` asserts every hostile enemy has an emitter.
---
## Leaving a run
**Escape is a 1-second channel that damage does not interrupt.** An
interruptible channel makes killing the process better than pressing the button.
**A disconnect runs the same channel.** The player stays in the world as
`linkdead`, still killable. All four exits (key, menu button, clean disconnect,
SIGKILL) converge on one server-side path keyed on the socket closing — there is
deliberately no "clean leave" message. `tools/smoke.sh` asserts both the hard
kill and the polite disconnect.
**Boss rooms do not lock.** *(User, this session.)* You can always walk out of a
boss fight, and the boss cannot follow. The consequence: fights cannot rely on
trapping the player, and disengaging is always available.
---
## World
**Tile grid, generated layout, hand-authored boss arenas.** *(User, this
session.)* A grid because collision, line of sight and interest management all
become array lookups; authored arenas because a generated boss room is a bad one
about as often as a good one.
**Hard fog.** *(User, this session.)* No remembered terrain — anything outside
current line of sight is not drawn, including ground already walked over.
**Dungeon size scales with depth.** *(User, this session.)* `--depth N` is a dev
flag; what raises depth in actual play is still open.
**Never send the map, or its seed.** *(User, this session — corrected an earlier
choice of mine.)* Sending `(seed, depth)` and regenerating client-side is far
cheaper on the wire and hands any modified client the entire floor plan. Tiles
stream per peer instead. The accepted trade, in the user's words: a cheater
seeing further than they should is tolerable; seeing the whole map is not.
Consequence to preserve: the client holds *real* geometry it cannot see, because
it predicts movement against walls and simulates bullets that die on them. So
hard fog is a rendering rule, not secrecy. The secrecy is in what the server
declines to send.
---
## Identity and persistence
**Steam-shaped auth abstraction.** *(User, this session.)* The goal is
eventually Steam, so build the shape Steamworks uses and keep it swappable:
client presents an opaque ticket, server validates it and gets a stable 64-bit
account id (SteamID64's stand-in). A local dev provider persists a generated id
in `user://`, so **no Steam account is needed now**.
Not integrating GodotSteam yet: it needs a running Steam client and an app ID,
which would break the "no Steam account" requirement. Swapping it in later
should be one provider class and no schema change.
---
## Progression
**XP from kills, bosses worth far more.** *(User, this session.)* A first full
dungeon should give a bit more than is needed for the first level-up.
**Permadeath.** Death marks a character inactive — never deleted, for archival
and troubleshooting — and the player picks another character or creates one.