diff options
| author | workhorse <workhorse@localhost.localdomain> | 2026-06-19 03:57:06 -0400 |
|---|---|---|
| committer | workhorse <workhorse@localhost.localdomain> | 2026-06-19 03:57:06 -0400 |
| commit | 52a3ce6a4b4a254dc5d3067979a09e93c060fa20 (patch) | |
| tree | 167c62990013733bc8669c1387078bc86ea48db5 | |
| parent | fbd1c30d7b9777c9df6653f0c393afa7a7dfa519 (diff) | |
| download | thehouseoficarus-52a3ce6a4b4a254dc5d3067979a09e93c060fa20.tar.gz | |
feat: shops and rough assassin skill
133 files changed, 2568 insertions, 432 deletions
@@ -67,6 +67,9 @@ internal/ | `cmd_stats.go` | doStats — equipment bonus totals, attack roll, max hit display | | `cmd_clean.go` | doClean — background herb cleaning (Pharmacy) | | `cmd_mix.go` | doMix — potion mixing via production system (Pharmacy) | +| `cmd_shop.go` | handleShopInput — buy/sell/browse/leave in shop interface | +| `action_steal.go` | doSteal, startSteal, advanceSteal — thieving action lifecycle | +| `cmd_sneak.go` | doSneak, SneakTick — sneak toggle + guard watch notifications | | `action_clean.go` | advanceClean — background clean action lifecycle | | `equip_stats.go` | playerEquipBonuses — sums all equipped items into one ItemStats | | `aggro.go` | checkAggro — aggressive mob attack on room enter | @@ -276,6 +279,7 @@ The `verbSkill` map maps verb → skill name for default target resolution: | cook | `action_production.go` | Recipe-driven cooking on fire/range stations | | smelt | `action_production.go` | Recipe-driven smelting of ore into bars at furnace | | smith | `action_production.go` | Recipe-driven smithing of bars into items at anvil (requires hammer) | +| steal | `action_steal.go` | Thieving: pickpocket mobs, steal from objects, guard watching, sneak mode | ### GatherConfig Fields @@ -316,6 +320,8 @@ clamped to [0, cfg.Cap] | `take_item` | Removes an item from inventory | | `teleport` | Moves player to a room ID | | `heal` | Restores hitpoints | +| `cost` | Deducts credits from player | +| `shop` | Opens a buy/sell interface (see Shop section) | ### Condition System @@ -520,6 +526,7 @@ From `internal/net/server.go`: | `StateRecipeChoice` | Recipe/product selection table menu (number or name input, enter cancels) | | `StateHowMany` | "How many?" production count prompt | | `StateSmithProduct` | Smith product selection with numbered table (#, name, or partial name input) | +| `StateShop` | Buy/sell/browse in shop (buy, sell, browse, leave commands) | | `StateColorChoice` | New account color preference | ## Adding a New Skill @@ -16,39 +16,12 @@ Detailed implementation plans for each skill/system are in `skill_plans/`. - [x] Step 3: Combat Feel — Aggressive mobs (auto-attack on room enter, OSRS combat level rule), equipment skill requirements on all tiered gear - [x] Step 4: Technology — 25 techs (attack/str/def/ranged/science boosts, 3 protection techs, utility techs), Battery drain, 1-tick flicking, tech command, TechTick, charging station, score/prompt integration - [x] Step 5: Scavenging — mine_scrap behavior verified. Created 10 junk items (solar, hydro, cosmic, eco, chaos, bio, nature, law, death, blood — all stackable) with matching identifier tools and altar objects. `id`/`identify` command (Active): scans room for altar, checks for matching identifier in inventory, converts all scrap to junk on next tick with level-based multipliers, awards Scavenging XP. 10 altar rooms (150-159) branching from Scavenging Post (room 9). Help files created. -- [x] Step 6: Science — 40 mods (20 combat, 2 processing, 3 utility, 7 transport, 4 enchant, 4 chip), ModDef/AllMods in science.go. `trigger`/`cast` command (Active). `autocast`/`auto` (Instant). `mods`/`modlist` (Instant). Autocast in startCombat with 5-tick science speed and melee fallback. `scienceAttack()` with elemental weakness +30%. Junk cost system with deck exemptions. `ProvidesJunk` on ItemDef, `AutocastMod` on Player, `FindByInput` on RecipeStore, `science_mod` color target. 9 deck items, 24 jewelry items, 8 bolt items, nutrient_bar. Help files created. - ---- - -## Step 5: Scavenging - -See `skill_plans/scavenging.md`. The simplest new skill — mine scrap (already exists), identify it at altars to produce junk. Must be implemented before Science since Science consumes junk. - -- [x] Step 5: Scavenging — mine_scrap behavior verified. Created 10 junk items (solar, hydro, cosmic, eco, chaos, bio, nature, law, death, blood — all stackable) with matching identifier tools and altar objects. `id`/`identify` command (Active): scans room for altar, checks for matching identifier in inventory, converts all scrap to junk on next tick with level-based multipliers, awards Scavenging XP. 10 altar rooms (150-159) branching from Scavenging Post (room 9). Help files created. - ---- - -## Step 6: Science (Magic System) - -See `skill_plans/science.md`. Players "trigger mods" (cast spells) powered by junk (runes). Decks (staves) provide unlimited supply of one junk type and remove scrap requirement. Depends on combat overhaul + scavenging. - - [x] Step 6: Science — 40 mods (20 combat, 2 processing, 3 utility, 7 transport, 4 enchant, 4 chip), ModDef/AllMods in science.go. `trigger`/`cast` command (Active) routes to combat/transport/processing/utility/enchant handlers. `autocast`/`auto` command (Instant) sets science combat autocast. `mods`/`modlist` command (Instant) shows available mods with effective junk costs. Integrated autocast into `startCombat()` in cmd_attack.go with 5-tick science speed and melee fallback on junk depletion. `scienceAttack()` with Science level + science_attack roll, elemental weakness +30% accuracy, mod MaxHit damage. Junk cost system: deck-equipped removes scrap_metal, elemental deck provides unlimited junk type. Added `ProvidesJunk` to ItemDef, `AutocastMod` to Player, `FindByInput` to RecipeStore, `science_mod` color target. 9 deck items, 24 jewelry items, 8 bolt items, nutrient_bar item. Help files created. +- [x] Step 7: Shop System — Added `shop` talk node action that opens a buy/sell interface. `ShopConfig` with items + buy/sell prices defined in behavior YAML. `StateShop` session state with `buy`, `sell`, `browse`/`list`, `leave` commands. Items bought with credits, sold back at reduced prices. Created General Store clerk (Town Square) and Weapons Smith (Forge) NPCs. Created help files and worldbuilding guide docs. --- -## Step 7: Shop System - -Originally Phase 2, but needed before Assassin and generally useful. Add a buy/sell interface via talk node actions. - -- [ ] **Shop System** — Add a `shop` talk node action that opens a buy/sell interface. The shop inventory is defined in the behavior YAML as a list of items with buy/sell prices. Player enters `StateShop`. Commands: `buy <item>`, `sell <item>`, `browse`/`list`, `leave`. Items are bought with credits, sold back at a percentage. Add a General Store NPC and a Weapons Shop NPC. Place in appropriate rooms. - ---- - -## Step 8: Thieving - -See `skill_plans/thieving.md`. Steal from mobs and objects. Sneak mode, guard mechanics. - -- [ ] **Thieving** — Add `steal`/`thieve` command (Active) and `sneak` command (Instant toggle). Stealing from mobs: roll success based on Thieving level vs mob's thieving level requirement. On fail, mob turns hostile (starts combat). Stealing from objects: market stall gives random low-value food. Guard watching mechanic: mobs can guard objects, `sneak` mode shows guard attention cycle, lower success when watched, guard called on watched failure. Create credit_stick item (non-stackable, searchable for credits). Create Farmer and Bioengineer mobs (steal seeds from them). Add guard confrontation dialog (bribe/jail/fight). Create jail room. Update Man mob to be stealable. +- [x] Step 8: Thieving — `steal`/`thieve` command (Active) with continuous steal action. `sneak` command (Instant toggle) with guard-watching notifications. Success formula: 0.5 base + 0.03 per level, halved when guard watches. Mob stealing with aggro on failure. Object stealing with guard mechanic (SneakTick, watch/away cycles). Guard confrontation dialog (bribe 500cr, jail, fight). Created Market Stall object, Farmer + Bioengineer mobs, credit_stick item (searchable), 11 seed items, 5 new drop tables, 3 new rooms (160-162), 3 help files. Updated Man mob for stealability. --- @@ -56,7 +29,7 @@ See `skill_plans/thieving.md`. Steal from mobs and objects. Sneak mode, guard me See `skill_plans/assassin.md`. Task system, reputation, slayer-only mobs. Depends on combat being solid and having enough mobs to assign as tasks. -- [ ] **Assassin System** — Add `task` command (Instant) to check current assignment. Create Client NPC with talk behavior: assigns random mob kill tasks, skip/extend via reputation, reputation shop. Store task state in player flags (task_mob, task_count, task_remaining, tasks_completed, streak, reputation). In `endCombat()`, check if killed mob is on-task and award Assassin XP + reputation. Streak bonuses at 10th/50th/100th/250th/1000th task. Create 4+ slayer-only mobs with assassin_level requirements, finishing blow items (slug+salt, crawler+acid_vial), and damage-without equipment (drone needs insulated_gloves, phantom needs spectral_visor). Add assassin equipment items (cheap, sold by Client). Place slayer mobs in appropriate rooms. +- [x] **Assassin System** — Added `task` command (Instant) to check current assignment. Created Client NPC with talk behavior: assigns random mob kill tasks, skip/extend via reputation, reputation shop. Task state stored in player flags (task_mob, task_count, task_remaining, tasks_completed, streak, reputation). In `endCombat()`, on-task kills award Assassin XP (mob.MaxHP * 2) + reputation. Streak bonuses at 10th/50th/100th/250th/1000th task. Created 4 slayer-only mobs (slug/drone/crawler/phantom) with assassin_level requirements, finishing blow items (slug+salt, crawler+acid_vial), and damage-without equipment (drone needs insulated_gloves, phantom needs spectral_visor). Added assassin equipment items (salt, acid_vial, insulated_gloves, spectral_visor) sold by Client. Added 5 new rooms (50-54: Assassin Den + Sewer Tunnels chain). Created help files (task, assassin). --- diff --git a/cmd/mud/main.go b/cmd/mud/main.go index 55804d3..82427ba 100644 --- a/cmd/mud/main.go +++ b/cmd/mud/main.go @@ -54,6 +54,7 @@ func main() { g.ConsumeTick() g.BroadcastRespawns() g.VisualTick() + g.SneakTick() return true }) diff --git a/data/behaviors/client_talk.yaml b/data/behaviors/client_talk.yaml new file mode 100644 index 0000000..84e4f6a --- /dev/null +++ b/data/behaviors/client_talk.yaml @@ -0,0 +1,217 @@ +id: client_talk +type: talk +nodes: + start: + message: "The Client looks up from their data pad. \"What do you need?\"" + options: + - text: "\"I need a job.\"" + goto: assign_task + condition: + player_flag: assassin_task_mob + not: true + - text: "\"What's my current task?\"" + goto: current_task + condition: + player_flag: assassin_task_mob + not: false + - text: "\"I want to skip my task.\"" + goto: skip_confirm + condition: + player_flag: assassin_task_mob + not: false + - text: "\"I want to extend my task.\"" + goto: extend_confirm + condition: + all_of: + - player_flag: assassin_task_mob + not: false + - player_flag: assassin_unlocked_extend + value: true + - text: "\"I'd like to browse the Reputation Shop.\"" + goto: rep_shop + - text: "\"I need supplies.\"" + goto: supplies + - text: "\"Goodbye.\"" + end: true + + assign_task: + message: "\"Let me check what's available...\" The Client scrolls through their data pad." + action: + assign_task: true + options: + - text: "\"Understood.\"" + end: true + - text: "\"What else do you have?\"" + goto: start + + current_task: + message: "\"Let me check your file.\" The Client pulls up your record." + options: + - text: "\"Okay.\"" + end: true + + skip_confirm: + message: "\"Skipping a task costs 30 Reputation and resets your streak. Are you sure?\"" + options: + - text: "\"Yes, skip it.\"" + goto: skip_done + - text: "\"Never mind.\"" + goto: start + + skip_done: + message: "\"Task cancelled. Your streak has been reset.\"" + action: + skip_task: true + options: + - text: "\"Give me a new one.\"" + goto: assign_task + - text: "\"Goodbye.\"" + end: true + + extend_confirm: + message: "\"Extending your task costs 30 Reputation and adds more kills. Want to proceed?\"" + options: + - text: "\"Yes, extend it.\"" + goto: extend_done + - text: "\"Never mind.\"" + goto: start + + extend_done: + message: "\"Done. I've added more targets to your contract.\"" + action: + extend_task: true + options: + - text: "\"Thanks.\"" + end: true + + rep_shop: + message: "\"Here's what I've got. All purchases are permanent.\"" + options: + - text: "\"Auto-finish: Salt (200 Rep) - Never consume salt on finishing blows.\"" + goto: buy_auto_salt + condition: + player_flag: assassin_unlocked_auto_salt + not: true + - text: "\"Auto-finish: Acid Vial (200 Rep) - Never consume acid vials on finishing blows.\"" + goto: buy_auto_acid + condition: + player_flag: assassin_unlocked_auto_acid_vial + not: true + - text: "\"Unlock Superior Mobs (300 Rep) - Rare superior variants may spawn.\"" + goto: buy_superiors + condition: + player_flag: assassin_unlocked_superiors + not: true + - text: "\"Unlock Extended Tasks (100 Rep) - Allows extending tasks.\"" + goto: buy_extend_unlock + condition: + player_flag: assassin_unlocked_extend + not: true + - text: "\"Back.\"" + goto: start + + buy_auto_salt: + message: "\"Auto-salt purchased. You'll no longer consume salt on finishing blows.\"" + action: + reputation_cost: 200 + set_player_flags: + assassin_unlocked_auto_salt: true + options: + - text: "\"Thanks.\"" + goto: rep_shop + + buy_auto_acid: + message: "\"Auto-acid purchased. Acid vials will no longer be consumed.\"" + action: + reputation_cost: 200 + set_player_flags: + assassin_unlocked_auto_acid_vial: true + options: + - text: "\"Thanks.\"" + goto: rep_shop + + buy_superiors: + message: "\"Superior encounters unlocked. Watch yourself out there.\"" + action: + reputation_cost: 300 + set_player_flags: + assassin_unlocked_superiors: true + options: + - text: "\"Thanks.\"" + goto: rep_shop + + buy_extend_unlock: + message: "\"You can now extend tasks via our conversation.\"" + action: + reputation_cost: 100 + set_player_flags: + assassin_unlocked_extend: true + options: + - text: "\"Thanks.\"" + goto: rep_shop + + supplies: + message: "\"I stock everything you need for the job. Cheap, too.\"" + options: + - text: "\"Buy salt (5 credits each).\"" + goto: buy_salt + condition: + min_credits: 5 + - text: "\"Buy acid vial (10 credits each).\"" + goto: buy_acid + condition: + min_credits: 10 + - text: "\"Buy insulated gloves (50 credits).\"" + goto: buy_gloves + condition: + min_credits: 50 + - text: "\"Buy spectral visor (75 credits).\"" + goto: buy_visor + condition: + min_credits: 75 + - text: "\"Back.\"" + goto: start + + buy_salt: + message: "\"Here you go.\" The Client slides a packet of salt across the table." + action: + give_item: salt + cost: 5 + options: + - text: "\"Buy more.\"" + goto: buy_salt + condition: + min_credits: 5 + - text: "\"Thanks.\"" + goto: supplies + + buy_acid: + message: "\"Handle with care.\" The Client passes you a vial of corrosive acid." + action: + give_item: acid_vial + cost: 10 + options: + - text: "\"Buy more.\"" + goto: buy_acid + condition: + min_credits: 10 + - text: "\"Thanks.\"" + goto: supplies + + buy_gloves: + message: "\"These'll keep the current from frying your hands.\" The Client tosses you a pair of thick rubber gloves." + action: + give_item: insulated_gloves + cost: 50 + options: + - text: "\"Thanks.\"" + goto: supplies + + buy_visor: + message: "\"Spectral frequency filter. Makes the invisible visible — and keeps their attacks from scrambling your brain.\"" + action: + give_item: spectral_visor + cost: 75 + options: + - text: "\"Thanks.\"" + goto: supplies diff --git a/data/behaviors/general_store.yaml b/data/behaviors/general_store.yaml new file mode 100644 index 0000000..657e842 --- /dev/null +++ b/data/behaviors/general_store.yaml @@ -0,0 +1,49 @@ +id: general_store +type: talk +nodes: + start: + message: "\"Welcome to the General Store! Need any supplies?\"" + options: + - text: "\"I'd like to browse.\"" + goto: shop + - text: "\"Goodbye.\"" + end: true + shop: + message: "\"Take your time. What can I get you?\"" + action: + shop: + message: "What would you like to buy or sell?" + items: + - item_id: fishing_rod + buy_price: 10 + sell_price: 2 + - item_id: fishing_bait + buy_price: 2 + sell_price: 0 + - item_id: matches + buy_price: 10 + sell_price: 2 + - item_id: firesteel + buy_price: 40 + sell_price: 10 + - item_id: lighter + buy_price: 30 + sell_price: 7 + - item_id: hammer + buy_price: 20 + sell_price: 5 + - item_id: knife + buy_price: 10 + sell_price: 2 + - item_id: chisel + buy_price: 10 + sell_price: 2 + - item_id: bread + buy_price: 10 + sell_price: 2 + - item_id: bowstring + buy_price: 20 + sell_price: 5 + options: + - text: "\"I'm done.\"" + goto: start diff --git a/data/behaviors/stall_guard_talk.yaml b/data/behaviors/stall_guard_talk.yaml new file mode 100644 index 0000000..c9434af --- /dev/null +++ b/data/behaviors/stall_guard_talk.yaml @@ -0,0 +1,36 @@ +id: stall_guard_talk +type: talk +nodes: + start: + message: "Caught you red-handed! You have three options, thief." + options: + - text: "\"I'll pay a fine. (500 credits)\"" + goto: bribe + condition: + min_credits: 500 + - text: "\"Take me to jail.\"" + goto: jail + - text: "\"You'll have to catch me first!\"" + goto: fight + bribe: + message: "Smart choice. Hand over 500 credits and we'll forget this happened." + action: + cost: 500 + options: + - text: "\"Fine, take it.\"" + end: true + jail: + message: "Off to the detention cell with you!" + action: + teleport: 162 + options: + - text: "(You are dragged away)" + end: true + fight: + message: "Then defend yourself!" + action: + set_flags: + guard_hostile: true + options: + - text: "(The guard attacks!)" + end: true
\ No newline at end of file diff --git a/data/behaviors/weapons_shop.yaml b/data/behaviors/weapons_shop.yaml new file mode 100644 index 0000000..d12a4e6 --- /dev/null +++ b/data/behaviors/weapons_shop.yaml @@ -0,0 +1,43 @@ +id: weapons_shop +type: talk +nodes: + start: + message: "\"Welcome to the Smithy! Finest weapons and armor on the asteroid.\"" + options: + - text: "\"Show me what you've got.\"" + goto: shop + - text: "\"Nothing right now.\"" + end: true + shop: + message: "\"Here's the current inventory. All quality guaranteed.\"" + action: + shop: + message: "The Smithy — buy or sell weapons and armor." + items: + - item_id: bronze_sword + buy_price: 25 + sell_price: 6 + - item_id: iron_sword + buy_price: 60 + sell_price: 15 + - item_id: bronze_axe + buy_price: 20 + sell_price: 5 + - item_id: iron_axe + buy_price: 50 + sell_price: 12 + - item_id: bronze_pickaxe + buy_price: 20 + sell_price: 5 + - item_id: iron_pickaxe + buy_price: 50 + sell_price: 12 + - item_id: bronze_med_helm + buy_price: 16 + sell_price: 4 + - item_id: iron_med_helm + buy_price: 40 + sell_price: 10 + options: + - text: "\"That's all for now.\"" + goto: start
\ No newline at end of file diff --git a/data/drops/bioengineer_steal.yaml b/data/drops/bioengineer_steal.yaml new file mode 100644 index 0000000..3e5e7ce --- /dev/null +++ b/data/drops/bioengineer_steal.yaml @@ -0,0 +1,20 @@ +id: bioengineer_steal +drops: + - item_id: sweetcorn_seed + weight: 25 + quantity: 1 + - item_id: strawberry_seed + weight: 20 + quantity: 1 + - item_id: watermelon_seed + weight: 20 + quantity: 1 + - item_id: ranarr_seed + weight: 15 + quantity: 1 + - item_id: snapdragon_seed + weight: 12 + quantity: 1 + - item_id: torstol_seed + weight: 8 + quantity: 1
\ No newline at end of file diff --git a/data/drops/credit_stick_drop.yaml b/data/drops/credit_stick_drop.yaml new file mode 100644 index 0000000..d78dc57 --- /dev/null +++ b/data/drops/credit_stick_drop.yaml @@ -0,0 +1,17 @@ +id: credit_stick_drop +drops: + - item_id: credits + weight: 40 + quantity: 15 + - item_id: credits + weight: 30 + quantity: 30 + - item_id: credits + weight: 20 + quantity: 50 + - item_id: credits + weight: 8 + quantity: 100 + - item_id: credits + weight: 2 + quantity: 250
\ No newline at end of file diff --git a/data/drops/farmer_steal.yaml b/data/drops/farmer_steal.yaml new file mode 100644 index 0000000..e64f245 --- /dev/null +++ b/data/drops/farmer_steal.yaml @@ -0,0 +1,20 @@ +id: farmer_steal +drops: + - item_id: potato_seed + weight: 30 + quantity: 1 + - item_id: onion_seed + weight: 25 + quantity: 1 + - item_id: cabbage_seed + weight: 20 + quantity: 1 + - item_id: tomato_seed + weight: 15 + quantity: 1 + - item_id: sweetcorn_seed + weight: 8 + quantity: 1 + - item_id: strawberry_seed + weight: 2 + quantity: 1
\ No newline at end of file diff --git a/data/drops/man_steal.yaml b/data/drops/man_steal.yaml new file mode 100644 index 0000000..d0ec45f --- /dev/null +++ b/data/drops/man_steal.yaml @@ -0,0 +1,8 @@ +id: man_steal +drops: + - item_id: credit_stick + weight: 80 + quantity: 1 + - item_id: credits + weight: 20 + quantity: 5
\ No newline at end of file diff --git a/data/drops/market_stall_steal.yaml b/data/drops/market_stall_steal.yaml new file mode 100644 index 0000000..e45c878 --- /dev/null +++ b/data/drops/market_stall_steal.yaml @@ -0,0 +1,11 @@ +id: market_stall_steal +drops: + - item_id: bread + weight: 40 + quantity: 1 + - item_id: apple + weight: 35 + quantity: 1 + - item_id: cheese + weight: 25 + quantity: 1
\ No newline at end of file diff --git a/data/help/assassin.yaml b/data/help/assassin.yaml new file mode 100644 index 0000000..60508e0 --- /dev/null +++ b/data/help/assassin.yaml @@ -0,0 +1,27 @@ +name: "assassin" +category: "Skills" +description: | + The Assassin skill (Slayer equivalent). + + Talk to The Client in The Assassin's Den to receive tasks. Each task + assigns you a number of specific mobs to kill. Killing mobs on-task + awards Assassin XP (mob's max HP x 2) in addition to normal combat XP. + + Some mobs require a minimum Assassin level to attack: + Slug - Level 1 (needs salt to finish off) + Drone - Level 15 (extra damage without insulated gloves) + Crawler - Level 30 (needs acid vial to finish off) + Phantom - Level 45 (extra damage without spectral visor) + + Finishing Blow: Some mobs cannot be killed below 1 HP. Use the + required item on them during combat: "use salt on slug" + + Protection: Some mobs deal 1.5x damage unless you have the + required protective item equipped. + + Reputation is earned on task completion (1 per task + streak bonuses + at 10th, 50th, 100th, 250th, and 1000th consecutive tasks). + Spend Reputation at The Client's Reputation Shop for permanent + unlocks and task management options. + + Related: task, attack, use diff --git a/data/help/shop.yaml b/data/help/shop.yaml new file mode 100644 index 0000000..a0fd187 --- /dev/null +++ b/data/help/shop.yaml @@ -0,0 +1,20 @@ +name: "shop" +category: "Interaction" +description: | + Buy and sell items at shops. + + Find a shopkeeper NPC and talk to them to open their shop. While + browsing a shop, these commands are available: + + browse List all items for sale with buy/sell prices + list Same as browse + buy <item> Purchase an item with credits (use # to buy by number) + sell <item> Sell an item from your inventory for credits + leave Exit the shop and return to conversation + + Shops buy items at a lower price than they sell them. Some items + cannot be sold back. + + Shopkeepers can be found throughout the world: + - General Store: Town Square (sells tools and supplies) + - Smithy: Forge (sells weapons, armor, and tools)
\ No newline at end of file diff --git a/data/help/sneak.yaml b/data/help/sneak.yaml new file mode 100644 index 0000000..b10866c --- /dev/null +++ b/data/help/sneak.yaml @@ -0,0 +1,19 @@ +name: "sneak" +category: "Skills" +description: | + Toggle sneak mode on and off. + + Usage: sneak + + While sneaking, you receive messages telling you when guards are + watching or looking away from objects they protect. Use this + information to time your steals for when the guard is distracted. + + Stealing from a guarded object while the guard is looking away + has no penalty on failure. Stealing while the guard is watching + halves your success chance, and a failure causes the guard to + confront you. + + Sneak mode is lost when you disconnect. + + See also: help steal, help thieving
\ No newline at end of file diff --git a/data/help/steal.yaml b/data/help/steal.yaml new file mode 100644 index 0000000..dc12a2f --- /dev/null +++ b/data/help/steal.yaml @@ -0,0 +1,29 @@ +name: "steal" +category: "Skills" +description: | + Steal from mobs or objects. + + Usage: steal [target] + + Attempts to pickpocket a mob or shoplift from an object. Requires + a minimum thieving level depending on the target. + + If there is only one stealable target in the room, you can type + just "steal". If there are multiple different targets, you must + specify: "steal man", "steal stall", "steal 2.man". + + On success, you receive a random item from the target's loot table + and gain thieving XP. The action repeats automatically until you + run out of inventory space or are interrupted. + + On failure against a mob, the mob turns hostile and attacks you. + + On failure against a guarded object while the guard is watching, + the guard confronts you with options to pay a bribe, go to jail, + or fight. + + Use "sneak" to see when guards are watching or looking away. + + Aliases: thieve + + See also: help sneak, help thieving
\ No newline at end of file diff --git a/data/help/task.yaml b/data/help/task.yaml new file mode 100644 index 0000000..2b38e04 --- /dev/null +++ b/data/help/task.yaml @@ -0,0 +1,13 @@ +name: "task" +category: "Assassin" +description: | + Check your current Assassin task status. + + Usage: task + + Displays your current task target, kills remaining, streak count, + and unspent Reputation points. If you have no active task, visit + The Client to receive a new assignment. + + The Client can be found in The Assassin's Den, accessible from + the lower levels of the settlement. diff --git a/data/help/thieving.yaml b/data/help/thieving.yaml new file mode 100644 index 0000000..e5aea10 --- /dev/null +++ b/data/help/thieving.yaml @@ -0,0 +1,21 @@ +name: "thieving" +category: "Skills" +description: | + Thieving lets you steal from mobs and objects for loot and XP. + + Targets: + Man - Level 1, 8 XP - Credit sticks + Market Stall - Level 5, 12 XP - Food items (guarded) + Farmer - Level 10, 15 XP - Low/mid seeds + Bioengineer - Level 38, 45 XP - Mid/high seeds + + Success chance increases with your thieving level relative to + the target's requirement. Failing against a mob starts combat. + Failing against a guarded object while the guard watches triggers + a confrontation (bribe, jail, or fight). + + Credit sticks obtained from stealing can be searched for credits. + + Commands: steal, sneak + + See also: help steal, help sneak
\ No newline at end of file diff --git a/data/items/acid_vial.yaml b/data/items/acid_vial.yaml new file mode 100644 index 0000000..7a49e2d --- /dev/null +++ b/data/items/acid_vial.yaml @@ -0,0 +1,6 @@ +id: acid_vial +name: acid vial +color: "46" +description: "A small vial of concentrated acid. Used to dissolve crawlers." +value: 10 +stackable: true diff --git a/data/items/apple.yaml b/data/items/apple.yaml new file mode 100644 index 0000000..10c09c1 --- /dev/null +++ b/data/items/apple.yaml @@ -0,0 +1,8 @@ +id: apple +name: apple +color: "196" +description: "A bright red apple." +value: 3 +stackable: false +heal_value: 2 +eat_message: "You eat the apple. Refreshing."
\ No newline at end of file diff --git a/data/items/cabbage_seed.yaml b/data/items/cabbage_seed.yaml new file mode 100644 index 0000000..35ebee3 --- /dev/null +++ b/data/items/cabbage_seed.yaml @@ -0,0 +1,6 @@ +id: cabbage_seed +name: cabbage seed +color: "34" +description: "A seed for growing cabbages." +value: 4 +stackable: true
\ No newline at end of file diff --git a/data/items/cheese.yaml b/data/items/cheese.yaml new file mode 100644 index 0000000..79d9fe8 --- /dev/null +++ b/data/items/cheese.yaml @@ -0,0 +1,8 @@ +id: cheese +name: cheese +color: "226" +description: "A wedge of sharp cheese." +value: 4 +stackable: false +heal_value: 2 +eat_message: "You eat the cheese. Tasty."
\ No newline at end of file diff --git a/data/items/chitin_plate.yaml b/data/items/chitin_plate.yaml new file mode 100644 index 0000000..4148a2a --- /dev/null +++ b/data/items/chitin_plate.yaml @@ -0,0 +1,6 @@ +id: chitin_plate +name: chitin plate +color: "130" +description: "A thick plate of biological armor from a crawler's exoskeleton." +value: 50 +stackable: false diff --git a/data/items/circuit_board.yaml b/data/items/circuit_board.yaml new file mode 100644 index 0000000..5b4db83 --- /dev/null +++ b/data/items/circuit_board.yaml @@ -0,0 +1,6 @@ +id: circuit_board +name: circuit board +color: "40" +description: "A scorched circuit board salvaged from a destroyed drone." +value: 25 +stackable: false diff --git a/data/items/credit_stick.yaml b/data/items/credit_stick.yaml new file mode 100644 index 0000000..46b3050 --- /dev/null +++ b/data/items/credit_stick.yaml @@ -0,0 +1,9 @@ +id: credit_stick +name: credit stick +color: "220" +description: "A small electronic stick loaded with credits. You can search it to extract the credits." +value: 10 +stackable: false +search_table: credit_stick_drop +search_ticks: 2 +search_message: "cracking open the credit stick"
\ No newline at end of file diff --git a/data/items/ectoplasm.yaml b/data/items/ectoplasm.yaml new file mode 100644 index 0000000..8417e2d --- /dev/null +++ b/data/items/ectoplasm.yaml @@ -0,0 +1,6 @@ +id: ectoplasm +name: ectoplasm +color: "159" +description: "A shimmering residue left behind by a destroyed phantom." +value: 75 +stackable: false diff --git a/data/items/insulated_gloves.yaml b/data/items/insulated_gloves.yaml new file mode 100644 index 0000000..3e53614 --- /dev/null +++ b/data/items/insulated_gloves.yaml @@ -0,0 +1,9 @@ +id: insulated_gloves +name: insulated gloves +color: "214" +description: "Heavy rubber gloves that protect against electrical attacks. Essential when fighting drones." +value: 50 +stackable: false +equip_slot: hands +stats: + defense_bonus: 1 diff --git a/data/items/onion_seed.yaml b/data/items/onion_seed.yaml new file mode 100644 index 0000000..fbf6a8a --- /dev/null +++ b/data/items/onion_seed.yaml @@ -0,0 +1,6 @@ +id: onion_seed +name: onion seed +color: "229" +description: "A seed for growing onions." +value: 3 +stackable: true
\ No newline at end of file diff --git a/data/items/potato_seed.yaml b/data/items/potato_seed.yaml new file mode 100644 index 0000000..8cc4669 --- /dev/null +++ b/data/items/potato_seed.yaml @@ -0,0 +1,6 @@ +id: potato_seed +name: potato seed +color: "94" +description: "A seed for growing potatoes." +value: 2 +stackable: true
\ No newline at end of file diff --git a/data/items/ranarr_seed.yaml b/data/items/ranarr_seed.yaml new file mode 100644 index 0000000..af464ec --- /dev/null +++ b/data/items/ranarr_seed.yaml @@ -0,0 +1,6 @@ +id: ranarr_seed +name: ranarr seed +color: "28" +description: "A rare herb seed with potent alchemical properties." +value: 500 +stackable: true
\ No newline at end of file diff --git a/data/items/salt.yaml b/data/items/salt.yaml new file mode 100644 index 0000000..d9c1b2a --- /dev/null +++ b/data/items/salt.yaml @@ -0,0 +1,6 @@ +id: salt +name: salt +color: "255" +description: "A packet of coarse industrial salt. Used to destroy slugs." +value: 5 +stackable: true diff --git a/data/items/slug_mucus.yaml b/data/items/slug_mucus.yaml new file mode 100644 index 0000000..620038a --- /dev/null +++ b/data/items/slug_mucus.yaml @@ -0,0 +1,6 @@ +id: slug_mucus +name: slug mucus +color: "82" +description: "A glob of toxic slug mucus. Unpleasant." +value: 5 +stackable: false diff --git a/data/items/snapdragon_seed.yaml b/data/items/snapdragon_seed.yaml new file mode 100644 index 0000000..3ec440d --- /dev/null +++ b/data/items/snapdragon_seed.yaml @@ -0,0 +1,6 @@ +id: snapdragon_seed +name: snapdragon seed +color: "92" +description: "An extremely rare herb seed. Highly valued by alchemists." +value: 1500 +stackable: true
\ No newline at end of file diff --git a/data/items/spectral_visor.yaml b/data/items/spectral_visor.yaml new file mode 100644 index 0000000..851a929 --- /dev/null +++ b/data/items/spectral_visor.yaml @@ -0,0 +1,9 @@ +id: spectral_visor +name: spectral visor +color: "141" +description: "A visor fitted with spectral frequency filters. Dampens psychic attacks from phantoms." +value: 75 +stackable: false +equip_slot: head +stats: + defense_bonus: 2 diff --git a/data/items/strawberry_seed.yaml b/data/items/strawberry_seed.yaml new file mode 100644 index 0000000..bcf8ddc --- /dev/null +++ b/data/items/strawberry_seed.yaml @@ -0,0 +1,6 @@ +id: strawberry_seed +name: strawberry seed +color: "197" +description: "A seed for growing strawberries." +value: 40 +stackable: true
\ No newline at end of file diff --git a/data/items/sweetcorn_seed.yaml b/data/items/sweetcorn_seed.yaml new file mode 100644 index 0000000..56bc587 --- /dev/null +++ b/data/items/sweetcorn_seed.yaml @@ -0,0 +1,6 @@ +id: sweetcorn_seed +name: sweetcorn seed +color: "226" +description: "A seed for growing sweetcorn." +value: 25 +stackable: true
\ No newline at end of file diff --git a/data/items/tomato_seed.yaml b/data/items/tomato_seed.yaml new file mode 100644 index 0000000..622db9b --- /dev/null +++ b/data/items/tomato_seed.yaml @@ -0,0 +1,6 @@ +id: tomato_seed +name: tomato seed +color: "196" +description: "A seed for growing tomatoes." +value: 8 +stackable: true
\ No newline at end of file diff --git a/data/items/torstol_seed.yaml b/data/items/torstol_seed.yaml new file mode 100644 index 0000000..5db6f9d --- /dev/null +++ b/data/items/torstol_seed.yaml @@ -0,0 +1,6 @@ +id: torstol_seed +name: torstol seed +color: "46" +description: "The rarest of herb seeds. Worth a small fortune." +value: 5000 +stackable: true
\ No newline at end of file diff --git a/data/items/watermelon_seed.yaml b/data/items/watermelon_seed.yaml new file mode 100644 index 0000000..adfecc2 --- /dev/null +++ b/data/items/watermelon_seed.yaml @@ -0,0 +1,6 @@ +id: watermelon_seed +name: watermelon seed +color: "34" +description: "A seed for growing watermelons." +value: 80 +stackable: true
\ No newline at end of file diff --git a/data/mobs/bioengineer.yaml b/data/mobs/bioengineer.yaml new file mode 100644 index 0000000..a6f2823 --- /dev/null +++ b/data/mobs/bioengineer.yaml @@ -0,0 +1,44 @@ +id: bioengineer +name: Bioengineer +description: "A lab-coated scientist carrying a satchel of genetically modified seeds. Her pockets are stuffed with rare specimens." +combat_descriptions: + - "jabs a syringe at %s" + - "is being overpowered by %s" + - "fights desperately against %s" +idle_descriptions: + - "scribbles notes on a clipboard" + - "carefully inspects a vial of green liquid" + - "adjusts her safety goggles" + - "mutters about gene splicing yields" +attack: 6 +strength: 4 +defense: 5 +hp: 25 +speed: 5 +aggressive: false +respawn_ticks: 50 +attack_type: crush +stab_defense: 0 +slash_defense: 0 +crush_defense: 0 +science_defense: 0 +ranged_defense: 0 +steal_table: bioengineer_steal +steal_level: 38 +steal_xp: 45 +steal_speed: 4 +drops: + remains: "bones" + loot: + - item_id: "sweetcorn_seed" + weight: 30 + quantity: 2 + - item_id: "strawberry_seed" + weight: 25 + quantity: 1 + - item_id: "watermelon_seed" + weight: 15 + quantity: 1 + - item_id: "ranarr_seed" + weight: 5 + quantity: 1
\ No newline at end of file diff --git a/data/mobs/client.yaml b/data/mobs/client.yaml new file mode 100644 index 0000000..9bcae4e --- /dev/null +++ b/data/mobs/client.yaml @@ -0,0 +1,20 @@ +id: client +name: The Client +description: "A shadowy figure in a dark coat. They speak in clipped, measured tones and seem to know everything about every creature in the sector." +unique: true +protected: true +behavior: client_talk +attack: 1 +strength: 1 +defense: 1 +hp: 100 +speed: 5 +aggressive: false +respawn_ticks: 30 +idle_descriptions: + - "reviews a holographic dossier" + - "marks a location on a worn star chart" + - "flips a credit chit between their fingers" + - "mutters coordinates under their breath" + - "glances at you appraisingly" + - "taps a data pad with a stylus" diff --git a/data/mobs/crawler.yaml b/data/mobs/crawler.yaml new file mode 100644 index 0000000..ace034b --- /dev/null +++ b/data/mobs/crawler.yaml @@ -0,0 +1,32 @@ +id: crawler +name: crawler +description: "A heavily armored bio-mechanical creature with a chitinous exoskeleton. Its regenerative biology prevents death unless dissolved with acid." +assassin_level: 30 +finishing_blow: acid_vial +attack: 25 +strength: 22 +defense: 30 +hp: 80 +speed: 5 +aggressive: false +respawn_ticks: 45 +idle_descriptions: + - "scrapes its mandibles together menacingly" + - "clicks and chitters in an alien rhythm" + - "tests the air with feathered antennae" + - "coils its segmented body defensively" +combat_descriptions: + - "snaps its mandibles at %s" + - "lashes out with a barbed tail at %s" + - "charges headlong into %s" +drops: + remains: chitin_plate + loot: + - item_id: credits + weight: 70 + quantity: 250 + - item_id: acid_vial + weight: 10 + - item_id: credits + weight: 20 + quantity: 500 diff --git a/data/mobs/drone.yaml b/data/mobs/drone.yaml new file mode 100644 index 0000000..4ceafc0 --- /dev/null +++ b/data/mobs/drone.yaml @@ -0,0 +1,32 @@ +id: drone +name: drone +description: "A malfunctioning security drone crackling with electrical discharge. Its attacks are especially dangerous to anyone not wearing insulated gloves." +assassin_level: 15 +damage_without: insulated_gloves +attack: 15 +strength: 14 +defense: 12 +hp: 45 +speed: 4 +aggressive: true +respawn_ticks: 35 +idle_descriptions: + - "hovers erratically, sparking" + - "emits a high-pitched whine" + - "scans the area with a flickering red beam" + - "rotates its weapon array with a mechanical click" +combat_descriptions: + - "fires an electrical bolt at %s" + - "charges its capacitors and zaps %s" + - "swoops down on %s with crackling energy" +drops: + remains: circuit_board + loot: + - item_id: credits + weight: 80 + quantity: 75 + - item_id: insulated_gloves + weight: 5 + - item_id: credits + weight: 15 + quantity: 200 diff --git a/data/mobs/farmer.yaml b/data/mobs/farmer.yaml new file mode 100644 index 0000000..7b12b9f --- /dev/null +++ b/data/mobs/farmer.yaml @@ -0,0 +1,44 @@ +id: farmer +name: Farmer +description: "A weathered farmer in muddy overalls, pockets bulging with seeds." +combat_descriptions: + - "swings a shovel at %s" + - "is getting beaten by %s" + - "grapples with %s" +idle_descriptions: + - "examines a handful of seeds" + - "wipes dirt from his hands" + - "mutters about the growing season" + - "adjusts his wide-brimmed hat" +attack: 3 +strength: 3 +defense: 3 +hp: 15 +speed: 5 +aggressive: false +respawn_ticks: 40 +attack_type: crush +stab_defense: 0 +slash_defense: 0 +crush_defense: 0 +science_defense: 0 +ranged_defense: 0 +steal_table: farmer_steal +steal_level: 10 +steal_xp: 15 +steal_speed: 4 +drops: + remains: "bones" + loot: + - item_id: "potato_seed" + weight: 40 + quantity: 3 + - item_id: "onion_seed" + weight: 30 + quantity: 2 + - item_id: "cabbage_seed" + weight: 20 + quantity: 2 + - item_id: "tomato_seed" + weight: 10 + quantity: 1
\ No newline at end of file diff --git a/data/mobs/general_store_clerk.yaml b/data/mobs/general_store_clerk.yaml new file mode 100644 index 0000000..842a98b --- /dev/null +++ b/data/mobs/general_store_clerk.yaml @@ -0,0 +1,24 @@ +id: general_store_clerk +name: Shopkeeper +description: "A friendly merchant with a tidy apron, standing behind a well-stocked counter." +behavior: general_store +unique: true +protected: true +hp: 50 +attack: 1 +strength: 1 +defense: 1 +speed: 5 +aggressive: false +respawn_ticks: 30 +attack_type: crush +stab_defense: 0 +slash_defense: 0 +crush_defense: 0 +science_defense: 0 +ranged_defense: 0 +idle_descriptions: + - "arranges items on the shelves" + - "polishes the counter" + - "counts credits in the register" + - "greets a passerby with a nod"
\ No newline at end of file diff --git a/data/mobs/man.yaml b/data/mobs/man.yaml index fe60cbd..c65f85c 100644 --- a/data/mobs/man.yaml +++ b/data/mobs/man.yaml @@ -21,6 +21,10 @@ hp: 7 speed: 5 aggressive: false respawn_ticks: 30 +steal_table: man_steal +steal_level: 1 +steal_xp: 8 +steal_speed: 4 attack_type: crush stab_defense: 0 slash_defense: 0 diff --git a/data/mobs/phantom.yaml b/data/mobs/phantom.yaml new file mode 100644 index 0000000..3fb1893 --- /dev/null +++ b/data/mobs/phantom.yaml @@ -0,0 +1,32 @@ +id: phantom +name: phantom +description: "A semi-transparent entity that phases in and out of visible light. Its psychic attacks are devastating to anyone without a spectral visor." +assassin_level: 45 +damage_without: spectral_visor +attack: 35 +strength: 30 +defense: 25 +hp: 100 +speed: 3 +aggressive: true +respawn_ticks: 50 +idle_descriptions: + - "flickers between visible and invisible" + - "emits a low, resonant hum" + - "drifts through a wall and back again" + - "stares at you with hollow, glowing eyes" +combat_descriptions: + - "blasts %s with a psychic wave" + - "phases through %s's defenses" + - "unleashes a spectral shriek at %s" +drops: + remains: ectoplasm + loot: + - item_id: credits + weight: 60 + quantity: 500 + - item_id: spectral_visor + weight: 3 + - item_id: credits + weight: 37 + quantity: 1000 diff --git a/data/mobs/slug.yaml b/data/mobs/slug.yaml new file mode 100644 index 0000000..2275ca7 --- /dev/null +++ b/data/mobs/slug.yaml @@ -0,0 +1,27 @@ +id: slug +name: slug +description: "A bloated, translucent slug the size of a dog. Its skin glistens with toxic mucus. It cannot be killed by conventional means — only salt can destroy it." +assassin_level: 1 +finishing_blow: salt +attack: 3 +strength: 3 +defense: 1 +hp: 15 +speed: 6 +aggressive: false +respawn_ticks: 25 +idle_descriptions: + - "oozes along the floor leaving a slimy trail" + - "contracts and expands rhythmically" + - "extends its eyestalks toward you" + - "secretes a glob of toxic mucus" +combat_descriptions: + - "lunges slimily at %s" + - "sprays mucus toward %s" + - "writhes in combat with %s" +drops: + remains: slug_mucus + loot: + - item_id: credits + weight: 100 + quantity: 15 diff --git a/data/mobs/weapons_smith.yaml b/data/mobs/weapons_smith.yaml new file mode 100644 index 0000000..da6ef3b --- /dev/null +++ b/data/mobs/weapons_smith.yaml @@ -0,0 +1,24 @@ +id: weapons_smith +name: Weaponsmith +description: "A burly smith with soot-stained hands and a heavy leather apron, standing proudly by the forge." +behavior: weapons_shop +unique: true +protected: true +hp: 80 +attack: 1 +strength: 1 +defense: 1 +speed: 5 +aggressive: false +respawn_ticks: 30 +attack_type: crush +stab_defense: 0 +slash_defense: 0 +crush_defense: 0 +science_defense: 0 +ranged_defense: 0 +idle_descriptions: + - "hammers at a glowing blade on the anvil" + - "wipes soot from his brow" + - "inspects a finished sword" + - "tends to the furnace fire"
\ No newline at end of file diff --git a/data/objects/market_stall.yaml b/data/objects/market_stall.yaml new file mode 100644 index 0000000..a94d5cb --- /dev/null +++ b/data/objects/market_stall.yaml @@ -0,0 +1,11 @@ +id: market_stall +name: Market Stall +description: "A wooden stall piled with food and sundries. The vendor doesn't seem particularly attentive." +color: "179" +inroom_description: "A bustling {179}market stall{/} is set up here." +hidden: false +steal_table: market_stall_steal +steal_level: 5 +steal_xp: 12 +steal_speed: 5 +guard_mob: guard
\ No newline at end of file diff --git a/data/rooms/1.yaml b/data/rooms/1.yaml index 6fe6518..fa3fb9b 100644 --- a/data/rooms/1.yaml +++ b/data/rooms/1.yaml @@ -12,6 +12,7 @@ objects: - id: charging_station mobs: - "newbie_trainer" + - "general_store_clerk" spawns: - item_id: cape_of_agility quantity: 1 diff --git a/data/rooms/100.yaml b/data/rooms/100.yaml deleted file mode 100644 index b8b0f1e..0000000 --- a/data/rooms/100.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 100 -name: "Grand Concourse" -description: "The expansive white platform of Station X1's main thoroughfare. Neon strips pulse along the ceiling, reflecting off polished permacrete floors. Citizens and synthetics stream past in a constant dance of commerce and purpose." -map_symbol: "+" -exits: - east: 101 - north: 110 diff --git a/data/rooms/101.yaml b/data/rooms/101.yaml deleted file mode 100644 index fa5af97..0000000 --- a/data/rooms/101.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 101 -name: "Customs & Immigration" -description: "A long hall lined with automated kiosks. Holographic scanners bathe travelers in cool blue light as bio-signatures are verified. The air smells of ozone and recycled stationery." -map_symbol: "#" -exits: - west: 100 - east: 102 - north: 111 diff --git a/data/rooms/102.yaml b/data/rooms/102.yaml deleted file mode 100644 index 1bc8573..0000000 --- a/data/rooms/102.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 102 -name: "Port Authority Office" -description: "Cluttered desks float in low gravity, terminals displaying shipping manifests and docking schedules. A large viewscreen shows the constant traffic of orbital freighters beyond the station." -map_symbol: "P" -exits: - west: 101 - east: 103 - north: 112 diff --git a/data/rooms/103.yaml b/data/rooms/103.yaml deleted file mode 100644 index f216fc4..0000000 --- a/data/rooms/103.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 103 -name: "Interstellar Cargo Bay 3" -description: "Massive crates stamped with corporate logos are stacked to the ceiling. Loading clamps hum with magnetic tension. Echoes of distant announcements reverberate through the spacious bay." -map_symbol: "C" -exits: - west: 102 - east: 104 - north: 113 diff --git a/data/rooms/104.yaml b/data/rooms/104.yaml deleted file mode 100644 index c7821c1..0000000 --- a/data/rooms/104.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 104 -name: "Freight Elevator Lobby" -description: "A utilitarian chamber where cargo lifters descend into the station's underbelly. Indicator lights blink in sequence as massive elevator doors cycle open and closed with hydraulic sighs." -map_symbol: "E" -exits: - west: 103 - east: 105 diff --git a/data/rooms/105.yaml b/data/rooms/105.yaml deleted file mode 100644 index f07c8aa..0000000 --- a/data/rooms/105.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 105 -name: "Hydroponic Distribution Hub" -description: "Towering racks of fresh produce await sorting. Leafy greens spill from aeroponic towers as workers in sterile suits package the station's food supply for distribution." -map_symbol: "H" -exits: - west: 104 - east: 106 - north: 115 diff --git a/data/rooms/106.yaml b/data/rooms/106.yaml deleted file mode 100644 index a264b01..0000000 --- a/data/rooms/106.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 106 -name: "Recycled Water Processing" -description: "The air is humid and warm. Pipes of all sizes crisscross the ceiling, carrying reclaimed water through filtration stages. The low rumble of pumps is a constant companion." -map_symbol: "W" -exits: - west: 105 - east: 107 diff --git a/data/rooms/107.yaml b/data/rooms/107.yaml deleted file mode 100644 index 34401bc..0000000 --- a/data/rooms/107.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 107 -name: "Atmospheric Scrubber Array" -description: "Wall-mounted scrubbers cycle station air through banks of moss and activated carbon. Green LED strips indicate optimal CO2 absorption. The room breathes like a mechanical lung." -map_symbol: "S" -exits: - west: 106 - east: 108 - north: 117 diff --git a/data/rooms/108.yaml b/data/rooms/108.yaml deleted file mode 100644 index 3f6914a..0000000 --- a/data/rooms/108.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 108 -name: "Power Relay Station Gamma" -description: "Crackling conduits channel raw solar energy from the exterior arrays. Warning signs in three languages adorn every surface. The hum is powerful enough to feel in your chest." -map_symbol: "G" -exits: - west: 107 - east: 109 diff --git a/data/rooms/109.yaml b/data/rooms/109.yaml deleted file mode 100644 index c22b9d0..0000000 --- a/data/rooms/109.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 109 -name: "Solar Array Access Corridor" -description: "A narrow passage leading to the station's exterior skin. Small viewports reveal the blindingly bright solar panels stretching into the void, harvesting the star's energy." -map_symbol: "A" -exits: - west: 108 - north: 119 diff --git a/data/rooms/110.yaml b/data/rooms/110.yaml deleted file mode 100644 index fd9c061..0000000 --- a/data/rooms/110.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 110 -name: "Cryo-Sleep Recovery Ward" -description: "Pale blue light fills this sterile room. Recovery pods line the walls, their occupants slowly reacclimating to standard gravity after decades in suspended animation." -map_symbol: "R" -exits: - south: 100 - east: 111 - north: 120 diff --git a/data/rooms/111.yaml b/data/rooms/111.yaml deleted file mode 100644 index d3a5a2a..0000000 --- a/data/rooms/111.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 111 -name: "Medical Bay Alpha" -description: "State-of-the-art diagnostic bays with hovering holographic displays. Automated medbots glide silently between stations. The scent of antiseptic gel lingers in the recycled air." -map_symbol: "+" -exits: - south: 101 - west: 110 - east: 112 - north: 121 diff --git a/data/rooms/112.yaml b/data/rooms/112.yaml deleted file mode 100644 index d5d46c0..0000000 --- a/data/rooms/112.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 112 -name: "Pharmacy & Genetek" -description: "Racks of color-coded pharmaceuticals glow under UV-protected lighting. A geneprinter in the corner hums as it synthesizes custom therapies and genetic patches on demand." -map_symbol: "G" -exits: - south: 102 - west: 111 - east: 113 - north: 122 diff --git a/data/rooms/113.yaml b/data/rooms/113.yaml deleted file mode 100644 index 4a3333e..0000000 --- a/data/rooms/113.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 113 -name: "Café Synthesizer" -description: "A cozy establishment where food printers whir and steam. Patrons sit at floating tables sipping replicated coffee that is surprisingly close to the real thing." -map_symbol: "c" -exits: - west: 112 - south: 103 diff --git a/data/rooms/115.yaml b/data/rooms/115.yaml deleted file mode 100644 index b0c9a4f..0000000 --- a/data/rooms/115.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 115 -name: "Community Garden Atrium" -description: "Sunlight-mimicking lamps nourish a lush indoor forest of ferns, flowering vines, and small fruit trees. A path of stepping stones winds through the greenery. Birds chirp from hidden perches." -map_symbol: "G" -exits: - south: 105 - east: 116 diff --git a/data/rooms/116.yaml b/data/rooms/116.yaml deleted file mode 100644 index 9c6c258..0000000 --- a/data/rooms/116.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 116 -name: "School of Zero-G Arts" -description: "Students float in a vast open chamber, practicing zero-gravity dance and sculpture. Paint droplets hang suspended mid-air, forming impromptu art installations that drift lazily." -map_symbol: "Z" -exits: - west: 115 - east: 117 - north: 126 diff --git a/data/rooms/117.yaml b/data/rooms/117.yaml deleted file mode 100644 index 5a1ecf0..0000000 --- a/data/rooms/117.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 117 -name: "Library of Forgotten Code" -description: "Data crystals line the walls from floor to ceiling. Vintage terminals allow access to archived knowledge from before the Collapse. The soft click of crystal readers fills the reverent silence." -map_symbol: "L" -exits: - west: 116 - east: 118 - south: 107 - north: 127 diff --git a/data/rooms/118.yaml b/data/rooms/118.yaml deleted file mode 100644 index 95a4a1f..0000000 --- a/data/rooms/118.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 118 -name: "Quiet Meditation Spire" -description: "A narrow tower lined with sound-dampening panels. Cushions arranged in concentric circles face a central fountain that flows upward into a recycling grate. No words are spoken here." -map_symbol: "M" -exits: - west: 117 - east: 119 - north: 128 diff --git a/data/rooms/119.yaml b/data/rooms/119.yaml deleted file mode 100644 index dbe9402..0000000 --- a/data/rooms/119.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 119 -name: "Sleep Pod Cluster 7" -description: "Hundreds of stacked sleep pods line the corridor, their occupants visible through frosted transparencies. A few are open, revealing neatly made bunks with personal hologram projections." -map_symbol: "p" -exits: - west: 118 - south: 109 - north: 129 diff --git a/data/rooms/12.yaml b/data/rooms/12.yaml index d8850eb..0c4443b 100644 --- a/data/rooms/12.yaml +++ b/data/rooms/12.yaml @@ -4,6 +4,8 @@ description: "A blazing forge and anvil dominate this smoky workshop. The heat f exits: east: 13 west: 11 +mobs: + - "weapons_smith" objects: - id: furnace - id: anvil diff --git a/data/rooms/120.yaml b/data/rooms/120.yaml deleted file mode 100644 index a03d2f2..0000000 --- a/data/rooms/120.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 120 -name: "The Neon Drake Tavern" -description: "Neon dragons coil across the ceiling in animated loops. The bar serves synthesized spirits that pack a surprising punch. A jukebox plays synthwave classics from the pre-Collapse era." -map_symbol: "T" -exits: - south: 110 - east: 121 - north: 130 diff --git a/data/rooms/121.yaml b/data/rooms/121.yaml deleted file mode 100644 index 8adf8c3..0000000 --- a/data/rooms/121.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 121 -name: "Dance Floor Theta-9" -description: "The floor pulses with reactive LED tiles that shift color with every step. A spherical DJ booth hovers in the center, surrounded by a crowd of moving silhouettes lost in the beat." -map_symbol: "D" -exits: - west: 120 - east: 122 - south: 111 - north: 131 diff --git a/data/rooms/122.yaml b/data/rooms/122.yaml deleted file mode 100644 index d4ec067..0000000 --- a/data/rooms/122.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 122 -name: "VR Combat Arena" -description: "Rows of neural-interface pods line the walls. Spectators watch live feeds of digital battles where players fight with light and code in simulated zero-G deathmatches." -map_symbol: "V" -exits: - west: 121 - south: 112 diff --git a/data/rooms/126.yaml b/data/rooms/126.yaml deleted file mode 100644 index a76b7d1..0000000 --- a/data/rooms/126.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 126 -name: "Holographic Tattoo Parlor" -description: "Clients float in zero-g chairs while artists etch animated holograms onto their skin. A woman displays a sleeve of swimming koi that ripple and dart as she moves her arm." -map_symbol: "t" -exits: - south: 116 - east: 127 - north: 136 diff --git a/data/rooms/127.yaml b/data/rooms/127.yaml deleted file mode 100644 index f7565c8..0000000 --- a/data/rooms/127.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 127 -name: "Underground Fight Club" -description: "In a repurposed cargo container, bare-knuckle brawls draw crowds of gamblers. The ring is marked by glow tape. A hulking enforcer named Jax watches the crowd with narrowed eyes." -map_symbol: "F" -exits: - west: 126 - east: 128 - south: 117 - north: 137 diff --git a/data/rooms/128.yaml b/data/rooms/128.yaml deleted file mode 100644 index 93abe6c..0000000 --- a/data/rooms/128.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 128 -name: "Ration Exchange Trade Post" -description: "Bulletin boards display trading offers for ration cards, spare parts, and favors. A grumpy quartermaster oversees the exchange from behind a counter of reinforced polyglass." -map_symbol: "X" -exits: - west: 127 - east: 129 - south: 118 - north: 138 diff --git a/data/rooms/129.yaml b/data/rooms/129.yaml deleted file mode 100644 index 35a5087..0000000 --- a/data/rooms/129.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 129 -name: "Viewing Platform Nebula Vista" -description: "A panoramic window overlooks the Crab Nebula in all its colorful glory. Telescopes mounted on tracks allow close-up viewing of distant stars and planets. Benches face the void." -map_symbol: "V" -exits: - west: 128 - south: 119 diff --git a/data/rooms/130.yaml b/data/rooms/130.yaml deleted file mode 100644 index 35c0b35..0000000 --- a/data/rooms/130.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 130 -name: "Reactor Core Access" -description: "Warning lights pulse a deep amber. The massive blast door is reinforced with thermal shielding. A placard reads 'Authorized Personnel Only — Class 3 Radiation Zone.'" -map_symbol: "R" -exits: - south: 120 - east: 131 - north: 140 diff --git a/data/rooms/131.yaml b/data/rooms/131.yaml deleted file mode 100644 index 2825c15..0000000 --- a/data/rooms/131.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 131 -name: "Engineering Bay 4" -description: "Tools hang on magnetic strips along every wall. Schematic blueprints float in holographic displays. Engineers in grease-stained jumpsuits argumentatively debate power allocation." -map_symbol: "E" -exits: - west: 130 - south: 121 - north: 141 diff --git a/data/rooms/136.yaml b/data/rooms/136.yaml deleted file mode 100644 index a5a87ae..0000000 --- a/data/rooms/136.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 136 -name: "Research Lab Xenobotany" -description: "Terrariums contain alien flora from a dozen star systems. A pulsating purple fungus emits a low-frequency hum. The lead researcher takes meticulous notes on bioluminescent patterns." -map_symbol: "L" -exits: - south: 126 - east: 137 - north: 146 diff --git a/data/rooms/137.yaml b/data/rooms/137.yaml deleted file mode 100644 index a14651b..0000000 --- a/data/rooms/137.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 137 -name: "Zero-G Forge" -description: "Molten metal floats in perfect spheres as magnetic fields shape it into precision components. A welder in a full environmental suit guides the process with practiced hand gestures." -map_symbol: "F" -exits: - west: 136 - east: 138 - south: 127 - north: 147 diff --git a/data/rooms/138.yaml b/data/rooms/138.yaml deleted file mode 100644 index c00d30d..0000000 --- a/data/rooms/138.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 138 -name: "Tool Locker Fabrication" -description: "Walls covered with every tool imaginable, from micro-screwdrivers to plasma cutters. A 3D fabricator in the corner hums quietly, ready to print custom tool heads on demand." -map_symbol: "T" -exits: - west: 137 - east: 139 - south: 128 - north: 148 diff --git a/data/rooms/139.yaml b/data/rooms/139.yaml deleted file mode 100644 index a8226fa..0000000 --- a/data/rooms/139.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: 139 -name: "Maintenance Tunnel 9" -description: "A narrow, cramped tunnel lined with exposed conduit and crawling with maintenance drones. The flickering fluorescent light makes the shadows dance along the curved walls." -map_symbol: "M" -exits: - west: 138 diff --git a/data/rooms/140.yaml b/data/rooms/140.yaml deleted file mode 100644 index c4d7db3..0000000 --- a/data/rooms/140.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 140 -name: "Captain's Quarter Balcony" -description: "A private balcony overlooking the grand concourse below. Potted orchids thrive in the artificial light. The captain's telescope is aimed at a distant binary star system." -map_symbol: "K" -exits: - south: 130 - east: 141 diff --git a/data/rooms/141.yaml b/data/rooms/141.yaml deleted file mode 100644 index d1ba9bf..0000000 --- a/data/rooms/141.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 141 -name: "Executive Lounge Nebula" -description: "Chrome and glass furniture floats in arranged clusters. A fully stocked bar offers genuine Earth spirits at exorbitant prices. The view of the nebula through the window is breathtaking." -map_symbol: "E" -exits: - west: 140 - east: 142 - south: 131 diff --git a/data/rooms/142.yaml b/data/rooms/142.yaml deleted file mode 100644 index e903d24..0000000 --- a/data/rooms/142.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 142 -name: "Board Room Round Table" -description: "A circular table dominates the room, its surface a holographic display of the sector map. Chairs hover at precise intervals. Decisions that affect thousands are made around this table." -map_symbol: "B" -exits: - west: 141 - east: 143 diff --git a/data/rooms/143.yaml b/data/rooms/143.yaml deleted file mode 100644 index 7e16905..0000000 --- a/data/rooms/143.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: 143 -name: "Observation Dome" -description: "The entire ceiling is a transparent dome offering an unobstructed view of deep space. Stars wheel slowly overhead as the station rotates. A gentle chime marks each passing hour." -map_symbol: "O" -exits: - west: 142 diff --git a/data/rooms/145.yaml b/data/rooms/145.yaml deleted file mode 100644 index fc8948e..0000000 --- a/data/rooms/145.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: 145 -name: "AI Core Chamber" -description: "A massive crystalline structure pulses with internal light. The station's artificial intelligence, AURA, processes billions of operations per second. A respectful silence is observed here." -map_symbol: "A" -exits: - east: 146 diff --git a/data/rooms/146.yaml b/data/rooms/146.yaml deleted file mode 100644 index b3512b4..0000000 --- a/data/rooms/146.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 146 -name: "Master Timekeeper's Office" -description: "Clocks of every conceivable design cover the walls — analog, digital, atomic, celestial. The Master Timekeeper ensures all station systems are synchronized to the galactic standard." -map_symbol: "T" -exits: - west: 145 - east: 147 - south: 136 diff --git a/data/rooms/147.yaml b/data/rooms/147.yaml deleted file mode 100644 index 4c89429..0000000 --- a/data/rooms/147.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 147 -name: "Archives of Station Law" -description: "A solemn chamber where the station's founding charter is displayed in a sealed crys-glass case. Legal precedents and arbitration records line the walls in data-slate form." -map_symbol: "A" -exits: - west: 146 - east: 148 - south: 137 diff --git a/data/rooms/148.yaml b/data/rooms/148.yaml deleted file mode 100644 index 603536d..0000000 --- a/data/rooms/148.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 148 -name: "Comm Spire Antenna Array" -description: "A vertical shaft rises into a transparent dome bristling with antennae and dishes. Signal boosters amplify transmissions across the sector. The air crackles with silent communication." -map_symbol: "C" -exits: - west: 147 - east: 149 - south: 138 diff --git a/data/rooms/149.yaml b/data/rooms/149.yaml deleted file mode 100644 index a5bb1d9..0000000 --- a/data/rooms/149.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 149 -name: "Escape Pod Bay 0" -description: "A circular chamber lined with escape pods, their hatches open for inspection. A sign reminds all personnel to familiarize themselves with emergency procedures before departure." -map_symbol: "E" -exits: - west: 148 -mobs: - - id: iron_sentinel diff --git a/data/rooms/150.yaml b/data/rooms/150.yaml deleted file mode 100644 index ba13c81..0000000 --- a/data/rooms/150.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 150 -name: "Solar Altar Chamber" -description: "A circular chamber bathed in warm {220}golden light{/}. At its center stands a humming {220 bold}solar altar{/}, its surface etched with fractal circuit patterns that glow softly." -exits: - south: 9 - east: 156 -objects: - - id: solar_altar diff --git a/data/rooms/151.yaml b/data/rooms/151.yaml deleted file mode 100644 index 8d79273..0000000 --- a/data/rooms/151.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 151 -name: "Hydro Altar Chamber" -description: "Condensation drips from the ceiling of this cool, blue-lit chamber. A {39 bold}hydro altar{/} dominates the room, its crystalline surface rippling with patterns like flowing water." -exits: - north: 9 - east: 157 -objects: - - id: hydro_altar diff --git a/data/rooms/152.yaml b/data/rooms/152.yaml deleted file mode 100644 index 26523a4..0000000 --- a/data/rooms/152.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 152 -name: "Eco Altar Chamber" -description: "Bioluminescent moss covers the walls of this overgrown chamber. An {34 bold}eco altar{/} rises from the floor, entwined with living circuitry that pulses with green light." -exits: - southwest: 9 - north: 158 -objects: - - id: eco_altar diff --git a/data/rooms/153.yaml b/data/rooms/153.yaml deleted file mode 100644 index 36ef05c..0000000 --- a/data/rooms/153.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 153 -name: "Bio Altar Chamber" -description: "The air here is thick and warm. A {196 bold}bio altar{/} throbs at the center of the room, its surface covered in red organic membranes threaded with copper wire." -exits: - northwest: 9 - north: 159 -objects: - - id: bio_altar diff --git a/data/rooms/154.yaml b/data/rooms/154.yaml deleted file mode 100644 index b86be53..0000000 --- a/data/rooms/154.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 154 -name: "Chaos Altar Chamber" -description: "The air crackles with {226 bold}unstable energy{/} in this chamber. A {226 bold}chaos altar{/} of twisting metal and sparking wires stands at the center, humming with discordant frequencies." -exits: - southeast: 9 -objects: - - id: chaos_altar diff --git a/data/rooms/155.yaml b/data/rooms/155.yaml deleted file mode 100644 index c333836..0000000 --- a/data/rooms/155.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 155 -name: "Death Altar Chamber" -description: "Shadows pool in the corners of this cold, silent chamber. A {52 bold}death altar{/} of polished obsidian dominates the room, absorbing all light that touches it." -exits: - northeast: 9 -objects: - - id: death_altar diff --git a/data/rooms/156.yaml b/data/rooms/156.yaml deleted file mode 100644 index d77f1e5..0000000 --- a/data/rooms/156.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 156 -name: "Blood Altar Chamber" -description: "The walls of this chamber are veined with dark crimson lines that pulse rhythmically. A {124 bold}blood altar{/} of deep red crystal stands at the center, radiating gentle warmth." -exits: - west: 150 -objects: - - id: blood_altar diff --git a/data/rooms/157.yaml b/data/rooms/157.yaml deleted file mode 100644 index 40f6b5b..0000000 --- a/data/rooms/157.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 157 -name: "Law Altar Chamber" -description: "Orderly lines of blue light trace geometric patterns across every surface of this pristine chamber. A {27 bold}law altar{/} of perfect blue stone rises from the floor, its surface engraved with precise mathematical formulas." -exits: - west: 151 -objects: - - id: law_altar diff --git a/data/rooms/158.yaml b/data/rooms/158.yaml deleted file mode 100644 index ec0f760..0000000 --- a/data/rooms/158.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 158 -name: "Cosmic Altar Chamber" -description: "Darkness envelops this chamber save for thousands of {201}twinkling lights{/} that float like stars. A {201 bold}cosmic altar{/} of translucent pink crystal hovers above the floor, surrounded by orbiting motes of light." -exits: - south: 152 -objects: - - id: cosmic_altar diff --git a/data/rooms/159.yaml b/data/rooms/159.yaml deleted file mode 100644 index ac65867..0000000 --- a/data/rooms/159.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 159 -name: "Nature Altar Chamber" -description: "Vines and flowering moss carpet every surface of this verdant chamber. A {46 bold}nature altar{/} of living green stone rises from the floor, entwined with blooming plants that turn toward you as you enter." -exits: - south: 153 -objects: - - id: nature_altar diff --git a/data/rooms/160.yaml b/data/rooms/160.yaml new file mode 100644 index 0000000..dea9d00 --- /dev/null +++ b/data/rooms/160.yaml @@ -0,0 +1,17 @@ +id: 160 +name: "Market Square" +description: "A noisy open-air market wedged between crumbling hab-blocks. Vendors hawk salvaged tech and reconstituted food from makeshift stalls. A {220 bold}Guard{/} watches over the area with a stern expression." +map_symbol: "M" +exits: + north: 1 + east: 161 +objects: + - id: market_stall +mobs: + - id: man + wander_interval: 15 + - id: man + wander_interval: 18 + - id: man + wander_interval: 20 + - id: guard
\ No newline at end of file diff --git a/data/rooms/161.yaml b/data/rooms/161.yaml new file mode 100644 index 0000000..c887347 --- /dev/null +++ b/data/rooms/161.yaml @@ -0,0 +1,13 @@ +id: 161 +name: "Farm Outpost" +description: "A cluster of hydroponic grow-pods on the asteroid's surface, shielded by a flickering atmospheric dome. Rows of bio-luminescent crops stretch into the distance." +map_symbol: "F" +exits: + west: 160 +mobs: + - id: farmer + wander_rooms: [161] + - id: farmer + wander_rooms: [161] + - id: bioengineer + wander_rooms: [161]
\ No newline at end of file diff --git a/data/rooms/162.yaml b/data/rooms/162.yaml new file mode 100644 index 0000000..a8333d8 --- /dev/null +++ b/data/rooms/162.yaml @@ -0,0 +1,6 @@ +id: 162 +name: "Detention Cell" +description: "A small, grimy holding cell. The walls are scratched with tally marks from previous occupants. A heavy door bars the only exit." +map_symbol: "J" +exits: + north: 1
\ No newline at end of file diff --git a/data/rooms/31.yaml b/data/rooms/31.yaml index fc57b2d..3d010df 100644 --- a/data/rooms/31.yaml +++ b/data/rooms/31.yaml @@ -4,3 +4,4 @@ description: "The central square of Lumbridge. A fountain gurgles peacefully in map_symbol: "b" exits: up: 1 + down: 50 diff --git a/data/rooms/50.yaml b/data/rooms/50.yaml new file mode 100644 index 0000000..0c9ebf2 --- /dev/null +++ b/data/rooms/50.yaml @@ -0,0 +1,7 @@ +id: 50 +name: "The Assassin's Den" +description: "A dimly lit basement accessible through a trapdoor. Tactical maps and bounty posters line the walls. The air smells of gun oil and burnt ozone. {141}The Client{/} sits behind a reinforced desk." +exits: + up: 31 +mobs: + - "client" diff --git a/data/rooms/51.yaml b/data/rooms/51.yaml new file mode 100644 index 0000000..74a97ef --- /dev/null +++ b/data/rooms/51.yaml @@ -0,0 +1,10 @@ +id: 51 +name: "Sewer Tunnels" +description: "Dark, damp tunnels beneath the settlement. The floor is slick with moisture and something {82}slimy{/}. The smell is indescribable." +exits: + north: 50 + south: 52 +mobs: + - "slug" + - "slug" + - "slug" diff --git a/data/rooms/52.yaml b/data/rooms/52.yaml new file mode 100644 index 0000000..2f90abd --- /dev/null +++ b/data/rooms/52.yaml @@ -0,0 +1,9 @@ +id: 52 +name: "Abandoned Sector" +description: "A decommissioned sector of the asteroid's infrastructure. Broken monitors flicker and exposed wiring {214}sparks{/} dangerously." +exits: + north: 51 + south: 53 +mobs: + - "drone" + - "drone" diff --git a/data/rooms/53.yaml b/data/rooms/53.yaml new file mode 100644 index 0000000..126719b --- /dev/null +++ b/data/rooms/53.yaml @@ -0,0 +1,9 @@ +id: 53 +name: "Deep Tunnels" +description: "The tunnels descend deeper into the asteroid's core. Strange chitinous scraping echoes from the darkness. The walls are scarred with {130}claw marks{/}." +exits: + north: 52 + south: 54 +mobs: + - "crawler" + - "crawler" diff --git a/data/rooms/54.yaml b/data/rooms/54.yaml new file mode 100644 index 0000000..5bb6a1d --- /dev/null +++ b/data/rooms/54.yaml @@ -0,0 +1,8 @@ +id: 54 +name: "The Void Chamber" +description: "A vast cavern where reality seems to thin. The air shimmers with {141}spectral energy{/} and strange whispers fill your mind." +exits: + north: 53 +mobs: + - "phantom" + - "phantom" diff --git a/internal/action/behavior.go b/internal/action/behavior.go index ccd95bf..236ab81 100644 --- a/internal/action/behavior.go +++ b/internal/action/behavior.go @@ -50,6 +50,22 @@ type NodeAction struct { Teleport int `yaml:"teleport"` Heal int `yaml:"heal"` Cost int `yaml:"cost"` + Shop *ShopConfig `yaml:"shop"` + AssignTask bool `yaml:"assign_task"` + SkipTask bool `yaml:"skip_task"` + ExtendTask bool `yaml:"extend_task"` + ReputationCost int `yaml:"reputation_cost"` +} + +type ShopConfig struct { + Message string `yaml:"message"` + Items []ShopItem `yaml:"items"` +} + +type ShopItem struct { + ItemID string `yaml:"item_id"` + BuyPrice int `yaml:"buy_price"` + SellPrice int `yaml:"sell_price"` } type Condition struct { diff --git a/internal/combat/state.go b/internal/combat/state.go index 9c8d346..7a4f24c 100644 --- a/internal/combat/state.go +++ b/internal/combat/state.go @@ -3,9 +3,10 @@ package combat import "sync" type State struct { - PlayerName string - MobID string - Active bool + PlayerName string + MobID string + Active bool + DamageWarningShown bool } var ( diff --git a/internal/config/config.go b/internal/config/config.go index 932af4a..ad90103 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -51,6 +51,8 @@ func DefaultColors() ColorsConfig { "battery": "45", "tech_depleted": "196", "science_mod": "99", + "assassin_task": "219", + "warning": "208", } } diff --git a/internal/game/action.go b/internal/game/action.go index 2d30010..3cc51c6 100644 --- a/internal/game/action.go +++ b/internal/game/action.go @@ -25,6 +25,8 @@ var verbAliases = map[string]string{ "ask": "talk", "pull": "toggle", "push": "toggle", + "steal": "steal", + "thieve": "steal", } var verbSkill = map[string]string{ @@ -33,6 +35,8 @@ var verbSkill = map[string]string{ "cut": "woodcutting", "fish": "fishing", "shear": "crafting", + "steal": "thieving", + "thieve": "thieving", } func normalizeVerb(v string) string { @@ -235,6 +239,8 @@ func (g *Game) AdvanceActions() { g.advanceSearch(sess, p) case "identify": g.advanceIdentify(sess, p) + case "steal": + g.advanceSteal(sess, p) default: if productionActionTypes[p.Action.Type] { g.advanceProduction(sess, p) diff --git a/internal/game/action_state.go b/internal/game/action_state.go index 596eb4e..2dc42ae 100644 --- a/internal/game/action_state.go +++ b/internal/game/action_state.go @@ -26,6 +26,7 @@ const ( ActionMixing ActionType = "mixing" ActionIdentifying ActionType = "identifying" ActionTriggering ActionType = "triggering" + ActionStealing ActionType = "stealing" ) type ActionState struct { @@ -85,6 +86,8 @@ func (a *ActionState) Description() string { return "identifying scrap at " + a.TargetName case ActionTriggering: return "triggering " + a.TargetName + case ActionStealing: + return "stealing from " + a.TargetName } return "" } diff --git a/internal/game/action_steal.go b/internal/game/action_steal.go new file mode 100644 index 0000000..dac90f2 --- /dev/null +++ b/internal/game/action_steal.go @@ -0,0 +1,449 @@ +package game + +import ( + "fmt" + "math/rand" + "sort" + "strconv" + "strings" + + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +var stealSuccess = action.SuccessFormula{ + Base: 0.5, + PerLevel: 0.03, + Cap: 0.95, +} + +func (g *Game) doSteal(sess *net.Session, input string) { + p := sess.Player.(*player.Player) + + if combat.GetCombat(p.Name) != nil { + sess.WriteLine("You can't do that during combat!") + return + } + + g.CancelAction(p) + + targetType, mob, objDef, errMsg := g.resolveStealTarget(sess, p, input) + if errMsg != "" { + sess.WriteLine(errMsg) + return + } + + g.startSteal(sess, p, targetType, mob, objDef) +} + +func (g *Game) resolveStealTarget(sess *net.Session, p *player.Player, input string) (targetType string, mob *world.MobInstance, objDef *object.ObjectDef, errMsg string) { + input = strings.ToLower(strings.TrimSpace(input)) + instanceIdx := -1 + searchName := input + + if dotPos := strings.Index(input, "."); dotPos > 0 { + if n, err := strconv.Atoi(input[:dotPos]); err == nil && n > 0 { + instanceIdx = n - 1 + searchName = input[dotPos+1:] + } + } + + roomMobs := g.MobStore.MobsInRoom(p.RoomID) + var stealMobs []*world.MobInstance + for _, m := range roomMobs { + if m.StealTable != "" { + stealMobs = append(stealMobs, m) + } + } + + roomObjs := g.World.AllObjInstances(p.RoomID) + var stealObjs []*object.ObjectDef + for _, st := range roomObjs { + def, err := g.ObjectStore.Load(st.DefID) + if err != nil || def.StealTable == "" { + continue + } + stealObjs = append(stealObjs, def) + } + + if input == "" { + if len(stealMobs) == 0 && len(stealObjs) == 0 { + return "", nil, nil, "There's nothing here to steal from." + } + + uniqueMobDefs := make(map[string]bool) + for _, m := range stealMobs { + uniqueMobDefs[m.DefID] = true + } + + if len(stealMobs) > 0 && len(stealObjs) == 0 { + if len(uniqueMobDefs) <= 1 { + return "mob", stealMobs[0], nil, "" + } + } + if len(stealObjs) > 0 && len(stealMobs) == 0 { + uniqueObjDefs := make(map[string]bool) + for _, o := range stealObjs { + uniqueObjDefs[o.ID] = true + } + if len(uniqueObjDefs) <= 1 { + return "object", nil, stealObjs[0], "" + } + } + return "", nil, nil, "Steal from what?" + } + + var matchingMobs []*world.MobInstance + for _, m := range stealMobs { + if m.MatchQuality(searchName) >= world.MatchPrefix { + matchingMobs = append(matchingMobs, m) + } + } + sort.Slice(matchingMobs, func(i, j int) bool { + return matchingMobs[i].InstanceID < matchingMobs[j].InstanceID + }) + + if len(matchingMobs) > 0 { + uniqueDefs := make(map[string]bool) + for _, m := range matchingMobs { + uniqueDefs[m.DefID] = true + } + if len(uniqueDefs) > 1 && instanceIdx < 0 { + return "", nil, nil, "Which one?" + } + if instanceIdx >= 0 && instanceIdx < len(matchingMobs) { + return "mob", matchingMobs[instanceIdx], nil, "" + } + if len(matchingMobs) == 1 { + return "mob", matchingMobs[0], nil, "" + } + for i := range matchingMobs { + if instanceIdx == i || instanceIdx < 0 { + return "mob", matchingMobs[i], nil, "" + } + } + } + + var matchingObjs []*object.ObjectDef + for _, o := range stealObjs { + if world.WordPrefixMatch(searchName, o.Name) { + matchingObjs = append(matchingObjs, o) + } + } + if len(matchingObjs) > 1 { + return "", nil, nil, "Which one?" + } + if len(matchingObjs) == 1 { + return "object", nil, matchingObjs[0], "" + } + + return "", nil, nil, "There's nothing here to steal from." +} + +func (g *Game) startSteal(sess *net.Session, p *player.Player, targetType string, mob *world.MobInstance, objDef *object.ObjectDef) { + var stealTable string + var stealLevel int + var stealXP int + var stealSpeed float64 + var targetName string + var targetID string + var mobInstanceID string + var objDefID string + var guardMob string + guardWatching := false + + if targetType == "mob" { + stealTable = mob.StealTable + stealLevel = mob.StealLevel + stealXP = mob.StealXP + stealSpeed = mob.StealSpeed + targetName = mob.Name + targetID = mob.InstanceID + mobInstanceID = mob.InstanceID + + if stealTable == "" { + sess.WriteLine(fmt.Sprintf("You can't steal from the %s.", targetName)) + return + } + if thievingLevel := p.Level(player.Thieving); stealLevel > 0 && thievingLevel < stealLevel { + sess.WriteLine(fmt.Sprintf("You need level %d thieving to steal from the %s.", stealLevel, targetName)) + return + } + if mob.HP <= 0 { + sess.WriteLine("That is already dead.") + return + } + if combat.IsMobInCombat(mob.InstanceID) { + sess.WriteLine(fmt.Sprintf("The %s is busy.", targetName)) + return + } + } else { + stealTable = objDef.StealTable + stealLevel = objDef.StealLevel + stealXP = objDef.StealXP + stealSpeed = objDef.StealSpeed + targetName = objDef.Name + targetID = objDef.ID + objDefID = objDef.ID + guardMob = objDef.GuardMob + + if stealTable == "" { + sess.WriteLine(fmt.Sprintf("You can't steal from the %s.", targetName)) + return + } + if thievingLevel := p.Level(player.Thieving); stealLevel > 0 && thievingLevel < stealLevel { + sess.WriteLine(fmt.Sprintf("You need level %d thieving to steal from the %s.", stealLevel, targetName)) + return + } + + if guardMob != "" { + guard := g.findGuardInRoom(p.RoomID, guardMob) + if guard != nil { + timerKey := fmt.Sprintf("%d:%s", p.RoomID, objDefID) + if _, exists := g.guardWatchTimers[timerKey]; !exists { + g.guardWatchTimers[timerKey] = 0 + } + guardWatching = g.isGuardWatching(p.RoomID, objDefID) + } + } + } + + if p.FirstFreeSlot() == -1 { + sess.WriteLine("Your inventory is too full!") + return + } + + sess.WriteLine(fmt.Sprintf("You attempt to steal from the %s...", targetName)) + p.ActionState = &ActionState{Type: ActionStealing, TargetName: targetName} + + p.Action = &action.Action{ + Type: "steal", + TargetID: targetID, + TargetName: targetName, + WaitLeft: engine.ToTicks(stealSpeed), + Data: map[string]any{ + "target_type": targetType, + "steal_table": stealTable, + "steal_level": stealLevel, + "steal_xp": stealXP, + "steal_speed": stealSpeed, + "target_name": targetName, + "mob_instance_id": mobInstanceID, + "obj_def_id": objDefID, + "guard_mob": guardMob, + "guard_watching": guardWatching, + }, + } +} + +func (g *Game) advanceSteal(sess *net.Session, p *player.Player) { + data := p.Action.Data + targetType := data["target_type"].(string) + stealTable := data["steal_table"].(string) + stealLevel := data["steal_level"].(int) + stealXP := data["steal_xp"].(int) + stealSpeed := data["steal_speed"].(float64) + targetName := data["target_name"].(string) + guardMob := data["guard_mob"].(string) + guardWatching := data["guard_watching"].(bool) + + if targetType == "mob" { + mobInstanceID := data["mob_instance_id"].(string) + mob := g.MobStore.GetInstance(mobInstanceID) + if mob == nil || mob.HP <= 0 || mob.RoomID != p.RoomID { + sess.WriteLine("Your target is gone.") + g.CancelAction(p) + return + } + if combat.IsMobInCombat(mob.InstanceID) { + sess.WriteLine(fmt.Sprintf("The %s is busy.", targetName)) + g.CancelAction(p) + return + } + data["steal_level"] = mob.StealLevel + data["steal_xp"] = mob.StealXP + data["steal_speed"] = mob.StealSpeed + stealLevel = mob.StealLevel + stealXP = mob.StealXP + stealSpeed = mob.StealSpeed + } else { + objDefID := data["obj_def_id"].(string) + found := false + for _, st := range g.World.AllObjInstances(p.RoomID) { + if st.DefID == objDefID && !st.Depleted { + found = true + break + } + } + if !found { + sess.WriteLine("Your target is gone.") + g.CancelAction(p) + return + } + objDef, err := g.ObjectStore.Load(objDefID) + if err == nil { + data["steal_level"] = objDef.StealLevel + data["steal_xp"] = objDef.StealXP + data["steal_speed"] = objDef.StealSpeed + stealLevel = objDef.StealLevel + stealXP = objDef.StealXP + stealSpeed = objDef.StealSpeed + data["guard_mob"] = objDef.GuardMob + guardMob = objDef.GuardMob + + if guardMob != "" { + guard := g.findGuardInRoom(p.RoomID, guardMob) + if guard != nil { + timerKey := fmt.Sprintf("%d:%s", p.RoomID, objDefID) + if _, exists := g.guardWatchTimers[timerKey]; !exists { + g.guardWatchTimers[timerKey] = 0 + } + guardWatching = g.isGuardWatching(p.RoomID, objDefID) + data["guard_watching"] = guardWatching + } + } + } + } + + level := p.Level(player.Thieving) + chance := action.SuccessChance(stealSuccess, level, stealLevel) + if guardWatching { + chance *= 0.5 + if chance < 0.05 { + chance = 0.05 + } + } + + if rand.Float64() < chance { + dt, err := g.BehaviorStore.LoadDropTable(stealTable) + if err != nil || len(dt.Drops) == 0 { + sess.WriteLine("You steal nothing of value.") + } else { + drop := g.BehaviorStore.ResolveDrop(dt.Drops) + if drop == nil || drop.ItemID == "" { + sess.WriteLine("You steal nothing of value.") + } else { + g.giveStealLoot(sess, p, drop) + } + } + + if stealXP > 0 { + prevLevel := p.Level(player.Thieving) + p.AddSkillXP(player.Thieving, stealXP) + newLevel := p.Level(player.Thieving) + if newLevel > prevLevel { + sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d thieving! ***", newLevel))) + } + } + + g.AccountStore.SaveCharacter(p) + + if p.FirstFreeSlot() == -1 { + sess.WriteLine("Your inventory is full.") + g.CancelAction(p) + return + } + + p.Action.WaitLeft = engine.ToTicks(stealSpeed) + } else { + if targetType == "mob" { + mobInstanceID := data["mob_instance_id"].(string) + mob := g.MobStore.GetInstance(mobInstanceID) + sess.WriteLine(fmt.Sprintf("The %s notices you! They attack!", targetName)) + g.CancelAction(p) + if mob != nil && mob.HP > 0 { + g.startCombat(sess, p, mob) + } + } else { + if guardMob != "" && guardWatching { + guard := g.findGuardInRoom(p.RoomID, guardMob) + if guard != nil { + sess.WriteLine("You fumble and the Guard spots you!") + g.CancelAction(p) + g.startStealGuardTalk(sess, p, guard) + return + } + } + sess.WriteLine("You fail to steal anything.") + p.Action.WaitLeft = engine.ToTicks(stealSpeed) + } + } +} + +func (g *Game) giveStealLoot(sess *net.Session, p *player.Player, drop *action.DropEntry) { + qty := drop.Quantity + if qty <= 0 { + qty = 1 + } + + if drop.ItemID == "credits" { + p.Credits += qty + sess.WriteLine(g.colorize(sess, "credits_pickup", fmt.Sprintf("You steal %d credits.", qty))) + return + } + + name := drop.ItemID + lootDef, _ := g.ItemStore.Load(drop.ItemID) + if lootDef != nil { + name = lootDef.Name + } + + freeSlot := p.FirstFreeSlot() + if freeSlot == -1 { + g.World.AddGroundItem(p.RoomID, drop.ItemID, qty) + sess.WriteLine(fmt.Sprintf("You steal %s but your inventory is full. It falls to the ground.", g.itemColorize(sess, lootDef, name))) + return + } + + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty}) + sess.WriteLine(fmt.Sprintf("You steal %s.", g.itemColorize(sess, lootDef, name))) +} + +func (g *Game) findGuardInRoom(roomID int, guardDefID string) *world.MobInstance { + mobs := g.MobStore.MobsInRoom(roomID) + for _, m := range mobs { + if m.DefID == guardDefID && m.HP > 0 { + return m + } + } + return nil +} + +const guardWatchDuration = 8 +const guardLookAwayDuration = 4 +const guardCycleLength = guardWatchDuration + guardLookAwayDuration + +func (g *Game) isGuardWatching(roomID int, objDefID string) bool { + key := fmt.Sprintf("%d:%s", roomID, objDefID) + counter := g.guardWatchTimers[key] + return counter%guardCycleLength < guardWatchDuration +} + +func (g *Game) startStealGuardTalk(sess *net.Session, p *player.Player, guardMob *world.MobInstance) { + cfg, err := g.BehaviorStore.LoadTalk("stall_guard_talk") + if err != nil { + sess.WriteLine("The Guard glares at you but says nothing.") + return + } + + node, ok := cfg.Nodes["start"] + if !ok { + return + } + + p.ActionState = &ActionState{Type: ActionTalking, TargetName: guardMob.Name} + + p.Action = &action.Action{ + Type: "talk", + TargetID: guardMob.DefID, + TargetName: guardMob.Name, + Data: map[string]any{"node": "start", "behavior_id": "stall_guard_talk", "steal_guard": true}, + } + + g.showTalkNode(sess, node) +} diff --git a/internal/game/action_talk.go b/internal/game/action_talk.go index 2d4a814..1a21e7f 100644 --- a/internal/game/action_talk.go +++ b/internal/game/action_talk.go @@ -57,7 +57,27 @@ func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) { sess.WriteLine(fmt.Sprintf("\n%s", g.colorize(sess, "dialog", node.Message))) if node.Action != nil { + if node.Action.Shop != nil { + g.applyNodeAction(sess, node.Action) + g.enterShop(sess, node.Action.Shop) + return + } g.applyNodeAction(sess, node.Action) + + if g.WorldFlags["guard_hostile"] != nil { + delete(g.WorldFlags, "guard_hostile") + p, _ := sess.Player.(*player.Player) + if p != nil && p.Action != nil && p.Action.Data["steal_guard"] == true { + guardMob := g.findGuardInRoom(p.RoomID, "guard") + if guardMob != nil { + sess.State = net.StateGame + g.CancelAction(p) + guardMob.Protected = false + g.startCombat(sess, p, guardMob) + } + return + } + } } if len(node.Options) == 0 { @@ -162,11 +182,38 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) { g.writePrompt(sess) } +func (g *Game) enterShop(sess *net.Session, cfg *action.ShopConfig) { + sess.Shop = cfg + sess.State = net.StateShop + g.showShopBrowse(sess) + g.writeShopPrompt(sess) +} + func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) { p, ok := sess.Player.(*player.Player) if !ok { return } + + if na.AssignTask { + g.assignAssassinTask(sess, p) + } + if na.SkipTask { + g.skipAssassinTask(sess, p) + } + if na.ExtendTask { + g.extendAssassinTask(sess, p) + } + if na.ReputationCost > 0 { + rep := getPlayerFlagInt(p, "assassin_reputation") + if rep < na.ReputationCost { + sess.WriteLine(fmt.Sprintf("You don't have enough Reputation. (Need %d, have %d)", na.ReputationCost, rep)) + return + } + setPlayerFlag(p, "assassin_reputation", rep-na.ReputationCost) + g.AccountStore.SaveCharacter(p) + } + for k, v := range na.SetFlags { g.WorldFlags[k] = v } diff --git a/internal/game/assassin.go b/internal/game/assassin.go new file mode 100644 index 0000000..d7af1a4 --- /dev/null +++ b/internal/game/assassin.go @@ -0,0 +1,284 @@ +package game + +import ( + "fmt" + "math/rand" + "strings" + + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +func getPlayerFlagInt(p *player.Player, key string) int { + if p.Flags == nil { + return 0 + } + val, ok := p.Flags[key] + if !ok { + return 0 + } + switch v := val.(type) { + case int: + return v + case int64: + return int(v) + case float64: + return int(v) + } + return 0 +} + +func getPlayerFlagString(p *player.Player, key string) string { + if p.Flags == nil { + return "" + } + val, ok := p.Flags[key] + if !ok { + return "" + } + s, _ := val.(string) + return s +} + +func setPlayerFlag(p *player.Player, key string, val any) { + if p.Flags == nil { + p.Flags = make(map[string]any) + } + p.Flags[key] = val +} + +type assassinTaskEntry struct { + MobID string + MinLevel int + MaxLevel int + MinCount int + MaxCount int + Weight int +} + +var assassinTaskTable = []assassinTaskEntry{ + {"man", 1, 15, 10, 25, 8}, + {"cow", 1, 15, 10, 25, 8}, + {"slug", 1, 99, 15, 45, 15}, + {"drone", 15, 99, 20, 50, 12}, + {"crawler", 30, 99, 15, 40, 10}, + {"phantom", 45, 99, 10, 30, 8}, +} + +func (g *Game) assignAssassinTask(sess *net.Session, p *player.Player) { + level := p.Level(player.Assassin) + var eligible []assassinTaskEntry + totalWeight := 0 + for _, entry := range assassinTaskTable { + if level >= entry.MinLevel && level <= entry.MaxLevel { + eligible = append(eligible, entry) + totalWeight += entry.Weight + } + } + if len(eligible) == 0 { + sess.WriteLine("The Client shakes their head. \"Nothing available for your level.\"") + return + } + roll := rand.Intn(totalWeight) + var chosen assassinTaskEntry + for _, entry := range eligible { + roll -= entry.Weight + if roll < 0 { + chosen = entry + break + } + } + count := chosen.MinCount + rand.Intn(chosen.MaxCount-chosen.MinCount+1) + setPlayerFlag(p, "assassin_task_mob", chosen.MobID) + setPlayerFlag(p, "assassin_task_total", count) + setPlayerFlag(p, "assassin_task_remaining", count) + g.AccountStore.SaveCharacter(p) + + def, err := g.MobStore.LoadDef(chosen.MobID) + name := chosen.MobID + if err == nil { + name = def.Name + } + sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf("\"Your target: %d %ss. Get to work.\"", count, name))) +} + +func (g *Game) onAssassinKill(sess *net.Session, p *player.Player, mob *world.MobInstance) { + taskMob := getPlayerFlagString(p, "assassin_task_mob") + if taskMob == "" || taskMob != mob.DefID { + return + } + remaining := getPlayerFlagInt(p, "assassin_task_remaining") + if remaining <= 0 { + return + } + xp := mob.MaxHP * 2 + newLevel := p.AddSkillXP(player.Assassin, xp) + if newLevel > 0 { + sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d Assassin! ***", newLevel))) + } + if p.OptionBool("xp_drops") { + sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp asm)", xp))) + } + remaining-- + setPlayerFlag(p, "assassin_task_remaining", remaining) + if remaining <= 0 { + completed := getPlayerFlagInt(p, "assassin_tasks_completed") + 1 + streak := getPlayerFlagInt(p, "assassin_streak") + 1 + setPlayerFlag(p, "assassin_tasks_completed", completed) + setPlayerFlag(p, "assassin_streak", streak) + delete(p.Flags, "assassin_task_mob") + setPlayerFlag(p, "assassin_task_remaining", 0) + setPlayerFlag(p, "assassin_task_total", 0) + rep := 1 + bonus := streakBonus(streak) + rep += bonus + currentRep := getPlayerFlagInt(p, "assassin_reputation") + setPlayerFlag(p, "assassin_reputation", currentRep+rep) + sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf("\n*** Assassin task complete! ***"))) + if bonus > 0 { + sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf(" Streak bonus! %d tasks in a row. +%d bonus reputation.", streak, bonus))) + } + sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf(" Reputation earned: %d (total: %d)", rep, currentRep+rep))) + } else { + total := getPlayerFlagInt(p, "assassin_task_total") + def, _ := g.MobStore.LoadDef(taskMob) + name := taskMob + if def != nil { + name = def.Name + } + sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf(" Assassin task: %d of %d %ss remaining.", remaining, total, name))) + } + g.AccountStore.SaveCharacter(p) +} + +func (g *Game) skipAssassinTask(sess *net.Session, p *player.Player) { + rep := getPlayerFlagInt(p, "assassin_reputation") + if rep < 30 { + sess.WriteLine("You don't have enough Reputation to skip. (Need 30, have " + fmt.Sprint(rep) + ")") + return + } + setPlayerFlag(p, "assassin_reputation", rep-30) + delete(p.Flags, "assassin_task_mob") + setPlayerFlag(p, "assassin_task_remaining", 0) + setPlayerFlag(p, "assassin_task_total", 0) + setPlayerFlag(p, "assassin_streak", 0) + g.AccountStore.SaveCharacter(p) +} + +func (g *Game) extendAssassinTask(sess *net.Session, p *player.Player) { + rep := getPlayerFlagInt(p, "assassin_reputation") + if rep < 30 { + sess.WriteLine("You don't have enough Reputation to extend. (Need 30, have " + fmt.Sprint(rep) + ")") + return + } + taskMob := getPlayerFlagString(p, "assassin_task_mob") + if taskMob == "" { + sess.WriteLine("You don't have an active task to extend.") + return + } + setPlayerFlag(p, "assassin_reputation", rep-30) + total := getPlayerFlagInt(p, "assassin_task_total") + remaining := getPlayerFlagInt(p, "assassin_task_remaining") + extension := total / 2 + if extension < 5 { + extension = 5 + } + setPlayerFlag(p, "assassin_task_total", total+extension) + setPlayerFlag(p, "assassin_task_remaining", remaining+extension) + g.AccountStore.SaveCharacter(p) + + def, _ := g.MobStore.LoadDef(taskMob) + name := taskMob + if def != nil { + name = def.Name + } + sess.WriteLine(fmt.Sprintf("Task extended by %d. Kill %d more %ss (%d total).", extension, remaining+extension, name, total+extension)) +} + +func streakBonus(streak int) int { + if streak%1000 == 0 { + return 50 + } + if streak%250 == 0 { + return 35 + } + if streak%100 == 0 { + return 25 + } + if streak%50 == 0 { + return 15 + } + if streak%10 == 0 { + return 5 + } + return 0 +} + +func (g *Game) tryFinishingBlow(sess *net.Session, input string) bool { + p := sess.Player.(*player.Player) + cs := combat.GetCombat(p.Name) + if cs == nil { + return false + } + mob := g.MobStore.GetInstance(cs.MobID) + if mob == nil || mob.FinishingBlow == "" || mob.HP != 1 { + return false + } + lower := strings.ToLower(input) + var itemPart, targetPart string + for _, sep := range []string{" on ", " with "} { + if idx := strings.Index(lower, sep); idx > 0 { + itemPart = strings.TrimSpace(input[:idx]) + targetPart = strings.TrimSpace(input[idx+len(sep):]) + break + } + } + if targetPart == "" { + itemPart = strings.TrimSpace(input) + if mob.MatchQuality(itemPart) != world.MatchNone { + targetPart = itemPart + itemPart = strings.TrimSpace(mob.FinishingBlow) + } + } + if targetPart == "" { + return false + } + if mob.MatchQuality(targetPart) == world.MatchNone { + return false + } + g.doFinishingBlow(sess, p, mob, itemPart) + return true +} + +func (g *Game) doFinishingBlow(sess *net.Session, p *player.Player, mob *world.MobInstance, itemInput string) { + fbDef, err := g.ItemStore.Load(mob.FinishingBlow) + if err != nil { + sess.WriteLine("Something went wrong.") + return + } + if !fbDef.MatchesName(itemInput) && !strings.EqualFold(itemInput, fbDef.ID) { + sess.WriteLine(fmt.Sprintf("That won't work on %s. You need %s.", mobDisplayName(mob, true), fbDef.Name)) + return + } + if !p.HasItem(mob.FinishingBlow) { + sess.WriteLine(fmt.Sprintf("You don't have any %s.", fbDef.Name)) + return + } + autoKey := "assassin_unlocked_auto_" + mob.FinishingBlow + consumed := true + if p.Flags != nil { + if val, ok := p.Flags[autoKey]; ok { + if b, ok := val.(bool); ok && b { + consumed = false + } + } + } + if consumed { + p.RemoveItem(mob.FinishingBlow, 1) + } + sess.WriteLine(fmt.Sprintf("\nYou use the %s on %s!", g.itemColorize(sess, fbDef, fbDef.Name), g.colorize(sess, "mob_name", mobDisplayName(mob, true)))) + mob.HP = 0 + g.endCombat(sess, p, mob) +} diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go index 29bff30..a8dc5d3 100644 --- a/internal/game/cmd_attack.go +++ b/internal/game/cmd_attack.go @@ -41,6 +41,11 @@ func (g *Game) doAttack(sess *net.Session, input string) { return } + if mob.AssassinLevel > 0 && p.Level(player.Assassin) < mob.AssassinLevel { + sess.WriteLine(fmt.Sprintf("You need Assassin level %d to attack %s.", mob.AssassinLevel, mobDisplayName(mob, true))) + return + } + if combat.IsMobInCombat(mob.InstanceID) { sess.WriteLine(fmt.Sprintf("%s is already engaged in combat!", mobDisplayName(mob, false))) return @@ -268,9 +273,23 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI if mob.HP < 0 { mob.HP = 0 } + if mob.FinishingBlow != "" && mob.HP <= 0 { + mob.HP = 1 + } if mob.HP < mob.MaxHP && mob.HP > 0 { mob.StartRegen() } + + if mob.FinishingBlow != "" && mob.HP == 1 { + fbDef, _ := g.ItemStore.Load(mob.FinishingBlow) + fbName := mob.FinishingBlow + if fbDef != nil { + fbName = fbDef.Name + } + sess.WriteLine(g.colorize(sess, "warning", + fmt.Sprintf(" %s resists death! Use %s on it to finish it off.", + mobDisplayName(mob, false), fbName))) + } gains, leveled := g.awardCombatXP(p, dmg, isRanged) if isRanged { @@ -329,6 +348,7 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI } func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) { + cs := combat.GetCombat(p.Name) _, _, defStyleBonus := combat.AttackStyleBonus(string(p.AttackStyle)) mobAttackType := mob.AttackType @@ -349,6 +369,37 @@ func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInst dmg := combat.RollDamage(maxHit) dmg = g.applyTechProtection(p, mob, dmg) + if mob.DamageWithout != "" { + hasProtection := false + for _, itemID := range p.Equipment { + if itemID == mob.DamageWithout { + hasProtection = true + break + } + } + if !hasProtection { + dmg = dmg * 3 / 2 + if dmg < 1 { + dmg = 1 + } + if cs != nil && !cs.DamageWarningShown { + cs.DamageWarningShown = true + fbDef, _ := g.ItemStore.Load(mob.DamageWithout) + fbName := mob.DamageWithout + if fbDef != nil { + fbName = fbDef.Name + } + attacker := mob.Name + if !mob.Unique { + attacker = "The " + mob.Name + } + sess.WriteLine(g.colorize(sess, "warning", + fmt.Sprintf(" %s's attack is extra effective! Equip %s for protection.", + attacker, fbName))) + } + } + } + p.HP -= dmg if p.HP < 0 { p.HP = 0 @@ -421,6 +472,9 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst if mob != nil && mob.HP <= 0 { sess.WriteLine(g.colorize(sess, "victory", fmt.Sprintf("\nYou have defeated %s!", mobDisplayName(mob, true)))) + + g.onAssassinKill(sess, p, mob) + if g.Hub != nil { for _, other := range g.Hub.PlayersInRoom(p.RoomID) { if other != sess && other.Player != nil { diff --git a/internal/game/cmd_shop.go b/internal/game/cmd_shop.go new file mode 100644 index 0000000..a3f7613 --- /dev/null +++ b/internal/game/cmd_shop.go @@ -0,0 +1,295 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +func (g *Game) handleShopInput(sess *net.Session, input string) { + if sess.Player == nil { + sess.State = net.StateGame + g.writePrompt(sess) + return + } + + input = strings.TrimSpace(input) + parts := strings.Fields(strings.ToLower(input)) + if len(parts) == 0 { + g.showShopBrowse(sess) + return + } + + cmd := parts[0] + + switch cmd { + case "buy": + if len(parts) < 2 { + sess.WriteLine("Buy what? Use 'browse' to see available items.") + } else { + g.doShopBuy(sess, strings.Join(parts[1:], " ")) + } + case "sell": + if len(parts) < 2 { + sess.WriteLine("Sell what?") + } else { + g.doShopSell(sess, strings.Join(parts[1:], " ")) + } + case "browse", "list": + g.showShopBrowse(sess) + case "leave", "bye", "exit", "quit": + g.leaveShop(sess) + return + default: + sess.WriteLine("Commands: buy <item>, sell <item>, browse, leave") + } + g.writeShopPrompt(sess) +} + +func (g *Game) writeShopPrompt(sess *net.Session) { + sess.Write(fmt.Sprintf("\n%s", g.colorize(sess, "dialog", "> "))) +} + +func (g *Game) shopConfig(sess *net.Session) *action.ShopConfig { + cfg, _ := sess.Shop.(*action.ShopConfig) + return cfg +} + +func (g *Game) showShopBrowse(sess *net.Session) { + cfg := g.shopConfig(sess) + if cfg == nil { + return + } + + if cfg.Message != "" { + sess.WriteLine(fmt.Sprintf("\n%s", g.colorize(sess, "dialog", cfg.Message))) + } + + if len(cfg.Items) == 0 { + sess.WriteLine("This shop has nothing for sale.") + return + } + + unicode := true + if p, ok := sess.Player.(*player.Player); ok { + unicode = p.OptionBool("unicode") + } + + t := Table{ + Title: "Shop Inventory", + Columns: []string{"#", "Item", "Buy Price", "Sell Price"}, + } + + for i, si := range cfg.Items { + def, _ := g.ItemStore.Load(si.ItemID) + itemName := si.ItemID + if def != nil { + itemName = def.Name + } + buyStr := fmt.Sprintf("%d cr", si.BuyPrice) + sellStr := "\u2014" + if si.SellPrice > 0 { + sellStr = fmt.Sprintf("%d cr", si.SellPrice) + } + t.Rows = append(t.Rows, []string{ + fmt.Sprintf("%d", i+1), + g.colorize(sess, "item", itemName), + g.colorize(sess, "credits_pickup", buyStr), + g.colorize(sess, "credits_pickup", sellStr), + }) + } + + for _, line := range t.Render(unicode) { + sess.WriteLine(line) + } +} + +func (g *Game) doShopBuy(sess *net.Session, input string) { + cfg := g.shopConfig(sess) + if cfg == nil { + return + } + + idx, err := parseChoiceIndex(input) + if err == nil && idx > 0 && idx <= len(cfg.Items) { + si := cfg.Items[idx-1] + g.buyShopItem(sess, si) + return + } + + var match *action.ShopItem + for i := range cfg.Items { + si := &cfg.Items[i] + def, _ := g.ItemStore.Load(si.ItemID) + itemName := si.ItemID + if def != nil { + itemName = def.Name + } + + invMatches := g.collectMatches(input, func(yield func(string, int) bool) { + yield(itemName, -1) + }) + + if len(invMatches) > 0 { + if match != nil { + sess.WriteLine(fmt.Sprintf("That's ambiguous, which one? (use #, e.g. buy %s 1)", si.ItemID)) + return + } + match = si + } + } + + if match != nil { + g.buyShopItem(sess, *match) + return + } + + sess.WriteLine("That item isn't for sale here. Use 'browse' to see shop inventory.") +} + +func (g *Game) buyShopItem(sess *net.Session, si action.ShopItem) { + p, _ := sess.Player.(*player.Player) + + if p.Credits < si.BuyPrice { + sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", si.BuyPrice, p.Credits)) + return + } + + def, _ := g.ItemStore.Load(si.ItemID) + displayName := si.ItemID + if def != nil { + displayName = def.Name + } + + if def != nil && def.Stackable { + for i := 0; i < 28; i++ { + slot := p.InvSlot(i) + if slot != nil && slot.ItemID == si.ItemID { + p.Credits -= si.BuyPrice + slot.Quantity++ + g.AccountStore.SaveCharacter(p) + itemColor := g.itemColorize(sess, def, displayName) + sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice)))) + return + } + } + } + + freeSlot := p.FirstFreeSlot() + if freeSlot == -1 { + sess.WriteLine("Your inventory is full.") + return + } + + p.Credits -= si.BuyPrice + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: si.ItemID, Quantity: 1}) + g.AccountStore.SaveCharacter(p) + + itemColor := g.itemColorize(sess, def, displayName) + sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice)))) +} + +func (g *Game) doShopSell(sess *net.Session, input string) { + cfg := g.shopConfig(sess) + if cfg == nil { + return + } + + p, _ := sess.Player.(*player.Player) + + matches := g.findInventoryMatches(input, p) + if len(matches) == 0 { + sess.WriteLine("You don't have that item.") + return + } + + if len(matches) > 1 { + sess.WriteLine("That's ambiguous, which one?") + return + } + + match := matches[0] + + var shopItem *action.ShopItem + for i := range cfg.Items { + si := &cfg.Items[i] + if si.ItemID == match.ID && si.SellPrice > 0 { + shopItem = si + break + } + } + + if shopItem == nil { + def, _ := g.ItemStore.Load(match.ID) + displayName := match.ID + if def != nil { + displayName = def.Name + } + sess.WriteLine(fmt.Sprintf("The shop doesn't want to buy %s.", g.colorize(sess, "item", displayName))) + return + } + + p.RemoveItem(match.ID, 1) + p.Credits += shopItem.SellPrice + g.AccountStore.SaveCharacter(p) + + def, _ := g.ItemStore.Load(match.ID) + displayName := match.ID + if def != nil { + displayName = def.Name + } + itemColor := g.itemColorize(sess, def, displayName) + sess.WriteLine(fmt.Sprintf("You sell a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", shopItem.SellPrice)))) +} + +func (g *Game) leaveShop(sess *net.Session) { + sess.Shop = nil + p, ok := sess.Player.(*player.Player) + if !ok || p == nil || p.Action == nil || p.Action.Type != "talk" { + sess.State = net.StateGame + if p != nil { + g.CancelAction(p) + } + g.writePrompt(sess) + return + } + + behaviorID, _ := p.Action.Data["behavior_id"].(string) + nodeKey, _ := p.Action.Data["node"].(string) + + cfg, err := g.BehaviorStore.LoadTalk(behaviorID) + if err != nil { + sess.State = net.StateGame + g.CancelAction(p) + g.writePrompt(sess) + return + } + + node, ok := cfg.Nodes[nodeKey] + if !ok { + sess.State = net.StateGame + g.CancelAction(p) + g.writePrompt(sess) + return + } + + sess.State = net.StateTalk + visible := 0 + for _, opt := range node.Options { + if opt.Condition != nil && !g.checkCondition(sess, opt.Condition) { + continue + } + visible++ + sess.WriteLine(fmt.Sprintf(" %d. %s", visible, opt.Text)) + } + if visible == 0 { + sess.State = net.StateGame + g.CancelAction(p) + g.writePrompt(sess) + return + } + sess.Write("\nChoice: ") +} diff --git a/internal/game/cmd_sneak.go b/internal/game/cmd_sneak.go new file mode 100644 index 0000000..97aab8a --- /dev/null +++ b/internal/game/cmd_sneak.go @@ -0,0 +1,71 @@ +package game + +import ( + "fmt" + + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +func (g *Game) doSneak(sess *net.Session) { + p := sess.Player.(*player.Player) + p.Sneaking = !p.Sneaking + if p.Sneaking { + p.SneakNotified = make(map[string]bool) + sess.WriteLine("You begin sneaking.") + } else { + p.SneakNotified = nil + sess.WriteLine("You stop sneaking.") + } +} + +func (g *Game) SneakTick() { + if g.Hub == nil { + return + } + + for key := range g.guardWatchTimers { + g.guardWatchTimers[key]++ + } + + for _, sess := range g.Hub.AllSessions() { + p, ok := sess.Player.(*player.Player) + if !ok || p == nil || !p.Sneaking { + continue + } + + objs := g.World.AllObjInstances(p.RoomID) + for _, st := range objs { + objDef, err := g.ObjectStore.Load(st.DefID) + if err != nil || objDef.GuardMob == "" { + continue + } + + guard := g.findGuardInRoom(p.RoomID, objDef.GuardMob) + if guard == nil { + continue + } + + timerKey := fmt.Sprintf("%d:%s", p.RoomID, st.DefID) + if _, exists := g.guardWatchTimers[timerKey]; !exists { + g.guardWatchTimers[timerKey] = 0 + } + + watching := g.isGuardWatching(p.RoomID, st.DefID) + + if p.SneakNotified == nil { + p.SneakNotified = make(map[string]bool) + } + + lastState, known := p.SneakNotified[timerKey] + if !known || lastState != watching { + if watching { + sess.WriteLine(fmt.Sprintf("The %s is watching the %s.", guard.Name, objDef.Name)) + } else { + sess.WriteLine(fmt.Sprintf("The %s looks away from the %s.", guard.Name, objDef.Name)) + } + p.SneakNotified[timerKey] = watching + } + } + } +} diff --git a/internal/game/cmd_task.go b/internal/game/cmd_task.go new file mode 100644 index 0000000..2dcf3f4 --- /dev/null +++ b/internal/game/cmd_task.go @@ -0,0 +1,34 @@ +package game + +import ( + "fmt" + + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +func (g *Game) doTask(sess *net.Session) { + p := sess.Player.(*player.Player) + + mobID := getPlayerFlagString(p, "assassin_task_mob") + remaining := getPlayerFlagInt(p, "assassin_task_remaining") + total := getPlayerFlagInt(p, "assassin_task_total") + streak := getPlayerFlagInt(p, "assassin_streak") + rep := getPlayerFlagInt(p, "assassin_reputation") + + if mobID == "" || remaining <= 0 { + if getPlayerFlagInt(p, "assassin_tasks_completed") > 0 { + sess.WriteLine("You have no active task. Talk to The Client for a new assignment.") + } else { + sess.WriteLine("You don't have an Assassin task. Talk to The Client to get one.") + } + } else { + def, err := g.MobStore.LoadDef(mobID) + name := mobID + if err == nil { + name = def.Name + } + sess.WriteLine(fmt.Sprintf("Assassin task: Kill %ss. %d of %d remaining.", name, remaining, total)) + } + sess.WriteLine(fmt.Sprintf("Streak: %d | Reputation: %d", streak, rep)) +} diff --git a/internal/game/game.go b/internal/game/game.go index 9d8a2c9..8b99731 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -52,8 +52,9 @@ type Game struct { combatPadWidth int freeQueue map[string][]QueuedCommand activeQueue map[string]*QueuedCommand - consumeQueue map[string]*QueuedCommand + consumeQueue map[string]*QueuedCommand pendingDepletions []pendingDepletion + guardWatchTimers map[string]int } func New(dataDir string, colorConfig *config.ColorsConfig) *Game { @@ -75,6 +76,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig) *Game { activeQueue: make(map[string]*QueuedCommand), consumeQueue: make(map[string]*QueuedCommand), pendingDepletions: nil, + guardWatchTimers: make(map[string]int), } } @@ -121,6 +123,8 @@ func (g *Game) HandleSession(sess *net.Session, input string) { g.handleDescriptionChange(sess, input) case net.StateTalk: g.handleTalkInput(sess, input) + case net.StateShop: + g.handleShopInput(sess, input) case net.StateDropAllConfirm: g.handleDropAllConfirm(sess, input) case net.StateRecipeChoice: @@ -141,8 +145,9 @@ func classifyCommand(cmd string) CommandClass { "map", "option", "options", "alias", "unalias", "description", "desc", "queued", "color", "colors", "colortable", "prompt", "style", "stats", - "tech", "t", - "autocast", "auto", "mods", "modlist": + "tech", "t", "sneak", + "autocast", "auto", "mods", "modlist", + "task": return ClassInstant case "equip", "eq", "equipment", "wear", "wield", "remove", "unwear", "unwield": return ClassFree @@ -152,6 +157,7 @@ func classifyCommand(cmd string) CommandClass { "west", "w", "up", "u", "down", "d", "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft", "mix", "id", "identify", + "steal", "thieve", "trigger", "cast": return ClassActive case "eat", "fletch", "clean": @@ -319,6 +325,8 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI } case "sc", "score": g.doScore(sess) + case "task": + g.doTask(sess) case "stats": g.doStats(sess) case "tech", "t": @@ -374,6 +382,9 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI return } case "use": + if g.tryFinishingBlow(sess, strings.Join(args, " ")) { + return + } g.doUse(sess, strings.Join(args, " ")) return case "cook": @@ -407,6 +418,8 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI g.doAutocast(sess, strings.Join(args, " ")) case "mods", "modlist": g.doMods(sess) + case "sneak": + g.doSneak(sess) case "trigger", "cast": g.doTrigger(sess, strings.Join(args, " ")) return @@ -440,6 +453,14 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI g.doSearch(sess, strings.Join(args, " ")) } return + case "steal", "thieve": + g.CancelAction(p) + if len(args) == 0 { + g.doSteal(sess, "") + } else { + g.doSteal(sess, strings.Join(args, " ")) + } + return case "walk": g.doWalk(sess, args) return @@ -495,7 +516,8 @@ func (g *Game) ProcessQueuedCommands() { } switch as.Type { case ActionGathering, ActionCombating, ActionUsing, ActionTalking, - ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionProducing: + ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionProducing, + ActionStealing: default: if as.Type != ActionMoving || p.MoveTicks <= 0 { p.ActionState = nil diff --git a/internal/net/server.go b/internal/net/server.go index f6776fc..7c1b79a 100644 --- a/internal/net/server.go +++ b/internal/net/server.go @@ -38,6 +38,7 @@ const ( StateCraftProduct StateProductChoice StateColorChoice + StateShop ) type Session struct { @@ -54,6 +55,7 @@ type Session struct { PendingBackground bool Disconnecting bool DisconnectTicks int + Shop interface{} } type AccountEntry struct { diff --git a/internal/object/object.go b/internal/object/object.go index fe55865..8c4c752 100644 --- a/internal/object/object.go +++ b/internal/object/object.go @@ -19,4 +19,9 @@ type ObjectDef struct { RemovalItem string `yaml:"removal_item"` Description string `yaml:"description"` UseInteractions []UseInteraction `yaml:"use_interactions"` + StealTable string `yaml:"steal_table"` + StealLevel int `yaml:"steal_level"` + StealXP int `yaml:"steal_xp"` + StealSpeed float64 `yaml:"steal_speed"` + GuardMob string `yaml:"guard_mob"` } diff --git a/internal/player/player.go b/internal/player/player.go index dbbd4da..a3ca4ce 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -182,6 +182,8 @@ type Player struct { ActiveTechs map[string]bool `yaml:"-"` QuickTech string `yaml:"quick_tech,omitempty"` TechActivatedSinceTick map[string]bool `yaml:"-"` + Sneaking bool `yaml:"-"` + SneakNotified map[string]bool `yaml:"-"` } func (p *Player) ClearMoveState() { diff --git a/internal/world/mob.go b/internal/world/mob.go index daf73dc..ce9eddd 100644 --- a/internal/world/mob.go +++ b/internal/world/mob.go @@ -47,7 +47,15 @@ type MobDef struct { ScienceDefense int `yaml:"science_defense"` RangedDefense int `yaml:"ranged_defense"` - Weakness string `yaml:"weakness"` + Weakness string `yaml:"weakness"` + StealTable string `yaml:"steal_table"` + StealLevel int `yaml:"steal_level"` + StealXP int `yaml:"steal_xp"` + StealSpeed float64 `yaml:"steal_speed"` + + AssassinLevel int `yaml:"assassin_level"` + FinishingBlow string `yaml:"finishing_blow"` + DamageWithout string `yaml:"damage_without"` } type MobInstance struct { @@ -86,7 +94,15 @@ type MobInstance struct { ScienceDefense int RangedDefense int - Weakness string + Weakness string + StealTable string + StealLevel int + StealXP int + StealSpeed float64 + + AssassinLevel int + FinishingBlow string + DamageWithout string } const ( @@ -292,6 +308,13 @@ func (s *MobStore) SeedMobs(roomID int, entries []RoomMob) { ScienceDefense: dw.def.ScienceDefense, RangedDefense: dw.def.RangedDefense, Weakness: dw.def.Weakness, + StealTable: dw.def.StealTable, + StealLevel: dw.def.StealLevel, + StealXP: dw.def.StealXP, + StealSpeed: dw.def.StealSpeed, + AssassinLevel: dw.def.AssassinLevel, + FinishingBlow: dw.def.FinishingBlow, + DamageWithout: dw.def.DamageWithout, } inst.IdleDescription = pickIdleDescription(dw.def.IdleDescriptions) s.instances[instID] = inst diff --git a/worldbuilding_guide/behaviors.md b/worldbuilding_guide/behaviors.md index 8e47883..aa46073 100644 --- a/worldbuilding_guide/behaviors.md +++ b/worldbuilding_guide/behaviors.md @@ -197,6 +197,12 @@ nodes: | `take_item` | Removes an item from the player's inventory | | `teleport` | Moves the player to a room ID | | `heal` | Restores that many hitpoints | +| `cost` | Credits charged for the action | +| `shop` | Opens a buy/sell shop interface (see Shop section below) | +| `assign_task` | Assigns a random assassin task to the player based on their assassin level | +| `skip_task` | Cancels current assassin task, costs 30 reputation, resets streak | +| `extend_task` | Adds 50% more kills to current task, costs 30 reputation | +| `reputation_cost` | Deducts reputation from the player's `assassin_reputation` flag (fails node if insufficient) | 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. @@ -212,6 +218,67 @@ action: teleport: 1 # return to town ``` +#### Shop (buy/sell interface) + +The `shop` node action opens a dedicated buy/sell interface. The player can browse +items, buy with credits, and sell items back. Define a shop on a talk node: + +```yaml +action: + shop: + message: "What would you like to buy or sell?" + items: + - item_id: fishing_rod + buy_price: 10 + sell_price: 2 + - item_id: fishing_bait + buy_price: 2 + sell_price: 0 +``` + +| Field | Description | +|---|---| +| `shop.message` | Greeting shown when entering the shop | +| `shop.items` | List of items for sale | +| `item_id` | Item definition ID | +| `buy_price` | Credits to buy from shop | +| `sell_price` | Credits shop pays (0 = won't buy) | + +Typical shop flow: a talk node with a "Browse" option leads to a `shop` node. +When the player leaves the shop, they return to the talk node's options. + +Full example — General Store: +```yaml +id: general_store +type: talk +nodes: + start: + message: "\"Welcome to the General Store!\"" + options: + - text: "\"I'd like to browse.\"" + goto: shop + - text: "\"Goodbye.\"" + end: true + shop: + message: "\"Take your time.\"" + action: + shop: + message: "What would you like to buy or sell?" + items: + - item_id: fishing_rod + buy_price: 10 + sell_price: 2 + - item_id: hammer + buy_price: 20 + sell_price: 5 + options: + - text: "\"I'm done.\"" + goto: start +``` + +Node options on the shop node are shown when the player leaves the shop. +Use `goto: start` to loop back to the main greeting. + ### Toggle (levers, switches, gates) Simple toggle that sets a world flag: diff --git a/worldbuilding_guide/conditions.md b/worldbuilding_guide/conditions.md index ce9f74c..773230b 100644 --- a/worldbuilding_guide/conditions.md +++ b/worldbuilding_guide/conditions.md @@ -28,6 +28,10 @@ condition: condition: has_item: bronze_key not: true + +# Check if player has enough credits +condition: + min_credits: 50 ``` ### Compound conditions diff --git a/worldbuilding_guide/mobs.md b/worldbuilding_guide/mobs.md index 55c41b0..3826dda 100644 --- a/worldbuilding_guide/mobs.md +++ b/worldbuilding_guide/mobs.md @@ -104,5 +104,69 @@ idle_descriptions: Note: `wander_rooms` and `wander_interval` are NOT set on the mob definition. Wander config is per-instance in the room YAML (see Rooms > Mobs section above). +### Stealable Mobs + +Mobs can be stealable via the `steal` command. On failure, the mob turns hostile and attacks. + +```yaml +id: farmer +name: Farmer +steal_table: farmer_steal # Drop table for steal loot +steal_level: 10 # Required thieving level +steal_xp: 15 # XP per successful steal +steal_speed: 4 # Ticks per steal attempt +``` + +| Field | Description | +|---|---| +| `steal_table` | Drop table ID for loot when stealing | +| `steal_level` | Required thieving level | +| `steal_xp` | XP awarded per successful steal | +| `steal_speed` | Ticks per steal attempt (base wait) | + +### Assassin Mobs + +Assassin (Slayer) mobs restrict combat by assassin level and introduce finishing blows and protective equipment: + +```yaml +id: slug +name: slug +assassin_level: 1 # required assassin level to attack +finishing_blow: salt # item required to kill (mob stays at 1 HP otherwise) +attack: 3 +strength: 3 +defense: 1 +hp: 15 +speed: 6 +drops: + remains: slug_mucus + loot: + - item_id: credits + weight: 100 + quantity: 15 +``` + +Damage-without mobs deal 1.5x damage unless the player has the specified item equipped: +```yaml +id: drone +name: drone +assassin_level: 15 +damage_without: insulated_gloves # 1.5x damage without this item equipped +attack: 15 +strength: 14 +defense: 12 +hp: 45 +speed: 4 +aggressive: true +``` + +| Field | Description | +|---|---| +| `assassin_level` | Required assassin skill level to attack this mob | +| `finishing_blow` | Item ID required to kill this mob (mob stays at 1 HP until used via `use <item> on <target>`) | +| `damage_without` | Item ID for protection — mob deals 1.5x damage if player doesn't have it equipped | + +Mobs without these fields work normally (assassin_level defaults to 0, finishing_blow and damage_without default to empty). + --- diff --git a/worldbuilding_guide/objects.md b/worldbuilding_guide/objects.md index 3644a18..1c1aebb 100644 --- a/worldbuilding_guide/objects.md +++ b/worldbuilding_guide/objects.md @@ -119,3 +119,34 @@ use_interactions: --- +## Stealable Objects + +Objects can be stealable via the `steal` command. These use the same drop table system as mobs and searches. + +```yaml +id: market_stall +name: Market Stall +description: "A wooden stall piled with food and sundries." +steal_table: market_stall_steal +steal_level: 5 +steal_xp: 12 +steal_speed: 5 +guard_mob: guard +``` + +| Field | Description | +| ------------- | ------------------------------------------------ | +| `steal_table` | Drop table ID for loot when stealing | +| `steal_level` | Required thieving level | +| `steal_xp` | XP awarded per successful steal | +| `steal_speed` | Ticks per steal attempt (base wait) | +| `guard_mob` | Mob def ID that guards this object (watches it) | + +When `guard_mob` is set, a mob with that def ID in the same room watches the object +on a tick-based cycle (8 ticks watching, 4 ticks looking away). While the guard is +watching, steal success chance is halved and failures trigger a confrontation dialog. + +Use the `sneak` command to see guard watch state changes in real time. + +--- + |
