diff --git a/Server/data/ObjectParser.xml b/Server/data/ObjectParser.xml
index b10a0d445..bfd1c5167 100644
--- a/Server/data/ObjectParser.xml
+++ b/Server/data/ObjectParser.xml
@@ -11,7 +11,6 @@
-
diff --git a/Server/data/configs/door_configs.json b/Server/data/configs/door_configs.json
index 4dd902862..f342b1856 100644
--- a/Server/data/configs/door_configs.json
+++ b/Server/data/configs/door_configs.json
@@ -343,7 +343,7 @@
},
{
"id": "2054",
- "replaceId": "24375",
+ "replaceId": "2055",
"fence": "false",
"metal": "false"
},
@@ -419,18 +419,6 @@
"fence": "false",
"metal": "false"
},
- {
- "id": "2391",
- "replaceId": "1561",
- "fence": "false",
- "metal": "true"
- },
- {
- "id": "2392",
- "replaceId": "1561",
- "fence": "false",
- "metal": "true"
- },
{
"id": "2397",
"replaceId": "24383",
@@ -2169,7 +2157,7 @@
},
{
"id": "24376",
- "replaceId": "24377",
+ "replaceId": "24375",
"fence": "false",
"metal": "false"
},
diff --git a/Server/data/configs/ground_spawns.json b/Server/data/configs/ground_spawns.json
index 4be6a19b0..727fe50bc 100644
--- a/Server/data/configs/ground_spawns.json
+++ b/Server/data/configs/ground_spawns.json
@@ -141,7 +141,7 @@
},
{
"item_id": "401",
- "loc_data": "{1,2959,2959,0,160}-{1,2943,2950,0,160}-{1,2866,2976,0,160}-{1,2854,2977,0,160}-{1,2839,2975,0,160}-{1,2757,2947,0,160}-{1,2755,2952,0,160}-{1,2786,2960,0,160}-{1,2782,2987,0,160}-{1,2771,2995,0,160}-{1,2761,3000,0,160}-{1,3829,3053,0,160}-{1,3813,3066,0,160}-{1,3814,3064,0,160}-{1,3808,3060,0,160}-{1,2945,3068,0,160}-{1,2975,3013,0,160}-{1,2955,3010,0,160}-{1,2755,3008,0,160}-{1,2754,3017,0,160}-{1,2756,3060,0,160}-{1,2755,3070,0,160}-{1,2896,3119,0,160}-{1,2914,3111,0,160}-{1,2926,3110,0,160}-{1,2939,3102,0,160}-{1,2938,3073,0,160}-{1,2756,3074,0,160}-{1,2806,3128,0,160}-{1,2764,3131,0,160}-{1,2753,3125,0,160}-{1,2757,3109,0,160}-{1,2864,3195,0,160}-{1,2810,3387,0,160}-{1,2809,3388,0,160}-{1,2809,3385,0,160}-{1,2807,3386,0,160}-{1,2804,3385,0,160}-{1,2807,3384,0,160}-{1,2805,3384,0,160}-{1,2802,3382,0,160}-{1,2805,3381,0,160}-{1,2804,3380,0,160}-{1,2693,3727,0,160}-{1,2698,3729,0,160}-{1,2700,3731,0,160}-{1,2708,3728,0,160}-{1,2725,3731,0,160}-{1,2721,3730,0,160}-{1,2719,3733,0,160}-{1,2714,3733,0,160}-{1,2712,3732,0,160}-"
+ "loc_data": "{1,2959,2959,0,160}-{1,2866,2976,0,160}-{1,2854,2977,0,160}-{1,2839,2975,0,160}-{1,2943,2950,0,160}-{1,2757,2947,0,160}-{1,2755,2952,0,160}-{1,2786,2960,0,160}-{1,2782,2987,0,160}-{1,2771,2995,0,160}-{1,2761,3000,0,160}-{1,2945,3068,0,160}-{1,2975,3013,0,160}-{1,2955,3010,0,160}-{1,3829,3053,0,160}-{1,3813,3066,0,160}-{1,3814,3064,0,160}-{1,3808,3060,0,160}-{1,2755,3008,0,160}-{1,2754,3017,0,160}-{1,2756,3060,0,160}-{1,2755,3070,0,160}-{1,2896,3119,0,160}-{1,2914,3111,0,160}-{1,2926,3110,0,160}-{1,2939,3102,0,160}-{1,2938,3073,0,160}-{1,2756,3074,0,160}-{1,2806,3128,0,160}-{1,2764,3131,0,160}-{1,2753,3125,0,160}-{1,2757,3109,0,160}-{1,2864,3195,0,160}-{1,2810,3387,0,160}-{1,2809,3388,0,160}-{1,2809,3385,0,160}-{1,2807,3386,0,160}-{1,2804,3385,0,160}-{1,2807,3384,0,160}-{1,2805,3384,0,160}-{1,2802,3382,0,160}-{1,2805,3381,0,160}-{1,2804,3380,0,160}-{1,2693,3727,0,160}-{1,2698,3729,0,160}-{1,2700,3731,0,160}-{1,2708,3728,0,160}-{1,2725,3731,0,160}-{1,2721,3730,0,160}-{1,2719,3733,0,160}-{1,2714,3733,0,160}-{1,2712,3732,0,160}-"
},
{
"item_id": "444",
@@ -369,7 +369,7 @@
},
{
"item_id": "1573",
- "loc_data": "{1,2559,2975,0,10}-{1,2560,2974,0,10}-{1,2561,2973,0,10}-{1,2561,2976,0,10}-{1,2740,3637,0,30}-{1,2736,3638,0,30}-{1,2735,3636,0,30}-{1,2743,3636,0,30}-{1,2739,3634,0,30}-{1,2743,3640,0,30}-{1,2741,3639,0,30}-{1,2736,3641,0,30}-{1,2738,3641,0,30}-{1,2734,3640,0,30}-{1,2738,3636,0,30}-"
+ "loc_data": "{1,2560,2974,0,10}-{1,2561,2973,0,10}-{1,2561,2976,0,10}-{1,2559,2975,0,10}-{1,2740,3637,0,30}-{1,2736,3638,0,30}-{1,2735,3636,0,30}-{1,2743,3636,0,30}-{1,2739,3634,0,30}-{1,2743,3640,0,30}-{1,2741,3639,0,30}-{1,2736,3641,0,30}-{1,2738,3641,0,30}-{1,2734,3640,0,30}-{1,2738,3636,0,30}-"
},
{
"item_id": "1590",
@@ -469,7 +469,7 @@
},
{
"item_id": "1931",
- "loc_data": "{1,3209,3214,0,80}-{1,3166,3310,0,90}-{1,3144,3449,2,90}-"
+ "loc_data": "{1,3209,3214,0,80}-{1,3166,3310,0,90}-{1,3144,3449,2,90}-{1,2918,10191,1,90}-"
},
{
"item_id": "1935",
diff --git a/Server/data/configs/item_configs.json b/Server/data/configs/item_configs.json
index 07c04813b..64b2c1e89 100644
--- a/Server/data/configs/item_configs.json
+++ b/Server/data/configs/item_configs.json
@@ -17702,7 +17702,7 @@
{
"shop_price": "350",
"ge_buy_limit": "5000",
- "examine": "A plain gold amulet.",
+ "examine": "It needs a string so I can wear it.",
"grand_exchange_price": "172",
"durability": null,
"name": "Gold amulet",
@@ -17722,7 +17722,7 @@
{
"shop_price": "810",
"ge_buy_limit": "5000",
- "examine": "I wonder if I can get this enchanted.",
+ "examine": "It needs a string so I can wear it.",
"grand_exchange_price": "473",
"durability": null,
"name": "Sapphire amulet",
@@ -17742,7 +17742,7 @@
{
"shop_price": "1147",
"ge_buy_limit": "5000",
- "examine": "I wonder if I can get this enchanted.",
+ "examine": "It needs a string so I can wear it.",
"grand_exchange_price": "695",
"durability": null,
"name": "Emerald amulet",
@@ -17762,7 +17762,7 @@
{
"shop_price": "2025",
"ge_buy_limit": "5000",
- "examine": "I wonder if I can get this enchanted.",
+ "examine": "It needs a string so I can wear it.",
"grand_exchange_price": "1268",
"durability": null,
"name": "Ruby amulet",
@@ -17783,7 +17783,7 @@
{
"shop_price": "3307",
"ge_buy_limit": "5000",
- "examine": "I wonder if I can get this enchanted.",
+ "examine": "It needs a string so I can wear it.",
"grand_exchange_price": "3359",
"durability": null,
"name": "Diamond amulet",
@@ -17802,7 +17802,7 @@
},
{
"ge_buy_limit": "100",
- "examine": "I wonder if I can get this enchanted.",
+ "examine": "It needs a string so I can wear it.",
"grand_exchange_price": "26900",
"durability": null,
"name": "Dragonstone ammy",
@@ -48457,14 +48457,14 @@
},
{
"shop_price": "200",
- "examine": "An acorn has been sown and watered in this plant pot.",
+ "examine": "An acorn has been sown in this plant pot.",
"durability": null,
"name": "Oak seedling",
"archery_ticket_price": "0",
"id": "5358"
},
{
- "examine": "A willow tree seed has been sown and watered in this plant pot",
+ "examine": "A willow tree seed has been sown in this plant pot.",
"durability": null,
"name": "Willow seedling",
"weight": "0.9",
@@ -48472,7 +48472,7 @@
"id": "5359"
},
{
- "examine": "A maple tree has been sown (and watered) in this plant pot.",
+ "examine": "A maple tree seed has been sown in this plant pot.",
"durability": null,
"name": "Maple seedling",
"weight": "0.9",
@@ -48480,7 +48480,7 @@
"id": "5360"
},
{
- "examine": "A yew seed has been sown in the plant pot",
+ "examine": "A yew tree seed has been sown in this plant pot.",
"durability": null,
"name": "Yew seedling",
"weight": "1",
@@ -48488,7 +48488,7 @@
"id": "5361"
},
{
- "examine": "A magic tree seed has been sown and watered in this plant pot.",
+ "examine": "A magic tree seed has been sown in this plant pot.",
"durability": null,
"name": "Magic seedling",
"weight": "0.9",
@@ -48496,7 +48496,7 @@
"id": "5362"
},
{
- "examine": "A spirit seedling is growing in this pot. It has been sown and watered.",
+ "examine": "A spirit tree seed has been sown in this plant pot.",
"durability": null,
"name": "Spirit seedling",
"archery_ticket_price": "0",
@@ -48511,7 +48511,7 @@
"id": "5364"
},
{
- "examine": "A willow tree seed has been sown and watered in this plant pot",
+ "examine": "A willow tree seed has been sown and watered in this plant pot.",
"durability": null,
"name": "Willow seedling",
"weight": "0.9",
@@ -48519,7 +48519,7 @@
"id": "5365"
},
{
- "examine": "A maple tree has been sown (and watered) in this plant pot.",
+ "examine": "A maple tree seed has been sown and watered in this plant pot.",
"durability": null,
"name": "Maple seedling",
"weight": "0.9",
@@ -48527,7 +48527,7 @@
"id": "5366"
},
{
- "examine": "A yew seed has been sown in the plant pot",
+ "examine": "A yew tree seed has been sown and watered in this plant pot.",
"durability": null,
"name": "Yew seedling",
"weight": "1",
@@ -48543,7 +48543,7 @@
"id": "5368"
},
{
- "examine": "A spirit seedling is growing in this pot. It has been sown and watered.",
+ "examine": "A spirit tree seed has been sown and watered in this plant pot.",
"durability": null,
"name": "Spirit seedling",
"archery_ticket_price": "0",
@@ -49497,7 +49497,7 @@
"id": "5479"
},
{
- "examine": "An apple tree has been sown (and watered) in this plant pot.",
+ "examine": "An apple tree seed has been sown in this plant pot.",
"durability": null,
"name": "Apple seedling",
"weight": "0.9",
@@ -49505,7 +49505,7 @@
"id": "5480"
},
{
- "examine": "A banana tree seed has been sown and watered in this plant pot.",
+ "examine": "A banana tree seed has been sown in this plant pot.",
"durability": null,
"name": "Banana seedling",
"weight": "0.9",
@@ -49513,14 +49513,14 @@
"id": "5481"
},
{
- "examine": "An orange tree seed has been sown (and watered)in this plant pot.",
+ "examine": "An orange tree seed has been sown in this plant pot.",
"durability": null,
"name": "Orange seedling",
"archery_ticket_price": "0",
"id": "5482"
},
{
- "examine": "A curry seed has been sown and watered in this plant pot.",
+ "examine": "A curry tree seed has been sown in this plant pot.",
"durability": null,
"name": "Curry seedling",
"weight": "0.9",
@@ -49528,7 +49528,7 @@
"id": "5483"
},
{
- "examine": "A pineapple tree has been sown (and watered) in this plant pot.",
+ "examine": "A pineapple plant seed has been sown in this plant pot.",
"durability": null,
"name": "Pineapple seedling",
"weight": "0.9",
@@ -49536,7 +49536,7 @@
"id": "5484"
},
{
- "examine": "This needs watering before it will grow.",
+ "examine": "A papaya tree seed has been sown in this plant pot.",
"durability": null,
"name": "Papaya seedling",
"weight": "0.9",
@@ -49544,7 +49544,7 @@
"id": "5485"
},
{
- "examine": "This sapling is ready to be replanted in a fruit tree patch.",
+ "examine": "A palm tree seed has been sown in this plant pot.",
"durability": null,
"name": "Palm seedling",
"weight": "0.66",
@@ -49552,7 +49552,7 @@
"id": "5486"
},
{
- "examine": "A calquat tree has been sown (and watered) in this plant pot.",
+ "examine": "A calquat tree seed has been sown in this plant pot.",
"durability": null,
"name": "Calquat seedling",
"weight": "0.9",
@@ -49560,7 +49560,7 @@
"id": "5487"
},
{
- "examine": "An apple tree has been sown (and watered) in this plant pot.",
+ "examine": "An apple tree seed has been sown and watered in this plant pot.",
"durability": null,
"name": "Apple seedling",
"weight": "0.9",
@@ -49576,14 +49576,14 @@
"id": "5489"
},
{
- "examine": "An orange tree seed has been sown (and watered)in this plant pot.",
+ "examine": "An orange tree seed has been sown and watered in this plant pot.",
"durability": null,
"name": "Orange seedling",
"archery_ticket_price": "0",
"id": "5490"
},
{
- "examine": "A curry seed has been sown and watered in this plant pot.",
+ "examine": "A curry tree seed has been sown and watered in this plant pot.",
"durability": null,
"name": "Curry seedling",
"weight": "0.9",
@@ -49591,7 +49591,7 @@
"id": "5491"
},
{
- "examine": "A pineapple tree has been sown (and watered) in this plant pot.",
+ "examine": "A pineapple plant seed has been sown and watered in this plant pot.",
"durability": null,
"name": "Pineapple seedling",
"weight": "0.9",
@@ -49599,7 +49599,7 @@
"id": "5492"
},
{
- "examine": "This needs watering before it will grow.",
+ "examine": "A papaya tree seed has been sown and watered in this plant pot.",
"durability": null,
"name": "Papaya seedling",
"weight": "0.9",
@@ -49607,7 +49607,7 @@
"id": "5493"
},
{
- "examine": "This sapling is ready to be replanted in a fruit tree patch.",
+ "examine": "A palm tree seed has been sown and watered in this plant pot.",
"durability": null,
"name": "Palm seedling",
"weight": "0.66",
@@ -49615,7 +49615,7 @@
"id": "5494"
},
{
- "examine": "A calquat tree has been sown (and watered) in this plant pot.",
+ "examine": "A calquat tree seed has been sown and watered in this plant pot.",
"durability": null,
"name": "Calquat seedling",
"weight": "0.9",
@@ -51571,9 +51571,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "483",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "Iron dagger(p+)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -51600,9 +51600,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "350",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "Bronze dagger(p+)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -51630,9 +51630,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "575",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "Steel dagger(p+)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -51660,9 +51660,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "538",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "Mithril dagger(p+)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -51686,9 +51686,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "800",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "Adamant dagger(p+)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -51783,9 +51783,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "664",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "Black dagger(p+)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -51831,9 +51831,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "4128",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "Iron dagger(p++)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -51860,9 +51860,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "5088",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "Br'ze dagger(p++)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -51890,9 +51890,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "4729",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "Steel dagger(p++)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -51920,9 +51920,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "3476",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "Mithril dagger(p++)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -51946,9 +51946,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "4612",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "Adamant dagger(p++)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -51976,10 +51976,10 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"lendable": "true",
"grand_exchange_price": "5061",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "Rune dagger(p++)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -52043,9 +52043,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "4921",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "Black dagger(p++)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -52751,7 +52751,7 @@
},
{
"ge_buy_limit": "100",
- "examine": "This looks a good deal stronger than normal dwarven stout.",
+ "examine": "This looks a good deal stronger than normal Dwarven Stout.",
"grand_exchange_price": "56400",
"durability": null,
"name": "Dwarven stout(m)",
@@ -53779,7 +53779,7 @@
},
{
"ge_buy_limit": "100",
- "examine": "This looks a good deal stronger than normal dwarven stout.",
+ "examine": "This looks a good deal stronger than normal Dwarven Stout.",
"grand_exchange_price": "44600",
"durability": null,
"name": "Dwarven stout(m1)",
@@ -53798,7 +53798,7 @@
},
{
"ge_buy_limit": "100",
- "examine": "This looks a good deal stronger than normal dwarven stout.",
+ "examine": "This looks a good deal stronger than normal Dwarven Stout.",
"grand_exchange_price": "68900",
"durability": null,
"name": "Dwarven stout(m2)",
@@ -53817,7 +53817,7 @@
},
{
"ge_buy_limit": "100",
- "examine": "This looks a good deal stronger than normal dwarven stout.",
+ "examine": "This looks a good deal stronger than normal Dwarven Stout.",
"grand_exchange_price": "98800",
"durability": null,
"name": "Dwarven stout(m3)",
@@ -53836,7 +53836,7 @@
},
{
"ge_buy_limit": "100",
- "examine": "This looks a good deal stronger than normal dwarven stout.",
+ "examine": "This looks a good deal stronger than normal Dwarven Stout.",
"grand_exchange_price": "237400",
"durability": null,
"name": "Dwarven stout(m4)",
@@ -60136,7 +60136,7 @@
"id": "6578"
},
{
- "examine": "I wonder if I can get this enchanted.",
+ "examine": "It needs a string so I can wear it.",
"grand_exchange_price": "9058124",
"durability": null,
"name": "Onyx amulet",
@@ -60318,9 +60318,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "497",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "White dagger(p)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -60348,9 +60348,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "982",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "White dagger(p+)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -60378,9 +60378,9 @@
"equip_audio": "2248",
"render_anim": "2584",
"equipment_slot": "3",
+ "attack_anims": "400,400,401,400",
"grand_exchange_price": "8422",
"attack_audios": "2517,2517,2500,2517",
- "attack_anims": "400,400,401,400",
"name": "White dagger(p++)",
"tradeable": "true",
"archery_ticket_price": "0",
@@ -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 73413dccb..e2292904b 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.",
@@ -44609,7 +44611,9 @@
"strength_level": "1",
"id": "5079",
"range_level": "1",
- "attack_level": "1"
+ "attack_level": "1",
+ "death_animation": "5183",
+ "examine": "Handle with care."
},
{
"examine": "It looks fluffy and cute; it's probably deadly.",
@@ -59158,7 +59162,7 @@
"magic_animation": "7515",
"death_animation": "7461",
"name": "Revenant hellhound",
- "defence_level": "1",
+ "defence_level": "50",
"safespot": null,
"lifepoints": "80",
"strength_level": "50",
@@ -60004,7 +60008,7 @@
"magic_animation": "7515",
"death_animation": "7461",
"name": "Revenant hellhound",
- "defence_level": "1",
+ "defence_level": "50",
"safespot": null,
"lifepoints": "80",
"strength_level": "50",
@@ -74213,6 +74217,7 @@
"id": "880"
},
{
+ "examine": "The Carnillean family chef.",
"name": "Claus the chef",
"id": "886"
},
@@ -74221,14 +74226,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"
},
@@ -76127,6 +76135,7 @@
"id": "2308"
},
{
+ "examine": "In his former life an expert brewer.",
"name": "Metarialus",
"id": "2322"
},
@@ -77684,6 +77693,7 @@
"id": "3311"
},
{
+ "examine": "They're going to sing to you!",
"name": "Zanaris choir",
"id": "3312"
},
@@ -83520,6 +83530,7 @@
"id": "6741"
},
{
+ "examine": "A traditional snowman.",
"name": "Snowman",
"id": "6746"
},
@@ -87757,6 +87768,7 @@
"id": "8516"
},
{
+ "examine": "The rebellious teenage son of the Queen of Snow. Interesting outfit!",
"name": "Jack Frost",
"id": "8517"
},
@@ -87789,6 +87801,7 @@
"id": "8539"
},
{
+ "examine": "A jolly-looking present-distributor.",
"name": "Santa Claus",
"id": "8540"
},
@@ -88188,5 +88201,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 8e1f93407..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",
@@ -5547,6 +5563,10 @@
"npc_id": "2321",
"loc_data": "{2915,10194,1,1,0}-"
},
+ {
+ "npc_id": "2322",
+ "loc_data": "{3679,9957,0,1,1}-"
+ },
{
"npc_id": "2323",
"loc_data": "{3056,3306,0,1,1}-"
diff --git a/Server/data/configs/object_configs.json b/Server/data/configs/object_configs.json
index ae364fd37..1dffc0568 100644
--- a/Server/data/configs/object_configs.json
+++ b/Server/data/configs/object_configs.json
@@ -19168,87 +19168,87 @@
"ids": "13614"
},
{
- "examine": "A gateway to Varrock",
+ "examine": "A gateway to Varrock.",
"ids": "13615"
},
{
- "examine": "A gateway to Lumbridge",
+ "examine": "A gateway to Lumbridge.",
"ids": "13616"
},
{
- "examine": "A gateway to Falador",
+ "examine": "A gateway to Falador.",
"ids": "13617"
},
{
- "examine": "A gateway to Camelot",
+ "examine": "A gateway to Camelot.",
"ids": "13618"
},
{
- "examine": "A gateway to Ardougne",
+ "examine": "A gateway to Ardougne.",
"ids": "13619"
},
{
- "examine": "A gateway to the Yanille watchtower",
+ "examine": "A gateway to the Yanille watchtower.",
"ids": "13620"
},
{
- "examine": "A gateway to Trollheim",
+ "examine": "A gateway to Kharyrll.",
"ids": "13621"
},
{
- "examine": "A gateway to Varrock",
+ "examine": "A gateway to Varrock.",
"ids": "13622"
},
{
- "examine": "A gateway to Lumbridge",
+ "examine": "A gateway to Lumbridge.",
"ids": "13623"
},
{
- "examine": "A gateway to Falador",
+ "examine": "A gateway to Falador.",
"ids": "13624"
},
{
- "examine": "A gateway to Camelot",
+ "examine": "A gateway to Camelot.",
"ids": "13625"
},
{
- "examine": "A gateway to Ardougne",
+ "examine": "A gateway to Ardougne.",
"ids": "13626"
},
{
- "examine": "A gateway to the Yanille watchtower",
+ "examine": "A gateway to the Yanille watchtower.",
"ids": "13627"
},
{
- "examine": "A gateway to Trollheim",
+ "examine": "A gateway to Kharyrll.",
"ids": "13628"
},
{
- "examine": "A gateway to Varrock",
+ "examine": "A gateway to Varrock.",
"ids": "13629"
},
{
- "examine": "A gateway to Lumbridge",
+ "examine": "A gateway to Lumbridge.",
"ids": "13630"
},
{
- "examine": "A gateway to Falador",
+ "examine": "A gateway to Falador.",
"ids": "13631"
},
{
- "examine": "A gateway to Camelot",
+ "examine": "A gateway to Camelot.",
"ids": "13632"
},
{
- "examine": "A gateway to Ardougne",
+ "examine": "A gateway to Ardougne.",
"ids": "13633"
},
{
- "examine": "A gateway to the Yanille watchtower",
+ "examine": "A gateway to the Yanille watchtower.",
"ids": "13634"
},
{
- "examine": "A gateway to Trollheim",
+ "examine": "A gateway to Kharyrll.",
"ids": "13635"
},
{
diff --git a/Server/src/main/content/data/EnchantedJewellery.kt b/Server/src/main/content/data/EnchantedJewellery.kt
index cb94545e5..a0c53ad00 100644
--- a/Server/src/main/content/data/EnchantedJewellery.kt
+++ b/Server/src/main/content/data/EnchantedJewellery.kt
@@ -11,11 +11,13 @@ import core.game.node.entity.player.Player
import core.game.node.entity.player.link.TeleportManager
import core.game.node.item.Item
import core.game.system.task.Pulse
+import core.game.world.GameWorld
import core.game.world.map.Location
import core.game.world.update.flag.context.Animation
import core.game.world.update.flag.context.Graphics
import org.rs09.consts.Items
import core.game.world.GameWorld.Pulser
+import core.tools.Log
import org.rs09.consts.Sounds
import java.util.*
@@ -73,17 +75,17 @@ enum class EnchantedJewellery(
),
AMULET_OF_GLORY(
arrayOf(
- "Edgeville.",
- "Karamja.",
- "Draynor Village.",
- "Al-Kharid.",
- "Nowhere."
+ "Edgeville",
+ "Karamja",
+ "Draynor Village",
+ "Al Kharid",
+ "Nowhere"
),
arrayOf(
Location.create(3087, 3495, 0),
Location.create(2919, 3175, 0),
Location.create(3104, 3249, 0),
- Location.create(3304, 3124, 0)
+ Location.create(3292, 3175, 0)
),
Items.AMULET_OF_GLORY4_1712,
Items.AMULET_OF_GLORY3_1710,
@@ -209,7 +211,7 @@ enum class EnchantedJewellery(
constructor(options: Array, locations: Array, vararg ids: Int) : this(options, locations, false, *ids)
/**
- * Method used when the player "Use"s the jewellery piece.
+ * Method used when the player clicks one of the options in the jewellery piece's options dialog.
* @param player the player.
* @param item the used jewellery item.
* @param buttonID the button id.
@@ -222,7 +224,7 @@ enum class EnchantedJewellery(
}
return
}
- attemptTeleport(player, item, buttonID, isEquipped)
+ attemptTeleport(player, item, buttonID, true, isEquipped)
}
/**
@@ -230,43 +232,69 @@ enum class EnchantedJewellery(
* @param player the player.
* @param item the used jewellery item.
* @param buttonID the button id.
+ * @param replace Whether to attempt replacing the item with its lower-charge version. Always true unless you generated a fake item in code.
* @param isEquipped If the player is operating.
*/
- fun attemptTeleport(player: Player, item: Item, buttonID: Int, isEquipped: Boolean): Boolean {
+ fun attemptTeleport(player: Player, item: Item, buttonID: Int, replace: Boolean, isEquipped: Boolean): Boolean {
val itemIndex = getItemIndex(item)
val nextJewellery = Item(getNext(itemIndex))
if (!canTeleport(player, nextJewellery)) {
return false
}
- Pulser.submit(object : Pulse(0) {
- private var count = 0
- private var location = getLocation(buttonID)
- override fun pulse(): Boolean {
- when (count) {
- 0 -> {
- lock(player,4)
- visualize(player, ANIMATION, GRAPHICS)
- playGlobalAudio(player.location, Sounds.TELEPORT_ALL_200)
- player.impactHandler.disabledTicks = 4
- closeInterface(player)
+ val location = getLocation(buttonID)
+ player.scripts.delay = GameWorld.ticks + 4
+ queueScript(player, 0, QueueStrength.SOFT) { stage ->
+ when (stage) {
+ 0 -> {
+ lock(player, 4)
+ visualize(player, ANIMATION, GRAPHICS)
+ playGlobalAudio(player.location, Sounds.TELEPORT_ALL_200)
+ player.impactHandler.disabledTicks = 4
+ closeInterface(player)
+ return@queueScript delayScript(player, 3)
+ }
+ 1 -> {
+ teleport(player, location)
+ resetAnimator(player)
+ unlock(player)
+ player.dispatch(TeleportEvent(TeleportManager.TeleportType.NORMAL, TeleportMethod.JEWELRY, item, location))
+ if (!replace) {
+ return@queueScript stopExecuting(player)
}
- 3 -> {
- teleport(player,location)
- resetAnimator(player)
- if (isLastItemIndex(itemIndex)) {
- if (isCrumble) crumbleJewellery(player, item, isEquipped)
- } else {
- replaceJewellery(player, item, nextJewellery, isEquipped)
+ if (isLastItemIndex(itemIndex)) {
+ if (isSlayerRing(item)) {
+ if (isEquipped) {
+ // Remove it now, but only addItemOrDrop the gem after the teleport has procced
+ if (removeItem(player, item, Container.EQUIPMENT)) {
+ return@queueScript delayScript(player, 1)
+ }
+ log(this.javaClass, Log.ERR, "Error replacing slayer ring with enchanted gem (removeItem from equipment)")
+ }
+ // Not equipped -> can replace fluently
+ if (replaceSlot(player, item.slot, Item(Items.ENCHANTED_GEM_4155), item) == item) {
+ return@queueScript stopExecuting(player)
+ }
+ log(this.javaClass, Log.ERR, "Error replacing slayer ring with enchanted gem (replaceSlot)")
}
- unlock(player)
- player.dispatch(TeleportEvent(TeleportManager.TeleportType.NORMAL, TeleportMethod.JEWELRY, item, location))
- return true
+ // Not slayer ring
+ if (isCrumble) {
+ val removeFrom = if (isEquipped) Container.EQUIPMENT else Container.INVENTORY
+ if (removeItem(player, item, removeFrom)) {
+ return@queueScript stopExecuting(player)
+ }
+ log(this.javaClass, Log.ERR, "Error crumbling jewelry")
+ }
+ } else {
+ replaceJewellery(player, item, nextJewellery, isEquipped)
}
}
- count += 1
- return false
+ 2 -> {
+ addItemOrDrop(player, Items.ENCHANTED_GEM_4155)
+ sendMessage(player, "Your Ring of Slaying reverts back into a regular enchanted gem.")
+ }
}
- })
+ return@queueScript stopExecuting(player)
+ }
return true
}
@@ -278,21 +306,6 @@ enum class EnchantedJewellery(
}
}
- private fun crumbleJewellery(player: Player, item: Item, isEquipped: Boolean) {
- if (isEquipped) {
- removeItem(player, item, Container.EQUIPMENT)
- } else {
- removeItem(player, item)
- }
- if (isSlayerRing(item)) {
- queueScript(player, 1, QueueStrength.SOFT) {
- addItemOrDrop(player, Items.ENCHANTED_GEM_4155)
- sendMessage(player, "Your Ring of Slaying reverts back into a regular enchanted gem.")
- return@queueScript stopExecuting(player)
- }
- }
- }
-
private fun isSlayerRing(item: Item): Boolean {
return (item.id in RING_OF_SLAYING.ids)
}
diff --git a/Server/src/main/content/data/consumables/Consumables.java b/Server/src/main/content/data/consumables/Consumables.java
index e16b6aa23..2d3744679 100644
--- a/Server/src/main/content/data/consumables/Consumables.java
+++ b/Server/src/main/content/data/consumables/Consumables.java
@@ -203,8 +203,8 @@ public enum Consumables {
ASGARNIAN_ALE(new Drink(new int[] {1905, 1919}, new MultiEffect(new HealingEffect(2), new SkillEffect(Skills.STRENGTH, 2, 0), new SkillEffect(Skills.ATTACK, -4, 0)), "You drink the ale. You feel slightly reinvigorated...", "...and slightly dizzy too.")),
ASGARNIAN_ALE_POH(new Drink(new int[] {7744, 7742}, new MultiEffect(new HealingEffect(2), new SkillEffect(Skills.STRENGTH, 2, 0), new SkillEffect(Skills.ATTACK, -4, 0)), "You drink the ale. You feel slightly reinvigorated...", "...and slightly dizzy too.")),
ASGARNIAN_ALE_KEG(new Drink(new int[] {5785, 5783, 5781, 5779, 5769}, new MultiEffect(new SkillEffect(Skills.STRENGTH, 2, 0), new SkillEffect(Skills.ATTACK, -4, 0)), new Animation(2289), "You drink the ale. You feel slightly reinvigorated...", "...and slightly dizzy too.")),
- ASGARNIAN_ALE_M(new Drink(new int[] {5739, 1919}, new MultiEffect(new SkillEffect(Skills.STRENGTH, 3, 0), new SkillEffect(Skills.ATTACK, -6, 0)))),
- ASGARNIAN_ALE_M_KEG(new Drink(new int[] {5865, 5863, 5861, 5859, 5769}, new MultiEffect(new SkillEffect(Skills.STRENGTH, 3, 0), new SkillEffect(Skills.ATTACK, -6, 0)), new Animation(2289))),
+ ASGARNIAN_ALE_M(new Drink(new int[] {5739, 1919}, new MultiEffect(new SkillEffect(Skills.STRENGTH, 3, 0), new SkillEffect(Skills.ATTACK, -6, 0)), "You drink the ale. You feel slightly reinvigorated...", "...and slightly dizzy too.")),
+ ASGARNIAN_ALE_M_KEG(new Drink(new int[] {5865, 5863, 5861, 5859, 5769}, new MultiEffect(new SkillEffect(Skills.STRENGTH, 3, 0), new SkillEffect(Skills.ATTACK, -6, 0)), new Animation(2289), "You drink the ale. You feel slightly reinvigorated...", "...and slightly dizzy too.")),
AXEMANS_FOLLY(new Drink(new int[] {5751, 1919}, new MultiEffect(new SkillEffect(Skills.WOODCUTTING, 1, 0), new HealingEffect(1), new SkillEffect(Skills.STRENGTH, -3, 0), new SkillEffect(Skills.ATTACK, -3, 0)))),
AXEMANS_FOLLY_KEG(new Drink(new int[] {5825, 5823, 5821, 5819, 5769}, new MultiEffect(new SkillEffect(Skills.WOODCUTTING, 1, 0), new HealingEffect(1), new SkillEffect(Skills.STRENGTH, -3, 0), new SkillEffect(Skills.ATTACK, -3, 0)), new Animation(2289))),
AXEMANS_FOLLY_M(new Drink(new int[] {5753, 1919}, new MultiEffect(new SkillEffect(Skills.WOODCUTTING, 2, 0), new HealingEffect(2), new SkillEffect(Skills.STRENGTH, -4, 0), new SkillEffect(Skills.ATTACK, -4, 0)))),
@@ -231,24 +231,26 @@ public enum Consumables {
DRAGON_BITTER_M_KEG(new Drink(new int[] {5889, 5887, 5885, 5883, 5769}, new MultiEffect(new HealingEffect(2), new SkillEffect(Skills.STRENGTH, 3, 0), new SkillEffect(Skills.ATTACK, -6, 0)), new Animation(2289))),
DWARVEN_STOUT(new Drink(new int[] {1913, 1919}, new MultiEffect(new SkillEffect(Skills.MINING, 1, 0), new SkillEffect(Skills.SMITHING, 1 ,0), new SkillEffect(Skills.ATTACK, -2, 0), new SkillEffect(Skills.STRENGTH, -2, 0), new SkillEffect(Skills.DEFENCE, -2, 0), new HealingEffect(1)), "You drink the Dwarven Stout. It tastes foul.", "It tastes pretty strong too.")),
DWARVEN_STOUT_KEG(new Drink(new int[] {5777, 5775, 5773, 5771, 5769}, new MultiEffect(new SkillEffect(Skills.MINING, 1, 0), new SkillEffect(Skills.SMITHING, 1 ,0), new SkillEffect(Skills.ATTACK, -2, 0), new SkillEffect(Skills.STRENGTH, -2, 0), new SkillEffect(Skills.DEFENCE, -2, 0), new HealingEffect(1)), new Animation(2289), "You drink the Dwarven Stout. It tastes foul.", "It tastes pretty strong too.")),
- DWARVEN_STOUT_M(new Drink(new int[] {5747, 1919}, new MultiEffect(new SkillEffect(Skills.MINING, 2, 0), new SkillEffect(Skills.SMITHING, 2 ,0), new SkillEffect(Skills.ATTACK, -7, 0), new SkillEffect(Skills.STRENGTH, -7, 0), new SkillEffect(Skills.DEFENCE, -7, 0), new HealingEffect(1)))),
- DWARVEN_STOUT_M_KEG(new Drink(new int[] {5857, 5855, 5853, 5851, 5769}, new MultiEffect(new SkillEffect(Skills.MINING, 2, 0), new SkillEffect(Skills.SMITHING, 2 ,0), new SkillEffect(Skills.ATTACK, -7, 0), new SkillEffect(Skills.STRENGTH, -7, 0), new SkillEffect(Skills.DEFENCE, -7, 0), new HealingEffect(1)), new Animation(2289))),
+ DWARVEN_STOUT_M(new Drink(new int[] {5747, 1919}, new MultiEffect(new SkillEffect(Skills.MINING, 2, 0), new SkillEffect(Skills.SMITHING, 2 ,0), new SkillEffect(Skills.ATTACK, -7, 0), new SkillEffect(Skills.STRENGTH, -7, 0), new SkillEffect(Skills.DEFENCE, -7, 0), new HealingEffect(1)), "You drink the Dwarven Stout. It tastes foul.", "It tastes pretty strong too.")),
+ DWARVEN_STOUT_M_KEG(new Drink(new int[] {5857, 5855, 5853, 5851, 5769}, new MultiEffect(new SkillEffect(Skills.MINING, 2, 0), new SkillEffect(Skills.SMITHING, 2 ,0), new SkillEffect(Skills.ATTACK, -7, 0), new SkillEffect(Skills.STRENGTH, -7, 0), new SkillEffect(Skills.DEFENCE, -7, 0), new HealingEffect(1)), new Animation(2289), "You drink the Dwarven Stout. It tastes foul.", "It tastes pretty strong too.")),
GREENMANS_ALE(new Drink(new int[] {1909, 1919}, new MultiEffect(new HealingEffect(1), new SkillEffect(Skills.HERBLORE, 1, 0), new SkillEffect(Skills.ATTACK, -3, 0), new SkillEffect(Skills.STRENGTH, -3, 0), new SkillEffect(Skills.DEFENCE, -3, 0)))),
GREENMANS_ALE_POH(new Drink(new int[] {7746, 7742}, new MultiEffect(new HealingEffect(1), new SkillEffect(Skills.HERBLORE, 1, 0), new SkillEffect(Skills.ATTACK, -3, 0), new SkillEffect(Skills.STRENGTH, -3, 0), new SkillEffect(Skills.DEFENCE, -3, 0)))),
GREENMANS_ALE_KEG(new Drink(new int[] {5793, 5791, 5789, 5787, 5769}, new MultiEffect(new HealingEffect(1), new SkillEffect(Skills.HERBLORE, 1, 0), new SkillEffect(Skills.ATTACK, -3, 0), new SkillEffect(Skills.STRENGTH, -3, 0), new SkillEffect(Skills.DEFENCE, -3, 0)), new Animation(2289))),
GREENMANS_ALE_M(new Drink(new int[] {5743, 1919}, new MultiEffect(new HealingEffect(1), new SkillEffect(Skills.HERBLORE, 2, 0), new SkillEffect(Skills.ATTACK, -2, 0), new SkillEffect(Skills.STRENGTH, -2, 0)))),
GREENMANS_ALE_M_KEG(new Drink(new int[] {5873, 5871, 5869, 5867, 5769}, new MultiEffect(new HealingEffect(1), new SkillEffect(Skills.HERBLORE, 2, 0), new SkillEffect(Skills.ATTACK, -2, 0), new SkillEffect(Skills.STRENGTH, -2, 0)), new Animation(2289))),
GROG(new Drink(new int[] {1915, 1919}, new MultiEffect(new SkillEffect(Skills.STRENGTH, 3, 0), new SkillEffect(Skills.ATTACK, -6, 0)))),
- MOONLIGHT_MEAD(new Drink(new int[] {2955, 1919}, new HealingEffect(4), "It tastes like something just died in your mouth.")),
- MOONLIGHT_MEAD_KEG(new Drink(new int[] {5817, 5815, 5813, 5811, 5769}, new HealingEffect(4), new Animation(2289), "It tastes like something just died in your mouth.")),
- MOONLIGHT_MEAD_M(new Drink(new int[] {5749, 1919}, new HealingEffect(6))),
- MOONLIGHT_MEAD_M_KEG(new Drink(new int[] {5897, 5895, 5893, 5891, 5769}, new HealingEffect(6), new Animation(2289))),
+ MOONLIGHT_MEAD(new Drink(new int[] {2955, 1919}, new HealingEffect(5), "It tastes like something just died in your mouth.")),
+ MOONLIGHT_MEAD_KEG(new Drink(new int[] {5817, 5815, 5813, 5811, 5769}, new HealingEffect(5), new Animation(2289), "It tastes like something just died in your mouth.")),
+ MOONLIGHT_MEAD_M(new Drink(new int[] {5749, 1919}, new HealingEffect(4), "It tastes like something just died in your mouth.")),
+ MOONLIGHT_MEAD_M_KEG(new Drink(new int[] {5897, 5895, 5893, 5891, 5769}, new HealingEffect(4), new Animation(2289), "It tastes like something just died in your mouth.")),
SLAYERS_RESPITE(new Drink(new int[] {5759, 1919}, new MultiEffect(new HealingEffect(1), new SkillEffect(Skills.SLAYER, 2, 0), new SkillEffect(Skills.ATTACK, -2, 0), new SkillEffect(Skills.STRENGTH, -2, 0)))),
SLAYERS_RESPITE_KEG(new Drink(new int[] {5841, 5839, 5837, 5835, 5769}, new MultiEffect(new HealingEffect(1), new SkillEffect(Skills.SLAYER, 2, 0), new SkillEffect(Skills.ATTACK, -2, 0), new SkillEffect(Skills.STRENGTH, -2, 0)), new Animation(2289))),
SLAYERS_RESPITE_M(new Drink(new int[] {5761, 1919}, new MultiEffect(new HealingEffect(1), new SkillEffect(Skills.SLAYER, 4, 0), new SkillEffect(Skills.ATTACK, -2, 0), new SkillEffect(Skills.STRENGTH, -2, 0)))),
SLAYERS_RESPITE_M_KEG(new Drink(new int[] {5841, 5839, 5837, 5835, 5769}, new MultiEffect(new HealingEffect(1), new SkillEffect(Skills.SLAYER, 4, 0), new SkillEffect(Skills.ATTACK, -2, 0), new SkillEffect(Skills.STRENGTH, -2, 0)), new Animation(2289))),
WIZARDS_MIND_BOMB(new Drink(new int[] {1907, 1919}, new WizardsMindBombEffect(), "You drink the Wizard's Mind Bomb.", "You feel very strange.")),
- MATURE_WMB(new Drink(new int[] {5741, 1919}, new MatureWmbEffect())),
+ WIZARDS_MIND_BOMB_KEG(new Drink(new int[] {5801, 5799, 5797, 5795, 5769}, new WizardsMindBombEffect(), new Animation(2289), "You drink the Wizard's Mind Bomb.", "You feel very strange.")),
+ MATURE_WMB(new Drink(new int[] {5741, 1919}, new MatureWmbEffect(), "You drink the mature Wizard's Mind Bomb.", "You feel very strange.")),
+ MATURE_WMB_KEG(new Drink(new int[] {5881, 5879, 5877, 5875, 5769}, new MatureWmbEffect(), new Animation(2289), "You drink the mature Wizard's Mind Bomb.", "You feel very strange.")),
/** Cocktails */
FRUIT_BLAST(new Drink(new int[] {2084, 2026}, new HealingEffect(9))),
diff --git a/Server/src/main/content/global/activity/shootingstar/ShootingStar.kt b/Server/src/main/content/global/activity/shootingstar/ShootingStar.kt
index 9432ea009..074ab27a6 100644
--- a/Server/src/main/content/global/activity/shootingstar/ShootingStar.kt
+++ b/Server/src/main/content/global/activity/shootingstar/ShootingStar.kt
@@ -49,7 +49,7 @@ class ShootingStar(var level: ShootingStarType = ShootingStarType.values().rando
"South-west Varrock mine" to Location.create(3176, 3362, 0), // South-west Varrock mine / Champion's Guild mine
"Varrock east bank" to Location.create(3259, 3407, 0), // Varrock east bank / Rune shop
"Lumbridge Swamp south-east mine" to Location.create(3227, 3150, 0), // Lumbridge Swamp south-east mine
- //"Burgh de Rott bank" to Location.create(3500, 3219, 0), // Burgh de Rott bank (requires quest to enter)
+ "Burgh de Rott bank" to Location.create(3500, 3219, 0), // Burgh de Rott bank (requires quest to enter)
"Canifis Bank" to Location.create(3504, 3487, 0), // Canifis bank
"Mos Le'Harmless bank" to Location.create(3687, 2969, 0), // Mos Le'Harmless bank (requires quest to enter but is currently accessible for Slayer)
"Gnome stronghold Bank" to Location.create(2460, 3432, 0), // Gnome stronghold bank
diff --git a/Server/src/main/content/global/activity/shootingstar/ShootingStarPlugin.kt b/Server/src/main/content/global/activity/shootingstar/ShootingStarPlugin.kt
index 234c49c61..7368b7689 100644
--- a/Server/src/main/content/global/activity/shootingstar/ShootingStarPlugin.kt
+++ b/Server/src/main/content/global/activity/shootingstar/ShootingStarPlugin.kt
@@ -97,7 +97,7 @@ class ShootingStarPlugin : LoginListener, InteractionListener, TickListener, Com
fun teleportToStar(player: Player) {
val condition: (p: Player) -> Boolean = when (star.location.toLowerCase()) {
"canifis bank" -> {p -> requireQuest(p, Quests.PRIEST_IN_PERIL, "to access this.")}
- //"burgh de rott bank" -> {p -> hasRequirement(p, Quests.IN_AID_OF_THE_MYREQUE)} //disabled: crash
+ "burgh de rott bank" -> {p -> hasRequirement(p, Quests.IN_AID_OF_THE_MYREQUE)}
"crafting guild" -> {p -> hasLevelStat(p, Skills.CRAFTING, 40)}
"lletya bank" -> {p -> hasRequirement(p, Quests.MOURNINGS_END_PART_I)}
"jatizso mine" -> {p -> hasRequirement(p, Quests.THE_FREMENNIK_ISLES)}
@@ -120,7 +120,7 @@ class ShootingStarPlugin : LoginListener, InteractionListener, TickListener, Com
1 -> dialogue("WARNING: The star is located in the wilderness.").also { stage++ }
2 -> player.dialogueInterpreter.sendOptions("Teleport to the star?", "Yes", "No").also { stage++ }
3 -> when (buttonID) {
- 1 -> end().also { teleportToStar(player) }
+ 1 -> teleportToStar(player)
2 -> end()
}
}
diff --git a/Server/src/main/content/global/handlers/item/EmptyOptionListener.kt b/Server/src/main/content/global/handlers/item/EmptyOptionListener.kt
index da4ddef6c..c58bf62ed 100644
--- a/Server/src/main/content/global/handlers/item/EmptyOptionListener.kt
+++ b/Server/src/main/content/global/handlers/item/EmptyOptionListener.kt
@@ -41,6 +41,7 @@ class EmptyOptionListener : InteractionListener {
BUCKET_OF_COMPOST(Items.COMPOST_6032, Items.BUCKET_1925, "You empty the bucket of compost."),
BUCKET_OF_SUPERCOMPOST(Items.SUPERCOMPOST_6034, Items.BUCKET_1925, "You empty the bucket of supercompost."),
BUCKET_OF_SLIME(Items.BUCKET_OF_SLIME_4286, Items.BUCKET_1925, "You empty the contents of the bucket on the floor.", Sounds.LIQUID_2401),
+ APPLE_MUSH(Items.APPLE_MUSH_5992,Items.BUCKET_1925, "You empty the bucket of apple mush."),
VIAL_OF_WATER(Items.VIAL_OF_WATER_227, Items.VIAL_229, "You empty the vial.", Sounds.LIQUID_2401),
BOWL_OF_WATER(Items.BOWL_OF_WATER_1921, Items.BOWL_1923, "You empty the contents of the bowl onto the floor.", Sounds.LIQUID_2401),
BOWL_OF_HOT_WATER(Items.BOWL_OF_HOT_WATER_4456, Items.BOWL_1923, "You empty the contents of the bowl onto the floor.", Sounds.LIQUID_2401),
diff --git a/Server/src/main/content/global/handlers/item/EnchantedJewelleryListener.kt b/Server/src/main/content/global/handlers/item/EnchantedJewelleryListener.kt
index 2878628ac..549b69051 100644
--- a/Server/src/main/content/global/handlers/item/EnchantedJewelleryListener.kt
+++ b/Server/src/main/content/global/handlers/item/EnchantedJewelleryListener.kt
@@ -50,15 +50,15 @@ class EnchantedJewelleryListener : InteractionListener {
jewellery.use(player, item, 0, isEquipped)
return
}
- openDialogue(player,EnchantedJewelleryDialogueFile(jewellery,item,isEquipped))
+ openDialogue(player,EnchantedJewelleryDialogueFile(jewellery, item, true, isEquipped))
}
}
- class EnchantedJewelleryDialogueFile(val jewellery: EnchantedJewellery, val item: Item, val isEquipped: Boolean) : DialogueFile() {
+ class EnchantedJewelleryDialogueFile(val jewellery: EnchantedJewellery, val item: Item, val replace: Boolean, val isEquipped: Boolean) : DialogueFile() {
override fun handle(componentID: Int, buttonID: Int) {
when (stage) {
START_DIALOGUE -> {
- interpreter!!.sendOptions("Where would you like to go?", *jewellery.options)
+ interpreter!!.sendOptions("Where would you like to teleport to?", *jewellery.options)
stage++
}
1 -> end().also { jewellery.use(player!!, item, buttonID - 1, isEquipped) }
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/item/withitem/CalquatKeg.kt b/Server/src/main/content/global/handlers/item/withitem/CalquatKeg.kt
new file mode 100644
index 000000000..56b5cb77e
--- /dev/null
+++ b/Server/src/main/content/global/handlers/item/withitem/CalquatKeg.kt
@@ -0,0 +1,19 @@
+package content.global.handlers.item.withitem
+
+import core.api.addItem
+import core.api.animate
+import core.api.removeItem
+import core.game.interaction.InteractionListener
+import org.rs09.consts.Items
+
+class CalquatKeg : InteractionListener{
+ override fun defineListeners() {
+ onUseWith(ITEM, Items.CALQUAT_FRUIT_5980, Items.KNIFE_946) { player, _, _ ->
+ if (removeItem(player, Items.CALQUAT_FRUIT_5980)){
+ animate(player,2290)
+ addItem(player, Items.CALQUAT_KEG_5769)
+ }
+ return@onUseWith true
+ }
+ }
+}
\ No newline at end of file
diff --git a/Server/src/main/content/global/handlers/npc/ChinchompaDeathListener.kt b/Server/src/main/content/global/handlers/npc/ChinchompaDeathListener.kt
new file mode 100644
index 000000000..3a146eaf4
--- /dev/null
+++ b/Server/src/main/content/global/handlers/npc/ChinchompaDeathListener.kt
@@ -0,0 +1,52 @@
+package content.global.handlers.npc
+
+import core.api.*
+import core.api.playGlobalAudio
+import core.api.sendGraphics
+import core.game.node.entity.Entity
+import core.game.node.entity.npc.NPCBehavior
+import org.rs09.consts.NPCs
+import core.game.node.entity.npc.NPC
+import core.game.node.entity.player.Player
+import core.tools.RandomFunction
+import core.game.world.update.flag.context.Graphics
+import core.game.node.entity.combat.ImpactHandler.HitsplatType;
+
+private val chinchompaIDs = intArrayOf(
+ NPCs.CHINCHOMPA_5079,
+ NPCs.CARNIVOROUS_CHINCHOMPA_5080
+)
+
+/**
+ * Minimal evasive approach to the Chinchompas exploding on death cause by combat and not by trapping.
+ */
+class ChinchompaDeathListener: NPCBehavior(*chinchompaIDs) {
+ override fun onDeathFinished(self: NPC, killer: Entity) {
+
+ // If it 'died' from getting trapped an attribute is set of 'hunter'.
+ if (!self.attributes.containsKey("hunter") && killer is Player) {
+ val explosionLoc = self.location
+ val validExplosionDmgTiles = explosionLoc.surroundingTiles
+
+ val chinExplosionGFX = Graphics(157, 96)
+ sendGraphics(chinExplosionGFX, explosionLoc)
+ playGlobalAudio(explosionLoc, 158)
+
+ if (validExplosionDmgTiles.contains(killer.location)) {
+ //As weird this looks, it does allow to have a hit from 0 to 2
+ val hitAmount = RandomFunction.random(0, 3)
+ impact(killer, hitAmount, HitsplatType.NORMAL)
+ }
+ }
+ }
+
+ /**
+ * At content.global.skill.hunter.TrapSetting
+ * sets an attribute of 'hunter' during it's getCatchPulse for reward rolling, but it never cleans it up after.
+ * These NPCs is a member of NPC class although exists a HunterNPC class, but it's mainly used for imps.
+ * Logically you'd do this after death finishes, but it would interfere with the reward rolls and I fear it may affect other hunter NPCs.
+ */
+ override fun onRespawn(self: NPC) {
+ self.removeAttribute("hunter")
+ }
+}
\ No newline at end of file
diff --git a/Server/src/main/content/global/handlers/npc/DecantListener.kt b/Server/src/main/content/global/handlers/npc/DecantListener.kt
index 9a63c78c2..368835e32 100644
--- a/Server/src/main/content/global/handlers/npc/DecantListener.kt
+++ b/Server/src/main/content/global/handlers/npc/DecantListener.kt
@@ -1,6 +1,7 @@
package content.global.handlers.npc
import content.data.consumables.Consumables
+import content.global.skill.cooking.fermenting.CalquatDecant
import core.api.*
import core.game.consumable.Potion
import core.game.dialogue.DialogueFile
@@ -14,23 +15,39 @@ class DecantListener : InteractionListener {
companion object {
val potions = Consumables.potions.toIntArray()
+ val decantables = potions + CalquatDecant.allKegIds
}
override fun defineListeners() {
on(IntType.NPC,"decant"){ player, node ->
- val (toRemove, toAdd) = decantContainer(player.inventory)
- for (item in toRemove)
- removeItem(player, item)
- for (item in toAdd)
+ // Decant potions
+ val (potionRemove, potionAdd) = decantContainer(player.inventory)
+ for (item in potionRemove)
+ if (!removeItem(player, item)) { return@on false }
+ for (item in potionAdd)
addItem(player, item.id, item.amount)
+
+ // Decant kegs
+ val (kegRemove, kegAdd) = CalquatDecant.decantCalquat(player.inventory)
+ for (item in kegRemove)
+ if (!removeItem(player, item)) { return@on false }
+ for (item in kegAdd)
+ addItem(player, item.id, item.amount)
+
player.dialogueInterpreter.open(DecantingDialogue(),node.asNpc())
return@on true
}
- onUseWith(IntType.ITEM, potions, *potions) { player, used, with ->
+ onUseWith(IntType.ITEM, decantables, *decantables) { player, used, with ->
if (used !is Item) return@onUseWith false
if (with !is Item) return@onUseWith false
+ // Check if these are kegs - handle with CalquatDecant
+ if (CalquatDecant.isKeg(used.id) || CalquatDecant.isKeg(with.id)) {
+ return@onUseWith CalquatDecant.combineKegs(player, used, with)
+ }
+
+ // Otherwise handle as potions
// Verify these are both the same potion types
val potionUsed = Consumables.getConsumableById(used.id)?.consumable as? Potion ?: return@onUseWith false
val potionWith = Consumables.getConsumableById(with.id)?.consumable as? Potion ?: return@onUseWith false
@@ -83,6 +100,57 @@ class DecantListener : InteractionListener {
return@onUseWith true
}
+
+ // Decant keg into beer glass
+ onUseWith(IntType.ITEM, Items.BEER_GLASS_1919, *CalquatDecant.allKegIds) { player, used, with ->
+ if (used !is Item || with !is Item) return@onUseWith false
+
+ // Determine which is the glass and which is the keg
+ val (glass, keg) = if (used.id == Items.BEER_GLASS_1919) {
+ used to with
+ } else {
+ with to used
+ }
+
+ return@onUseWith CalquatDecant.pourIntoGlass(player, glass, keg)
+ }
+
+ // Barbarian decanting: use a 4-dose potion and an empty vial together to make two 2-dose potions
+ onUseWith(IntType.ITEM, potions, Items.VIAL_229) { player, used, with ->
+ // Check if player has started barbarian herblore training
+ if (getAttribute(player, "/save:barbtraining:herblore", 0) == 0) {
+ return@onUseWith false
+ }
+
+ if (used !is Item) return@onUseWith false
+ if (with !is Item) return@onUseWith false
+
+ // Determine which item is the potion and which is the vial
+ val (potionItem) = if (used.id == Items.VIAL_229) {
+ Pair(with, used)
+ } else {
+ Pair(used, with)
+ }
+
+ // Check if the potion is 4 doses
+ val potion = Consumables.getConsumableById(potionItem.id)?.consumable as? Potion ?: return@onUseWith false
+ val dose = potion.getDose(potionItem)
+
+ if (dose != 4) {
+ return@onUseWith false
+ }
+
+ // Get the 2-dose version of this potion by index
+ val twoDoseId = potion.ids[2]
+
+ // Replace ingredients with products
+ replaceSlot(player, with.slot, Item(twoDoseId))
+ replaceSlot(player, used.slot, Item(twoDoseId))
+
+ playAudio(player, 2401)
+
+ return@onUseWith true
+ }
}
internal class DecantingDialogue : DialogueFile(){
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/BuildingUtils.java b/Server/src/main/content/global/skill/construction/BuildingUtils.java
index 02bfbd9c2..2ee636337 100644
--- a/Server/src/main/content/global/skill/construction/BuildingUtils.java
+++ b/Server/src/main/content/global/skill/construction/BuildingUtils.java
@@ -90,8 +90,6 @@ public final class BuildingUtils {
for (int menuIndex = 0; menuIndex < 7; menuIndex++) {
int itemsStringOffset = 97 + (menuIndex * 5);
-
- //97 +
if (menuIndex >= hotspot.getDecorations().length || (hotspot.getDecorations()[menuIndex] != null && hotspot.getDecorations()[menuIndex].isInvisibleNode())) {
for (int j = 0; j < 5; j++) {
player.getPacketDispatch().sendString("", 396, itemsStringOffset + j);
@@ -104,43 +102,24 @@ public final class BuildingUtils {
Decoration decoration = hotspot.getDecorations()[menuIndex];
items[BUILD_INDEXES[menuIndex]] = new Item(decoration.getInterfaceItem());
player.getPacketDispatch().sendString(ItemDefinition.forId(decoration.getInterfaceItem()).getName(), 396, itemsStringOffset);
- boolean hasRequirements = player.getSkills().getLevel(Skills.CONSTRUCTION) >= decoration.getLevel();
+ boolean hasRequirements = player.getSkills().getLevel(Skills.CONSTRUCTION) >= decoration.getLevel(); //nails are checked in buildDecoration
+ Item[] reqItems = decoration.getItems();
+ String[] reqsText = decoration.getReqsText();
for (int j = 0; j < 4; j++) {
- if (j >= decoration.getItems().length) {
- if (j == decoration.getItems().length && decoration.getNailAmount() > 0) {
- player.getPacketDispatch().sendString("Nails: " + decoration.getNailAmount(), 396, (itemsStringOffset + 1) + j);
- } else {
- player.getPacketDispatch().sendString("", 396, (itemsStringOffset + 1) + j);
- }
- } else {
- Item item = decoration.getItems()[j];
- if (!player.getInventory().containsItem(item)) {
- hasRequirements = false;
- }
- String s = item.getName() + ": " + item.getAmount();
- /*if (j > 1 && (decoration == Decoration.RUNE_CASE1 || decoration == Decoration.RUNE_CASE2)) {
- if (j == 3) {
- offset--;
- item = decoration.getItems()[++j];
- s = item.getName() + ": " + item.getAmount();
- }
- item = decoration.getItems()[j + 1];
- s += ", " + item.getName() + ": " + item.getAmount();
- player.getPacketDispatch().sendString(s, 396, 15 + offset + j);
- continue;
- }*/
- player.getPacketDispatch().sendString(s, 396, (itemsStringOffset + 1) + j);
+ if (j < reqItems.length && !player.getInventory().containsItem(reqItems[j])) {
+ hasRequirements = false;
}
+ player.getPacketDispatch().sendString(reqsText[j], 396, (itemsStringOffset + 1) + j);
}
if (hasRequirements) {
c261Value += (1 << (menuIndex + 1));
}
- setVarp(player, 1485 + menuIndex, hasRequirements || player.isStaff() ? 1 : 0);
+ setVarp(player, 1485 + menuIndex, hasRequirements || player.isStaff() ? 1 : 0);
player.getPacketDispatch().sendString("Level " + decoration.getLevel(), 396, 140 + menuIndex);
//player.getPacketDispatch().sendItemZoomOnInterface(items[i].protocol(), 50000, 396, 49 + i);
}
- setVarp(player, 261, c261Value);
+ setVarp(player, 261, c261Value);
PacketRepository.send(ContainerPacket.class, new ContainerContext(player, 396, 132, 8, items, false));
}
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/Decoration.java b/Server/src/main/content/global/skill/construction/Decoration.java
index 6229dff82..4191cb1a9 100644
--- a/Server/src/main/content/global/skill/construction/Decoration.java
+++ b/Server/src/main/content/global/skill/construction/Decoration.java
@@ -454,7 +454,7 @@ public enum Decoration {
* Quest hall decorations.
*/
ANTIDRAGON_SHIELD(13522, 8282, 47, 280, new Item[] { new Item(Items.TEAK_PLANK_8780, 3), new Item(Items.ANTI_DRAGON_SHIELD_1540) }, new Item[] { new Item(Items.ANTI_DRAGON_SHIELD_1540) }),
- AMULET_OF_GLORY (13523, 8283, 47, 290, new Item[] { new Item(Items.TEAK_PLANK_8780, 3), new Item(Items.AMULET_OF_GLORY_1704) }, new Item[] { new Item(Items.AMULET_OF_GLORY_1704) }),
+ AMULET_OF_GLORY (13523, 8283, 47, 290, new Item[] { new Item(Items.TEAK_PLANK_8780, 3), new Item(Items.AMULET_OF_GLORY_1704) }, new Item[] { new Item(Items.AMULET_OF_GLORY_1704) }, new String[] { "Teak plank: 3", "Amulet of Glory", "(uncharged): 1" }),
CAPE_OF_LEGENDS (13524, 8284, 47, 300, new Item[] { new Item(Items.TEAK_PLANK_8780, 3), new Item(Items.CAPE_OF_LEGENDS_1052) }, new Item[] { new Item(Items.CAPE_OF_LEGENDS_1052) }),
KING_ARTHUR (13510, 8285, 35, 211, new Item[] { new Item(Items.TEAK_PLANK_8780, 3), new Item(Items.ARTHUR_PORTRAIT_7995) }),
ELENA (13511, 8286, 35, 211, new Item[] { new Item(Items.TEAK_PLANK_8780, 3), new Item(Items.ELENA_PORTRAIT_7996) }),
@@ -727,23 +727,42 @@ public enum Decoration {
private final int[] objectIds;
/**
- * If this node should be invisible to user build options
+ * If this node should be invisible to user build options.
*/
private boolean invisibleNode;
/**
- * Constructs a new object, no items, no tools, no refund items.
+ * The requirements text to show for this node.
+ */
+ private String[] reqsText = {"", "", "", ""};
+
+ private String[] generateDefaultReqsText(Item[] items, int nailAmount) {
+ String[] ret = {"", "", "", ""};
+ for (int i = 0; i < 4; i++) {
+ if (i >= items.length) {
+ if (i == items.length && nailAmount > 0) {
+ ret[i] = "Nails: " + nailAmount;
+ }
+ } else {
+ ret[i] = items[i].getName() + ": " + items[i].getAmount();
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Constructs a new object, default items, default tools, default refund items, default requirements text.
* @param objectId The object id.
* @param interfaceItem The item id for the building interface.
* @param level The level required.
* @param experience The experience gained.
*/
Decoration(int objectId, int interfaceItem, int level, int experience) {
- this(objectId, interfaceItem, level, experience, new int[] { Items.HAMMER_2347, Items.SAW_8794 }, new Item[] {}, new Item[] {});
+ this(objectId, interfaceItem, level, experience, new int[] { Items.HAMMER_2347, Items.SAW_8794 }, new Item[] {}, new Item[] {}, new String[] {});
}
/**
- * Constructs a new object, no tools, no refund items.
+ * Constructs a new object, default tools, default refund items, default requirements text.
* @param objectId The object id.
* @param interfaceItem The item id for the building interface.
* @param level The level required.
@@ -751,11 +770,12 @@ public enum Decoration {
* @param items The items required.
*/
Decoration(int objectId, int interfaceItem, int level, int experience, Item[] items) {
- this(objectId, interfaceItem, level, experience, new int[] { Items.HAMMER_2347, Items.SAW_8794 }, items, new Item[] {});
+ this(objectId, interfaceItem, level, experience, new int[] { Items.HAMMER_2347, Items.SAW_8794 }, items, new Item[] {}, new String[] {});
+ this.reqsText = generateDefaultReqsText(items, getNailAmount());
}
/**
- * Constructs a new object, no refund items.
+ * Constructs a new object, default refund items, default requirements text.
* @param objectId The object id.
* @param interfaceItem The item id for the building interface.
* @param level The level required.
@@ -764,7 +784,8 @@ public enum Decoration {
* @param items The items required.
*/
Decoration(int objectId, int interfaceItem, int level, int experience, int[] tools, Item[] items) {
- this(objectId, interfaceItem, level, experience, tools, items, new Item[] {});
+ this(objectId, interfaceItem, level, experience, tools, items, new Item[] {}, new String[] {});
+ this.reqsText = generateDefaultReqsText(items, getNailAmount());
}
/**
@@ -777,7 +798,21 @@ public enum Decoration {
* @param refundItems The items to be refunded when the item is removed.
*/
Decoration(int objectId, int interfaceItem, int level, int experience, Item[] items, Item[] refundItems) {
- this(objectId, interfaceItem, level, experience, new int[] { Items.HAMMER_2347, Items.SAW_8794 }, items, refundItems);
+ this(objectId, interfaceItem, level, experience, new int[] { Items.HAMMER_2347, Items.SAW_8794 }, items, refundItems, new String[] {});
+ this.reqsText = generateDefaultReqsText(items, getNailAmount());
+ }
+
+ /**
+ * Constructs a new object, default tools.
+ * @param objectId The object id.
+ * @param interfaceItem The item id for the building interface.
+ * @param level The level required.
+ * @param experience The experience gained.
+ * @param items The items required.
+ * @param refundItems The items to be refunded when the item is removed.
+ */
+ Decoration(int objectId, int interfaceItem, int level, int experience, Item[] items, Item[] refundItems, String[] reqsText) {
+ this(objectId, interfaceItem, level, experience, new int[] { Items.HAMMER_2347, Items.SAW_8794 }, items, refundItems, reqsText);
}
/**
@@ -789,8 +824,9 @@ public enum Decoration {
* @param tools The tools needed.
* @param items The items required.
* @param refundItems The items to be refunded when the item is removed.
+ * @param reqsText The requirements text to be shown in the interface.
*/
- Decoration(int objectId, int interfaceItem, int level, int experience, int[] tools, Item[] items, Item[] refundItems) {
+ Decoration(int objectId, int interfaceItem, int level, int experience, int[] tools, Item[] items, Item[] refundItems, String[] reqsText) {
this.objectId = objectId;
this.objectIds = null;
this.interfaceItem = interfaceItem;
@@ -799,6 +835,9 @@ public enum Decoration {
this.tools = tools;
this.items = items;
this.refundItems = refundItems;
+ if (reqsText.length > 0) {
+ System.arraycopy(reqsText, 0, this.reqsText, 0, reqsText.length);
+ }
}
/**
@@ -822,18 +861,6 @@ public enum Decoration {
Decoration(int[] objectIds, int interfaceItem, int level, int experience, Item[] items) {
this(objectIds, interfaceItem, level, experience, new int[] { Items.HAMMER_2347, Items.SAW_8794 }, items, new Item[] {});
}
- /**
- * Constructs a new object no refund items.
- * @param objectIds The object id.
- * @param interfaceItem The item id for the building interface.
- * @param level The level required.
- * @param experience The experience gained.
- * @param tools The tools needed.
- * @param items The items required.
- */
- Decoration(int[] objectIds, int interfaceItem, int level, int experience, int[] tools, Item[] items) {
- this(objectIds, interfaceItem, level, experience, tools, items, new Item[] {});
- }
/**
* Constructs a new object.
@@ -1000,4 +1027,12 @@ public enum Decoration {
public boolean isInvisibleNode() {
return invisibleNode;
}
+
+ /**
+ * Gets the requirements text.
+ * @return The requirements text.
+ */
+ public String[] getReqsText() {
+ return reqsText;
+ }
}
diff --git a/Server/src/main/content/global/skill/construction/HouseZone.java b/Server/src/main/content/global/skill/construction/HouseZone.java
index 64165f49f..4a6cb6464 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;
@@ -93,6 +94,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()) {
@@ -105,11 +128,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/construction/decoration/portalchamber/PortalChamberPlugin.java b/Server/src/main/content/global/skill/construction/decoration/portalchamber/PortalChamberPlugin.java
index bf1f26c54..c154bef68 100644
--- a/Server/src/main/content/global/skill/construction/decoration/portalchamber/PortalChamberPlugin.java
+++ b/Server/src/main/content/global/skill/construction/decoration/portalchamber/PortalChamberPlugin.java
@@ -1,6 +1,7 @@
package content.global.skill.construction.decoration.portalchamber;
+import content.data.Quests;
import content.global.skill.construction.Hotspot;
import content.global.skill.runecrafting.Rune;
import core.cache.def.impl.SceneryDefinition;
@@ -18,6 +19,7 @@ import core.plugin.Plugin;
import core.plugin.ClassScanner;
import static content.region.kandarin.ardougne.quest.plaguecity.PlagueCityListeners.ARDOUGNE_TELE_ATTRIBUTE;
+import static core.api.ContentAPIKt.*;
/**
* PortalChamberPlugin
@@ -89,15 +91,14 @@ public class PortalChamberPlugin extends OptionHandler {
}
for (Locations l : Locations.values()) {
if (l.name().contains(identifier)) {
- if (l == Locations.ARDOUGNE){
- if (!player.getAttribute(ARDOUGNE_TELE_ATTRIBUTE, false)) {
- player.sendMessage("You do not have the requirements to direct the portal there");
- return;
- }
+ if ((l == Locations.ARDOUGNE && !getAttribute(player, ARDOUGNE_TELE_ATTRIBUTE, false)) ||
+ (l == Locations.KHARYRLL && !isQuestComplete(player, Quests.DESERT_TREASURE))) {
+ sendMessage(player, "You do not have the requirements to direct the portal there.");
+ return;
}
Item[] runes = l.runes;
if (!player.getInventory().containsItems(runes)) {
- player.sendMessage("You do not have the required runes to build this portal");
+ sendMessage(player, "You do not have the required runes to build this portal.");
return;
}
player.getInventory().remove(runes);
diff --git a/Server/src/main/content/global/skill/construction/decoration/questhall/MountedGlory.kt b/Server/src/main/content/global/skill/construction/decoration/questhall/MountedGlory.kt
index 47051e46f..b5196af4d 100644
--- a/Server/src/main/content/global/skill/construction/decoration/questhall/MountedGlory.kt
+++ b/Server/src/main/content/global/skill/construction/decoration/questhall/MountedGlory.kt
@@ -1,53 +1,53 @@
package content.global.skill.construction.decoration.questhall
+import content.data.EnchantedJewellery
+import content.global.handlers.item.EnchantedJewelleryListener.EnchantedJewelleryDialogueFile
+import core.api.delayScript
+import core.api.lock
+import core.api.openDialogue
import core.api.playGlobalAudio
+import core.api.queueScript
+import core.api.resetAnimator
+import core.api.stopExecuting
import core.api.teleport
+import core.api.visualize
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
+import core.game.interaction.QueueStrength
import core.game.node.Node
import core.game.node.entity.player.Player
import core.game.node.item.Item
import core.game.node.scenery.Scenery
-import core.game.system.task.Pulse
-import core.game.world.map.Location
import core.game.world.update.flag.context.Animation
import core.game.world.update.flag.context.Graphics
import org.rs09.consts.Items
import org.rs09.consts.Sounds
-import java.util.concurrent.Executors
-import java.util.concurrent.TimeUnit
class MountedGlory : InteractionListener {
-
val MOUNTED_GLORY = 13523
-
- val TELEPORTS = arrayOf(
- Location.create(3087, 3495, 0),
- Location.create(2919, 3175, 0),
- Location.create(3104, 3249, 0),
- Location.create(3304, 3124, 0)
- )
+ val TELEPORTS = EnchantedJewellery.AMULET_OF_GLORY.locations
override fun defineListeners() {
on(MOUNTED_GLORY, IntType.SCENERY, "Edgeville") { player, `object` ->
mountedGloryAction(player, `object`, 0)
return@on true
}
-
on(MOUNTED_GLORY, IntType.SCENERY, "Karamja") { player, `object` ->
mountedGloryAction(player, `object`, 1)
return@on true
}
-
on(MOUNTED_GLORY, IntType.SCENERY, "Draynor Village") { player, `object` ->
mountedGloryAction(player, `object`, 2)
return@on true
}
-
on(MOUNTED_GLORY, IntType.SCENERY, "Al Kharid") { player, `object` ->
mountedGloryAction(player, `object`, 3)
return@on true
}
+ on(MOUNTED_GLORY, IntType.SCENERY, "Rub") { player, _ ->
+ openDialogue(player,EnchantedJewelleryDialogueFile(EnchantedJewellery.AMULET_OF_GLORY, Item(Items.AMULET_OF_GLORY4_1712), false, false))
+ return@on true
+ }
}
private fun mountedGloryAction(player : Player, `object` : Node, int : Int) {
if (player.houseManager.isBuildingMode) {
@@ -57,21 +57,16 @@ class MountedGlory : InteractionListener {
if (!player.zoneMonitor.teleport(1, Item(Items.AMULET_OF_GLORY_1704))) {
return
}
- Executors.newSingleThreadScheduledExecutor().schedule({
- player.pulseManager.run(object : Pulse() {
- var counter = 0
- override fun pulse(): Boolean {
- when (counter++) {
- 1 -> {
- player.lock(5)
- player.visualize(Animation(714), Graphics(308, 100, 50))
- playGlobalAudio(player.location, Sounds.TELEPORT_ALL_200)
- }
- 4 -> player.animator.reset().also { teleport(player, TELEPORTS[int]) }
- }
- return false
+ queueScript(player, 0, QueueStrength.SOFT) { stage ->
+ if (stage == 0) {
+ lock(player, 4)
+ visualize(player, Animation(714), Graphics(308, 100, 50))
+ playGlobalAudio(player.location, Sounds.TELEPORT_ALL_200)
+ return@queueScript delayScript(player, 3)
}
- })
- }, 0, TimeUnit.SECONDS)
+ resetAnimator(player)
+ teleport(player, TELEPORTS[int])
+ return@queueScript stopExecuting(player)
+ }
}
}
diff --git a/Server/src/main/content/global/skill/construction/decoration/questhall/MountedGloryPlugin.java b/Server/src/main/content/global/skill/construction/decoration/questhall/MountedGloryPlugin.java
deleted file mode 100644
index 90d96eb80..000000000
--- a/Server/src/main/content/global/skill/construction/decoration/questhall/MountedGloryPlugin.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package content.global.skill.construction.decoration.questhall;
-
-
-import core.cache.def.impl.SceneryDefinition;
-import core.game.dialogue.DialogueAction;
-import core.game.dialogue.DialogueInterpreter;
-import core.game.interaction.OptionHandler;
-import core.game.node.Node;
-import core.game.node.entity.player.Player;
-import core.game.system.task.Pulse;
-import core.game.world.GameWorld;
-import core.game.world.map.Location;
-import core.game.world.update.flag.context.Animation;
-import core.game.world.update.flag.context.Graphics;
-import core.plugin.Initializable;
-import core.plugin.Plugin;
-import org.rs09.consts.Sounds;
-
-import static core.api.ContentAPIKt.playGlobalAudio;
-
-/**
- * Handles the POH Mounted Glory.
- * @author Splinter
- */
-@Initializable
-public class MountedGloryPlugin extends OptionHandler {
-
- /**
- * Represents the teleport animation.
- */
- private static final Animation ANIMATION = new Animation(714);
-
- /**
- * Represents the graphics to use.
- */
- private static final Graphics GRAPHICS = new Graphics(308, 100, 50);
-
- @Override
- public Plugin