Skip to content

feat(game)!: Mob Pit world, fold demos in - #288

Draft
patrickleet wants to merge 23 commits into
feat/v5-celld-zonesfrom
feat/v5-mobpit-world
Draft

patrickleet wants to merge 23 commits into
feat/v5-celld-zonesfrom
feat/v5-mobpit-world

Conversation

@patrickleet

@patrickleet patrickleet commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #287 (feat/v5-celld-zones). Mob Pit's room grid on celld becomes the one game demo: each mechanic of the earlier standalone demos moves into it and the old demo is deleted as it is merged (GitKB tasks/gr-p44-mobpit-world).

Phases

  • P1 Network panel (Arena) — distributed_game::netsim is portable; celld zone objects delay every socket's frames/inputs (dev settings object zone-dev/netsim, ZONE_DEV_SETTINGS=1). Mob Pit (Vite + SvelteKit): latency/jitter/loss sliders for every room, prediction toggle, server ghost, pending/ack/corrections readout, interest ring (24 m). Arena deleted.
  • P2 Neighbour rooms (Endless Grid) — on celld zones observers are passive watchers (never occupancy, never wake a sleeping zone: new error ASLEEP = 10); SDK watchZones; Mob Pit draws the mobs of its 8 neighbour rooms (Vite + SvelteKit). Endless Grid deleted.
  • P2b Proximity wake — a watcher can be near (Hello.near, client message Near 0x06): a near watcher with a signed wake ticket grant wakes and keeps a celld zone awake; passive watchers are unchanged; WAKE_DENIED = 11 without the grant (SDK reopens with a fresh ticket). Mob Pit wakes only the 4 doorway neighbours: near at ≤ 11 m from the doorway, released at > 19 m, at most 2 at once, corner rooms never wake. HUD/SvelteKit toggle "Wake rooms in reach" (N, default on, remembered); minimap solid cyan border for awake rooms.
  • P3 Hierarchy + bosses + weather (Boss Ripple) — hood / borough / city as aggregate cells (new mobpit_tiers), seeded boss rooms, boss kills ripple up through a Queue-consumer saga, rooms read their tiers via the new CelldZone::refresh, tougher rooms, borough weather + darkness thickening toward bosses. Boss Ripple and its multi-host plumbing deleted.
  • P3b room variety — six seeded obstacle patterns, collision in pit_step with the room as predicted context, fog gradient toward boss rooms.
  • P-jump — PitState (z, vz) predicted, carried through doorways, slams cleared mid-air.
  • P4 World events — boroughs schedule invasion / frenzy / blessing (ctx.schedule, armed by rooms waking), effects in rooms, banners, boroughs read model + SvelteKit @live list.
  • P5 Bow + first person (Shooting Gallery) — lag-compensated arrows (ctx.rewind() at the view time), compensation toggle, predicted vs confirmed markers, first person with pointer lock; Shooting Gallery deleted.
  • P6 Live content update (The Freeze) — world::content_rollout cell (freeze → apply → resume on its own schedule) followed by every celld zone (ZONE_CONTENT_ROLLOUT); protocol Content (0x05) for live-patched clients; Mob Pit content packs, admin rollout (Vite ?admin=, SvelteKit dev route), old clients held until the entrance reloads them, facts stamped with the version. The Freeze deleted (with gem-pouch/content-log cells, common::Demo, the Arena step).
  • Doorway jump root-caused — the ~1.5 m "doorway" jump was a respawn: the player died at the doorway and the teleport was eased over 150 ms. New #[correct(ease, snap_above)] / SDK snapAbove cuts teleports; Mob Pit uses 2 m. Respawn + bow-then-doorway headless tests; SvelteKit bow check back before the walks, walks measured only while alive, jump bound unchanged.

