aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-25 19:37:20 -0400
committerhistoria <[not public]>2026-06-25 19:37:20 -0400
commitc55d0e4150b23f2c5c9f96bbc3d4dc2fc6dbaa36 (patch)
treed1c02acea2b4c8d0f672c1539cfdb373b6272ed0
parent068ce514ea3eebdc42b595c631b4b00ce4594577 (diff)
downloadthehouseoficarus-c55d0e4150b23f2c5c9f96bbc3d4dc2fc6dbaa36.tar.gz
feat: yaml conversation trees more robust
-rw-r--r--building_guide/behaviors.md170
-rw-r--r--building_guide/construction.md2
-rw-r--r--building_guide/mobs.md39
-rw-r--r--building_guide/objects.md15
-rw-r--r--building_guide/quests.md3
-rw-r--r--data/mobs/examples/ancient_construct.yaml (renamed from data/mobs/ancient_construct.yaml)0
-rw-r--r--data/mobs/examples/bandit.yaml (renamed from data/mobs/bandit.yaml)0
-rw-r--r--data/mobs/examples/bioengineer.yaml (renamed from data/mobs/bioengineer.yaml)0
-rw-r--r--data/mobs/examples/client.yaml (renamed from data/mobs/client.yaml)15
-rw-r--r--data/mobs/examples/corrupted_scientist.yaml (renamed from data/mobs/corrupted_scientist.yaml)0
-rw-r--r--data/mobs/examples/cow.yaml (renamed from data/mobs/cow.yaml)0
-rw-r--r--data/mobs/examples/crawler.yaml (renamed from data/mobs/crawler.yaml)0
-rw-r--r--data/mobs/examples/drone.yaml (renamed from data/mobs/drone.yaml)0
-rw-r--r--data/mobs/examples/elite_synth.yaml (renamed from data/mobs/elite_synth.yaml)0
-rw-r--r--data/mobs/examples/enforcer_bot.yaml (renamed from data/mobs/enforcer_bot.yaml)0
-rw-r--r--data/mobs/examples/estate_broker.yaml (renamed from data/mobs/estate_broker.yaml)17
-rw-r--r--data/mobs/examples/farmer.yaml (renamed from data/mobs/farmer.yaml)0
-rw-r--r--data/mobs/examples/feral_cyborg.yaml (renamed from data/mobs/feral_cyborg.yaml)0
-rw-r--r--data/mobs/examples/general_store_clerk.yaml (renamed from data/mobs/general_store_clerk.yaml)3
-rw-r--r--data/mobs/examples/ghoul.yaml (renamed from data/mobs/ghoul.yaml)0
-rw-r--r--data/mobs/examples/goblin.yaml (renamed from data/mobs/goblin.yaml)0
-rw-r--r--data/mobs/examples/greater_drone.yaml (renamed from data/mobs/greater_drone.yaml)0
-rw-r--r--data/mobs/examples/guard.yaml (renamed from data/mobs/guard.yaml)15
-rw-r--r--data/mobs/examples/heavy_mech.yaml (renamed from data/mobs/heavy_mech.yaml)0
-rw-r--r--data/mobs/examples/iron_sentinel.yaml (renamed from data/mobs/iron_sentinel.yaml)0
-rw-r--r--data/mobs/examples/lesser_drone.yaml (renamed from data/mobs/lesser_drone.yaml)0
-rw-r--r--data/mobs/examples/man.yaml (renamed from data/mobs/man.yaml)0
-rw-r--r--data/mobs/examples/molten_elemental.yaml (renamed from data/mobs/molten_elemental.yaml)0
-rw-r--r--data/mobs/examples/moss_giant.yaml (renamed from data/mobs/moss_giant.yaml)0
-rw-r--r--data/mobs/examples/mutant_rat.yaml (renamed from data/mobs/mutant_rat.yaml)0
-rw-r--r--data/mobs/examples/newbie_trainer.yaml (renamed from data/mobs/newbie_trainer.yaml)0
-rw-r--r--data/mobs/examples/phantom.yaml (renamed from data/mobs/phantom.yaml)0
-rw-r--r--data/mobs/examples/plasma_wraith.yaml (renamed from data/mobs/plasma_wraith.yaml)0
-rw-r--r--data/mobs/examples/psionic_entity.yaml (renamed from data/mobs/psionic_entity.yaml)0
-rw-r--r--data/mobs/examples/rat.yaml (renamed from data/mobs/rat.yaml)0
-rw-r--r--data/mobs/examples/sawmill_operator.yaml (renamed from data/mobs/sawmill_operator.yaml)11
-rw-r--r--data/mobs/examples/scrap_bot.yaml (renamed from data/mobs/scrap_bot.yaml)0
-rw-r--r--data/mobs/examples/security_turret.yaml (renamed from data/mobs/security_turret.yaml)0
-rw-r--r--data/mobs/examples/skeleton.yaml (renamed from data/mobs/skeleton.yaml)0
-rw-r--r--data/mobs/examples/slug.yaml (renamed from data/mobs/slug.yaml)0
-rw-r--r--data/mobs/examples/solar_panel_frame.yaml (renamed from data/mobs/solar_panel_frame.yaml)0
-rw-r--r--data/mobs/examples/tanner.yaml (renamed from data/mobs/tanner.yaml)9
-rw-r--r--data/mobs/examples/titan_sentinel.yaml (renamed from data/mobs/titan_sentinel.yaml)0
-rw-r--r--data/mobs/examples/veteran_merc.yaml (renamed from data/mobs/veteran_merc.yaml)0
-rw-r--r--data/mobs/examples/void_reaper.yaml (renamed from data/mobs/void_reaper.yaml)0
-rw-r--r--data/mobs/examples/war_drone.yaml (renamed from data/mobs/war_drone.yaml)0
-rw-r--r--data/mobs/examples/weapons_smith.yaml (renamed from data/mobs/weapons_smith.yaml)3
-rw-r--r--data/mobs/examples/zombie.yaml (renamed from data/mobs/zombie.yaml)0
-rw-r--r--data/mobs/flight_attendant.yaml32
-rw-r--r--data/objects/netrunner.yaml18
-rw-r--r--data/objects/tool_shed.yaml3
-rw-r--r--data/objects/unique/1001_sign.yaml6
-rw-r--r--data/rooms/intro/1001.yaml11
-rw-r--r--internal/behavior/behavior.go5
-rw-r--r--internal/behavior/types.go28
-rw-r--r--internal/game/act_steal.go14
-rw-r--r--internal/game/act_talk.go119
-rw-r--r--internal/game/cmd_shop.go42
-rw-r--r--internal/game/look_entities.go11
-rw-r--r--internal/game/look_target.go49
-rw-r--r--internal/object/object.go1
-rw-r--r--internal/validate/checks.go27
-rw-r--r--internal/world/mob.go10
63 files changed, 478 insertions, 200 deletions
diff --git a/building_guide/behaviors.md b/building_guide/behaviors.md
index 438e859..a2c1f89 100644
--- a/building_guide/behaviors.md
+++ b/building_guide/behaviors.md
@@ -3,7 +3,7 @@
Behaviors are **inline configs** placed directly inside object YAML (`gather:`, `talk:`,
`use:`, `safespot:`) or mob YAML (`talk:`). Each section below shows the keys you can use under each behavior type.
-The examples below are object/mob files; remember **the filename is the ID** — don't include an `id:` field (see `objects.md` / `mobs.md`).
+The examples below are object/mob files; remember the filename is the ID for these.
### Gather (mining, fishing, woodcutting)
@@ -183,15 +183,16 @@ toggle is on, the output includes the XP gain: `(+37xp wct)`.
### Talk (dialog trees)
-Talk configs go under the `talk:` key on objects or mobs. Full conversation with
-conditions, actions, and player flag tracking:
+Talk configs go under the `talk:` key on objects or mobs. A conversation is a set of named
+**nodes**, each with a message, optional action, optional auto-advance target, and choices
+for the player.
```yaml
name: Guard
talk:
nodes:
start:
- message: "\"Halt! This area is restricted.\""
+ message: "\"Halt! This area is restricted.\" The guard eyes you suspiciously."
options:
- text: "\"What's behind that gate?\""
goto: about_gate
@@ -205,16 +206,15 @@ talk:
player_flag: got_pass
value: true
- text: "\"Goodbye.\""
- end: true
+ # no goto = end conversation
about_gate:
- message: "\"Bring me some copper ore and I'll stamp you a pass.\""
+ message: "\"Supplies. Weapons.\" He shifts his weight. \"Bring me some copper ore and I'll stamp you a pass.\""
action:
set_player_flags:
talked_to_guard: true
options:
- text: "\"I'll be back.\""
- end: true
- text: "\"I have some right here.\""
goto: trade_ore
condition:
@@ -229,40 +229,57 @@ talk:
got_pass: true
options:
- text: "\"Thanks.\""
- end: true
has_pass:
- message: "\"Alright, I'll open the gate for you.\""
+ message: "\"Alright, I'll open the gate.\""
action:
set_flags:
gate_open: true
options:
- text: "\"Thanks.\""
- end: true
```
+#### TalkNode fields
+
+| Field | Type | Description |
+|---|---|---|
+| `message` | string or list | NPC dialogue. May be a single string or a list of strings (pick one at random). |
+| `action` | NodeAction | Fires when the node is entered (give items, set flags, etc). |
+| `options` | []TalkOption | Player choices. If empty and `next` is set, auto-advances. |
+| `next` | string | Node ID to auto-advance to when there are no visible options. |
+
+#### TalkOption fields
+
+| Field | Type | Description |
+|---|---|---|
+| `text` | string | Player-facing choice text. |
+| `goto` | string | Node ID to navigate to if this choice is selected. Omit to end the conversation. |
+| `condition` | Condition | Only show this option if the condition passes. |
+| `action` | NodeAction | Fires when this option is selected, BEFORE navigating to `goto`. |
+
#### Node action reference
| Field | Effect |
|---|---|
-| `set_flags` | Sets world flags (global, shared by all players) |
-| `set_player_flags` | Sets player-local flags (per-character, quest progress) |
-| `give_item` | Gives an item to the player's inventory |
-| `take_item` | Removes an item from the player's inventory |
-| `teleport` | Moves the player to a room ID |
-| `heal` | Restores that many hitpoints |
-| `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) |
-| `sawmill` | Opens sawmill plank conversion interface |
-
-All fields in a single action are processed together — you can give an item, take an item,
-set flags, and heal all in one node.
-
-Example — quest completion:
+| `set_flags` | Sets world flags (global, shared by all players). |
+| `set_player_flags` | Sets player-local flags (per-character, quest progress). |
+| `give_item` | Gives an item to the player's inventory. |
+| `take_item` | Removes an item from the player's inventory. |
+| `teleport` | Moves the player to a room ID. |
+| `heal` | Restores that many hitpoints. |
+| `cost` | Credits charged for the action. |
+| `shop` | Opens a buy/sell shop interface. See Shop section below. |
+| `assign_task` | Assigns a random assassin task based on 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 `assassin_reputation` flag before other actions. |
+| `sawmill` | Opens sawmill plank conversion interface. |
+| `aps_node` | Marks this room's APS node as unlocked. |
+
+All fields in a single action are processed together — give an item, take an item, set flags,
+and heal in one node.
+
+Example — quest completion with multiple actions:
```yaml
action:
take_item: dragon_head
@@ -274,10 +291,85 @@ action:
teleport: 1
```
+#### Option-level actions
+
+Actions can be placed directly on options. The action fires when the player selects that
+choice, BEFORE navigating to `goto`. This eliminates needing a separate "action node" for
+simple transactions:
+
+```yaml
+nodes:
+ start:
+ message: "Welcome! Houses are 10 credits. Interested?"
+ options:
+ - text: "Yes, I'll buy one."
+ action:
+ cost: 10
+ set_player_flags:
+ owns_house: true
+ goto: purchased
+ condition:
+ player_flag: owns_house
+ not: true
+ - text: "No thanks."
+ purchased:
+ message: "Excellent! The plot is yours."
+ options:
+ - text: "Thanks!"
+ goto: start
+```
+
+If an option has both an `action` and a `goto`, the action runs first, then the player
+navigates to the target node (which may also have its own action, fired on entry).
+
+#### Randomized messages
+
+`message` accepts either a plain string or a list. A list picks one entry at random each
+time the node is visited:
+
+```yaml
+nodes:
+ greeting:
+ message:
+ - "\"Hello, traveller.\""
+ - "\"Ah, you again.\""
+ - "\"A visitor! Pull up a chair.\""
+ - "\"Back so soon?\""
+ options:
+ - text: "\"Hello.\""
+```
+
+#### Linear auto-advance
+
+When a node has no options (or all its options are hidden by conditions) and `next` is set,
+the conversation automatically advances to the target node. This creates dialogue chains
+without prompting the player:
+
+```yaml
+nodes:
+ monologue_1:
+ message: "The elder clears his throat and begins..."
+ next: monologue_2
+ monologue_2:
+ message: "\"Long ago, before the asteroid was settled...\""
+ next: monologue_3
+ monologue_3:
+ message: "\"...a great darkness fell upon these halls.\" He pauses."
+ next: choice_point
+ choice_point:
+ message: "\"Do you understand the weight of what I'm telling you?\""
+ options:
+ - text: "\"I think so.\""
+ - text: "\"Not really.\""
+```
+
+If a node has both `next` and options, the options take priority — `next` is only used
+when no options are visible. A node with a shop action ignores `next` entirely.
+
#### 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:
+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:
@@ -294,14 +386,11 @@ action:
| 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.
+| `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). |
Full example — General Store:
```yaml
@@ -314,7 +403,6 @@ talk:
- text: "\"I'd like to browse.\""
goto: shop
- text: "\"Goodbye.\""
- end: true
shop:
message: "\"Take your time.\""
action:
@@ -332,8 +420,8 @@ talk:
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.
+When the player leaves the shop, the current node's options are shown (or auto-advance
+fires if no options are visible). Use `goto: start` to loop back to the main greeting.
### Object Interactions (levers, switches, gates)
diff --git a/building_guide/construction.md b/building_guide/construction.md
index 81e4249..40e4b9f 100644
--- a/building_guide/construction.md
+++ b/building_guide/construction.md
@@ -129,7 +129,6 @@ talk:
- text: "\"How much?\""
goto: offer
- text: "\"No thanks.\""
- end: true
offer:
message: "\"10 credits for a plot.\""
action:
@@ -138,7 +137,6 @@ talk:
owns_house_local_neighborhood: "local_neighborhood"
options:
- text: "\"Deal!\""
- end: true
unique: true
```
diff --git a/building_guide/mobs.md b/building_guide/mobs.md
index d10f685..32ee408 100644
--- a/building_guide/mobs.md
+++ b/building_guide/mobs.md
@@ -77,18 +77,46 @@ ranged_defense: 3
### Protected/Unique Mobs
-Mob with talk behavior — can be talked to via `talk:` inline config:
+Protected mobs cannot be attacked. All combat and stat fields (`attack`, `strength`,
+`defense`, `hp`, `speed`, `attack_type`, `attack_bonus`, `strength_bonus`,
+`*_defense`, `weakness`, `size`, `drops`, `aggressive`, `respawn_ticks`) are
+**optional for protected mobs** — omit them entirely. Protected mobs don't show
+a combat level in room listings or when examined.
+
+Minimal protected mob (shopkeeper, quest giver, NPC):
+```yaml
+name: flight attendant
+description: A smiling flight attendant in a crisp uniform.
+unique: true # displays as "Flight attendant" not "a flight attendant"
+protected: true # cannot be attacked, combat stats are optional
+idle_descriptions:
+ - adjusts her name badge
+ - smiles politely at passengers
+talk: # inline talk config
+ nodes:
+ start:
+ message: "\"Welcome aboard!\""
+ options:
+ - text: "\"What should I do?\""
+ goto: check_sign
+ - text: "\"Goodbye.\""
+ check_sign:
+ message: "\"Review the safety information.\""
+ options:
+ - text: "\"Will do.\""
+```
+
+Protected mob with combat stats (for internal testing or debug purposes):
```yaml
name: "Guard"
-talk: # inline talk config (no separate behavior file)
+unique: true
+protected: true
+talk:
nodes:
start:
message: "\"Halt! Who goes there?\""
options:
- text: "\"Just passing through.\""
- end: true
-unique: true # displays as "Guard" not "a guard"
-protected: true # cannot be attacked
attack: 5
strength: 5
defense: 5
@@ -106,7 +134,6 @@ science_defense: -5
ranged_defense: 12
idle_descriptions:
- "scans the area with a watchful eye"
- - "adjusts the grip on his weapon"
```
Note: `wander_rooms` and `wander_interval` are NOT set on the mob definition. Wander config
diff --git a/building_guide/objects.md b/building_guide/objects.md
index 110eb56..87b9710 100644
--- a/building_guide/objects.md
+++ b/building_guide/objects.md
@@ -120,9 +120,22 @@ talk:
- text: "\"What do you have?\""
goto: shop
- text: "\"Goodbye.\""
- end: true
```
+On-look action (runs when a player examines an object with `look <name>`):
+```yaml
+name: sign
+aliases: [notice, board]
+description: "A wooden signpost with faded writing."
+on_look:
+ set_player_flags:
+ read_sign: true
+```
+`on_look` fires the action AFTER showing the object's description. It uses the same
+`NodeAction` type as talk nodes — supports `set_player_flags`, `set_flags`,
+`give_item`, `take_item`, `teleport`, `heal`, `cost`, and everything else in the
+[node action reference](behaviors.md#node-action-reference).
+
See the [Talk section of behaviors](behaviors.md#talk-dialog-trees) for the full
dialog tree format. See [Stealable Objects](#stealable-objects) for theft mechanics.
diff --git a/building_guide/quests.md b/building_guide/quests.md
index a764f8e..b9556dc 100644
--- a/building_guide/quests.md
+++ b/building_guide/quests.md
@@ -23,7 +23,6 @@ talk:
player_flag: rat_quest
value: started
- text: "\"Goodbye.\""
- end: true
accept_quest:
message: "\"Good lad. The cellar is west of here. Come back when they're dead.\""
@@ -32,7 +31,6 @@ talk:
rat_quest: started
options:
- text: "\"On my way.\""
- end: true
turn_in:
message: "\"You did it! The village owes you a debt. Here — take this.\""
@@ -43,7 +41,6 @@ talk:
heal: 10
options:
- text: "\"Thanks!\""
- end: true
attack: 1
strength: 1
defense: 1
diff --git a/data/mobs/ancient_construct.yaml b/data/mobs/examples/ancient_construct.yaml
index 190e8c2..190e8c2 100644
--- a/data/mobs/ancient_construct.yaml
+++ b/data/mobs/examples/ancient_construct.yaml
diff --git a/data/mobs/bandit.yaml b/data/mobs/examples/bandit.yaml
index 51e4b8e..51e4b8e 100644
--- a/data/mobs/bandit.yaml
+++ b/data/mobs/examples/bandit.yaml
diff --git a/data/mobs/bioengineer.yaml b/data/mobs/examples/bioengineer.yaml
index 4761991..4761991 100644
--- a/data/mobs/bioengineer.yaml
+++ b/data/mobs/examples/bioengineer.yaml
diff --git a/data/mobs/client.yaml b/data/mobs/examples/client.yaml
index 678ba41..91b6802 100644
--- a/data/mobs/client.yaml
+++ b/data/mobs/examples/client.yaml
@@ -22,8 +22,7 @@ talk:
assign_task: true
message: '"Let me check what''s available..." The Client scrolls through their data pad.'
options:
- - end: true
- text: '"Understood."'
+ - text: '"Understood."'
- goto: start
text: '"What else do you have?"'
buy_acid:
@@ -105,8 +104,7 @@ talk:
current_task:
message: '"Let me check your file." The Client pulls up your record.'
options:
- - end: true
- text: '"Okay."'
+ - text: '"Okay."'
extend_confirm:
message: '"Extending your task costs 30 Reputation and adds more kills. Want to proceed?"'
options:
@@ -119,8 +117,7 @@ talk:
extend_task: true
message: '"Done. I''ve added more targets to your contract."'
options:
- - end: true
- text: '"Thanks."'
+ - text: '"Thanks."'
rep_shop:
message: '"Here''s what I''ve got. All purchases are permanent."'
options:
@@ -160,8 +157,7 @@ talk:
options:
- goto: assign_task
text: '"Give me a new one."'
- - end: true
- text: '"Goodbye."'
+ - text: '"Goodbye."'
start:
message: The Client looks up from their data pad. "What do you need?"
options:
@@ -192,8 +188,7 @@ talk:
text: '"I''d like to browse the Reputation Shop."'
- goto: supplies
text: '"I need supplies."'
- - end: true
- text: '"Goodbye."'
+ - text: '"Goodbye."'
supplies:
message: '"I stock everything you need for the job. Cheap, too."'
options:
diff --git a/data/mobs/corrupted_scientist.yaml b/data/mobs/examples/corrupted_scientist.yaml
index 2083c03..2083c03 100644
--- a/data/mobs/corrupted_scientist.yaml
+++ b/data/mobs/examples/corrupted_scientist.yaml
diff --git a/data/mobs/cow.yaml b/data/mobs/examples/cow.yaml
index 7f1ee3c..7f1ee3c 100644
--- a/data/mobs/cow.yaml
+++ b/data/mobs/examples/cow.yaml
diff --git a/data/mobs/crawler.yaml b/data/mobs/examples/crawler.yaml
index 9021c51..9021c51 100644
--- a/data/mobs/crawler.yaml
+++ b/data/mobs/examples/crawler.yaml
diff --git a/data/mobs/drone.yaml b/data/mobs/examples/drone.yaml
index 5ac6552..5ac6552 100644
--- a/data/mobs/drone.yaml
+++ b/data/mobs/examples/drone.yaml
diff --git a/data/mobs/elite_synth.yaml b/data/mobs/examples/elite_synth.yaml
index b095716..b095716 100644
--- a/data/mobs/elite_synth.yaml
+++ b/data/mobs/examples/elite_synth.yaml
diff --git a/data/mobs/enforcer_bot.yaml b/data/mobs/examples/enforcer_bot.yaml
index 78150c3..78150c3 100644
--- a/data/mobs/enforcer_bot.yaml
+++ b/data/mobs/examples/enforcer_bot.yaml
diff --git a/data/mobs/estate_broker.yaml b/data/mobs/examples/estate_broker.yaml
index 3196012..a156e83 100644
--- a/data/mobs/estate_broker.yaml
+++ b/data/mobs/examples/estate_broker.yaml
@@ -2,12 +2,13 @@ color: "227"
description: A sharply-dressed real estate agent with a datapad and a practiced smile.
idle: The estate broker flashes a professional smile.
name: estate broker
+protected: true
talk:
nodes:
already_own:
message: Yes, you're already a proud homeowner here! Your house is waiting for you. Use the Estate Directory to enter it, or expand your workshop.
options:
- - next: start
+ - goto: start
text: Good to know.
goodbye:
message: Come back when you're ready to invest in your future!
@@ -21,14 +22,14 @@ talk:
condition:
not: true
player_flag: owns_house_local_neighborhood
- next: purchased
+ goto: purchased
text: Sounds great! I'll buy one.
- - next: goodbye
+ - goto: goodbye
text: Maybe later.
purchased:
message: Excellent! The plot is yours. You'll find your new home in the Local Neighborhood just north of here. Use the Estate Directory terminal to enter your house. The home comes with a workshop — perfect for construction projects.
options:
- - next: start
+ - goto: start
text: Thanks!
start:
message: Welcome to the Local Neighborhood development! I'm selling premium housing plots for only 10 credits. Interested in buying a house?
@@ -40,15 +41,15 @@ talk:
condition:
not: true
player_flag: owns_house_local_neighborhood
- next: purchased
+ goto: purchased
text: Yes, I'd like to buy a house.
- condition:
player_flag: owns_house_local_neighborhood
- next: already_own
+ goto: already_own
text: I already own a house here.
- - next: goodbye
+ - goto: goodbye
text: No thanks.
- - next: info
+ - goto: info
text: What do I get with a house?
unique: true
size: small
diff --git a/data/mobs/farmer.yaml b/data/mobs/examples/farmer.yaml
index 591f1a3..591f1a3 100644
--- a/data/mobs/farmer.yaml
+++ b/data/mobs/examples/farmer.yaml
diff --git a/data/mobs/feral_cyborg.yaml b/data/mobs/examples/feral_cyborg.yaml
index 8b30cc3..8b30cc3 100644
--- a/data/mobs/feral_cyborg.yaml
+++ b/data/mobs/examples/feral_cyborg.yaml
diff --git a/data/mobs/general_store_clerk.yaml b/data/mobs/examples/general_store_clerk.yaml
index 351b4c0..8a07191 100644
--- a/data/mobs/general_store_clerk.yaml
+++ b/data/mobs/examples/general_store_clerk.yaml
@@ -65,7 +65,6 @@ talk:
options:
- goto: shop
text: '"I''d like to browse."'
- - end: true
- text: '"Goodbye."'
+ - text: '"Goodbye."'
unique: true
size: small
diff --git a/data/mobs/ghoul.yaml b/data/mobs/examples/ghoul.yaml
index ae8fed6..ae8fed6 100644
--- a/data/mobs/ghoul.yaml
+++ b/data/mobs/examples/ghoul.yaml
diff --git a/data/mobs/goblin.yaml b/data/mobs/examples/goblin.yaml
index a02c112..a02c112 100644
--- a/data/mobs/goblin.yaml
+++ b/data/mobs/examples/goblin.yaml
diff --git a/data/mobs/greater_drone.yaml b/data/mobs/examples/greater_drone.yaml
index 11cf8e2..11cf8e2 100644
--- a/data/mobs/greater_drone.yaml
+++ b/data/mobs/examples/greater_drone.yaml
diff --git a/data/mobs/guard.yaml b/data/mobs/examples/guard.yaml
index 46c235c..7bd7d19 100644
--- a/data/mobs/guard.yaml
+++ b/data/mobs/examples/guard.yaml
@@ -29,22 +29,19 @@ talk:
talked_to_guard: true
message: '"Supplies. Weapons. Things you don''t need to worry about." He shifts his weight. "Tell you what — bring me some copper ore and I''ll stamp you a pass."'
options:
- - end: true
- text: '"I''ll be back."'
+ - text: '"I''ll be back."'
- condition:
has_item: copper_ore
goto: trade_ore
text: '"I have some right here."'
- - end: true
- text: '"Goodbye."'
+ - text: '"Goodbye."'
has_pass:
action:
set_flags:
gate_open: true
message: '"Let me see that..." He examines your pass and nods. "Alright, I''ll open the gate for you."'
options:
- - end: true
- text: '"Thanks."'
+ - text: '"Thanks."'
start:
message: '"Halt! This area is restricted." The guard eyes you suspiciously.'
options:
@@ -59,8 +56,7 @@ talk:
value: true
goto: has_pass
text: '"I have a pass."'
- - end: true
- text: '"Goodbye."'
+ - text: '"Goodbye."'
trade_ore:
action:
give_item: pass_stub
@@ -70,7 +66,6 @@ talk:
take_item: copper_ore
message: '"Good quality ore. This''ll do." He stamps a slip of paper and hands it to you. "That''s your pass. Don''t lose it."'
options:
- - end: true
- text: '"Thanks."'
+ - text: '"Thanks."'
unique: true
size: medium
diff --git a/data/mobs/heavy_mech.yaml b/data/mobs/examples/heavy_mech.yaml
index e2f5f5a..e2f5f5a 100644
--- a/data/mobs/heavy_mech.yaml
+++ b/data/mobs/examples/heavy_mech.yaml
diff --git a/data/mobs/iron_sentinel.yaml b/data/mobs/examples/iron_sentinel.yaml
index bf8b9b5..bf8b9b5 100644
--- a/data/mobs/iron_sentinel.yaml
+++ b/data/mobs/examples/iron_sentinel.yaml
diff --git a/data/mobs/lesser_drone.yaml b/data/mobs/examples/lesser_drone.yaml
index c9e50a5..c9e50a5 100644
--- a/data/mobs/lesser_drone.yaml
+++ b/data/mobs/examples/lesser_drone.yaml
diff --git a/data/mobs/man.yaml b/data/mobs/examples/man.yaml
index 8478f41..8478f41 100644
--- a/data/mobs/man.yaml
+++ b/data/mobs/examples/man.yaml
diff --git a/data/mobs/molten_elemental.yaml b/data/mobs/examples/molten_elemental.yaml
index 9c46ccb..9c46ccb 100644
--- a/data/mobs/molten_elemental.yaml
+++ b/data/mobs/examples/molten_elemental.yaml
diff --git a/data/mobs/moss_giant.yaml b/data/mobs/examples/moss_giant.yaml
index f19f0a3..f19f0a3 100644
--- a/data/mobs/moss_giant.yaml
+++ b/data/mobs/examples/moss_giant.yaml
diff --git a/data/mobs/mutant_rat.yaml b/data/mobs/examples/mutant_rat.yaml
index 7836ef9..7836ef9 100644
--- a/data/mobs/mutant_rat.yaml
+++ b/data/mobs/examples/mutant_rat.yaml
diff --git a/data/mobs/newbie_trainer.yaml b/data/mobs/examples/newbie_trainer.yaml
index 3897dba..3897dba 100644
--- a/data/mobs/newbie_trainer.yaml
+++ b/data/mobs/examples/newbie_trainer.yaml
diff --git a/data/mobs/phantom.yaml b/data/mobs/examples/phantom.yaml
index d871b76..d871b76 100644
--- a/data/mobs/phantom.yaml
+++ b/data/mobs/examples/phantom.yaml
diff --git a/data/mobs/plasma_wraith.yaml b/data/mobs/examples/plasma_wraith.yaml
index c4ced2b..c4ced2b 100644
--- a/data/mobs/plasma_wraith.yaml
+++ b/data/mobs/examples/plasma_wraith.yaml
diff --git a/data/mobs/psionic_entity.yaml b/data/mobs/examples/psionic_entity.yaml
index 1c518bf..1c518bf 100644
--- a/data/mobs/psionic_entity.yaml
+++ b/data/mobs/examples/psionic_entity.yaml
diff --git a/data/mobs/rat.yaml b/data/mobs/examples/rat.yaml
index 4265674..4265674 100644
--- a/data/mobs/rat.yaml
+++ b/data/mobs/examples/rat.yaml
diff --git a/data/mobs/sawmill_operator.yaml b/data/mobs/examples/sawmill_operator.yaml
index 81521bd..b70362f 100644
--- a/data/mobs/sawmill_operator.yaml
+++ b/data/mobs/examples/sawmill_operator.yaml
@@ -2,6 +2,7 @@ color: "186"
description: A burly worker in a thick apron, covered in sawdust. He operates the industrial sawmill.
idle: The sawmill operator wipes sawdust from his goggles.
name: sawmill operator
+protected: true
talk:
nodes:
goodbye:
@@ -22,9 +23,9 @@ talk:
- has_item: oak_logs
- has_item: teak_logs
- has_item: mahogany_logs
- next: process
+ goto: process
text: Yes, process all my logs.
- - next: goodbye
+ - goto: goodbye
text: No thanks.
process:
action:
@@ -39,11 +40,11 @@ talk:
- has_item: oak_logs
- has_item: teak_logs
- has_item: mahogany_logs
- next: prices
+ goto: prices
text: Turn all my logs into planks.
- - next: prices
+ - goto: prices
text: What are your prices?
- - next: goodbye
+ - goto: goodbye
text: Nothing for now.
unique: true
size: small
diff --git a/data/mobs/scrap_bot.yaml b/data/mobs/examples/scrap_bot.yaml
index a8b34d8..a8b34d8 100644
--- a/data/mobs/scrap_bot.yaml
+++ b/data/mobs/examples/scrap_bot.yaml
diff --git a/data/mobs/security_turret.yaml b/data/mobs/examples/security_turret.yaml
index 9929106..9929106 100644
--- a/data/mobs/security_turret.yaml
+++ b/data/mobs/examples/security_turret.yaml
diff --git a/data/mobs/skeleton.yaml b/data/mobs/examples/skeleton.yaml
index ba82afc..ba82afc 100644
--- a/data/mobs/skeleton.yaml
+++ b/data/mobs/examples/skeleton.yaml
diff --git a/data/mobs/slug.yaml b/data/mobs/examples/slug.yaml
index 64082ec..64082ec 100644
--- a/data/mobs/slug.yaml
+++ b/data/mobs/examples/slug.yaml
diff --git a/data/mobs/solar_panel_frame.yaml b/data/mobs/examples/solar_panel_frame.yaml
index 90269f2..90269f2 100644
--- a/data/mobs/solar_panel_frame.yaml
+++ b/data/mobs/examples/solar_panel_frame.yaml
diff --git a/data/mobs/tanner.yaml b/data/mobs/examples/tanner.yaml
index 1325468..2e5d89a 100644
--- a/data/mobs/tanner.yaml
+++ b/data/mobs/examples/tanner.yaml
@@ -36,8 +36,7 @@ talk:
- min_credits: 50
goto: tan_hard_leather
text: '"Tan a cowhide into hard leather (50 credits)."'
- - end: true
- text: '"Never mind."'
+ - text: '"Never mind."'
tan_hard_leather:
action:
cost: 50
@@ -49,8 +48,7 @@ talk:
has_item: cowhide
goto: start
text: '"Tan another."'
- - end: true
- text: '"Thanks."'
+ - text: '"Thanks."'
tan_leather:
action:
cost: 10
@@ -62,7 +60,6 @@ talk:
has_item: cowhide
goto: start
text: '"Tan another."'
- - end: true
- text: '"Thanks."'
+ - text: '"Thanks."'
unique: true
size: small
diff --git a/data/mobs/titan_sentinel.yaml b/data/mobs/examples/titan_sentinel.yaml
index 32ddcc2..32ddcc2 100644
--- a/data/mobs/titan_sentinel.yaml
+++ b/data/mobs/examples/titan_sentinel.yaml
diff --git a/data/mobs/veteran_merc.yaml b/data/mobs/examples/veteran_merc.yaml
index 88238b6..88238b6 100644
--- a/data/mobs/veteran_merc.yaml
+++ b/data/mobs/examples/veteran_merc.yaml
diff --git a/data/mobs/void_reaper.yaml b/data/mobs/examples/void_reaper.yaml
index e54d776..e54d776 100644
--- a/data/mobs/void_reaper.yaml
+++ b/data/mobs/examples/void_reaper.yaml
diff --git a/data/mobs/war_drone.yaml b/data/mobs/examples/war_drone.yaml
index 587ad09..587ad09 100644
--- a/data/mobs/war_drone.yaml
+++ b/data/mobs/examples/war_drone.yaml
diff --git a/data/mobs/weapons_smith.yaml b/data/mobs/examples/weapons_smith.yaml
index 2af6299..0ba8237 100644
--- a/data/mobs/weapons_smith.yaml
+++ b/data/mobs/examples/weapons_smith.yaml
@@ -59,7 +59,6 @@ talk:
options:
- goto: shop
text: '"Show me what you''ve got."'
- - end: true
- text: '"Nothing right now."'
+ - text: '"Nothing right now."'
unique: true
size: small
diff --git a/data/mobs/zombie.yaml b/data/mobs/examples/zombie.yaml
index f5989e0..f5989e0 100644
--- a/data/mobs/zombie.yaml
+++ b/data/mobs/examples/zombie.yaml
diff --git a/data/mobs/flight_attendant.yaml b/data/mobs/flight_attendant.yaml
new file mode 100644
index 0000000..81bc3bf
--- /dev/null
+++ b/data/mobs/flight_attendant.yaml
@@ -0,0 +1,32 @@
+name: Flight attendant
+description: A smiling flight attendant in a crisp uniform, ready to assist passengers.
+unique: false
+protected: true
+idle_descriptions:
+ - straightens a seat cushion
+ - checks the cabin for loose items
+ - adjusts her name badge
+ - smiles politely at passengers
+talk:
+ nodes:
+ start:
+ message: '"Welcome aboard! Is there anything I can help you with?"'
+ options:
+ - text: '"I''m ready to disembark."'
+ goto: disembark
+ condition:
+ player_flag: 1001_look_sign
+ - text: '"What should I do?"'
+ goto: check_sign
+ condition:
+ player_flag: 1001_look_sign
+ not: true
+ - text: '"Goodbye."'
+ disembark:
+ message: '"Of course! Please proceed down the stairs when you''re ready. Have a wonderful stay."'
+ options:
+ - text: '"Thanks!"'
+ check_sign:
+ message: '"Before we land, please review the safety information posted on the wall. Just type {11 bold}look sign{/} to see it."'
+ options:
+ - text: '"Okay, I''ll take a look."'
diff --git a/data/objects/netrunner.yaml b/data/objects/netrunner.yaml
index 21049e9..ced3f10 100644
--- a/data/objects/netrunner.yaml
+++ b/data/objects/netrunner.yaml
@@ -23,15 +23,13 @@ talk:
value: true
goto: tips_secure
text: '"Tell me about the secure terminal."'
- - end: true
- text: '"Goodbye."'
+ - text: '"Goodbye."'
terminals:
message: '"There''s a basic terminal right here in this lab -- good for beginners. The station down east has an advanced one if you''ve got the chops. And if you''re really good, there''s a secure terminal deep in the compound. You''ll need the skills to match though."'
options:
- goto: tips_basic
text: '"Any tips?"'
- - end: true
- text: '"Thanks."'
+ - text: '"Thanks."'
tips_advanced:
action:
set_player_flags:
@@ -40,8 +38,7 @@ talk:
options:
- goto: tips_secure
text: '"Tell me about the secure terminal."'
- - end: true
- text: '"Thanks."'
+ - text: '"Thanks."'
tips_basic:
action:
set_player_flags:
@@ -50,17 +47,14 @@ talk:
options:
- goto: tips_advanced
text: '"What about the advanced terminal?"'
- - end: true
- text: '"Thanks."'
+ - text: '"Thanks."'
tips_secure:
message: '"Secure terminal? Signal Bluff. It''s a bluffing game -- you and the system each roll hidden dice and take turns making claims about the total. Ones are wild. Call the bluff or raise the stakes. Read the probabilities, play the odds, and don''t let it psych you out."'
options:
- - end: true
- text: '"Thanks."'
+ - text: '"Thanks."'
what_is:
message: '"Hacking''s about jacking into terminals and running their gauntlets. Each terminal runs a different challenge -- puzzles, hunts, pattern games. Crack ''em and you get better at it. Simple as that."'
options:
- goto: terminals
text: '"Where can I find terminals?"'
- - end: true
- text: '"Goodbye."'
+ - text: '"Goodbye."'
diff --git a/data/objects/tool_shed.yaml b/data/objects/tool_shed.yaml
index 583becb..87c9716 100644
--- a/data/objects/tool_shed.yaml
+++ b/data/objects/tool_shed.yaml
@@ -59,8 +59,7 @@ talk:
player_flag: tool_shed_watering_can
goto: retrieve_watering_can
text: Retrieve watering can
- - end: true
- text: Never mind
+ - text: Never mind
store_rake:
action:
set_player_flags:
diff --git a/data/objects/unique/1001_sign.yaml b/data/objects/unique/1001_sign.yaml
index 83b6a9c..2769563 100644
--- a/data/objects/unique/1001_sign.yaml
+++ b/data/objects/unique/1001_sign.yaml
@@ -1,5 +1,6 @@
name: sign
-hidden: true
+aliases: [safety card, safety instructions, frame]
+inroom_description: A large framed sign is mounted near the cockpit door.
description: |-
We hope you enjoy your trip on our shuttle. Your pilot is [TYLER].
@@ -22,3 +23,6 @@ description: |-
Type {12 bold}what{/} for a list of possible common actions
Make sure to check the {10}help{/} pages, especially {10}help newplayer{/}!
+on_look:
+ set_player_flags:
+ 1001_look_sign: true
diff --git a/data/rooms/intro/1001.yaml b/data/rooms/intro/1001.yaml
index 090e97c..fd7f2f3 100644
--- a/data/rooms/intro/1001.yaml
+++ b/data/rooms/intro/1001.yaml
@@ -2,11 +2,14 @@ name: "Inside Transport Shuttle"
description: "A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large framed sign hangs next to the closed cockpit door. A piston-powered staircase leads down out the back of the small craft with a smiling attendant ready to help disembark."
objects:
- id: 1001_sign
+mobs:
+ - flight_attendant
exits:
- down: 1002
+ down:
+ room: 1002
condition:
player_flag: 1001_look_sign
- blocked_message: "The attendant points to the sign and insists you 'look sign' before disembarking."
+ blocked_message: "The attendant points to the sign and insists you {10}look sign{/} before disembarking."
on_enter:
- condition:
player_flag: 1001_welcome
@@ -17,11 +20,11 @@ on_enter:
player_flag: 1001_welcome
not: true
delay: 7
- message: "{11}Type 'look sign' to check out the sign over there and get started{/}"
+ message: "{11}Type{/} {10}look sign{/} {11}or{/} {10}talk attendant{/} {11}to get started{/}"
set_player_flags:
1001_welcome: true
- condition:
player_flag: 1001_welcome
not: true
delay: 7
- message: "{11}Type 'help newplayer' for the new player's guide. That's it. Explore and have fun.{/}"
+ message: "{11}Type{/} {10}help newplayer{/} {11}for the new player's guide. That's it. Explore and have fun.{/}"
diff --git a/internal/behavior/behavior.go b/internal/behavior/behavior.go
index 57b2a7b..f81c73c 100644
--- a/internal/behavior/behavior.go
+++ b/internal/behavior/behavior.go
@@ -30,16 +30,17 @@ type TalkConfig struct {
}
type TalkNode struct {
- Message string `yaml:"message"`
+ Message MessageList `yaml:"message"`
Options []TalkOption `yaml:"options"`
Action *NodeAction `yaml:"action"`
+ Next string `yaml:"next,omitempty"`
}
type TalkOption struct {
Text string `yaml:"text"`
Goto string `yaml:"goto"`
- End bool `yaml:"end"`
Condition *Condition `yaml:"condition"`
+ Action *NodeAction `yaml:"action,omitempty"`
}
type NodeAction struct {
diff --git a/internal/behavior/types.go b/internal/behavior/types.go
index 3c6e290..f8e65f7 100644
--- a/internal/behavior/types.go
+++ b/internal/behavior/types.go
@@ -1,6 +1,11 @@
package behavior
-import "strings"
+import (
+ "math/rand"
+ "strings"
+
+ "gopkg.in/yaml.v3"
+)
func WordPrefixMatch(input, name string) bool {
inputWords := strings.Fields(strings.ToLower(input))
@@ -186,3 +191,24 @@ type DropEntry struct {
type DropTableDef struct {
Drops []DropEntry `yaml:"drops"`
}
+
+type MessageList []string
+
+func (ml *MessageList) UnmarshalYAML(value *yaml.Node) error {
+ if value.Kind == yaml.ScalarNode {
+ var s string
+ if err := value.Decode(&s); err != nil {
+ return err
+ }
+ *ml = MessageList{s}
+ return nil
+ }
+ return value.Decode((*[]string)(ml))
+}
+
+func (ml MessageList) Pick() string {
+ if len(ml) == 0 {
+ return ""
+ }
+ return ml[rand.Intn(len(ml))]
+}
diff --git a/internal/game/act_steal.go b/internal/game/act_steal.go
index 6f67944..7371782 100644
--- a/internal/game/act_steal.go
+++ b/internal/game/act_steal.go
@@ -28,7 +28,7 @@ func (g *Game) executeSteal(sess *net.Session, args []string, rawInput string) {
var stallGuardTalk = &behavior.TalkConfig{
Nodes: map[string]behavior.TalkNode{
"start": {
- Message: "Caught you red-handed! You have three options, thief.",
+ Message: behavior.MessageList{"Caught you red-handed! You have three options, thief."},
Options: []behavior.TalkOption{
{Text: "\"I'll pay a fine. (500 credits)\"", Goto: "bribe", Condition: &behavior.Condition{MinCredits: 500}},
{Text: "\"Take me to jail.\"", Goto: "jail"},
@@ -36,19 +36,19 @@ var stallGuardTalk = &behavior.TalkConfig{
},
},
"bribe": {
- Message: "Smart choice. Hand over 500 credits and we'll forget this happened.",
+ Message: behavior.MessageList{"Smart choice. Hand over 500 credits and we'll forget this happened."},
Action: &behavior.NodeAction{Cost: 500},
- Options: []behavior.TalkOption{{Text: "\"Fine, take it.\"", End: true}},
+ Options: []behavior.TalkOption{{Text: "\"Fine, take it.\""}},
},
"jail": {
- Message: "Off to the detention cell with you!",
+ Message: behavior.MessageList{"Off to the detention cell with you!"},
Action: &behavior.NodeAction{Teleport: 162},
- Options: []behavior.TalkOption{{Text: "(You are dragged away)", End: true}},
+ Options: []behavior.TalkOption{{Text: "(You are dragged away)"}},
},
"fight": {
- Message: "Then defend yourself!",
+ Message: behavior.MessageList{"Then defend yourself!"},
Action: &behavior.NodeAction{SetFlags: map[string]any{"guard_hostile": true}},
- Options: []behavior.TalkOption{{Text: "(The guard attacks!)", End: true}},
+ Options: []behavior.TalkOption{{Text: "(The guard attacks!)"}},
},
},
}
diff --git a/internal/game/act_talk.go b/internal/game/act_talk.go
index e9047b2..f54290a 100644
--- a/internal/game/act_talk.go
+++ b/internal/game/act_talk.go
@@ -6,6 +6,7 @@ import (
"strings"
"thehouseoficarus/internal/behavior"
+ "thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/object"
"thehouseoficarus/internal/player"
@@ -52,51 +53,99 @@ func (g *Game) startTalk(sess *net.Session, p *player.Player, obj *object.Object
}
}
+func (g *Game) handleNodeAction(sess *net.Session, na *behavior.NodeAction) (intercepted bool) {
+ if na.Shop != nil {
+ g.applyNodeAction(sess, na)
+ g.enterShop(sess, na.Shop)
+ return true
+ }
+ g.applyNodeAction(sess, na)
+
+ if v, ok := g.Flags.Get("guard_hostile"); ok && v != nil {
+ g.Flags.Delete("guard_hostile")
+ p := sess.Player
+ if p != nil && p.Action != nil {
+ if td, ok := p.Action.Data.(*behavior.TalkData); ok && td.StealGuard {
+ 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 true
+ }
+ }
+ }
+ return false
+}
+
func (g *Game) showTalkNode(sess *net.Session, node behavior.TalkNode) {
- sess.WriteLine(fmt.Sprintf("%s", g.colorize(sess, "dialog", node.Message)))
+ p := sess.Player
+ td, _ := p.Action.Data.(*behavior.TalkData)
+
+ dialogSpec := g.resolveColor(sess, "dialog")
+ msg := node.Message.Pick()
+ if msg != "" {
+ sess.WriteLine(color.ExpandTagsDefault(g.colorMode(sess), dialogSpec, msg))
+ }
if node.Action != nil {
- if node.Action.Shop != nil {
- g.applyNodeAction(sess, node.Action)
- g.enterShop(sess, node.Action.Shop)
+ if g.handleNodeAction(sess, node.Action) {
+ return
+ }
+ }
+
+ cfg := td.Cfg
+ for {
+ visible := g.visibleOptions(sess, node.Options)
+ if len(visible) > 0 {
+ for i, opt := range visible {
+ sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, opt.Text))
+ }
+ sess.State = net.StateTalk
+ sess.Write("\nChoice: ")
return
}
- g.applyNodeAction(sess, node.Action)
-
- if v, ok := g.Flags.Get("guard_hostile"); ok && v != nil {
- g.Flags.Delete("guard_hostile")
- p := sess.Player
- if p != nil && p.Action != nil {
- if td, ok := p.Action.Data.(*behavior.TalkData); ok && td.StealGuard {
- guardMob := g.findGuardInRoom(p.RoomID, "guard")
- if guardMob != nil {
- sess.State = net.StateGame
- g.cancelAction(p)
- guardMob.Protected = false
- g.startCombat(sess, p, guardMob)
- }
+
+ if node.Next != "" && cfg != nil {
+ next, ok := cfg.Nodes[node.Next]
+ if !ok {
+ break
+ }
+ if td != nil {
+ td.Node = node.Next
+ }
+ node = next
+
+ msg := node.Message.Pick()
+ if msg != "" {
+ sess.WriteLine(color.ExpandTagsDefault(g.colorMode(sess), dialogSpec, msg))
+ }
+
+ if node.Action != nil {
+ if g.handleNodeAction(sess, node.Action) {
return
}
}
+ } else {
+ break
}
}
- if len(node.Options) == 0 {
- sess.State = net.StateGame
- g.writePrompt(sess)
- return
- }
+ sess.State = net.StateGame
+ g.writePrompt(sess)
+}
- visible := 0
- for _, opt := range node.Options {
+func (g *Game) visibleOptions(sess *net.Session, options []behavior.TalkOption) []behavior.TalkOption {
+ var visible []behavior.TalkOption
+ for _, opt := range options {
if opt.Condition != nil && !g.checkCondition(sess, opt.Condition) {
continue
}
- visible++
- sess.WriteLine(fmt.Sprintf(" %d. %s", visible, opt.Text))
+ visible = append(visible, opt)
}
- sess.State = net.StateTalk
- sess.Write("\nChoice: ")
+ return visible
}
func (g *Game) handleTalkInput(sess *net.Session, input string) {
@@ -108,6 +157,9 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
}
input = strings.TrimSpace(input)
+ if input == "" {
+ input = "1"
+ }
lower := strings.ToLower(input)
if lower == "bye" || lower == "exit" || lower == "end" || lower == "quit" {
g.cancelAction(p)
@@ -168,11 +220,10 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
return
}
- if chosen.End {
- g.cancelAction(p)
- sess.State = net.StateGame
- g.writePrompt(sess)
- return
+ if chosen.Action != nil {
+ if g.handleNodeAction(sess, chosen.Action) {
+ return
+ }
}
if chosen.Goto != "" {
diff --git a/internal/game/cmd_shop.go b/internal/game/cmd_shop.go
index 51d0ab1..d1b0e9c 100644
--- a/internal/game/cmd_shop.go
+++ b/internal/game/cmd_shop.go
@@ -401,10 +401,8 @@ func (g *Game) leaveShop(sess *net.Session) {
g.writePrompt(sess)
return
}
- nodeKey := td.Node
- cfg := td.Cfg
- node, ok := cfg.Nodes[nodeKey]
+ node, ok := td.Cfg.Nodes[td.Node]
if !ok {
sess.State = net.StateGame
g.cancelAction(p)
@@ -412,20 +410,30 @@ func (g *Game) leaveShop(sess *net.Session) {
return
}
- sess.State = net.StateTalk
- visible := 0
- for _, opt := range node.Options {
- if opt.Condition != nil && !g.checkCondition(sess, opt.Condition) {
- continue
+ for {
+ visible := g.visibleOptions(sess, node.Options)
+ if len(visible) > 0 {
+ for i, opt := range visible {
+ sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, opt.Text))
+ }
+ sess.State = net.StateTalk
+ sess.Write("\nChoice: ")
+ return
+ }
+
+ if node.Next != "" {
+ next, ok := td.Cfg.Nodes[node.Next]
+ if !ok {
+ break
+ }
+ td.Node = node.Next
+ node = next
+ } else {
+ break
}
- 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: ")
+
+ sess.State = net.StateGame
+ g.cancelAction(p)
+ g.writePrompt(sess)
}
diff --git a/internal/game/look_entities.go b/internal/game/look_entities.go
index f9412d4..915317f 100644
--- a/internal/game/look_entities.go
+++ b/internal/game/look_entities.go
@@ -59,9 +59,14 @@ func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Roo
if m.Protected {
mobColor = "protected_mob"
}
- mobLevel := mobCombatLevel(m)
- levelStr := g.levelColorize(sess, playerLevel, mobLevel, fmt.Sprintf("(level %d)", mobLevel))
- lines = append(lines, fmt.Sprintf("%s %s%s%s", g.colorize(sess, mobColor, displayName), levelStr, hp, desc))
+ var levelStr string
+ if m.Protected {
+ levelStr = ""
+ } else {
+ mobLevel := mobCombatLevel(m)
+ levelStr = " " + g.levelColorize(sess, playerLevel, mobLevel, fmt.Sprintf("(level %d)", mobLevel))
+ }
+ lines = append(lines, fmt.Sprintf("%s%s%s%s", g.colorize(sess, mobColor, displayName), levelStr, hp, desc))
}
return lines
}
diff --git a/internal/game/look_target.go b/internal/game/look_target.go
index 4819411..96dc174 100644
--- a/internal/game/look_target.go
+++ b/internal/game/look_target.go
@@ -57,33 +57,40 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
if best.Protected {
mobColor = "protected_mob"
}
- mobLevel := mobCombatLevel(best)
- levelStr := g.levelColorize(sess, p.CombatLevel(), mobLevel, fmt.Sprintf("(level %d)", mobLevel))
+ var levelStr string
+ if best.Protected {
+ levelStr = color.Render(g.colorMode(sess), color.Parse("223"), "(protected)")
+ } else {
+ mobLevel := mobCombatLevel(best)
+ levelStr = g.levelColorize(sess, p.CombatLevel(), mobLevel, fmt.Sprintf("(level %d)", mobLevel))
+ }
sess.WriteLines(
"",
fmt.Sprintf("%s %s", g.colorize(sess, mobColor, best.Name), levelStr),
)
- if best.IdleDescription != "" {
- sess.WriteLine(best.IdleDescription)
+ if best.Description != "" {
+ sess.WriteLine(color.ExpandTags(g.colorMode(sess), best.Description))
}
- sess.WriteLines(
- "",
- fmt.Sprintf("Attack: %d", best.Attack),
- fmt.Sprintf("Strength: %d", best.Strength),
- fmt.Sprintf("Defense: %d", best.Defense),
- fmt.Sprintf("HP: %d/%d", best.HP, best.MaxHP),
- )
- if best.StabDefense != 0 || best.SlashDefense != 0 || best.CrushDefense != 0 ||
- best.ScienceDefense != 0 || best.RangedDefense != 0 {
+ if !best.Protected {
sess.WriteLines(
"",
- "Defense bonuses:",
- fmt.Sprintf(" Stab: %+d Slash: %+d Crush: %+d", best.StabDefense, best.SlashDefense, best.CrushDefense),
- fmt.Sprintf(" Science: %+d Ranged: %+d", best.ScienceDefense, best.RangedDefense),
+ fmt.Sprintf("Attack: %d", best.Attack),
+ fmt.Sprintf("Strength: %d", best.Strength),
+ fmt.Sprintf("Defense: %d", best.Defense),
+ fmt.Sprintf("HP: %d/%d", best.HP, best.MaxHP),
)
- }
- if best.Weakness != "" {
- sess.WriteLine(fmt.Sprintf("Weakness: %s", best.Weakness))
+ if best.StabDefense != 0 || best.SlashDefense != 0 || best.CrushDefense != 0 ||
+ best.ScienceDefense != 0 || best.RangedDefense != 0 {
+ sess.WriteLines(
+ "",
+ "Defense bonuses:",
+ fmt.Sprintf(" Stab: %+d Slash: %+d Crush: %+d", best.StabDefense, best.SlashDefense, best.CrushDefense),
+ fmt.Sprintf(" Science: %+d Ranged: %+d", best.ScienceDefense, best.RangedDefense),
+ )
+ }
+ if best.Weakness != "" {
+ sess.WriteLine(fmt.Sprintf("Weakness: %s", best.Weakness))
+ }
}
return
}
@@ -142,6 +149,10 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
sess.WriteLine(color.ExpandTags(g.colorMode(sess), objDescText))
}
+ if def.OnLook != nil {
+ g.applyNodeAction(sess, def.OnLook)
+ }
+
if def.Safespot != nil {
realSt := g.World.GetObjState(p.RoomID, st.DefID, st.Index)
if realSt != nil {
diff --git a/internal/object/object.go b/internal/object/object.go
index 8d1e02e..2dcc4bf 100644
--- a/internal/object/object.go
+++ b/internal/object/object.go
@@ -29,6 +29,7 @@ type ObjectDef struct {
Talk *behavior.TalkConfig `yaml:"talk,omitempty"`
Use *behavior.UseConfig `yaml:"use,omitempty"`
Safespot *SafespotConfig `yaml:"safespot,omitempty"`
+ OnLook *behavior.NodeAction `yaml:"on_look,omitempty"`
}
type SafespotConfig struct {
diff --git a/internal/validate/checks.go b/internal/validate/checks.go
index bb7cbd9..6aa0a81 100644
--- a/internal/validate/checks.go
+++ b/internal/validate/checks.go
@@ -152,6 +152,14 @@ func validateMobs(s Source) []Issue {
})
}
+ if !def.Protected && def.HP <= 0 {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("Mob %q: is not protected but has HP=%d — mobs must have hp > 0 (or set protected: true)", id, def.HP),
+ })
+ }
+
if def.Kind != "" && def.Kind != "combat" && def.Kind != "task" {
issues = append(issues, Issue{
Level: "ERROR",
@@ -322,6 +330,10 @@ func validateObjects(s Source) []Issue {
if obj.Use != nil {
issues = append(issues, validateUseConfig(fmt.Sprintf("Object %q: use", id), obj.Use, itemIDs)...)
}
+
+ if obj.OnLook != nil {
+ issues = append(issues, validateNodeAction(fmt.Sprintf("Object %q: on_look", id), obj.OnLook, itemIDs, roomIndex)...)
+ }
}
return issues
@@ -666,6 +678,21 @@ func validateTalkConfig(prefix string, cfg *behavior.TalkConfig, itemIDs map[str
if node.Action != nil {
issues = append(issues, validateNodeAction(nodePrefix, node.Action, itemIDs, roomIndex)...)
}
+ if node.Next != "" {
+ if _, ok := cfg.Nodes[node.Next]; !ok {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("%s: next points to nonexistent node %q", nodePrefix, node.Next),
+ })
+ }
+ }
+ for i, opt := range node.Options {
+ optPrefix := fmt.Sprintf("%s: option %d", nodePrefix, i+1)
+ if opt.Action != nil {
+ issues = append(issues, validateNodeAction(optPrefix, opt.Action, itemIDs, roomIndex)...)
+ }
+ }
}
return issues
}
diff --git a/internal/world/mob.go b/internal/world/mob.go
index 4c6becb..f11d6fe 100644
--- a/internal/world/mob.go
+++ b/internal/world/mob.go
@@ -78,6 +78,7 @@ type MobInstance struct {
InstanceID string
DefID string
Name string
+ Description string
HP int
MaxHP int
Attack int
@@ -133,12 +134,17 @@ func (m *MobInstance) IsTalkable() bool { return m.TalkConfig != nil }
func (m *MobInstance) IsTask() bool { return m.Kind == "task" }
func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []int, wanderInterval float64) *MobInstance {
+ hp := def.HP
+ if def.Protected && hp <= 0 {
+ hp = 1
+ }
return &MobInstance{
InstanceID: instanceID,
DefID: def.ID,
Name: def.Name,
- HP: def.HP,
- MaxHP: def.HP,
+ Description: def.Description,
+ HP: hp,
+ MaxHP: hp,
Attack: def.Attack,
Strength: def.Strength,
Defense: def.Defense,