diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Params.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Params.kt
index d899e60979..c27b9b4aad 100644
--- a/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Params.kt
+++ b/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Params.kt
@@ -137,6 +137,10 @@ object Params {
const val REGION = 5136
const val REQ_COMBAT = 5137
const val REQ_ITEMS = 5138
+
+ /** Item ids a quest expects the player to bring; used by the questprep command. */
+ const val REQ_ITEM_IDS = 5267
+
const val REQ_QUEST_POINTS = 5139
const val REQ_QUESTS = 5140
const val REQ_SKILLS = 5141
@@ -472,6 +476,7 @@ object Params {
"regen_rate_ticks" -> REGEN_RATE_TICKS
"region" -> REGION
"req_combat" -> REQ_COMBAT
+ "req_item_ids" -> REQ_ITEM_IDS
"req_items" -> REQ_ITEMS
"req_quest_points" -> REQ_QUEST_POINTS
"req_quests" -> REQ_QUESTS
diff --git a/data/area/morytania/braindeath_island/braindeath_island.anims.toml b/data/area/morytania/braindeath_island/braindeath_island.anims.toml
index 2a619f7fdb..cce4e03f7c 100644
--- a/data/area/morytania/braindeath_island/braindeath_island.anims.toml
+++ b/data/area/morytania/braindeath_island/braindeath_island.anims.toml
@@ -14,4 +14,28 @@ id = 5653
id = 5651
[zombie_pirate_death]
-id = 5654
\ No newline at end of file
+id = 5654
+
+[deal_press_press]
+id = 2808
+
+[deal_smite_control]
+id = 2810
+
+[deal_wrench_flourish]
+id = 2811
+
+[deal_bowl_fish]
+id = 2813
+
+[spirit_attack]
+id = 2804
+
+[spirit_death]
+id = 2805
+
+[spirit_parry]
+id = 2806
+
+[swab_panic]
+id = 9560
diff --git a/data/area/morytania/braindeath_island/braindeath_island.combat.toml b/data/area/morytania/braindeath_island/braindeath_island.combat.toml
index 37b7449706..9649013d51 100644
--- a/data/area/morytania/braindeath_island/braindeath_island.combat.toml
+++ b/data/area/morytania/braindeath_island/braindeath_island.combat.toml
@@ -30,3 +30,15 @@ range = 1
anim = "zombie_pirate_attack"
target_sound = "zombie_pirate_attack"
target_hit = { offense = "crush", max = 60 }
+
+[evil_spirit]
+defend_anim = "spirit_parry"
+defend_sound = "deal_spirit_attack"
+death_anim = "spirit_death"
+death_sound = "rumdeal_spirit_depart"
+
+[evil_spirit.melee]
+range = 1
+anim = "spirit_attack"
+target_sound = "deal_spirit_hit"
+target_hit = { offense = "slash", max = 280 }
diff --git a/data/area/morytania/braindeath_island/braindeath_island.npcs.toml b/data/area/morytania/braindeath_island/braindeath_island.npcs.toml
index cd7f78a027..2327b955d2 100644
--- a/data/area/morytania/braindeath_island/braindeath_island.npcs.toml
+++ b/data/area/morytania/braindeath_island/braindeath_island.npcs.toml
@@ -7,6 +7,7 @@ examine = "A shabby-looking leader."
[50_luke]
id = 2828
+dialogue = "old"
[davey]
id = 2829
@@ -14,33 +15,41 @@ examine = "Looks like he's taking a break."
[captain_donnie]
id = 2830
+dialogue = "old"
[zombie_protester]
id = 2831
+dialogue = "old"
hunt_mode = "spotted"
[zombie_protester_2]
id = 2832
+dialogue = "old"
hunt_mode = "spotted"
[zombie_protester_3]
id = 2833
+dialogue = "old"
hunt_mode = "spotted"
[zombie_protester_4]
id = 2834
+dialogue = "old"
hunt_mode = "spotted"
[zombie_protester_5]
id = 2835
+dialogue = "old"
hunt_mode = "spotted"
[zombie_protester_6]
id = 2836
+dialogue = "old"
hunt_mode = "spotted"
[zombie_pirate]
id = 2837
+dialogue = "old"
hitpoints = 520
att = 49
str = 50
@@ -75,6 +84,7 @@ clone = "zombie_pirate"
[zombie_swab]
id = 2843
+dialogue = "old"
hitpoints = 500
att = 45
str = 50
@@ -85,32 +95,32 @@ categories = ["zombies"]
respawn_delay = 37
drop_table = "pirate"
examine = "He talks a good fight."
-hunt_mode = "cowardly"
+hunt_mode = "zombie_swab"
[zombie_swab_2]
id = 2844
clone = "zombie_swab"
-hunt_mode = "cowardly"
+hunt_mode = "zombie_swab"
[zombie_swab_3]
id = 2845
clone = "zombie_swab"
-hunt_mode = "cowardly"
+hunt_mode = "zombie_swab"
[zombie_swab_4]
id = 2846
clone = "zombie_swab"
-hunt_mode = "cowardly"
+hunt_mode = "zombie_swab"
[zombie_swab_5]
id = 2847
clone = "zombie_swab"
-hunt_mode = "cowardly"
+hunt_mode = "zombie_swab"
[zombie_swab_6]
id = 2848
clone = "zombie_swab"
-hunt_mode = "cowardly"
+hunt_mode = "zombie_swab"
[fever_spider]
id = 2850
diff --git a/data/area/morytania/braindeath_island/braindeath_island.objs.toml b/data/area/morytania/braindeath_island/braindeath_island.objs.toml
index b8b6063350..7f9e91bea3 100644
--- a/data/area/morytania/braindeath_island/braindeath_island.objs.toml
+++ b/data/area/morytania/braindeath_island/braindeath_island.objs.toml
@@ -12,4 +12,43 @@ examine = "It leads up."
[braindeath_island_ladder_down]
id = 10168
+
+[braindeath_island_stagnant_lake]
+id = 10105
+
+[braindeath_island_brewing_controls_multi]
+id = 10104
+
+[braindeath_island_brewing_controls]
+id = 10142
+
+[braindeath_island_brewing_controls_possessed]
+id = 10143
+
+[braindeath_island_brewing_controls_working]
+id = 10144
+
+[braindeath_island_output_tap]
+id = 10148
+
+[braindeath_island_tool_cupboard]
+id = 10162
+
+[braindeath_island_tool_cupboard_open]
+id = 10163
+
+[braindeath_island_pressure_lever_multi]
+id = 10164
+
+[braindeath_island_pressure_lever_up]
+id = 10165
+
+[braindeath_island_pressure_lever_down]
+id = 10166
+
+[braindeath_island_intake_hopper]
+id = 10170
+
+[braindeath_island_pressure_barrel]
+id = 10171
examine = "I can climb down this."
diff --git a/data/area/morytania/braindeath_island/braindeath_island.sounds.toml b/data/area/morytania/braindeath_island/braindeath_island.sounds.toml
index 0ea7fe50cc..ffd8e24958 100644
--- a/data/area/morytania/braindeath_island/braindeath_island.sounds.toml
+++ b/data/area/morytania/braindeath_island/braindeath_island.sounds.toml
@@ -6,3 +6,32 @@ id = 924
[zombie_pirate_death]
id = 925
+
+
+[rumdeal_hopper_1]
+id = 1762
+
+[rumdeal_pressurize]
+id = 1763
+
+[rumdeal_stuff_other]
+id = 1764
+
+[rumdeal_stuff_slugling]
+id = 1765
+
+[wrench_crush]
+id = 1766
+
+# Not present in the 474 synth list; the reference plays it at the fishing spot.
+[rumdeal_bowl_fish]
+id = 2600
+
+[deal_spirit_attack]
+id = 1756
+
+[deal_spirit_hit]
+id = 1757
+
+[rumdeal_spirit_depart]
+id = 1755
diff --git a/data/entity/npc/hunt_modes.toml b/data/entity/npc/hunt_modes.toml
index 2e5d655560..a313141cac 100644
--- a/data/entity/npc/hunt_modes.toml
+++ b/data/entity/npc/hunt_modes.toml
@@ -44,6 +44,15 @@ check_not_combat_self = true
check_not_too_strong = true
check_not_busy = true
+# Rum Deal zombie swabs; they lose interest once the quest is over, filtered in ZombieSwab
+[zombie_swab]
+type = "player"
+check_visual = "line_of_sight"
+check_not_combat = true
+check_not_combat_self = true
+check_not_too_strong = true
+check_not_busy = true
+
# Attacking tzhaar crafters
[tzhaar_hunter]
type = "player"
diff --git a/data/entity/player/player.sounds.toml b/data/entity/player/player.sounds.toml
index 241885bf0f..9f3c98844d 100644
--- a/data/entity/player/player.sounds.toml
+++ b/data/entity/player/player.sounds.toml
@@ -39,3 +39,6 @@ id = 761
[unarmed_kick]
id = 2565
+
+[pour_tea]
+id = 2134
diff --git a/data/quest/members/rum_deal/rum_deal.gfx.toml b/data/quest/members/rum_deal/rum_deal.gfx.toml
index 5f46016a96..1a1485e6f9 100644
--- a/data/quest/members/rum_deal/rum_deal.gfx.toml
+++ b/data/quest/members/rum_deal/rum_deal.gfx.toml
@@ -1,2 +1,5 @@
[karamthulhu]
id = 627
+
+[deal_spotanim]
+id = 465
diff --git a/data/quest/members/rum_deal/rum_deal.ifaces.toml b/data/quest/members/rum_deal/rum_deal.ifaces.toml
new file mode 100644
index 0000000000..5695b9c58c
--- /dev/null
+++ b/data/quest/members/rum_deal/rum_deal.ifaces.toml
@@ -0,0 +1,7 @@
+[rum_deal_censor]
+id = 294
+type = "main_screen"
+
+[rum_deal_title]
+id = 295
+type = "main_screen"
diff --git a/data/quest/members/rum_deal/rum_deal.items.toml b/data/quest/members/rum_deal/rum_deal.items.toml
index c501425214..aecd87d583 100644
--- a/data/quest/members/rum_deal/rum_deal.items.toml
+++ b/data/quest/members/rum_deal/rum_deal.items.toml
@@ -2,6 +2,10 @@
[blindweed_seed]
id = 6710
tradeable = false
+farming_level = 40
+farming_crop = "blindweed"
+farming_xp = 32.0
+farming_patch = "blindweed"
examine = "A Blindweed seed - plant in a Blindweed patch."
kept = "Wilderness"
@@ -9,6 +13,7 @@ kept = "Wilderness"
id = 6711
tradeable = false
weight = 0.007
+farming_xp = 45.0
examine = "An inedible, foul smelling herb."
kept = "Wilderness"
diff --git a/data/quest/members/rum_deal/rum_deal.jingles.toml b/data/quest/members/rum_deal/rum_deal.jingles.toml
new file mode 100644
index 0000000000..2c4b9b3468
--- /dev/null
+++ b/data/quest/members/rum_deal/rum_deal.jingles.toml
@@ -0,0 +1,5 @@
+[zombie_pirates]
+id = 209
+
+[captain_braindeath]
+id = 79
diff --git a/data/quest/members/rum_deal/rum_deal.npcs.toml b/data/quest/members/rum_deal/rum_deal.npcs.toml
index 0ac7608cbd..b4e220e67b 100644
--- a/data/quest/members/rum_deal/rum_deal.npcs.toml
+++ b/data/quest/members/rum_deal/rum_deal.npcs.toml
@@ -4,7 +4,7 @@ hitpoints = 900
att = 170
str = 146
def = 100
-style = "crush"
+style = "slash"
max_hit_melee = 280
hunt_mode = "cowardly"
drop_table = "ashes"
diff --git a/data/quest/members/rum_deal/rum_deal.varbits.toml b/data/quest/members/rum_deal/rum_deal.varbits.toml
new file mode 100644
index 0000000000..c3a1973fd4
--- /dev/null
+++ b/data/quest/members/rum_deal/rum_deal.varbits.toml
@@ -0,0 +1,47 @@
+[rum_deal_pressure_count]
+id = 1354
+persist = true
+
+[rum_deal_brewing_control]
+id = 1355
+persist = true
+
+[rum_deal_squid_spot]
+id = 1356
+persist = true
+
+[rum_deal_slugling_count]
+id = 1357
+persist = true
+
+[rum_deal_karamthulhu_count]
+id = 1358
+persist = true
+
+[rum_deal_swab_a]
+id = 1359
+persist = true
+
+[rum_deal_swab_b]
+id = 1360
+persist = true
+
+[rum_deal_swab_c]
+id = 1361
+persist = true
+
+[rum_deal_swab_d]
+id = 1362
+persist = true
+
+[rum_deal_swab_e]
+id = 1363
+persist = true
+
+[rum_deal_swab_f]
+id = 1364
+persist = true
+
+[rum_deal_diversion]
+id = 1365
+persist = true
diff --git a/data/quest/members/rum_deal/rum_deal.varps.toml b/data/quest/members/rum_deal/rum_deal.varps.toml
index b6be5efc10..1dbb27a5f6 100644
--- a/data/quest/members/rum_deal/rum_deal.varps.toml
+++ b/data/quest/members/rum_deal/rum_deal.varps.toml
@@ -3,4 +3,4 @@ id = 600
persist = true
format = "map"
default = "unstarted"
-values = { unstarted = 0, started = 1 }
+values = { unstarted = 0, agreed_to_help = 1, slay_barrelor = 2, met_braindeath = 3, given_seeds = 4, grown_blindweed = 5, show_blindweed = 6, blindweed_added = 7, fetch_water = 8, collected_water = 9, water_added = 10, catch_creatures = 11, pressurised = 12, bless_wrench = 13, spirit_banished = 14, kill_spider = 15, spider_added = 16, collect_swill = 17, told_donnie = 18, completed = 19 }
diff --git a/data/quest/quests.toml b/data/quest/quests.toml
index c802a88f6c..026c80bf55 100644
--- a/data/quest/quests.toml
+++ b/data/quest/quests.toml
@@ -3216,7 +3216,25 @@ req_skills = {
Crafting = 42,
Slayer = 42
}
+req_quests = ["zogre_flesh_eaters"]
req_items = "Combat equipment and food, Slayer gloves."
+# Everything else is handed out by Captain Braindeath or the basement tool cupboard.
+req_item_ids = ["slayer_gloves", "spade"]
+variables = [
+ "rum_deal",
+ "rum_deal_pressure_count",
+ "rum_deal_brewing_control",
+ "rum_deal_squid_spot",
+ "rum_deal_slugling_count",
+ "rum_deal_karamthulhu_count",
+ "rum_deal_swab_a",
+ "rum_deal_swab_b",
+ "rum_deal_swab_c",
+ "rum_deal_swab_d",
+ "rum_deal_swab_e",
+ "rum_deal_swab_f",
+ "rum_deal_diversion",
+]
req_combat = "You will need to defeat a level 56 melee enemy and a level 70 ranged enemy."
points = 2
reward = "Holy wrench
Access to Braindeath Island
Tangled fishbowl and net"
diff --git a/data/skill/farming/farming.varbits.toml b/data/skill/farming/farming.varbits.toml
index a7ca74b4f8..778fb737d6 100644
--- a/data/skill/farming/farming.varbits.toml
+++ b/data/skill/farming/farming.varbits.toml
@@ -40,6 +40,13 @@ persist = true
default = "weeds_3"
values = { weeds_3 = 0, weeds_2 = 1, weeds_1 = 2, weeds_0 = 3, belladonna_0 = 4, belladonna_1 = 5, belladonna_2 = 6, belladonna_3 = 7, belladonna_life1 = 8, belladonna_diseased_1 = 9, belladonna_diseased_2 = 10, belladonna_diseased_3 = 11, belladonna_dead_1 = 12, belladonna_dead_2 = 13, belladonna_dead_3 = 14 }
+[farming_blindweed_patch_braindeath_island]
+id = 1366
+format = "map"
+persist = true
+default = "weeds_3"
+values = { weeds_3 = 0, weeds_2 = 1, weeds_1 = 2, weeds_0 = 3, blindweed_0 = 4, blindweed_life1 = 5 }
+
[farming_evil_turnip_patch_draynor]
id = 4291
format = "map"
diff --git a/data/skill/farming/farming_produce.toml b/data/skill/farming/farming_produce.toml
index df0ec0e27f..0cad2a567e 100644
--- a/data/skill/farming/farming_produce.toml
+++ b/data/skill/farming/farming_produce.toml
@@ -192,3 +192,6 @@ disease_chance = 30
[potato]
disease_chance = 30
+
+[blindweed]
+disease_chance = 0
diff --git a/data/skill/farming/patch/blindweed_patch.objs.toml b/data/skill/farming/patch/blindweed_patch.objs.toml
new file mode 100644
index 0000000000..c63891f3f1
--- /dev/null
+++ b/data/skill/farming/patch/blindweed_patch.objs.toml
@@ -0,0 +1,31 @@
+[blindweed_patch_weeds_3]
+id = 10097
+examine = "Weedy."
+
+[blindweed_patch_weeds_2]
+id = 10098
+examine = "Less weedy."
+
+[blindweed_patch_weeds_1]
+id = 10099
+examine = "Almost clear."
+
+[blindweed_patch_weeded]
+id = 10100
+examine = "Free from weeds."
+
+[blindweed_seedling]
+id = 10101
+examine = "Something is growing here."
+
+[blindweed_fullygrown]
+id = 10102
+harvest = "blindweed"
+examine = "Fully grown Blindweed."
+
+[blindweed_patch_trashed]
+id = 10103
+examine = "Weedy, wrecked and infertile."
+
+[farming_blindweed_patch_braindeath_island]
+id = 10096
diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/EncodeExtensions.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/EncodeExtensions.kt
index 9b1c282650..b7fd5a63c0 100644
--- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/EncodeExtensions.kt
+++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/EncodeExtensions.kt
@@ -6,6 +6,7 @@ import world.gregs.voidps.engine.data.definition.FontDefinitions
import world.gregs.voidps.engine.entity.character.Character
import world.gregs.voidps.engine.entity.character.npc.NPC
import world.gregs.voidps.engine.entity.character.player.Player
+import world.gregs.voidps.engine.entity.character.player.Players
import world.gregs.voidps.engine.entity.character.player.chat.ChatType
import world.gregs.voidps.engine.get
import world.gregs.voidps.network.login.protocol.encode.*
@@ -214,52 +215,81 @@ fun Player.clearMinimap() = client?.sendMinimapState(0) ?: Unit
/**
* Add an [arrow] hint to a [tile] with [radius]
*/
-fun Player.hint(tile: Tile, radius: Int = 1, arrow: Int = HintArrow.FILLED, direction: Direction = Direction.NONE, height: Int = 0): Int {
- val viewport = viewport ?: return -1
- val index = viewport.hints.firstOrNull { it == 0 } ?: return -1
+fun Player.markHint(tile: Tile, radius: Int = 1, arrow: Int = HintArrow.FILLED, direction: Direction = Direction.NONE, height: Int = 0) {
+ val viewport = viewport ?: return
val type = when (direction) {
- Direction.WEST -> 3
- Direction.EAST -> 4
- Direction.SOUTH -> 5
- Direction.NORTH -> 6
- else -> 2
+ Direction.WEST -> HintType.WEST
+ Direction.EAST -> HintType.EAST
+ Direction.SOUTH -> HintType.SOUTH
+ Direction.NORTH -> HintType.NORTH
+ else -> HintType.TILE
}
+ val index = hintIndex(type) ?: return
viewport.hints[index] = type
client?.arrowHint(type, index, sprite = arrow, x = tile.x, y = tile.y, level = tile.level, z = height, radius = radius)
- return index
+ return
}
/**
- * Add an [arrow] hint over [character]
+ * Clears arrow hints for those who have this character hinted
*/
-fun Player.hint(character: Character, arrow: Int = HintArrow.FILLED): Int {
- val viewport = viewport ?: return -1
- val index = viewport.hints.firstOrNull { it == 0 } ?: return -1
- val type = when (character) {
- is Player -> 10
- is NPC -> 1
- else -> return -1
+fun Character.clearHinted() {
+ val hints: Set = get("hinted_players") ?: return
+ val type = if (this is NPC) HintType.NPC else HintType.PLAYER
+ for (accountName in hints) {
+ val player = Players.findByAccount(accountName) ?: continue
+ player.clearHint(type, index)
}
- viewport.hints[index] = type
+ clear("hinted_players")
+}
+
+/**
+ * Add an [arrow] hint over [character]
+ */
+fun Player.markHint(character: Character, arrow: Int = HintArrow.FILLED) {
+ val viewport = viewport ?: return
+ val type = if (character is NPC) HintType.NPC else HintType.PLAYER
+ val hints = character.getOrPut("hinted_players") { mutableSetOf() }
+ hints.add(accountName)
+ val index = hintIndex(type) ?: return
+ viewport.hints[index] = character.index
client?.arrowHint(type, index, sprite = arrow, entityIndex = character.index)
- return index
+}
+
+fun Player.clearHint(type: Int, entityIndex: Int) {
+ val viewport = viewport ?: return
+ val index = hintIndex(type) ?: return
+ if (viewport.hints[index] == entityIndex) {
+ clearHint(type)
+ }
}
/**
- * Clear the hint at [index] (or all if -1)
+ * Clear the [type] hint
*/
-fun Player.clearHint(index: Int = -1) {
+fun Player.clearHint(type: Int) {
+ val viewport = viewport ?: return
+ val index = hintIndex(type) ?: return
+ if (viewport.hints[index] == 0) {
+ return
+ }
+ client?.arrowHint(HintType.CLEAR, index)
+ viewport.hints[index] = 0
+}
+
+private fun hintIndex(type: Int) = when (type) {
+ HintType.CLEAR -> null
+ HintType.PLAYER -> 7
+ else -> type - 1
+}
+
+fun Player.clearHints() {
val viewport = viewport ?: return
- if (index == -1) {
- for (i in viewport.hints.indices) {
- if (viewport.hints[i] == 0) {
- continue
- }
- client?.arrowHint(0, i)
- viewport.hints[i] = 0
+ for (i in viewport.hints.indices) {
+ if (viewport.hints[i] == 0) {
+ continue
}
- } else {
- client?.arrowHint(0, index)
- viewport.hints[index] = 0
+ client?.arrowHint(HintType.CLEAR, i)
+ viewport.hints[i] = 0
}
}
\ No newline at end of file
diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/Leash.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/Leash.kt
new file mode 100644
index 0000000000..e22c39bb09
--- /dev/null
+++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/Leash.kt
@@ -0,0 +1,104 @@
+package world.gregs.voidps.engine.entity.character.mode
+
+import world.gregs.voidps.engine.entity.character.Character
+import world.gregs.voidps.engine.entity.character.mode.move.Movement
+import world.gregs.voidps.engine.entity.character.mode.move.Step
+import world.gregs.voidps.engine.entity.character.mode.move.target.FollowTargetStrategy
+import world.gregs.voidps.engine.entity.character.mode.move.target.TargetStrategy
+import world.gregs.voidps.engine.entity.character.npc.NPC
+import world.gregs.voidps.engine.entity.distanceTo
+import world.gregs.voidps.engine.map.Overlap
+import world.gregs.voidps.type.Direction
+import world.gregs.voidps.type.Tile
+import world.gregs.voidps.type.random
+
+/**
+ * Follows [target] on a leash; the npc trails them for as long as both stay within [range] of
+ * [spawn] and loses interest the moment either doesn't.
+ *
+ * Unlike [Follow] - which is written for pets and teleports an npc that falls more than 15 tiles
+ * behind or ends up on another level - a leashed npc never teleports. It can only ever be [range]
+ * tiles from [spawn], and a target that outruns, teleports or climbs away from it is simply
+ * forgotten.
+ */
+class Leash(
+ private val npc: NPC,
+ val target: Character,
+ private val range: Int,
+ private val spawn: Tile = npc["spawn_tile"]!!,
+ private val strategy: TargetStrategy = FollowTargetStrategy(target),
+) : Movement(npc, strategy) {
+
+ override fun start() {
+ npc.watch(target)
+ }
+
+ override fun tick() {
+ // within also requires matching levels, so climbing away loses interest too
+ if (!npc.tile.within(spawn, range) || !target.tile.within(spawn, range)) {
+ npc.mode = EmptyMode
+ return
+ }
+ if (!npc.watching(target)) {
+ npc.watch(target)
+ }
+ npc.walkTrigger()
+ // An npc already beside its target just faces it rather than shuffling onto the follow
+ // tile. A target standing on top of the npc falls through so [stepOut] can move it to a
+ // free adjacent tile.
+ if (!underTarget() && target.tile.distanceTo(npc) <= 1) {
+ npc.steps.clearDestination()
+ return
+ }
+ // Npcs single-step rather than pathfind, so the destination is recalculated every tick
+ npc.steps.clearDestination()
+ super.tick()
+ }
+
+ override fun recalculate(): Boolean {
+ if (target.tile.distanceTo(npc) <= 1) {
+ return false
+ }
+ if (equals(strategy.tile, npc.steps.destination)) {
+ return false
+ }
+ npc.steps.queueStep(strategy.tile, noRun = strategy.forceWalk(npc))
+ return true
+ }
+
+ override fun getTarget(): Step? {
+ val step = npc.steps.peek()
+ if (step == null) {
+ recalculate()
+ return npc.steps.peek()
+ }
+ return super.getTarget()
+ }
+
+ /**
+ * The target standing on top of the npc forces it out to a free adjacent tile, the same way
+ * combat movement steps npcs out from under their opponent.
+ */
+ override fun stepOut(): Boolean {
+ if (npc.def["allowed_under", false] || !underTarget()) {
+ return false
+ }
+ clearSteps()
+ for (direction in Direction.cardinal.shuffled(random)) {
+ if (canStep(direction.delta.x, direction.delta.y)) {
+ npc.steps.queueStep(npc.tile.add(direction), noRun = strategy.forceWalk(npc))
+ break
+ }
+ }
+ return true
+ }
+
+ private fun underTarget(): Boolean = Overlap.isUnder(npc.tile, npc.size, npc.size, target.tile, target.size, target.size)
+
+ override fun onCompletion() {
+ }
+
+ override fun stop(replacement: Mode) {
+ npc.clearWatch()
+ }
+}
diff --git a/game/src/main/kotlin/content/area/misthalin/lumbridge/swamp/chams_of_tears/Juna.kt b/game/src/main/kotlin/content/area/misthalin/lumbridge/swamp/chams_of_tears/Juna.kt
index 652feee85e..297671a42e 100644
--- a/game/src/main/kotlin/content/area/misthalin/lumbridge/swamp/chams_of_tears/Juna.kt
+++ b/game/src/main/kotlin/content/area/misthalin/lumbridge/swamp/chams_of_tears/Juna.kt
@@ -1,22 +1,12 @@
package content.area.misthalin.lumbridge.swamp.chams_of_tears
import content.entity.player.dialogue.*
-import content.entity.player.dialogue.type.ChoiceOption
-import content.entity.player.dialogue.type.choice
-import content.entity.player.dialogue.type.npc
-import content.entity.player.dialogue.type.player
-import content.entity.player.dialogue.type.statement
+import content.entity.player.dialogue.type.*
import content.entity.player.modal.Tab
import content.entity.player.modal.tab
-import content.quest.closeTabs
-import content.quest.openTabs
-import content.quest.quest
-import content.quest.questComplete
-import content.quest.questCompleted
-import content.quest.refreshQuestJournal
+import content.quest.*
import world.gregs.voidps.engine.Script
import world.gregs.voidps.engine.client.clearCamera
-import world.gregs.voidps.engine.client.command.adminCommand
import world.gregs.voidps.engine.client.message
import world.gregs.voidps.engine.client.moveCamera
import world.gregs.voidps.engine.client.turnCamera
@@ -58,12 +48,6 @@ import kotlin.math.floor
class Juna : Script {
init {
- adminCommand("test") {
- clearCamera()
- moveCamera(Tile(3233, 9505, 2), height = 700, speed = 5, acceleration = 5)
- turnCamera(Tile(3227, 9493, 2), height = 200, speed = 5, acceleration = 5)
- }
-
objectOperate("Talk-to", "juna_tears,juna") { (target) ->
talkWith(junaNpc())
if (equipped(EquipSlot.Weapon).id == "stone_bowl") {
diff --git a/game/src/main/kotlin/content/area/morytania/braindeath_island/Brewer.kt b/game/src/main/kotlin/content/area/morytania/braindeath_island/Brewer.kt
index b71c3e7505..1bc7418d4f 100644
--- a/game/src/main/kotlin/content/area/morytania/braindeath_island/Brewer.kt
+++ b/game/src/main/kotlin/content/area/morytania/braindeath_island/Brewer.kt
@@ -6,107 +6,204 @@ import content.entity.player.dialogue.Happy
import content.entity.player.dialogue.Laugh
import content.entity.player.dialogue.Neutral
import content.entity.player.dialogue.Quiz
+import content.entity.player.dialogue.Sad
import content.entity.player.dialogue.Scared
import content.entity.player.dialogue.Shifty
import content.entity.player.dialogue.Shock
import content.entity.player.dialogue.type.npc
import content.entity.player.dialogue.type.player
-import content.quest.questCompleted
+import content.quest.questStage
import world.gregs.voidps.engine.Script
+import world.gregs.voidps.engine.entity.character.npc.NPC
+import world.gregs.voidps.engine.entity.character.player.Player
+import world.gregs.voidps.engine.inv.inventory
+/**
+ * Brewers on Braindeath Island. Eight unique NPCs all share this dialogue
+ * handler with personality-specific responses at each branch point.
+ *
+ * NPC IDs (Java) -> roles:
+ * 2851 - "one of THEM" / paranoid
+ * 2852 - "Game over, man!" / panicked
+ * 2853 - calm but secretly crying
+ * 2854 - angry war veteran
+ * 2855 - opportunistic traitor / Hungry Frank
+ * 2856 - sleep-deprived / drunk
+ * 2857 - paranoid about disguised zombies
+ * 2858 - resigned to dying
+ */
class Brewer : Script {
init {
- npcOperate("Talk-to", "brewer") {
- if (!questCompleted("rum_deal")) {
- return@npcOperate
+ npcOperate("Talk-to", "brewer*") { (target) ->
+ val brewer = target.number()
+ when (questStage("rum_deal")) {
+ 13 -> wrenchSearch(brewer)
+ 19 -> postQuestGreeting(brewer)
+ else -> initialGreeting(brewer)
}
- player("Hello there!")
- npc("I don't know what your game is, but I know you're one of THEM!")
- player("But I just saved you!")
- npc("The voices tell me different. It's all part of a plot! Confess!")
}
+ }
- npcOperate("Talk-to", "brewer_2") {
- if (!questCompleted("rum_deal")) {
- return@npcOperate
- }
- player("Hello there!")
- npc("Have they gone yet?")
- player("Well, no, but they are a lot calmer now.")
- npc("What are we gonna do now, huh? What are we gunna do now?")
- player("In your case I would say relax.")
- }
+ // ===== Default greeting (during the quest, before the wrench step) =====
- npcOperate("Talk-to", "brewer_3") {
- if (!questCompleted("rum_deal")) {
- return@npcOperate
+ private suspend fun Player.initialGreeting(brewer: Int) {
+ player("So... how are you holding up?")
+ when (brewer) {
+ 1 -> {
+ npc("Who are you?")
+ npc("You're one of THEM arent you!")
+ player("O...kay...walking away now, smiling and not making eye contact.")
}
- player("Hello there!")
- npc("Hello yourself!")
- player("How's things?")
- npc("Fine...")
- player("Excellent! Since I get the feeling I don't want to know why you said that so oddly I'll just go over here!")
- npc("I think that would be for the best!")
- }
-
- npcOperate("Talk-to", "brewer_4") {
- if (!questCompleted("rum_deal")) {
- return@npcOperate
+ 2 -> {
+ npc("Game over, man! Game over!")
+ player("Errr...keep up the good work.")
}
- player("Hello there!")
- npc("Hello yerself Landlubber!")
- player("Everything ok with you now?")
- npc("Hmmm...Overall everything is good!")
- player("Great!")
- }
-
- npcOperate("Talk-to", "brewer_5") {
- if (!questCompleted("rum_deal")) {
- return@npcOperate
+ 3 -> {
+ npc("Very well, considering.")
+ player("Considering what?")
+ npc("That I've been crawling into the corner to cry whenever nobody is looking.")
+ player("Wow... I always thought pirates were tough...")
+ }
+ 4 -> {
+ npc("Don't sneak up on me like that!")
+ npc("Don't you know there's a war on?")
+ player("I didn't think this counted as a war.")
+ npc("That's because you're a landlubber!")
+ npc("I bet ye've never had to beat a dozen zombies to death with a blunt toothpick!")
+ player("That's right. Have you?")
+ npc("No, I've spent most of my time in the toilet to be honest.")
+ }
+ 5 -> npc("Don't talk to me! If I play this right, I can sell you lot out and make it to the mainland!")
+ 6 -> {
+ npc("No school today mother, my brain hurts real baaaad...")
+ player("What?")
+ npc("I no sleep in many, many, many days.")
+ }
+ 7 -> {
+ npc("How do I know you're not one of them?")
+ player("Well, I don't know... the way I'm breathing could give it away.")
+ npc("That means nothing! NOTHING!")
+ }
+ 8 -> {
+ npc("Very well.")
+ player("You sure?")
+ npc("Oh Guthix, who am I kidding?")
+ npc("We're all gonna die!")
}
- player("Hello there!")
- npc("Hello there, brave hero, in whom I had total confidence!")
- player("Total confidence?")
- npc("Yes! I was so confident that I would never, ever have sold your soft, edible body to the pirates outside!")
- player("Well, great...")
- npc("On a completely unrelated note, I would steer clear of Hungry Frank for a while.")
- npc("He's a filthy liar. And a forger. It wouldn't surprise me if he has written out a note detailing the terms of our surrender and your dismemberment and cooking in MY handwriting.")
- npc("Imagine that, the fiend.")
}
+ }
- npcOperate("Talk-to", "brewer_6") {
- if (!questCompleted("rum_deal")) {
- return@npcOperate
+ // ===== Progress 13: Looking for a priest and a wrench =====
+
+ private suspend fun Player.wrenchSearch(brewer: Int) {
+ if (inventory.contains("holy_wrench")) {
+ // Show off the blessed wrench
+ player("Bask in the glory of my Holy Wrench!")
+ when (brewer) {
+ 1 -> npc("Oooh...It's so shiny...")
+ 2 -> npc("(Sob) It's so beautiful...")
+ 3 -> npc("I am not worthy to gaze upon it!")
+ 4 -> npc("I can feel the aura of Wrenchly Power radiating from it!")
+ 5 -> npc("Take it away, I am not worthy to be in its Wrenchly presence.")
+ 6 -> npc("Wow...I don't feel tired any more...")
+ 7 -> npc("Such power!")
+ 8 -> npc("We're saved!")
+ }
+ } else {
+ // Asking around for help
+ player("I'm looking for a priest. And a wrench.")
+ when (brewer) {
+ 1 -> npc("What possible plan could involve a priest and a wrench?")
+ 2 -> {
+ npc("What? Are you gonna build some strange mechanical priest to save us?")
+ npc("What if it runs riot? We'll all be killed!")
+ }
+ 3 -> npc("Well good luck. If you find any spare trousers please pass them my way.")
+ 4 -> npc("Well the usual combo is an old priest and a young priest, but whatever floats your boat lad.")
+ 5 -> npc("What are you going to do to that priest? No, wait, I don't want to know.")
+ 6 -> {
+ npc("Try Davey, he might have seen it.")
+ player("Thanks!")
+ npc("His giant, green, inflatable dwarf might be able to tell you where one is as well.")
+ }
+ 7 -> npc("If this is some sort of secret zombie code then I don't get it.")
+ 8 -> {
+ npc("Well I can't help you there.")
+ player("Oh, sorry to have bothered you.")
+ npc("No problem.")
+ npc("It's not like I'm going to live through this or anything.")
+ npc("What if they get me while I sleep?")
+ npc("I don't wanna die!")
+ }
}
- player("So...got any sleep yet?")
- npc("My brain is no longer capable of sleep.")
- player("So...what are you going to do now?")
- npc("I was gonna try and will myself dead.")
- player("Right...good luck with that.")
}
+ }
- npcOperate("Talk-to", "brewer_7") {
- if (!questCompleted("rum_deal")) {
- return@npcOperate
- }
+ // ===== Progress 19: Post-quest greeting =====
+
+ private suspend fun Player.postQuestGreeting(brewer: Int) {
+ // The sleep-deprived brewer gets a different opener
+ if (brewer == 6) {
+ player("So...got any sleep yet?")
+ } else {
player("Hello there!")
- npc("Well you proved that you're probably not a zombie.")
- npc("So what are you then? A ghoul? A vampyre?")
- player("I'm not any form of undead!")
- npc("Oh...oh god I'm sorry, I didn't realise it was natural...")
- player("What?")
- npc("Nothing...")
}
-
- npcOperate("Talk-to", "brewer_8") {
- if (!questCompleted("rum_deal")) {
- return@npcOperate
+ when (brewer) {
+ 1 -> {
+ npc("I don't know what your game is, but I know you're one of THEM!")
+ player("But I just saved you!")
+ npc("The voices tell me different. It's all part of a plot! Confess!")
+ }
+ 2 -> {
+ npc("Have they gone yet?")
+ player("Well, no, but they are a lot calmer now.")
+ npc("What are we gonna do now, huh? What are we gunna do now?")
+ player("In your case I would say relax.")
+ }
+ 3 -> {
+ npc("Hello yourself!")
+ player("How's things?")
+ npc("Fine...")
+ player("Excellent! Since I get the feeling I don't want to know why you said that so oddly I'll just go over here!")
+ npc("I think that would be for the best!")
+ }
+ 4 -> {
+ npc("Hello yerself Landlubber!")
+ player("Everything ok with you now?")
+ npc("Hmmm...Overall everything is good!")
+ player("Great!")
+ }
+ 5 -> {
+ npc("Hello there, brave hero, in whom I had total confidence!")
+ player("Total confidence?")
+ npc("Yes! I was so confident that I would never, ever have sold your soft, edible body to the pirates outside!")
+ player("Well, great...")
+ npc("On a completely unrelated note, I would steer clear of Hungry Frank for a while.")
+ npc("He's a filthy liar. And a forger. It wouldn't surprise me if he has written out a note detailing the terms of our surrender and your dismemberment and cooking in MY handwriting.")
+ npc("Imagine that, the fiend.")
+ }
+ 6 -> {
+ npc("My brain is no longer capable of sleep.")
+ player("So...what are you going to do now?")
+ npc("I was gonna try and will myself dead.")
+ player("Right...good luck with that.")
+ }
+ 7 -> {
+ npc("Well you proved that you're probably not a zombie.")
+ npc("So what are you then? A ghoul? A vampire?")
+ player("I'm not any form of undead!")
+ player("What?")
+ npc("Nothing...")
+ }
+ 8 -> {
+ npc("You saved us! Huzzah!")
+ player("All in a day's work, think nothing of it.")
+ npc("I don't have anything to reward you with except my collection of bleak, gothic poetry I wrote when I assumed we were all done for. Do you want it?")
+ player("I may come for it later, you hang on to it for now.")
}
- player("Hello there!")
- npc("You saved us! Huzzah!")
- player("All in a day's work, think nothing of it.")
- npc("I don't have anything to reward you with except my collection of bleak, gothic poetry I wrote when I assumed we were all done for. Do you want it?")
- player("I may come for it later, you hang on to it for now.")
}
}
+
+ /** Brewers are named `brewer` then `brewer_2` through `brewer_8`. */
+ private fun NPC.number(): Int = id.substringAfterLast('_').toIntOrNull() ?: 1
}
diff --git a/game/src/main/kotlin/content/area/morytania/braindeath_island/CaptainBraindeath.kt b/game/src/main/kotlin/content/area/morytania/braindeath_island/CaptainBraindeath.kt
new file mode 100644
index 0000000000..3a80fb095e
--- /dev/null
+++ b/game/src/main/kotlin/content/area/morytania/braindeath_island/CaptainBraindeath.kt
@@ -0,0 +1,650 @@
+package content.area.morytania.braindeath_island
+
+import content.entity.effect.clearTransform
+import content.entity.effect.transform
+import content.entity.player.dialogue.Angry
+import content.entity.player.dialogue.Drunk
+import content.entity.player.dialogue.Happy
+import content.entity.player.dialogue.Laugh
+import content.entity.player.dialogue.Neutral
+import content.entity.player.dialogue.Quiz
+import content.entity.player.dialogue.Sad
+import content.entity.player.dialogue.Shifty
+import content.entity.player.dialogue.Shock
+import content.entity.player.dialogue.type.ChoiceOption
+import content.entity.player.dialogue.type.choice
+import content.entity.player.dialogue.type.npc
+import content.entity.player.dialogue.type.player
+import content.entity.player.dialogue.type.statement
+import content.entity.player.inv.item.addOrDrop
+import content.quest.questComplete
+import content.quest.questStage
+import content.quest.refreshQuestJournal
+import world.gregs.voidps.engine.Script
+import world.gregs.voidps.engine.client.clearCamera
+import world.gregs.voidps.engine.client.moveCamera
+import world.gregs.voidps.engine.client.turnCamera
+import world.gregs.voidps.engine.client.ui.close
+import world.gregs.voidps.engine.client.ui.open
+import world.gregs.voidps.engine.entity.character.jingle
+import world.gregs.voidps.engine.entity.character.move.tele
+import world.gregs.voidps.engine.entity.character.player.Player
+import world.gregs.voidps.engine.entity.character.player.equip.equipped
+import world.gregs.voidps.engine.entity.character.player.male
+import world.gregs.voidps.engine.entity.character.player.skill.Skill
+import world.gregs.voidps.engine.entity.character.player.skill.exp.exp
+import world.gregs.voidps.engine.event.AuditLog
+import world.gregs.voidps.engine.inv.add
+import world.gregs.voidps.engine.inv.inventory
+import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot
+import world.gregs.voidps.type.Tile
+
+class CaptainBraindeath : Script {
+ init {
+ npcOperate("Talk-to", "captain_braindeath") { (target) ->
+ when (questStage("rum_deal")) {
+ 2 -> introCutsceneIntro()
+ 3 -> handoutBlindweedSeed()
+ 5 -> deliverBlindweed()
+ 7 -> nextStagnantWater()
+ 10 -> nextSluglings()
+ 12 -> slugShockSnap()
+ 14 -> finalIngredient()
+ 16 -> spiderInHopper()
+ 17 -> spokeToCaptainNo()
+ 18 -> spokeToCaptainYes()
+ 19 -> offerRum()
+ else -> defaultGreeting()
+ }
+ }
+ }
+
+ // ===== Progress 2: Wake-up + intro cutscene =====
+
+ private suspend fun Player.introCutsceneIntro() {
+ npc("Are ye alright, ${ladOrLass()}?")
+ player("Ohhhh... My head...")
+ player("It feels like someone has smacked me one with a bottle...")
+ npc("Arr... Those devils gave ye a nasty knock when ye came to aid us.")
+ npc("But now yer here we'll run those evil brain-eatin' dogs off the island fer good!")
+ player("What? What is going on here?")
+ player("I can't seem to remember anything beyond chatting to a man at the docks.")
+ npc("Arr. Well, ${ladOrLass()}, that would be Pete, one of my men.")
+ npc("He's been out lookin' fer heroes like yerself to aid us in our peril.")
+ npc("When ye arrived ye took a nasty knock to the head, so ye probably don't remember agreein' to help us out. But I swear to ye that ye did.")
+ player("Okay... I'll buy that. It sounds like something I would do.")
+ player("So where am I, and what is going on?")
+ npc("Yer on Braindeath Island!")
+ npc("Where it lies is a secret, because ye are standin' in the brewery of Cap'n Braindeath, purveyor of the most vitriolic alcoholic beverages in the world!")
+ player("Wow!")
+ npc("I am the notorious alchemist Cap'n Braindeath, and this whole operation be my idea!")
+ npc("With my crew of sturdy, upright pirate brewers, we sail the seven seas, distributing cheap 'alcohol' to all and sundry.")
+ npc("Well, fer a price, at any rate.")
+ player("Oooh!")
+ npc("These be dark times, though, ${ladOrLass()}. See, a week ago we awoke to find ourselves beseiged.")
+ npc("The lads and I have held them off so far, but 'tis only a matter of time before they sweep through the buildin' and put us all to the sword.")
+ player("Who?")
+ npc("Them!")
+ statement("The Captain points out of the window...", clickToContinue = false)
+ playProtestCutscene()
+ player("...protesting?")
+ npc("Arr, ${ladOrLass()}! That they are!")
+ npc("Day and night they seek to break our will with their chantin', and their singin' and their passive resistance!")
+ npc("Seems they lost their fightin' spirit after the first few days. Now most of them just protest all the time.")
+ player("So, what do you want me to do?")
+ npc("Well, me and the lads got our heads together and decided that if we can get their Cap'n drunk enough, perhaps they'll stop protestin'.")
+ npc("If that happens, we'll slip out the back and set up shop somewhere else.")
+ player("Well, how can I help?")
+ npc("Well, first of all, we need someone to go out the front and grow us some Blindweed.")
+ npc("'Tis one of the ingredients of our 'rum'.")
+ npc("The only problem is that those rottin' fiends have torn up and destroyed all but one of the Blindweed Patches.")
+ if (hasSpace()) {
+ addOrDrop("blindweed_seed")
+ set("rum_deal", "given_seeds")
+ npc("Here, ${ladOrLass()}. I'll give ye the seed you'll need fer growin' the herb. Help yerself to the gardenin' equipment in the basement.")
+ blindweedWarnings()
+ } else {
+ set("rum_deal", "met_braindeath")
+ npc("Well, I have some Blindweed seeds fer ye. When ye have some free space fer them, come and talk to me.")
+ }
+ }
+
+ private suspend fun Player.blindweedWarnings() {
+ npc("I'll warn ye again that those devils are sat right on top of the patch.")
+ npc("Try hecklin' 'em from a distance. Those Swabs may talk a good fight, but if ye can put a scare in 'em they'll keep out of yer way!")
+ }
+
+ // ===== Progress 3: Came back with space, hand out the seed =====
+
+ private suspend fun Player.handoutBlindweedSeed() {
+ if (hasSpace()) {
+ addOrDrop("blindweed_seed")
+ set("rum_deal", "given_seeds")
+ npc("Finally, ${ladOrLass()}! Here, take this seed and grow us some Blindweed.")
+ npc("Ye can help yerself to the gardenin' equipment in the basement, just be quick about it!")
+ blindweedWarnings()
+ } else {
+ defaultGreeting()
+ }
+ }
+
+ // ===== Progress 5: Player either has Blindweed or lost it =====
+
+ private suspend fun Player.deliverBlindweed() {
+ if (inventory.contains("blindweed")) {
+ player("Here! I have your Blindweed!")
+ npc("Splendid, ${ladOrLass()}! Go shove it in the Intake Hopper upstairs.")
+ set("rum_deal", "show_blindweed")
+ npc("We'll beat them zombies yet! Arr!")
+ } else {
+ player("Captain... I grew some blindweed but then I lost it...")
+ npc("There there, ${ladOrLass()}. 'Tis all right.")
+ if (hasSpace()) {
+ npc("Here, I've been savin' some fer an emergency.")
+ set("rum_deal", "show_blindweed")
+ npc("Go shove that in the Intake Hopper, and try not ter lose this lot!")
+ addOrDrop("blindweed")
+ } else {
+ npc("When ye've quite finished blubbin', come see me and I'll give ye some Blindweed.")
+ npc("Clear a little space from yer pack first though, ${ladOrLass()}, or ye'll be hard pressed to hold it.")
+ }
+ }
+ }
+
+ // ===== Progress 7: Stagnant water step =====
+
+ private suspend fun Player.nextStagnantWater() {
+ player("Well, that takes care of the Blindweed. What now?")
+ npc("Well, now ye've shoved the Blindweed into the mix, what we need is a bucket of stagnant water.")
+ player("Where can I get some of that?")
+ npc("Ye won't have to go far, ${ladOrLass()}, we have a pool of the stuff here!")
+ player("Here in the brewery?")
+ npc("No, ${ladOrLass()}, that would be a strange thing to have in a brewery!")
+ npc("It's up the mountain to the north.")
+ player("Up a mountain?")
+ npc("Well, 'tis technically a volcano, but ye get the general idea.")
+ player("And I assume the place is crawling with these zombies?")
+ npc("No ${ladOrLass()}, there be not a zombie in sight.")
+ player("Oh, good!")
+ npc("All ye have to do is get past the keen-eyed lookout that's been spottin' my men when I send 'em.")
+ npc("I'll tell ye that it won't be easy!")
+ player("When is it ever...")
+ npc("Don't ferget yer bucket!")
+ if (!inventory.contains("bucket") && hasSpace()) {
+ addOrDrop("bucket")
+ }
+ set("rum_deal", "fetch_water")
+ }
+
+ // ===== Progress 10: Sluglings step =====
+
+ private suspend fun Player.nextSluglings() {
+ player("So is that everything?")
+ npc("No, ${ladOrLass()}, next ye'll need to go outside and catch five loads of Sluglings fer the brew.")
+ player("What? Sluglings? That's disgusting!")
+ npc("Arr, ${ladOrLass()}, that it be.")
+ npc("To a weak-stomached, knock-kneed landlubber...")
+ player("Why Sluglings?")
+ npc("'Cos, ${ladOrLass()} they're one of our super-secret ingredients!")
+ npc("Yer not too susceptible to mind control are ye ${ladOrLass()}?")
+ player("Why?")
+ npc("Because they have been known te, well, influence people every now and again.")
+ if (isSeaSlugComplete()) {
+ player("They aren't related to those Seaslugs are they?")
+ npc("Aye they're related! Tis a good job they'll starve if they tried te eat yer brains!")
+ } else {
+ npc("I'm sure ye've got nothing te worry about!")
+ }
+ player("So what should I do with these Sluglings anyway?")
+ npc("Well, ye shove them in the Pressure Barrel in the attic.")
+ npc("And then ye...")
+ player("And then I...")
+ npc("And then ye...")
+ npc("Pressurise 'em.")
+ player("Pressurise them?!")
+ npc("Look, we don't have all day, get movin'.")
+ if (hasSpace()) {
+ set("rum_deal", "catch_creatures")
+ if (inventory.contains("fishbowl_and_net")) {
+ npc("Arr! I see ye've already got a fishbowl in a net.")
+ } else {
+ addOrDrop("fishbowl_and_net")
+ npc("Here. Ye'll need this to catch them.")
+ }
+ player("What should I do with it?")
+ npc("Just dunk it in the water. I'm sure a clever ${ladOrLass()} like yerself will have no problem.")
+ npc("Oh, and if ye haul up some squiddy-looking things, don't tate to shove 'em in the barrel too.")
+ npc("They add a special, fishy texture to the drink.")
+ } else {
+ npc("Ye'll be needin' a fishbowl in a big net to help ye catch them slimy little blighters.")
+ set("rum_deal", "catch_creatures")
+ npc("Pity ye ain't got any free space to carry one.")
+ }
+ }
+
+ // ===== Progress 12: Slug-shock recovery =====
+
+ private suspend fun Player.slugShockSnap() {
+ player("How could I kill my sluggy brethren...?")
+ npc("Snap out of it, ${ladOrLass()}! Yer in slug-shock!")
+ player("What? Who?")
+ player("Gah! Sorry about that.")
+ npc("No problem. Well, now ye have just one more ingredient to grab, and then we can get this 'rum' flowin!")
+ player("Well, how far away will I have to go to grab it?")
+ npc("Not far at all, ${ladOrLass()}. Ye've just got to get it from the basement!")
+ player("Great! What is it?")
+ npc("Hold yer horses, ${ladOrLass()}! While ye was off gallivantin' with yer slimy aquatic playmates, the 'rum' achieved spiritual critical mass.")
+ npc("To put it in terms ye'll understand: the brewin' equipment is possessed.")
+ player("Possessed!")
+ npc("Don't ye worry yerself about it! This happens all the time.")
+ npc("Well, to tell the truth, my lads are a little quicker off the mark, so it only happens occasionally.")
+ npc("Not that I'm criticisin' yer performance, ${ladOrLass()}.")
+ if (hasSpace()) {
+ inventory.add("wrench")
+ set("rum_deal", "bless_wrench")
+ npc("Give the controls a couple of belts with this wrench.")
+ npc("One of the lads did a little priestin' on the side before he came here. Get him to bless it and ye'll do fine.")
+ } else {
+ set("rum_deal", "bless_wrench")
+ npc("We've got a special item fer dealin' with this problem. Dump some of that stuff yer carryin' and I'll hand it over!")
+ }
+ }
+
+ // ===== Progress 14: Final ingredient (fever spider body) =====
+
+ private suspend fun Player.finalIngredient() {
+ npc("Well, now that ye've got that spirit out of there ye can dump in the final ingredient.")
+ player("And that is?")
+ npc("We need the body of a diseased Fever Spider!")
+ player("Remind me never to drink anything you have ever made. Or touched.")
+ npc("When yer quite done flappin' yer lips, go down into the basement and whack spiders until ye find a fever spider body.")
+ npc