-
Notifications
You must be signed in to change notification settings - Fork 0
Game Systems
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).
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.
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.
Everything lives in world.json:
-
Rooms need
name,description, andexits(direction → room id). -
NPCs need
room,hp/max_hp,attack,hostile,behavior("idle"or"wander"),loot,gold, andrespawn_seconds. -
Items need
name,type("weapon"grants attack bonus viadamage), and any custom fields you want. -
Recipes for crafting go in the
recipessection. Each recipe hasinputsand aresult; optionaloutput_qtycreates a bundle (default 1), while optionaltierandcategoryfields 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.
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_infomanage membership (maxPARTY_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_playercalls_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.
-
Escalating cooldown: base 60s (
-
Enemies scale without bound — until floor 50. HP grows
×1.35per floor, attack×1.5, and guard count rises 1→4. Kills drop aDungeon Relicworth 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 Trophyplus 150 gold. The trophy crafts into theWarden'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
upexit 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 showsup(anddownto the next one). Movingupfrom a deep uncleared floor is rejected withThe 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 × floorseconds: 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.
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).
Any logged-in player can post, buy, and cancel sell orders from anywhere in the world:
-
market_postremoves the item from your inventory and lists it at your price (or the server's suggested price if you omit it). -
market_buycharges your gold, takes the tax, pays the seller (offline sellers are credited agold_bankpaid out on their next login), and puts the item in your inventory. With noidit 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_RATEinto the GM treasury (tax_treasury), tracked separately from the lifetime stat (tax_collected_lifetime). Rounding is commercial half-up (not banker's half-even), minimumTAX_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 withTEXTMMO_GM_SEED=<gold>. - Each seller holds at most
MARKET_ORDER_SLOTS_BASE(3) open orders. Extra stall slots are bought withmarket_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 instatsasmarket_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 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).
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
riskinworld.json, death additionally scatters the unequipped pack as floor piles (up toDEATH_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. Everystatsevent carries adeath_previewobject 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.
- 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.
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.
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.
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.
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.