Merge branch 2009scape:master into master

This commit is contained in:
Tooze 2026-04-04 20:57:59 +00:00
commit ee175c96c4
69 changed files with 3631 additions and 121 deletions

View file

@ -132205,5 +132205,15 @@
"examine": "Emperor of all he surveys.",
"name": "Penguin",
"id": "12764"
},
{
"examine": "This key opens a chest in the Carnillean household.",
"name": "Chest key",
"id": "709"
},
{
"examine": "This stuff looks nasty.",
"name": "Poison",
"id": "273"
}
]

View file

@ -887,6 +887,10 @@
"region": "10390",
"id": "102"
},
{
"region": "10391",
"id": "478"
},
{
"region": "10393",
"id": "346"

View file

@ -9386,6 +9386,7 @@
"attack_level": "1"
},
{
"examine": "On special duty to protect the Canilleans.",
"name": "Guard",
"defence_level": "1",
"safespot": null,
@ -9398,18 +9399,19 @@
"attack_level": "1"
},
{
"examine": "A member of the Hazeel cult.",
"melee_animation": "811",
"respawn_delay": "60",
"defence_animation": "425",
"death_animation": "836",
"name": "Clivet",
"defence_level": "1",
"safespot": null,
"lifepoints": "25",
"melee_animation": "811",
"strength_level": "1",
"id": "893",
"range_level": "1",
"respawn_delay": "60",
"attack_level": "1",
"defence_animation": "425"
"attack_level": "1"
},
{
"examine": "A member of the Hazeel cult.",
@ -74201,6 +74203,7 @@
"id": "880"
},
{
"examine": "The Carnillean family chef.",
"name": "Claus the chef",
"id": "886"
},
@ -74209,14 +74212,17 @@
"id": "888"
},
{
"examine": "Ceril Carnillean's wife.",
"name": "Henryeta Carnillean",
"id": "889"
},
{
"examine": "The Carnellian family butler.",
"name": "Butler Jones",
"id": "890"
},
{
"examine": "Leader of the Hazeel cult.",
"name": "Alomone",
"id": "891"
},
@ -77673,6 +77679,7 @@
"id": "3311"
},
{
"examine": "They're going to sing to you!",
"name": "Zanaris choir",
"id": "3312"
},
@ -83339,6 +83346,7 @@
"id": "6741"
},
{
"examine": "A traditional snowman.",
"name": "Snowman",
"id": "6746"
},
@ -87576,6 +87584,7 @@
"id": "8516"
},
{
"examine": "The rebellious teenage son of the Queen of Snow. Interesting outfit!",
"name": "Jack Frost",
"id": "8517"
},
@ -87608,6 +87617,7 @@
"id": "8539"
},
{
"examine": "A jolly-looking present-distributor.",
"name": "Santa Claus",
"id": "8540"
},
@ -88007,5 +88017,10 @@
"examine": "Angry and stressed, he doesn't seem to have time for anything other than sand.",
"name": "Sandy",
"id": "3112"
},
{
"examine": "I hope she can find all the ingredients!",
"name": "Cook",
"id": "4239"
}
]

View file

