diff --git a/Server/data/configs/item_configs.json b/Server/data/configs/item_configs.json index f66a72738..64b2c1e89 100644 --- a/Server/data/configs/item_configs.json +++ b/Server/data/configs/item_configs.json @@ -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" } ] \ No newline at end of file diff --git a/Server/data/configs/music_regions.json b/Server/data/configs/music_regions.json index 51d9a41ae..ef23c3ecf 100644 --- a/Server/data/configs/music_regions.json +++ b/Server/data/configs/music_regions.json @@ -887,6 +887,10 @@ "region": "10390", "id": "102" }, + { + "region": "10391", + "id": "478" + }, { "region": "10393", "id": "346" diff --git a/Server/data/configs/npc_configs.json b/Server/data/configs/npc_configs.json index fad58089b..96b4883b6 100644 --- a/Server/data/configs/npc_configs.json +++ b/Server/data/configs/npc_configs.json @@ -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" } ] \ No newline at end of file diff --git a/Server/data/configs/npc_spawns.json b/Server/data/configs/npc_spawns.json index 2fda3c449..77ef33f02 100644 --- a/Server/data/configs/npc_spawns.json +++ b/Server/data/configs/npc_spawns.json @@ -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", diff --git a/Server/src/main/content/global/handlers/item/RingForgingOperateListener.kt b/Server/src/main/content/global/handlers/item/RingForgingOperateListener.kt new file mode 100644 index 000000000..f9ed2c366 --- /dev/null +++ b/Server/src/main/content/global/handlers/item/RingForgingOperateListener.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/Server/src/main/content/global/handlers/item/book/GoblinBook.kt b/Server/src/main/content/global/handlers/item/book/GoblinBook.kt index 5f9661fa6..a7a276d41 100644 --- a/Server/src/main/content/global/handlers/item/book/GoblinBook.kt +++ b/Server/src/main/content/global/handlers/item/book/GoblinBook.kt @@ -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 } } diff --git a/Server/src/main/content/global/handlers/item/book/SecurityBookPlugin.kt b/Server/src/main/content/global/handlers/item/book/SecurityBookPlugin.kt index 4813fed18..672890422 100644 --- a/Server/src/main/content/global/handlers/item/book/SecurityBookPlugin.kt +++ b/Server/src/main/content/global/handlers/item/book/SecurityBookPlugin.kt @@ -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 } } diff --git a/Server/src/main/content/global/handlers/item/book/StrongholdNotes.kt b/Server/src/main/content/global/handlers/item/book/StrongholdNotes.kt index 4a67ee93f..c72efce0f 100644 --- a/Server/src/main/content/global/handlers/item/book/StrongholdNotes.kt +++ b/Server/src/main/content/global/handlers/item/book/StrongholdNotes.kt @@ -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 } } diff --git a/Server/src/main/content/global/handlers/item/toys/DiangoReclaimInterface.java b/Server/src/main/content/global/handlers/item/toys/DiangoReclaimInterface.java index 4fda74d6b..57f51641e 100644 --- a/Server/src/main/content/global/handlers/item/toys/DiangoReclaimInterface.java +++ b/Server/src/main/content/global/handlers/item/toys/DiangoReclaimInterface.java @@ -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)) )) diff --git a/Server/src/main/content/global/handlers/scenery/TableListener.kt b/Server/src/main/content/global/handlers/scenery/TableListener.kt new file mode 100644 index 000000000..c35ebd27b --- /dev/null +++ b/Server/src/main/content/global/handlers/scenery/TableListener.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/Server/src/main/content/global/skill/construction/ConstructionGuideBook.kt b/Server/src/main/content/global/skill/construction/ConstructionGuideBook.kt index a6c8beeae..750654efd 100644 --- a/Server/src/main/content/global/skill/construction/ConstructionGuideBook.kt +++ b/Server/src/main/content/global/skill/construction/ConstructionGuideBook.kt @@ -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 } diff --git a/Server/src/main/content/global/skill/construction/HouseZone.java b/Server/src/main/content/global/skill/construction/HouseZone.java index 903d47215..940ab8af1 100644 --- a/Server/src/main/content/global/skill/construction/HouseZone.java +++ b/Server/src/main/content/global/skill/construction/HouseZone.java @@ -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; } diff --git a/Server/src/main/content/global/skill/construction/HousingStyle.java b/Server/src/main/content/global/skill/construction/HousingStyle.java index e4d59b90d..eace0e7bb 100644 --- a/Server/src/main/content/global/skill/construction/HousingStyle.java +++ b/Server/src/main/content/global/skill/construction/HousingStyle.java @@ -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. diff --git a/Server/src/main/content/global/skill/construction/Room.java b/Server/src/main/content/global/skill/construction/Room.java index 883a4776c..ac7b56cec 100644 --- a/Server/src/main/content/global/skill/construction/Room.java +++ b/Server/src/main/content/global/skill/construction/Room.java @@ -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); diff --git a/Server/src/main/content/global/skill/construction/decoration/costume/ToyBoxPlugin.kt b/Server/src/main/content/global/skill/construction/decoration/costume/ToyBoxPlugin.kt deleted file mode 100644 index 86b5007d1..000000000 --- a/Server/src/main/content/global/skill/construction/decoration/costume/ToyBoxPlugin.kt +++ /dev/null @@ -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 { - SceneryDefinition.forId(18802).handlers["option:open"] = this - return this - } - - override fun handle(player: Player, node: Node, option: String): Boolean { - DiangoReclaimInterface.open(player) - return true - } -} \ No newline at end of file diff --git a/Server/src/main/content/global/skill/construction/decoration/pohstorage/POHStorageProxyContainer.java b/Server/src/main/content/global/skill/construction/decoration/pohstorage/POHStorageProxyContainer.java new file mode 100644 index 000000000..07c934d4a --- /dev/null +++ b/Server/src/main/content/global/skill/construction/decoration/pohstorage/POHStorageProxyContainer.java @@ -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"; + } +} \ No newline at end of file diff --git a/Server/src/main/content/global/skill/construction/decoration/pohstorage/Storable.kt b/Server/src/main/content/global/skill/construction/decoration/pohstorage/Storable.kt new file mode 100644 index 000000000..d4b0b71c5 --- /dev/null +++ b/Server/src/main/content/global/skill/construction/decoration/pohstorage/Storable.kt @@ -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 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 by lazy { idToStorable.keys } + + /** + * Precomputed set of item ids for each Storable for fast lookup. + */ + private val storableIdSets: Map> by lazy { + values().associateWith { it.takeIds.toSet() } + } + + /** + * Returns all item ids from all storables. + */ + fun allIds(): Set = 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 { + val bankIds = player.bank.toArray().mapNotNull { it?.id }.toSet() + return values().filter { storable -> + storableIdSets[storable]?.any { it in bankIds } ?: false + } + } + } +} + diff --git a/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorableFamily.kt b/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorableFamily.kt new file mode 100644 index 000000000..ab8832baf --- /dev/null +++ b/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorableFamily.kt @@ -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 +} \ No newline at end of file diff --git a/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorageContainer.kt b/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorageContainer.kt new file mode 100644 index 000000000..a10cc532f --- /dev/null +++ b/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorageContainer.kt @@ -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 = 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 = 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 + } +} \ No newline at end of file diff --git a/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorageInterface.kt b/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorageInterface.kt new file mode 100644 index 000000000..f6d9ccf81 --- /dev/null +++ b/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorageInterface.kt @@ -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 { + 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, type: StorableFamily): List { + // 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(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() + + 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(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) + } +} diff --git a/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorageOptionPlugin.kt b/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorageOptionPlugin.kt new file mode 100644 index 000000000..549b543d0 --- /dev/null +++ b/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorageOptionPlugin.kt @@ -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 { + 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) + } +} diff --git a/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorageState.kt b/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorageState.kt new file mode 100644 index 000000000..9108a6013 --- /dev/null +++ b/Server/src/main/content/global/skill/construction/decoration/pohstorage/StorageState.kt @@ -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() } + } +} diff --git a/Server/src/main/content/global/skill/farming/ToolLeprechaunDialogue.kt b/Server/src/main/content/global/skill/farming/ToolLeprechaunDialogue.kt index 50ec9662f..4ad7953f9 100644 --- a/Server/src/main/content/global/skill/farming/ToolLeprechaunDialogue.kt +++ b/Server/src/main/content/global/skill/farming/ToolLeprechaunDialogue.kt @@ -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 { diff --git a/Server/src/main/content/global/skill/smithing/smelting/SmeltingPulse.java b/Server/src/main/content/global/skill/smithing/smelting/SmeltingPulse.java index 5f7876db9..7951ab0ed 100644 --- a/Server/src/main/content/global/skill/smithing/smelting/SmeltingPulse.java +++ b/Server/src/main/content/global/skill/smithing/smelting/SmeltingPulse.java @@ -178,7 +178,7 @@ public class SmeltingPulse extends SkillPulse { } /** - * 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 { 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 diff --git a/Server/src/main/content/region/asgarnia/taverley/quest/witchshouse/WitchesDiaryBook.kt b/Server/src/main/content/region/asgarnia/taverley/quest/witchshouse/WitchesDiaryBook.kt index 8417d0cca..0bf3fffce 100644 --- a/Server/src/main/content/region/asgarnia/taverley/quest/witchshouse/WitchesDiaryBook.kt +++ b/Server/src/main/content/region/asgarnia/taverley/quest/witchshouse/WitchesDiaryBook.kt @@ -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 } } diff --git a/Server/src/main/content/region/desert/quest/deserttreasure/TranslationBook.kt b/Server/src/main/content/region/desert/quest/deserttreasure/TranslationBook.kt index b9ff24e33..6778a0f43 100644 --- a/Server/src/main/content/region/desert/quest/deserttreasure/TranslationBook.kt +++ b/Server/src/main/content/region/desert/quest/deserttreasure/TranslationBook.kt @@ -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 } } diff --git a/Server/src/main/content/region/desert/quest/thegolem/VarmensNotes.kt b/Server/src/main/content/region/desert/quest/thegolem/VarmensNotes.kt index 75cafd258..dda6a88bf 100644 --- a/Server/src/main/content/region/desert/quest/thegolem/VarmensNotes.kt +++ b/Server/src/main/content/region/desert/quest/thegolem/VarmensNotes.kt @@ -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 } } diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/AlomoneDialogue.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/AlomoneDialogue.kt new file mode 100644 index 000000000..e6c54d22a --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/AlomoneDialogue.kt @@ -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) +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/AlomoneNPC.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/AlomoneNPC.kt new file mode 100644 index 000000000..83b28833a --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/AlomoneNPC.kt @@ -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) + } + } +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ButlerJonesDialogue.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ButlerJonesDialogue.kt new file mode 100644 index 000000000..f8bcc91d3 --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ButlerJonesDialogue.kt @@ -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) +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/CerilCarnilleanDialogue.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/CerilCarnilleanDialogue.kt new file mode 100644 index 000000000..083e561d5 --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/CerilCarnilleanDialogue.kt @@ -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) +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ClausTheChefDialogue.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ClausTheChefDialogue.kt new file mode 100644 index 000000000..2680171c3 --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ClausTheChefDialogue.kt @@ -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) +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ClivetDialogue.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ClivetDialogue.kt new file mode 100644 index 000000000..34409ef83 --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ClivetDialogue.kt @@ -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) +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ClivetNPC.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ClivetNPC.kt new file mode 100644 index 000000000..84474795a --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ClivetNPC.kt @@ -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() + } +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/GuardHCDialogue.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/GuardHCDialogue.kt new file mode 100644 index 000000000..c3e8fd224 --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/GuardHCDialogue.kt @@ -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) +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HazeelCult.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HazeelCult.kt new file mode 100644 index 000000000..c0d0dfa27 --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HazeelCult.kt @@ -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, "QUEST COMPLETE!", 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 +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HazeelCultListeners.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HazeelCultListeners.kt new file mode 100644 index 000000000..cd6ac6007 --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HazeelCultListeners.kt @@ -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) + } +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HazeelCultistDialogue.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HazeelCultistDialogue.kt new file mode 100644 index 000000000..cb986e49d --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HazeelCultistDialogue.kt @@ -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) +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HazeelNPC.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HazeelNPC.kt new file mode 100644 index 000000000..ae249d3ea --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HazeelNPC.kt @@ -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) +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HenryetaDialogue.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HenryetaDialogue.kt new file mode 100644 index 000000000..bef204d6a --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/HenryetaDialogue.kt @@ -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) +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/PhilipeDialogue.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/PhilipeDialogue.kt new file mode 100644 index 000000000..a3135eebe --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/PhilipeDialogue.kt @@ -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) +} diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/WardrobeRevealDialogue.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/WardrobeRevealDialogue.kt new file mode 100644 index 000000000..22234e521 --- /dev/null +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/WardrobeRevealDialogue.kt @@ -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 } + } + } + } + } +} \ No newline at end of file diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/plaguecity/PlagueCityListeners.kt b/Server/src/main/content/region/kandarin/ardougne/quest/plaguecity/PlagueCityListeners.kt index 7b1638b0b..99270a12b 100644 --- a/Server/src/main/content/region/kandarin/ardougne/quest/plaguecity/PlagueCityListeners.kt +++ b/Server/src/main/content/region/kandarin/ardougne/quest/plaguecity/PlagueCityListeners.kt @@ -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 } diff --git a/Server/src/main/content/region/kandarin/dialogue/BindingBook.kt b/Server/src/main/content/region/kandarin/dialogue/BindingBook.kt index 3318d9077..6c1aa1171 100644 --- a/Server/src/main/content/region/kandarin/dialogue/BindingBook.kt +++ b/Server/src/main/content/region/kandarin/dialogue/BindingBook.kt @@ -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 } } diff --git a/Server/src/main/content/region/kandarin/dialogue/GiannesCookBook.kt b/Server/src/main/content/region/kandarin/dialogue/GiannesCookBook.kt index e663c99ba..54fe778fb 100644 --- a/Server/src/main/content/region/kandarin/dialogue/GiannesCookBook.kt +++ b/Server/src/main/content/region/kandarin/dialogue/GiannesCookBook.kt @@ -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 } } diff --git a/Server/src/main/content/region/kandarin/dialogue/ShamansTomeBook.kt b/Server/src/main/content/region/kandarin/dialogue/ShamansTomeBook.kt index 9559fa9fa..3ec20f532 100644 --- a/Server/src/main/content/region/kandarin/dialogue/ShamansTomeBook.kt +++ b/Server/src/main/content/region/kandarin/dialogue/ShamansTomeBook.kt @@ -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 } } diff --git a/Server/src/main/content/region/kandarin/quest/dwarfcannon/dmc/DMCManual.kt b/Server/src/main/content/region/kandarin/quest/dwarfcannon/dmc/DMCManual.kt index fa22f2bed..cf4ac3326 100644 --- a/Server/src/main/content/region/kandarin/quest/dwarfcannon/dmc/DMCManual.kt +++ b/Server/src/main/content/region/kandarin/quest/dwarfcannon/dmc/DMCManual.kt @@ -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 } } diff --git a/Server/src/main/content/region/kandarin/quest/grandtree/TranslationBook.kt b/Server/src/main/content/region/kandarin/quest/grandtree/TranslationBook.kt index a4c5c7bd9..2a4bc37c5 100644 --- a/Server/src/main/content/region/kandarin/quest/grandtree/TranslationBook.kt +++ b/Server/src/main/content/region/kandarin/quest/grandtree/TranslationBook.kt @@ -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 } } diff --git a/Server/src/main/content/region/kandarin/quest/observatoryquest/AstronomyBook.kt b/Server/src/main/content/region/kandarin/quest/observatoryquest/AstronomyBook.kt index f228e0864..9af08c1b1 100644 --- a/Server/src/main/content/region/kandarin/quest/observatoryquest/AstronomyBook.kt +++ b/Server/src/main/content/region/kandarin/quest/observatoryquest/AstronomyBook.kt @@ -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 } } diff --git a/Server/src/main/content/region/kandarin/quest/waterfall/BaxtorianBook.kt b/Server/src/main/content/region/kandarin/quest/waterfall/BaxtorianBook.kt index fe92e9136..c4c0d1ef0 100644 --- a/Server/src/main/content/region/kandarin/quest/waterfall/BaxtorianBook.kt +++ b/Server/src/main/content/region/kandarin/quest/waterfall/BaxtorianBook.kt @@ -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 } } diff --git a/Server/src/main/content/region/kandarin/seers/quest/elementalworkshop/BatteredBookHandler.kt b/Server/src/main/content/region/kandarin/seers/quest/elementalworkshop/BatteredBookHandler.kt index f46fcfd97..8d25adaf1 100644 --- a/Server/src/main/content/region/kandarin/seers/quest/elementalworkshop/BatteredBookHandler.kt +++ b/Server/src/main/content/region/kandarin/seers/quest/elementalworkshop/BatteredBookHandler.kt @@ -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 } } diff --git a/Server/src/main/content/region/karamja/quest/tribaltotem/ArdougneGuideBook.kt b/Server/src/main/content/region/karamja/quest/tribaltotem/ArdougneGuideBook.kt index 74ce79ed9..a8d2da26e 100644 --- a/Server/src/main/content/region/karamja/quest/tribaltotem/ArdougneGuideBook.kt +++ b/Server/src/main/content/region/karamja/quest/tribaltotem/ArdougneGuideBook.kt @@ -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 } } diff --git a/Server/src/main/content/region/misthalin/digsite/dialogue/ChemicalsBook.kt b/Server/src/main/content/region/misthalin/digsite/dialogue/ChemicalsBook.kt index a13b7c858..e8310e992 100644 --- a/Server/src/main/content/region/misthalin/digsite/dialogue/ChemicalsBook.kt +++ b/Server/src/main/content/region/misthalin/digsite/dialogue/ChemicalsBook.kt @@ -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 } } diff --git a/Server/src/main/content/region/misthalin/dorgeshuun/quest/thelosttribe/LostTribeOptionHandler.kt b/Server/src/main/content/region/misthalin/dorgeshuun/quest/thelosttribe/LostTribeOptionHandler.kt index a361dcfbc..de266e188 100644 --- a/Server/src/main/content/region/misthalin/dorgeshuun/quest/thelosttribe/LostTribeOptionHandler.kt +++ b/Server/src/main/content/region/misthalin/dorgeshuun/quest/thelosttribe/LostTribeOptionHandler.kt @@ -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){ diff --git a/Server/src/main/content/region/misthalin/varrock/dialogue/SinkethsDiary.kt b/Server/src/main/content/region/misthalin/varrock/dialogue/SinkethsDiary.kt index 79a1f59cc..e61b9d5dc 100644 --- a/Server/src/main/content/region/misthalin/varrock/dialogue/SinkethsDiary.kt +++ b/Server/src/main/content/region/misthalin/varrock/dialogue/SinkethsDiary.kt @@ -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 } } diff --git a/Server/src/main/content/region/misthalin/varrock/dialogue/surok/AbyssalBook.kt b/Server/src/main/content/region/misthalin/varrock/dialogue/surok/AbyssalBook.kt index 677934f19..949786650 100644 --- a/Server/src/main/content/region/misthalin/varrock/dialogue/surok/AbyssalBook.kt +++ b/Server/src/main/content/region/misthalin/varrock/dialogue/surok/AbyssalBook.kt @@ -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 } } diff --git a/Server/src/main/content/region/misthalin/varrock/quest/shieldofarrav/ShieldofArravBook.kt b/Server/src/main/content/region/misthalin/varrock/quest/shieldofarrav/ShieldofArravBook.kt index 10ec52e4e..867aa08d3 100644 --- a/Server/src/main/content/region/misthalin/varrock/quest/shieldofarrav/ShieldofArravBook.kt +++ b/Server/src/main/content/region/misthalin/varrock/quest/shieldofarrav/ShieldofArravBook.kt @@ -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 } } diff --git a/Server/src/main/core/api/ContentAPI.kt b/Server/src/main/core/api/ContentAPI.kt index 2be6916cd..71eb72f91 100644 --- a/Server/src/main/core/api/ContentAPI.kt +++ b/Server/src/main/core/api/ContentAPI.kt @@ -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, 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, 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(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) } diff --git a/Server/src/main/core/api/utils/Permadeath.kt b/Server/src/main/core/api/utils/Permadeath.kt index a49d575cf..478c9b90a 100644 --- a/Server/src/main/core/api/utils/Permadeath.kt +++ b/Server/src/main/core/api/utils/Permadeath.kt @@ -81,6 +81,7 @@ fun permadeath(target: Player) { // House data target.houseManager.createNewHouseAt(HouseLocation.NOWHERE) + target.getPOHStorageState().clear() // Achievements for (type in DiaryType.values()) { diff --git a/Server/src/main/core/game/dialogue/DialogueInterpreter.java b/Server/src/main/core/game/dialogue/DialogueInterpreter.java index a64838e33..85a0e94a8 100644 --- a/Server/src/main/core/game/dialogue/DialogueInterpreter.java +++ b/Server/src/main/core/game/dialogue/DialogueInterpreter.java @@ -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. diff --git a/Server/src/main/core/game/dialogue/DialogueLabeller.kt b/Server/src/main/core/game/dialogue/DialogueLabeller.kt index 8259474a0..1d39ce22b 100644 --- a/Server/src/main/core/game/dialogue/DialogueLabeller.kt +++ b/Server/src/main/core/game/dialogue/DialogueLabeller.kt @@ -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 = 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. */ diff --git a/Server/src/main/core/game/global/Skillcape.java b/Server/src/main/core/game/global/Skillcape.java index 619b87f04..4ae5ef8a5 100644 --- a/Server/src/main/core/game/global/Skillcape.java +++ b/Server/src/main/core/game/global/Skillcape.java @@ -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 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()); + } } /** diff --git a/Server/src/main/core/game/node/entity/npc/NPCBehavior.kt b/Server/src/main/core/game/node/entity/npc/NPCBehavior.kt index 55f9b99e6..e4fa6676b 100644 --- a/Server/src/main/core/game/node/entity/npc/NPCBehavior.kt +++ b/Server/src/main/core/game/node/entity/npc/NPCBehavior.kt @@ -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")) } /** diff --git a/Server/src/main/core/game/node/entity/player/Player.java b/Server/src/main/core/game/node/entity/player/Player.java index 7d6b15038..8d399c864 100644 --- a/Server/src/main/core/game/node/entity/player/Player.java +++ b/Server/src/main/core/game/node/entity/player/Player.java @@ -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; diff --git a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt index 028e8121d..74d82e646 100644 --- a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt +++ b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt @@ -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 diff --git a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt index 470818396..351c8dc34 100644 --- a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt +++ b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt @@ -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) } diff --git a/Server/src/main/core/game/system/command/sets/ConfigCommandSet.kt b/Server/src/main/core/game/system/command/sets/ConfigCommandSet.kt index 25398ebfa..825dd326d 100644 --- a/Server/src/main/core/game/system/command/sets/ConfigCommandSet.kt +++ b/Server/src/main/core/game/system/command/sets/ConfigCommandSet.kt @@ -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 <(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 <(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) + } + } } } diff --git a/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt b/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt index 5c91ef09f..fb8a8d50e 100644 --- a/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt +++ b/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt @@ -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() if (args.size == 2) { diff --git a/dumps/530/530_interface_names.txt b/dumps/530/530_interface_names.txt index 8013a2693..77cbeac07 100644 --- a/dumps/530/530_interface_names.txt +++ b/dumps/530/530_interface_names.txt @@ -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 \ No newline at end of file