7ef972e3b3
ci / verify (push) Successful in 47s
Character swapping is hub-only, and refused by the SERVER rather than merely greyed out in the menu. Allowing it inside a dungeon would be an instant, uninterruptible exit from danger -- strictly better than the one-second escape channel, which would make that channel pointless. Creating a character in a dungeon is refused for the same reason. Both are covered by diag_progression. Health regenerates at 0.5% of MAXIMUM per second. A percentage rather than a flat rate so it does not become irrelevant at level 15: a capped character regains 1.2 hp/s against a level 1's 0.5, and both take about 200 seconds to heal from nothing. No out-of-combat gate -- at this rate it cannot out-heal anything actually shooting at you, and a trickle that never stops is easier to reason about than a timer players have to learn. A fractional carry is needed because a tick heals well under one hit point, so truncating each tick would heal exactly nothing; there is a test for that specifically. The XP bar now states the percentage and the level it leads to, since a bar answers "how far" vaguely and a number answers it exactly. Recorded the two Stage 3 answers: 4 inventory slots, and the boss's food item is player-instanced now so the mechanism gets exercised rather than deferred. Writing the swap guard's test caught my own mistake: the first version created its spare character through the store, which has no opinion about where you are, so it bypassed the guard it was meant to prove and left a stray character behind that broke a later assertion. 200 tests. check.sh, test.sh, smoke.sh and both diagnostics pass.
177 lines
7.9 KiB
Markdown
177 lines
7.9 KiB
Markdown
# 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.
|
|
|
|
---
|
|
|
|
## Characters and progression *(this session)*
|
|
|
|
**Level 1 is base health; each level adds 10.** So level 15 is
|
|
`PLAYER_MAX_HP + 14 * 10` = 240. Level is *derived* from lifetime experience
|
|
rather than stored alongside it, so the two can never disagree — a hand-edited
|
|
save cannot produce a level 12 character with a level 3's experience.
|
|
|
|
**The five-character cap counts LIVING characters only.** Retired ones stay in
|
|
the store forever but free their slot. Counting the dead would lock a player out
|
|
of their own account permanently after five deaths, which is not a punishment
|
|
anyone signed up for.
|
|
|
|
**Death unbinds the character entirely.** There is deliberately no "return to
|
|
the hub as the character who just died" — the run is over, so the peer is
|
|
removed from the instance and left at the roster screen. The one exception is a
|
|
linkdead player, which has nobody to show a roster to, so its body is left for
|
|
the escape channel to resolve as before.
|
|
|
|
**Experience is shared across the party, undivided.** Everyone alive in the
|
|
instance receives the full amount for a kill. Splitting it would make bringing a
|
|
friend cost you progress, which is the opposite of what the hub roster exists to
|
|
encourage.
|
|
|
|
**A level-up heals by the amount it added.** Gaining a level mid-fight should
|
|
feel like relief, not like the bar you were watching got further from full.
|
|
|
|
**The character store refuses to start rather than starting empty.** A corrupt
|
|
or unreadable save aborts the server. Loading empty would look like it worked
|
|
and then overwrite every character on the first level-up.
|
|
|
|
**Account ids are written as decimal strings in JSON.** They are 64-bit and JSON
|
|
numbers are doubles, which would silently round them.
|
|
|
|
**Character swapping is hub-only, enforced on the server.** Swapping inside a
|
|
dungeon would be an instant, uninterruptible exit from danger — strictly better
|
|
than the one-second escape channel, and it would make that channel pointless.
|
|
The menu greys the button out so the rule is visible, but the server refuses
|
|
regardless of what any client's UI allows.
|
|
|
|
**Health regenerates at 0.5% of MAXIMUM per second, with no out-of-combat
|
|
gate.** A percentage rather than a flat rate, so it does not become irrelevant
|
|
at level 15 — a capped character regains 1.2 hp/s against a level 1's 0.5, and
|
|
both take about 200 seconds to heal from nothing. No combat gate because at this
|
|
rate it cannot out-heal anything actually shooting at you, and a trickle that
|
|
never stops is easier to reason about than a timer players have to learn.
|