Breaking

  • distributed_game::net::NetSim moved to distributed_game::netsim (re-exported from net).
  • Mob Pit Hello has view_radius; PIT_WORLD carries player_radius, dash_cooldown, pillars.
  • The Arena, Endless Grid, Boss Ripple and Shooting Gallery demos are gone (with cluster.rs and the worker's zone lease / zone state cells).
  • CelldZone gains refresh / refresh_every.
  • Mob Pit's predicted step takes the room as context and its state is PitState (adds z, vz).
  • celld zones: observers no longer keep a zone awake or wake it; protocol error code 10 ASLEEP.
  • Protocol: new client message Content (0x05); SDK setContentVersion sends it on open player connections.
  • Correction::Ease gains snap_above (no longer Eq).
  • Protocol: Hello gains a trailing near bool (not additive); new client message Near (0x06); error 11 WAKE_DENIED; TicketClaims.wake.
  • The Freeze demo is gone; game_demos::common keeps only CLI options, dev ticket types, guests, celld, relay, zone status.

Proximity wake (P2b)

Abuse bound: a room honours near only when the ticket carries a signed wake grant; /dev/ticket and SvelteKit /api/game/ticket grant it only for rooms within 2 doorways (Manhattan) of the player's presence room (one doorway of slack for presence lag), and tickets are rate limited. Residual: the grant is checked at issue time, so a client that keeps near watchers open after walking away keeps those rooms awake until it closes them (follow-up: lapse near at ticket expiry).

Checks: cargo test --workspace --all-features --all-targets 2330 passed, 1 failed (cli_lifecycle::dev_stop_cancels_a_running_readiness_probe, unrelated flake; passes 12/12 on rerun), 11 ignored · JS SDK 502 tests, 500 pass, 2 skipped, npm run check OK · check-wasm32-domains.sh OK · examples/game/run.sh smoke: server 38/38, client 21/21 (incl. mobpit-wake-headless), SvelteKit 1/1 (neighbour wakes, N toggles it off).

Checks (P6 + fix)

  • cargo test --workspace --all-features --all-targets --no-fail-fast: 2621 passed, 0 failed, 22 ignored
  • JS SDK npm run check + npm test: 498 tests, 496 pass, 2 skipped; wasm32 domain check OK
  • run.sh smoke OK (client 19/19 incl. content, respawn, doorway-bow; SvelteKit 1/1 incl. rollout); SvelteKit repeated 7×: doorway walk-back 0.167 m every time (6 pass, 1 unrelated deepEqual failure not captured)

Checks (P5)

  • run.sh smoke OK (server 39, client 19/19 incl. mobpit-ranged-headless 8/8, SvelteKit 1/1); wasm32 OK

Checks (P4)

  • run.sh smoke OK (server 41, client 20/20, SvelteKit 1/1)

Checks (P-jump)

  • run.sh smoke OK (server 40, client 19/19, SvelteKit 1/1)

Checks (P3)

  • run.sh smoke OK (server 36, client 19/19 incl. mobpit-bosses-headless, SvelteKit 1/1)
  • distributed_game --all-features --lib 67 passed; wasm32 OK

Checks (P2)

  • run.sh smoke OK (server 35 passed, client 21/21, SvelteKit 1/1)
  • cargo test --workspace --all-features --all-targets 2619 passed, 0 failed (131 suites)
  • celld_zone integration single_node + new watchers passed on celld dev
  • JS SDK 495 pass / 2 skipped; wasm32 check OK

Checks (P1)

  • examples/game/run.sh smoke OK (server workspace 44 passed, client 25/25, SvelteKit Playwright 1/1: "rtt 56 → 208 ms with 100 ms one way")
  • cargo test -p distributed_game --all-features --lib 67 passed
  • scripts/check-wasm32-domains.sh OK
  • JS SDK npm run check OK, npm test 494 pass / 2 skipped

The development network simulation (latency, jitter, loss as a
retransmit delay) moves to a portable `distributed_game::netsim`
module (`NetSim`, an order-preserving `DelayLine` over millisecond
timestamps, `NetSimJson` for dev routes); `net` re-exports `NetSim`.

Zones hosted as celld objects apply it per socket and direction:
outbound frames (and closes) and inbound inputs and pings go through
delay lines drained by `setTimeout` tasks. With ZONE_DEV_SETTINGS=1 a
zone reads the setting from the worker's dev settings object
(`celld_zone::ZoneDevSettings`, `zone-dev/netsim`) on wake and every
second while it ticks; `/rooms/{zone}/info` reports it.

Refs [[tasks/gr-p44-mobpit-world]]
Mob Pit gets the Arena's network panel on its celld rooms: the HUD's
latency / jitter / loss sliders write the cells worker's dev settings
(`/dev/netsim` on the services, a dev-only SvelteKit route) and every
room delays its sockets; P toggles prediction, G shows the server ghost
(the room's authoritative copy), the readout counts pending inputs,
acks and corrections, and the dashed ring is the interest radius
(24 m, now in Hello): mobs and players outside are not replicated.

The Arena demo is deleted (server module, smoke test, page, generated
client, headless test, run.sh entry, README rows). The Freeze keeps
the Arena capsule view and bot encoder; PIT_WORLD carries the floor
constants Mob Pit drew from ARENA_FLOOR.

Tests: new mobpit-network-headless (exact WASM prediction against the
room, NetSim raising RTT through celld, interest culling); SvelteKit
Playwright checks the latency slider and the ghost gap.

Refs [[tasks/gr-p44-mobpit-world]]
@coderabbitai

coderabbitai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

On zones hosted as celld objects an observer is a passive watcher: it
never counts as occupancy (a zone with only observers stops ticking,
goes dormant and hibernates, closing their sockets) and never wakes a
sleeping zone — its Hello is refused with the new error code ASLEEP
(10). Only a player's Hello wakes a zone (the WebSocket is accepted
before waking); a resume to a cold zone is RECONNECT_FAILED without
waking. A watcher joining a dormant zone is greeted with its current
state (`ZoneHost::patch_now`).

SDK: `watchZones(client, around)` keeps the zones around the authority
observed across handoffs, lets go of closed watchers and retries them
with back-off; `GameErrorCode.ASLEEP`.

The celld_zone integration tests gain `watchers` and run one celld at
a time.

Refs [[tasks/gr-p44-mobpit-world]]
Mob Pit watches the 8 rooms around the player's own (Vite and
SvelteKit, `watchZones`): their mobs are drawn where they are, a
sleeping room is not woken and shows nothing, and a watched room still
goes dormant and hibernates when its last player leaves. The room gives
a watcher the whole room; the HUD counts rooms watched and asleep, the
minimap outlines the watched ones; the leaderboard page's entrance
watcher retries while the entrance sleeps. The SvelteKit ticket handler
allows the observer tickets this needs.

