arena: fix ghost-players and frozen-positions after reload
Two bugs users hit in the first live test:
1. Ghost players never leave. arena:hello handler didn't set
clients[id].handle, so the WS-close handler couldn't find a
handle to call playerLeave with. Player records leaked forever
on every hello. Fixed by setting clients[id].handle in the
arena:hello handler, same shape as the chat login path.
2. Positions stuck after page refresh. ArenaManager.playerJoin
reused the existing record on rejoin (good for continuity), but
kept its lastCmdSeq/lastCmdMs/lastAckMessageNum from the prior
session. New client's cmds start at seq=1 so they got dropped
as "already seen", freezing the avatar. Fix: reset those
counters and snapHistory on rejoin. Position continuity is
preserved, cmd stream starts fresh.
Plus two belt-and-suspenders:
- playerLeave(handle, wsId?) — guards the reload race so a late
close from the old socket doesn't delete the rebound player.
- sweepStale() — every second, evict players whose lastSeenMs is
older than 30s. Catches crashed tabs / NATed mobile backgrounds
that the close handler would otherwise miss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>