One account is now one session. Two peers signed in as the same account
each got their own player built from the SAME character record, so both
held a copy of one bag and whichever wrote last won: drop the single
potion from both copies and two potions hit the floor. Reconciling two
live mirrors of one record cannot be made to work, so the handshake
refuses the second connection -- the newcomer, not the incumbent, or
claiming an account would be a way to kick anyone off the server.
A muzzle offset is a teleport and the bullet pool never sees it: it
samples geometry once per tick. Forward that is 19px, but Doubleshot
adds up to 45px sideways, wider than a 32px wall plus the 6px hitbox
holding you off it. Six Doubleshots, stand flush against a wall, fire
along it, and live shots came out the far side. Shots now start from the
shooter when geometry stands in the way, which keeps the count honest.
Bullet-spawn events carry the firing actor's exact position, and they
were filtered by a flat 2900-unit radius against an 800-unit actor cull.
A depth-1 map's diagonal is about 2521, so that radius culled nothing:
every enemy that fired was pinpointed for every player, through fog, on
terrain the client was deliberately not streamed. The test is now
per bullet -- can THIS one reach ground you can see -- which keeps the
guarantee the wide radius existed for and drops the radar.
Found while fixing the above: a rejection never reached the client. The
RPC is queued and only leaves on the frame's flush, so kicking in the
same call threw the reason away, and _on_server_disconnected overwrote
last_error anyway. Both were true of protocol mismatch too, which is the
one message a player most needs. Smoke asserts the reason arrives.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the fixed centred arena with per-world tile geometry, which is the
foundation the remaining features sit on.
- MapGrid: tile grid with three independent flags -- blocks movement, bullets,
sight -- so a pit stops feet but not bullets or eyes, and a barricade stops
feet and bullets but not eyes. Circle collision with per-axis sliding, and
Bresenham line of sight shared by fog and aggro.
- MapGen: rooms and corridors generated from (seed, depth), with hand-authored
boss arenas from Rooms stamped in first so a corridor can never carve through
a designed fight. Reachability from spawn to boss is asserted over 40 seeds --
"usually connected" is the failure mode that ruins one run in twenty.
- Dungeons are populated at creation, per room, instead of gating on waves. You
explore and choose your fights; the run ends when the boss dies, not when the
map is swept. Boss rooms have no lock, so walking out is always available.
- Aggro: enemies need range AND line of sight, so a dungeon stays quiet until
engaged and cover actually protects.
- Hard fog, scrolling camera, and terrain rendering.
Maps are streamed per peer in chunks around that peer's player, and the seed is
deliberately NOT sent -- a client holding it could regenerate the whole dungeon,
which is a map hack for free. Stream radius (900u) is wider than view radius
(460u) because the client predicts movement against walls and simulates bullets
that die on them; the accepted cost is a cheater seeing a little further than
the fog, never the floor plan.
Partial map knowledge means wall deaths must be announced rather than derived.
A test caught the subtle half of that: out-of-bounds tiles read as WALL by
design, so checking geometry before bounds reported every bullet leaving the map
as a wall kill.
128 tests (was 103); check.sh, test.sh and smoke.sh pass. 0.26 ms/tick with 4
players and a boss, ~65x headroom.