mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-21 16:18:33 +02:00
Hand-authored architecture docs (the 'why'/seams that autodocs can't capture):
- docs/ARCHITECTURE.md index + system map + the 5 pillars + chain profiles + conventions
- docs/CHAT_AND_NOSTR.md ChatProvider seam, bridge vs per-player signing, two-relay
tier model, verifyChatEvent impersonation guard, relay policy
- docs/PAYMENTS.md provider contract + registry, BTCPay/checkout/native, hot path
- docs/RULESETS.md Ruleset data object, win-condition strategies, PvP, MatchRoom seam
- docs/MONETIZATION.md credits, cosmetic_catalog, the four-way unlock rule, premium->tier
Autodocs (reference layer): added a docs:api npm script (jsdoc-to-markdown) that
regenerates docs/API.md from the existing JSDoc. Enable with 'cd src && npm install'
then 'npm run docs:api'. Hybrid by design — auto reference + hand-authored architecture.
No runtime code changed. Suite 56 suites / 407 tests green.
81 lines
3.9 KiB
Markdown
81 lines
3.9 KiB
Markdown
# Rulesets & PvP (P4)
|
|
|
|
Gameplay is a **data object**. Instead of hardcoding "first to the exit wins," the match engine reads a
|
|
`Ruleset`, so new modes (PvP last-alive, score attack, co-op) are data — not new subsystems.
|
|
|
|
## The Ruleset
|
|
|
|
`src/game/rulesets/Ruleset.js` — `defineRuleset(spec)` normalizes a partial spec to a frozen, clamped
|
|
object:
|
|
|
|
```
|
|
id, label, mode
|
|
world { difficultyPreset, maps }
|
|
entities { monster, monsterCount, pvpCombat }
|
|
players { min, max }
|
|
winCondition { type } first-to-exit | last-alive | high-score | all-escape
|
|
timing { tickMs, minDurationMs, hardCeilingMs, blockDeadline }
|
|
economy { model, houseFeePercent, payoutMultipliers }
|
|
```
|
|
|
|
`registry.js` ships the built-ins — `solo-classic` (descriptor for the live single-player mode),
|
|
`race`, `last-alive`, `score-attack`, `coop-escape` — plus `getRuleset(id)`, `listRulesets()`, and
|
|
`rulesetFromMatchOpts()` (maps the legacy MatchRoom option bag to a `race` ruleset, preserving old
|
|
behavior).
|
|
|
|
## Win conditions as strategies
|
|
|
|
`src/game/rulesets/winConditions.js` — each condition is a small strategy the `MatchRoom` consults at
|
|
three decision points:
|
|
|
|
```
|
|
onExit(room, id) a player reached the exit — end the match? set a winner?
|
|
onDeath(room) a player died — is the match over?
|
|
rank(room) final ordering (best-first) for placement + winnerId
|
|
```
|
|
|
|
- **`FIRST_TO_EXIT`** is a *verbatim port* of the original race logic, so existing matches are
|
|
byte-identical.
|
|
- **`LAST_ALIVE`** (PvP): reaching the exit means you *survived* (safe, not an instant win); the match
|
|
ends when ≤1 contender remains; `rank` orders last-standing → escaped → died-later. Combined with
|
|
`entities.pvpCombat`, stepping onto a living rival **strikes them down** (attacker holds position).
|
|
- **`HIGH_SCORE`**: no instant win; rank by score at the end.
|
|
- **`ALL_ESCAPE`** (co-op): ends when everyone alive has escaped.
|
|
|
|
## Where it plugs in
|
|
|
|
`MatchRoom` (`src/multiplayer/MatchRoom.js`) resolves a ruleset from `opts.ruleset` / `opts.rulesetId` /
|
|
legacy opts, then delegates: `_checkResolution` → `winCondition.onExit`, `_killPlayer` →
|
|
`winCondition.onDeath`, `finalize` → `winCondition.rank`. It honors `entities.monster` (optional monster)
|
|
and `entities.pvpCombat`, and takes timing from the ruleset. `MatchEngine` is a thin tick driver
|
|
(`room.resolveTick()` on an interval).
|
|
|
|
**Scope note:** the *live single-player* engine (`src/game/game.js`) is intentionally **not** refactored to
|
|
consume rulesets (too risky to touch the deployed path) — `solo-classic` is a catalog descriptor only. PvP
|
|
modes are opt-in via `rulesetId`; the match scheduler still builds `race`, and match mode is gated by
|
|
`MATCH_ENABLED` (inert by default), so nothing changes until wired.
|
|
|
|
## Economy per mode
|
|
|
|
A ruleset's `economy` field (`model`, `houseFeePercent`, `payoutMultipliers`) maps onto the existing
|
|
`gameModeManager` fields (single-player: multiplier-on-entry-fee) and `matchPayoutService` (PvP:
|
|
winner-takes-pot-minus-house). See [MONETIZATION.md](MONETIZATION.md).
|
|
|
|
## Pending: block-time-aware difficulty
|
|
|
|
`difficultyConfig` presets (easy/normal/hard/casino) tune dungeon size, monster behavior, treasure, and a
|
|
target house-win-rate — but currently key off payment mode, **not** mean block time. Coupling difficulty
|
|
to `ChainProfile.meanBlockTimeMsFor(chain)` (so Grin ~1 min stays fair vs BTC ~10 min) is a pending
|
|
game-balance change, not just wiring.
|
|
|
|
## Files
|
|
|
|
```
|
|
src/game/rulesets/Ruleset.js defineRuleset + WIN/ECONOMY enums
|
|
src/game/rulesets/winConditions.js FIRST_TO_EXIT (race) / LAST_ALIVE / HIGH_SCORE / ALL_ESCAPE
|
|
src/game/rulesets/registry.js built-ins + getRuleset/listRulesets/rulesetFromMatchOpts
|
|
src/multiplayer/MatchRoom.js consumes the ruleset; PvP combat; delegates win/lose/rank
|
|
src/multiplayer/MatchEngine.js tick driver
|
|
src/game/difficultyConfig.js world presets (the ruleset's `world` half)
|
|
```
|