The Endless Grid demo is deleted (zone, dev map routes, page, generated
client, tests, run.sh entry, README). `game_sim::grid` keeps only the
seeded noise, biomes and props Mob Pit's rooms use.

Tests: mobpit-neighbours-headless; the SvelteKit test checks a player
sees the mobs next door while the watched entrance still hibernates.

Refs [[tasks/gr-p44-mobpit-world]]
A zone hosted as a celld object can declare `refresh_every` and
`refresh(env, zone_id, round)`: async work off the tick (e.g. reading
the cells of the tiers it belongs to) that runs on every wake and at
that interval while the zone ticks; the message it returns is
delivered into the zone. `round` counts calls per activation so slower
tiers can be read less often.

Refs [[tasks/gr-p44-mobpit-world]]
Rooms group into hoods (3 × 3) and boroughs (3 × 3 hoods) under one
city. Each tier is an aggregate cell on celld (new crate mobpit_tiers):
every hood has a seeded boss room; a boss kill is a zone fact into the
hood (only from that room, only into its hood) that raises the hood's
tier and schedules the boss's return (ctx.schedule). The worker's Queue
consumer runs the saga that ripples hood events into the borough
(bosses down, weather) and borough events into the city (banner):
idempotent commands keyed by event id, stale steps ignored by stream
sequence. Rooms read their tiers off the tick (CelldZone::refresh) and
tell their players; a hood's tier makes its rooms tougher, a fallen
boss stays down until its hood brings it back.

