diff --git a/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml b/data/area/kandarin/ardougne/east_ardougne.npc-spawns.toml index 65f55008ba..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_1", 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 141630cbc8..ef1f088e27 100644 --- a/data/area/kandarin/ardougne/east_ardougne.objs.toml +++ b/data/area/kandarin/ardougne/east_ardougne.objs.toml @@ -192,3 +192,15 @@ 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." + +[captain_barnaby_ship_ladder] +id = 9745 +examine = "A sturdy wooden ladder." 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/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/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/area/kandarin/ardougne/CaptainBarnaby.kt b/game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt new file mode 100644 index 0000000000..b437dca662 --- /dev/null +++ b/game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt @@ -0,0 +1,66 @@ +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") { (ladder) -> + if (ladder.tile != Tile(2682, 3267, 1)) { + return@objectOperate + } + 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/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt b/game/src/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt index 9c1ddaf62f..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 @@ -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", 5, 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/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) { 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 + } +} 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.")) + } +} 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) + } +} 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