diff --git a/Server/data/configs/npc_spawns.json b/Server/data/configs/npc_spawns.json index 46a5a18e7..c116a2b89 100644 --- a/Server/data/configs/npc_spawns.json +++ b/Server/data/configs/npc_spawns.json @@ -4127,6 +4127,26 @@ "npc_id": "1658", "loc_data": "{2595,3087,1,1,1}-" }, + { + "npc_id": "1660", + "loc_data": "{3549,9867,0,0,6}-" + }, + { + "npc_id": "1661", + "loc_data": "{3540,9873,0,0,1}-" + }, + { + "npc_id": "1662", + "loc_data": "{3554,9886,0,1,0}-{3560,9908,0,1,0}-{3565,9865,0,1,0}-{3572,9908,0,1,0}-{3573,9891,0,1,0}-{3577,9875,0,1,0}-" + }, + { + "npc_id": "1663", + "loc_data": "{3527,9909,0,1,0}-{3533,9912,0,1,0}-{3540,9892,0,1,0}-{3540,9902,0,1,0}-" + }, + { + "npc_id": "1664", + "loc_data": "{3528,9865,0,1,0}-" + }, { "npc_id": "1665", "loc_data": "{3544,3462,0,0,1}-" diff --git a/Server/src/main/content/region/morytania/handlers/MorytaniaArea.kt b/Server/src/main/content/region/morytania/handlers/MorytaniaArea.kt index 5edfa55f7..9241c19e2 100644 --- a/Server/src/main/content/region/morytania/handlers/MorytaniaArea.kt +++ b/Server/src/main/content/region/morytania/handlers/MorytaniaArea.kt @@ -17,14 +17,12 @@ class MorytaniaArea : MapArea { override fun defineAreaBorders(): Array { return arrayOf( ZoneBorders(3426, 3191, 3715, 3588), //Morytania overworld - ZoneBorders(3520, 9856, 3583, 9919) //Werewolf agility course ) } override fun areaEnter(entity: Entity) { if (entity is Player && entity !is AIPlayer && ( - !isQuestComplete(entity, Quests.PRIEST_IN_PERIL) || //not allowed to be anywhere in Morytania - defineAreaBorders()[1].insideBorder(entity) //Werewolf agility course is not implemented + !isQuestComplete(entity, Quests.PRIEST_IN_PERIL) //not allowed to be anywhere in Morytania )) { kickThemOut(entity) } diff --git a/Server/src/main/content/region/morytania/canifis/dialogue/AgilityBossDialogue.kt b/Server/src/main/content/region/morytania/werewolfagility/AgilityBossDialogue.kt similarity index 65% rename from Server/src/main/content/region/morytania/canifis/dialogue/AgilityBossDialogue.kt rename to Server/src/main/content/region/morytania/werewolfagility/AgilityBossDialogue.kt index 5b476b620..d0f6ae331 100644 --- a/Server/src/main/content/region/morytania/canifis/dialogue/AgilityBossDialogue.kt +++ b/Server/src/main/content/region/morytania/werewolfagility/AgilityBossDialogue.kt @@ -1,13 +1,16 @@ -package content.region.morytania.canifis.dialogue +package content.region.morytania.werewolfagility +import core.api.anyInEquipment import core.game.dialogue.DialoguePlugin import core.game.dialogue.FacialExpression import core.game.node.entity.npc.NPC import core.game.node.entity.player.Player import core.plugin.Initializable +import org.rs09.consts.Items /** * @author qmqz + * https://www.youtube.com/watch?v=9u_qJW1eKR0 */ @Initializable @@ -16,29 +19,29 @@ class AgilityBossDialogue(player: Player? = null) : DialoguePlugin(player){ override fun open(vararg args: Any?): Boolean { npc = args[0] as NPC - if(player.equipment.contains(4202,1)) { + if(anyInEquipment(player, Items.RING_OF_CHAROS_4202, Items.RING_OF_CHAROSA_6465)) { player(FacialExpression.ASKING,"How do I use the agility course?").also { stage = 0 } } else { - npc(FacialExpression.CHILD_SUSPICIOUS,"Grrr - you don't belong in here, human!").also { stage = 99 } + npc(FacialExpression.WEREWOLF_NEUTRAL,"Grrr - you don't belong in here, human!").also { stage = 99 } } return true } override fun handle(interfaceId: Int, buttonId: Int): Boolean { when(stage){ - 0 -> npc(FacialExpression.CHILD_NORMAL,"I'll throw you a stick, which you need to", + 0 -> npc(FacialExpression.WEREWOLF_NEUTRAL,"I'll throw you a stick, which you need to", "fetch as quickly as possible, ", "from the area beyond the pipes.").also { stage++ } - 1 -> npc(FacialExpression.CHILD_NORMAL,"Be wary of the deathslide - you must hang by your teeth,", + 1 -> npc(FacialExpression.WEREWOLF_NEUTRAL,"Be wary of the deathslide - you must hang by your teeth,", "and if your strength is not up to the job you will", "fall into a pit of spikes. Also, I would advise not", "carrying too much extra weight.").also { stage++ } - 2 -> npc(FacialExpression.CHILD_NORMAL,"Bring the stick back to the werewolf waiting at", + 2 -> npc(FacialExpression.WEREWOLF_NEUTRAL,"Bring the stick back to the werewolf waiting at", "the end of the death slide to get your agility bonus.").also { stage++ } - 3 ->npc(FacialExpression.CHILD_NORMAL,"I will throw your stick as soon as you jump onto the", + 3 ->npc(FacialExpression.WEREWOLF_NEUTRAL,"I will throw your stick as soon as you jump onto the", "first stone.").also { stage = 99 } 99 -> end() diff --git a/Server/src/main/content/region/morytania/werewolfagility/AgilityCourse.kt b/Server/src/main/content/region/morytania/werewolfagility/AgilityCourse.kt new file mode 100644 index 000000000..7ce2392ac --- /dev/null +++ b/Server/src/main/content/region/morytania/werewolfagility/AgilityCourse.kt @@ -0,0 +1,322 @@ +package content.region.morytania.werewolfagility + +import core.api.* +import core.game.dialogue.FacialExpression +import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.interaction.QueueStrength +import core.game.node.entity.Entity +import core.game.node.entity.combat.ImpactHandler +import core.game.node.entity.impl.ForceMovement +import core.game.node.entity.player.Player +import core.game.node.entity.skill.Skills +import core.game.world.map.Direction +import core.game.world.map.Location +import core.game.world.map.zone.ZoneBorders +import core.game.world.update.flag.context.Animation +import core.tools.RandomFunction +import org.rs09.consts.Items +import org.rs09.consts.NPCs +import org.rs09.consts.Scenery + +class AgilityCourse : InteractionListener, MapArea { + + companion object { + + private const val LAST_VISITED_STONE_TILE_KEY = "lastVisitedStoneTile" + + val steppingStones = listOf( + Location(3538, 9873, 0), + Location(3538, 9875, 0), // 1 + Location(3538, 9877, 0), // 2 + Location(3540, 9877, 0), // 3 + Location(3540, 9879, 0), // 4 + Location(3540, 9881, 0), // 5 + Location(3540, 9882, 0), + ) + + // https://www.youtube.com/watch?v=JN_1c7r9PVo - Popular courses GOOD! + // https://www.youtube.com/watch?v=fmzzLy5fXK4 - a fall location on the other side + // https://www.youtube.com/watch?v=RnhjHPuae3Q - best with 2 fall locations nearest and furthest. + // https://www.youtube.com/watch?v=_Re3MRhHbZk - middle location fall. + // 180 160 140 exp for the 3 failure locations + // 200 exp for success + val startTile: Location = Location(3528, 9910, 0) + val midwayTile1: Location = Location(3528, 9890, 0) + val midwayTile2: Location = Location(3528, 9885, 0) + val midwayTile3: Location = Location(3528, 9880, 0) + val failureTileLeft1: Location = Location(3526, 9888, 0) + val failureTileLeft2: Location = Location(3526, 9883, 0) + val failureTileLeft3: Location = Location(3526, 9878, 0) + val failureTileRight1: Location = Location(3530, 9888, 0) + val failureTileRight2: Location = Location(3530, 9883, 0) + val failureTileRight3: Location = Location(3530, 9878, 0) + val endTile: Location = Location(3528, 9873, 0) + + // anim 767 - Landing on stomach + + fun nearestWerewolfSay(loc: Location, chatText: String) { + var werewolfNpc = findLocalNPCs(loc, NPCs.AGILITY_TRAINER_1663).sortedWith { a, b -> + a.location.getDistanceSquared(loc) - b.location.getDistanceSquared(loc) + }.getOrNull(0) + if (werewolfNpc != null) { + // println("werewolf ${werewolfNpc.location}") + sendChat(werewolfNpc, chatText) + } + } + + fun randomWerewolfSay(): String { + return listOf( + "Remember - a slow wolf is a hungry wolf!!", + "Get on with it - you need your whiskers perking!!!!", + "Claws first - think later.", + "Imagine the smell of blood in your nostrils!!!", + "I never really wanted to be an agility trainer...", + "It'll be worth it when you hunt!!", + "Let's see those powerful backlegs at work!!", + "Let the bloodlust take you!!", + "You're the slowest wolf I've ever had the misfortune to witness!!", + "When you're done there's a human with your name on it!!", + ).random() + } + + fun randomZiplineWerewolfSay(): String { + return listOf( + "Give my regards to the ground...", + "Don't let the spikes or the blood put you off...", + "Now for a true test of teeth...", + ).random() + } + + } + + override fun defineListeners() { + + on(Scenery.TRAPDOOR_5131, IntType.SCENERY, "open") { player, node -> + replaceScenery(node as core.game.node.scenery.Scenery, Scenery.TRAPDOOR_5132, 20) + return@on true + } + + // Ladder Down + on(Scenery.TRAPDOOR_5132, IntType.SCENERY, "climb-down") { player, node -> + if (!anyInEquipment(player, Items.RING_OF_CHAROS_4202, Items.RING_OF_CHAROSA_6465)) { + sendNPCDialogue(player, NPCs.WEREWOLF_1665, "You can't go down there, human. If it wasn't my duty to guard this trapdoor, I would be relieving you of the burden of your life right now.", FacialExpression.WEREWOLF_NEUTRAL) + } else { + sendNPCDialogue(player, NPCs.WEREWOLF_1665, "Good luck down there, my friend. Remember, to the west is the main agility course, while to the east is a skullball course. ", FacialExpression.WEREWOLF_NEUTRAL) + teleport(player, Location(3549, 9865, 0)) + } + return@on true + } + + // Ladder Up + on(Scenery.LADDER_5130, IntType.SCENERY, "climb-up") { player, node -> + teleport(player, Location(3543, 3463, 0)) + return@on true + } + + + // Stepping stones (destination overrides below) + on(Scenery.STEPPING_STONE_35996, IntType.SCENERY, "jump-to") { player, node -> + if (!hasLevelStat(player, Skills.AGILITY, 60)) { + sendDialogue(player, "You need an Agility level of at least 60 to do this.") + return@on false + } + val arrIndex = steppingStones.indexOf(node.location) + ForceMovement.run(player, steppingStones[arrIndex-1], steppingStones[arrIndex], Animation(741), Animation(741), if (arrIndex == 3) Direction.EAST else Direction.NORTH, 20).endAnimation = Animation.RESET + rewardXP(player, Skills.AGILITY, 10.0) + + if (arrIndex == 1 && !inInventory(player, Items.STICK_4179)) { + val agilityBoss = findLocalNPC(player, NPCs.AGILITY_BOSS_1661) + if (agilityBoss != null) { + sendChat(agilityBoss, "FETCH!!!!!") + face(agilityBoss, Location(3540, 9877, 0)) + animate(agilityBoss, 6547) + produceGroundItem(player, Items.STICK_4179, 1, Location(3543, 9912)) + spawnProjectile(Location(3540, 9873), Location(3540, 9883), 1158, 0, 0, 1, 60, 0) + } + } + return@on true + } + + // Hurdles + on(intArrayOf(Scenery.HURDLE_5133, Scenery.HURDLE_5134, Scenery.HURDLE_5135), IntType.SCENERY, "jump") { player, node -> + if (!hasLevelStat(player, Skills.AGILITY, 60)) { + sendDialogue(player, "You need an Agility level of at least 60 to do this.") + return@on false + } + if (player.location.y < node.location.y) { + rewardXP(player, Skills.AGILITY, 20.0) + ForceMovement.run(player, player.location, player.location.transform(0, 2, 0), Animation(1603), Animation(1603), Direction.NORTH, 20).endAnimation = Animation.RESET + } else { + sendMessage(player, "You've already jumped over the hurdle.") + } + return@on true + } + + // Pipes + on(intArrayOf(Scenery.PIPE_5152), IntType.SCENERY, "squeeze-through") { player, node -> + if (!hasLevelStat(player, Skills.AGILITY, 60)) { + sendDialogue(player, "You need an Agility level of at least 60 to do this.") + return@on false + } + if (player.location.y < node.location.y) { + nearestWerewolfSay(Location(3540, 9902), randomWerewolfSay()) + rewardXP(player, Skills.AGILITY, 15.0) + ForceMovement.run(player, player.location, player.location.transform(0, 5, 0), Animation(10580), Animation(844), Direction.NORTH, 10).endAnimation = Animation(10579) + } else { + sendMessage(player, "You've already squeezed through the pipe.") + } + return@on true + } + + // Skull slopes + on(intArrayOf(Scenery.SKULL_SLOPE_5136), IntType.SCENERY, "climb-up") { player, node -> + if (!hasLevelStat(player, Skills.AGILITY, 60)) { + sendDialogue(player, "You need an Agility level of at least 60 to do this.") + return@on false + } + if (player.location.x > node.location.x) { + nearestWerewolfSay(Location(3536, 9912), randomWerewolfSay()) + rewardXP(player, Skills.AGILITY, 25.0) + ForceMovement.run(player, player.location, player.location.transform(-2, 0, 0), Animation(2049), Animation(2049), Direction.WEST, 10).endAnimation = Animation.RESET + } else { + sendMessage(player, "You've already climbed the skull wall.") + } + return@on true + } + + // Zip line + on(intArrayOf(Scenery.ZIP_LINE_5139, Scenery.ZIP_LINE_5140, Scenery.ZIP_LINE_5141), IntType.SCENERY, "teeth-grip") { player, node -> + if (!hasLevelStat(player, Skills.AGILITY, 60)) { + sendDialogue(player, "You need an Agility level of at least 60 to do this.") + return@on false + } + + var successChancePercent = 100.0 + // "With level 80 in Agility and Strength and a weight of 2 kg or lower, this obstacle will never be failed." + // Otherwise, this is up to my decision on whether to torture you + if (!(getDynLevel(player, Skills.AGILITY) >= 80 && getDynLevel(player, Skills.STRENGTH) >= 80 && player.settings.weight <= 2.0)) { + // All the successes are between 0.0 to 1.0 range + val agilitySuccess = RandomFunction.getSkillSuccessChance(0.0, 320.0, 60) / 100// 0 at lvl1, 256 at lvl80 extrapolate to 320 at lvl 99 + val strengthSuccess = RandomFunction.getSkillSuccessChance(0.0, 320.0, 60) / 100 // 0 at lvl1, 256 at lvl80 extrapolate to 320 at lvl 99 + val weightSuccess = Math.max(80.0 - player.settings.weight, 0.0) / 100 // 80% chance, minus 1% per weight gain. + successChancePercent *= agilitySuccess + successChancePercent *= strengthSuccess + successChancePercent *= weightSuccess + } + + // sendMessage(player, "Total Success " + successChancePercent.toString()) + + + // Align player up on the zipline + forceMove(player, player.location, Location(3528, 9910, 0), 0,10) + face(player, Location(3528, 9915, 0)) + lock(player, 6) + // roll a number, between 0-totalSuccess means you succeed, otherwise between totalSuccess-100 you fail. + if(RandomFunction.random(0.0, 100.0) < successChancePercent) { // Success + nearestWerewolfSay(Location(3527, 9909), randomZiplineWerewolfSay()) + queueScript(player, 2, QueueStrength.SOFT) { stage -> + when (stage) { + 0 -> { + face(player, Location(3528, 9915, 0)) + animate(player, 1601) + sendMessage(player, "You bravely cling on to the death slide by your teeth ...") + return@queueScript delayScript(player, 2) + } + 1 -> { + sendChat(player, "WAAAAAARRRGGGHHH!!!!!!") + ForceMovement.run(player, startTile, endTile, Animation(1602), Animation(1602), Direction.SOUTH, 60).endAnimation = Animation.RESET + return@queueScript delayScript(player, 8) + } + 2 -> { + rewardXP(player, Skills.AGILITY, 200.0) + sendMessage(player, ".. and land safely on your feet.") + teleport(player, endTile) + return@queueScript stopExecuting(player) + } + else -> return@queueScript stopExecuting(player) + } + } + } else { + // Based on total success, find where to land. If you had a lower chance, you get the early drop and lower XP. + var finalTile = endTile + var animTicks = 6 + var fallTile = endTile + var rewardXP = 200.0 + if (successChancePercent <= 20.0) { + finalTile = midwayTile1 + fallTile = arrayOf(failureTileLeft1, failureTileRight1).random() + animTicks = 4 + rewardXP = 140.0 + } + if (successChancePercent > 20.0 && successChancePercent <= 40.0 ) { + finalTile = midwayTile2 + fallTile = arrayOf(failureTileLeft2, failureTileRight2).random() + animTicks = 5 + rewardXP = 160.0 + } + if (successChancePercent > 40.0) { + finalTile = midwayTile3 + fallTile = arrayOf(failureTileLeft3, failureTileRight3).random() + animTicks = 6 + rewardXP = 180.0 + } + + nearestWerewolfSay(Location(3527, 9909), randomZiplineWerewolfSay()) + queueScript(player, 2, QueueStrength.SOFT) { stage -> + when (stage) { + 0 -> { + face(player, Location(3528, 9915, 0)) + animate(player, 1601) + sendMessage(player, "You bravely cling on to the death slide by your teeth ...") + return@queueScript delayScript(player, 2) + } + 1 -> { + sendChat(player, "WAAAAAARRRGGGHHH!!!!!!") + ForceMovement.run(player, startTile, finalTile, Animation(1602), Animation(1602), Direction.SOUTH, 60).endAnimation = Animation(767) + return@queueScript delayScript(player, animTicks) + } + 2 -> { + rewardXP(player, Skills.AGILITY, rewardXP) + sendMessage(player, ".. only to fall from a great height!") + teleport(player, fallTile) + // Can't get this to chain animations. + //ForceMovement.run(player, finalTile, fallTile, Animation(767), Animation(767), Direction.SOUTH, 60).endAnimation = Animation(767) + return@queueScript delayScript(player, 2) + } + 3 -> { + teleport(player, fallTile) + player.impactHandler.manualHit(player, (1..30).random(), ImpactHandler.HitsplatType.NORMAL) + return@queueScript stopExecuting(player) + } + else -> return@queueScript stopExecuting(player) + } + } + } + + return@on true + } + } + + override fun defineDestinationOverrides() { + setDest(IntType.SCENERY, intArrayOf(Scenery.STEPPING_STONE_35996),"jump-to"){ player, node -> + val arrIndex = steppingStones.indexOf(node.location) + return@setDest steppingStones[arrIndex - 1] + } + } + + override fun defineAreaBorders(): Array { + // Area of the zipline. + return arrayOf(ZoneBorders(3527, 9876, 3529, 9907)) + } + + override fun areaLeave(entity: Entity, logout: Boolean) { + // In case you log out during the zipline of death slide, you won't be left on it. + // You lose that XP though... + if (entity is Player) { + if (logout) { + teleport(entity, endTile) + } + } + } +} \ No newline at end of file diff --git a/Server/src/main/content/region/morytania/werewolfagility/AgilityTrainerDialogue.kt b/Server/src/main/content/region/morytania/werewolfagility/AgilityTrainerDialogue.kt new file mode 100644 index 000000000..ee059338a --- /dev/null +++ b/Server/src/main/content/region/morytania/werewolfagility/AgilityTrainerDialogue.kt @@ -0,0 +1,36 @@ +package content.region.morytania.werewolfagility + +import core.api.* +import core.game.dialogue.* +import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.node.entity.npc.NPC +import core.game.node.entity.skill.Skills +import org.rs09.consts.Items +import org.rs09.consts.NPCs + +/** + * https://www.youtube.com/watch?v=mIKPpc30XBQ - What stick + */ +class AgilityTrainerDialogue : InteractionListener { + + override fun defineListeners() { + on(NPCs.AGILITY_TRAINER_1664, IntType.NPC, "give-stick") { player, node -> + if (inInventory(player, Items.STICK_4179)) { + removeItem(player, Items.STICK_4179) + rewardXP(player, Skills.AGILITY, 190.0) + return@on true + } + DialogueLabeller.open(player, object : DialogueLabeller() { + override fun addConversation() { + assignToIds(NPCs.AGILITY_TRAINER_1664) + // shorts/TO-vdlyOa3E + npc(ChatAnim.WEREWOLF_NEUTRAL, "Have you brought the stick yet?") + player("What stick?") + npc(ChatAnim.WEREWOLF_NEUTRAL, "Come on, get round that course - I need something to chew!") + } + }, node as NPC) + return@on true + } + } +} \ No newline at end of file diff --git a/Server/src/main/content/region/morytania/werewolfagility/SkullballBossDialogue.kt b/Server/src/main/content/region/morytania/werewolfagility/SkullballBossDialogue.kt new file mode 100644 index 000000000..50df12d0a --- /dev/null +++ b/Server/src/main/content/region/morytania/werewolfagility/SkullballBossDialogue.kt @@ -0,0 +1,79 @@ +package content.region.morytania.werewolfagility + +import core.api.* +import core.game.dialogue.* +import core.game.node.entity.npc.NPC +import core.game.node.entity.player.Player +import core.plugin.Initializable +import org.rs09.consts.Items +import org.rs09.consts.NPCs + +@Initializable +class SkullballBossDialogue (player: Player? = null) : DialoguePlugin(player) { + override fun newInstance(player: Player): DialoguePlugin { + return SkullballBossDialogue(player) + } + override fun handle(interfaceId: Int, buttonId: Int): Boolean { + openDialogue(player, SkullballBossDialogueFile(), npc) + return false + } + override fun getIds(): IntArray { + return intArrayOf(NPCs.SKULLBALL_BOSS_1660) + } +} +class SkullballBossDialogueFile : DialogueLabeller() { + override fun addConversation() { + assignToIds(NPCs.SKULLBALL_BOSS_1660) + + exec { player, npc -> + if(!anyInEquipment(player, Items.RING_OF_CHAROS_4202, Items.RING_OF_CHAROSA_6465)) { + goto("ishuman") + } else if (getAttribute(player, SkullballCourse.attributeSkullballInstance, null) != null) { + goto("skullballinprogress") + } else { + goto("noskullball") + } + } + + label("ishuman") + player(ChatAnim.WEREWOLF_SUSPICIOUS, "Grrr - you don't belong in here, human!") + + label("skullballinprogress") + options( + DialogueOption("explainskullball", "What are the instructions for using the skullball course?", expression=ChatAnim.THINKING), + DialogueOption("lostskullball", "I seem to have lost my ball - can I have another one?", expression=ChatAnim.THINKING), + DialogueOption("clearskullball", "I give up, I can't do it - take my ball away.", expression=ChatAnim.NEUTRAL), + ) + + label("noskullball") + options( + DialogueOption("startskullball", "I would like to do the skullball course.", expression=ChatAnim.NEUTRAL), + DialogueOption("explainskullball", "What are the instructions for using the skullball course?", expression=ChatAnim.THINKING), + ) + + label("startskullball") + exec { player, npc -> + SkullballCourse.startBall(player) + } + + + label("lostskullball") + npc(ChatAnim.WEREWOLF_NEUTRAL, "No problem, here's another one. You'll have to start from the beginning again, but the timer will be restarted too.") + exec { player, npc -> + SkullballCourse.clearBall(player) + SkullballCourse.startBall(player) + } + + label("clearskullball") + npc(ChatAnim.WEREWOLF_NEUTRAL, "Oh dear, such a defeatist.") + exec { player, npc -> + SkullballCourse.clearBall(player) + } + + label("explainskullball") + npc(ChatAnim.WEREWOLF_NEUTRAL, "The skullball comes out of one of these four spawnholes. Just kick the ball through the middle of each goal, through the skeleton's feet.") + npc(ChatAnim.WEREWOLF_NEUTRAL, "There are 10 goals, which you must complete in order, and one final goal.") + npc(ChatAnim.WEREWOLF_NEUTRAL, "An arrow will point to your ball, just in case lots of people are using the course at the same time as yourself.") + npc(ChatAnim.WEREWOLF_NEUTRAL, "The better your time, the more agility XP you will be awarded. The timer starts when you score your first goal.") + } +} diff --git a/Server/src/main/content/region/morytania/werewolfagility/SkullballCourse.kt b/Server/src/main/content/region/morytania/werewolfagility/SkullballCourse.kt new file mode 100644 index 000000000..fd94c9033 --- /dev/null +++ b/Server/src/main/content/region/morytania/werewolfagility/SkullballCourse.kt @@ -0,0 +1,328 @@ +package content.region.morytania.werewolfagility + +import content.region.morytania.werewolfagility.SkullballCourse.Companion.skullballGoals +import core.api.* +import core.game.interaction.InteractionListener +import core.game.interaction.QueueStrength +import core.game.node.entity.Entity +import core.game.node.entity.npc.NPC +import core.game.node.entity.npc.NPCBehavior +import core.game.node.entity.player.Player +import core.game.node.entity.skill.Skills +import core.game.world.map.Direction +import core.game.world.map.Location +import core.game.world.map.RegionManager +import core.game.world.map.zone.ZoneBorders +import core.game.world.update.flag.context.Animation +import org.rs09.consts.NPCs + +class SkullballCourse : MapArea { + + companion object { + val attributeSkullballInstance = "skullball-instance" + val attributeSkullballCurrentGoal = "skullball-currentgoal" // 0 to 10 + val attributeSkullballStartTime = "skullball-starttime" + + val skullballGoalIface = 379 + + val startingBall = arrayOf( + Location.create(3552, 9859), + Location.create(3554, 9860), + Location.create(3555, 9860), + Location.create(3557, 9859), + ) + /** Array of ZoneBorders with Skullball Goals */ + val skullballGoals = arrayOf( + ZoneBorders(3555,9870,3555,9870), // rot 0 + ZoneBorders(3556,9883,3556,9883), + ZoneBorders(3558,9891,3558,9891), + ZoneBorders(3557,9900,3557,9900), + ZoneBorders(3558,9906,3558,9906), + ZoneBorders(3563,9911,3563,9911), // rot 1 + ZoneBorders(3575,9905,3575,9905), // rot 2 + ZoneBorders(3574,9888,3574,9888), + ZoneBorders(3575,9878,3575,9878), + ZoneBorders(3568,9864,3568,9864), // rot 3 + ZoneBorders(3563,9865,3563,9865), // End goal tunnel + ) + + /** Extract Location from ZoneBorders **/ + fun extractLoc(z :ZoneBorders): Location { + return Location(z.northEastX, z.northEastY, 0) + } + + /** Creates a skullball for the player to kick around. */ + fun startBall(player: Player) { + if (getAttribute(player, attributeSkullballInstance, null) == null) { + val npc = NPC(NPCs.SKULLBALL_1659) + setAttribute(npc, "target", player) + setAttribute(player, attributeSkullballInstance, npc) + npc.isRespawn = false + npc.isWalks = false + npc.location = startingBall.random() + npc.direction = Direction.NORTH + npc.walkRadius = 100 + npc.init() + clearHintIcon(player) + registerHintIcon(player, npc) + npc.lock(5) + // Force walk doesn't work here because this npc isn't flagged to be forced walked. + npc.walkingQueue.reset() + val newLoc = npc.location.transform(Location(0, 4, 0)) + npc.walkingQueue.addPath(newLoc.x, newLoc.y) + } + } + + /** Clears the skullball from the world for that player. */ + fun clearBall(player: Player) { + val npcBall = getAttribute(player, attributeSkullballInstance, null) + if (npcBall != null) { + clearHintIcon(player) + removeAttribute(npcBall, "target") + npcBall.clear() + removeAttribute(player, attributeSkullballCurrentGoal) + removeAttribute(player, attributeSkullballStartTime) + removeAttribute(player, attributeSkullballInstance) + } + } + + fun nearestWerewolfSay(loc: Location, chatText: String) { + var werewolfNpc = findLocalNPCs(loc, 40).filter { npc -> + npc.id == NPCs.SKULLBALL_TRAINER_1662 + }.sortedWith { a, b -> + a.location.getDistanceSquared(loc) - b.location.getDistanceSquared(loc) + }.getOrNull(0) + if (werewolfNpc != null) { + // println("werewolf ${werewolfNpc.location}") + sendChat(werewolfNpc, chatText) + } + } + + fun randomWerewolfSay(): String { + return listOf( + "You have truly gifted paws!", + "I've never seen anything like it!", + "Claws first - think later.", + "You need a few more skullball lessons.", + "Keep it up!", + "Don't give up the day job!", + "Look at @g[her,him] go!", + "Pathetic!", + "What - a - goal !!!", + "That was just plain lucky.", + ).random() + } + + fun randomZiplineWerewolfSay(): String { + return listOf( + "Give my regards to the ground...", + "Don't let the spikes or the blood put you off...", + "Now for a true test of teeth...", + ).random() + } + } + + override fun defineAreaBorders(): Array { + return skullballGoals.copyOf(skullballGoals.lastIndex).filterNotNull().toTypedArray() // remove last goal + } + + override fun areaLeave(entity: Entity, logout: Boolean) { + // When leaving the area with the goals, the scenery of the goal is one tile adjacent to it. + val surroundingScenery = + getScenery(entity.location.transform(1,0,0)) ?: + getScenery(entity.location.transform(0,1,0)) ?: + getScenery(entity.location.transform(-1,0,0)) ?: + getScenery(entity.location.transform(0,-1,0)) + // If that tile is the actual goal, + if (surroundingScenery != null && surroundingScenery.id == 5146) { + animateScenery(surroundingScenery, 1598) // anim 1599 is when skullball enters from behind. + + val player = getAttribute(entity, "target", null) + if (player == null) { return } + // On the first goal, start the time. + if (surroundingScenery.location.equals(extractLoc(skullballGoals[0]))) { + if (getAttribute(player, attributeSkullballStartTime, null) == null) { + setAttribute(player, attributeSkullballStartTime, System.currentTimeMillis()) + } + } + val currGoal = getAttribute(player, attributeSkullballCurrentGoal, 0) + if (surroundingScenery.location.equals(extractLoc(skullballGoals[currGoal]))) { + setAttribute(player, attributeSkullballCurrentGoal, currGoal + 1) + val nextGoal = getAttribute(player, attributeSkullballCurrentGoal, 0) + clearHintIcon(player) + if (nextGoal < skullballGoals.size) { + registerHintIcon(player, getScenery(extractLoc(skullballGoals[nextGoal]))!!) + } + if (nextGoal < skullballGoals.size) { + nearestWerewolfSay(entity.location, randomWerewolfSay()) + } else { + nearestWerewolfSay(entity.location, "@g[He,She] shoots - @g[He,She] scores!!!!!") + } + } + } + } +} + +class SkullballBehavior : NPCBehavior(NPCs.SKULLBALL_1659), InteractionListener, MapArea { + + companion object { + + /** Generates a movement queue for a kicked/pushed thing. Bounces against walls. **/ + private fun generatePath(npc: NPC, kickDirection: Location, distanceToMove: Int) { + npc.walkingQueue.reset() + var moveDirection = kickDirection + var nextTile = npc.location + // For each tile to move, + for (i in 1..distanceToMove) { + nextTile = nextTile.transform(moveDirection) + if (!(RegionManager.isTeleportPermitted(nextTile) || // walkable square + nextTile.equals(3563, 9865, 0) || // final goal + nextTile.equals(3563, 9866, 0) || // final goal + getScenery(nextTile)?.id == 5146 // skeleton goal + ) + ) { + // Tile is blocked, reverse ball direction and set a walkingQueue Path. + moveDirection = Location(-moveDirection.x, -moveDirection.y, moveDirection.z) + nextTile = nextTile.transform(moveDirection) + npc.walkingQueue.addPath(nextTile.x, nextTile.y) + nextTile = nextTile.transform(moveDirection) + if (nextTile.equals(3563, 9866, 0)) { break } + } + } + npc.walkingQueue.addPath(nextTile.x, nextTile.y) + } + + /** Moves the ball a certain distance (1,4,9 as authentic). */ + fun moveBall(player: Player, ballNpc: NPC, distance: Int) { + if (getAttribute(ballNpc, "target", null)?.username == player.username) { + clearHintIcon(player) + registerHintIcon(player, ballNpc) + animate(player, 1606) + generatePath(ballNpc, Location.getDelta(player.location, ballNpc.location), distance) + } else { + sendMessage(player, "That is not your skullball.") + } + } + /** Show current goal to score. */ + fun showGoal(player: Player, ballNpc: NPC) { + if (getAttribute(ballNpc, "target", null)?.username == player.username) { + val currGoal = getAttribute(player, SkullballCourse.attributeSkullballCurrentGoal, 0) + clearHintIcon(player) + if (currGoal < skullballGoals.size) { + registerHintIcon(player, getScenery(SkullballCourse.extractLoc(SkullballCourse.skullballGoals[currGoal]))!!) + } + } else { + sendMessage(player, "That is not your skullball.") + } + } + + /** Calculate the amount of time from the first goal to the final goal. */ + fun calcTime(player: Player) : String { + val startTime = getAttribute(player, SkullballCourse.attributeSkullballStartTime, System.currentTimeMillis()) + val endTime = System.currentTimeMillis() + val timeDiffInSecs = (endTime - startTime) / 1000 + val finalMins = timeDiffInSecs / 60 + val finalSecs = timeDiffInSecs % 60 + return String.format("%01d:%02d", finalMins, finalSecs) + } + + /** Calculate the amount exp earned when kicked into the last goal. */ + fun calcExp(player: Player) : Int { + val startTime = getAttribute(player, SkullballCourse.attributeSkullballStartTime, System.currentTimeMillis()) + val endTime = System.currentTimeMillis() + val timeDiffInSecs = (((endTime - startTime) / 1000) - 240).toInt().coerceAtLeast(0) + return (750 - timeDiffInSecs / 3).coerceAtLeast(0) // Kotlin what the fuck is this function + } + } + + var clearTime = 0 + + override fun onRemoval(self: NPC) { + clearTime = 0 + } + + override fun tick(self: NPC): Boolean { + // You have 800 ticks = 8 mins to kick the ball into the goal. + if (clearTime++ > 800) { + clearTime = 0 + val player = getAttribute(self, "target", null) + if (player != null) { + removeAttribute(player, SkullballCourse.attributeSkullballInstance) + } + removeAttribute(self, "target") + self.clear() + } + if (!self.location.equals(3563, 9866, 0)) { + return true + } + val player = getAttribute(self, "target", null) + if (player == null) { + return true + } + if (getAttribute(player, SkullballCourse.attributeSkullballCurrentGoal, 0) != 10) { + sendMessage(player, "You did not score all the goals.") + return true + } + sendMessage(player, "Well done - you've finished the skullball course!!!") + lock(player, Animation(1605).duration) + queueScript(player, 0, QueueStrength.SOFT) { stage: Int -> + when (stage) { + 0 -> { + animate(player, 1605) + return@queueScript delayScript(player, Animation(1605).duration) + } + + 1 -> { + setInterfaceText(player, calcTime(player), SkullballCourse.skullballGoalIface, 5) + val finalExp = calcExp(player) + setInterfaceText(player, finalExp.toString(), SkullballCourse.skullballGoalIface, 6) + rewardXP(player, Skills.AGILITY, finalExp.toDouble()) + + openInterface(player, SkullballCourse.skullballGoalIface) + + removeAttribute(player, SkullballCourse.attributeSkullballInstance) + removeAttribute(player, SkullballCourse.attributeSkullballCurrentGoal) + removeAttribute(player, SkullballCourse.attributeSkullballStartTime) + removeAttribute(self, "target") + self.clear() + return@queueScript stopExecuting(player) + } + + else -> return@queueScript stopExecuting(player) + } + } + return true + } + + override fun defineListeners() { + on(NPCs.SKULLBALL_1659, NPC, "tap") { player, node -> + moveBall(player, node as NPC, 1) + return@on true + } + + on(NPCs.SKULLBALL_1659, NPC, "kick") { player, node -> + moveBall(player, node as NPC, 4) + return@on true + } + + on(NPCs.SKULLBALL_1659, NPC, "shoot") { player, node -> + moveBall(player, node as NPC, 9) + return@on true + } + + on(NPCs.SKULLBALL_1659, NPC, "show-goal") { player, node -> + showGoal(player, node as NPC) + return@on true + } + } + + override fun defineAreaBorders(): Array { + return arrayOf(getRegionBorders(14234)) + } + + override fun areaLeave(entity: Entity, logout: Boolean) { + if (entity is Player) { + SkullballCourse.clearBall(entity) + } + } +} \ No newline at end of file diff --git a/Server/src/main/content/region/morytania/werewolfagility/SkullballTrainerDialogue.kt b/Server/src/main/content/region/morytania/werewolfagility/SkullballTrainerDialogue.kt new file mode 100644 index 000000000..33de25c63 --- /dev/null +++ b/Server/src/main/content/region/morytania/werewolfagility/SkullballTrainerDialogue.kt @@ -0,0 +1,66 @@ +package content.region.morytania.werewolfagility + +import core.api.* +import core.game.dialogue.ChatAnim +import core.game.dialogue.DialogueLabeller +import core.game.dialogue.DialoguePlugin +import core.game.node.entity.npc.NPC +import core.game.node.entity.player.Player +import core.plugin.Initializable +import org.rs09.consts.Items +import org.rs09.consts.NPCs + +@Initializable +class SkullballTrainerDialogue (player: Player? = null) : DialoguePlugin(player) { + override fun newInstance(player: Player): DialoguePlugin { + return SkullballTrainerDialogue(player) + } + override fun handle(interfaceId: Int, buttonId: Int): Boolean { + openDialogue(player, SkullballTrainerDialogueFile(), npc) + return false + } + override fun getIds(): IntArray { + return intArrayOf(NPCs.SKULLBALL_TRAINER_1662) + } +} +class SkullballTrainerDialogueFile : DialogueLabeller() { + override fun addConversation() { + assignToIds(NPCs.SKULLBALL_TRAINER_1662) + + exec { player, npc -> + if(!anyInEquipment(player, Items.RING_OF_CHAROS_4202, Items.RING_OF_CHAROSA_6465)) { + goto("ishuman") + } else if (getAttribute(player, SkullballCourse.attributeSkullballInstance, null) != null) { + goto("skullballinprogress") + } else { + goto("noskullball") + } + } + + label("ishuman") + player(ChatAnim.WEREWOLF_SUSPICIOUS, "Grrr - you don't belong in here, human!") + + label("noskullball") + player(ChatAnim.THINKING, "What is this place?") + npc(ChatAnim.WEREWOLF_NEUTRAL, "This is the Skullball Course") + npc(ChatAnim.WEREWOLF_NEUTRAL, "Go talk to the boss at the beginning of the course if you'd like a go.") + + // Eovq4QBY39c + label("skullballinprogress") + player(ChatAnim.THINKING, "How many goals have I got left?") + exec { player, npc -> + if (getAttribute(player, SkullballCourse.attributeSkullballCurrentGoal, 0) != 10) { + goto("xgoals") + } else { + goto("finalgoal") + } + } + + label("xgoals") + npc(ChatAnim.WEREWOLF_NEUTRAL, "You have ${10 - getAttribute(player!!, + SkullballCourse.attributeSkullballCurrentGoal, 0)} goals left to complete.") + + label("finalgoal") + npc(ChatAnim.WEREWOLF_NEUTRAL, "You have the final goal left to complete.") + } +} \ No newline at end of file diff --git a/Server/src/main/content/region/morytania/werewolfagility/WerewolfGuardDialogue.kt b/Server/src/main/content/region/morytania/werewolfagility/WerewolfGuardDialogue.kt new file mode 100644 index 000000000..718946602 --- /dev/null +++ b/Server/src/main/content/region/morytania/werewolfagility/WerewolfGuardDialogue.kt @@ -0,0 +1,59 @@ +package content.region.morytania.werewolfagility + +import core.api.* +import core.game.dialogue.DialogueBuilder +import core.game.dialogue.DialogueBuilderFile +import core.game.dialogue.DialoguePlugin +import core.game.dialogue.FacialExpression +import core.game.node.entity.player.Player +import core.plugin.Initializable +import org.rs09.consts.Items +import org.rs09.consts.NPCs + +@Initializable +class WerewolfGuardDialogue (player: Player? = null) : DialoguePlugin(player) { + override fun handle(interfaceId: Int, buttonId: Int): Boolean { + openDialogue(player, WerewolfGuardDialogueFile(), npc) + return true + } + override fun newInstance(player: Player): DialoguePlugin { + return WerewolfGuardDialogue(player) + } + override fun getIds(): IntArray { + return intArrayOf(NPCs.WEREWOLF_1665) + } +} + +class WerewolfGuardDialogueFile : DialogueBuilderFile() { + override fun create(b: DialogueBuilder) { + b.onPredicate { _ -> true } + .playerl(FacialExpression.FRIENDLY, "What's beneath the trapdoor?") + .branch { player -> if (anyInEquipment(player, Items.RING_OF_CHAROS_4202, Items.RING_OF_CHAROSA_6465)) { 1 } else { 0 } } + .let { branch -> + branch.onValue(0) +// .let { builder -> +// val returnJoin = b.placeholder() +// returnJoin.builder() +// .manualStage { _, player, _, _ -> +// sendNPCDialogue(player, NPCs.WEREWOLF_1665, "Face. " + expCount, expCount) +// expCount++ +// } +// .goto(returnJoin) +// } + .npcl(FacialExpression.WEREWOLF_NEUTRAL, "That's none of your business, human, and I'll never tell.") + .playerl(FacialExpression.FRIENDLY, "Oh, come on - I'm only curious.") + .npcl(FacialExpression.WEREWOLF_NEUTRAL, "If it wasn't my duty to stand here and guard our agility course from the likes of you, I would be relieving you of your life right now.") + .playerl(FacialExpression.THINKING, "So it's an agility course, then?") + .npcl(FacialExpression.WEREWOLF_SUSPICIOUS, "No ... yes ... oh blast - you didn't hear me say anything, right?") + .playerl(FacialExpression.FRIENDLY, "No problem. Can I come in?") + .npcl(FacialExpression.WEREWOLF_NEUTRAL, "No, human - it's werewolves only.") + .end() + + branch.onValue(1) + .npcl(FacialExpression.WEREWOLF_NEUTRAL, "It's an agility course designed for lycanthropes like ourselves, my friend.") + .playerl(FacialExpression.FRIENDLY, "Can I come in and use it?") + .npc(FacialExpression.WEREWOLF_HAPPY, "Certainly. The cavern contains two courses - on the", "west side is a level 60 Agility Course, and on the east", "side is a level 25 Skullball Course.") + .end() + } + } +} \ No newline at end of file diff --git a/Server/src/main/core/game/dialogue/FacialExpression.java b/Server/src/main/core/game/dialogue/FacialExpression.java index 6d415cf0f..5153ad7c0 100644 --- a/Server/src/main/core/game/dialogue/FacialExpression.java +++ b/Server/src/main/core/game/dialogue/FacialExpression.java @@ -89,6 +89,13 @@ public enum FacialExpression { STRUGGLE(9865), //TODO: More? //9855-9857 are like disgusted? does it just repeat after this? + //Chatheads for werewolves + WEREWOLF_SAD(6550), + WEREWOLF_NEUTRAL(6551), + WEREWOLF_SUSPICIOUS(6552), + WEREWOLF_THINKING(6553), + WEREWOLF_HAPPY(6555), + //Child Chathead? CHILD_ANGRY(7168), CHILD_SIDE_EYE(7169), diff --git a/Server/src/main/core/game/world/map/Location.java b/Server/src/main/core/game/world/map/Location.java index ea7d1794c..ed0927280 100644 --- a/Server/src/main/core/game/world/map/Location.java +++ b/Server/src/main/core/game/world/map/Location.java @@ -261,6 +261,17 @@ public final class Location extends Node { return Math.sqrt(xdiff * xdiff + ydiff * ydiff); } + /** + * Returns the distance between you and the other squared. This removes the square root for comparison functions. + * @param other The other location. + * @return The amount of distance between you and other, squared. + */ + public int getDistanceSquared(Location other) { + int xdiff = this.getX() - other.getX(); + int ydiff = this.getY() - other.getY(); + return xdiff * xdiff + ydiff * ydiff; + } + /** * Returns the distance between the first and the second specified distance. * @param first The first location.