Skip to content

Game Systems

local-code edited this page Sep 25, 2026 · 13 revisions

Game Systems

Equipment slots

Three slots coexist: weapon (attack), armor (damage reduction), and offhand (shields; small damage reduction). equip routes each piece by its type (weapon / armor / offhand); selling, dropping, or posting a worn piece unequips it. Incoming damage is reduced by worn defense plus any active damage-reduction buff. Example armor: Reinforced Leather (DR 2), Iron Plate (DR 3), Old Shield (offhand, DR 1).

Carry cap

Packs hold 24 units: worn gear and up to 5 arrows ride free. At the cap, take / gather / buy / market_buy refuse with a "pack full" error (gold is never charged on rejection); crafting, quest/commission rewards, and GM grants always go through. drop (single unit, optional "amount") exists solely to shed load, so it only works with a full pack. Pack load rides in stats as pack / pack_max.

Ammunition

Ranged weapons declare their ammo family root in world.json (currently the Oak Longbow needs "ammo": "arrow"). Any family member fires, best variant first, adding its flat bonus: arrow +0, iron arrow +1, steel arrow +2. Every attack with such a weapon equipped consumes 1 unit from inventory; firing empty-handed is rejected with an error and costs nothing. Arrows are ordinary junk otherwise (sellable, marketable, droppable): buy them from the merchant (2 gold), loot them from Cave Bandits, pick them up at the Lumber Camp, or craft all three variants. ML agents get buy_arrows / craft_iron_arrow / craft_steel_arrow actions plus arrows_norm (family count) and ammo_best_norm (best loaded bonus) scalars.

Adding NPCs / expanding the world

