mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-11-14 02:53:53 +01:00
Adds a complete block-cadence multiplayer race mode alongside solo and tavern.
Disabled by default; enable with MATCH_ENABLED=true.
Highlights:
- New shared engine: MatchRoom, MatchEngine, MatchState (transport-agnostic, unit-tested).
- Per-economy queues: free, credits_prestige, crypto_race with atomic escrow/refund.
- MatchScheduler drains queues on every new block and starts 2–32 player races.
- MatchManager handles Socket.IO lifecycle, server-authoritative movement, reconnect grace,
forfeit/AFK handling, and end-of-match persistence.
- Crypto races use race-entry tickets (grants.race_entries) to avoid on-chain refunds; winner
payout is queued through the existing batch payout/retry service.
- Leaderboard integration: free → Pleb, prestige → /api/leaderboard?board=prestige,
crypto → Hall of Champions.
- Tavern integration: Queue for Race panel, active race list, and live spectator rendering.
- Dedicated /match.html client with economy selector, race HUD, and post-match scoreboard.
- Migration 022 adds matches, match_entrants, match_events, match_queue_entries,
race_entry_transactions, users.race_entries, and payouts.match_id.
Docs:
- New docs/MATCH_MODE.md with full architecture, config, events, and operational notes.
- Updated README.md, docs/TAVERN_AND_MULTIPLAYER.md, docs/TODO.md, src/.env.example,
and src/.env.
Tests: 40 new unit tests across 7 suites, all passing.
Known follow-ups for real-world validation:
- Full integration test with Postgres + Socket.IO.
- Server boot check with MATCH_ENABLED=false (default) to ensure solo/tavern remain unaffected.
- Browser render-kit verification in /match.html and tavern spectator.
- Crypto-race payout end-to-end with real wallet-rpc.
265 lines
14 KiB
Markdown
265 lines
14 KiB
Markdown
# Tavern & Multiplayer — Design & Build Plan
|
||
|
||
Match mode (multiplayer races) now has its own dedicated design doc: see `docs/MATCH_MODE.md`.
|
||
|
||
**Status:** Tavern is complete; multiplayer race mode is implemented (see `docs/MATCH_MODE.md`).
|
||
PvP combat remains a future extension. Both modes are disabled by default and gated by config, so
|
||
existing single-player play is unaffected.
|
||
|
||
## Shared engine: one Room, two modes
|
||
|
||
Both modes are built on a single shared component — a real-time, multi-occupant `Room`:
|
||
|
||
- **Tavern** — a peaceful Room: players pick an avatar, walk around, chat, and watch live games.
|
||
- **Multiplayer** — the same Room engine with a dungeon, monsters, hazards, and player-vs-player
|
||
combat added.
|
||
|
||
The engine (`src/multiplayer/`) has no Socket.IO, database, or Wownerogue coupling. A manager
|
||
layer owns transport (Socket.IO rooms, broadcasts) and the server-tick timer. Keeping the engine
|
||
transport-agnostic makes it unit-testable in isolation and reusable as a standalone package.
|
||
|
||
## Modes
|
||
|
||
**Tavern** — a social space that reuses the existing lobby and spectator infrastructure. Players
|
||
choose an avatar, move around a shared map, and chat. A spectator camera lets them watch live
|
||
single-player or multiplayer games from inside the room.
|
||
|
||
**Multiplayer Race** — a shared match Room. Players race to escape a dungeon before the next block
|
||
and contend with a shared monster. Match rooms enable occupant collision; the Tavern does not.
|
||
PvP combat is planned for a later milestone.
|
||
|
||
## Operator Policy
|
||
|
||
The server operator controls which modes are enabled and, for multiplayer, the economic model.
|
||
Configuration is driven through the existing `config/` layer (DB-backed, hot-reloadable) rather
|
||
than static env vars alone, so operators can adjust settings without a restart. A later extension
|
||
allows per-room settings.
|
||
|
||
Which modes are enabled is surfaced to clients in the `game_mode_info` event as
|
||
`modes: { solo, tavern, multiplayer }` (from `SOLO_ENABLED` / `TAVERN_ENABLED` /
|
||
`MULTIPLAYER_ENABLED`). The client shows or hides entry points from this, so any single mode can
|
||
run on its own — including a Tavern-only instance. Solo is on unless explicitly disabled;
|
||
Tavern and Multiplayer are opt-in.
|
||
|
||
Multiplayer economic models (operator-selected per instance):
|
||
|
||
| Model | Description |
|
||
|---|---|
|
||
| Free | No entry cost or payout. |
|
||
| Entry fee, leaderboard only | Players pay to enter; results affect a leaderboard, no payout. |
|
||
| Winner-take-pot | Entrants stake into a pot awarded to the winner. |
|
||
| Even-return | Entrants recover their stake; only a flat fee is deducted. |
|
||
|
||
The default configuration is conservative (free, no payout), matching the current wownerogue
|
||
instance. Operators are responsible for ensuring their chosen configuration complies with
|
||
applicable laws in their jurisdiction.
|
||
|
||
## Avatars
|
||
|
||
Avatars are cosmetic. Availability is controlled by Operator Policy — for example, a set available
|
||
to everyone plus additional avatars unlocked by credit purchases. The avatar id is stored on the
|
||
occupant and broadcast to the room; unlock eligibility is enforced at join time.
|
||
|
||
## Character Appearance Contract
|
||
|
||
The public identity shape is `appearance: { avatar, tint, equipment }`. The server normalizes this
|
||
shape, persists it on `users.appearance`, stores it on the `Occupant` at join time, and broadcasts
|
||
it in room snapshots. Renderers use `appearance` as the canonical cosmetic identity; the flat
|
||
`avatar` field remains as a compatibility shortcut and as the first discriminator for sprite/skin
|
||
loading.
|
||
|
||
This contract is intentionally renderer-agnostic:
|
||
|
||
- **Top-down grid packs** resolve `avatar` to a base tile/frame and composite `equipment` overlays
|
||
above it. `tint` recolours only the authored recolour pixels so outlines, skin, wood, and metal
|
||
remain intact.
|
||
- **Isometric packs** should keep the same `appearance` ids and supply an isometric resolver for
|
||
base frames, direction rows, anchor/feet position, and equipment overlays. If an exact item is
|
||
missing, the resolver should fall back by role (`round_shield -> shield -> none`) rather than
|
||
changing the saved identity.
|
||
- **True 3D packs** should resolve the same ids to GLB assets/material variants/animation clips.
|
||
Source models and intermediates stay out of git; shipped GLB or baked sprite outputs are hosted
|
||
and lazy-loaded like premium sprite sheets.
|
||
|
||
Premium costumes are catalog entries with an entitlement requirement. The client may hide or label
|
||
locked choices, but real gating belongs at the server/operator policy layer: identity saves and
|
||
join requests normalize unauthorized premium `avatar` ids to an allowed fallback.
|
||
|
||
Current baseline premium policy: a player is treated as premium after any successful credits
|
||
purchase (`users.total_credits_purchased > 0`). That unlocks the preview premium packs
|
||
(`generated-skins`, `iso-dungeon`, and `kenney-3d-characters`) unless a future operator policy
|
||
tightens them. The policy shape is already pack-based so products can sell or grant different packs
|
||
independently. Operator/admin grants go through
|
||
`user_pack_entitlements(user_id, pack_id, source, expires_at, metadata)`, while
|
||
`users.premium_level` is reserved for broader tiering.
|
||
|
||
## Product Grants
|
||
|
||
Payments stay separated from entitlement policy:
|
||
|
||
- **Single-game entries** buy one paid game attempt and do not grant premium by default.
|
||
- **Credits packages** grant credits and may bundle cosmetic/render packs or a premium tier.
|
||
- **Cosmetic products** grant packs or tiers directly and may grant zero credits.
|
||
|
||
Catalog configuration supports `grants` on credit packages and a separate `COSMETIC_PRODUCTS`
|
||
catalog. A grant payload is normalized to:
|
||
`{ credits, packs: [{ id, expiresAt, source }], premiumLevel }`.
|
||
|
||
Examples:
|
||
|
||
```json
|
||
{
|
||
"id": "supporter_25",
|
||
"label": "25 Credit Supporter Pack",
|
||
"credits": 20,
|
||
"bonus": 5,
|
||
"price": 1000000000000,
|
||
"grants": {
|
||
"packs": ["generated-skins", "kenney-3d-characters"],
|
||
"premiumLevel": "supporter"
|
||
}
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"id": "pack_3d",
|
||
"label": "3D Character Pack",
|
||
"price": 250000000000,
|
||
"grants": {
|
||
"packs": ["kenney-3d-characters"]
|
||
}
|
||
}
|
||
```
|
||
|
||
Confirmed product payments persist the normalized grant on `payments.product_grants`, update
|
||
credit balances only when credits are part of the product, and upsert pack ownership in
|
||
`user_pack_entitlements`. The client receives the updated entitlement snapshot through
|
||
`credits_update` / `identity_update`, so the same premium state gates character saves, tavern joins,
|
||
render modes, and later multiplayer rooms.
|
||
|
||
## Chat
|
||
|
||
The existing chat backend is retained and wrapped behind a `ChatProvider` interface so the backend
|
||
can be swapped later without changing callers. The current backend remains the default.
|
||
|
||
## Rendering (art pass)
|
||
|
||
Rendering goes through a shared, renderer-agnostic **scene model**: game/tavern state is adapted
|
||
into a `Scene` (a tile grid + entities), and any renderer draws it. This lives in
|
||
`html/js/render/` and is used by both the Tavern and (planned) the main game.
|
||
|
||
Render tiers:
|
||
- **ASCII** — monospace glyph grid (canvas). Always-available fallback; accessible.
|
||
- **Tiled** — coloured tiles + entity sprites (canvas 2D). The default. Real tilesets/atlases
|
||
slot in here later.
|
||
- **Fancy** — WebGL via PixiJS: a blurred bloom on entities, flickering warm lights along the
|
||
bar, drifting dust motes, a vignette, and smooth movement — all programmatic (no art assets).
|
||
- **Iso** — canvas isometric projection using Kenney's Isometric Miniature Dungeon runtime PNGs.
|
||
- **3D** — Three.js projection. It loads generated GLB avatars with `idle` / `run` / `jump`
|
||
clips when available and falls back to lightweight low-poly avatars during asset iteration.
|
||
- Planned: **Fancy ASCII** — shader-lit, animated glyphs.
|
||
|
||
Render mode is a per-user choice (persisted) with an operator default. Modes can be marked
|
||
**premium** — a cosmetic entitlement intended to be unlocked with credits (the same Operator
|
||
Policy pattern as avatar unlocks). Because rendering is client-side, this is a soft/cosmetic gate,
|
||
not a hard boundary; premium-only *assets* (special tilesets, shaders) are the real lever if
|
||
stronger gating is wanted. The render kit provides the engine and programmatic tiles/effects —
|
||
polished pixel-art tilesets, sprite sheets, and shader effects are a separate content effort.
|
||
|
||
**CSP note:** PixiJS compiles shaders with `eval`, which the app's strict CSP blocks. The Fancy
|
||
tier therefore loads `@pixi/unsafe-eval` (it precompiles shaders without eval) immediately after
|
||
`pixi.js`, so no `'unsafe-eval'` is added to the CSP. Any page using the Fancy renderer must
|
||
include both scripts, in that order.
|
||
|
||
## Asset delivery & performance
|
||
|
||
Rules for keeping the tree lean and the client fast:
|
||
|
||
- **Heavy source artifacts never go in git.** 3D models and generation intermediates (`*.glb`,
|
||
`*.fbx`, `*.blend`, …) are produced by the sprite pipeline (`~/src/docs/animated-sprite-pipeline.md`)
|
||
and are gitignored. Only the *outputs* the game loads are considered for shipping.
|
||
- **Generated / premium assets are hosted, not committed.** AI-generated premium skins live under
|
||
`html/assets/generated/` (gitignored) and are delivered from an external asset host / CDN by URL.
|
||
The base tier's Kenney CC0 tiles are small and stay in-repo.
|
||
- **Lazy-load by mode / entitlement.** Nothing heavy loads until it's actually needed:
|
||
- PixiJS (+ `@pixi/unsafe-eval`) loads only when the **Fancy** mode is first selected — ASCII and
|
||
Tiled need no WebGL library.
|
||
- A premium skin's sprite sheet is fetched only when that skin is selected and the player is
|
||
entitled to it.
|
||
- Three.js loads only when the **3D** mode is selected. Generated GLBs are emitted by
|
||
`scripts/build_kenney_3d_characters.py` from the local Kenney FBX sources into
|
||
`html/assets/generated/3d/` (gitignored).
|
||
- **Optimize what ships.** Sprite sheets are delivered as **WebP** (alpha), right-sized to display
|
||
resolution, content-hashed with a long cache TTL. (Example: the demo walk sheet is 633 KB as PNG,
|
||
65 KB as WebP — ~10×.)
|
||
|
||
## Milestones
|
||
|
||
Tavern:
|
||
- **T0** — Shared engine scaffold: `Room`, `Occupant`, tavern map, unit tests. (Done)
|
||
- **T1** — `TavernManager`: Socket.IO room, server-tick timer, join/move/leave broadcasts; minimal
|
||
client (`html/tavern.html`) rendering avatars; gated behind `TAVERN_ENABLED`. (Done)
|
||
- **T2a** — `ChatProvider` seam + tavern chat. (Done)
|
||
- **T2b** — Spectator camera into live games from the Tavern.
|
||
- **T3** — Avatar picker with policy-gated unlocks.
|
||
|
||
Multiplayer:
|
||
- **M0** — Match Room with a dungeon and shared monsters; players race the block. Includes a load
|
||
test with many simulated clients to measure server-tick performance under concurrency.
|
||
- **M1** — Player-vs-player combat.
|
||
- **M2** — Environmental hazards.
|
||
- **M3** — Operator Policy economic models wired in.
|
||
|
||
Rendering:
|
||
- **R0** — Shared render kit: scene model + ASCII / Tiled / Fancy (PixiJS) renderers + mode
|
||
switch, wired to the Tavern. Premium-mode gating scaffolded. (Done)
|
||
- **R1** — Wire the main game to the render kit (refactor the `display/` layer behind it).
|
||
- **R2** — Real tilesets / sprite sheets and the Fancy-ASCII tier.
|
||
- **R3** — Wire premium render modes to credit entitlements (Operator Policy).
|
||
|
||
All milestones are additive and config-gated; the single-player path is unchanged.
|
||
|
||
## Current status
|
||
|
||
`src/multiplayer/`
|
||
- `Occupant.js` — a shared-world avatar (position, name, avatar id, facing).
|
||
- `Room.js` — map/walkability, server-authoritative one-tile movement, optional occupant
|
||
collision, a `tick()` hook for future autonomous systems, and snapshot/full-state serialization.
|
||
- `tavernMap.js` — a procedurally generated (rectangular) default tavern layout.
|
||
|
||
`src/network/tavernManager.js` — transport + lifecycle: a shared Room, the server-tick timer,
|
||
and Socket.IO room broadcasts. Wired into `SocketHandlers` (`tavern_join` / `tavern_move` /
|
||
`tavern_leave`, plus disconnect cleanup and shutdown). Inert unless `TAVERN_ENABLED=true`.
|
||
|
||
`html/tavern.html` — a minimal self-contained client: connect, choose a name/avatar, enter the
|
||
room, walk with arrow keys / WASD, and chat. Renders the map and occupants on a canvas.
|
||
|
||
`src/network/chat/` — the `ChatProvider` seam. `ChatProvider` (interface) + `SocketChatProvider`
|
||
(default: Socket.IO delivery + Postgres history). Both the global chat (`ChatHandler`) and tavern
|
||
chat deliver through it, so the backend can be swapped (e.g. a Nostr channel) without changing
|
||
callers. Tavern chat is room-scoped and ephemeral.
|
||
|
||
`html/js/render/` — the render kit: `sceneModel.js` (renderer-agnostic scene + tavern adapter),
|
||
`asciiRenderer.js`, `tileRenderer.js`, `fancyRenderer.js` (PixiJS), and `renderModes.js`
|
||
(registry, factory with graceful fallback, entitlements, persistence). The tavern client uses it
|
||
with a mode toolbar; premium modes are marked and gated by `RK.entitlements`.
|
||
|
||
Tests: `test/tavernRoom.test.js` (engine), `test/tavernManager.test.js` (manager incl. chat),
|
||
`test/chatProvider.test.js`, `test/renderScene.test.js` (scene adapter),
|
||
`test/entitlements.test.js`, `test/identityService.test.js`, and `test/productGrants.test.js`.
|
||
End-to-end join/move/leave and chat over a real socket verified out-of-band.
|
||
|
||
Payment/product entitlement pieces:
|
||
- `src/migrations/020_user_appearance_and_pack_entitlements.sql` — persisted appearance,
|
||
premium tier, and pack grants.
|
||
- `src/migrations/021_payment_product_grants.sql` — product ids and normalized grants on payments.
|
||
- `src/network/identityService.js` — server-side appearance persistence and entitlement-aware
|
||
normalization.
|
||
- `src/payments/productGrants.js` — product grant normalization/public summaries.
|
||
- `src/multiplayer/entitlements.js` — server policy snapshot used by identity, tavern joins, and
|
||
payment confirmations.
|
||
|
||
Enable locally with `TAVERN_ENABLED=true` (optionally `TAVERN_TICK_MS`), then open `/tavern.html`
|
||
in two browser tabs.
|