Merge branch 'werewolf-course-yoink' into 'master'

Werewolf Course

See merge request 2009scape/2009scape!1976
This commit is contained in:
Oven Bread 2025-11-29 02:57:45 +00:00
commit a144513751
11 changed files with 939 additions and 10 deletions

View file

@ -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}-"

View file

@ -17,14 +17,12 @@ class MorytaniaArea : MapArea {
override fun defineAreaBorders(): Array<ZoneBorders> {
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)
}

View file

@ -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()

View file

@ -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<ZoneBorders> {
// 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)
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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<NPC?>(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.")
}
}

View file

@ -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<NPC?>(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<NPC?>(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<ZoneBorders> {
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<Player?>(entity, "target", null)
if (player == null) { return }
// On the first goal, start the time.
if (surroundingScenery.location.equals(extractLoc(skullballGoals[0]))) {
if (getAttribute<Long?>(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<Player?>(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<Player?>(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<Player?>(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<Player?>(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<ZoneBorders> {
return arrayOf(getRegionBorders(14234))
}
override fun areaLeave(entity: Entity, logout: Boolean) {
if (entity is Player) {
SkullballCourse.clearBall(entity)
}
}
}

View file

@ -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<NPC?>(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.")
}
}

View file

@ -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()
}
}
}

View file

@ -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),

View file

@ -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.