Vite and SvelteKit show the borough's weather (Boss Ripple's fog,
clear colour, light) with fog and darkness thickening toward boss rooms
(boss_distance from the shared room generator), boss rooms and hood /
borough borders on the minimap (/dev/mobpit/tiers), and the tiers in
the HUD.

Boss Ripple is deleted with the native multi-host plumbing only it used
(cluster.rs, test support, run.sh cluster runner, the worker's zone
lease and zone state cells).

Tests: mobpit_tiers (hood → borough → city on the reference host,
respawn schedule, stale steps), rooms (a party fells a boss, the fact,
the boss stays down), mobpit-bosses-headless (the saga end to end on
celld), SvelteKit weather and darkness.

Refs [[tasks/gr-p44-mobpit-world]]
pit_room gives each room one of six seeded obstacle patterns (classic
pillars in the entrance, colonnade, ring with a plinth, boulders, a
rounded hall, a grove), generated clear of the door corridors, walls,
spawn points, the boss's home and every mob home with a player's width
between obstacles, so each room stays one connected floor and the
doorways stay where they are.

Collision is part of the predicted step: pit_step takes the room
(PitRoomContext) as its context and generates the obstacles inside
(cached), so the room and the browser's WASM prediction agree bit for
bit (predict-trace replays streams in rooms of every pattern); both
pages set the context per room. The pages draw the obstacles by look
(column, boulder, tree, crystal); walls and light follow the biome.

Refs [[tasks/gr-p44-mobpit-world]]
Mob Pit's predicted state becomes `PitState`: the Arena capsule plus a
height (`z`, `vz`; take-off 8.5 m/s, gravity 24 m/s²). The jump is part
of `pit_step`, so it is predicted in WASM and reconciled like the rest,
replicated to others (`z`), and carried through a doorway mid-air (the
handoff carries `z`/`vz`). A mob's slam rings the floor: a player 0.9 m
up or higher clears it. The Freeze keeps the plain Arena capsule.

Q (gamepad Y) jumps on both pages; the Vite page lifts the capsule,
the SvelteKit canvas draws a shadow under a bigger body.

Tests: game_sim jump arc and carried height, rooms `a_jump_clears_a_
mobs_slam`, the headless network test predicts a jump bit for bit,
predict-trace replays jump streams, SvelteKit presses Q.

Refs [[tasks/gr-p44-mobpit-world]]
A room that wakes tells its borough (zone fact borough.room_awake, only
as itself and only into its own borough); the borough's cell arms an
event cycle on its own schedule (ctx.schedule): invasion (mobs aggro
from twice as far and rise again after a second), frenzy (mobs 40 %
faster), blessing (players heal five times as fast), 30 s each with
30 s between, for as long as rooms of the borough keep waking. Rooms
read the event with their tiers and apply it; both pages show a banner
with the time left. Effects only: XP stays the Crawler cell's decision.

The borough's domain events feed a `boroughs` read model in
mobpit-service (GraphQL @LiVe): the SvelteKit page lists the running
world events live; the Vite page reads the borough cells.