@ -2573,7 +2573,11 @@
},
{
"npc_id": "885",
"loc_data": "{2565,3271,0,0,0}-{2573,3268,1,0,0}-"
"loc_data": "{2565,3271,0,1,0}-{2573,3268,1,1,0}-"
},
{
"npc_id": "886",
"loc_data": "{2567,9669,0,1,0}-"
},
{
"npc_id": "887",
@ -2581,15 +2585,27 @@
},
{
"npc_id": "888",
"loc_data": "{2565,3273,0,0,0}-"
"loc_data": "{2565,3273,0,1,0}-"
},
{
"npc_id": "889",
"loc_data": "{2571,3270,0,0,0}-"
"loc_data": "{2571,3270,0,1,0}-"
},
{
"npc_id": "890",
"loc_data": "{2569,3272,0,0,0}-{2573,3269,1,0,0}-"
"loc_data": "{2569,3272,0,1,0}-{2573,3269,1,1,0}-"
},
{
"npc_id": "891",
"loc_data": "{2609,9670,0,1,0}-"
},
{
"npc_id": "893",
"loc_data": "{2567,9683,0,1,0}-"
},
{
"npc_id": "894",
"loc_data": "{2609,9686,0,1,0}-{2607,9681,0,1,0}-{2609,9674,0,1,0}-{2608,9672,0,1,0}-{2604,9674,0,1,0}-{2605,9671,0,1,0}-"
},
{
"npc_id": "895",

View file

@ -0,0 +1,22 @@
package content.global.handlers.item
import core.api.getAttribute
import core.api.sendMessage
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import org.rs09.consts.Items
class RingForgingOperateListener: InteractionListener {
override fun defineListeners() {
on(Items.RING_OF_FORGING_2568, IntType.ITEM, "operate") { player, node ->
//Attribute set at SmeltingPulse.java
val charges = getAttribute(player, "ringOfForgingCharges", 140)
sendMessage(player, "You can smelt $charges more iron ore before the ring disintegrates.")
return@on true
}
}
}

View file

@ -5,6 +5,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.ServerConstants
import core.api.*
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -244,8 +245,9 @@ class GoblinBook : InteractionListener {
}
override fun defineListeners() {
on(Items.GOBLIN_BOOK_10999, IntType.ITEM, "read") { player, _ ->
on(Items.GOBLIN_BOOK_10999, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_26, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -5,6 +5,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -126,8 +127,9 @@ class SecurityBookPlugin : InteractionListener {
}
override fun defineListeners() {
on(Items.SECURITY_BOOK_9003, IntType.ITEM, "read") { player, _ ->
on(Items.SECURITY_BOOK_9003, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_2_27, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -5,6 +5,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -128,8 +129,9 @@ class StrongholdNotes : InteractionListener {
}
override fun defineListeners() {
on(Items.STRONGHOLD_NOTES_9004, IntType.ITEM, "read") { player, _ ->
on(Items.STRONGHOLD_NOTES_9004, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_2_27, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -1,5 +1,6 @@
package content.global.handlers.item.toys;
import content.global.skill.construction.decoration.pohstorage.StorableFamily;
import core.game.component.Component;
import core.game.component.ComponentDefinition;
import core.game.component.ComponentPlugin;
@ -46,7 +47,7 @@ public class DiangoReclaimInterface extends ComponentPlugin {
Item[] reclaimables = getEligibleItems(player);
player.setAttribute("diango-reclaimables", reclaimables);
//filter out items the player already has in their bank, inventory, or equipped
//filter out items the player already has in their bank, POH, inventory, or equipped
//only send items if there are some to send
if(reclaimables.length > 0) {
@ -90,7 +91,7 @@ public class DiangoReclaimInterface extends ComponentPlugin {
public static Item[] getEligibleItems (Player player) {
return ITEMS.stream().filter(Objects::nonNull)
.filter(item -> !player.getEquipment().containsItem(item) && !player.getInventory().containsItem(item) && !player.getBank().containsItem(item)
.filter(item -> !player.getEquipment().containsItem(item) && !player.getInventory().containsItem(item) && !player.getBank().containsItem(item) && !player.getPOHStorageState().getContainer(StorableFamily.TOY_BOX).contains(item.getId())
&& (item.getId() != 14654
|| (!(inInventory(player, 14655, 1) || inEquipment(player, 14656, 1)) && player.getAttribute("sotr:purchased",false))
))

View file

@ -0,0 +1,426 @@
package content.global.handlers.scenery
import content.global.skill.summoning.pet.Pets
import core.api.*
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.interaction.QueueStrength
import core.game.node.entity.combat.graves.GraveController
import core.game.node.entity.player.info.LogType
import core.game.node.entity.player.info.PlayerMonitor
import core.game.node.entity.player.info.login.PlayerParser
import core.game.node.item.GroundItem
import core.game.node.item.GroundItemManager
import core.game.node.item.Item
import core.game.system.config.ItemConfigParser
import core.game.world.map.Location
import core.game.world.update.flag.context.Animation
import org.rs09.consts.Items
import org.rs09.consts.Scenery
import org.rs09.consts.Sounds
/** To allow players to place items on tables. Copies DropListener for restrictions. */
class TableListener : InteractionListener {
companion object {
private val DROP_COINS_SOUND = Sounds.EYEGLO_COIN_10
private val DROP_ITEM_SOUND = Sounds.PUT_DOWN_2739
}
override fun defineListeners() {
onUseAnyWith(
IntType.SCENERY,
Scenery.TABLE_10258,
Scenery.TABLE_10414,
Scenery.TABLE_10490,
Scenery.TABLE_10509,
Scenery.TABLE_10808,
Scenery.TABLE_11202,
Scenery.TABLE_11203,
Scenery.TABLE_11500,
Scenery.TABLE_11501,
Scenery.TABLE_11502,
Scenery.TABLE_11503,
Scenery.TABLE_11504,
Scenery.TABLE_11551,
Scenery.TABLE_11619,
Scenery.TABLE_11660,
Scenery.TABLE_11682,
Scenery.TABLE_11743,
Scenery.TABLE_11748,
Scenery.TABLE_11749,
Scenery.TABLE_11849,
Scenery.TABLE_11892,
Scenery.TABLE_12106,
Scenery.TABLE_12108,
Scenery.TABLE_12386,
Scenery.TABLE_12387,
Scenery.TABLE_12388,
Scenery.TABLE_12450,
Scenery.TABLE_12541,
Scenery.TABLE_12544,
Scenery.TABLE_12734,
Scenery.TABLE_12855,
Scenery.TABLE_12884,
Scenery.TABLE_12962,
Scenery.TABLE_12971,
Scenery.TABLE_12972,
Scenery.TABLE_14044,
Scenery.TABLE_14049,
Scenery.TABLE_14163,
Scenery.TABLE_14363,
Scenery.TABLE_14762,
Scenery.TABLE_14763,
Scenery.TABLE_14920,
Scenery.TABLE_15044,
Scenery.TABLE_15092,
Scenery.TABLE_15137,
Scenery.TABLE_15680,
Scenery.TABLE_15684,
Scenery.TABLE_15719,
Scenery.TABLE_15982,
Scenery.TABLE_16169,
Scenery.TABLE_16170,
Scenery.TABLE_16562,
Scenery.TABLE_16737,
Scenery.TABLE_16738,
Scenery.TABLE_16739,
Scenery.TABLE_16740,
Scenery.TABLE_16741,
Scenery.TABLE_16742,
Scenery.TABLE_16815,
Scenery.TABLE_16827,
Scenery.TABLE_16828,
Scenery.TABLE_16874,
Scenery.TABLE_16974,
Scenery.TABLE_17099,
Scenery.TABLE_17145,
Scenery.TABLE_17235,
Scenery.TABLE_17299,
Scenery.TABLE_17348,
Scenery.TABLE_17545,
Scenery.TABLE_17546,
Scenery.TABLE_17661,
Scenery.TABLE_17662,
Scenery.TABLE_17666,
Scenery.TABLE_17762,
Scenery.TABLE_18051,
Scenery.TABLE_18059,
Scenery.TABLE_18062,
Scenery.TABLE_18314,
Scenery.TABLE_18486,
Scenery.TABLE_18902,
Scenery.TABLE_1901,
Scenery.TABLE_19158,
Scenery.TABLE_19928,
Scenery.TABLE_20058,
Scenery.TABLE_20060,
Scenery.TABLE_20351,
Scenery.TABLE_20388,
Scenery.TABLE_20996,
Scenery.TABLE_21359,
Scenery.TABLE_21360,
Scenery.TABLE_21388,
Scenery.TABLE_21389,
Scenery.TABLE_21608,
Scenery.TABLE_21609,
Scenery.TABLE_21610,
Scenery.TABLE_21793,
Scenery.TABLE_22159,
Scenery.TABLE_22160,
Scenery.TABLE_22431,
Scenery.TABLE_22672,
Scenery.TABLE_22677,
Scenery.TABLE_22678,
Scenery.TABLE_22690,
Scenery.TABLE_22693,
Scenery.TABLE_22694,
Scenery.TABLE_22705,
Scenery.TABLE_22830,
Scenery.TABLE_22831,
Scenery.TABLE_22832,
Scenery.TABLE_22844,
Scenery.TABLE_22845,
Scenery.TABLE_22846,
Scenery.TABLE_22847,
Scenery.TABLE_22848,
Scenery.TABLE_22849,
Scenery.TABLE_23177,
Scenery.TABLE_23942,
Scenery.TABLE_24011,
Scenery.TABLE_24012,
Scenery.TABLE_24013,
Scenery.TABLE_24014,
Scenery.TABLE_24028,
Scenery.TABLE_24076,
Scenery.TABLE_24084,
Scenery.TABLE_24088,
Scenery.TABLE_24104,
Scenery.TABLE_24301,
Scenery.TABLE_24302,
Scenery.TABLE_24303,
Scenery.TABLE_24307,
Scenery.TABLE_24308,
Scenery.TABLE_24309,
Scenery.TABLE_24320,
Scenery.TABLE_24335,
Scenery.TABLE_24336,
Scenery.TABLE_24667,
Scenery.TABLE_24684,
Scenery.TABLE_24900,
Scenery.TABLE_24922,
Scenery.TABLE_24927,
Scenery.TABLE_25047,
Scenery.TABLE_25603,
Scenery.TABLE_25615,
Scenery.TABLE_25733,
Scenery.TABLE_25734,
Scenery.TABLE_25778,
Scenery.TABLE_25928,
Scenery.TABLE_25930,
Scenery.TABLE_25931,
Scenery.TABLE_26020,
Scenery.TABLE_26074,
Scenery.TABLE_26142,
Scenery.TABLE_2650,
Scenery.TABLE_26783,
Scenery.TABLE_26807,
Scenery.TABLE_27014,
Scenery.TABLE_27052,
Scenery.TABLE_27056,
Scenery.TABLE_27083,
Scenery.TABLE_27245,
Scenery.TABLE_27850,
Scenery.TABLE_28199,
Scenery.TABLE_28262,
Scenery.TABLE_28508,
Scenery.TABLE_28621,
Scenery.TABLE_28628,
Scenery.TABLE_28629,
Scenery.TABLE_28660,
Scenery.TABLE_2906,
Scenery.TABLE_29131,
Scenery.TABLE_2940,
Scenery.TABLE_29467,
Scenery.TABLE_29468,
Scenery.TABLE_30074,
Scenery.TABLE_30075,
Scenery.TABLE_30076,
Scenery.TABLE_30077,
Scenery.TABLE_30078,
Scenery.TABLE_30079,
Scenery.TABLE_30080,
Scenery.TABLE_30081,
Scenery.TABLE_30222,
Scenery.TABLE_30734,
Scenery.TABLE_30827,
Scenery.TABLE_30828,
Scenery.TABLE_30830,
Scenery.TABLE_30939,
Scenery.TABLE_32121,
Scenery.TABLE_33052,
Scenery.TABLE_33054,
Scenery.TABLE_33129,
Scenery.TABLE_33159,
Scenery.TABLE_33201,
Scenery.TABLE_33202,
Scenery.TABLE_33233,
Scenery.TABLE_34204,
Scenery.TABLE_34429,
Scenery.TABLE_34431,
Scenery.TABLE_34469,
Scenery.TABLE_34470,
Scenery.TABLE_34473,
Scenery.TABLE_34474,
Scenery.TABLE_34518,
Scenery.TABLE_34519,
Scenery.TABLE_34520,
Scenery.TABLE_34521,
Scenery.TABLE_34799,
Scenery.TABLE_34927,
Scenery.TABLE_34968,
Scenery.TABLE_36255,
Scenery.TABLE_36350,
Scenery.TABLE_36366,
Scenery.TABLE_36573,
Scenery.TABLE_36574,
Scenery.TABLE_36575,
Scenery.TABLE_36576,
Scenery.TABLE_36577,
Scenery.TABLE_36578,
Scenery.TABLE_36579,
Scenery.TABLE_36580,
Scenery.TABLE_36581,
Scenery.TABLE_36582,
Scenery.TABLE_36583,
Scenery.TABLE_36584,
Scenery.TABLE_36585,
Scenery.TABLE_36586,
Scenery.TABLE_36727,
Scenery.TABLE_36728,
Scenery.TABLE_36730,
Scenery.TABLE_36731,
Scenery.TABLE_36820,
Scenery.TABLE_36821,
Scenery.TABLE_36910,
Scenery.TABLE_37160,
Scenery.TABLE_37161,
Scenery.TABLE_37162,
Scenery.TABLE_37163,
Scenery.TABLE_37164,
Scenery.TABLE_37165,
Scenery.TABLE_37166,
Scenery.TABLE_37167,
Scenery.TABLE_37350,
Scenery.TABLE_37419,
Scenery.TABLE_37815,
Scenery.TABLE_38020,
Scenery.TABLE_38021,
Scenery.TABLE_38102,
Scenery.TABLE_38316,
Scenery.TABLE_38317,
Scenery.TABLE_38318,
Scenery.TABLE_38319,
Scenery.TABLE_38320,
Scenery.TABLE_39167,
Scenery.TABLE_39168,
Scenery.TABLE_40030,
Scenery.TABLE_40031,
Scenery.TABLE_40818,
Scenery.TABLE_40819,
Scenery.TABLE_40820,
Scenery.TABLE_41105,
Scenery.TABLE_41106,
Scenery.TABLE_41107,
Scenery.TABLE_41196,
Scenery.TABLE_41289,
Scenery.TABLE_41462,
Scenery.TABLE_41469,
Scenery.TABLE_4272,
Scenery.TABLE_4372,
Scenery.TABLE_4458,
Scenery.TABLE_4459,
Scenery.TABLE_4460,
Scenery.TABLE_4461,
Scenery.TABLE_4462,
Scenery.TABLE_4463,
Scenery.TABLE_4464,
Scenery.TABLE_4651,
Scenery.TABLE_4652,
Scenery.TABLE_4653,
Scenery.TABLE_5349,
Scenery.TABLE_5879,
Scenery.TABLE_593,
Scenery.TABLE_594,
Scenery.TABLE_595,
Scenery.TABLE_596,
Scenery.TABLE_597,
Scenery.TABLE_598,
Scenery.TABLE_599,
Scenery.TABLE_600,
Scenery.TABLE_601,
Scenery.TABLE_602,
Scenery.TABLE_603,
Scenery.TABLE_604,
Scenery.TABLE_605,
Scenery.TABLE_606,
Scenery.TABLE_607,
Scenery.TABLE_6075,
Scenery.TABLE_613,
Scenery.TABLE_614,
Scenery.TABLE_616,
Scenery.TABLE_6196,
Scenery.TABLE_6197,
Scenery.TABLE_6198,
Scenery.TABLE_6199,
Scenery.TABLE_620,
Scenery.TABLE_6200,
Scenery.TABLE_6201,
Scenery.TABLE_621,
Scenery.TABLE_6211,
Scenery.TABLE_622,
Scenery.TABLE_623,
Scenery.TABLE_6231,
Scenery.TABLE_624,
Scenery.TABLE_6246,
Scenery.TABLE_7331,
Scenery.TABLE_8700,
Scenery.TABLE_8701,
Scenery.TABLE_8751,
Scenery.TABLE_8768,
Scenery.TABLE_8769,
Scenery.TABLE_9082,
Scenery.TABLE_9430,
Scenery.TABLE_9431,
Scenery.TABLE_9432,
Scenery.TABLE_9433,
Scenery.TABLE_9434,
Scenery.TABLE_9614,
Scenery.TABLE_9685,
Scenery.TABLE_9702
) { player, used, with ->
used as Item
with as core.game.node.scenery.Scenery
// Similar to DropListener.handleDropAction to keep it consistent
if (Pets.forId(used.id) != null) {
player.familiarManager.summon(used, true, true)
return@onUseAnyWith true
}
if (player.locks.equipmentLock != null) {
return@onUseAnyWith false
}
// determine if item can be dropped
var canDrop = false;
for (option in used.getDefinition().getOptions()) {
if (option != null && (option.contains("drop"))) {
canDrop = true
}
}
if (!canDrop) {
sendMessage(player, "You cannot place this item on the table!")
return@onUseAnyWith false
}
// Calculate nearest
val minX = with.location.x
val maxX = with.location.x + with.getSizeX() - 1
val minY = with.location.y
val maxY = with.location.y + with.getSizeY() - 1
val nearestX = when {
player.location.x < minX -> minX
player.location.x > maxX -> maxX
else -> player.location.x
}
val nearestY = when {
player.location.y < minY -> minY
player.location.y > maxY -> maxY
else -> player.location.y
}
// Similar to DropListener.handleDropAction to keep it consistent
closeAllInterfaces(player)
player.animate(Animation(537))
queueScript(player, strength = QueueStrength.SOFT) { //do this as a script to allow dropping multiple items in the same tick (authentic)
// It's possible for state to change between queueing the script and executing it at the end of the tick (https://forum.2009scape.org/viewtopic.php?f=8&t=1195-lost-bandos-chestplate-whilst-making-iron-titans&p=5292). So, sanity check:
val current = player.inventory.get(used.slot)
if (current == null || current !== used) {
return@queueScript stopExecuting(player)
}
if (player.inventory.replace(null, used.slot) !== used) {
PlayerMonitor.log(player, LogType.DUPE_ALERT, "Potential exploit attempt when player ${player.name} tried to place on table ${used.amount}x ${used.id}. The item has been lost and will need to be refunded to the player, but how did they get this to happen?")
return@queueScript stopExecuting(player)
}
val droppedItem = used.dropItem
if (droppedItem.id == Items.COINS_995) playAudio(player, DROP_COINS_SOUND) else playAudio(player, DROP_ITEM_SOUND)
GroundItemManager.create(droppedItem, Location(nearestX, nearestY), player)
setAttribute(player, "droppedItem:${droppedItem.id}", getWorldTicks() + 2)
PlayerParser.save(player)
return@queueScript stopExecuting(player)
}
return@onUseAnyWith true
}
}
}

View file

@ -1,6 +1,7 @@
package content.global.skill.construction
import core.api.sendMessage
import core.api.storeBookInHouse
import core.game.dialogue.DialogueInterpreter
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
@ -52,7 +53,7 @@ class ConstructionGuideBook : InteractionListener {
override fun defineListeners() {
// There is supposedly a book here.
on(Items.CONSTRUCTION_GUIDE_8463, IntType.ITEM, "read") { player, _ ->
on(Items.CONSTRUCTION_GUIDE_8463, IntType.ITEM, "read") { player, node ->
if (settings!!.isDevMode && settings!!.isBeta) {
for (item in RESOURCES) {
if (!player.inventory.contains(item.id, item.amount)) {
@ -60,6 +61,7 @@ class ConstructionGuideBook : InteractionListener {
}
}
}
storeBookInHouse(player, node)
sendMessage(player, "Upon reading the book you discover you're supposed to use these resources to test out construction. Report all bugs on the forums.")
return@on true
}

View file

@ -2,6 +2,7 @@ package content.global.skill.construction;
import core.api.Container;
import core.game.world.map.Location;
import org.rs09.consts.Items;
import core.game.node.entity.Entity;
import core.game.node.entity.player.Player;
@ -95,6 +96,28 @@ public final class HouseZone extends MapZone {
public boolean leave(Entity e, boolean logout) {
if (e instanceof Player) {
Player p = (Player) e;
// if the player is moving between the dungeon and main house, don't trigger the leave sequence
if (!logout) {
// current loc
Location dest = p.getProperties().getTeleportLocation();
int currentRegionId = p.getLocation().getRegionId();
int houseId = house.getHouseRegion().getId();
int dungeonId = (house.getDungeonRegion() != null) ? house.getDungeonRegion().getId() : -1;
// check if still in house
boolean currentlyInHouse = (currentRegionId == houseId || currentRegionId == dungeonId);
// check if moving between house regions
int destRegionId = (dest != null) ? dest.getRegionId() : -1;
boolean movingToHouse = (destRegionId == houseId || destRegionId == dungeonId);
// if yes, return true and stop the leave sequence
if (currentlyInHouse || movingToHouse) {
return true;
}
}
remove_items(p);
// The below tears down the house if the owner was the one who left
if (house == p.getHouseManager()) {
@ -107,11 +130,11 @@ public final class HouseZone extends MapZone {
Region dr = dungRemove != -1 ? RegionManager.forId(dungRemove) : null;
RegionManager.removeRegion(toRemove);
unregisterRegion(toRemove);
r.setActive(false);
r.flagInactive();
if (dungRemove != -1) {
RegionManager.removeRegion(dungRemove);
unregisterRegion(dungRemove);
dr.setActive(false);
dr.flagInactive();
}
return true;
}

View file

@ -3,12 +3,16 @@ package content.global.skill.construction;
import core.game.node.entity.player.Player;
import core.game.node.entity.skill.Skills;
import java.util.Arrays;
/**
* The styles of houses.
* @author Emperor
*
*/
public enum HousingStyle {
// open door ids are doorId + 1
BASIC_WOOD (1, 5000, 7503, 0, 13100, 13101, 13098, Decoration.BASIC_WOOD_WINDOW),
BASIC_STONE (10, 5000, 7503, 1, 13094, 13096, 1902, Decoration.BASIC_STONE_WINDOW),
@ -16,6 +20,28 @@ public enum HousingStyle {
FREMENNIK_STYLE_WOOD(30, 10000, 7503, 3, 13109, 13107, 13111, Decoration.FREMENNIK_WINDOW),
TROPICAL_WOOD (40, 15000, 7759, 0, 13016, 13015, 13011, Decoration.TROPICAL_WOOD_WINDOW),
FANCY_STONE (50, 25000, 7759, 1, 13119, 13118, 13116, Decoration.FANCY_STONE_WINDOW);
/**
* Array of all Dungeon Wall IDs.
* From Region 7503, Location.create(1898, 5084, 0)
*/
private static final int[] DUNGEON_WALL_IDS = {
13019, 13020, 13021, 13022, 13023, 13024, 13025, 13026, 13027, 13028,
13029, 13030, 13031, 13032, 13033, 13034, 13035, 13036, 13037, 13046,
13048, 13049, 13050, 13051, 13055, 13056, 13058, 13059, 13060, 13061,
13062, 13063, 13065, 13066, 13067, 13068, 13069, 13070, 13072, 13073,
13074, 13075, 13076, 13077, 13079, 13080, 13081, 13082, 13083, 13084,
13086, 13087, 13088, 13089
};
/**
* Checks if the provided ID is a dungeon wall.
* @param id The object ID.
* @return {@code true} if it's a dungeon wall.
*/
public static boolean isDungeonWall(int id) {
return Arrays.binarySearch(DUNGEON_WALL_IDS, id) >= 0;
}
/**
* The level required.

View file

@ -176,9 +176,10 @@ public final class Room {
for (int i = 0; i < BuildRegionChunk.ARRAY_SIZE; i++) {
Scenery object = chunk.get(x, y, i);
if (object != null) {
int id = object.getId();
boolean isBuilt = object instanceof Constructed;
boolean isWall = object.getId() == 13065 || object.getId() == house.getStyle().getWallId();
boolean isDoor = object.getId() == house.getStyle().getDoorId() || object.getId() == house.getStyle().getSecondDoorId();
boolean isWall = HousingStyle.isDungeonWall(id) || id == house.getStyle().getWallId();
boolean isDoor = id == house.getStyle().getDoorId() || id == house.getStyle().getSecondDoorId();
if (!isBuilt && !isWall && !isDoor) {
SceneryBuilder.remove(object);
chunk.remove(object);

View file

@ -1,29 +0,0 @@
package content.global.skill.construction.decoration.costume
import core.cache.def.impl.SceneryDefinition
import core.game.interaction.OptionHandler
import content.global.handlers.item.toys.DiangoReclaimInterface
import core.game.node.Node
import core.game.node.entity.player.Player
import core.plugin.Initializable
import core.plugin.Plugin
/**
* Handles the Toy Box POH
* ToyBoxPlugin.java
* @author Lee
* @date 10/2/2017
*/
@Initializable
class ToyBoxPlugin : OptionHandler() {
@Throws(Throwable::class)
override fun newInstance(arg: Any?): Plugin<Any?> {
SceneryDefinition.forId(18802).handlers["option:open"] = this
return this
}
override fun handle(player: Player, node: Node, option: String): Boolean {
DiangoReclaimInterface.open(player)
return true
}
}

View file

@ -0,0 +1,28 @@
package content.global.skill.construction.decoration.pohstorage;
import core.game.container.Container;
import core.game.container.ContainerType;
import core.game.node.entity.player.Player;
import core.game.node.item.Item;
/**
* A proxy Container used to indicate an item was found in POH storage.
* This is not a real container, just a dummy so that hasAnItem() can be easily extended to check POH storage
*/
public class POHStorageProxyContainer extends Container {
public POHStorageProxyContainer(Player player) {
super(1, ContainerType.DEFAULT);
}
// Prevents external code from trying to fetch items from this container because it doesn't actually store anything
@Override
public Item get(int slot) {
return null;
}
@Override
public String toString() {
return "POHStorageProxyContainer";
}
}

View file

@ -0,0 +1,444 @@
package content.global.skill.construction.decoration.pohstorage
import content.data.Quests
import core.api.getItemName
import core.api.isQuestComplete
import core.game.node.entity.player.Player
import core.game.world.GameWorld
import org.rs09.consts.Items
/**
* Represents a database of all the storable items in the POH Costume Room and Bookcase across all box types.
*/
/*
* The below function should be copied into each book's listener to store the book in the POH. It is part of ContentAPI.
* import core.api.*
* storeBookInHouse(player, node)
*
* Example: src/main/content/region/kandarin/quest/observatoryquest/AstronomyBook.kt
*
* todo: Periodically, this storables list should be audited as new books are added to the game. Make sure the new books are going in the bookcase.
*/
// todo: Right now there is no logic to check:
// 1. for complete or incomplete sets
// 2. for single instances of an item
// This allows things like storing multiple god capes, a Rune (g) set with both platelegs and plateskirt,
// an elegant shirt/pants and blouse/skirt at once, and all 3 sets of castle wars armour at once. I don't
// think any of this is game-breaking, so have chosen to omit this logic for now.
enum class Storable(
val displayId: Int, // display ID is the item that is shown in the UI
val takeIds: IntArray, // take IDs is the item(s) that are taken from the player's inventory
val type: StorableFamily, // the family (mapped to container) that the items get stored in
val setName: String = getItemName(displayId), // the set/book name for each item. defaults to display item
// special properties just for the books
val bitIndex: Int = -1, // This index is used to save the big list of books the player has read, instead of having an individual attribute per book.
// todo make the quest requirement check part of a separate object against this list of books. only a subset of books are applicable.
val questReq: Quests? = null // Once read, books should only show up in the POH after completing their attached quest. https://runescape.wiki/w/Bookcase?oldid=1650176
) {
// Books - https://runescape.wiki/w/List_of_texts_and_tomes?oldid=848552
AbyssalBook(Items.ABYSSAL_BOOK_5520, intArrayOf(Items.ABYSSAL_BOOK_5520), type = StorableFamily.BOOKCASE, "Abyssal Research Notes", bitIndex = 0), // abyss miniquest - IMPLEMENTED
AncientBook(Items.ANCIENT_BOOK_7633, intArrayOf(Items.ANCIENT_BOOK_7633), type = StorableFamily.BOOKCASE, "The Sleeping Seven", bitIndex = 1, Quests.HORROR_FROM_THE_DEEP), // horror from the deep quest
AncientDiary(Items.DIARY_3846, intArrayOf(Items.DIARY_3846), type = StorableFamily.BOOKCASE, "Ancient Diary", bitIndex = 2, Quests.HORROR_FROM_THE_DEEP), // horror from the deep quest
ArenaBook(Items.ARENA_BOOK_6891, intArrayOf(Items.ARENA_BOOK_6891), type = StorableFamily.BOOKCASE, "Magic Training Arena Lore Book", bitIndex = 3), // mage training arena minigame
AstronomyBook(Items.ASTRONOMY_BOOK_600, intArrayOf(Items.ASTRONOMY_BOOK_600), type = StorableFamily.BOOKCASE, "Astronomy Book", bitIndex = 4, Quests.OBSERVATORY_QUEST), // observatory quest - IMPLEMENTED
BatteredBook(Items.BATTERED_BOOK_2886, intArrayOf(Items.BATTERED_BOOK_2886), type = StorableFamily.BOOKCASE, "Book of the Elemental Shield", bitIndex = 5, Quests.ELEMENTAL_WORKSHOP_I), // elemental workshop I quest - IMPLEMENTED
BatteredTome(Items.BATTERED_TOME_7634, intArrayOf(Items.BATTERED_TOME_7634), type = StorableFamily.BOOKCASE, "Histories of Hallowland", bitIndex = 6, Quests.IN_AID_OF_THE_MYREQUE), // in aid of myreque quest
BeatenBook(Items.BEATEN_BOOK_9717, intArrayOf(Items.BEATEN_BOOK_9717), type = StorableFamily.BOOKCASE, "Book of the Elemental Helm", bitIndex = 7, Quests.ELEMENTAL_WORKSHOP_II), // elemental workshop II quest
BigBookOfBangs(Items.BIG_BOOK_OF_BANGS_3230, intArrayOf(Items.BIG_BOOK_OF_BANGS_3230), type = StorableFamily.BOOKCASE, "Big Book of Bangs", bitIndex = 8, Quests.REGICIDE), // regicide quest
BindingBook(Items.BINDING_BOOK_730, intArrayOf(Items.BINDING_BOOK_730), type = StorableFamily.BOOKCASE, "Book of Binding", bitIndex = 9, Quests.LEGENDS_QUEST), // legends quest - IMPLEMENTED
BirdBook(Items.BIRD_BOOK_10173, intArrayOf(Items.BIRD_BOOK_10173), type = StorableFamily.BOOKCASE, "William Oddity's Guide to the Avian", bitIndex = 10, Quests.EAGLES_PEAK), // eagles peak quest
BookOnBaxtorian(Items.BOOK_ON_BAXTORIAN_292, intArrayOf(Items.BOOK_ON_BAXTORIAN_292), type = StorableFamily.BOOKCASE, "Book on Baxtorian", bitIndex = 11, Quests.WATERFALL_QUEST), // waterfall quest - IMPLEMENTED
BookOnChemicals(Items.BOOK_ON_CHEMICALS_711, intArrayOf(Items.BOOK_ON_CHEMICALS_711), type = StorableFamily.BOOKCASE, "Volatile chemicals - Experimental Test Notes", bitIndex = 12, Quests.THE_DIG_SITE), // dig site quest - IMPLEMENTED
BookOPiracy(Items.BOOK_O_PIRACY_7144, intArrayOf(Items.BOOK_O_PIRACY_7144), type = StorableFamily.BOOKCASE, "Book o' Piracy", bitIndex = 13, Quests.CABIN_FEVER), // cabin fever quest
BrewinGuide(Items.BREWIN_GUIDE_8989, intArrayOf(Items.BREWIN_GUIDE_8989), type = StorableFamily.BOOKCASE, "Brewin' Guide", bitIndex = 14), // trouble brewing quest/minigame
BurntDiary(Items.BURNT_DIARY_7965, intArrayOf(Items.BURNT_DIARY_7965), type = StorableFamily.BOOKCASE, "Burnt Diary", bitIndex = 15, Quests.ROYAL_TROUBLE), // royal trouble quest
CadarnLineage(Items.CADARN_LINEAGE_4209, intArrayOf(Items.CADARN_LINEAGE_4209), type = StorableFamily.BOOKCASE, "Cadarn Lineage", bitIndex = 16, Quests.ROVING_ELVES), // roving elves quest
ClockworkBook(Items.CLOCKWORK_BOOK_10594, intArrayOf(Items.CLOCKWORK_BOOK_10594), type = StorableFamily.BOOKCASE, "Clockwork Toys - A Clockwork Mechanism, Chapter 1.0", bitIndex = 17, Quests.COLD_WAR), // cold war quest
CocktailGuide(Items.COCKTAIL_GUIDE_2023, intArrayOf(Items.COCKTAIL_GUIDE_2023), type = StorableFamily.BOOKCASE, "The Blurberry Cocktail Guide", bitIndex = 18), // gnome restaurant minigame
ConstructionGuide(Items.CONSTRUCTION_GUIDE_8463, intArrayOf(Items.CONSTRUCTION_GUIDE_8463), type = StorableFamily.BOOKCASE, "Guide to Construction", bitIndex = 19), // after you buy a house from estate agent - IMPLEMENTED ish
CrumblingTome(Items.CRUMBLING_TOME_4707, intArrayOf(Items.CRUMBLING_TOME_4707), type = StorableFamily.BOOKCASE, "Legend of the Brothers", bitIndex = 20), // barrows minigame
CrystalSinging(Items.CRYSTAL_OF_SEREN_4313, intArrayOf(Items.CRYSTAL_OF_SEREN_4313), type = StorableFamily.BOOKCASE, "Crystal singing for beginners", bitIndex = 21, Quests.ROVING_ELVES), // roving elves quest
DagonhaiHistory(Items.DAGONHAI_HISTORY_11001, intArrayOf(Items.DAGONHAI_HISTORY_11001), type = StorableFamily.BOOKCASE, "History of the Dagon'hai", bitIndex = 22), // found in varrock castle library
DemonicTome(Items.DEMONIC_TOME_6749, intArrayOf(Items.DEMONIC_TOME_6749), type = StorableFamily.BOOKCASE, "The Confessions of Ellemar", bitIndex = 23, Quests.SHADOW_OF_THE_STORM), // shadow of the storm quest
DiaryHerbi(Items.DIARY_3395, intArrayOf(Items.DIARY_3395), type = StorableFamily.BOOKCASE, "Diary of Herbi Flax", bitIndex = 24, Quests.SHADES_OF_MORTTON), // shades of mort'ton quest
DrozalsJournal(Items.JOURNAL_6755, intArrayOf(Items.JOURNAL_6755), type = StorableFamily.BOOKCASE, "Drozal's Journal", bitIndex = 25, Quests.MAKING_HISTORY), // making history quest
DwarvenLore(Items.DWARVEN_LORE_4568, intArrayOf(Items.DWARVEN_LORE_4568), type = StorableFamily.BOOKCASE, "The Arzinian Being of Bordanzan", bitIndex = 26, Quests.BETWEEN_A_ROCK), // between a rock quest
EasternDiscovery(Items.EASTERN_DISCOVERY_6075, intArrayOf(Items.EASTERN_DISCOVERY_6075), type = StorableFamily.BOOKCASE, "The Exploration of the Eastern Realm", bitIndex = 27, Quests.MOURNINGS_END_PART_I), // mourning's end I quest
EasternSettlement(Items.EASTERN_SETTLEMENT_6077, intArrayOf(Items.EASTERN_SETTLEMENT_6077), type = StorableFamily.BOOKCASE, "The Settlement of the East", bitIndex = 28, Quests.MOURNINGS_END_PART_I), // mourning's end I quest
EdernsJournal(Items.EDERNS_JOURNAL_6649, intArrayOf(Items.EDERNS_JOURNAL_6649), type = StorableFamily.BOOKCASE, "Journal of Nissyen Edern", bitIndex = 29, Quests.MOURNINGS_END_PART_II), // mourning's end II quest
ExplorersNotes(Items.EXPLORERS_NOTES_11677, intArrayOf(Items.EXPLORERS_NOTES_11677), type = StorableFamily.BOOKCASE, "Beyond Trollheim", bitIndex = 30), // found in keldagrim library
EmbalmingManual(Items.EMBALMING_MANUAL_4686, intArrayOf(Items.EMBALMING_MANUAL_4686), type = StorableFamily.BOOKCASE, "The Little Book of Embalming", bitIndex = 31, Quests.ICTHLARINS_LITTLE_HELPER), // icthlarin's little helper quest
FarmingManual(Items.FARMING_MANUAL_9903, intArrayOf(Items.FARMING_MANUAL_9903), type = StorableFamily.BOOKCASE, "Farmer Gricoller's Farming Manual", bitIndex = 32, Quests.MY_ARMS_BIG_ADVENTURE), // my arm's big adventure quest
FeatheredJournal(Items.FEATHERED_JOURNAL_10179, intArrayOf(Items.FEATHERED_JOURNAL_10179), type = StorableFamily.BOOKCASE, "Feathered Journal of Arthur Artimus", bitIndex = 33, Quests.EAGLES_PEAK), // eagle's peak
GameBook(Items.GAME_BOOK_7681, intArrayOf(Items.GAME_BOOK_7681), type = StorableFamily.BOOKCASE, "Party Pete's Bumper Book of Games", bitIndex = 34), // poh game room. I think the bookshelf is the only source for this
GhrimsBook(Items.GHRIMS_BOOK_3901, intArrayOf(Items.GHRIMS_BOOK_3901), type = StorableFamily.BOOKCASE, "Managing Thine Kingdom for Noobes", bitIndex = 35, Quests.THRONE_OF_MISCELLANIA), // kingdom of misc quest
GiannesCookBook(Items.GIANNES_COOK_BOOK_2167, intArrayOf(Items.GIANNES_COOK_BOOK_2167), type = StorableFamily.BOOKCASE, "Gianne's Cook Book", bitIndex = 36), // gnome retaurant minigame - IMPLEMENTED
GlassblowingBook(Items.GLASSBLOWING_BOOK_11656, intArrayOf(Items.GLASSBLOWING_BOOK_11656), type = StorableFamily.BOOKCASE, "Glassblowing Book", bitIndex = 37), // found at the observatory
GoblinBook(Items.GOBLIN_BOOK_10999, intArrayOf(Items.GOBLIN_BOOK_10999), type = StorableFamily.BOOKCASE, "The Book of the Big High War God", bitIndex = 38), // dropped by goblins - IMPLEMENTED
GoblinSymbolBook(Items.GOBLIN_SYMBOL_BOOK_5009, intArrayOf(Items.GOBLIN_SYMBOL_BOOK_5009), type = StorableFamily.BOOKCASE, "A History of the Goblin Race", bitIndex = 39, Quests.THE_LOST_TRIBE), // lost tribe quest - IMPLEMENTED
ElvenBook(Items.THE_GREAT_DIVIDE_6079, intArrayOf(Items.THE_GREAT_DIVIDE_6079), type = StorableFamily.BOOKCASE, "The Great Divide", bitIndex = 40, Quests.MOURNINGS_END_PART_I), // mourning's end I quest
GuideBook(Items.GUIDE_BOOK_1856, intArrayOf(Items.GUIDE_BOOK_1856), type = StorableFamily.BOOKCASE, "Tourist Guide to Ardougne", bitIndex = 41), // found by estate agent in ardy - IMPLEMENTED
AHandwrittenBook(Items.A_HANDWRITTEN_BOOK_9627, intArrayOf(Items.A_HANDWRITTEN_BOOK_9627), type = StorableFamily.BOOKCASE, "Crystal Singing for Beginners (trans. Oaknock) (Transcribed from the original Elvish by Oaknock the Engineer)", bitIndex = 42, Quests.THE_EYES_OF_GLOUPHRIE), // eyes of glouphrie quest
HermansBook(Items.HERMANS_BOOK_7951, intArrayOf(Items.HERMANS_BOOK_7951), type = StorableFamily.BOOKCASE, "Dionysius: A Legend in His Own Lifetime", bitIndex = 43, Quests.SWAN_SONG), // swan song quest
HistoryOfIban(Items.HISTORY_OF_IBAN_1494, intArrayOf(Items.HISTORY_OF_IBAN_1494), type = StorableFamily.BOOKCASE, "The Tale of Iban", bitIndex = 44, Quests.UNDERGROUND_PASS), // underground pass quest
InstructionManual(Items.INSTRUCTION_MANUAL_5, intArrayOf(Items.INSTRUCTION_MANUAL_5), type = StorableFamily.BOOKCASE, "Dwarf Multicannon Manual", bitIndex = 45, Quests.DWARF_CANNON), // dwarf cannon quest - IMPLEMENTED
JossiksJournal(Items.JOURNAL_3845, intArrayOf(Items.JOURNAL_3845), type = StorableFamily.BOOKCASE, "Jossik's Diary", bitIndex = 46, Quests.HORROR_FROM_THE_DEEP), // horror from the deep quest
LeatherBook(Items.LEATHER_BOOK_7635, intArrayOf(Items.LEATHER_BOOK_7635), type = StorableFamily.BOOKCASE, "Modern Day Morytania", bitIndex = 47, Quests.IN_AID_OF_THE_MYREQUE), // in aid of the myreque quest
Manual(Items.MANUAL_3847, intArrayOf(Items.MANUAL_3847), type = StorableFamily.BOOKCASE, "Lighthouse Manual", bitIndex = 48, Quests.HORROR_FROM_THE_DEEP), // horror from the deep quest
MoonclanManual(Items.MOONCLAN_MANUAL_9078, intArrayOf(Items.MOONCLAN_MANUAL_9078), type = StorableFamily.BOOKCASE, "Basics of Magic", bitIndex = 49, Quests.LUNAR_DIPLOMACY), // lunar diplomacy quest
MyNotes(Items.MY_NOTES_11339, intArrayOf(Items.MY_NOTES_11339), type = StorableFamily.BOOKCASE, "'My notes'", bitIndex = 50), // during barbarian training
OldJournal(Items.OLD_JOURNAL_1493, intArrayOf(Items.OLD_JOURNAL_1493), type = StorableFamily.BOOKCASE, "The Journal of Randas", bitIndex = 51, Quests.UNDERGROUND_PASS), // underground pass quest
OldTome(Items.OLD_TOME_13593, intArrayOf(Items.OLD_TOME_13593), type = StorableFamily.BOOKCASE, "Old Tome", bitIndex = 52, Quests.MEETING_HISTORY), // meeting history quest
PrayerBook(Items.PRAYER_BOOK_10890, intArrayOf(Items.PRAYER_BOOK_10890), type = StorableFamily.BOOKCASE, "Prayer of Deliverance from Poisons", bitIndex = 53, Quests.THE_GREAT_BRAIN_ROBBERY), // the great brain robbery quest
PieRecipeBook(Items.PIE_RECIPE_BOOK_7162, intArrayOf(Items.PIE_RECIPE_BOOK_7162), type = StorableFamily.BOOKCASE, "Pie Recipe Book", bitIndex = 54), // from romily weaklax
PrifddinasHistory(Items.PRIFDDINAS_HISTORY_6073, intArrayOf(Items.PRIFDDINAS_HISTORY_6073), type = StorableFamily.BOOKCASE, "The Creation of Prifddinas", bitIndex = 55), // found in lletya bookshelves
QueenHelpBook(Items.QUEEN_HELP_BOOK_10562, intArrayOf(Items.QUEEN_HELP_BOOK_10562), type = StorableFamily.BOOKCASE, "Queen Help Book", bitIndex = 56), // barbarian assault minigame
RatPitsManual(Items.BOOK_6767, intArrayOf(Items.BOOK_6767), type = StorableFamily.BOOKCASE, "The RatPits Manual", bitIndex = 57, Quests.RATCATCHERS), // rat catchers quest
ScabariteNotes(Items.SCABARITE_NOTES_11975, intArrayOf(Items.SCABARITE_NOTES_11975), type = StorableFamily.BOOKCASE, "My Notes, Archaeological exploration and the cult of Scabaras", bitIndex = 58, Quests.DEALING_WITH_SCABARAS), // dealing with scabaras quest
SecurityBook(Items.SECURITY_BOOK_9003, intArrayOf(Items.SECURITY_BOOK_9003), type = StorableFamily.BOOKCASE, "" + GameWorld.settings!!.name + " Account Security", bitIndex = 59), // from stronghold of security - IMPLEMENTED
ShamansTome(Items.SHAMANS_TOME_729, intArrayOf(Items.SHAMANS_TOME_729), type = StorableFamily.BOOKCASE, "Shaman's Tome", bitIndex = 60, Quests.LEGENDS_QUEST), // legends quest - IMPLEMENTED
TheShieldOfArravBook(Items.BOOK_757, intArrayOf(Items.BOOK_757), type = StorableFamily.BOOKCASE, "The Shield of Arrav", bitIndex = 61, Quests.SHIELD_OF_ARRAV), // shield of arrav quest - IMPLEMENTED
SinkethsDiary(Items.SINKETHS_DIARY_11002, intArrayOf(Items.SINKETHS_DIARY_11002), type = StorableFamily.BOOKCASE, "Sin'keth's Diary", bitIndex = 62, Quests.WHAT_LIES_BELOW), // what lies below quest - IMPLEMENTED
StrongholdNotes(Items.STRONGHOLD_NOTES_9004, intArrayOf(Items.STRONGHOLD_NOTES_9004), type = StorableFamily.BOOKCASE, "Stronghold of Security - Notes", bitIndex = 63), // from stronghold of security - IMPLEMENTED
TarnsDiary(Items.TARNS_DIARY_10587, intArrayOf(Items.TARNS_DIARY_10587), type = StorableFamily.BOOKCASE, "The Diary of Tarn Razorlor", bitIndex = 64), // lair of tarn miniquest
Translation(Items.TRANSLATION_4655, intArrayOf(Items.TRANSLATION_4655), type = StorableFamily.BOOKCASE, "Translation Primer (Four Diamonds Translation)", bitIndex = 65, Quests.DESERT_TREASURE), // desert treasure quest - IMPLEMENTED
TranslationBook(Items.TRANSLATION_BOOK_784, intArrayOf(Items.TRANSLATION_BOOK_784), type = StorableFamily.BOOKCASE, "Gnome-English Translation Dictionary", bitIndex = 66, Quests.THE_EYES_OF_GLOUPHRIE), // eyes of glouphrie quest - IMPLEMENTED
TzhaarTouristGuide(Items.TZHAAR_TOURIST_GUIDE_13244, intArrayOf(Items.TZHAAR_TOURIST_GUIDE_13244), type = StorableFamily.BOOKCASE, "Tzhaar Tourist Guide", bitIndex = 67, Quests.TOKTZ_KET_DILL), // toktz-ket-dill quest
VarmensNotes(Items.VARMENS_NOTES_4616, intArrayOf(Items.VARMENS_NOTES_4616), type = StorableFamily.BOOKCASE, "Notes from Varmen's Expedition to Uzer", bitIndex = 68, Quests.THE_GOLEM), // the golem quest - IMPLEMENTED
WitchesDiary(Items.DIARY_2408, intArrayOf(Items.DIARY_2408), type = StorableFamily.BOOKCASE, "Witch's Diary", bitIndex = 69, Quests.WITCHS_HOUSE), // witch's house quest - IMPLEMENTED
YewnocksNotes(Items.YEWNOCKS_NOTES_11750, intArrayOf(Items.YEWNOCKS_NOTES_11750), type = StorableFamily.BOOKCASE, "Yewnock's Notes on Crystals", bitIndex = 70, Quests.THE_PATH_OF_GLOUPHRIE), // path of glouphrie quest
// Capes - https://runescape.wiki/w/Cape_rack?oldid=848849
LegendsCape(Items.CAPE_OF_LEGENDS_1052, intArrayOf(Items.CAPE_OF_LEGENDS_1052), type = StorableFamily.CAPE_RACK, "Legends cape"),
ObsidianCape(Items.OBSIDIAN_CAPE_6568, intArrayOf(Items.OBSIDIAN_CAPE_6568), type = StorableFamily.CAPE_RACK),
FireCape(Items.FIRE_CAPE_6570, intArrayOf(Items.FIRE_CAPE_6570), type = StorableFamily.CAPE_RACK),
TeamCape(Items.TEAM_1_CAPE_10638, (4315..4413 step 2).toList().toIntArray(), type = StorableFamily.CAPE_RACK, "Wilderness cape"),
GodCape(Items.GUTHIX_CAPE_2413, intArrayOf(Items.GUTHIX_CAPE_2413, Items.SARADOMIN_CAPE_2412, Items.ZAMORAK_CAPE_2414), type = StorableFamily.CAPE_RACK, "God cape"),
AttackCape(Items.ATTACK_CAPE_10639, intArrayOf(Items.ATTACK_CAPE_9747, Items.ATTACK_HOOD_9749, Items.ATTACK_CAPET_9748), type = StorableFamily.CAPE_RACK_SKILL),
DefenceCape(Items.DEFENCE_CAPE_10641, intArrayOf(Items.DEFENCE_CAPE_9753, Items.DEFENCE_CAPET_9754, Items.DEFENCE_HOOD_9755), type = StorableFamily.CAPE_RACK_SKILL),
StrengthCape(Items.STRENGTH_CAPE_10640, intArrayOf(Items.STRENGTH_CAPE_9750, Items.STRENGTH_CAPET_9751, Items.STRENGTH_HOOD_9752), type = StorableFamily.CAPE_RACK_SKILL),
HitpointsCape(Items.HITPOINTS_CAPE_10647, intArrayOf(Items.HITPOINTS_CAPE_9768, Items.HITPOINTS_CAPET_9769, Items.HITPOINTS_HOOD_9770), type = StorableFamily.CAPE_RACK_SKILL),
AgilityCape(Items.AGILITY_CAPE_10648, intArrayOf(Items.AGILITY_CAPE_9771, Items.AGILITY_CAPET_9772, Items.AGILITY_HOOD_9773), type = StorableFamily.CAPE_RACK_SKILL),
CookingCape(Items.COOKING_CAPE_10658, intArrayOf(Items.COOKING_CAPE_9801, Items.COOKING_CAPET_9802, Items.COOKING_HOOD_9803), type = StorableFamily.CAPE_RACK_SKILL),
ConstructionCape(Items.CONSTRUCT_CAPE_10654, intArrayOf(Items.CONSTRUCT_CAPE_9789, Items.CONSTRUCT_CAPET_9790, Items.CONSTRUCT_HOOD_9791), type = StorableFamily.CAPE_RACK_SKILL, "Construction cape"),
CraftingCape(Items.CRAFTING_CAPE_10651, intArrayOf(Items.CRAFTING_CAPE_9780, Items.CRAFTING_CAPET_9781, Items.CRAFTING_HOOD_9782), type = StorableFamily.CAPE_RACK_SKILL),
FarmingCape(Items.FARMING_CAPE_10661, intArrayOf(Items.FARMING_CAPE_9810, Items.FARMING_CAPET_9811, Items.FARMING_HOOD_9812), type = StorableFamily.CAPE_RACK_SKILL),
FiremakingCape(Items.FIREMAKING_CAPE_10659, intArrayOf(Items.FIREMAKING_CAPE_9804, Items.FIREMAKING_CAPET_9805, Items.FIREMAKING_HOOD_9806), type = StorableFamily.CAPE_RACK_SKILL),
FishingCape(Items.FISHING_CAPE_10657, intArrayOf(Items.FISHING_CAPE_9798, Items.FISHING_CAPET_9799, Items.FISHING_HOOD_9800), type = StorableFamily.CAPE_RACK_SKILL),
FletchingCape(Items.FLETCHING_CAPE_10652, intArrayOf(Items.FLETCHING_CAPE_9783, Items.FLETCHING_CAPET_9784, Items.FLETCHING_HOOD_9785), type = StorableFamily.CAPE_RACK_SKILL),
HerbloreCape(Items.HERBLORE_CAPE_10649, intArrayOf(Items.HERBLORE_CAPE_9774, Items.HERBLORE_CAPET_9775, Items.HERBLORE_HOOD_9776), type = StorableFamily.CAPE_RACK_SKILL),
MagicCape(Items.MAGIC_CAPE_10644, intArrayOf(Items.MAGIC_CAPE_9762, Items.MAGIC_CAPET_9763, Items.MAGIC_HOOD_9764), type = StorableFamily.CAPE_RACK_SKILL),
MiningCape(Items.MINING_CAPE_10655, intArrayOf(Items.MINING_CAPE_9792, Items.MINING_CAPET_9793, Items.MINING_HOOD_9794), type = StorableFamily.CAPE_RACK_SKILL),
PrayerCape(Items.PRAYER_CAPE_10643, intArrayOf(Items.PRAYER_CAPE_9759, Items.PRAYER_CAPET_9760, Items.PRAYER_HOOD_9761), type = StorableFamily.CAPE_RACK_SKILL),
RangingCape(Items.RANGING_CAPE_10642, intArrayOf(Items.RANGING_CAPE_9756, Items.RANGING_CAPET_9757, Items.RANGING_HOOD_9758), type = StorableFamily.CAPE_RACK_SKILL),
RunecraftCape(Items.RUNECRAFT_CAPE_10645, intArrayOf(Items.RUNECRAFT_CAPE_9765, Items.RUNECRAFT_CAPET_9766, Items.RUNECRAFTING_HOOD_9767), type = StorableFamily.CAPE_RACK_SKILL, "Runecrafting cape"),
SlayerCape(Items.SLAYER_CAPE_10653, intArrayOf(Items.SLAYER_CAPE_9786, Items.SLAYER_CAPET_9787, Items.SLAYER_HOOD_9788), type = StorableFamily.CAPE_RACK_SKILL),
SmithingCape(Items.SMITHING_CAPE_10656, intArrayOf(Items.SMITHING_CAPE_9795, Items.SMITHING_CAPET_9796, Items.SMITHING_HOOD_9797), type = StorableFamily.CAPE_RACK_SKILL),
ThievingCape(Items.THIEVING_CAPE_10650, intArrayOf(Items.THIEVING_CAPE_9777, Items.THIEVING_CAPET_9778, Items.THIEVING_HOOD_9779), type = StorableFamily.CAPE_RACK_SKILL),
WoodcuttingCape(Items.WOODCUTTING_CAPE_10660, intArrayOf(Items.WOODCUTTING_CAPE_9807, Items.WOODCUT_CAPET_9808, Items.WOODCUTTING_HOOD_9809), type = StorableFamily.CAPE_RACK_SKILL),
HunterCape(Items.HUNTER_CAPE_10646, intArrayOf(Items.HUNTER_CAPE_9948, Items.HUNTER_CAPET_9949, Items.HUNTER_HOOD_9950), type = StorableFamily.CAPE_RACK_SKILL),
QuestCape(Items.QUEST_POINT_CAPE_10662, intArrayOf(Items.QUEST_POINT_CAPE_9813, Items.QUEST_POINT_HOOD_9814), type = StorableFamily.CAPE_RACK_SKILL),
SummoningCape(Items.SUMMONING_CAPE_12524, intArrayOf(Items.SUMMONING_CAPE_12169, Items.SUMMONING_CAPET_12170, Items.SUMMONING_HOOD_12171), type = StorableFamily.CAPE_RACK_SKILL),
SpottedCape(Items.SPOTTED_CAPE_10663, intArrayOf(Items.SPOTTED_CAPE_10663), type = StorableFamily.CAPE_RACK, "Spotted hunting cape"),
SpottierCape(Items.SPOTTIER_CAPE_10664, intArrayOf(Items.SPOTTIER_CAPE_10664), type = StorableFamily.CAPE_RACK, "Spottier hunting cape"),
// Fancy Dress - https://runescape.wiki/w/Fancy_dress_box?oldid=848873
MimeCostume(Items.MIME_MASK_10629, intArrayOf(Items.MIME_MASK_3057, Items.MIME_TOP_3058, Items.MIME_LEGS_3059, Items.MIME_GLOVES_3060, Items.MIME_BOOTS_3061), type = StorableFamily.FANCY_DRESS, "Mime Costume"),
RoyalFrogCostume(Items.PRINCESS_BLOUSE_10630, intArrayOf(Items.PRINCE_TUNIC_6184, Items.PRINCE_LEGGINGS_6185, Items.PRINCESS_BLOUSE_6186, Items.PRINCESS_SKIRT_6187), type = StorableFamily.FANCY_DRESS, "Royal frog costume"),
FrogMask(Items.FROG_MASK_10721, intArrayOf(Items.FROG_MASK_6188), type = StorableFamily.FANCY_DRESS),
ZombieOutfit(Items.ZOMBIE_SHIRT_10631, intArrayOf(Items.ZOMBIE_MASK_7594, Items.ZOMBIE_SHIRT_7592, Items.ZOMBIE_TROUSERS_7593, Items.ZOMBIE_GLOVES_7595, Items.ZOMBIE_BOOTS_7596), type = StorableFamily.FANCY_DRESS, "Zombie Outfit"),
CamoOutfit(Items.CAMO_TOP_10632, intArrayOf(Items.CAMO_HELMET_6656, Items.CAMO_TOP_6654, Items.CAMO_BOTTOMS_6655), type = StorableFamily.FANCY_DRESS, "Camo outfit"),
LederhosenOutfit(Items.LEDERHOSEN_TOP_10633, intArrayOf(Items.LEDERHOSEN_TOP_6180, Items.LEDERHOSEN_SHORTS_6181, Items.LEDERHOSEN_HAT_6182), type = StorableFamily.FANCY_DRESS, "Lederhosen outfit"),
ShadeRobes(Items.SHADE_ROBE_10634, intArrayOf(Items.SHADE_ROBE_546, Items.SHADE_ROBE_548), type = StorableFamily.FANCY_DRESS, "Shade robes"),
// Toys - https://runescape.wiki/w/Toy_box?oldid=848855
BunnyEars(Items.BUNNY_EARS_10734, intArrayOf(Items.BUNNY_EARS_1037), type = StorableFamily.TOY_BOX),
Scythe(Items.SCYTHE_10735, intArrayOf(Items.SCYTHE_1419), type = StorableFamily.TOY_BOX),
YoYo(Items.YO_YO_10733, intArrayOf(Items.YO_YO_4079), type = StorableFamily.TOY_BOX),
RubberChicken(Items.RUBBER_CHICKEN_10732, intArrayOf(Items.RUBBER_CHICKEN_4566), type = StorableFamily.TOY_BOX),
ZombieHead(Items.ZOMBIE_HEAD_10731, intArrayOf(Items.ZOMBIE_HEAD_6722), type = StorableFamily.TOY_BOX),
EasterRing(Items.EASTER_RING_10729, intArrayOf(Items.EASTER_RING_7927), type = StorableFamily.TOY_BOX),
BobbleHat(Items.BOBBLE_HAT_9815, intArrayOf(Items.BOBBLE_HAT_6856), type = StorableFamily.TOY_BOX),
BobbleScarf(Items.BOBBLE_SCARF_9816, intArrayOf(Items.BOBBLE_SCARF_6857), type = StorableFamily.TOY_BOX),
JesterHat(Items.JESTER_HAT_6858, intArrayOf(Items.JESTER_HAT_6858), type = StorableFamily.TOY_BOX),
JesterScarf(Items.JESTER_SCARF_6859, intArrayOf(Items.JESTER_SCARF_6859), type = StorableFamily.TOY_BOX),
TriJesterHat(Items.TRI_JESTER_HAT_6860, intArrayOf(Items.TRI_JESTER_HAT_6860), type = StorableFamily.TOY_BOX),
TriJesterScarf(Items.TRI_JESTER_SCARF_6861, intArrayOf(Items.TRI_JESTER_SCARF_6861), type = StorableFamily.TOY_BOX),
WoollyHat(Items.WOOLLY_HAT_6862, intArrayOf(Items.WOOLLY_HAT_6862), type = StorableFamily.TOY_BOX),
WoollyScarf(Items.WOOLLY_SCARF_6863, intArrayOf(Items.WOOLLY_SCARF_6863), type = StorableFamily.TOY_BOX),
Marionette(Items.BLUE_MARIONETTE_10730, intArrayOf(Items.BLUE_MARIONETTE_6865,Items.GREEN_MARIONETTE_6866,Items.RED_MARIONETTE_6867), type = StorableFamily.TOY_BOX), // this authentically displays as 'blue marionette' in the source video
JackLanternMask(Items.JACK_LANTERN_MASK_10723, intArrayOf(Items.JACK_LANTERN_MASK_9920), type = StorableFamily.TOY_BOX),
SkeletonBoots(Items.SKELETON_BOOTS_10724, intArrayOf(Items.SKELETON_BOOTS_9921), type = StorableFamily.TOY_BOX),
SkeletonGloves(Items.SKELETON_GLOVES_10725, intArrayOf(Items.SKELETON_GLOVES_9922), type = StorableFamily.TOY_BOX),
SkeletonLeggings(Items.SKELETON_LEGGINGS_10726, intArrayOf(Items.SKELETON_LEGGINGS_9923), type = StorableFamily.TOY_BOX),
SkeletonShirt(Items.SKELETON_SHIRT_10727, intArrayOf(Items.SKELETON_SHIRT_9924), type = StorableFamily.TOY_BOX),
SkeletonMask(Items.SKELETON_MASK_10728, intArrayOf(Items.SKELETON_MASK_9925), type = StorableFamily.TOY_BOX),
ReindeerHat(Items.REINDEER_HAT_10722, intArrayOf(Items.REINDEER_HAT_10507), type = StorableFamily.TOY_BOX),
WintumberTree(Items.WINTUMBER_TREE_10508, intArrayOf(Items.WINTUMBER_TREE_10508), type = StorableFamily.TOY_BOX),
ChickenFeet(Items.CHICKEN_FEET_11019, intArrayOf(Items.CHICKEN_FEET_11019), type = StorableFamily.TOY_BOX),
ChickenLegs(Items.CHICKEN_LEGS_11022, intArrayOf(Items.CHICKEN_LEGS_11022), type = StorableFamily.TOY_BOX),
ChickenWings(Items.CHICKEN_WINGS_11020, intArrayOf(Items.CHICKEN_WINGS_11020), type = StorableFamily.TOY_BOX),
ChickenHead(Items.CHICKEN_HEAD_11021, intArrayOf(Items.CHICKEN_HEAD_11021), type = StorableFamily.TOY_BOX),
GrimReaperHood(Items.GRIM_REAPER_HOOD_11789, intArrayOf(Items.GRIM_REAPER_HOOD_11789), type = StorableFamily.TOY_BOX),
SnowGlobe(Items.SNOW_GLOBE_11949, intArrayOf(Items.SNOW_GLOBE_11949), type = StorableFamily.TOY_BOX),
ChocaticeCape(Items.CHOCATRICE_CAPE_12634, intArrayOf(Items.CHOCATRICE_CAPE_12645), type = StorableFamily.TOY_BOX),
WarlockTop(Items.WARLOCK_TOP_14076, intArrayOf(Items.WARLOCK_TOP_14076, Items.WITCH_TOP_14078), type = StorableFamily.TOY_BOX),
WarlockLegs(Items.WARLOCK_LEGS_14077, intArrayOf(Items.WARLOCK_LEGS_14077, Items.WITCH_SKIRT_14079), type = StorableFamily.TOY_BOX),
WarlockCloak(Items.WARLOCK_CLOAK_14081, intArrayOf(Items.WARLOCK_CLOAK_14081, Items.WITCH_CLOAK_14088), type = StorableFamily.TOY_BOX),
SantaCostumeTop(Items.SANTA_COSTUME_TOP_14595, intArrayOf(Items.SANTA_COSTUME_TOP_14595, Items.SANTA_COSTUME_TOP_14600), type = StorableFamily.TOY_BOX),
SantaCostumeLegs(Items.SANTA_COSTUME_LEGS_14603, intArrayOf(Items.SANTA_COSTUME_LEGS_14603, Items.SANTA_COSTUME_LEGS_14604), type = StorableFamily.TOY_BOX),
SantaCostumeGloves(Items.SANTA_COSTUME_GLOVES_14602, intArrayOf(Items.SANTA_COSTUME_GLOVES_14602), type = StorableFamily.TOY_BOX),
SantaCostumeBoots(Items.SANTA_COSTUME_BOOTS_14605, intArrayOf(Items.SANTA_COSTUME_BOOTS_14605), type = StorableFamily.TOY_BOX),
IceAmulet(Items.ICE_AMULET_14596, intArrayOf(Items.ICE_AMULET_14596), type = StorableFamily.TOY_BOX),
Cornucopia(Items.CORNUCOPIA_14570, intArrayOf(Items.CORNUCOPIA_14537), type = StorableFamily.TOY_BOX),
// Treasure Trails - low - https://runescape.wiki/w/Treasure_chest?oldid=848831
TrimmedBlackArmour(Items.BLACK_PLATEBODY_T_10690, intArrayOf(Items.BLACK_FULL_HELMT_2587, Items.BLACK_PLATEBODY_T_2583, Items.BLACK_PLATELEGS_T_2585, Items.BLACK_KITESHIELD_T_2589, Items.BLACK_PLATESKIRT_T_3472), type = StorableFamily.TREASURE_CHEST_LOW, "Trimmed black armour"),
GoldTrimmedBlackArmour(Items.BLACK_PLATEBODY_G_10691, intArrayOf(Items.BLACK_FULL_HELMG_2595, Items.BLACK_PLATEBODY_G_2591, Items.BLACK_PLATELEGS_G_2593, Items.BLACK_KITESHIELD_G_2597, Items.BLACK_PLATESKIRT_G_3473), type = StorableFamily.TREASURE_CHEST_LOW, "Gold-trimmed black armour"),
BlackHeraldicH1(Items.BLACK_HELM_H1_10699, intArrayOf(Items.BLACK_HELM_H1_10306), type = StorableFamily.TREASURE_CHEST_LOW, "Black heraldic helm"),
BlackHeraldicH2(Items.BLACK_HELM_H2_10700, intArrayOf(Items.BLACK_HELM_H2_10308), type = StorableFamily.TREASURE_CHEST_LOW, "Black heraldic helm"),
BlackHeraldicH3(Items.BLACK_HELM_H3_10701, intArrayOf(Items.BLACK_HELM_H3_10310), type = StorableFamily.TREASURE_CHEST_LOW, "Black heraldic helm"),
BlackHeraldicH4(Items.BLACK_HELM_H4_10702, intArrayOf(Items.BLACK_HELM_H4_10312), type = StorableFamily.TREASURE_CHEST_LOW, "Black heraldic helm"),
BlackHeraldicH5(Items.BLACK_HELM_H5_10703, intArrayOf(Items.BLACK_HELM_H5_10314), type = StorableFamily.TREASURE_CHEST_LOW, "Black heraldic helm"),
BlackHeraldicKiteshieldH1(Items.BLACK_SHIELDH1_10665, intArrayOf(Items.BLACK_SHIELDH1_7332), type = StorableFamily.TREASURE_CHEST_LOW, "Black heraldic kiteshield"),
BlackHeraldicKiteshieldH2(Items.BLACK_SHIELDH2_10668, intArrayOf(Items.BLACK_SHIELDH2_7338), type = StorableFamily.TREASURE_CHEST_LOW, "Black heraldic kiteshield"),
BlackHeraldicKiteshieldH3(Items.BLACK_SHIELDH3_10671, intArrayOf(Items.BLACK_SHIELDH3_7344), type = StorableFamily.TREASURE_CHEST_LOW, "Black heraldic kiteshield"),
BlackHeraldicKiteshieldH4(Items.BLACK_SHIELDH4_10674, intArrayOf(Items.BLACK_SHIELDH4_7350), type = StorableFamily.TREASURE_CHEST_LOW, "Black heraldic kiteshield"),
BlackHeraldicKiteshieldH5(Items.BLACK_SHIELDH5_10677, intArrayOf(Items.BLACK_SHIELDH5_7356), type = StorableFamily.TREASURE_CHEST_LOW, "Black heraldic kiteshield"),
TrimmedStuddedArmour(Items.STUDDED_BODY_T_10681, intArrayOf(Items.STUDDED_BODY_T_7364, Items.STUDDED_CHAPS_T_7368), type = StorableFamily.TREASURE_CHEST_LOW, "Fur-trimmed studded leather"),
GoldTrimmedStuddedArmour(Items.STUDDED_BODY_G_10680, intArrayOf(Items.STUDDED_BODY_G_7362, Items.STUDDED_CHAPS_G_7366), type = StorableFamily.TREASURE_CHEST_LOW, "Gold-trimmed studded leather"),
TrimmedWizardRobes(Items.WIZARD_ROBE_T_10687, intArrayOf(Items.WIZARD_HAT_T_7396, Items.WIZARD_ROBE_T_7392, Items.BLUE_SKIRT_T_7388), type = StorableFamily.TREASURE_CHEST_LOW, "Trimmed wizard's robes"),
GoldTrimmedWizardRobes(Items.WIZARD_ROBE_G_10686, intArrayOf(Items.WIZARD_HAT_G_7394, Items.WIZARD_ROBE_G_7390, Items.BLUE_SKIRT_G_7386), type = StorableFamily.TREASURE_CHEST_LOW, "Gold-trimmed wizard's robes"),
WizardBoots(Items.WIZARD_BOOTS_10689, intArrayOf(Items.WIZARD_BOOTS_2579), type = StorableFamily.TREASURE_CHEST_LOW),
TrimmedAmuletOfMagic(Items.AMULET_OF_MAGICT_10738, intArrayOf(Items.AMULET_OF_MAGICT_10366), type = StorableFamily.TREASURE_CHEST_LOW, "Trimmed amulet of magic"),
HighwaymanMask(Items.HIGHWAYMAN_MASK_10692, intArrayOf(Items.HIGHWAYMAN_MASK_2631), type = StorableFamily.TREASURE_CHEST_LOW),
Pantaloons(Items.PANTALOONS_10744, intArrayOf(Items.PANTALOONS_10396), type = StorableFamily.TREASURE_CHEST_LOW),
PowderedWig(Items.A_POWDERED_WIG_10740, intArrayOf(Items.A_POWDERED_WIG_10392), type = StorableFamily.TREASURE_CHEST_LOW),
FlaredTrousers(Items.FLARED_TROUSERS_10742, intArrayOf(Items.FLARED_TROUSERS_10394), type = StorableFamily.TREASURE_CHEST_LOW),
SleepingCap(Items.SLEEPING_CAP_10746, intArrayOf(Items.SLEEPING_CAP_10398), type = StorableFamily.TREASURE_CHEST_LOW),
BlackBeret(Items.BLACK_BERET_10694, intArrayOf(Items.BLACK_BERET_2635), type = StorableFamily.TREASURE_CHEST_LOW),
WhiteBeret(Items.WHITE_BERET_10695, intArrayOf(Items.WHITE_BERET_2637), type = StorableFamily.TREASURE_CHEST_LOW),
BlueBeret(Items.BLUE_BERET_10693, intArrayOf(Items.BLUE_BERET_2633), type = StorableFamily.TREASURE_CHEST_LOW),
BobTheCatShirt(Items.BOB_SHIRT_10714, intArrayOf(Items.BOB_SHIRT_10316), type = StorableFamily.TREASURE_CHEST_LOW, "Bob the Cat shirt (red)"),
BobTheCatShirt1(Items.BOB_SHIRT_10715, intArrayOf(Items.BOB_SHIRT_10318), type = StorableFamily.TREASURE_CHEST_LOW, "Bob the Cat shirt (blue)"),
BobTheCatShirt2(Items.BOB_SHIRT_10716, intArrayOf(Items.BOB_SHIRT_10320), type = StorableFamily.TREASURE_CHEST_LOW, "Bob the Cat shirt (green)"),
BobTheCatShirt3(Items.BOB_SHIRT_10717, intArrayOf(Items.BOB_SHIRT_10322), type = StorableFamily.TREASURE_CHEST_LOW, "Bob the Cat shirt (black)"),
BobTheCatShirt4(Items.BOB_SHIRT_10718, intArrayOf(Items.BOB_SHIRT_10324), type = StorableFamily.TREASURE_CHEST_LOW, "Bob the Cat shirt (purple)"),
RedElegant(Items.ELEGANT_SHIRT_10750, intArrayOf(Items.RED_ELE_SHIRT_10404, Items.RED_ELE_LEGS_10406, Items.RED_ELE_BLOUSE_10424, Items.RED_ELE_SKIRT_10426), type = StorableFamily.TREASURE_CHEST_LOW, "Elegant clothes (red)"),
BlueElegant(Items.ELEGANT_SHIRT_10752, intArrayOf(Items.BLUE_ELE_SHIRT_10408, Items.BLUE_ELE_LEGS_10410, Items.BLUE_ELE_BLOUSE_10428, Items.BLUE_ELE_SKIRT_10430), type = StorableFamily.TREASURE_CHEST_LOW, "Elegant clothes (blue)"),
GreenElegant(Items.ELEGANT_SHIRT_10754, intArrayOf(Items.GREEN_ELE_SHIRT_10412,Items.GREEN_ELE_LEGS_10414, Items.GREEN_ELE_BLOUSE_10432, Items.GREEN_ELE_SKIRT_10434), type = StorableFamily.TREASURE_CHEST_LOW, "Elegant clothes (green)"),
BeretAndMask(Items.BERET_MASK_11278, intArrayOf(Items.BERET_AND_MASK_11282), type = StorableFamily.TREASURE_CHEST_LOW),
BlackCane(Items.BLACK_CANE_13163, intArrayOf(Items.BLACK_CANE_13095), type = StorableFamily.TREASURE_CHEST_LOW),
SpikedHelmet(Items.SPIKED_HELMET_13168, intArrayOf(Items.SPIKED_HELMET_13105), type = StorableFamily.TREASURE_CHEST_LOW),
// Treasure Trails - medium - https://runescape.wiki/w/Treasure_chest?oldid=848831
TrimmedAdamantiteArmour(Items.ADAM_PLATEBODY_T_10697, intArrayOf(Items.ADAM_FULL_HELMT_2605, Items.ADAM_PLATEBODY_T_2599, Items.ADAM_PLATELEGS_T_2601, Items.ADAM_KITESHIELD_T_2603, Items.ADAM_PLATESKIRT_T_3474), type = StorableFamily.TREASURE_CHEST_MED, "Trimmed adamantite armour"),
GoldTrimmedAdamantiteArmour(Items.ADAM_PLATEBODY_G_10698, intArrayOf(Items.ADAM_FULL_HELMG_2613, Items.ADAM_PLATEBODY_G_2607, Items.ADAM_PLATELEGS_G_2609, Items.ADAM_KITESHIELD_G_2611, Items.ADAM_PLATESKIRT_G_3475), type = StorableFamily.TREASURE_CHEST_MED, "Gold-trimmed adamantite armour"),
AdamantHeraldicH1(Items.ADAMANT_HELM_H1_10709, intArrayOf(Items.ADAMANT_HELM_H1_10296), type = StorableFamily.TREASURE_CHEST_MED, "Adamant heraldic helm"),
AdamantHeraldicH2(Items.ADAMANT_HELM_H2_10710, intArrayOf(Items.ADAMANT_HELM_H2_10298), type = StorableFamily.TREASURE_CHEST_MED, "Adamant heraldic helm"),
AdamantHeraldicH3(Items.ADAMANT_HELM_H3_10711, intArrayOf(Items.ADAMANT_HELM_H3_10300), type = StorableFamily.TREASURE_CHEST_MED, "Adamant heraldic helm"),
AdamantHeraldicH4(Items.ADAMANT_HELM_H4_10712, intArrayOf(Items.ADAMANT_HELM_H4_10302), type = StorableFamily.TREASURE_CHEST_MED, "Adamant heraldic helm"),
AdamantHeraldicH5(Items.ADAMANT_HELM_H5_10713, intArrayOf(Items.ADAMANT_HELM_H5_10304), type = StorableFamily.TREASURE_CHEST_MED, "Adamant heraldic helm"),
AdamantKiteshieldH1(Items.ADAMANT_SHIELDH1_10666, intArrayOf(Items.ADAMANT_SHIELDH1_7334), type = StorableFamily.TREASURE_CHEST_MED, "Adamant heraldic kiteshield"),
AdamantKiteshieldH2(Items.ADAMANT_SHIELDH2_10669, intArrayOf(Items.ADAMANT_SHIELDH2_7340), type = StorableFamily.TREASURE_CHEST_MED, "Adamant heraldic kiteshield"),
AdamantKiteshieldH3(Items.ADAMANT_SHIELDH3_10672, intArrayOf(Items.ADAMANT_SHIELDH3_7346), type = StorableFamily.TREASURE_CHEST_MED, "Adamant heraldic kiteshield"),
AdamantKiteshieldH4(Items.ADAMANT_SHIELDH4_10675, intArrayOf(Items.ADAMANT_SHIELDH4_7352), type = StorableFamily.TREASURE_CHEST_MED, "Adamant heraldic kiteshield"),
AdamantKiteshieldH5(Items.ADAMANT_SHIELDH5_10678, intArrayOf(Items.ADAMANT_SHIELDH5_7358), type = StorableFamily.TREASURE_CHEST_MED, "Adamant heraldic kiteshield"),
TrimmedGreenDhide(Items.DHIDE_BODY_T_10683, intArrayOf(Items.DHIDE_BODY_T_7372, Items.DHIDE_CHAPS_T_7380), type = StorableFamily.TREASURE_CHEST_MED, "Trimmed green dragonhide armour"),
GoldTrimmedGreenDhide(Items.DHIDE_BODYG_10682, intArrayOf(Items.DHIDE_BODYG_7370, Items.DHIDE_CHAPS_G_7378), type = StorableFamily.TREASURE_CHEST_MED, "Gold-trimmed green dragonhide armour"),
RangerBoots(Items.RANGER_BOOTS_10696, intArrayOf(Items.RANGER_BOOTS_2577), type = StorableFamily.TREASURE_CHEST_MED),
TrimmedAmuletOfStrength(Items.STRENGTH_AMULETT_10736, intArrayOf(Items.STRENGTH_AMULETT_10364), type = StorableFamily.TREASURE_CHEST_MED, "Trimmed amulet of strength"),
RedHeadband(Items.RED_HEADBAND_10768, intArrayOf(Items.RED_HEADBAND_2645), type = StorableFamily.TREASURE_CHEST_MED),
BlackHeadband(Items.BLACK_HEADBAND_10770, intArrayOf(Items.BLACK_HEADBAND_2647), type = StorableFamily.TREASURE_CHEST_MED),
BrownHeadband(Items.BROWN_HEADBAND_10772, intArrayOf(Items.BROWN_HEADBAND_2649), type = StorableFamily.TREASURE_CHEST_MED),
RedBoater(Items.RED_BOATER_10758, intArrayOf(Items.RED_BOATER_7319), type = StorableFamily.TREASURE_CHEST_MED, "Red straw boater"),
OrangeBoater(Items.ORANGE_BOATER_10760, intArrayOf(Items.ORANGE_BOATER_7321), type = StorableFamily.TREASURE_CHEST_MED, "Orange straw boater"),
GreenBoater(Items.GREEN_BOATER_10762, intArrayOf(Items.GREEN_BOATER_7323), type = StorableFamily.TREASURE_CHEST_MED, "Green straw boater"),
BlueBoater(Items.BLUE_BOATER_10764, intArrayOf(Items.BLUE_BOATER_7325), type = StorableFamily.TREASURE_CHEST_MED, "Blue straw boater"),
BlackBoater(Items.BLACK_BOATER_10766, intArrayOf(Items.BLACK_BOATER_7327), type = StorableFamily.TREASURE_CHEST_MED, "Black straw boater"),
BlackElegant(Items.ELEGANT_SHIRT_10748, intArrayOf(Items.BLACK_ELE_SHIRT_10400, Items.BLACK_ELE_LEGS_10402, Items.WHITE_ELE_BLOUSE_10420, Items.WHITE_ELE_SKIRT_10422), type = StorableFamily.TREASURE_CHEST_MED, "Elegant clothes (black)"),
PurpleElegant(Items.ELEGANT_SHIRT_10756, intArrayOf(Items.PURPLE_ELE_BLOUSE_10436, Items.PURPLE_ELE_SKIRT_10438), type = StorableFamily.TREASURE_CHEST_MED, "Elegant clothes (purple)"),
AdamantCane(Items.ADAMANT_CANE_13164, intArrayOf(Items.ADAMANT_CANE_13097), type = StorableFamily.TREASURE_CHEST_MED),
SheepMask(Items.SHEEP_MASK_13169, intArrayOf(Items.SHEEP_MASK_13107), type = StorableFamily.TREASURE_CHEST_MED),
BatMask(Items.BAT_MASK_13171, intArrayOf(Items.BAT_MASK_13111), type = StorableFamily.TREASURE_CHEST_MED),
WolfMask(Items.WOLF_MASK_13173, intArrayOf(Items.WOLF_MASK_13115), type = StorableFamily.TREASURE_CHEST_MED),
PenguinMask(Items.PENGUIN_MASK_13170, intArrayOf(Items.PENGUIN_MASK_13109), type = StorableFamily.TREASURE_CHEST_MED),
CatMask(Items.CAT_MASK_13172, intArrayOf(Items.CAT_MASK_13113), type = StorableFamily.TREASURE_CHEST_MED),
PithHelmet(Items.PITH_HELMET_13167, intArrayOf(Items.PITH_HELMET_13103), type = StorableFamily.TREASURE_CHEST_MED),
// Treasure Trails - hard - https://runescape.wiki/w/Treasure_chest?oldid=848831
TrimmedRuneArmour(Items.RUNE_PLATEBODY_T_10800, intArrayOf(Items.RUNE_FULL_HELM_T_2627, Items.RUNE_PLATEBODY_T_2623, Items.RUNE_PLATELEGS_T_2625, Items.RUNE_KITESHIELD_T_2629, Items.RUNE_PLATESKIRT_T_3477), type = StorableFamily.TREASURE_CHEST_HIGH, "Trimmed rune armour"),
GoldTrimmedRuneArmour(Items.RUNE_PLATEBODY_G_10798, intArrayOf(Items.RUNE_FULL_HELMG_2619, Items.RUNE_PLATEBODY_G_2615, Items.RUNE_PLATELEGS_G_2617, Items.RUNE_KITESHIELD_G_2621, Items.RUNE_PLATESKIRT_G_3476), type = StorableFamily.TREASURE_CHEST_HIGH, "Gold-trimmed rune armour"),
GildedArmour(Items.GILDED_PLATEBODY_10782, intArrayOf(Items.GILDED_FULL_HELM_3486, Items.GILDED_PLATEBODY_3481, Items.GILDED_PLATELEGS_3483, Items.GILDED_KITESHIELD_3488, Items.GILDED_PLATESKIRT_3485), type = StorableFamily.TREASURE_CHEST_HIGH, "Gold-plated rune armour"),
RuneHelmH1(Items.RUNE_HELM_H1_10704, intArrayOf(Items.RUNE_HELM_H1_10286), type = StorableFamily.TREASURE_CHEST_HIGH, "Rune heraldic helm"),
RuneHelmH2(Items.RUNE_HELM_H2_10705, intArrayOf(Items.RUNE_HELM_H2_10288), type = StorableFamily.TREASURE_CHEST_HIGH, "Rune heraldic helm"),
RuneHelmH3(Items.RUNE_HELM_H3_10706, intArrayOf(Items.RUNE_HELM_H3_10290), type = StorableFamily.TREASURE_CHEST_HIGH, "Rune heraldic helm"),
RuneHelmH4(Items.RUNE_HELM_H4_10707, intArrayOf(Items.RUNE_HELM_H4_10292), type = StorableFamily.TREASURE_CHEST_HIGH, "Rune heraldic helm"),
RuneHelmH5(Items.RUNE_HELM_H5_10708, intArrayOf(Items.RUNE_HELM_H5_10294), type = StorableFamily.TREASURE_CHEST_HIGH, "Rune heraldic helm"),
RuneShieldH1(Items.RUNE_SHIELDH1_10667, intArrayOf(Items.RUNE_SHIELDH1_7336), type = StorableFamily.TREASURE_CHEST_HIGH, "Rune heraldic kiteshield"),
RuneShieldH2(Items.RUNE_SHIELDH2_10670, intArrayOf(Items.RUNE_SHIELDH2_7342), type = StorableFamily.TREASURE_CHEST_HIGH, "Rune heraldic kiteshield"),
RuneShieldH3(Items.RUNE_SHIELDH3_10673, intArrayOf(Items.RUNE_SHIELDH3_7348), type = StorableFamily.TREASURE_CHEST_HIGH, "Rune heraldic kiteshield"),
RuneShieldH4(Items.RUNE_SHIELDH4_10676, intArrayOf(Items.RUNE_SHIELDH4_7354), type = StorableFamily.TREASURE_CHEST_HIGH, "Rune heraldic kiteshield"),
RuneShieldH5(Items.RUNE_SHIELDH5_10679, intArrayOf(Items.RUNE_SHIELDH5_7360), type = StorableFamily.TREASURE_CHEST_HIGH, "Rune heraldic kiteshield"),
ZamorakRuneArmour(Items.ZAMORAK_PLATEBODY_10776, intArrayOf(Items.ZAMORAK_FULL_HELM_2657, Items.ZAMORAK_PLATEBODY_2653, Items.ZAMORAK_PLATELEGS_2655, Items.ZAMORAK_KITESHIELD_2659, Items.ZAMORAK_PLATESKIRT_3478), type = StorableFamily.TREASURE_CHEST_HIGH, "Zamorak rune armour"),
SaradominRuneArmour(Items.SARADOMIN_PLATE_10778, intArrayOf(Items.SARADOMIN_FULL_HELM_2665, Items.SARADOMIN_PLATEBODY_2661, Items.SARADOMIN_PLATELEGS_2663, Items.SARADOMIN_KITESHIELD_2667, Items.SARADOMIN_PLATESKIRT_3479), type = StorableFamily.TREASURE_CHEST_HIGH, "Saradomin rune armour"),
GuthixRuneArmour(Items.GUTHIX_PLATEBODY_10780, intArrayOf(Items.GUTHIX_FULL_HELM_2673, Items.GUTHIX_PLATEBODY_2669, Items.GUTHIX_PLATELEGS_2671, Items.GUTHIX_KITESHIELD_2675, Items.GUTHIX_PLATESKIRT_3480), type = StorableFamily.TREASURE_CHEST_HIGH, "Guthix rune armour"),
TrimmedBlueDhide(Items.DHIDE_BODY_T_10685, intArrayOf(Items.DHIDE_BODY_T_7376, Items.DHIDE_CHAPS_T_7384), type = StorableFamily.TREASURE_CHEST_HIGH, "Trimmed blue dragonhide armour"),
GoldTrimmedBlueDhide(Items.DHIDE_BODY_G_10684, intArrayOf(Items.DHIDE_BODY_G_7374, Items.DHIDE_CHAPS_G_7382), type = StorableFamily.TREASURE_CHEST_HIGH, "Gold-trimmed blue dragonhide armour"),
SaradominBlessedDhide(Items.SARADOMIN_DHIDE_10792, intArrayOf(Items.SARADOMIN_DHIDE_10386, Items.SARADOMIN_CHAPS_10388, Items.SARADOMIN_BRACERS_10384, Items.SARADOMIN_COIF_10390), type = StorableFamily.TREASURE_CHEST_HIGH, "Saradomin blessed dragonhide"),
GuthixBlessedDhide(Items.GUTHIX_DRAGONHIDE_10794, intArrayOf(Items.GUTHIX_DRAGONHIDE_10378, Items.GUTHIX_CHAPS_10380, Items.GUTHIX_BRACERS_10376, Items.GUTHIX_COIF_10382), type = StorableFamily.TREASURE_CHEST_HIGH, "Guthix blessed dragonhide"),
ZamorakBlessedDhide(Items.ZAMORAK_DHIDE_10790, intArrayOf(Items.ZAMORAK_DHIDE_10370, Items.ZAMORAK_CHAPS_10372, Items.ZAMORAK_BRACERS_10368, Items.ZAMORAK_COIF_10374), type = StorableFamily.TREASURE_CHEST_HIGH, "Zamorak blessed dragonhide"),
RobinHoodHat(Items.ROBIN_HOOD_HAT_10796, intArrayOf(Items.ROBIN_HOOD_HAT_2581), type = StorableFamily.TREASURE_CHEST_HIGH),
EnchantedRobes(Items.ENCHANTED_TOP_10688, intArrayOf(Items.ENCHANTED_HAT_7400, Items.ENCHANTED_TOP_7399, Items.ENCHANTED_ROBE_7398), type = StorableFamily.TREASURE_CHEST_HIGH, "Enchanted robes"),
SaradominVestments(Items.SARADOMIN_ROBE_TOP_10784, intArrayOf(Items.SARADOMIN_MITRE_10452, Items.SARADOMIN_ROBE_TOP_10458, Items.SARADOMIN_ROBE_LEGS_10464, Items.SARADOMIN_STOLE_10470, Items.SARADOMIN_CLOAK_10446, Items.SARADOMIN_CROZIER_10440), type = StorableFamily.TREASURE_CHEST_HIGH, "Saradomin vestments"),
ZamorakVestments(Items.ZAMORAK_ROBE_TOP_10786, intArrayOf(Items.ZAMORAK_MITRE_10456, Items.ZAMORAK_ROBE_TOP_10460, Items.ZAMORAK_ROBE_LEGS_10468, Items.ZAMORAK_STOLE_10474, Items.ZAMORAK_CLOAK_10450, Items.ZAMORAK_CROZIER_10444), type = StorableFamily.TREASURE_CHEST_HIGH, "Zamorak vestments"),
GuthixVestments(Items.GUTHIX_ROBE_TOP_10788, intArrayOf(Items.GUTHIX_MITRE_10454, Items.GUTHIX_ROBE_TOP_10462, Items.GUTHIX_ROBE_LEGS_10466, Items.GUTHIX_STOLE_10472, Items.GUTHIX_CLOAK_10448, Items.GUTHIX_CROZIER_10442), type = StorableFamily.TREASURE_CHEST_HIGH, "Guthix vestments"),
TrimmedAmuletOfGlory(Items.AMULET_OF_GLORYT_10719, intArrayOf(Items.AMULET_OF_GLORYT_10362), type = StorableFamily.TREASURE_CHEST_HIGH, "Trimmed amulet of glory"),
PiratesHat(Items.PIRATES_HAT_10774, intArrayOf(Items.PIRATES_HAT_2651), type = StorableFamily.TREASURE_CHEST_HIGH),
CavalierMask(Items.CAVALIER_MASK_11277, intArrayOf(Items.CAVALIER_AND_MASK_11280), type = StorableFamily.TREASURE_CHEST_HIGH),
DarkCavalier(Items.DARK_CAVALIER_10804, intArrayOf(Items.DARK_CAVALIER_2641), type = StorableFamily.TREASURE_CHEST_HIGH, "Dark brown cavalier"),
TanCavalier(Items.TAN_CAVALIER_10802, intArrayOf(Items.TAN_CAVALIER_2639), type = StorableFamily.TREASURE_CHEST_HIGH),
BlackCavalier(Items.BLACK_CAVALIER_10806, intArrayOf(Items.BLACK_CAVALIER_2643), type = StorableFamily.TREASURE_CHEST_HIGH),
RuneCane(Items.RUNE_CANE_13165, intArrayOf(Items.RUNE_CANE_13099), type = StorableFamily.TREASURE_CHEST_HIGH),
TopHat(Items.TOP_HAT_13166, intArrayOf(Items.TOP_HAT_13101), type = StorableFamily.TREASURE_CHEST_HIGH),
// Magic Wardrobe - https://runescape.wiki/w/Magic_wardrobe?oldid=848846
MysticBlueSet(Items.MYSTIC_HAT_10601, intArrayOf(Items.MYSTIC_HAT_4089, Items.MYSTIC_ROBE_TOP_4091, Items.MYSTIC_ROBE_BOTTOM_4093, Items.MYSTIC_GLOVES_4095, Items.MYSTIC_BOOTS_4097), type = StorableFamily.MAGIC_WARDROBE, "Mystic robes"),
MysticDarkSet(Items.MYSTIC_HAT_10602, intArrayOf(Items.MYSTIC_HAT_4099, Items.MYSTIC_ROBE_TOP_4101, Items.MYSTIC_ROBE_BOTTOM_4103, Items.MYSTIC_GLOVES_4105, Items.MYSTIC_BOOTS_4107), type = StorableFamily.MAGIC_WARDROBE, "Dark mystic robes"),
MysticLightSet(Items.MYSTIC_HAT_10603, intArrayOf(Items.MYSTIC_HAT_4109, Items.MYSTIC_ROBE_TOP_4111, Items.MYSTIC_ROBE_BOTTOM_4113, Items.MYSTIC_GLOVES_4115, Items.MYSTIC_BOOTS_4117), type = StorableFamily.MAGIC_WARDROBE, "Light mystic robes"),
SkeletalSet(Items.SKELETAL_HELM_10604, intArrayOf(Items.SKELETAL_HELM_6137, Items.SKELETAL_TOP_6139, Items.SKELETAL_BOTTOMS_6141, Items.SKELETAL_GLOVES_6153, Items.SKELETAL_BOOTS_6147), type = StorableFamily.MAGIC_WARDROBE, "Skeletal"),
InfinitySet(Items.INFINITY_TOP_10605, intArrayOf(Items.INFINITY_HAT_6918, Items.INFINITY_TOP_6916, Items.INFINITY_BOTTOMS_6924, Items.INFINITY_GLOVES_6922, Items.INFINITY_BOOTS_6920), type = StorableFamily.MAGIC_WARDROBE, "Infinity robes"),
SplitbarkSet(Items.SPLITBARK_HELM_10606, intArrayOf(Items.SPLITBARK_HELM_3385, Items.SPLITBARK_BODY_3387, Items.SPLITBARK_LEGS_3389, Items.SPLITBARK_GAUNTLETS_3391, Items.SPLITBARK_BOOTS_3393), type = StorableFamily.MAGIC_WARDROBE, "Splitbark armour"),
GhostlySet(Items.GHOSTLY_BOOTS_10607, intArrayOf(Items.GHOSTLY_ROBE_6107, Items.GHOSTLY_ROBE_6108, Items.GHOSTLY_HOOD_6109, Items.GHOSTLY_GLOVES_6110, Items.GHOSTLY_BOOTS_6106, Items.GHOSTLY_CLOAK_6111), type = StorableFamily.MAGIC_WARDROBE, "Ghostly robes"),
MoonclanSet(Items.MOONCLAN_HAT_10608, intArrayOf(Items.MOONCLAN_HELM_9068, Items.MOONCLAN_ARMOUR_9070, Items.MOONCLAN_SKIRT_9071, Items.MOONCLAN_GLOVES_9072, Items.MOONCLAN_BOOTS_9073), type = StorableFamily.MAGIC_WARDROBE, "Moonclan robes"),
LunarSet(Items.LUNAR_HELM_10609, intArrayOf(Items.LUNAR_HELM_9096, Items.LUNAR_TORSO_9097, Items.LUNAR_LEGS_9098, Items.LUNAR_GLOVES_9099, Items.LUNAR_BOOTS_9100, Items.LUNAR_CAPE_9101, Items.LUNAR_AMULET_9102, Items.LUNAR_RING_9104), type = StorableFamily.MAGIC_WARDROBE, "Lunar robes"),
RunecrafterYellow(Items.RUNECRAFTER_HAT_13656, intArrayOf(Items.RUNECRAFTER_HAT_13615, Items.RUNECRAFTER_ROBE_13614, Items.RUNECRAFTER_SKIRT_13617), type = StorableFamily.MAGIC_WARDROBE, "Runecrafter robes (yellow)"),
RunecrafterGreen(Items.RUNECRAFTER_HAT_13657, intArrayOf(Items.RUNECRAFTER_HAT_13620, Items.RUNECRAFTER_ROBE_13619, Items.RUNECRAFTER_SKIRT_13622), type = StorableFamily.MAGIC_WARDROBE, "Runecrafter robes (green)"),
RunecrafterBlue(Items.RUNECRAFTER_HAT_13658, intArrayOf(Items.RUNECRAFTER_HAT_13625, Items.RUNECRAFTER_ROBE_13624, Items.RUNECRAFTER_SKIRT_13627), type = StorableFamily.MAGIC_WARDROBE, "Runecrafter robes (blue)"),
DagonHaiSet(Items.DAGONHAI_ROBE_TOP_14497, intArrayOf(Items.DAGONHAI_HAT_14499, Items.DAGONHAI_ROBE_TOP_14497, Items.DAGONHAI_ROBE_BOTTOM_14501), type = StorableFamily.MAGIC_WARDROBE, "Dagon'Hai Robes"),
// Armour Case - https://runescape.wiki/w/Armour_case?oldid=848835
DecorativeArmour(Items.DECORATIVE_ARMOUR_10610, intArrayOf(Items.DECORATIVE_ARMOUR_4069, Items.DECORATIVE_SWORD_4068, Items.DECORATIVE_ARMOUR_4070, Items.DECORATIVE_HELM_4071, Items.DECORATIVE_SHIELD_4072, Items.DECORATIVE_ARMOUR_4504, Items.DECORATIVE_SWORD_4503, Items.DECORATIVE_ARMOUR_4505, Items.DECORATIVE_HELM_4506, Items.DECORATIVE_SHIELD_4507, Items.DECORATIVE_ARMOUR_4509, Items.DECORATIVE_SWORD_4508, Items.DECORATIVE_ARMOUR_4510, Items.DECORATIVE_HELM_4511, Items.DECORATIVE_SHIELD_4512), type = StorableFamily.ARMOUR_CASE, "Castlewars armour"), // should be any one colour set per wiki
VoidKnightArmour(Items.VOID_KNIGHT_TOP_10611, intArrayOf(Items.VOID_KNIGHT_TOP_8839, Items.VOID_KNIGHT_ROBE_8840, Items.VOID_KNIGHT_GLOVES_8842), type = StorableFamily.ARMOUR_CASE, "Void Knight armour"),
RogueArmour(Items.ROGUE_MASK_10612, intArrayOf(Items.ROGUE_MASK_5554, Items.ROGUE_TOP_5553, Items.ROGUE_TROUSERS_5555), type = StorableFamily.ARMOUR_CASE, "Rogue armour"),
SpinedArmour(Items.SPINED_HELM_10614, intArrayOf(Items.SPINED_HELM_6131, Items.SPINED_BODY_6133, Items.SPINED_CHAPS_6135, Items.SPINED_BOOTS_6143, Items.SPINED_GLOVES_6149), type = StorableFamily.ARMOUR_CASE, "Spined armour"),
RockshellArmour(Items.ROCK_SHELL_HELM_10613, intArrayOf(Items.ROCK_SHELL_HELM_6128, Items.ROCK_SHELL_PLATE_6129, Items.ROCK_SHELL_LEGS_6130, Items.ROCK_SHELL_BOOTS_6145, Items.ROCK_SHELL_GLOVES_6151), type = StorableFamily.ARMOUR_CASE, "Rockshell armour"),
TribalMaskP(Items.TRIBAL_MASK_10615, intArrayOf(Items.TRIBAL_MASK_6335), type = StorableFamily.ARMOUR_CASE, "Broodoo mask (poison)"),
TribalMaskD(Items.TRIBAL_MASK_10616, intArrayOf(Items.TRIBAL_MASK_6337), type = StorableFamily.ARMOUR_CASE, "Broodoo mask (disease)"),
TribalMaskC(Items.TRIBAL_MASK_10617, intArrayOf(Items.TRIBAL_MASK_6339), type = StorableFamily.ARMOUR_CASE, "Broodoo mask (combat)"),
WhiteKnightArmour(Items.WHITE_PLATEBODY_10618, intArrayOf(Items.WHITE_PLATEBODY_6617, Items.WHITE_FULL_HELM_6623, Items.WHITE_PLATELEGS_6625, Items.WHITE_PLATESKIRT_6627), type = StorableFamily.ARMOUR_CASE, "White knight armour"),
TempleKnightInitiateArmour(Items.INITIATE_HAUBERK_10619, intArrayOf(Items.INITIATE_SALLET_5574, Items.INITIATE_HAUBERK_5575, Items.INITIATE_CUISSE_5576), type = StorableFamily.ARMOUR_CASE, "Temple Knight initiate armour"),
TempleKnightProselyteArmour(Items.PROSELYTE_HAUBERK_10620, intArrayOf(Items.PROSELYTE_SALLET_9672, Items.PROSELYTE_HAUBERK_9674, Items.PROSELYTE_CUISSE_9676, Items.PROSELYTE_TASSET_9678), type = StorableFamily.ARMOUR_CASE, "Temple Knight proselyte armour"),
MournerGear(Items.MOURNER_TOP_10621, intArrayOf(Items.MOURNER_TOP_6065, Items.MOURNER_TROUSERS_6066, Items.MOURNER_BOOTS_6069, Items.MOURNER_CLOAK_6070), type = StorableFamily.ARMOUR_CASE, "Mourner gear"),
GraahkHunterGear(Items.GRAAHK_TOP_10624, intArrayOf(Items.GRAAHK_TOP_10049, Items.GRAAHK_LEGS_10047, Items.GRAAHK_HEADDRESS_10051), type = StorableFamily.ARMOUR_CASE, "Graahk hunter gear"),
LarupiaHunterGear(Items.LARUPIA_TOP_10623, intArrayOf(Items.LARUPIA_TOP_10043, Items.LARUPIA_LEGS_10041, Items.LARUPIA_HAT_10045), type = StorableFamily.ARMOUR_CASE, "Larupia hunter gear"),
KyattHunterGear(Items.KYATT_TOP_10622, intArrayOf(Items.KYATT_TOP_10037, Items.KYATT_LEGS_10035, Items.KYATT_HAT_10039), type = StorableFamily.ARMOUR_CASE, "Kyatt hunter gear"),
PolarCamouflageGear(Items.POLAR_CAMO_TOP_10628, intArrayOf(Items.POLAR_CAMO_TOP_10628, Items.POLAR_CAMO_LEGS_10067), type = StorableFamily.ARMOUR_CASE, "Polar camouflage gear"),
JungleCamouflageGear(Items.JUNGLE_CAMO_TOP_10626, intArrayOf(Items.JUNGLE_CAMO_TOP_10057, Items.JUNGLE_CAMO_LEGS_10059), type = StorableFamily.ARMOUR_CASE, "Jungle camouflage gear"),
WoodCamouflageGear(Items.WOOD_CAMO_TOP_10625, intArrayOf(Items.WOOD_CAMO_TOP_10053, Items.WOOD_CAMO_LEGS_10055), type = StorableFamily.ARMOUR_CASE, "Wood camouflage gear"),
DesertCamouflageGear(Items.DESERT_CAMO_TOP_10627, intArrayOf(Items.DESERT_CAMO_TOP_10061, Items.DESERT_CAMO_LEGS_10063), type = StorableFamily.ARMOUR_CASE, "Desert camouflage gear"),
BuildersCostume(Items.BUILDERS_SHIRT_10863, intArrayOf(Items.BUILDERS_SHIRT_10863, Items.BUILDERS_TROUSERS_10864, Items.BUILDERS_BOOTS_10865), type = StorableFamily.ARMOUR_CASE, "Builder's costume"),
LumberjackCostume(Items.LUMBERJACK_TOP_10945, intArrayOf(Items.LUMBERJACK_TOP_10939, Items.LUMBERJACK_LEGS_10940, Items.LUMBERJACK_HAT_10941), type = StorableFamily.ARMOUR_CASE, "Lumberjack costume"),
BomberJacketCostume(Items.BOMBER_JACKET_11135, intArrayOf(Items.BOMBER_JACKET_9944, Items.BOMBER_CAP_9945), type = StorableFamily.ARMOUR_CASE, "Bomber jacket costume"),
HAMRobes(Items.HAM_SHIRT_11274, intArrayOf(Items.HAM_SHIRT_4298, Items.HAM_ROBE_4300, Items.HAM_HOOD_4302, Items.HAM_CLOAK_4304, Items.HAM_LOGO_4306, Items.BOOTS_4310, Items.GLOVES_4308), type = StorableFamily.ARMOUR_CASE, "H.A.M. robes"),
VoidMelee(Items.VOID_MELEE_HELM_11676, intArrayOf(Items.VOID_MELEE_HELM_11665), type = StorableFamily.ARMOUR_CASE, "Void melee helm"),
VoidRanger(Items.VOID_RANGER_HELM_11675, intArrayOf(Items.VOID_RANGER_HELM_11664), type = StorableFamily.ARMOUR_CASE, "Void ranger helm"),
VoidMage(Items.VOID_MAGE_HELM_11674, intArrayOf(Items.VOID_MAGE_HELM_11663), type = StorableFamily.ARMOUR_CASE, "Void mage helm"),
BlackNaval(Items.BLACK_NAVAL_SHIRT_13185, intArrayOf(Items.BLACK_NAVAL_SHIRT_8956, Items.BLACK_NAVY_SLACKS_8995, Items.BLACK_TRICORN_HAT_8963),type = StorableFamily.ARMOUR_CASE, "Black naval costume"),
RedNaval(Items.RED_NAVAL_SHIRT_13183, intArrayOf(Items.RED_TRICORN_HAT_8961, Items.RED_NAVAL_SHIRT_8954, Items.RED_NAVY_SLACKS_8993), type = StorableFamily.ARMOUR_CASE, "Red naval costume"),
BlueNaval(Items.BLUE_NAVAL_SHIRT_13181, intArrayOf(Items.BLUE_TRICORN_HAT_8959, Items.BLUE_NAVAL_SHIRT_8952, Items.BLUE_NAVY_SLACKS_8991), type = StorableFamily.ARMOUR_CASE, "Blue naval costume"),
BrownNaval(Items.BROWN_NAVAL_SHIRT_13184, intArrayOf(Items.BROWN_TRICORN_HAT_8962, Items.BROWN_NAVAL_SHIRT_8955, Items.BROWN_NAVY_SLACKS_8994), type = StorableFamily.ARMOUR_CASE, "Brown naval costume"),
GreenNaval(Items.GREEN_NAVAL_SHIRT_13182, intArrayOf(Items.GREEN_TRICORN_HAT_8960, Items.GREEN_NAVAL_SHIRT_8953, Items.GREEN_NAVY_SLACKS_8992), type = StorableFamily.ARMOUR_CASE, "Green naval costume"),
GreyNaval(Items.GREY_NAVAL_SHIRT_13187, intArrayOf(Items.GREY_TRICORN_HAT_8965, Items.GREY_NAVAL_SHIRT_8958, Items.GREY_NAVY_SLACKS_8997), type = StorableFamily.ARMOUR_CASE, "Grey naval costume"),
PurpleNaval(Items.PURPLE_NAVAL_SHIRT_13186, intArrayOf(Items.PURPLE_TRICORN_HAT_8964, Items.PURPLE_NAVAL_SHIRT_8957, Items.PURPLE_NAVY_SLACKS_8996), type = StorableFamily.ARMOUR_CASE, "Purple naval costume"),
EliteBlackArmour(Items.ELITE_BLACK_PLATEBODY_14492, intArrayOf(Items.ELITE_BLACK_FULL_HELM_14494, Items.ELITE_BLACK_PLATEBODY_14492, Items.ELITE_BLACK_PLATELEGS_14490), type = StorableFamily.ARMOUR_CASE, "Elite Black Armour")
;
companion object {
/**
* Map of all item ids to their [Storable].
*/
private val idToStorable: Map<Int, Storable> by lazy {
values().flatMap { storable ->
storable.takeIds.map { id -> id to storable }
}.toMap()
}
/**
* Set of all item ids across all [Storable].
*/
private val allItemIds: Set<Int> by lazy { idToStorable.keys }
/**
* Precomputed set of item ids for each Storable for fast lookup.
*/
private val storableIdSets: Map<Storable, Set<Int>> by lazy {
values().associateWith { it.takeIds.toSet() }
}
/**
* Returns all item ids from all storables.
*/
fun allIds(): Set<Int> = allItemIds
/**
* Finds the storable for the given item id.
*/
fun fromId(id: Int): Storable? = idToStorable[id]
/**
* Checks if the player owns at least one item from
* the storable in equipment or bank.
*/
fun hasStorable(player: Player, storable: Storable): Boolean {
val ownedIds = (player.equipment.toArray() + player.bank.toArray())
.mapNotNull { it?.id }
.toSet()
return storableIdSets[storable]?.any { it in ownedIds } ?: false
}
/**
* Checks if the player owns any item
* from any storable in equipment or bank.
*/
fun hasAnyStorable(player: Player): Boolean {
val ownedIds = (player.equipment.toArray() + player.bank.toArray())
.mapNotNull { it?.id }
.toSet()
return allItemIds.any { it in ownedIds }
}
/**
* Checks if the player has at least one
* item from the given storable equipped.
*/
fun hasStorableEquipped(player: Player, storable: Storable): Boolean {
val equippedIds = player.equipment.toArray().mapNotNull { it?.id }.toSet()
return storableIdSets[storable]?.any { it in equippedIds } ?: false
}
/**
* Checks if the player has at least one
* item from the given storable in inventory.
*/
fun hasStorableInInventory(player: Player, storable: Storable): Boolean {
val inventoryIds = player.inventory.toArray().mapNotNull { it?.id }.toSet()
return storableIdSets[storable]?.any { it in inventoryIds } ?: false
}
/**
* Gets all [Storable]s that the player owns in their bank.
*/
fun getStorablesInBank(player: Player): List<Storable> {
val bankIds = player.bank.toArray().mapNotNull { it?.id }.toSet()
return values().filter { storable ->
storableIdSets[storable]?.any { it in bankIds } ?: false
}
}
}
}

View file

@ -0,0 +1,18 @@
package content.global.skill.construction.decoration.pohstorage
/**
* Types (families) of storables in Construction's POH Costume Room or Bookcase.
*/
enum class StorableFamily {
FANCY_DRESS,
ARMOUR_CASE,
MAGIC_WARDROBE,
TREASURE_CHEST_LOW,
TREASURE_CHEST_MED,
TREASURE_CHEST_HIGH,
BOOKCASE,
CAPE_RACK,
CAPE_RACK_SKILL,
TOY_BOX
}

View file

@ -0,0 +1,74 @@
package content.global.skill.construction.decoration.pohstorage
import org.json.simple.JSONArray
import org.json.simple.JSONObject
/**
* This represents the container where all the POH storable items are.
*/
class StorageContainer {
// item IDs stored in this container
private val stored: MutableSet<Int> = mutableSetOf()
// page index for the active interface
private var currentPage: Int = 0
// functions to add item, withdraw, check for, etc.
fun addItem(id: Int) {
stored.add(id)
}
fun withdraw(id: Int): Boolean {
return stored.remove(id)
}
fun contains(id: Int): Boolean = id in stored
fun getItems(): List<Int> = stored.toList()
// paging
fun getPageIndex(): Int = currentPage
fun nextPage(totalItems: Int, pageSize: Int) {
val next = currentPage + 1
if (next * pageSize < totalItems) {
currentPage = next
}
}
fun prevPage() {
if (currentPage > 0) {
currentPage--
}
}
fun resetPage() {
currentPage = 0
}
// saving to json and reading back
fun toJson(): JSONObject {
val json = JSONObject()
val items = JSONArray()
stored.forEach { items.add(it) }
json["items"] = items
return json
}
fun readJson(json: JSONObject) {
stored.clear()
val items = json["items"] as? JSONArray ?: return
for (id in items) {
stored.add((id as Number).toInt())
}
currentPage = 0
}
// clear storage
fun clear() {
stored.clear()
currentPage = 0
}
}

View file

@ -0,0 +1,385 @@
package content.global.skill.construction.decoration.pohstorage
import core.api.*
import core.game.interaction.InterfaceListener
import core.game.node.entity.player.Player
import core.game.node.item.Item
import core.net.packet.PacketRepository
import core.net.packet.context.ContainerContext
import core.net.packet.out.ContainerPacket
import org.rs09.consts.Items
/**
* Handles the interface and interactions for
* the Construction Costume Room storage boxes and Bookshelves
* https://youtu.be/tAIDQRh4U2g?si=oDaKEJT-ZPs4MoZa&t=261
* https://www.youtube.com/watch?v=D2aVfpPCAgY&t=41s - great view of all the treasure chests
* https://youtu.be/AK6wVwumvHY?si=XAJsQeSdRlBZSM1f - mid-level costume room
* https://www.youtube.com/watch?v=sNEOq_Barlg&t=126s - 2011, but a good video
*/
class StorageInterface : InterfaceListener {
companion object {
private const val INTERFACE = 467
private const val COMPONENT = 164
private const val SIZE = 30
private const val PAGE_SIZE = SIZE - 1
private const val BUTTON_MORE = Items.MORE_10165
private const val BUTTON_BACK = Items.BACK_10166
/**
* Singleton instance of the storage interface.
*/
lateinit var instance: StorageInterface
private set
/**
* Public method to get the storage container for a specific family.
*/
fun getStorageContainer(player: Player, type: StorableFamily) =
player.getPOHStorageState().getContainer(type)
/**
* Opens the storage box.
*
* @param player The player.
* @param type The type of storage to open.
* @param box the specific tier of storage we are opening.
*/
fun openStorage(player: Player, type: StorableFamily, box: StorageOptionPlugin.StorageBox) {
instance.openStorageForType(player, type, box)
}
/**
* Finds the Storable enum entry associated with a given display item ID.
*/
fun findStorableByDisplayId(id: Int): Storable? =
Storable.values().find { it.displayId == id }
}
/**
* The attribute of whether the player has read a book or not is stored in a set of two bitfields.
* There are 70 books so that's why two longs are needed.
*/
object BookcaseBitfields {
// 0-63
const val BOOKCASE_LOW_ATTR = "/save:bookcase_bitfield_low"
// 64-127
const val BOOKCASE_HIGH_ATTR = "/save:bookcase_bitfield_high"
data class BookcaseBits(var low: Long, var high: Long)
// Retrieves both Long bitfields.
fun get(player: Player): BookcaseBits {
val low = getAttribute(player, BOOKCASE_LOW_ATTR, 0L) as? Long ?: 0L
val high = getAttribute(player, BOOKCASE_HIGH_ATTR, 0L) as? Long ?: 0L
return BookcaseBits(low, high)
}
// Sets both Long bitfields.
fun set(player: Player, bits: BookcaseBits) {
setAttribute(player, BOOKCASE_LOW_ATTR, bits.low)
setAttribute(player, BOOKCASE_HIGH_ATTR, bits.high)
}
// Checks if a specific bit is set within the two Longs.
fun isBitSet(bits: BookcaseBits, bitIndex: Int): Boolean {
return if (bitIndex < 64) {
(bits.low and (1L shl bitIndex)) != 0L
} else {
val highIndex = bitIndex - 64
(bits.high and (1L shl highIndex)) != 0L
}
}
// Sets a specific bit within the two Longs.
fun setBit(bits: BookcaseBits, bitIndex: Int): BookcaseBits {
if (bitIndex < 64) {
bits.low = bits.low or (1L shl bitIndex)
} else {
val highIndex = bitIndex - 64
bits.high = bits.high or (1L shl highIndex)
}
return bits
}
}
init {
instance = this
}
override fun defineInterfaceListeners() {
on(INTERFACE) { player, _, _, buttonId, _, _ ->
val typeName = getAttribute(player, "con:storage:type", null) as? String ?: return@on true
val type = StorableFamily.valueOf(typeName.uppercase())
handleStorageInteraction(player, buttonId, type)
return@on true
}
}
/**
* Gets the StorableFamilies that should be displayed in the interface for a given primary type.
* This handles the special case for the Cape Rack because capes are classified as two different families/types
* (normal and skillcape) but displayed in the same storage.
*/
private fun getFamiliesToDisplay(primaryType: StorableFamily): List<StorableFamily> {
return when (primaryType) {
// if the cape rack can hold skill capes, also show normal capes
StorableFamily.CAPE_RACK_SKILL -> listOf(StorableFamily.CAPE_RACK, StorableFamily.CAPE_RACK_SKILL)
// else only display the items for the primary family
else -> listOf(primaryType)
}
}
/**
* This handles filtering the list for books.
* Only items are shown that have already been read.
* If the book is part of a quest, the quest must be complete before it shows up in the POH.
* If the index is -1 it is not a book, and not filtered.
*/
private fun getFilteredItems(player: Player, families: List<StorableFamily>, type: StorableFamily): List<Storable> {
// filter for normal items
var allItems = Storable.values().filter { it.type in families }
// filter for bookcase
if (type == StorableFamily.BOOKCASE) {
val bookcaseBits = BookcaseBitfields.get(player)
allItems = allItems.filter { storable ->
// check if read
val isRead = if (storable.bitIndex == -1) true else BookcaseBitfields.isBitSet(bookcaseBits, storable.bitIndex)
// check if quest is done
val questMet = storable.questReq == null || isQuestComplete(player, storable.questReq)
isRead && questMet
}
}
return allItems
}
/**
* Handles interaction with storage interface.
*/
private fun handleStorageInteraction(player: Player, buttonId: Int, type: StorableFamily) {
val container = getStorageContainer(player, type)
val families = getFamiliesToDisplay(type)
val allItems = getFilteredItems(player, families, type)
val pageIndex = container.getPageIndex()
val pageItems = allItems.drop(pageIndex * PAGE_SIZE).take(PAGE_SIZE)
val slots = MutableList<Any?>(SIZE) { null }
var idx = 0
pageItems.forEach {
if (idx >= SIZE) return@forEach
slots[idx++] = it
}
val hasPrev = pageIndex > 0
val hasNext = allItems.size > (pageIndex + 1) * PAGE_SIZE
if (hasNext && idx < SIZE) slots[idx++] = "MORE"
if (hasPrev && idx < SIZE) slots[idx] = "BACK"
val slotIndex = when {
buttonId in 56..(56 + (SIZE - 1) * 2) step 2 -> (buttonId - 56) / 2
buttonId in 165..223 step 2 -> (buttonId - 165) / 2
else -> return
}
when (val clicked = slots.getOrNull(slotIndex)) {
"MORE" -> {
container.nextPage(allItems.size, PAGE_SIZE)
openInterface(player, INTERFACE)
updateStorageInterface(player, type)
}
"BACK" -> {
container.prevPage()
openInterface(player, INTERFACE)
updateStorageInterface(player, type)
}
is Storable -> processItemTransaction(player, clicked, type)
}
}
/**
* Handles taking or depositing an item from/to the storage.
*/
private fun processItemTransaction(player: Player, item: Storable, type: StorableFamily) {
// container for the interface/capacity check based on the object the player clicked
val interfaceContainer = getStorageContainer(player, type)
// container for the actual storage/retrieval based on the item's family
val targetContainer = getStorageContainer(player, item.type)
// get stored items from the target container
val storedItems = targetContainer.getItems().toSet()
val box = getAttribute(player, "con:storage:box", null) as? StorageOptionPlugin.StorageBox ?: return
val capacity = box.capacity
// gets the currently stored IDs
val storedTakeIds = item.takeIds.filter { it in storedItems }
if (storedTakeIds.isNotEmpty()) {
// withdraw items
if (freeSlots(player) < storedTakeIds.size) {
sendMessage(player, "You don't have enough inventory space.")
return
}
sendMessage(player, "You take the item from the box.")
storedTakeIds.forEach { id ->
addItem(player, id, 1)
targetContainer.withdraw(id)
}
} else {
// deposit items
// capacity check
if (capacity < Int.MAX_VALUE) {
val familyToCheck = when (type) {
// for cape racks, only skillcapes are counted against the capacity
StorableFamily.CAPE_RACK_SKILL -> StorableFamily.CAPE_RACK_SKILL
// for all other storage, check against the primary family type for capacity
else -> type
}
// check if skillcape
if (item.type == familyToCheck) {
val capacityCheckItems = interfaceContainer.getItems().toSet()
// calculate stored skillcapes
val storedSetsCount = Storable.values()
.filter { it.type == familyToCheck }
.count { storable ->
storable.takeIds.any { it in capacityCheckItems }
}
// enforce capacity limits
if (storedSetsCount >= capacity) {
val message = when (familyToCheck) {
StorableFamily.CAPE_RACK_SKILL -> "You cannot store any more skillcapes in this container."
else -> "You cannot store any more items in this container."
}
sendMessage(player, message)
return
}
}
}
val presentTakeIds = item.takeIds.filter { player.inventory.contains(it, 1) }
if (presentTakeIds.isEmpty()) {
sendMessage(player, "You don't have that item in your inventory.")
return
}
sendMessage(player, "You put the item into the box.")
presentTakeIds.forEach { id ->
removeItem(player, Item(id))
targetContainer.addItem(id)
}
}
updateStorageInterface(player, type)
}
private fun updateStorageInterface(player: Player, type: StorableFamily) {
val container = getStorageContainer(player, type)
val families = getFamiliesToDisplay(type)
val allItems = getFilteredItems(player, families, type)
val allStoredItems = mutableSetOf<Int>()
families.forEach { family ->
val familyContainer = getStorageContainer(player, family)
allStoredItems.addAll(familyContainer.getItems())
}
val stored = allStoredItems.toSet()
val pageIndex = container.getPageIndex()
val pageItems = allItems.drop(pageIndex * PAGE_SIZE).take(PAGE_SIZE)
val slots = MutableList<Any?>(SIZE) { null }
var idx = 0
pageItems.forEach {
if (idx >= SIZE) return@forEach
slots[idx++] = it
}
val hasPrev = pageIndex > 0
val hasNext = allItems.size > (pageIndex + 1) * PAGE_SIZE
if (hasNext && idx < SIZE) slots[idx++] = "MORE"
if (hasPrev && idx < SIZE) slots[idx] = "BACK"
// names each box
val title = when {
type.name.contains("FANCY_DRESS") -> "Fancy dress box"
type.name.contains("TOY_BOX") -> "Toy box"
type.name.contains("MAGIC_WARDROBE") -> "Magic wardrobe"
type.name.contains("ARMOUR_CASE") -> "Armour case"
type.name.contains("BOOKCASE") -> "Bookcase"
type.name.contains("CAPE_RACK") -> "Cape rack"
type.name.contains("TREASURE_CHEST_LOW") -> "Low-level Treasure Trail rewards"
type.name.contains("TREASURE_CHEST_MED") -> "Medium-level Treasure Trail rewards"
type.name.contains("TREASURE_CHEST_HIGH") -> "High-level Treasure Trail rewards"
// to catch any other name
else -> type.name.lowercase().replaceFirstChar(Char::titlecase) + " box"
}
// child 225 is the title block
setInterfaceText(player, title, INTERFACE, 225)
val itemsArray = slots.mapNotNull {
when (it) {
is Storable -> Item(it.displayId)
"MORE" -> Item(BUTTON_MORE)
"BACK" -> Item(BUTTON_BACK)
else -> null
}
}.toTypedArray()
PacketRepository.send(
ContainerPacket::class.java,
ContainerContext(player, INTERFACE, COMPONENT, SIZE, itemsArray, false)
)
repeat(SIZE) { i ->
val nameComponent = 55 + i * 2
val iconComponent = 165 + i * 2
val hiddenIconComponent = 166 + i * 2
val obj = slots[i]
val (name, isStored) = when (obj) {
is Storable -> {
val isStored = obj.takeIds.any { it in stored }
obj.setName to isStored
}
"MORE" -> "More..." to true
"BACK" -> "Back..." to true
else -> "" to false
}
setInterfaceText(player, name, INTERFACE, nameComponent)
setComponentVisibility(player, INTERFACE, nameComponent, false)
if (obj is Storable) {
// this displays if the items are stored or not stored
setComponentVisibility(player, INTERFACE, iconComponent, !isStored)
setComponentVisibility(player, INTERFACE, hiddenIconComponent, isStored)
} else {
// this is for navigation buttons or empty slots
setComponentVisibility(player, INTERFACE, iconComponent, true)
setComponentVisibility(player, INTERFACE, hiddenIconComponent, true)
}
}
}
private fun openStorageForType(player: Player, type: StorableFamily, box: StorageOptionPlugin.StorageBox) {
setAttribute(player, "con:storage:type", type.name)
setAttribute(player, "con:storage:box", box)
openInterface(player, INTERFACE)
updateStorageInterface(player, type)
}
}

View file

@ -0,0 +1,182 @@
package content.global.skill.construction.decoration.pohstorage
import core.api.*
import core.cache.def.impl.SceneryDefinition
import core.game.interaction.OptionHandler
import core.game.node.Node
import core.game.node.entity.player.Player
import core.game.node.scenery.Scenery
import core.plugin.Initializable
import core.plugin.Plugin
import org.rs09.consts.Animations
import org.rs09.consts.Items
import org.rs09.consts.Sounds
import org.rs09.consts.Scenery as Obj
/**
* Handles interactions with storage objects.
*/
@Initializable
class StorageOptionPlugin : OptionHandler() {
/**
* Represents all storage boxes & configuration.
*/
enum class StorageBox(val objectIds: IntArray, val storableFamily: StorableFamily? = null, val openable: Boolean = false, val closable: Boolean = false, val capacity: Int = Int.MAX_VALUE) {
/* Bookcase
* Wooden: all
* Oak: all
* Mahogany: all */
BOOKCASE(intArrayOf(Obj.BOOKCASE_13597, Obj.BOOKCASE_13598, Obj.BOOKCASE_13599), StorableFamily.BOOKCASE),
/* Cape Rack
* Oak: normal capes
* Teak: 1 skillcape
* Mahogany: 5 skillcapes
* Gilded: 10 skillcapes
* Marble: all
* Magic: all */
CAPE_RACK(intArrayOf(Obj.OAK_CAPE_RACK_18766), StorableFamily.CAPE_RACK),
CAPE_RACK_ONE(intArrayOf(Obj.TEAK_CAPE_RACK_18767), StorableFamily.CAPE_RACK_SKILL, capacity = 1),
CAPE_RACK_FIVE(intArrayOf(Obj.MAHOGANY_CAPE_RACK_18768), StorableFamily.CAPE_RACK_SKILL, capacity = 5),
CAPE_RACK_TEN(intArrayOf(Obj.GILDED_CAPE_RACK_18769), StorableFamily.CAPE_RACK_SKILL, capacity = 10),
CAPE_RACK_ALL(intArrayOf(Obj.MARBLE_CAPE_RACK_18770, Obj.MAGIC_CAPE_RACK_18771), StorableFamily.CAPE_RACK_SKILL),
/* Fancy Dress Box
* Oak: 2
* Teak: 4
* Mahogany: all */
FANCY_BOX_2(intArrayOf(Obj.FANCY_DRESS_BOX_18773), StorableFamily.FANCY_DRESS, false, true, 2),
FANCY_BOX_4(intArrayOf(Obj.FANCY_DRESS_BOX_18775), StorableFamily.FANCY_DRESS, false, true, 4),
FANCY_BOX_ALL(intArrayOf(Obj.FANCY_DRESS_BOX_18777), StorableFamily.FANCY_DRESS, false, true),
FANCY_BOX_OPEN(intArrayOf(Obj.FANCY_DRESS_BOX_18772, Obj.FANCY_DRESS_BOX_18774, Obj.FANCY_DRESS_BOX_18776), openable = true),
/* Toy Box
* Oak: all
* Teak: all
* Mahogany: all */
TOY_BOX_OPEN(intArrayOf(Obj.TOY_BOX_18798, Obj.TOY_BOX_18800, Obj.TOY_BOX_18802), openable = true),
TOY_BOX_CLOSE(intArrayOf(Obj.TOY_BOX_18799, Obj.TOY_BOX_18801, Obj.TOY_BOX_18803), closable = true, storableFamily = StorableFamily.TOY_BOX),
/* Treasure Chest
* Oak: Low
* Teak: Low, Medium
* Mahogany: Low, Medium, High */
TREASURE_LOW(intArrayOf(Obj.TREASURE_CHEST_18805), StorableFamily.TREASURE_CHEST_LOW),
TREASURE_MED(intArrayOf(Obj.TREASURE_CHEST_18807), StorableFamily.TREASURE_CHEST_MED),
TREASURE_HIGH(intArrayOf(Obj.TREASURE_CHEST_18809), StorableFamily.TREASURE_CHEST_HIGH),
TREASURE_OPEN(intArrayOf(Obj.TREASURE_CHEST_18804, Obj.TREASURE_CHEST_18806, Obj.TREASURE_CHEST_18808), openable = true),
/* Magic Wardobe
* Oak: 1 set
* Carved oak: 2 sets
* Teak: 3 sets
* Carved Teak: 4 sets
* Mahogany: 5 sets
* Gilded: 6 sets
* Marble: all sets */
WARDROBE_1(intArrayOf(Obj.MAGIC_WARDROBE_18785), StorableFamily.MAGIC_WARDROBE, false, true, 1),
WARDROBE_2(intArrayOf(Obj.MAGIC_WARDROBE_18787), StorableFamily.MAGIC_WARDROBE, false, true, 2),
WARDROBE_3(intArrayOf(Obj.MAGIC_WARDROBE_18789), StorableFamily.MAGIC_WARDROBE, false, true, 3),
WARDROBE_4(intArrayOf(Obj.MAGIC_WARDROBE_18791), StorableFamily.MAGIC_WARDROBE, false, true, 4),
WARDROBE_5(intArrayOf(Obj.MAGIC_WARDROBE_18793), StorableFamily.MAGIC_WARDROBE, false, true, 5),
WARDROBE_6(intArrayOf(Obj.MAGIC_WARDROBE_18795), StorableFamily.MAGIC_WARDROBE, false, true, 6),
WARDROBE_ALL(intArrayOf(Obj.MAGIC_WARDROBE_18797), StorableFamily.MAGIC_WARDROBE),
WARDROBE_OPEN(intArrayOf(Obj.MAGIC_WARDROBE_18784, Obj.MAGIC_WARDROBE_18786, Obj.MAGIC_WARDROBE_18788, Obj.MAGIC_WARDROBE_18790, Obj.MAGIC_WARDROBE_18792, Obj.MAGIC_WARDROBE_18794, Obj.MAGIC_WARDROBE_18796), openable = true),
/* Armour Case
* Oak: 2
* Teak: 4
* Mahogany: all */
ARMOUR_2(intArrayOf(Obj.ARMOUR_CASE_18779), StorableFamily.ARMOUR_CASE, false, true, 2),
ARMOUR_4(intArrayOf(Obj.ARMOUR_CASE_18781), StorableFamily.ARMOUR_CASE, false, true, 4),
ARMOUR_ALL(intArrayOf(Obj.ARMOUR_CASE_18783), StorableFamily.ARMOUR_CASE),
ARMOUR_OPEN(intArrayOf(Obj.ARMOUR_CASE_18778, Obj.ARMOUR_CASE_18780, Obj.ARMOUR_CASE_18782), openable = true)
}
private val allBoxes = StorageBox.values()
override fun newInstance(arg: Any?): Plugin<Any> {
allBoxes.forEach { box ->
box.objectIds.forEach { id ->
SceneryDefinition.forId(id)?.let { def ->
if (box.storableFamily != null) def.handlers["option:search"] = this
if (box.openable) def.handlers["option:open"] = this
if (box.closable) def.handlers["option:close"] = this
}
}
}
return this
}
override fun handle(player: Player, node: Node, option: String): Boolean {
val obj = node as Scenery
val box = allBoxes.firstOrNull { obj.id in it.objectIds } ?: return true
when (option) {
"search" -> {
when (obj.id) {
// special handling for med and high level treasure chests since they have a dialogue option first
Obj.TREASURE_CHEST_18807 -> {
sendDialogueOptions(player, "What sort of Treasure Trail items do you want to see?", "Low-level rewards", "Medium-level rewards")
addDialogueAction(player) { p, button ->
val family = when (button) {
2 -> StorableFamily.TREASURE_CHEST_LOW
3 -> StorableFamily.TREASURE_CHEST_MED
else -> null
}
family?.let { StorageInterface.openStorage(p, it, box) }
return@addDialogueAction
}
}
Obj.TREASURE_CHEST_18809 -> {
sendDialogueOptions(player, "What sort of Treasure Trail items do you want to see?", "Low-level rewards", "Medium-level rewards", "High-level rewards")
addDialogueAction(player) { p, button ->
val family = when (button) {
2 -> StorableFamily.TREASURE_CHEST_LOW
3 -> StorableFamily.TREASURE_CHEST_MED
4 -> StorableFamily.TREASURE_CHEST_HIGH
else -> null
}
family?.let { StorageInterface.openStorage(p, it, box) }
return@addDialogueAction
}
}
// bookcases need to re-store the games book in them on open. I think the bookshelf is the only source.
Obj.BOOKCASE_13597 -> {
storeBookInHouse(player, Items.GAME_BOOK_7681)
box.storableFamily?.let { StorageInterface.openStorage(player, it, box) }
}
Obj.BOOKCASE_13598 -> {
storeBookInHouse(player, Items.GAME_BOOK_7681)
box.storableFamily?.let { StorageInterface.openStorage(player, it, box) }
}
Obj.BOOKCASE_13599 -> {
storeBookInHouse(player, Items.GAME_BOOK_7681)
box.storableFamily?.let { StorageInterface.openStorage(player, it, box) }
}
else -> {
// default open for rest
box.storableFamily?.let { StorageInterface.openStorage(player, it, box) }
}
}
}
"open" -> if (box.openable) openBox(player, obj)
"close" -> if (box.closable) closeBox(player, obj)
}
return true
}
private fun openBox(player: Player, obj: Scenery) {
playAudio(player, Sounds.CHEST_OPEN_52)
animate(player, Animations.HUMAN_OPEN_CHEST_536)
replaceScenery(obj, obj.id + 1, -1)
}
private fun closeBox(player: Player, obj: Scenery) {
playAudio(player, Sounds.CHEST_CLOSE_51)
animate(player, 538)
replaceScenery(obj, obj.id - 1, -1)
}
}

View file

@ -0,0 +1,37 @@
package content.global.skill.construction.decoration.pohstorage
import org.json.simple.JSONObject
import core.game.node.entity.player.Player
class StorageState(val player: Player) {
private val containers = StorableFamily.values().associateWith { StorageContainer() }.toMutableMap()
fun getContainer(type: StorableFamily): StorageContainer = containers[type]!!
fun toJson(): JSONObject {
val root = JSONObject()
val containerData = JSONObject()
containers.forEach { (group, container) ->
containerData[group.name.lowercase()] = container.toJson()
}
root["containers"] = containerData
return root
}
fun readJson(data: JSONObject) {
val containerData = data["containers"] as? JSONObject ?: return
for (group in StorableFamily.values()) {
val json = containerData[group.name.lowercase()] as? JSONObject ?: continue
containers[group]?.readJson(json)
}
}
// clear storage
fun clear() {
containers.values.forEach { it.clear() }
}
}

View file

@ -10,6 +10,7 @@ import core.plugin.Initializable
import core.tools.END_DIALOGUE
import core.tools.START_DIALOGUE
import org.rs09.consts.Components
import org.rs09.consts.Items
import org.rs09.consts.NPCs
/**
@ -130,10 +131,9 @@ class ToolLeprechaunOnVacationDialogue(player: Player? = null) : DialoguePlugin(
}
/**
* Note:
* Goth chatheads are unfortunately updated and have frozen FacialExpressions. Disabled talk-to for now.
* This Leprechaun Goth should appear once you have Spirit of Summer done, but is currently defaulted as not.
*/
// @Initializable
@Initializable
class ToolLeprechaunGothDialogue(player: Player? = null) : DialoguePlugin(player) {
override fun handle(interfaceId: Int, buttonId: Int): Boolean {
@ -178,7 +178,7 @@ class ToolLeprechaunGothDialogue(player: Player? = null) : DialoguePlugin(player
}
override fun newInstance(player: Player?): DialoguePlugin {
return ToolLeprechaunDialogue(player)
return ToolLeprechaunGothDialogue(player)
}
override fun getIds(): IntArray {

View file

@ -178,7 +178,7 @@ public class SmeltingPulse extends SkillPulse<Item> {
}
/**
* Checks if the forging is a succes.
* Checks if the forging is a success.
*
* @param player the player.
* @return {@code True} if success.
@ -190,7 +190,9 @@ public class SmeltingPulse extends SkillPulse<Item> {
if (charges <= 0) {
if (removeItem(player, Items.RING_OF_FORGING_2568, Container.EQUIPMENT)) {
charges = 140;
sendMessage(player, "Your ring of forging uses up its last charge and disintegrates.");
sendMessage(player, "Your Ring of forging uses up its last charge and disintegrates.");
// stop the smelting queue
stop();
} else {
log(this.getClass(), Log.ERR, "Failed to delete empty ring of forging for player " + player.getName());
return false; //unfair but prevents exploit if the impossible happens

View file

@ -5,6 +5,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -129,8 +130,9 @@ class WitchesDiaryBook : InteractionListener {
}
override fun defineListeners() {
on(Items.DIARY_2408, IntType.ITEM, "read") { player, _ ->
on(Items.DIARY_2408, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -4,6 +4,7 @@ import content.global.handlers.iface.BookInterface
import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -177,8 +178,9 @@ class TranslationBook : InteractionListener {
}
override fun defineListeners() {
on(Items.TRANSLATION_4655, IntType.ITEM, "read") { player, _ ->
on(Items.TRANSLATION_4655, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -5,6 +5,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.InteractionListener
import core.game.interaction.IntType
import core.game.node.entity.player.Player
@ -288,8 +289,9 @@ class VarmensNotes : InteractionListener {
}
override fun defineListeners() {
on(Items.VARMENS_NOTES_4616, IntType.ITEM, "read") { player, _ ->
on(Items.VARMENS_NOTES_4616, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -0,0 +1,146 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import content.data.Quests
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.mahjarratArc
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCultListeners.Companion.HAZEEL_SCROLL
import core.api.*
import core.game.dialogue.*
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
import core.game.world.map.Location
import core.plugin.Initializable
import core.tools.END_DIALOGUE
import org.rs09.consts.*
@Initializable
class AlomoneDialogue(player: Player? = null) : DialoguePlugin(player) {
val hazeel = HazeelNPC(NPCs.HAZEEL_892, Location.create(2607, 9669, 0))
override fun handle(componentID: Int, buttonID: Int, ): Boolean {
val questStage = getQuestStage(player, Quests.HAZEEL_CULT)
when {
// you can't access Alomone before stage 2
// stage 2 - talk to clivet (set attr for carnillean)
// stage 2 - talk to clivet (set attr for mahjarrat)
// stage 3 - poison poured in food (mahjarrat-only stage)
(questStage in 2..3) -> when (stage) {
0 -> {
if (mahjarratArc(player)) {
playerl(FacialExpression.FRIENDLY, "Hi there.").also { stage = 20 }
} else {
npcl(FacialExpression.ANGRY, "How did YOU get in here?").also { stage ++}
}
}
// carnillean arc
1 -> playerl(FacialExpression.NEUTRAL, "I've come for the Carnillean family armour. Hand it over, or face the consequences.").also { stage++ }
2 -> npcl(FacialExpression.ANNOYED, "I thought I made it clear to the butler you could not be allowed to interfere with our mission. The incompetent fool must be going soft.").also { stage++ }
3 -> playerl(FacialExpression.NEUTRAL, "So, the butler's part of your sordid little cult, huh? Why is it ALWAYS the butler? I should have known...").also { stage++ }
4 -> npcl(FacialExpression.ANNOYED, "Well, you won't live long enough to tell anyone! DIE!!!").also { stage++ }
5 -> {
end()
setQuestStage(player, Quests.HAZEEL_CULT, 4)
npc!!.attack(player!!)
}
// mahjarrat arc
20 -> npcl(FacialExpression.NEUTRAL, "Well well well... So we have a new recruit. Clivet told me of your desire to join us in our glorious task to resurrect the mighty Hazeel from his slumber.").also { stage++ }
21 -> npcl(FacialExpression.NEUTRAL, "To accomplish this, we require the ancient words of summoning which will restore his shattered body, so that he can bring vengeance upon his enemies.").also { stage++ }
22 -> npcl(FacialExpression.NEUTRAL, "Hazeel in his mighty cunning anticipated that he might be defeated and secured within his home somewhere a powerful magical scroll that could restore him.").also { stage++ }
23 -> npcl(FacialExpression.NEUTRAL, "The words to this powerful enchantment are hidden within the Carnillean mansion, right under the nose of his unsuspecting foes. We already have an agent in place there").also { stage++}
24 -> npcl(FacialExpression.NEUTRAL, "in their Butler Jones, a faithful follower of Hazeel. His loyalty to Hazeel is beyond questioning, yet he has been unsuccessful in locating the scroll containing the spell so far.").also { stage++ }
25 -> npcl(FacialExpression.NEUTRAL, "Go back to the mansion, and assist us in finding this enchantment, so that together we can restore Hazeel to his true power and glory!").also { stage = END_DIALOGUE}.also { setQuestStage(player, Quests.HAZEEL_CULT, 4) }
}
// stage 4 - alomone either fought or he tells you he needs scroll
// stage 5 - either returning the armour, or finding the scroll
(questStage in 4..5 && mahjarratArc(player)) -> when (stage) {
0 -> if(npc.id == NPCs.ALOMONE_891 && !getAttribute(player, "hazeelSpawned", false)) {
playerl(FacialExpression.NEUTRAL, "Hi.").also { stage++ }
} else {
playerl(FacialExpression.NEUTRAL, "Hi.").also { stage = 12 }
}
1 -> npcl(FacialExpression.NEUTRAL, "Have you brought me the scroll of restoration to complete the ritual?").also { stage++ }
2 -> if(inInventory(player, HAZEEL_SCROLL)){
playerl(FacialExpression.NEUTRAL, "Yep. Got it right here.").also { stage++ }
} else {
playerl(FacialExpression.NEUTRAL, "No, not yet.").also { stage = END_DIALOGUE }
}
3 -> npcl(FacialExpression.NEUTRAL, "FINALLY! Quickly, give it to me and I will begin the ritual!").also { stage++ }
4 -> sendDialogue(player, "You hand Alomone the scroll of Hazeel.").also { stage++ }
5 -> npcl(FacialExpression.NEUTRAL, "Yes... YES! With the words contained within this scroll our Lord shall return to this realm and bring destruction to our enemies!").also { stage++ }
6 -> npcl(FacialExpression.NEUTRAL, "Watch adventurer, and witness the glorious rebirth of Hazeel!").also { stage++ }
7 -> npcl(FacialExpression.NEUTRAL, "Lord Hazeel... Lord Zamorak... I call upon the powers of evil... The forces of chaos... The strength of hatred...").also { stage++ }
8 -> npcl(FacialExpression.NEUTRAL, "Sentente sillaberi junque dithmento! Ia! Ia! dextrimon encanto! termando... imcando... solly enty rando... sentente! Ia! Ia! Indenti zaggarati g'thxa!").also { stage++ }
9 -> sendDialogue(player, "Alomone continues to recite the scroll. It is in a language you have never heard and do not understand at all.").also { stage++ }
10 -> npcl(FacialExpression.NEUTRAL, "Dintenta! Sententa! Retenta! Q'exjta! Ia! Sottottott! Ia! Dysmenta junque fammatio svelken! Sottey! Sentey! SOLOMENT!").also { stage++ }
11 -> {
sendDialogue(player, "As Alomone finishes reading the scroll you hear a shrill scream emanate from Hazeel's coffin.").also { stage++ }
hazeel.init()
setAttribute(player,"hazeelSpawned", true)
player.face(hazeel)
hazeel.isWalks = false
}
12 -> sendNPCDialogue(player, NPCs.HAZEEL_892, "My loyal followers. I have pride in you all. Never did I expect to return to this land and I see I have much to attend to.").also { stage++ }
13 -> sendNPCDialogue(player, NPCs.HAZEEL_892, "Soon this world will cower once more at the name Hazeel, and my fury will blaze across mine enemies just as my loyal followers gain their rewards.").also { stage++ }
14 -> sendDialogue(player, "Hazeel turns to look at you directly.").also{ hazeel.face(player) }.also { stage++ }
15 -> sendNPCDialogue(player, NPCs.HAZEEL_892, "Adventurer. I know that thy efforts were principal in my return to this plain of existence. I offer you my thanks directly.").also { stage++ }
16 -> sendNPCDialogue(player, NPCs.HAZEEL_892, "You may not be a follower of Zamorak directly but your cunning and treachery mark you as a friend to the Mahjarrat. Rest assured; I will call on your assistance again.").also { stage++ }
17 -> sendNPCDialogue(player, NPCs.HAZEEL_892, "Weak as I am now, my strength shall return, and my followers will be greatly rewarded when my powers peak once more. Join me in my cause as my servant, and all").also { stage++ }
18 -> sendNPCDialogue(player, NPCs.HAZEEL_892, "the riches of this plane shall be yours to share, in domination over all who oppose my kind. You would be wise to pledge yourself now while you still may.").also { stage++ }
19 -> playerl(FacialExpression.ANGRY, "I serve nobody but myself.").also { stage++ }
20 -> sendNPCDialogue(player, NPCs.HAZEEL_892, "Your insolence shall be overlooked this time for I am in truth in your debt. Come close: I shall reward you with wealth such as you deserve.").also { stage++ }
21 -> sendNPCDialogue(player, NPCs.HAZEEL_892, "Although your true reward will be later, when Zamorak and I, together, lay waste to this miserable planet, and you are spared as an ally to us.").also { stage++ }
22 -> sendDialogue(player, "Hazeel gives you some coins. They seem to be extremely ancient.").also { stage++ }
23 -> sendNPCDialogue(player, NPCs.HAZEEL_892, "And now I must leave you my loyal subjects for time is short, and I have many pressing matters to address before I make my return. I must leave now, and head Northwards").also { stage++ }
24 -> sendNPCDialogue(player, NPCs.HAZEEL_892, "to join my fellow mahjarrat and regain my strength and powers. I shall return when I am ready to fight my enemies and much blood shall flow across these lands!").also { stage++ }
25 -> sendDialogue(player, "The cultists let out a mighty cheer.").also { stage++ }
26 -> {
end()
removeItem(player, HAZEEL_SCROLL)
val hazeel = player.findNpcInViewport(NPCs.HAZEEL_892)
hazeel?.clear()
removeAttribute(player,"hazeelSpawned")
finishQuest(player, Quests.HAZEEL_CULT)
}
}
// stage 100 - quest complete - either presenting proof that jones is a bad guy (duh) or resurrecting hazeel
// also valid for stages 4 and 5 in the carnillean arc.
(questStage in 4..100) -> when (stage) {
0 -> {
if (mahjarratArc(player)) {
playerl(FacialExpression.FRIENDLY, "Hi there.").also { stage++ }
} else {
npcl(FacialExpression.ANNOYED, "You have crossed my path too many times intruder. Leave or face my wrath.").also { stage = 2 }
}
}
1 -> npcl(FacialExpression.NEUTRAL, "Welcome, adventurer. Know that as a friend to Hazeel, you are always welcome here.").also { stage = END_DIALOGUE }
2 -> playerl(FacialExpression.FRIENDLY, "Yeah, whatever.").also { stage = END_DIALOGUE }
}
}
return true
}
// function used to find any hazeels and remove them at the end of the quest.
// this has the potential to remove other players' hazeels, but the player could still complete the quest by either continuing their
// active dialogue or speaking to alomone, and just dealing with the fact that their hazeel is not on the map. relogging and restarting
// the dialogue would get these players a new hazeel.
private fun Player.findNpcInViewport(npcId: Int): NPC? {
val viewport = viewport ?: return null
for (plane in viewport.viewingPlanes) {
for (npc in plane.npcs) {
if (npc.id == npcId) {
return npc
}
}
}
return null
}
// Hazeel is in this array because in the event the player quits the dialogue halfway through in the final stage, it can be restarted by speaking to either alomone or hazeel.
override fun getIds(): IntArray = intArrayOf(NPCs.ALOMONE_891, NPCs.HAZEEL_892)
}

View file

@ -0,0 +1,39 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import content.data.Quests
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.carnilleanArc
import core.api.isQuestInProgress
import core.api.produceGroundItem
import core.game.node.entity.Entity
import core.game.node.entity.npc.AbstractNPC
import core.game.node.entity.player.Player
import core.game.world.map.Location
import core.plugin.Initializable
import org.rs09.consts.*
/**
* Alomone drops the Carnillean Amour during the Hazeel Cult quest.
*/
@Initializable
class AlomoneNPC(
id: Int = 0,
location: Location? = null,
) : AbstractNPC(id, location) {
override fun construct(
id: Int,
location: Location,
vararg objects: Any,
): AbstractNPC = AlomoneNPC(id, location)
override fun getIds(): IntArray = intArrayOf(NPCs.ALOMONE_891)
override fun finalizeDeath(killer: Entity?) {
if (killer is Player) {
if (isQuestInProgress(killer, Quests.HAZEEL_CULT, 3, 4) && carnilleanArc(killer)) {
produceGroundItem(killer, Items.CARNILLEAN_ARMOUR_2405, 1, this.location)
}
super.finalizeDeath(killer)
}
}
}

View file

@ -0,0 +1,80 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import content.data.Quests
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.mahjarratArc
import core.api.getQuestStage
import core.api.inEquipmentOrInventory
import core.game.dialogue.DialoguePlugin
import core.game.dialogue.FacialExpression
import core.game.node.entity.player.Player
import core.plugin.Initializable
import core.tools.END_DIALOGUE
import org.rs09.consts.*
@Initializable
class ButlerJonesDialogue(player: Player? = null) : DialoguePlugin(player) {
override fun handle(componentID: Int, buttonID: Int): Boolean {
val questStage = getQuestStage(player!!, Quests.HAZEEL_CULT)
when {
// stage 3 - poison poured in food (mahjarrat-only stage)
(questStage == 3) -> when (stage) {
0 -> playerl(FacialExpression.FRIENDLY, "Hello there.").also { stage++ }
1 -> npcl(FacialExpression.NEUTRAL, "Hello, adventurer. Such a terrible shame about Scruffy. I wonder if the family will ever fully recover.").also { stage++ }
2 -> npcl(FacialExpression.FRIENDLY, "Anyway, I hear your quest is going well.").also { stage++ }
3 -> playerl(FacialExpression.FRIENDLY, "Really?").also { stage++ }
4 -> npcl(FacialExpression.ANNOYED, "Oh yes. Do keep up the good work.").also { stage = END_DIALOGUE }
}
// stage 4 - alomone either fought or he tells you he needs scroll
(questStage == 4 && mahjarratArc(player) && inEquipmentOrInventory(player, Items.HAZEELS_MARK_2406)) -> when (stage) {
0 -> npcl(FacialExpression.NEUTRAL, "Hello again friend. I see you have the mark now; you should keep it covered up.").also { stage++ }
1 -> playerl(FacialExpression.FRIENDLY, "That? Oh, it's nothing. Just an old amulet.").also { stage++ }
2 -> npcl(FacialExpression.NEUTRAL, "You don't have to pretend with me, friend! Our cause is one and the same; the sooner Lord Hazeel is avenged the better for this city... and for us!").also { stage++ }
3 -> playerl(FacialExpression.FRIENDLY, "So do you have any idea where the scroll with the enchantment is?").also { stage++ }
4 -> npcl(FacialExpression.ANNOYED, "No idea I'm afraid. I KNOW it's somewhere in this house, but for the life of me I can't find it anywhere. I've searched high and low for it!").also { stage++ }
5 -> playerl(FacialExpression.FRIENDLY, "And Sir Ceril doesn't suspect a thing?").also { stage++ }
6 -> npcl(FacialExpression.ANNOYED, "Ha! That silly old fool? He can't see the forest for the trees.").also { stage++ }
7 -> playerl(FacialExpression.FRIENDLY, "I'll keep on looking then...").also { stage = END_DIALOGUE }
}
// stage 0 - unstarted
// stage 1 - after talking to ceril
// stage 2 - talk to clivet (set attr for carnillean)
// stage 2 - talk to clivet (set attr for mahjarrat)
// stage 5 - either returning the armour, or finding the scroll
// also stage 4 if you are carnillean side
(questStage in 0..5) -> when (stage) {
0 -> playerl(FacialExpression.FRIENDLY, "Hello there.").also { stage++ }
1 -> npcl(FacialExpression.FRIENDLY, "Hello. How are you today?").also { stage++ }
2 -> playerl(FacialExpression.FRIENDLY, "Good thank you, and yourself?").also { stage++ }
3 -> npcl(FacialExpression.FRIENDLY, "Very well, thank you.").also { stage = END_DIALOGUE }
}
// stage 100 - quest complete - either presenting proof that jones is a bad guy (duh) or resurrecting hazeel
(questStage == 100) -> when (stage) {
0 -> {
if (mahjarratArc(player)) {
playerl(FacialExpression.FRIENDLY, "Hello stranger.").also { stage = 1 }
} else {
playerl(FacialExpression.FRIENDLY, "Hello there.").also { stage = 6 }
}
}
1 -> npcl(FacialExpression.NEUTRAL, "It's an honour to be in your presence again, adventurer. I hope things are well?").also { stage++ }
2 -> playerl(FacialExpression.FRIENDLY, "Not bad, thanks. Yourself?").also { stage++ }
3 -> npcl(FacialExpression.ANNOYED, "Unfortunately, I am still forced to deal with this insufferable family. Many generations have passed, but they are still the enemy. As such, they must be kept a close eye on.").also { stage++ }
4 -> npcl(FacialExpression.ANNOYED, "Still, I have no doubt that the time will soon come for me to leave this place. Our lord will certainly have need of me elsewhere once his current work is complete.").also { stage++ }
5 -> playerl(FacialExpression.FRIENDLY, "I see. Well good luck with it all.").also { stage = END_DIALOGUE }
6 -> npcl(FacialExpression.ANNOYED, "Why hello there.").also { stage++ }
7 -> playerl(FacialExpression.FRIENDLY, "I take it you're the new butler...?").also { stage++ }
8 -> npcl(FacialExpression.ANNOYED, "That's right. I hear that they had some problems with the last one.").also { stage++ }
9 -> playerl(FacialExpression.FRIENDLY, "Yes, you could say that...").also { stage = END_DIALOGUE }
}
}
return true
}
override fun getIds(): IntArray = intArrayOf(NPCs.BUTLER_JONES_890)
}

View file

@ -0,0 +1,147 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import content.data.Quests
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.carnilleanArc
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.fakeFinish
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.mahjarratArc
import core.api.*
import core.game.dialogue.DialoguePlugin
import core.game.dialogue.FacialExpression
import core.game.node.entity.player.Player
import core.plugin.Initializable
import core.tools.END_DIALOGUE
import org.rs09.consts.*
@Initializable
class CerilCarnilleanDialogue(player: Player? = null) : DialoguePlugin(player) {
override fun handle(componentID: Int, buttonID: Int): Boolean {
val questStage = getQuestStage(player!!, Quests.HAZEEL_CULT)
when {
// stage 0 - unstarted
(questStage == 0) -> {
when (stage) {
0 -> npcl(FacialExpression.ANGRY, "Blooming, thieving, weirdo cultists! Why don't they leave me alone? String 'em all up, that's what I say!").also { stage++ }
1 -> options("What's wrong?", "You probably deserve it.", "You seem uptight, I'll leave you alone.").also { stage++ }
2 -> when (buttonID) {
1 -> playerl(FacialExpression.HALF_ASKING, "What's wrong?").also { stage++ }
2 -> playerl(FacialExpression.FRIENDLY, "You probably deserve it.").also { stage = 20 }
3 -> playerl(FacialExpression.FRIENDLY, "You seem uptight, I'll leave you alone.").also { stage = 21 }
}
3 -> npcl(FacialExpression.ANGRY, "It's those blooming cultists from the forest! Those freaks keep breaking into my house!").also { stage++ }
4 -> playerl(FacialExpression.FRIENDLY, "Have they taken much?").also { stage++ }
5 -> npcl(FacialExpression.ANGRY, "They first broke in months ago and stole a suit of armour. The strange thing is that they've broken in four times since but taken nothing.").also { stage++ }
6 -> playerl(FacialExpression.FRIENDLY, "And you are...?").also { stage++ }
7 -> npcl(FacialExpression.ANGRY, "Why, I am Sir Ceril Carnillean! We really are quite a famous bloodline... who've played a vital role in the politics of Ardougne for many generations.").also { stage++ }
8 -> npcl(FacialExpression.FRIENDLY, "Perhaps you would be able to assist me in returning the stolen armour? For a modest cash reward of course!").also { stage++ }
9 -> options("Yes, of course, I'd be happy to help.", "No thanks. I've got other plans.").also { stage++ }
10 -> when (buttonID) {
1 -> playerl(FacialExpression.FRIENDLY, "Yes, of course, I'd be happy to help.").also { stage++ }
2 -> playerl(FacialExpression.FRIENDLY, "No thanks. I've got other plans.").also { stage = 19 }
}
11 -> npcl(FacialExpression.FRIENDLY, "That's very noble of you sirrah! I caught a glimpse of the thieves leaving but due to... uh... yeah, my cold I was unable to give chase.").also { stage++ }
12 -> npcl(FacialExpression.FRIENDLY, "They're some kind of crazy cult who dress all in black, and hang out near the cave entrance in the forest south of here.").also { stage++ }
13 -> playerl(FacialExpression.FRIENDLY, "How do you know that?").also { stage++ }
14 -> npcl(FacialExpression.FRIENDLY, "My old butler, Higson, once followed them to their hideout there. Unfortunately the next night he died in his sleep.").also { stage++ }
15 -> playerl(FacialExpression.FRIENDLY, "That's awful!").also { stage++ }
16 -> npcl(FacialExpression.FRIENDLY, "No, it's okay. A replacement arrived the next day. He's been great - cooks an excellent broth!").also { stage++ }
17 -> playerl(FacialExpression.FRIENDLY, "Ok. I'll see what I can do.").also { stage++ }
18 -> {
end()
setQuestStage(player, Quests.HAZEEL_CULT, 1)
}
19 -> npcl(FacialExpression.FRIENDLY, "Well no wonder I'm the one with the big house and you're the one on the streets.").also { stage = END_DIALOGUE }
20 -> npcl(FacialExpression.FRIENDLY, "And who are you to judge me? You look like a peasant anyway. I'm wasting my time talking to you.").also { stage = END_DIALOGUE }
21 -> npcl(FacialExpression.FRIENDLY, "Yes, I doubt you could help anyway.").also { stage = END_DIALOGUE }
}
}
// stage 3 - poison poured in food (mahjarrat-only stage)
(questStage == 3) -> when (stage) {
0 -> playerl(FacialExpression.FRIENDLY, "Hello again.").also { stage++ }
1 -> npcl(FacialExpression.ANNOYED, "Oh the inhumanity... the cruelty... the misery... the pain...").also { stage++ }
2 -> npcl(FacialExpression.ANNOYED, "My son is a good boy, really, but how could he give his dinner to Scruffy without having the servants test it for poison first? How?").also { stage++ }
3 -> npcl(FacialExpression.ANNOYED, "How could he be so thoughtless and careless? He knows we are all under threat!").also { stage++ }
4 -> playerl(FacialExpression.FRIENDLY, "Scruffy?").also { stage++ }
5 -> npcl(FacialExpression.ANNOYED, "He's been with our family for twenty years... that's 140 in dog years! The poor dog... What did he ever do to deserve such a fate?").also { stage++ }
6 -> playerl(FacialExpression.FRIENDLY, "Your dog got poisoned? That's not right.").also { stage++ }
7 -> npcl(FacialExpression.NEUTRAL, "I agree! I hope whichever evildoer is responsible gets the full weight of the law brought upon them!").also { stage++ }
8 -> playerl(FacialExpression.NEUTRAL, "Uh... yeah... me too.").also { stage = END_DIALOGUE }
}
// stage 4 - alomone either fought or he tells you he needs scroll
(questStage == 4 && carnilleanArc(player) && inInventory(player, Items.CARNILLEAN_ARMOUR_2405)) -> when (stage) {
0 -> playerl(FacialExpression.FRIENDLY, "Look! I've recovered your armour!").also { stage++ }
1 -> npcl(FacialExpression.FRIENDLY, "Well done! I must say I am very impressed! Come on, hand it over.").also { stage++ }
2 -> npcl(FacialExpression.FRIENDLY, "Before we send you on your way with your payment, I'll just get Jones to whip you up a batch of his special broth.").also { stage++ }
3 -> playerl(FacialExpression.FRIENDLY, "I'd rather not if it's all the same to you. I overheard the cultists talking, and apparently Jones is in league with them.").also { stage++ }
4 -> {
// check if we are upstairs
if(player.location.z == 1) {
npcl(FacialExpression.ANGRY, "W-what? RIGHT! You! We're going to blooming well sort this out right now once and for all!").also { stage++ }
} else {
npcl(FacialExpression.ANGRY, "W-what? RIGHT! You! Come with me upstairs! We're going to blooming-well sort this out, once and for all!").also { stage = END_DIALOGUE }
}
}
5 -> npcl(FacialExpression.HALF_ASKING, "Jones! This commoner says you had something to do with the theft of my armour. What do you have to say for yourself about that?").also { stage++ }
6 -> sendNPCDialogue(player, NPCs.BUTLER_JONES_890, "It wasn't me m'lud. I am, as you know, a loyal servant.").also { stage++ }
7 -> npcl(FacialExpression.FRIENDLY, "Humph. Quite right too. I cannot fathom why this scoundrel would accuse you of such a crime without evidence to back up @g[his,her] accusations.").also { stage++ }
8 -> npcl(FacialExpression.FRIENDLY, "Right, I have decided. I have given my word as a nobleman to reward you for your efforts in retrieving my armour.").also { stage++ }
// it is authentic that the previous line ends in a period and the next line starts with a non-capital 'but'
9 -> npcl(FacialExpression.FRIENDLY, "but I must also compensate Jones for this terrible slander you have made against him.").also { stage++ }
10 -> {
sendDialogue(player, "Sir Ceril gives you 5 gold. Sir Ceril gives Jones 1995 gold.")
addItemOrDrop(player, Items.COINS_995, 5)
stage++
}
11 -> npcl(FacialExpression.ANGRY, "Now take it and leave you scoundrel! And don't darken my doorstep again!").also { stage++ }
12 -> sendNPCDialogue(player, NPCs.BUTLER_JONES_890, "Don't worry m'lud, this fool won't be bothering us any more.").also { stage++ }
13 -> sendDialogue(player, "Jones smirks at you. You are going to need more than words to prove Jones' treachery.").also { stage++ }
14 -> {
end()
removeItem(player, Items.CARNILLEAN_ARMOUR_2405)
setQuestStage(player, Quests.HAZEEL_CULT, 5)
fakeFinish(player)
}
}
// stage 5 - either returning the armour, or finding the scroll
(questStage == 5 && carnilleanArc(player)) -> when (stage) {
// The ceril-jones-player dialogue is continued in the listener for the wardrobe where the player finds the evidence that Jones is bad.
0 -> npcl(FacialExpression.ANGRY, "Leave, you scoundrel! And don't darken my doorstep again!").also { stage = END_DIALOGUE }
}
// stage 1 - after talking to ceril
// stage 2 - talk to clivet (set attr for carnillean)
// stage 2 - talk to clivet (set attr for mahjarrat)
// also covers stage 4 and 5 if you're playing the mahjarrat side
(questStage in 1..99) -> when (stage) {
0 -> playerl(FacialExpression.FRIENDLY, "Hello, Ceril.").also { stage++ }
1 -> npcl(FacialExpression.ANNOYED, "That's Sir Ceril to you, you impudent scamp. Show a bit of respect to your betters. Now, shouldn't you be recovering my armour?").also { stage++ }
2 -> playerl(FacialExpression.FRIENDLY, "Yeah yeah, I'm on it.").also { stage++ }
3 -> npcl(FacialExpression.ANNOYED, "Good. I suggest you start with that cave south of the city, near the Clock Tower. That is where the cult is based.").also { stage = END_DIALOGUE }
}
// stage 100 - quest complete - either presenting proof that jones is a bad guy (duh) or resurrecting hazeel
(questStage == 100) -> when (stage) {
0 -> playerl(FacialExpression.FRIENDLY, "Hello, Ceril.").also { stage++ }
1 -> {
if (mahjarratArc(player)) {
npcl(FacialExpression.FRIENDLY, "Oh... I may be wrong... but ever since I asked for your help, things around here have gone from bad to worse...").also { stage = 3 }
} else {
npcl(FacialExpression.FRIENDLY, "Well hello again adventurer! It's good to see you again! If it wasn't for your quick thinking the treacherous Jones would have poisoned my family and me by now! We are in your debt.").also { stage = 2 }
}
}
2 -> playerl(FacialExpression.FRIENDLY, "Don't worry about it, a good deed is its own reward. And that 2000 gold didn't hurt either.").also { stage = END_DIALOGUE }
3 -> npcl(FacialExpression.NEUTRAL, "I think you'd better keep out of my way. And for the last time, it's Sir Ceril! Sir! It's not that hard to blooming remember!").also { stage = END_DIALOGUE }
}
}
return true
}
override fun getIds(): IntArray = intArrayOf(NPCs.CERIL_CARNILLEAN_885)
}

View file

@ -0,0 +1,56 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import content.data.Quests
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.mahjarratArc
import core.api.*
import core.game.dialogue.DialoguePlugin
import core.game.dialogue.FacialExpression
import core.game.node.entity.player.Player
import core.plugin.Initializable
import core.tools.END_DIALOGUE
import org.rs09.consts.*
@Initializable
class ClausTheChefDialogue(player: Player? = null) : DialoguePlugin(player) {
override fun handle(componentID: Int, buttonID: Int): Boolean {
val questStage = getQuestStage(player!!, Quests.HAZEEL_CULT)
when {
// stage 0 - unstarted
// stage 1 - after talking to ceril
// stage 2 - talk to clivet (set attr for carnillean)
// stage 2 - talk to clivet (set attr for mahjarrat)
// stage 3 - poison poured in food (mahjarrat-only stage)
// stage 4 - alomone either fought or he tells you he needs scroll
// stage 5 - either returning the armour, or finding the scroll
(questStage in 0..99) -> {
when (stage) {
0 -> playerl(FacialExpression.FRIENDLY, "Hello there.").also { stage++ }
1 -> npcl(FacialExpression.FRIENDLY, "Sorry, can't stop to chat! You would be amazed at how many meals this family gets through daily!").also { stage = END_DIALOGUE }
}
}
// stage 100 - quest complete - either presenting proof that jones is a bad guy (duh) or resurrecting hazeel
(questStage == 100) -> {
when (stage) {
0 -> {
if (mahjarratArc(player)) {
playerl(FacialExpression.FRIENDLY, "Hey.").also { stage++ }
} else {
playerl(FacialExpression.FRIENDLY, "Hiya.").also { stage = 2 }
}
}
1 -> npcl(FacialExpression.NEUTRAL, "Oh, hello there. Sorry, but I can't really talk right now. Things haven't been great here recently, and I have a lot of work to do.").also { stage = END_DIALOGUE }
2 -> npcl(FacialExpression.HALF_ASKING, "Well hello there adventurer! Are we fit and well?").also { stage++ }
3 -> playerl(FacialExpression.FRIENDLY, "Yep, fine thanks.").also { stage++ }
4 -> npcl(FacialExpression.FRIENDLY, "Glad to hear it.").also { stage = END_DIALOGUE }
}
}
}
return true
}
override fun getIds(): IntArray = intArrayOf(NPCs.CLAUS_THE_CHEF_886)
}

View file

@ -0,0 +1,162 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import content.data.Quests
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.mahjarratArc
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCultListeners.Companion.attrCarnillean
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCultListeners.Companion.attrHazeel
import core.api.*
import core.game.dialogue.DialoguePlugin
import core.game.dialogue.FacialExpression
import core.game.node.entity.player.Player
import core.plugin.Initializable
import core.tools.END_DIALOGUE
import org.rs09.consts.*
@Initializable
class ClivetDialogue(player: Player? = null) : DialoguePlugin(player) {
override fun handle(componentID: Int, buttonID: Int, ): Boolean {
val questStage = getQuestStage(player!!, Quests.HAZEEL_CULT)
when {
// stage 0 - unstarted
(questStage == 0) -> when (stage) {
0 -> npcl(FacialExpression.ANNOYED, "What do you want traveller?").also { stage++ }
1 -> playerl(FacialExpression.ANNOYED, "Just passing by.").also { stage++ }
2 -> npcl(FacialExpression.ANNOYED, "You have no business here.").also { stage++ }
3 -> npcl(FacialExpression.ANGRY, "Leave...now").also { stage = END_DIALOGUE }
}
// stage 1 - after talking to ceril
(questStage == 1) -> when (stage) {
0 -> playerl(FacialExpression.ANNOYED, "Do you know the Carnilleans?").also { stage++ }
1 -> npcl(FacialExpression.ANNOYED, "You mind your business, I'll mind mine.").also { stage++ }
2 -> playerl(FacialExpression.ANNOYED, "Look I KNOW you're hiding something. I've heard there's a cult hideout down here.").also { stage++ }
3 -> npcl(FacialExpression.ANGRY, "If you want to stay healthy you'll leave now.").also { stage++ }
4 -> playerl(FacialExpression.ANNOYED, "I have my orders.").also { stage++ }
5 -> npcl(FacialExpression.HALF_ASKING, "So... that two faced cold hearted snob has made you fall for his propaganda eh?").also { stage++ }
6 -> playerl(FacialExpression.FRIENDLY, "Sir Ceril Carnillean is a man of honour!").also { stage++ }
7 -> npcl(FacialExpression.ANNOYED, "Is he now? Is he REALLY? There is a lot more to the Carnilleans than meets the eye you fool...").also { stage++ }
8 -> npcl(FacialExpression.ANNOYED, "And none of it is honourable.").also { stage++ }
9 -> options("What do you mean?", "I've heard enough of your rubbish.").also { stage++ }
10 -> when (buttonID) {
1 -> playerl(FacialExpression.HALF_ASKING, "What do you mean?").also { stage++ }
2 -> playerl(FacialExpression.ANNOYED, "I've heard enough of your rubbish.").also { stage = 22 }
}
11 -> npcl(FacialExpression.ANNOYED, "The Carnillean home does not belong to them. The builder of the house was Lord Hazeel, one of the Mahjarrat followers of Zamorak. Many years ago there was a civil war").also { stage++ }
12 -> npcl(FacialExpression.ANNOYED, "in this land, and the hateful Saradominists declared war upon all Zamorakians who lived here. Lord Hazeel nobly would not repent his beliefs, and the Carnilleans harassed").also { stage++ }
13 -> npcl(FacialExpression.ANNOYED, "Hazeel and his followers for many decades. One fateful night, under cover of darkness, they stormed his home in an angry mob torturing and butchering all loyal").also { stage++ }
14 -> npcl(FacialExpression.ANNOYED, "Zamorakians they encountered inside. The following morning, the Carnillean forefathers moved into the empty household and claimed it as their own.").also { stage++ }
15 -> npcl(FacialExpression.ANNOYED, "They have grown fat on the hard work of Lord Hazeel ever since. Unluckily for them Lord Hazeel as a Mahjarrat had access to powers and enchantments they knew nothing").also { stage++ }
16 -> npcl(FacialExpression.ANNOYED, "about, and made preparations for his return when first they began to storm his home. Soon the day will come when he will return to wreak his vengeance upon the thieves!").also { stage++ }
17 -> playerl(FacialExpression.ANNOYED, "The politics and histories of Ardougne do not concern me. I have been given a job and intend to see it through to the end.").also { stage++ }
18 -> npcl(FacialExpression.ANNOYED, "Well then friend, perhaps I can offer you a different job then? Sooner as later Our master WILL return to this land; those faithful to him will be well rewarded.").also { stage++ }
19 -> npcl(FacialExpression.ANNOYED, "Join our cult and assist us in his restoration, and you will be well rewarded; try and prevent his return and suffer the wrath of Zamorak and the Mahjarrat.").also { stage++ }
20 -> options("You're crazy, I'd never help you.", "So what would I have to do?").also { stage++ }
21 -> when (buttonID) {
1 -> playerl(FacialExpression.DISGUSTED_HEAD_SHAKE, "You're crazy, I'd never help you.").also { stage++ }
2 -> playerl(FacialExpression.HALF_ASKING, "So what would I have to do?").also { stage = 40 }
}
// SET CARNILLEAN ARC
22 -> npcl(FacialExpression.ANNOYED, "Then you are a fool. Go back to your small minded mundane little life; you will never know the glories you could have tasted as one of us!").also { stage++ }
23 -> {
end()
setAttribute(player, attrCarnillean, true)
setQuestStage(player, Quests.HAZEEL_CULT, 2)
runTask(player, 1) {
npc.isInvisible = true
sendDialogue(player, "The man jumps onto the raft and pushes off down into the sewer system.")
sendMessage(player, "Clivet: You'll never find us...")
stage = END_DIALOGUE
}
}
// SET MAHJARRAT ARC
40 -> npcl(FacialExpression.NEUTRAL, "You must prove your loyalty to our cause. Killing one of the Carnillean household should be sufficient proof of your dedication to resurrecting our master Lord Hazeel.").also { stage++ }
41 -> npcl(FacialExpression.NEUTRAL, "So what say you adventurer? Join our side against the Carnillean thieves?").also { stage++ }
42 -> options("No. I won't do it.", "Ok, count me in.").also { stage++ }
43 -> when (buttonID) {
1 -> playerl(FacialExpression.DISGUSTED_HEAD_SHAKE, "No. I won't do it.").also { stage = 22 }
2 -> playerl(FacialExpression.NEUTRAL, "Ok, count me in.").also { stage++ }
}
44 -> npcl(FacialExpression.NEUTRAL, "Excellent. It takes a rare character to see through the cursed Carnillean lies. I can see you are a person of exactly the right character to join the followers of Hazeel.").also { stage++ }
45 -> npcl(FacialExpression.NEUTRAL, "Here, take this poison. Pour it into their food, and once the deed is done, return here and speak to me once more.").also { stage++ }
46 -> {
end()
setAttribute(player, attrHazeel, true)
setQuestStage(player, Quests.HAZEEL_CULT, 2)
addItemOrDrop(player, Items.POISON_273)
}
}
// stage 2 - talk to clivet (set attr for carnillean)
// stage 2 - talk to clivet (set attr for mahjarrat)
(questStage == 2) -> when (stage) {
0 -> {
if (mahjarratArc(player) && !inInventory(player, Items.POISON_273)) {
playerl(FacialExpression.FRIENDLY, "I need some more poison.").also { stage++ }
} else if (mahjarratArc(player) && inInventory(player, Items.POISON_273)){
npcl(FacialExpression.FRIENDLY, "You have a mission for us, adventurer. Go to the Carnilliean household and poison Ceril Carnillean's meal to prove your loyalty.").also { stage = END_DIALOGUE }
} else {
playerl(FacialExpression.HALF_ASKING, "WHERE is the cult hideout?").also { stage = 3 }
}
}
1 -> npcl(FacialExpression.ANNOYED, "Fool! Be more careful with it this time.").also { stage++ }
2 -> {
end()
addItemOrDrop(player, Items.POISON_273)
}
3 -> npcl(FacialExpression.ANNOYED, "You're more of a fool than you look if you think you will ever find it. When Lord Hazeel is revived you will be the first to grovel for his mercy!").also { stage = END_DIALOGUE }
}
// stage 3 - poison poured in food (mahjarrat-only stage)
(questStage == 3 && mahjarratArc(player)) -> when (stage) {
0 -> {
if (!inEquipmentOrInventory(player, Items.HAZEELS_MARK_2406)) {
playerl(FacialExpression.FRIENDLY, "I poured the poison into the Carnillean's meal as requested. It didn't quite go to plan however.").also { stage++ }
} else {
playerl(FacialExpression.HALF_ASKING, "What was I supposed to do again?").also { stage = 6 }
}
}
1 -> npcl(FacialExpression.NEUTRAL, "Yes, we heard all about it from one of our sources. Hazeel has eyes everywhere! You have now proved your loyalty and will be rewarded when Hazeel is revived!").also { stage++ }
2 -> playerl(FacialExpression.HALF_ASKING, "Ok. So what's next?").also { stage++ }
3 -> npcl(FacialExpression.NEUTRAL, "Here. Wear this amulet; it is called The Sign of Hazeel. Not only will it identify you as one of our brotherhood to other followers but it is also the key to finding our hideout.").also { addItemOrDrop(player, Items.HAZEELS_MARK_2406) }.also { stage++ }
4 -> playerl(FacialExpression.NEUTRAL, "How does this amulet help do that then?").also { stage++ }
5 -> npcl(FacialExpression.NEUTRAL, "Hazeel in his wisdom when he built this lair did so by making it inaccessible unless the flow of the sewers is controlled exactly by the sewer valves above ground.").also { stage++ }
6 -> npcl(FacialExpression.NEUTRAL, "Each sewer valve must be correctly set before the water level is such that you may enter. The secret of the correct settings is contained in these amulets which honour him.").also { stage++ }
7 -> npcl(FacialExpression.NEUTRAL, "Starting from left to right, follow the design of the amulet to locate each sewer valve, and then turn each so as to follow the amulet design.").also { stage++ }
8 -> npcl(FacialExpression.NEUTRAL, "When you solve the sequence you may enter our hideout where our leader, Alomone, awaits you.").also { stage = END_DIALOGUE }
}
// stage 4 - alomone either fought or he tells you he needs scroll
// stage 5 - either returning the armour, or finding the scroll
// stage 100 - quest complete - either presenting proof that jones is a bad guy (duh) or resurrecting hazeel
(questStage in 4..100) -> when (stage) {
0 -> {
if (mahjarratArc(player)) {
playerl(FacialExpression.FRIENDLY, "Hello.").also { stage++ }
} else {
playerl(FacialExpression.FRIENDLY, "Hello.").also { stage = 4 }
}
}
1 -> npcl(FacialExpression.FRIENDLY, "It is good to see you once more, adventurer. Glory to Hazeel!").also { stage++ }
2 -> {
// if player doesn't have hazeel's mark they can get a new copy.
if (hasAnItem(player, Items.HAZEELS_MARK_2406).container == null) {
playerl(FacialExpression.FRIENDLY, "I've lost my Mark of Hazeel.").also { stage++ }
} else {
playerl(FacialExpression.FRIENDLY, "Glory to Hazeel!").also { stage = END_DIALOGUE }
}
}
3 -> npcl(FacialExpression.ANNOYED, "Here, take this.").also { addItemOrDrop(player, Items.HAZEELS_MARK_2406) }.also { stage = END_DIALOGUE }
4 -> npcl(FacialExpression.ANNOYED, "You may have won this battle meddler, but the war rages on. Go bother some goblins or something, for when Hazeel returns your destruction is assured.").also { stage = END_DIALOGUE }
}
}
return true
}
override fun getIds(): IntArray = intArrayOf(NPCs.CLIVET_893)
}

View file

@ -0,0 +1,34 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import core.api.*
import core.game.node.entity.npc.AbstractNPC
import core.game.world.map.Location
import core.plugin.Initializable
import org.rs09.consts.*
/**
* Clivet disappears briefly during the Hazeel Cult quest.
*/
@Initializable
class ClivetNPC(id: Int = 0, location: Location? = null, ) : AbstractNPC(id, location) {
override fun construct(id: Int, location: Location, vararg objects: Any, ): AbstractNPC = ClivetNPC(id, location)
override fun getIds(): IntArray = intArrayOf(NPCs.CLIVET_893)
private var invisibilityTimerRunning = false
override fun tick() {
if (isInvisible && !invisibilityTimerRunning) {
invisibilityTimerRunning = true
runTask(this, 20) {
isInvisible = false
invisibilityTimerRunning = false
}
}
super.tick()
}
}

View file

@ -0,0 +1,74 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import content.data.Quests
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.mahjarratArc
import core.api.getQuestStage
import core.game.dialogue.DialoguePlugin
import core.game.dialogue.FacialExpression
import core.game.node.entity.player.Player
import core.plugin.Initializable
import core.tools.END_DIALOGUE
import org.rs09.consts.*
@Initializable
class GuardHCDialogue(player: Player? = null) : DialoguePlugin(player) {
override fun handle(componentID: Int, buttonID: Int): Boolean {
val questStage = getQuestStage(player!!, Quests.HAZEEL_CULT)
when {
// stage 0 - unstarted
(questStage == 0) -> {
when (stage) {
0 -> playerl(FacialExpression.FRIENDLY, "Hello.").also { stage++ }
1 -> npcl(FacialExpression.FRIENDLY, "Hello. If you've come to admire the Carnillean family home, I must warn you not to cause any trouble.").also { stage++ }
2 -> npcl(FacialExpression.FRIENDLY, "Due to recent criminal activities against such a prominent member of Ardougnian politics, I have been sent here on special guard duty to ensure the Carnilleans' security.").also { stage++ }
3 -> npcl(FacialExpression.FRIENDLY, "Anyone caught interfering with the person or possessions of said Carnilleans will be taken to jail so quickly that their feet won't touch the floor!").also { stage++ }
4 -> playerl(FacialExpression.FRIENDLY, "Well, I wasn't actually planning on doing anything like that...").also { stage++ }
5 -> npcl(FacialExpression.FRIENDLY, "Glad to hear it. Let's keep it that way, hmmm?").also { stage = END_DIALOGUE }
}
}
// stage 3 - poison poured in food (mahjarrat-only stage)
(questStage == 3) -> {
when (stage) {
0 -> npcl(FacialExpression.SAD, "Today is a dark day. Those cultists have been back, and this time they've gone further than ever before! Murder! We can't afford to keep letting them get away with this.").also { stage = END_DIALOGUE }
}
}
// stage 1 - after talking to ceril
// stage 2 - talk to clivet (set attr for carnillean)
// stage 2 - talk to clivet (set attr for mahjarrat)
// stage 4 - alomone either fought or he tells you he needs scroll
// stage 5 - either returning the armour, or finding the scroll
(questStage in 1..99) -> {
when (stage) {
0 -> playerl(FacialExpression.FRIENDLY, "Hello there.").also { stage++ }
1 -> npcl(FacialExpression.FRIENDLY, "Hello there. I hear you're after that cult who broke in the other night. It always gladdens me when civilians assist the law like this.").also { stage++ }
2 -> playerl(FacialExpression.FRIENDLY, "I'm just happy to be of help.").also { stage = END_DIALOGUE }
}
}
// stage 100 - quest complete - either presenting proof that jones is a bad guy (duh) or resurrecting hazeel
(questStage == 100) -> when (stage) {
0 -> {
if (mahjarratArc(player)) {
playerl(FacialExpression.FRIENDLY, "Hello there.").also { stage = 1 }
} else {
playerl(FacialExpression.FRIENDLY, "Hello.").also { stage = 6 }
}
}
1 -> npcl(FacialExpression.NEUTRAL, "Hello, adventurer. It's a shame you never found that cult. We have a horrible suspicion that there's been another burglary.").also { stage++ }
2 -> playerl(FacialExpression.NEUTRAL, "That's worrying.").also { stage++ }
3 -> npcl(FacialExpression.NEUTRAL, "Yes, it is. The thing is, we can't even work out what they've taken! It's all very odd.").also { stage++ }
4 -> playerl(FacialExpression.HALF_ASKING, "Is there anything more I can do to help?").also { stage++ }
5 -> npcl(FacialExpression.NEUTRAL, "I don't think so. Sir Ceril says you've done enough.").also { stage = END_DIALOGUE }
6 -> npcl(FacialExpression.ANNOYED, "Well well well... if it isn't our very own local hero. Good to see you managed to clear your name. I always had faith in you!").also { stage = END_DIALOGUE }
}
}
return true
}
override fun getIds(): IntArray = intArrayOf(NPCs.GUARD_887)
}

View file

@ -0,0 +1,198 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import content.data.Quests
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCultListeners.Companion.HAZEEL_CULT_VARP
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCultListeners.Companion.attrCarnillean
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCultListeners.Companion.attrHazeel
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCultListeners.Companion.attrSewer1R
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCultListeners.Companion.attrSewer2R
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCultListeners.Companion.attrSewer3L
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCultListeners.Companion.attrSewer4R
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCultListeners.Companion.attrSewer5R
import core.api.*
import core.game.node.entity.player.Player
import core.game.node.entity.player.link.quest.Quest
import core.game.node.entity.skill.Skills
import core.plugin.Initializable
import org.rs09.consts.Items
/* HAZEEL CULT
*
* Primary video source: https://www.youtube.com/watch?v=hFBJx_xERQ4 (carnillean playthrough)
* Primary video source: https://www.youtube.com/watch?v=ZYPB823IyRk (mahjarrat playthrough)
* https://web.archive.org/web/20081217064620/http://runescape.salmoneus.net/quests/HazeelCult.html
* https://web.archive.org/web/20090121182721/http://runehq.com/guide.php?type=quest&id=00297
* https://runescape.wiki/w/Hazeel_Cult?oldid=815270
*
* Varp 223 - Hazeel Cult progress
* https://chisel.weirdgloop.org/varbs/display?varplayer=223
* Values Stage Description
* 0 0 unstarted
* 2 1 after talking to ceril
* 3 2 talk to clivet (set attr for carnillean)
* 4 2 talk to clivet (set attr for mahjarrat)
* 5 3 poison poured in food (mahjarrat-only stage)
* 6 4 alomone either fought or he tells you he needs scroll
* 7 5 either returning the armour, or finding the scroll
* 9 100 quest complete - either presenting proof that jones is a bad guy (duh) or resurrecting hazeel
*
* The varp values just appear to track quest stages, so I don't update the varp at intermediate quest stages.
*
* Good source of half the quest log: https://youtu.be/Kf95y-YdviA?si=kWeA-8oa-22TttE4&t=140
*
* Original framework of the quest and about half the dialogue is credit to szumaster.
*
*/
@Initializable
class HazeelCult : Quest(Quests.HAZEEL_CULT, 74, 73, 1, HAZEEL_CULT_VARP, 0, 1, 9) {
// These functions are used at various points to check whether the player is following the Carnillean's side, or Hazeel's.
companion object {
fun carnilleanArc(player: Player) : Boolean {
return (!getAttribute(player, attrHazeel, false) && getAttribute(player, attrCarnillean, false))
}
fun mahjarratArc(player: Player) : Boolean {
return (getAttribute(player, attrHazeel, false) && !getAttribute(player, attrCarnillean, false))
}
// the fake ending where you don't prove Jones guilty yet. lol.
fun fakeFinish(player: Player) {
openInterface(player, 277)
player.packetDispatch.sendString("" + player.getQuestRepository().getPoints() + "", 277, 7)
player.packetDispatch.sendString("You have... kind of... completed the Hazeel Cult Quest!", 277, 4)
player.packetDispatch.sendItemZoomOnInterface(Items.COINS_995, 230, 277, 5)
player.packetDispatch.sendString("5 coins", 277, 10)
player.packetDispatch.sendString("", 277, 11)
player.packetDispatch.sendString("", 277, 12)
player.packetDispatch.sendString("", 277, 13)
player.packetDispatch.sendString("", 277, 14)
player.packetDispatch.sendString("", 277, 15)
player.packetDispatch.sendString("", 277, 16)
player.packetDispatch.sendString("", 277, 17)
}
}
override fun drawJournal(player: Player, stage: Int) {
super.drawJournal(player, stage)
var line = 11
val attributeMahjarrat = mahjarratArc(player)
val attributeCarnillean = carnilleanArc(player)
if (stage >= 0) {
line(player, "I can start the quest by talking to !!Sir Ceril Carnillean?? at", line++, stage >= 1)
line(player, "the house due !!West?? of !!Ardougne Zoo??.", line++, stage >= 1)
line++
}
if (stage >= 1) {
line(player, "I spoke to !!Sir Ceril Carnillean?? at his house and agreed to", line++, stage >= 2)
line(player, "help him !!investigate?? the theft of a !!family heirloom??.", line++, stage >= 2)
line++
}
if (stage >= 2) {
line(player, "I found a !!member of the cult?? called !!Clivet?? at the entrance", line++, stage >= 3)
line(player, "to the !!cult's hideout??, south of !!Ardougne??.", line++, stage >= 3)
line++
}
/* --------------- *
* HAZEEL SIDE
* --------------- */
if (stage >= 3 && attributeMahjarrat && !attributeCarnillean) {
line(player, "Having decided to assist the cult in their mission to revive !!Hazeel??,", line++, stage >= 4)
line(player, "I tried to !!poison?? the !!Carnilleans?? but instead", line++, stage >= 4)
line(player, "only managed to kill their pet dog.", line++, stage >= 4)
line++
}
if (stage >= 4 && attributeMahjarrat && !attributeCarnillean) {
line(player, "I spoke to !!Clivet?? and he told me that I had failed,", line++, stage >= 5)
line(player, "but he gave me an !!amulet?? anyway.", line++, stage >= 5)
line(player, "Then I managed to take the sewer rafts to the cult hideout.", line++, stage >= 5)
line++
}
if (stage >= 5 && attributeMahjarrat && !attributeCarnillean) {
line(player, "I found the scroll needed to revive !!Hazeel?? in a", line++, stage >= 100)
line(player, "secret compartment upstairs at the !!Carnillean Mansion??.", line++, stage >= 100)
line++
}
if (stage == 100 && attributeMahjarrat && !attributeCarnillean) {
line(player, "I gave !!Alomone?? the scroll from the !!Carnillean Mansion?? and", line++, true)
line(player, "helped perform the revivification of !!Hazeel??.", line++, true)
line(player, "I was rewarded for all of my help", line++, true)
line(player, "in returning !!Hazeel?? to his followers.", line++, true)
line++
}
/* --------------- *
* CARNILLEAN SIDE
* --------------- */
if (stage >= 2 && attributeCarnillean && !attributeMahjarrat) {
line(player, "He told me a pack of lies about the !!Carnilleans??,", line++, stage >= 4)
line(player, "then asked me to join the !!cult??. Obviously, I refused.", line++, stage >= 4)
line(player, "I was still no closer to recovering the !!missing armour??.", line++, stage >= 4)
line(player, "After speaking to him, he jumped onto a !!raft?? and headed", line++, stage >= 4)
line(player, "into the !!sewer system??. I need to find a way to follow", line++, stage >= 4)
line++
}
if (stage >= 4 && attributeCarnillean && !attributeMahjarrat) {
line(player, "I managed to enter !!the hideout??, kill the !!cult leader?? and", line++, stage >= 5)
line(player, "retrieve !!the armour??. I discovered that !!Jones the Butler??", line++, stage >= 5)
line(player, "was secretly a !!member of the cult?? and a !!traitor??.", line++, stage >= 5)
line++
}
// this stage displays a 'fake' quest ending!
// https://youtu.be/hFBJx_xERQ4?si=ft_1rZGKAHpDjind&t=648
if (stage >= 5 && attributeCarnillean && !attributeMahjarrat) {
line(player, "I returned !!the armour??, but !!Ceril?? didn't believe !!Jones was involved??", line++, stage >= 100)
line(player, "with the cult and was responsible for the theft.", line++, stage >= 100)
line++
}
if (stage == 100 && attributeCarnillean && !attributeMahjarrat) {
line(player, "I found undeniable evidence that the !!Butler?? was", line++, true)
line(player, "involved with the cult and gave it to !!Ceril??.", line++, true)
line(player, "My name was cleared and", line++, true)
line(player, "I graciously accepted the reward for all of my help.", line++, true)
line++
}
/* --------------- *
* QUEST COMPLETE
* --------------- */
if (stage == 100) {
line(player, "<col=FF0000>QUEST COMPLETE!</col>", line, false)
}
}
override fun finish(player: Player) {
var ln = 10
super.finish(player)
player.packetDispatch.sendItemZoomOnInterface(Items.COINS_995, 230, 277, 5)
drawReward(player, "1 Quest Point", ln++)
drawReward(player, "2,000 Coins", ln++)
drawReward(player, "1,500 Thieving XP", ln)
rewardXP(player, Skills.THIEVING, 1500.0)
addItemOrDrop(player, Items.COINS_995, 2000)
// no attributes need removing on quest completion. the sewer valves are not saved, and which quest arc you chose affects the quest journal and post-quest dialogue.
}
// use ::setqueststage 74 0 to reset quest.
override fun reset(player: Player) {
removeAttribute(player, attrSewer1R)
removeAttribute(player, attrSewer2R)
removeAttribute(player, attrSewer3L)
removeAttribute(player, attrSewer4R)
removeAttribute(player, attrSewer5R)
removeAttribute(player, attrHazeel)
removeAttribute(player, attrCarnillean)
}
override fun newInstance(`object`: Any?): Quest = this
}

View file

@ -0,0 +1,266 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import content.data.Quests
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.carnilleanArc
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.mahjarratArc
import core.api.*
import core.game.dialogue.FacialExpression
import core.game.global.action.DoorActionHandler
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.world.map.Location
import org.rs09.consts.*
class HazeelCultListeners : InteractionListener {
override fun defineListeners() {
on(CLIVET_STAIRS, IntType.SCENERY, "climb-up") { player, _ ->
teleport(player, location(2586, 3237, 0))
sendMessage(player, "You climb up the stairs.")
return@on true
}
on(CAVE_ENTRANCE, IntType.SCENERY, "enter") { player, _ ->
teleport(player, location(2570, 9682, 0))
sendMessage(player, "You enter the cave.")
return@on true
}
on(CLIVET_RAFT, IntType.SCENERY, "board") { player, node ->
// to use the raft, you must also be at either
// Carnillean Arc Stage 2 or greater,
// Mahjarrat Arc Stage 3 with Hazeel's Mark,
// Mahjarrat Arc Stage 4 or greater
val questStage = getQuestStage(player, Quests.HAZEEL_CULT)
val questReq = if(
(carnilleanArc(player) && questStage >= 2) ||
(mahjarratArc(player) && inEquipmentOrInventory(player, Items.HAZEELS_MARK_2406) && questStage >= 3) ||
(mahjarratArc(player) && questStage >= 4)
) {true} else false
val raftUnlock = if(
getAttribute(player, attrSewer1R, false) &&
getAttribute(player, attrSewer2R, false) &&
getAttribute(player, attrSewer3L, false) &&
getAttribute(player, attrSewer4R, false) &&
getAttribute(player, attrSewer5R, false) &&
questReq
) {true} else false
// check which raft this is.
val x = node.location.x
if (x == 2567) { // raft to cultists. only use if you've turned the valves.
if (!raftUnlock) {
sendNPCDialogue(player, CLIVET_NPC, "Hey! I don't remember saying you could use that raft!", FacialExpression.ANNOYED)
} else {
teleport(player, location(2606, 9692, 0))
sendDialogue(player, "The raft washes up the sewer, past the islands until it reaches the end of the sewer passage.")
}
} else { // raft away from cultists. no check on this one since the valve attributes aren't saved.
teleport(player, location(2567, 9680, 0))
sendDialogue(player, "The raft flows back to the cave entrance.")
}
return@on true
}
/*
* Valve 1 - This is right across the bridge south of the Carnilleans' house. Turn it right.
* Valve 2 - This is right behind Carnilleans' house. Turn it right.
* Valve 3 - This is north of the cave. Turn it left.
* Valve 4 - This is south of the zoo's penguin cage. Turn it right.
* Valve 5 - Go southeast of the fourth valve, past the monk. Turn it right.
*
* All 4 of the below sources turn the valves in different orders. Text quest guides make no mention of needing a specific order,
* even though Clivet's bad-side dialogue makes it seem like it. For this implementation, the player needs to set each valve in the
* correct direction, order of the valves does not matter.
*
* Bad side - turns in order: https://www.youtube.com/watch?v=ZYPB823IyRk
* Good side - turns out of order: https://www.youtube.com/watch?v=OuEvnA6Cyrw
* Good side - turns out of order: https://www.youtube.com/watch?v=JG7E3uVx4rs
* Bad side - turns out of order: https://www.youtube.com/watch?v=T8JyyhDEVSg
*
* I allow the valve attributes to be set even if quest is unstarted, but getting on the raft checks the quest stage.
*/
// valves 1, 2, 4, 5 should be turned right
on(SEWERS, IntType.SCENERY, "turn-right") { player, node ->
if (!player.location.withinDistance(node.asScenery().location, 2)) return@on true
val currentSewer = node.id
when (currentSewer) {
SEWER_1 -> setAttribute(player, attrSewer1R, true)
SEWER_2 -> setAttribute(player, attrSewer2R, true)
SEWER_3 -> removeAttribute(player, attrSewer3L)
SEWER_4 -> setAttribute(player, attrSewer4R, true)
SEWER_5 -> setAttribute(player, attrSewer5R, true)
}
lock(player, 4)
animate(player, TURN_VALVE_ANIMATION)
sendDialogueLines(player, "You turn the large metal valve to the right. Beneath your feet you", "can hear the sudden sound of rushing water from the sewer.")
return@on true
}
// valve 3 should be turned left
on(SEWERS, IntType.SCENERY, "turn-left") { player, node ->
if (!player.location.withinDistance(node.asScenery().location, 2)) return@on true
val currentSewer = node.id
when (currentSewer) {
SEWER_1 -> removeAttribute(player, attrSewer1R)
SEWER_2 -> removeAttribute(player, attrSewer2R)
SEWER_3 -> setAttribute(player, attrSewer3L, true)
SEWER_4 -> removeAttribute(player, attrSewer4R)
SEWER_5 -> removeAttribute(player, attrSewer5R)
}
lock(player, 4)
animate(player, TURN_VALVE_ANIMATION)
sendDialogueLines(player, "You turn the large metal valve to the left. Beneath your feet you", "can hear the sudden sound of rushing water from the sewer.")
return@on true
}
// this wardrobe contains some poison and an amulet, used to convince ceril that jones is a baddie
on(WARDROBE, IntType.SCENERY, "search") { player, _ ->
openDialogue(player, WardrobeRevealDialogueFile())
return@on true
}
// siding with Hazeel and trying to poison the Carnilleans
onUseWith(IntType.SCENERY, POISON, COOKING_RANGE) { player, _, _ ->
if (mahjarratArc(player) && removeItem(player, POISON)) {
sendDialogueLines(player, "You pour the poison into the hot pot.", "The poison dissolves into the soup.")
setQuestStage(player, Quests.HAZEEL_CULT, 3)
} else {
sendNPCDialogue(player, NPCs.CLAUS_THE_CHEF_886, "Oi - I don't want people messing around with my range!")
}
return@onUseWith true
}
// crate in the basement. they key can only be obtained in the mahjarrat arc, which locks the secret passage, chest, and hazeel scroll to that arc as well.
on(CRATE, IntType.SCENERY, "search") { player, _ ->
if (getQuestStage(player, Quests.HAZEEL_CULT) >= 1 &&
mahjarratArc(player) &&
!inInventory(player, CHEST_KEY)
) {
sendDialogue(player, "You search the crate. Under various food packages you find an old rusty key.")
addItemOrDrop(player, CHEST_KEY, 1)
} else {
sendMessage(player, "You search the crate but find nothing.")
}
return@on true
}
// secret wall on the way up to retrieve the hazeel scroll
on(SECRET_PASSAGE, IntType.SCENERY, "Knock-at") { player, node ->
if (inInventory(player, CHEST_KEY) && player.location.y == 3274) {
DoorActionHandler.handleAutowalkDoor(player, node.asScenery())
sendMessage(player, "There is something odd about the wall here.")
sendMessage(player, "You find a secret passageway")
} else if (player.location.y == 3275) {
DoorActionHandler.handleAutowalkDoor(player, node.asScenery())
} else {
sendMessage(player, "Nothing interesting happens.")
}
return@on true
}
// chest with hazeel scroll. unlocks with key, opens, and contains the scroll.
on(Scenery.CHEST_2856, IntType.SCENERY, "Open") { player, _ ->
sendMessage(player, "This chest is locked shut.")
return@on true
}
// chest with hazeel scroll. unlocks with key, opens, and contains the scroll.
onUseWith(IntType.SCENERY, CHEST_KEY, Scenery.CHEST_2856) { player, _, _ ->
sendMessage(player, "You unlock the chest.")
animate(player, Animations.HUMAN_OPEN_CHEST_536)
addScenery(Scenery.CHEST_2857,CHEST_LOCATION, 2, 10)
removeScenery(core.game.node.scenery.Scenery(Scenery.CHEST_2856, Location(2565, 3272, 2)))
return@onUseWith true
}
// chest with hazeel scroll. unlocks with key, opens, and contains the scroll.
on(Scenery.CHEST_2857, IntType.SCENERY, "Close") { _, _ ->
addScenery(Scenery.CHEST_2856, CHEST_LOCATION, 2, 10)
removeScenery(core.game.node.scenery.Scenery(Scenery.CHEST_2857, CHEST_LOCATION))
return@on true
}
// chest with hazeel scroll. unlocks with key, opens, and contains the scroll.
on(Scenery.CHEST_2857, IntType.SCENERY, "Search") { player, _ ->
val hasScroll = hasAnItem(player, HAZEEL_SCROLL).container != null
if (hasScroll) {
sendDialogue(player, "You already have the scroll from this chest.")
return@on true
}
addItemOrDrop(player, HAZEEL_SCROLL, 1)
sendItemDialogue(player, HAZEEL_SCROLL, "Inside the chest you find the Scroll of Hazeel.")
setQuestStage(player, Quests.HAZEEL_CULT, 5)
return@on true
}
}
// interact locations for the sewer valves
override fun defineDestinationOverrides() {
setDest(IntType.SCENERY, intArrayOf(SEWER_1), "turn-right", "turn-left") { _, _ ->
return@setDest Location.create(2563, 3246, 0)
}
setDest(IntType.SCENERY, intArrayOf(SEWER_2), "turn-right", "turn-left") { _, _ ->
return@setDest Location.create(2571, 3263, 0)
}
setDest(IntType.SCENERY, intArrayOf(SEWER_3), "turn-right", "turn-left") { _, _ ->
return@setDest Location.create(2586, 3244, 0)
}
setDest(IntType.SCENERY, intArrayOf(SEWER_4), "turn-right", "turn-left") { _, _ ->
return@setDest Location.create(2597, 3262, 0)
}
setDest(IntType.SCENERY, intArrayOf(SEWER_5), "turn-right", "turn-left") { _, _ ->
return@setDest Location.create(2610, 3242, 0)
}
}
companion object {
const val HAZEEL_CULT_VARP = 223
const val attrSewer1R = "quest:hazeelcult-sewer1"
const val attrSewer2R = "quest:hazeelcult-sewer2"
const val attrSewer3L = "quest:hazeelcult-sewer3"
const val attrSewer4R = "quest:hazeelcult-sewer4"
const val attrSewer5R = "quest:hazeelcult-sewer5"
const val attrHazeel = "/save:quest:hazeelcult-hazeel" // true when you agree to help Clivet
const val attrCarnillean = "/save:quest:hazeelcult-carnillean" // true when you DON'T agree to help Clivet
const val CLIVET_NPC = NPCs.CLIVET_893
const val CLIVET_RAFT = Scenery.RAFT_2849
const val CAVE_ENTRANCE = Scenery.CAVE_ENTRANCE_2852
const val CLIVET_STAIRS = Scenery.STAIRS_2853
const val HAZEEL_SCROLL = Items.HAZEEL_SCROLL_2403
const val SEWER_1 = Scenery.SEWER_VALVE_2844 // Valve 1 - This is right across the bridge south of the Carnilleans' house. Turn it right.
const val SEWER_2 = Scenery.SEWER_VALVE_2845 // Valve 2 - This is right behind Carnilleans' house. Turn it right.
const val SEWER_3 = Scenery.SEWER_VALVE_2846 // Valve 3 - This is north of the cave. Turn it left.
const val SEWER_4 = Scenery.SEWER_VALVE_2847 // Valve 4 - This is south of the zoo's penguin cage. Turn it right.
const val SEWER_5 = Scenery.SEWER_VALVE_2848 // Valve 5 - Go southeast of the fourth valve, past the monk. Turn it right.
val SEWERS = intArrayOf(SEWER_1, SEWER_2, SEWER_3, SEWER_4, SEWER_5)
const val WARDROBE = Scenery.WARDROBE_2851
const val TURN_VALVE_ANIMATION = 4861
const val CRATE = Scenery.CRATE_34585
const val CHEST_KEY = Items.CHEST_KEY_709
const val POISON = Items.POISON_273
const val COOKING_RANGE = Scenery.COOKING_RANGE_2859
const val SECRET_PASSAGE = Scenery.WALL_26940
val CHEST_LOCATION = Location(2565, 3272, 2)
}
}

View file

@ -0,0 +1,45 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import content.data.Quests
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.mahjarratArc
import core.api.getQuestStage
import core.game.dialogue.DialoguePlugin
import core.game.dialogue.FacialExpression
import core.game.node.entity.player.Player
import core.plugin.Initializable
import core.tools.END_DIALOGUE
import org.rs09.consts.*
@Initializable
class HazeelCultistDialogue(player: Player? = null) : DialoguePlugin(player) {
override fun handle(componentID: Int, buttonID: Int, ): Boolean {
val questStage = getQuestStage(player!!, Quests.HAZEEL_CULT)
when {
(questStage in 1..99) -> when (stage) {
0 -> playerl(FacialExpression.FRIENDLY, "Hi.").also { stage++ }
1 -> npcl(FacialExpression.ANNOYED, "What? Who are you and why did you come here? It doesn't matter, leave now adventurer. While you still can.",).also { stage = END_DIALOGUE }
}
(questStage == 100) -> when (stage) {
0 -> {
if (mahjarratArc(player)) {
playerl(FacialExpression.FRIENDLY, "Hello.").also { stage = 1 }
} else {
playerl(FacialExpression.FRIENDLY, "Hello.").also { stage = 2 }
}
}
1 -> npcl(FacialExpression.FRIENDLY, "The adventurer returns! Glory to you for your efforts in bringing about the return of Lord Hazeel!").also { stage = END_DIALOGUE }
2 -> npcl(FacialExpression.ANNOYED, "It's the meddler! The one who blew Jones' cover! I don't know why you came to this place but it is here that you will die.").also { stage++ }
3 -> {
end()
npc.attack(player)
}
}
}
return true
}
override fun getIds(): IntArray = intArrayOf(NPCs.HAZEEL_CULTIST_894)
}

View file

@ -0,0 +1,17 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import core.game.node.entity.npc.AbstractNPC
import core.game.world.map.Location
import core.plugin.Initializable
import org.rs09.consts.*
/**
* Hazeel is spawned in briefly during the Mahjarrat end of Hazeel Cult quest.
*/
@Initializable
class HazeelNPC(id: Int = 0, location: Location? = null, ) : AbstractNPC(id, location) {
override fun construct(id: Int, location: Location, vararg objects: Any, ): AbstractNPC = HazeelNPC(id, location)
override fun getIds(): IntArray = intArrayOf(NPCs.HAZEEL_892)
}

View file

@ -0,0 +1,64 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import content.data.Quests
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.mahjarratArc
import core.api.getQuestStage
import core.game.dialogue.DialoguePlugin
import core.game.dialogue.FacialExpression
import core.game.node.entity.player.Player
import core.plugin.Initializable
import core.tools.END_DIALOGUE
import org.rs09.consts.*
@Initializable
class HenryetaDialogue(player: Player? = null) : DialoguePlugin(player) {
override fun handle(componentID: Int, buttonID: Int): Boolean {
val questStage = getQuestStage(player!!, Quests.HAZEEL_CULT)
when {
// stage 3 - poison poured in food (mahjarrat-only stage)
(questStage == 3) -> {
when (stage) {
0 -> playerl(FacialExpression.FRIENDLY, "Hello. Are you okay?").also { stage++ }
1 -> npcl(FacialExpression.SAD, "No, no I am very far from OK. Those hooligans slaughtered my precious Scruffy! I shall never recover! I am emotionally scarred for life!").also { stage = END_DIALOGUE }
2 -> playerl(FacialExpression.NEUTRAL, "It WAS only a dog you know...").also { stage = END_DIALOGUE }
}
}
// stage 0 - unstarted
// stage 1 - after talking to ceril
// stage 2 - talk to clivet (set attr for carnillean)
// stage 2 - talk to clivet (set attr for mahjarrat)
// stage 4 - alomone either fought or he tells you he needs scroll
// stage 5 - either returning the armour, or finding the scroll
(questStage in 0..99) -> {
when (stage) {
0 -> npcl(FacialExpression.FRIENDLY, "Oh, hello. I'm afraid I'm not very presentable at the moment. I've been under incredible stress of late! It's ruining my complexion!").also { stage++ }
1 -> playerl(FacialExpression.FRIENDLY, "Why? What's wrong?").also { stage++ }
2 -> npcl(FacialExpression.FRIENDLY, "It's those awful cultists! I just don't feel safe here anymore!").also { stage = END_DIALOGUE }
}
}
// stage 100 - quest complete - either presenting proof that jones is a bad guy (duh) or resurrecting hazeel
(questStage == 100) -> when (stage) {
0 -> {
if (mahjarratArc(player)) {
playerl(FacialExpression.FRIENDLY, "Hello.").also { stage = 1 }
} else {
playerl(FacialExpression.FRIENDLY, "Hello.").also { stage = 3 }
}
}
1 -> npcl(FacialExpression.HALF_ASKING, "Oh, it's you. I'm very disappointed that you never found those awful cultists. You're quite useless really, aren't you?").also { stage++ }
2 -> playerl(FacialExpression.HAPPY, "Yup, I guess I am.").also { stage = END_DIALOGUE }
3 -> npcl(FacialExpression.FRIENDLY, "Hello again adventurer! Since you dealt with those horrible hooligan cultists, things have vastly improved for us around here!").also { stage++ }
4 -> playerl(FacialExpression.HAPPY, "My pleasure, Lady Henryeta.").also { stage = END_DIALOGUE }
}
}
return true
}
override fun getIds(): IntArray = intArrayOf(NPCs.HENRYETA_CARNILLEAN_889)
}

View file

@ -0,0 +1,80 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import content.data.Quests
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.mahjarratArc
import core.api.getQuestStage
import core.game.dialogue.DialoguePlugin
import core.game.dialogue.FacialExpression
import core.game.node.entity.player.Player
import core.plugin.Initializable
import core.tools.END_DIALOGUE
import org.rs09.consts.*
@Initializable
class PhilipeDialogue(player: Player? = null) : DialoguePlugin(player) {
override fun handle(
componentID: Int,
buttonID: Int,
): Boolean {
val questStage = getQuestStage(player!!, Quests.HAZEEL_CULT)
when {
// stage 0 - unstarted
(questStage == 0) -> {
when (stage) {
0 -> npcl(FacialExpression.CHILD_NORMAL, "Hello, how are you today?").also { stage++ }
1 -> playerl(FacialExpression.FRIENDLY, "Good thank you. And yourself?").also { stage++ }
2 -> npcl(FacialExpression.CHILD_NORMAL, "Fine and dandy.").also { stage = END_DIALOGUE }
}
}
// stage 3 - poison poured in food (mahjarrat-only stage)
(questStage == 3) -> {
when (stage) {
0 -> playerl(FacialExpression.FRIENDLY, "Hello there.").also { stage++ }
1 -> npcl(FacialExpression.CHILD_SAD, "Someone killed Scruffy. I liked Scruffy. He never told me off.").also { stage++ }
2 -> playerl(FacialExpression.HALF_GUILTY, "Yeah... it's a real shame.").also { stage++ }
3 -> npcl(FacialExpression.CHILD_SAD, "I want my mommy.").also { stage = END_DIALOGUE }
}
}
// stage 1 - after talking to ceril
// stage 2 - talk to clivet (set attr for carnillean)
// stage 2 - talk to clivet (set attr for mahjarrat)
// stage 4 - alomone either fought or he tells you he needs scroll
// stage 5 - either returning the armour, or finding the scroll
(questStage in 1..99) -> {
when (stage) {
0 -> npcl(FacialExpression.CHILD_NORMAL, "Mommy said you're here to kill all the nasty people that keep breaking in.").also { stage++ }
1 -> playerl(FacialExpression.FRIENDLY, "Something like that.").also { stage++ }
2 -> npcl(FacialExpression.CHILD_FRIENDLY, "Can I watch?").also { stage++ }
3 -> playerl(FacialExpression.FRIENDLY, "No!").also { stage = END_DIALOGUE }
}
}
// stage 100 - quest complete - either presenting proof that jones is a bad guy (duh) or resurrecting hazeel
(questStage == 100) -> when (stage) {
0 -> {
if (mahjarratArc(player)) {
playerl(FacialExpression.FRIENDLY, "Hello there.").also { stage = 1 }
} else {
playerl(FacialExpression.FRIENDLY, "Hello youngster.").also { stage = 4 }
}
}
1 -> npcl(FacialExpression.CHILD_NORMAL, "What have you brought me? I want some more toys!").also { stage++ }
2 -> playerl(FacialExpression.FRIENDLY, "I'm afraid I don't have any toys.").also { stage++ }
3 -> npcl(FacialExpression.CHILD_SAD, "Toys! I want toys!").also { stage = END_DIALOGUE }
4 -> npcl(FacialExpression.CHILD_NORMAL, "Daddy says you don't like Mr. Jones. Mr. Jones is nice. He brings me toys and sweets.").also { stage++ }
5 -> playerl(FacialExpression.FRIENDLY, "Jones is a bad man, Philipe.").also { stage++ }
6 -> npcl(FacialExpression.CHILD_NORMAL, "You're a bad " + (if (player.isMale) "man" else "lady") + " I don't like you!").also { stage++ }
7 -> playerl(FacialExpression.FRIENDLY, "I'll try and console myself about that disappointment somehow.").also { stage = END_DIALOGUE }
}
}
return true
}
override fun getIds(): IntArray = intArrayOf(NPCs.PHILIPE_CARNILLEAN_888)
}

View file

@ -0,0 +1,81 @@
package content.region.kandarin.ardougne.quest.hazeelcult
import content.data.Quests
import content.region.kandarin.ardougne.quest.hazeelcult.HazeelCult.Companion.carnilleanArc
import core.api.*
import core.game.dialogue.DialogueFile
import core.game.dialogue.DialoguePlugin
import core.game.dialogue.FacialExpression
import core.game.node.entity.player.Player
import core.plugin.Initializable
import core.tools.END_DIALOGUE
import org.rs09.consts.NPCs
/**
* This dialogue is triggered at stage 5 of Hazeel Cult during the Carnillean Arc by searching the wardrobe for evidence.
*/
@Initializable
class WardrobeRevealDialogue (player: Player? = null) : DialoguePlugin(player) {
override fun handle(interfaceId: Int, buttonId: Int): Boolean {
openDialogue(player, WardrobeRevealDialogueFile())
return true
}
override fun newInstance(player: Player): DialoguePlugin {
return WardrobeRevealDialogue(player)
}
override fun getIds(): IntArray {
return intArrayOf() // dialogue is triggered manually
}
}
class WardrobeRevealDialogueFile : DialogueFile() {
override fun handle(componentID: Int, buttonID: Int) {
val questStage = getQuestStage(player!!, Quests.HAZEEL_CULT)
when {
(questStage == 5 && carnilleanArc(player!!)) -> {
when (stage) {
// note that it's authentic for the 'wardrobe' to be referred to as 'cupboard' in the dialogue. it used to be a cupboard pre-graphical update. https://youtu.be/CH7i06Noc18?si=zAdxBNlIS92cseBR&t=606
0 -> sendDialogue(player!!, "You search the cupboard thoroughly. You find a bottle of poison and a mysterious amulet. You pass your discovery on to Ceril.").also { stage++ }
1 -> sendPlayerDialogue(player!!, "Ceril!").also { stage++ }
2 -> sendNPCDialogue(player!!, NPCs.CERIL_CARNILLEAN_885, "What do you want now you scoundrel?", FacialExpression.ANGRY).also { stage++ }
3 -> sendPlayerDialogue(player!!, "Look what I've found in the cupboard!", FacialExpression.ANGRY).also { stage++ }
4 -> sendDialogue(player!!, "You hand Ceril the bottle of poison.").also { stage++ }
5 -> sendNPCDialogue(player!!, NPCs.CERIL_CARNILLEAN_885, "I... I don't believe it! Poison! JONES!").also { stage++ }
6 -> sendNPCDialogue(player!!, NPCs.BUTLER_JONES_890, "You called m'lud?").also { stage++ }
7 -> sendNPCDialogue(player!!, NPCs.CERIL_CARNILLEAN_885, "JONES! Just WHAT is this POISON doing here???").also { stage++ }
8 -> sendNPCDialogue(player!!, NPCs.BUTLER_JONES_890, "P-p-poison??? Yeah... uh... well, hmm... Oh, it's for Killing rats.").also { stage++ } // '...for Killing...' is authentic
9 -> sendNPCDialogue(player!!, NPCs.BUTLER_JONES_890, "Yes, that's right. Killing rats. I am but a loyal butler sir.").also { stage++ }
10 -> sendNPCDialogue(player!!, NPCs.CERIL_CARNILLEAN_885, "Rats eh? Yes, fair enough. Sorry to have doubted you old boy.").also { stage++ }
11 -> sendPlayerDialogue(player!!, "Then how about this?").also { stage++ }
12 -> sendDialogue(player!!, "You show Ceril the amulet from the cupboard.").also { stage++ }
13 -> sendNPCDialogue(player!!, NPCs.CERIL_CARNILLEAN_885, "Wh-what??? I've seen this amulet before... The thieves that broke in... One of them was wearing an identical amulet! Jones! I don't believe it!").also { stage++ }
14 -> sendNPCDialogue(player!!, NPCs.CERIL_CARNILLEAN_885, "Jones! We trusted you! We took you into our home!").also { stage++ }
15 -> sendNPCDialogue(player!!, NPCs.BUTLER_JONES_890, "You senile old fool... It was all too easy! I should have killed you and your pathetic family weeks ago!", FacialExpression.ANGRY).also { stage++ }
16 -> sendNPCDialogue(player!!, NPCs.CERIL_CARNILLEAN_885, "To think we took you in and trusted you... and this is how you repay us...").also { stage++ }
17 -> sendNPCDialogue(player!!, NPCs.CERIL_CARNILLEAN_885, "GUARD!").also { stage++ }
18 -> sendDialogue(player!!, "A guard rushes into the house.").also { stage++ }
19 -> sendNPCDialogue(player!!, NPCs.CERIL_CARNILLEAN_885, "Take him away. Attempted murder and burglary.").also { stage++ }
20 -> sendNPCDialogue(player!!, NPCs.BUTLER_JONES_890, "Don't think this is the last you have heard of me Ceril. You and your family will pay dearly for this. GLORY TO HAZEEL! ALL PRAISE ZAMORAK!").also { stage++ }
21 -> sendNPCDialogue(player!!, NPCs.GUARD_887, "Now now. Come along quietly. There'll be no revenge where YOU'RE going - Port Sarim Jail.").also { stage++ }
22 -> sendNPCDialogue(player!!, NPCs.CERIL_CARNILLEAN_885, "It looks like I am indebted to you sirrah!").also { stage++ }
23 -> sendPlayerDialogue(player!!, "No problem. We all make mistakes.").also { stage++ }
24 -> sendNPCDialogue(player!!, NPCs.CERIL_CARNILLEAN_885, "But if it weren't for you... My whole family... we could have been... I apologize for my harshness before.").also { stage++ }
25 -> sendNPCDialogue(player!!, NPCs.CERIL_CARNILLEAN_885, "The very least I can do is to reward you for your noble efforts, and to offer my sincerest apologies as a Lord and a gentleman.").also { stage++ }
26 -> sendPlayerDialogue(player!!, "Thank you Lord Ceril.").also { stage++ }
27 -> sendNPCDialogue(player!!, NPCs.CERIL_CARNILLEAN_885, "No, no, thank YOU! Feel free to stop by anytime adventurer!"
).also {
end()
finishQuest(player!!, Quests.HAZEEL_CULT)
}
}
}
(questStage in 0..100) -> {
when(stage) {
0 -> sendDialogue(player!!, "You search the cupboard thoroughly but find nothing of interest.").also {stage = END_DIALOGUE }
}
}
}
}
}

View file

@ -96,9 +96,15 @@ class PlagueCityListeners : InteractionListener {
addItem(player, Items.ASHES_592)
}
else{
sendItemDialogue(player, Items.A_MAGIC_SCROLL_1505, "You memorise what is written on the scroll.")
sendDialogue(player, "You can now cast the Ardougne Teleport spell provided you have the required runes and magic level.")
setAttribute(player, ARDOUGNE_TELE_ATTRIBUTE, true)
openDialogue(player, object : DialogueFile() {
override fun handle(componentID: Int, buttonID: Int) {
when (stage) {
0 -> sendItemDialogue(player, Items.A_MAGIC_SCROLL_1505, "You memorise what is written on the scroll.").also { stage++ }
1 -> sendDialogue(player, "You can now cast the Ardougne Teleport spell provided you have the required runes and magic level.").also { stage = END_DIALOGUE }
}
}
})
}
return@on true
}

View file

@ -5,6 +5,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -141,8 +142,9 @@ class BindingBook : InteractionListener {
}
override fun defineListeners() {
on(Items.BINDING_BOOK_730, IntType.ITEM, "read") { player, _ ->
on(Items.BINDING_BOOK_730, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -5,6 +5,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -330,8 +331,9 @@ class GiannesCookBook : InteractionListener {
}
override fun defineListeners() {
on(Items.GIANNES_COOK_BOOK_2167, IntType.ITEM, "read") { player, _ ->
on(Items.GIANNES_COOK_BOOK_2167, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -5,6 +5,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -47,8 +48,9 @@ class ShamansTomeBook : InteractionListener {
}
override fun defineListeners() {
on(Items.SHAMANS_TOME_729, IntType.ITEM, "read") { player, _ ->
on(Items.SHAMANS_TOME_729, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -5,6 +5,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -87,8 +88,9 @@ class DMCManual : InteractionListener {
override fun defineListeners() {
on(Items.INSTRUCTION_MANUAL_5, IntType.ITEM, "read") { player, _ ->
on(Items.INSTRUCTION_MANUAL_5, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -5,6 +5,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -131,8 +132,9 @@ class TranslationBook : InteractionListener {
}
override fun defineListeners() {
on(Items.TRANSLATION_BOOK_784, IntType.ITEM, "read") { player, _ ->
on(Items.TRANSLATION_BOOK_784, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -2,6 +2,7 @@ package content.region.kandarin.quest.observatoryquest
import content.global.handlers.iface.*
import core.ServerConstants
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -161,8 +162,9 @@ class AstronomyBook : InteractionListener {
}
override fun defineListeners() {
on(Items.ASTRONOMY_BOOK_600, IntType.ITEM, "read") { player, _ ->
on(Items.ASTRONOMY_BOOK_600, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -6,6 +6,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -152,8 +153,9 @@ class BaxtorianBook : InteractionListener {
}
override fun defineListeners() {
on(Items.BOOK_ON_BAXTORIAN_292, IntType.ITEM, "read") { player, _ ->
on(Items.BOOK_ON_BAXTORIAN_292, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -7,6 +7,7 @@ import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
import org.rs09.consts.Items
import content.data.Quests
import core.api.*
/**
* Battered book handler for the Elemental Workshop I quest
@ -31,8 +32,9 @@ class BatteredBookHandler : InteractionListener {
}
override fun defineListeners() {
on(Items.BATTERED_BOOK_2886, IntType.ITEM, "read") { player, _ ->
on(Items.BATTERED_BOOK_2886, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -5,6 +5,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -116,8 +117,9 @@ class ArdougneGuideBook : InteractionListener {
}
override fun defineListeners() {
on(Items.GUIDE_BOOK_1856, IntType.ITEM, "read") { player, _ ->
on(Items.GUIDE_BOOK_1856, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -5,6 +5,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -70,8 +71,9 @@ class ChemicalsBook : InteractionListener {
}
override fun defineListeners() {
on(Items.BOOK_ON_CHEMICALS_711, IntType.ITEM, "read") { player, _ ->
on(Items.BOOK_ON_CHEMICALS_711, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -13,6 +13,7 @@ import core.game.node.item.Item
import core.plugin.Initializable
import core.plugin.Plugin
import org.rs09.consts.Items
import core.api.*
private val BOOK = Item(Items.GOBLIN_SYMBOL_BOOK_5009)
@Initializable
@ -38,7 +39,7 @@ class LostTribeOptionHandler : OptionHandler(){
node ?: return false
when(node.id){
5008 -> player.interfaceManager.open(Component(50))
5009 -> player.interfaceManager.open(Component(183))
5009 -> player.interfaceManager.open(Component(183)).also { storeBookInHouse(player, node) }
6916 -> {
if(!player.inventory.containsItem(BOOK) && !player.bank.containsItem(BOOK) && player.questRepository.getQuest(
Quests.THE_LOST_TRIBE).getStage(player) >= 41){

View file

@ -5,6 +5,7 @@ import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -192,10 +193,11 @@ class SinkethsDiary : InteractionListener {
}
override fun defineListeners() {
on(Items.SINKETHS_DIARY_11002, IntType.ITEM, "read") { player, _ ->
on(Items.SINKETHS_DIARY_11002, IntType.ITEM, "read") { player, node ->
setAttribute(player, "bookInterfaceCallback", ::display)
setAttribute(player, "bookInterfaceCurrentPage", 0)
display(player, 0, 0)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -4,7 +4,7 @@ import content.global.handlers.iface.BookInterface
import content.global.handlers.iface.BookLine
import content.global.handlers.iface.Page
import content.global.handlers.iface.PageSet
import core.api.setAttribute
import core.api.storeBookInHouse
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.entity.player.Player
@ -371,8 +371,9 @@ class AbyssalBook : InteractionListener {
}
override fun defineListeners() {
on(Items.ABYSSAL_BOOK_5520, IntType.ITEM, "read") { player, _ ->
on(Items.ABYSSAL_BOOK_5520, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -11,6 +11,7 @@ import org.rs09.consts.Items
import content.data.Quests
import core.api.getQuestStage
import core.api.setQuestStage
import core.api.storeBookInHouse
/**
* Shield of Arrav Book
@ -97,8 +98,9 @@ class ShieldofArravBook : InteractionListener {
}
override fun defineListeners() {
on(Items.BOOK_757, IntType.ITEM, "read") { player, _ ->
on(Items.BOOK_757, IntType.ITEM, "read") { player, node ->
BookInterface.openBook(player, BookInterface.FANCY_BOOK_3_49, ::display)
storeBookInHouse(player, node)
return@on true
}
}

View file

@ -5,6 +5,8 @@ import content.data.Quests
import content.data.consumables.*
import content.data.skill.SkillingTool
import content.global.handlers.iface.ge.StockMarket
import content.global.skill.construction.decoration.pohstorage.StorableFamily
import content.global.skill.construction.decoration.pohstorage.StorageInterface
import content.global.skill.slayer.SlayerEquipmentFlags
import content.global.skill.slayer.SlayerFlags
import content.global.skill.slayer.SlayerManager
@ -287,6 +289,7 @@ fun hasAnItem(player: Player, id: Int, checkSecondBank: Boolean): ContainerisedI
* @return A ContainerisedItem containing the container and the item ID if found, otherwise ContainerisedItem(null, -1) if not found
*/
fun hasAnItem(player: Player, ids: Array<Int>, checkSecondBank: Boolean): ContainerisedItem {
// Search bank/equip/inv and return if item is found there.
val searchSpace = if (checkSecondBank) {
arrayOf(player.inventory, player.equipment, player.bankPrimary, player.bankSecondary)
} else {
@ -299,6 +302,20 @@ fun hasAnItem(player: Player, ids: Array<Int>, checkSecondBank: Boolean): Contai
}
}
}
// Check all costume storage containers and return if item is found there.
for (id in ids) {
if (StorableFamily.values().any { family ->
player.getPOHStorageState()
.getContainer(family)
.contains(id)
}) {
// Note that this proxy container is a fake container just used to get the correct return type.
return ContainerisedItem(player.POHStorageProxyContainer, id)
}
}
// Item is not found.
return ContainerisedItem(null, -1)
}
@ -2377,6 +2394,68 @@ fun Player.getCutscene(): Cutscene? {
return getAttribute<Cutscene?>(this, Cutscene.ATTRIBUTE_CUTSCENE, null)
}
/**
* Stores a book in the POH storage
* If this is the first time reading the book, store it in POH bookcase if eligible
* @param player the player
* @param itemID the ID of the book that was interacted with
* @return True if the item was identified and processed (whether read or not)
* False if the node was not a book or did not have a bitIndex
*/
fun storeBookInHouse(player: Player, itemId: Int): Boolean {
// find the corresponding Storable object
val storable = StorageInterface.findStorableByDisplayId(itemId) ?: return false
// check if the storable is tracked
if (storable.bitIndex == -1) {
return true
}
// retrieve bitfields where the values of already-read books are stored, and if the book has been read previously
val bits = StorageInterface.BookcaseBitfields.get(player)
val isRead = StorageInterface.BookcaseBitfields.isBitSet(bits, storable.bitIndex)
// special check for games book. It isn't normally available and is only obtainable from the bookcase. You can't read it without first having it, and if you lose it you can't go back to somewhere else to get a new copy. Thus, it is always spawned in.
if(itemId == Items.GAME_BOOK_7681) {
val updatedBits = StorageInterface.BookcaseBitfields.setBit(bits, storable.bitIndex)
val container = StorageInterface.getStorageContainer(player, StorableFamily.BOOKCASE)
if (!isRead) {
StorageInterface.BookcaseBitfields.set(player, updatedBits)
container.addItem(itemId)
} else {
container.addItem(itemId)
}
return true
}
// if book is not read, store it in house
if (!isRead) {
// update bitfields
val updatedBits = StorageInterface.BookcaseBitfields.setBit(bits, storable.bitIndex)
StorageInterface.BookcaseBitfields.set(player, updatedBits)
// add to the POH container
val container = StorageInterface.getStorageContainer(player, StorableFamily.BOOKCASE)
container.addItem(itemId)
}
return true
}
/**
* Stores a book in the POH storage
* If this is the first time reading the book, store it in POH bookcase if eligible
* Used inside interaction listeners ("read") on nodes (books)
* @param player the player
* @param node the node (book) that was interacted with
* @return True if the item was identified and processed (whether read or not)
* False if the node was not a book or did not have a bitIndex
*/
fun storeBookInHouse(player: Player, node: Node): Boolean {
val bookId = (node as? Item)?.id ?: return false
return storeBookInHouse(player, bookId)
}
fun Player.getCutsceneStage(): Int {
return getAttribute(this, Cutscene.ATTRIBUTE_CUTSCENE_STAGE, 0)
}

View file

@ -81,6 +81,7 @@ fun permadeath(target: Player) {
// House data
target.houseManager.createNewHouseAt(HouseLocation.NOWHERE)
target.getPOHStorageState().clear()
// Achievements
for (type in DiaryType.values()) {

View file

@ -323,9 +323,17 @@ public final class DialogueInterpreter {
/**
* Send a message with an item next to it.
* @param itemId The item id.
* Accepts item IDs.
*/
public Component sendItemMessage(int itemId, String... messages) {
return sendItemMessage(new Item(itemId), messages);
}
/**
* Send a message with an item next to it.
* Accepts Items.
*/
public Component sendItemMessage(final Item item, String... messages) {
// Select interface based on number of messages - 241 (1 line) to 244 (4 lines)
int interfaceId = 240 + messages.length;
player.getPacketDispatch().sendInterfaceConfig(interfaceId, 1, false);
@ -337,8 +345,8 @@ public final class DialogueInterpreter {
player.getPacketDispatch().sendString(messages[i], interfaceId, 4+i);
}
// Set the first Item (child 1)
ItemDefinition itemDef = ItemDefinition.forId(itemId);
player.getPacketDispatch().sendItemOnInterface(itemId, 1, interfaceId, 2);
ItemDefinition itemDef = ItemDefinition.forId(item.getId());
player.getPacketDispatch().sendItemOnInterface(item.getId(), item.getAmount(), interfaceId, 2);
player.getPacketDispatch().sendAngleOnInterface(interfaceId, 2, itemDef.getModelZoom() / 2, itemDef.getModelRotationX(), itemDef.getModelRotationY());
player.getPacketDispatch().sendRepositionOnInterface(interfaceId, 2, 45, 46);
// Hide the second item which seems to be used for double items (child 1)
@ -362,15 +370,8 @@ public final class DialogueInterpreter {
}
/**
* Send a message with an item next to it.
*/
public Component sendItemMessage(final Item item, String... messages) {
return sendItemMessage(item.getId(), messages);
}
/**
* Send a message with an item next to it.
* @param message The message.
* Send a message with two items next to it.
* Accepts item IDs.
*/
public Component sendDoubleItemMessage(int first, int second, String... message) {
return sendDoubleItemMessage(new Item(first), new Item(second), message);
@ -378,6 +379,7 @@ public final class DialogueInterpreter {
/**
* Send a message with two items next to it.
* Accepts Items.
* Note that interface 241 to 244 contains 2 childs for images, which is used for sendDoubleItemMessage.
* @param first The first item to display.
* @param second The second item to display.

View file

@ -71,6 +71,8 @@ abstract class DialogueLabeller : DialogueFile() {
var jumpTo: Int? = null
/** ButtonID on every click */
var buttonID: Int? = null
/** Filtered options from option stage 1 for storing the order for option stage 2 and 3 */
var filteredOptions: List<DialogueOption> = listOf()
/** ButtonID when user clicks on an option */
var optButton: Int? = null
/** Input value after a user enters a value */
@ -80,7 +82,7 @@ abstract class DialogueLabeller : DialogueFile() {
abstract fun addConversation()
/** Helper functions to create an individual stage for each of the dialogue stages. */
private fun assignIndividualStage(callback: () -> Component?, unclosable: Boolean) {
private fun assignIndividualStage(unclosable: Boolean = false, callback: () -> Component?) {
if (startingStage == null) { startingStage = 0 }
if (stage == dialogueCounter && jumpTo == null) { // Run this stage when the stage equals to the dialogueCounter of this dialogue
val component = callback() // CALLBACK FUNCTION
@ -107,11 +109,11 @@ abstract class DialogueLabeller : DialogueFile() {
/** Does absolutely nothing but to make it look like the other API. */
fun assignToIds(npcid: Int) { /* super.npc = NPC(npcid) */ }
/** Marks the start of a series of dialogue that can be jumped to using a [goto]. */
fun label(label: String, nesting: () -> Unit = {}) {
/** Marks the start of a series of dialogue that can be jumped to using a [goto]. You can nest after label(...) with a {} purely for organization only. */
fun label(vararg label: String, nesting: () -> Unit = {}) {
if (startingStage == null) { startingStage = 1 }
dialogueCounter++
labelStageMap[label] = dialogueCounter
for (l in label) { labelStageMap[l] = dialogueCounter }
nesting()
}
@ -141,12 +143,12 @@ abstract class DialogueLabeller : DialogueFile() {
/** Manual stage. For custom creation of an individual stage. Must call interpreter in some form. **/
fun manual(unclosable: Boolean = false, callback: (player: Player, npc: NPC) -> Component?) {
assignIndividualStage({ return@assignIndividualStage callback(player!!, npc!!) }, unclosable)
assignIndividualStage(unclosable) { return@assignIndividualStage callback(player!!, npc!!) }
}
/** Dialogue player/playerl. Shows player chathead with text. **/
fun player(chatAnim: ChatAnim = ChatAnim.NEUTRAL, vararg messages: String, unclosable: Boolean = false) {
assignIndividualStage({ return@assignIndividualStage interpreter!!.sendDialogues(player, chatAnim, *formatMessages(messages)) }, unclosable)
assignIndividualStage(unclosable) { return@assignIndividualStage interpreter!!.sendDialogues(player, chatAnim, *formatMessages(messages)) }
}
/** Dialogue player/playerl. Shows player chathead with text. **/
fun player(vararg messages: String) { player(ChatAnim.NEUTRAL, *messages) }
@ -157,16 +159,15 @@ abstract class DialogueLabeller : DialogueFile() {
/** Dialogue npc/npcl. Shows npc chathead with text. **/
fun npc(chatAnim: ChatAnim = ChatAnim.NEUTRAL, vararg messages: String, unclosable: Boolean = false) {
val callback = callback@{ return@callback interpreter!!.sendDialogues(npc, chatAnim, *formatMessages(messages)) }
assignIndividualStage(callback, unclosable)
assignIndividualStage(unclosable) { return@assignIndividualStage interpreter!!.sendDialogues(npc, chatAnim, *formatMessages(messages)) }
}
/** Dialogue npc/npcl. Shows npcId chathead with text. **/
fun npc(chatAnim: ChatAnim = ChatAnim.NEUTRAL, npcId: Int = npc!!.id, vararg messages: String, unclosable: Boolean = false) {
assignIndividualStage({ return@assignIndividualStage interpreter!!.sendDialogues(NPC(npcId), chatAnim, *formatMessages(messages)) }, unclosable)
assignIndividualStage(unclosable) { return@assignIndividualStage interpreter!!.sendDialogues(NPC(npcId), chatAnim, *formatMessages(messages)) }
}
/** Dialogue npc/npcl. Shows npcId chathead with text. **/
fun npc(npcId: Int = npc!!.id, vararg messages: String, unclosable: Boolean = false) {
assignIndividualStage({ return@assignIndividualStage interpreter!!.sendDialogues(NPC(npcId), ChatAnim.NEUTRAL, *formatMessages(messages)) }, unclosable)
assignIndividualStage(unclosable) { return@assignIndividualStage interpreter!!.sendDialogues(NPC(npcId), ChatAnim.NEUTRAL, *formatMessages(messages)) }
}
/** Dialogue npc/npcl. Shows npc chathead with text. **/
fun npc(vararg messages: String) { npc(ChatAnim.NEUTRAL, *messages) }
@ -177,21 +178,19 @@ abstract class DialogueLabeller : DialogueFile() {
/** Dialogue item/iteml. Shows item with text. **/
fun item(item: Item, vararg messages: String, message: String = "", unclosable: Boolean = false) {
val callback = callback@{ return@callback interpreter!!.sendItemMessage(item, *formatMessages(messages, message)) }
assignIndividualStage(callback, unclosable)
assignIndividualStage(unclosable) { return@assignIndividualStage interpreter!!.sendItemMessage(item, *formatMessages(messages, message)) }
}
@Deprecated("Use item() instead.", ReplaceWith("item(item, *messages)"))
fun iteml(item: Item, vararg messages: String) { throw Exception("Deprecated DialogueLabel: Use item() instead.") }
/** Dialogue overloaded doubleItem/doubleIteml. Shows two items with text. **/
fun item(item: Item, item2: Item, vararg messages: String, message: String = "", unclosable: Boolean = false) {
assignIndividualStage({ return@assignIndividualStage interpreter!!.sendDoubleItemMessage(item, item2, *formatMessages(messages, message)) }, unclosable)
assignIndividualStage(unclosable) { return@assignIndividualStage interpreter!!.sendDoubleItemMessage(item, item2, *formatMessages(messages, message)) }
}
/** Dialogue line/linel. Simply shows text. **/
fun line(vararg messages: String, unclosable: Boolean = false) {
val callback = callback@{ return@callback interpreter!!.sendDialogue(*messages) }
assignIndividualStage(callback, unclosable)
assignIndividualStage(unclosable) { return@assignIndividualStage interpreter!!.sendDialogue(*messages) }
}
@Deprecated("Use line() instead.", ReplaceWith("line(*messages)"))
fun linel(vararg messages: String) { throw Exception("Deprecated DialogueLabel: Use line() instead.") }
@ -199,13 +198,18 @@ abstract class DialogueLabeller : DialogueFile() {
/** Dialogue option. Shows the option dialogue with choices for the user to select. **/
fun options(vararg options: DialogueOption, title: String = "Select an Option", unclosable: Boolean = false) {
// Filter out options that aren't shown.
val filteredOptions = options.filter{ if (it.optionIf != null) { it.optionIf.invoke(player!!, npc!!) } else { true } }
// Stage Part 1: Options List Dialogue
val callback = callback@{ return@callback interpreter!!.sendOptions(title, *filteredOptions.map{ it.option }.toTypedArray()) }
assignIndividualStage(callback, unclosable)
assignIndividualStage(unclosable) {
filteredOptions = options.filter{ if (it.optionIf != null) { it.optionIf.invoke(player!!, npc!!) } else { true } }
if (filteredOptions.size == 1) { // If there is only one option left, jump to that option.
jumpTo = labelStageMap[filteredOptions[0].goto]
return@assignIndividualStage null
}
return@assignIndividualStage interpreter!!.sendOptions(title, *filteredOptions.map{ it.option }.toTypedArray())
}
// Stage Part 2: Show spoken text.
val opt = if (buttonID != null && buttonID in 1..filteredOptions.size) { filteredOptions[buttonID!! - 1] } else { null }
assignIndividualStage({
assignIndividualStage(unclosable) {
var component: Component? = null
if (opt?.skipPlayer == true) {
jumpTo = stage + 1
@ -214,7 +218,7 @@ abstract class DialogueLabeller : DialogueFile() {
}
optButton = buttonID // transfer the buttonID to a temp memory for the next stage
return@assignIndividualStage component
}, unclosable)
}
// Stage Part 3: Jump To goto
if (stage == dialogueCounter && optButton != null && optButton in 1..filteredOptions.size) {
jumpTo = labelStageMap[filteredOptions[optButton!! - 1].goto]
@ -226,7 +230,7 @@ abstract class DialogueLabeller : DialogueFile() {
/** Dialogue input. Shows the input dialogue with an input box for the user to type in. Read [optInput] for the value. **/
fun input(type: InputType, prompt: String = "Enter the amount") {
assignIndividualStage({
assignIndividualStage {
// These are similar to calling sendInputDialogue
when (type) {
InputType.AMOUNT -> interpreter!!.sendInput(true, prompt)
@ -246,7 +250,7 @@ abstract class DialogueLabeller : DialogueFile() {
}
player!!.setAttribute("input-type", type)
return@assignIndividualStage null
}, false)
}
}
/** Dialogue input. Shows the input dialogue with an input box for the user to type in. Read [optInput] in an [exec] function for the value. **/
fun input(numeric: Boolean, prompt: String = "Enter the amount") { input( if (numeric) { InputType.NUMERIC } else { InputType.STRING_SHORT }, prompt) }
@ -265,11 +269,10 @@ abstract class DialogueLabeller : DialogueFile() {
/** Calls another dialogue file. Always use this to open another dialogue file instead of calling openDialogue() in exec{} due to interfaces clashing. **/
fun open(player: Player, dialogue: Any, vararg args: Any) {
val callback = callback@{
assignIndividualStage {
core.api.openDialogue(player, dialogue, *args)
return@callback null
return@assignIndividualStage null
}
assignIndividualStage(callback, false)
}
/** WARNING: DIALOGUE LABELLER WILL BREAK IN CERTAIN FUNCTIONS. USE open() instead. */

View file

@ -1,5 +1,6 @@
package core.game.global;
import content.global.skill.construction.decoration.pohstorage.StorableFamily;
import core.game.container.Container;
import core.game.dialogue.FacialExpression;
import core.game.dialogue.FacialExpression;
@ -66,6 +67,25 @@ public final class Skillcape {
}
}
}
// trim any skillcapes in the player's POH storage
content.global.skill.construction.decoration.pohstorage.StorageState poh = player.getPOHStorageState();
content.global.skill.construction.decoration.pohstorage.StorageContainer rack = poh.getContainer(StorableFamily.CAPE_RACK_SKILL);
// count the capes
java.util.List<Integer> toReplace = new java.util.ArrayList<>();
for (int itemId : rack.getItems()) {
if (getCapeIndex(new Item(itemId)) != -1) {
toReplace.add(itemId);
}
}
// replace the untrimmed IDs with trimmed IDs
for (int oldId : toReplace) {
int skillIndex = getCapeIndex(new Item(oldId));
rack.withdraw(oldId);
rack.addItem(getTrimmed(skillIndex).getId());
}
}
/**

View file

@ -113,10 +113,8 @@ open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface {
* @param shouldSendMessage whether the core combat code believes you should send a message e.g. "You can't attack this NPC with that weapon"
* @return whether the attacker should be able to attack this NPC.
*/
open fun canBeAttackedBy(self: NPC, attacker: Entity, style: CombatStyle, shouldSendMessage: Boolean) : Boolean {
if (attacker is Player && !self.definition.hasAction("attack"))
return false
return true
open fun canBeAttackedBy(self: NPC, attacker: Entity, style: CombatStyle, shouldSendMessage: Boolean): Boolean {
return !(attacker is Player && !self.definition.hasAction("attack") && !self.definition.hasAction("smash-ice"))
}
/**

View file

@ -2,6 +2,8 @@ package core.game.node.entity.player;
import content.global.handlers.item.equipment.BarrowsEquipment;
import content.global.handlers.item.equipment.special.SalamanderSwingHandler;
import content.global.skill.construction.decoration.pohstorage.POHStorageProxyContainer;
import content.global.skill.construction.decoration.pohstorage.StorageState;
import content.global.skill.runecrafting.PouchManager;
import content.global.skill.slayer.SlayerEquipmentFlags;
import core.api.ContentAPIKt;
@ -130,7 +132,22 @@ public class Player extends Entity {
public Boolean isAfkLogout;
/**
private StorageState POHStorageState;
public final Container POHStorageProxyContainer = new POHStorageProxyContainer(this);
public StorageState getPOHStorageState() {
if (POHStorageState == null) {
POHStorageState = new StorageState(this);
}
return POHStorageState;
}
public void setPOHStorageState(StorageState state) {
this.POHStorageState = state;
}
/**
* The inventory.
*/
private final Container inventory = new Container(28).register(new InventoryListener(this));
@ -772,10 +789,14 @@ public class Player extends Entity {
@Override
public boolean continueAttack(Entity target, CombatStyle style, boolean message) {
if (target instanceof NPC
&& !((NPC) target).getDefinition().hasAction("attack")
&& !((NPC) target).isIgnoreAttackRestrictions(this)) {
return false;
if (target instanceof NPC) {
NPC npc = (NPC) target;
NPC shown = npc.getShownNPC(this);
boolean canAttack = shown.getDefinition().hasAction("attack")
|| shown.getDefinition().hasAction("smash-ice");
if (!canAttack && !npc.isIgnoreAttackRestrictions(this)) {
return false;
}
}
if (target instanceof Player) {
Player p = (Player) target;

View file

@ -16,7 +16,6 @@ import core.ServerConstants
import core.api.log
import core.game.node.entity.combat.graves.GraveController
import core.game.node.entity.combat.graves.GraveType
import core.game.world.GameWorld
import core.tools.Log
import java.io.File
import java.io.FileReader
@ -74,6 +73,7 @@ class PlayerSaveParser(val player: Player) {
parseStatistics()
parseAchievements()
parsePouches()
parsePOHStorable()
}
fun runContentHooks()
@ -93,6 +93,12 @@ class PlayerSaveParser(val player: Player) {
player.pouchManager.parse(saveFile!!["pouches"] as JSONArray)
}
fun parsePOHStorable() {
val storableRaw = saveFile?.get("pohstorage") ?: return
val storableJson = storableRaw as JSONObject
player.getPOHStorageState().readJson(storableJson)
}
fun parseAttributes() {
if (saveFile!!.containsKey("attributes")) {
val attrData = saveFile!!["attributes"] as JSONArray

View file

@ -55,6 +55,7 @@ class PlayerSaver (val player: Player){
saveStatManager(saveFile)
saveAttributes(saveFile)
savePouches(saveFile)
savePOHStorageRoom(saveFile)
contentHooks.forEach { it.savePlayer(player, saveFile) }
return saveFile
}
@ -95,6 +96,10 @@ class PlayerSaver (val player: Player){
player.pouchManager.save(root)
}
fun savePOHStorageRoom(root: JSONObject) {
root.put("pohstorage", player.getPOHStorageState().toJson())
}
fun saveVersion(root: JSONObject){
root.put("version", player.version)
}

View file

@ -4,6 +4,7 @@ import core.plugin.Initializable
import core.game.system.command.Command
import core.game.system.command.Privilege
import core.api.*
import core.game.interaction.QueueStrength
@Initializable
class ConfigCommandSet : CommandSet(Privilege.ADMIN){
@ -50,5 +51,27 @@ class ConfigCommandSet : CommandSet(Privilege.ADMIN){
val id = args[1].toIntOrNull() ?: reject(player, "INVALID INTERFACE ID")
player.interfaceManager.openComponent(id as Int)
}
define("ifaces", Privilege.ADMIN, "::ifaces <From Interface ID> <To Interface ID> <(opt) Duration Per Interface>", "Opens interfaces from the From ID to the To ID, with a delay of 3 between each, unless specified otherwise"){ player, args ->
if (args.size < 3) {
reject(player, "Syntax error: ::anims <From Interface ID> <To Interface ID> <(opt) Duration Per Interface>")
}
val ifaceFrom = args[1].toInt()
val ifaceTo = args[2].toInt()
val ifaceDelay = (args.getOrNull(3) ?: "3").toInt()
queueScript(player, 1, QueueStrength.WEAK) { stage: Int ->
val ifaceId = ifaceFrom + stage
player.interfaceManager.openComponent(ifaceId)
notify(player, "Opening interface $ifaceId")
if (ifaceId == ifaceTo) {
return@queueScript stopExecuting(player)
}
return@queueScript delayScript(player, ifaceDelay)
}
}
}
}

View file

@ -1,6 +1,8 @@
package core.game.system.command.sets
import content.global.activity.jobs.JobManager
import content.global.skill.construction.decoration.pohstorage.Storable
import content.global.skill.construction.decoration.pohstorage.StorableFamily
import core.api.*
import core.cache.Cache
import core.cache.def.impl.DataMap
@ -54,6 +56,57 @@ class DevelopmentCommandSet : CommandSet(Privilege.ADMIN) {
}
}
/**
* Adds a subset of POH storable item takeIds to the player's bank, filtered by StorableFamily type. These are stored in the costume room or bookcase.
* Usage: ::pohstorableitems [TYPE]
* Example: ::pohstorableitems BOOKCASE
* Example: ::pohstorableitems TREASURE_CHEST_MED
*/
define("pohstorableitems", Privilege.ADMIN, "::pohstorableitems TYPE", "Adds POH storable items to bank. To see TYPEs, use the command with no argument.") { player, args ->
// Check for the argument (StorableFamily type)
val typeArg = if (args.size > 1) args[1].uppercase() else null
val allStorables = Storable.values()
// Filter the list of items based on the provided argument
val itemsToProcess = if (typeArg != null) {
try {
val targetFamily = StorableFamily.valueOf(typeArg)
allStorables.filter { it.type == targetFamily }
} catch (e: IllegalArgumentException) {
sendMessage(player, "Error: Unknown StorableFamily type '$typeArg'. Valid types are:")
StorableFamily.values().forEach { family ->
sendMessage(player, family.toString())
}
return@define
}
} else {
// If no argument is provided, default to all storables
sendMessage(player, "Valid types are:")
StorableFamily.values().forEach { family ->
sendMessage(player, family.toString())
}
return@define
}
if (itemsToProcess.isEmpty()) {
sendMessage(player, "No storable items found for type: '$typeArg'.")
return@define
}
var totalItemsAdded = 0
// Process the filtered list of storables
itemsToProcess.forEach { storable ->
storable.takeIds.forEach { itemId ->
if (player.bank.add(Item(itemId))) {
totalItemsAdded++
}
}
}
return@define
}
define("cs2", Privilege.ADMIN, "::cs2 id args", "Allows you to call arbitrary cs2 scripts during runtime") {player, args ->
var scriptArgs = ArrayList<Any>()
if (args.size == 2) {

View file

@ -10,10 +10,15 @@
9 ahoy_runedraw
10 ahoy_windspeed
11 bank_deposit_box
12 pulsing yellow rectangle
13 bankpin_main
14 bankpin_settings
15 banner_padlock_keys
16 banner_anti_virus
17 nothing
18 heads for the welcome screen?
19 heads for the welcome screen?
20 heads for the welcome screen?
21 banner_scamming
22 banner_security
23 banner_xmas
@ -85,6 +90,7 @@
89 stab slash lunge block
90 staff with autocast
91 darts or knives
92 nothing
93 whip
94 confirm_destroy
95 sailing_transport_world_map
@ -315,6 +321,7 @@
320 stats
321 swamp_boatjourney
322 swamp_boatjourney_back
323 tai bwo wannai parcel service
324 tanner
325 target
326 teleport_other
@ -458,7 +465,7 @@
464 emotes
465 ooglog ogress opens health spa
466 switch diagram
467 menu with big buttons
467 POH_costume room
468 diango item return
469 zep_balloon_map
470 zep_interface
@ -511,6 +518,7 @@
517 turn off black
518 old follower inv?
519 objbox
520 dream_cyrisus's courage
521 dream_cyrisus
522 dream_monster_stat
523 dream_player_stats
@ -565,6 +573,7 @@
572 cws_warning_13
573 cws_warning_12
574 cws_warning_1
575 all fired up beacon status
576 cws_warning_8
577 cws_warning_18
578 cws_warning_15
@ -575,6 +584,8 @@
583 cws_doomsayer
584 kr_king_statue
585 kr_pyramid_escape
586 change of address form for the citizens of seers' village
587 meet at camel left half
588 kr_picklock
589 clanjoin
590 clansetup
@ -694,7 +705,21 @@
704 p safety 6
705 p safety 7
706 fuck this
707 player safety
708 player safety
709 player safety
710 player safety
711 player safety
712 meet at legend's guild from kent
713 slight darkness overlay
714 nothing
715 banner_easter08
716 summoning_chocatrice
717 incubator controls
718 select an option
719 operating your incubator
720 chocatrice hatching
721 chocatrice hatching
722 summoning_side
723 carpet_info
724 carpet_runesquares
@ -703,6 +728,18 @@
727 carpet_draughts
728 carpet_main
729 carpet_ticket
730 bounty hunter?
731 bounty hunter?
732 fog rewards shop
733 ivandis flail
734 fairy ring main
735 fairy ring log
736 dragon hatchling forge
737 pick a kitten
738 dillo_tok-xil-im
739 dillo_tzhaar numbers
740 open your stats prompt
741 congratulations!
742 graphics_options
743 sound_options
744 loginscreen
@ -714,42 +751,79 @@
750 topstat_run
751 filterbuttons
752 chattop
753 nothing
754 pmchat
755 worldmap
756 boardgames_options
757 npcchatlarge
758 canoe_travel
759 display mode prompt
760 tutorial2_objbox
761 tutorial2_sir vant's requests
762 bank_v2_main
763 bank_v2_side
764 tutorial2_minimap tutorial
765 tutorial2_switch_task
766 tutorial2_death
767 bank_v2_help
768 nothing
769 tutorial2_text
770 black screen
771 tutorial character creation
772 returned items screen
773 luc2?_guthix and swirling runes cutscene
774 where do you want to rub the key
775 sum1?_spooky overlay, spirit of summer?
776 sum1?_spooky overlay fade to white
777 sum1?_sepia overlay
778 rcguild_side
779 rcguild_rewards
780 rcguild_map
781 rcguild_overlay
782 telescope star
783 crcs_tightrope
784 crcs_side
785 crcs_rewards
786 crcs_equipment
787 star landing time discovered by board
788 crcs_scoreboard
789 set/clear caller
790 clanwars_end
791 clanwars_setup
792 clanwars_setup_side
793 clanwars_free for all safe
794 varrock census
795 black overlay
796 ghostly mage
797 ancient magicks
798 warning you are currently carrying a lot of items
799 holding pen
800 banner_halloween
801 quickchat_locked
802 sc_tutorial_overlay
803 sc_scores_border
804 nothing
805 sc_item_transfer
806 sc_remaining_clay
807 nothing
808 yellow squares overlay
809 sc_score overlay and timer
810 sc_scores
811 sc_reward_shop
812 clan leader and allied leaders
813 sc_processing
814 luc2_chapter2
815 luc2_door opening text
816 luc2_telescope_view
817 luc2_door opening
818 luc2_descending cliff
819 luc2_probably
820 luc2_book pages
821 luc2_swirling mass
822 luc2_chapter3
823 luc2_hand with opening orb
824 luc2_pulsing black screen
825 luc2_ascending cliff
826 luc2_chapter1
827 luc2_lucien_projectiles
828 journal with map?
@ -758,3 +832,8 @@
831 drawings on ice again
832 again
833 again ALL SEEM TO BE FOR BARB ASSAULT
834 nothing
835 nothing
836 nothing
837 2009scape rewards shop
838 anything beyond this is a crash