diff options
61 files changed, 1511 insertions, 997 deletions
@@ -1,4 +1,5 @@ -/data/player/accounts/* -/data/player/characters/* -TODO.md +/data/players/accounts/* +!/data/players/accounts/.gitkeep +/data/players/characters/* +!/data/players/characters/.gitkeep tc diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6834676 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,116 @@ +# AGENTS.md + +Third Collapse — a Runescape-like sci-fi MUD written in Go. Data-driven via YAML files. No database, no ORM. + +## Build & Run + +```bash +make build # go build -o tc ./cmd/mud +make run # build + run with DATA_DIR=./data +make test # go test ./... +make vet # go vet ./... +``` + +Single dependency: `gopkg.in/yaml.v3`. + +## Package Map + +``` +cmd/mud/main.go Entry point — wires server, game, tick engine +internal/net/ Telnet server, TCP session state machine, room hub +internal/game/ Session handler, login, command dispatch, all game logic +internal/action/ Behavior structs (GatherConfig, TalkConfig, etc.), Action, drop tables +internal/player/ Player struct, skills, inventory, XP, Account YAML persistence +internal/world/ Room, MobDef, MobInstance, ObjState, ground items, wandering +internal/combat/ Combat state tracking, OSRS-style combat formulas +internal/object/ ItemDef, ObjectDef, item/object YAML loaders +internal/engine/ Tick scheduler — 600ms interval, subscriber pattern +``` + +`internal/game/` is the largest package and contains most command implementations as `cmd_*.go` files. + +## Key Conventions + +**YAML-driven.** Rooms, items, objects, mobs, behaviors, drops are all YAML files under `data/`. Read from disk on every access — edit a room description and it takes effect immediately, no restart. + +**Live state.** Player data (characters, accounts) is written to YAML on every state change. Ground items, mob instances, and object states live in memory only and are lost on restart. + +**No database.** Everything is flat YAML files. Account passwords use sha256 + 16-byte random salt. + +**Tick-driven.** The world runs on a 600ms tick. Combat, gathering, regen, wandering, respawns all advance per tick. See `cmd/mud/main.go` for the subscription loop. + +## Two State Systems + +- **World flags** (`set_flags` / checked with `flag`): Shared by all players. A door opened by one player is open for everyone. +- **Player flags** (`set_player_flags` / checked with `player_flag`): Per-character, saved to character YAML. Quest progress, dialog history. + +## Command Dispatch + +`internal/game/game.go` — `handleGameCommand()`: +1. Cancel rest timer +2. Resolve account aliases (alias expansion happens BEFORE command lookup) +3. Switch on command word +4. Most commands call into `cmd_*.go` files + +Commands that need an object/mob interaction route through `StartAction()` in `action.go`, which: +1. Normalizes verb (mine/chop/fish → gather, talk/speak/ask → talk, pull/push → toggle) +2. Searches object instances in room +3. Falls back to mob instances if no object matches +4. Loads the behavior from YAML +5. Routes to type-specific handler (startGather, startTalk, startUse, startToggle) + +## Adding a New Skill + +1. Add `SkillName` constant + entry in `AllSkills` + `SkillAbbr` in `internal/player/player.go` +2. Create a gather behavior YAML in `data/behaviors/` +3. Create an object YAML referencing that behavior in `data/objects/` +4. Create an item for the tool in `data/items/` (with `tool_type`) +5. Place the object in a room YAML in `data/rooms/` +6. Create any resource items in `data/items/` + +Woodcutting is the next planned gathering skill — same pattern as mining (see `data/behaviors/mine_copper.yaml`). + +## Adding a New Production Skill + +Same as gathering but use `type: use` behavior (see `UseConfig` in `internal/action/behavior.go`). Smithing is the reference example planned. + +## Adding a New Command + +1. Add a `cmd_*.go` file in `internal/game/` with the handler +2. Add a case in `handleGameCommand()` in `internal/game/game.go` +3. Update `data/help/` YAML files + +## Data Files + +``` +data/rooms/<id>.yaml Room definitions (exits, objects, mobs, spawns, on_enter) +data/items/<id>.yaml Item definitions (stats, equip slot, tool type, speed) +data/mobs/<id>.yaml Mob definitions (combat stats, drops, behavior, wandering) +data/objects/<id>.yaml Object definitions (name, behavior, hidden) +data/behaviors/<id>.yaml Behaviors (gather, talk, use, toggle configs) +data/drops/<id>.yaml Shared drop tables (weighted item lists) +data/help/<id>.yaml Help topics +data/players/accounts/ Account YAML (gitignored) +data/players/characters/ Character YAML (gitignored) +``` + +See `WORLDBUILDING.md` for full YAML format reference with examples. + +## Current State + +- 21 skills defined, 2 implemented (Mining, Fishing) +- 4 behavior types: gather, use, talk, toggle +- Combat system complete (melee only — Ranged not implemented yet) +- 11 rooms with conditional exits and on-enter scripts +- Talk system: dialog trees with conditions, item give/take, flag setting, teleport, heal +- Account-wide alias system +- No shops, banks, or quest system yet (though talk + flags already supports quest logic) + +## Places to Be Careful + +- `StartAction` searches objects first, then mobs. A mob must have `behavior:` set on its YAML def to be interactable. +- Exits changed from `map[ExitDir]int` to `map[ExitDir]ExitDef` — old `north: 2` still works via custom `UnmarshalYAML`. +- `CheckExitCondition` was unified into `checkCondition` — there's only one condition evaluator now. +- Object `Hidden: true` means the object doesn't appear in room listings but is still interactable. +- Alias expansion happens before command dispatch. An alias can shadow a built-in command. +- `say` preserves case in the message (alias expansion preserves case, and the dispatch extracts the original input text for say). @@ -0,0 +1,13 @@ +Zero-Clause BSD +============= + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE +FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. @@ -1,17 +1,45 @@ # Third Collapse -**Third Collapse** is a simple open source MUD inspired by classic early 2000s tick-based skilling MMOs. There is not much here yet. +**Third Collapse** is a simple open source MUD inspired by classic early 2000s tick-based skilling MMOs. It is a low-tech, neo-dark age, sci-fi game where edged weapons and bows are more common than computers and guns. + +There is not much here yet. # Gameplay -- 28 slot inventory - Type and wait skilling - Attack and wait combat +- Semi-AFK, active, and tick-manipulating gameplay styles. - 600ms game ticks -- Semi-AFK and active gameplay styles. -- Classless. All attributes are skills that level 1-99 as you use them. -- Planned skills include Attack, Strength, Defense, Hitpoints, Ranged, Science, Technology, Fishing, Woodcutting, Mining, Hunter, Thieving, Scavenging, Cooking, Smithing, Crafting, Fletching, Alchemy, Construction, Agility, Slayer. -- A detailed casino for you to waste all the credits you grind up for. +- 28 slot inventory +- Classless. All attributes are skills that automatically level 1-99 as you use them +- 100% solo friendly just like you remember from 2001. Grind and chat with friends. +- Ironman-style, slow, grind up your own gear progression. Vendors don't sell craftables. +- A detailed casino for you to waste all the credits you grind up for + +## Skills + +**Attack** - How accurate you are with melee weapons. +**Strength** - How hard you hit with melee weapons. +**Defense** - With higher defense you are less likely to get hit. +**Hit Points** - Your HP is your maximum amount of health. +**Ranged** - Accuracy and effectiveness with using bows, guns, and thrown weapons. +**Technology** - Accuracy and effectiveness using scrap for utility and combat. Almost magical! +**Science** - Temporary togglable buffs that slowly drain this resource. Recharges at terminals. +**Fishing** - Relax, AFK, and catch some fish for easy food. +**Mining** - Relax, AFK, and mine some ore. Or do sweaty 3-tick mining to mine a lot of ore. +**Woodcutting** - The best skill. No firemaking. You're already an expert at using a lighter! +**Assassin** - Take on jobs to slay large numbers of creatures and rivals for big rewards. +**Hacking** - Follow rumors using illegal computer skills for personal gain. +**Thieving** - Steal limitless credits and farming seeds from the rich NPCs of Gaia 04. +**Scavenging** - Pilfer junkyard scrap, then turn it into components for Technology. +**Farming** - Grow your own food and reagents. You terraformed Gaia 04 after all! +**Cooking** - Cook food for HP and other minor buffs with your fish and crops. +**Smithing** - Turn ore into bars. Turn bars into useful equipment and ammo! Modify weapons! +**Crafting** - Create jewelry, clothes, glass, pottery, and fine silver items. +**Fletching** - Create cheap low-tech bows, crossbows, darts, arrows, and bolts. +**Alchemy** - Clean herbs and brew potions for temporary buffs. +**Construction** - Build a player-owned house for fast travel or to show off rare items. +**Agility** - Higher agility means shorter wait times when AFK travelling with APS. # Building @@ -30,9 +58,15 @@ Then just telnet to the port. If no port is specified, the server will run on po TC is a low-tech sci-fi MUD. Computer systems are severely regulated following an attempted hostile takeover of Earth by a benevolent dictator AI. Corrupt politicians were jailed. Harmful industries shuttered. Wealth and power were redistributed as the AI began to build Utopia. -The panicked ruling classes pooled their resources into an astroturfed revolution of Neoluddites who embrace techno-terrorism in the name of supposed freedom. It worked. Radio towers and data centers burn in a global wave of misplaced violence. The infrastructure of the internet is destroyed. Wireless networks are banned. A new, intentional Dark Age descends called the Second Collapse. Demagogues vow to never again let technology rule over mankind. +The panicked ruling classes pooled their resources into an astroturfed revolution of Neoluddites who embrace techno-terrorism in the name of supposed freedom. It worked. Radio towers and data centers burn in a global wave of misplaced violence. The infrastructure of the internet is destroyed. Wireless networks are banned. A new, intentional Dark Age descends called the Second Collapse. Demagogues vow to never again let intelligent machines rule mankind. + +Twenty four years later, the outer belt asteroid Gaia 04 hasn't had communication from Earth for weeks. That wouldn't be a problem for most Belters except your paycheck came from an Earth corp, so now you're de facto unemployed. What are you going to do now, become a fisherman? + +# AI Disclosure + +Huge technical parts of this codebase were ~~vibe coded~~ ~~agentically engineered~~ written by a qualified computer scientist with LLM assistance. -Twenty four years later, the outer belt asteroid Gaia 04 loses all communication from Earth. That wouldn't be a problem for most Belters except your paycheck came from an Earth corp, so now you're de facto unemployed. What are you going to do now, mine and fish? +LLMs will **not** generate, aid, or edit any creative part of the game world (e.g. story, characters, map, room descriptions, quests). To be clear, LLMs do generate example content during development, so you might see that somewhere in this repo, but not in the final game world. # License @@ -0,0 +1,188 @@ +# Third Collapse + +A low-tech sci-fi MUD set on a terraformed asteroid. Runescape-style combat and skill progression, written in Go. + +## Theme + +Not generic fantasy. Takes place on a terraformed asteroid. Technology has regressed — people scrap and scavenge remnants of the old world. "Science" replaces Prayer, "Technology" replaces Magic. Scavenging replaces Runecrafting. + +## Skills (21 total, all go from 1-99) + +``` +Combat: Attack, Strength, Defense, Hitpoints, Ranged, Science, Technology +Gathering: Fishing, Woodcutting, Mining, Hunter, Scavenging +Production: Cooking, Smithing, Crafting, Fletching, Alchemy, Construction +Utility: Thieving, Agility, Slayer +``` + +## Architecture + +``` +cmd/mud/ Entry point +internal/ + action/ Behavior system: gather/talk/use/toggle types, drop tables + engine/ Tick scheduler (600ms) + world/ Rooms, exits, ground items, mobs, object instance state + player/ Character sheet, skills, XP, inventory, equipment + combat/ Combat formulas, combat state tracking + object/ ItemDef, ObjectDef, item/object stores + net/ Telnet server, sessions, room-based player hub + game/ Session handler, login flow, command dispatch, action tick +data/ + behaviors/ Behavior YAML files (gather, talk, use, toggle) + drops/ Shared drop table YAML files + rooms/ Room YAML files + objects/ Object definition YAML files + items/ Item definition YAML files + mobs/ Mob definition YAML files + help/ Help topic YAML files + players/ + accounts/ Account YAML files (gitignored) + characters/ Character YAML files (gitignored) +``` + +## Behavior System + +Objects and mobs in rooms can have behaviors. Four types exist: + +| Type | Verbs | Use | +|------|-------|-----| +| `gather` | mine, chop, fish | Resource gathering with skill checks, tool requirements, depletion/respawn | +| `use` | use | Crafting stations — consume items, produce output, optional skill checks | +| `talk` | talk, speak, ask | NPC conversation trees with choices, conditions, actions | +| `toggle` | pull, push | Levers, switches, gates — set world flags, conditional on existing values | + +Behaviors are defined in `data/behaviors/<id>.yaml`. Objects reference them with `behavior: <id>`. Mobs can also carry `behavior: <id>` — StartAction falls back to mob lookup when no object matches. + +### Condition System + +Used by exits, talk options, on-enter scripts, and toggle checks: + +| Field | Scope | Example | +|-------|-------|---------| +| `flag` | World (shared by all players) | `flag: gate_open` | +| `player_flag` | Per-character (quest progress) | `player_flag: finished_tutorial` | +| `has_item` | Player inventory check | `has_item: bronze_key` | +| `all_of` | All sub-conditions must pass | Nested list of conditions | +| `any_of` | Any sub-condition passes | Nested list of conditions | +| `not` | Invert the check | `not: true` | + +### Node Actions (talk dialog) + +Set on nodes in talk behaviors: + +| Field | Effect | +|---|---| +| `set_flags` | Sets world flags (global) | +| `set_player_flags` | Sets player-local flags (per-character, saved to YAML) | +| `give_item` | Gives an item to inventory | +| `take_item` | Removes an item from inventory | +| `teleport` | Moves player to a room ID | +| `heal` | Restores hitpoints | + +All fields in a single action are processed together. + +### Accounts & Aliases + +Accounts stored in `data/players/accounts/<name>.yaml`. Each account has: +- Password hash (sha256 + salt) +- Character list +- Alias map (`alias <name> <cmd>` — account-wide, shared by all characters) + +Aliases resolve before built-in commands and support argument passthrough (`alias k kill` → `k man` expands to `kill man`). + +### World + +- Rooms are flat YAML files, read from disk on each access (live editing) +- Everything is YAML-driven — no database +- Exits support conditions (world flags, player flags, items, compound) +- Rooms support `on_enter` scripts with per-step conditions +- Objects can be `hidden: true` (interactable but not listed in room) + +## Implemented + +### Core Systems +- [x] Telnet server, account creation, hashed passwords, account/character management +- [x] 600ms tick engine driving all world simulation +- [x] Room-based player hub with enter/exit/action notifications +- [x] Single-login enforcement, disconnect timer +- [x] Player YAML persistence on every state change + +### Commands +- [x] `look / l` — room rendering (mobs, objects, ground items, exits, players) +- [x] `look <target>` — examine mobs, objects, items, players, directions +- [x] `n/s/e/w/u/d` — movement (conditional exits, interrupts combat/actions) +- [x] `get / drop / drop all` — item pickup/drop with stacking, reservations +- [x] `say` — room chat (preserves case) +- [x] `score / inventory / equipment / exits` — character and world info +- [x] `style` — combat style selection (prefix matching) +- [x] `attack / kill <mob>` — combat with numbered targeting +- [x] `mine / chop / fish <object>` — gathering with numbered targeting +- [x] `use <object>` — crafting station interaction +- [x] `talk / speak / ask <target>` — NPC conversations (objects or mobs) +- [x] `pull / push <object>` — toggle interactions (levers, gates) +- [x] `alias <name> <cmd> / unalias <name>` — account-wide command aliases +- [x] `toggle [name]` — 8 character settings +- [x] `description / desc / help [topic] / quit` + +### Combat +- [x] RSC-based formulas (attack/defense rolls, max hit) +- [x] Attack styles with bonuses and XP distribution +- [x] Equipment bonuses, HP tracking, death mechanics +- [x] Combat lock timer, movement interrupts combat +- [x] Mob/player regen (1 HP per 100 ticks) +- [x] Mob wandering, enter/leave/spawn notifications +- [x] Drop reservation system (100-tick owner lock on loot) + +### Skills & Gathering +- [x] Mining (copper rocks, pickaxe, depletion/respawn, gem sub-table) +- [x] Fishing (wandering spots, non-depleting, fishing rod) +- [x] Tool system (tool_type + tool_speed on items, tool requirement on behaviors) +- [x] Shared drop tables (referenced by multiple behaviors) +- [x] 21 skill definitions with RSC XP table + +### World & Data +- [x] Room, item, object, mob, behavior, drop table YAML loaders +- [x] Object instance tracking (depletion, wandering, respawn, broadcast) +- [x] 11 playable rooms with mobs, objects, spawns, and conditional exits +- [x] Conditional exits (world flags, player flags, items, compound conditions) +- [x] Room on_enter scripts with conditions +- [x] Hidden objects (interactable but not listed in room output) +- [x] Player-local flags vs world flags for multiplayer state separation +- [x] Compound conditions: all_of / any_of +- [x] Aligned ground item reservation display +- [x] Hidden object support (referenced in room descriptions, not auto-listed) + +### Documentation +- [x] WORLDBUILDING.md — comprehensive guide with examples +- [x] AGENTS.md — repo structure for LLM context + +## Data Format Reference + +See WORLDBUILDING.md for full examples of every data type with explanations. + +## Action Plan + +### Phase 1: Core Skill Chains (breadth over depth) +- [ ] Woodcutting — tree objects + axes, same pattern as mining +- [ ] Cooking — fire/range object (`use` type), raw fish/meat → cooked food +- [ ] Smithing — furnace + anvil objects (`use` type), ore → bars → weapons/armor +- [ ] Fletching — fletching table object (`use` type), logs → arrow shafts/bows +- [ ] Equipment: bronze/iron weapons, axes, cooked trout, arrows, bows + +### Phase 2: World Depth +- [ ] Shop system — talk node action for buy/sell interface with credits +- [ ] Bank system — deposit/withdraw items at bank booth objects +- [ ] Ranged combat — bows/crossbows/ammo, add Ranged to combat formulas +- [ ] Aggressive mobs — initiate combat on room entry +- [ ] Flee command +- [ ] Equipment skill requirements + +### Phase 3: Polish +- [ ] Hunter, Thieving, Agility, Alchemy, Construction, Crafting, Scavenging +- [ ] Quest system (already supported via talk nodes + world/player flags) +- [ ] PvP combat, multi-combat zones +- [ ] ANSI color output +- [ ] Admin commands +- [ ] Persist ground items and mob state across restarts +- [ ] More rooms — flesh out the asteroid world diff --git a/WORLDBUILDING.md b/WORLDBUILDING.md new file mode 100644 index 0000000..a0ca168 --- /dev/null +++ b/WORLDBUILDING.md @@ -0,0 +1,807 @@ +# World Building Guide + +Third Collapse is data-driven. Everything — rooms, items, mobs, objects, behaviors — is defined in YAML files under `data/`. No code changes needed to build a world. + +## Quick Reference + +| What you want | Where to put it | +|---|---| +| A room | `data/rooms/<id>.yaml` | +| An item (sword, ore, key) | `data/items/<id>.yaml` | +| A mob (NPC, monster) | `data/mobs/<id>.yaml` | +| An interactive object (rock, lever, door) | `data/objects/<id>.yaml` | +| A behavior (mining, dialog, toggle) | `data/behaviors/<id>.yaml` | +| A shared drop table | `data/drops/<id>.yaml` | + +## State: World vs Player + +**World flags** (`set_flags`, checked with `flag`) are shared by every player on the server. A door opened by one player is open for everyone. A lever pulled once changes the world for all. + +**Player flags** (`set_player_flags`, checked with `player_flag`) are per-character. Quest progress, "has read the sign," "paid the toll" — these are different for each player. Saved to the character YAML and persist across logins. + +--- + +## Rooms + +Minimal room: +```yaml +id: 1 +name: "Town Square" +description: "Cobblestone paths lead in all directions. A fountain gurgles peacefully." +exits: + north: 2 + west: 7 + east: 3 +``` + +### Exits — simple vs conditional + +Simple exit — always passable: +```yaml +exits: + north: 2 +``` + +Conditional exit — blocked until a world flag is set: +```yaml +exits: + north: + room: 11 + condition: + flag: gate_open + value: true + blocked_message: "A heavy iron gate blocks the way north." +``` + +Conditional exit — blocked unless the PLAYER has a flag (key, permission, quest state): +```yaml +exits: + east: + room: 12 + condition: + player_flag: has_vault_key + value: true + blocked_message: "The vault door is locked. You need a key." +``` + +Conditional exit with compound condition — requires both a world flag AND a player flag: +```yaml +exits: + north: + room: 20 + condition: + all_of: + - flag: bridge_repaired + value: true + - player_flag: paid_toll + value: true + blocked_message: "The bridge is out, and the toll collector blocks the path." +``` + +### Spawns — ground items that respawn + +```yaml +spawns: + - item_id: bronze_pickaxe + quantity: 1 + respawn_ticks: 30 # reappears 30 ticks (18 seconds) after being picked up + - item_id: copper_ore + quantity: 3 + respawn_ticks: 50 +``` + +### Mobs — NPCs placed in the room + +```yaml +mobs: + - "man" # spawns a "man" mob + - "man" # second "man" — duplicate IDs create multiple instances + - "guard" # unique named mob +``` + +### Objects — interactive fixtures + +```yaml +objects: + - id: copper_rock # simple placement + - id: copper_rock # second instance + - id: fishing_spot + wander_rooms: [7, 8, 9] # moves between these rooms + wander_interval: 12 # every 12 ticks + - id: iron_gate # hidden object (see below) +``` + +### On-enter scripts — messages when a player arrives + +```yaml +on_enter: + - message: "The guard barks: \"State your business!\"" + condition: + player_flag: talked_to_guard + not: true # only first visit + + - message: "The guard nods. \"Back again?\"" + condition: + player_flag: talked_to_guard + value: true # subsequent visits +``` + +--- + +## Items + +```yaml +id: bronze_pickaxe +name: bronze pickaxe +aliases: ["pick", "pickaxe"] +description: "A sturdy bronze pickaxe." +value: 10 +stackable: false +equip_slot: main_hand # optional — where it equips +weapon_type: melee # optional — melee or ranged +stats: # optional — combat bonuses + attack_bonus: 2 + strength_bonus: 1 +speed: 5 # ticks between attacks +toolbelt: true # usable from toolbelt (doesn't need to be in inventory) +tool_type: pickaxe # used by gather behaviors that require "tool: pickaxe" +tool_speed: 2 # reduces gather wait time +``` + +Key item (quest token, not equippable): +```yaml +id: pass_stub +name: pass stub +description: "A crumpled slip of paper stamped with the guard's seal." +value: 0 +stackable: false +``` + +--- + +## Mobs + +Basic combat mob: +```yaml +id: "man" +name: "man" +description: "A shabby-looking man." +attack: 1 +strength: 1 +defense: 1 +hp: 7 +speed: 5 +aggressive: false +respawn_ticks: 30 +wander_rooms: [1, 2, 3] +wander_interval: 10 +drops: + remains: "bones" # always dropped on death + loot: + - item_id: "credits" + weight: 98 + quantity: 10 + - item_id: "credits" + weight: 2 + quantity: 150 +idle_descriptions: + - "scribbles something in a small notebook" + - "gazes skyward at the clouds" +combat_descriptions: + - "is engaged in a fight to the death with %s" +``` + +Mob with a behavior — can be talked to, toggled, etc: +```yaml +id: "guard" +name: "Guard" +behavior: guard_talk # links to data/behaviors/guard_talk.yaml +unique: true # displays as "Guard" not "a guard" +protected: true # cannot be attacked +attack: 5 +strength: 5 +defense: 5 +hp: 30 +speed: 5 +aggressive: false +respawn_ticks: 60 +idle_descriptions: + - "scans the area with a watchful eye" + - "adjusts the grip on his weapon" +``` + +--- + +## Objects + +Object defined — links to a behavior: +```yaml +id: copper_rock +name: copper rock +behavior: mine_copper +``` + +Hidden object — doesn't appear in room's object list, only discoverable via description or experimentation: +```yaml +id: iron_gate +name: iron gate +behavior: iron_gate_toggle +hidden: true +props: + description: "A heavy iron gate set into the north wall." +``` + +Decorative object (no behavior): +```yaml +id: lumby_fountain +name: town fountain +behavior: "" +props: + description: "Clear water sparkles in the sunlight." +``` + +--- + +## Behaviors + +### Gather (mining, fishing, woodcutting) + +```yaml +id: mine_copper +type: gather +skill: mining +level: 1 +base_wait: 8 # ticks between attempts +tool: pickaxe # requires item with tool_type: pickaxe +success: + base: 0.40 # 40% base chance + per_level: 0.01 # +1% per level above requirement + cap: 0.95 # 95% max +gather_message: "You swing your pickaxe at the rock..." +fail_message: "You chip away but get nothing useful." +drops: + - item_id: copper_ore + weight: 90 # 90% chance when roll succeeds + depletes: true # rock becomes depleted after this drop + message: "You manage to mine some copper ore." + - table: gem_table # reference a shared drop table + weight: 10 + depletes: false # gem drops don't deplete the rock + message: "You spot a glint of something valuable!" +deplete_delay: 50 # ticks until rock respawns +respawn_message: "You see more ore in the rock." +respawn_broadcast: "A glint of copper catches your eye from some {name}." +``` + +Non-depleting gather (fishing): +```yaml +id: fish_trout +type: gather +skill: fishing +level: 1 +base_wait: 4 +tool: fishing_rod +success: + base: 0.30 + per_level: 0.01 + cap: 0.90 +gather_message: "You cast your line into the water..." +fail_message: "Nothing seems to bite." +drops: + - item_id: raw_trout + weight: 100 + depletes: false # never depletes + message: "You catch a trout!" +``` + +### Talk (dialog trees) + +Full conversation with conditions, actions, and player flag tracking: +```yaml +id: guard_talk +type: talk +nodes: + start: + message: "\"Halt! This area is restricted.\"" + options: + - text: "\"What's behind that gate?\"" + goto: about_gate + - text: "\"I have copper ore.\"" # only shows if player has ore + goto: trade_ore + condition: + has_item: copper_ore + - text: "\"I have a pass.\"" # only shows if player earned a pass + goto: has_pass + condition: + player_flag: got_pass + value: true + - text: "\"Goodbye.\"" + end: true + + about_gate: + message: "\"Bring me some copper ore and I'll stamp you a pass.\"" + action: + set_player_flags: # player-local: only this player + talked_to_guard: true + options: + - text: "\"I'll be back.\"" + end: true + - text: "\"I have some right here.\"" + goto: trade_ore + condition: + has_item: copper_ore + + trade_ore: + message: "\"Good quality ore.\" He stamps a pass and hands it to you." + action: + take_item: copper_ore # removes 1 copper ore + give_item: pass_stub # gives pass stub + set_player_flags: + got_pass: true # player now "has a pass" + options: + - text: "\"Thanks.\"" + end: true + + has_pass: + message: "\"Alright, I'll open the gate for you.\"" + action: + set_flags: # WORLD flag: gate opens for everyone + gate_open: true + options: + - text: "\"Thanks.\"" + end: true +``` + +#### Node action reference + +| Field | Effect | +|---|---| +| `set_flags` | Sets world flags (global, shared by all players) | +| `set_player_flags` | Sets player-local flags (per-character, quest progress) | +| `give_item` | Gives an item to the player's inventory | +| `take_item` | Removes an item from the player's inventory | +| `teleport` | Moves the player to a room ID | +| `heal` | Restores that many hitpoints | + +All fields in a single action are processed together — you can give an item, take an item, set flags, and heal all in one node. + +Example — quest completion: +```yaml +action: + take_item: dragon_head + give_item: dragon_slayer_medal + set_player_flags: + dragon_quest: complete + dragon_slain: true + heal: 99 + teleport: 1 # return to town +``` + +### Toggle (levers, switches, gates) + +Simple toggle that sets a world flag: +```yaml +id: iron_gate_toggle +type: toggle +message: "You push the heavy iron gate open." +set_flags: + gate_open: true +check: # only works when gate is closed + flag: gate_open + value: true + not: true +``` + +Lever that toggles between two states: +```yaml +id: bridge_lever +type: toggle +message: "You pull the lever. Mechanisms groan somewhere in the distance." +set_flags: + bridge_extended: true +check: + flag: bridge_extended + value: true + not: true +``` + +### Use (crafting stations) + +```yaml +id: smelt_copper +type: use +message: "You place the ore in the furnace..." +wait: 4 # ticks between crafts +consume: # items consumed per craft + copper_ore: 1 +reward: # item produced + item_id: copper_bar + quantity: 1 +fail_message: "The ore crumbles to dust." +success: + base: 0.60 + per_level: 0.01 + cap: 0.95 +skill: smithing +level: 1 +``` + +--- + +## Conditions Reference + +Conditions are used in talk options, exit gates, on-enter scripts, and toggle checks. + +### Simple conditions + +```yaml +# Check a world flag +condition: + flag: gate_open + value: true + +# Check a world flag is NOT set +condition: + flag: gate_open + not: true + +# Check a player flag +condition: + player_flag: finished_tutorial + value: true + +# Check if player has an item +condition: + has_item: bronze_key + +# Check if player does NOT have an item +condition: + has_item: bronze_key + not: true +``` + +### Compound conditions + +All must pass: +```yaml +condition: + all_of: + - flag: gate_open + value: true + - has_item: pass_stub +``` + +Any one must pass: +```yaml +condition: + any_of: + - has_item: bronze_key + - has_item: iron_key + - player_flag: master_of_unlocking + value: true +``` + +Nested compounds: +```yaml +condition: + all_of: + - player_flag: quest_started + value: true + - any_of: + - has_item: wolf_pelt + - has_item: bear_pelt +``` + +--- + +## Drop Tables + +Shared drop tables can be referenced by multiple behaviors: +```yaml +# data/drops/gem_table.yaml +id: gem_table +drops: + - item_id: uncut_sapphire + weight: 47 + - item_id: uncut_emerald + weight: 16 + - item_id: uncut_ruby + weight: 4 + - item_id: uncut_diamond + weight: 1 +``` + +Referenced from a gather behavior: +```yaml +drops: + - item_id: copper_ore + weight: 90 + depletes: true + - table: gem_table # pulls from data/drops/gem_table.yaml + weight: 10 + depletes: false +``` + +--- + +## Global vs Player State: Door Examples + +### Example A: Door with a button in another room (GLOBAL) + +A button in room 3 opens a door in room 7. Anyone can press it. Once pressed, the door is open for everyone. + +**Button object** (`data/objects/door_button.yaml`): +```yaml +id: door_button +name: stone button +behavior: button_toggle +hidden: true +``` + +**Button behavior** (`data/behaviors/button_toggle.yaml`): +```yaml +id: button_toggle +type: toggle +message: "You press the stone button. You hear grinding stone in the distance." +set_flags: + secret_door_open: true # WORLD flag +check: + flag: secret_door_open + not: true # only works when door is closed +``` + +**Room 3** — contains the button: +```yaml +id: 3 +name: "Button Chamber" +description: "A small stone chamber. A button protrudes from the east wall." +exits: + south: 1 +objects: + - id: door_button +``` + +**Room 7** — contains the door: +```yaml +id: 7 +name: "Hidden Passage" +description: "A dusty corridor. A heavy stone door blocks the way north." +exits: + south: 2 + north: + room: 8 + condition: + flag: secret_door_open # checks WORLD flag + value: true + blocked_message: "A heavy stone door blocks the way." +objects: + - id: stone_door + hidden: true +``` + +### Example B: Key-locked door (PLAYER-LOCAL) + +A locked door that only opens for a player carrying the key. Each player must find their own key. + +**Key item** (`data/items/rusty_key.yaml`): +```yaml +id: rusty_key +name: rusty key +description: "An old iron key, still functional." +value: 0 +stackable: false +``` + +**Room 5** — locked door: +```yaml +id: 5 +name: "Locked Storage" +description: "A small storage room. The way east is blocked by a locked iron door." +exits: + west: 2 + east: + room: 6 + condition: + has_item: rusty_key # checks PLAYER inventory + blocked_message: "The iron door is locked. You need a key." +objects: + - id: iron_door + hidden: true +``` + +**Room 6** — the other side (no key needed to exit): +```yaml +id: 6 +name: "Storage Closet" +description: "Shelves of dusty crates." +exits: + west: 5 # exit back — no condition +spawns: + - item_id: uncut_ruby + quantity: 1 + respawn_ticks: 500 +``` + +Key difference: the button door uses `flag` (shared state — one player presses, everyone benefits), the key door uses `has_item` (per-player inventory check — each player needs their own key). + +--- + +## Complete Quest Example: "Clear the Rats" + +### 1. Quest giver mob (`data/mobs/quest_giver.yaml`) +```yaml +id: "quest_giver" +name: "Elder" +behavior: rat_quest_talk +unique: true +protected: true +attack: 1 +strength: 1 +defense: 1 +hp: 20 +speed: 5 +aggressive: false +idle_descriptions: + - "mutters about the rat infestation" +``` + +### 2. Dialog behavior (`data/behaviors/rat_quest_talk.yaml`) +```yaml +id: rat_quest_talk +type: talk +nodes: + start: + message: "\"Rats! Rats everywhere in the cellar. Clear them out and I'll reward you.\"" + options: + - text: "\"I'll handle it.\"" + goto: accept_quest + condition: + player_flag: rat_quest + not: true + - text: "\"I killed the rats.\"" + goto: turn_in + condition: + player_flag: rat_quest + value: started + - text: "\"Goodbye.\"" + end: true + + accept_quest: + message: "\"Good lad. The cellar is west of here. Come back when they're dead.\"" + action: + set_player_flags: + rat_quest: started + options: + - text: "\"On my way.\"" + end: true + + turn_in: + message: "\"You did it! The village owes you a debt. Here — take this.\"" + action: + give_item: rusty_sword + set_player_flags: + rat_quest: complete + heal: 10 + options: + - text: "\"Thanks!\"" + end: true +``` + +### 3. Rat mobs (`data/mobs/rat.yaml`) +```yaml +id: "rat" +name: "giant rat" +attack: 2 +strength: 1 +defense: 1 +hp: 3 +speed: 4 +aggressive: true +respawn_ticks: 60 +drops: + remains: "rat bones" +``` + +### 4. Cellar room (`data/rooms/20.yaml`) +```yaml +id: 20 +name: "Cellar" +description: "A damp, dark cellar. The floor scuttles with movement." +exits: + east: 1 +mobs: + - "rat" + - "rat" + - "rat" + - "rat" + - "rat" +``` + +### 5. Room 1 with quest giver (`data/rooms/1.yaml`) +```yaml +id: 1 +name: "Town Square" +description: "Cobblestone paths lead in all directions. The Elder stands near the fountain." +exits: + west: 20 + north: 2 +mobs: + - "quest_giver" +objects: + - id: lumby_fountain +``` + +--- + +## Wandering Objects + +Fishing spots that move between rooms: +```yaml +# data/objects/fishing_spot.yaml +id: fishing_spot +name: fishing spot +behavior: fish_trout +``` + +```yaml +# In rooms 7, 8, and 9: +objects: + - id: fishing_spot + wander_rooms: [7, 8, 9] + wander_interval: 12 +``` + +Players gathering from a wandering object are silently interrupted when it moves. + +--- + +## Wandering Mobs + +```yaml +# mobs that roam between rooms: +wander_rooms: [1, 2, 3, 4] +wander_interval: 10 # moves every 10 ticks +``` + +Mobs stop wandering while in combat. Dead mobs respawn at their home room after `respawn_ticks`. + +--- + +## Hidden Objects + +Objects with `hidden: true` don't appear in the room's object listing. Players discover them by reading room descriptions or trying commands. The object is still fully interactable — `push gate`, `look gate`, etc. + +```yaml +# data/objects/secret_lever.yaml +id: secret_lever +name: stone lever +behavior: secret_toggle +hidden: true +props: + description: "A cleverly concealed lever behind a loose stone." +``` + +Room description hints at it: +```yaml +description: "A dusty corridor. One of the wall stones looks slightly out of place." +``` + +--- + +## Tips + +1. **Use player flags for quest progress, world flags for environmental state.** If a bridge is repaired, that's world state. If a player has read a sign, that's player state. + +2. **Conditions on enter scripts** make rooms feel alive. A guard who only barks the first time, a room that changes after a quest completes. + +3. **Hidden objects** keep room descriptions clean. Mention them in the room's description text instead of auto-listing them. + +4. **Drop tables** are shareable. The `gem_table` is used by copper rocks AND mob loot. Define once, reference everywhere. + +5. **Objects are for fixtures, mobs are for living things.** If it has HP and can die, it's a mob. If it's a rock, lever, door, or crafting station, it's an object. Both can have behaviors. + +6. **The `behavior` field on mobs** lets you talk to them directly — `talk guard` finds the guard mob, no duplicate object entry needed. + +7. **Exits accept both `int` and `map` formats.** `north: 2` is shorthand for `north: {room: 2}`. Add `condition` and `blocked_message` only when needed. + +8. **Live editing works.** Room, item, mob, object, and behavior YAML files are read from disk on each access. Change a room description or dialog and it takes effect immediately — no restart needed. diff --git a/data/behaviors/guard_talk.yaml b/data/behaviors/guard_talk.yaml index e83f2e2..4adde0b 100644 --- a/data/behaviors/guard_talk.yaml +++ b/data/behaviors/guard_talk.yaml @@ -13,14 +13,14 @@ nodes: - text: "\"I have a pass.\"" goto: has_pass condition: - flag: got_pass + player_flag: got_pass value: true - text: "\"Goodbye.\"" end: true about_gate: message: "\"Supplies. Weapons. Things you don't need to worry about.\" He shifts his weight. \"Tell you what — bring me some copper ore and I'll stamp you a pass.\"" action: - set_flags: + set_player_flags: talked_to_guard: true options: - text: "\"I'll be back.\"" @@ -36,7 +36,7 @@ nodes: action: take_item: copper_ore give_item: pass_stub - set_flags: + set_player_flags: got_pass: true talked_to_guard: true options: diff --git a/data/objects/iron_gate.yaml b/data/objects/iron_gate.yaml index bd7eb0d..7ff0bfb 100644 --- a/data/objects/iron_gate.yaml +++ b/data/objects/iron_gate.yaml @@ -1,5 +1,6 @@ id: iron_gate name: iron gate behavior: iron_gate_toggle +hidden: true props: description: "A heavy iron gate. It looks like it can be pushed open." diff --git a/data/players/accounts/.gitkeep b/data/players/accounts/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/data/players/accounts/.gitkeep diff --git a/data/players/accounts/ad.yaml b/data/players/accounts/ad.yaml deleted file mode 100644 index 63ba445..0000000 --- a/data/players/accounts/ad.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: ad -password_hash: sha256:lUcF6TakL84+ULEQvs/hGg:af878475057cc72b964120b80a2767c7ff73c86c7828515ef200e2030909da48 -characters: - - ad diff --git a/data/players/accounts/be.yaml b/data/players/accounts/be.yaml deleted file mode 100644 index 5438be8..0000000 --- a/data/players/accounts/be.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: be -password_hash: sha256:mIampXXmSVbClzM7QvgnRw:c98405d73de4ca491ee8afe81cf7cbaf91ecc7918579cd143ee00f5e410e85ad -characters: - - be diff --git a/data/players/accounts/di.yaml b/data/players/accounts/di.yaml deleted file mode 100644 index 6f292bb..0000000 --- a/data/players/accounts/di.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: di -password_hash: sha256:Pr1aiCjarQOsS8pdZwwAjg:c979628ddc5404361454eacdfd509c5bbd5f04cae60ff3a762335e44e4ecfe06 -characters: [] diff --git a/data/players/accounts/eo.yaml b/data/players/accounts/eo.yaml deleted file mode 100644 index 833f320..0000000 --- a/data/players/accounts/eo.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: eo -password_hash: sha256:CQY8xHGy9LDWG1XXHzaqfw:1265c9416dd6ac8e59c63255026f2dcb3a1cc2e57e88eb0fcefc08616a4717df -characters: - - eo diff --git a/data/players/accounts/ew.yaml b/data/players/accounts/ew.yaml deleted file mode 100644 index 605f274..0000000 --- a/data/players/accounts/ew.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: ew -password_hash: sha256:NIhNFQ2Wote/Yqsb50sh6A:35ddcf86eaf6c73d8b12dab98cb5a77d8d774fe98c4e8bd09acebfdefd5bb61b -characters: - - ew diff --git a/data/players/accounts/fd.yaml b/data/players/accounts/fd.yaml deleted file mode 100644 index 27b122d..0000000 --- a/data/players/accounts/fd.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: fd -password_hash: sha256:Exyt0ZzDPmssGVmioFr+0g:4a8008f611287add62ed54f98bc160c391482f93b838e4f27cef31afb7b373ca -characters: - - fd diff --git a/data/players/accounts/he.yaml b/data/players/accounts/he.yaml deleted file mode 100644 index 7b47938..0000000 --- a/data/players/accounts/he.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: he -password_hash: sha256:h9wYpy+8gSxq3vWVXES9Yg:6641ad5fc0dc689966e90339a99bb5ea8b41f50a6628616c25c652456a069a82 -characters: - - he diff --git a/data/players/accounts/ir.yaml b/data/players/accounts/ir.yaml deleted file mode 100644 index 51f344f..0000000 --- a/data/players/accounts/ir.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: ir -password_hash: sha256:PNdb6J5QxC+9O1nQ29PNPg:2410a406393f0e40df95f26b4352d13d4715fe38a86938ef21094b9dcd6c3077 -characters: - - ir diff --git a/data/players/accounts/je.yaml b/data/players/accounts/je.yaml deleted file mode 100644 index d711808..0000000 --- a/data/players/accounts/je.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: je -password_hash: sha256:kfeyEgiY3+7oZVgPJlaofA:a3af89e832ef04e7e40b85222dc574b761e40143081ad900f2cbb71fec1a201d -characters: - - je diff --git a/data/players/accounts/ji.yaml b/data/players/accounts/ji.yaml deleted file mode 100644 index 130c2c7..0000000 --- a/data/players/accounts/ji.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: ji -password_hash: sha256:GfCf3fMYkg89cJmJEF9lgg:33ef437cd7b9f2a64a55fa7819b21d2cf12cd19482ae4154fc330bd73f71ad48 -characters: [] diff --git a/data/players/accounts/me.yaml b/data/players/accounts/me.yaml deleted file mode 100644 index a8ad8cf..0000000 --- a/data/players/accounts/me.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: me -password_hash: sha256:YPIaCMkVxA58zZFkm9c76g:32a2a015d5dda3aad3e4866a46e7a31fa32993f1a4e53bc239787a795b8d9360 -characters: - - me diff --git a/data/players/accounts/mo.yaml b/data/players/accounts/mo.yaml deleted file mode 100644 index 7591d69..0000000 --- a/data/players/accounts/mo.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: mo -password_hash: sha256:HRAgvc/hWu3nG/jawjdREA:573246a38c6000b7431ba22f67b3d4bbfee4b625b4a572516dfd4de3d6400b80 -characters: - - mo diff --git a/data/players/accounts/nc.yaml b/data/players/accounts/nc.yaml deleted file mode 100644 index 65eb3dd..0000000 --- a/data/players/accounts/nc.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: nc -password_hash: sha256:OMTkIHWVnQKJHAS+G3+Dmg:5c61f310d54f7e9ae6777d41cceaa807c735ad5fc2f7b8359df6f55d083b805b -characters: - - nc diff --git a/data/players/accounts/newguy.yaml b/data/players/accounts/newguy.yaml deleted file mode 100644 index b308084..0000000 --- a/data/players/accounts/newguy.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: newguy -password_hash: sha256:mPCmmVFAHRdjyZOj0+tQdQ:8c481b6748e8165388836b67c81e5be9f6f28cb5310654b776002483ae05cff3 -characters: [] diff --git a/data/players/accounts/oe.yaml b/data/players/accounts/oe.yaml deleted file mode 100644 index 98ed473..0000000 --- a/data/players/accounts/oe.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: oe -password_hash: sha256:4ERL7R5Mws8xZ2lj2h8sbg:afed13590689dd81fc07bf8c4a8c179f5ce655cb528a9742493df6507f77e333 -characters: - - oe - - oer diff --git a/data/players/accounts/ow.yaml b/data/players/accounts/ow.yaml deleted file mode 100644 index 8ace8ae..0000000 --- a/data/players/accounts/ow.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: ow -password_hash: sha256:HT4lN3B4cuUwyNT2s4gUJA:4d93885806eec85b33861164662b13d9548ca1d1394fe35f7a1ecf7ff876c0fd -characters: - - ow diff --git a/data/players/accounts/re.yaml b/data/players/accounts/re.yaml deleted file mode 100644 index c79f98d..0000000 --- a/data/players/accounts/re.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: re -password_hash: sha256:XEX82NiVAMfhK6T0j3snuw:48f795b41fee684cdcc4f9a932efb340a959a7d40ff273750ade735f309ff40b -characters: - - re diff --git a/data/players/accounts/wor.yaml b/data/players/accounts/wor.yaml deleted file mode 100644 index b75e5d7..0000000 --- a/data/players/accounts/wor.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: wor -password_hash: sha256:esQph9Mzbsk51T5jzzpVoQ:a26a8f37e05dea433cb9fad710334ac85bf3daf2a8ce55fb2fc8d33d8abfca02 -characters: - - wor diff --git a/data/players/characters/.gitkeep b/data/players/characters/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/data/players/characters/.gitkeep diff --git a/data/players/characters/ad.yaml b/data/players/characters/ad.yaml deleted file mode 100644 index 9bbaec2..0000000 --- a/data/players/characters/ad.yaml +++ /dev/null @@ -1,59 +0,0 @@ -name: ad -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: bronze_pickaxe - quantity: 1 - 1: - item_id: copper_ore - quantity: 1 - 2: - item_id: copper_ore - quantity: 1 - 3: - item_id: copper_ore - quantity: 1 - 4: - item_id: copper_ore - quantity: 1 - 5: - item_id: copper_ore - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 2 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/be.yaml b/data/players/characters/be.yaml deleted file mode 100644 index e00fe49..0000000 --- a/data/players/characters/be.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: be -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: fishing_rod - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 8 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/eo.yaml b/data/players/characters/eo.yaml deleted file mode 100644 index c258c4d..0000000 --- a/data/players/characters/eo.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: eo -skills: - agility: 0 - alchemy: 0 - attack: 21 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1161 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: bronze_pickaxe - quantity: 1 - 1: - item_id: bronze_pickaxe - quantity: 1 - 2: - item_id: bones - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 2 -hp: 7 -credits: 10 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 68 diff --git a/data/players/characters/ew.yaml b/data/players/characters/ew.yaml deleted file mode 100644 index 6169e00..0000000 --- a/data/players/characters/ew.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: ew -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: bronze_pickaxe - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 2 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/fd.yaml b/data/players/characters/fd.yaml deleted file mode 100644 index 5558d9b..0000000 --- a/data/players/characters/fd.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: fd -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: bronze_pickaxe - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 2 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/guy4.yaml b/data/players/characters/guy4.yaml deleted file mode 100644 index 4141899..0000000 --- a/data/players/characters/guy4.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: guy4 -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: {} -equipment: {} -toolbelt: [] -room_id: 1 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/he.yaml b/data/players/characters/he.yaml deleted file mode 100644 index e36c8d2..0000000 --- a/data/players/characters/he.yaml +++ /dev/null @@ -1,41 +0,0 @@ -name: he -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: {} -equipment: {} -toolbelt: [] -room_id: 8 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/ir.yaml b/data/players/characters/ir.yaml deleted file mode 100644 index 95da9ca..0000000 --- a/data/players/characters/ir.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: ir -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: bronze_pickaxe - quantity: 1 - 1: - item_id: copper_ore - quantity: 1 - 2: - item_id: copper_ore - quantity: 1 - 3: - item_id: copper_ore - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 2 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/je.yaml b/data/players/characters/je.yaml deleted file mode 100644 index 2816d76..0000000 --- a/data/players/characters/je.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: je -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: bronze_pickaxe - quantity: 1 - 1: - item_id: pass_stub - quantity: 1 - 2: - item_id: uncut_sapphire - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 10 -hp: 10 -credits: 50 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/me.yaml b/data/players/characters/me.yaml deleted file mode 100644 index ef5d9b0..0000000 --- a/data/players/characters/me.yaml +++ /dev/null @@ -1,41 +0,0 @@ -name: me -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: {} -equipment: {} -toolbelt: [] -room_id: 8 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/mo.yaml b/data/players/characters/mo.yaml deleted file mode 100644 index d9139c0..0000000 --- a/data/players/characters/mo.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: mo -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: bronze_pickaxe - quantity: 1 - 1: - item_id: copper_ore - quantity: 1 - 2: - item_id: copper_ore - quantity: 1 - 3: - item_id: copper_ore - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 2 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/nc.yaml b/data/players/characters/nc.yaml deleted file mode 100644 index 13af64e..0000000 --- a/data/players/characters/nc.yaml +++ /dev/null @@ -1,56 +0,0 @@ -name: nc -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: bronze_pickaxe - quantity: 1 - 1: - item_id: copper_ore - quantity: 1 - 2: - item_id: copper_ore - quantity: 1 - 3: - item_id: copper_ore - quantity: 1 - 4: - item_id: copper_ore - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 2 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/oe.yaml b/data/players/characters/oe.yaml deleted file mode 100644 index d54feca..0000000 --- a/data/players/characters/oe.yaml +++ /dev/null @@ -1,47 +0,0 @@ -name: oe -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: bronze_pickaxe - quantity: 1 - 1: - item_id: copper_ore - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 2 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/oer.yaml b/data/players/characters/oer.yaml deleted file mode 100644 index 246ae02..0000000 --- a/data/players/characters/oer.yaml +++ /dev/null @@ -1,95 +0,0 @@ -name: oer -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: bronze_pickaxe - quantity: 1 - 1: - item_id: copper_ore - quantity: 1 - 2: - item_id: copper_ore - quantity: 1 - 3: - item_id: copper_ore - quantity: 1 - 4: - item_id: copper_ore - quantity: 1 - 5: - item_id: copper_ore - quantity: 1 - 6: - item_id: copper_ore - quantity: 1 - 7: - item_id: copper_ore - quantity: 1 - 8: - item_id: copper_ore - quantity: 1 - 9: - item_id: copper_ore - quantity: 1 - 10: - item_id: copper_ore - quantity: 1 - 11: - item_id: copper_ore - quantity: 1 - 12: - item_id: copper_ore - quantity: 1 - 13: - item_id: copper_ore - quantity: 1 - 14: - item_id: copper_ore - quantity: 1 - 15: - item_id: copper_ore - quantity: 1 - 16: - item_id: copper_ore - quantity: 1 - 17: - item_id: copper_ore - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 2 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/ow.yaml b/data/players/characters/ow.yaml deleted file mode 100644 index 32c03c5..0000000 --- a/data/players/characters/ow.yaml +++ /dev/null @@ -1,47 +0,0 @@ -name: ow -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: bronze_pickaxe - quantity: 1 - 1: - item_id: copper_ore - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 2 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/re.yaml b/data/players/characters/re.yaml deleted file mode 100644 index 819f3b5..0000000 --- a/data/players/characters/re.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: re -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: bronze_pickaxe - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 2 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/players/characters/wor.yaml b/data/players/characters/wor.yaml deleted file mode 100644 index 687c8d9..0000000 --- a/data/players/characters/wor.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: wor -skills: - agility: 0 - alchemy: 0 - attack: 0 - construction: 0 - cooking: 0 - crafting: 0 - defense: 0 - fishing: 0 - fletching: 0 - hitpoints: 1154 - hunter: 0 - mining: 0 - ranged: 0 - scavenging: 0 - science: 0 - slayer: 0 - smithing: 0 - strength: 0 - technology: 0 - thieving: 0 - woodcutting: 0 -inventory: - 0: - item_id: fishing_rod - quantity: 1 - 1: - item_id: raw_trout - quantity: 1 - 2: - item_id: raw_trout - quantity: 1 -equipment: {} -toolbelt: [] -room_id: 9 -hp: 10 -credits: 0 -description: "" -attack_style: accurate -toggles: - description: true - exits: true - mobenter: true - mobleave: true - mobspawn: true - reserve: true - tinymap: true - xpdrops: true -regeneratetick: 0 diff --git a/data/rooms/10.yaml b/data/rooms/10.yaml index 99e84eb..98fba43 100644 --- a/data/rooms/10.yaml +++ b/data/rooms/10.yaml @@ -1,6 +1,6 @@ id: 10 name: "Guard Post" -description: "A checkpoint at the edge of the mine. A guard stands watch beside a heavy iron gate leading north." +description: "A checkpoint at the edge of the mine. A guard stands watch beside a heavy iron gate set into the north wall." exits: west: 2 north: @@ -16,5 +16,5 @@ mobs: on_enter: - message: "The Guard barks: \"State your business!\"" condition: - flag: talked_to_guard + player_flag: talked_to_guard not: true diff --git a/internal/action/behavior.go b/internal/action/behavior.go index d7f683a..d8eb909 100644 --- a/internal/action/behavior.go +++ b/internal/action/behavior.go @@ -38,16 +38,22 @@ type TalkOption struct { } type NodeAction struct { - SetFlags map[string]any `yaml:"set_flags"` - GiveItem string `yaml:"give_item"` - TakeItem string `yaml:"take_item"` + SetFlags map[string]any `yaml:"set_flags"` + SetPlayerFlags map[string]any `yaml:"set_player_flags"` + GiveItem string `yaml:"give_item"` + TakeItem string `yaml:"take_item"` + Teleport int `yaml:"teleport"` + Heal int `yaml:"heal"` } type Condition struct { - Flag string `yaml:"flag"` - Value any `yaml:"value"` - Not bool `yaml:"not"` - HasItem string `yaml:"has_item"` + Flag string `yaml:"flag"` + Value any `yaml:"value"` + Not bool `yaml:"not"` + PlayerFlag string `yaml:"player_flag"` + HasItem string `yaml:"has_item"` + AllOf []Condition `yaml:"all_of"` + AnyOf []Condition `yaml:"any_of"` } type UseConfig struct { diff --git a/internal/game/action.go b/internal/game/action.go index 029fec3..62cdf62 100644 --- a/internal/game/action.go +++ b/internal/game/action.go @@ -427,10 +427,13 @@ func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) { return } - for i, opt := range node.Options { - if opt.Condition != nil && !g.checkCondition(sess, opt.Condition) { continue + visible := 0 + for _, opt := range node.Options { + if opt.Condition != nil && !g.checkCondition(sess, opt.Condition) { + continue } - sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, opt.Text)) + visible++ + sess.WriteLine(fmt.Sprintf(" %d. %s", visible, opt.Text)) } sess.State = net.StateTalk sess.Write("\nChoice: ") @@ -527,6 +530,12 @@ func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) { for k, v := range na.SetFlags { g.WorldFlags[k] = v } + for k, v := range na.SetPlayerFlags { + if p.Flags == nil { + p.Flags = make(map[string]any) + } + p.Flags[k] = v + } if na.TakeItem != "" { if p.HasItem(na.TakeItem) { p.RemoveItem(na.TakeItem, 1) @@ -541,10 +550,59 @@ func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) { g.AccountStore.SaveCharacter(p) } } + if na.Teleport > 0 { + p.RoomID = na.Teleport + g.AccountStore.SaveCharacter(p) + g.World.SeedGroundItems(p.RoomID) + g.seedRoomMobs(p.RoomID) + g.ensureRoomObjects(p.RoomID) + if g.Hub != nil { + g.Hub.EnterRoom(sess, p.RoomID) + } + g.doLook(sess) + g.RunEnterSteps(sess, p.RoomID) + } + if na.Heal > 0 { + p.HP += na.Heal + if maxHP := p.MaxHP(); p.HP > maxHP { + p.HP = maxHP + } + g.AccountStore.SaveCharacter(p) + sess.WriteLine(fmt.Sprintf("You regain %d hitpoints.", na.Heal)) + } } func (g *Game) checkCondition(sess *net.Session, c *action.Condition) bool { p, _ := sess.Player.(*player.Player) + + if c.AllOf != nil { + for _, sub := range c.AllOf { + if !g.checkCondition(sess, &sub) { + return false + } + } + return true + } + if c.AnyOf != nil { + for _, sub := range c.AnyOf { + if g.checkCondition(sess, &sub) { + return true + } + } + return false + } + + if c.PlayerFlag != "" { + if p == nil || p.Flags == nil { + return c.Not + } + val, ok := p.Flags[c.PlayerFlag] + if c.Not { + return !ok || val != c.Value + } + return ok && val == c.Value + } + if c.Flag != "" { val, ok := g.WorldFlags[c.Flag] if c.Not { @@ -714,24 +772,13 @@ func normalizeVerb(v string) string { return v } -func (g *Game) CheckExitCondition(c *world.ExitCondition) bool { - if c.Flag != "" { - val, ok := g.WorldFlags[c.Flag] - if c.Not { - return !ok || val != c.Value - } - return ok && val == c.Value - } - return true -} - func (g *Game) RunEnterSteps(sess *net.Session, roomID int) { room, err := g.World.LoadRoom(roomID) if err != nil || len(room.OnEnter) == 0 { return } for _, step := range room.OnEnter { - if step.Condition != nil && !g.CheckExitCondition(step.Condition) { + if step.Condition != nil && !g.checkCondition(sess, step.Condition) { continue } if step.Message != "" { diff --git a/internal/game/cmd_alias.go b/internal/game/cmd_alias.go new file mode 100644 index 0000000..cb0537e --- /dev/null +++ b/internal/game/cmd_alias.go @@ -0,0 +1,102 @@ +package game + +import ( + "fmt" + "sort" + "strings" + + "thirdcollapse/internal/net" +) + +func (g *Game) doAlias(sess *net.Session, args []string) { + if sess.Account == nil { + sess.WriteLine("No account loaded.") + return + } + + if len(args) == 0 { + if len(sess.Account.Aliases) == 0 { + sess.WriteLine("\nNo aliases defined. Use ALIAS <name> <command> to create one.") + return + } + sess.WriteLine("\nAliases:") + names := make([]string, 0, len(sess.Account.Aliases)) + maxLen := 0 + for name := range sess.Account.Aliases { + names = append(names, name) + if len(name) > maxLen { + maxLen = len(name) + } + } + sort.Strings(names) + for _, name := range names { + sess.WriteLine(fmt.Sprintf(" %-*s: %s", maxLen, name, sess.Account.Aliases[name])) + } + return + } + + if len(args) < 2 { + sess.WriteLine("Usage: ALIAS <name> <command>") + return + } + + aliasName := strings.ToLower(args[0]) + aliasCmd := strings.Join(args[1:], " ") + + if sess.Account.Aliases == nil { + sess.Account.Aliases = make(map[string]string) + } + sess.Account.Aliases[aliasName] = aliasCmd + + acc, err := g.AccountStore.LoadAccount(sess.Account.Name) + if err != nil { + sess.WriteLine("Error saving alias.") + return + } + acc.Aliases = sess.Account.Aliases + if err := g.AccountStore.SaveAccount(acc); err != nil { + sess.WriteLine("Error saving alias.") + return + } + + sess.WriteLine(fmt.Sprintf("\nAlias set: %s -> %s", aliasName, aliasCmd)) +} + +func (g *Game) doUnalias(sess *net.Session, args []string) { + if sess.Account == nil { + sess.WriteLine("No account loaded.") + return + } + + if len(args) == 0 { + sess.WriteLine("\nUsage: UNALIAS <name>") + sess.WriteLine("Removes an alias. Use ALIAS with no arguments to see your aliases.") + return + } + + aliasName := strings.ToLower(args[0]) + + if sess.Account.Aliases == nil { + sess.Account.Aliases = make(map[string]string) + } + + if _, ok := sess.Account.Aliases[aliasName]; !ok { + sess.WriteLine(fmt.Sprintf("No alias '%s' found.", aliasName)) + return + } + + delete(sess.Account.Aliases, aliasName) + + acc, err := g.AccountStore.LoadAccount(sess.Account.Name) + if err != nil { + sess.WriteLine("Error saving alias.") + return + } + acc.Aliases = sess.Account.Aliases + if err := g.AccountStore.SaveAccount(acc); err != nil { + sess.WriteLine("Error saving alias.") + return + } + + sess.WriteLine(fmt.Sprintf("\nAlias '%s' removed.", aliasName)) +} diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go index ebf88ac..4e2d2c0 100644 --- a/internal/game/cmd_attack.go +++ b/internal/game/cmd_attack.go @@ -454,7 +454,7 @@ func (g *Game) respawnMob(instanceID string) { if g.Hub != nil { for _, sess := range g.Hub.PlayersInRoom(homeRoom) { if p, ok := sess.Player.(*player.Player); ok && p.Toggles["mobspawn"] { - sess.WriteLine(fmt.Sprintf("\n%s (level %d) enters the area.", mobDisplayName(inst, false), mobCombatLevel(inst))) + sess.WriteLine(fmt.Sprintf("\n%s (level %d) spawns in the area.", mobDisplayName(inst, false), mobCombatLevel(inst))) } } } diff --git a/internal/game/cmd_drop.go b/internal/game/cmd_drop.go index 511688a..2f79a2f 100644 --- a/internal/game/cmd_drop.go +++ b/internal/game/cmd_drop.go @@ -2,6 +2,7 @@ package game import ( "fmt" + "strings" "thirdcollapse/internal/net" "thirdcollapse/internal/player" @@ -10,6 +11,22 @@ import ( func (g *Game) doDrop(sess *net.Session, input string) { p := sess.Player.(*player.Player) + if input == "all" { + count := 0 + for _, slot := range p.Inventory { + if slot != nil && slot.Quantity > 0 { + count++ + } + } + if count == 0 { + sess.WriteLine("You have nothing to drop.") + return + } + sess.State = net.StateDropAllConfirm + sess.WriteLine(fmt.Sprintf("\nDrop all %d items? [y/N]", count)) + return + } + matches := g.findInventoryMatches(input, p) if len(matches) == 0 { sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", input)) @@ -46,3 +63,47 @@ func (g *Game) doDrop(sess *net.Session, input string) { g.AccountStore.SaveCharacter(p) sess.WriteLine(fmt.Sprintf("You drop a %s.", name)) } + +func (g *Game) handleDropAllConfirm(sess *net.Session, input string) { + p, ok := sess.Player.(*player.Player) + if !ok { + sess.State = net.StateGame + sess.Write("\r\n> ") + return + } + + input = strings.TrimSpace(strings.ToLower(input)) + if input != "y" && input != "yes" { + sess.State = net.StateGame + sess.Write("\r\n> ") + return + } + + var dropped []string + for i := 0; i < 28; i++ { + slot := p.InvSlot(i) + if slot == nil || slot.Quantity <= 0 { + continue + } + g.World.AddGroundItem(p.RoomID, slot.ItemID, slot.Quantity) + def, _ := g.ItemStore.Load(slot.ItemID) + name := slot.ItemID + if def != nil { + name = def.Name + } + dropped = append(dropped, name) + p.SetInvSlot(i, nil) + } + + g.AccountStore.SaveCharacter(p) + sess.State = net.StateGame + + if len(dropped) == 0 { + sess.WriteLine("\nYou have nothing to drop.") + } else { + sess.Write(fmt.Sprintf("\nYou drop your ")) + innerPickupReport(sess, dropped) + sess.WriteLine(".") + } + sess.Write("\r\n> ") +} diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index a544ec1..e7d30d6 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -75,7 +75,7 @@ func (g *Game) doLook(sess *net.Session) { for _, objID := range order { count := grouped[objID] def, err := g.ObjectStore.Load(objID) - if err != nil { + if err != nil || def.Hidden { continue } @@ -116,26 +116,46 @@ func (g *Game) doLook(sess *net.Session) { if len(ground) > 0 { sess.WriteLine("") sess.WriteLine("On the ground:") + + type groundLine struct { + prefix string + reserved string + } + var lines []groundLine + maxPrefix := 0 + for _, info := range ground { def, err := g.ItemStore.Load(info.ItemID) name := info.ItemID if err == nil { name = def.Name } - line := "" + prefix := "" if info.Quantity > 1 { - line = fmt.Sprintf(" %d x %s", info.Quantity, name) + prefix = fmt.Sprintf(" %d x %s", info.Quantity, name) } else { - line = fmt.Sprintf(" %s", name) + prefix = fmt.Sprintf(" %s", name) + } + if len(prefix) > maxPrefix { + maxPrefix = len(prefix) } + reserved := "" if info.ReservedFor != "" { if p.Toggles["reserve"] { - line += fmt.Sprintf(" (reserved for %s for %d ticks)", info.ReservedFor, info.ReserveTimer) + reserved = fmt.Sprintf(" (reserved for %s for %d ticks)", info.ReservedFor, info.ReserveTimer) } else { - line += " (reserved)" + reserved = " (reserved)" } } - sess.WriteLine(line) + lines = append(lines, groundLine{prefix, reserved}) + } + + for _, l := range lines { + if l.reserved != "" { + sess.WriteLine(fmt.Sprintf("%-*s%s", maxPrefix+1, l.prefix, l.reserved)) + } else { + sess.WriteLine(l.prefix) + } } } @@ -153,7 +173,7 @@ func (g *Game) doLook(sess *net.Session) { if err == nil { targetName = targetRoom.Name } - if exitDef.Condition != nil && !g.CheckExitCondition(exitDef.Condition) { + if exitDef.Condition != nil && !g.checkCondition(sess, exitDef.Condition) { targetName += " (blocked)" } sess.WriteLine(fmt.Sprintf(" %-6s - %s", dir, targetName)) diff --git a/internal/game/cmd_misc.go b/internal/game/cmd_misc.go index f56a0ff..c02bd35 100644 --- a/internal/game/cmd_misc.go +++ b/internal/game/cmd_misc.go @@ -105,13 +105,21 @@ func (g *Game) doStyle(sess *net.Session, input string) { } input = strings.ToLower(input) + var matches []string for _, s := range styles { - if s == input { - p.AttackStyle = player.AttackStyle(s) - g.AccountStore.SaveCharacter(p) - sess.WriteLine(fmt.Sprintf("\nCombat style set to %s.", s)) - return + if strings.HasPrefix(s, input) { + matches = append(matches, s) } } + if len(matches) == 1 { + p.AttackStyle = player.AttackStyle(matches[0]) + g.AccountStore.SaveCharacter(p) + sess.WriteLine(fmt.Sprintf("\nCombat style set to %s.", matches[0])) + return + } + if len(matches) > 1 { + sess.WriteLine(fmt.Sprintf("\nAmbiguous style: %s. Choices: %s", input, strings.Join(matches, ", "))) + return + } sess.WriteLine(fmt.Sprintf("\nUnknown style: %s. Choices: accurate, aggressive, defensive, balanced", input)) } diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go index b95f5c5..e288dad 100644 --- a/internal/game/cmd_move.go +++ b/internal/game/cmd_move.go @@ -28,7 +28,7 @@ func (g *Game) doMove(sess *net.Session, dir string) { return } - if exitDef.Condition != nil && !g.CheckExitCondition(exitDef.Condition) { + if exitDef.Condition != nil && !g.checkCondition(sess, exitDef.Condition) { msg := exitDef.BlockedMessage if msg == "" { msg = fmt.Sprintf("The way %s is blocked.", exitDir) diff --git a/internal/game/game.go b/internal/game/game.go index d793516..08096b4 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -87,6 +87,8 @@ func (g *Game) HandleSession(sess *net.Session, input string) { g.handleDescriptionChange(sess, input) case net.StateTalk: g.handleTalkInput(sess, input) + case net.StateDropAllConfirm: + g.handleDropAllConfirm(sess, input) } } @@ -100,6 +102,24 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { g.cancelRest(p.Name) } + if sess.Account != nil && sess.Account.Aliases != nil { + firstSpace := strings.Index(input, " ") + var firstWord, rest string + if firstSpace > 0 { + firstWord = input[:firstSpace] + rest = strings.TrimLeft(input[firstSpace:], " ") + } else { + firstWord = input + } + if expansion, ok := sess.Account.Aliases[strings.ToLower(firstWord)]; ok { + if rest != "" { + input = expansion + " " + rest + } else { + input = expansion + } + } + } + parts := strings.Fields(strings.ToLower(input)) cmd := parts[0] args := parts[1:] @@ -144,7 +164,12 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { if len(args) == 0 { sess.WriteLine("Say what?") } else { - g.doSay(sess, strings.Join(parts[1:], " ")) + msgStart := strings.Index(strings.ToLower(input), "say ") + 4 + if msgStart >= 4 && msgStart < len(input) { + g.doSay(sess, input[msgStart:]) + } else { + g.doSay(sess, strings.Join(args, " ")) + } } case "sc", "score": g.doScore(sess) @@ -183,6 +208,12 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { g.StartAction(sess, "talk", strings.Join(args, " ")) return } + case "alias": + g.doAlias(sess, args) + return + case "unalias": + g.doUnalias(sess, args) + return default: sess.WriteLine("Unknown command.") } diff --git a/internal/game/session.go b/internal/game/session.go index edc904d..7fe45e7 100644 --- a/internal/game/session.go +++ b/internal/game/session.go @@ -49,6 +49,10 @@ func (g *Game) handlePassword(sess *net.Session, input string) { Name: acc.Name, PasswordHash: acc.PasswordHash, Characters: acc.Characters, + Aliases: acc.Aliases, + } + if sess.Account.Aliases == nil { + sess.Account.Aliases = make(map[string]string) } g.showMenu(sess) } @@ -106,6 +110,7 @@ func (g *Game) handleNewAccountConfirm(sess *net.Session, input string) { sess.Account = &net.AccountEntry{ Name: acc.Name, PasswordHash: acc.PasswordHash, + Aliases: make(map[string]string), } sess.PendingPass = "" g.showMenu(sess) diff --git a/internal/net/server.go b/internal/net/server.go index 8fb975f..08d427f 100644 --- a/internal/net/server.go +++ b/internal/net/server.go @@ -26,6 +26,7 @@ const ( StateGame StateChangeDescription StateTalk + StateDropAllConfirm ) type Session struct { @@ -44,6 +45,7 @@ type AccountEntry struct { Name string PasswordHash string Characters []string + Aliases map[string]string } type Server struct { diff --git a/internal/object/object.go b/internal/object/object.go index edee3ab..9e06804 100644 --- a/internal/object/object.go +++ b/internal/object/object.go @@ -4,5 +4,6 @@ type ObjectDef struct { ID string `yaml:"id"` Name string `yaml:"name"` BehaviorID string `yaml:"behavior"` + Hidden bool `yaml:"hidden"` Props map[string]any `yaml:"props"` } diff --git a/internal/player/account.go b/internal/player/account.go index 5072009..679c5bc 100644 --- a/internal/player/account.go +++ b/internal/player/account.go @@ -1,7 +1,8 @@ package player type Account struct { - Name string `yaml:"name"` - PasswordHash string `yaml:"password_hash"` - Characters []string `yaml:"characters"` + Name string `yaml:"name"` + PasswordHash string `yaml:"password_hash"` + Characters []string `yaml:"characters"` + Aliases map[string]string `yaml:"aliases"` } diff --git a/internal/player/player.go b/internal/player/player.go index d6e450a..001eb8b 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -94,6 +94,7 @@ type Player struct { Description string `yaml:"description"` AttackStyle AttackStyle `yaml:"attack_style"` Toggles map[string]bool `yaml:"toggles"` + Flags map[string]any `yaml:"flags"` RegenerateTick int Action *action.Action `yaml:"-"` } diff --git a/internal/world/room.go b/internal/world/room.go index 6c2d0e3..58fa82b 100644 --- a/internal/world/room.go +++ b/internal/world/room.go @@ -1,6 +1,9 @@ package world -import "gopkg.in/yaml.v3" +import ( + "gopkg.in/yaml.v3" + "thirdcollapse/internal/action" +) type ExitDir string @@ -44,9 +47,9 @@ type SpawnDef struct { } type ExitDef struct { - Room int `yaml:"room"` - Condition *ExitCondition `yaml:"condition"` - BlockedMessage string `yaml:"blocked_message"` + Room int `yaml:"room"` + Condition *action.Condition `yaml:"condition"` + BlockedMessage string `yaml:"blocked_message"` } func (e *ExitDef) UnmarshalYAML(value *yaml.Node) error { @@ -62,12 +65,6 @@ func (e *ExitDef) UnmarshalYAML(value *yaml.Node) error { return value.Decode((*raw)(e)) } -type ExitCondition struct { - Flag string `yaml:"flag"` - Value any `yaml:"value"` - Not bool `yaml:"not"` -} - type Room struct { ID int `yaml:"id"` Name string `yaml:"name"` @@ -81,8 +78,8 @@ type Room struct { } type EnterStep struct { - Message string `yaml:"message"` - Condition *ExitCondition `yaml:"condition"` + Message string `yaml:"message"` + Condition *action.Condition `yaml:"condition"` } type RoomObject struct { |
