e0c1e0d5c6
ci / verify (push) Successful in 49s
Boss movement is a property of the PHASE, not of the boss -- a fight that
stands still and then starts hunting you is one boss with two phases. Four
modes (STATIC, ORBIT, CHASE, WAYPOINTS) handled generically in
SimWorld._move_boss, so a boss that moves is still data. BossDef.stationary
is gone rather than kept beside the phases: a flag claiming the boss stood
still while a phase walked around would be a second source of truth and the
wrong one, so moves() is derived.
CHASE holds a distance instead of closing, because a boss standing on top of
you is a boss whose bullets cannot be read. Waypoints are fractions of the
arena so one phase works in rooms of different sizes. Every mode is speed
clamped in one place -- ORBIT computes an absolute destination and would
otherwise snap onto its circle on the first tick -- and movement slides
against geometry so a boss cannot walk through the pillars its own arena was
designed around.
The room clamp moved to after movement, where it is finally load-bearing. It
was a no-op while every boss stood still, which is exactly when an invariant
is cheapest to establish: boss rooms deliberately do not lock, so walking out
is always an escape, and that only holds if the boss cannot follow.
TelegraphedStrikeEmitter marks spots and fills them a moment later. The moment
between is the feature: a burst at your feet is a coin flip, the same burst
with a second of notice is a question. It stays stateless like every other
emitter -- they are shared resources and two bosses of the same kind must not
stomp each other -- so strike positions are derived from the volley number and
a test asserts the burst lands where the marker promised. Markers are drawn
through fog and through walls, unlike everything else in the view, because a
warning you cannot see is an unavoidable hit with extra steps.
The Cantor of the Vault fights in the choir vault: static, then a four-corner
circuit, then a chase, then orbiting while marking. It exists to prove the
format stretched, and a test asserts it uses both new mechanisms.
Which boss a run has now comes from its SEED rather than its depth. Depth is a
dev flag nothing in play raises, so the arena was keyed to something no player
can change and the second boss was unreachable in an actual game.
Two things found while finishing:
- tools/export_content.gd had a hand-maintained boss list and had already
gone stale, silently not writing the Cantor. Content.ALL_ENEMIES and
ALL_BOSSES now feed the export tool, the renderer and five tests that each
kept their own copy.
- diag_loot failed intermittently after another diagnostic. Taking over from
the bot cleared its input queue but not its HELD input, so a starved server
coasted on the bot's last movement vector for half a second and walked the
player off the item it had been placed on. The press arrived correctly,
which is why "the press reached the simulation" passed while everything it
should have caused failed.
check.sh clean, 409 tests, SMOKE PASS (19 assertions), all four diagnostics
green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
71 lines
3.3 KiB
GDScript
71 lines
3.3 KiB
GDScript
class_name Protocol
|
|
extends RefCounted
|
|
## Wire constants. Bump [constant VERSION] whenever a codec layout changes; the
|
|
## server refuses mismatched clients at handshake rather than desyncing later.
|
|
|
|
## 2: added spawn-grace flag + cleared countdown to the snapshot, and the
|
|
## roster message.
|
|
## 3: dropped the post-hit invulnerability flag, added the respawn-lockout byte
|
|
## to each player record.
|
|
## 4: added SimEvent.Type.PLAYER_FIRED. It was inserted mid-enum, which shifts
|
|
## the wire value of every event after it -- a mismatched client would
|
|
## mis-decode every hit and death, so the handshake has to reject it.
|
|
## 5: handshake carries an auth ticket instead of a bare name; added character
|
|
## list/select/create messages, per-player max health and colour in the
|
|
## snapshot.
|
|
## 6: inventory and loot. The snapshot gained the observer's own inventory and
|
|
## the ground-loot list, the input frame gained a slot byte, and three item
|
|
## events were appended. Every one of those changes the byte layout of a
|
|
## message both ends parse positionally.
|
|
## 7: more than one dungeon. enter_instance carries a portal LIST and the id of
|
|
## the dungeon you are standing in, replacing the single portal position.
|
|
## 8: upgrades. A new server -> client upgrade-state message, a new
|
|
## client -> server choice message, and two more SelectResult values.
|
|
## 9: telegraphed boss attacks. A new TELEGRAPH event, appended to the enum.
|
|
const VERSION := 9
|
|
const DEFAULT_PORT := 27015
|
|
const MAX_CLIENTS := 32
|
|
|
|
## ENet channels. Separating them stops a burst of reliable bullet events from
|
|
## head-of-line blocking the unreliable snapshot stream.
|
|
const CH_CONTROL := 1 ## handshake, instance transitions -- reliable
|
|
const CH_SNAPSHOT := 2 ## world state -- unreliable, newest wins
|
|
const CH_EVENTS := 3 ## bullet spawns/despawns, hits -- reliable ordered
|
|
const CH_INPUT := 4 ## client -> server intent -- unreliable ordered
|
|
## Must be identical on both ends, and larger than the highest channel above.
|
|
const CHANNEL_COUNT := 8
|
|
|
|
enum InstanceKind { LOBBY, DUNGEON }
|
|
|
|
## Why a character selection failed. Sent rather than a bare "no", so the UI can
|
|
## say something useful instead of appearing broken.
|
|
enum SelectResult {
|
|
OK,
|
|
NO_SUCH_CHARACTER,
|
|
CHARACTER_IS_DEAD,
|
|
LIMIT_REACHED,
|
|
NOT_AUTHENTICATED,
|
|
## Swapping is hub-only. Allowing it 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.
|
|
NOT_IN_HUB,
|
|
## Upgrades are spent standing at the hub's NPC. Same rule as the portal:
|
|
## where you are is the one thing a modified client cannot fake.
|
|
NOT_AT_THE_NPC,
|
|
## No unspent level-up, or an option index that was not on the table.
|
|
NOTHING_TO_CHOOSE,
|
|
}
|
|
|
|
## Player flags packed into the snapshot's per-player byte.
|
|
const F_ALIVE := 1
|
|
const F_ESCAPING := 4
|
|
## Arrival protection: invulnerable and unable to shoot. The only invulnerable
|
|
## state there is -- see the i-frames note in SimConfig.
|
|
const F_SPAWN_GRACE := 8
|
|
## The peer behind this player has dropped and is being channelled out.
|
|
const F_LINKDEAD := 16
|
|
|
|
## Snapshot's cleared-countdown byte when the instance is not in the cleared
|
|
## state -- 30s fits in a byte, so 255 is free to mean "not applicable".
|
|
const COUNTDOWN_NONE := 255
|