From 9d4b799db4868bcab291b83599ff088763cd4ed6 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 24 Jun 2026 20:58:25 -0400 Subject: feat: new type of non-violent 'combat': work --- AGENTS.md | 37 ++- IDEAS.md | 10 - README.md | 24 +- building_guide/bank.md | 68 ++++ building_guide/behaviors.md | 402 ++++++++++++++++++++++++ building_guide/conditions.md | 80 +++++ building_guide/construction.md | 223 +++++++++++++ building_guide/courses.md | 119 +++++++ building_guide/doors.md | 97 ++++++ building_guide/drops.md | 52 +++ building_guide/hacking.md | 49 +++ building_guide/hazards.md | 75 +++++ building_guide/hidden_objects.md | 92 ++++++ building_guide/items.md | 190 +++++++++++ building_guide/mobs.md | 221 +++++++++++++ building_guide/objects.md | 392 +++++++++++++++++++++++ building_guide/quests.md | 103 ++++++ building_guide/recipes.md | 277 ++++++++++++++++ building_guide/rooms.md | 209 ++++++++++++ building_guide/search.md | 15 + building_guide/state.md | 8 + building_guide/tips.md | 21 ++ building_guide/wandering_mobs.md | 20 ++ building_guide/wandering_objects.md | 39 +++ cmd/mud/main.go | 2 + config.yaml | 3 + data/courses/reactor.yaml | 16 +- data/courses/rooftop.yaml | 14 +- data/courses/vent_shaft.yaml | 12 +- data/hazards/solar_radiation.yaml | 12 + data/help/combat.yaml | 2 +- data/help/drink.yaml | 2 +- data/help/prompt.yaml | 2 +- data/help/style.yaml | 2 +- data/items/consumables/stim_potion.yaml | 2 +- data/items/consumables/super_stim.yaml | 2 +- data/items/equipment/adamant_dagger.yaml | 2 +- data/items/equipment/adamant_sword.yaml | 2 +- data/items/equipment/black_axe.yaml | 2 +- data/items/equipment/dragon_axe.yaml | 2 +- data/items/equipment/mithril_dagger.yaml | 2 +- data/items/equipment/mithril_sword.yaml | 2 +- data/items/equipment/rune_dagger.yaml | 2 +- data/items/equipment/rune_sword.yaml | 2 +- data/items/equipment/steel_dagger.yaml | 2 +- data/items/equipment/steel_sword.yaml | 2 +- data/items/tools/adamant_axe.yaml | 2 +- data/items/tools/adamant_pickaxe.yaml | 2 +- data/items/tools/black_pickaxe.yaml | 2 +- data/items/tools/dragon_pickaxe.yaml | 2 +- data/items/tools/mithril_axe.yaml | 2 +- data/items/tools/mithril_pickaxe.yaml | 2 +- data/items/tools/rune_axe.yaml | 2 +- data/items/tools/rune_pickaxe.yaml | 2 +- data/items/tools/steel_axe.yaml | 2 +- data/items/tools/steel_pickaxe.yaml | 2 +- data/mobs/solar_panel_frame.yaml | 34 ++ data/objects/1002_crowd.yaml | 18 ++ data/objects/1002_pilot.yaml | 10 + data/objects/1002_shuttle.yaml | 9 + data/rooms/agility/200.yaml | 8 - data/rooms/agility/201.yaml | 9 - data/rooms/agility/202.yaml | 9 - data/rooms/agility/203.yaml | 9 - data/rooms/agility/204.yaml | 9 - data/rooms/agility/205.yaml | 9 - data/rooms/agility/210.yaml | 9 - data/rooms/agility/211.yaml | 9 - data/rooms/agility/212.yaml | 9 - data/rooms/agility/213.yaml | 9 - data/rooms/agility/214.yaml | 9 - data/rooms/agility/215.yaml | 9 - data/rooms/agility/220.yaml | 9 - data/rooms/agility/221.yaml | 9 - data/rooms/agility/222.yaml | 9 - data/rooms/agility/223.yaml | 9 - data/rooms/agility/224.yaml | 9 - data/rooms/agility/225.yaml | 9 - data/rooms/agility/226.yaml | 9 - data/rooms/example/agility/99999200.yaml | 8 + data/rooms/example/agility/99999201.yaml | 9 + data/rooms/example/agility/99999202.yaml | 9 + data/rooms/example/agility/99999203.yaml | 9 + data/rooms/example/agility/99999204.yaml | 9 + data/rooms/example/agility/99999205.yaml | 9 + data/rooms/example/agility/99999210.yaml | 9 + data/rooms/example/agility/99999211.yaml | 9 + data/rooms/example/agility/99999212.yaml | 9 + data/rooms/example/agility/99999213.yaml | 9 + data/rooms/example/agility/99999214.yaml | 9 + data/rooms/example/agility/99999215.yaml | 9 + data/rooms/example/agility/99999220.yaml | 9 + data/rooms/example/agility/99999221.yaml | 9 + data/rooms/example/agility/99999222.yaml | 9 + data/rooms/example/agility/99999223.yaml | 9 + data/rooms/example/agility/99999224.yaml | 9 + data/rooms/example/agility/99999225.yaml | 9 + data/rooms/example/agility/99999226.yaml | 9 + data/rooms/example/player_housing/99999300.yaml | 8 + data/rooms/example/player_housing/99999301.yaml | 8 + data/rooms/example/town/999991.yaml | 80 +++++ data/rooms/example/town/9999910.yaml | 10 + data/rooms/example/town/9999911.yaml | 17 + data/rooms/example/town/9999912.yaml | 28 ++ data/rooms/example/town/9999913.yaml | 46 +++ data/rooms/example/town/9999914.yaml | 18 ++ data/rooms/example/town/9999915.yaml | 10 + data/rooms/example/town/9999916.yaml | 12 + data/rooms/example/town/9999917.yaml | 10 + data/rooms/example/town/9999918.yaml | 6 + data/rooms/example/town/9999919.yaml | 6 + data/rooms/example/town/999992.yaml | 16 + data/rooms/example/town/9999920.yaml | 6 + data/rooms/example/town/9999921.yaml | 12 + data/rooms/example/town/9999922.yaml | 6 + data/rooms/example/town/9999923.yaml | 6 + data/rooms/example/town/9999924.yaml | 6 + data/rooms/example/town/9999925.yaml | 6 + data/rooms/example/town/9999926.yaml | 6 + data/rooms/example/town/9999927.yaml | 6 + data/rooms/example/town/9999928.yaml | 5 + data/rooms/example/town/999993.yaml | 14 + data/rooms/example/town/9999930.yaml | 5 + data/rooms/example/town/9999931.yaml | 6 + data/rooms/example/town/999994.yaml | 36 +++ data/rooms/example/town/999995.yaml | 11 + data/rooms/example/town/999996.yaml | 15 + data/rooms/example/town/999997.yaml | 9 + data/rooms/example/town/999998.yaml | 11 + data/rooms/example/town/999999.yaml | 13 + data/rooms/example/underground/9999950.yaml | 7 + data/rooms/example/underground/9999951.yaml | 10 + data/rooms/example/underground/9999952.yaml | 9 + data/rooms/example/underground/9999953.yaml | 9 + data/rooms/example/underground/9999954.yaml | 9 + data/rooms/example/underground/9999955.yaml | 12 + data/rooms/example/underground/9999956.yaml | 9 + data/rooms/example/underground/9999957.yaml | 8 + data/rooms/example/underground/9999958.yaml | 8 + data/rooms/example/underground/9999959.yaml | 9 + data/rooms/example/underground/9999960.yaml | 8 + data/rooms/example/underground/9999961.yaml | 7 + data/rooms/example/underground/9999962.yaml | 8 + data/rooms/example/underground/9999963.yaml | 8 + data/rooms/example/underground/9999964.yaml | 8 + data/rooms/example/underground/9999965.yaml | 8 + data/rooms/example/underground/9999966.yaml | 8 + data/rooms/example/underground/9999967.yaml | 8 + data/rooms/example/underground/9999968.yaml | 7 + data/rooms/example/wilderness/99999150.yaml | 31 ++ data/rooms/example/wilderness/99999151.yaml | 10 + data/rooms/example/wilderness/99999152.yaml | 10 + data/rooms/example/wilderness/99999160.yaml | 16 + data/rooms/example/wilderness/99999161.yaml | 12 + data/rooms/example/wilderness/99999162.yaml | 5 + data/rooms/example/wilderness/99999170.yaml | 7 + data/rooms/example/wilderness/99999171.yaml | 7 + data/rooms/example/wilderness/99999180.yaml | 5 + data/rooms/example/wilderness/99999181.yaml | 11 + data/rooms/intro/1001.yaml | 5 + data/rooms/intro/1002.yaml | 43 +++ data/rooms/intro/1003.yaml | 5 + data/rooms/player_housing/300.yaml | 8 - data/rooms/player_housing/301.yaml | 8 - data/rooms/town/1.yaml | 80 ----- data/rooms/town/10.yaml | 10 - data/rooms/town/11.yaml | 17 - data/rooms/town/12.yaml | 28 -- data/rooms/town/13.yaml | 46 --- data/rooms/town/14.yaml | 18 -- data/rooms/town/15.yaml | 10 - data/rooms/town/16.yaml | 12 - data/rooms/town/17.yaml | 10 - data/rooms/town/18.yaml | 6 - data/rooms/town/19.yaml | 6 - data/rooms/town/2.yaml | 16 - data/rooms/town/20.yaml | 6 - data/rooms/town/21.yaml | 12 - data/rooms/town/22.yaml | 6 - data/rooms/town/23.yaml | 6 - data/rooms/town/24.yaml | 6 - data/rooms/town/25.yaml | 6 - data/rooms/town/26.yaml | 6 - data/rooms/town/27.yaml | 6 - data/rooms/town/28.yaml | 5 - data/rooms/town/3.yaml | 14 - data/rooms/town/30.yaml | 5 - data/rooms/town/31.yaml | 6 - data/rooms/town/4.yaml | 36 --- data/rooms/town/5.yaml | 11 - data/rooms/town/6.yaml | 15 - data/rooms/town/7.yaml | 9 - data/rooms/town/8.yaml | 11 - data/rooms/town/9.yaml | 13 - data/rooms/underground/50.yaml | 7 - data/rooms/underground/51.yaml | 10 - data/rooms/underground/52.yaml | 9 - data/rooms/underground/53.yaml | 9 - data/rooms/underground/54.yaml | 9 - data/rooms/underground/55.yaml | 12 - data/rooms/underground/56.yaml | 9 - data/rooms/underground/57.yaml | 8 - data/rooms/underground/58.yaml | 8 - data/rooms/underground/59.yaml | 9 - data/rooms/underground/60.yaml | 8 - data/rooms/underground/61.yaml | 7 - data/rooms/underground/62.yaml | 8 - data/rooms/underground/63.yaml | 8 - data/rooms/underground/64.yaml | 8 - data/rooms/underground/65.yaml | 8 - data/rooms/underground/66.yaml | 8 - data/rooms/underground/67.yaml | 8 - data/rooms/underground/68.yaml | 7 - data/rooms/wilderness/150.yaml | 31 -- data/rooms/wilderness/151.yaml | 10 - data/rooms/wilderness/152.yaml | 10 - data/rooms/wilderness/160.yaml | 16 - data/rooms/wilderness/161.yaml | 12 - data/rooms/wilderness/162.yaml | 5 - data/rooms/wilderness/170.yaml | 7 - data/rooms/wilderness/171.yaml | 7 - internal/game/action.go | 57 +++- internal/game/action_room.go | 179 ++++++++++- internal/game/action_state.go | 3 + internal/game/cmd_attack.go | 275 ++++++++++------ internal/game/cmd_consume.go | 2 +- internal/game/cmd_look.go | 92 +++++- internal/game/cmd_move.go | 67 +++- internal/game/cmd_registry.go | 1 + internal/game/cmd_score.go | 4 +- internal/game/cmd_stats.go | 2 +- internal/game/condition_test.go | 155 +++++++++ internal/game/core_login_char.go | 21 +- internal/game/core_startup.go | 53 ++++ internal/game/game.go | 6 + internal/game/map_test.go | 36 +-- internal/game/sys_hazard.go | 136 ++++++++ internal/game/sys_labor.go | 81 +++++ internal/game/sys_safespot.go | 7 + internal/game/sys_technology.go | 30 +- internal/net/server.go | 1 + internal/object/object.go | 11 + internal/player/player.go | 16 +- internal/world/hazard.go | 82 +++++ internal/world/mob.go | 23 ++ internal/world/objects.go | 21 +- internal/world/room.go | 24 +- internal/world/world.go | 5 + worldbuilding_guide/bank.md | 68 ---- worldbuilding_guide/behaviors.md | 402 ------------------------ worldbuilding_guide/conditions.md | 70 ----- worldbuilding_guide/construction.md | 223 ------------- worldbuilding_guide/courses.md | 119 ------- worldbuilding_guide/doors.md | 97 ------ worldbuilding_guide/drops.md | 52 --- worldbuilding_guide/hacking.md | 49 --- worldbuilding_guide/hidden_objects.md | 40 --- worldbuilding_guide/index.md | 17 - worldbuilding_guide/items.md | 190 ----------- worldbuilding_guide/mobs.md | 179 ----------- worldbuilding_guide/objects.md | 392 ----------------------- worldbuilding_guide/quests.md | 103 ------ worldbuilding_guide/recipes.md | 277 ---------------- worldbuilding_guide/rooms.md | 134 -------- worldbuilding_guide/search.md | 15 - worldbuilding_guide/state.md | 8 - worldbuilding_guide/tips.md | 21 -- worldbuilding_guide/wandering_mobs.md | 20 -- worldbuilding_guide/wandering_objects.md | 39 --- 269 files changed, 5093 insertions(+), 3650 deletions(-) delete mode 100644 IDEAS.md create mode 100644 building_guide/bank.md create mode 100644 building_guide/behaviors.md create mode 100644 building_guide/conditions.md create mode 100644 building_guide/construction.md create mode 100644 building_guide/courses.md create mode 100644 building_guide/doors.md create mode 100644 building_guide/drops.md create mode 100644 building_guide/hacking.md create mode 100644 building_guide/hazards.md create mode 100644 building_guide/hidden_objects.md create mode 100644 building_guide/items.md create mode 100644 building_guide/mobs.md create mode 100644 building_guide/objects.md create mode 100644 building_guide/quests.md create mode 100644 building_guide/recipes.md create mode 100644 building_guide/rooms.md create mode 100644 building_guide/search.md create mode 100644 building_guide/state.md create mode 100644 building_guide/tips.md create mode 100644 building_guide/wandering_mobs.md create mode 100644 building_guide/wandering_objects.md create mode 100644 data/hazards/solar_radiation.yaml create mode 100644 data/mobs/solar_panel_frame.yaml create mode 100644 data/objects/1002_crowd.yaml create mode 100644 data/objects/1002_pilot.yaml create mode 100644 data/objects/1002_shuttle.yaml delete mode 100644 data/rooms/agility/200.yaml delete mode 100644 data/rooms/agility/201.yaml delete mode 100644 data/rooms/agility/202.yaml delete mode 100644 data/rooms/agility/203.yaml delete mode 100644 data/rooms/agility/204.yaml delete mode 100644 data/rooms/agility/205.yaml delete mode 100644 data/rooms/agility/210.yaml delete mode 100644 data/rooms/agility/211.yaml delete mode 100644 data/rooms/agility/212.yaml delete mode 100644 data/rooms/agility/213.yaml delete mode 100644 data/rooms/agility/214.yaml delete mode 100644 data/rooms/agility/215.yaml delete mode 100644 data/rooms/agility/220.yaml delete mode 100644 data/rooms/agility/221.yaml delete mode 100644 data/rooms/agility/222.yaml delete mode 100644 data/rooms/agility/223.yaml delete mode 100644 data/rooms/agility/224.yaml delete mode 100644 data/rooms/agility/225.yaml delete mode 100644 data/rooms/agility/226.yaml create mode 100644 data/rooms/example/agility/99999200.yaml create mode 100644 data/rooms/example/agility/99999201.yaml create mode 100644 data/rooms/example/agility/99999202.yaml create mode 100644 data/rooms/example/agility/99999203.yaml create mode 100644 data/rooms/example/agility/99999204.yaml create mode 100644 data/rooms/example/agility/99999205.yaml create mode 100644 data/rooms/example/agility/99999210.yaml create mode 100644 data/rooms/example/agility/99999211.yaml create mode 100644 data/rooms/example/agility/99999212.yaml create mode 100644 data/rooms/example/agility/99999213.yaml create mode 100644 data/rooms/example/agility/99999214.yaml create mode 100644 data/rooms/example/agility/99999215.yaml create mode 100644 data/rooms/example/agility/99999220.yaml create mode 100644 data/rooms/example/agility/99999221.yaml create mode 100644 data/rooms/example/agility/99999222.yaml create mode 100644 data/rooms/example/agility/99999223.yaml create mode 100644 data/rooms/example/agility/99999224.yaml create mode 100644 data/rooms/example/agility/99999225.yaml create mode 100644 data/rooms/example/agility/99999226.yaml create mode 100644 data/rooms/example/player_housing/99999300.yaml create mode 100644 data/rooms/example/player_housing/99999301.yaml create mode 100644 data/rooms/example/town/999991.yaml create mode 100644 data/rooms/example/town/9999910.yaml create mode 100644 data/rooms/example/town/9999911.yaml create mode 100644 data/rooms/example/town/9999912.yaml create mode 100644 data/rooms/example/town/9999913.yaml create mode 100644 data/rooms/example/town/9999914.yaml create mode 100644 data/rooms/example/town/9999915.yaml create mode 100644 data/rooms/example/town/9999916.yaml create mode 100644 data/rooms/example/town/9999917.yaml create mode 100644 data/rooms/example/town/9999918.yaml create mode 100644 data/rooms/example/town/9999919.yaml create mode 100644 data/rooms/example/town/999992.yaml create mode 100644 data/rooms/example/town/9999920.yaml create mode 100644 data/rooms/example/town/9999921.yaml create mode 100644 data/rooms/example/town/9999922.yaml create mode 100644 data/rooms/example/town/9999923.yaml create mode 100644 data/rooms/example/town/9999924.yaml create mode 100644 data/rooms/example/town/9999925.yaml create mode 100644 data/rooms/example/town/9999926.yaml create mode 100644 data/rooms/example/town/9999927.yaml create mode 100644 data/rooms/example/town/9999928.yaml create mode 100644 data/rooms/example/town/999993.yaml create mode 100644 data/rooms/example/town/9999930.yaml create mode 100644 data/rooms/example/town/9999931.yaml create mode 100644 data/rooms/example/town/999994.yaml create mode 100644 data/rooms/example/town/999995.yaml create mode 100644 data/rooms/example/town/999996.yaml create mode 100644 data/rooms/example/town/999997.yaml create mode 100644 data/rooms/example/town/999998.yaml create mode 100644 data/rooms/example/town/999999.yaml create mode 100644 data/rooms/example/underground/9999950.yaml create mode 100644 data/rooms/example/underground/9999951.yaml create mode 100644 data/rooms/example/underground/9999952.yaml create mode 100644 data/rooms/example/underground/9999953.yaml create mode 100644 data/rooms/example/underground/9999954.yaml create mode 100644 data/rooms/example/underground/9999955.yaml create mode 100644 data/rooms/example/underground/9999956.yaml create mode 100644 data/rooms/example/underground/9999957.yaml create mode 100644 data/rooms/example/underground/9999958.yaml create mode 100644 data/rooms/example/underground/9999959.yaml create mode 100644 data/rooms/example/underground/9999960.yaml create mode 100644 data/rooms/example/underground/9999961.yaml create mode 100644 data/rooms/example/underground/9999962.yaml create mode 100644 data/rooms/example/underground/9999963.yaml create mode 100644 data/rooms/example/underground/9999964.yaml create mode 100644 data/rooms/example/underground/9999965.yaml create mode 100644 data/rooms/example/underground/9999966.yaml create mode 100644 data/rooms/example/underground/9999967.yaml create mode 100644 data/rooms/example/underground/9999968.yaml create mode 100644 data/rooms/example/wilderness/99999150.yaml create mode 100644 data/rooms/example/wilderness/99999151.yaml create mode 100644 data/rooms/example/wilderness/99999152.yaml create mode 100644 data/rooms/example/wilderness/99999160.yaml create mode 100644 data/rooms/example/wilderness/99999161.yaml create mode 100644 data/rooms/example/wilderness/99999162.yaml create mode 100644 data/rooms/example/wilderness/99999170.yaml create mode 100644 data/rooms/example/wilderness/99999171.yaml create mode 100644 data/rooms/example/wilderness/99999180.yaml create mode 100644 data/rooms/example/wilderness/99999181.yaml create mode 100644 data/rooms/intro/1001.yaml create mode 100644 data/rooms/intro/1002.yaml create mode 100644 data/rooms/intro/1003.yaml delete mode 100644 data/rooms/player_housing/300.yaml delete mode 100644 data/rooms/player_housing/301.yaml delete mode 100644 data/rooms/town/1.yaml delete mode 100644 data/rooms/town/10.yaml delete mode 100644 data/rooms/town/11.yaml delete mode 100644 data/rooms/town/12.yaml delete mode 100644 data/rooms/town/13.yaml delete mode 100644 data/rooms/town/14.yaml delete mode 100644 data/rooms/town/15.yaml delete mode 100644 data/rooms/town/16.yaml delete mode 100644 data/rooms/town/17.yaml delete mode 100644 data/rooms/town/18.yaml delete mode 100644 data/rooms/town/19.yaml delete mode 100644 data/rooms/town/2.yaml delete mode 100644 data/rooms/town/20.yaml delete mode 100644 data/rooms/town/21.yaml delete mode 100644 data/rooms/town/22.yaml delete mode 100644 data/rooms/town/23.yaml delete mode 100644 data/rooms/town/24.yaml delete mode 100644 data/rooms/town/25.yaml delete mode 100644 data/rooms/town/26.yaml delete mode 100644 data/rooms/town/27.yaml delete mode 100644 data/rooms/town/28.yaml delete mode 100644 data/rooms/town/3.yaml delete mode 100644 data/rooms/town/30.yaml delete mode 100644 data/rooms/town/31.yaml delete mode 100644 data/rooms/town/4.yaml delete mode 100644 data/rooms/town/5.yaml delete mode 100644 data/rooms/town/6.yaml delete mode 100644 data/rooms/town/7.yaml delete mode 100644 data/rooms/town/8.yaml delete mode 100644 data/rooms/town/9.yaml delete mode 100644 data/rooms/underground/50.yaml delete mode 100644 data/rooms/underground/51.yaml delete mode 100644 data/rooms/underground/52.yaml delete mode 100644 data/rooms/underground/53.yaml delete mode 100644 data/rooms/underground/54.yaml delete mode 100644 data/rooms/underground/55.yaml delete mode 100644 data/rooms/underground/56.yaml delete mode 100644 data/rooms/underground/57.yaml delete mode 100644 data/rooms/underground/58.yaml delete mode 100644 data/rooms/underground/59.yaml delete mode 100644 data/rooms/underground/60.yaml delete mode 100644 data/rooms/underground/61.yaml delete mode 100644 data/rooms/underground/62.yaml delete mode 100644 data/rooms/underground/63.yaml delete mode 100644 data/rooms/underground/64.yaml delete mode 100644 data/rooms/underground/65.yaml delete mode 100644 data/rooms/underground/66.yaml delete mode 100644 data/rooms/underground/67.yaml delete mode 100644 data/rooms/underground/68.yaml delete mode 100644 data/rooms/wilderness/150.yaml delete mode 100644 data/rooms/wilderness/151.yaml delete mode 100644 data/rooms/wilderness/152.yaml delete mode 100644 data/rooms/wilderness/160.yaml delete mode 100644 data/rooms/wilderness/161.yaml delete mode 100644 data/rooms/wilderness/162.yaml delete mode 100644 data/rooms/wilderness/170.yaml delete mode 100644 data/rooms/wilderness/171.yaml create mode 100644 internal/game/condition_test.go create mode 100644 internal/game/sys_hazard.go create mode 100644 internal/game/sys_labor.go create mode 100644 internal/world/hazard.go delete mode 100644 worldbuilding_guide/bank.md delete mode 100644 worldbuilding_guide/behaviors.md delete mode 100644 worldbuilding_guide/conditions.md delete mode 100644 worldbuilding_guide/construction.md delete mode 100644 worldbuilding_guide/courses.md delete mode 100644 worldbuilding_guide/doors.md delete mode 100644 worldbuilding_guide/drops.md delete mode 100644 worldbuilding_guide/hacking.md delete mode 100644 worldbuilding_guide/hidden_objects.md delete mode 100644 worldbuilding_guide/index.md delete mode 100644 worldbuilding_guide/items.md delete mode 100644 worldbuilding_guide/mobs.md delete mode 100644 worldbuilding_guide/objects.md delete mode 100644 worldbuilding_guide/quests.md delete mode 100644 worldbuilding_guide/recipes.md delete mode 100644 worldbuilding_guide/rooms.md delete mode 100644 worldbuilding_guide/search.md delete mode 100644 worldbuilding_guide/state.md delete mode 100644 worldbuilding_guide/tips.md delete mode 100644 worldbuilding_guide/wandering_mobs.md delete mode 100644 worldbuilding_guide/wandering_objects.md diff --git a/AGENTS.md b/AGENTS.md index 8b65314..c2d420f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,7 +43,7 @@ internal/ | `core_*.go` | Infrastructure — types, utils, login, production engine, skill XP, flags, courses, equipment, stations, messages, startup validation | 13 | | `action_*.go` | Tick-based action lifecycles — gather, firemaking, talk, use, search, thieving, farming, agility, scavenging, assassin, fletching, cleaning, room scripts, targeting | 16 | | `cmd_*.go` | Command handlers — one file per command or command group | 49 | -| `sys_*.go` | Game subsystems — technology, science, combat (death + aggro), assassin tasks, buffs, construction | 6 | +| `sys_*.go` | Game subsystems — technology, science, combat (death + aggro), assassin tasks, buffs, construction, hazards, labor (task mobs) | 8 | | `ui_*.go` | Display/output — color, prompt, table, map, help | 5 | | `hacking/` | Hacking sub-package — Minigame interface, TerminalDef, CalcXP, WumpusGame, MastermindGame, LiarsDiceGame | 4 | @@ -81,15 +81,22 @@ The pattern is: lock, snapshot/unmarshal, unlock, then process. The engine creat - **World flags** (`set_flags` / checked with `flag`): Shared by all players. A door opened by one player is open for everyone. - **Player flags** (`set_player_flags` / checked with `player_flag`): Per-character, saved to character YAML. Quest progress, dialog history. +## Room Scripting + +- **`on_enter` steps** fire when a player enters. Each has a `message` + optional `condition`. A step may also have a `delay` (ticks) and/or `set_flags`/`set_player_flags`. If any surviving step is timed (has a delay or sets a flag) the sequence runs as a scheduled **cutscene** (driven by `EnterSeqTick`); otherwise messages print synchronously. Step conditions are snapshotted once at entry. A cutscene persists `Player.CutsceneRoom` and **resumes on reconnect** if interrupted; plain on_enter never runs on login. +- **Conditional descriptions** (`descriptions: [{text, condition}]`) on rooms and objects pick the first variant whose condition passes (per-looker), else the base `description`. For an **object**, if it has `descriptions` and none match, the object is **absent** for that player (`look` falls through). Objects also support `aliases: [...]` for extra names. `look` lists candidates when multiple distinct objects match. +- **Exits** support `set_flags`/`set_player_flags`, applied only on a successful traverse. + ## Data Files ``` -data/rooms/.yaml Room definitions (exits, objects, mobs, item_spawns, on_enter) +data/rooms/.yaml Room definitions (exits, objects, mobs, item_spawns, on_enter, descriptions) data/rooms/player_housing/ Player house rooms data/items/.yaml Item definitions (stats, equip_slot, tool_type, craft, burn_ticks, search_table, etc.) -data/mobs/.yaml Mob definitions (combat stats, drops, behavior, steal config) -data/objects/.yaml Object definitions (name, behavior, hidden, inroom_description, removal_item) +data/mobs/.yaml Mob definitions (combat stats, drops, behavior, steal config; kind: task for worksites) +data/objects/.yaml Object definitions (name, aliases, behavior, hidden, inroom_description, descriptions, removal_item) data/drops/.yaml Shared drop tables (weighted item lists, sub-table references) +data/hazards/.yaml Shared room hazard definitions (attack_type, attack, max_hit, speed, messages, required_item) data/help/.yaml Help topics data/modules/.yaml Science module definitions (id, level, max_hit, base_xp, junk_cost, category) data/courses/.yaml Agility course definitions (name, required_level, start_room, obstacles) @@ -99,7 +106,7 @@ data/players/characters/ Character YAML (gitignored) Wander config (`wander_rooms`, `wander_interval`) is set per-instance in room YAML — mob defs are generic. Mobs wander via legal (unconditioned) room exits; objects teleport between rooms in their assigned list. -See `worldbuilding_guide/` for full YAML format reference with examples. +See `building_guide/` for full YAML format reference with examples. ## Startup Validation @@ -217,6 +224,7 @@ The `verbSkill` map maps verb → skill name for default target resolution: | `curing` | cure | Active | | `traversing` | obstacle verbs (scramble/jump/etc.) | Active | | `hacking` | jack/jackin | Active | +| `working` | work/attack on a task mob (worksite) | Active | ### Behavior Types (YAML-driven in behavior.go) @@ -286,12 +294,13 @@ clamped to [0, cfg.Cap] ### Condition System -Used by exits, talk options, on-enter scripts, and use_interactions checks: +Used by exits, talk options, on-enter scripts, and use_interactions checks. A bare `flag`/`player_flag` passes when the flag is set to a truthy value; `value` matches a specific value; `not` inverts. | Field | Scope | Example | |---|---|---| | `flag` | World (shared by all) | `flag: gate_open` | | `player_flag` | Per-character | `player_flag: finished_tutorial` | +| `value` | Exact value to match (else truthy) | `value: 3` | | `has_item` | Player inventory | `has_item: bronze_key` | | `min_credits` | Player credits | `min_credits: 100` | | `all_of` | All sub-conditions pass | Nested list | @@ -341,6 +350,14 @@ Used by exits, talk options, on-enter scripts, and use_interactions checks: **Mob DropTable:** `Remains` (guaranteed drop item), `Loot` (weighted DropEntry table). +## Labor System (sys_labor.go, sys_hazard.go) + +A **non-violent reuse** of the entire combat engine for worksites — building solar panels, surveying flora, prospecting rocks — so combat skills/weapons train without the theme of killing. + +**Task mobs.** A `MobDef` with `kind: task` (default `combat`). Internally its `HP` drains to 0 to **complete** the work; the player sees a progress bar filling to 100% (`progressBar`, the inverse of `hpBar`). It reuses the whole pipeline: `startCombat`/`playerAttack`/`applyPlayerHit`/`endCombat`, all formulas, drops (= "payment"), respawn, the 1-worker `IsMobInCombat` lock. Differences: it **never attacks back** (`startCombat` skips the mob-attack subscription for tasks), display/messages are labor-flavored, and `ActionWorking` is used for `look`. Progress **decays** when unattended — this is the standard mob HP regen, unchanged, shown inverted. Commands `work` and `attack`/`kill` all route to `doAttack`, which auto-detects the kind. Flavor fields: `verb`, `progress_noun`, `complete_message`. Per-type defenses (`crush_defense`…`science_defense`) become **method efficiency** — every style works, designers steer which is efficient. XP/buffs are the normal style-based combat split (no change). + +**Hazards.** A room references a shared hazard by ID: `hazard: ` → `data/hazards/.yaml` (`world.HazardDef`, cached via `World.LoadHazard`). `HazardTick()` (registered in `main.go`) rolls an attack against **everyone in a hazardous room every `speed` ticks** (cadence in non-persisted `Player.HazardTimer`, reset on room change) using the same `EffectiveRoll`/`HitCheck`/`MaxHit`/`RollDamage`. Player defends with Defense level + equip bonus selected by `attack_type`; `damageAfterTechProtection` applies the matching protect tech. `required_item` equipped negates it. A safespot blocks **all** hazard damage (`safespotBlocksHazard` = `isSafespotted`, type-agnostic). A lethal hazard hit calls `killPlayer` (extracted from `endCombat`). Hazards **do not interrupt movement** (no flee-abort), and stack with mobs. Entering a safe→dangerous room prompts `[Y/n]` (`StateDangerConfirm`) unless the `danger_warning` option is off. + ## Technology System (tech.go) 25 hardcoded techs in 4 categories, unlocked by Technology level: @@ -410,6 +427,7 @@ Account-wide options set via `option `. Stored in account YAML, sh | `map_padding` | string | "none" | none/x/y/xy — blank-row stripping | | `automap` | bool | false | Auto-show map after moving | | `queue_silently` | bool | true | Suppress queue messages | +| `danger_warning` | bool | true | Confirm `[Y/n]` before entering a hazardous area from a safe one | | `room_desc_width` | int | 70 | Room desc line wrap width | | `unicode` | bool | true | Unicode box-drawing characters | | `run_countdown` | bool | false | Flee countdown messages | @@ -454,6 +472,7 @@ Prompt variables: `%h` (HP), `%H` (max HP), `%b` (battery), `%B` (max battery), | `StateShop` | Shop interface (buy, sell, browse, leave) | | `StateBank` | Bank interface (deposit, withdraw, browse, leave) | | `StateHacking` | Jacked into a terminal running a minigame | +| `StateDangerConfirm` | Confirming `[Y/n]` entry into a hazardous room | ## ItemDef Fields (object/item.go) @@ -500,6 +519,10 @@ Prompt variables: `%h` (HP), `%H` (max HP), `%b` (battery), `%B` (max battery), - Cape of Agility makes movement instant (1 tick). Graceful pieces each reduce by 0.25 ticks; full set +0.5 bonus for 2 ticks total. Cape overrides Graceful. - During combat, movement is a flee attempt. Mob hits during countdown abort the flee. - Level-up messages output `*** You are now level N ! ***` across all skills that gain XP. +- Task mobs (`kind: task`) reuse the combat engine 1:1 — internally HP drains to 0 to complete; the bar is just displayed inverted. They never attack back; danger comes only from a room `hazard:`. +- Hazard damage uses its own `applyHazardHit` path that deliberately omits the flee-abort (`MoveTicks`) logic, so hazards never interrupt walking. Mob hits still do. +- A safespot blocks ALL hazard damage regardless of type (`safespotBlocksHazard`), but a melee work/attack step still forces you out of cover (so only ranged/science work keeps you sheltered). +- `killPlayer` is the shared death path (extracted from `endCombat`); both combat death and lethal hazard hits go through it. `mob` may be nil for a hazard kill. ## Adding a New Skill @@ -514,7 +537,7 @@ Prompt variables: `%h` (HP), `%H` (max HP), `%b` (battery), `%B` (max battery), Production skills share a unified system in `core_production.go`: -1. Create the output item YAML in `data/items/` with a `craft:` block. See `worldbuilding_guide/recipes.md`. +1. Create the output item YAML in `data/items/` with a `craft:` block. See `building_guide/recipes.md`. 2. Create a station object in `data/objects/` if needed 3. Add a `cmd_.go` command handler following existing patterns 4. Add the command to `commandRegistry` in `cmd_registry.go` with the appropriate class diff --git a/IDEAS.md b/IDEAS.md deleted file mode 100644 index 15031c5..0000000 --- a/IDEAS.md +++ /dev/null @@ -1,10 +0,0 @@ -- Quick swap outfits at bank (melee, range, tech, mining, etc.) -- Graceful allows 20% longer walks -- Rare card pack drops (mobs and skills). Lore, items, mob, character, mob cars. Super scope creep: Card game? -- Make sure this doesn't ruin the casino but: A rare drop that improves your casino odds? Blocks low cards in BJ or something for X rounds? Something that tilts EV to like 102%. -+ Better idea: A temporary pass to a room with gimmicked odds or payouts to like 105%. -+ Training gambling room? Very low max bets but 101% payouts? Would have to fit into lore. Maybe it's a way to create problem gamblers. - -Story: -Spaceport at Casino -Corp owns Casino, only they're rich enough to keep the NL at bay enough to operate spaceships diff --git a/README.md b/README.md index fabf6f7..5b2b89a 100644 --- a/README.md +++ b/README.md @@ -93,32 +93,14 @@ Pockets of radicalized settlers on the terraformed asteroid Gaia 04 took the ten Wounded, Gaia 04 struggles on. Everyone from astrobotanists to business executives must remake their lives as farmers, fishers, and miners as they survive in the primitive ashes of the fallen colony. -## Major Factions -**Neoluddites** -Anti-tech mostly-extremists who brought about the Second Collapse by destroying electronic networks and thinking machines. Once pawns of Earth's ruling class, they've evolved into a self-sufficient traditionalist culture with inconsistent views about what technology should be permitted to exist. - -**Utopians** -Those who embraced AI control of society and tried to stop the Neoluddites through both policy and violence. Their views range from religious exaltation of thinking machines to mere agreement that life was easier when the AI handled the governance and marginalia. - -**Craggers** -Colonists who don't take a side in the conflict one way or the other. Everyone from criminals escaping their past to once well-paid Earth engineers overseeing terraforming operations. - -# Todo - -- Fix and test all skills -- Pharmacy is broken -- Make sure "use" works with everything as expected (e.g. id stations) -- Play around with Hacking. Needs interesting but semi-afk minigames. -- Prettify and make menus consistent -- Start worldbuilding with a tutorial island. # AI Disclosure -Huge technical parts of this codebase were ~~completely vibe coded~~ ~~agentically engineered~~ written with LLM assistance. +Huge technical parts of the **engine** were ~~completely vibe coded~~ ~~agentically engineered~~ written with LLM assistance. -LLMs will **not** generate any creative part of the game world (e.g. story, characters, rooms, quests). LLMs do generate *example* content for dev testing, so you might see that somewhere, but not in the final game. +**LLMs will not generate any creative content** (e.g. story, characters, rooms, quests). LLMs do generate *example* content for dev testing, so you might see that somewhere, but not in the final game. # License -0BSD. Do whatever you want. +0BSD. Do whatever you want. Have fun. diff --git a/building_guide/bank.md b/building_guide/bank.md new file mode 100644 index 0000000..0a5ec88 --- /dev/null +++ b/building_guide/bank.md @@ -0,0 +1,68 @@ +# Bank System + +The bank system allows players to store items permanently, freeing up inventory space. + +## Objects + +### bank_booth + +A bank terminal that players can interact with. Has no special behavior — the `bank` command checks for any `bank_booth` object in the room. + +```yaml +id: bank_booth +name: bank booth +color: "226" +description: "..." +inroom_description: "A bank booth is set into the wall." +``` + +To place a bank booth in a room, add it to the room's `objects:` list: + +```yaml +objects: + - id: bank_booth +``` + +## Commands + +| Command | Description | +|---------|-------------| +| `bank` | Opens bank interface (requires bank booth in room) | +| `use bank` | Same as `bank` | + +### Bank Interface Commands + +| Command | Description | +|---------|-------------| +| `deposit ` | Move item from inventory to bank | +| `deposit all` | Deposit entire inventory | +| `withdraw ` | Move item from bank to inventory | +| `browse` / `list` | Show bank contents | +| `leave` | Exit bank interface | + +## Implementation + +### Session State + +`StateBank` (defined in `internal/net/server.go`) is the session state for the bank interface. Input is routed through `handleBankInput` in `internal/game/cmd_bank.go`. + +### Player Storage + +Bank items are stored in the `Player.Bank` field (`map[int]*InventorySlot`), saved in character YAML. Bank slots are sequential and reuse freed slots. There is no slot limit. + +### Command Dispatch + +- `bank` is classified as `ClassActive` in `classifyCommand()` +- `bank` case in `executeCommand()` calls `doBank(sess)` +- `doUse()` checks for `"bank"` or `"bank booth"` targets and routes to `doBank()` +- `doBank()` checks for a `bank_booth` object in the current room +- `handleBankInput()` dispatches deposit/withdraw/browse/leave commands + +### Data Files + +| File | Purpose | +|------|---------| +| `data/objects/bank_booth.yaml` | Bank booth object definition | +| `data/help/bank.yaml` | Help topic for the bank system | + +Default bank booth locations: Town Square (room 1), Forge (room 12). diff --git a/building_guide/behaviors.md b/building_guide/behaviors.md new file mode 100644 index 0000000..22e97fe --- /dev/null +++ b/building_guide/behaviors.md @@ -0,0 +1,402 @@ +## Behaviors + +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. + +### Gather (mining, fishing, woodcutting) + +Mining — per-drop depletion on a rock object: +```yaml +id: copper_rock +name: copper rock +color: "178" +gather: + skill: mining + level: 1 + xp: 17 # XP awarded per successful gather + base_wait: 8 # ticks between attempts + tools: # requires item with matching tool_type + - pickaxe + success: + base: 0.40 # 40% base chance + per_level: 0.01 # +1% per level above requirement + cap: 0.95 # 95% max + gather_message: "You swing your pickaxe at the rock..." + fail_message: "You chip away but get nothing useful." + drops: + - item_id: copper_ore + weight: 90 # 90% chance when roll succeeds + depletes: true # rock becomes depleted after this drop + message: "You manage to mine some {178}copper ore{/}." + - table: gem_table # reference a shared drop table + weight: 10 + depletes: false # gem drops don't deplete the rock + message: "You spot a glint of something valuable!" + respawn_timer: 50 # ticks until rock respawns + respawn_broadcast: "A glint of copper catches your eye from some {name}." +``` + +Drop messages support inline color tags: `{<0-255>}text{/}`. Use the item's color +index to match its display color. Gradients also work: `{g:196,82}text{/}`. + +Non-depleting gather (fishing on a fishing spot object): +```yaml +id: bait_fishing_spot +name: fishing spot +gather: + skill: fishing + level: 1 + xp: 10 + base_wait: 4 + tools: + - fishing_rod + bait: fishing_bait + success: + base: 0.30 + per_level: 0.01 + cap: 0.90 + gather_message: "You cast your line into the water..." + fail_message: "Nothing seems to bite." + drops: + - item_id: raw_trout + weight: 100 + depletes: false # never depletes + message: "You catch a {69}raw trout{/}!" +``` + +Woodcutting with shared depletion and bird's nests: +```yaml +id: oak_tree +name: oak tree +color: "113" +gather: + skill: woodcutting + level: 15 + xp: 37 + base_wait: 6 + tools: + - axe + success: + base: 0.40 + per_level: 0.01 + cap: 0.90 + gather_message: "You swing your axe at the oak tree..." + fail_message: "You swing but get no logs." + drops: + - item_id: oak_logs + weight: 100 + depletes: false # depletion is timer-based (deplete_timer) + message: "You get some {113}oak logs{/}." + respawn_timer: 14 # ticks until tree respawns after being cut down + deplete_timer: 45 # max ticks before next gather depletes (counts down while chopping) + nest_chance: 256 # 1/256 chance for a bird's nest on each successful gather + respawn_broadcast: "An {name} grows back." +``` + +Regular tree — always depletes on first gather, no shared timer, no nests: +```yaml +id: tree +name: tree +gather: + skill: woodcutting + level: 1 + xp: 25 + base_wait: 4 + tools: + - axe + success: + base: 0.50 + per_level: 0.01 + cap: 0.95 + gather_message: "You swing your axe at the tree..." + fail_message: "You swing but get no logs." + drops: + - item_id: logs + weight: 100 + depletes: true # regular tree depletes on first successful gather + message: "You get some {107}logs{/}." + respawn_timer: 80 + respawn_broadcast: "A {name} grows back." +``` + +#### GatherConfig Fields + +| Field | Type | Description | +|---|---|---| +| `skill` | string | Skill name for level check | +| `level` | int | Required level to gather | +| `xp` | int | XP per successful gather | +| `base_wait` | float64 | Base ticks per action cycle | +| `tools` | []string | Required tool_type list (e.g. `[pickaxe]`) | +| `bait` | string | Required bait item (for fishing) | +| `success` | SuccessFormula | Base + per_level, capped | +| `gather_message` | string | Custom "You gather..." message | +| `depleted_message` | string | Custom "The rock is depleted" message | +| `exhausted_message` | string | Custom "The tree falls" message | +| `fail_message` | string | Custom failure message | +| `drops` | []DropEntry | Weighted drop entries | +| `respawn_timer` | float64 | Ticks until depleted object respawns | +| `respawn_broadcast` | string | Broadcast message when object respawns | +| `deplete_timer` | float64 | Ticks for shared depletion (trees) | +| `nest_chance` | int | 1/N chance for bird's nest alongside normal drop | + +#### SuccessFormula + +| Field | Type | Description | +|---|---|---| +| `base` | float64 | Base success chance (0.0-1.0) | +| `per_level` | float64 | Chance increase per level above requirement | +| `cap` | float64 | Maximum success chance | + +#### DropEntry + +| Field | Type | Description | +|---|---|---| +| `item_id` | string | Item ID to drop | +| `table` | string | Reference to a shared drop table in `data/drops/` | +| `weight` | int | Relative drop weight | +| `depletes` | bool | Resource depletes on this drop | +| `quantity` | int | Amount to drop (1-3 for random) | +| `message` | string | Player message on drop | +| `level` | int | Skill level required for this drop | +| `xp` | int | Bonus XP on this drop | + +#### Shared depletion explained + +When `deplete_timer > 0`, the tree has a shared despawn timer: +- The timer starts at `deplete_timer` max when the first player begins chopping. +- Each tick, if anyone is chopping, the timer counts down. +- When the timer reaches 0, the NEXT successful gather depletes the tree. +- If no one is chopping and the tree isn't depleted, the timer ticks back UP. +- All players chopping the same tree are interrupted when it depletes. + +Use `deplete_timer` for trees. Use `depletes: true` on individual drops for rocks. + +#### Bird's nests + +When `nest_chance > 0`, each successful gather has a 1/N independent chance to also drop +a bird's nest. The nest goes to inventory (or to the ground if inventory is full). +Use the `search` command to open nests — they roll on the `birds_nest_drop` table. + +#### XP drops + +When `xp > 0`, the gather awards XP on each successful drop. If the player's `xpdrops` +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: + +```yaml +id: guard +name: Guard +talk: + nodes: + start: + message: "\"Halt! This area is restricted.\"" + options: + - text: "\"What's behind that gate?\"" + goto: about_gate + - text: "\"I have copper ore.\"" + goto: trade_ore + condition: + has_item: copper_ore + - text: "\"I have a pass.\"" + goto: has_pass + condition: + player_flag: got_pass + value: true + - text: "\"Goodbye.\"" + end: true + + about_gate: + message: "\"Bring me some copper ore and I'll stamp you a pass.\"" + action: + set_player_flags: + talked_to_guard: true + options: + - text: "\"I'll be back.\"" + end: true + - text: "\"I have some right here.\"" + goto: trade_ore + condition: + has_item: copper_ore + + trade_ore: + message: "\"Good quality ore.\" He stamps a pass and hands it to you." + action: + take_item: copper_ore + give_item: pass_stub + set_player_flags: + got_pass: true + options: + - text: "\"Thanks.\"" + end: true + + has_pass: + message: "\"Alright, I'll open the gate for you.\"" + action: + set_flags: + gate_open: true + options: + - text: "\"Thanks.\"" + end: true +``` + +#### Node action reference + +| Field | Effect | +|---|---| +| `set_flags` | Sets world flags (global, shared by all players) | +| `set_player_flags` | Sets player-local flags (per-character, quest progress) | +| `give_item` | Gives an item to the player's inventory | +| `take_item` | Removes an item from the player's inventory | +| `teleport` | Moves the player to a room ID | +| `heal` | Restores that many hitpoints | +| `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: +```yaml +action: + take_item: dragon_head + give_item: dragon_slayer_medal + set_player_flags: + dragon_quest: complete + dragon_slain: true + heal: 99 + teleport: 1 +``` + +#### 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_clerk +name: General Store Clerk +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. + +### Object Interactions (levers, switches, gates) + +Interaction configs go under the `use_interactions:` key on objects. +Entries with no `item` field are bare interactions triggered by "use" +or "push"/"pull": + +```yaml +id: iron_gate +name: iron gate +hidden: true +description: "A heavy iron gate set into the north wall." +use_interactions: + - condition: + flag: gate_open + value: true + not: true + message: "You push the heavy iron gate open." + action: + set_flags: + gate_open: true +``` + +Item-specific interaction: +```yaml +id: bookshelf +name: bookshelf +use_interactions: + - item: dusty_tome + message: "The bookshelf slides aside, revealing a secret passage!" + action: + set_flags: + secret_passage_open: true +``` + +### Use (crafting stations) + +Use configs go under the `use:` key on objects (e.g. furnaces, ranges): + +```yaml +id: furnace +name: furnace +color: "208" +use: + message: "You place the ore in the furnace..." + wait: 4 + consume: + copper_ore: 1 + reward: + item_id: copper_bar + quantity: 1 + fail_message: "The ore crumbles to dust." + success: + base: 0.60 + per_level: 0.01 + cap: 0.95 + skill: smithing + level: 1 + xp: 15 +``` + +--- diff --git a/building_guide/conditions.md b/building_guide/conditions.md new file mode 100644 index 0000000..ea17ed6 --- /dev/null +++ b/building_guide/conditions.md @@ -0,0 +1,80 @@ +## Conditions Reference + +Conditions are used in talk options, exit gates, on-enter scripts, and use_interactions checks. + +### Simple conditions + +A bare `flag:` / `player_flag:` check passes when the flag is **set to a truthy +value** (`true`, a non-zero number, a non-empty string). Add `value:` only when +you need to match a specific value (e.g. a numeric quest stage). `not: true` +inverts any check. + +```yaml +# Check a world flag is set +condition: + flag: gate_open + +# Check a world flag is NOT set +condition: + flag: gate_open + not: true + +# Check a player flag is set +condition: + player_flag: finished_tutorial + +# Check a player flag is NOT set (e.g. only on the first visit) +condition: + player_flag: finished_tutorial + not: true + +# Match a specific (non-boolean) value +condition: + player_flag: quest_stage + value: 3 + +# Check if player has an item +condition: + has_item: bronze_key + +# Check if player does NOT have an item +condition: + has_item: bronze_key + not: true + +# Check if player has enough credits +condition: + min_credits: 50 +``` + +### Compound conditions + +All must pass: +```yaml +condition: + all_of: + - flag: gate_open + - has_item: pass_stub +``` + +Any one must pass: +```yaml +condition: + any_of: + - has_item: bronze_key + - has_item: iron_key + - player_flag: master_of_unlocking +``` + +Nested compounds: +```yaml +condition: + all_of: + - player_flag: quest_started + - any_of: + - has_item: wolf_pelt + - has_item: bear_pelt +``` + +--- + diff --git a/building_guide/construction.md b/building_guide/construction.md new file mode 100644 index 0000000..2c4b7ce --- /dev/null +++ b/building_guide/construction.md @@ -0,0 +1,223 @@ +# Construction Skill + +The Construction skill allows players to process logs into planks and assemble planks into furniture at a workbench. Higher-tier logs produce more valuable planks and furniture. + +## Skill + +- Skill name: `construction` +- Abbreviation: `con` +- Level range: 1-99 +- XP table: RSC standard + +## Commands + +| Command | Description | +|---------|-------------| +| `construct` / `make` | Open construction menu at a workbench | +| `construct ` | Build a specific item | +| `construct ` | Build multiple of an item | + +## Objects + +### workbench + +The primary construction station. Placed in rooms to enable construction. + +```yaml +id: workbench +name: workbench +color: "172" +description: "A sturdy wooden workbench..." +inroom_description: "A workbench stands against the wall..." +``` + +### estate_directory + +A terminal that shows registered homeowners and lets them enter their house. Handled entirely in Go code (`internal/game/cmd_estate_directory.go`), not via YAML behaviors. + +```yaml +id: estate_directory +name: Estate Directory +color: "39" +description: "A holographic terminal displaying property records..." +inroom_description: "An estate directory terminal..." +``` + +When a player looks at the directory, it scans `data/players/characters/*.yaml` for the `owns_house_local_neighborhood` player flag and lists all homeowners. When a player uses or talks to the directory and owns a house, they can teleport to their house entrance room. + +## Tools + +### saw + +Required tool for cutting logs into planks at the workbench. + +```yaml +id: saw +name: saw +tool_type: saw +value: 30 +``` + +## Items + +### Planks (4 tiers) + +| Item ID | Name | Level | Value | Made From | +|---------|------|-------|-------|-----------| +| `planks` | planks | 1 | 20 | logs | +| `oak_planks` | oak planks | 15 | 40 | oak_logs | +| `teak_planks` | teak planks | 35 | 80 | teak_logs | +| `mahogany_planks` | mahogany planks | 50 | 120 | mahogany_logs | + +### Furniture (sellables) + +| Item ID | Level | Planks Required | +|---------|-------|-----------------| +| `wooden_shelf` | 4 | 2 planks | +| `wooden_table` | 6 | 3 planks | +| `wooden_chair` | 8 | 2 planks | +| `oak_shelf` | 18 | 2 oak planks | +| `oak_table` | 20 | 3 oak planks | +| `teak_table` | 38 | 3 teak planks | +| `mahogany_table` | 52 | 3 mahogany planks | + +## Recipes + +All recipes use `type: construction` and `station: [workbench]`. Log-to-plank recipes additionally require `tool: saw`. + +Example plank recipe: +```yaml +id: construct_oak_planks +type: construction +level: 15 +xp: 10 +wait: 20 +station: [workbench] +tool: saw +consume: + - items: [oak_logs] + quantity: 1 +output: oak_planks +message: "You saw the oak logs into sturdy oak planks." +``` + +Example furniture recipe: +```yaml +id: construct_wooden_table +type: construction +level: 6 +xp: 25 +wait: 6 +station: [workbench] +consume: + - items: [planks] + quantity: 3 +output: wooden_table +message: "You build a sturdy wooden table." +``` + +## NPCs + +### estate_broker + +Located in room 16 (Construction Site). Talk behavior sells a house plot for 10 credits via `cost` + `set_player_flags`. Sets `owns_house_local_neighborhood: "local_neighborhood"` on purchase. + +```yaml +id: estate_broker +name: estate broker +talk: + nodes: + start: + message: "\"Interested in a house?\"" + options: + - text: "\"How much?\"" + goto: offer + - text: "\"No thanks.\"" + end: true + offer: + message: "\"10 credits for a plot.\"" + action: + cost: 10 + set_player_flags: + owns_house_local_neighborhood: "local_neighborhood" + options: + - text: "\"Deal!\"" + end: true +unique: true +``` + +### sawmill_operator + +Located in room 171 (Lumberyard). Talk behavior processes all logs in inventory into planks for a credit fee. Uses the `sawmill: true` node action which triggers `processSawmill()` in `internal/game/action_talk.go`. + +Fees: regular (5cr), oak (10cr), teak (20cr), mahogany (40cr). + +## Science Mod: Plank Make + +Level 86 Science utility mod that converts logs to planks at 70% of sawmill cost. Uses naturejunk, solarjunk, and scrap. + +```go +{ID: "plank_make", Name: "Plank Make", Level: 86, BaseXP: 90.0, + JunkCost: map[string]int{"naturejunk": 5, "solarjunk": 1, "scrap": 1}, + Category: ModUtility, TargetType: "inventory"}, +``` + +Usage: `trigger plank make` (all logs) or `trigger plank make ` (specific). + +## Player Houses + +Player houses are real YAML rooms stored in `data/rooms/player_housing/`. Each house area (e.g. Local Neighborhood) has entrance + workshop rooms with unique room IDs in the 200+ range. + +House room files follow the same format as world rooms: +```yaml +id: 200 +name: "Player House: Entrance Hall" +description: "..." +exits: + south: 170 + north: 201 +objects: + - id: charging_station +``` + +The room loader (`World.LoadRoom`) checks `data/rooms/.yaml` first, then falls back to `data/rooms/player_housing/.yaml`. + +Ownership is tracked via player flags (e.g. `owns_house_local_neighborhood: "local_neighborhood"`). The Estate Directory scans character YAML files for these flags. + +## Implementation + +### Production Integration + +Construction is added to `productionTypes` in `action_production.go`: +```go +"construction": {"construct", "constructing"}, +``` + +This enables automatic support through the unified production system (`advanceProduction`, `startProductionFromRecipe`, etc.). + +### Command + +`cmd_construct.go` follows the same pattern as `cmd_craft.go`, using per-recipe `Station` and `Tool` checks. Station: `workbench`, Tool: `saw` (for plank recipes only). + +### Option + +`construct_all` (OptBool, default false): Auto-start construction when only one product is available. + +## Data File Summary + +| Path | Purpose | +|------|---------| +| `data/objects/workbench.yaml` | Workbench station object | +| `data/objects/estate_directory.yaml` | Directory terminal object | +| `data/items/saw.yaml` | Saw tool | +| `data/items/planks.yaml` - `mahogany_planks.yaml` | Plank items (4 tiers) | +| `data/items/wooden_shelf.yaml` - `mahogany_table.yaml` | Furniture items (7 items) | +| Item YAMLs with `craft:` blocks (planks, furniture) | Construction recipes — inline on output item YAML | +| `data/mobs/estate_broker.yaml` | Estate broker mob (talk inline) | +| `data/mobs/sawmill_operator.yaml` | Sawmill operator mob (talk inline) | +| `data/rooms/16.yaml` | Construction Site (updated) | +| `data/rooms/170.yaml` | Local Neighborhood | +| `data/rooms/171.yaml` | Lumberyard | +| `data/rooms/player_housing/200.yaml` | Player house entrance | +| `data/rooms/player_housing/201.yaml` | Player house workshop | +| `data/help/construct.yaml` | Help topic | diff --git a/building_guide/courses.md b/building_guide/courses.md new file mode 100644 index 0000000..491e2bd --- /dev/null +++ b/building_guide/courses.md @@ -0,0 +1,119 @@ +## Agility Courses + +Agility courses are defined in `data/courses/.yaml`. Each course defines a sequence of obstacle rooms, each with a specific verb the player must type to advance. Obstacle rooms themselves are standard room YAML files (typically in `data/rooms/agility/`). + +### Course YAML + +```yaml +id: "vent_shaft" +name: "Ventilation Shaft Course" +required_level: 1 +start_room: 200 +completion_xp: 40 +obstacles: + - room_id: 201 + verb: scramble + ticks_per_phase: 2 + xp: 8 + fail_damage: [1, 2] + messages: + - "You approach the corroded ventilation wall..." + - "You find footholds in the rusted panels and begin to climb..." + - "You scramble up the wall and haul yourself onto the ledge!" + - room_id: 202 + verb: balance + ticks_per_phase: 2 + xp: 8 + fail_damage: [1, 2] + messages: + - "You step onto the narrow coolant pipe..." + - "Arms outstretched, you carefully place one foot in front of the other..." + - "You reach the other side of the pipe and step onto solid ground!" +``` + +### CourseConfig Fields + +| Field | Type | Description | +| ---------------- | ------------- | ---------------------------------------------------- | +| `id` | string | Unique course identifier | +| `name` | string | Display name shown to players | +| `required_level` | int | Minimum Agility level to attempt the course | +| `start_room` | int | Hub room — teleported here on fail or lap completion | +| `completion_xp` | int | Bonus XP awarded when a full lap is completed | +| `obstacles` | []ObstacleDef | Ordered list of obstacles | + +### ObstacleDef Fields + +| Field | Type | Description | +| ----------------- | -------- | --------------------------------------------------------------------------- | +| `room_id` | int | Room ID for this obstacle | +| `verb` | string | Command the player types to attempt it (e.g. `scramble`, `jump`, `climb`) | +| `ticks_per_phase` | float64 | Ticks between each phase message (supports fractional via `engine.ToTicks`) | +| `xp` | int | XP awarded for successfully completing this single obstacle | +| `fail_damage` | [2]int | `[min, max]` damage on failure (HP clamped to minimum 1 — cannot kill) | +| `messages` | []string | Exactly 3 strings for the 3-phase advancement system | + +### 3-Phase Message System + +Each obstacle advances through 3 phases, printing one message per phase: + +``` +Phase 0 (start): messages[0] printed immediately +Phase 1 (middle): messages[1] printed — FAILURE CHECK happens here +Phase 2 (end): messages[2] printed — XP awarded, teleport to next room +``` + +Failure only occurs at phase 1. Success chance is based on Agility level: +- At required level: 70% success (30% fail) +- Each level above reduces fail chance by 1% (down to minimum 5%) +- Fail chance capped at 60% + +On failure, the player takes random damage in `[fail_damage[0], fail_damage[1]]`, is teleported back to `start_room`, and must restart the course. + +### Lap Counting + +Completing all obstacles in a course increments a lap counter stored as a player flag (`agility_laps_`). The `completion_xp` bonus is awarded on the final obstacle only. + +### Obstacle Room YAML + +Each obstacle room should have an `on_enter` message telling the player which verb to use, and a `down` exit back to the course hub: + +```yaml +id: 201 +name: "Ventilation Shaft - Corroded Wall" +description: "A towering wall of corroded ventilation panels..." +on_enter: + - message: "Type 'scramble' to climb the wall." +exits: + down: + room: 200 + blocked_message: "" +``` + +The hub room (e.g. room 200) links to the first obstacle of each course: + +```yaml +id: 200 +name: "Agility Training Grounds" +exits: + south: 17 + north: 201 # Vent Shaft (level 1) + east: 210 # Rooftop (level 20) + west: 220 # Reactor (level 50) +``` + +### Supported Obstacle Verbs + +| Verb | Gerund (display) | +| ---------- | ---------------- | +| `scramble` | scrambling | +| `jump` | jumping | +| `swing` | swinging | +| `balance` | balancing | +| `climb` | climbing | +| `crawl` | crawling | +| `vault` | vaulting | +| `leap` | leaping | +| `slide` | sliding | + +Obstacle verbs are auto-registered at startup from all course YAML files. No changes to `cmd_registry.go` needed when adding new courses — any verb in a `data/courses/` file will work. diff --git a/building_guide/doors.md b/building_guide/doors.md new file mode 100644 index 0000000..1e9fb7a --- /dev/null +++ b/building_guide/doors.md @@ -0,0 +1,97 @@ +## Global vs Player State: Door Examples + +### Example A: Door with a button in another room (GLOBAL) + +A button in room 3 opens a door in room 7. Anyone can press it. Once pressed, the door is open for everyone. + +**Button object** (`data/objects/door_button.yaml`): +```yaml +id: door_button +name: stone button +hidden: true +use_interactions: + - condition: + flag: secret_door_open + not: true + message: "You press the stone button. You hear grinding stone in the distance." + action: + set_flags: + secret_door_open: true +``` + +**Room 3** — contains the button: +```yaml +id: 3 +name: "Button Chamber" +description: "A small stone chamber. A button protrudes from the east wall." +exits: + south: 1 +objects: + - id: door_button +``` + +**Room 7** — contains the door: +```yaml +id: 7 +name: "Hidden Passage" +description: "A dusty corridor. A heavy stone door blocks the way north." +exits: + south: 2 + north: + room: 8 + condition: + flag: secret_door_open # checks WORLD flag + value: true + blocked_message: "A heavy stone door blocks the way." +objects: + - id: stone_door + hidden: true +``` + +### Example B: Key-locked door (PLAYER-LOCAL) + +A locked door that only opens for a player carrying the key. Each player must find their own key. + +**Key item** (`data/items/rusty_key.yaml`): +```yaml +id: rusty_key +name: rusty key +description: "An old iron key, still functional." +value: 0 +stackable: false +``` + +**Room 5** — locked door: +```yaml +id: 5 +name: "Locked Storage" +description: "A small storage room. The way east is blocked by a locked iron door." +exits: + west: 2 + east: + room: 6 + condition: + has_item: rusty_key # checks PLAYER inventory + blocked_message: "The iron door is locked. You need a key." +objects: + - id: iron_door + hidden: true +``` + +**Room 6** — the other side (no key needed to exit): +```yaml +id: 6 +name: "Storage Closet" +description: "Shelves of dusty crates." +exits: + west: 5 # exit back — no condition +spawns: + - item_id: uncut_ruby + quantity: 1 + respawn_ticks: 500 +``` + +Key difference: the button door uses `flag` (shared state — one player presses, everyone benefits), the key door uses `has_item` (per-player inventory check — each player needs their own key). + +--- + diff --git a/building_guide/drops.md b/building_guide/drops.md new file mode 100644 index 0000000..e5a17ff --- /dev/null +++ b/building_guide/drops.md @@ -0,0 +1,52 @@ +## Drop Tables + +Shared drop tables can be referenced by multiple behaviors: +```yaml +# data/drops/gem_table.yaml +id: gem_table +drops: + - item_id: uncut_sapphire + weight: 47 + - item_id: uncut_emerald + weight: 16 + - item_id: uncut_ruby + weight: 4 + - item_id: uncut_diamond + weight: 1 +``` + +Bird's nest drop table: +```yaml +# data/drops/birds_nest_drop.yaml +id: birds_nest_drop +drops: + - item_id: credits + weight: 50 + quantity: 200 + - item_id: credits + weight: 30 + quantity: 500 + - item_id: credits + weight: 15 + quantity: 1000 + - item_id: credits + weight: 4 + quantity: 3000 + - item_id: credits + weight: 1 + quantity: 10000 +``` + +Referenced from a gather behavior: +```yaml +drops: + - item_id: copper_ore + weight: 90 + depletes: true + - table: gem_table # pulls from data/drops/gem_table.yaml + weight: 10 + depletes: false +``` + +--- + diff --git a/building_guide/hacking.md b/building_guide/hacking.md new file mode 100644 index 0000000..8927b3e --- /dev/null +++ b/building_guide/hacking.md @@ -0,0 +1,49 @@ +## Hacking + +Hacking minigames are played by jacking into terminal objects. Terminals are YAML objects that are registered in Go's `terminalDefs` map. + +### Terminal Object YAML + +```yaml +id: terminal_basic +name: basic terminal +color: "40" +description: "A battered terminal with a cracked screen. {40}[Level 1 Hacking]{/}" +inroom_description: "A {40}basic terminal{/} hums quietly against the wall." +``` + +- `id` must match a key in `terminalDefs` (Go code in `internal/game/hacking.go`) +- `name` is used for matching in the `jack` command +- `color` controls the display color +- `description` is shown when examining the terminal +- `inroom_description` is shown in room listings +- No `behavior` field — terminals are registered in Go, not YAML behaviors + +### Adding a New Terminal + +1. Create an object YAML file in `data/objects/terminal_.yaml` +2. Add an entry to `terminalDefs` in `internal/game/hacking.go` +3. Optionally create a new minigame implementation in `internal/game/hacking_.go` +4. Place the terminal object in a room's `objects:` list + +### Adding a New Minigame + +New minigames implement the `HackingMinigame` interface: + +```go +type HackingMinigame interface { + Init(level int) string + HandleInput(input string) (output string, done bool, won bool) + Name() string +} +``` + +Optionally implement `XPBonuser` for bonus XP: + +```go +type XPBonuser interface { + BonusXP() int +} +``` + + diff --git a/building_guide/hazards.md b/building_guide/hazards.md new file mode 100644 index 0000000..453ca18 --- /dev/null +++ b/building_guide/hazards.md @@ -0,0 +1,75 @@ +## Hazards + +A **hazard** is an environmental threat attached to a room — a sandstorm, solar +radiation, falling rocks, toxic spores. It rolls an "attack" against everyone in +the room every few ticks using the **same combat formulas as a mob**: the hazard +supplies the accuracy and max hit, and the player defends with their Defense +level plus the equipment defense bonus selected by the hazard's attack type. No +new combat math, no special gear — your existing armour and Defense skill are +what keep you alive. + +Hazards are **shared, ID-referenced definitions** (like drop tables and modules): +define a hazard once in `data/hazards/.yaml`, then reference it from any number +of rooms with `hazard: ` (see `rooms.md`). This makes one hazard reusable +across a whole area and lets a future `examine` command resolve it by name. + +```yaml +id: solar_radiation +name: scorching solar radiation +description: "Unfiltered sunlight pours through a gap in the dome, burning exposed skin." +attack_type: science # stab/slash/crush/ranged/science — picks player defense + protect tech +attack: 40 # accuracy roll level +attack_bonus: 0 # equipment-equivalent accuracy bonus +max_hit: 6 # maximum damage per hit +speed: 5 # ticks between hazard rolls +warn_message: "The dome shielding fails here — raw solar radiation pours in." +hit_message: "Solar radiation sears you for %d damage." # optional single %d for the damage +miss_message: "You shield your eyes and weather a wave of solar glare." +required_item: "" # optional: an equipped item that NEGATES the hazard entirely +``` + +### Fields + +| Field | Description | +|---|---| +| `id` | Unique identifier (matches the filename) | +| `name` | Display name used in warnings and default messages | +| `description` | Flavour text (for a future `examine`) | +| `attack_type` | `stab`/`slash`/`crush`/`ranged`/`science` — selects which player defense bonus applies and which protection tech reduces the damage (empty = `crush`) | +| `attack` | Hazard accuracy level | +| `attack_bonus` | Equipment-equivalent accuracy bonus | +| `max_hit` | Maximum damage per hit | +| `speed` | Ticks between hazard rolls (default 5 if ≤ 0) | +| `warn_message` | Shown when warning the player about the area | +| `hit_message` | Message on a hit; include a single `%d` to show the damage | +| `miss_message` | Message on a miss | +| `required_item` | If set and equipped, the hazard is fully negated for that player | + +### How it behaves + +- **Applies to everyone in the room, always** — idle, passing through, working a task, + or fighting a mob. Strong incentive to work fast, take cover, or leave. +- **Tech protection applies automatically.** Because the hazard has an attack type, the + matching protection tech reduces its damage: Kinetic Barrier (melee types), + Projectile Screen (`ranged`), Neural Firewall (`science`). +- **Safespots block all hazard damage.** A hidden player behind a safespot object takes + no hazard damage, regardless of hazard type. Note that doing a *melee* attack/work step + forces you out of cover — so ranged/science work lets you stay sheltered, melee does not. +- **Right gear negates it.** If `required_item` is set and equipped, the hazard is skipped + for that player entirely. The item must exist (validated at startup). +- **Hazards never interrupt movement.** Unlike a mob hit (which aborts a flee with + "Can't escape!"), a hazard hit does not touch your walk — you can keep moving out of + the danger zone while taking hits. + +### Entry warning + +Players walking from a **safe** room into a **hazardous** one get a `[Y/n]` confirmation +(pressing return defaults to yes; anything other than empty/`y`/`yes` cancels). Moving +between two hazardous rooms does not re-prompt. Players can disable the prompt with the +account option `danger_warning` (`option danger_warning false`). + +### Examine (forward-compatible) + +Because the hazard is a shared def referenced by ID, a future `examine sandstorm` can +resolve the current room's `hazard` ID, load the def, and render its attack type, accuracy, +max hit and description — no hidden objects required. diff --git a/building_guide/hidden_objects.md b/building_guide/hidden_objects.md new file mode 100644 index 0000000..e79c026 --- /dev/null +++ b/building_guide/hidden_objects.md @@ -0,0 +1,92 @@ +## Hidden Objects + +Objects with `hidden: true` don't appear in the room's object listing. Players discover them by reading room descriptions or trying commands. The object is still fully interactable — `push gate`, `look gate`, etc. + +```yaml +# data/objects/secret_lever.yaml +id: secret_lever +name: stone lever +hidden: true +description: "A cleverly concealed lever behind a loose stone." +use_interactions: + - condition: + flag: secret_passage_open + value: true + not: true + message: "You pull the lever. A grinding sound echoes from the east." + action: + set_flags: + secret_passage_open: true +``` + +Room description hints at it: +```yaml +description: "A dusty corridor. One of the wall stones looks slightly out of place." +``` + +### Multiple names (`aliases`) + +By default an object matches its `name` (word-prefix matching) and the words in +its `id`. Add `aliases` to accept extra names: + +```yaml +id: 1002_shuttle +name: landing craft # matches "landing", "craft", "landing craft" +aliases: [shuttle, ship] # also matches "shuttle" and "ship" +hidden: true +``` + +If a player's input matches more than one distinct object, `look` lists the +candidates ("That's ambiguous, which one?") instead of guessing — so keep aliases +specific enough to avoid overlap between objects in the same room. + +### Descriptions that react to flags + +An object can show different `look` text depending on the looking player's flags, +using a `descriptions` list (first matching condition wins). This is the simplest +way to fake per-player scenery — a crowd that becomes a queue, an NPC that +"appears" partway through a scene — without spawning real objects or mobs. + +**If none of the variants match, the object is treated as absent for that +player** — `look ` reports nothing is there. Combined with `hidden: true`, the +same shared object can appear only while a player's flags warrant it (e.g. during +an intro) and vanish on return visits. + +```yaml +id: 1002_crowd +name: crowd of people +aliases: [people, crowd, passengers] +hidden: true +descriptions: + - condition: # after the pilot arrives + all_of: + - player_flag: boarded + not: true + - player_flag: lined_up + text: "The passengers have formed a single-file line." + - condition: # before the pilot arrives + all_of: + - player_flag: boarded + not: true + - player_flag: lined_up + not: true + text: "A couple dozen anxious passengers mill about the pad." + # once `boarded` is set, no variant matches -> the crowd is gone +``` + +A plain `description:` (with no `descriptions:` list) is always present, as before. + +### Safespot Objects + +Safespot objects should always be `hidden: true`. They only become visible in `look` when the player's effective safespot tier meets the object's `safespot.tier` requirement. This creates a natural discovery mechanic: completing quests and achievements reveals new coverage opportunities. + +Players can discover safespots by: +- Reading room descriptions for hints +- Trying `hide ` (name matching works even for hidden objects) +- Using `look ` once they know the name +- Gaining tier through progression, which reveals safespots in `look` + +See [Safespots](objects.md#safespots) for the full YAML format. + +--- + diff --git a/building_guide/items.md b/building_guide/items.md new file mode 100644 index 0000000..52bfa7d --- /dev/null +++ b/building_guide/items.md @@ -0,0 +1,190 @@ +## Items + +```yaml +id: bronze_pickaxe +name: bronze pickaxe +color: "178" # xterm-256 color index (0-255) +aliases: ["pick", "pickaxe"] +description: "A sturdy bronze pickaxe." +value: 10 +stackable: false +equip_slot: main_hand # optional — where it equips +weapon_type: melee # optional — melee, ranged, or science +attack_type: stab # stab/slash/crush/ranged/science — determines accuracy type +stats: # optional — per-type combat bonuses + stab_attack: 4 + slash_attack: -2 + crush_attack: 2 + strength_bonus: 3 +speed: 5 # ticks between attacks +tool_type: pickaxe # used by gather behaviors that require "tool: pickaxe" +tool_speed: 2 # reduces gather wait time +requirements: # optional — skill levels needed to equip + accuracy: 1 +``` + +### Equipment Stats (ItemStats) + +Weapons have per-type **attack bonuses** — these determine accuracy based on the weapon's `attack_type`: + +| Field | Description | +|---|---| +| `stab_attack` | Accuracy bonus for stab attacks | +| `slash_attack` | Accuracy bonus for slash attacks | +| `crush_attack` | Accuracy bonus for crush attacks | +| `science_attack` | Accuracy bonus for science attacks | +| `ranged_attack` | Accuracy bonus for ranged attacks | + +Armor has per-type **defense bonuses** — the mob's `attack_type` determines which defense is used: + +| Field | Description | +|---|---| +| `stab_defense` | Defense vs stab attacks | +| `slash_defense` | Defense vs slash attacks | +| `crush_defense` | Defense vs crush attacks | +| `science_defense` | Defense vs science attacks (negative on metal armor) | +| `ranged_defense` | Defense vs ranged attacks | + +Other damage/utility bonuses: + +| Field | Description | +|---|---| +| `strength_bonus` | Melee max hit bonus | +| `ranged_strength` | Ranged max hit bonus (on ammo) | +| `science_damage` | Science max hit bonus | +| `technology_bonus` | Reduces tech battery drain rate | + +### Attack Type + +Weapons should always set `attack_type`. Common mappings: + +| Category | attack_type | +|---|---| +| Swords | slash | +| Daggers | stab | +| Axes | slash | +| Pickaxes | stab | +| Bows | ranged | +| Unarmed | crush (default) | + +### Equipment Requirements + +Higher-tier equipment requires skill levels to equip: + +```yaml +requirements: + accuracy: 20 # melee weapons + defense: 20 # armor + ranged: 30 # ranged weapons/armor +``` + +Standard tiers: bronze/iron (none), steel (5), black (10), mithril (20), adamant (30), rune (40), dragon (60). + +### Ranged Ammo + +Arrows and bolts use `ranged_strength` for damage. Accuracy comes from the bow: + +```yaml +id: iron_arrow +name: iron arrow +stackable: true +equip_slot: ammo +stats: + ranged_strength: 10 +``` + +### Color + +The `color` field accepts xterm-256 palette indices (0-255) with optional modifiers: + +```yaml +color: "178" # bronze/gold +color: "75 bold" # bold steel blue +color: "g:196,208,226" # gradient red → orange → gold +``` + +In ANSI mode, extended colors (16-255) automatically downgrade to the nearest ANSI color. Use `colortable` in-game to see all 256 colors. + +Key item (quest token, not equippable): +```yaml +id: pass_stub +name: pass stub +description: "A crumpled slip of paper stamped with the guard's seal." +value: 0 +stackable: false +``` + +--- + +## Firemaking + +```yaml +id: logs +name: logs +fire_level: 1 # firemaking level required +fire_xp: 40 # XP for burning/stoking +burn_ticks: 24 # how long the fire burns +``` + +## Food / Healing + +```yaml +id: bread +name: bread +color: "222" +description: "A fresh loaf of bread, still warm from the oven." +value: 5 +heal_value: 5 # positive = heal, negative = damage +eat_message: "You eat the bread. Warm and satisfying." +``` + +## MadeFrom — Item Combinations + +Define what items combine to make this item. Used by the `use` command. Combinations run through the production system — players are prompted "How many?" and the action loops with a timer. + +```yaml +id: bread_dough +name: bread dough +color: "222" +ticks: 2 +made_from: + - items: [pot_of_flour] + quantity: 1 + byproducts: [empty_pot] + - items: [bucket_of_water, pitcher_of_water] + quantity: 1 + byproducts: [empty_bucket, empty_pitcher] +``` + +Each entry is an ingredient slot. Multiple `items` means any of them works. The first matching item found in inventory is consumed. + +`ticks` controls how many game ticks each production cycle takes (default 2). + +### Byproducts + +`byproducts` is a list matching the `items` list by index. When `bucket_of_water` (items[0]) is consumed, `empty_bucket` (byproducts[0]) is returned to inventory. When `pitcher_of_water` (items[1]) is consumed, `empty_pitcher` (byproducts[1]) is returned instead. + +Use `""` for items that produce no byproduct: + +```yaml +made_from: + - items: [pot_of_flour, loose_flour] + quantity: 1 + byproducts: [empty_pot, ""] # pot returns empty_pot, loose flour returns nothing + - items: [bucket_of_water] + quantity: 1 + byproducts: [empty_bucket] +``` + +Omit `byproducts` entirely if no items in that slot produce a byproduct: + +```yaml +made_from: + - items: [pot_of_flour] + quantity: 1 + byproducts: [empty_pot] # returns empty_pot + - items: [salt] + quantity: 1 # no byproducts field — salt is consumed entirely +``` + +See also `recipes.md` for station-based recipes. diff --git a/building_guide/mobs.md b/building_guide/mobs.md new file mode 100644 index 0000000..c1f9332 --- /dev/null +++ b/building_guide/mobs.md @@ -0,0 +1,221 @@ +## Mobs + +Basic combat mob with per-type defense bonuses: +```yaml +id: "man" +name: "man" +description: "A shabby-looking man." +attack: 1 +strength: 1 +defense: 1 +hp: 7 +speed: 5 +aggressive: false +respawn_ticks: 30 +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 +drops: + remains: "bones" # always dropped on death + loot: + - item_id: "credits" + weight: 98 + quantity: 10 + - item_id: "credits" + weight: 2 + quantity: 150 +idle_descriptions: + - "scribbles something in a small notebook" + - "gazes skyward at the clouds" +combat_descriptions: + - "is engaged in a fight to the death with %s" +``` + +### 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. + +```yaml +id: goblin +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 +``` + +### Mob Attack/Defense Fields + +| Field | Description | +|---|---| +| `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_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 | +| `size` | Mob size for safespot blocking: small/medium/large/massive (default: medium) | + +### Protected/Unique Mobs + +Mob with talk behavior — can be talked to via `talk:` inline config: +```yaml +id: "guard" +name: "Guard" +talk: # inline talk config (no separate behavior file) + 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 +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" + - "adjusts the grip on his weapon" +``` + +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 on `) | +| `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. + +```yaml +id: solar_panel_frame +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 + loot: + - item_id: credits + weight: 95 + quantity: 40 + - item_id: coal + weight: 5 + quantity: 1 +``` + +Key points: + +- **All combat styles work.** Melee, ranged (consumes ammo as "supplies") and science decks (consume junk) all apply progress. The per-type `*_defense` values steer which method is *efficient* without forbidding any — a rock might have low `crush_defense` but high `stab_defense`. +- **XP and buffs are identical to combat** — driven by your attack style (accurate/aggressive/defensive/balanced) and split into the combat skills. This is a non-violent way to train Attack/Strength/Defense/Ranged/Science/Hitpoints. +- **No special equipment.** Task mobs are designed around the weapons and bonuses you already have. There is no separate "tool" equipment set. +- **Progress decays exactly like inverted mob regen.** A task you stop working on slowly loses progress over time (the same regen that heals combat mobs, displayed inverted). +- **One worker at a time**, the same 1-v-1 lock as combat. + +--- + + diff --git a/building_guide/objects.md b/building_guide/objects.md new file mode 100644 index 0000000..427968a --- /dev/null +++ b/building_guide/objects.md @@ -0,0 +1,392 @@ +## Objects + +Objects interact with the world through **inline behavior configs** under +`gather:`, `talk:`, or `use:` keys. There is no separate behavior +directory — everything goes directly in the object's YAML. + +Gathering object (mining): +```yaml +id: copper_rock +name: copper rock +color: "178" # xterm-256 color index (0-255) +gather: + skill: mining + level: 1 + xp: 17 + base_wait: 8 + tools: + - pickaxe + success: + base: 0.40 + per_level: 0.01 + cap: 0.95 + gather_message: "You swing your pickaxe at the rock..." + fail_message: "You chip away but get nothing useful." + drops: + - item_id: copper_ore + weight: 90 + depletes: true + message: "You manage to mine some {178}copper ore{/}." + - table: gem_table + weight: 10 + depletes: false + message: "You spot a glint of something valuable!" + respawn_timer: 50 + respawn_broadcast: "A glint of copper catches your eye from some {name}." +``` + +Tree object (woodcutting with shared depletion): +```yaml +id: oak_tree +name: oak tree +color: "113" +gather: + skill: woodcutting + level: 15 + xp: 37 + base_wait: 6 + tools: + - axe + success: + base: 0.40 + per_level: 0.01 + cap: 0.90 + gather_message: "You swing your axe at the oak tree..." + fail_message: "You swing but get no logs." + drops: + - item_id: oak_logs + weight: 100 + depletes: false + message: "You get some {113}oak logs{/}." + respawn_timer: 14 + deplete_timer: 45 + nest_chance: 256 + respawn_broadcast: "An {name} grows back." +``` + +Color accepts xterm-256 indices with optional modifiers (`bold`, `dim`, `underline`) +and gradients (`g:196,82`). In ANSI mode, extended colors downgrade to the nearest +ANSI color. + +Object interaction (gate, lever — uses `use_interactions:` key): +```yaml +id: iron_gate +name: iron gate +hidden: true +description: "A heavy iron gate set into the north wall." +use_interactions: + - condition: + flag: gate_open + value: true + not: true + message: "You push the heavy iron gate open." + action: + set_flags: + gate_open: true +``` + +Decorative object (no behavior keys — just a name/description): +```yaml +id: lumby_fountain +name: town fountain +description: "Clear water sparkles in the sunlight." +``` + +Talk object (NPC conversations — uses `talk:` key): +```yaml +id: tool_shed +name: Tool Shed +description: "A small shed with an open window." +talk: + nodes: + start: + message: "\"Welcome to the tool shed!\"" + options: + - text: "\"What do you have?\"" + goto: shop + - text: "\"Goodbye.\"" + end: true +``` + +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. + +--- + +## Use Interactions + +Objects can define `use_interactions` to handle when a player uses a specific item on the object. Each interaction can check conditions, show a message, and execute actions — using the same condition and action primitives as the talk system. + +Entries are checked top-to-bottom; the first match with a passing condition wins. + +Simple message (no action): +```yaml +id: anvil +name: anvil +use_interactions: + - item_id: silver_bar + message: "You should use this with a mold at a furnace." + - item_id: gold_bar + message: "You should use this with a mold at a furnace." +``` + +Puzzle interaction (take item, set flag): +```yaml +id: crystal_slot +name: crystal slot +use_interactions: + - item_id: crystal_key + condition: + flag: crystal_inserted + message: "The crystal key is already in the slot." + - item_id: crystal_key + message: "You insert the crystal key into the slot. It clicks into place." + action: + take_item: crystal_key + set_flags: + crystal_inserted: true +``` + +Quest item exchange: +```yaml +use_interactions: + - item_id: ancient_scroll + condition: + player_flag: quest_started + message: "You place the scroll on the pedestal. The door rumbles open!" + action: + take_item: ancient_scroll + set_player_flags: + quest_complete: true + set_flags: + temple_door_open: true +``` + +### UseInteraction Fields + +| Field | Type | Description | +| ----------- | ---------- | ------------------------------------------------ | +| `item_id` | string | Item ID that triggers this interaction | +| `condition` | Condition | Optional condition (same as exits/talk/use_interactions) | +| `message` | string | Message shown to the player | +| `action` | NodeAction | Optional actions (same as talk node actions) | + +### Available Actions (same as talk) + +| Field | Type | Description | +| ------------------ | -------------- | ---------------------------------- | +| `set_flags` | map[string]any | Set world flags (shared) | +| `set_player_flags` | map[string]any | Set player flags (per-character) | +| `give_item` | string | Give an item to inventory | +| `take_item` | string | Remove an item from inventory | +| `teleport` | int | Move player to a room ID | +| `heal` | int | Restore hitpoints | + +### Available Conditions (same as exits/talk) + +| Field | Description | +| ------------- | -------------------------------- | +| `flag` | World flag check | +| `player_flag` | Per-character flag check | +| `has_item` | Inventory item check | +| `value` | Expected value for flag checks | +| `not` | Invert the condition | +| `all_of` | All sub-conditions must pass | +| `any_of` | Any sub-condition must pass | + +--- + +## 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. + +--- + +## Safespots + +Safespot objects provide cover that blocks melee attacks. Players use `hide ` to +crouch behind them and attack with ranged or science weapons. Melee attacks force the player +out of cover. + +```yaml +id: rock_outcrop +name: rock outcrop +color: "248" +hidden: true +inroom_description: "A jagged rock outcrop juts from the floor." +description: "A large, jagged rock formation providing natural cover." +safespot: + tier: 2 + max_block_size: large + max_occupants: 3 + unsafe_chance: 0.008 + decay_ticks: 500 + decay_chance: 0.01 + respawn_on_hide: false + respawn_ticks: 300 + levels: + - message: "The rock formation stands solid." + degrade_message: "The rock structure begins crumbling!" + - message: "The rock outcrop is cracked and worn." + degrade_message: "A few jagged bits are all that's left of the rock outcrop!" + - message: "Only a few jagged rocks remain." + degrade_message: "The rock outcrop crumbles to nothing!" +``` + +Safespot objects should be `hidden: true`. They never appear in room descriptions — players +discover them through quest guidance, `inroom_description` text in the room's YAML, or by +examining objects directly with `look `. + +### SafespotConfig Fields + +| Field | Type | Default | Description | +|---|---|---|---| +| `tier` | int | 0 | Required safespot tier to use this object | +| `max_block_size` | string | `""` (all) | Largest mob size blocked: small/medium/large/massive | +| `max_occupants` | int | 0 | Max players that can hide behind this object (0 = unlimited) | +| `unsafe_chance` | float64 | 0 | Per-tick chance (0.0-1.0) to be forced out of cover | +| `decay_ticks` | float64 | 0 | Guaranteed ticks before object degrades one level | +| `decay_chance` | float64 | 0 | Per-tick random chance (0.0-1.0) to degrade one level | +| `respawn_on_hide` | bool | false | If true, hiding behind an empty safespot resets it to max level. Only triggers when no occupants are present. | +| `respawn_ticks` | float64 | 0 | Ticks until a destroyed safespot respawns (0 = never respawns — object is deleted from the room) | +| `levels` | []SafespotLevel | required | At least one level entry | + +### SafespotLevel Fields + +| Field | Type | Description | +|---|---|---| +| `message` | string | Shown to the player when the safespot is at this level | +| `degrade_message` | string | Broadcast to the room when the safespot degrades FROM this level | + +Levels are ordered from highest to lowest. The first entry is the safespot's best state; +the last entry is its weakest state before destruction. When the last level degrades, +the safespot is destroyed. + +### Three Safespot Patterns + +The combination of `respawn_on_hide` and `respawn_ticks` creates three distinct use cases: + +**1. Standard Safespot (always available)** + +```yaml +safespot: + respawn_on_hide: true + respawn_ticks: 0 + levels: + - message: "You crouch behind the rock formation, using it as cover." +``` + +The safespot starts at full health each time a player hides behind it (first occupant only — +subsequent players don't reset the level). Ideal for skilling areas and general exploration. +Since `respawn_ticks: 0`, a destroyed safespot would be deleted, but with a single level and +no decay, it never degrades. Simple and permanent. + +**2. Regenerating Safespot (timer-based respawn)** + +```yaml +safespot: + respawn_on_hide: false + respawn_ticks: 300 + levels: + - message: "The rock formation stands solid." + degrade_message: "The rock structure begins crumbling!" + - message: "The rock outcrop is cracked and worn." + degrade_message: "A few jagged bits are all that's left!" + - message: "Only a few jagged rocks remain." + degrade_message: "The rock outcrop crumbles to nothing!" +``` + +Degradation persists between hides — a player who leaves and re-hides finds the +safespot at its previous level. After total destruction, a timer (`respawn_ticks`) +counts down, then the safespot reforms at full health. Suits areas with heavy use +where a temporary respite is valuable. + +**3. Disposable Safespot (boss fight cover)** + +```yaml +safespot: + respawn_on_hide: false + respawn_ticks: 0 + levels: + - message: "The barricade offers solid cover." + degrade_message: "The barricade splinters under the assault!" + - message: "The barricade is splintering badly." + degrade_message: "The barricade shatters completely!" +``` + +Degradation persists between hides. When the last level degrades, the safespot is +**deleted from the room entirely** — it does not respawn. Perfect for boss encounters +where a room script places temporary cover that the party consumes during the fight. +Once destroyed, it's gone for good (until the boss room re-instantiates it). + +### Mob Size + +Mobs must have a `size` field for safespot blocking to work: + +```yaml +# data/mobs/cow.yaml +size: small + +# data/mobs/moss_giant.yaml +size: large +``` + +Size ordering: `small` < `medium` < `large` < `massive`. Defaults to `medium` when absent. +A safespot with `max_block_size: large` blocks mobs of size small, medium, and large, +but not massive. + +### Progression + +Players gain safespot tier by completing quests and combat achievements, which set +player flags via talk node actions: + +| Flag | Source | Tier bonus | +|---|---|---| +| `quest_animal_magnetism` | Quest completion | +1 | +| `quest_dragon_slayer` | Quest completion | +1 | +| `achieve_medium_combat` | Combat achievement | +1 | +| `achieve_hard_combat` | Combat achievement | +2 | + +### Combat Integration + +- Melee mobs (attack_type: stab/slash/crush) cannot initiate aggro or land hits on + safespotted players if the mob's size is within the safespot's `max_block_size`. +- Ranged and science mobs (attack_type: ranged/science) ignore safespots entirely. +- If a safespotted player attacks with melee, they automatically leave cover. +- Hiding in combat takes 4 ticks; if the mob hits during this time, the hide fails. +- Hiding out of combat takes 1 tick. + +### Option: safespot_alert + +Players can customize the message shown when forced out of a safespot: + + option safespot_alert "{196 bold}** DANGER **{/} Cover blown!" +Default: `"{196 bold}** Your safespot has been compromised! **{/}"` + +--- + diff --git a/building_guide/quests.md b/building_guide/quests.md new file mode 100644 index 0000000..b5f18fa --- /dev/null +++ b/building_guide/quests.md @@ -0,0 +1,103 @@ +## Complete Quest Example: "Clear the Rats" + +### 1. Quest giver mob (`data/mobs/quest_giver.yaml`) +```yaml +id: "quest_giver" +name: "Elder" +unique: true +protected: true +talk: + nodes: + start: + message: "\"Rats! Rats everywhere in the cellar. Clear them out and I'll reward you.\"" + options: + - text: "\"I'll handle it.\"" + goto: accept_quest + condition: + player_flag: rat_quest + not: true + - text: "\"I killed the rats.\"" + goto: turn_in + condition: + player_flag: rat_quest + value: started + - text: "\"Goodbye.\"" + end: true + + accept_quest: + message: "\"Good lad. The cellar is west of here. Come back when they're dead.\"" + action: + set_player_flags: + rat_quest: started + options: + - text: "\"On my way.\"" + end: true + + turn_in: + message: "\"You did it! The village owes you a debt. Here — take this.\"" + action: + give_item: rusty_sword + set_player_flags: + rat_quest: complete + heal: 10 + options: + - text: "\"Thanks!\"" + end: true +attack: 1 +strength: 1 +defense: 1 +hp: 20 +speed: 5 +aggressive: false +idle_descriptions: + - "mutters about the rat infestation" +``` + +Talk configs are inline on the mob under the `talk:` key. No separate behavior file needed. + +### 2. Rat mobs (`data/mobs/rat.yaml`) +```yaml +id: "rat" +name: "giant rat" +attack: 2 +strength: 1 +defense: 1 +hp: 3 +speed: 4 +aggressive: true +respawn_ticks: 60 +drops: + remains: "rat bones" +``` + +### 3. Cellar room (`data/rooms/20.yaml`) +```yaml +id: 20 +name: "Cellar" +description: "A damp, dark cellar. The floor scuttles with movement." +exits: + east: 1 +mobs: + - "rat" + - "rat" + - "rat" + - "rat" + - "rat" +``` + +### 4. Room 1 with quest giver (`data/rooms/1.yaml`) +```yaml +id: 1 +name: "Town Square" +description: "Cobblestone paths lead in all directions. The Elder stands near the fountain." +exits: + west: 20 + north: 2 +mobs: + - "quest_giver" +objects: + - id: lumby_fountain +``` + +--- + diff --git a/building_guide/recipes.md b/building_guide/recipes.md new file mode 100644 index 0000000..450ded9 --- /dev/null +++ b/building_guide/recipes.md @@ -0,0 +1,277 @@ +# The House of Icarus - World Building Guide + +## Crafting + +Crafting information lives on the **output item's YAML file** via the `craft:` block. There is no separate recipe directory. To find out how to make `bronze_dagger`, open `data/items/equipment/bronze_dagger.yaml` and look for the `craft:` block. + +### Craft YAML Reference + +| Field | Type | Description | +| -------------- | -------------- | ----------------------------------------------- | +| `type` | string | Craft type (pharmacy, smithing, cooking, smelting, crafting, fletching, construction, clean, or "" for skill-less) | +| `skill` | string | Skill checked (defaults to type if omitted) | +| `level` | int | Required skill level | +| `xp` | int | XP awarded on success | +| `wait` | float64 | Ticks per craft cycle | +| `station` | []string | Station object IDs required (optional) | +| `tool` | string | Required tool_type (optional) | +| `consume` | []ConsumeEntry | Ingredients consumed (see below) | +| `output_qty` | int | Quantity produced (default 1, for stackables) | +| `fail` | string | ItemID produced on failure (optional) | +| `message` | string | Success message per cycle (optional, see message defaults) | +| `fail_message` | string | Failure message (optional, empty = silent fail) | +| `start_message`| string | Message when action begins (optional, see message defaults) | +| `end_message` | string | Message when action completes (optional, see message defaults) | +| `steps` | []CraftStep | Multi-step messages at tick intervals (optional)| +| `success` | SuccessFormula | Optional skill check formula (see Behaviors) | + +The output is the item itself — no `output` field needed. The item ID IS the craft ID. + +### Message Variables + +All message fields (`message`, `fail_message`, `start_message`, `end_message`, and `steps[].message`) support variables that expand to colorized item names: + +| Variable | Expands to | +| -------- | ----------------------------------------------- | +| `%n` | Output item name (colored, e.g. "stim potion") | +| `%i1` | 1st consume entry's matched item (colored) | +| `%i2` | 2nd consume entry's matched item (colored) | +| `%b1` | 1st byproduct item name (colored, success only) | +| `%b2` | 2nd byproduct item name (colored, success only) | + +Numbering follows YAML consume/byproduct order. If a consume entry has multiple alternative items (`items: [a, b]`), the variable expands to whichever the player actually possesses, colored with that item's `color:` field. + +Variables work alongside inline color tags (`{196}text{/}`) which are expanded after variable substitution. + +### Message Defaults + +If a message field is omitted from the YAML, the game uses a skill-level default based on the craft `type`: + +| Type | Default `message` | Default `start_message` | +|------|-------------------|------------------------| +| `cooking` | `"Cooked to perfection. %n looks great!"` | `"You start cooking %i1."` | +| `smelting` | `"You remove a white hot %n!"` | `"You place the %i1 into the furnace."` | +| `smithing` | `"You smith a %n."` | `"You begin smithing %i1."` | +| `crafting` | `"You craft a %n."` | `"You begin crafting %i1."` | +| `fletching` | `"You fletch a %n."` | `"You begin fletching %i1."` | +| `pharmacy` | `"You mix a %n."` | `"You start mixing %i1."` | +| `construction` | `"You construct a %n."` | `"You begin constructing %i1."` | +| `clean` | `"You clean the %i1."` | `"You begin cleaning herbs."` | +| (unset) | `"You produce %n."` | `"You start %i1."` | + +Any of these can be overridden per-item by setting the field in YAML. For skills that never fail (smithing, crafting, fletching, pharmacy, construction), `fail_message` defaults to empty — no message is shown on failure. + +### CraftStep — Multi-Step Messages + +Optional interim messages fired at specific tick offsets during the craft cycle. The `tick` is the number of ticks elapsed since the start of the cycle. + +```yaml +craft: + type: "" + wait: 6 + consume: + - items: [map_piece_1] + quantity: 1 + - items: [map_piece_2] + quantity: 1 + - items: [map_piece_3] + quantity: 1 + steps: + - tick: 1 + message: "You try to puzzle how the pieces fit together." + - tick: 3 + message: "Aha, this edge lines up here!" + message: "You assemble the map!" +``` + +### ConsumeEntry — Multi-Item Ingredient Slots + +Each consume entry defines an ingredient slot with multiple valid items. The first matching item found in the player's inventory is consumed. + +```yaml +consume: + - items: [item_id, alternative_id, ...] + quantity: 1 + byproducts: [byproduct_for_item, byproduct_for_alt, ...] +``` + +`byproducts` is optional. When present, it matches the `items` list by index — consuming `items[0]` returns `byproducts[0]` to inventory. Use `""` for items with no byproduct: + +```yaml +consume: + - items: [bucket_of_water, vial_of_water] + quantity: 1 + byproducts: [empty_bucket, ""] # bucket returns empty, vial is consumed entirely + - items: [herb] + quantity: 1 # no byproducts — herb is consumed entirely +``` + +### Skill-Based Production + +```yaml +# data/items/consumables/stim_potion.yaml +craft: + type: pharmacy + skill: pharmacy + level: 3 + xp: 25 + wait: 4 + consume: + - items: [guam_potion_unf] + quantity: 1 + - items: [eye_of_newt] + quantity: 1 + message: "You mix a %n." # %n expands to "stim potion" colored +``` + +### Station-Based Production + +```yaml +# data/items/materials/bronze_bar.yaml +craft: + type: smelting + skill: smithing + level: 1 + xp: 6 + wait: 4 + station: [furnace] + consume: + - items: [copper_ore] + quantity: 1 + - items: [tin_ore] + quantity: 1 + message: "You smelt a %n." # default is "You remove a white hot %n!" + # overridden here for simpler flavor +``` + +```yaml +# data/items/equipment/bronze_dagger.yaml +craft: + type: smithing + skill: smithing + level: 1 + xp: 12 + wait: 4 + station: [anvil] + consume: + - items: [bronze_bar] + quantity: 1 + # message omitted — uses default "You smith a %n." +``` + +For stackable outputs, use `output_qty`: + +```yaml +# data/items/ammo/bronze_nails.yaml +craft: + type: smithing + skill: smithing + level: 4 + xp: 12 + wait: 4 + station: [anvil] + consume: + - items: [bronze_bar] + quantity: 1 + output_qty: 15 + # message omitted — uses default "You smith a %n." +``` + +### Tool-Based Production + +```yaml +# data/items/ammo/arrow_shafts.yaml +craft: + type: fletching + level: 1 + xp: 5 + wait: 3 + tool: knife + consume: + - items: [logs, oak_logs, willow_logs] + quantity: 1 + output_qty: 15 +``` + +The `tool` field requires the player to have an item with that `tool_type` equipped or in inventory. The tool is NOT consumed. + +### Skill-Less Combinations (Item-on-Item) + +Omit `type` (or leave it empty) for combinations with no skill check, no XP, and 100% success: + +```yaml +# data/items/consumables/bucket_of_water.yaml +craft: + wait: 0 + consume: + - items: [vial_of_water, jug_of_water] + quantity: 1 + byproducts: [empty_vial, empty_jug] + - items: [empty_bucket] + quantity: 1 + message: "You pour the %i1 into the %i2." # %i1 = water source, %i2 = bucket +``` + +### Multi-Piece Assembly (3+ items → 1) + +```yaml +# data/items/quest/ancient_map.yaml +craft: + wait: 0 + consume: + - items: [torn_page_1] + quantity: 1 + - items: [torn_page_2] + quantity: 1 + - items: [torn_page_3] + quantity: 1 + steps: + - tick: 1 + message: "You try to puzzle how the pieces fit together." + - tick: 3 + message: "Aha, this edge lines up here!" + message: "You assemble the %n." +``` + +### Clean Recipes + +Clean herb recipes use `type: clean` and are handled as background actions (one herb per cycle, directly mutating inventory): + +```yaml +# data/items/materials/guam.yaml (clean guam) +craft: + type: clean + skill: pharmacy + level: 3 + xp: 3 + wait: 2 + consume: + - items: [grimy_guam] + quantity: 1 + # message omitted — uses default "You clean the %i1." + # %i1 expands to the grimy herb name with its color +``` + +### Food/Healing Items + +Items with `heal_value` and `eat_message` can be consumed via the `eat` command. + +```yaml +id: bread +name: bread +color: "222" +description: "A fresh loaf of bread, still warm from the oven." +value: 5 +heal_value: 5 +eat_message: "You eat the bread. Warm and satisfying." +``` + +### Architecture + +The `CraftIndex` (`internal/game/craft_index.go`) is built once at startup from all item YAMLs that have a `craft:` block. It provides `O(1)` lookups by type, input item, and station. All crafting commands query the CraftIndex — no runtime file scanning. + +**Indexes:** +- `byType["pharmacy"]` → all pharmacy-craftable items (used by `mix`) +- `byInput["guam"]` → all items that consume guam (used by `use` to find combinations) +- `byStation["anvil"]` → all items craftable at an anvil (used by `use bar on anvil`) +- `FindByTwoInputs(a, b)` → items consuming both inputs in different consume entries diff --git a/building_guide/rooms.md b/building_guide/rooms.md new file mode 100644 index 0000000..735a6b4 --- /dev/null +++ b/building_guide/rooms.md @@ -0,0 +1,209 @@ +## Rooms + +Minimal room: +```yaml +id: 1 +name: "Town Square" +description: "Cobblestone paths lead in all directions. A fountain gurgles peacefully." +exits: + north: 2 + west: 7 + east: 3 +``` + +### Inline Color Tags + +Room descriptions support inline color tags using `{spec}text{/}` syntax. Untagged text uses the `room_desc` color. + +```yaml +description: "On the table lies a {182 bold}mysterious vase{/} with a rose in it." +``` + +Tag spec format: `{<0-255> [bold] [dim] [underline]}text{/}` + +Gradients: `{g:196,82}gradient text{/}`. Multi-stop: `{g:45,39,59}three stops{/}`. + +### Exits — simple vs conditional + +Simple exit — always passable: +```yaml +exits: + north: 2 +``` + +Conditional exit — blocked until a world flag is set: +```yaml +exits: + north: + room: 11 + condition: + flag: gate_open + blocked_message: "A heavy iron gate blocks the way north." +``` + +Conditional exit — blocked unless the PLAYER has a flag (key, permission, quest state): +```yaml +exits: + east: + room: 12 + condition: + player_flag: has_vault_key + blocked_message: "The vault door is locked. You need a key." +``` + +Conditional exit with compound condition — requires both a world flag AND a player flag: +```yaml +exits: + north: + room: 20 + condition: + all_of: + - flag: bridge_repaired + - player_flag: paid_toll + blocked_message: "The bridge is out, and the toll collector blocks the path." +``` + +Exit that sets flags when used — `set_flags` / `set_player_flags` are applied +only when the player actually moves through the exit (not when it's blocked): +```yaml +exits: + north: + room: 21 + condition: + player_flag: lined_up + set_player_flags: + boarded_shuttle: true # marks "left this area" on the way out +``` + +### Item Spawns — ground items that respawn + +```yaml +item_spawns: + - id: bronze_pickaxe + quantity: 1 + respawn_ticks: 30 # reappears 30 ticks (18 seconds) after being picked up + - id: copper_ore + quantity: 3 + respawn_ticks: 50 +``` + +### Mobs — NPCs placed in the room + +Simple string (no wandering): +```yaml +mobs: + - "newbie_trainer" + - "man" +``` + +With wander config per-instance: +```yaml +mobs: + - id: man + wander_interval: 10 # attempts to wander every 10 ticks + - id: man + wander_interval: 15 + wander_rooms: [1, 4, 5] # optional — only exit to these rooms +``` + +Mob wander config lives in the room YAML, not in the mob definition. This keeps mobs generic +so the same `man` can wander differently depending on where it's placed. Mobs wander through +legal (unconditioned) room exits. If no legal exits exist, the mob stays still. Mobs with +no `wander_interval` never wander. + +### Objects — interactive fixtures + +```yaml +objects: + - id: copper_rock # simple placement + - id: copper_rock # second instance + - id: fishing_spot + wander_rooms: [7, 8, 9] # teleports between these rooms + wander_interval: 12 # every 12 ticks + - id: iron_gate # hidden object (see below) +``` + +### On-enter scripts — messages and timed cutscenes when a player arrives + +Each `on_enter` step shows a `message`, optionally gated by a `condition`. Steps +whose condition fails are skipped. + +```yaml +on_enter: + - message: "The guard barks: \"State your business!\"" + condition: + player_flag: talked_to_guard + not: true # only the first visit + + - message: "The guard nods. \"Back again?\"" + condition: + player_flag: talked_to_guard # subsequent visits +``` + +**Timed cutscenes.** A step may also carry a `delay` (ticks to wait before it +fires) and/or set flags (`set_flags` / `set_player_flags`). If any surviving step +has a delay or sets a flag, the whole sequence runs as a scheduled cutscene; +plain message-only scripts still print instantly. + +```yaml +on_enter: + - condition: { player_flag: boarded, not: true } + delay: 5 + message: "Some of the crowd look you up and down." + - condition: { player_flag: boarded, not: true } + delay: 5 + message: "The pilot calls out: \"Tickets, please! Nice and orderly!\"" + set_player_flags: + lined_up: true # opens an exit, flips a description, etc. + - condition: { player_flag: boarded, not: true } + message: "The crowd forms a single-file line." +``` + +Notes: +- `delay` counts ticks before the step fires; `delay: 0` (or omitted) fires on the next tick. +- Conditions are evaluated **once** on entry, so a flag a step sets won't cancel a later step in the same sequence. +- Gate a cutscene on a flag the sequence itself sets (above, `lined_up`) so it doesn't replay on a return visit. +- If a player disconnects mid-cutscene it resumes on reconnect, so they can't get stuck behind an exit the cutscene was meant to open. Plain `on_enter` messages never replay on login. + +### Conditional room descriptions + +A room can show a different description depending on the looking player's flags. +`descriptions` are checked top-to-bottom; the first whose condition passes wins, +otherwise the base `description` is used. + +```yaml +description: "A large, empty concrete pad in the middle of the ocean." +descriptions: + - condition: + player_flag: boarded + not: true + text: "A concrete pad swarming with a couple dozen anxious passengers." +``` + +### Hazardous rooms — environmental danger + +A room can reference a shared hazard by ID. The hazard rolls an attack against +everyone in the room every few ticks using the combat formulas (see `hazards.md`). + +```yaml +id: 99999181 +name: "Exposed Solar Array" +hazard: solar_radiation # ID of a data/hazards/.yaml definition +exits: + south: 99999180 +objects: + - id: rock_outcrop # a safespot object shields players from the hazard +mobs: + - solar_panel_frame # a task worksite (see mobs.md > Task Mobs) +``` + +- Players are warned with a `[Y/n]` prompt before walking from a **safe** room into a + **hazardous** one (unless they disable the `danger_warning` option). Moving between + two hazardous rooms does not re-prompt. +- A safespot object (an overhang, alcove, rock outcrop, …) shields a hidden player from + **all** hazard damage — but performing a melee attack/work step forces you out of cover. +- Hazards stack with mobs: an aggressive mob in a hazardous room hits you while the room + hazard also rolls against you. + +--- + diff --git a/building_guide/search.md b/building_guide/search.md new file mode 100644 index 0000000..e6e2a14 --- /dev/null +++ b/building_guide/search.md @@ -0,0 +1,15 @@ +## The `search` Command + +Used to open searchable items in your inventory (bird's nests, etc.): +``` +Usage: search + +Example: search nest + search birds nest +``` + +Searches your inventory for the named item. If found, removes it and rolls on a drop table. +Currently only bird's nests are searchable. + +--- + diff --git a/building_guide/state.md b/building_guide/state.md new file mode 100644 index 0000000..2eefcc4 --- /dev/null +++ b/building_guide/state.md @@ -0,0 +1,8 @@ +## State: World vs Player + +**World flags** (`set_flags`, checked with `flag`) are shared by every player on the server. A door opened by one player is open for everyone. A lever pulled once changes the world for all. + +**Player flags** (`set_player_flags`, checked with `player_flag`) are per-character. Quest progress, "has read the sign," "paid the toll" — these are different for each player. Saved to the character YAML and persist across logins. + +--- + diff --git a/building_guide/tips.md b/building_guide/tips.md new file mode 100644 index 0000000..307ea1e --- /dev/null +++ b/building_guide/tips.md @@ -0,0 +1,21 @@ +## Tips + +1. **Use player flags for quest progress, world flags for environmental state.** If a bridge is repaired, that's world state. If a player has read a sign, that's player state. + +2. **Conditions on enter scripts** make rooms feel alive. A guard who only barks the first time, a room that changes after a quest completes. + +3. **Hidden objects** keep room descriptions clean. Mention them in the room's description text instead of auto-listing them. + +4. **Drop tables** are shareable. The `gem_table` is used by copper rocks AND mob loot. Define once, reference everywhere. + +5. **Objects are for fixtures, mobs are for living things.** If it has HP and can die, it's a mob. If it's a rock, lever, door, or crafting station, it's an object. Both can have behaviors. + +6. **The `talk` field on mobs** lets you talk to them directly — `talk guard` finds the guard mob, no duplicate object entry needed. + +7. **Exits accept both `int` and `map` formats.** `north: 2` is shorthand for `north: {room: 2}`. Add `condition` and `blocked_message` only when needed. Mob room entries accept both `"man"` and `{id: man, ...}`. + +8. **Live editing works.** Room, item, mob, object, and behavior YAML files are read from disk on each access. Change a room description or dialog and it takes effect immediately — no restart needed. + +9. **Shared depletion vs per-drop depletion.** Use `deplete_timer` for trees — the timer counts down while being chopped and regens when left alone. Use `depletes: true` on individual drops for rocks — they deplete on first successful gather. + +10. **Mob wander config goes in the room YAML**, not the mob definition. This lets the same `man` wander differently in different rooms. diff --git a/building_guide/wandering_mobs.md b/building_guide/wandering_mobs.md new file mode 100644 index 0000000..8d49ed9 --- /dev/null +++ b/building_guide/wandering_mobs.md @@ -0,0 +1,20 @@ +## Wandering Mobs + +Mob wandering is configured per-instance in the room YAML, not on the mob definition: +```yaml +# In a room: +mobs: + - id: man + wander_interval: 10 # attempt to wander every 10 ticks + - id: man + wander_interval: 15 + wander_rooms: [5, 6, 7] # optional — restrict which rooms via exits +``` + +Mobs wander through legal (unconditioned) room exits. When the wander interval expires, +the mob picks a random exit with no conditions and moves through it. If `wander_rooms` +is set, only exits leading to those room IDs are legal. Mobs without `wander_interval` +never wander. Mobs stop wandering while in combat. Dead mobs respawn at their home room. + +--- + diff --git a/building_guide/wandering_objects.md b/building_guide/wandering_objects.md new file mode 100644 index 0000000..f1f91c5 --- /dev/null +++ b/building_guide/wandering_objects.md @@ -0,0 +1,39 @@ +## Wandering Objects + +Fishing spots that move between rooms: +```yaml +# data/objects/fishing_spot.yaml +id: fishing_spot +name: fishing spot +gather: + skill: fishing + level: 1 + xp: 10 + base_wait: 4 + tools: + - fishing_rod + bait: fishing_bait + success: + base: 0.30 + per_level: 0.01 + cap: 0.90 + gather_message: "You cast your line into the water..." + drops: + - item_id: raw_trout + weight: 100 + message: "You catch a raw trout!" +``` + +```yaml +# In a room: +objects: + - id: fishing_spot + wander_rooms: [7, 8, 9] + wander_interval: 12 +``` + +Objects teleport between rooms in their `wander_rooms` list. Players gathering from a +wandering object are silently interrupted when it moves. + +--- + diff --git a/cmd/mud/main.go b/cmd/mud/main.go index e9d0aed..773d3c9 100644 --- a/cmd/mud/main.go +++ b/cmd/mud/main.go @@ -42,6 +42,7 @@ func main() { g.Ticks.Subscribe(1, func() bool { g.MoveTick() g.ProcessQueuedCommands() + g.EnterSeqTick() g.World.Tick() g.MobStore.Tick() g.RegenTick() @@ -58,6 +59,7 @@ func main() { g.FarmTick() g.BuffTick() g.SafespotTick() + g.HazardTick() return true }) diff --git a/config.yaml b/config.yaml index ffe9d3b..5c3e3bc 100644 --- a/config.yaml +++ b/config.yaml @@ -1,5 +1,8 @@ game: tick_length: 600 + startup_validation: + check_sources: [1001] + ignore_unreachable: [] colors: room_name: "81 bold" diff --git a/data/courses/reactor.yaml b/data/courses/reactor.yaml index fcf2824..22d1a2b 100644 --- a/data/courses/reactor.yaml +++ b/data/courses/reactor.yaml @@ -1,10 +1,10 @@ id: "reactor" name: "Reactor Course" required_level: 50 -start_room: 200 +start_room: 99999200 completion_xp: 350 obstacles: - - room_id: 220 + - room_id: 99999220 verb: climb ticks_per_phase: 3 xp: 40 @@ -13,7 +13,7 @@ obstacles: - "You grip the reactor scaffolding and begin your ascent..." - "The metal groans under your weight as you climb higher, heat radiating from below..." - "You pull yourself onto the upper platform, the reactor humming beneath you!" - - room_id: 221 + - room_id: 99999221 verb: swing ticks_per_phase: 3 xp: 45 @@ -22,7 +22,7 @@ obstacles: - "You seize the heavy coolant chain dangling above the reactor pit..." - "You swing out over the glowing core, heat blasting your face..." - "You release and land hard on the maintenance gantry, chain clanging behind you!" - - room_id: 222 + - room_id: 99999222 verb: balance ticks_per_phase: 3 xp: 50 @@ -31,7 +31,7 @@ obstacles: - "You step onto the massive steam pipe spanning the reactor chamber..." - "Steam jets hiss from valves on either side as you shuffle along the pipe..." - "You reach the junction platform and hop off the pipe with relief!" - - room_id: 223 + - room_id: 99999223 verb: jump ticks_per_phase: 2 xp: 45 @@ -40,7 +40,7 @@ obstacles: - "A section of the reactor platform is missing, leaving a gaping void..." - "You back up, sprint, and leap with everything you've got..." - "You slam into the far platform and roll to safety!" - - room_id: 224 + - room_id: 99999224 verb: crawl ticks_per_phase: 3 xp: 40 @@ -49,7 +49,7 @@ obstacles: - "You squeeze into the narrow service conduit, radiation warnings plastered on every surface..." - "You drag yourself through on your elbows, sparks showering from damaged wiring above..." - "You tumble out the far end and gulp down clean air!" - - room_id: 225 + - room_id: 99999225 verb: vault ticks_per_phase: 2 xp: 50 @@ -58,7 +58,7 @@ obstacles: - "A radiation containment barrier blocks the path, humming with energy..." - "You time the pulse cycle, sprint at the barrier, and throw yourself over it..." - "You clear the barrier and land on the other side, heart pounding!" - - room_id: 226 + - room_id: 99999226 verb: leap ticks_per_phase: 3 xp: 55 diff --git a/data/courses/rooftop.yaml b/data/courses/rooftop.yaml index 4cc6748..3a24f89 100644 --- a/data/courses/rooftop.yaml +++ b/data/courses/rooftop.yaml @@ -1,10 +1,10 @@ id: "rooftop" name: "Rooftop Course" required_level: 20 -start_room: 200 +start_room: 99999200 completion_xp: 120 obstacles: - - room_id: 210 + - room_id: 99999210 verb: climb ticks_per_phase: 2 xp: 15 @@ -13,7 +13,7 @@ obstacles: - "You grab the rough permacrete wall and begin to climb..." - "Your fingers find cracks and ledges as you pull yourself higher..." - "You heave yourself over the edge and onto the rooftop!" - - room_id: 211 + - room_id: 99999211 verb: jump ticks_per_phase: 2 xp: 22 @@ -22,7 +22,7 @@ obstacles: - "You eye the gap between this building and the next..." - "You sprint toward the edge, boots pounding on the rooftop..." - "You launch yourself across and roll to a stop on the other roof!" - - room_id: 212 + - room_id: 99999212 verb: swing ticks_per_phase: 3 xp: 20 @@ -31,7 +31,7 @@ obstacles: - "You grab the dangling power cable with both hands..." - "You kick off the ledge and swing out over the street far below..." - "You release at the peak of the arc and land on the opposite platform!" - - room_id: 213 + - room_id: 99999213 verb: balance ticks_per_phase: 3 xp: 18 @@ -40,7 +40,7 @@ obstacles: - "You step onto the narrow structural beam spanning the alley..." - "The beam sways slightly as you inch forward, arms out for balance..." - "You reach the far side and step gratefully onto solid rooftop!" - - room_id: 214 + - room_id: 99999214 verb: vault ticks_per_phase: 2 xp: 15 @@ -49,7 +49,7 @@ obstacles: - "You run toward the maintenance railing at full speed..." - "You plant one hand on the rail and swing your legs over..." - "You clear the railing and land in a crouch on the other side!" - - room_id: 215 + - room_id: 99999215 verb: leap ticks_per_phase: 2 xp: 20 diff --git a/data/courses/vent_shaft.yaml b/data/courses/vent_shaft.yaml index a59a16c..643b39f 100644 --- a/data/courses/vent_shaft.yaml +++ b/data/courses/vent_shaft.yaml @@ -1,10 +1,10 @@ id: "vent_shaft" name: "Ventilation Shaft Course" required_level: 1 -start_room: 200 +start_room: 99999200 completion_xp: 40 obstacles: - - room_id: 201 + - room_id: 99999201 verb: scramble ticks_per_phase: 2 xp: 8 @@ -13,7 +13,7 @@ obstacles: - "You approach the corroded ventilation wall..." - "You find footholds in the rusted panels and begin to climb..." - "You scramble up the wall and haul yourself onto the ledge!" - - room_id: 202 + - room_id: 99999202 verb: balance ticks_per_phase: 2 xp: 8 @@ -22,7 +22,7 @@ obstacles: - "You step onto the narrow coolant pipe..." - "Arms outstretched, you carefully place one foot in front of the other..." - "You reach the other side of the pipe and step onto solid ground!" - - room_id: 203 + - room_id: 99999203 verb: jump ticks_per_phase: 2 xp: 7 @@ -31,7 +31,7 @@ obstacles: - "You peer across the dark gap in the shaft floor..." - "You take a few steps back, then sprint toward the edge..." - "You leap across the gap and land safely on the other side!" - - room_id: 204 + - room_id: 99999204 verb: crawl ticks_per_phase: 3 xp: 7 @@ -40,7 +40,7 @@ obstacles: - "You drop to your hands and knees at the narrow vent opening..." - "You squeeze through the tight passage, metal scraping against your back..." - "You emerge from the vent and stand up, brushing dust from your clothes!" - - room_id: 205 + - room_id: 99999205 verb: slide ticks_per_phase: 2 xp: 10 diff --git a/data/hazards/solar_radiation.yaml b/data/hazards/solar_radiation.yaml new file mode 100644 index 0000000..40afee6 --- /dev/null +++ b/data/hazards/solar_radiation.yaml @@ -0,0 +1,12 @@ +id: solar_radiation +name: scorching solar radiation +description: "Unfiltered sunlight hammers down through a gap in the atmospheric dome. Without shade or shielding, the radiation burns exposed skin and overheats equipment." +attack_type: science +attack: 40 +attack_bonus: 0 +max_hit: 6 +speed: 5 +warn_message: "The dome shielding fails here — raw solar radiation pours in." +hit_message: "Solar radiation sears you for %d damage." +miss_message: "You shield your eyes and weather a wave of solar glare." +required_item: "" diff --git a/data/help/combat.yaml b/data/help/combat.yaml index 210a6da..f588d81 100644 --- a/data/help/combat.yaml +++ b/data/help/combat.yaml @@ -11,7 +11,7 @@ description: | but weak crush defense takes more hits from a mace than a sword. Use the 'style' command to change your combat style: - accurate - +3 attack level, XP to attack + accurate - +3 accuracy level, XP to accuracy aggressive - +3 strength level, XP to strength defensive - +3 defense level, XP to defense balanced - +1 to all, XP split evenly diff --git a/data/help/drink.yaml b/data/help/drink.yaml index 98843b3..b9506d7 100644 --- a/data/help/drink.yaml +++ b/data/help/drink.yaml @@ -6,7 +6,7 @@ description: | Usage: drink Potions provide various effects: - - Stim potions: boost Attack + - Stim potions: boost Accuracy - Amp potions: boost Strength - Shield potions: boost Defense - Tech serum: boosts Technology diff --git a/data/help/prompt.yaml b/data/help/prompt.yaml index 2b35f41..37a98f9 100644 --- a/data/help/prompt.yaml +++ b/data/help/prompt.yaml @@ -23,7 +23,7 @@ description: | %S Attack style longform (accurate, aggressive, defensive, balanced) %i Free inventory slots %x_ XP to next level for a skill (e.g. %x_mining) - %X_ Total XP for a skill (e.g. %X_attack) + %X_ Total XP for a skill (e.g. %X_accuracy) Use %% for a literal % sign. diff --git a/data/help/style.yaml b/data/help/style.yaml index c4f4e67..663dc07 100644 --- a/data/help/style.yaml +++ b/data/help/style.yaml @@ -6,7 +6,7 @@ description: | Changes your combat style. Prefix matching is supported. Styles: - accurate - +3 to attack level, XP goes to Attack + accurate - +3 to accuracy level, XP goes to Accuracy aggressive - +3 to strength level, XP goes to Strength defensive - +3 to defense level, XP goes to Defense balanced - +1 to all three, XP split evenly diff --git a/data/items/consumables/stim_potion.yaml b/data/items/consumables/stim_potion.yaml index 34e7282..4ae10ff 100644 --- a/data/items/consumables/stim_potion.yaml +++ b/data/items/consumables/stim_potion.yaml @@ -17,6 +17,6 @@ id: stim_potion name: stim potion potion_bonus: 10 potion_duration: 200 -potion_effect: attack +potion_effect: accuracy stackable: false value: 30 diff --git a/data/items/consumables/super_stim.yaml b/data/items/consumables/super_stim.yaml index 00f3aa1..f8e4756 100644 --- a/data/items/consumables/super_stim.yaml +++ b/data/items/consumables/super_stim.yaml @@ -17,6 +17,6 @@ id: super_stim name: super stim potion_bonus: 20 potion_duration: 200 -potion_effect: attack +potion_effect: accuracy stackable: false value: 120 diff --git a/data/items/equipment/adamant_dagger.yaml b/data/items/equipment/adamant_dagger.yaml index a483667..7875c3a 100644 --- a/data/items/equipment/adamant_dagger.yaml +++ b/data/items/equipment/adamant_dagger.yaml @@ -17,7 +17,7 @@ equip_slot: main_hand id: adamant_dagger name: adamant dagger requirements: - attack: 30 + accuracy: 30 speed: 3 stackable: false stats: diff --git a/data/items/equipment/adamant_sword.yaml b/data/items/equipment/adamant_sword.yaml index cce058c..b1aaccd 100644 --- a/data/items/equipment/adamant_sword.yaml +++ b/data/items/equipment/adamant_sword.yaml @@ -17,7 +17,7 @@ equip_slot: main_hand id: adamant_sword name: adamant sword requirements: - attack: 30 + accuracy: 30 speed: 4 stackable: false stats: diff --git a/data/items/equipment/black_axe.yaml b/data/items/equipment/black_axe.yaml index e708645..49fc8f5 100644 --- a/data/items/equipment/black_axe.yaml +++ b/data/items/equipment/black_axe.yaml @@ -16,4 +16,4 @@ speed: 5 tool_type: axe tool_speed: 2.5 requirements: - attack: 10 + accuracy: 10 diff --git a/data/items/equipment/dragon_axe.yaml b/data/items/equipment/dragon_axe.yaml index ddc9758..caef5bb 100644 --- a/data/items/equipment/dragon_axe.yaml +++ b/data/items/equipment/dragon_axe.yaml @@ -16,4 +16,4 @@ speed: 5 tool_type: axe tool_speed: 1 requirements: - attack: 60 + accuracy: 60 diff --git a/data/items/equipment/mithril_dagger.yaml b/data/items/equipment/mithril_dagger.yaml index 0d531e1..8543198 100644 --- a/data/items/equipment/mithril_dagger.yaml +++ b/data/items/equipment/mithril_dagger.yaml @@ -17,7 +17,7 @@ equip_slot: main_hand id: mithril_dagger name: mithril dagger requirements: - attack: 20 + accuracy: 20 speed: 3 stackable: false stats: diff --git a/data/items/equipment/mithril_sword.yaml b/data/items/equipment/mithril_sword.yaml index 53dcd25..397d044 100644 --- a/data/items/equipment/mithril_sword.yaml +++ b/data/items/equipment/mithril_sword.yaml @@ -17,7 +17,7 @@ equip_slot: main_hand id: mithril_sword name: mithril sword requirements: - attack: 20 + accuracy: 20 speed: 4 stackable: false stats: diff --git a/data/items/equipment/rune_dagger.yaml b/data/items/equipment/rune_dagger.yaml index f1f53f6..2068380 100644 --- a/data/items/equipment/rune_dagger.yaml +++ b/data/items/equipment/rune_dagger.yaml @@ -17,7 +17,7 @@ equip_slot: main_hand id: rune_dagger name: rune dagger requirements: - attack: 40 + accuracy: 40 speed: 3 stackable: false stats: diff --git a/data/items/equipment/rune_sword.yaml b/data/items/equipment/rune_sword.yaml index 3451450..12daf4d 100644 --- a/data/items/equipment/rune_sword.yaml +++ b/data/items/equipment/rune_sword.yaml @@ -17,7 +17,7 @@ equip_slot: main_hand id: rune_sword name: rune sword requirements: - attack: 40 + accuracy: 40 speed: 4 stackable: false stats: diff --git a/data/items/equipment/steel_dagger.yaml b/data/items/equipment/steel_dagger.yaml index 9df7d14..fc9f81f 100644 --- a/data/items/equipment/steel_dagger.yaml +++ b/data/items/equipment/steel_dagger.yaml @@ -17,7 +17,7 @@ equip_slot: main_hand id: steel_dagger name: steel dagger requirements: - attack: 5 + accuracy: 5 speed: 3 stackable: false stats: diff --git a/data/items/equipment/steel_sword.yaml b/data/items/equipment/steel_sword.yaml index b0ca858..66023f4 100644 --- a/data/items/equipment/steel_sword.yaml +++ b/data/items/equipment/steel_sword.yaml @@ -17,7 +17,7 @@ equip_slot: main_hand id: steel_sword name: steel sword requirements: - attack: 5 + accuracy: 5 speed: 4 stackable: false stats: diff --git a/data/items/tools/adamant_axe.yaml b/data/items/tools/adamant_axe.yaml index 81232fe..0172019 100644 --- a/data/items/tools/adamant_axe.yaml +++ b/data/items/tools/adamant_axe.yaml @@ -16,4 +16,4 @@ speed: 5 tool_type: axe tool_speed: 2 requirements: - attack: 30 + accuracy: 30 diff --git a/data/items/tools/adamant_pickaxe.yaml b/data/items/tools/adamant_pickaxe.yaml index 4296b0d..862f6e6 100644 --- a/data/items/tools/adamant_pickaxe.yaml +++ b/data/items/tools/adamant_pickaxe.yaml @@ -16,4 +16,4 @@ speed: 5 tool_type: pickaxe tool_speed: 0.75 requirements: - attack: 30 + accuracy: 30 diff --git a/data/items/tools/black_pickaxe.yaml b/data/items/tools/black_pickaxe.yaml index c3df955..083ad3a 100644 --- a/data/items/tools/black_pickaxe.yaml +++ b/data/items/tools/black_pickaxe.yaml @@ -16,4 +16,4 @@ speed: 5 tool_type: pickaxe tool_speed: 1 requirements: - attack: 10 + accuracy: 10 diff --git a/data/items/tools/dragon_pickaxe.yaml b/data/items/tools/dragon_pickaxe.yaml index 6f187b8..20b43b1 100644 --- a/data/items/tools/dragon_pickaxe.yaml +++ b/data/items/tools/dragon_pickaxe.yaml @@ -16,4 +16,4 @@ speed: 5 tool_type: pickaxe tool_speed: 0.25 requirements: - attack: 60 + accuracy: 60 diff --git a/data/items/tools/mithril_axe.yaml b/data/items/tools/mithril_axe.yaml index ca18195..f153f12 100644 --- a/data/items/tools/mithril_axe.yaml +++ b/data/items/tools/mithril_axe.yaml @@ -16,4 +16,4 @@ speed: 5 tool_type: axe tool_speed: 2.5 requirements: - attack: 20 + accuracy: 20 diff --git a/data/items/tools/mithril_pickaxe.yaml b/data/items/tools/mithril_pickaxe.yaml index 5da6937..81da209 100644 --- a/data/items/tools/mithril_pickaxe.yaml +++ b/data/items/tools/mithril_pickaxe.yaml @@ -16,4 +16,4 @@ speed: 5 tool_type: pickaxe tool_speed: 1 requirements: - attack: 20 + accuracy: 20 diff --git a/data/items/tools/rune_axe.yaml b/data/items/tools/rune_axe.yaml index a694bdf..bc99732 100644 --- a/data/items/tools/rune_axe.yaml +++ b/data/items/tools/rune_axe.yaml @@ -16,4 +16,4 @@ speed: 5 tool_type: axe tool_speed: 1.5 requirements: - attack: 40 + accuracy: 40 diff --git a/data/items/tools/rune_pickaxe.yaml b/data/items/tools/rune_pickaxe.yaml index 265231d..7691513 100644 --- a/data/items/tools/rune_pickaxe.yaml +++ b/data/items/tools/rune_pickaxe.yaml @@ -16,4 +16,4 @@ speed: 5 tool_type: pickaxe tool_speed: 0.5 requirements: - attack: 40 + accuracy: 40 diff --git a/data/items/tools/steel_axe.yaml b/data/items/tools/steel_axe.yaml index 6f2ac99..1f62e33 100644 --- a/data/items/tools/steel_axe.yaml +++ b/data/items/tools/steel_axe.yaml @@ -16,4 +16,4 @@ speed: 5 tool_type: axe tool_speed: 3 requirements: - attack: 5 + accuracy: 5 diff --git a/data/items/tools/steel_pickaxe.yaml b/data/items/tools/steel_pickaxe.yaml index cc048a1..560da07 100644 --- a/data/items/tools/steel_pickaxe.yaml +++ b/data/items/tools/steel_pickaxe.yaml @@ -16,4 +16,4 @@ speed: 5 tool_type: pickaxe tool_speed: 1.25 requirements: - attack: 5 + accuracy: 5 diff --git a/data/mobs/solar_panel_frame.yaml b/data/mobs/solar_panel_frame.yaml new file mode 100644 index 0000000..4270bda --- /dev/null +++ b/data/mobs/solar_panel_frame.yaml @@ -0,0 +1,34 @@ +id: solar_panel_frame +name: solar panel frame +description: "A half-assembled photovoltaic array on a steel frame. Panels, brackets and cabling are laid out, waiting to be fitted. This is work, not a fight — fix the panels into place to complete the array." +kind: task +verb: assemble +progress_noun: assembly +complete_message: "You bolt the final panel into place and the solar array hums to life!" +hp: 40 +defense: 10 +speed: 5 +aggressive: false +respawn_ticks: 40 +attack_type: crush +crush_defense: 0 +stab_defense: 40 +slash_defense: 40 +ranged_defense: 10 +science_defense: 10 +idle_descriptions: + - "stands half-assembled, panels waiting to be fitted" + - "has loose cabling dangling from an open junction box" + - "catches the light on its few mounted panels" +size: medium +drops: + loot: + - item_id: credits + weight: 70 + quantity: 40 + - item_id: credits + weight: 25 + quantity: 90 + - item_id: coal + weight: 5 + quantity: 1 diff --git a/data/objects/1002_crowd.yaml b/data/objects/1002_crowd.yaml new file mode 100644 index 0000000..8f8f099 --- /dev/null +++ b/data/objects/1002_crowd.yaml @@ -0,0 +1,18 @@ +id: 1002_crowd +name: crowd of people +aliases: [people, crowd, group, passengers] +hidden: true +descriptions: + - condition: + all_of: + - player_flag: denali_intro_done + not: true + - player_flag: denali_lined_up + text: "The passengers have sorted themselves into a single-file line, tickets clutched in hand, shuffling toward the craft." + - condition: + all_of: + - player_flag: denali_intro_done + not: true + - player_flag: denali_lined_up + not: true + text: "A couple dozen anxious passengers mill about the pad, glancing between you and the small building to the north." diff --git a/data/objects/1002_pilot.yaml b/data/objects/1002_pilot.yaml new file mode 100644 index 0000000..61264b2 --- /dev/null +++ b/data/objects/1002_pilot.yaml @@ -0,0 +1,10 @@ +id: 1002_pilot +name: pilot +hidden: true +descriptions: + - condition: + all_of: + - player_flag: denali_intro_done + not: true + - player_flag: denali_lined_up + text: "The shuttle pilot stands by the hatch in a faded flight suit, waving passengers into line and checking tickets with a practiced eye." diff --git a/data/objects/1002_shuttle.yaml b/data/objects/1002_shuttle.yaml new file mode 100644 index 0000000..6ac3a90 --- /dev/null +++ b/data/objects/1002_shuttle.yaml @@ -0,0 +1,9 @@ +id: 1002_shuttle +name: landing craft +aliases: [shuttle, ship] +hidden: true +descriptions: + - condition: + player_flag: denali_intro_done + not: true + text: "The battered landing craft that carried you down still ticks and steams as it cools on the pad." diff --git a/data/rooms/agility/200.yaml b/data/rooms/agility/200.yaml deleted file mode 100644 index 469e214..0000000 --- a/data/rooms/agility/200.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 200 -name: "Agility Training Grounds" -description: "A cavernous space beneath the asteroid's surface, repurposed as a training facility. Scaffolding, pipes, and platforms fill the chamber. Signs point to three courses of increasing difficulty: {33}Ventilation Shaft{/} (beginner), {214}Rooftop{/} (intermediate), and {196}Reactor{/} (advanced)." -exits: - south: 17 - north: 201 - east: 210 - west: 220 diff --git a/data/rooms/agility/201.yaml b/data/rooms/agility/201.yaml deleted file mode 100644 index 2beb2ec..0000000 --- a/data/rooms/agility/201.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 201 -name: "Ventilation Shaft - Corroded Wall" -description: "A towering wall of corroded ventilation panels rises before you. Rust-eaten handholds and buckled seams offer a treacherous path upward. The air smells of old metal and recycled atmosphere." -on_enter: - - message: "Type 'scramble' to climb the wall." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/202.yaml b/data/rooms/agility/202.yaml deleted file mode 100644 index b9c0292..0000000 --- a/data/rooms/agility/202.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 202 -name: "Ventilation Shaft - Coolant Pipe Walkway" -description: "A narrow coolant pipe stretches across a dark chasm. Condensation drips from its surface, making it slick. Far below, you can hear the distant hum of machinery." -on_enter: - - message: "Type 'balance' to cross the pipe." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/203.yaml b/data/rooms/agility/203.yaml deleted file mode 100644 index 4f5d883..0000000 --- a/data/rooms/agility/203.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 203 -name: "Ventilation Shaft - Shaft Gap" -description: "The ventilation shaft floor is missing here -- a jagged gap drops into darkness. The far side is just barely within jumping distance. Exposed wiring sparks intermittently below." -on_enter: - - message: "Type 'jump' to leap across the gap." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/204.yaml b/data/rooms/agility/204.yaml deleted file mode 100644 index 2d82040..0000000 --- a/data/rooms/agility/204.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 204 -name: "Ventilation Shaft - Narrow Vent" -description: "The passage narrows dramatically here, becoming a tight rectangular vent barely wide enough to fit through. Scratches on the metal walls suggest others have squeezed through before you." -on_enter: - - message: "Type 'crawl' to squeeze through the vent." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/205.yaml b/data/rooms/agility/205.yaml deleted file mode 100644 index d814f7d..0000000 --- a/data/rooms/agility/205.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 205 -name: "Ventilation Shaft - Emergency Chute" -description: "A smooth metal chute angles steeply downward, polished by countless slides. An old emergency evacuation sign hangs crookedly on the wall. This is the final obstacle -- the chute leads back to the training grounds." -on_enter: - - message: "Type 'slide' to descend the chute." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/210.yaml b/data/rooms/agility/210.yaml deleted file mode 100644 index 2b86500..0000000 --- a/data/rooms/agility/210.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 210 -name: "Rooftop Course - Hab Block Wall" -description: "The exterior wall of Hab Block 7 rises three stories above the street. Rough permacrete and maintenance handholds provide a challenging climb. The city spreads out below, neon signs flickering in the perpetual twilight." -on_enter: - - message: "Type 'climb' to scale the wall." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/211.yaml b/data/rooms/agility/211.yaml deleted file mode 100644 index 8b1f069..0000000 --- a/data/rooms/agility/211.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 211 -name: "Rooftop Course - Building Gap" -description: "You stand on the edge of Hab Block 7's roof. Across a three-meter gap, the roof of Hab Block 8 awaits. The street below is a dizzying drop. A few old bootprints mark the takeoff point." -on_enter: - - message: "Type 'jump' to leap to the next building." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/212.yaml b/data/rooms/agility/212.yaml deleted file mode 100644 index 0393a7e..0000000 --- a/data/rooms/agility/212.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 212 -name: "Rooftop Course - Cable Array" -description: "A tangle of power cables and data lines stretches between two antenna towers. One thick cable hangs low enough to grab. The gap below drops to a dark alleyway between hab blocks." -on_enter: - - message: "Type 'swing' to cross on the cable." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/213.yaml b/data/rooms/agility/213.yaml deleted file mode 100644 index be3b736..0000000 --- a/data/rooms/agility/213.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 213 -name: "Rooftop Course - Narrow Beam" -description: "A structural I-beam extends across the gap between two buildings, no wider than your foot. It sways slightly in the recycled air currents. Someone has scratched tally marks into the near end." -on_enter: - - message: "Type 'balance' to cross the beam." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/214.yaml b/data/rooms/agility/214.yaml deleted file mode 100644 index 9f9e081..0000000 --- a/data/rooms/agility/214.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 214 -name: "Rooftop Course - Maintenance Railing" -description: "A high maintenance railing blocks the path forward, topped with sensor equipment and warning labels. It's too high to step over but the right technique could clear it. Beyond the railing, the course continues." -on_enter: - - message: "Type 'vault' to clear the railing." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/215.yaml b/data/rooms/agility/215.yaml deleted file mode 100644 index 7ea39f9..0000000 --- a/data/rooms/agility/215.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 215 -name: "Rooftop Course - Rooftop Edge" -description: "The final jump. The gap here is wider than any before -- a full four meters of empty air between you and the landing platform. Far below, the streets of the asteroid colony pulse with dim light. This is the last obstacle." -on_enter: - - message: "Type 'leap' to make the final jump." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/220.yaml b/data/rooms/agility/220.yaml deleted file mode 100644 index d6826a3..0000000 --- a/data/rooms/agility/220.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 220 -name: "Reactor Course - Scaffolding" -description: "Massive metal scaffolding surrounds the outer reactor housing. The structure vibrates with the reactor's pulse. Heat radiates from every surface, and warning klaxons sound periodically in the distance." -on_enter: - - message: "Type 'climb' to ascend the scaffolding." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/221.yaml b/data/rooms/agility/221.yaml deleted file mode 100644 index 936ee65..0000000 --- a/data/rooms/agility/221.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 221 -name: "Reactor Course - Coolant Chain" -description: "A heavy chain hangs from an overhead crane, suspended above the reactor cooling pit. The pit glows with an eerie blue-green light. The chain is your only way across -- the gantry ahead is the landing zone." -on_enter: - - message: "Type 'swing' to cross on the chain." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/222.yaml b/data/rooms/agility/222.yaml deleted file mode 100644 index 119db01..0000000 --- a/data/rooms/agility/222.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 222 -name: "Reactor Course - Steam Pipe" -description: "An enormous steam pipe, two meters in diameter, stretches across the reactor chamber. Steam vents periodically blast from pressure valves along its length. The pipe's surface is warm but not scalding -- yet." -on_enter: - - message: "Type 'balance' to traverse the pipe." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/223.yaml b/data/rooms/agility/223.yaml deleted file mode 100644 index 0e557f6..0000000 --- a/data/rooms/agility/223.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 223 -name: "Reactor Course - Platform Gap" -description: "A section of the reactor maintenance platform has collapsed into the void below. Emergency barriers block the edges, but someone has moved them aside here. The gap is intimidating but clearable." -on_enter: - - message: "Type 'jump' to clear the gap." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/224.yaml b/data/rooms/agility/224.yaml deleted file mode 100644 index 4e70f7a..0000000 --- a/data/rooms/agility/224.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 224 -name: "Reactor Course - Service Conduit" -description: "A narrow service conduit leads through the reactor shielding. Radiation warning symbols are painted on every surface. Damaged wiring hangs from the ceiling, sparking occasionally. It's the only way forward." -on_enter: - - message: "Type 'crawl' to enter the conduit." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/225.yaml b/data/rooms/agility/225.yaml deleted file mode 100644 index 3413792..0000000 --- a/data/rooms/agility/225.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 225 -name: "Reactor Course - Radiation Barrier" -description: "A containment barrier hums with energy, its surface shimmering with a faint purple glow. It pulses on and off in a regular cycle. Beyond it, the final stretch of the course is visible." -on_enter: - - message: "Type 'vault' to clear the barrier." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/agility/226.yaml b/data/rooms/agility/226.yaml deleted file mode 100644 index 6faa2c1..0000000 --- a/data/rooms/agility/226.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 226 -name: "Reactor Course - Reactor Chasm" -description: "The final obstacle. A massive chasm separates you from the exit platform, the reactor's coolant pool churning far below in shades of luminous green. A narrow runway of grating leads to the edge. This is the longest leap on the course." -on_enter: - - message: "Type 'leap' to make the final jump." -exits: - down: - room: 200 - blocked_message: "" diff --git a/data/rooms/example/agility/99999200.yaml b/data/rooms/example/agility/99999200.yaml new file mode 100644 index 0000000..c44cb6a --- /dev/null +++ b/data/rooms/example/agility/99999200.yaml @@ -0,0 +1,8 @@ +id: 99999200 +name: "Agility Training Grounds" +description: "A cavernous space beneath the asteroid's surface, repurposed as a training facility. Scaffolding, pipes, and platforms fill the chamber. Signs point to three courses of increasing difficulty: {33}Ventilation Shaft{/} (beginner), {214}Rooftop{/} (intermediate), and {196}Reactor{/} (advanced)." +exits: + south: 9999917 + north: 99999201 + east: 99999210 + west: 99999220 diff --git a/data/rooms/example/agility/99999201.yaml b/data/rooms/example/agility/99999201.yaml new file mode 100644 index 0000000..f551fe1 --- /dev/null +++ b/data/rooms/example/agility/99999201.yaml @@ -0,0 +1,9 @@ +id: 99999201 +name: "Ventilation Shaft - Corroded Wall" +description: "A towering wall of corroded ventilation panels rises before you. Rust-eaten handholds and buckled seams offer a treacherous path upward. The air smells of old metal and recycled atmosphere." +on_enter: + - message: "Type 'scramble' to climb the wall." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999202.yaml b/data/rooms/example/agility/99999202.yaml new file mode 100644 index 0000000..1d33043 --- /dev/null +++ b/data/rooms/example/agility/99999202.yaml @@ -0,0 +1,9 @@ +id: 99999202 +name: "Ventilation Shaft - Coolant Pipe Walkway" +description: "A narrow coolant pipe stretches across a dark chasm. Condensation drips from its surface, making it slick. Far below, you can hear the distant hum of machinery." +on_enter: + - message: "Type 'balance' to cross the pipe." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999203.yaml b/data/rooms/example/agility/99999203.yaml new file mode 100644 index 0000000..529bad6 --- /dev/null +++ b/data/rooms/example/agility/99999203.yaml @@ -0,0 +1,9 @@ +id: 99999203 +name: "Ventilation Shaft - Shaft Gap" +description: "The ventilation shaft floor is missing here -- a jagged gap drops into darkness. The far side is just barely within jumping distance. Exposed wiring sparks intermittently below." +on_enter: + - message: "Type 'jump' to leap across the gap." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999204.yaml b/data/rooms/example/agility/99999204.yaml new file mode 100644 index 0000000..7ddf6a1 --- /dev/null +++ b/data/rooms/example/agility/99999204.yaml @@ -0,0 +1,9 @@ +id: 99999204 +name: "Ventilation Shaft - Narrow Vent" +description: "The passage narrows dramatically here, becoming a tight rectangular vent barely wide enough to fit through. Scratches on the metal walls suggest others have squeezed through before you." +on_enter: + - message: "Type 'crawl' to squeeze through the vent." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999205.yaml b/data/rooms/example/agility/99999205.yaml new file mode 100644 index 0000000..3810925 --- /dev/null +++ b/data/rooms/example/agility/99999205.yaml @@ -0,0 +1,9 @@ +id: 99999205 +name: "Ventilation Shaft - Emergency Chute" +description: "A smooth metal chute angles steeply downward, polished by countless slides. An old emergency evacuation sign hangs crookedly on the wall. This is the final obstacle -- the chute leads back to the training grounds." +on_enter: + - message: "Type 'slide' to descend the chute." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999210.yaml b/data/rooms/example/agility/99999210.yaml new file mode 100644 index 0000000..0ecfb1a --- /dev/null +++ b/data/rooms/example/agility/99999210.yaml @@ -0,0 +1,9 @@ +id: 99999210 +name: "Rooftop Course - Hab Block Wall" +description: "The exterior wall of Hab Block 7 rises three stories above the street. Rough permacrete and maintenance handholds provide a challenging climb. The city spreads out below, neon signs flickering in the perpetual twilight." +on_enter: + - message: "Type 'climb' to scale the wall." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999211.yaml b/data/rooms/example/agility/99999211.yaml new file mode 100644 index 0000000..99497e5 --- /dev/null +++ b/data/rooms/example/agility/99999211.yaml @@ -0,0 +1,9 @@ +id: 99999211 +name: "Rooftop Course - Building Gap" +description: "You stand on the edge of Hab Block 7's roof. Across a three-meter gap, the roof of Hab Block 8 awaits. The street below is a dizzying drop. A few old bootprints mark the takeoff point." +on_enter: + - message: "Type 'jump' to leap to the next building." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999212.yaml b/data/rooms/example/agility/99999212.yaml new file mode 100644 index 0000000..c4bc14d --- /dev/null +++ b/data/rooms/example/agility/99999212.yaml @@ -0,0 +1,9 @@ +id: 99999212 +name: "Rooftop Course - Cable Array" +description: "A tangle of power cables and data lines stretches between two antenna towers. One thick cable hangs low enough to grab. The gap below drops to a dark alleyway between hab blocks." +on_enter: + - message: "Type 'swing' to cross on the cable." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999213.yaml b/data/rooms/example/agility/99999213.yaml new file mode 100644 index 0000000..79da0d2 --- /dev/null +++ b/data/rooms/example/agility/99999213.yaml @@ -0,0 +1,9 @@ +id: 99999213 +name: "Rooftop Course - Narrow Beam" +description: "A structural I-beam extends across the gap between two buildings, no wider than your foot. It sways slightly in the recycled air currents. Someone has scratched tally marks into the near end." +on_enter: + - message: "Type 'balance' to cross the beam." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999214.yaml b/data/rooms/example/agility/99999214.yaml new file mode 100644 index 0000000..8a040cd --- /dev/null +++ b/data/rooms/example/agility/99999214.yaml @@ -0,0 +1,9 @@ +id: 99999214 +name: "Rooftop Course - Maintenance Railing" +description: "A high maintenance railing blocks the path forward, topped with sensor equipment and warning labels. It's too high to step over but the right technique could clear it. Beyond the railing, the course continues." +on_enter: + - message: "Type 'vault' to clear the railing." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999215.yaml b/data/rooms/example/agility/99999215.yaml new file mode 100644 index 0000000..4a740f7 --- /dev/null +++ b/data/rooms/example/agility/99999215.yaml @@ -0,0 +1,9 @@ +id: 99999215 +name: "Rooftop Course - Rooftop Edge" +description: "The final jump. The gap here is wider than any before -- a full four meters of empty air between you and the landing platform. Far below, the streets of the asteroid colony pulse with dim light. This is the last obstacle." +on_enter: + - message: "Type 'leap' to make the final jump." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999220.yaml b/data/rooms/example/agility/99999220.yaml new file mode 100644 index 0000000..f063231 --- /dev/null +++ b/data/rooms/example/agility/99999220.yaml @@ -0,0 +1,9 @@ +id: 99999220 +name: "Reactor Course - Scaffolding" +description: "Massive metal scaffolding surrounds the outer reactor housing. The structure vibrates with the reactor's pulse. Heat radiates from every surface, and warning klaxons sound periodically in the distance." +on_enter: + - message: "Type 'climb' to ascend the scaffolding." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999221.yaml b/data/rooms/example/agility/99999221.yaml new file mode 100644 index 0000000..bebfcf1 --- /dev/null +++ b/data/rooms/example/agility/99999221.yaml @@ -0,0 +1,9 @@ +id: 99999221 +name: "Reactor Course - Coolant Chain" +description: "A heavy chain hangs from an overhead crane, suspended above the reactor cooling pit. The pit glows with an eerie blue-green light. The chain is your only way across -- the gantry ahead is the landing zone." +on_enter: + - message: "Type 'swing' to cross on the chain." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999222.yaml b/data/rooms/example/agility/99999222.yaml new file mode 100644 index 0000000..17d6395 --- /dev/null +++ b/data/rooms/example/agility/99999222.yaml @@ -0,0 +1,9 @@ +id: 99999222 +name: "Reactor Course - Steam Pipe" +description: "An enormous steam pipe, two meters in diameter, stretches across the reactor chamber. Steam vents periodically blast from pressure valves along its length. The pipe's surface is warm but not scalding -- yet." +on_enter: + - message: "Type 'balance' to traverse the pipe." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999223.yaml b/data/rooms/example/agility/99999223.yaml new file mode 100644 index 0000000..b945e7e --- /dev/null +++ b/data/rooms/example/agility/99999223.yaml @@ -0,0 +1,9 @@ +id: 99999223 +name: "Reactor Course - Platform Gap" +description: "A section of the reactor maintenance platform has collapsed into the void below. Emergency barriers block the edges, but someone has moved them aside here. The gap is intimidating but clearable." +on_enter: + - message: "Type 'jump' to clear the gap." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999224.yaml b/data/rooms/example/agility/99999224.yaml new file mode 100644 index 0000000..0c9f2e0 --- /dev/null +++ b/data/rooms/example/agility/99999224.yaml @@ -0,0 +1,9 @@ +id: 99999224 +name: "Reactor Course - Service Conduit" +description: "A narrow service conduit leads through the reactor shielding. Radiation warning symbols are painted on every surface. Damaged wiring hangs from the ceiling, sparking occasionally. It's the only way forward." +on_enter: + - message: "Type 'crawl' to enter the conduit." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999225.yaml b/data/rooms/example/agility/99999225.yaml new file mode 100644 index 0000000..feb32e9 --- /dev/null +++ b/data/rooms/example/agility/99999225.yaml @@ -0,0 +1,9 @@ +id: 99999225 +name: "Reactor Course - Radiation Barrier" +description: "A containment barrier hums with energy, its surface shimmering with a faint purple glow. It pulses on and off in a regular cycle. Beyond it, the final stretch of the course is visible." +on_enter: + - message: "Type 'vault' to clear the barrier." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/agility/99999226.yaml b/data/rooms/example/agility/99999226.yaml new file mode 100644 index 0000000..cd1c979 --- /dev/null +++ b/data/rooms/example/agility/99999226.yaml @@ -0,0 +1,9 @@ +id: 99999226 +name: "Reactor Course - Reactor Chasm" +description: "The final obstacle. A massive chasm separates you from the exit platform, the reactor's coolant pool churning far below in shades of luminous green. A narrow runway of grating leads to the edge. This is the longest leap on the course." +on_enter: + - message: "Type 'leap' to make the final jump." +exits: + down: + room: 99999200 + blocked_message: "" diff --git a/data/rooms/example/player_housing/99999300.yaml b/data/rooms/example/player_housing/99999300.yaml new file mode 100644 index 0000000..124b278 --- /dev/null +++ b/data/rooms/example/player_housing/99999300.yaml @@ -0,0 +1,8 @@ +id: 99999300 +name: "Player House: Entrance Hall" +description: "A cozy entrance hall with polished floors and warm lighting. A welcome mat greets visitors at the door." +exits: + south: 99999170 + north: 99999301 +objects: + - id: charging_station diff --git a/data/rooms/example/player_housing/99999301.yaml b/data/rooms/example/player_housing/99999301.yaml new file mode 100644 index 0000000..ebaba3d --- /dev/null +++ b/data/rooms/example/player_housing/99999301.yaml @@ -0,0 +1,8 @@ +id: 99999301 +name: "Player House: Workshop" +description: "A well-lit workshop with a sturdy workbench covered in tools and blueprints. Wood shavings litter the floor around the bench." +exits: + south: 99999300 +objects: + - id: workbench + - id: bank_booth diff --git a/data/rooms/example/town/999991.yaml b/data/rooms/example/town/999991.yaml new file mode 100644 index 0000000..0231940 --- /dev/null +++ b/data/rooms/example/town/999991.yaml @@ -0,0 +1,80 @@ +id: 999991 +name: "Town Square" +description: "The central square of Lumbridge. A fountain gurgles peacefully in the center. Paths lead west toward the gathering grounds and east toward the production district." +exits: + east: 999992 + west: 9999922 + up: 9999930 + down: 9999931 +objects: + - id: lumby_fountain + - id: charging_station + - id: bank_booth + - id: aps_tower +mobs: + - "newbie_trainer" + - "general_store_clerk" +item_spawns: + - id: cape_of_agility + quantity: 1 + respawn_ticks: 60 + - id: graceful_hat + quantity: 1 + respawn_ticks: 60 + - id: graceful_torso + quantity: 1 + respawn_ticks: 60 + - id: graceful_legs + quantity: 1 + respawn_ticks: 60 + - id: graceful_gloves + quantity: 1 + respawn_ticks: 60 + - id: graceful_boots + quantity: 1 + respawn_ticks: 60 + - id: graceful_cape + quantity: 1 + respawn_ticks: 60 + - id: basic_deck + quantity: 1 + respawn_ticks: 60 + - id: solar_identifier + quantity: 1 + respawn_ticks: 60 + - id: hydro_identifier + quantity: 1 + respawn_ticks: 60 + - id: cosmic_identifier + quantity: 1 + respawn_ticks: 60 + - id: eco_identifier + quantity: 1 + respawn_ticks: 60 + - id: chaos_identifier + quantity: 1 + respawn_ticks: 60 + - id: bio_identifier + quantity: 1 + respawn_ticks: 60 + - id: nature_identifier + quantity: 1 + respawn_ticks: 60 + - id: law_identifier + quantity: 1 + respawn_ticks: 60 + - id: death_identifier + quantity: 1 + respawn_ticks: 60 + - id: blood_identifier + quantity: 1 + respawn_ticks: 60 + - id: grimy_guam + quantity: 5 + respawn_ticks: 60 + - id: vial_of_water + quantity: 5 + respawn_ticks: 60 + - id: eye_of_newt + quantity: 5 + respawn_ticks: 60 diff --git a/data/rooms/example/town/9999910.yaml b/data/rooms/example/town/9999910.yaml new file mode 100644 index 0000000..7dcbc2e --- /dev/null +++ b/data/rooms/example/town/9999910.yaml @@ -0,0 +1,10 @@ +id: 9999910 +name: "Farming Plot" +description: "A small patch of tilled soil behind a weathered fence. This area is not yet accessible." +exits: + west: 999999 + east: 9999911 +mobs: + - id: skeleton + - id: skeleton + - id: mutant_rat diff --git a/data/rooms/example/town/9999911.yaml b/data/rooms/example/town/9999911.yaml new file mode 100644 index 0000000..72b5337 --- /dev/null +++ b/data/rooms/example/town/9999911.yaml @@ -0,0 +1,17 @@ +id: 9999911 +name: "Kitchen" +description: "A warm kitchen with a stone hearth and wooden counters. A large iron cooking range dominates one wall, and shelves are stocked with ingredients." +exits: + east: 9999912 + west: 9999910 +objects: + - id: cooking_range +mobs: + - id: zombie +item_spawns: + - id: bucket_of_water + quantity: 1 + respawn_ticks: 200 + - id: pot_of_flour + quantity: 1 + respawn_ticks: 200 diff --git a/data/rooms/example/town/9999912.yaml b/data/rooms/example/town/9999912.yaml new file mode 100644 index 0000000..eee3aeb --- /dev/null +++ b/data/rooms/example/town/9999912.yaml @@ -0,0 +1,28 @@ +id: 9999912 +name: "Forge" +description: "A blazing forge and anvil dominate this smoky workshop. The heat from the furnace is intense." +exits: + east: 9999913 + west: 9999911 +mobs: + - "weapons_smith" +objects: + - id: furnace + - id: anvil + - id: bank_booth +item_spawns: + - id: copper_ore + quantity: 3 + respawn_ticks: 30 + - id: tin_ore + quantity: 3 + respawn_ticks: 30 + - id: iron_ore + quantity: 3 + respawn_ticks: 30 + - id: coal + quantity: 5 + respawn_ticks: 30 + - id: hammer + quantity: 1 + respawn_ticks: 30 diff --git a/data/rooms/example/town/9999913.yaml b/data/rooms/example/town/9999913.yaml new file mode 100644 index 0000000..369fb66 --- /dev/null +++ b/data/rooms/example/town/9999913.yaml @@ -0,0 +1,46 @@ +id: 9999913 +name: "Crafting Workshop" +description: "A bustling workshop filled with crafting stations. A {130}spinning wheel{/} sits in one corner, a {173}pottery wheel{/} and {196}pottery oven{/} in another. Racks of tools and raw materials line the walls. The forge is just to the west." +exits: + east: 9999914 + west: 9999912 +objects: + - id: spinning_wheel + - id: pottery_wheel + - id: pottery_oven + - id: sheep + - id: clay_rock + - id: clay_rock +mobs: + - id: tanner + - id: cow + - id: cow + - id: cow +item_spawns: + - id: flax + quantity: 5 + respawn_ticks: 60 + - id: thread + quantity: 10 + respawn_ticks: 60 + - id: shears + quantity: 1 + respawn_ticks: 60 + - id: needle + quantity: 1 + respawn_ticks: 60 + - id: chisel + quantity: 1 + respawn_ticks: 60 + - id: ring_mould + quantity: 1 + respawn_ticks: 60 + - id: necklace_mould + quantity: 1 + respawn_ticks: 60 + - id: amulet_mould + quantity: 1 + respawn_ticks: 60 + - id: bracelet_mould + quantity: 1 + respawn_ticks: 60 diff --git a/data/rooms/example/town/9999914.yaml b/data/rooms/example/town/9999914.yaml new file mode 100644 index 0000000..c003cf7 --- /dev/null +++ b/data/rooms/example/town/9999914.yaml @@ -0,0 +1,18 @@ +id: 9999914 +name: "Fletching Bench" +description: "A workbench covered in wood shavings, feathers, and arrowheads. Racks of unfinished bows line the walls, and a pile of bowstrings sits in the corner." +exits: + east: 9999915 + west: 9999913 +mobs: + - id: moss_giant +item_spawns: + - id: bowstring + quantity: 5 + respawn_ticks: 50 + - id: knife + quantity: 1 + respawn_ticks: 50 + - id: chisel + quantity: 1 + respawn_ticks: 50 diff --git a/data/rooms/example/town/9999915.yaml b/data/rooms/example/town/9999915.yaml new file mode 100644 index 0000000..f632f3e --- /dev/null +++ b/data/rooms/example/town/9999915.yaml @@ -0,0 +1,10 @@ +id: 9999915 +name: "Pharmacy Lab" +description: "Shelves lined with glass vials, dried herbs, and bubbling cauldrons. This area is not yet accessible." +exits: + east: 9999916 + west: 9999914 + south: 99999150 +mobs: + - id: lesser_drone + - id: lesser_drone diff --git a/data/rooms/example/town/9999916.yaml b/data/rooms/example/town/9999916.yaml new file mode 100644 index 0000000..0a8fb95 --- /dev/null +++ b/data/rooms/example/town/9999916.yaml @@ -0,0 +1,12 @@ +id: 9999916 +name: "Construction Site" +description: "A half-built structure surrounded by planks, nails, and blueprints. A workbench stands ready for crafting. Signs point north toward the Local Neighborhood and east toward the lumberyard." +exits: + east: 99999171 + west: 9999915 + north: 99999170 +objects: + - id: workbench +mobs: + - id: greater_drone + - id: estate_broker diff --git a/data/rooms/example/town/9999917.yaml b/data/rooms/example/town/9999917.yaml new file mode 100644 index 0000000..82ff07d --- /dev/null +++ b/data/rooms/example/town/9999917.yaml @@ -0,0 +1,10 @@ +id: 9999917 +name: "Bonfire" +description: "A ring of stones surrounds a bed of ash. A few unlit logs wait nearby. A narrow passage leads north toward what sounds like echoing footsteps and clanging metal." +exits: + east: 9999918 + west: 9999916 + north: 99999200 +mobs: + - id: scrap_bot + - id: scrap_bot diff --git a/data/rooms/example/town/9999918.yaml b/data/rooms/example/town/9999918.yaml new file mode 100644 index 0000000..baa21f4 --- /dev/null +++ b/data/rooms/example/town/9999918.yaml @@ -0,0 +1,6 @@ +id: 9999918 +name: "Thieves' Den" +description: "A shadowy hideout with lockpicks, dark cloaks, and wanted posters on the walls. This area is not yet accessible." +exits: + east: 9999919 + west: 9999917 diff --git a/data/rooms/example/town/9999919.yaml b/data/rooms/example/town/9999919.yaml new file mode 100644 index 0000000..2fd5c5d --- /dev/null +++ b/data/rooms/example/town/9999919.yaml @@ -0,0 +1,6 @@ +id: 9999919 +name: "Agility Course" +description: "A training ground with balance beams, monkey bars, and climbing nets. This area is not yet accessible." +exits: + east: 9999920 + west: 9999918 diff --git a/data/rooms/example/town/999992.yaml b/data/rooms/example/town/999992.yaml new file mode 100644 index 0000000..90bacac --- /dev/null +++ b/data/rooms/example/town/999992.yaml @@ -0,0 +1,16 @@ +id: 999992 +name: "Fishing Spot" +description: "A quiet spot along the riverbank. The water is clear and fish can be seen swimming beneath the surface. You can cast a line here with a fishing rod and bait." +exits: + east: 999994 + north: 999993 + west: 999991 +objects: + - id: bait_fishing_spot +item_spawns: + - id: fishing_rod + quantity: 1 + respawn_ticks: 30 + - id: fishing_bait + quantity: 20 + respawn_ticks: 60 diff --git a/data/rooms/example/town/9999920.yaml b/data/rooms/example/town/9999920.yaml new file mode 100644 index 0000000..88a2e3c --- /dev/null +++ b/data/rooms/example/town/9999920.yaml @@ -0,0 +1,6 @@ +id: 9999920 +name: "Assassin's Bureau" +description: "A discreet office with job postings pinned to a corkboard. This area is not yet accessible." +exits: + east: 9999921 + west: 9999919 diff --git a/data/rooms/example/town/9999921.yaml b/data/rooms/example/town/9999921.yaml new file mode 100644 index 0000000..8c399fe --- /dev/null +++ b/data/rooms/example/town/9999921.yaml @@ -0,0 +1,12 @@ +id: 9999921 +name: "Combat Arena" +description: "A sandy training ground with practice dummies and sparring rings. Several men wander here looking for a fight." +exits: + west: 9999920 +mobs: + - id: man + wander_interval: 10 + - id: man + wander_interval: 12 + - id: man + wander_interval: 15 diff --git a/data/rooms/example/town/9999922.yaml b/data/rooms/example/town/9999922.yaml new file mode 100644 index 0000000..fb2fe9f --- /dev/null +++ b/data/rooms/example/town/9999922.yaml @@ -0,0 +1,6 @@ +id: 9999922 +name: "West of Town Square" +description: "" +exits: + east: 999991 + west: 9999923 diff --git a/data/rooms/example/town/9999923.yaml b/data/rooms/example/town/9999923.yaml new file mode 100644 index 0000000..6160f00 --- /dev/null +++ b/data/rooms/example/town/9999923.yaml @@ -0,0 +1,6 @@ +id: 9999923 +name: "West of West of Town Square" +description: "Here is a description of where we are!" +exits: + east: 9999922 + south: 9999924 diff --git a/data/rooms/example/town/9999924.yaml b/data/rooms/example/town/9999924.yaml new file mode 100644 index 0000000..4c4592f --- /dev/null +++ b/data/rooms/example/town/9999924.yaml @@ -0,0 +1,6 @@ +id: 9999924 +name: "South of West of West of Town Square" +description: "Here is a description of where we are!" +exits: + north: 9999923 + east: 9999925 diff --git a/data/rooms/example/town/9999925.yaml b/data/rooms/example/town/9999925.yaml new file mode 100644 index 0000000..b9ad750 --- /dev/null +++ b/data/rooms/example/town/9999925.yaml @@ -0,0 +1,6 @@ +id: 9999925 +name: "East of South of West of West of Town Square" +description: "Here is a description of where we are!" +exits: + west: 9999924 + east: 9999926 diff --git a/data/rooms/example/town/9999926.yaml b/data/rooms/example/town/9999926.yaml new file mode 100644 index 0000000..f776337 --- /dev/null +++ b/data/rooms/example/town/9999926.yaml @@ -0,0 +1,6 @@ +id: 9999926 +name: "East of South of West of West of Town Square" +description: "Here is a description of where we are!" +exits: + west: 9999925 + south: 9999927 diff --git a/data/rooms/example/town/9999927.yaml b/data/rooms/example/town/9999927.yaml new file mode 100644 index 0000000..1c108e6 --- /dev/null +++ b/data/rooms/example/town/9999927.yaml @@ -0,0 +1,6 @@ +id: 9999927 +name: "East of South of West of West of Town Square" +description: "Here is a description of where we are!" +exits: + west: 9999928 + north: 9999926 diff --git a/data/rooms/example/town/9999928.yaml b/data/rooms/example/town/9999928.yaml new file mode 100644 index 0000000..79917f1 --- /dev/null +++ b/data/rooms/example/town/9999928.yaml @@ -0,0 +1,5 @@ +id: 9999928 +name: "East of South of West of West of Town Square" +description: "Here is a description of where we are!" +exits: + east: 9999927 diff --git a/data/rooms/example/town/999993.yaml b/data/rooms/example/town/999993.yaml new file mode 100644 index 0000000..078da89 --- /dev/null +++ b/data/rooms/example/town/999993.yaml @@ -0,0 +1,14 @@ +id: 999993 +name: "Fly Fishing Pool" +description: "A calm pool where insects hover over the water. Perfect for fly fishing." +exits: + south: 999992 +objects: + - id: fly_fishing_spot +item_spawns: + - id: fly_fishing_rod + quantity: 1 + respawn_ticks: 30 + - id: feather + quantity: 15 + respawn_ticks: 60 diff --git a/data/rooms/example/town/9999930.yaml b/data/rooms/example/town/9999930.yaml new file mode 100644 index 0000000..6e8a4c2 --- /dev/null +++ b/data/rooms/example/town/9999930.yaml @@ -0,0 +1,5 @@ +id: 9999930 +name: "Above Town Square" +description: "The central square of Lumbridge. A fountain gurgles peacefully in the center. Paths lead west toward the gathering grounds and east toward the production district." +exits: + down: 999991 diff --git a/data/rooms/example/town/9999931.yaml b/data/rooms/example/town/9999931.yaml new file mode 100644 index 0000000..9a93023 --- /dev/null +++ b/data/rooms/example/town/9999931.yaml @@ -0,0 +1,6 @@ +id: 9999931 +name: "Below Town Square" +description: "The central square of Lumbridge. A fountain gurgles peacefully in the center. Paths lead west toward the gathering grounds and east toward the production district." +exits: + up: 999991 + down: 9999950 diff --git a/data/rooms/example/town/999994.yaml b/data/rooms/example/town/999994.yaml new file mode 100644 index 0000000..e8c3453 --- /dev/null +++ b/data/rooms/example/town/999994.yaml @@ -0,0 +1,36 @@ +id: 999994 +name: "Woodcutting Glade" +description: "A sunny glade at the edge of the forest. A sturdy tree stands here, perfect for practicing woodcutting. A solid rock formation near the edge of the glade looks like it could provide cover." +exits: + east: 999996 + north: 999995 + west: 999992 +objects: + - id: tree + - id: tree + - id: tree + - id: tree + - id: simple_rock + - id: tutorial_lever +mobs: + - id: rat + - id: rat +item_spawns: + - id: bronze_axe + quantity: 1 + respawn_ticks: 60 + - id: matches + quantity: 100 + respawn_ticks: 120 + - id: firesteel + quantity: 1 + respawn_ticks: 120 + - id: lighter + quantity: 1 + respawn_ticks: 120 + - id: shortbow + quantity: 1 + respawn_ticks: 60 + - id: bronze_arrow + quantity: 100 + respawn_ticks: 60 diff --git a/data/rooms/example/town/999995.yaml b/data/rooms/example/town/999995.yaml new file mode 100644 index 0000000..767fe73 --- /dev/null +++ b/data/rooms/example/town/999995.yaml @@ -0,0 +1,11 @@ +id: 999995 +name: "Oak Grove" +description: "A stand of young oak trees. The wood here is tougher but more valuable." +exits: + south: 999994 +objects: + - id: oak_tree +item_spawns: + - id: iron_axe + quantity: 1 + respawn_ticks: 60 diff --git a/data/rooms/example/town/999996.yaml b/data/rooms/example/town/999996.yaml new file mode 100644 index 0000000..86b1b26 --- /dev/null +++ b/data/rooms/example/town/999996.yaml @@ -0,0 +1,15 @@ +id: 999996 +name: "Mining Pit" +description: "A small mining pit. The rocky walls glint with veins of copper ore." +exits: + east: 999998 + north: 999997 + west: 999994 +objects: + - id: copper_rock + - id: copper_rock + - id: copper_rock +item_spawns: + - id: bronze_pickaxe + quantity: 1 + respawn_ticks: 30 diff --git a/data/rooms/example/town/999997.yaml b/data/rooms/example/town/999997.yaml new file mode 100644 index 0000000..847da5c --- /dev/null +++ b/data/rooms/example/town/999997.yaml @@ -0,0 +1,9 @@ +id: 999997 +name: "Deep Mine" +description: "A deeper mine shaft. Higher quality ores can be found here. This area is not yet accessible." +exits: + south: 999996 +mobs: + - id: goblin + - id: goblin + - id: bandit diff --git a/data/rooms/example/town/999998.yaml b/data/rooms/example/town/999998.yaml new file mode 100644 index 0000000..6700e9a --- /dev/null +++ b/data/rooms/example/town/999998.yaml @@ -0,0 +1,11 @@ +id: 999998 +name: "Hacking Lab" +description: "A dimly lit room filled with old terminals and flickering screens. Cables dangle from the ceiling, some still sparking. The air smells of ozone and burnt circuitry. A {40}basic terminal{/} against the far wall still has power." +exits: + east: 999999 + west: 999996 +objects: + - id: terminal_basic + - id: terminal_advanced + - id: terminal_secure + - id: netrunner diff --git a/data/rooms/example/town/999999.yaml b/data/rooms/example/town/999999.yaml new file mode 100644 index 0000000..71b7f5f --- /dev/null +++ b/data/rooms/example/town/999999.yaml @@ -0,0 +1,13 @@ +id: 999999 +name: "Scavenging Post" +description: "A cluttered yard full of {243}scrap{/} and discarded technology. Piles of salvageable debris are scattered across the ground. The air smells of ozone and rust." +exits: + east: 9999910 + west: 999998 + north: 99999150 + south: 99999151 +objects: + - id: scrap_pile + - id: scrap_pile + - id: scrap_pile + - id: solar_altar diff --git a/data/rooms/example/underground/9999950.yaml b/data/rooms/example/underground/9999950.yaml new file mode 100644 index 0000000..adbad79 --- /dev/null +++ b/data/rooms/example/underground/9999950.yaml @@ -0,0 +1,7 @@ +id: 9999950 +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: 9999931 +mobs: + - "client" diff --git a/data/rooms/example/underground/9999951.yaml b/data/rooms/example/underground/9999951.yaml new file mode 100644 index 0000000..4dbd966 --- /dev/null +++ b/data/rooms/example/underground/9999951.yaml @@ -0,0 +1,10 @@ +id: 9999951 +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: 9999950 + south: 9999952 +mobs: + - "slug" + - "slug" + - "slug" diff --git a/data/rooms/example/underground/9999952.yaml b/data/rooms/example/underground/9999952.yaml new file mode 100644 index 0000000..7c8d871 --- /dev/null +++ b/data/rooms/example/underground/9999952.yaml @@ -0,0 +1,9 @@ +id: 9999952 +name: "Abandoned Sector" +description: "A decommissioned sector of the asteroid's infrastructure. Broken monitors flicker and exposed wiring {214}sparks{/} dangerously." +exits: + north: 9999951 + south: 9999953 +mobs: + - "drone" + - "drone" diff --git a/data/rooms/example/underground/9999953.yaml b/data/rooms/example/underground/9999953.yaml new file mode 100644 index 0000000..bad2d27 --- /dev/null +++ b/data/rooms/example/underground/9999953.yaml @@ -0,0 +1,9 @@ +id: 9999953 +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: 9999952 + south: 9999954 +mobs: + - "crawler" + - "crawler" diff --git a/data/rooms/example/underground/9999954.yaml b/data/rooms/example/underground/9999954.yaml new file mode 100644 index 0000000..315a831 --- /dev/null +++ b/data/rooms/example/underground/9999954.yaml @@ -0,0 +1,9 @@ +id: 9999954 +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: 9999953 + down: 9999955 +mobs: + - "phantom" + - "phantom" diff --git a/data/rooms/example/underground/9999955.yaml b/data/rooms/example/underground/9999955.yaml new file mode 100644 index 0000000..63f9dbd --- /dev/null +++ b/data/rooms/example/underground/9999955.yaml @@ -0,0 +1,12 @@ +id: 9999955 +name: "The Deeper Tunnels" +description: "A junction deep beneath the void chamber. The walls here are reinforced with pre-regression alloys. Several passages branch off into the darkness, and you can hear the sounds of machinery echoing from all directions. A steep shaft descends further into the depths." +exits: + up: 9999954 + north: 9999956 + east: 9999957 + south: 9999958 + west: 9999960 + down: 9999962 +mobs: + - id: security_turret diff --git a/data/rooms/example/underground/9999956.yaml b/data/rooms/example/underground/9999956.yaml new file mode 100644 index 0000000..74b75a9 --- /dev/null +++ b/data/rooms/example/underground/9999956.yaml @@ -0,0 +1,9 @@ +id: 9999956 +name: "Irradiated Sector" +description: "Warning symbols cover every surface, their paint long since faded. A faint green glow emanates from cracks in the walls. The air is thick with the taste of old radiation." +exits: + south: 9999955 +mobs: + - id: ghoul + - id: ghoul + - id: corrupted_scientist diff --git a/data/rooms/example/underground/9999957.yaml b/data/rooms/example/underground/9999957.yaml new file mode 100644 index 0000000..b32c7aa --- /dev/null +++ b/data/rooms/example/underground/9999957.yaml @@ -0,0 +1,8 @@ +id: 9999957 +name: "Drone Assembly" +description: "A long-abandoned drone manufacturing bay. Conveyor belts lie still, and half-assembled drone chassis hang from the ceiling. A few of them still twitch with residual power." +exits: + west: 9999955 +mobs: + - id: war_drone + - id: war_drone diff --git a/data/rooms/example/underground/9999958.yaml b/data/rooms/example/underground/9999958.yaml new file mode 100644 index 0000000..1834260 --- /dev/null +++ b/data/rooms/example/underground/9999958.yaml @@ -0,0 +1,8 @@ +id: 9999958 +name: "Security Checkpoint" +description: "A fortified security station with blast shields and a decommissioned weapons locker. Scorch marks on the walls tell the story of a last stand." +exits: + north: 9999955 + south: 9999959 +mobs: + - id: enforcer_bot diff --git a/data/rooms/example/underground/9999959.yaml b/data/rooms/example/underground/9999959.yaml new file mode 100644 index 0000000..21f526b --- /dev/null +++ b/data/rooms/example/underground/9999959.yaml @@ -0,0 +1,9 @@ +id: 9999959 +name: "Plasma Core" +description: "A massive plasma containment chamber with cracked magnetic coils lining the walls. Streams of superheated gas leak from the containment vessel, forming shimmering wraith-like shapes in the air." +exits: + north: 9999958 +mobs: + - id: plasma_wraith + - id: plasma_wraith + - id: feral_cyborg diff --git a/data/rooms/example/underground/9999960.yaml b/data/rooms/example/underground/9999960.yaml new file mode 100644 index 0000000..9a45e32 --- /dev/null +++ b/data/rooms/example/underground/9999960.yaml @@ -0,0 +1,8 @@ +id: 9999960 +name: "Heavy Mech Bay" +description: "An enormous maintenance bay built to house assault mechs. A gantry crane hangs overhead, and hydraulic lifts dot the floor. The bay's occupant is still active — and it has noticed you." +exits: + east: 9999955 + west: 9999961 +mobs: + - id: heavy_mech diff --git a/data/rooms/example/underground/9999961.yaml b/data/rooms/example/underground/9999961.yaml new file mode 100644 index 0000000..caf4eab --- /dev/null +++ b/data/rooms/example/underground/9999961.yaml @@ -0,0 +1,7 @@ +id: 9999961 +name: "Synth Lab" +description: "A sterile laboratory where combat androids were designed and assembled. White panels line the walls, some splattered with old coolant stains. Diagnostic screens flicker with corrupted data." +exits: + east: 9999960 +mobs: + - id: elite_synth diff --git a/data/rooms/example/underground/9999962.yaml b/data/rooms/example/underground/9999962.yaml new file mode 100644 index 0000000..531ead1 --- /dev/null +++ b/data/rooms/example/underground/9999962.yaml @@ -0,0 +1,8 @@ +id: 9999962 +name: "Abandoned Bunker" +description: "A fortified bunker from some forgotten conflict. Sandbags are piled in the corners and spent shell casings litter the floor. A solitary figure stands watch over the remains." +exits: + up: 9999955 + down: 9999963 +mobs: + - id: veteran_merc diff --git a/data/rooms/example/underground/9999963.yaml b/data/rooms/example/underground/9999963.yaml new file mode 100644 index 0000000..99b8f90 --- /dev/null +++ b/data/rooms/example/underground/9999963.yaml @@ -0,0 +1,8 @@ +id: 9999963 +name: "Magma Conduit" +description: "A geothermal vent has been tapped and routed through industrial conduits. The heat is oppressive, and molten rivulets course through channels in the floor. Something moves within the glowing flow." +exits: + up: 9999962 + down: 9999964 +mobs: + - id: molten_elemental diff --git a/data/rooms/example/underground/9999964.yaml b/data/rooms/example/underground/9999964.yaml new file mode 100644 index 0000000..6bac466 --- /dev/null +++ b/data/rooms/example/underground/9999964.yaml @@ -0,0 +1,8 @@ +id: 9999964 +name: "The Foundry" +description: "A vast smelting facility where sentinels were once forged. Massive crucibles hang from chains, and rivers of cooled slag form a maze-like floor. A single iron sentinel stands guard over the dormant machinery." +exits: + up: 9999963 + down: 9999965 +mobs: + - id: iron_sentinel diff --git a/data/rooms/example/underground/9999965.yaml b/data/rooms/example/underground/9999965.yaml new file mode 100644 index 0000000..47aed6b --- /dev/null +++ b/data/rooms/example/underground/9999965.yaml @@ -0,0 +1,8 @@ +id: 65 +name: "Titan's Rest" +description: "A colossal vault built to house something immense. The ceiling vanishes into darkness above, and the walls are etched with maintenance schematics for a machine of staggering proportions. At the center, a titan sentinel awakens." +exits: + up: 64 + down: 66 +mobs: + - id: titan_sentinel diff --git a/data/rooms/example/underground/9999966.yaml b/data/rooms/example/underground/9999966.yaml new file mode 100644 index 0000000..755d5e2 --- /dev/null +++ b/data/rooms/example/underground/9999966.yaml @@ -0,0 +1,8 @@ +id: 66 +name: "Psionic Nexus" +description: "Reality grows thin here. The walls are covered in strange geometric patterns that hurt to look at directly. A low hum vibrates through your skull, and the air seems to bend around a central point of unseen pressure." +exits: + up: 65 + down: 67 +mobs: + - id: psionic_entity diff --git a/data/rooms/example/underground/9999967.yaml b/data/rooms/example/underground/9999967.yaml new file mode 100644 index 0000000..6b8d663 --- /dev/null +++ b/data/rooms/example/underground/9999967.yaml @@ -0,0 +1,8 @@ +id: 67 +name: "Ancient Chamber" +description: "A perfectly spherical room carved from an unknown black metal. Glowing runes spiral across every surface in patterns that predate the asteroid itself. At the center, an ancient construct stirs from its eons-long vigil." +exits: + up: 66 + down: 68 +mobs: + - id: ancient_construct diff --git a/data/rooms/example/underground/9999968.yaml b/data/rooms/example/underground/9999968.yaml new file mode 100644 index 0000000..f631981 --- /dev/null +++ b/data/rooms/example/underground/9999968.yaml @@ -0,0 +1,7 @@ +id: 68 +name: "The Void" +description: "The bottom of the world. Light itself seems to hesitate before entering this place. The floor is unnaturally smooth black glass, and the only sound is the beating of your own heart. Something waits in the darkness — something that was here before the asteroid, before the colony, before anything." +exits: + up: 67 +mobs: + - id: void_reaper diff --git a/data/rooms/example/wilderness/99999150.yaml b/data/rooms/example/wilderness/99999150.yaml new file mode 100644 index 0000000..3e74da2 --- /dev/null +++ b/data/rooms/example/wilderness/99999150.yaml @@ -0,0 +1,31 @@ +id: 150 +name: "Hydroponics Bay" +description: "A large enclosed area with climate-controlled growing stations. {34}Lush green patches{/} of soil are laid out in neat rows under artificial UV lamps. A {94}tool shed{/} stands near the entrance. Exits lead to specialized growing areas." +exits: + north: 15 + east: 151 + south: 152 +objects: + - id: tool_shed +item_spawns: + - id: rake + quantity: 1 + respawn_ticks: 120 + - id: spade + quantity: 1 + respawn_ticks: 120 + - id: watering_can + quantity: 1 + respawn_ticks: 120 + - id: plant_cure + quantity: 3 + respawn_ticks: 200 + - id: guam_seed + quantity: 5 + respawn_ticks: 300 + - id: potato_seed + quantity: 10 + respawn_ticks: 300 + - id: onion_seed + quantity: 10 + respawn_ticks: 300 diff --git a/data/rooms/example/wilderness/99999151.yaml b/data/rooms/example/wilderness/99999151.yaml new file mode 100644 index 0000000..4242270 --- /dev/null +++ b/data/rooms/example/wilderness/99999151.yaml @@ -0,0 +1,10 @@ +id: 151 +name: "Herb Garden" +description: "A dedicated section of the hydroponics bay for growing herbs. {28}Herb patches{/} are arranged in two rows under specialized grow lights. The air here smells faintly of fresh vegetation." +exits: + west: 150 +objects: + - id: herb_patch + - id: herb_patch + - id: herb_patch + - id: herb_patch diff --git a/data/rooms/example/wilderness/99999152.yaml b/data/rooms/example/wilderness/99999152.yaml new file mode 100644 index 0000000..787bbbd --- /dev/null +++ b/data/rooms/example/wilderness/99999152.yaml @@ -0,0 +1,10 @@ +id: 152 +name: "Allotment Field" +description: "A section with larger soil beds for growing vegetables. {94}Allotment patches{/} stretch across the floor under warm overhead lamps. A single {213}flower patch{/} sits in the corner." +exits: + north: 150 +objects: + - id: allotment_patch + - id: allotment_patch + - id: allotment_patch + - id: flower_patch diff --git a/data/rooms/example/wilderness/99999160.yaml b/data/rooms/example/wilderness/99999160.yaml new file mode 100644 index 0000000..7f30c34 --- /dev/null +++ b/data/rooms/example/wilderness/99999160.yaml @@ -0,0 +1,16 @@ +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." +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/example/wilderness/99999161.yaml b/data/rooms/example/wilderness/99999161.yaml new file mode 100644 index 0000000..7c2c140 --- /dev/null +++ b/data/rooms/example/wilderness/99999161.yaml @@ -0,0 +1,12 @@ +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." +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/example/wilderness/99999162.yaml b/data/rooms/example/wilderness/99999162.yaml new file mode 100644 index 0000000..6aab585 --- /dev/null +++ b/data/rooms/example/wilderness/99999162.yaml @@ -0,0 +1,5 @@ +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." +exits: + north: 1 \ No newline at end of file diff --git a/data/rooms/example/wilderness/99999170.yaml b/data/rooms/example/wilderness/99999170.yaml new file mode 100644 index 0000000..35d539f --- /dev/null +++ b/data/rooms/example/wilderness/99999170.yaml @@ -0,0 +1,7 @@ +id: 170 +name: "Local Neighborhood" +description: "A quiet cul-de-sac of modest homes, each with a small garden plot. An estate directory terminal glows in the center, listing residents and property information." +exits: + south: 16 +objects: + - id: estate_directory diff --git a/data/rooms/example/wilderness/99999171.yaml b/data/rooms/example/wilderness/99999171.yaml new file mode 100644 index 0000000..552ccd4 --- /dev/null +++ b/data/rooms/example/wilderness/99999171.yaml @@ -0,0 +1,7 @@ +id: 171 +name: "Lumberyard" +description: "Stacks of fresh-cut timber fill the air with the scent of pine. A massive industrial sawmill hums in the background, operated by a burly worker." +exits: + west: 16 +mobs: + - id: sawmill_operator diff --git a/data/rooms/example/wilderness/99999180.yaml b/data/rooms/example/wilderness/99999180.yaml new file mode 100644 index 0000000..866d658 --- /dev/null +++ b/data/rooms/example/wilderness/99999180.yaml @@ -0,0 +1,5 @@ +id: 99999180 +name: "Construction Yard Entrance" +description: "A fenced staging area at the edge of the terraforming works. Crates of brackets and coils of cable are stacked under a sturdy section of atmospheric dome. To the {220}north{/} the dome shielding gives out and the open solar array field begins." +exits: + north: 99999181 diff --git a/data/rooms/example/wilderness/99999181.yaml b/data/rooms/example/wilderness/99999181.yaml new file mode 100644 index 0000000..aae861b --- /dev/null +++ b/data/rooms/example/wilderness/99999181.yaml @@ -0,0 +1,11 @@ +id: 99999181 +name: "Exposed Solar Array" +description: "A wide field of steel frames stretches under a torn section of the atmospheric dome. Raw sunlight blazes down with nothing to filter it. Half-built {220}solar panel frames{/} wait to be finished. A jagged {248}rock outcrop{/} on the south edge throws a patch of shade. The yard entrance lies back to the {34}south{/}." +hazard: solar_radiation +exits: + south: 99999180 +objects: + - id: rock_outcrop +mobs: + - solar_panel_frame + - solar_panel_frame diff --git a/data/rooms/intro/1001.yaml b/data/rooms/intro/1001.yaml new file mode 100644 index 0000000..7ad95fa --- /dev/null +++ b/data/rooms/intro/1001.yaml @@ -0,0 +1,5 @@ +id: 1001 +name: "Inside the Landing Craft" +description: "Rows of sturdy metal seats fill the cramped cabin of the landing craft, harnesses still swinging from the descent. A thin haze of recycled air hangs in the light spilling through the open hatch. The only way out is north, onto the landing pad." +exits: + north: 1002 diff --git a/data/rooms/intro/1002.yaml b/data/rooms/intro/1002.yaml new file mode 100644 index 0000000..2c9bb4f --- /dev/null +++ b/data/rooms/intro/1002.yaml @@ -0,0 +1,43 @@ +id: 1002 +name: "Landing Pad" +description: "A large, empty concrete pad in the middle of the ocean. The horizon stretches out in all directions." +descriptions: + - condition: + player_flag: denali_intro_done + not: true + text: "A large concrete pad sits in the middle of an endless ocean. A mass of a couple dozen people spill from a small building to the north, their faces a churn of anxiety, impatience, and relief." +objects: + - id: 1002_crowd + - id: 1002_shuttle + - id: 1002_pilot +exits: + south: + room: 1001 + condition: + player_flag: has_ticket + blocked_message: "You don't have a ticket!" + north: + room: 1003 + condition: + player_flag: denali_lined_up + blocked_message: "A mass of people blocks the way." + set_player_flags: + denali_intro_done: true +on_enter: + - condition: + player_flag: denali_lined_up + not: true + delay: 5 + message: "Some of the group look you up and down." + - condition: + player_flag: denali_lined_up + not: true + delay: 5 + message: "The pilot pops out of the hatch of the craft and calls out, \"Shuttle to Passenger Liner Denali. Tickets out, please! Nice and orderly!\"" + set_player_flags: + denali_lined_up: true + - condition: + player_flag: denali_lined_up + not: true + delay: 0 + message: "The people line up single file." diff --git a/data/rooms/intro/1003.yaml b/data/rooms/intro/1003.yaml new file mode 100644 index 0000000..a3fd89f --- /dev/null +++ b/data/rooms/intro/1003.yaml @@ -0,0 +1,5 @@ +id: 1003 +name: "Customs Building" +description: "A cramped customs checkpoint lit by flickering panels. Officials in grey uniforms wave the new arrivals through, one at a time. (This area is still under construction.)" +exits: + south: 1002 diff --git a/data/rooms/player_housing/300.yaml b/data/rooms/player_housing/300.yaml deleted file mode 100644 index 06f1a07..0000000 --- a/data/rooms/player_housing/300.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 300 -name: "Player House: Entrance Hall" -description: "A cozy entrance hall with polished floors and warm lighting. A welcome mat greets visitors at the door." -exits: - south: 170 - north: 301 -objects: - - id: charging_station diff --git a/data/rooms/player_housing/301.yaml b/data/rooms/player_housing/301.yaml deleted file mode 100644 index c2f3e65..0000000 --- a/data/rooms/player_housing/301.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 301 -name: "Player House: Workshop" -description: "A well-lit workshop with a sturdy workbench covered in tools and blueprints. Wood shavings litter the floor around the bench." -exits: - south: 300 -objects: - - id: workbench - - id: bank_booth diff --git a/data/rooms/town/1.yaml b/data/rooms/town/1.yaml deleted file mode 100644 index 81e9351..0000000 --- a/data/rooms/town/1.yaml +++ /dev/null @@ -1,80 +0,0 @@ -id: 1 -name: "Town Square" -description: "The central square of Lumbridge. A fountain gurgles peacefully in the center. Paths lead west toward the gathering grounds and east toward the production district." -exits: - east: 2 - west: 22 - up: 30 - down: 31 -objects: - - id: lumby_fountain - - id: charging_station - - id: bank_booth - - id: aps_tower -mobs: - - "newbie_trainer" - - "general_store_clerk" -item_spawns: - - id: cape_of_agility - quantity: 1 - respawn_ticks: 60 - - id: graceful_hat - quantity: 1 - respawn_ticks: 60 - - id: graceful_torso - quantity: 1 - respawn_ticks: 60 - - id: graceful_legs - quantity: 1 - respawn_ticks: 60 - - id: graceful_gloves - quantity: 1 - respawn_ticks: 60 - - id: graceful_boots - quantity: 1 - respawn_ticks: 60 - - id: graceful_cape - quantity: 1 - respawn_ticks: 60 - - id: basic_deck - quantity: 1 - respawn_ticks: 60 - - id: solar_identifier - quantity: 1 - respawn_ticks: 60 - - id: hydro_identifier - quantity: 1 - respawn_ticks: 60 - - id: cosmic_identifier - quantity: 1 - respawn_ticks: 60 - - id: eco_identifier - quantity: 1 - respawn_ticks: 60 - - id: chaos_identifier - quantity: 1 - respawn_ticks: 60 - - id: bio_identifier - quantity: 1 - respawn_ticks: 60 - - id: nature_identifier - quantity: 1 - respawn_ticks: 60 - - id: law_identifier - quantity: 1 - respawn_ticks: 60 - - id: death_identifier - quantity: 1 - respawn_ticks: 60 - - id: blood_identifier - quantity: 1 - respawn_ticks: 60 - - id: grimy_guam - quantity: 5 - respawn_ticks: 60 - - id: vial_of_water - quantity: 5 - respawn_ticks: 60 - - id: eye_of_newt - quantity: 5 - respawn_ticks: 60 diff --git a/data/rooms/town/10.yaml b/data/rooms/town/10.yaml deleted file mode 100644 index edeac4d..0000000 --- a/data/rooms/town/10.yaml +++ /dev/null @@ -1,10 +0,0 @@ -id: 10 -name: "Farming Plot" -description: "A small patch of tilled soil behind a weathered fence. This area is not yet accessible." -exits: - west: 9 - east: 11 -mobs: - - id: skeleton - - id: skeleton - - id: mutant_rat diff --git a/data/rooms/town/11.yaml b/data/rooms/town/11.yaml deleted file mode 100644 index 9bc45a7..0000000 --- a/data/rooms/town/11.yaml +++ /dev/null @@ -1,17 +0,0 @@ -id: 11 -name: "Kitchen" -description: "A warm kitchen with a stone hearth and wooden counters. A large iron cooking range dominates one wall, and shelves are stocked with ingredients." -exits: - east: 12 - west: 10 -objects: - - id: cooking_range -mobs: - - id: zombie -item_spawns: - - id: bucket_of_water - quantity: 1 - respawn_ticks: 200 - - id: pot_of_flour - quantity: 1 - respawn_ticks: 200 diff --git a/data/rooms/town/12.yaml b/data/rooms/town/12.yaml deleted file mode 100644 index e44ae3a..0000000 --- a/data/rooms/town/12.yaml +++ /dev/null @@ -1,28 +0,0 @@ -id: 12 -name: "Forge" -description: "A blazing forge and anvil dominate this smoky workshop. The heat from the furnace is intense." -exits: - east: 13 - west: 11 -mobs: - - "weapons_smith" -objects: - - id: furnace - - id: anvil - - id: bank_booth -item_spawns: - - id: copper_ore - quantity: 3 - respawn_ticks: 30 - - id: tin_ore - quantity: 3 - respawn_ticks: 30 - - id: iron_ore - quantity: 3 - respawn_ticks: 30 - - id: coal - quantity: 5 - respawn_ticks: 30 - - id: hammer - quantity: 1 - respawn_ticks: 30 diff --git a/data/rooms/town/13.yaml b/data/rooms/town/13.yaml deleted file mode 100644 index dc07dc1..0000000 --- a/data/rooms/town/13.yaml +++ /dev/null @@ -1,46 +0,0 @@ -id: 13 -name: "Crafting Workshop" -description: "A bustling workshop filled with crafting stations. A {130}spinning wheel{/} sits in one corner, a {173}pottery wheel{/} and {196}pottery oven{/} in another. Racks of tools and raw materials line the walls. The forge is just to the west." -exits: - east: 14 - west: 12 -objects: - - id: spinning_wheel - - id: pottery_wheel - - id: pottery_oven - - id: sheep - - id: clay_rock - - id: clay_rock -mobs: - - id: tanner - - id: cow - - id: cow - - id: cow -item_spawns: - - id: flax - quantity: 5 - respawn_ticks: 60 - - id: thread - quantity: 10 - respawn_ticks: 60 - - id: shears - quantity: 1 - respawn_ticks: 60 - - id: needle - quantity: 1 - respawn_ticks: 60 - - id: chisel - quantity: 1 - respawn_ticks: 60 - - id: ring_mould - quantity: 1 - respawn_ticks: 60 - - id: necklace_mould - quantity: 1 - respawn_ticks: 60 - - id: amulet_mould - quantity: 1 - respawn_ticks: 60 - - id: bracelet_mould - quantity: 1 - respawn_ticks: 60 diff --git a/data/rooms/town/14.yaml b/data/rooms/town/14.yaml deleted file mode 100644 index ad515de..0000000 --- a/data/rooms/town/14.yaml +++ /dev/null @@ -1,18 +0,0 @@ -id: 14 -name: "Fletching Bench" -description: "A workbench covered in wood shavings, feathers, and arrowheads. Racks of unfinished bows line the walls, and a pile of bowstrings sits in the corner." -exits: - east: 15 - west: 13 -mobs: - - id: moss_giant -item_spawns: - - id: bowstring - quantity: 5 - respawn_ticks: 50 - - id: knife - quantity: 1 - respawn_ticks: 50 - - id: chisel - quantity: 1 - respawn_ticks: 50 diff --git a/data/rooms/town/15.yaml b/data/rooms/town/15.yaml deleted file mode 100644 index a67624d..0000000 --- a/data/rooms/town/15.yaml +++ /dev/null @@ -1,10 +0,0 @@ -id: 15 -name: "Pharmacy Lab" -description: "Shelves lined with glass vials, dried herbs, and bubbling cauldrons. This area is not yet accessible." -exits: - east: 16 - west: 14 - south: 150 -mobs: - - id: lesser_drone - - id: lesser_drone diff --git a/data/rooms/town/16.yaml b/data/rooms/town/16.yaml deleted file mode 100644 index 3776f4f..0000000 --- a/data/rooms/town/16.yaml +++ /dev/null @@ -1,12 +0,0 @@ -id: 16 -name: "Construction Site" -description: "A half-built structure surrounded by planks, nails, and blueprints. A workbench stands ready for crafting. Signs point north toward the Local Neighborhood and east toward the lumberyard." -exits: - east: 171 - west: 15 - north: 170 -objects: - - id: workbench -mobs: - - id: greater_drone - - id: estate_broker diff --git a/data/rooms/town/17.yaml b/data/rooms/town/17.yaml deleted file mode 100644 index 512ec9e..0000000 --- a/data/rooms/town/17.yaml +++ /dev/null @@ -1,10 +0,0 @@ -id: 17 -name: "Bonfire" -description: "A ring of stones surrounds a bed of ash. A few unlit logs wait nearby. A narrow passage leads north toward what sounds like echoing footsteps and clanging metal." -exits: - east: 18 - west: 16 - north: 200 -mobs: - - id: scrap_bot - - id: scrap_bot diff --git a/data/rooms/town/18.yaml b/data/rooms/town/18.yaml deleted file mode 100644 index 9dfc446..0000000 --- a/data/rooms/town/18.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: 18 -name: "Thieves' Den" -description: "A shadowy hideout with lockpicks, dark cloaks, and wanted posters on the walls. This area is not yet accessible." -exits: - east: 19 - west: 17 diff --git a/data/rooms/town/19.yaml b/data/rooms/town/19.yaml deleted file mode 100644 index 35800a0..0000000 --- a/data/rooms/town/19.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: 19 -name: "Agility Course" -description: "A training ground with balance beams, monkey bars, and climbing nets. This area is not yet accessible." -exits: - east: 20 - west: 18 diff --git a/data/rooms/town/2.yaml b/data/rooms/town/2.yaml deleted file mode 100644 index 8d3fe2d..0000000 --- a/data/rooms/town/2.yaml +++ /dev/null @@ -1,16 +0,0 @@ -id: 2 -name: "Fishing Spot" -description: "A quiet spot along the riverbank. The water is clear and fish can be seen swimming beneath the surface. You can cast a line here with a fishing rod and bait." -exits: - east: 4 - north: 3 - west: 1 -objects: - - id: bait_fishing_spot -item_spawns: - - id: fishing_rod - quantity: 1 - respawn_ticks: 30 - - id: fishing_bait - quantity: 20 - respawn_ticks: 60 diff --git a/data/rooms/town/20.yaml b/data/rooms/town/20.yaml deleted file mode 100644 index 379322b..0000000 --- a/data/rooms/town/20.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: 20 -name: "Assassin's Bureau" -description: "A discreet office with job postings pinned to a corkboard. This area is not yet accessible." -exits: - east: 21 - west: 19 diff --git a/data/rooms/town/21.yaml b/data/rooms/town/21.yaml deleted file mode 100644 index ac6dfad..0000000 --- a/data/rooms/town/21.yaml +++ /dev/null @@ -1,12 +0,0 @@ -id: 21 -name: "Combat Arena" -description: "A sandy training ground with practice dummies and sparring rings. Several men wander here looking for a fight." -exits: - west: 20 -mobs: - - id: man - wander_interval: 10 - - id: man - wander_interval: 12 - - id: man - wander_interval: 15 diff --git a/data/rooms/town/22.yaml b/data/rooms/town/22.yaml deleted file mode 100644 index 2199783..0000000 --- a/data/rooms/town/22.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: 22 -name: "West of Town Square" -description: "" -exits: - east: 1 - west: 23 diff --git a/data/rooms/town/23.yaml b/data/rooms/town/23.yaml deleted file mode 100644 index 45a9d18..0000000 --- a/data/rooms/town/23.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: 23 -name: "West of West of Town Square" -description: "Here is a description of where we are!" -exits: - east: 22 - south: 24 diff --git a/data/rooms/town/24.yaml b/data/rooms/town/24.yaml deleted file mode 100644 index e1c9068..0000000 --- a/data/rooms/town/24.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: 24 -name: "South of West of West of Town Square" -description: "Here is a description of where we are!" -exits: - north: 23 - east: 25 diff --git a/data/rooms/town/25.yaml b/data/rooms/town/25.yaml deleted file mode 100644 index 78bc181..0000000 --- a/data/rooms/town/25.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: 25 -name: "East of South of West of West of Town Square" -description: "Here is a description of where we are!" -exits: - west: 24 - east: 26 diff --git a/data/rooms/town/26.yaml b/data/rooms/town/26.yaml deleted file mode 100644 index 0db976e..0000000 --- a/data/rooms/town/26.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: 26 -name: "East of South of West of West of Town Square" -description: "Here is a description of where we are!" -exits: - west: 25 - south: 27 diff --git a/data/rooms/town/27.yaml b/data/rooms/town/27.yaml deleted file mode 100644 index d820374..0000000 --- a/data/rooms/town/27.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: 27 -name: "East of South of West of West of Town Square" -description: "Here is a description of where we are!" -exits: - west: 28 - north: 26 diff --git a/data/rooms/town/28.yaml b/data/rooms/town/28.yaml deleted file mode 100644 index b34ada0..0000000 --- a/data/rooms/town/28.yaml +++ /dev/null @@ -1,5 +0,0 @@ -id: 28 -name: "East of South of West of West of Town Square" -description: "Here is a description of where we are!" -exits: - east: 27 diff --git a/data/rooms/town/3.yaml b/data/rooms/town/3.yaml deleted file mode 100644 index 8a1cade..0000000 --- a/data/rooms/town/3.yaml +++ /dev/null @@ -1,14 +0,0 @@ -id: 3 -name: "Fly Fishing Pool" -description: "A calm pool where insects hover over the water. Perfect for fly fishing." -exits: - south: 2 -objects: - - id: fly_fishing_spot -item_spawns: - - id: fly_fishing_rod - quantity: 1 - respawn_ticks: 30 - - id: feather - quantity: 15 - respawn_ticks: 60 diff --git a/data/rooms/town/30.yaml b/data/rooms/town/30.yaml deleted file mode 100644 index ddce834..0000000 --- a/data/rooms/town/30.yaml +++ /dev/null @@ -1,5 +0,0 @@ -id: 30 -name: "Above Town Square" -description: "The central square of Lumbridge. A fountain gurgles peacefully in the center. Paths lead west toward the gathering grounds and east toward the production district." -exits: - down: 1 diff --git a/data/rooms/town/31.yaml b/data/rooms/town/31.yaml deleted file mode 100644 index 5b5f4d5..0000000 --- a/data/rooms/town/31.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: 31 -name: "Below Town Square" -description: "The central square of Lumbridge. A fountain gurgles peacefully in the center. Paths lead west toward the gathering grounds and east toward the production district." -exits: - up: 1 - down: 50 diff --git a/data/rooms/town/4.yaml b/data/rooms/town/4.yaml deleted file mode 100644 index 9453651..0000000 --- a/data/rooms/town/4.yaml +++ /dev/null @@ -1,36 +0,0 @@ -id: 4 -name: "Woodcutting Glade" -description: "A sunny glade at the edge of the forest. A sturdy tree stands here, perfect for practicing woodcutting. A solid rock formation near the edge of the glade looks like it could provide cover." -exits: - east: 6 - north: 5 - west: 2 -objects: - - id: tree - - id: tree - - id: tree - - id: tree - - id: simple_rock - - id: tutorial_lever -mobs: - - id: rat - - id: rat -item_spawns: - - id: bronze_axe - quantity: 1 - respawn_ticks: 60 - - id: matches - quantity: 100 - respawn_ticks: 120 - - id: firesteel - quantity: 1 - respawn_ticks: 120 - - id: lighter - quantity: 1 - respawn_ticks: 120 - - id: shortbow - quantity: 1 - respawn_ticks: 60 - - id: bronze_arrow - quantity: 100 - respawn_ticks: 60 diff --git a/data/rooms/town/5.yaml b/data/rooms/town/5.yaml deleted file mode 100644 index cdf84d8..0000000 --- a/data/rooms/town/5.yaml +++ /dev/null @@ -1,11 +0,0 @@ -id: 5 -name: "Oak Grove" -description: "A stand of young oak trees. The wood here is tougher but more valuable." -exits: - south: 4 -objects: - - id: oak_tree -item_spawns: - - id: iron_axe - quantity: 1 - respawn_ticks: 60 diff --git a/data/rooms/town/6.yaml b/data/rooms/town/6.yaml deleted file mode 100644 index 34b5bd0..0000000 --- a/data/rooms/town/6.yaml +++ /dev/null @@ -1,15 +0,0 @@ -id: 6 -name: "Mining Pit" -description: "A small mining pit. The rocky walls glint with veins of copper ore." -exits: - east: 8 - north: 7 - west: 4 -objects: - - id: copper_rock - - id: copper_rock - - id: copper_rock -item_spawns: - - id: bronze_pickaxe - quantity: 1 - respawn_ticks: 30 diff --git a/data/rooms/town/7.yaml b/data/rooms/town/7.yaml deleted file mode 100644 index 37dfed9..0000000 --- a/data/rooms/town/7.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 7 -name: "Deep Mine" -description: "A deeper mine shaft. Higher quality ores can be found here. This area is not yet accessible." -exits: - south: 6 -mobs: - - id: goblin - - id: goblin - - id: bandit diff --git a/data/rooms/town/8.yaml b/data/rooms/town/8.yaml deleted file mode 100644 index b82c1bb..0000000 --- a/data/rooms/town/8.yaml +++ /dev/null @@ -1,11 +0,0 @@ -id: 8 -name: "Hacking Lab" -description: "A dimly lit room filled with old terminals and flickering screens. Cables dangle from the ceiling, some still sparking. The air smells of ozone and burnt circuitry. A {40}basic terminal{/} against the far wall still has power." -exits: - east: 9 - west: 6 -objects: - - id: terminal_basic - - id: terminal_advanced - - id: terminal_secure - - id: netrunner diff --git a/data/rooms/town/9.yaml b/data/rooms/town/9.yaml deleted file mode 100644 index 2835128..0000000 --- a/data/rooms/town/9.yaml +++ /dev/null @@ -1,13 +0,0 @@ -id: 9 -name: "Scavenging Post" -description: "A cluttered yard full of {243}scrap{/} and discarded technology. Piles of salvageable debris are scattered across the ground. The air smells of ozone and rust." -exits: - east: 10 - west: 8 - north: 150 - south: 151 -objects: - - id: scrap_pile - - id: scrap_pile - - id: scrap_pile - - id: solar_altar diff --git a/data/rooms/underground/50.yaml b/data/rooms/underground/50.yaml deleted file mode 100644 index 0c9ebf2..0000000 --- a/data/rooms/underground/50.yaml +++ /dev/null @@ -1,7 +0,0 @@ -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/underground/51.yaml b/data/rooms/underground/51.yaml deleted file mode 100644 index 74a97ef..0000000 --- a/data/rooms/underground/51.yaml +++ /dev/null @@ -1,10 +0,0 @@ -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/underground/52.yaml b/data/rooms/underground/52.yaml deleted file mode 100644 index 2f90abd..0000000 --- a/data/rooms/underground/52.yaml +++ /dev/null @@ -1,9 +0,0 @@ -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/underground/53.yaml b/data/rooms/underground/53.yaml deleted file mode 100644 index 126719b..0000000 --- a/data/rooms/underground/53.yaml +++ /dev/null @@ -1,9 +0,0 @@ -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/underground/54.yaml b/data/rooms/underground/54.yaml deleted file mode 100644 index e86e5b7..0000000 --- a/data/rooms/underground/54.yaml +++ /dev/null @@ -1,9 +0,0 @@ -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 - down: 55 -mobs: - - "phantom" - - "phantom" diff --git a/data/rooms/underground/55.yaml b/data/rooms/underground/55.yaml deleted file mode 100644 index 8000170..0000000 --- a/data/rooms/underground/55.yaml +++ /dev/null @@ -1,12 +0,0 @@ -id: 55 -name: "The Deeper Tunnels" -description: "A junction deep beneath the void chamber. The walls here are reinforced with pre-regression alloys. Several passages branch off into the darkness, and you can hear the sounds of machinery echoing from all directions. A steep shaft descends further into the depths." -exits: - up: 54 - north: 56 - east: 57 - south: 58 - west: 60 - down: 62 -mobs: - - id: security_turret diff --git a/data/rooms/underground/56.yaml b/data/rooms/underground/56.yaml deleted file mode 100644 index 375696d..0000000 --- a/data/rooms/underground/56.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 56 -name: "Irradiated Sector" -description: "Warning symbols cover every surface, their paint long since faded. A faint green glow emanates from cracks in the walls. The air is thick with the taste of old radiation." -exits: - south: 55 -mobs: - - id: ghoul - - id: ghoul - - id: corrupted_scientist diff --git a/data/rooms/underground/57.yaml b/data/rooms/underground/57.yaml deleted file mode 100644 index 714197d..0000000 --- a/data/rooms/underground/57.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 57 -name: "Drone Assembly" -description: "A long-abandoned drone manufacturing bay. Conveyor belts lie still, and half-assembled drone chassis hang from the ceiling. A few of them still twitch with residual power." -exits: - west: 55 -mobs: - - id: war_drone - - id: war_drone diff --git a/data/rooms/underground/58.yaml b/data/rooms/underground/58.yaml deleted file mode 100644 index f4d1829..0000000 --- a/data/rooms/underground/58.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 58 -name: "Security Checkpoint" -description: "A fortified security station with blast shields and a decommissioned weapons locker. Scorch marks on the walls tell the story of a last stand." -exits: - north: 55 - south: 59 -mobs: - - id: enforcer_bot diff --git a/data/rooms/underground/59.yaml b/data/rooms/underground/59.yaml deleted file mode 100644 index a1cc594..0000000 --- a/data/rooms/underground/59.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: 59 -name: "Plasma Core" -description: "A massive plasma containment chamber with cracked magnetic coils lining the walls. Streams of superheated gas leak from the containment vessel, forming shimmering wraith-like shapes in the air." -exits: - north: 58 -mobs: - - id: plasma_wraith - - id: plasma_wraith - - id: feral_cyborg diff --git a/data/rooms/underground/60.yaml b/data/rooms/underground/60.yaml deleted file mode 100644 index e9b7114..0000000 --- a/data/rooms/underground/60.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 60 -name: "Heavy Mech Bay" -description: "An enormous maintenance bay built to house assault mechs. A gantry crane hangs overhead, and hydraulic lifts dot the floor. The bay's occupant is still active — and it has noticed you." -exits: - east: 55 - west: 61 -mobs: - - id: heavy_mech diff --git a/data/rooms/underground/61.yaml b/data/rooms/underground/61.yaml deleted file mode 100644 index a85c7ba..0000000 --- a/data/rooms/underground/61.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 61 -name: "Synth Lab" -description: "A sterile laboratory where combat androids were designed and assembled. White panels line the walls, some splattered with old coolant stains. Diagnostic screens flicker with corrupted data." -exits: - east: 60 -mobs: - - id: elite_synth diff --git a/data/rooms/underground/62.yaml b/data/rooms/underground/62.yaml deleted file mode 100644 index 9ed1fad..0000000 --- a/data/rooms/underground/62.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 62 -name: "Abandoned Bunker" -description: "A fortified bunker from some forgotten conflict. Sandbags are piled in the corners and spent shell casings litter the floor. A solitary figure stands watch over the remains." -exits: - up: 55 - down: 63 -mobs: - - id: veteran_merc diff --git a/data/rooms/underground/63.yaml b/data/rooms/underground/63.yaml deleted file mode 100644 index b6bf53e..0000000 --- a/data/rooms/underground/63.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 63 -name: "Magma Conduit" -description: "A geothermal vent has been tapped and routed through industrial conduits. The heat is oppressive, and molten rivulets course through channels in the floor. Something moves within the glowing flow." -exits: - up: 62 - down: 64 -mobs: - - id: molten_elemental diff --git a/data/rooms/underground/64.yaml b/data/rooms/underground/64.yaml deleted file mode 100644 index b412992..0000000 --- a/data/rooms/underground/64.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 64 -name: "The Foundry" -description: "A vast smelting facility where sentinels were once forged. Massive crucibles hang from chains, and rivers of cooled slag form a maze-like floor. A single iron sentinel stands guard over the dormant machinery." -exits: - up: 63 - down: 65 -mobs: - - id: iron_sentinel diff --git a/data/rooms/underground/65.yaml b/data/rooms/underground/65.yaml deleted file mode 100644 index 47aed6b..0000000 --- a/data/rooms/underground/65.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 65 -name: "Titan's Rest" -description: "A colossal vault built to house something immense. The ceiling vanishes into darkness above, and the walls are etched with maintenance schematics for a machine of staggering proportions. At the center, a titan sentinel awakens." -exits: - up: 64 - down: 66 -mobs: - - id: titan_sentinel diff --git a/data/rooms/underground/66.yaml b/data/rooms/underground/66.yaml deleted file mode 100644 index 755d5e2..0000000 --- a/data/rooms/underground/66.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 66 -name: "Psionic Nexus" -description: "Reality grows thin here. The walls are covered in strange geometric patterns that hurt to look at directly. A low hum vibrates through your skull, and the air seems to bend around a central point of unseen pressure." -exits: - up: 65 - down: 67 -mobs: - - id: psionic_entity diff --git a/data/rooms/underground/67.yaml b/data/rooms/underground/67.yaml deleted file mode 100644 index 6b8d663..0000000 --- a/data/rooms/underground/67.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: 67 -name: "Ancient Chamber" -description: "A perfectly spherical room carved from an unknown black metal. Glowing runes spiral across every surface in patterns that predate the asteroid itself. At the center, an ancient construct stirs from its eons-long vigil." -exits: - up: 66 - down: 68 -mobs: - - id: ancient_construct diff --git a/data/rooms/underground/68.yaml b/data/rooms/underground/68.yaml deleted file mode 100644 index f631981..0000000 --- a/data/rooms/underground/68.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 68 -name: "The Void" -description: "The bottom of the world. Light itself seems to hesitate before entering this place. The floor is unnaturally smooth black glass, and the only sound is the beating of your own heart. Something waits in the darkness — something that was here before the asteroid, before the colony, before anything." -exits: - up: 67 -mobs: - - id: void_reaper diff --git a/data/rooms/wilderness/150.yaml b/data/rooms/wilderness/150.yaml deleted file mode 100644 index 3e74da2..0000000 --- a/data/rooms/wilderness/150.yaml +++ /dev/null @@ -1,31 +0,0 @@ -id: 150 -name: "Hydroponics Bay" -description: "A large enclosed area with climate-controlled growing stations. {34}Lush green patches{/} of soil are laid out in neat rows under artificial UV lamps. A {94}tool shed{/} stands near the entrance. Exits lead to specialized growing areas." -exits: - north: 15 - east: 151 - south: 152 -objects: - - id: tool_shed -item_spawns: - - id: rake - quantity: 1 - respawn_ticks: 120 - - id: spade - quantity: 1 - respawn_ticks: 120 - - id: watering_can - quantity: 1 - respawn_ticks: 120 - - id: plant_cure - quantity: 3 - respawn_ticks: 200 - - id: guam_seed - quantity: 5 - respawn_ticks: 300 - - id: potato_seed - quantity: 10 - respawn_ticks: 300 - - id: onion_seed - quantity: 10 - respawn_ticks: 300 diff --git a/data/rooms/wilderness/151.yaml b/data/rooms/wilderness/151.yaml deleted file mode 100644 index 4242270..0000000 --- a/data/rooms/wilderness/151.yaml +++ /dev/null @@ -1,10 +0,0 @@ -id: 151 -name: "Herb Garden" -description: "A dedicated section of the hydroponics bay for growing herbs. {28}Herb patches{/} are arranged in two rows under specialized grow lights. The air here smells faintly of fresh vegetation." -exits: - west: 150 -objects: - - id: herb_patch - - id: herb_patch - - id: herb_patch - - id: herb_patch diff --git a/data/rooms/wilderness/152.yaml b/data/rooms/wilderness/152.yaml deleted file mode 100644 index 787bbbd..0000000 --- a/data/rooms/wilderness/152.yaml +++ /dev/null @@ -1,10 +0,0 @@ -id: 152 -name: "Allotment Field" -description: "A section with larger soil beds for growing vegetables. {94}Allotment patches{/} stretch across the floor under warm overhead lamps. A single {213}flower patch{/} sits in the corner." -exits: - north: 150 -objects: - - id: allotment_patch - - id: allotment_patch - - id: allotment_patch - - id: flower_patch diff --git a/data/rooms/wilderness/160.yaml b/data/rooms/wilderness/160.yaml deleted file mode 100644 index 7f30c34..0000000 --- a/data/rooms/wilderness/160.yaml +++ /dev/null @@ -1,16 +0,0 @@ -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." -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/wilderness/161.yaml b/data/rooms/wilderness/161.yaml deleted file mode 100644 index 7c2c140..0000000 --- a/data/rooms/wilderness/161.yaml +++ /dev/null @@ -1,12 +0,0 @@ -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." -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/wilderness/162.yaml b/data/rooms/wilderness/162.yaml deleted file mode 100644 index 6aab585..0000000 --- a/data/rooms/wilderness/162.yaml +++ /dev/null @@ -1,5 +0,0 @@ -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." -exits: - north: 1 \ No newline at end of file diff --git a/data/rooms/wilderness/170.yaml b/data/rooms/wilderness/170.yaml deleted file mode 100644 index 35d539f..0000000 --- a/data/rooms/wilderness/170.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 170 -name: "Local Neighborhood" -description: "A quiet cul-de-sac of modest homes, each with a small garden plot. An estate directory terminal glows in the center, listing residents and property information." -exits: - south: 16 -objects: - - id: estate_directory diff --git a/data/rooms/wilderness/171.yaml b/data/rooms/wilderness/171.yaml deleted file mode 100644 index 552ccd4..0000000 --- a/data/rooms/wilderness/171.yaml +++ /dev/null @@ -1,7 +0,0 @@ -id: 171 -name: "Lumberyard" -description: "Stacks of fresh-cut timber fill the air with the scent of pine. A massive industrial sawmill hums in the background, operated by a burly worker." -exits: - west: 16 -mobs: - - id: sawmill_operator diff --git a/internal/game/action.go b/internal/game/action.go index 9a07419..8d50eea 100644 --- a/internal/game/action.go +++ b/internal/game/action.go @@ -284,22 +284,17 @@ func (g *Game) checkCondition(sess *net.Session, c *action.Condition) bool { } if c.PlayerFlag != "" { - if p == nil || p.Flags == nil { - return c.Not + var val any + present := false + if p != nil && p.Flags != nil { + val, present = p.Flags[c.PlayerFlag] } - val, ok := p.Flags[c.PlayerFlag] - if c.Not { - return !ok || val != c.Value - } - return ok && val == c.Value + return flagMatches(present, val, c.Value, c.Not) } if c.Flag != "" { - val, ok := g.WorldFlags[c.Flag] - if c.Not { - return !ok || val != c.Value - } - return ok && val == c.Value + val, present := g.WorldFlags[c.Flag] + return flagMatches(present, val, c.Value, c.Not) } if c.HasItem != "" { has := p != nil && p.HasItem(c.HasItem) @@ -317,3 +312,41 @@ func (g *Game) checkCondition(sess *net.Session, c *action.Condition) bool { } return true } + +// flagMatches evaluates a flag/player_flag condition. +// +// If the condition specifies no `value`, the flag matches when it is present +// and truthy (so `{player_flag: x}` means "x is set" and +// `{player_flag: x, not: true}` means "x is not set"). If a `value` is given, +// the flag must be present and equal to it. `not` inverts the result. +func flagMatches(present bool, val, want any, not bool) bool { + var match bool + if want == nil { + match = present && isTruthy(val) + } else { + match = present && val == want + } + if not { + return !match + } + return match +} + +func isTruthy(v any) bool { + switch x := v.(type) { + case nil: + return false + case bool: + return x + case int: + return x != 0 + case int64: + return x != 0 + case float64: + return x != 0 + case string: + return x != "" + default: + return true + } +} diff --git a/internal/game/action_room.go b/internal/game/action_room.go index af5b99b..6c886c1 100644 --- a/internal/game/action_room.go +++ b/internal/game/action_room.go @@ -5,19 +5,194 @@ import ( "strings" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" ) +// enterSeq is an in-flight on_enter cutscene for a single player. It is driven +// one step at a time by EnterSeqTick. +type enterSeq struct { + sess *net.Session + playerName string + roomID int + steps []world.EnterStep + wait int + started bool +} + +// runEnterSteps evaluates a room's on_enter script for the player. Steps whose +// condition fails are dropped. If none of the surviving steps are "timed" +// (no delay, no flag mutation) the messages are printed synchronously, exactly +// as before. Otherwise the surviving steps become a scheduled cutscene driven +// by EnterSeqTick. func (g *Game) runEnterSteps(sess *net.Session, roomID int) { room, err := g.World.LoadRoom(roomID) if err != nil || len(room.OnEnter) == 0 { return } + p := sess.Player + if p == nil { + return + } + + var steps []world.EnterStep + cutscene := false for _, step := range room.OnEnter { if step.Condition != nil && !g.checkCondition(sess, step.Condition) { continue } - if step.Message != "" { - sess.WriteLine(fmt.Sprintf("%s", step.Message)) + steps = append(steps, step) + if step.IsTimed() { + cutscene = true + } + } + if len(steps) == 0 { + return + } + + if !cutscene { + for _, step := range steps { + if step.Message != "" { + sess.WriteLine(step.Message) + } + } + return + } + + g.startEnterCutscene(sess, p, roomID, steps) +} + +// startEnterCutscene registers a scheduled on_enter sequence and persists a +// marker on the player so the cutscene can be resumed on reconnect if it is +// interrupted (disconnect, server restart). +func (g *Game) startEnterCutscene(sess *net.Session, p *player.Player, roomID int, steps []world.EnterStep) { + g.cancelEnterSeq(p.Name) + if p.CutsceneRoom != roomID { + p.CutsceneRoom = roomID + g.AccountStore.SaveCharacter(p) + } + g.enterMu.Lock() + g.enterSeqs[p.Name] = &enterSeq{ + sess: sess, + playerName: p.Name, + roomID: roomID, + steps: steps, + wait: steps[0].Delay, + } + g.enterMu.Unlock() +} + +// EnterSeqTick advances every in-flight cutscene by one tick. Sequences for a +// player who is no longer connected are paused (left for resume); sequences are +// removed from the map on completion or cancellation. +func (g *Game) EnterSeqTick() { + g.enterMu.Lock() + seqs := make([]*enterSeq, 0, len(g.enterSeqs)) + for _, s := range g.enterSeqs { + seqs = append(seqs, s) + } + g.enterMu.Unlock() + + for _, seq := range seqs { + if !g.isCharLive(seq.playerName, seq.sess) { + continue + } + + var jobs []world.EnterStep + done := false + + g.enterMu.Lock() + if g.enterSeqs[seq.playerName] != seq { + g.enterMu.Unlock() + continue + } + seq.wait-- + for seq.wait <= 0 && len(seq.steps) > 0 { + step := seq.steps[0] + seq.steps = seq.steps[1:] + jobs = append(jobs, step) + if len(seq.steps) > 0 { + seq.wait = seq.steps[0].Delay + } else { + seq.wait = 0 + } + } + if len(seq.steps) == 0 { + done = true + delete(g.enterSeqs, seq.playerName) + } + g.enterMu.Unlock() + + for _, step := range jobs { + g.fireEnterStep(seq, step) + } + if done { + g.completeEnterSeq(seq) + } + } +} + +func (g *Game) fireEnterStep(seq *enterSeq, step world.EnterStep) { + p := seq.sess.Player + if p == nil || p.RoomID != seq.roomID { + return + } + if step.Message != "" { + // Break from the prompt on the first line only, so a multi-line + // cutscene reads as tight consecutive lines. + if !seq.started { + seq.sess.WriteLine("\n" + step.Message) + seq.started = true + } else { + seq.sess.WriteLine(step.Message) + } + } + if len(step.SetFlags) > 0 || len(step.SetPlayerFlags) > 0 { + g.applyFlagMutations(p, step.SetFlags, step.SetPlayerFlags) + g.AccountStore.SaveCharacter(p) + } +} + +func (g *Game) completeEnterSeq(seq *enterSeq) { + if !g.isCharLive(seq.playerName, seq.sess) { + return + } + p := seq.sess.Player + if p == nil { + return + } + if p.CutsceneRoom == seq.roomID { + p.CutsceneRoom = 0 + g.AccountStore.SaveCharacter(p) + } + g.writePrompt(seq.sess) +} + +// cancelEnterSeq drops any in-flight cutscene for the player. It does not touch +// the persisted CutsceneRoom marker, so a cutscene cancelled by disconnect can +// still be resumed on reconnect. +func (g *Game) cancelEnterSeq(name string) { + g.enterMu.Lock() + delete(g.enterSeqs, name) + g.enterMu.Unlock() +} + +func (g *Game) isCharLive(name string, sess *net.Session) bool { + g.charsMu.Lock() + defer g.charsMu.Unlock() + return g.loggedInChars[name] == sess +} + +// applyFlagMutations sets world and player flags, shared by on_enter steps and +// exit traversal. +func (g *Game) applyFlagMutations(p *player.Player, setFlags, setPlayerFlags map[string]any) { + for k, v := range setFlags { + g.WorldFlags[k] = v + } + if len(setPlayerFlags) > 0 { + p.EnsureFlags() + for k, v := range setPlayerFlags { + p.Flags[k] = v } } } diff --git a/internal/game/action_state.go b/internal/game/action_state.go index ffce545..bed936d 100644 --- a/internal/game/action_state.go +++ b/internal/game/action_state.go @@ -30,6 +30,7 @@ const ( ActionTraversing ActionType = "traversing" ActionHacking ActionType = "hacking" ActionHiding ActionType = "hiding" + ActionWorking ActionType = "working" ) type ActionState struct { @@ -101,6 +102,8 @@ func (a *ActionState) Description() string { return "jacked into a " + a.TargetName case ActionHiding: return "positioning behind a " + a.TargetName + case ActionWorking: + return "working on a " + a.TargetName } return "" } diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go index 1f18d00..f3b99c0 100644 --- a/internal/game/cmd_attack.go +++ b/internal/game/cmd_attack.go @@ -35,7 +35,11 @@ func (g *Game) doAttack(sess *net.Session, input string) { } if mob.HP <= 0 { - sess.WriteLine("That is already dead.") + if mob.IsTask() { + sess.WriteLine(fmt.Sprintf("%s is already complete.", mobDisplayName(mob, true))) + } else { + sess.WriteLine("That is already dead.") + } return } @@ -133,6 +137,7 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn combat.EnterCombat(p.Name, mob.InstanceID) p.AttackTimer = 0 + isTask := mob.IsTask() autotriggerActive := p.AutotriggerMod != "" if !autotriggerActive { @@ -151,20 +156,35 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn if len(styleParts) > 0 { styleStr = " Style: " + string(p.AttackStyle) + " (" + strings.Join(styleParts, ", ") + ")" } - sess.WriteLine(fmt.Sprintf("You attack %s!%s", g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr)) + if isTask { + sess.WriteLine(fmt.Sprintf("You begin to %s %s.%s", taskWorkVerb(mob), g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr)) + } else { + sess.WriteLine(fmt.Sprintf("You attack %s!%s", g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr)) + } } else { mod := GetMod(p.AutotriggerMod) modName := p.AutotriggerMod if mod != nil { modName = mod.Name } - sess.WriteLine(fmt.Sprintf("You attack %s with %s!", - g.colorize(sess, "mob", mobDisplayName(mob, true)), - g.colorize(sess, "science_mod", modName))) + if isTask { + sess.WriteLine(fmt.Sprintf("You begin to %s %s using %s!", + taskWorkVerb(mob), + g.colorize(sess, "mob", mobDisplayName(mob, true)), + g.colorize(sess, "science_mod", modName))) + } else { + sess.WriteLine(fmt.Sprintf("You attack %s with %s!", + g.colorize(sess, "mob", mobDisplayName(mob, true)), + g.colorize(sess, "science_mod", modName))) + } + } + if isTask { + p.ActionState = &ActionState{Type: ActionWorking, TargetName: mob.Name} + g.broadcastAction(sess, "\n%s begins working on %s.", p.Name, mobDisplayName(mob, true)) + } else { + p.ActionState = &ActionState{Type: ActionCombating, TargetName: mob.Name} + g.broadcastAction(sess, "\n%s attacks %s!", p.Name, mobDisplayName(mob, true)) } - p.ActionState = &ActionState{Type: ActionCombating, TargetName: mob.Name} - - g.broadcastAction(sess, "\n%s attacks %s!", p.Name, mobDisplayName(mob, true)) g.Ticks.Subscribe(1, func() bool { cs := combat.GetCombat(p.Name) @@ -223,22 +243,24 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn return true }) - g.Ticks.Subscribe(engine.ToTicks(mob.Speed), func() bool { - cs := combat.GetCombat(p.Name) - if cs == nil || !cs.Active { - return false - } - currentMob := g.MobStore.GetInstance(cs.MobID) - if currentMob == nil || currentMob.HP <= 0 { - return false - } - g.mobAttack(sess, p, currentMob) - if p.HP <= 0 { - g.endCombat(sess, p, currentMob) - return false - } - return true - }) + if !isTask { + g.Ticks.Subscribe(engine.ToTicks(mob.Speed), func() bool { + cs := combat.GetCombat(p.Name) + if cs == nil || !cs.Active { + return false + } + currentMob := g.MobStore.GetInstance(cs.MobID) + if currentMob == nil || currentMob.HP <= 0 { + return false + } + g.mobAttack(sess, p, currentMob) + if p.HP <= 0 { + g.endCombat(sess, p, currentMob) + return false + } + return true + }) + } } func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) { @@ -299,8 +321,8 @@ func (g *Game) calculatePlayerAttackRoll(p *player.Player, mob *world.MobInstanc equipAttack := combat.SelectBonus(attackType, totals.StabAttack, totals.SlashAttack, totals.CrushAttack, totals.ScienceAttack, totals.RangedAttack) - effectiveAttack := p.Level(player.Attack) + g.techLevelBonus(p, "attack") + g.buffLevelBonus(p, "attack") - attRoll = combat.EffectiveRoll(effectiveAttack, attBonus, equipAttack) + effectiveAccuracy := p.Level(player.Accuracy) + g.techLevelBonus(p, "accuracy") + g.buffLevelBonus(p, "accuracy") + attRoll = combat.EffectiveRoll(effectiveAccuracy, attBonus, equipAttack) mobDefBonus := combat.SelectBonus(attackType, mob.StabDefense, mob.SlashDefense, mob.CrushDefense, @@ -331,8 +353,8 @@ func (g *Game) calculateUnarmedAttackRoll(p *player.Player, mob *world.MobInstan weaponType = object.WeaponMelee attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle)) - effectiveAttack := p.Level(player.Attack) + g.techLevelBonus(p, "attack") + g.buffLevelBonus(p, "attack") - attRoll = combat.EffectiveRoll(effectiveAttack, attBonus, 0) + effectiveAccuracy := p.Level(player.Accuracy) + g.techLevelBonus(p, "accuracy") + g.buffLevelBonus(p, "accuracy") + attRoll = combat.EffectiveRoll(effectiveAccuracy, attBonus, 0) mobDefBonus := combat.SelectBonus(attackType, mob.StabDefense, mob.SlashDefense, mob.CrushDefense, @@ -374,30 +396,34 @@ func (g *Game) applyPlayerHit(sess *net.Session, p *player.Player, mob *world.Mo g.consumeAmmo(sess, p) } - mobName := mobDisplayName(mob, true) - attacker := mob.Name - if !mob.Unique { - attacker = "The " + mob.Name - } - w := len(fmt.Sprintf("You hit %s for %d damage.", mobName, 999)) - if w2 := len(fmt.Sprintf("%s hits you for %d damage.", attacker, 999)); w2 > w { - w = w2 - } - prefix := fmt.Sprintf("You hit %s for %s damage.", g.colorize(sess, "mob", mobName), g.colorize(sess, "damage_dealt", fmt.Sprint(dmg))) - visLen := color.VisibleLen(prefix) - if visLen < w { - prefix += strings.Repeat(" ", w-visLen+1) - } - hpSuffix := fmt.Sprintf("%s [%2d/%d]", g.hpBar(sess, mob.HP, mob.MaxHP), mob.HP, mob.MaxHP) - line := prefix + hpSuffix - if p.OptionBool("xp_drops") && len(gains) > 0 { - var parts []string - for _, gain := range gains { - parts = append(parts, fmt.Sprintf("+%dxp %s", gain.XP, player.SkillAbbr[player.SkillName(gain.Skill)])) + if mob.IsTask() { + g.writeTaskProgress(sess, p, mob, dmg, gains) + } else { + mobName := mobDisplayName(mob, true) + attacker := mob.Name + if !mob.Unique { + attacker = "The " + mob.Name + } + w := len(fmt.Sprintf("You hit %s for %d damage.", mobName, 999)) + if w2 := len(fmt.Sprintf("%s hits you for %d damage.", attacker, 999)); w2 > w { + w = w2 + } + prefix := fmt.Sprintf("You hit %s for %s damage.", g.colorize(sess, "mob", mobName), g.colorize(sess, "damage_dealt", fmt.Sprint(dmg))) + visLen := color.VisibleLen(prefix) + if visLen < w { + prefix += strings.Repeat(" ", w-visLen+1) + } + hpSuffix := fmt.Sprintf("%s [%2d/%d]", g.hpBar(sess, mob.HP, mob.MaxHP), mob.HP, mob.MaxHP) + line := prefix + hpSuffix + if p.OptionBool("xp_drops") && len(gains) > 0 { + var parts []string + for _, gain := range gains { + parts = append(parts, fmt.Sprintf("+%dxp %s", gain.XP, player.SkillAbbr[player.SkillName(gain.Skill)])) + } + line += g.colorize(sess, "xp", " ("+strings.Join(parts, ", ")+")") } - line += g.colorize(sess, "xp", " ("+strings.Join(parts, ", ")+")") + sess.WriteLine(line) } - sess.WriteLine(line) if isRanged && p.AmmoQty <= 0 { sess.WriteLine("You've run out of ammo!") @@ -559,60 +585,51 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst p.ActionState = nil if p.HP <= 0 { - if p.HasActiveTech("retribution") { - retDef := GetTechDef("retribution") - if retDef != nil && mob != nil && mob.HP > 0 { - retDmg := int(float64(p.MaxHP()) * retDef.Effects.RetributionPct) - if retDmg > 0 { - mob.HP -= retDmg - if mob.HP < 0 { - mob.HP = 0 - } - sess.WriteLine(fmt.Sprintf("\nDead Man's Switch activates! %s takes %d damage!", - mobDisplayName(mob, true), retDmg)) - } - } - } - p.DeactivateAllTechs() - p.Stats.RecordDeath() - sess.WriteLine(g.colorize(sess, "death", "\nOh dear, you are dead!")) - p.ClearMoveState() - g.safespotMu.Lock() - if ss, ok := g.safespotStates[p.Name]; ok { - g.safespotMu.Unlock() - g.forceLeaveSafespot(sess, p, ss, "") - } else { - g.safespotMu.Unlock() - } - g.dropItemsOnDeath(p) - p.HP = p.MaxHP() - p.RoomID = 1 - g.AccountStore.SaveCharacter(p) - if g.Hub != nil { - g.Hub.EnterRoom(sess, p.RoomID) - } - g.doLook(sess) - g.writePrompt(sess) + g.killPlayer(sess, p, mob) return } if mob != nil && mob.HP <= 0 { + isTask := mob.IsTask() p.Stats.RecordMobKill(mob.DefID) - sess.WriteLine(g.colorize(sess, "victory", fmt.Sprintf("\nYou have defeated %s!", mobDisplayName(mob, true)))) - g.onAssassinKill(sess, p, mob) + if isTask { + complete := mob.CompleteMessage + if complete == "" { + complete = fmt.Sprintf("You finish your work on %s!", mobDisplayName(mob, true)) + } + sess.WriteLine(g.colorize(sess, "victory", "\n"+complete)) + } else { + 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 { - op := other.Player - mobLvl := mobCombatLevel(mob) - levelStr := g.levelColorize(other, op.CombatLevel(), mobLvl, fmt.Sprintf("(level %d)", mobLvl)) - other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s has slain %s %s!", p.Name, mobDisplayName(mob, false), levelStr))) + if isTask { + other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s finishes working on %s.", p.Name, mobDisplayName(mob, false)))) + } else { + op := other.Player + mobLvl := mobCombatLevel(mob) + levelStr := g.levelColorize(other, op.CombatLevel(), mobLvl, fmt.Sprintf("(level %d)", mobLvl)) + other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s has slain %s %s!", p.Name, mobDisplayName(mob, false), levelStr))) + } } } } + dropLabel := func() string { + if isTask { + return "You receive:" + } + dropper := mob.Name + if !mob.Unique { + dropper = "The " + mob.Name + } + return dropper + " drops:" + }() + if mob.Drops.Remains != "" { g.World.AddReservedItem(p.RoomID, mob.Drops.Remains, 1, p.Name) def, _ := g.ItemStore.Load(mob.Drops.Remains) @@ -620,12 +637,8 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst if def != nil { name = def.Name } - dropper := mob.Name - if !mob.Unique { - dropper = "The " + mob.Name - } coloredName := g.itemColorize(sess, def, name) - sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropper+" drops:"), coloredName)) + sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropLabel), coloredName)) } if len(mob.Drops.Loot) > 0 { @@ -641,15 +654,11 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst if def != nil { name = def.Name } - dropper := mob.Name - if !mob.Unique { - dropper = "The " + mob.Name - } coloredName := g.itemColorize(sess, def, name) if qty > 1 { - sess.WriteLine(fmt.Sprintf("%s %d x %s", g.colorize(sess, "drop_message", dropper+" drops:"), qty, coloredName)) + sess.WriteLine(fmt.Sprintf("%s %d x %s", g.colorize(sess, "drop_message", dropLabel), qty, coloredName)) } else { - sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropper+" drops:"), coloredName)) + sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropLabel), coloredName)) } } } @@ -667,6 +676,50 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst } } +// killPlayer handles a player death from any source (combat or a room hazard). +// mob may be nil (e.g. a hazard kill); it is only used for Dead Man's Switch +// retribution. +func (g *Game) killPlayer(sess *net.Session, p *player.Player, mob *world.MobInstance) { + combat.LeaveCombat(p.Name) + p.ActionState = nil + + if p.HasActiveTech("retribution") { + retDef := GetTechDef("retribution") + if retDef != nil && mob != nil && mob.HP > 0 { + retDmg := int(float64(p.MaxHP()) * retDef.Effects.RetributionPct) + if retDmg > 0 { + mob.HP -= retDmg + if mob.HP < 0 { + mob.HP = 0 + } + sess.WriteLine(fmt.Sprintf("\nDead Man's Switch activates! %s takes %d damage!", + mobDisplayName(mob, true), retDmg)) + } + } + } + p.DeactivateAllTechs() + p.Stats.RecordDeath() + sess.WriteLine(g.colorize(sess, "death", "\nOh dear, you are dead!")) + p.ClearMoveState() + p.HazardTimer = 0 + g.safespotMu.Lock() + if ss, ok := g.safespotStates[p.Name]; ok { + g.safespotMu.Unlock() + g.forceLeaveSafespot(sess, p, ss, "") + } else { + g.safespotMu.Unlock() + } + g.dropItemsOnDeath(p) + p.HP = p.MaxHP() + p.RoomID = 1001 + g.AccountStore.SaveCharacter(p) + if g.Hub != nil { + g.Hub.EnterRoom(sess, p.RoomID) + } + g.doLook(sess) + g.writePrompt(sess) +} + func (g *Game) awardCombatXP(sess *net.Session, p *player.Player, dmg int, isRanged bool) []xpGain { baseXP := dmg * 4 var gains []xpGain @@ -679,7 +732,7 @@ func (g *Game) awardCombatXP(sess *net.Session, p *player.Player, dmg int, isRan } else { switch p.AttackStyle { case player.Accurate: - gains = []xpGain{{string(player.Attack), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} + gains = []xpGain{{string(player.Accuracy), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} case player.Aggressive: gains = []xpGain{{string(player.Strength), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} case player.Defensive: @@ -687,7 +740,7 @@ func (g *Game) awardCombatXP(sess *net.Session, p *player.Player, dmg int, isRan case player.Balanced: quarter := baseXP / 4 gains = []xpGain{ - {string(player.Attack), quarter}, + {string(player.Accuracy), quarter}, {string(player.Strength), quarter}, {string(player.Defense), quarter}, {string(player.Hitpoints), quarter}, @@ -722,6 +775,22 @@ func (g *Game) executeAttack(sess *net.Session, args []string, rawInput string) } } +// executeWork is the non-violent alias of attack. It routes to the same engine; +// doAttack auto-detects whether the target is a task worksite or a combat mob. +func (g *Game) executeWork(sess *net.Session, args []string, rawInput string) { + if len(args) == 0 { + p := sess.Player + target := g.resolveDefaultMob(p.RoomID) + if target == "" { + sess.WriteLine("Work on what?") + return + } + g.doAttack(sess, target) + } else { + g.doAttack(sess, strings.Join(args, " ")) + } +} + func (g *Game) respawnMob(instanceID string) { inst := g.MobStore.GetInstance(instanceID) if inst == nil { diff --git a/internal/game/cmd_consume.go b/internal/game/cmd_consume.go index fa55d3d..223d803 100644 --- a/internal/game/cmd_consume.go +++ b/internal/game/cmd_consume.go @@ -258,7 +258,7 @@ func (g *Game) applyPotion(sess *net.Session, p *player.Player, itemID string, d case "all_combat": duration := engine.ToTicks(def.PotionDuration) - buffs := []string{"attack", "strength", "defense"} + buffs := []string{"accuracy", "strength", "defense"} for _, stat := range buffs { p.ActiveBuffs = append(p.ActiveBuffs, player.PotionBuff{ Stat: stat, diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index 98d48fd..266cd15 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -62,7 +62,7 @@ func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *wo if descWidth <= 0 { descWidth = 70 } - rawLines := wrapText(room.Description, descWidth) + rawLines := wrapText(g.roomDescription(sess, room), descWidth) mode := g.colorMode(sess) roomDescSpec := g.resolveColor(sess, "room_desc") lines := make([]string, len(rawLines)) @@ -72,6 +72,35 @@ func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *wo return lines } +// roomDescription resolves the room's effective description for this player, +// picking the first conditional variant whose condition passes, else the base +// Description. +func (g *Game) roomDescription(sess *net.Session, room *world.Room) string { + for _, d := range room.Descriptions { + if d.Condition == nil || g.checkCondition(sess, d.Condition) { + return d.Text + } + } + return room.Description +} + +// resolveObjDesc resolves the description an object shows to this player. For an +// object with conditional Descriptions, the first variant whose condition +// passes wins; if none pass the object is reported as not present (false), so +// look falls through as if it were not there. Objects without Descriptions use +// their plain Description and are always present. +func (g *Game) resolveObjDesc(sess *net.Session, def *object.ObjectDef) (string, bool) { + if len(def.Descriptions) == 0 { + return def.Description, true + } + for _, d := range def.Descriptions { + if d.Condition == nil || g.checkCondition(sess, d.Condition) { + return d.Text, true + } + } + return "", false +} + func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Room) []string { mobs := g.MobStore.MobsInRoom(p.RoomID) if len(mobs) == 0 { @@ -91,7 +120,15 @@ func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Roo for _, m := range mobs { hp := "" if m.HP < m.MaxHP { - hp = fmt.Sprintf(" [%s/%dhp]", color.Render(g.colorMode(sess), color.Parse("167"), fmt.Sprint(m.HP)), m.MaxHP) + if m.IsTask() { + pct := 0 + if m.MaxHP > 0 { + pct = (m.MaxHP - m.HP) * 100 / m.MaxHP + } + hp = fmt.Sprintf(" [%d%% complete]", pct) + } else { + hp = fmt.Sprintf(" [%s/%dhp]", color.Render(g.colorMode(sess), color.Parse("167"), fmt.Sprint(m.HP)), m.MaxHP) + } } var desc string if combat.IsMobInCombat(m.InstanceID) { @@ -467,6 +504,14 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { sess.WriteLine("You can't see anything that way.") return } + if exitDef.Condition != nil && !g.checkCondition(sess, exitDef.Condition) { + msg := exitDef.BlockedMessage + if msg == "" { + msg = fmt.Sprintf("The way %s is blocked.", exitDir) + } + sess.WriteLine(msg) + return + } g.World.SeedGroundItems(exitDef.Room) g.seedRoomMobs(exitDef.Room) origRoom := p.RoomID @@ -521,8 +566,43 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { return } - instances := g.World.FindObjInstances(p.RoomID, lower) - if len(instances) > 0 { + rawInstances := g.World.FindObjInstances(p.RoomID, lower) + + // Group matched object instances by definition, keeping only those visible + // to this player (an object whose conditional descriptions all fail is + // absent). If more than one distinct object matches, disambiguate. + var presentDefs []string + byDef := map[string][]world.ObjState{} + descByDef := map[string]string{} + for _, ist := range rawInstances { + def, err := g.ObjectStore.Load(ist.DefID) + if err != nil { + continue + } + text, present := g.resolveObjDesc(sess, def) + if !present { + continue + } + if _, seen := byDef[ist.DefID]; !seen { + presentDefs = append(presentDefs, ist.DefID) + descByDef[ist.DefID] = text + } + byDef[ist.DefID] = append(byDef[ist.DefID], ist) + } + + if len(presentDefs) > 1 { + sess.WriteLine("That's ambiguous, which one?") + for _, defID := range presentDefs { + if def, err := g.ObjectStore.Load(defID); err == nil { + sess.WriteLine(fmt.Sprintf(" %s", def.Name)) + } + } + return + } + + if len(presentDefs) == 1 { + instances := byDef[presentDefs[0]] + objDescText := descByDef[presentDefs[0]] st := &instances[0] def, _ := g.ObjectStore.Load(st.DefID) @@ -538,8 +618,8 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { sess.WriteLine(def.Name) } - if def.Description != "" && !strings.Contains(def.Description, "{quality}") { - sess.WriteLine(fmt.Sprintf(" %s", def.Description)) + if objDescText != "" && !strings.Contains(objDescText, "{quality}") { + sess.WriteLine(fmt.Sprintf(" %s", objDescText)) } if def.Safespot != nil { diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go index 85abbdf..b7cab37 100644 --- a/internal/game/cmd_move.go +++ b/internal/game/cmd_move.go @@ -42,12 +42,34 @@ func (g *Game) doMove(sess *net.Session, dir string, multiplier float64) { targetID := exitDef.Room - _, err = g.World.LoadRoom(targetID) + targetRoom, err := g.World.LoadRoom(targetID) if err != nil { sess.WriteLine("That path seems blocked.") return } + // Warn before walking from a safe room into a dangerous (hazardous) one. + // Dangerous->dangerous never prompts (the current room is already hazardous). + if targetRoom.Hazard != "" && room.Hazard == "" && p.OptionBool("danger_warning") { + if p.PendingDangerDir == dir { + p.PendingDangerDir = "" + } else { + p.PendingDangerDir = dir + p.WalkSequence = nil + sess.State = net.StateDangerConfirm + name := targetRoom.Hazard + if hz, herr := g.World.LoadHazard(targetRoom.Hazard); herr == nil && hz.Name != "" { + name = hz.Name + } + sess.WriteLine(g.colorize(sess, "warning", fmt.Sprintf("WARNING: that area is exposed to %s.", name))) + sess.WriteLine("Enter anyway? [Y/n]") + return + } + } + + p.MovePendingFlags = exitDef.SetFlags + p.MovePendingPlayerFlags = exitDef.SetPlayerFlags + inCombat := combat.GetCombat(p.Name) != nil if !inCombat && p.Action != nil { @@ -108,6 +130,8 @@ func (g *Game) moveTicks(p *player.Player, multiplier float64) int { func (g *Game) completeMove(sess *net.Session, p *player.Player) { exitDir := p.MoveDirection targetID := p.MoveTarget + pendingFlags := p.MovePendingFlags + pendingPlayerFlags := p.MovePendingPlayerFlags p.ClearMoveState() @@ -127,7 +151,16 @@ func (g *Game) completeMove(sess *net.Session, p *player.Player) { oldRoom := p.RoomID p.RoomID = targetID + p.HazardTimer = 0 p.Stats.RecordRoomVisit(targetID) + + g.cancelEnterSeq(p.Name) + if p.CutsceneRoom != 0 && p.CutsceneRoom == oldRoom { + p.CutsceneRoom = 0 + } + if len(pendingFlags) > 0 || len(pendingPlayerFlags) > 0 { + g.applyFlagMutations(p, pendingFlags, pendingPlayerFlags) + } g.AccountStore.SaveCharacter(p) g.World.SeedGroundItems(p.RoomID) @@ -179,6 +212,35 @@ func (g *Game) executeMove(sess *net.Session, args []string, rawInput string) { g.doMove(sess, strings.TrimSpace(rawInput), 1.0) } +// handleDangerConfirm processes the [Y/n] reply to the dangerous-area warning. +// Pressing return (empty), "y", or "yes" enters; anything else cancels. +func (g *Game) handleDangerConfirm(sess *net.Session, input string) { + p := sess.Player + if p == nil { + sess.State = net.StateGame + g.writePrompt(sess) + return + } + + dir := p.PendingDangerDir + choice := strings.TrimSpace(strings.ToLower(input)) + sess.State = net.StateGame + + if choice != "" && choice != "y" && choice != "yes" { + p.PendingDangerDir = "" + sess.WriteLine("You decide against it.") + g.writePrompt(sess) + return + } + + if dir == "" { + g.writePrompt(sess) + return + } + g.doMove(sess, dir, 1.0) + g.writePrompt(sess) +} + func (g *Game) seedRoomObjects(roomID int) { room, err := g.World.LoadRoom(roomID) if err != nil { @@ -195,6 +257,9 @@ func (g *Game) seedRoomObjects(roomID int) { continue } g.World.SetObjName(roomID, obj.ID, def.Name) + if len(def.Aliases) > 0 { + g.World.SetObjAliases(roomID, obj.ID, def.Aliases) + } if len(obj.WanderRooms) > 0 { g.World.SetObjWander(roomID, obj.ID, obj.WanderRooms, obj.WanderInterval) } diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go index fe3d90a..be1c276 100644 --- a/internal/game/cmd_registry.go +++ b/internal/game/cmd_registry.go @@ -22,6 +22,7 @@ var commandRegistry = map[string]commandDef{ "drop": {(*Game).executeDrop, ClassActive}, "attack": {(*Game).executeAttack, ClassActive}, "kill": {(*Game).executeAttack, ClassActive}, + "work": {(*Game).executeWork, ClassActive}, "style": {(*Game).executeStyle, ClassInstant}, "look": {(*Game).executeLook, ClassInstant}, "l": {(*Game).executeLook, ClassInstant}, diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go index 9f3c786..635c4b4 100644 --- a/internal/game/cmd_score.go +++ b/internal/game/cmd_score.go @@ -43,7 +43,7 @@ func (g *Game) doScore(sess *net.Session) { } skillDisplay := map[player.SkillName]string{ - player.Attack: "Attack", player.Strength: "Strength", player.Defense: "Defense", + player.Accuracy: "Accuracy", player.Strength: "Strength", player.Defense: "Defense", player.Hitpoints: "Hitpoints", player.Ranged: "Ranged", player.Science: "Science", player.Technology: "Technology", player.Assassin: "Assassin", player.Fishing: "Fishing", player.Woodcutting: "Woodcutting", player.Mining: "Mining", @@ -61,7 +61,7 @@ func (g *Game) doScore(sess *net.Session) { } categories := []skillCat{ {"Combat", []player.SkillName{ - player.Attack, player.Strength, player.Defense, player.Hitpoints, + player.Accuracy, player.Strength, player.Defense, player.Hitpoints, player.Ranged, player.Science, player.Technology, player.Assassin, }}, {"Gathering", []player.SkillName{ diff --git a/internal/game/cmd_stats.go b/internal/game/cmd_stats.go index 9987a1d..2466a5f 100644 --- a/internal/game/cmd_stats.go +++ b/internal/game/cmd_stats.go @@ -67,7 +67,7 @@ func (g *Game) doStats(sess *net.Session) { equipAtt := combat.SelectBonus(attackType, totals.StabAttack, totals.SlashAttack, totals.CrushAttack, totals.ScienceAttack, totals.RangedAttack) - attRoll = combat.EffectiveRoll(p.Level(player.Attack), attBonus, equipAtt) + attRoll = combat.EffectiveRoll(p.Level(player.Accuracy), attBonus, equipAtt) maxHitVal = combat.MaxHit(p.Level(player.Strength), strBonus, totals.StrengthBonus) } diff --git a/internal/game/condition_test.go b/internal/game/condition_test.go new file mode 100644 index 0000000..5e5967d --- /dev/null +++ b/internal/game/condition_test.go @@ -0,0 +1,155 @@ +package game + +import ( + "testing" + + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +func sessWithFlags(flags map[string]any) *net.Session { + return &net.Session{Player: &player.Player{Flags: flags}} +} + +func TestIsTruthy(t *testing.T) { + cases := []struct { + v any + want bool + }{ + {nil, false}, + {true, true}, + {false, false}, + {0, false}, + {1, true}, + {int64(0), false}, + {int64(2), true}, + {0.0, false}, + {1.5, true}, + {"", false}, + {"x", true}, + } + for _, c := range cases { + if got := isTruthy(c.v); got != c.want { + t.Errorf("isTruthy(%v) = %v, want %v", c.v, got, c.want) + } + } +} + +func TestCheckConditionPlayerFlagTruthy(t *testing.T) { + g := &Game{WorldFlags: map[string]any{}} + + set := sessWithFlags(map[string]any{"x": true}) + unset := sessWithFlags(map[string]any{}) + + // {player_flag: x} -> set means present+truthy + posCond := &action.Condition{PlayerFlag: "x"} + if !g.checkCondition(set, posCond) { + t.Error("expected positive flag check to pass when flag is true") + } + if g.checkCondition(unset, posCond) { + t.Error("expected positive flag check to fail when flag is unset") + } + + // {player_flag: x, not: true} -> negative + negCond := &action.Condition{PlayerFlag: "x", Not: true} + if g.checkCondition(set, negCond) { + t.Error("expected negative flag check to fail when flag is true") + } + if !g.checkCondition(unset, negCond) { + t.Error("expected negative flag check to pass when flag is unset") + } + + // flag explicitly set to false counts as not-set for truthy checks + falseSet := sessWithFlags(map[string]any{"x": false}) + if g.checkCondition(falseSet, posCond) { + t.Error("expected positive check to fail when flag is false") + } + if !g.checkCondition(falseSet, negCond) { + t.Error("expected negative check to pass when flag is false") + } +} + +func TestCheckConditionValueComparisonPreserved(t *testing.T) { + g := &Game{WorldFlags: map[string]any{}} + sess := sessWithFlags(map[string]any{"n": 1}) + + if !g.checkCondition(sess, &action.Condition{PlayerFlag: "n", Value: 1}) { + t.Error("value match should pass for equal int") + } + if g.checkCondition(sess, &action.Condition{PlayerFlag: "n", Value: 2}) { + t.Error("value match should fail for unequal int") + } + if !g.checkCondition(sess, &action.Condition{PlayerFlag: "n", Value: 2, Not: true}) { + t.Error("negated value match should pass for unequal int") + } +} + +func TestCheckConditionWorldFlag(t *testing.T) { + g := &Game{WorldFlags: map[string]any{"gate_open": true}} + sess := sessWithFlags(map[string]any{}) + + if !g.checkCondition(sess, &action.Condition{Flag: "gate_open"}) { + t.Error("world flag truthy check should pass") + } + if g.checkCondition(sess, &action.Condition{Flag: "gate_open", Not: true}) { + t.Error("negated world flag should fail when set") + } + if g.checkCondition(sess, &action.Condition{Flag: "missing"}) { + t.Error("missing world flag should not pass") + } +} + +func TestResolveObjDescPresence(t *testing.T) { + g := &Game{WorldFlags: map[string]any{}} + + def := &object.ObjectDef{ + Descriptions: []object.ConditionalDesc{ + {Text: "lined", Condition: &action.Condition{PlayerFlag: "lined_up"}}, + {Text: "milling", Condition: &action.Condition{PlayerFlag: "lined_up", Not: true}}, + }, + } + + // not lined up -> milling, present + if text, present := g.resolveObjDesc(sessWithFlags(map[string]any{}), def); !present || text != "milling" { + t.Errorf("expected milling/present, got %q/%v", text, present) + } + // lined up -> lined, present + if text, present := g.resolveObjDesc(sessWithFlags(map[string]any{"lined_up": true}), def); !present || text != "lined" { + t.Errorf("expected lined/present, got %q/%v", text, present) + } + + // gated entirely off -> absent + gated := &object.ObjectDef{ + Descriptions: []object.ConditionalDesc{ + {Text: "x", Condition: &action.Condition{PlayerFlag: "done", Not: true}}, + }, + } + if _, present := g.resolveObjDesc(sessWithFlags(map[string]any{"done": true}), gated); present { + t.Error("expected object to be absent when no variant matches") + } + + // plain object (no Descriptions) always present + plain := &object.ObjectDef{Description: "plain"} + if text, present := g.resolveObjDesc(sessWithFlags(nil), plain); !present || text != "plain" { + t.Errorf("expected plain/present, got %q/%v", text, present) + } +} + +func TestRoomDescriptionSelection(t *testing.T) { + g := &Game{WorldFlags: map[string]any{}} + room := &world.Room{ + Description: "empty pad", + Descriptions: []world.RoomDesc{ + {Text: "crowd", Condition: &action.Condition{PlayerFlag: "done", Not: true}}, + }, + } + if got := g.roomDescription(sessWithFlags(map[string]any{}), room); got != "crowd" { + t.Errorf("first visit: want crowd, got %q", got) + } + if got := g.roomDescription(sessWithFlags(map[string]any{"done": true}), room); got != "empty pad" { + t.Errorf("revisit: want empty pad, got %q", got) + } +} diff --git a/internal/game/core_login_char.go b/internal/game/core_login_char.go index f0d6863..dec6dae 100644 --- a/internal/game/core_login_char.go +++ b/internal/game/core_login_char.go @@ -41,8 +41,8 @@ func (g *Game) handleNewCharName(sess *net.Session, input string) { } p := player.New(name) - p.RoomID = 1 - p.Stats.RecordRoomVisit(1) + p.RoomID = 1001 + p.Stats.RecordRoomVisit(1001) if err := g.AccountStore.SaveCharacter(p); err != nil { sess.WriteLine(fmt.Sprintf("Error creating character: %v", err)) @@ -101,6 +101,23 @@ func (g *Game) connectCharacter(sess *net.Session, name string) { g.doLook(sess) g.checkAggro(sess) + + // Resume an interrupted on_enter cutscene (e.g. disconnected mid-sequence). + // Only fires when a persisted marker matches the room the player is in, so + // ordinary on_enter scripts never run on login. + if p.CutsceneRoom != 0 { + if p.CutsceneRoom == p.RoomID { + g.runEnterSteps(sess, p.RoomID) + } + g.enterMu.Lock() + _, active := g.enterSeqs[p.Name] + g.enterMu.Unlock() + if !active { + p.CutsceneRoom = 0 + g.AccountStore.SaveCharacter(p) + } + } + sess.Write("\r\n> ") } diff --git a/internal/game/core_startup.go b/internal/game/core_startup.go index 4bdb50d..323e519 100644 --- a/internal/game/core_startup.go +++ b/internal/game/core_startup.go @@ -31,6 +31,7 @@ func (g *Game) ValidateStartup() []StartupIssue { issues = append(issues, validateRooms(g)...) issues = append(issues, validateMobs(g)...) + issues = append(issues, validateHazards(g)...) issues = append(issues, validateObjects(g)...) issues = append(issues, validateItems(g)...) issues = append(issues, validateCraftBlocks(g)...) @@ -66,6 +67,7 @@ func validateRooms(g *Game) []StartupIssue { itemIDs := g.ItemStore.IDSet() mobIDs := g.MobStore.AllDefIDs() objIDs := g.ObjectStore.IDSet() + hazardIDs := g.World.HazardIndex() for id := range roomIndex { room, err := g.World.LoadRoom(id) @@ -164,6 +166,15 @@ func validateRooms(g *Game) []StartupIssue { } } } + + if room.Hazard != "" && !hazardIDs[room.Hazard] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Room %d: references nonexistent hazard %q", + id, room.Hazard), + }) + } } return issues @@ -193,6 +204,14 @@ func validateMobs(g *Game) []StartupIssue { }) } + if def.Kind != "" && def.Kind != "combat" && def.Kind != "task" { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Mob %q: invalid kind %q (must be \"combat\" or \"task\")", id, def.Kind), + }) + } + if def.Drops.Remains != "" && !itemIDs[def.Drops.Remains] { issues = append(issues, StartupIssue{ Level: "ERROR", @@ -243,6 +262,40 @@ func validateMobs(g *Game) []StartupIssue { return issues } +func validateHazards(g *Game) []StartupIssue { + var issues []StartupIssue + itemIDs := g.ItemStore.IDSet() + validTypes := map[string]bool{"stab": true, "slash": true, "crush": true, "ranged": true, "science": true} + + for id := range g.World.HazardIndex() { + def, err := g.World.LoadHazard(id) + if err != nil { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Hazard %q: failed to load: %v", id, err), + }) + continue + } + if def.AttackType != "" && !validTypes[def.AttackType] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Hazard %q: invalid attack_type %q (must be stab/slash/crush/ranged/science)", id, def.AttackType), + }) + } + if def.RequiredItem != "" && !itemIDs[def.RequiredItem] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Hazard %q: required_item %q does not exist", id, def.RequiredItem), + }) + } + } + + return issues +} + func validateObjects(g *Game) []StartupIssue { var issues []StartupIssue objIDs := g.ObjectStore.IDSet() diff --git a/internal/game/game.go b/internal/game/game.go index a1b7bbe..b9b54ba 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -59,6 +59,8 @@ type Game struct { hackingStates map[string]*hacking.Session safespotMu sync.Mutex safespotStates map[string]*SafespotState + enterMu sync.Mutex + enterSeqs map[string]*enterSeq } type SafespotState struct { @@ -92,6 +94,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig, valConfig config.Vali guardWatchTimers: make(map[string]int), hackingStates: make(map[string]*hacking.Session), safespotStates: make(map[string]*SafespotState), + enterSeqs: make(map[string]*enterSeq), } g.CourseStore.LoadAll() g.LoadMods() @@ -105,6 +108,7 @@ func (g *Game) SetHub(hub *net.Hub) { hub.OnRemove(func(sess *net.Session) { if p := sess.Player; p != nil { p.DeactivateAllTechs() + g.cancelEnterSeq(p.Name) g.charsMu.Lock() delete(g.loggedInChars, p.Name) g.charsMu.Unlock() @@ -167,6 +171,8 @@ func (g *Game) HandleSession(sess *net.Session, input string) { g.handleColorChoice(sess, input) case net.StateHacking: g.handleHackingInput(sess, input) + case net.StateDangerConfirm: + g.handleDangerConfirm(sess, input) } } diff --git a/internal/game/map_test.go b/internal/game/map_test.go index b7d9f01..dded62e 100644 --- a/internal/game/map_test.go +++ b/internal/game/map_test.go @@ -21,32 +21,32 @@ func TestBuildTinyMap(t *testing.T) { want []string // expected lines, or nil to just check count }{ { - name: "room 1 has east exit", - roomID: 1, + name: "room 999991 has east exit", + roomID: 999991, }, { - name: "room 2 has west/north/east", - roomID: 2, + name: "room 999992 has west/north/east", + roomID: 999992, }, { - name: "room 4 has west/north/east", - roomID: 4, + name: "room 999994 has west/north/east", + roomID: 999994, }, { - name: "room 21 has west only", - roomID: 21, + name: "room 9999921 has west only", + roomID: 9999921, }, { - name: "room 22 west of town square", - roomID: 22, + name: "room 9999922 west of town square", + roomID: 9999922, }, { - name: "room 25 east of south of west", - roomID: 25, + name: "room 9999925 east of south of west", + roomID: 9999925, }, { - name: "room 8 hacking lab", - roomID: 8, + name: "room 999998 hacking lab", + roomID: 999998, }, } @@ -108,10 +108,10 @@ func TestBuildFullMap(t *testing.T) { width int height int }{ - {"room 1 30x20", 1, 30, 20}, - {"room 1 20x10", 1, 20, 10}, - {"room 25 30x20", 25, 30, 20}, - {"min size 5x5", 1, 5, 5}, + {"room 999991 30x20", 999991, 30, 20}, + {"room 999991 20x10", 999991, 20, 10}, + {"room 9999925 30x20", 9999925, 30, 20}, + {"min size 5x5", 999991, 5, 5}, } for _, tt := range tests { diff --git a/internal/game/sys_hazard.go b/internal/game/sys_hazard.go new file mode 100644 index 0000000..29bf65e --- /dev/null +++ b/internal/game/sys_hazard.go @@ -0,0 +1,136 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +// HazardTick rolls environmental attacks against every player standing in a +// hazardous room. It reuses the combat formulas exactly: the hazard "attacks" +// and the player defends with their Defense level + equipment bonus selected by +// the hazard's attack type. It runs independently of combat, so a player can be +// hit by BOTH a mob and the room hazard in the same area. It deliberately never +// touches movement state — a hazard hit does NOT interrupt walking. +func (g *Game) HazardTick() { + if g.Hub == nil { + return + } + for _, sess := range g.Hub.AllSessions() { + p := sess.Player + if p == nil { + continue + } + + room, err := g.World.LoadRoom(p.RoomID) + if err != nil || room.Hazard == "" { + p.HazardTimer = 0 + continue + } + hz, err := g.World.LoadHazard(room.Hazard) + if err != nil || hz == nil { + p.HazardTimer = 0 + continue + } + + // A safespot shields the player from all hazard damage while hidden. + if g.safespotBlocksHazard(p) { + continue + } + + // The right protective gear negates the hazard entirely. + if hz.RequiredItem != "" && g.hasEquipped(p, hz.RequiredItem) { + continue + } + + speed := hz.Speed + if speed <= 0 { + speed = 5 + } + p.HazardTimer++ + if float64(p.HazardTimer) >= speed { + p.HazardTimer = 0 + g.rollHazard(sess, p, hz) + } + } +} + +func (g *Game) hasEquipped(p *player.Player, itemID string) bool { + for _, id := range p.Equipment { + if id == itemID { + return true + } + } + return false +} + +func (g *Game) rollHazard(sess *net.Session, p *player.Player, hz *world.HazardDef) { + attackType := hz.AttackType + if attackType == "" { + attackType = "crush" + } + + attRoll := combat.EffectiveRoll(hz.Attack, 0, hz.AttackBonus) + + _, _, defStyleBonus := combat.AttackStyleBonus(string(p.AttackStyle)) + totals := g.playerEquipBonuses(p) + equipDef := combat.SelectBonus(attackType, + totals.StabDefense, totals.SlashDefense, totals.CrushDefense, + totals.ScienceDefense, totals.RangedDefense) + defRoll := combat.EffectiveRoll( + p.Level(player.Defense)+g.techLevelBonus(p, "defense")+g.buffLevelBonus(p, "defense"), + defStyleBonus, equipDef) + + if combat.HitCheck(attRoll, defRoll) { + g.applyHazardHit(sess, p, hz) + } else { + miss := hz.MissMessage + if miss == "" { + miss = fmt.Sprintf("You weather the %s.", hz.Name) + } + sess.WriteLine("\n" + g.colorize(sess, "miss", miss)) + g.writePrompt(sess) + } +} + +func (g *Game) applyHazardHit(sess *net.Session, p *player.Player, hz *world.HazardDef) { + maxHit := hz.MaxHit + if maxHit < 1 { + maxHit = 1 + } + dmg := combat.RollDamage(maxHit) + dmg = g.damageAfterTechProtection(p, hz.AttackType, dmg) + if dmg < 0 { + dmg = 0 + } + + p.HP -= dmg + if p.HP < 0 { + p.HP = 0 + } + p.StartRegen() + g.AccountStore.SaveCharacter(p) + + msg := hz.HitMessage + switch { + case msg == "": + msg = fmt.Sprintf("The %s hits you for %d damage.", hz.Name, dmg) + case strings.Contains(msg, "%d"): + msg = fmt.Sprintf(msg, dmg) + } + hpSuffix := fmt.Sprintf(" %s [%d/%d]", g.hpBar(sess, p.HP, p.MaxHP()), p.HP, p.MaxHP()) + sess.WriteLine("\n" + g.colorize(sess, "damage_taken", msg) + hpSuffix) + + // NB: a hazard never clears MoveState, so it cannot interrupt a walk the way + // a mob hit ("Can't escape!") does. + + if p.HP <= 0 { + g.killPlayer(sess, p, nil) + return + } + g.writePrompt(sess) +} diff --git a/internal/game/sys_labor.go b/internal/game/sys_labor.go new file mode 100644 index 0000000..102c54e --- /dev/null +++ b/internal/game/sys_labor.go @@ -0,0 +1,81 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +// taskWorkVerb returns the flavor verb for working on a task worksite. Designers +// set `verb:` on the mob (e.g. "survey", "prospect", "build"); the default is +// "work on". The player-facing command is always `work` (or `attack`). +func taskWorkVerb(mob *world.MobInstance) string { + if mob != nil && mob.Verb != "" { + return mob.Verb + } + return "work on" +} + +// progressBar renders a bar that fills as progress rises toward max — the +// inverse of hpBar. It is used for task worksites whose internal HP drains to 0 +// to complete the work (progress = MaxHP - HP). +func (g *Game) progressBar(sess *net.Session, progress, max int) string { + if max <= 0 { + max = 1 + } + if progress < 0 { + progress = 0 + } + if progress > max { + progress = max + } + + pct := float64(progress) / float64(max) * 100.0 + var fgColor int + switch { + case pct >= 70: + fgColor = 82 + case pct >= 40: + fgColor = 220 + default: + fgColor = 39 + } + + unicode := sess.Player != nil && sess.Player.OptionBool("unicode") + style := &BarStyle{FilledColor: fgColor, EmptyColor: 238, EmptyDim: true} + bar := RenderColoredBar(progress, max, 10, unicode, style, g.colorMode(sess)) + return "[" + bar + "]" +} + +// writeTaskProgress prints the per-tick progress line for a task worksite, +// mirroring the combat hit line but framed as labor instead of damage. +func (g *Game) writeTaskProgress(sess *net.Session, p *player.Player, mob *world.MobInstance, dmg int, gains []xpGain) { + progress := mob.MaxHP - mob.HP + if progress < 0 { + progress = 0 + } + pct := 0 + if mob.MaxHP > 0 { + pct = progress * 100 / mob.MaxHP + } + + noun := mob.ProgressNoun + if noun == "" { + noun = "work" + } + + prefix := fmt.Sprintf("You advance the %s on %s.", noun, g.colorize(sess, "mob", mobDisplayName(mob, true))) + line := fmt.Sprintf("%s %s %3d%%", prefix, g.progressBar(sess, progress, mob.MaxHP), pct) + + if p.OptionBool("xp_drops") && len(gains) > 0 { + var parts []string + for _, gain := range gains { + parts = append(parts, fmt.Sprintf("+%dxp %s", gain.XP, player.SkillAbbr[player.SkillName(gain.Skill)])) + } + line += g.colorize(sess, "xp", " ("+strings.Join(parts, ", ")+")") + } + sess.WriteLine(line) +} diff --git a/internal/game/sys_safespot.go b/internal/game/sys_safespot.go index da999c0..44a8f9f 100644 --- a/internal/game/sys_safespot.go +++ b/internal/game/sys_safespot.go @@ -46,6 +46,13 @@ func blocksMob(maxBlockSize, mobSize string) bool { return mob <= max } +// safespotBlocksHazard reports whether the player's active safespot shields them +// from room hazard damage. Unlike mobs, hazard type does not matter: an active +// safespot blocks ALL hazard damage, no safespot blocks none. +func (g *Game) safespotBlocksHazard(p *player.Player) bool { + return g.isSafespotted(p.Name) +} + func (g *Game) safespotBlocksMob(p *player.Player, mob *world.MobInstance) bool { g.safespotMu.Lock() ss, ok := g.safespotStates[p.Name] diff --git a/internal/game/sys_technology.go b/internal/game/sys_technology.go index bb48375..e673dcb 100644 --- a/internal/game/sys_technology.go +++ b/internal/game/sys_technology.go @@ -10,7 +10,7 @@ import ( ) type TechEffects struct { - AttackPercent int + AccuracyPercent int StrengthPercent int DefensePercent int RangedPercent int @@ -39,9 +39,9 @@ var techByID map[string]*TechDef func init() { AllTechs = []TechDef{ - {ID: "clarity_1", Name: "Clarity", Level: 4, DrainRate: 0.05, Category: "attack", Group: "attack_tier", Effects: TechEffects{AttackPercent: 5}}, - {ID: "clarity_2", Name: "Enhanced Clarity", Level: 16, DrainRate: 0.10, Category: "attack", Group: "attack_tier", Effects: TechEffects{AttackPercent: 10}}, - {ID: "clarity_3", Name: "Superior Clarity", Level: 44, DrainRate: 0.15, Category: "attack", Group: "attack_tier", Effects: TechEffects{AttackPercent: 15}}, + {ID: "clarity_1", Name: "Clarity", Level: 4, DrainRate: 0.05, Category: "accuracy", Group: "accuracy_tier", Effects: TechEffects{AccuracyPercent: 5}}, + {ID: "clarity_2", Name: "Enhanced Clarity", Level: 16, DrainRate: 0.10, Category: "accuracy", Group: "accuracy_tier", Effects: TechEffects{AccuracyPercent: 10}}, + {ID: "clarity_3", Name: "Superior Clarity", Level: 44, DrainRate: 0.15, Category: "accuracy", Group: "accuracy_tier", Effects: TechEffects{AccuracyPercent: 15}}, {ID: "amplifier_1", Name: "Power Amplifier", Level: 7, DrainRate: 0.05, Category: "strength", Group: "strength_tier", Effects: TechEffects{StrengthPercent: 5}}, {ID: "amplifier_2", Name: "Enhanced Amplifier", Level: 23, DrainRate: 0.10, Category: "strength", Group: "strength_tier", Effects: TechEffects{StrengthPercent: 10}}, @@ -68,7 +68,7 @@ func init() { {ID: "preserve", Name: "Power Saver", Level: 55, DrainRate: 0.05, Category: "utility", Group: "", Effects: TechEffects{PreserveDrain: 0.2}}, {ID: "retribution", Name: "Dead Man's Switch", Level: 46, DrainRate: 0.10, Category: "utility", Group: "", Effects: TechEffects{RetributionPct: 0.25}}, - {ID: "overclock", Name: "Overclock", Level: 60, DrainRate: 0.25, Category: "combo", Group: "attack_tier", Effects: TechEffects{AttackPercent: 15, StrengthPercent: 15}}, + {ID: "overclock", Name: "Overclock", Level: 60, DrainRate: 0.25, Category: "combo", Group: "accuracy_tier", Effects: TechEffects{AccuracyPercent: 15, StrengthPercent: 15}}, {ID: "fortify", Name: "Fortify", Level: 65, DrainRate: 0.25, Category: "combo", Group: "defense_tier", Effects: TechEffects{DefensePercent: 15, HPRegenMulti: 2.0}}, } @@ -104,8 +104,8 @@ func TechByPrefixMatch(input string) []*TechDef { func techEffectString(tech TechDef) string { var parts []string e := tech.Effects - if e.AttackPercent > 0 { - parts = append(parts, fmt.Sprintf("+%d%% Attack", e.AttackPercent)) + if e.AccuracyPercent > 0 { + parts = append(parts, fmt.Sprintf("+%d%% Accuracy", e.AccuracyPercent)) } if e.StrengthPercent > 0 { parts = append(parts, fmt.Sprintf("+%d%% Strength", e.StrengthPercent)) @@ -163,8 +163,8 @@ func (g *Game) techLevelBonus(p *player.Player, stat string) int { continue } switch stat { - case "attack": - totalPercent += def.Effects.AttackPercent + case "accuracy": + totalPercent += def.Effects.AccuracyPercent case "strength": totalPercent += def.Effects.StrengthPercent case "defense": @@ -182,8 +182,8 @@ func (g *Game) techLevelBonus(p *player.Player, stat string) int { var baseLevel int switch stat { - case "attack": - baseLevel = p.Level(player.Attack) + case "accuracy": + baseLevel = p.Level(player.Accuracy) case "strength": baseLevel = p.Level(player.Strength) case "defense": @@ -217,11 +217,17 @@ func (g *Game) tryRecharge(sess *net.Session, p *player.Player, input string) bo } func (g *Game) applyTechProtection(p *player.Player, mob *world.MobInstance, dmg int) int { + return g.damageAfterTechProtection(p, mob.AttackType, dmg) +} + +// damageAfterTechProtection reduces incoming damage if the player has the +// protection tech matching the given attack type active. Shared by mob combat +// and room hazards. +func (g *Game) damageAfterTechProtection(p *player.Player, attackType string, dmg int) int { if len(p.ActiveTechs) == 0 { return dmg } - attackType := mob.AttackType if attackType == "" { attackType = "crush" } diff --git a/internal/net/server.go b/internal/net/server.go index eaa8f17..b2cc3be 100644 --- a/internal/net/server.go +++ b/internal/net/server.go @@ -41,6 +41,7 @@ const ( StateShop StateBank StateHacking + StateDangerConfirm ) type Session struct { diff --git a/internal/object/object.go b/internal/object/object.go index 12e5585..f7a0e7b 100644 --- a/internal/object/object.go +++ b/internal/object/object.go @@ -12,11 +12,13 @@ type UseInteraction struct { type ObjectDef struct { ID string `yaml:"id"` Name string `yaml:"name"` + Aliases []string `yaml:"aliases"` Color string `yaml:"color"` Hidden bool `yaml:"hidden"` InRoomDescription string `yaml:"inroom_description"` RemovalItem string `yaml:"removal_item"` Description string `yaml:"description"` + Descriptions []ConditionalDesc `yaml:"descriptions,omitempty"` UseInteractions []UseInteraction `yaml:"use_interactions"` StealTable string `yaml:"steal_table"` StealLevel int `yaml:"steal_level"` @@ -30,6 +32,15 @@ type ObjectDef struct { Safespot *SafespotConfig `yaml:"safespot,omitempty"` } +// ConditionalDesc is a description variant gated by a condition. They are +// evaluated in order per-looker; the first whose condition passes (or has no +// condition) wins. If a def has Descriptions but none match for a given player, +// the object is treated as absent for that player. +type ConditionalDesc struct { + Text string `yaml:"text"` + Condition *action.Condition `yaml:"condition"` +} + type SafespotConfig struct { Tier int `yaml:"tier"` MaxBlockSize string `yaml:"max_block_size"` diff --git a/internal/player/player.go b/internal/player/player.go index ebba376..03ed7ac 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -10,7 +10,7 @@ import ( type SkillName string const ( - Attack SkillName = "attack" + Accuracy SkillName = "accuracy" Strength SkillName = "strength" Defense SkillName = "defense" Hitpoints SkillName = "hitpoints" @@ -36,13 +36,13 @@ const ( ) var AllSkills = []SkillName{ - Attack, Strength, Defense, Hitpoints, Ranged, Science, Technology, + Accuracy, Strength, Defense, Hitpoints, Ranged, Science, Technology, Fishing, Cooking, Woodcutting, Firemaking, Mining, Smithing, Crafting, Fletching, Pharmacy, Thieving, Agility, Construction, Scavenging, Hacking, Assassin, Farming, } var SkillAbbr = map[SkillName]string{ - Attack: "atk", + Accuracy: "acc", Strength: "str", Defense: "def", Hitpoints: "hp", @@ -139,6 +139,7 @@ var OptionDefs = []OptionDef{ {"construct_all", OptBool, false, nil, "Auto-start constructing when only one product is possible"}, {"craft_all", OptBool, false, nil, "Auto-start crafting when only one product is possible"}, {"safespot_alert", OptString, "{196 bold}** Your safespot has been compromised! **{/}", nil, "Message shown when forced out of a safespot"}, + {"danger_warning", OptBool, true, nil, "Confirm before entering a dangerous (hazardous) area"}, } var optionByName map[string]*OptionDef @@ -169,6 +170,7 @@ type Player struct { Prompt string `yaml:"prompt"` Options map[string]any `yaml:"-"` Flags map[string]any `yaml:"flags"` + CutsceneRoom int `yaml:"cutscene_room,omitempty"` RegenerateTick int AmmoQty int `yaml:"ammo_qty,omitempty"` Action *action.Action `yaml:"-"` @@ -180,10 +182,14 @@ type Player struct { MoveTicks int `yaml:"-"` MoveDirection string `yaml:"-"` MoveTarget int `yaml:"-"` + MovePendingFlags map[string]any `yaml:"-"` + MovePendingPlayerFlags map[string]any `yaml:"-"` VisualTickCurrent int `yaml:"-"` AutotriggerMod string `yaml:"-"` QueuedTrigger string `yaml:"-"` AttackTimer int `yaml:"-"` + HazardTimer int `yaml:"-"` + PendingDangerDir string `yaml:"-"` Battery float64 `yaml:"battery"` ActiveTechs map[string]bool `yaml:"-"` QuickTech string `yaml:"quick_tech,omitempty"` @@ -210,6 +216,8 @@ func (p *Player) ClearMoveState() { p.MoveTicks = 0 p.MoveDirection = "" p.MoveTarget = 0 + p.MovePendingFlags = nil + p.MovePendingPlayerFlags = nil } func (p *Player) OptionBool(name string) bool { @@ -358,7 +366,7 @@ func (p *Player) AddSkillXP(s SkillName, amount int) int { func (p *Player) CombatLevel() int { base := 0.25 * float64(p.Level(Defense)+p.Level(Hitpoints)+p.Level(Technology)) - att := float64(p.Level(Attack)) + att := float64(p.Level(Accuracy)) str := float64(p.Level(Strength)) if float64(p.Level(Ranged))*1.5 > att+str { diff --git a/internal/world/hazard.go b/internal/world/hazard.go new file mode 100644 index 0000000..396343b --- /dev/null +++ b/internal/world/hazard.go @@ -0,0 +1,82 @@ +package world + +import ( + "fmt" + "os" + "path/filepath" + + "thehouseoficarus/internal/action" + "gopkg.in/yaml.v3" +) + +// HazardDef describes an environmental threat attached to a room (a sandstorm, +// solar radiation, falling rocks, etc.). It is a shared, ID-referenced +// definition loaded from data/hazards/.yaml, so one hazard can be reused +// across a whole area and resolved by name for a future `examine` command. +// +// A hazard rolls an attack against everyone in the room every Speed ticks using +// the SAME combat formulas as a mob: accuracy comes from Attack/AttackBonus, max +// hit from MaxHit, and the player defends with their Defense level + equipment +// bonus selected by AttackType (so existing armour and the Defense skill apply). +// Tech protection (Kinetic Barrier / Projectile Screen / Neural Firewall) keys +// off AttackType automatically. +type HazardDef struct { + ID string `yaml:"id"` + Name string `yaml:"name"` + Description string `yaml:"description"` + AttackType string `yaml:"attack_type"` // stab/slash/crush/ranged/science + Attack int `yaml:"attack"` + AttackBonus int `yaml:"attack_bonus"` + MaxHit int `yaml:"max_hit"` + Speed float64 `yaml:"speed"` // ticks between hazard rolls + WarnMessage string `yaml:"warn_message"` + HitMessage string `yaml:"hit_message"` // accepts a single %d for damage + MissMessage string `yaml:"miss_message"` + RequiredItem string `yaml:"required_item"` // equipped item that negates the hazard +} + +// LoadHazard returns the shared hazard definition for id, caching results. +func (w *World) LoadHazard(id string) (*HazardDef, error) { + w.hazardMu.Lock() + if w.hazardPathIndex == nil { + w.hazardPathIndex = action.BuildPathIndex(filepath.Join(w.dataDir, "hazards")) + } + if def, ok := w.hazardDefs[id]; ok { + w.hazardMu.Unlock() + return def, nil + } + path, ok := w.hazardPathIndex[id] + w.hazardMu.Unlock() + if !ok { + return nil, fmt.Errorf("read hazard %s: no such hazard", id) + } + + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read hazard %s: %w", id, err) + } + var def HazardDef + if err := yaml.Unmarshal(data, &def); err != nil { + return nil, fmt.Errorf("parse hazard %s: %w", id, err) + } + def.ID = id + + w.hazardMu.Lock() + w.hazardDefs[id] = &def + w.hazardMu.Unlock() + return &def, nil +} + +// HazardIndex returns the set of known hazard IDs (used by startup validation). +func (w *World) HazardIndex() map[string]bool { + w.hazardMu.Lock() + defer w.hazardMu.Unlock() + if w.hazardPathIndex == nil { + w.hazardPathIndex = action.BuildPathIndex(filepath.Join(w.dataDir, "hazards")) + } + ids := make(map[string]bool, len(w.hazardPathIndex)) + for id := range w.hazardPathIndex { + ids[id] = true + } + return ids +} diff --git a/internal/world/mob.go b/internal/world/mob.go index ec7db36..4864e5c 100644 --- a/internal/world/mob.go +++ b/internal/world/mob.go @@ -58,11 +58,23 @@ type MobDef struct { DamageWithout string `yaml:"damage_without"` Size string `yaml:"size"` + // Kind discriminates a normal combat mob ("" or "combat") from a "task" + // worksite (build a solar panel, survey a rock, etc.). A task mob reuses the + // entire combat engine but its HP drains to 0 to COMPLETE the work; it never + // attacks back (the room hazard, if any, supplies the danger). The progress + // bar is displayed inverted (filling toward 100%). + Kind string `yaml:"kind"` + Verb string `yaml:"verb"` // flavor verb for messages (default "work") + ProgressNoun string `yaml:"progress_noun"` // flavor noun, e.g. "construction" + CompleteMessage string `yaml:"complete_message"` // shown on completion + Talk *action.TalkConfig `yaml:"talk,omitempty"` } func (d *MobDef) IsTalkable() bool { return d.Talk != nil } +func (d *MobDef) IsTask() bool { return d.Kind == "task" } + type MobInstance struct { InstanceID string DefID string @@ -109,11 +121,18 @@ type MobInstance struct { DamageWithout string Size string + Kind string + Verb string + ProgressNoun string + CompleteMessage string + TalkConfig *action.TalkConfig } 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 { return &MobInstance{ InstanceID: instanceID, @@ -153,6 +172,10 @@ func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []in FinishingBlow: def.FinishingBlow, DamageWithout: def.DamageWithout, Size: def.Size, + Kind: def.Kind, + Verb: def.Verb, + ProgressNoun: def.ProgressNoun, + CompleteMessage: def.CompleteMessage, TalkConfig: def.Talk, } } diff --git a/internal/world/objects.go b/internal/world/objects.go index bf4b592..a7e0605 100644 --- a/internal/world/objects.go +++ b/internal/world/objects.go @@ -14,6 +14,7 @@ type ObjState struct { DepleteTimer float64 DefID string Name string + Aliases []string Index int RoomID int JustRespawned bool @@ -160,12 +161,13 @@ func (w *World) FindObjInstances(roomID int, name string) []ObjState { if st.RoomID != roomID { continue } - if !wordMatchesObj(lower, st.DefID, st.Name) { + if !wordMatchesObj(lower, st.DefID, st.Name, st.Aliases) { continue } out = append(out, ObjState{ DefID: st.DefID, Name: st.Name, + Aliases: st.Aliases, Index: st.Index, RoomID: st.RoomID, Depleted: st.Depleted, @@ -184,10 +186,15 @@ func (w *World) FindObjInstances(roomID int, name string) []ObjState { return out } -func wordMatchesObj(lower, defID, objName string) bool { +func wordMatchesObj(lower, defID, objName string, aliases []string) bool { if object.WordPrefixMatch(lower, objName) { return true } + for _, alias := range aliases { + if object.WordPrefixMatch(lower, alias) { + return true + } + } inputWords := strings.Fields(strings.ToLower(lower)) if strings.HasPrefix(strings.ToLower(defID), lower) || strings.HasPrefix(lower, strings.ToLower(defID)) { @@ -214,6 +221,16 @@ func (w *World) SetObjName(roomID int, defID string, name string) { } } +func (w *World) SetObjAliases(roomID int, defID string, aliases []string) { + w.mu.Lock() + defer w.mu.Unlock() + for _, st := range w.objStates { + if st.RoomID == roomID && st.DefID == defID { + st.Aliases = aliases + } + } +} + func (w *World) SetObjWander(roomID int, defID string, rooms []int, interval float64) { w.mu.Lock() defer w.mu.Unlock() diff --git a/internal/world/room.go b/internal/world/room.go index 752ec41..2441c30 100644 --- a/internal/world/room.go +++ b/internal/world/room.go @@ -48,6 +48,8 @@ type ExitDef struct { Room int `yaml:"room"` Condition *action.Condition `yaml:"condition"` BlockedMessage string `yaml:"blocked_message"` + SetFlags map[string]any `yaml:"set_flags"` + SetPlayerFlags map[string]any `yaml:"set_player_flags"` } func (e *ExitDef) UnmarshalYAML(value *yaml.Node) error { @@ -67,11 +69,18 @@ type Room struct { ID int `yaml:"id"` Name string `yaml:"name"` Description string `yaml:"description"` + Descriptions []RoomDesc `yaml:"descriptions,omitempty"` Exits map[ExitDir]ExitDef `yaml:"exits"` Objects []RoomObject `yaml:"objects"` ItemSpawns []SpawnDef `yaml:"item_spawns"` Mobs []RoomMob `yaml:"mobs"` OnEnter []EnterStep `yaml:"on_enter"` + Hazard string `yaml:"hazard"` +} + +type RoomDesc struct { + Text string `yaml:"text"` + Condition *action.Condition `yaml:"condition"` } type RoomMob struct { @@ -94,8 +103,19 @@ func (rm *RoomMob) UnmarshalYAML(value *yaml.Node) error { } type EnterStep struct { - Message string `yaml:"message"` - Condition *action.Condition `yaml:"condition"` + Message string `yaml:"message"` + Condition *action.Condition `yaml:"condition"` + Delay int `yaml:"delay"` + SetFlags map[string]any `yaml:"set_flags"` + SetPlayerFlags map[string]any `yaml:"set_player_flags"` +} + +// IsTimed reports whether the step carries cutscene semantics: a tick delay or +// a flag mutation. A room whose qualifying on_enter steps are all non-timed is +// printed synchronously; any timed step turns the sequence into a scheduled +// cutscene. +func (e EnterStep) IsTimed() bool { + return e.Delay > 0 || len(e.SetFlags) > 0 || len(e.SetPlayerFlags) > 0 } type RoomObject struct { diff --git a/internal/world/world.go b/internal/world/world.go index b305c4b..8ed9a7c 100644 --- a/internal/world/world.go +++ b/internal/world/world.go @@ -19,6 +19,10 @@ type World struct { seeded map[int]bool objStates map[string]*ObjState objMoves []ObjMove + + hazardMu sync.Mutex + hazardPathIndex map[string]string + hazardDefs map[string]*HazardDef } func New(dataDir string) *World { @@ -28,6 +32,7 @@ func New(dataDir string) *World { groundItems: make(map[int][]*groundEntry), seeded: make(map[int]bool), objStates: make(map[string]*ObjState), + hazardDefs: make(map[string]*HazardDef), } } diff --git a/worldbuilding_guide/bank.md b/worldbuilding_guide/bank.md deleted file mode 100644 index 0a5ec88..0000000 --- a/worldbuilding_guide/bank.md +++ /dev/null @@ -1,68 +0,0 @@ -# Bank System - -The bank system allows players to store items permanently, freeing up inventory space. - -## Objects - -### bank_booth - -A bank terminal that players can interact with. Has no special behavior — the `bank` command checks for any `bank_booth` object in the room. - -```yaml -id: bank_booth -name: bank booth -color: "226" -description: "..." -inroom_description: "A bank booth is set into the wall." -``` - -To place a bank booth in a room, add it to the room's `objects:` list: - -```yaml -objects: - - id: bank_booth -``` - -## Commands - -| Command | Description | -|---------|-------------| -| `bank` | Opens bank interface (requires bank booth in room) | -| `use bank` | Same as `bank` | - -### Bank Interface Commands - -| Command | Description | -|---------|-------------| -| `deposit ` | Move item from inventory to bank | -| `deposit all` | Deposit entire inventory | -| `withdraw ` | Move item from bank to inventory | -| `browse` / `list` | Show bank contents | -| `leave` | Exit bank interface | - -## Implementation - -### Session State - -`StateBank` (defined in `internal/net/server.go`) is the session state for the bank interface. Input is routed through `handleBankInput` in `internal/game/cmd_bank.go`. - -### Player Storage - -Bank items are stored in the `Player.Bank` field (`map[int]*InventorySlot`), saved in character YAML. Bank slots are sequential and reuse freed slots. There is no slot limit. - -### Command Dispatch - -- `bank` is classified as `ClassActive` in `classifyCommand()` -- `bank` case in `executeCommand()` calls `doBank(sess)` -- `doUse()` checks for `"bank"` or `"bank booth"` targets and routes to `doBank()` -- `doBank()` checks for a `bank_booth` object in the current room -- `handleBankInput()` dispatches deposit/withdraw/browse/leave commands - -### Data Files - -| File | Purpose | -|------|---------| -| `data/objects/bank_booth.yaml` | Bank booth object definition | -| `data/help/bank.yaml` | Help topic for the bank system | - -Default bank booth locations: Town Square (room 1), Forge (room 12). diff --git a/worldbuilding_guide/behaviors.md b/worldbuilding_guide/behaviors.md deleted file mode 100644 index 22e97fe..0000000 --- a/worldbuilding_guide/behaviors.md +++ /dev/null @@ -1,402 +0,0 @@ -## Behaviors - -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. - -### Gather (mining, fishing, woodcutting) - -Mining — per-drop depletion on a rock object: -```yaml -id: copper_rock -name: copper rock -color: "178" -gather: - skill: mining - level: 1 - xp: 17 # XP awarded per successful gather - base_wait: 8 # ticks between attempts - tools: # requires item with matching tool_type - - pickaxe - success: - base: 0.40 # 40% base chance - per_level: 0.01 # +1% per level above requirement - cap: 0.95 # 95% max - gather_message: "You swing your pickaxe at the rock..." - fail_message: "You chip away but get nothing useful." - drops: - - item_id: copper_ore - weight: 90 # 90% chance when roll succeeds - depletes: true # rock becomes depleted after this drop - message: "You manage to mine some {178}copper ore{/}." - - table: gem_table # reference a shared drop table - weight: 10 - depletes: false # gem drops don't deplete the rock - message: "You spot a glint of something valuable!" - respawn_timer: 50 # ticks until rock respawns - respawn_broadcast: "A glint of copper catches your eye from some {name}." -``` - -Drop messages support inline color tags: `{<0-255>}text{/}`. Use the item's color -index to match its display color. Gradients also work: `{g:196,82}text{/}`. - -Non-depleting gather (fishing on a fishing spot object): -```yaml -id: bait_fishing_spot -name: fishing spot -gather: - skill: fishing - level: 1 - xp: 10 - base_wait: 4 - tools: - - fishing_rod - bait: fishing_bait - success: - base: 0.30 - per_level: 0.01 - cap: 0.90 - gather_message: "You cast your line into the water..." - fail_message: "Nothing seems to bite." - drops: - - item_id: raw_trout - weight: 100 - depletes: false # never depletes - message: "You catch a {69}raw trout{/}!" -``` - -Woodcutting with shared depletion and bird's nests: -```yaml -id: oak_tree -name: oak tree -color: "113" -gather: - skill: woodcutting - level: 15 - xp: 37 - base_wait: 6 - tools: - - axe - success: - base: 0.40 - per_level: 0.01 - cap: 0.90 - gather_message: "You swing your axe at the oak tree..." - fail_message: "You swing but get no logs." - drops: - - item_id: oak_logs - weight: 100 - depletes: false # depletion is timer-based (deplete_timer) - message: "You get some {113}oak logs{/}." - respawn_timer: 14 # ticks until tree respawns after being cut down - deplete_timer: 45 # max ticks before next gather depletes (counts down while chopping) - nest_chance: 256 # 1/256 chance for a bird's nest on each successful gather - respawn_broadcast: "An {name} grows back." -``` - -Regular tree — always depletes on first gather, no shared timer, no nests: -```yaml -id: tree -name: tree -gather: - skill: woodcutting - level: 1 - xp: 25 - base_wait: 4 - tools: - - axe - success: - base: 0.50 - per_level: 0.01 - cap: 0.95 - gather_message: "You swing your axe at the tree..." - fail_message: "You swing but get no logs." - drops: - - item_id: logs - weight: 100 - depletes: true # regular tree depletes on first successful gather - message: "You get some {107}logs{/}." - respawn_timer: 80 - respawn_broadcast: "A {name} grows back." -``` - -#### GatherConfig Fields - -| Field | Type | Description | -|---|---|---| -| `skill` | string | Skill name for level check | -| `level` | int | Required level to gather | -| `xp` | int | XP per successful gather | -| `base_wait` | float64 | Base ticks per action cycle | -| `tools` | []string | Required tool_type list (e.g. `[pickaxe]`) | -| `bait` | string | Required bait item (for fishing) | -| `success` | SuccessFormula | Base + per_level, capped | -| `gather_message` | string | Custom "You gather..." message | -| `depleted_message` | string | Custom "The rock is depleted" message | -| `exhausted_message` | string | Custom "The tree falls" message | -| `fail_message` | string | Custom failure message | -| `drops` | []DropEntry | Weighted drop entries | -| `respawn_timer` | float64 | Ticks until depleted object respawns | -| `respawn_broadcast` | string | Broadcast message when object respawns | -| `deplete_timer` | float64 | Ticks for shared depletion (trees) | -| `nest_chance` | int | 1/N chance for bird's nest alongside normal drop | - -#### SuccessFormula - -| Field | Type | Description | -|---|---|---| -| `base` | float64 | Base success chance (0.0-1.0) | -| `per_level` | float64 | Chance increase per level above requirement | -| `cap` | float64 | Maximum success chance | - -#### DropEntry - -| Field | Type | Description | -|---|---|---| -| `item_id` | string | Item ID to drop | -| `table` | string | Reference to a shared drop table in `data/drops/` | -| `weight` | int | Relative drop weight | -| `depletes` | bool | Resource depletes on this drop | -| `quantity` | int | Amount to drop (1-3 for random) | -| `message` | string | Player message on drop | -| `level` | int | Skill level required for this drop | -| `xp` | int | Bonus XP on this drop | - -#### Shared depletion explained - -When `deplete_timer > 0`, the tree has a shared despawn timer: -- The timer starts at `deplete_timer` max when the first player begins chopping. -- Each tick, if anyone is chopping, the timer counts down. -- When the timer reaches 0, the NEXT successful gather depletes the tree. -- If no one is chopping and the tree isn't depleted, the timer ticks back UP. -- All players chopping the same tree are interrupted when it depletes. - -Use `deplete_timer` for trees. Use `depletes: true` on individual drops for rocks. - -#### Bird's nests - -When `nest_chance > 0`, each successful gather has a 1/N independent chance to also drop -a bird's nest. The nest goes to inventory (or to the ground if inventory is full). -Use the `search` command to open nests — they roll on the `birds_nest_drop` table. - -#### XP drops - -When `xp > 0`, the gather awards XP on each successful drop. If the player's `xpdrops` -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: - -```yaml -id: guard -name: Guard -talk: - nodes: - start: - message: "\"Halt! This area is restricted.\"" - options: - - text: "\"What's behind that gate?\"" - goto: about_gate - - text: "\"I have copper ore.\"" - goto: trade_ore - condition: - has_item: copper_ore - - text: "\"I have a pass.\"" - goto: has_pass - condition: - player_flag: got_pass - value: true - - text: "\"Goodbye.\"" - end: true - - about_gate: - message: "\"Bring me some copper ore and I'll stamp you a pass.\"" - action: - set_player_flags: - talked_to_guard: true - options: - - text: "\"I'll be back.\"" - end: true - - text: "\"I have some right here.\"" - goto: trade_ore - condition: - has_item: copper_ore - - trade_ore: - message: "\"Good quality ore.\" He stamps a pass and hands it to you." - action: - take_item: copper_ore - give_item: pass_stub - set_player_flags: - got_pass: true - options: - - text: "\"Thanks.\"" - end: true - - has_pass: - message: "\"Alright, I'll open the gate for you.\"" - action: - set_flags: - gate_open: true - options: - - text: "\"Thanks.\"" - end: true -``` - -#### Node action reference - -| Field | Effect | -|---|---| -| `set_flags` | Sets world flags (global, shared by all players) | -| `set_player_flags` | Sets player-local flags (per-character, quest progress) | -| `give_item` | Gives an item to the player's inventory | -| `take_item` | Removes an item from the player's inventory | -| `teleport` | Moves the player to a room ID | -| `heal` | Restores that many hitpoints | -| `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: -```yaml -action: - take_item: dragon_head - give_item: dragon_slayer_medal - set_player_flags: - dragon_quest: complete - dragon_slain: true - heal: 99 - teleport: 1 -``` - -#### 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_clerk -name: General Store Clerk -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. - -### Object Interactions (levers, switches, gates) - -Interaction configs go under the `use_interactions:` key on objects. -Entries with no `item` field are bare interactions triggered by "use" -or "push"/"pull": - -```yaml -id: iron_gate -name: iron gate -hidden: true -description: "A heavy iron gate set into the north wall." -use_interactions: - - condition: - flag: gate_open - value: true - not: true - message: "You push the heavy iron gate open." - action: - set_flags: - gate_open: true -``` - -Item-specific interaction: -```yaml -id: bookshelf -name: bookshelf -use_interactions: - - item: dusty_tome - message: "The bookshelf slides aside, revealing a secret passage!" - action: - set_flags: - secret_passage_open: true -``` - -### Use (crafting stations) - -Use configs go under the `use:` key on objects (e.g. furnaces, ranges): - -```yaml -id: furnace -name: furnace -color: "208" -use: - message: "You place the ore in the furnace..." - wait: 4 - consume: - copper_ore: 1 - reward: - item_id: copper_bar - quantity: 1 - fail_message: "The ore crumbles to dust." - success: - base: 0.60 - per_level: 0.01 - cap: 0.95 - skill: smithing - level: 1 - xp: 15 -``` - ---- diff --git a/worldbuilding_guide/conditions.md b/worldbuilding_guide/conditions.md deleted file mode 100644 index 972dadc..0000000 --- a/worldbuilding_guide/conditions.md +++ /dev/null @@ -1,70 +0,0 @@ -## Conditions Reference - -Conditions are used in talk options, exit gates, on-enter scripts, and use_interactions checks. - -### Simple conditions - -```yaml -# Check a world flag -condition: - flag: gate_open - value: true - -# Check a world flag is NOT set -condition: - flag: gate_open - not: true - -# Check a player flag -condition: - player_flag: finished_tutorial - value: true - -# Check if player has an item -condition: - has_item: bronze_key - -# Check if player does NOT have an item -condition: - has_item: bronze_key - not: true - -# Check if player has enough credits -condition: - min_credits: 50 -``` - -### Compound conditions - -All must pass: -```yaml -condition: - all_of: - - flag: gate_open - value: true - - has_item: pass_stub -``` - -Any one must pass: -```yaml -condition: - any_of: - - has_item: bronze_key - - has_item: iron_key - - player_flag: master_of_unlocking - value: true -``` - -Nested compounds: -```yaml -condition: - all_of: - - player_flag: quest_started - value: true - - any_of: - - has_item: wolf_pelt - - has_item: bear_pelt -``` - ---- - diff --git a/worldbuilding_guide/construction.md b/worldbuilding_guide/construction.md deleted file mode 100644 index 2c4b7ce..0000000 --- a/worldbuilding_guide/construction.md +++ /dev/null @@ -1,223 +0,0 @@ -# Construction Skill - -The Construction skill allows players to process logs into planks and assemble planks into furniture at a workbench. Higher-tier logs produce more valuable planks and furniture. - -## Skill - -- Skill name: `construction` -- Abbreviation: `con` -- Level range: 1-99 -- XP table: RSC standard - -## Commands - -| Command | Description | -|---------|-------------| -| `construct` / `make` | Open construction menu at a workbench | -| `construct ` | Build a specific item | -| `construct ` | Build multiple of an item | - -## Objects - -### workbench - -The primary construction station. Placed in rooms to enable construction. - -```yaml -id: workbench -name: workbench -color: "172" -description: "A sturdy wooden workbench..." -inroom_description: "A workbench stands against the wall..." -``` - -### estate_directory - -A terminal that shows registered homeowners and lets them enter their house. Handled entirely in Go code (`internal/game/cmd_estate_directory.go`), not via YAML behaviors. - -```yaml -id: estate_directory -name: Estate Directory -color: "39" -description: "A holographic terminal displaying property records..." -inroom_description: "An estate directory terminal..." -``` - -When a player looks at the directory, it scans `data/players/characters/*.yaml` for the `owns_house_local_neighborhood` player flag and lists all homeowners. When a player uses or talks to the directory and owns a house, they can teleport to their house entrance room. - -## Tools - -### saw - -Required tool for cutting logs into planks at the workbench. - -```yaml -id: saw -name: saw -tool_type: saw -value: 30 -``` - -## Items - -### Planks (4 tiers) - -| Item ID | Name | Level | Value | Made From | -|---------|------|-------|-------|-----------| -| `planks` | planks | 1 | 20 | logs | -| `oak_planks` | oak planks | 15 | 40 | oak_logs | -| `teak_planks` | teak planks | 35 | 80 | teak_logs | -| `mahogany_planks` | mahogany planks | 50 | 120 | mahogany_logs | - -### Furniture (sellables) - -| Item ID | Level | Planks Required | -|---------|-------|-----------------| -| `wooden_shelf` | 4 | 2 planks | -| `wooden_table` | 6 | 3 planks | -| `wooden_chair` | 8 | 2 planks | -| `oak_shelf` | 18 | 2 oak planks | -| `oak_table` | 20 | 3 oak planks | -| `teak_table` | 38 | 3 teak planks | -| `mahogany_table` | 52 | 3 mahogany planks | - -## Recipes - -All recipes use `type: construction` and `station: [workbench]`. Log-to-plank recipes additionally require `tool: saw`. - -Example plank recipe: -```yaml -id: construct_oak_planks -type: construction -level: 15 -xp: 10 -wait: 20 -station: [workbench] -tool: saw -consume: - - items: [oak_logs] - quantity: 1 -output: oak_planks -message: "You saw the oak logs into sturdy oak planks." -``` - -Example furniture recipe: -```yaml -id: construct_wooden_table -type: construction -level: 6 -xp: 25 -wait: 6 -station: [workbench] -consume: - - items: [planks] - quantity: 3 -output: wooden_table -message: "You build a sturdy wooden table." -``` - -## NPCs - -### estate_broker - -Located in room 16 (Construction Site). Talk behavior sells a house plot for 10 credits via `cost` + `set_player_flags`. Sets `owns_house_local_neighborhood: "local_neighborhood"` on purchase. - -```yaml -id: estate_broker -name: estate broker -talk: - nodes: - start: - message: "\"Interested in a house?\"" - options: - - text: "\"How much?\"" - goto: offer - - text: "\"No thanks.\"" - end: true - offer: - message: "\"10 credits for a plot.\"" - action: - cost: 10 - set_player_flags: - owns_house_local_neighborhood: "local_neighborhood" - options: - - text: "\"Deal!\"" - end: true -unique: true -``` - -### sawmill_operator - -Located in room 171 (Lumberyard). Talk behavior processes all logs in inventory into planks for a credit fee. Uses the `sawmill: true` node action which triggers `processSawmill()` in `internal/game/action_talk.go`. - -Fees: regular (5cr), oak (10cr), teak (20cr), mahogany (40cr). - -## Science Mod: Plank Make - -Level 86 Science utility mod that converts logs to planks at 70% of sawmill cost. Uses naturejunk, solarjunk, and scrap. - -```go -{ID: "plank_make", Name: "Plank Make", Level: 86, BaseXP: 90.0, - JunkCost: map[string]int{"naturejunk": 5, "solarjunk": 1, "scrap": 1}, - Category: ModUtility, TargetType: "inventory"}, -``` - -Usage: `trigger plank make` (all logs) or `trigger plank make ` (specific). - -## Player Houses - -Player houses are real YAML rooms stored in `data/rooms/player_housing/`. Each house area (e.g. Local Neighborhood) has entrance + workshop rooms with unique room IDs in the 200+ range. - -House room files follow the same format as world rooms: -```yaml -id: 200 -name: "Player House: Entrance Hall" -description: "..." -exits: - south: 170 - north: 201 -objects: - - id: charging_station -``` - -The room loader (`World.LoadRoom`) checks `data/rooms/.yaml` first, then falls back to `data/rooms/player_housing/.yaml`. - -Ownership is tracked via player flags (e.g. `owns_house_local_neighborhood: "local_neighborhood"`). The Estate Directory scans character YAML files for these flags. - -## Implementation - -### Production Integration - -Construction is added to `productionTypes` in `action_production.go`: -```go -"construction": {"construct", "constructing"}, -``` - -This enables automatic support through the unified production system (`advanceProduction`, `startProductionFromRecipe`, etc.). - -### Command - -`cmd_construct.go` follows the same pattern as `cmd_craft.go`, using per-recipe `Station` and `Tool` checks. Station: `workbench`, Tool: `saw` (for plank recipes only). - -### Option - -`construct_all` (OptBool, default false): Auto-start construction when only one product is available. - -## Data File Summary - -| Path | Purpose | -|------|---------| -| `data/objects/workbench.yaml` | Workbench station object | -| `data/objects/estate_directory.yaml` | Directory terminal object | -| `data/items/saw.yaml` | Saw tool | -| `data/items/planks.yaml` - `mahogany_planks.yaml` | Plank items (4 tiers) | -| `data/items/wooden_shelf.yaml` - `mahogany_table.yaml` | Furniture items (7 items) | -| Item YAMLs with `craft:` blocks (planks, furniture) | Construction recipes — inline on output item YAML | -| `data/mobs/estate_broker.yaml` | Estate broker mob (talk inline) | -| `data/mobs/sawmill_operator.yaml` | Sawmill operator mob (talk inline) | -| `data/rooms/16.yaml` | Construction Site (updated) | -| `data/rooms/170.yaml` | Local Neighborhood | -| `data/rooms/171.yaml` | Lumberyard | -| `data/rooms/player_housing/200.yaml` | Player house entrance | -| `data/rooms/player_housing/201.yaml` | Player house workshop | -| `data/help/construct.yaml` | Help topic | diff --git a/worldbuilding_guide/courses.md b/worldbuilding_guide/courses.md deleted file mode 100644 index 491e2bd..0000000 --- a/worldbuilding_guide/courses.md +++ /dev/null @@ -1,119 +0,0 @@ -## Agility Courses - -Agility courses are defined in `data/courses/.yaml`. Each course defines a sequence of obstacle rooms, each with a specific verb the player must type to advance. Obstacle rooms themselves are standard room YAML files (typically in `data/rooms/agility/`). - -### Course YAML - -```yaml -id: "vent_shaft" -name: "Ventilation Shaft Course" -required_level: 1 -start_room: 200 -completion_xp: 40 -obstacles: - - room_id: 201 - verb: scramble - ticks_per_phase: 2 - xp: 8 - fail_damage: [1, 2] - messages: - - "You approach the corroded ventilation wall..." - - "You find footholds in the rusted panels and begin to climb..." - - "You scramble up the wall and haul yourself onto the ledge!" - - room_id: 202 - verb: balance - ticks_per_phase: 2 - xp: 8 - fail_damage: [1, 2] - messages: - - "You step onto the narrow coolant pipe..." - - "Arms outstretched, you carefully place one foot in front of the other..." - - "You reach the other side of the pipe and step onto solid ground!" -``` - -### CourseConfig Fields - -| Field | Type | Description | -| ---------------- | ------------- | ---------------------------------------------------- | -| `id` | string | Unique course identifier | -| `name` | string | Display name shown to players | -| `required_level` | int | Minimum Agility level to attempt the course | -| `start_room` | int | Hub room — teleported here on fail or lap completion | -| `completion_xp` | int | Bonus XP awarded when a full lap is completed | -| `obstacles` | []ObstacleDef | Ordered list of obstacles | - -### ObstacleDef Fields - -| Field | Type | Description | -| ----------------- | -------- | --------------------------------------------------------------------------- | -| `room_id` | int | Room ID for this obstacle | -| `verb` | string | Command the player types to attempt it (e.g. `scramble`, `jump`, `climb`) | -| `ticks_per_phase` | float64 | Ticks between each phase message (supports fractional via `engine.ToTicks`) | -| `xp` | int | XP awarded for successfully completing this single obstacle | -| `fail_damage` | [2]int | `[min, max]` damage on failure (HP clamped to minimum 1 — cannot kill) | -| `messages` | []string | Exactly 3 strings for the 3-phase advancement system | - -### 3-Phase Message System - -Each obstacle advances through 3 phases, printing one message per phase: - -``` -Phase 0 (start): messages[0] printed immediately -Phase 1 (middle): messages[1] printed — FAILURE CHECK happens here -Phase 2 (end): messages[2] printed — XP awarded, teleport to next room -``` - -Failure only occurs at phase 1. Success chance is based on Agility level: -- At required level: 70% success (30% fail) -- Each level above reduces fail chance by 1% (down to minimum 5%) -- Fail chance capped at 60% - -On failure, the player takes random damage in `[fail_damage[0], fail_damage[1]]`, is teleported back to `start_room`, and must restart the course. - -### Lap Counting - -Completing all obstacles in a course increments a lap counter stored as a player flag (`agility_laps_`). The `completion_xp` bonus is awarded on the final obstacle only. - -### Obstacle Room YAML - -Each obstacle room should have an `on_enter` message telling the player which verb to use, and a `down` exit back to the course hub: - -```yaml -id: 201 -name: "Ventilation Shaft - Corroded Wall" -description: "A towering wall of corroded ventilation panels..." -on_enter: - - message: "Type 'scramble' to climb the wall." -exits: - down: - room: 200 - blocked_message: "" -``` - -The hub room (e.g. room 200) links to the first obstacle of each course: - -```yaml -id: 200 -name: "Agility Training Grounds" -exits: - south: 17 - north: 201 # Vent Shaft (level 1) - east: 210 # Rooftop (level 20) - west: 220 # Reactor (level 50) -``` - -### Supported Obstacle Verbs - -| Verb | Gerund (display) | -| ---------- | ---------------- | -| `scramble` | scrambling | -| `jump` | jumping | -| `swing` | swinging | -| `balance` | balancing | -| `climb` | climbing | -| `crawl` | crawling | -| `vault` | vaulting | -| `leap` | leaping | -| `slide` | sliding | - -Obstacle verbs are auto-registered at startup from all course YAML files. No changes to `cmd_registry.go` needed when adding new courses — any verb in a `data/courses/` file will work. diff --git a/worldbuilding_guide/doors.md b/worldbuilding_guide/doors.md deleted file mode 100644 index 1e9fb7a..0000000 --- a/worldbuilding_guide/doors.md +++ /dev/null @@ -1,97 +0,0 @@ -## Global vs Player State: Door Examples - -### Example A: Door with a button in another room (GLOBAL) - -A button in room 3 opens a door in room 7. Anyone can press it. Once pressed, the door is open for everyone. - -**Button object** (`data/objects/door_button.yaml`): -```yaml -id: door_button -name: stone button -hidden: true -use_interactions: - - condition: - flag: secret_door_open - not: true - message: "You press the stone button. You hear grinding stone in the distance." - action: - set_flags: - secret_door_open: true -``` - -**Room 3** — contains the button: -```yaml -id: 3 -name: "Button Chamber" -description: "A small stone chamber. A button protrudes from the east wall." -exits: - south: 1 -objects: - - id: door_button -``` - -**Room 7** — contains the door: -```yaml -id: 7 -name: "Hidden Passage" -description: "A dusty corridor. A heavy stone door blocks the way north." -exits: - south: 2 - north: - room: 8 - condition: - flag: secret_door_open # checks WORLD flag - value: true - blocked_message: "A heavy stone door blocks the way." -objects: - - id: stone_door - hidden: true -``` - -### Example B: Key-locked door (PLAYER-LOCAL) - -A locked door that only opens for a player carrying the key. Each player must find their own key. - -**Key item** (`data/items/rusty_key.yaml`): -```yaml -id: rusty_key -name: rusty key -description: "An old iron key, still functional." -value: 0 -stackable: false -``` - -**Room 5** — locked door: -```yaml -id: 5 -name: "Locked Storage" -description: "A small storage room. The way east is blocked by a locked iron door." -exits: - west: 2 - east: - room: 6 - condition: - has_item: rusty_key # checks PLAYER inventory - blocked_message: "The iron door is locked. You need a key." -objects: - - id: iron_door - hidden: true -``` - -**Room 6** — the other side (no key needed to exit): -```yaml -id: 6 -name: "Storage Closet" -description: "Shelves of dusty crates." -exits: - west: 5 # exit back — no condition -spawns: - - item_id: uncut_ruby - quantity: 1 - respawn_ticks: 500 -``` - -Key difference: the button door uses `flag` (shared state — one player presses, everyone benefits), the key door uses `has_item` (per-player inventory check — each player needs their own key). - ---- - diff --git a/worldbuilding_guide/drops.md b/worldbuilding_guide/drops.md deleted file mode 100644 index e5a17ff..0000000 --- a/worldbuilding_guide/drops.md +++ /dev/null @@ -1,52 +0,0 @@ -## Drop Tables - -Shared drop tables can be referenced by multiple behaviors: -```yaml -# data/drops/gem_table.yaml -id: gem_table -drops: - - item_id: uncut_sapphire - weight: 47 - - item_id: uncut_emerald - weight: 16 - - item_id: uncut_ruby - weight: 4 - - item_id: uncut_diamond - weight: 1 -``` - -Bird's nest drop table: -```yaml -# data/drops/birds_nest_drop.yaml -id: birds_nest_drop -drops: - - item_id: credits - weight: 50 - quantity: 200 - - item_id: credits - weight: 30 - quantity: 500 - - item_id: credits - weight: 15 - quantity: 1000 - - item_id: credits - weight: 4 - quantity: 3000 - - item_id: credits - weight: 1 - quantity: 10000 -``` - -Referenced from a gather behavior: -```yaml -drops: - - item_id: copper_ore - weight: 90 - depletes: true - - table: gem_table # pulls from data/drops/gem_table.yaml - weight: 10 - depletes: false -``` - ---- - diff --git a/worldbuilding_guide/hacking.md b/worldbuilding_guide/hacking.md deleted file mode 100644 index 8927b3e..0000000 --- a/worldbuilding_guide/hacking.md +++ /dev/null @@ -1,49 +0,0 @@ -## Hacking - -Hacking minigames are played by jacking into terminal objects. Terminals are YAML objects that are registered in Go's `terminalDefs` map. - -### Terminal Object YAML - -```yaml -id: terminal_basic -name: basic terminal -color: "40" -description: "A battered terminal with a cracked screen. {40}[Level 1 Hacking]{/}" -inroom_description: "A {40}basic terminal{/} hums quietly against the wall." -``` - -- `id` must match a key in `terminalDefs` (Go code in `internal/game/hacking.go`) -- `name` is used for matching in the `jack` command -- `color` controls the display color -- `description` is shown when examining the terminal -- `inroom_description` is shown in room listings -- No `behavior` field — terminals are registered in Go, not YAML behaviors - -### Adding a New Terminal - -1. Create an object YAML file in `data/objects/terminal_.yaml` -2. Add an entry to `terminalDefs` in `internal/game/hacking.go` -3. Optionally create a new minigame implementation in `internal/game/hacking_.go` -4. Place the terminal object in a room's `objects:` list - -### Adding a New Minigame - -New minigames implement the `HackingMinigame` interface: - -```go -type HackingMinigame interface { - Init(level int) string - HandleInput(input string) (output string, done bool, won bool) - Name() string -} -``` - -Optionally implement `XPBonuser` for bonus XP: - -```go -type XPBonuser interface { - BonusXP() int -} -``` - - diff --git a/worldbuilding_guide/hidden_objects.md b/worldbuilding_guide/hidden_objects.md deleted file mode 100644 index 24fd6bc..0000000 --- a/worldbuilding_guide/hidden_objects.md +++ /dev/null @@ -1,40 +0,0 @@ -## Hidden Objects - -Objects with `hidden: true` don't appear in the room's object listing. Players discover them by reading room descriptions or trying commands. The object is still fully interactable — `push gate`, `look gate`, etc. - -```yaml -# data/objects/secret_lever.yaml -id: secret_lever -name: stone lever -hidden: true -description: "A cleverly concealed lever behind a loose stone." -use_interactions: - - condition: - flag: secret_passage_open - value: true - not: true - message: "You pull the lever. A grinding sound echoes from the east." - action: - set_flags: - secret_passage_open: true -``` - -Room description hints at it: -```yaml -description: "A dusty corridor. One of the wall stones looks slightly out of place." -``` - -### Safespot Objects - -Safespot objects should always be `hidden: true`. They only become visible in `look` when the player's effective safespot tier meets the object's `safespot.tier` requirement. This creates a natural discovery mechanic: completing quests and achievements reveals new coverage opportunities. - -Players can discover safespots by: -- Reading room descriptions for hints -- Trying `hide ` (name matching works even for hidden objects) -- Using `look ` once they know the name -- Gaining tier through progression, which reveals safespots in `look` - -See [Safespots](objects.md#safespots) for the full YAML format. - ---- - diff --git a/worldbuilding_guide/index.md b/worldbuilding_guide/index.md deleted file mode 100644 index e79ee1b..0000000 --- a/worldbuilding_guide/index.md +++ /dev/null @@ -1,17 +0,0 @@ -## Quick Reference - -| What you want | Where to put it | -|---|---| -| A room | `data/rooms/.yaml` | -| An item (sword, ore, key) | `data/items/.yaml` | -| A mob (NPC, monster) | `data/mobs/.yaml` | -| An interactive object (rock, lever, door) | `data/objects/.yaml` | -| A shared drop table | `data/drops/.yaml` | -| A crafting recipe | In the output item's YAML under a `craft:` block | -| A help topic | `data/help/.yaml` | -| A science module | `data/modules/.yaml` | -| An agility course | `data/courses/.yaml` | -| A player house | `data/rooms/player_housing/.yaml` | - -Behaviors (gather, talk, use) are defined **inline** in object and mob YAML -under `gather:`, `talk:`, or `use:` keys. There is no separate behavior directory. diff --git a/worldbuilding_guide/items.md b/worldbuilding_guide/items.md deleted file mode 100644 index 2e36349..0000000 --- a/worldbuilding_guide/items.md +++ /dev/null @@ -1,190 +0,0 @@ -## Items - -```yaml -id: bronze_pickaxe -name: bronze pickaxe -color: "178" # xterm-256 color index (0-255) -aliases: ["pick", "pickaxe"] -description: "A sturdy bronze pickaxe." -value: 10 -stackable: false -equip_slot: main_hand # optional — where it equips -weapon_type: melee # optional — melee, ranged, or science -attack_type: stab # stab/slash/crush/ranged/science — determines accuracy type -stats: # optional — per-type combat bonuses - stab_attack: 4 - slash_attack: -2 - crush_attack: 2 - strength_bonus: 3 -speed: 5 # ticks between attacks -tool_type: pickaxe # used by gather behaviors that require "tool: pickaxe" -tool_speed: 2 # reduces gather wait time -requirements: # optional — skill levels needed to equip - attack: 1 -``` - -### Equipment Stats (ItemStats) - -Weapons have per-type **attack bonuses** — these determine accuracy based on the weapon's `attack_type`: - -| Field | Description | -|---|---| -| `stab_attack` | Accuracy bonus for stab attacks | -| `slash_attack` | Accuracy bonus for slash attacks | -| `crush_attack` | Accuracy bonus for crush attacks | -| `science_attack` | Accuracy bonus for science attacks | -| `ranged_attack` | Accuracy bonus for ranged attacks | - -Armor has per-type **defense bonuses** — the mob's `attack_type` determines which defense is used: - -| Field | Description | -|---|---| -| `stab_defense` | Defense vs stab attacks | -| `slash_defense` | Defense vs slash attacks | -| `crush_defense` | Defense vs crush attacks | -| `science_defense` | Defense vs science attacks (negative on metal armor) | -| `ranged_defense` | Defense vs ranged attacks | - -Other damage/utility bonuses: - -| Field | Description | -|---|---| -| `strength_bonus` | Melee max hit bonus | -| `ranged_strength` | Ranged max hit bonus (on ammo) | -| `science_damage` | Science max hit bonus | -| `technology_bonus` | Reduces tech battery drain rate | - -### Attack Type - -Weapons should always set `attack_type`. Common mappings: - -| Category | attack_type | -|---|---| -| Swords | slash | -| Daggers | stab | -| Axes | slash | -| Pickaxes | stab | -| Bows | ranged | -| Unarmed | crush (default) | - -### Equipment Requirements - -Higher-tier equipment requires skill levels to equip: - -```yaml -requirements: - attack: 20 # melee weapons - defense: 20 # armor - ranged: 30 # ranged weapons/armor -``` - -Standard tiers: bronze/iron (none), steel (5), black (10), mithril (20), adamant (30), rune (40), dragon (60). - -### Ranged Ammo - -Arrows and bolts use `ranged_strength` for damage. Accuracy comes from the bow: - -```yaml -id: iron_arrow -name: iron arrow -stackable: true -equip_slot: ammo -stats: - ranged_strength: 10 -``` - -### Color - -The `color` field accepts xterm-256 palette indices (0-255) with optional modifiers: - -```yaml -color: "178" # bronze/gold -color: "75 bold" # bold steel blue -color: "g:196,208,226" # gradient red → orange → gold -``` - -In ANSI mode, extended colors (16-255) automatically downgrade to the nearest ANSI color. Use `colortable` in-game to see all 256 colors. - -Key item (quest token, not equippable): -```yaml -id: pass_stub -name: pass stub -description: "A crumpled slip of paper stamped with the guard's seal." -value: 0 -stackable: false -``` - ---- - -## Firemaking - -```yaml -id: logs -name: logs -fire_level: 1 # firemaking level required -fire_xp: 40 # XP for burning/stoking -burn_ticks: 24 # how long the fire burns -``` - -## Food / Healing - -```yaml -id: bread -name: bread -color: "222" -description: "A fresh loaf of bread, still warm from the oven." -value: 5 -heal_value: 5 # positive = heal, negative = damage -eat_message: "You eat the bread. Warm and satisfying." -``` - -## MadeFrom — Item Combinations - -Define what items combine to make this item. Used by the `use` command. Combinations run through the production system — players are prompted "How many?" and the action loops with a timer. - -```yaml -id: bread_dough -name: bread dough -color: "222" -ticks: 2 -made_from: - - items: [pot_of_flour] - quantity: 1 - byproducts: [empty_pot] - - items: [bucket_of_water, pitcher_of_water] - quantity: 1 - byproducts: [empty_bucket, empty_pitcher] -``` - -Each entry is an ingredient slot. Multiple `items` means any of them works. The first matching item found in inventory is consumed. - -`ticks` controls how many game ticks each production cycle takes (default 2). - -### Byproducts - -`byproducts` is a list matching the `items` list by index. When `bucket_of_water` (items[0]) is consumed, `empty_bucket` (byproducts[0]) is returned to inventory. When `pitcher_of_water` (items[1]) is consumed, `empty_pitcher` (byproducts[1]) is returned instead. - -Use `""` for items that produce no byproduct: - -```yaml -made_from: - - items: [pot_of_flour, loose_flour] - quantity: 1 - byproducts: [empty_pot, ""] # pot returns empty_pot, loose flour returns nothing - - items: [bucket_of_water] - quantity: 1 - byproducts: [empty_bucket] -``` - -Omit `byproducts` entirely if no items in that slot produce a byproduct: - -```yaml -made_from: - - items: [pot_of_flour] - quantity: 1 - byproducts: [empty_pot] # returns empty_pot - - items: [salt] - quantity: 1 # no byproducts field — salt is consumed entirely -``` - -See also `recipes.md` for station-based recipes. diff --git a/worldbuilding_guide/mobs.md b/worldbuilding_guide/mobs.md deleted file mode 100644 index c19efff..0000000 --- a/worldbuilding_guide/mobs.md +++ /dev/null @@ -1,179 +0,0 @@ -## Mobs - -Basic combat mob with per-type defense bonuses: -```yaml -id: "man" -name: "man" -description: "A shabby-looking man." -attack: 1 -strength: 1 -defense: 1 -hp: 7 -speed: 5 -aggressive: false -respawn_ticks: 30 -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 -drops: - remains: "bones" # always dropped on death - loot: - - item_id: "credits" - weight: 98 - quantity: 10 - - item_id: "credits" - weight: 2 - quantity: 150 -idle_descriptions: - - "scribbles something in a small notebook" - - "gazes skyward at the clouds" -combat_descriptions: - - "is engaged in a fight to the death with %s" -``` - -### 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. - -```yaml -id: goblin -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 -``` - -### Mob Attack/Defense Fields - -| Field | Description | -|---|---| -| `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_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 | -| `size` | Mob size for safespot blocking: small/medium/large/massive (default: medium) | - -### Protected/Unique Mobs - -Mob with talk behavior — can be talked to via `talk:` inline config: -```yaml -id: "guard" -name: "Guard" -talk: # inline talk config (no separate behavior file) - 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 -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" - - "adjusts the grip on his weapon" -``` - -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 on `) | -| `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 deleted file mode 100644 index 427968a..0000000 --- a/worldbuilding_guide/objects.md +++ /dev/null @@ -1,392 +0,0 @@ -## Objects - -Objects interact with the world through **inline behavior configs** under -`gather:`, `talk:`, or `use:` keys. There is no separate behavior -directory — everything goes directly in the object's YAML. - -Gathering object (mining): -```yaml -id: copper_rock -name: copper rock -color: "178" # xterm-256 color index (0-255) -gather: - skill: mining - level: 1 - xp: 17 - base_wait: 8 - tools: - - pickaxe - success: - base: 0.40 - per_level: 0.01 - cap: 0.95 - gather_message: "You swing your pickaxe at the rock..." - fail_message: "You chip away but get nothing useful." - drops: - - item_id: copper_ore - weight: 90 - depletes: true - message: "You manage to mine some {178}copper ore{/}." - - table: gem_table - weight: 10 - depletes: false - message: "You spot a glint of something valuable!" - respawn_timer: 50 - respawn_broadcast: "A glint of copper catches your eye from some {name}." -``` - -Tree object (woodcutting with shared depletion): -```yaml -id: oak_tree -name: oak tree -color: "113" -gather: - skill: woodcutting - level: 15 - xp: 37 - base_wait: 6 - tools: - - axe - success: - base: 0.40 - per_level: 0.01 - cap: 0.90 - gather_message: "You swing your axe at the oak tree..." - fail_message: "You swing but get no logs." - drops: - - item_id: oak_logs - weight: 100 - depletes: false - message: "You get some {113}oak logs{/}." - respawn_timer: 14 - deplete_timer: 45 - nest_chance: 256 - respawn_broadcast: "An {name} grows back." -``` - -Color accepts xterm-256 indices with optional modifiers (`bold`, `dim`, `underline`) -and gradients (`g:196,82`). In ANSI mode, extended colors downgrade to the nearest -ANSI color. - -Object interaction (gate, lever — uses `use_interactions:` key): -```yaml -id: iron_gate -name: iron gate -hidden: true -description: "A heavy iron gate set into the north wall." -use_interactions: - - condition: - flag: gate_open - value: true - not: true - message: "You push the heavy iron gate open." - action: - set_flags: - gate_open: true -``` - -Decorative object (no behavior keys — just a name/description): -```yaml -id: lumby_fountain -name: town fountain -description: "Clear water sparkles in the sunlight." -``` - -Talk object (NPC conversations — uses `talk:` key): -```yaml -id: tool_shed -name: Tool Shed -description: "A small shed with an open window." -talk: - nodes: - start: - message: "\"Welcome to the tool shed!\"" - options: - - text: "\"What do you have?\"" - goto: shop - - text: "\"Goodbye.\"" - end: true -``` - -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. - ---- - -## Use Interactions - -Objects can define `use_interactions` to handle when a player uses a specific item on the object. Each interaction can check conditions, show a message, and execute actions — using the same condition and action primitives as the talk system. - -Entries are checked top-to-bottom; the first match with a passing condition wins. - -Simple message (no action): -```yaml -id: anvil -name: anvil -use_interactions: - - item_id: silver_bar - message: "You should use this with a mold at a furnace." - - item_id: gold_bar - message: "You should use this with a mold at a furnace." -``` - -Puzzle interaction (take item, set flag): -```yaml -id: crystal_slot -name: crystal slot -use_interactions: - - item_id: crystal_key - condition: - flag: crystal_inserted - message: "The crystal key is already in the slot." - - item_id: crystal_key - message: "You insert the crystal key into the slot. It clicks into place." - action: - take_item: crystal_key - set_flags: - crystal_inserted: true -``` - -Quest item exchange: -```yaml -use_interactions: - - item_id: ancient_scroll - condition: - player_flag: quest_started - message: "You place the scroll on the pedestal. The door rumbles open!" - action: - take_item: ancient_scroll - set_player_flags: - quest_complete: true - set_flags: - temple_door_open: true -``` - -### UseInteraction Fields - -| Field | Type | Description | -| ----------- | ---------- | ------------------------------------------------ | -| `item_id` | string | Item ID that triggers this interaction | -| `condition` | Condition | Optional condition (same as exits/talk/use_interactions) | -| `message` | string | Message shown to the player | -| `action` | NodeAction | Optional actions (same as talk node actions) | - -### Available Actions (same as talk) - -| Field | Type | Description | -| ------------------ | -------------- | ---------------------------------- | -| `set_flags` | map[string]any | Set world flags (shared) | -| `set_player_flags` | map[string]any | Set player flags (per-character) | -| `give_item` | string | Give an item to inventory | -| `take_item` | string | Remove an item from inventory | -| `teleport` | int | Move player to a room ID | -| `heal` | int | Restore hitpoints | - -### Available Conditions (same as exits/talk) - -| Field | Description | -| ------------- | -------------------------------- | -| `flag` | World flag check | -| `player_flag` | Per-character flag check | -| `has_item` | Inventory item check | -| `value` | Expected value for flag checks | -| `not` | Invert the condition | -| `all_of` | All sub-conditions must pass | -| `any_of` | Any sub-condition must pass | - ---- - -## 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. - ---- - -## Safespots - -Safespot objects provide cover that blocks melee attacks. Players use `hide ` to -crouch behind them and attack with ranged or science weapons. Melee attacks force the player -out of cover. - -```yaml -id: rock_outcrop -name: rock outcrop -color: "248" -hidden: true -inroom_description: "A jagged rock outcrop juts from the floor." -description: "A large, jagged rock formation providing natural cover." -safespot: - tier: 2 - max_block_size: large - max_occupants: 3 - unsafe_chance: 0.008 - decay_ticks: 500 - decay_chance: 0.01 - respawn_on_hide: false - respawn_ticks: 300 - levels: - - message: "The rock formation stands solid." - degrade_message: "The rock structure begins crumbling!" - - message: "The rock outcrop is cracked and worn." - degrade_message: "A few jagged bits are all that's left of the rock outcrop!" - - message: "Only a few jagged rocks remain." - degrade_message: "The rock outcrop crumbles to nothing!" -``` - -Safespot objects should be `hidden: true`. They never appear in room descriptions — players -discover them through quest guidance, `inroom_description` text in the room's YAML, or by -examining objects directly with `look `. - -### SafespotConfig Fields - -| Field | Type | Default | Description | -|---|---|---|---| -| `tier` | int | 0 | Required safespot tier to use this object | -| `max_block_size` | string | `""` (all) | Largest mob size blocked: small/medium/large/massive | -| `max_occupants` | int | 0 | Max players that can hide behind this object (0 = unlimited) | -| `unsafe_chance` | float64 | 0 | Per-tick chance (0.0-1.0) to be forced out of cover | -| `decay_ticks` | float64 | 0 | Guaranteed ticks before object degrades one level | -| `decay_chance` | float64 | 0 | Per-tick random chance (0.0-1.0) to degrade one level | -| `respawn_on_hide` | bool | false | If true, hiding behind an empty safespot resets it to max level. Only triggers when no occupants are present. | -| `respawn_ticks` | float64 | 0 | Ticks until a destroyed safespot respawns (0 = never respawns — object is deleted from the room) | -| `levels` | []SafespotLevel | required | At least one level entry | - -### SafespotLevel Fields - -| Field | Type | Description | -|---|---|---| -| `message` | string | Shown to the player when the safespot is at this level | -| `degrade_message` | string | Broadcast to the room when the safespot degrades FROM this level | - -Levels are ordered from highest to lowest. The first entry is the safespot's best state; -the last entry is its weakest state before destruction. When the last level degrades, -the safespot is destroyed. - -### Three Safespot Patterns - -The combination of `respawn_on_hide` and `respawn_ticks` creates three distinct use cases: - -**1. Standard Safespot (always available)** - -```yaml -safespot: - respawn_on_hide: true - respawn_ticks: 0 - levels: - - message: "You crouch behind the rock formation, using it as cover." -``` - -The safespot starts at full health each time a player hides behind it (first occupant only — -subsequent players don't reset the level). Ideal for skilling areas and general exploration. -Since `respawn_ticks: 0`, a destroyed safespot would be deleted, but with a single level and -no decay, it never degrades. Simple and permanent. - -**2. Regenerating Safespot (timer-based respawn)** - -```yaml -safespot: - respawn_on_hide: false - respawn_ticks: 300 - levels: - - message: "The rock formation stands solid." - degrade_message: "The rock structure begins crumbling!" - - message: "The rock outcrop is cracked and worn." - degrade_message: "A few jagged bits are all that's left!" - - message: "Only a few jagged rocks remain." - degrade_message: "The rock outcrop crumbles to nothing!" -``` - -Degradation persists between hides — a player who leaves and re-hides finds the -safespot at its previous level. After total destruction, a timer (`respawn_ticks`) -counts down, then the safespot reforms at full health. Suits areas with heavy use -where a temporary respite is valuable. - -**3. Disposable Safespot (boss fight cover)** - -```yaml -safespot: - respawn_on_hide: false - respawn_ticks: 0 - levels: - - message: "The barricade offers solid cover." - degrade_message: "The barricade splinters under the assault!" - - message: "The barricade is splintering badly." - degrade_message: "The barricade shatters completely!" -``` - -Degradation persists between hides. When the last level degrades, the safespot is -**deleted from the room entirely** — it does not respawn. Perfect for boss encounters -where a room script places temporary cover that the party consumes during the fight. -Once destroyed, it's gone for good (until the boss room re-instantiates it). - -### Mob Size - -Mobs must have a `size` field for safespot blocking to work: - -```yaml -# data/mobs/cow.yaml -size: small - -# data/mobs/moss_giant.yaml -size: large -``` - -Size ordering: `small` < `medium` < `large` < `massive`. Defaults to `medium` when absent. -A safespot with `max_block_size: large` blocks mobs of size small, medium, and large, -but not massive. - -### Progression - -Players gain safespot tier by completing quests and combat achievements, which set -player flags via talk node actions: - -| Flag | Source | Tier bonus | -|---|---|---| -| `quest_animal_magnetism` | Quest completion | +1 | -| `quest_dragon_slayer` | Quest completion | +1 | -| `achieve_medium_combat` | Combat achievement | +1 | -| `achieve_hard_combat` | Combat achievement | +2 | - -### Combat Integration - -- Melee mobs (attack_type: stab/slash/crush) cannot initiate aggro or land hits on - safespotted players if the mob's size is within the safespot's `max_block_size`. -- Ranged and science mobs (attack_type: ranged/science) ignore safespots entirely. -- If a safespotted player attacks with melee, they automatically leave cover. -- Hiding in combat takes 4 ticks; if the mob hits during this time, the hide fails. -- Hiding out of combat takes 1 tick. - -### Option: safespot_alert - -Players can customize the message shown when forced out of a safespot: - - option safespot_alert "{196 bold}** DANGER **{/} Cover blown!" -Default: `"{196 bold}** Your safespot has been compromised! **{/}"` - ---- - diff --git a/worldbuilding_guide/quests.md b/worldbuilding_guide/quests.md deleted file mode 100644 index b5f18fa..0000000 --- a/worldbuilding_guide/quests.md +++ /dev/null @@ -1,103 +0,0 @@ -## Complete Quest Example: "Clear the Rats" - -### 1. Quest giver mob (`data/mobs/quest_giver.yaml`) -```yaml -id: "quest_giver" -name: "Elder" -unique: true -protected: true -talk: - nodes: - start: - message: "\"Rats! Rats everywhere in the cellar. Clear them out and I'll reward you.\"" - options: - - text: "\"I'll handle it.\"" - goto: accept_quest - condition: - player_flag: rat_quest - not: true - - text: "\"I killed the rats.\"" - goto: turn_in - condition: - player_flag: rat_quest - value: started - - text: "\"Goodbye.\"" - end: true - - accept_quest: - message: "\"Good lad. The cellar is west of here. Come back when they're dead.\"" - action: - set_player_flags: - rat_quest: started - options: - - text: "\"On my way.\"" - end: true - - turn_in: - message: "\"You did it! The village owes you a debt. Here — take this.\"" - action: - give_item: rusty_sword - set_player_flags: - rat_quest: complete - heal: 10 - options: - - text: "\"Thanks!\"" - end: true -attack: 1 -strength: 1 -defense: 1 -hp: 20 -speed: 5 -aggressive: false -idle_descriptions: - - "mutters about the rat infestation" -``` - -Talk configs are inline on the mob under the `talk:` key. No separate behavior file needed. - -### 2. Rat mobs (`data/mobs/rat.yaml`) -```yaml -id: "rat" -name: "giant rat" -attack: 2 -strength: 1 -defense: 1 -hp: 3 -speed: 4 -aggressive: true -respawn_ticks: 60 -drops: - remains: "rat bones" -``` - -### 3. Cellar room (`data/rooms/20.yaml`) -```yaml -id: 20 -name: "Cellar" -description: "A damp, dark cellar. The floor scuttles with movement." -exits: - east: 1 -mobs: - - "rat" - - "rat" - - "rat" - - "rat" - - "rat" -``` - -### 4. Room 1 with quest giver (`data/rooms/1.yaml`) -```yaml -id: 1 -name: "Town Square" -description: "Cobblestone paths lead in all directions. The Elder stands near the fountain." -exits: - west: 20 - north: 2 -mobs: - - "quest_giver" -objects: - - id: lumby_fountain -``` - ---- - diff --git a/worldbuilding_guide/recipes.md b/worldbuilding_guide/recipes.md deleted file mode 100644 index 450ded9..0000000 --- a/worldbuilding_guide/recipes.md +++ /dev/null @@ -1,277 +0,0 @@ -# The House of Icarus - World Building Guide - -## Crafting - -Crafting information lives on the **output item's YAML file** via the `craft:` block. There is no separate recipe directory. To find out how to make `bronze_dagger`, open `data/items/equipment/bronze_dagger.yaml` and look for the `craft:` block. - -### Craft YAML Reference - -| Field | Type | Description | -| -------------- | -------------- | ----------------------------------------------- | -| `type` | string | Craft type (pharmacy, smithing, cooking, smelting, crafting, fletching, construction, clean, or "" for skill-less) | -| `skill` | string | Skill checked (defaults to type if omitted) | -| `level` | int | Required skill level | -| `xp` | int | XP awarded on success | -| `wait` | float64 | Ticks per craft cycle | -| `station` | []string | Station object IDs required (optional) | -| `tool` | string | Required tool_type (optional) | -| `consume` | []ConsumeEntry | Ingredients consumed (see below) | -| `output_qty` | int | Quantity produced (default 1, for stackables) | -| `fail` | string | ItemID produced on failure (optional) | -| `message` | string | Success message per cycle (optional, see message defaults) | -| `fail_message` | string | Failure message (optional, empty = silent fail) | -| `start_message`| string | Message when action begins (optional, see message defaults) | -| `end_message` | string | Message when action completes (optional, see message defaults) | -| `steps` | []CraftStep | Multi-step messages at tick intervals (optional)| -| `success` | SuccessFormula | Optional skill check formula (see Behaviors) | - -The output is the item itself — no `output` field needed. The item ID IS the craft ID. - -### Message Variables - -All message fields (`message`, `fail_message`, `start_message`, `end_message`, and `steps[].message`) support variables that expand to colorized item names: - -| Variable | Expands to | -| -------- | ----------------------------------------------- | -| `%n` | Output item name (colored, e.g. "stim potion") | -| `%i1` | 1st consume entry's matched item (colored) | -| `%i2` | 2nd consume entry's matched item (colored) | -| `%b1` | 1st byproduct item name (colored, success only) | -| `%b2` | 2nd byproduct item name (colored, success only) | - -Numbering follows YAML consume/byproduct order. If a consume entry has multiple alternative items (`items: [a, b]`), the variable expands to whichever the player actually possesses, colored with that item's `color:` field. - -Variables work alongside inline color tags (`{196}text{/}`) which are expanded after variable substitution. - -### Message Defaults - -If a message field is omitted from the YAML, the game uses a skill-level default based on the craft `type`: - -| Type | Default `message` | Default `start_message` | -|------|-------------------|------------------------| -| `cooking` | `"Cooked to perfection. %n looks great!"` | `"You start cooking %i1."` | -| `smelting` | `"You remove a white hot %n!"` | `"You place the %i1 into the furnace."` | -| `smithing` | `"You smith a %n."` | `"You begin smithing %i1."` | -| `crafting` | `"You craft a %n."` | `"You begin crafting %i1."` | -| `fletching` | `"You fletch a %n."` | `"You begin fletching %i1."` | -| `pharmacy` | `"You mix a %n."` | `"You start mixing %i1."` | -| `construction` | `"You construct a %n."` | `"You begin constructing %i1."` | -| `clean` | `"You clean the %i1."` | `"You begin cleaning herbs."` | -| (unset) | `"You produce %n."` | `"You start %i1."` | - -Any of these can be overridden per-item by setting the field in YAML. For skills that never fail (smithing, crafting, fletching, pharmacy, construction), `fail_message` defaults to empty — no message is shown on failure. - -### CraftStep — Multi-Step Messages - -Optional interim messages fired at specific tick offsets during the craft cycle. The `tick` is the number of ticks elapsed since the start of the cycle. - -```yaml -craft: - type: "" - wait: 6 - consume: - - items: [map_piece_1] - quantity: 1 - - items: [map_piece_2] - quantity: 1 - - items: [map_piece_3] - quantity: 1 - steps: - - tick: 1 - message: "You try to puzzle how the pieces fit together." - - tick: 3 - message: "Aha, this edge lines up here!" - message: "You assemble the map!" -``` - -### ConsumeEntry — Multi-Item Ingredient Slots - -Each consume entry defines an ingredient slot with multiple valid items. The first matching item found in the player's inventory is consumed. - -```yaml -consume: - - items: [item_id, alternative_id, ...] - quantity: 1 - byproducts: [byproduct_for_item, byproduct_for_alt, ...] -``` - -`byproducts` is optional. When present, it matches the `items` list by index — consuming `items[0]` returns `byproducts[0]` to inventory. Use `""` for items with no byproduct: - -```yaml -consume: - - items: [bucket_of_water, vial_of_water] - quantity: 1 - byproducts: [empty_bucket, ""] # bucket returns empty, vial is consumed entirely - - items: [herb] - quantity: 1 # no byproducts — herb is consumed entirely -``` - -### Skill-Based Production - -```yaml -# data/items/consumables/stim_potion.yaml -craft: - type: pharmacy - skill: pharmacy - level: 3 - xp: 25 - wait: 4 - consume: - - items: [guam_potion_unf] - quantity: 1 - - items: [eye_of_newt] - quantity: 1 - message: "You mix a %n." # %n expands to "stim potion" colored -``` - -### Station-Based Production - -```yaml -# data/items/materials/bronze_bar.yaml -craft: - type: smelting - skill: smithing - level: 1 - xp: 6 - wait: 4 - station: [furnace] - consume: - - items: [copper_ore] - quantity: 1 - - items: [tin_ore] - quantity: 1 - message: "You smelt a %n." # default is "You remove a white hot %n!" - # overridden here for simpler flavor -``` - -```yaml -# data/items/equipment/bronze_dagger.yaml -craft: - type: smithing - skill: smithing - level: 1 - xp: 12 - wait: 4 - station: [anvil] - consume: - - items: [bronze_bar] - quantity: 1 - # message omitted — uses default "You smith a %n." -``` - -For stackable outputs, use `output_qty`: - -```yaml -# data/items/ammo/bronze_nails.yaml -craft: - type: smithing - skill: smithing - level: 4 - xp: 12 - wait: 4 - station: [anvil] - consume: - - items: [bronze_bar] - quantity: 1 - output_qty: 15 - # message omitted — uses default "You smith a %n." -``` - -### Tool-Based Production - -```yaml -# data/items/ammo/arrow_shafts.yaml -craft: - type: fletching - level: 1 - xp: 5 - wait: 3 - tool: knife - consume: - - items: [logs, oak_logs, willow_logs] - quantity: 1 - output_qty: 15 -``` - -The `tool` field requires the player to have an item with that `tool_type` equipped or in inventory. The tool is NOT consumed. - -### Skill-Less Combinations (Item-on-Item) - -Omit `type` (or leave it empty) for combinations with no skill check, no XP, and 100% success: - -```yaml -# data/items/consumables/bucket_of_water.yaml -craft: - wait: 0 - consume: - - items: [vial_of_water, jug_of_water] - quantity: 1 - byproducts: [empty_vial, empty_jug] - - items: [empty_bucket] - quantity: 1 - message: "You pour the %i1 into the %i2." # %i1 = water source, %i2 = bucket -``` - -### Multi-Piece Assembly (3+ items → 1) - -```yaml -# data/items/quest/ancient_map.yaml -craft: - wait: 0 - consume: - - items: [torn_page_1] - quantity: 1 - - items: [torn_page_2] - quantity: 1 - - items: [torn_page_3] - quantity: 1 - steps: - - tick: 1 - message: "You try to puzzle how the pieces fit together." - - tick: 3 - message: "Aha, this edge lines up here!" - message: "You assemble the %n." -``` - -### Clean Recipes - -Clean herb recipes use `type: clean` and are handled as background actions (one herb per cycle, directly mutating inventory): - -```yaml -# data/items/materials/guam.yaml (clean guam) -craft: - type: clean - skill: pharmacy - level: 3 - xp: 3 - wait: 2 - consume: - - items: [grimy_guam] - quantity: 1 - # message omitted — uses default "You clean the %i1." - # %i1 expands to the grimy herb name with its color -``` - -### Food/Healing Items - -Items with `heal_value` and `eat_message` can be consumed via the `eat` command. - -```yaml -id: bread -name: bread -color: "222" -description: "A fresh loaf of bread, still warm from the oven." -value: 5 -heal_value: 5 -eat_message: "You eat the bread. Warm and satisfying." -``` - -### Architecture - -The `CraftIndex` (`internal/game/craft_index.go`) is built once at startup from all item YAMLs that have a `craft:` block. It provides `O(1)` lookups by type, input item, and station. All crafting commands query the CraftIndex — no runtime file scanning. - -**Indexes:** -- `byType["pharmacy"]` → all pharmacy-craftable items (used by `mix`) -- `byInput["guam"]` → all items that consume guam (used by `use` to find combinations) -- `byStation["anvil"]` → all items craftable at an anvil (used by `use bar on anvil`) -- `FindByTwoInputs(a, b)` → items consuming both inputs in different consume entries diff --git a/worldbuilding_guide/rooms.md b/worldbuilding_guide/rooms.md deleted file mode 100644 index 3d6db7d..0000000 --- a/worldbuilding_guide/rooms.md +++ /dev/null @@ -1,134 +0,0 @@ -## Rooms - -Minimal room: -```yaml -id: 1 -name: "Town Square" -description: "Cobblestone paths lead in all directions. A fountain gurgles peacefully." -exits: - north: 2 - west: 7 - east: 3 -``` - -### Inline Color Tags - -Room descriptions support inline color tags using `{spec}text{/}` syntax. Untagged text uses the `room_desc` color. - -```yaml -description: "On the table lies a {182 bold}mysterious vase{/} with a rose in it." -``` - -Tag spec format: `{<0-255> [bold] [dim] [underline]}text{/}` - -Gradients: `{g:196,82}gradient text{/}`. Multi-stop: `{g:45,39,59}three stops{/}`. - -### Exits — simple vs conditional - -Simple exit — always passable: -```yaml -exits: - north: 2 -``` - -Conditional exit — blocked until a world flag is set: -```yaml -exits: - north: - room: 11 - condition: - flag: gate_open - value: true - blocked_message: "A heavy iron gate blocks the way north." -``` - -Conditional exit — blocked unless the PLAYER has a flag (key, permission, quest state): -```yaml -exits: - east: - room: 12 - condition: - player_flag: has_vault_key - value: true - blocked_message: "The vault door is locked. You need a key." -``` - -Conditional exit with compound condition — requires both a world flag AND a player flag: -```yaml -exits: - north: - room: 20 - condition: - all_of: - - flag: bridge_repaired - value: true - - player_flag: paid_toll - value: true - blocked_message: "The bridge is out, and the toll collector blocks the path." -``` - -### Item Spawns — ground items that respawn - -```yaml -item_spawns: - - id: bronze_pickaxe - quantity: 1 - respawn_ticks: 30 # reappears 30 ticks (18 seconds) after being picked up - - id: copper_ore - quantity: 3 - respawn_ticks: 50 -``` - -### Mobs — NPCs placed in the room - -Simple string (no wandering): -```yaml -mobs: - - "newbie_trainer" - - "man" -``` - -With wander config per-instance: -```yaml -mobs: - - id: man - wander_interval: 10 # attempts to wander every 10 ticks - - id: man - wander_interval: 15 - wander_rooms: [1, 4, 5] # optional — only exit to these rooms -``` - -Mob wander config lives in the room YAML, not in the mob definition. This keeps mobs generic -so the same `man` can wander differently depending on where it's placed. Mobs wander through -legal (unconditioned) room exits. If no legal exits exist, the mob stays still. Mobs with -no `wander_interval` never wander. - -### Objects — interactive fixtures - -```yaml -objects: - - id: copper_rock # simple placement - - id: copper_rock # second instance - - id: fishing_spot - wander_rooms: [7, 8, 9] # teleports between these rooms - wander_interval: 12 # every 12 ticks - - id: iron_gate # hidden object (see below) -``` - -### On-enter scripts — messages when a player arrives - -```yaml -on_enter: - - message: "The guard barks: \"State your business!\"" - condition: - player_flag: talked_to_guard - not: true # only first visit - - - message: "The guard nods. \"Back again?\"" - condition: - player_flag: talked_to_guard - value: true # subsequent visits -``` - ---- - diff --git a/worldbuilding_guide/search.md b/worldbuilding_guide/search.md deleted file mode 100644 index e6e2a14..0000000 --- a/worldbuilding_guide/search.md +++ /dev/null @@ -1,15 +0,0 @@ -## The `search` Command - -Used to open searchable items in your inventory (bird's nests, etc.): -``` -Usage: search - -Example: search nest - search birds nest -``` - -Searches your inventory for the named item. If found, removes it and rolls on a drop table. -Currently only bird's nests are searchable. - ---- - diff --git a/worldbuilding_guide/state.md b/worldbuilding_guide/state.md deleted file mode 100644 index 2eefcc4..0000000 --- a/worldbuilding_guide/state.md +++ /dev/null @@ -1,8 +0,0 @@ -## State: World vs Player - -**World flags** (`set_flags`, checked with `flag`) are shared by every player on the server. A door opened by one player is open for everyone. A lever pulled once changes the world for all. - -**Player flags** (`set_player_flags`, checked with `player_flag`) are per-character. Quest progress, "has read the sign," "paid the toll" — these are different for each player. Saved to the character YAML and persist across logins. - ---- - diff --git a/worldbuilding_guide/tips.md b/worldbuilding_guide/tips.md deleted file mode 100644 index 307ea1e..0000000 --- a/worldbuilding_guide/tips.md +++ /dev/null @@ -1,21 +0,0 @@ -## Tips - -1. **Use player flags for quest progress, world flags for environmental state.** If a bridge is repaired, that's world state. If a player has read a sign, that's player state. - -2. **Conditions on enter scripts** make rooms feel alive. A guard who only barks the first time, a room that changes after a quest completes. - -3. **Hidden objects** keep room descriptions clean. Mention them in the room's description text instead of auto-listing them. - -4. **Drop tables** are shareable. The `gem_table` is used by copper rocks AND mob loot. Define once, reference everywhere. - -5. **Objects are for fixtures, mobs are for living things.** If it has HP and can die, it's a mob. If it's a rock, lever, door, or crafting station, it's an object. Both can have behaviors. - -6. **The `talk` field on mobs** lets you talk to them directly — `talk guard` finds the guard mob, no duplicate object entry needed. - -7. **Exits accept both `int` and `map` formats.** `north: 2` is shorthand for `north: {room: 2}`. Add `condition` and `blocked_message` only when needed. Mob room entries accept both `"man"` and `{id: man, ...}`. - -8. **Live editing works.** Room, item, mob, object, and behavior YAML files are read from disk on each access. Change a room description or dialog and it takes effect immediately — no restart needed. - -9. **Shared depletion vs per-drop depletion.** Use `deplete_timer` for trees — the timer counts down while being chopped and regens when left alone. Use `depletes: true` on individual drops for rocks — they deplete on first successful gather. - -10. **Mob wander config goes in the room YAML**, not the mob definition. This lets the same `man` wander differently in different rooms. diff --git a/worldbuilding_guide/wandering_mobs.md b/worldbuilding_guide/wandering_mobs.md deleted file mode 100644 index 8d49ed9..0000000 --- a/worldbuilding_guide/wandering_mobs.md +++ /dev/null @@ -1,20 +0,0 @@ -## Wandering Mobs - -Mob wandering is configured per-instance in the room YAML, not on the mob definition: -```yaml -# In a room: -mobs: - - id: man - wander_interval: 10 # attempt to wander every 10 ticks - - id: man - wander_interval: 15 - wander_rooms: [5, 6, 7] # optional — restrict which rooms via exits -``` - -Mobs wander through legal (unconditioned) room exits. When the wander interval expires, -the mob picks a random exit with no conditions and moves through it. If `wander_rooms` -is set, only exits leading to those room IDs are legal. Mobs without `wander_interval` -never wander. Mobs stop wandering while in combat. Dead mobs respawn at their home room. - ---- - diff --git a/worldbuilding_guide/wandering_objects.md b/worldbuilding_guide/wandering_objects.md deleted file mode 100644 index f1f91c5..0000000 --- a/worldbuilding_guide/wandering_objects.md +++ /dev/null @@ -1,39 +0,0 @@ -## Wandering Objects - -Fishing spots that move between rooms: -```yaml -# data/objects/fishing_spot.yaml -id: fishing_spot -name: fishing spot -gather: - skill: fishing - level: 1 - xp: 10 - base_wait: 4 - tools: - - fishing_rod - bait: fishing_bait - success: - base: 0.30 - per_level: 0.01 - cap: 0.90 - gather_message: "You cast your line into the water..." - drops: - - item_id: raw_trout - weight: 100 - message: "You catch a raw trout!" -``` - -```yaml -# In a room: -objects: - - id: fishing_spot - wander_rooms: [7, 8, 9] - wander_interval: 12 -``` - -Objects teleport between rooms in their `wander_rooms` list. Players gathering from a -wandering object are silently interrupted when it moves. - ---- - -- cgit v1.2.3