arena: fix self-takeover on reconnect + free-fly spectator cam
Two bugs users reported:
1. Self-takeover on single-tab reloads. When a tab reconnects (page
reload, transient network drop, live-reload polling), it sends a
fresh arena:hello on a new wsId. My code treated any wsId change as
a takeover and demoted the "old" connection to spectator — but that
old connection was actually already dead. The tab that should have
been the active player was instead told it had been displaced.
Fix: session.mjs now exposes isLive(wsId) (= ws is OPEN). In
ArenaManager.playerJoin, only emit arena:takeover + register a
spectator probe if the old wsId is still live. Otherwise it's a
silent reconnect — just refresh the bookkeeping.
2. Spectator cam still driven by cam-doll. The user asked for a
free-float cam (Quake noclip) while spectating. Added a spectator
controller in netSim that, when netSpectator is true, overwrites
cam.x/y/z each frame from a local specPos vector driven by WASD +
space/shift. Runs after cam-doll, so cam-doll's pushes get stomped
on. Rotation still handled by cam-doll (mouselook works normally).
Speed 20u/s, includes pitch in forward vector so look-down-and-W
flies you down. specPos resets to null when re-entering play mode.
Also simplified netSim's non-spectator path (removed the now-redundant
!netSpectator guard since spectator returns early).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>