Everything lives in world.json:

  • Rooms need name, description, and exits (direction → room id).

  • NPCs need room, hp/max_hp, attack, hostile, behavior ("idle" or "wander"), loot, gold, and respawn_seconds.

  • Items need name, type ("weapon" grants attack bonus via damage), and any custom fields you want.

  • Recipes for crafting go in the recipes section. Each recipe has inputs and a result; optional output_qty creates a bundle (default 1), while optional tier and category fields classify recipes for the expanding crafting economy. Gather materials are sunk into recipes by difficulty: easy nodes feed tier-1 (reed fiber → bandage, salt + springwater → rations), medium nodes feed tier-2 (resin → oils, mountain herb → tonic, scrap iron → plate), heron feathers fletch steel arrows (tier 3).

    Multi-step pinnacles need crafted intermediates plus dungeon parts: Relic Aegis (floor-10 shard + Iron Plate + ectoplasm), Bulwark of the Deep (Warden's Trophy + troll hides + timber), Warden's Elixir (frost crystals + Ironhide Draught + ectoplasm), and the mid-tier Serpentbrand blade (serpent scales + iron + resin).

Consumable items may define a buff object with a category, amount, duration_actions, and optional description. Using one consumes the item and replaces the active effect in that category. Buffs are action-based, exposed in stats, and currently support attack bonuses and incoming-damage reduction.

Parties & the dungeon

Dungeons are instanced and per-party, not one shared map. The graveyard has an enter doorway. Entering auto-creates a solo party for the entering character (or reuses its existing party) and instantiates a private, 50-floor staircase owned by that party: every member walks the exact same floors, and other players/bots get their own instance. Floors are built lazily the first time anyone steps onto them.

Rules:

  • Auto-party: solo players enter alone; parties enter together and share one instance. party_invite/party_accept/party_leave/ party_info manage membership (max PARTY_MAX_MEMBERS = 4). Leaving a solo party destroys its dungeon instance. Leaving (or switching away from) a party whose dungeon still has live guards stamps a re-entry delay — quit-and-re-enter no longer mints a fresh Floor 1 on demand, which closes Floor-1 reset farming. Clean leaves and disconnects from cleared instances stamp nothing. Death in a dungeon also stamps abandon (respawn_player calls _stamp_dungeon_death), so dying no longer bypasses the delay (death-teleport closed).
    • Escalating cooldown: base 60s (DUNGEON_REENTER_DELAY_SECONDS)
    • 60s per consecutive death at depth (DUNGEON_REENTER_DELAY_GROWTH_SECONDS = 60; delay = base + (streak-1) x growth), resetting to base on a clear. Fresh characters are expected to fail Floor 1; prepared (geared, leveled) characters clear it — the dungeon is late-game content.
  • Enemies scale without bound — until floor 50. HP grows ×1.35 per floor, attack ×1.5, and guard count rises 1→4. Kills drop a Dungeon Relic worth more each floor. There is no level cap; nobody can farm it forever.
  • The Warden of the Deep holds the last floor (50): a single fixed boss (350 HP / 28 attack, 10-minute respawn) dropping a Warden's Trophy plus 150 gold. The trophy crafts into the Warden's Blade (fixed 13 damage, best weapon in the game — 1× Warden's Trophy + 2× Iron Ore + 1× Serpent Scale, tier 4; the Serpentbrand is the separate mid-tier blade). No scaling blades exist; the stairs crumble below floor 50.
  • Exit rules: floor 1 always has an up exit back to the graveyard (the retreat path). Deeper floors show no exits until the floor is cleared — once you descend, you're committed to that floor. A cleared floor shows up (and down to the next one). Moving up from a deep uncleared floor is rejected with The exit is sealed....
  • Floors are visible only once entered. Room snapshots describe just the room you're standing in; the next floor's layout, enemies, and loot are revealed only when you enter.
  • Cleared floors stay open — until the guards respawn. Guards always come back, on a per-floor timer that scales with depth (20 + 10 × floor seconds: 30 s on floor 1, 120 s on floor 10). When a guard returns to a cleared floor, the floor re-seals: the exits lock again, the blade reward is withdrawn, and you must fight the guard again to escape.

Because floors are generated on demand at runtime, the ML env picks up the dungeon automatically: its observation/action space already includes the dungeon rooms, guards, relic/blade items, and the enter/up/down moves.

Leveling & XP

Characters earn XP from kills, room discovery, dungeon floor clears, crafting, market trades, and quest turn-ins. The curve is closed-form: xp_to_next(level) = round(100 × 1.5^(level-1)). Leveling up grants +5 max HP and +1 attack and restores that much HP (not a full heal; login still fully restores). Levels persist in score_entries (restored on login). Brand-new characters are staked 10 gold on first login (STARTING_GOLD in server_config.json, once per score entry). TTL-evicted entries re-grant on return -- a ~10g/week welcome-back stipend at most. XP scales by the same variety × difficulty curve as score, so repeat-loop XP farms decay exactly like score farms (reported gains are post-curve). Tuning constants (LEVEL_HP_PER_LEVEL, LEVEL_ATK_PER_LEVEL, XP_BASE, XP_GROWTH) live in server_config.json; see Configuration. On death a character loses XP_LOSS_PCT% (5%) of their total XP, which de-levels them naturally when it crosses a threshold (no cap on levels lost, no separate de-level system).

Player market & GM treasury

Any logged-in player can post, buy, and cancel sell orders from anywhere in the world:

  • market_post removes the item from your inventory and lists it at your price (or the server's suggested price if you omit it).
  • market_buy charges your gold, takes the tax, pays the seller (offline sellers are credited a gold_bank paid out on their next login), and puts the item in your inventory. With no id it auto-buys the cheapest affordable order. There is no bank UI or balance readout — the payout simply arrives as gold on your next login.
  • Every completed sale pays TAX_RATE into the GM treasury (tax_treasury), tracked separately from the lifetime stat (tax_collected_lifetime). Rounding is commercial half-up (not banker's half-even), minimum TAX_MINIMUM = 1 gold, and the tax never takes the whole price — 1g trades pay the seller in full at 0 tax. An operator can seed initial liquidity with TEXTMMO_GM_SEED=<gold>.
  • Each seller holds at most MARKET_ORDER_SLOTS_BASE (3) open orders. Extra stall slots are bought with market_expand: the fee doubles per slot (50g, 100g, 200g, …) and goes to the GM treasury, so big traders fund the world events. Your slot count rides in stats as market_slots.

The GM treasury is spent on GM actions, only accepted on the dedicated GM stream (ws://127.0.0.1:8767, loopback-only) that the dashboard's GM tab connects to. The game port rejects all gm_* commands. There is deliberately no auth/token on GM actions — the only gate is the loopback-only stream. See Protocol for the GM command list.

Quests

Quests are repeatable objectives from specific NPCs. All live quests share the same pattern — accept by the NPC, complete the objective, turn in by the NPC — and take a quest field (guard_charm default) to select which one. Send {"cmd": "quest", "action": "list"} any time to see every quest, its giver and room, and whether you hold it or it is ready to turn in. New quest givers only need a QUEST_GIVERS entry. Quest givers cannot be attacked by players at all (town protection rejects the attack pre-damage); GM slay still works for stuck NPCs. (The old −0.5 score penalty branch still exists in the kill path but is unreachable via player attacks.)

1. Town Guard's charm quest (guard_charm): accept in Town Square → craft the Ancient Guardian Charm (1× Treant Bark + 1× Troll Hide + 1× Ectoplasm) → turn in for 50 XP, 25 gold, 15 score. Repeatable. Turn-in (and the ready flag) requires the crafted flag and the charm in your inventory — like remedy/tonic gate on their inputs — so dropping or selling the charm blocks turn-in instead of consuming what you don't hold.

2. Depth Delver quest (delver): accept (records cleared-floor baseline) → clear QUEST_DELVER_FLOORS (default 1) new floors → turn in for 30 XP, 15 gold, 10 score. Baseline resets, repeats forever.

3. Sister Maren's remedy quest (remedy, Healing Spring): bring 3× Healing Herb → 20 XP, 10 gold, 8 score. Repeatable.

4. Sister Maren's tonic quest (tonic): brew 1× Fortitude Tonic (Iron Ore + Mountain Berry + Mountain Herb) → 40 XP, 20 gold, 12 score. Repeatable.

Sister Maren also heals: {"cmd": "heal"} restores the character fully, but only on her tile. rest (+5 HP in rest areas) still works as field dressing.

Quest state rides along in stats events and persists in scores.json. Tuning constants live in the quests block of server_config.json (see Configuration) and are mirrored symbolically in ml_env.py (QUESTS). The /api/state snapshot exposes a quests section (catalog, live counts, lifetime completions, turn-ins/min).

Scoring system

Scores persist across restarts in scores.json. Points come from killing NPCs (weighted by difficulty) and discovering rooms, with a wealth-scaled penalty for death. Income-side tuning (#334): room discovery pays DISCOVERY_POINTS = 2 score + DISCOVERY_XP = 2 XP, and every world.json gather node pays score 1 — gather volume no longer dwarfs death cost the way the old 5/2 rates did.

Death also splits your carried gold: 40% drops as a loose pile where you fell (pick it up with {"cmd": "take", "item": "gold"}), 10% vanishes permanently, you keep the rest. The score penalty scales with what you lost: a flat 5.0 floor plus 0.1 per gold removed.

Risk zones (#157): in a room flagged risk in world.json, death additionally scatters the unequipped pack as floor piles (up to DEATH_ITEM_DROP_PCT% of it, floor value only), and that value joins the score penalty. Equipped weapon/armor/offhand are protected (never dropped); safe lands keep the gold-only rules above. No item immunity anywhere — protection is positional (safe lands, risk gating), never item-based. Every stats event carries a death_preview object telegraphing the worst case in your current room.

The system resists grinding the same loop forever via two mechanics:

  • Variety decay — repeating the same actions multiplies gains down.
  • Global difficulty curve — marginal reward shrinks as total score grows.

Tuning constants live in server_config.json (see Configuration); server.py holds code fallbacks.

Multiplayer mechanics

  • Ally attack bonus — other players in your room add +1 attack (capped at +3).
  • Teamwork kill splitting — damage contributors share a boosted point pool on kill. Cooperating is strictly better than solo grinding.
  • Parties — group up (party_invite → party_accept) to share one instanced dungeon. Kill credit is shared normally; party size expands the ally bonus and lets the group clear deep floors together.

Capacity limits / scaling

The server keeps per-room and per-name indexes (no full players scan on every event) and writes scores.json at most once every few seconds, so a bot swarm / ML farm can grow to hundreds of connections without bogging down.

  • MAX_TOTAL_CONNECTIONS (default 1000) — new connections beyond this are rejected. Env override: TEXTMMO_MAX_CONNECTIONS.
  • MAX_PLAYERS_PER_ROOM (default 12) — move into a full room is rejected. Env override: TEXTMMO_MAX_ROOM_PLAYERS.
  • SCORES_SAVE_SECONDS (default 5.0) — score flush interval.
  • SCORE_ENTRY_MAX (default 2000) / SCORE_ENTRY_TTL_SECONDS (default 7 days) — score entries untouched for the TTL are evicted (never online players or entries owed banked gold).
  • COMMISSION_TTL_SECONDS (default 1 hour) — completed/cancelled commissions are pruned. Open bounties hold real escrow and are never pruned. Repeated poster+filler pairs earn diminishing rewards (1/(1+prior_fills), floor 10%); escrow remainder is sunk to the treasury.
  • DUNGEON_MAX_FLOOR (default 50) — the stairs crumble below this.
  • Stale party invitations are dropped on disconnect.

Resilient background tasks

NPC AI, score persistence, and the dashboard snapshot each run in their own background task. If any crashes, the engine restarts it after a short delay rather than letting the world silently degrade.

Outbound queue

Each player gets a bounded outbound queue (TEXTMMO_OUTBOUND_QUEUE, default 64). When a slow or stuck client falls behind, old messages are dropped rather than stalling the event loop for everyone else.

Accounts & identity

Every name can only be used by one connection at a time. Trying to login with a name someone is already using returns an error; open a fresh connection to switch characters.

Token-protected names (optional): when a name is first used with a token, it is claimed; later attempts without the same token are rejected. Tokens persist in scores.json and survive restarts. To require tokens for every name, set TEXTMMO_REQUIRE_TOKEN=1. A token never affects gameplay or score — it only stops a different script from taking over someone's name. See Protocol for the login shapes.

Clone this wiki locally