Tests: mobpit_tiers event cycle on the reference host (arming, start,
end, next kind, idle stop), mobpit-events-headless (an invasion starts,
a slain mob is back after ~1 s), SvelteKit (the @LiVe list and the
room's banner).

Refs [[tasks/gr-p44-mobpit-world]]
A wake is often a doorway into a sleeping zone with the player's
inputs waiting; reading the dev network simulation now runs off the
join path (it applies within a second anyway).

Refs [[tasks/gr-p44-mobpit-world]]
Mob Pit gets a ranged weapon with lag compensation: the input frame
carries the aim (yaw, pitch), a shot id and the view delay behind
client_tick; the room records the mobs' hit capsules every tick and
resolves each arrow with ctx.rewind() at the shooter's view time
(clamped to 600 ms), or the latest state with compensation off.
Obstacles stop arrows, a jump raises the eye, the shooter gets a
verdict per shot. First person (V): pointer-lock mouse look, you face
where you look (aim bit), arrows leave from the eye with the look's
pitch; third person and the SvelteKit top-down view shoot level at the
hip along the aim. Both pages predict the hit against what they render
(yellow marker) and show the verdict (green / red); C toggles
compensation. Doorways into sleeping rooms keep up to 3 s of inputs.

The Shooting Gallery demo is deleted.

Tests: rooms `an_arrow_hits_the_mob…` (rewind, no-comp, pillar),
mobpit-ranged-headless (8/8 compensated arrows confirmed as seen under
80 ms one-way latency), predict-trace replays aim/shot inputs, SvelteKit
looses arrows in first person.

Refs [[tasks/gr-p44-mobpit-world]]
A world's content rollout as a cell (`world::content_rollout`,
`ContentRolloutCell` on celld): an admin session starts it; the cell
freezes, applies and settles on its own schedule, each phase an outward
event. Zone objects with `ZONE_CONTENT_ROLLOUT` follow it (read on wake
and every second while ticking; a Hello on another version waits for the
read): freeze with the change notes, `set_content_version`, resume; a
hard rollout reloads stale clients. The snapshot's content version is
restored on wake.

Protocol: client message `Content` (0x05) tells the zone a client patched
its content live; recorded only for the zone's current version. The SDK
sends it from `setContentVersion`. `Ctx::is_stale` and
`player_content_version` let a zone gate doorways for old clients.

[[tasks/gr-p44-mobpit-world]]
`#[correct(ease = MS, snap_above = M)]` (SDK `snapAbove`): when a
reconciliation moves an eased field further than M, the whole entity
cuts to the rebased state instead of easing. A respawn or other
server-side teleport was eased over the ease window, sliding the player
across the floor at metres per frame. Generated clients carry the field.

[[tasks/gr-p44-mobpit-world]]
An admin rolls a content pack (Classic, Frostfall, Emberfall: mob pace,
toughness, tint) out to every live room through the world's
content-rollout cell: rooms freeze with the change notes, switch packs
and resume; facts carry the new version in their stamp. Old clients
(SafeRoom, no roaming) keep their pack, are held at every doorway but
the entrance's and reloaded there; a fresh join on old content reloads.

