From fa8d173b8c28f9ccf49c41f937f0c7def0c859d1 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Wed, 24 Jun 2026 03:11:20 -0700 Subject: [PATCH 01/10] bug: fix default hair on new player character creation --- .../engine/entity/character/player/equip/BodyParts.kt | 9 ++++++--- .../voidps/tools/photobooth/PlayerModelAssembler.kt | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/equip/BodyParts.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/equip/BodyParts.kt index 036bb54539..cfc1a6df27 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/equip/BodyParts.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/equip/BodyParts.kt @@ -13,7 +13,7 @@ import world.gregs.voidps.network.login.protocol.visual.update.player.BodyPart data class BodyParts( override var male: Boolean = true, val looks: IntArray = if (male) DEFAULT_LOOK_MALE.clone() else DEFAULT_LOOK_FEMALE.clone(), - val colours: IntArray = DEFAULT_COLOURS.clone(), + val colours: IntArray = if (male) DEFAULT_COLOURS_MALE.clone() else DEFAULT_COLOURS_FEMALE.clone(), ) : Body { private val parts = IntArray(12) @@ -134,8 +134,11 @@ data class BodyParts( } companion object { - val DEFAULT_LOOK_MALE = intArrayOf(0, 14, 18, 26, 34, 38, 42) + // Hair, beard, chest, arms, hands, legs, feet. Hair 5 = "Short" body_look_id. + val DEFAULT_LOOK_MALE = intArrayOf(5, 14, 18, 26, 34, 38, 42) val DEFAULT_LOOK_FEMALE = intArrayOf(45, -1, 58, 61, 68, 72, 80) - val DEFAULT_COLOURS = IntArray(5) + // Hair, top, legs, feet, skin. Hair 7 = "Willow brown" (light brown). + val DEFAULT_COLOURS_MALE = intArrayOf(7, 0, 0, 0, 0) + val DEFAULT_COLOURS_FEMALE = IntArray(5) } } diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/photobooth/PlayerModelAssembler.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/photobooth/PlayerModelAssembler.kt index b5658eba5b..b6594a582e 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/photobooth/PlayerModelAssembler.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/photobooth/PlayerModelAssembler.kt @@ -141,7 +141,7 @@ class PlayerModelAssembler( companion object { // Mirrors BodyParts.DEFAULT_LOOK_*. - private val DEFAULT_LOOK_MALE = intArrayOf(0, 14, 18, 26, 34, 38, 42) + private val DEFAULT_LOOK_MALE = intArrayOf(5, 14, 18, 26, 34, 38, 42) private val DEFAULT_LOOK_FEMALE = intArrayOf(45, -1, 58, 61, 68, 72, 80) private const val KIT_ID_BASE = 0x100000 private const val PLAYER_MODEL_ID = 0x200000 From eafb0434c089bbff38bd9f81b29d37aa4dd871ad Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Wed, 24 Jun 2026 03:37:59 -0700 Subject: [PATCH 02/10] bug: fix infinite shooting star with negative remaining health Mining stardust while holding the max (>=200) returned -1 from addOre, which short-circuited deplete() in the mining loop. handleMinedStarDust never ran, so the shared totalCollected counter grew unbounded while the star never advanced a layer - causing an infinite star and a negative '% left of this layer' prospect message. Count the maxed swing as a successful mine (returns 1) so the star depletes normally and experience is awarded. Also clamp the layer percentage to >=0 as a guard. Fixes #1043 --- .../kotlin/content/activity/shooting_star/ShootingStar.kt | 2 +- game/src/main/kotlin/content/skill/mining/Mining.kt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/game/src/main/kotlin/content/activity/shooting_star/ShootingStar.kt b/game/src/main/kotlin/content/activity/shooting_star/ShootingStar.kt index 77dba40cbe..240811bba8 100644 --- a/game/src/main/kotlin/content/activity/shooting_star/ShootingStar.kt +++ b/game/src/main/kotlin/content/activity/shooting_star/ShootingStar.kt @@ -213,7 +213,7 @@ class ShootingStar : Script { } fun getLayerPercentage(totalCollected: Int, totalNeeded: Int): String { - val remaining = totalNeeded - totalCollected + val remaining = (totalNeeded - totalCollected).coerceAtLeast(0) val percentageRemaining = (remaining.toDouble() / totalNeeded.toDouble()) * 100 return String.format("%.2f", percentageRemaining) } diff --git a/game/src/main/kotlin/content/skill/mining/Mining.kt b/game/src/main/kotlin/content/skill/mining/Mining.kt index f31fd2da59..cb307de785 100644 --- a/game/src/main/kotlin/content/skill/mining/Mining.kt +++ b/game/src/main/kotlin/content/skill/mining/Mining.kt @@ -161,7 +161,10 @@ class Mining : Script { val totalStarDust = player.inventory.count(ore) + player.bank.count(ore) if (totalStarDust >= 200) { player.message("You have the maximum amount of stardust but was still rewarded experience.") - return -1 + // Still count as a successful mine so the star depletes and experience is awarded, + // even though the dust can't be carried. Returning <1 here would skip deplete() and + // let totalCollected grow unbounded (negative "% left of this layer"). + return 1 } } var amount = when (target.id) { From b3d7265619ecd52c28ea782227b52dc67c87580f Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Wed, 24 Jun 2026 03:47:18 -0700 Subject: [PATCH 03/10] test: maxed-stardust mining still depletes a shooting star Regression test for the infinite shooting star (#1043): a player holding 200 stardust mining a crashed star one layer-collection away from advancing must push it to the next tier. Fails against the old return -1 path (star never depletes, hits the tick limit). --- .../shooting_star/ShootingStarTest.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 game/src/test/kotlin/content/activity/shooting_star/ShootingStarTest.kt diff --git a/game/src/test/kotlin/content/activity/shooting_star/ShootingStarTest.kt b/game/src/test/kotlin/content/activity/shooting_star/ShootingStarTest.kt new file mode 100644 index 0000000000..933ba33814 --- /dev/null +++ b/game/src/test/kotlin/content/activity/shooting_star/ShootingStarTest.kt @@ -0,0 +1,51 @@ +package content.activity.shooting_star + +import WorldTest +import content.entity.player.bank.bank +import objectOption +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.entity.obj.ObjectLayer +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.setRandom +import kotlin.random.Random + +internal class ShootingStarTest : WorldTest() { + + /** + * Regression test for https://github.com/GregHib/void/issues/1043 + * + * A player holding the maximum amount of stardust (>= 200) used to make `addOre` return -1, + * which short-circuited `deplete()` in the mining loop so the star's layer never advanced and + * the shared `totalCollected` counter grew without bound - an infinite star with a negative + * "% left of this layer". A maxed-out mine must still count towards depleting the star. + */ + @Test + fun `Mining a star with maximum stardust still depletes a layer`() { + setRandom(Random) + val player = createPlayer(emptyTile) + player.levels.set(Skill.Mining, 99) + player.inventory.add("rune_pickaxe") + // Maxed out on stardust so every successful mine goes through the >= 200 branch of addOre. + player.bank.add("stardust", 200) + + val tile = emptyTile.addY(1) + val star = createObject("crashed_star_tier_9", tile) // collect_for_next_layer = 15 + ShootingStarHandler.currentStarTile = tile + ShootingStarHandler.currentActiveObject = star + ShootingStarHandler.totalCollected = 14 // one successful mine away from the next layer + + player.objectOption(star, "Mine") + // The very next stardust mined by the maxed player must advance the star to the next tier. + tickIf(limit = 200) { + GameObjects.getLayer(tile, ObjectLayer.GROUND)?.id == "crashed_star_tier_9" + } + + assertEquals("crashed_star_tier_8", GameObjects.getLayer(tile, ObjectLayer.GROUND)?.id) + assertEquals(0, ShootingStarHandler.totalCollected) // counter reset, never runs away negative + assertEquals(0, player.inventory.count("stardust")) // none carried while maxed out + } +} From 0100fd7c9ddb8f7167123cd562108e075570d160 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Wed, 24 Jun 2026 04:10:13 -0700 Subject: [PATCH 04/10] bug: Brimhaven customs officer ship sails to Ardougne not Port Sarim The customs officer (npc 380) spawns at both Musa Point and Brimhaven docks but the shared script always sent players to Port Sarim. The Brimhaven officer now routes to Ardougne docks via the brimhaven_to_ardougne journey, branching on the officer's location. Adds the Brimhaven and Ardougne dock gangplank objects/teleports (2085-2088) so players can board and disembark the legacy ship. Closes #1045 --- .../kandarin/ardougne/east_ardougne.objs.toml | 8 +++ .../ardougne/east_ardougne.teles.toml | 10 +++ .../karamja/brimhaven/brimhaven.objs.toml | 10 ++- .../karamja/brimhaven/brimhaven.teles.toml | 12 +++- .../area/karamja/musa_point/CustomsOfficer.kt | 21 ++++-- .../karamja/musa_point/CustomsOfficerTest.kt | 64 +++++++++++++++++++ 6 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 game/src/test/kotlin/content/area/karamja/musa_point/CustomsOfficerTest.kt diff --git a/data/area/kandarin/ardougne/east_ardougne.objs.toml b/data/area/kandarin/ardougne/east_ardougne.objs.toml index 141630cbc8..a121baaab0 100644 --- a/data/area/kandarin/ardougne/east_ardougne.objs.toml +++ b/data/area/kandarin/ardougne/east_ardougne.objs.toml @@ -192,3 +192,11 @@ id = 5792 [dairy_churn_ardougne] id = 34800 examine = "Used to make dairy products." + +[gangplank_ardougne_enter] +id = 2085 +examine = "Handy for boarding the ship." + +[gangplank_ardougne_exit] +id = 2086 +examine = "Handy for boarding the ship." diff --git a/data/area/kandarin/ardougne/east_ardougne.teles.toml b/data/area/kandarin/ardougne/east_ardougne.teles.toml index 67fe945f9b..dc28934ae8 100644 --- a/data/area/kandarin/ardougne/east_ardougne.teles.toml +++ b/data/area/kandarin/ardougne/east_ardougne.teles.toml @@ -272,3 +272,13 @@ to = { x = 2044, y = 4649 } option = "Climb-up" tile = { x = 2044, y = 4650 } to = { x = 2543, y = 3327 } + +[gangplank_ardougne_enter] +option = "Cross" +tile = { x = 2683, y = 3270 } +to = { x = 2683, y = 3268, level = 1 } + +[gangplank_ardougne_exit] +option = "Cross" +tile = { x = 2683, y = 3269, level = 1 } +to = { x = 2683, y = 3271 } diff --git a/data/area/karamja/brimhaven/brimhaven.objs.toml b/data/area/karamja/brimhaven/brimhaven.objs.toml index a95fd654d2..611596070f 100644 --- a/data/area/karamja/brimhaven/brimhaven.objs.toml +++ b/data/area/karamja/brimhaven/brimhaven.objs.toml @@ -60,4 +60,12 @@ examine = "A closed overgrown dungeon entrance" [brimhaven_vine_4] id = 5106 -examine = "A closed overgrown dungeon entrance" \ No newline at end of file +examine = "A closed overgrown dungeon entrance" + +[gangplank_brimhaven_enter] +id = 2087 +examine = "Handy for boarding the ship." + +[gangplank_brimhaven_exit] +id = 2088 +examine = "Handy for boarding the ship." \ No newline at end of file diff --git a/data/area/karamja/brimhaven/brimhaven.teles.toml b/data/area/karamja/brimhaven/brimhaven.teles.toml index c54280d0c3..b156b684f7 100644 --- a/data/area/karamja/brimhaven/brimhaven.teles.toml +++ b/data/area/karamja/brimhaven/brimhaven.teles.toml @@ -56,4 +56,14 @@ to = { x = 2636, y = 9510, level = 2} [brimhaven_walk_down_4] option = "Walk-down" tile = { x = 2635, y = 9511, level = 2} -to = { x = 2636, y = 9517} \ No newline at end of file +to = { x = 2636, y = 9517} + +[gangplank_brimhaven_enter] +option = "Cross" +tile = { x = 2773, y = 3234 } +to = { x = 2775, y = 3234, level = 1 } + +[gangplank_brimhaven_exit] +option = "Cross" +tile = { x = 2774, y = 3234, level = 1 } +to = { x = 2772, y = 3234 } \ No newline at end of file diff --git a/game/src/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt b/game/src/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt index 9c1ddaf62f..a17a095454 100644 --- a/game/src/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt +++ b/game/src/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt @@ -10,6 +10,8 @@ import content.entity.player.dialogue.type.player import content.entity.player.dialogue.type.statement import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.data.definition.Areas +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 import world.gregs.voidps.engine.inv.remove @@ -18,7 +20,7 @@ import world.gregs.voidps.type.Tile class CustomsOfficer : Script { init { - npcOperate("Talk-to", "customs_officer_brimhaven") { + npcOperate("Talk-to", "customs_officer_brimhaven") { (officer) -> npc("Can I help you?") choice { option("Can I journey on this ship?") { @@ -30,7 +32,7 @@ class CustomsOfficer : Script { player("Oh dear, I don't seem to have enough money.") return@option } - travel() + travel(officer) } option("Oh, I'll not bother then.") } @@ -42,18 +44,23 @@ class CustomsOfficer : Script { } } - npcOperate("Pay-Fare", "customs_officer_brimhaven") { + npcOperate("Pay-Fare", "customs_officer_brimhaven") { (officer) -> if (!inventory.remove("coins", 30)) { message("You do not have enough money for that.") return@npcOperate } - travel() + travel(officer) } } - private suspend fun Player.travel() { + private suspend fun Player.travel(officer: NPC) { message("You pay 30 coins and board the ship.") - boatTravel("karamja_to_port_sarim", 7, Tile(3032, 3217, 1)) - statement("The ship arrives at Port Sarim.") + if (officer.tile in Areas["brimhaven"]) { + boatTravel("brimhaven_to_ardougne", 7, Tile(2683, 3268, 1)) + statement("The ship arrives at Ardougne.") + } else { + boatTravel("karamja_to_port_sarim", 7, Tile(3032, 3217, 1)) + statement("The ship arrives at Port Sarim.") + } } } diff --git a/game/src/test/kotlin/content/area/karamja/musa_point/CustomsOfficerTest.kt b/game/src/test/kotlin/content/area/karamja/musa_point/CustomsOfficerTest.kt new file mode 100644 index 0000000000..9ef4f7d75c --- /dev/null +++ b/game/src/test/kotlin/content/area/karamja/musa_point/CustomsOfficerTest.kt @@ -0,0 +1,64 @@ +package content.area.karamja.musa_point + +import WorldTest +import npcOption +import objectOption +import org.junit.jupiter.api.Test +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.Tile +import kotlin.test.assertEquals + +class CustomsOfficerTest : WorldTest() { + + @Test + fun `Pay fare from Brimhaven sails to Ardougne`() { + val player = createPlayer(Tile(2772, 3226)) + player.inventory.add("coins", 30) + val officer = createNPC("customs_officer_brimhaven", Tile(2772, 3225)) + + player.npcOption(officer, "Pay-Fare") + + tickIf(limit = 200) { player.tile != Tile(2683, 3268, 1) } + + assertEquals(Tile(2683, 3268, 1), player.tile) + assertEquals(0, player.inventory.count("coins")) + } + + @Test + fun `Pay fare from Musa Point sails to Port Sarim`() { + val player = createPlayer(Tile(2953, 3148)) + player.inventory.add("coins", 30) + val officer = createNPC("customs_officer_brimhaven", Tile(2953, 3147)) + + player.npcOption(officer, "Pay-Fare") + + tickIf(limit = 200) { player.tile != Tile(3032, 3217, 1) } + + assertEquals(Tile(3032, 3217, 1), player.tile) + assertEquals(0, player.inventory.count("coins")) + } + + @Test + fun `Cross gangplank to disembark at Ardougne docks`() { + val player = createPlayer(Tile(2683, 3268, 1)) + val gangplank = GameObjects.find(Tile(2683, 3269, 1), "gangplank_ardougne_exit") + + player.objectOption(gangplank, "Cross") + tickIf(limit = 20) { player.tile != Tile(2683, 3271) } + + assertEquals(Tile(2683, 3271), player.tile) + } + + @Test + fun `Cross gangplank to board ship at Brimhaven docks`() { + val player = createPlayer(Tile(2772, 3234)) + val gangplank = GameObjects.find(Tile(2773, 3234), "gangplank_brimhaven_enter") + + player.objectOption(gangplank, "Cross") + tickIf(limit = 20) { player.tile != Tile(2775, 3234, 1) } + + assertEquals(Tile(2775, 3234, 1), player.tile) + } +} From dfe8b77ff3a14f18c15dbf73453910c0888aca29 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Wed, 24 Jun 2026 07:17:59 -0700 Subject: [PATCH 05/10] fix: set Brimhaven->Ardougne ship cutscene delay to 5 ticks Matches the canonical journey delay for varp value 8 (~3s). The ship's stop position on the journey map is defined by the client cache for that varp value, not by server data, so the delay only controls how long the cutscene runs before teleporting. --- .../kotlin/content/area/karamja/musa_point/CustomsOfficer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/src/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt b/game/src/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt index a17a095454..24206222fb 100644 --- a/game/src/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt +++ b/game/src/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt @@ -56,7 +56,7 @@ class CustomsOfficer : Script { private suspend fun Player.travel(officer: NPC) { message("You pay 30 coins and board the ship.") if (officer.tile in Areas["brimhaven"]) { - boatTravel("brimhaven_to_ardougne", 7, Tile(2683, 3268, 1)) + boatTravel("brimhaven_to_ardougne", 5, Tile(2683, 3268, 1)) statement("The ship arrives at Ardougne.") } else { boatTravel("karamja_to_port_sarim", 7, Tile(3032, 3217, 1)) From c9599acf5d561dd7acc15ab489d6fe5c928eb718 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Wed, 24 Jun 2026 07:44:03 -0700 Subject: [PATCH 06/10] feat: add Captain Barnaby charter dialogue (Ardougne -> Brimhaven) - Captain Barnaby Talk-to/Pay-fare dialogue sails Ardougne -> Brimhaven (varp value 7). Repoints the dock spawn to captain_barnaby_2 (4974), the interactive variant; the previously-spawned captain_barnaby_1 (4961) has no Talk-to option. - Boarding either docked ship via the gangplank shows a hint to speak to the operator first (via objTeleportLand, so the teleport still runs). - Ship's ladder (9745) Climb-down gives the standard refusal message. Mirrors the Ardougne<->Brimhaven charter from 2011Scape/game#610. --- .../ardougne/east_ardougne.npc-spawns.toml | 2 +- .../kandarin/ardougne/east_ardougne.objs.toml | 4 ++ .../area/kandarin/ardougne/CaptainBarnaby.kt | 63 +++++++++++++++++++ .../kandarin/ardougne/CaptainBarnabyTest.kt | 53 ++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt create mode 100644 game/src/test/kotlin/content/area/kandarin/ardougne/CaptainBarnabyTest.kt diff --git a/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml b/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml index 65f55008ba..ba58f951b7 100644 --- a/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml +++ b/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml @@ -178,7 +178,7 @@ spawns = [ { id = "ambassador_gimblewap", x = 2572, y = 3299, level = 1 }, { id = "banker_4", x = 2616, y = 3330 }, { id = "my_arm_ardougne", x = 2684, y = 3274, members = true }, - { id = "captain_barnaby_1", x = 2683, y = 3275, members = true }, + { id = "captain_barnaby_2", x = 2683, y = 3275, members = true }, { id = "charlie_ardougne", x = 2607, y = 3264, members = true }, { id = "penguin_ardougne", x = 2596, y = 3270, members = true }, { id = "silver_merchant_ardougne", x = 2659, y = 3316 }, diff --git a/data/area/kandarin/ardougne/east_ardougne.objs.toml b/data/area/kandarin/ardougne/east_ardougne.objs.toml index a121baaab0..ef1f088e27 100644 --- a/data/area/kandarin/ardougne/east_ardougne.objs.toml +++ b/data/area/kandarin/ardougne/east_ardougne.objs.toml @@ -200,3 +200,7 @@ examine = "Handy for boarding the ship." [gangplank_ardougne_exit] id = 2086 examine = "Handy for boarding the ship." + +[captain_barnaby_ship_ladder] +id = 9745 +examine = "A sturdy wooden ladder." diff --git a/game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt b/game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt new file mode 100644 index 0000000000..9b3ca92239 --- /dev/null +++ b/game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt @@ -0,0 +1,63 @@ +package content.area.kandarin.ardougne + +import content.entity.obj.ship.boatTravel +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.Sad +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 world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.type.Tile + +class CaptainBarnaby : Script { + + init { + npcOperate("Talk-to", "captain_barnaby_2") { + npc("Do you want to go on a trip to Brimhaven?") + npc("The trip will cost you 30 coins.") + choice { + option("Yes please.") { + if (!inventory.remove("coins", 30)) { + player("Oh dear, I don't seem to have enough money.") + return@option + } + travel() + } + option("No, thank you.") + } + } + + npcOperate("Pay-fare", "captain_barnaby_2") { + if (!inventory.remove("coins", 30)) { + message("You do not have enough money for that.") + return@npcOperate + } + travel() + } + + // Boarding the docked ship doesn't set sail; the trip starts by talking to the operator. + objTeleportLand("Cross", "gangplank_ardougne_enter") { _, _ -> + message("You must speak to Captain Barnaby before it will set sail.") + } + objTeleportLand("Cross", "gangplank_brimhaven_enter") { _, _ -> + message("You must speak to the Customs officer before it will set sail.") + } + + objectOperate("Climb-down", "captain_barnaby_ship_ladder") { + message("I don't think Captain Barnaby wants me going down there.") + } + } + + private suspend fun Player.travel() { + message("You pay 30 coins and board the ship.") + boatTravel("ardougne_to_brimhaven", 5, Tile(2775, 3234, 1)) + statement("The ship arrives at Brimhaven.") + } +} diff --git a/game/src/test/kotlin/content/area/kandarin/ardougne/CaptainBarnabyTest.kt b/game/src/test/kotlin/content/area/kandarin/ardougne/CaptainBarnabyTest.kt new file mode 100644 index 0000000000..aefc7ff10f --- /dev/null +++ b/game/src/test/kotlin/content/area/kandarin/ardougne/CaptainBarnabyTest.kt @@ -0,0 +1,53 @@ +package content.area.kandarin.ardougne + +import WorldTest +import containsMessage +import npcOption +import objectOption +import org.junit.jupiter.api.Test +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.Tile +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class CaptainBarnabyTest : WorldTest() { + + @Test + fun `Pay fare from Ardougne sails to Brimhaven`() { + val player = createPlayer(Tile(2683, 3274)) + player.inventory.add("coins", 30) + val barnaby = createNPC("captain_barnaby_2", Tile(2683, 3275)) + + player.npcOption(barnaby, "Pay-fare") + + tickIf(limit = 200) { player.tile != Tile(2775, 3234, 1) } + + assertEquals(Tile(2775, 3234, 1), player.tile) + assertEquals(0, player.inventory.count("coins")) + } + + @Test + fun `Boarding the Ardougne ship warns to speak to Captain Barnaby`() { + val player = createPlayer(Tile(2683, 3270)) + val gangplank = GameObjects.find(Tile(2683, 3270), "gangplank_ardougne_enter") + + player.objectOption(gangplank, "Cross") + tickIf(limit = 20) { player.tile != Tile(2683, 3268, 1) } + + assertEquals(Tile(2683, 3268, 1), player.tile) + assertTrue(player.containsMessage("You must speak to Captain Barnaby before it will set sail.")) + } + + @Test + fun `Ship's ladder cannot be climbed`() { + val player = createPlayer(Tile(2683, 3268, 1)) + val ladder = GameObjects.find(Tile(2682, 3267, 1), "captain_barnaby_ship_ladder") + + player.objectOption(ladder, "Climb-down") + tick(2) + + assertTrue(player.containsMessage("I don't think Captain Barnaby wants me going down there.")) + } +} From 8adb46ff213582a4292eb58945eeb3d1c63d34d1 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Wed, 24 Jun 2026 07:48:51 -0700 Subject: [PATCH 07/10] Remove duplicate npc spawn --- data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml b/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml index ba58f951b7..e11418732a 100644 --- a/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml +++ b/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml @@ -178,7 +178,6 @@ spawns = [ { id = "ambassador_gimblewap", x = 2572, y = 3299, level = 1 }, { id = "banker_4", x = 2616, y = 3330 }, { id = "my_arm_ardougne", x = 2684, y = 3274, members = true }, - { id = "captain_barnaby_2", x = 2683, y = 3275, members = true }, { id = "charlie_ardougne", x = 2607, y = 3264, members = true }, { id = "penguin_ardougne", x = 2596, y = 3270, members = true }, { id = "silver_merchant_ardougne", x = 2659, y = 3316 }, From 2846f6e633558c9efd58bcf0b48be667f1be1b7e Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Wed, 24 Jun 2026 07:48:51 -0700 Subject: [PATCH 08/10] Remove duplicate npc spawn --- .../kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt b/game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt index 9b3ca92239..b437dca662 100644 --- a/game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt +++ b/game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt @@ -50,7 +50,10 @@ class CaptainBarnaby : Script { message("You must speak to the Customs officer before it will set sail.") } - objectOperate("Climb-down", "captain_barnaby_ship_ladder") { + objectOperate("Climb-down", "captain_barnaby_ship_ladder") { (ladder) -> + if (ladder.tile != Tile(2682, 3267, 1)) { + return@objectOperate + } message("I don't think Captain Barnaby wants me going down there.") } } From 09abf8840000e5fce0c2691004bc04505a7d0b31 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Wed, 24 Jun 2026 08:03:56 -0700 Subject: [PATCH 09/10] fix: spawn interactive Captain Barnaby variant at Ardougne dock The remaining captain_barnaby spawn (id 381) has no Talk-to/Pay-fare option; only captain_barnaby_2 (id 4974) is the charter operator. --- data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml b/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml index e11418732a..80417ad40a 100644 --- a/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml +++ b/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml @@ -163,7 +163,7 @@ spawns = [ { id = "guard_ardougne", x = 2665, y = 3300 }, { id = "edmond_ardougne", x = 2568, y = 3334, members = true }, { id = "jerico", x = 2612, y = 3324 }, - { id = "captain_barnaby", x = 2679, y = 3275, members = true }, + { id = "captain_barnaby_2", x = 2679, y = 3275, members = true }, { id = "rick", x = 2663, y = 3334, members = true }, { id = "maid_ardougne", x = 2666, y = 3330, members = true }, { id = "cook_ardougne", x = 2669, y = 3331, members = true }, From e1c609676ac6276a2168a8102db2eb418bb7cf69 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Wed, 24 Jun 2026 08:06:30 -0700 Subject: [PATCH 10/10] Revert "fix: spawn interactive Captain Barnaby variant at Ardougne dock" This reverts commit f61a6f525d5487743f139ce74dd5c85a5916adfe. --- data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml b/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml index 80417ad40a..e11418732a 100644 --- a/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml +++ b/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml @@ -163,7 +163,7 @@ spawns = [ { id = "guard_ardougne", x = 2665, y = 3300 }, { id = "edmond_ardougne", x = 2568, y = 3334, members = true }, { id = "jerico", x = 2612, y = 3324 }, - { id = "captain_barnaby_2", x = 2679, y = 3275, members = true }, + { id = "captain_barnaby", x = 2679, y = 3275, members = true }, { id = "rick", x = 2663, y = 3334, members = true }, { id = "maid_ardougne", x = 2666, y = 3330, members = true }, { id = "cook_ardougne", x = 2669, y = 3331, members = true },