diff --git a/cache/main_file_cache.idx5 b/cache/main_file_cache.idx5 index fe159062d..b5ea860a8 100644 Binary files a/cache/main_file_cache.idx5 and b/cache/main_file_cache.idx5 differ diff --git a/data/config/books/construction-guide.json5 b/data/config/books/construction-guide.json5 new file mode 100644 index 000000000..02468abb2 --- /dev/null +++ b/data/config/books/construction-guide.json5 @@ -0,0 +1,23 @@ +{ + bookId: 8463, + bookTitle: "Guide to Construction", + showTableOfContents: true, + bookSections: [ + { + header: "How to build in your house", + text: "In order to build you will need to turn building mode on. This can be done on entering the house or using a button on the options interface. If you have a bank PIN you must enter it when entering building mode.\n\nIn building mode the ghostly shapes of furniture and doorways you have not built yet will appear in your house. These are called hotspots. You can use these to build furniture and new rooms.\n\nTo build a piece of furniture, right-click the hotspot and select Build. You will then be able to select the piece of furniture you want to build from the menu. Below each furniture icon is a list of materials; to build the furniture you will need to have all these materials in your intentory. You will also need to have a hammer, a saw and have the correct Construction level.\n\nNails work slightly differently to other materials. You will sometimes find you break nails, especially if you have a low Construction level, so you may need to bring more nails than the furniture requires. Nails made of stronger metals will break less often.\n\nYou can remove a piece of furniture if you wish to build something else in the same space. To do this, right-click on it and select Remove. You will not get any of the materials back. Some pieces of furniture can be upgraded to better pieces of furniture without having to remove them first.\n\nTo build a new room, you must use one of the door hotspots at the edges of rooms or garden squares. Right-click on it and select Build. This will bring up a list of rooms. Different rooms cost different amounts of gold and have different Construction level requirements.\n\nIf you select Build on a door hotspot that already leads to a room, you will be asked whether you want to delete that room." + }, + { + header: "Raw Materials", + text: "The main raw material you will use to make furniture is planks. The sawmill operator north east of Varrock will turn logs into planks for you, for a fee. The useful planks are wood, oak, teak and mahogany. The sawmill operator also sells saws, cloth and nails.\n\nFor higher level furniture you may also need limestone, marble, gold leaf and magic building crystals. These are sold by the stonemason who lives in Keldagrim.\n\nSome pieces of furniture also require materials that are not specific to construction, such as steel bars and soft clay.", + }, + { + header: "Room types", + text: "Parlour: This is the lowest-level room and provides space for three people to sit around a fire\nGarden: The garden is largely decorative but it also contains the exit portal.\nKitchen: This room can be used for preparing food. As you build better furniture in it you will find yourself able to prepare better meals in it.\nDining room: Eight people can sit around the tables you build in this room.\nBedroom: Some of the furniture in this room can be used to change your hair and clothes. You will also need to have two of these rooms in order to hire a servant.\nHalls: These are primarily used to connect other rooms, but they also provide space to show off the owner's skill and quest achievements.\nGames room: Various games can be built in this room to allow friends to play and train together.\nCombat Room: With this room you can challenge your friends in a personal duelling ring.\nWorkshop: This room allows you to train Construction without modifying your own house, by making furniture that can be sold to other players. It also provides space for you to train Crafting and Smithing.\nChapel: This room can be dedicated to any of RuneScape's major gods, and the altar can be used to offer bones.\nMenagerie: You can keep your pets in this room.\nStudy: You can use the lectern in this room to create clay tablets recording magic spells. Eagle lecterns make teleport spells and demon lecterns make enchantment spells. The elemental sphere in this room can be used to change the element of an elemental staff.\nPortal chamber: In this room you can build portals to various places around the world.\nFormal garden: The formal garden can contain various plants and ornaments to beautify the grounds of your house.\nThrone Room: This room can be used to hold audiences with large numbers of friends. It also contains the lever that turns on challenge mode.\nOubliette: If you build an oubliette below your throne room you can drop people from there into a cage which you can fill with various horrors.\nDungeon: Dungeon corridors and junctions can be built to create an underground maze full of monsters, traps and doors.\nTreasure room: You can place a prize in this room for visitors to your dungeon to try to reach." + }, + { + header: "Servants", + text: "Once you have two bedrooms, you can hire a servant by going to the Servants' Guild in Ardougne. You will have to pay when you hire them, and the servant will then periodically demand wages. Servants can take items to and from the bank for you, and can also greet guests and serve food and drinks." + } + ] +} diff --git a/data/config/books/pie-recipe-book.json5 b/data/config/books/pie-recipe-book.json5 new file mode 100644 index 000000000..11b10b44d --- /dev/null +++ b/data/config/books/pie-recipe-book.json5 @@ -0,0 +1,43 @@ +{ + bookId: 7162, + bookTitle: "Pie recipe book", + showTableOfContents: true, + bookSections: [ + { + header: "Redberry pie", + text: "Pour a hand full of Redberries into an empty Pie Shell, bake until the berries are soft and serve warm." + }, + { + header: "Meat pie", + text: "Line a fresh Pie Shell with Cooked Meat and heat until the pastry starts to bronze, serve with a selection of sauces." + }, + { + header: "Mud pie", + text: "Start with a Pie Shell and add a Bucket of Compost, then pour in a Bucket of Water to keep the consistency gooey. To finish, cover with Clay and bake until a good shell forms. Serve at maximum speed a good over-arm throw!" + }, + { + header: "Apple pie", + text: "Take a Pie Shell and layer in Apple, cook until the juices start to bubble and leave to cool before serving." + }, + { + header: "Garden pie", + text: "Fill a Pie Shell with Tomato, then add Onion and top with Cabbage. Bake golden brown and serve with a steak." + }, + { + header: "Fish pie", + text: "Take one Pie Shell and fill with trout, add a Cod for flavour, and then top with Potato for texture. Cook well until the potato turns golden and serve." + }, + { + header: "Admiral pie", + text: "For a more upperclass fish pie, fill your Pie Shell with Salmon and then add Tuna for colour. Top with Potato and cook until golden." + }, + { + header: "Wild pie", + text: "Line a Pie Shell with raw Bear Meat, then add Raw Chompy for substance, and top with fresh Rabbit Meat. Bake until the juices start to bubble and serve." + }, + { + header: "Summer pie", + text: "Into a Pie Shell, put Strawberry, then a layer of Watermelon, and top with Apple. Cook well and leave to cool before serving." + } + ] +} diff --git a/data/config/books/security-book.json5 b/data/config/books/security-book.json5 new file mode 100644 index 000000000..854677b35 --- /dev/null +++ b/data/config/books/security-book.json5 @@ -0,0 +1,35 @@ +{ + bookId: 9004, + bookTitle: "Stronghold Notes", + showTableOfContents: true, + bookSections: [ + { + header: "Description", + text: "This stronghold was unearthed by a miner prospecting for new ores around the Barbarian Village. After gathering some equipment he ventured into the maze of tunnels and was missing for a long time. He finally emerged along with copious notes regarding the new beasts and strange experiences which had befallen him. He also mentioned that there was treasure to be had, but no one has been able to wring a word from him about this, he simply flapped his arms and slapped his head. This book details his notes and my diary of exploration. I am exploring to see if I can find out more..." + }, + { + header: "Level 1", + text: "As well as goblins, creatures like a man but also like a cow infest this place! I have never seen anything like this before. The area itself is reminiscent of frontline castles, with many walls, doors, and skeletons of dead enemies. I'm sure I hear voices in my head each time I pass through the gates. I have dubbed this level War as it seems like an eternal battleground. I found only one small peaceful area here." + }, + { + header: "Level 2", + text: "My supplies are running low and I find myself in barren passages with seemingly endless malnourished beasts attacking me, ravenous for food. Nothing appears to be able to grow, many adventurers have died through lack of food and the very air appears to such vitality from me. I've come to call this place famine.", + }, + { + header: "Level 3", + text: "Just breathing in this place makes me shudder at the thought of what foul disease I may contract. The walls and floor ooze and pulsate like something pox ridden. There is a very strange beast whom I narrowly escaped from. At first I thought it to be a cross between a cow and a sheep, something domesticated... but when it looked up at me I was overcome with weakness and barely got away with my life! Luckily I found a small place where I could heal myself and rest a while. I have named this area pestilence for it reeks with decay." + }, + { + header: "Level 4", + text: "On my first escapade into this place, I was utterly shocked. The adventurers who had come before me must have made up a tiny proportion of the skeletons of the dead. Nothing truly alive exists here, even those beings who do wander the halls are not alive as such, but they do know that I am and I get the distinct impression that were they to have their way, I would not be for long! Death is everywhere and thus I shall name this place. There is one small place of life, which was gladdening to find and very worth my while!" + }, + { + header: "Navigation", + text: "After getting lost several times I finally worked out the key to all the ladders and chains around this death infested place. All ropes and chains will take you to the start of the level that you are on. However most ladders will simply take you to the level above. The one exception is the ladder in the bottom level treasure room, which appears to lead through several extremely twisty passages and eventually takes you out of the dungeon completely. The portals may be used if you are of sufficient level or have already claimed your reward from the treasure room." + }, + { + header: "Diary", + text: "Day 1\nToday I set out to find out more about this place. From my research I knew about the strange creatures, so I have prepared with some good armour.\n\nDay 2\nI have fought my way through the fearsome beasts on the first level and am preparing myself to journey deeper. I hope that things are not too difficult further on as I am already sick of bread and cheese for dinner.\n\nDay 3\nI ventured down into the famine level today... I was wounded and have returned to the relative safety of the level above. I am going to try to make my way out through the goblins and mancow things... I hope I can make it..." + } + ] +} diff --git a/data/config/items/dungeons/stronghold-of-security/stronghold-of-security-items.json b/data/config/items/dungeons/stronghold-of-security/stronghold-of-security-items.json new file mode 100644 index 000000000..d07870b15 --- /dev/null +++ b/data/config/items/dungeons/stronghold-of-security/stronghold-of-security-items.json @@ -0,0 +1,11 @@ +{ + "rs:Stronghold_notes": { + "game_id": 9004 + }, + "rs:Fancy_boots": { + "game_id": 9005 + }, + "rs:Fighting_boots": { + "game_id": 9006 + } +} diff --git a/data/config/music/musicRegions.json b/data/config/music/musicRegions.json index aab659f8f..29fca08ea 100644 --- a/data/config/music/musicRegions.json +++ b/data/config/music/musicRegions.json @@ -603,7 +603,7 @@ "songName": "Dance of Death", "musicTabButtonId": 436, "regionIds": [ - 0 + 9297 ] }, { @@ -762,7 +762,7 @@ "songName": "Dogs of War", "musicTabButtonId": 437, "regionIds": [ - 0 + 7505 ] }, { @@ -1076,7 +1076,7 @@ "songName": "Food For Thought", "musicTabButtonId": 438, "regionIds": [ - 0 + 8017 ] }, { @@ -1824,7 +1824,7 @@ "songName": "Malady", "musicTabButtonId": 439, "regionIds": [ - 0 + 8530 ] }, { diff --git a/data/config/npc-spawns/dungeons/catacomb-of-famine.json b/data/config/npc-spawns/dungeons/catacomb-of-famine.json new file mode 100644 index 000000000..1dc8da70a --- /dev/null +++ b/data/config/npc-spawns/dungeons/catacomb-of-famine.json @@ -0,0 +1,1033 @@ +[ + { + "npc": "rs:Flesh Crawler_2", + "movement_radius": 10, + "face": "WEST", + "id": 2500, + "spawn_level": 0, + "spawn_x": 1988, + "spawn_y": 5235 + }, + { + "npc": "rs:Flesh Crawler_2", + "movement_radius": 10, + "face": "WEST", + "id": 2500, + "spawn_level": 0, + "spawn_x": 1989, + "spawn_y": 5239 + }, + { + "npc": "rs:Flesh Crawler_2", + "movement_radius": 10, + "face": "WEST", + "id": 2500, + "spawn_level": 0, + "spawn_x": 1991, + "spawn_y": 5242 + }, + { + "npc": "rs:Flesh Crawler_2", + "movement_radius": 10, + "face": "WEST", + "id": 2500, + "spawn_level": 0, + "spawn_x": 1992, + "spawn_y": 5235 + }, + { + "npc": "rs:Flesh Crawler_2", + "movement_radius": 10, + "face": "WEST", + "id": 2500, + "spawn_level": 0, + "spawn_x": 1994, + "spawn_y": 5239 + }, + { + "npc": "rs:Flesh Crawler_2", + "movement_radius": 10, + "face": "WEST", + "id": 2500, + "spawn_level": 0, + "spawn_x": 1994, + "spawn_y": 5241 + }, + { + "npc": "rs:Flesh Crawler_2", + "movement_radius": 10, + "face": "WEST", + "id": 2500, + "spawn_level": 0, + "spawn_x": 2003, + "spawn_y": 5202 + }, + { + "npc": "rs:Flesh Crawler_2", + "movement_radius": 10, + "face": "WEST", + "id": 2500, + "spawn_level": 0, + "spawn_x": 2003, + "spawn_y": 5205 + }, + { + "npc": "rs:Flesh Crawler_2", + "movement_radius": 10, + "face": "WEST", + "id": 2500, + "spawn_level": 0, + "spawn_x": 2005, + "spawn_y": 5199 + }, + { + "npc": "rs:Flesh Crawler_2", + "movement_radius": 10, + "face": "WEST", + "id": 2500, + "spawn_level": 0, + "spawn_x": 2006, + "spawn_y": 5202 + }, + { + "npc": "rs:Flesh Crawler_2", + "movement_radius": 10, + "face": "WEST", + "id": 2500, + "spawn_level": 0, + "spawn_x": 2006, + "spawn_y": 5205 + }, + { + "npc": "rs:Flesh Crawler_2", + "movement_radius": 10, + "face": "WEST", + "id": 2500, + "spawn_level": 0, + "spawn_x": 2008, + "spawn_y": 5201 + }, + { + "npc": "rs:Flesh Crawler_2", + "movement_radius": 10, + "face": "WEST", + "id": 2500, + "spawn_level": 0, + "spawn_x": 2008, + "spawn_y": 5204 + }, + { + "npc": "rs:Flesh Crawler_1", + "spawn_x": 2037, + "spawn_y": 5193, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Flesh Crawler_1", + "spawn_x": 2038, + "spawn_y": 5187, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Flesh Crawler_1", + "spawn_x": 2040, + "spawn_y": 5186, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Flesh Crawler_1", + "spawn_x": 2041, + "spawn_y": 5190, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Flesh Crawler_1", + "spawn_x": 2042, + "spawn_y": 5192, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Flesh Crawler_1", + "spawn_x": 2043, + "spawn_y": 5186, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Flesh Crawler_1", + "spawn_x": 2044, + "spawn_y": 5189, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Flesh Crawler_1", + "spawn_x": 2045, + "spawn_y": 5193, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Flesh Crawler", + "spawn_x": 2013, + "spawn_y": 5237, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Flesh Crawler", + "spawn_x": 2021, + "spawn_y": 5237, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Flesh Crawler", + "spawn_x": 2032, + "spawn_y": 5231, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Flesh Crawler", + "spawn_x": 2040, + "spawn_y": 5231, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Flesh Crawler", + "spawn_x": 2045, + "spawn_y": 5233, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Giant rat_7", + "spawn_x": 1988, + "spawn_y": 5189, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Giant rat_7", + "spawn_x": 1989, + "spawn_y": 5192, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + + { + "npc": "rs:Giant rat_7", + "spawn_x": 2015, + "spawn_y": 5236, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + + { + "npc": "rs:Giant rat_7", + "spawn_x": 2037, + "spawn_y": 5211, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Giant rat_7", + "spawn_x": 1990, + "spawn_y": 5190, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Giant rat_7", + "spawn_x": 1992, + "spawn_y": 5188, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Giant rat_7", + "spawn_x": 2019, + "spawn_y": 5235, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Giant rat_7", + "spawn_x": 2040, + "spawn_y": 5215, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Giant rat_7", + "spawn_x": 1995, + "spawn_y": 5190, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Giant rat_7", + "spawn_x": 2019, + "spawn_y": 5238, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1987, + "spawn_y": 5202, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1988, + "spawn_y": 5200, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1988, + "spawn_y": 5205, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1989, + "spawn_y": 5223, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1989, + "spawn_y": 5226, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1989, + "spawn_y": 5236, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1990, + "spawn_y": 5228, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1990, + "spawn_y": 5237, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1991, + "spawn_y": 5233, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1992, + "spawn_y": 5200, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1992, + "spawn_y": 5242, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1993, + "spawn_y": 5191, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1993, + "spawn_y": 5199, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1993, + "spawn_y": 5235, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1994, + "spawn_y": 5207, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1994, + "spawn_y": 5211, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1994, + "spawn_y": 5218, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1995, + "spawn_y": 5214, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 1995, + "spawn_y": 5239, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2000, + "spawn_y": 5187, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2004, + "spawn_y": 5186, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2004, + "spawn_y": 5189, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2004, + "spawn_y": 5204, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2004, + "spawn_y": 5207, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2005, + "spawn_y": 5190, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2007, + "spawn_y": 5203, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2009, + "spawn_y": 5187, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2012, + "spawn_y": 5194, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2013, + "spawn_y": 5236, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2014, + "spawn_y": 5190, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2014, + "spawn_y": 5192, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2015, + "spawn_y": 5186, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2017, + "spawn_y": 5192, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2018, + "spawn_y": 5190, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2018, + "spawn_y": 5238, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2022, + "spawn_y": 5188, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2024, + "spawn_y": 5193, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2027, + "spawn_y": 5188, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2027, + "spawn_y": 5192, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2028, + "spawn_y": 5232, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2029, + "spawn_y": 5185, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2030, + "spawn_y": 5235, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2030, + "spawn_y": 5244, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2031, + "spawn_y": 5191, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2031, + "spawn_y": 5243, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2032, + "spawn_y": 5230, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2032, + "spawn_y": 5242, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2033, + "spawn_y": 5242, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2033, + "spawn_y": 5246, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2034, + "spawn_y": 5234, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2034, + "spawn_y": 5244, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2036, + "spawn_y": 5202, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2038, + "spawn_y": 5207, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2040, + "spawn_y": 5190, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2041, + "spawn_y": 5186, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2042, + "spawn_y": 5200, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2042, + "spawn_y": 5204, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2044, + "spawn_y": 5190, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2044, + "spawn_y": 5193, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Rat_16", + "spawn_x": 2046, + "spawn_y": 5201, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_18", + "spawn_x": 2029, + "spawn_y": 5236, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_18", + "spawn_x": 2031, + "spawn_y": 5230, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_18", + "spawn_x": 2031, + "spawn_y": 5235, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_18", + "spawn_x": 2026, + "spawn_y": 5234, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_18", + "spawn_x": 2031, + "spawn_y": 5233, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 1988, + "spawn_y": 5192, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 1990, + "spawn_y": 5188, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 1993, + "spawn_y": 5190, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 2003, + "spawn_y": 5200, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 2005, + "spawn_y": 5201, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 2007, + "spawn_y": 5209, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 2039, + "spawn_y": 5212, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 2039, + "spawn_y": 5215, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 2039, + "spawn_y": 5218, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 2040, + "spawn_y": 5213, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 2041, + "spawn_y": 5218, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 2042, + "spawn_y": 5213, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 2042, + "spawn_y": 5215, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 2043, + "spawn_y": 5215, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_19", + "spawn_x": 2043, + "spawn_y": 5217, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_20", + "spawn_x": 1988, + "spawn_y": 5242, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_20", + "spawn_x": 1990, + "spawn_y": 5234, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_20", + "spawn_x": 1992, + "spawn_y": 5238, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_20", + "spawn_x": 2008, + "spawn_y": 5190, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_20", + "spawn_x": 2014, + "spawn_y": 5187, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_20", + "spawn_x": 2023, + "spawn_y": 5193, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_20", + "spawn_x": 2025, + "spawn_y": 5189, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_20", + "spawn_x": 1988, + "spawn_y": 5237, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_20", + "spawn_x": 2018, + "spawn_y": 5186, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_20", + "spawn_x": 2027, + "spawn_y": 5186, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + }, + { + "npc": "rs:Zombie_20", + "spawn_x": 2029, + "spawn_y": 5191, + "spawn_level": 0, + "face": "WEST", + "movement_radius": 10 + } +] diff --git a/data/config/npc-spawns/dungeons/sepulchre-of-death.json b/data/config/npc-spawns/dungeons/sepulchre-of-death.json new file mode 100644 index 000000000..ca5f1efce --- /dev/null +++ b/data/config/npc-spawns/dungeons/sepulchre-of-death.json @@ -0,0 +1,26 @@ +[ + {"npc":"rs:Shade","spawn_level":0,"spawn_x":2356,"spawn_y":5216,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade","spawn_level":0,"spawn_x":2357,"spawn_y":5213,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade","spawn_level":0,"spawn_x":2363,"spawn_y":5212,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade","spawn_level":0,"spawn_x":2365,"spawn_y":5216,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_1","spawn_level":0,"spawn_x":2356,"spawn_y":5216,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_1","spawn_level":0,"spawn_x":2357,"spawn_y":5213,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_1","spawn_level":0,"spawn_x":2363,"spawn_y":5212,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_1","spawn_level":0,"spawn_x":2365,"spawn_y":5216,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_2","spawn_level":0,"spawn_x":2356,"spawn_y":5216,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_2","spawn_level":0,"spawn_x":2357,"spawn_y":5213,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_2","spawn_level":0,"spawn_x":2363,"spawn_y":5212,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_2","spawn_level":0,"spawn_x":2365,"spawn_y":5216,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_3","spawn_level":0,"spawn_x":2356,"spawn_y":5216,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_3","spawn_level":0,"spawn_x":2357,"spawn_y":5213,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_3","spawn_level":0,"spawn_x":2363,"spawn_y":5212,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_3","spawn_level":0,"spawn_x":2365,"spawn_y":5216,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_4","spawn_level":0,"spawn_x":2356,"spawn_y":5216,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_4","spawn_level":0,"spawn_x":2357,"spawn_y":5213,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_4","spawn_level":0,"spawn_x":2363,"spawn_y":5212,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_4","spawn_level":0,"spawn_x":2365,"spawn_y":5216,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_5","spawn_level":0,"spawn_x":2356,"spawn_y":5216,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_5","spawn_level":0,"spawn_x":2357,"spawn_y":5213,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_5","spawn_level":0,"spawn_x":2363,"spawn_y":5212,"movement_radius":6,"face":"WEST"} +,{"npc":"rs:Shade_5","spawn_level":0,"spawn_x":2365,"spawn_y":5216,"movement_radius":6,"face":"WEST"} +] diff --git a/data/config/npc-spawns/dungeons/stronghold-of-security.json b/data/config/npc-spawns/dungeons/stronghold-of-security.json new file mode 100644 index 000000000..908ccb1bd --- /dev/null +++ b/data/config/npc-spawns/dungeons/stronghold-of-security.json @@ -0,0 +1,323 @@ +[ + {"npc":"rs:Ankou","spawn_level":0,"spawn_x":2355,"spawn_y":5241,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou","spawn_level":0,"spawn_x":2357,"spawn_y":5240,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou","spawn_level":0,"spawn_x":2358,"spawn_y":5243,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou","spawn_level":0,"spawn_x":2358,"spawn_y":5245,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou","spawn_level":0,"spawn_x":2359,"spawn_y":5239,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou","spawn_level":0,"spawn_x":2359,"spawn_y":5241,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou","spawn_level":0,"spawn_x":2361,"spawn_y":5240,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou","spawn_level":0,"spawn_x":2361,"spawn_y":5242,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou","spawn_level":0,"spawn_x":2363,"spawn_y":5240,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_1","spawn_level":0,"spawn_x":2324,"spawn_y":5198,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_1","spawn_level":0,"spawn_x":2325,"spawn_y":5197,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_1","spawn_level":0,"spawn_x":2326,"spawn_y":5201,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_1","spawn_level":0,"spawn_x":2326,"spawn_y":5205,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_1","spawn_level":0,"spawn_x":2326,"spawn_y":5206,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_1","spawn_level":0,"spawn_x":2327,"spawn_y":5199,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_1","spawn_level":0,"spawn_x":2327,"spawn_y":5202,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_1","spawn_level":0,"spawn_x":2328,"spawn_y":5197,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_1","spawn_level":0,"spawn_x":2329,"spawn_y":5203,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_1","spawn_level":0,"spawn_x":2330,"spawn_y":5195,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_1","spawn_level":0,"spawn_x":2331,"spawn_y":5202,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_2","spawn_level":0,"spawn_x":2315,"spawn_y":5229,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_2","spawn_level":0,"spawn_x":2317,"spawn_y":5226,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_2","spawn_level":0,"spawn_x":2318,"spawn_y":5230,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_2","spawn_level":0,"spawn_x":2320,"spawn_y":5224,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_2","spawn_level":0,"spawn_x":2321,"spawn_y":5232,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_2","spawn_level":0,"spawn_x":2322,"spawn_y":5222,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_2","spawn_level":0,"spawn_x":2322,"spawn_y":5227,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_2","spawn_level":0,"spawn_x":2322,"spawn_y":5229,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_2","spawn_level":0,"spawn_x":2323,"spawn_y":5224,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_2","spawn_level":0,"spawn_x":2324,"spawn_y":5230,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_2","spawn_level":0,"spawn_x":2324,"spawn_y":5236,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ankou_2","spawn_level":0,"spawn_x":2327,"spawn_y":5227,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_16","spawn_level":0,"spawn_x":2326,"spawn_y":5199,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_16","spawn_level":0,"spawn_x":2326,"spawn_y":5208,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_16","spawn_level":0,"spawn_x":2332,"spawn_y":5202,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_16","spawn_level":0,"spawn_x":2350,"spawn_y":5200,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_16","spawn_level":0,"spawn_x":2352,"spawn_y":5195,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_16","spawn_level":0,"spawn_x":2353,"spawn_y":5192,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_16","spawn_level":0,"spawn_x":2355,"spawn_y":5198,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_17","spawn_level":0,"spawn_x":2328,"spawn_y":5205,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_17","spawn_level":0,"spawn_x":2329,"spawn_y":5200,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_17","spawn_level":0,"spawn_x":2348,"spawn_y":5197,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_17","spawn_level":0,"spawn_x":2353,"spawn_y":5203,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_17","spawn_level":0,"spawn_x":2354,"spawn_y":5191,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_18","spawn_level":0,"spawn_x":2311,"spawn_y":5187,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Skeleton_18","spawn_level":0,"spawn_x":2311,"spawn_y":5193,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_8","spawn_level":0,"spawn_x":2352,"spawn_y":5189,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_8","spawn_level":0,"spawn_x":2353,"spawn_y":5197,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_8","spawn_level":0,"spawn_x":2356,"spawn_y":5204,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_8","spawn_level":0,"spawn_x":2357,"spawn_y":5194,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_9","spawn_level":0,"spawn_x":2305,"spawn_y":5233,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_9","spawn_level":0,"spawn_x":2305,"spawn_y":5241,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_9","spawn_level":0,"spawn_x":2306,"spawn_y":5229,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_9","spawn_level":0,"spawn_x":2306,"spawn_y":5243,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_9","spawn_level":0,"spawn_x":2309,"spawn_y":5246,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_9","spawn_level":0,"spawn_x":2311,"spawn_y":5237,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_9","spawn_level":0,"spawn_x":2314,"spawn_y":5237,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_9","spawn_level":0,"spawn_x":2314,"spawn_y":5244,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_9","spawn_level":0,"spawn_x":2315,"spawn_y":5236,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_9","spawn_level":0,"spawn_x":2316,"spawn_y":5227,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_9","spawn_level":0,"spawn_x":2322,"spawn_y":5235,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Ghost_9","spawn_level":0,"spawn_x":2325,"spawn_y":5228,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1859,"spawn_y":5193,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1859,"spawn_y":5203,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1861,"spawn_y":5192,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1861,"spawn_y":5200,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1861,"spawn_y":5229,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1861,"spawn_y":5231,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1862,"spawn_y":5188,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1862,"spawn_y":5204,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1863,"spawn_y":5227,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1864,"spawn_y":5202,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1865,"spawn_y":5188,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1866,"spawn_y":5204,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1868,"spawn_y":5189,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1869,"spawn_y":5226,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1870,"spawn_y":5192,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1871,"spawn_y":5232,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1872,"spawn_y":5190,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1872,"spawn_y":5218,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1872,"spawn_y":5239,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1873,"spawn_y":5188,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1874,"spawn_y":5200,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1874,"spawn_y":5210,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1876,"spawn_y":5189,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1876,"spawn_y":5216,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1877,"spawn_y":5220,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1879,"spawn_y":5198,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1879,"spawn_y":5230,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1879,"spawn_y":5234,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1880,"spawn_y":5200,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1880,"spawn_y":5239,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1880,"spawn_y":5243,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1881,"spawn_y":5232,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1881,"spawn_y":5241,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1882,"spawn_y":5199,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1882,"spawn_y":5201,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1882,"spawn_y":5243,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1883,"spawn_y":5206,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1883,"spawn_y":5234,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1884,"spawn_y":5230,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1884,"spawn_y":5232,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1885,"spawn_y":5205,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1886,"spawn_y":5231,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1887,"spawn_y":5203,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1889,"spawn_y":5206,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1890,"spawn_y":5242,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1895,"spawn_y":5242,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1898,"spawn_y":5241,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1900,"spawn_y":5240,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1900,"spawn_y":5244,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1904,"spawn_y":5235,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1908,"spawn_y":5203,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1909,"spawn_y":5243,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1910,"spawn_y":5202,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1911,"spawn_y":5205,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1912,"spawn_y":5238,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Rat_16","spawn_level":0,"spawn_x":1913,"spawn_y":5235,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon","spawn_level":0,"spawn_x":2142,"spawn_y":5252,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon","spawn_level":0,"spawn_x":2145,"spawn_y":5252,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon","spawn_level":0,"spawn_x":2145,"spawn_y":5256,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon","spawn_level":0,"spawn_x":2148,"spawn_y":5253,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon","spawn_level":0,"spawn_x":2149,"spawn_y":5255,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon","spawn_level":0,"spawn_x":2152,"spawn_y":5251,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon","spawn_level":0,"spawn_x":2153,"spawn_y":5254,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon","spawn_level":0,"spawn_x":2155,"spawn_y":5252,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_1","spawn_level":0,"spawn_x":2158,"spawn_y":5282,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_1","spawn_level":0,"spawn_x":2161,"spawn_y":5281,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_1","spawn_level":0,"spawn_x":2162,"spawn_y":5284,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_1","spawn_level":0,"spawn_x":2164,"spawn_y":5280,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2119,"spawn_y":5296,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2120,"spawn_y":5292,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2120,"spawn_y":5299,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2122,"spawn_y":5291,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2123,"spawn_y":5302,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2127,"spawn_y":5305,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2129,"spawn_y":5302,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2130,"spawn_y":5298,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2163,"spawn_y":5301,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2166,"spawn_y":5303,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2166,"spawn_y":5306,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2167,"spawn_y":5300,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2169,"spawn_y":5303,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Catablepon_2","spawn_level":0,"spawn_x":2170,"spawn_y":5306,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2120,"spawn_y":5275,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2121,"spawn_y":5273,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2123,"spawn_y":5270,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2123,"spawn_y":5275,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2124,"spawn_y":5272,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2124,"spawn_y":5274,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2125,"spawn_y":5270,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2126,"spawn_y":5274,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2127,"spawn_y":5272,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2129,"spawn_y":5268,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2129,"spawn_y":5270,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2130,"spawn_y":5272,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2131,"spawn_y":5267,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2142,"spawn_y":5256,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2143,"spawn_y":5251,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2145,"spawn_y":5253,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2145,"spawn_y":5306,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2146,"spawn_y":5259,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2146,"spawn_y":5306,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2147,"spawn_y":5255,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2147,"spawn_y":5305,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2147,"spawn_y":5307,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2149,"spawn_y":5304,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2149,"spawn_y":5305,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2149,"spawn_y":5306,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2150,"spawn_y":5307,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2150,"spawn_y":5308,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2151,"spawn_y":5268,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2151,"spawn_y":5305,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2151,"spawn_y":5308,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2152,"spawn_y":5268,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2152,"spawn_y":5306,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2153,"spawn_y":5270,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2153,"spawn_y":5305,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2153,"spawn_y":5306,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2153,"spawn_y":5307,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2154,"spawn_y":5269,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2154,"spawn_y":5271,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2154,"spawn_y":5273,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2154,"spawn_y":5305,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2155,"spawn_y":5254,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2155,"spawn_y":5305,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2157,"spawn_y":5271,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2157,"spawn_y":5273,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Giant spider_2","spawn_level":0,"spawn_x":2157,"spawn_y":5274,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2119,"spawn_y":5274,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2120,"spawn_y":5277,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2121,"spawn_y":5276,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2122,"spawn_y":5271,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2124,"spawn_y":5269,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2124,"spawn_y":5273,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2126,"spawn_y":5270,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2126,"spawn_y":5272,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2127,"spawn_y":5269,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2127,"spawn_y":5273,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2128,"spawn_y":5271,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2129,"spawn_y":5267,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2129,"spawn_y":5273,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2150,"spawn_y":5267,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2152,"spawn_y":5270,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2155,"spawn_y":5270,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2155,"spawn_y":5271,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2156,"spawn_y":5273,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2156,"spawn_y":5274,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2165,"spawn_y":5255,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2166,"spawn_y":5256,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2167,"spawn_y":5251,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2167,"spawn_y":5253,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2167,"spawn_y":5255,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2168,"spawn_y":5253,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2168,"spawn_y":5256,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2168,"spawn_y":5257,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2169,"spawn_y":5250,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2169,"spawn_y":5254,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2169,"spawn_y":5255,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2170,"spawn_y":5252,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2170,"spawn_y":5254,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2170,"spawn_y":5256,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2171,"spawn_y":5253,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Spider_5","spawn_level":0,"spawn_x":2172,"spawn_y":5254,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Scorpion_2","spawn_level":0,"spawn_x":2169,"spawn_y":5290,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Scorpion_2","spawn_level":0,"spawn_x":2170,"spawn_y":5285,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Scorpion_2","spawn_level":0,"spawn_x":2171,"spawn_y":5280,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Scorpion_2","spawn_level":0,"spawn_x":2172,"spawn_y":5274,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Scorpion_3","spawn_level":0,"spawn_x":2170,"spawn_y":5277,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Scorpion_3","spawn_level":0,"spawn_x":2170,"spawn_y":5288,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Scorpion_3","spawn_level":0,"spawn_x":2172,"spawn_y":5283,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Scorpion_3","spawn_level":0,"spawn_x":2173,"spawn_y":5272,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1862,"spawn_y":5190,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1863,"spawn_y":5189,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1867,"spawn_y":5188,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1870,"spawn_y":5235,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1871,"spawn_y":5191,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1871,"spawn_y":5242,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1872,"spawn_y":5227,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1873,"spawn_y":5212,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1873,"spawn_y":5238,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1874,"spawn_y":5216,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1875,"spawn_y":5214,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1875,"spawn_y":5218,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1878,"spawn_y":5216,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1879,"spawn_y":5218,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1880,"spawn_y":5217,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur","spawn_level":0,"spawn_x":1880,"spawn_y":5220,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1890,"spawn_y":5195,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1890,"spawn_y":5243,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1893,"spawn_y":5189,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1893,"spawn_y":5198,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1893,"spawn_y":5238,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1893,"spawn_y":5241,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1894,"spawn_y":5243,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1897,"spawn_y":5196,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1897,"spawn_y":5244,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1899,"spawn_y":5198,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1899,"spawn_y":5242,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1900,"spawn_y":5208,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1900,"spawn_y":5210,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1901,"spawn_y":5191,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1901,"spawn_y":5197,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1901,"spawn_y":5201,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1901,"spawn_y":5206,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1902,"spawn_y":5193,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1902,"spawn_y":5199,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1902,"spawn_y":5204,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1902,"spawn_y":5207,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1902,"spawn_y":5242,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Minotaur_2","spawn_level":0,"spawn_x":1904,"spawn_y":5191,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_47","spawn_level":0,"spawn_x":1860,"spawn_y":5205,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_47","spawn_level":0,"spawn_x":1860,"spawn_y":5222,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_47","spawn_level":0,"spawn_x":1860,"spawn_y":5226,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_47","spawn_level":0,"spawn_x":1862,"spawn_y":5201,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_48","spawn_level":0,"spawn_x":1859,"spawn_y":5201,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_48","spawn_level":0,"spawn_x":1860,"spawn_y":5215,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_48","spawn_level":0,"spawn_x":1862,"spawn_y":5220,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_48","spawn_level":0,"spawn_x":1863,"spawn_y":5215,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_48","spawn_level":0,"spawn_x":1863,"spawn_y":5217,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_48","spawn_level":0,"spawn_x":1864,"spawn_y":5218,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_49","spawn_level":0,"spawn_x":1859,"spawn_y":5219,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_49","spawn_level":0,"spawn_x":1861,"spawn_y":5224,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_49","spawn_level":0,"spawn_x":1862,"spawn_y":5228,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_49","spawn_level":0,"spawn_x":1864,"spawn_y":5204,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_50","spawn_level":0,"spawn_x":1884,"spawn_y":5207,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_50","spawn_level":0,"spawn_x":1885,"spawn_y":5203,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_50","spawn_level":0,"spawn_x":1891,"spawn_y":5236,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_50","spawn_level":0,"spawn_x":1894,"spawn_y":5229,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_50","spawn_level":0,"spawn_x":1910,"spawn_y":5236,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_50","spawn_level":0,"spawn_x":1913,"spawn_y":5239,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_51","spawn_level":0,"spawn_x":1876,"spawn_y":5198,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_51","spawn_level":0,"spawn_x":1884,"spawn_y":5201,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_51","spawn_level":0,"spawn_x":1887,"spawn_y":5205,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_51","spawn_level":0,"spawn_x":1893,"spawn_y":5235,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_51","spawn_level":0,"spawn_x":1895,"spawn_y":5228,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_51","spawn_level":0,"spawn_x":1896,"spawn_y":5232,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_51","spawn_level":0,"spawn_x":1911,"spawn_y":5240,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_51","spawn_level":0,"spawn_x":1912,"spawn_y":5244,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_51","spawn_level":0,"spawn_x":1913,"spawn_y":5236,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_52","spawn_level":0,"spawn_x":1877,"spawn_y":5199,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_52","spawn_level":0,"spawn_x":1892,"spawn_y":5226,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_52","spawn_level":0,"spawn_x":1894,"spawn_y":5233,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_52","spawn_level":0,"spawn_x":1905,"spawn_y":5235,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Goblin_52","spawn_level":0,"spawn_x":1912,"spawn_y":5242,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_3","spawn_level":0,"spawn_x":1870,"spawn_y":5226,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_3","spawn_level":0,"spawn_x":1871,"spawn_y":5229,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_3","spawn_level":0,"spawn_x":1871,"spawn_y":5236,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_3","spawn_level":0,"spawn_x":1872,"spawn_y":5233,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_3","spawn_level":0,"spawn_x":1873,"spawn_y":5239,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_4","spawn_level":0,"spawn_x":1886,"spawn_y":5188,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_4","spawn_level":0,"spawn_x":1887,"spawn_y":5221,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_4","spawn_level":0,"spawn_x":1889,"spawn_y":5187,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_4","spawn_level":0,"spawn_x":1889,"spawn_y":5191,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_4","spawn_level":0,"spawn_x":1889,"spawn_y":5214,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_4","spawn_level":0,"spawn_x":1889,"spawn_y":5217,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_4","spawn_level":0,"spawn_x":1890,"spawn_y":5224,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_4","spawn_level":0,"spawn_x":1892,"spawn_y":5217,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_4","spawn_level":0,"spawn_x":1892,"spawn_y":5220,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_4","spawn_level":0,"spawn_x":1894,"spawn_y":5193,"movement_radius": 10,"face":"WEST"} +,{"npc":"rs:Wolf_4","spawn_level":0,"spawn_x":1897,"spawn_y":5189,"movement_radius": 10,"face":"WEST"} +] diff --git a/data/config/npcs/dungeons/stronghold-of-security.json b/data/config/npcs/dungeons/stronghold-of-security.json new file mode 100644 index 000000000..1b30981b0 --- /dev/null +++ b/data/config/npcs/dungeons/stronghold-of-security.json @@ -0,0 +1,137 @@ +{ + "rs:Gate_of_War": { + "game_id": 4377 + }, + "rs:Ricketty_door": { + "game_id": 4378 + }, + "rs:Oozing_barrier": { + "game_id": 4379 + }, + "rs:Portal_of_Death": { + "game_id": 4380 + }, + "rs:Ankou": { + "game_id": 4381 + }, + "rs:Ankou_1": { + "game_id": 4382 + }, + "rs:Ankou_2": { + "game_id": 4383 + }, + "rs:Skeleton_16": { + "game_id": 4384 + }, + "rs:Skeleton_17": { + "game_id": 4385 + }, + "rs:Skeleton_18": { + "game_id": 4386 + }, + "rs:Ghost_8": { + "game_id": 4387 + }, + "rs:Ghost_9": { + "game_id": 4388 + }, + "rs:Flesh Crawler": { + "game_id": 4389 + }, + "rs:Flesh Crawler_1": { + "game_id": 4390 + }, + "rs:Flesh Crawler_2": { + "game_id": 4391 + }, + "rs:Zombie_18": { + "game_id": 4392 + }, + "rs:Zombie_19": { + "game_id": 4393 + }, + "rs:Zombie_20": { + "game_id": 4394 + }, + "rs:Giant rat_7": { + "game_id": 4395 + }, + "rs:Rat_16": { + "game_id": 4396 + }, + "rs:Catablepon": { + "game_id": 4397 + }, + "rs:Catablepon_1": { + "game_id": 4398 + }, + "rs:Catablepon_2": { + "game_id": 4399 + }, + "rs:Giant spider_2": { + "game_id": 4400 + }, + "rs:Spider_5": { + "game_id": 4401 + }, + "rs:Scorpion_2": { + "game_id": 4402 + }, + "rs:Scorpion_3": { + "game_id": 4403 + }, + "rs:Minotaur": { + "game_id": 4404 + }, + "rs:Minotaur_1": { + "game_id": 4405 + }, + "rs:Minotaur_2": { + "game_id": 4406 + }, + "rs:Goblin_47": { + "game_id": 4407 + }, + "rs:Goblin_48": { + "game_id": 4408 + }, + "rs:Goblin_49": { + "game_id": 4409 + }, + "rs:Goblin_50": { + "game_id": 4410 + }, + "rs:Goblin_51": { + "game_id": 4411 + }, + "rs:Goblin_52": { + "game_id": 4412 + }, + "rs:Wolf_3": { + "game_id": 4413 + }, + "rs:Wolf_4": { + "game_id": 4414 + }, + "rs:Rat_17": { + "game_id": 4415 + }, + "rs:Shade": { + "game_id": 425 + }, + "rs:Shade_1": { + "game_id": 426 + }, + "rs:Shade_2": { + "game_id": 427 + }, + "rs:Shade_3": { + "game_id": 428 + }, + "rs:Shade_4": { + "game_id": 429 + }, + "rs:Shade_5": { + "game_id": 430 + } +} diff --git a/data/config/stronghold-of-security-quiz.json5 b/data/config/stronghold-of-security-quiz.json5 new file mode 100644 index 000000000..60fb9c520 --- /dev/null +++ b/data/config/stronghold-of-security-quiz.json5 @@ -0,0 +1,470 @@ +{ + prefix: "To pass you must answer me this: ", + questions: [ + { + questionText: "How often should you change your recovery questions?", + options: [ + { + optionText: "Never", + passable: false, + doorResponse: "Correct! This is the ideal, every few months change your questions, but make sure you can remember the answers! Don't use personal details for your recoveries." + }, + { + optionText: "Every day", + passable: false, + doorResponse: "Normally recovery questions will take 14 days to become active, so there's no point in changing them everyday! Don't use personal details for your recoveries." + }, + { + optionText: "Every couple of months", + passable: true, + doorResponse: "Correct! This is the ideal, every few months change your questions, but make sure you can remember the answers! Don't use personal details for your recoveries." + } + ] + }, + { + questionText: "What do I do if a moderator asks me for my account details?", + options: [ + { + optionText: "Tell them whatever they want to know.", + passable: false, + doorResponse: "Wrong! Never give your account details to anyone! This includes things like account creation details, contact details and passwords. Never use personal details for passwords or bank PINs!" + }, + { + optionText: "Politely tell them no.", + passable: true, + doorResponse: "Ok! Don't tell them the details. But reporting the incident to Jagex would help. Use the Report Abuse button. Never use personal details for passwords or bank PINs!" + }, + { + optionText: "Politely tell them no then use the report abuse button.", + passable: true, + doorResponse: "Correct! Report any attempt to gain your account details as it is a very serious breach of RuneScape's rules. Never use personal details for security answers or bank PINs!" + } + ] + }, + { + questionText: "Who can I give my password to?", + options: [ + { + optionText: "My friends.", + passable: false, + doorResponse: "Wrong! Your password should be kept secret from everyone. You should *never* give it out under any circumstances." + }, + { + optionText: "My brother or sister.", + passable: false, + doorResponse: "Wrong! Your password should be kept secret from everyone. You should *never* give it out under any circumstances." + }, + { + optionText: "Nobody.", + passable: true, + doorResponse: "Correct! Your password should be kept secret from everyone. You should *never* give it out under any circumstances." + } + ] + }, + { + questionText: "How do I set a bank PIN?", + options: [ + { + optionText: "Use the account management section on the website.", + passable: false, + doorResponse: "Wrong! Your password can be changed from the account management section, but you must talk to a banker to set a bank PIN. Never use personal details for passwords or bank PINs!" + }, + { + optionText: "Talk to any banker.", + passable: true, + doorResponse: "Correct! Simply talking to a banker will give you the option to set a bank PIN. Never use personal details for passwords or bank PINs!" + } + ] + }, + { + questionText: "Who is it ok to share my account with?", + options: [ + { + optionText: "My friends.", + passable: false, + doorResponse: "Wrong! Your account may only be used by you." + }, + { + optionText: "My relatives.", + passable: false, + doorResponse: "Wrong! Your account may only be used by you." + }, + { + optionText: "Nobody.", + passable: true, + doorResponse: "Correct! Your account may only be used by you." + } + ] + }, + { + questionText: "My friend asks me for my password so that he can do a difficult quest for me. Do I give it to him?", + options: [ + { + optionText: "Yes. He is my best friend and I already spent ages trying this quest.", + passable: false, + doorResponse: "Wrong! Don't give your password to anyone otherwise you can lose everything you have worked so hard for." + }, + { + optionText: "Don't give him my passwoord.", + passable: true, + doorResponse: "Correct! You can make it alone and the success will taste even better. Don't forget you can ask people for advice too!" + }, + { + optionText: "Let him do the quest but in the same room the whole time.", + passable: false, + doorResponse: "Wrong! Never let anyone use your account for any reason. It only takes a few seconds to change your password." + } + ] + }, + { + questionText: "Will Jagex block me from saying my PIN in game?", + options: [ + { + optionText: "Yes.", + passable: false, + doorResponse: "Wrong! Jagex does NOT block your PIN so don't type it! Never use personal details for recoveries or bank PINs!" + }, + { + optionText: "No.", + passable: true, + doorResponse: "Correct! Jagex will not block your PIN so don't type it! Never use personal details for recoveries or bank PINs!" + } + ] + }, + { + questionText: "What do I do if I think I have a keylogger or a virus?", + options: [ + { + optionText: "Virus scan my computer then change my password and recoveries.", + passable: true, + doorResponse: "Correct! Removing the keylogger must be the priority, otherwise anything you type can be given away. Remember to change your password and bank PIN afterwards." + }, + { + optionText: "Change my password then virus scan my computer.", + passable: false, + doorResponse: "Wrong! If you change your password while you still have the keylogger, it will still be insecure. Remove the keylogger first. Never use personal details for passwords or bank PINs!" + }, + { + optionText: "Nothing. It will go away on its own.", + passable: false, + doorResponse: "Wrong! This could mean your account may be accessed by someone else. Remove the keylogger then change your password. Never use personal details for security answers or bank PINs!" + } + ] + }, + { + questionText: "A website says I can become a player moderator by giving them my password, what do I do?", + options: [ + { + optionText: "Nothing.", + passable: false, + doorResponse: "Quite good. But we should try to stop scammers. So please reoport any attempt to gain your account details as it is a very serious breach of RuneScape's rules. Never use personal details for passwords or bank PINs!" + }, + { + optionText: "Give them my password.", + passable: false, + doorResponse: "Wrong! This will almost certainly lead to your account being hijacked. No website can make you a moderator as they are hand picked by Jagex." + }, + { + optionText: "Don't tell them anything and imform Jagex through the game website.", + passable: true, + doorResponse: "Correct! By informing us we can have the site taken down so other people will not have their accounts hijacked by this scam. Remember that moderators are hand picked by Jagex." + } + ] + }, + { + questionText: "What do you do if someone asks you for your password or recoveries to make you a member for free?", + options: [ + { + optionText: "Give them the information they asked for.", + passable: false, + doorResponse: "Wrong! Never give your account details to anyone! This includes things like account creation details, contact details and passwords. Never use personal details for passwords or bank PINs!" + }, + { + optionText: "Don't tell them anything and ignore them.", + passable: true, + doorResponse: "Quite good. But we should try to stop scammers. So please report them using the 'Report Abuse' button." + }, + { + optionText: "Don't tell them anything and click the 'Report Abuse' button.", + passable: true, + doorResponse: "Correct! Press the 'Report Abuse' button and fill in the offending player's name and the correct category." + } + ] + }, + { + questionText: "Where should I enter my RuneScape password?", + options: [ + { + optionText: "On RuneScape and all fansites.", + passable: false, + doorResponse: "Wrong! Always use a unique password purely for your RuneScape account." + }, + { + optionText: "Only on the RuneScape website.", + passable: true, + doorResponse: "Correct! Always make sure you are entering password only on the RuneScape website as other sites may try to steal it." + }, + { + optionText: "On all websites I visit.", + passable: false, + doorResponse: "Wrong! This is very insecure and will may lead to your account being stolen.!" + } + ] + }, + { + questionText: "Where can I find cheats for RuneScape?", + options: [ + { + optionText: "On the RuneScape website.", + passable: false, + doorResponse: "Wrong! There are NO RuneScape cheats coded into the game. Any sites claiming to have cheats are fakes and may lead to your account being stolen if you given them your password." + }, + { + optionText: "By searching the internet.", + passable: false, + doorResponse: "Wrong! There are NO RuneScape cheats coded into the game. Any sites claiming to have cheats are fakes and may lead to your account being stolen if you given them your password." + }, + { + optionText: "Nowhere.", + passable: true, + doorResponse: "Correct! There are NO RuneScape cheats coded into the game. Any sites claiming to have cheats are fakes and may lead to your account being stolen if you given them your password." + } + ] + }, + { + questionText: "What do you do if someone asks you for your password or recoveries to make you a player moderator?", + options: [ + { + optionText: "Don't give them the information and send a 'Abuse report'.", + passable: true, + doorResponse: "Correct! Use the 'Report Abuse' button and fill in the offending player's name and the correct category." + }, + { + optionText: "Don't tell them anything and ignore them.", + passable: true, + doorResponse: "Quite good. But we should try to stop scammers. So please report them using the 'Report Abuse' button." + }, + { + optionText: "Give them the information they asked for.", + passable: false, + doorResponse: "Wrong! Jagex never ask for your account information - especially to become a player moderator. Press the 'Report Abuse' button and fill in the offending player's name and the correct category." + } + ] + }, + { + questionText: "Why do I need to type in recovery questions?", + options: [ + { + optionText: "To help me recover my password if I forget it or it is stolen.", + passable: true, + doorResponse: "Correct! Your recovery questions will help Jagex staff protect and return your account if it is stolen. Never use personal details for recoveries or bank PINs!" + }, + { + optionText: "To let Jagex know more about its players.", + passable: false, + doorResponse: "INCORRECTRESPONSE" + }, + { + optionText: "To see if I can type in random letters on my keyboard.", + passable: false, + doorResponse: "Incorrect! You should always remember this info so your account is as safe as possible." + } + ] + }, + { + questionText: "What should I do if I think someone knows my recovery answers?", + options: [ + { + optionText: "Tell them never to use them.", + passable: false, + doorResponse: "Wrong! Never give your account details to anyone! This includes things like account creation details, contact details and passwords. Never use personal details for passwords or bank PINs" + }, + { + optionText: "Use the Account Management section on the RuneScape website.", + passable: false, + doorResponse: "Wrong! If you use the Account Management section to change your recovery questions, it will take 14 days to come into effect, someone may have access to your account in this time." + }, + { + optionText: "Use the 'Recover a Lost Password' section on the RuneScape website.", + passable: true, + doorResponse: "Correct! If you provide all the correct information this will reset your questions within 24 hours and make your account secure again." + } + ] + }, + { + questionText: "Recovery answers should be...", + options: [ + { + optionText: "Memorable.", + passable: true, + doorResponse: "Correct! A good recovery answer that not many people will know, you won't forget, will stay the same and that you won't accidentally give away. Remember: don't use personal details for your recoveries." + }, + { + optionText: "Easy to guess.", + passable: false, + doorResponse: "This is a bad idea as anyone who knows you could guess them. Remember: Don't use personal details for your recoveries." + }, + { + optionText: "Random gibberish.", + passable: false, + doorResponse: "Wrong! A good recovery answer that not many people will know, you won't forget, will stay the same and that you won't accidentally give away. Remember: don't use personal details for your recoveries." + } + ] + }, + { + questionText: "What is an example of a good bank PIN?", + options: [ + { + optionText: "Your real life bank PIN.", + passable: false, + doorResponse: "This is a bad idea as if someone happen to find out your bank PIN on RuneScape, they then have your real life bank PIN! Never use personal details for security answers or bank PINs!" + }, + { + optionText: "Your birthday.", + passable: false, + doorResponse: "Not a good idea because you know how many presents you get for your birthday. So you can imagine how many people know this date. Never use personal details for recoveries or bank PINs!." + }, + { + optionText: "The birthday of a famous person or event.", + passable: true, + doorResponse: "Well done! Unless you tell someone, they are unlikely to guess who or what you have chosen, and you can always look it up. Never use personal details for recoveries or bank PINs!" + } + ] + }, + { + questionText: "How will Jagex contact me if I have been chosen to be a moderator?", + options: [ + { + optionText: "Email.", + passable: false, + doorResponse: "Incorrect. Jagex will never email you asking you to become a Moderator. We will contact you through your Message Inbox available on our website." + }, + { + optionText: "Website popup.", + passable: false, + doorResponse: "Incorrect, we will only contact our players via the game Inbox which you can access from our RuneScape website." + }, + { + optionText: "Game inbox on the RuneScape website.", + passable: true, + doorResponse: "Correct! We only contact our players via the game Inbox which you can access from our RuneScape website." + } + ] + }, + { + questionText: "What should you do if your real-life friend asks for your password so he can check your stats?", + options: [ + { + optionText: "Give them your password since they're a friend in real life.", + passable: false, + doorResponse: "Incorrect! Don't trust anybody with your account login details!" + }, + { + optionText: "Don't give out your password to anyone. Not even close friends.", + passable: true, + doorResponse: "Correct! Doing so could result in losing your items and gold and puts your account at risk." + }, + { + optionText: "Log in for your friend and let them play.", + passable: false, + doorResponse: "Incorrect! Never allow anybody else to access your account." + } + ] + }, + { + questionText: "Can I leave my account logged in while I'm out of the room?", + options: [ + { + optionText: "If I'm going to be quick.", + passable: false, + doorResponse: "Wrong! You should logout in case you are attacked or receive a random event. Leaving your character logged in can also allow someone to steal your items or entire account!" + }, + { + optionText: "Yes.", + passable: false, + doorResponse: "Wrong! You should logout in case you are attacked or receive a random event. Leaving your character logged in can also allow someone to steal your items or entire account!" + }, + { + optionText: "No.", + passable: true, + doorResponse: "Correct! This is the safest, both in terms of security and keeping your items! Leaving your character logged in can also allow someone to steal your items or entire account!" + } + ] + }, + { + questionText: "Does Jagex really hide your password if you accidentally say it in game?", + options: [ + { + optionText: "Yes - Jagex blocks your password.", + passable: false, + doorResponse: "Wrong! Jagex does NOT block your password so don't type it! Never use personal details for passwords or bank PINs!" + }, + { + optionText: "No - Jagex does not block your password.", + passable: true, + doorResponse: "Correct! We do not block your password. Do not say it in game as someone may steal your account!" + } + ] + }, + { + questionText: "My friend uses this great add-on program he got from a website, should I?", + options: [ + { + optionText: "No, it might steal my password.", + passable: true, + doorResponse: "Correct! The only safe add-on for RuneScape is the Windows client available from our RuneScape website." + }, + { + optionText: "I'll give it a try and see if I like it.", + passable: false, + doorResponse: "Wrong! The program may steal your password and is against the rules to use." + }, + { + optionText: "Sure, he's used it a lot, so can I.", + passable: false, + doorResponse: "Wrong! The program may steal your password and is against the rules to use." + } + ] + }, + { + questionText: "What do you do if someone tells you that you have won the RuneScape Lottery and asks you for your password or recoveries to award your prize?", + options: [ + { + optionText: "Give them the information they asked for.", + passable: false, + doorResponse: "Wrong! Never give your account details to anyone! This includes things like account creation details, contact details and passwords. Never use personal details for passwords or bank PINs!" + }, + { + optionText: "Don't tell them anything and ignore them.", + passable: true, + doorResponse: "Quite good. But we should try to stop scammers. So please report them using the 'Report Abuse' button." + }, + { + optionText: "Don't tell them anything and click the 'Report Abuse' button.", + passable: true, + doorResponse: "Correct! Press the 'Report Abuse' button and fill in the offending player's name and the correct category." + } + ] + }, + { + questionText: "What are recovery questions used for?", + options: [ + { + optionText: "Recovering your account if it is stolen.", + passable: true, + doorResponse: "Correct! If you set recovery questions that you can remember, you can use them to set a new password on your account if you forget the current one. Don't use personal details for your recoveries." + }, + { + optionText: "Recovering your account if you forget your password.", + passable: true, + doorResponse: "Correct! If you set recovery questions that you can remember, you can use them to set a new password on your account if you forget the current one. Don't use personal details for your recoveries." + }, + { + optionText: "Recovering your billing details.", + passable: false, + doorResponse: "INCORRECTRESPONSE" + } + ] + } + ] +} diff --git a/data/config/travel-locations-data.yaml b/data/config/travel-locations-data.yaml index 8ea3f67d6..229ef4141 100644 --- a/data/config/travel-locations-data.yaml +++ b/data/config/travel-locations-data.yaml @@ -1034,6 +1034,22 @@ x: 2329 y: 5097 z: 0 +- name: Vault of War + x: 1859 + y: 5243 + z: 0 +- name: Catacomb of Famine + x: 2042 + y: 5245 + z: 0 +- name: Pit of Pestilence + x: 2123 + y: 5251 + z: 0 +- name: Sepulchre of Death + x: 2358 + y: 5215 + z: 0 - name: Wizards' Guild x: 2583 y: 3078 diff --git a/data/config/widgets.json5 b/data/config/widgets.json5 index 6f4691631..67680cccc 100644 --- a/data/config/widgets.json5 +++ b/data/config/widgets.json5 @@ -76,6 +76,7 @@ questJournal: 275, questReward: 277, welcomeScreen: 378, + welcomeScreenConstruction: 405, welcomeScreenChildren: { cogs: 16, question: 17, @@ -87,5 +88,38 @@ christmas: 23, killcount: 24 }, - whatWouldYouLikeToSpin: 459 + whatWouldYouLikeToSpin: 459, + book: 27, + bookChildren: { + title: 3, + totalPageLineAmount: 14, + leftPage: { + pageTurnButton: 95, + clickableLines: { + firstLineId: 101, + incrementAmount: 2, + lastLine: 129 + }, + nonClickableLines: { + firstLineId: 33, + incrementAmount: 1, + lastLine: 47 + }, + pageNumber: 98 + }, + rightPage: { + pageTurnButton: 97, + clickableLines: { + firstLineId: 130, + incrementAmount: 2, + lastLine: 160 + }, + nonClickableLines: { + firstLineId: 48, + incrementAmount: 1, + lastLine: 62 + }, + pageNumber: 99 + } + } } diff --git a/data/config/xteas/455.json b/data/config/xteas/455.json new file mode 100644 index 000000000..f8bad3056 --- /dev/null +++ b/data/config/xteas/455.json @@ -0,0 +1,15 @@ +[ + { + "archive": 5, + "group": 779, + "name_hash": -1154306995, + "name": "l33_82", + "mapsquare": 8530, + "key": [ + -944370155, + 454273692, + -1617260039, + -2120635933 + ] + } +] \ No newline at end of file diff --git a/src/game-engine/config/index.ts b/src/game-engine/config/index.ts index 0e52d9bbe..670370267 100644 --- a/src/game-engine/config/index.ts +++ b/src/game-engine/config/index.ts @@ -19,10 +19,16 @@ import { Quest } from '@engine/world/actor/player/quest'; import { ItemSpawn, loadItemSpawnConfigurations } from '@engine/config/item-spawn-config'; import { loadSkillGuideConfigurations, SkillGuide } from '@engine/config/skill-guide-config'; import { loadMusicRegionConfigurations, MusicTrack } from '@engine/config/music-regions-config'; -import { LandscapeObject, loadXteaRegionFiles, ObjectConfig, XteaRegion } from '@runejs/filestore'; -require('json5/lib/register'); +import { + loadStrongholdOfSecurityQuizData, + StrongholdOfSecurityQuiz, + StrongholdOfSecurityQuizQuestion +} from '@engine/config/stronghold-of-security-quiz-config'; +import { BookData, loadBookData } from '@engine/config/sectioned-book-config'; +import { loadXteaRegionFiles, ObjectConfig, XteaRegion } from '@runejs/filestore'; +require('json5/lib/register'); export let itemMap: { [key: string]: ItemDetails }; @@ -38,6 +44,8 @@ export let itemSpawns: ItemSpawn[] = []; export let shopMap: { [key: string]: Shop }; export let skillGuides: SkillGuide[] = []; export let xteaRegions: { [key: number]: XteaRegion }; +export let strongholdOfSecurityQuizData: StrongholdOfSecurityQuiz; +export let bookData: BookData[]; export const musicRegionMap = new Map(); export const widgets: { [key: string]: any } = require('../../../data/config/widgets.json5'); @@ -59,6 +67,8 @@ export async function loadGameConfigurations(): Promise { npcIdMap = npcIds; npcPresetMap = npcPresets; + strongholdOfSecurityQuizData = await loadStrongholdOfSecurityQuizData(`data/config/stronghold-of-security-quiz.json5`); + bookData = await loadBookData(`data/config/books/`); npcSpawns = await loadNpcSpawnConfigurations('data/config/npc-spawns/'); musicRegions = await loadMusicRegionConfigurations(); musicRegions.forEach(song => song.regionIds.forEach(region => musicRegionMap.set(region, song.songId))); @@ -66,57 +76,56 @@ export async function loadGameConfigurations(): Promise { shopMap = await loadShopConfigurations('data/config/shops/'); skillGuides = await loadSkillGuideConfigurations('data/config/skill-guides/'); + logger.info(`Loaded ${strongholdOfSecurityQuizData.questions.length} Stronghold of Security questions.`); objectMap = {}; - logger.info(`Loaded ${musicRegions.length} music regions, ${Object.keys(itemMap).length} items, ${itemSpawns.length} item spawns, ` + `${Object.keys(npcMap).length} npcs, ${npcSpawns.length} npc spawns, ${Object.keys(shopMap).length} shops and ${skillGuides.length} skill guides.`); } - export const findItem = (itemKey: number | string): ItemDetails | null => { - if(!itemKey) { + if (!itemKey) { return null; } let gameId: number; - if(typeof itemKey === 'number') { + if (typeof itemKey === 'number') { gameId = itemKey; itemKey = itemIdMap[gameId]; - if(!itemKey) { + if (!itemKey) { logger.warn(`Item ${gameId} is not yet registered on the server.`); } } let item; - if(itemKey) { + if (itemKey) { item = itemMap[itemKey]; - if(!item) { + if (!item) { // Try fetching variation with suffix 0 item = itemMap[`${itemKey}:0`] } - if(item?.gameId) { + if (item?.gameId) { gameId = item.gameId; } - if(item?.extends) { + if (item?.extends) { let extensions = item.extends; - if(typeof extensions === 'string') { - extensions = [ extensions ]; + if (typeof extensions === 'string') { + extensions = [extensions]; } extensions.forEach(extKey => { const extensionItem = itemPresetMap[extKey]; - if(extensionItem) { + if (extensionItem) { item = _.merge(item, translateItemConfig(undefined, extensionItem)); } }); } } - if(gameId) { + if (gameId) { const cacheItem = filestore.configStore.itemStore.getItem(gameId); item = _.merge(item, cacheItem); } @@ -126,17 +135,17 @@ export const findItem = (itemKey: number | string): ItemDetails | null => { export const findNpc = (npcKey: number | string): NpcDetails | null => { - if(!npcKey) { + if (!npcKey) { return null; } - if(typeof npcKey === 'number') { + if (typeof npcKey === 'number') { const gameId = npcKey; npcKey = npcIdMap[gameId]; - if(!npcKey) { + if (!npcKey) { const cacheNpc = filestore.configStore.npcStore.getNpc(gameId); - if(cacheNpc) { + if (cacheNpc) { return cacheNpc as any; } else { logger.warn(`NPC ${gameId} is not yet configured on the server and a matching cache NPC was not found.`); @@ -146,25 +155,25 @@ export const findNpc = (npcKey: number | string): NpcDetails | null => { } let npc = npcMap[npcKey]; - if(!npc) { + if (!npc) { // Try fetching variation with suffix 0 npc = npcMap[`${npc}:0`] } - if(!npc) { + if (!npc) { logger.warn(`NPC ${npcKey} is not yet configured on the server and a matching cache NPC was not provided.`); return null; } - if(npc.extends) { + if (npc.extends) { let extensions = npc.extends; - if(typeof extensions === 'string') { - extensions = [ extensions ]; + if (typeof extensions === 'string') { + extensions = [extensions]; } extensions.forEach(extKey => { const extensionNpc = npcPresetMap[extKey]; - if(extensionNpc) { + if (extensionNpc) { npc = _.merge(npc, translateNpcServerConfig(undefined, extensionNpc)); } }); @@ -190,7 +199,7 @@ export const findObject = (objectId: number): ObjectConfig | null => { export const findShop = (shopKey: string): Shop | null => { - if(!shopKey) { + if (!shopKey) { return null; } @@ -213,3 +222,21 @@ export const findMusicTrackByButtonId = (buttonId: number): MusicTrack | null => export const findSongIdByRegionId = (regionId: number): number | null => { return musicRegionMap.has(regionId) ? musicRegionMap.get(regionId) : null; }; + +export function getRandomStrongholdOfSecurityQuizQuestion(): StrongholdOfSecurityQuizQuestion | null { + const randomIndex = Math.floor(Math.random() * strongholdOfSecurityQuizData.questions.length); + return strongholdOfSecurityQuizData.questions[randomIndex]; +} + +export function getBookFromId(bookId: number): BookData | null { + const bookExists = bookData.some(book => book.bookContents.bookId === bookId); + if(bookExists) { + for (const book of bookData) { + if(book.bookContents.bookId === bookId) { + return book; + } + } + } else { + return null; + } +} diff --git a/src/game-engine/config/sectioned-book-config.ts b/src/game-engine/config/sectioned-book-config.ts new file mode 100644 index 000000000..fd7c100ee --- /dev/null +++ b/src/game-engine/config/sectioned-book-config.ts @@ -0,0 +1,119 @@ +import { JSON_SCHEMA, safeLoad } from 'js-yaml'; +import { readFileSync } from 'fs'; +import { logger } from '@runejs/core'; +import { filestore } from '@engine/game-server'; +import { TextWidget } from '@runejs/filestore'; +import { wrapText } from '@engine/util/strings'; +import { loadConfigurationFiles } from '@runejs/core/fs'; +import * as fs from 'fs'; + + +export interface BookData { + sectionLocations: { [key: string]: number }; + bookPages: { [key: number]: BookPage }; + bookContents: BookContents; +} + +/** + * The contents of a Book, represented by an array of BookSections, an associated item ID, and book title. + */ +export interface BookContents { + bookId: number; + bookTitle: string; + showTableOfContents: boolean; + bookSections: BookSections[]; +} + +/** + * A book section that contains a title, and text. + */ +export interface BookSections { + header: string; + text: string; +} + +/** + * A BookPage represents a single page of a book. + */ +export interface BookPage { + header?: string; + lines: string[]; + pageNumber: number; +} + +/** + * Returns a boolean for whether or not the specified section header exists in the book. + * @param bookContents The book to find the header in. + * @param bookSectionHeader The header to search for. + */ +export function bookSectionHeaderExists(bookContents: BookContents, bookSectionHeader: string): boolean { + return bookContents.bookSections.some(section => section.header === bookSectionHeader); +} + +function getBookDataForBookContents(bookContents: BookContents): BookData { + const textWidget = filestore.widgetStore.decodeWidget(215) as TextWidget; + + const output = []; + let pageNumber = 1; + + const bookPages: { [key: number]: BookPage } = {}; + if (bookContents.showTableOfContents) { + output.push(``); + bookContents.bookSections.forEach(section => output.push(section.header)); + bookPages[pageNumber] = { header: `Chapters`, lines: output, pageNumber: pageNumber }; + pageNumber++; + } + + const sectionLocations: { [key: string]: number } = {}; + + bookContents.bookSections.forEach(section => { + const wrappedText = wrapText(section.text, 202, textWidget.fontId); + const pageLineAmount = 14; + + let pageContainsHeader = true; + while (wrappedText.length) { + const pageLines = wrappedText.splice(0, pageLineAmount); + bookPages[pageNumber] = { + header: (pageContainsHeader ? section.header : undefined), + lines: pageLines, + pageNumber: pageNumber + }; + if (pageContainsHeader) { + sectionLocations[section.header] = pageNumber; + } + pageContainsHeader = false; + pageNumber++; + } + }); + + logger.info(`Book: ` + bookContents.bookTitle + ` has ` + Object.keys(sectionLocations).length + ` sections.`) + return { sectionLocations: sectionLocations, bookPages: bookPages, bookContents: bookContents }; +} + +/** + * An enum that represents either the left, or the right page in a book. + */ +export enum PageSide { + LEFT_SIDE = 'LEFT', + RIGHT_SIDE = 'RIGHT' +} + +export const pageExists = (book: BookData, page: number): boolean => { + return (book.bookPages[page] !== undefined); +} + +export function loadBookData(path: string): BookData[] | null { + const books: BookData[] = []; + + fs.readdir(path, function(error, filenames) { + filenames.forEach(function(filename) { + const bookContents = safeLoad(readFileSync(path + filename, 'utf8'), + { schema: JSON_SCHEMA }) as BookContents; + const bookData = getBookDataForBookContents(bookContents); + books.push(bookData); + }); + }); + return books; +} + + diff --git a/src/game-engine/config/stronghold-of-security-quiz-config.ts b/src/game-engine/config/stronghold-of-security-quiz-config.ts new file mode 100644 index 000000000..c8b96f2a0 --- /dev/null +++ b/src/game-engine/config/stronghold-of-security-quiz-config.ts @@ -0,0 +1,45 @@ +import { loadConfigurationFiles } from '@runejs/core/fs'; +import { itemMap } from '@engine/config/index'; +import { ItemDetails } from '@engine/config/item-config'; +import { JSON_SCHEMA, safeLoad } from 'js-yaml'; +import { readFileSync } from 'fs'; +import { logger } from '@runejs/core'; + +/** + * Stronghold of Security quiz configuration + */ +export interface StrongholdOfSecurityQuiz { + prefix: string; + questions: StrongholdOfSecurityQuizQuestion[]; +} + +/** + * Stronghold of Security question + */ +export interface StrongholdOfSecurityQuizQuestion { + questionText: string; + options: StrongholdQuizOption[]; +} + +/** + * Stronghold of Security quiz option + */ +export interface StrongholdQuizOption { + optionText: string; + passable: boolean; + doorResponse: string; +} + +export function loadStrongholdOfSecurityQuizData(path: string): StrongholdOfSecurityQuiz | null { + try { + const quiz = safeLoad(readFileSync(path, 'utf8'), + { schema: JSON_SCHEMA }) as StrongholdOfSecurityQuiz; + + if(!quiz) { + throw new Error('Unable to read stronghold of security quiz data.'); + } + return quiz; + } catch(error) { + logger.error('Error parsing stronghold of security quiz data: ' + error); + } +} diff --git a/src/game-engine/util/strings.ts b/src/game-engine/util/strings.ts index c00c17a10..8a96cbd36 100644 --- a/src/game-engine/util/strings.ts +++ b/src/game-engine/util/strings.ts @@ -1,4 +1,6 @@ import { hexToHexString } from '@engine/util/colors'; +import { FontName } from '@runejs/filestore'; +import { filestore } from '@engine/game-server'; export const startsWithVowel = (str: string): boolean => { str = str.trim().toLowerCase(); @@ -23,46 +25,141 @@ const charWidths = [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 8, 8, 8, 8, 8, 8, 8, 8, 8, 13, 6, 8, 8, 8, 8, 4, 4, 5, 4, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8]; -export function wrapText(text: string, maxWidth: number): string[] { - const lines = []; +export enum TextDecoration { + Color, + Decoration +} - let lineStartIdx = 0; - let width = 0; - let lastSpace = 0; - let widthAfterSpace = 0; - let lastSpaceChar = ''; - for (let i = 0; i < text.length; i++) { - const char = text.charAt(i); +function getFont(font: number | string) { + if (font && typeof font === 'number') { + return filestore.fontStore.getFontById(font); + } else if (font && typeof font === 'string') { + return filestore.fontStore.getFontByName(FontName[font]); + } else { + // Default font, subject to change + return filestore.fontStore.getFontByName(FontName.p12_full); + } +} - // Ignore and strings... - if (char === '<' && (text.charAt(i + 1) === '/' || text.charAt(i + 1) === 'c' && text.charAt(i + 2) === 'o' && text.charAt(i + 3) === 'l')) { - const tagCloseIndex = text.indexOf('>', i); - i = tagCloseIndex; - continue; - } +function getStylingType(tag: string) { + let _tag = tag; + if (_tag.charAt(0) === '/') { + _tag = _tag.substring(1); + } - const charWidth = charWidths[text.charCodeAt(i)]; - width += charWidth; - widthAfterSpace += charWidth; + if (_tag.startsWith('col')) { + return TextDecoration.Color; + } else { + return TextDecoration.Decoration; + } +} - if (char === ' ' || char === '\n' || char === '-') { - lastSpaceChar = char; - lastSpace = i; - widthAfterSpace = 0; +// TODO refactor a bit +export function wrapText(text: string, maxWidth: number, font?: number | string): string[] { + const lines = []; + const selectedFont = getFont(font); + const colorQueue: string[] = []; + const decorationQueue: string[] = []; + const remainingText = text.split('').reverse(); + let currentLine = ''; + let currentWidth = 0; + let currentTagIndex = -1; + + while (remainingText.length > 0) { + const char = remainingText.pop(); + + let hidden = false; + let rendered = true; + + switch (char) { + case '<': + hidden = true; + currentTagIndex = currentLine.length + 1; + break; + case '>': + hidden = true; + // eslint-disable-next-line no-case-declarations + const currentTag = currentLine.substring(currentTagIndex, currentLine.length); + currentTagIndex = -1; + // eslint-disable-next-line no-case-declarations + const isClosing = currentTag.charAt(0) === '/'; + // eslint-disable-next-line no-case-declarations + const type = getStylingType(currentTag); + if (type === TextDecoration.Decoration) { + if (!isClosing) { + decorationQueue.push(currentTag); + } else { + decorationQueue.pop(); + } + } else { + if (!isClosing) { + colorQueue.push(currentTag); + } else { + colorQueue.pop(); + } + } + break; + case '@': + break; + case '\n': + hidden = true; + currentWidth = maxWidth; + rendered = false; + break; + case ' ': + if (currentLine[currentLine.length - 1] === ' ' || currentWidth === 0) { + hidden = true; + rendered = false; + } + break; + default: + break; + } + if (rendered) { + currentLine += char; + } + if (!hidden && currentTagIndex == -1) { + const charWidth = selectedFont.getCharWidth(char); + currentWidth += charWidth; } - if (width >= maxWidth || char === '\n') { - lines.push(text.substring(lineStartIdx, lastSpaceChar === '-' ? lastSpace + 1 : lastSpace)); - lineStartIdx = lastSpace + 1; - width = widthAfterSpace; + if (currentWidth >= maxWidth) { + let lastSpace = currentLine.lastIndexOf(' '); + const lastTag = currentLine.lastIndexOf('<'); + if (lastTag > lastSpace && char !== '\n') { + lastSpace = lastTag; + const type = getStylingType(currentLine.substring(lastTag + 1)); + if (type === TextDecoration.Decoration) { + decorationQueue.pop(); + } else { + colorQueue.pop(); + } + } + let lineToPush = currentLine; + let remainder = ''; + if (lastSpace != -1 && char != '\n') { + lineToPush = lineToPush.substring(0, lastSpace); + remainder = currentLine.substring(lastSpace); + } + + decorationQueue.slice(0).reverse().map(tag => lineToPush += ``); + colorQueue.slice(0).reverse().map(tag => lineToPush += ``); + lines.push(lineToPush.trim()); + currentLine = ''; + decorationQueue.slice(0).map(tag => currentLine += `<${tag}>`); + colorQueue.slice(0).map(tag => currentLine += `<${tag}>`); + remainingText.push(...remainder.split('').reverse()) + currentWidth = 0; } + } + if(currentLine !== '\n') { + lines.push(currentLine); - if (lineStartIdx !== text.length - 1) { - lines.push(text.substring(lineStartIdx, text.length)); } + // logger.info('split lines: ' + lines) return lines; } diff --git a/src/game-engine/world/actor/actor.ts b/src/game-engine/world/actor/actor.ts index e3e5b9f51..9418b70b5 100644 --- a/src/game-engine/world/actor/actor.ts +++ b/src/game-engine/world/actor/actor.ts @@ -131,10 +131,11 @@ export abstract class Actor { * Waits for the actor to reach the specified game object before resolving it's promise. * The promise will be rejected if the actor's walking queue changes or their movement is otherwise canceled. * @param target The position or game object that the actor needs to reach for the promise to resolve. + * @param ignoreInteractionDistanceRequirement Whether or not to disregard the fact that the actor is within interaction distance. */ - public async waitForPathing(target: Position | LandscapeObject): Promise; - public async waitForPathing(target: Position | LandscapeObject): Promise { - if(this.position.withinInteractionDistance(target)) { + public async waitForPathing(target: Position | LandscapeObject, ignoreInteractionDistanceRequirement?: boolean): Promise; + public async waitForPathing(target: Position | LandscapeObject, ignoreInteractionDistanceRequirement?: boolean): Promise { + if(this.position.withinInteractionDistance(target) && !ignoreInteractionDistanceRequirement) { return; } @@ -246,7 +247,7 @@ export abstract class Actor { if(distance <= 1) { return false; } - + if(distance > 16) { this.clearFaceActor(); this.metadata.faceActorClearedByWalking = true; diff --git a/src/game-engine/world/actor/dialogue.ts b/src/game-engine/world/actor/dialogue.ts index e686b3d47..23188025a 100644 --- a/src/game-engine/world/actor/dialogue.ts +++ b/src/game-engine/world/actor/dialogue.ts @@ -5,6 +5,7 @@ import { logger } from '@runejs/core'; import _ from 'lodash'; import { wrapText } from '@engine/util/strings'; import { findNpc } from '@engine/config'; +import { ParentWidget, TextWidget } from '@runejs/filestore'; export enum Emote { @@ -111,8 +112,29 @@ const continuableTextWidgetIds = [ 210, 211, 212, 213, 214 ]; const textWidgetIds = [ 215, 216, 217, 218, 219 ]; const titledTextWidgetId = 372; +/** + * Wraps dialogue text into multiple lines. + * @param text - The text to wrap. + * @param type - 'ACTOR' if the widget has a chat-head or an item sprite on the left, 'TEXT' if the dialogue is text only + */ function wrapDialogueText(text: string, type: 'ACTOR' | 'TEXT'): string[] { - return wrapText(text, type === 'ACTOR' ? 340 : 430); + let widget: TextWidget; + let width = 0; + + switch (type) { + case 'ACTOR': + widget = (filestore.widgetStore.decodeWidget(playerWidgetIds[0]) as ParentWidget).children[2] as TextWidget; + width = widget.width; + break; + case 'TEXT': + widget = filestore.widgetStore.decodeWidget(textWidgetIds[0]) as TextWidget; + width = widget.width; + break; + default: + throw new Error(`Unhandled widget type: ${type}`); + } + + return wrapText(text, width, widget.fontId); } function parseDialogueFunctionArgs(func: Function): string[] { @@ -141,6 +163,7 @@ export type DialogueTree = (Function | DialogueFunction | GoToAction)[]; export interface AdditionalOptions { closeOnWalk?: boolean; permanent?: boolean; + title?: string; } interface NpcParticipant { @@ -205,53 +228,55 @@ interface SubDialogueTreeAction extends DialogueAction { function parseDialogueTree(player: Player, npcParticipants: NpcParticipant[], dialogueTree: DialogueTree): ParsedDialogueTree { const parsedDialogueTree: ParsedDialogueTree = []; - for(let i = 0; i < dialogueTree.length; i++) { + let carryoverDialogue = []; + for (let i = 0; i < dialogueTree.length; i++) { const dialogueAction = dialogueTree[i]; - if(dialogueAction instanceof DialogueFunction) { + if (dialogueAction instanceof DialogueFunction) { // Code execution dialogue. parsedDialogueTree.push(dialogueAction as DialogueFunction); continue; } - if(dialogueAction instanceof GoToAction) { + if (dialogueAction instanceof GoToAction) { parsedDialogueTree.push(dialogueAction); continue; } let args = parseDialogueFunctionArgs(dialogueAction); - if(args === null) { + if (args === null) { args = ['()']; } + const dialogueType = args[0]; let tag: string = null; - if(args.length === 2 && typeof args[1] === 'string') { + if (args.length === 2 && typeof args[1] === 'string') { player.metadata.dialogueIndices[args[1]] = i; tag = args[1]; } - if(!dialogueType) { + if (!dialogueType) { logger.error('No arguments passed to dialogue function.'); continue; } let isOptions = false; - if(dialogueType === 'options' || dialogueType === '()') { + if (dialogueType === 'options' || dialogueType === '()') { // Options or custom function dialogue. let result = dialogueAction(); - if(dialogueType === '()') { + if (dialogueType === '()') { const funcResult = result(); - if(!Array.isArray(funcResult) || funcResult.length === 0) { + if (!Array.isArray(funcResult) || funcResult.length === 0) { logger.error('Invalid dialogue function response type.'); continue; } - if(typeof funcResult[0] === 'function') { + if (typeof funcResult[0] === 'function') { // given function returned a dialogue tree parsedDialogueTree.push(...parseDialogueTree(player, npcParticipants, funcResult)); } else { @@ -263,7 +288,7 @@ function parseDialogueTree(player: Player, npcParticipants: NpcParticipant[], di isOptions = true; } - if(isOptions) { + if (isOptions) { const options = (result as any[]).filter((option, index) => index % 2 === 0); const trees = (result as any[]).filter((option, index) => index % 2 !== 0); const optionsDialogueAction: OptionsDialogueAction = { @@ -271,7 +296,7 @@ function parseDialogueTree(player: Player, npcParticipants: NpcParticipant[], di tag, type: 'OPTIONS' }; - for(let j = 0; j < options.length; j++) { + for (let j = 0; j < options.length; j++) { const option = options[j]; const tree = parseDialogueTree(player, npcParticipants, trees[j]); optionsDialogueAction.options[option] = tree; @@ -279,30 +304,30 @@ function parseDialogueTree(player: Player, npcParticipants: NpcParticipant[], di parsedDialogueTree.push(optionsDialogueAction); } - } else if(dialogueType === 'text') { + } else if (dialogueType === 'text') { // Text-only dialogue (with the option to click continue). const text: string = dialogueAction(); const lines = wrapDialogueText(text, 'TEXT'); parsedDialogueTree.push({ lines, tag, type: 'TEXT', canContinue: true } as TextDialogueAction); - } else if(dialogueType === 'overlay') { + } else if (dialogueType === 'overlay') { // Text-only dialogue (no option to continue). const text: string = dialogueAction(); const lines = wrapDialogueText(text, 'TEXT'); parsedDialogueTree.push({ lines, tag, type: 'TEXT', canContinue: false } as TextDialogueAction); - } else if(dialogueType === 'titled') { + } else if (dialogueType === 'titled') { // Text-only dialogue (no option to continue). - const [ title, text ] = dialogueAction(); + const [title, text] = dialogueAction(); const lines = wrapDialogueText(text, 'TEXT'); - while(lines.length < 4) { + while (lines.length < 4) { lines.push(''); } parsedDialogueTree.push({ lines, title, tag, type: 'TITLED' } as TitledTextDialogueAction); - } else if(dialogueType === 'subtree') { + } else if (dialogueType === 'subtree') { // Dialogue sub-tree. const subTree: DialogueTree = dialogueAction(); @@ -310,19 +335,19 @@ function parseDialogueTree(player: Player, npcParticipants: NpcParticipant[], di } else { // Player or Npc dialogue. - let dialogueDetails: [ Emote, string ]; + let dialogueDetails: [Emote, string]; let npc: Npc | number | string; - if(dialogueType !== 'player') { + if (dialogueType !== 'player') { const participant = npcParticipants.find(p => p.key === dialogueType) as NpcParticipant; - if(!participant || !participant.npc) { + if (!participant || !participant.npc) { logger.error('No matching npc found for npc dialogue action.'); continue; } npc = participant.npc; - if(typeof npc !== 'number') { - if(typeof npc === 'string') { + if (typeof npc !== 'number') { + if (typeof npc === 'string') { npc = findNpc(npc)?.gameId || 0; } else { npc = npc.id; @@ -334,17 +359,46 @@ function parseDialogueTree(player: Player, npcParticipants: NpcParticipant[], di dialogueDetails = dialogueAction(player); } + const emote = dialogueDetails[0] as Emote; - const text = dialogueDetails[1] as string; - const lines = wrapDialogueText(text, 'ACTOR'); + const text = carryoverDialogue.join(' ') + dialogueDetails[1] as string; + carryoverDialogue = []; + + let lines = wrapDialogueText(text, 'ACTOR'); + // logger.info('length = ' + lines.length + ' - lines equals this: ' + lines); + const animation = nonLineEmotes.indexOf(emote) !== -1 ? EmoteAnimation[emote] : EmoteAnimation[`${emote}_${lines.length}LINE`]; - if(dialogueType !== 'player') { - const npcDialogueAction: NpcDialogueAction = { - npcId: npc as number, animation, lines, tag, type: 'NPC' - }; + if (dialogueType !== 'player') { + if (lines.length > 4) { + while (lines.length > 4) { + const copyOfLines = lines.slice(0, lines.length); + lines = lines.slice(0, 4); + + const npcDialogueAction: NpcDialogueAction = { + npcId: npc as number, animation, lines, tag, type: 'NPC' + }; + parsedDialogueTree.push(npcDialogueAction); + + lines = copyOfLines.slice(0, copyOfLines.length); + carryoverDialogue = lines.slice(4, lines.length) as string[]; + lines = carryoverDialogue; + + if(i === dialogueTree.length - 1 && lines.length <= 4) { + const npcDialogueAction: NpcDialogueAction = { + npcId: npc as number, animation, lines, tag, type: 'NPC' + }; + + parsedDialogueTree.push(npcDialogueAction); + } + } + } else { + const npcDialogueAction: NpcDialogueAction = { + npcId: npc as number, animation, lines, tag, type: 'NPC' + }; - parsedDialogueTree.push(npcDialogueAction); + parsedDialogueTree.push(npcDialogueAction); + } } else { const playerDialogueAction: PlayerDialogueAction = { player, animation, lines, tag, type: 'PLAYER' @@ -389,7 +443,6 @@ async function runDialogueAction(player: Player, dialogueAction: string | Dialog isOptions = true; const options = Object.keys(optionsAction.options); const trees = options.map(option => optionsAction.options[option]); - if(tag === undefined || dialogueAction.tag === tag) { tag = undefined; @@ -530,7 +583,6 @@ async function runDialogueAction(player: Player, dialogueAction: string | Dialog }); const widgetClosedEvent = await player.interfaceState.widgetClosed('chatbox'); - if(widgetClosedEvent.data !== undefined) { if(isOptions && typeof widgetClosedEvent.data === 'number') { const optionsAction = dialogueAction as OptionsDialogueAction; diff --git a/src/game-engine/world/actor/player/player.ts b/src/game-engine/world/actor/player/player.ts index fc0a3740c..3ac9cedce 100644 --- a/src/game-engine/world/actor/player/player.ts +++ b/src/game-engine/world/actor/player/player.ts @@ -58,7 +58,6 @@ import { SendMessageOptions } from '@engine/world/actor/player/model'; import { AutoAttackBehavior } from '../behaviors/auto-attack.behavior'; import { EventEmitter } from 'events'; - export const playerOptions: { option: string, index: number, placement: 'TOP' | 'BOTTOM' }[] = [ { option: 'Yeet', diff --git a/src/game-engine/world/config/animation-ids.ts b/src/game-engine/world/config/animation-ids.ts index d4bd7f8db..90053dd67 100644 --- a/src/game-engine/world/config/animation-ids.ts +++ b/src/game-engine/world/config/animation-ids.ts @@ -1,4 +1,9 @@ export const animationIds = { + openWardrobe: 545, + searchObject: 881, + touchStrongholdOfSecurityDoor: 4282, + lookAroundAfterStrongholdTeleportation: 4283, + closeWardrobe: 535, milkCow: 2305, lightingFire: 733, homeTeleportDraw: 4847, diff --git a/src/game-engine/world/config/item-ids.ts b/src/game-engine/world/config/item-ids.ts index 90473a893..fc0964d33 100644 --- a/src/game-engine/world/config/item-ids.ts +++ b/src/game-engine/world/config/item-ids.ts @@ -379,6 +379,11 @@ export const itemIds = { grappleTips: { mithril: 9415 }, + dungeons: { + strongholdOfSecurity: { + strongholdNotes: 9004, + } + }, roots: { oak: 6043, willow: 6047, diff --git a/src/game-engine/world/config/object-ids.ts b/src/game-engine/world/config/object-ids.ts index 6d26d6724..7848ee579 100644 --- a/src/game-engine/world/config/object-ids.ts +++ b/src/game-engine/world/config/object-ids.ts @@ -8,9 +8,78 @@ export const objectIds = { shortCuts: { stile: 12982 }, + strongholdOfSecurity: { + rewardObjects: { + giftOfPeace: 16135, + grainOfPlenty: 16077, + boxOfHealth: 16118, + cradleOfLife: 16047 + }, + + escapeRopes: { + spikeyChain: 16146, + catacombRope: 16078, + gooCoveredVine: 16112, + boneChain: 16048 + }, + + portals: { + levelOnePortal: 16150, + levelTwoPortal: 16082, + levelThreePortal: 16116, + levelFourPortal: 16050 + }, + + ascendingLadders: { + vaultOfWarLadder: 16148, + catacombLadder: 16080, + drippingVine: 16114, + boneyLadder: 16049 + }, + + descendingLadders: { + vaultOfWarLadder: 16149, + catacombLadder: 16081, + drippingVine: 16115 + }, + + gates: { + gateOfWarLeft: 16123, + gateOfWarRight: 16124, + ricketyDoorLeft: 16065, + ricketyDoorRight: 16066, + oozingBarrierLeft: 16090, + oozingBarrierRight: 16089, + thePortalOfDeathLeft: 16044, + thePortalOfDeathRight: 16043 + }, + + miscellaneous: { + deadExplorer: 16152 + } + }, + + draynorManor: { + wardrobeClosed: 388, + fountain: 153, + skeletonWardrobeOpened: 390, + nonSkeletonWardrobeOpened: 389 + }, + ladders: { taverlyDungeonOverworld: 1759, taverlyDungeonUnderground: 1755, + draynorManorOverworld: 133, + draynorManorUnderground: 132, + }, + + staircases: { + taverlyDungeonOverworld: 1759, + taverlyDungeonUnderground: 1755, + draynorManorGroundLevelStaircaseUp: 11498, + draynorManorGroundLevelStaircaseDown: 11499, + draynorManorSpiralUp: 11511, + draynorManorSpiralDown: 9584 }, brokenCart: 306, brokenCartWheel: 327, diff --git a/src/game-engine/world/config/static-positions.ts b/src/game-engine/world/config/static-positions.ts new file mode 100644 index 000000000..2b07b313a --- /dev/null +++ b/src/game-engine/world/config/static-positions.ts @@ -0,0 +1,13 @@ +import { Position } from '@engine/world/position'; + +export const staticPositions = { + strongholdOfSecurityEntrance: new Position(3081, 3421), + vaultOfWarEntrance: new Position(1858, 5244), + catacombOfFamineEntrance: new Position(2042, 5245), + pitOfPestilenceEntrance: new Position(2123, 5252), + sepulchreOfDeathEntrance: new Position(2358, 5215), + vaultOfWarPortalDestination: new Position(1858, 5244), + catacombOfFaminePortalDestination: new Position(2042, 5245), + pitOfPestilencePortalDestination: new Position(2123, 5252), + sepulchreOfDeathPortalDestination: new Position(2358, 5215) +} diff --git a/src/game-engine/world/config/stronghold-of-security-reward-data.ts b/src/game-engine/world/config/stronghold-of-security-reward-data.ts new file mode 100644 index 000000000..86d416e1f --- /dev/null +++ b/src/game-engine/world/config/stronghold-of-security-reward-data.ts @@ -0,0 +1,22 @@ +import { objectIds } from '@engine/world/config/object-ids'; + +export const strongholdOfSecurityRewardData = { + vaultOfWar: { + objectId: objectIds.strongholdOfSecurity.rewardObjects.giftOfPeace, + initialMessage: `The box hinges creak and appear to be forming audible words....`, + emoteUnlocked: `Flap`, + amountOfGoldRewarded: 2000 + }, + catacombOfFamine: { + objectId: objectIds.strongholdOfSecurity.rewardObjects.grainOfPlenty, + initialMessage: `The wheat shifts in the sack, sighing audible words....`, + emoteUnlocked: `Slap Head`, + amountOfGoldRewarded: 3000 + }, + pitOfPestilence: { + objectId: objectIds.strongholdOfSecurity.rewardObjects.boxOfHealth, + initialMessage: `The box hinges creak and appear to be forming audible words....`, + emoteUnlocked: `Idea`, + amountOfGoldRewarded: 5000 + } +} diff --git a/src/game-engine/world/direction.ts b/src/game-engine/world/direction.ts index c62f4d77c..ee3a44954 100644 --- a/src/game-engine/world/direction.ts +++ b/src/game-engine/world/direction.ts @@ -61,6 +61,18 @@ export const directionData: { [key: string]: DirectionData } = { }; export const WNES: Direction[] = ['WEST', 'NORTH', 'EAST', 'SOUTH']; +export const directionNameFromIndex = (index: number): string => { + const keys = Object.keys(directionData); + for (const key of keys) { + if (directionData[key].rotation === index) { + return key; + } + } + + return null; +}; + + export const directionFromIndex = (index: number): DirectionData => { const keys = Object.keys(directionData); for (const key of keys) { diff --git a/src/game-engine/world/skill-util/harvest-skill.ts b/src/game-engine/world/skill-util/harvest-skill.ts index 48c7fd862..63bfc0f54 100644 --- a/src/game-engine/world/skill-util/harvest-skill.ts +++ b/src/game-engine/world/skill-util/harvest-skill.ts @@ -16,7 +16,7 @@ export function canInitiateHarvest(player: Player, target: IHarvestable, skill: if (!target) { switch (skill) { case Skill.MINING: - player.sendMessage('There is current no ore available in this rock.'); + player.sendMessage('There is currently no ore available in this rock.'); break; default: player.sendMessage(colorText('HARVEST SKILL ERROR, PLEASE CONTACT DEVELOPERS', colors.red)); diff --git a/src/plugins/books/books.plugin.ts b/src/plugins/books/books.plugin.ts new file mode 100644 index 000000000..2304af286 --- /dev/null +++ b/src/plugins/books/books.plugin.ts @@ -0,0 +1,372 @@ +import { ItemInteractionAction, itemInteractionActionHandler } from '@engine/world/action/item-interaction.action'; +import { + getBookFromId, + widgets +} from '@engine/config'; +import { TaskExecutor } from '@engine/world/action'; +import { widgetInteractionActionHandler } from '@engine/world/action/widget-interaction.action'; +import { Player } from '@engine/world/actor/player/player'; +import { + BookData, + BookPage, + bookSectionHeaderExists, + BookSections, + pageExists, PageSide +} from '@engine/config/sectioned-book-config'; +import { Widget, WidgetClosedEvent } from '@engine/world/actor/player/interface-state'; +import { logger } from '@runejs/core'; + +/** + * Open the book interface and read the specified book. + * + * @param details Information about the action. + */ +export const activate: itemInteractionActionHandler = (details) => { + const itemId = details.itemId; + const book = getBookFromId(itemId); + if(!book) { + details.player.sendMessage(`Book data not found for item: ` + itemId + `.`); + return; + } + + details.player.sessionMetadata[`bookIdBeingRead`] = itemId; + details.player.playAnimation(1350); + + openBook(details.player, book, 1); + details.player.metadata['readingBook'] = details.player.interfaceState.closed.subscribe((whatClosed: WidgetClosedEvent) => { + if (whatClosed && whatClosed.widget && whatClosed.widget.widgetId === widgets.book) { + details.player.stopGraphics(); + details.player.stopAnimation(); + } + }); +}; + +/** + * Given book information, and a specified header from that book, return the page number that it's on. + * @param bookData The book with the section header you want to find. + * @param bookSectionHeader The name of the section to find the page of. + */ +function getPageNumberForBookSection(bookData: BookData, bookSectionHeader: string): number { + return bookData.sectionLocations[bookSectionHeader]; +} + +/** + * Toggles the visibility of clickable text widgets on the left page, given a range of line numbers. + * + * This is used for the table of contents, allowing only selectable sections to be clickable. + * + * @param player The player who's book widget will be modified. + * @param widget The widget being modified. + * @param hidden Whether to hide or unhide the particular clickable widget line + * @param fromLine The line number to begin with. + * @param toLine The line number to end on. + */ +function toggleVisibilityOfLeftPageClickableWidgets(player: Player, widget: Widget, hidden: boolean, fromLine?: number, toLine?: number) { + if (toLine === undefined) { + toLine = 14; + } + + if (fromLine === undefined) { + fromLine = 0; + } + for (let i = fromLine; i <= toLine; i++) { + player.modifyWidget(widget.widgetId, { + childId: 100 + (2 * i), + hidden: hidden, + text: `` + }); + } +} + +/** + * Clears the book interface to prepare it for new data. + * @param player The player who's book interface will be cleared. + * @param widget The book widget being modified. + */ +function clearBookInterface(player: Player, widget: Widget) { + let clickable = true; + for (let interfaceTextType = 0; interfaceTextType < 2; interfaceTextType++) { + for (const pageSide of Object.values(PageSide)) { + for (let lineNumber = 0; lineNumber <= widgets.bookChildren.totalPageLineAmount; lineNumber++) { + const childId = getLineChildId(pageSide as PageSide, clickable, lineNumber); + player.modifyWidget(widget.widgetId, { + childId: childId, + hidden: true, + text: `` + }); + } + } + clickable = !clickable; + } + + //Hide the right page turn button + player.modifyWidget(widget.widgetId, { + childId: widgets.bookChildren.rightPage.pageTurnButton, + hidden: true + }); + + //Hide the left page turn button + player.modifyWidget(widget.widgetId, { + childId: widgets.bookChildren.rightPage.pageTurnButton, + hidden: true + }); +} + +/** + * Creates the book widget from the specified Book object, and populates it according to what page you're on. + * @param player The player who will view the book. + * @param bookData The book the player will view. + * @param page The page-set that will be viewed. (This number accounts for two pages at a time. Page 1 and 2 would be 1. + * Pages 3 and 4 would be 2, etc.) + */ +export function openBook(player: Player, bookData: BookData, leftPageNumber: number): void { + const widget = player.interfaceState.openWidget(widgets.book, { + slot: 'screen', + fakeWidget: 3100003, + metadata: { + page: leftPageNumber + } + }); + + clearBookInterface(player, widget); + + addBookTitleToBookWidget(player, widget, bookData); + + toggleVisibilityOfLeftPageClickableWidgets(player, widget, true); + + const leftPage = bookData.bookPages[leftPageNumber]; + if (leftPage) { + addBookTextToWidget(player, widget, bookData, leftPageNumber, PageSide.LEFT_SIDE); + } else { + return; + } + + const rightPageNumber = leftPageNumber + 1; + const rightPage = bookData.bookPages[rightPageNumber]; + if (rightPage) { + addBookTextToWidget(player, widget, bookData, rightPageNumber, PageSide.RIGHT_SIDE); + } + + addPageNumbersToBookWidget(player, widget); +} + +/** + * Modifies the specified book widget to add page numbers according to the widget's metadata. + * @param player The player who's widget is being modified. + * @param widget The widget to modify with new page numbers. + */ +function addPageNumbersToBookWidget(player: Player, widget: Widget) { + player.modifyWidget(widget.widgetId, { + childId: widgets.bookChildren.rightPage.pageNumber, + text: `Page ${widget.metadata.page + 1}` + }); + player.modifyWidget(widget.widgetId, { + childId: widgets.bookChildren.leftPage.pageNumber, + text: `Page ${widget.metadata.page}` + }); +} + +function addBookTitleToBookWidget(player: Player, bookWidget: Widget, bookData: BookData) { + player.modifyWidget(bookWidget.widgetId, { + childId: widgets.bookChildren.title, + text: bookData.bookContents.bookTitle + }); +} + +/** + * Return the appropriate BookPage, given a particular page number in a book. + * @param bookData + * @param page + */ +function getBookPageFromPageNumber(bookData: BookData, page: number): BookPage { + return bookData.bookPages[page]; +} + +/** + * Given a BookData object, and information about where the book data should be applied to, add the appropriate book text + * to the specified book widget. + * @param player The player who's widget is being modified. + * @param widget The widget to modify with text from the book. + * @param book The book to get the text from. + * @param pageNumber The page to apply the text onto. + * @param side Whether or not the page is on the left or right side. + */ +function addBookTextToWidget(player: Player, widget: Widget, book: BookData, pageNumber: number, side: PageSide) { + const page = getBookPageFromPageNumber(book, pageNumber); + if(!page) { + logger.info(`Page number not found.`) + } + const clickable = (book.bookContents.showTableOfContents && pageNumber === 1); + const totalPageLines = widgets.bookChildren.totalPageLineAmount; + + const hideLeftPageTurn: boolean = (pageNumber === 1 || pageNumber === 2); + player.modifyWidget(widget.widgetId, { + childId: 95, + hidden: hideLeftPageTurn + }); + + player.modifyWidget(widget.widgetId, { + childId: 97, + hidden: (pageNumber === Object.keys(book.bookPages).length) + }); + let childId; + let lineText; + + for (let lineNumber = 0; lineNumber <= totalPageLines; lineNumber++) { + childId = getLineChildId(side, clickable, lineNumber); + + if (page.header && lineNumber === 0) { + childId = getLineChildId(side, false, lineNumber); + lineText = `` + page.header; + } else if (page.header && lineNumber !== 0) { + lineText = page.lines[lineNumber - 1]; + } else if (!page.header) { + lineText = page.lines[lineNumber]; + } + player.modifyWidget(widget.widgetId, { + childId: childId, + text: lineText + }); + } + + if (side === PageSide.LEFT_SIDE) { + if (pageNumber === 1 && book.bookContents.showTableOfContents) { + const bookSections = book.bookContents.bookSections.length + 1; + toggleVisibilityOfLeftPageClickableWidgets(player, widget, false, 2, bookSections); + } + } +} + +/** + * Given which side the page is on, and whether or not the line should be clickable, return widget information about the + * page. + * @param pageSide Whether the page is on the left or right side of the book. + * @param clickable Whether or not the line should be clickable. + */ +function getWidgetInformationForPage(pageSide: PageSide, clickable: boolean) { + let lineType; + + switch (pageSide) { + case PageSide.RIGHT_SIDE: + lineType = (clickable ? widgets.bookChildren.rightPage.clickableLines : widgets.bookChildren.rightPage.nonClickableLines) + break; + + case PageSide.LEFT_SIDE: + lineType = (clickable ? widgets.bookChildren.leftPage.clickableLines : widgets.bookChildren.leftPage.nonClickableLines) + break; + } + return { firstLineId: lineType.firstLineId, incrementAmount: lineType.incrementAmount }; +} + +/** + * Given which side the page is on, whether or not the line should be clickable, and the line number, return the + * appropriate child ID for the book interface. + * + * @param pageSide Whether the page is on the left or right side of the book. + * @param clickable Whether or not the line should be clickable. + * @param lineNumber Which particular line you want to get the child ID of. + */ +function getLineChildId(pageSide: PageSide, clickable: boolean, lineNumber: number): number { + const pageWidgetData: { firstLineId: number, incrementAmount: number } = getWidgetInformationForPage(pageSide, clickable); + + return pageWidgetData.firstLineId + (lineNumber * pageWidgetData.incrementAmount); +} + +/** + * Handles interactions with the book interface itself, such as using the left and right buttons + * @param details + */ +export const bookInteract: widgetInteractionActionHandler = (details) => { + const playerWidget = details.player.interfaceState.findWidget(27); + + if (!playerWidget || !playerWidget.metadata.page || playerWidget.fakeWidget !== 3100003) { + return; + } + + if(!details.player.sessionMetadata[`bookIdBeingRead`]) { + details.player.sendMessage(`Session metadata not found for book interface`); + } + + const bookData: BookData = getBookFromId(details.player.sessionMetadata[`bookIdBeingRead`]); + + + let pageNumber = playerWidget.metadata.page; + switch (details.childId) { + case 160: + openBook(details.player, bookData, 1); + return; + case 94: + pageNumber -= 2; + if (pageExists(bookData, pageNumber)) { + details.player.playAnimation(3141); + openBook(details.player, bookData, pageNumber); + } else { + openBook(details.player, bookData, pageNumber + 1); + } + return; + case 96: + pageNumber += 2; + if (pageExists(bookData, pageNumber)) { + details.player.playAnimation(3140); + openBook(details.player, bookData, pageNumber); + } else { + openBook(details.player, bookData, pageNumber - 1); + } + return; + } + + + let selectedIndex = undefined; + if (details.childId >= 101 && details.childId <= 129) { + selectedIndex = (details.childId - 99) / 2 - 1; + } + if (details.childId >= 131 && details.childId <= 159) { + selectedIndex = ((details.childId - 129) / 2 - 1) + 15; + } + if (selectedIndex !== undefined) { + const bookSection: BookSections = bookData.bookContents.bookSections[selectedIndex - 2]; + + if (bookSectionHeaderExists(bookData.bookContents, bookSection.header)) { + const sectionName = bookSection.header; + let selectedPage = getPageNumberForBookSection(bookData, sectionName); + details.player.sendMessage(`Selected page number: ` + selectedPage) + details.player.sendMessage(`Header: ` + sectionName) + if(selectedPage % 2 === 0) { + selectedPage--; + } + details.player.sendMessage(`Book section header exists.`) + + openBook(details.player, bookData, selectedPage); + } else { + details.player.sendMessage(`Book section header doesn't exist.`) + openBook(details.player, bookData, pageNumber); + } + } +} + +const canActivate = (task: TaskExecutor, taskIteration: number): boolean => { + return true; +} + +const onComplete = (task: TaskExecutor): void => { + task.actor.stopAnimation(); + task.actor.stopGraphics(); +} + +export default { + pluginId: 'rs:books', + hooks: [ + { + type: 'item_interaction', + widgets: widgets.inventory, + options: 'read', + handler: activate, + cancelOtherActions: true + }, + { + type: 'widget_interaction', + widgetId: 3100003, + handler: bookInteract + } + ] +}; diff --git a/src/plugins/buttons/player-emotes.plugin.ts b/src/plugins/buttons/player-emotes.plugin.ts index 355c2052d..426a78416 100644 --- a/src/plugins/buttons/player-emotes.plugin.ts +++ b/src/plugins/buttons/player-emotes.plugin.ts @@ -74,7 +74,7 @@ export const emotes: { [key: number]: Emote } = { 32: { animationId: 4276, name: 'IDEA', unlockable: true, graphicId: 712 }, 30: { animationId: 4278, name: 'STAMP', unlockable: true }, 31: { animationId: 4280, name: 'FLAP', unlockable: true }, - 29: { animationId: 4275, name: 'FACEPALM', unlockable: true }, + 29: { animationId: 4275, name: 'SLAP HEAD', unlockable: true }, 33: { animationId: 3544, name: 'ZOMBIE WALK', unlockable: true }, 34: { animationId: 3543, name: 'ZOMBIE DANCE', unlockable: true }, 35: { animationId: 2836, name: 'SCARED', unlockable: true }, @@ -115,7 +115,7 @@ export function unlockEmotes(player: Player): void { goblinConfig += 7; if(name === 'FLAP') sosConfig += 1; - if(name === 'FACEPALM') + if(name === 'SLAP HEAD') sosConfig += 2; if(name === 'IDEA') sosConfig += 4; @@ -153,7 +153,7 @@ export const handler: buttonActionHandler = (details) => { const { player, buttonId } = details; const emote = emotes[buttonId]; - + if(emote.name === 'SKILLCAPE') { if (player.getEquippedItem('back')) { if (skillCapeEmotes.some(item => item.itemIds.includes(player.getEquippedItem('back')?.itemId))) { @@ -173,7 +173,7 @@ export const handler: buttonActionHandler = (details) => { return; } } - + player.interfaceState.closeAllSlots(); player.playAnimation(emote.animationId); if(emote.graphicId !== undefined) { diff --git a/src/plugins/dungeons/stronghold-of-security/stronghold-of-security-objects.plugin.ts b/src/plugins/dungeons/stronghold-of-security/stronghold-of-security-objects.plugin.ts new file mode 100644 index 000000000..4aa91468b --- /dev/null +++ b/src/plugins/dungeons/stronghold-of-security/stronghold-of-security-objects.plugin.ts @@ -0,0 +1,282 @@ +import { ObjectInteractionAction, ObjectInteractionActionHook } from '@engine/world/action/object-interaction.action'; +import { TaskExecutor } from '@engine/world/action'; +import { schedule } from '@engine/world/task'; +import { dialogue, Emote, execute } from '@engine/world/actor/dialogue'; +import { Position } from '@engine/world/position'; +import { objectIds } from '@engine/world/config/object-ids'; +import { animationIds } from '@engine/world/config/animation-ids'; +import { staticPositions } from '@engine/world/config/static-positions'; + +const canActivate = (task: TaskExecutor, taskIteration: number): boolean => { + const { actor, actionData: { position, object } } = task; + + return true; +} + +const activateDescendingLadders = async (task: TaskExecutor, taskIteration: number): Promise => { + const { player, actionData: { position, object } } = task.getDetails(); + player.face(position); + + let descend = false; + await dialogue([player], [ + options => [ + `Yes, I know that it may be dangerous down there!`, [ + execute(() => { + descend = true; + }) + ], + `No thanks, I don't want to die!`, [ + player => [Emote.SHOCKED, `No thanks, I don't want to die!`] + ] + ], + ], + ); + + if (descend) { + player.playAnimation(animationIds.climbLadder); + await schedule(1); + + let teleportPosition; + switch (object.objectId) { + case objectIds.strongholdOfSecurity.descendingLadders.vaultOfWarLadder: + teleportPosition = staticPositions.catacombOfFamineEntrance; + break; + + case objectIds.strongholdOfSecurity.descendingLadders.catacombLadder: + teleportPosition = staticPositions.pitOfPestilenceEntrance; + break; + + case objectIds.strongholdOfSecurity.descendingLadders.drippingVine: + teleportPosition = staticPositions.sepulchreOfDeathEntrance; + break; + } + player.sendMessage(`You climb down the ladder to the next level.`) + player.teleport(teleportPosition); + } + return true; +} + +const activateEscapeRopesAndAscendingLadders = async (task: TaskExecutor, taskIteration: number): Promise => { + const { player, actionData: { position, object } } = task.getDetails(); + player.face(position); + let teleportPosition; + switch (object.objectId) { + case objectIds.strongholdOfSecurity.ascendingLadders.vaultOfWarLadder: + teleportPosition = staticPositions.strongholdOfSecurityEntrance; + break; + + case objectIds.strongholdOfSecurity.ascendingLadders.catacombLadder: + case objectIds.strongholdOfSecurity.escapeRopes.spikeyChain: + teleportPosition = staticPositions.vaultOfWarEntrance; + break; + + case objectIds.strongholdOfSecurity.ascendingLadders.drippingVine: + case objectIds.strongholdOfSecurity.escapeRopes.catacombRope: + teleportPosition = staticPositions.catacombOfFamineEntrance; + break; + case objectIds.strongholdOfSecurity.ascendingLadders.boneyLadder: + case objectIds.strongholdOfSecurity.escapeRopes.gooCoveredVine: + teleportPosition = staticPositions.pitOfPestilenceEntrance; + break; + + case objectIds.strongholdOfSecurity.escapeRopes.boneChain: + teleportPosition = staticPositions.strongholdOfSecurityEntrance; + break; + } + + const objectType = getObjectType(object.objectId); + + if (objectType === ObjectType.ESCAPE_ROPE) { + await player.sendMessage(`You shin up the rope, squeeze through a passage then climb a ladder.`) + } else { + await player.sendMessage(`You climb up the ladder to the level above.`) + } + + player.playAnimation(animationIds.climbLadder); + await schedule(1); + player.teleport(teleportPosition); + + if (objectType === ObjectType.ESCAPE_ROPE) { + await player.sendMessage(`You climb up the ladder which seems to twist and wind in all directions.`) + } + return true; +} + +const activatePortal = async (task: TaskExecutor, taskIteration: number): Promise => { + const { player, actionData: { position, object } } = task.getDetails(); + player.face(position); + let teleportPosition; + switch (object.objectId) { + case objectIds.strongholdOfSecurity.portals.levelOnePortal: + if (player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.vaultOfWar) { + teleportPosition = new Position(1914, 5222); + } + break; + + case objectIds.strongholdOfSecurity.portals.levelTwoPortal: + if (player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.catacombOfFamine) { + teleportPosition = new Position(2021, 5223); + } + break; + + case objectIds.strongholdOfSecurity.portals.levelThreePortal: + if (player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.pitOfPestilence) { + teleportPosition = new Position(2146, 5287); + } + break; + + case objectIds.strongholdOfSecurity.portals.levelFourPortal: + if (player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.sepulchreOfDeath) { + teleportPosition = new Position(2341, 5219); + } + break; + } + + if (!teleportPosition) { + await player.sendMessage(`You are not of sufficient experience to take the shortcut through this level.`); + return false; + } + + switch (task.actionData.option.toLowerCase()) { + case `climb-up`: + + break; + + + } + player.sendMessage(`You enter the portal to be whisked through to the treasure room.`) + player.teleport(teleportPosition); + return true; +} + + +const activateDeadExplorer = async (task: TaskExecutor, taskIteration: number): Promise => { + const { player, actionData: { position, object } } = task.getDetails(); + player.face(position); + player.playAnimation(animationIds.searchObject); + if (player.hasItemInInventory(9004)) { + player.sendMessage(`You don't find anything.`) + } else { + if (!player.inventory.hasSpace()) { + await dialogue([player], [ + text => (`I'd better make room in my inventory first!`) + ]); + } else { + player.giveItem(9004); + await dialogue([player], [ + text => (`You rummage around in the dead explorer's bag.....`), + text => (`You find a book of hand written notes.`) + ]); + } + } + return true; +} + +const onComplete = (task: TaskExecutor): void => { + // task.actor.face(task.actor.position); +}; + +const getObjectType = (objectId: number): ObjectType => { + for (const ascendingLaddersKey in objectIds.strongholdOfSecurity.ascendingLadders) { + const ladders = objectIds.strongholdOfSecurity.ascendingLadders; + if (ladders[ascendingLaddersKey] === objectId) { + return ObjectType.LADDER; + } + } + + for (const escapeRopesKey in objectIds.strongholdOfSecurity.escapeRopes) { + const escapeRopes = objectIds.strongholdOfSecurity.escapeRopes; + if (escapeRopes[escapeRopesKey] === objectId) { + return ObjectType.ESCAPE_ROPE; + } + } + return undefined; +} + +enum ObjectType { + ESCAPE_ROPE, + LADDER +} + +export default { + pluginId: 'rs:stronghold_of_security_objects', + hooks: [ + { + type: 'object_interaction', + options: ['climb-down'], + objectIds: [objectIds.strongholdOfSecurity.descendingLadders.vaultOfWarLadder, + objectIds.strongholdOfSecurity.descendingLadders.catacombLadder, + objectIds.strongholdOfSecurity.descendingLadders.drippingVine], + strength: 'normal', + multi: false, + walkTo: true, + task: { + canActivate, + activate: activateDescendingLadders, + onComplete + } + } as ObjectInteractionActionHook, + { + type: 'object_interaction', + options: ['climb-up'], + objectIds: [objectIds.strongholdOfSecurity.escapeRopes.boneChain, + objectIds.strongholdOfSecurity.escapeRopes.gooCoveredVine, + objectIds.strongholdOfSecurity.escapeRopes.catacombRope, + objectIds.strongholdOfSecurity.escapeRopes.spikeyChain], + strength: 'normal', + multi: false, + walkTo: true, + task: { + canActivate, + activate: activateEscapeRopesAndAscendingLadders, + onComplete + } + } as ObjectInteractionActionHook, + { + type: 'object_interaction', + options: ['climb-up'], + objectIds: [objectIds.strongholdOfSecurity.ascendingLadders.boneyLadder, + objectIds.strongholdOfSecurity.ascendingLadders.drippingVine, + objectIds.strongholdOfSecurity.ascendingLadders.catacombLadder, + objectIds.strongholdOfSecurity.ascendingLadders.vaultOfWarLadder], + strength: 'normal', + multi: false, + walkTo: true, + task: { + canActivate, + activate: activateEscapeRopesAndAscendingLadders, + onComplete + } + } as ObjectInteractionActionHook, + { + type: 'object_interaction', + options: ['search'], + objectIds: [objectIds.strongholdOfSecurity.miscellaneous.deadExplorer], + strength: 'normal', + multi: false, + walkTo: true, + task: { + canActivate, + activate: activateDeadExplorer, + onComplete + } + } as ObjectInteractionActionHook, + { + type: 'object_interaction', + options: ['use'], + objectIds: [objectIds.strongholdOfSecurity.portals.levelOnePortal, + objectIds.strongholdOfSecurity.portals.levelTwoPortal, + objectIds.strongholdOfSecurity.portals.levelThreePortal, + objectIds.strongholdOfSecurity.portals.levelFourPortal + ], + strength: 'normal', + multi: false, + walkTo: true, + task: { + canActivate, + activate: activatePortal, + onComplete + } + } as ObjectInteractionActionHook + ] +}; diff --git a/src/plugins/dungeons/stronghold-of-security/stronghold-of-security-rewards.plugin.ts b/src/plugins/dungeons/stronghold-of-security/stronghold-of-security-rewards.plugin.ts new file mode 100644 index 000000000..55a4d74de --- /dev/null +++ b/src/plugins/dungeons/stronghold-of-security/stronghold-of-security-rewards.plugin.ts @@ -0,0 +1,247 @@ +import { ObjectInteractionAction, ObjectInteractionActionHook } from '@engine/world/action/object-interaction.action'; +import { TaskExecutor } from '@engine/world/action'; +import { dialogue, Emote, execute } from '@engine/world/actor/dialogue'; +import { findItem } from '@engine/config'; +import { unlockEmote } from '@plugins/buttons/player-emotes.plugin'; +import { objectIds } from '@engine/world/config/object-ids'; +import { Player } from '@engine/world/actor/player/player'; +import { strongholdOfSecurityRewardData } from '@engine/world/config/stronghold-of-security-reward-data'; + +const canActivate = (task: TaskExecutor, taskIteration: number): boolean => { + const { actor, actionData: { position, object } } = task; + + return true; +} + +interface StrongholdOfSecurityRewardData { + objectId: number; + initialMessage: string; + emoteUnlocked: string; + amountOfGoldRewarded: number; +} + +export const getFloorCompletionFromObjectId = (player: Player, objectId: number): boolean => { + switch (objectId) { + case objectIds.strongholdOfSecurity.gates.gateOfWarLeft: + case objectIds.strongholdOfSecurity.gates.gateOfWarRight: + case objectIds.strongholdOfSecurity.rewardObjects.giftOfPeace: + return player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.vaultOfWar; + + case objectIds.strongholdOfSecurity.gates.ricketyDoorLeft: + case objectIds.strongholdOfSecurity.gates.ricketyDoorRight: + case objectIds.strongholdOfSecurity.rewardObjects.grainOfPlenty: + return player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.catacombOfFamine; + + case objectIds.strongholdOfSecurity.gates.oozingBarrierLeft: + case objectIds.strongholdOfSecurity.gates.oozingBarrierRight: + case objectIds.strongholdOfSecurity.rewardObjects.boxOfHealth: + return player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.pitOfPestilence; + + case objectIds.strongholdOfSecurity.gates.thePortalOfDeathLeft: + case objectIds.strongholdOfSecurity.gates.thePortalOfDeathRight: + case objectIds.strongholdOfSecurity.rewardObjects.cradleOfLife: + return player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.sepulchreOfDeath; + } +} + +export const getNpcKeyFromObjectId = (objectId: number): string => { + switch (objectId) { + case objectIds.strongholdOfSecurity.gates.gateOfWarLeft: + case objectIds.strongholdOfSecurity.gates.gateOfWarRight: + return `rs:Gate_of_War`; + + case objectIds.strongholdOfSecurity.gates.ricketyDoorLeft: + case objectIds.strongholdOfSecurity.gates.ricketyDoorRight: + return `rs:Ricketty_door`; + + case objectIds.strongholdOfSecurity.gates.oozingBarrierLeft: + case objectIds.strongholdOfSecurity.gates.oozingBarrierRight: + return `rs:Oozing_barrier`; + + case objectIds.strongholdOfSecurity.gates.thePortalOfDeathLeft: + case objectIds.strongholdOfSecurity.gates.thePortalOfDeathRight: + return `rs:Portal_of_Death`; + } +} + + +const setFloorCompletionFromObjectId = (player: Player, objectId: number, completion: boolean): void => { + switch (objectId) { + case objectIds.strongholdOfSecurity.rewardObjects.giftOfPeace: + player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.vaultOfWar = completion; + break; + + case objectIds.strongholdOfSecurity.rewardObjects.grainOfPlenty: + player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.catacombOfFamine = completion; + break; + + case objectIds.strongholdOfSecurity.rewardObjects.boxOfHealth: + player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.pitOfPestilence = completion; + break; + + case objectIds.strongholdOfSecurity.rewardObjects.cradleOfLife: + player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.sepulchreOfDeath = completion; + break; + } +} + +const getRewardDataFromObjectId = (objectId: number): StrongholdOfSecurityRewardData => { + for (const key in strongholdOfSecurityRewardData) { + const rewardData = strongholdOfSecurityRewardData[key]; + if(rewardData.objectId === objectId) { + return rewardData; + } + } +} + +const activate = async (task: TaskExecutor, taskIteration: number): Promise => { + const { player, actionData: { position, object } } = task.getDetails(); + + player.face(position); + + const floorComplete = getFloorCompletionFromObjectId(player, object.objectId); + + if (floorComplete && object.objectId !== objectIds.strongholdOfSecurity.rewardObjects.cradleOfLife) { + await dialogue([player], [ + text => ('You have already claimed your reward from this level.') + ]); + return true; + } + + + + const completedSepulchre = player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.sepulchreOfDeath; + + if (object.objectId === objectIds.strongholdOfSecurity.rewardObjects.cradleOfLife) { + const fancyBoots = findItem(`rs:Fancy_boots`); + const fightingBoots = findItem(`rs:Fighting_boots`); + + const lostBoots = player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.sepulchreOfDeath && + !(player.hasItemOnPerson(fancyBoots.gameId) || player.hasItemOnPerson(fightingBoots.gameId)) + + if (completedSepulchre) { + if (lostBoots) { + await dialogue([player], [ + text => (`As your hand touches the cradle, you hear a voice in your head of a million dead adventurers...`), + (text, lost) => (`You appear to have lost your boots!`), + (text, welcome) => (`....welcome adventurer... you have a choice....`), + text => (`You can choose between these two pairs of boots.`), + text => (`They will both protect your feet exactly the same, however they look very different. You can always come back and get another pair if you lose them, or even swap them for the other style!`), + + options => [ + `I'll take the colourful ones!`, [ + execute(() => { + player.giveItem(findItem(`rs:Fancy_boots`).gameId); + }) + ], + + `I'll take the fighting ones!`, [ + execute(() => { + player.giveItem(findItem(`rs:Fighting_boots`).gameId); + }) + ] + ], + (text, tag_congrats) => (`Congratulations! You have successfully navigated the Stronghold of Security and learned to secure your account. You have unlocked the 'Stamp Foot' emote. Remember to keep your account secure in the future!`) + ]); + } else { + await dialogue([player], [ + text => (`As your hand touches the cradle, you hear a voice in your head of a million dead adventurers...`), + options => [ + `Yes, I'd like the other pair instead please!`, [ + player => [Emote.HAPPY, `Yes, I'd like the other pair instead please!`], + execute(() => { + if (player.inventory.hasSpace()) { + if (player.hasItemOnPerson(fancyBoots.gameId)) { + player.removeFirstItem(fancyBoots.gameId); + player.giveItem(fightingBoots.gameId); + } else { + player.removeFirstItem(fightingBoots.gameId); + player.giveItem(fancyBoots.gameId); + } + } else { + dialogue([player], [ + player => [Emote.SAD, `Hmm, perhaps I should have some space in my pack before I do that.`] + ]) + } + }), + ], + `No thanks, I'll keep these!`, [ + player => [Emote.SAD, `No thanks, I'll keep these!`] + ] + ] + ]); + } + + + } else { + await dialogue([player], [ + text => (`As your hand touches the cradle, you hear a voice in your head of a million dead adventurers...`), + (text, welcome) => (`....welcome adventurer... you have a choice....`), + text => (`You can choose between these two pairs of boots.`), + text => (`They will both protect your feet exactly the same, however they look very different. You can always come back and get another pair if you lose them, or even swap them for the other style!`), + + options => [ + `I'll take the colourful ones!`, [ + execute(() => { + player.giveItem(findItem(`rs:Fancy_boots`).gameId); + unlockEmote(player, `STAMP`); + player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.sepulchreOfDeath = true; + }) + ], + + `I'll take the fighting ones!`, [ + execute(() => { + player.giveItem(findItem(`rs:Fighting_boots`).gameId); + unlockEmote(player, `STAMP`); + player.savedMetadata[`strongholdOfSecurityState`].floorCompletion.sepulchreOfDeath = true; + }) + ] + ], + (text, tag_congrats) => (`Congratulations! You have successfully navigated the Stronghold of Security and learned to secure your account. You have unlocked the 'Stamp Foot' emote. Remember to keep your account secure in the future!`) + ]); + } + } else { + const dialogueData = getRewardDataFromObjectId(object.objectId); + if(!dialogueData) { + await player.sendMessage(`Unable to get reward information for this object. Something is wrong.`); + return false; + } + await dialogue([player], [ + text => (dialogueData.initialMessage), + text => (`...congratulations adventurer, you have been deemed worthy of this reward. You have also unlocked the ` + dialogueData.emoteUnlocked + ` emote!`), + execute(() => { + player.giveItem({ itemId: findItem(`rs:coins`).gameId, amount: dialogueData.amountOfGoldRewarded }); + unlockEmote(player, dialogueData.emoteUnlocked.toUpperCase()); + setFloorCompletionFromObjectId(player, object.objectId, true); + }), + ]); + } + + return true; +} + +const onComplete = (task: TaskExecutor): void => { + +}; + +export default { + pluginId: 'rs:stronghold_of_security_rewards', + hooks: [ + { + type: 'object_interaction', + options: ['open', 'search'], + objectIds: [objectIds.strongholdOfSecurity.rewardObjects.giftOfPeace, + objectIds.strongholdOfSecurity.rewardObjects.grainOfPlenty, + objectIds.strongholdOfSecurity.rewardObjects.boxOfHealth, + objectIds.strongholdOfSecurity.rewardObjects.cradleOfLife], + strength: 'normal', + multi: false, + walkTo: true, + task: { + canActivate, + activate, + onComplete + } + } as ObjectInteractionActionHook + ] +}; diff --git a/src/plugins/dungeons/stronghold-of-security/stronghold-of-security.plugin.ts b/src/plugins/dungeons/stronghold-of-security/stronghold-of-security.plugin.ts new file mode 100644 index 000000000..4145491c9 --- /dev/null +++ b/src/plugins/dungeons/stronghold-of-security/stronghold-of-security.plugin.ts @@ -0,0 +1,223 @@ +import { ObjectInteractionAction, ObjectInteractionActionHook } from '@engine/world/action/object-interaction.action'; +import { TaskExecutor } from '@engine/world/action'; +import { directionNameFromIndex, WNES } from '@engine/world/direction'; +import { schedule } from '@engine/world/task'; +import { dialogue, DialogueTree, Emote, execute } from '@engine/world/actor/dialogue'; +import { Position } from '@engine/world/position'; +import { getRandomStrongholdOfSecurityQuizQuestion, strongholdOfSecurityQuizData } from '@engine/config'; +import { Player } from '@engine/world/actor/player/player'; +import { objectIds } from '@engine/world/config/object-ids'; +import { + getFloorCompletionFromObjectId, + getNpcKeyFromObjectId +} from '@plugins/dungeons/stronghold-of-security/stronghold-of-security-rewards.plugin'; +import { animationIds } from '@engine/world/config/animation-ids'; +import { StrongholdOfSecurityQuizQuestion } from '@engine/config/stronghold-of-security-quiz-config'; + +const canActivate = (task: TaskExecutor, taskIteration: number): boolean => { + const { actor, actionData: { position, object } } = task; + if (actor instanceof Player) { + if (!actor.savedMetadata[`strongholdOfSecurityState`]) { + actor.savedMetadata[`strongholdOfSecurityState`] = { + dueForSecurityQuestion: false, + floorCompletion: { + vaultOfWar: false, + catacombOfFamine: false, + pitOfPestilence: false, + sepulchreOfDeath: false + } + }; + } + } + return !(actor.position.distanceBetween(position) > 1); +} + +const activate = async (task: TaskExecutor, taskIteration: number): Promise => { + const { player, actionData: { position, object } } = task.getDetails(); + const objectOrientation = WNES[object.orientation]; + + if (player.position.distanceBetween(position) > 1) { + return false; + } + + if (player.position.distanceBetween(position) === 1) { + await player.waitForPathing(position, true); + } + + const doorOrientation = directionNameFromIndex(object.orientation); + const teleportPosition = position.clone(); + switch (doorOrientation) { + case 'NORTH': + if (player.position.y === position.y) { + teleportPosition.y++; + } + break; + + case 'EAST': + if (player.position.x === position.x) { + teleportPosition.x++; + } + break; + + case 'SOUTH': + if (player.position.y === position.y) { + teleportPosition.y--; + } + break; + + case 'WEST': + if (player.position.x === position.x) { + teleportPosition.x--; + } + break; + } + player.face(teleportPosition); + + const npcKey = getNpcKeyFromObjectId(object.objectId); + const floorCompleted = getFloorCompletionFromObjectId(player, object.objectId); + + if (player.savedMetadata[`strongholdOfSecurityState`].dueForSecurityQuestion && !floorCompleted) { + player.sessionMetadata[`correctAnswer`] = await promptPlayerWithSecurityQuestion(player, 0, object.objectId); + } else { + if (isWelcomeDoor(position) && player.position.y === position.y + 1) { + await dialogue([player, { npc: npcKey, key: 'gate' }], [ + gate => [Emote.GENERIC, `Greetings adventurer. This place is kept safe by the spirits within the doors. As you pass through you will be asked questions about security. Hopefully you will learn much from us.`], + gate => [Emote.GENERIC, `Please pass through and begin your adventure, beware of the various monsters that dwell within.`], + ]); + } + player.sessionMetadata[`correctAnswer`] = true; + } + + + if (player.sessionMetadata[`correctAnswer`]) { + player.sessionMetadata[`correctAnswer`] = false; + player.savedMetadata[`strongholdOfSecurityState`].dueForSecurityQuestion = !player.savedMetadata[`strongholdOfSecurityState`].dueForSecurityQuestion; + player.playAnimation(animationIds.touchStrongholdOfSecurityDoor); + player.playSound(2858); + await schedule(1); + + player.teleport(teleportPosition); + player.playAnimation(animationIds.lookAroundAfterStrongholdTeleportation); + await schedule(5); + } else { + return false; + } + + + return true; +} + +async function promptPlayerWithSecurityQuestion(player: Player, questionAttempt: number, objectId: number): Promise { + while (questionAttempt !== 3) { + if (questionAttempt === 3) { + return true; + } + + const npcKey = getNpcKeyFromObjectId(objectId); + + await dialogue([player, { + npc: npcKey, + key: 'gate' + }], generateStrongholdQuizDialogue(player, getRandomStrongholdOfSecurityQuizQuestion())); + + if (player.sessionMetadata[`correctAnswer`]) { + return true; + } else { + if(!player.sessionMetadata[`strongholdDialogueComplete`]) { + return false; + } + questionAttempt++; + } + } + return true; +} + +const isWelcomeDoor = (objectPosition: Position): boolean => { + const leftWelcomeDoor = new Position(1858, 5238); + const rightWelcomeDoor = new Position(1859, 5238); + + return (objectPosition.equals(leftWelcomeDoor) || objectPosition.equals(rightWelcomeDoor)); +}; + +function generateStrongholdQuizDialogue(player: Player, strongholdQuestion: StrongholdOfSecurityQuizQuestion): DialogueTree { + const questionText = strongholdOfSecurityQuizData.prefix + strongholdQuestion.questionText; + player.sessionMetadata[`strongholdDialogueComplete`] = false; + //TODO: Learn how to create option DialogueTrees, and make this more efficient. + switch (strongholdQuestion.options.length) { + case 2: + return [ + gate => [Emote.GENERIC, questionText], + options => [ + strongholdQuestion.options[0].optionText, [ + gate => [Emote.GENERIC, strongholdQuestion.options[0].doorResponse], + execute(() => player.sessionMetadata[`correctAnswer`] = strongholdQuestion.options[0].passable) + ], + strongholdQuestion.options[1].optionText, [ + gate => [Emote.GENERIC, strongholdQuestion.options[1].doorResponse], + execute(() => player.sessionMetadata[`correctAnswer`] = strongholdQuestion.options[1].passable) + ] + ], + execute(() => { + player.sessionMetadata[`strongholdDialogueComplete`] = true; + }) + ]; + + case 3: + return [ + gate => [Emote.GENERIC, questionText], + options => [ + strongholdQuestion.options[0].optionText, [ + gate => [Emote.GENERIC, strongholdQuestion.options[0].doorResponse], + execute(() => player.sessionMetadata[`correctAnswer`] = strongholdQuestion.options[0].passable) + ], + strongholdQuestion.options[1].optionText, [ + gate => [Emote.GENERIC, strongholdQuestion.options[1].doorResponse], + execute(() => player.sessionMetadata[`correctAnswer`] = strongholdQuestion.options[1].passable) + ], + strongholdQuestion.options[2].optionText, [ + gate => [Emote.GENERIC, strongholdQuestion.options[2].doorResponse], + execute(() => player.sessionMetadata[`correctAnswer`] = strongholdQuestion.options[2].passable) + ] + ], + execute(() => { + player.sessionMetadata[`strongholdDialogueComplete`] = true; + }) + ]; + + default: + return [ + gate => [Emote.GENERIC, `Something went wrong, beep boop!`], + execute(() => player.sessionMetadata[`correctAnswer`] = false) + ]; + } +} + +const onComplete = (task: TaskExecutor): void => { +}; + +export default { + pluginId: 'rs:stronghold_of_security_doors', + hooks: [ + { + type: 'object_interaction', + options: ['open'], + objectIds: [ + objectIds.strongholdOfSecurity.gates.gateOfWarLeft, + objectIds.strongholdOfSecurity.gates.gateOfWarRight, + objectIds.strongholdOfSecurity.gates.ricketyDoorLeft, + objectIds.strongholdOfSecurity.gates.ricketyDoorRight, + objectIds.strongholdOfSecurity.gates.oozingBarrierLeft, + objectIds.strongholdOfSecurity.gates.oozingBarrierRight, + objectIds.strongholdOfSecurity.gates.thePortalOfDeathLeft, + objectIds.strongholdOfSecurity.gates.thePortalOfDeathRight], + strength: 'normal', + multi: false, + walkTo: true, + task: { + canActivate, + activate, + onComplete + } + } as ObjectInteractionActionHook + ] +}; diff --git a/src/plugins/items/rotten-potato/helpers/rotten-potato-travel.ts b/src/plugins/items/rotten-potato/helpers/rotten-potato-travel.ts index 7685f08c9..3216e09f2 100644 --- a/src/plugins/items/rotten-potato/helpers/rotten-potato-travel.ts +++ b/src/plugins/items/rotten-potato/helpers/rotten-potato-travel.ts @@ -3,9 +3,10 @@ import { world } from '@engine/game-server'; import { widgetInteractionActionHandler } from '@engine/world/action/widget-interaction.action'; +import { widgets } from '@engine/config'; export function openTravel(player: Player, page: number) { - const widget = player.interfaceState.openWidget(27, { + const widget = player.interfaceState.openWidget(widgets.book, { slot: 'screen', fakeWidget: 3100002, metadata: { diff --git a/src/plugins/player/login-update-settings.plugin.ts b/src/plugins/player/login-update-settings.plugin.ts index b1d1b0a6d..c8e241c8d 100644 --- a/src/plugins/player/login-update-settings.plugin.ts +++ b/src/plugins/player/login-update-settings.plugin.ts @@ -13,6 +13,7 @@ export const handler: playerInitActionHandler = ({ player }) => { player.outgoingPackets.updateClientConfig(widgetScripts.chatEffects, settings.chatEffectsEnabled ? 0 : 1); player.outgoingPackets.updateClientConfig(widgetScripts.acceptAid, settings.acceptAidEnabled ? 1 : 0); player.outgoingPackets.updateClientConfig(widgetScripts.musicVolume, settings.musicVolume); + player.outgoingPackets.updateClientConfig(widgetScripts.musicPlayerAutoManual, settings.musicPlayerMode); player.outgoingPackets.updateClientConfig(widgetScripts.soundEffectVolume, settings.soundEffectVolume); player.outgoingPackets.updateClientConfig(widgetScripts.areaEffectVolume, settings.areaEffectVolume); player.outgoingPackets.updateClientConfig(widgetScripts.runMode, settings.runEnabled ? 1 : 0);