Services: `GET /dev/mobpit/content`, `POST /dev/mobpit/rollout` (admin
secret, run as the services' admin session). Vite page: admin panel
(`?admin=`), `?old=1`, freeze overlay, stale banner, mob tint. SvelteKit:
`/api/dev/rollout` (dev sign-in) and the same overlay and panel.

The Freeze is deleted with its two-host server, gem-pouch and
content-log cells, client, tests and run.sh entry; the common Demo
plumbing and the Arena predicted step go with it. README describes the
one game.

[[tasks/gr-p44-mobpit-world]]
Root cause of the ~1.5 m jumps walking back into the entrance after the
SvelteKit bow check: the player was killed by pit.1.0's mobs while it
stood by the doorway, and the respawn teleport (≈14 m to a spawn point)
was eased over 150 ms, sliding the view 1.4–1.6 m per frame. Not a
doorway or handoff fault.

Mob Pit's player x/y now use `snap_above = 2.0`, so a respawn is one
cut. New headless tests: a respawn cuts in one frame; a bow fight in
first person, then doorways into a sleeping room, stay exact. The
SvelteKit check is back before the doorway walks; a walk is measured
only while the player stays alive (a death redoes it), and the jump
bound stays strict. The network test's RTT baseline is the quietest
sample (busy machines spiked it).

[[tasks/gr-p44-mobpit-world]]
The exp2 fog was measured from the camera and tripled near bosses, so at
the default zoom a storm room near a boss rendered fully black. Fog is now
linear and starts at the player (camera distance + a clear radius per
weather), closing in to half near a boss; light never drops below 56%.
The SvelteKit canvas darkens with a radial vignette around the player
instead of a full-screen fill.

[[tasks/gr-p44-mobpit-world]]
The RTT is an EWMA seeded by the first ping, which can be slow while the
page boots; a baseline taken mid-decay (128 ms) made 'base + 120' out of
reach once the 200 ms round trip converged (203 ms).
The rooms minimap had become a 7×7 grid of status tiles that jumped a
room at every doorway and never showed the player. It now centres on
your world position every frame and draws each room's biome floor,
walls with doorway gaps and obstacles; room state is the tile border.
You (an arrow facing where you face), other players and mobs are dots
from the replicated entities of your room and the watched rooms; the
interest ring, boss marks and hood/borough borders stay. Scroll zooms.
Node names only show when rooms span more than one node.

[[tasks/gr-p44-mobpit-world]]
An observer can now say it is near a zone: Hello gains a trailing
`near` bool and the new client message `Near` (0x06) changes it without
reconnecting. On zones hosted as celld objects a near observer whose
ticket grants `wake` (new ticket claim, `TicketClaims::wake`) counts as
occupancy: its Hello wakes a sleeping zone instead of ASLEEP, and the
zone keeps ticking while it stays near. `near = false` or closing lets
the zone go dormant and hibernate as before. Without the grant the
watcher stays passive and is told the new error WAKE_DENIED (11, after
the join; the socket stays open). Passive watchers behave exactly as
before. The native `net` server ignores `Near`. Zone info reports
`observers` and `near`.

SDK: `GameConnection` `near` option and `setNear`, `GameClient.observe
(zone, { near })` and `setNear`, `watchZones(...).setNear(zones)` (a
sleeping zone that becomes near is retried at once; a WAKE_DENIED near
watcher is reopened with a fresh ticket, at once then backing off),
`mintGameTicket({ wake })` (observers only) and the ticket handler's
server-side `wake(context)`; the browser can never ask for it.

Breaking (pre-release): Hello's encoding gained the `near` byte.

Tests: celld_zone `near_watchers` (no grant or not near stays asleep;
near wakes and ticks past the hibernation delay; Near false → dormant,
Near true → active, released → hibernated; a dropped near watcher
releases; a watcher without the grant gets WAKE_DENIED), golden
hello_near/near/passive, ticket wake round trip, JS codec, connection,
watch and ticket handler.

Refs [[tasks/gr-p44-mobpit-world]]
Proximity wake ("Wake rooms in reach", HUD toggle N, on by default and
remembered per browser; the same on the SvelteKit page): walking toward a
doorway, the watcher of the room behind it becomes near and the room
wakes and stays live; it is released beyond the hysteresis. Only the 4
doorway neighbours can wake (rooms connect only through mid-wall
doorways); corner rooms stay passive watchers.

The rule is shared Rust, `game_sim::mobpit::rooms_in_reach` (a
`#[game_pure]` both pages run as WASM): near within 11 m of the doorway
(`PIT_WORLD.wakeReach`), released beyond 19 m (`wakeRelease`). Both stay
below the 20 m from the room's middle to any doorway, so at most the two
doorways of a corner are near and the middle releases everything (the
24 m view radius would wake all four from the middle).

Abuse bound: `/dev/ticket` and the SvelteKit ticket handler grant `wake`
only on observer tickets for rooms within two doorways of the room the
principal's presence was last in (`wake_allowed`).

Minimap: kept-awake rooms get a solid cyan border (dashed: watched only);
the SvelteKit minimap and canvas mark them too. README documents the
feature and the bound.

Tests: game_sim reach (lattice, every prior set, at most two, diagonals
never) and grant; native trace for rooms_in_reach; mobpit-wake-headless
(wakes 7 m before the doorway, released back in the middle → dormant →
hibernated, corner keeps exactly the two doorway rooms and never wakes
pit.1.1, toggle off wakes nothing); SvelteKit: toggle on by default,
pit.-1.0 wakes near the west wall, N off lets it sleep. The doorway-bow
test runs with wake off (it is about a doorway into a sleeping room);
the SvelteKit test steps away from doorways before waiting for the
entrance to sleep.

Refs [[tasks/gr-p44-mobpit-world]]

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant