diff options
Diffstat (limited to 'building_guide')
| -rw-r--r-- | building_guide/behaviors.md | 45 | ||||
| -rw-r--r-- | building_guide/mobs.md | 377 |
2 files changed, 266 insertions, 156 deletions
diff --git a/building_guide/behaviors.md b/building_guide/behaviors.md index c7b091d..ce266c1 100644 --- a/building_guide/behaviors.md +++ b/building_guide/behaviors.md @@ -185,7 +185,8 @@ name: Guard talk: nodes: start: - message: "\"Halt! This area is restricted.\" The guard eyes you suspiciously." + messages: + - "\"Halt! This area is restricted.\" The guard eyes you suspiciously." options: - text: "\"What's behind that gate?\"" goto: about_gate @@ -202,7 +203,8 @@ talk: # no goto = end conversation about_gate: - message: "\"Supplies. Weapons.\" He shifts his weight. \"Bring me some copper ore and I'll stamp you a pass.\"" + messages: + - "\"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 @@ -214,7 +216,8 @@ talk: has_item: copper_ore trade_ore: - message: "\"Good quality ore.\" He stamps a pass and hands it to you." + messages: + - "\"Good quality ore.\" He stamps a pass and hands it to you." action: take_item: copper_ore give_item: pass_stub @@ -224,7 +227,8 @@ talk: - text: "\"Thanks.\"" has_pass: - message: "\"Alright, I'll open the gate.\"" + messages: + - "\"Alright, I'll open the gate.\"" action: set_flags: gate_open: true @@ -236,9 +240,8 @@ talk: | Field | Type | Description | |---|---|---| -| `message` | string or list | NPC dialogue. May be a single string or a list of strings (pick one at random). | -| `sequence` | []string | NPC monologue — each message shown one at a time, player presses enter to advance. Options appear after the last message. Takes priority over `message` if both present. | -| `condition` | Condition | Optional. If the condition fails, the node is skipped entirely — its message, action, and options are not shown. Use with `goto` to auto-advance to a different node. | +| `messages` | []string | NPC dialogue, shown one at a time. The player presses enter to advance through each message. Options appear after the last message. A single-element list works like a simple one-shot line. | +| `condition` | Condition | Optional. If the condition fails, the node is skipped entirely — its messages, action, and options are not shown. Use with `goto` to auto-advance to a different node. | | `action` | NodeAction | Fires when the node is entered (give items, set flags, etc). | | `options` | []TalkOption | Player choices. If empty and `goto` is set, auto-advances. | | `goto` | string | Node ID to auto-advance to when there are no visible options OR when the node condition fails. | @@ -262,14 +265,12 @@ talk: | `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. | -| `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. | +| `credits` | Credits charged (negative) or awarded (positive). | | `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. | +> **Planned (not yet implemented):** `assign_task`, `skip_task`, `extend_task`, and `sawmill` are under design and will be added in a future update. + All fields in a single action are processed together — give an item, take an item, set flags, and heal in one node. @@ -294,11 +295,12 @@ simple transactions: ```yaml nodes: start: - message: "Welcome! Houses are 10 credits. Interested?" + messages: + - "Welcome! Houses are 10 credits. Interested?" options: - text: "Yes, I'll buy one." action: - cost: 10 + credits: -10 set_player_flags: owns_house: true goto: purchased @@ -307,7 +309,8 @@ nodes: not: true - text: "No thanks." purchased: - message: "Excellent! The plot is yours." + messages: + - "Excellent! The plot is yours." options: - text: "Thanks!" goto: start @@ -316,9 +319,9 @@ nodes: 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). -#### Sequences +#### Multi-message nodes -`sequence` is a flat list of NPC messages shown one at a time. The player presses +`messages` is a list of NPC lines shown one at a time. The player presses enter to advance through each message. After the last message, the node's options (if any) are displayed. This is the preferred way to deliver NPC monologues — no named nodes or `goto` chains needed: @@ -326,7 +329,7 @@ named nodes or `goto` chains needed: ```yaml nodes: sign_reminder: - sequence: + messages: - "\"Yeah, just us two! Not a lot of people heading into the belt these days.\"" - "\"But we'll be landing shortly! Please look at the information sign.\"" options: @@ -335,11 +338,11 @@ nodes: ``` Each `[enter to continue]` prompt is automatic. The node's action fires once on -entry, before the first sequence message. Node conditions still work normally — +entry, before the first message. Node conditions still work normally — if the condition fails, the entire sequence is skipped via `goto`. -If a sequence node has no options after the last message and `goto` is set, it -auto-advances to the target node. Non-empty input during a sequence cancels the +If a multi-message node has no options after the last message and `goto` is set, it +auto-advances to the target node. Non-empty input during messages cancels the conversation (just like invalid input does during option selection). #### Randomized messages diff --git a/building_guide/mobs.md b/building_guide/mobs.md index 745fb49..31ff109 100644 --- a/building_guide/mobs.md +++ b/building_guide/mobs.md @@ -4,91 +4,143 @@ `goblin`), and the loader derives `MobDef.ID` from it. Do not put an `id:` field in the file — it is ignored. Rooms reference mobs by that filename. -Basic combat mob with per-type defense bonuses: +All combat and stat fields are nested under a `combat:` block. Protected mobs (NPCs, shopkeepers, +quest givers) do not have a `combat:` block at all — they cannot be attacked. + +Basic combat mob: ```yaml name: "man" description: "A shabby-looking man." -attack: 1 -strength: 1 -defense: 1 -hp: 7 -speed: 5 -aggressive: false -respawn_ticks: 30 -attack_type: crush # stab/slash/crush/ranged/science (default: crush) -stab_defense: 0 # per-type defense bonuses -slash_defense: 0 -crush_defense: 0 -science_defense: 0 -ranged_defense: 0 +idle_descriptions: + - "scribbles something in a small notebook" + - "gazes skyward at the clouds" drops: - remains: "bones" # always dropped on death + remains: "bones" loot: - item_id: "credits" weight: 98 quantity: 10 - - item_id: "credits" - weight: 2 - quantity: 150 -idle_descriptions: - - "scribbles something in a small notebook" - - "gazes skyward at the clouds" -combat_descriptions: - - "is engaged in a fight to the death with %s" +combat: + kind: combat + stats: + hp: 7 + attack: 1 + strength: 1 + defense: 1 + attack_type: crush # stab/slash/crush/ranged/science (default: crush) + speed: 5 + aggressive: false + respawn_ticks: 30 + bonuses: + attack_bonus: 0 + strength_bonus: 0 + science_bonus: 0 + science_percent_bonus: 0 + ranged_bonus: 0 + ranged_strength_bonus: 0 + defenses: + stab_defense: 0 # per-type defense bonuses + slash_defense: 0 + crush_defense: 0 + science_defense: 0 + ranged_defense: 0 + size: small + combat_descriptions: + - "is engaged in a fight to the death with %s" ``` ### Aggressive Mobs -Mobs with `aggressive: true` auto-attack players entering their room (1-tick delay). Only aggros if the player's combat level is at most double the mob's combat level. Checked on room enter and login. +Mobs with `combat.stats.aggressive: true` auto-attack players entering their room (1-tick delay). Only aggros if the player's combat level is at most double the mob's combat level. Checked on room enter and login. ```yaml name: goblin -aggressive: true # attacks players on sight -attack: 1 -strength: 3 -defense: 3 -hp: 12 -attack_type: slash -attack_bonus: 3 # equipment-equivalent attack bonus for mob's attack roll -strength_bonus: 2 # equipment-equivalent strength bonus for mob's max hit -stab_defense: 4 -slash_defense: 3 -crush_defense: -2 # weak to crush — negative defense is valid -science_defense: 0 -ranged_defense: 3 +combat: + kind: combat + stats: + aggressive: true # attacks players on sight + hp: 12 + attack: 1 + strength: 3 + defense: 3 + attack_type: slash + speed: 5 + respawn_ticks: 25 + bonuses: + attack_bonus: 3 # equipment-equivalent attack bonus for mob's attack roll + strength_bonus: 2 # equipment-equivalent strength bonus for mob's max hit + science_bonus: 0 + science_percent_bonus: 0 + ranged_bonus: 0 + ranged_strength_bonus: 0 + defenses: + stab_defense: 4 + slash_defense: 3 + crush_defense: -2 # weak to crush — negative defense is valid + science_defense: 0 + ranged_defense: 3 + size: small ``` -### Mob Attack/Defense Fields +### Combat Stats Fields + +All combat stats live under `combat.stats:` | Field | Description | |---|---| +| `hp` | Hit points | | `attack` | Attack level (base accuracy) | | `strength` | Strength level (base max hit) | | `defense` | Defense level (base defense) | -| `hp` | Hit points | | `ranged` | Ranged level (for ranged/science mobs) | | `science` | Science level | +| `attack_type` | Determines which player defense is used against this mob (stab/slash/crush/ranged/science) | +| `speed` | Attack speed in ticks (default 5) | +| `aggressive` | Auto-attacks players on sight (false if omitted) | +| `respawn_ticks` | Ticks until respawn after death | + +#### Bonuses (`combat.stats.bonuses:`) + +| Field | Description | +|---|---| | `attack_bonus` | Equipment-equivalent attack bonus (added to attack roll) | | `strength_bonus` | Equipment-equivalent strength bonus (added to max hit) | -| `attack_type` | Determines which player defense is used against this mob | -| `stab_defense` through `ranged_defense` | Per-type defense bonuses | -| `weakness` | Elemental weakness (solar/hydro/eco/bio) — for future Science combat | +| `science_bonus` | Equipment-equivalent science attack bonus | +| `science_percent_bonus` | Percent boost to science damage | +| `ranged_bonus` | Equipment-equivalent ranged attack bonus | +| `ranged_strength_bonus` | Equipment-equivalent ranged strength bonus | + +#### Defenses (`combat.stats.defenses:`) + +| Field | Description | +|---|---| +| `stab_defense` | Stab defense bonus | +| `slash_defense` | Slash defense bonus | +| `crush_defense` | Crush defense bonus | +| `science_defense` | Science defense bonus | +| `ranged_defense` | Ranged defense bonus | +| `weakness` | Elemental weakness (solar/hydro/eco/bio/chaos) — for future Science combat | + +#### Combat-level fields (under `combat:`) + +| Field | Description | +|---|---| +| `kind` | `combat` (default) or `task` — task mobs are worksites | | `size` | Mob size for safespot blocking: small/medium/large/massive (default: medium) | +| `assassin_level` | Required assassin skill level to attack this mob | +| `finishing_blow` | Item ID required to kill (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 lacks it equipped | +| `combat_descriptions` | List of strings shown when the mob is in combat. Use `%s` for the opponent's name | ### Protected/Unique Mobs -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. +Protected mobs cannot be attacked. They have **no `combat:` block** at all. They 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 +protected: true # cannot be attacked, no combat section idle_descriptions: - adjusts her name badge - smiles politely at passengers @@ -98,18 +150,20 @@ talk: # inline talk config condition: # only show this node when flag is NOT set player_flag: 1001_look_sign not: true - message: "\"How can I help you?\"" + messages: + - "\"How can I help you?\"" goto: thanks # auto-advance here if condition fails options: - text: "\"Oh, nothing...\"" goto: sign_reminder - text: "\"Goodbye.\"" thanks: - message: "\"Thank you for flying with us. Be careful on the stairs.\"" + messages: + - "\"Thank you for flying with us. Be careful on the stairs.\"" options: - text: "\"Thanks!\"" sign_reminder: - sequence: + messages: - "\"Yeah, just us two! Not a lot of people heading into the belt these days.\"" - "\"But we'll be landing shortly! Look at the information sign so you'll know what to do.\"" options: @@ -119,16 +173,16 @@ talk: # inline talk config When the player has `1001_look_sign` set, the `start` node's condition fails, so it skips directly to `thanks` ("Thank you for flying..."). When the flag is unset, the player sees "How can I help you?" and can choose to be reminded about the sign -or say goodbye. The `sign_reminder` node uses a `sequence` so the flight attendant -delivers two lines, pausing for the player to press enter between them, before -showing the "Okay" option. +or say goodbye. The `sign_reminder` node uses a multi-message `messages` list so the +flight attendant delivers two lines, pausing for the player to press enter between +them, before showing the "Okay" option. See the [Talk section of behaviors](behaviors.md#talk-dialog-trees) for the full -dialog tree format, including sequences and conditional nodes. +dialog tree format, including multi-message nodes and conditional nodes. ### Shops -Give a mob a root-level `shop:` block to make it a shopkeeper. Players trade with +Give a protected mob a root-level `shop:` block to make it a shopkeeper. Players trade with it using the ordinary `list`, `buy`, and `sell` commands from the game prompt. Prices derive from each item's `value:` field. See the [Shops section of behaviors](behaviors.md#shops) for the full format, pricing @@ -137,7 +191,6 @@ formula, and stock/restock rules. ```yaml name: Shopkeeper protected: true -hp: 50 shop: items: - item_id: fishing_rod @@ -146,57 +199,55 @@ shop: stock: 10 ``` -Protected mob with combat stats (for internal testing or debug purposes): -```yaml -name: "Guard" -unique: true -protected: true -talk: - nodes: - start: - message: "\"Halt! Who goes there?\"" - options: - - text: "\"Just passing through.\"" -attack: 5 -strength: 5 -defense: 5 -hp: 30 -speed: 5 -aggressive: false -respawn_ticks: 60 -attack_type: slash -attack_bonus: 8 -strength_bonus: 6 -stab_defense: 12 -slash_defense: 15 -crush_defense: 10 -science_defense: -5 -ranged_defense: 12 -idle_descriptions: - - "scans the area with a watchful eye" -``` - 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. +Stealable mobs have a root-level `steal:` block. On failed steal, the mob turns hostile and attacks. ```yaml 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 +steal: + table: farmer_steal # Drop table for steal loot + level: 10 # Required thieving level + xp: 15 # XP per successful steal + speed: 4 # Ticks per steal attempt +combat: + kind: combat + stats: + hp: 15 + attack: 3 + strength: 3 + defense: 3 + attack_type: crush + speed: 5 + aggressive: false + respawn_ticks: 40 + bonuses: + attack_bonus: 0 + strength_bonus: 0 + science_bonus: 0 + science_percent_bonus: 0 + ranged_bonus: 0 + ranged_strength_bonus: 0 + defenses: + stab_defense: 0 + slash_defense: 0 + crush_defense: 0 + science_defense: 0 + ranged_defense: 0 + size: small + combat_descriptions: + - "swings a shovel at %s" ``` | 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) | +| `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 @@ -204,13 +255,35 @@ Assassin (Slayer) mobs restrict combat by assassin level and introduce finishing ```yaml 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 +combat: + kind: combat + stats: + hp: 15 + attack: 3 + strength: 3 + defense: 1 + attack_type: crush + speed: 6 + aggressive: false + respawn_ticks: 25 + bonuses: + attack_bonus: 0 + strength_bonus: 0 + science_bonus: 0 + science_percent_bonus: 0 + ranged_bonus: 0 + ranged_strength_bonus: 0 + defenses: + stab_defense: 0 + slash_defense: 0 + crush_defense: 0 + science_defense: 0 + ranged_defense: 0 + assassin_level: 1 # required assassin level to attack + finishing_blow: salt # item required to kill (mob stays at 1 HP otherwise) + size: small + combat_descriptions: + - "lunges slimily at %s" drops: remains: slug_mucus loot: @@ -222,47 +295,83 @@ drops: Damage-without mobs deal 1.5x damage unless the player has the specified item equipped: ```yaml 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 +combat: + kind: combat + stats: + hp: 45 + attack: 15 + strength: 14 + defense: 12 + attack_type: crush + speed: 4 + aggressive: true + respawn_ticks: 35 + bonuses: + attack_bonus: 0 + strength_bonus: 0 + science_bonus: 0 + science_percent_bonus: 0 + ranged_bonus: 0 + ranged_strength_bonus: 0 + defenses: + stab_defense: 0 + slash_defense: 0 + crush_defense: 0 + science_defense: 0 + ranged_defense: 0 + assassin_level: 15 + damage_without: insulated_gloves # 1.5x damage without this item equipped + size: small ``` | 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 | +| `combat.assassin_level` | Required assassin skill level to attack this mob | +| `combat.finishing_blow` | Item ID required to kill this mob (mob stays at 1 HP until used via `use <item> on <target>`) | +| `combat.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). ### Task Mobs (Worksites) -A task mob is a non-violent "worksite" — build a solar panel, survey flora, prospect a rock — that reuses the entire combat engine. Set `kind: task`. Mechanically its `hp` is a hidden work pool that drains to 0 to **complete** the work; the player sees a progress bar filling toward 100%. A task mob **never attacks back** — the danger (if any) comes from a room `hazard:` (see `hazards.md`). Use the `work` command (or `attack`) on it. +A task mob is a non-violent "worksite" — build a solar panel, survey flora, prospect a rock — that reuses the entire combat engine. Set `combat.kind: task`. Mechanically `combat.stats.hp` is the work pool that drains to 0 to **complete** the work; the player sees a progress bar filling toward 100%. A task mob **never attacks back** — the danger (if any) comes from a room `hazard:` (see `hazards.md`). Use the `work` command (or `attack`) on it. + +Task-specific fields live in the root-level `task:` block: ```yaml name: solar panel frame -kind: task # "" / "combat" (default) or "task" -verb: assemble # flavor verb shown in messages (default: "work on") -progress_noun: assembly # flavor noun in the progress line (default: "work") -complete_message: "You bolt the final panel into place and the array hums to life!" -hp: 40 # units of work to complete (NOT a health bar) -defense: 10 # base difficulty -speed: 5 -aggressive: false # task mobs do not fight; this is ignored -respawn_ticks: 40 # the worksite replenishes after this many ticks -attack_type: crush -# Per-type defenses become METHOD EFFICIENCY: low = that approach works well. -crush_defense: 0 # hammering panels in: efficient -stab_defense: 40 # stabbing a frame: useless -slash_defense: 40 -ranged_defense: 10 # a rivet gun / scanner (ranged weapon): works -science_defense: 10 # an analysis deck (science weapon): works -drops: # the "payment" for the labour — same drop system as combat +description: "A half-assembled photovoltaic array." +task: + verb: assemble # flavor verb shown in messages (default: "work on") + progress_noun: assembly # flavor noun in the progress line (default: "work") + complete_message: "You bolt the final panel into place and the solar array hums to life!" +idle_descriptions: + - "stands half-assembled, panels waiting to be fitted" +combat: + kind: task # "task" triggers the work-engine mode + stats: + hp: 40 # units of work to complete (NOT a health bar) + defense: 10 # base difficulty + attack_type: crush + speed: 5 + aggressive: false # task mobs do not fight; this is ignored + respawn_ticks: 40 # the worksite replenishes after this many ticks + bonuses: + attack_bonus: 0 + strength_bonus: 0 + science_bonus: 0 + science_percent_bonus: 0 + ranged_bonus: 0 + ranged_strength_bonus: 0 + # Per-type defenses become METHOD EFFICIENCY: low = that approach works well. + defenses: + stab_defense: 40 # stabbing a frame: useless + slash_defense: 40 + crush_defense: 0 # hammering panels in: efficient + science_defense: 10 # an analysis deck (science weapon): works + ranged_defense: 10 # a rivet gun / scanner (ranged weapon): works + size: medium +drops: # the "payment" for the labour — same drop system as combat loot: - item_id: credits weight: 95 @@ -281,5 +390,3 @@ Key points: - **One worker at a time**, the same 1-v-1 lock as combat. --- - - |
