From 0f8929d614cae30270f43c090a76421a513220cd Mon Sep 17 00:00:00 2001 From: Majesty <32709570+majestyotbr@users.noreply.github.com> Date: Thu, 29 Aug 2024 08:55:41 -0300 Subject: [PATCH 01/14] fix: price mismatch between NPCs and typo in the rent house message (#2830) Monitoring feature to alert whenever there's a price mismatch between NPCs for item transactions. This ensures any instance where an item is sold for less than its purchase price, or vice versa, is promptly identified and reported. Co-authored-by: Leilani A. <168607226+kaleohanopahala@users.noreply.github.com> --- data-otservbr-global/npc/arito.lua | 3 - data-otservbr-global/npc/armenius.lua | 2 - data-otservbr-global/npc/bolfona.lua | 1 - data-otservbr-global/npc/boozer.lua | 2 - data-otservbr-global/npc/clyde.lua | 3 - data-otservbr-global/npc/dane.lua | 2 - data-otservbr-global/npc/maria.lua | 2 - data-otservbr-global/npc/mirabell.lua | 3 - data-otservbr-global/npc/miraia.lua | 2 - data-otservbr-global/npc/norma.lua | 3 - data-otservbr-global/npc/pugwah.lua | 3 - data-otservbr-global/npc/ramina.lua | 4 +- data-otservbr-global/npc/red_lilly.lua | 2 +- data-otservbr-global/npc/swolt.lua | 2 - data-otservbr-global/npc/tanaro.lua | 2 - data-otservbr-global/npc/urkalio.lua | 3 - data/scripts/lib/register_npc_type.lua | 76 ++++++++++++++++++-------- src/map/house/house.cpp | 2 +- 18 files changed, 57 insertions(+), 60 deletions(-) diff --git a/data-otservbr-global/npc/arito.lua b/data-otservbr-global/npc/arito.lua index caa33f8348f..1eab9a186c3 100644 --- a/data-otservbr-global/npc/arito.lua +++ b/data-otservbr-global/npc/arito.lua @@ -134,9 +134,6 @@ npcConfig.shop = { { itemName = "mug of lemonade", clientId = 2880, buy = 2, count = 12 }, { itemName = "mug of water", clientId = 2880, buy = 1, count = 1 }, { itemName = "mug of wine", clientId = 2880, buy = 3, count = 2 }, - { itemName = "vial of beer", clientId = 2874, buy = 2, count = 1, subType = 3 }, - { itemName = "vial of wine", clientId = 2874, buy = 3, count = 1, subType = 2 }, - { itemName = "vial of water", clientId = 2874, buy = 1, count = 1 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/armenius.lua b/data-otservbr-global/npc/armenius.lua index 8b24dc1d419..9164a78240e 100644 --- a/data-otservbr-global/npc/armenius.lua +++ b/data-otservbr-global/npc/armenius.lua @@ -97,8 +97,6 @@ npcConfig.shop = { { itemName = "mug of rum", clientId = 2880, buy = 10, count = 13 }, { itemName = "mug of wine", clientId = 2880, buy = 4, count = 2 }, { itemName = "tomato", clientId = 3596, buy = 3 }, - { itemName = "vial of beer", clientId = 2874, buy = 3, count = 1, subType = 3 }, - { itemName = "vial of wine", clientId = 2874, buy = 4, count = 1, subType = 2 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/bolfona.lua b/data-otservbr-global/npc/bolfona.lua index 83ee91f7ac9..adf900568ff 100644 --- a/data-otservbr-global/npc/bolfona.lua +++ b/data-otservbr-global/npc/bolfona.lua @@ -103,7 +103,6 @@ npcConfig.shop = { { itemName = "ham", clientId = 3582, buy = 8 }, { itemName = "meat", clientId = 3577, buy = 5 }, { itemName = "mug of beer", clientId = 2880, buy = 2, count = 3 }, - { itemName = "vial of beer", clientId = 2874, buy = 2, count = 1, subType = 3 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/boozer.lua b/data-otservbr-global/npc/boozer.lua index a2eaa22d179..b63fa7620fb 100644 --- a/data-otservbr-global/npc/boozer.lua +++ b/data-otservbr-global/npc/boozer.lua @@ -98,8 +98,6 @@ npcConfig.shop = { { itemName = "mug of lemonade", clientId = 2880, buy = 2, count = 12 }, { itemName = "mug of water", clientId = 2880, buy = 2, count = 1 }, { itemName = "mug of wine", clientId = 2880, buy = 3, count = 2 }, - { itemName = "vial of beer", clientId = 2874, buy = 2, count = 1, subType = 3 }, - { itemName = "vial of wine", clientId = 2874, buy = 3, count = 1, subType = 2 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/clyde.lua b/data-otservbr-global/npc/clyde.lua index 4142cee2cbc..fcaf9ea011a 100644 --- a/data-otservbr-global/npc/clyde.lua +++ b/data-otservbr-global/npc/clyde.lua @@ -64,9 +64,6 @@ npcConfig.shop = { { itemName = "mug of water", clientId = 2880, buy = 2, count = 1 }, { itemName = "mug of wine", clientId = 2880, buy = 3, count = 2 }, { itemName = "tomato", clientId = 3596, buy = 3 }, - { itemName = "vial of beer", clientId = 2874, buy = 2, count = 1, subType = 3 }, - { itemName = "vial of wine", clientId = 2874, buy = 3, count = 1, subType = 2 }, - { itemName = "vial of water", clientId = 2874, buy = 1, count = 1 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/dane.lua b/data-otservbr-global/npc/dane.lua index f2dd71762c0..c2e3c612414 100644 --- a/data-otservbr-global/npc/dane.lua +++ b/data-otservbr-global/npc/dane.lua @@ -67,8 +67,6 @@ npcConfig.shop = { { itemName = "bottle of lemonade", clientId = 2875, buy = 12, count = 12 }, { itemName = "bottle of milk", clientId = 2875, buy = 4, count = 9 }, { itemName = "bottle of water", clientId = 2875, buy = 2, count = 1 }, - { itemName = "vial of milk", clientId = 2874, buy = 4, count = 1, subType = 9 }, - { itemName = "vial of water", clientId = 2874, buy = 2, count = 1 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/maria.lua b/data-otservbr-global/npc/maria.lua index 74780e4a85a..7c63e11ac02 100644 --- a/data-otservbr-global/npc/maria.lua +++ b/data-otservbr-global/npc/maria.lua @@ -65,8 +65,6 @@ npcConfig.shop = { { itemName = "mug of wine", clientId = 2880, buy = 3, count = 2 }, { itemName = "tomato", clientId = 3596, buy = 5 }, { itemName = "valentine's cake", clientId = 6392, buy = 100 }, - { itemName = "vial of beer", clientId = 2874, buy = 2, count = 1, subType = 3 }, - { itemName = "vial of wine", clientId = 2874, buy = 3, count = 1, subType = 2 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/mirabell.lua b/data-otservbr-global/npc/mirabell.lua index 06362976254..db5f256ccd4 100644 --- a/data-otservbr-global/npc/mirabell.lua +++ b/data-otservbr-global/npc/mirabell.lua @@ -134,9 +134,6 @@ npcConfig.shop = { { itemName = "mug of lemonade", clientId = 2880, buy = 2, count = 12 }, { itemName = "mug of water", clientId = 2880, buy = 1, count = 1 }, { itemName = "mug of wine", clientId = 2880, buy = 3, count = 2 }, - { itemName = "vial of beer", clientId = 2874, buy = 2, count = 1, subType = 3 }, - { itemName = "vial of wine", clientId = 2874, buy = 3, count = 1, subType = 2 }, - { itemName = "vial of water", clientId = 2874, buy = 1, count = 1 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/miraia.lua b/data-otservbr-global/npc/miraia.lua index ad74b08040e..d8fbf586f2a 100644 --- a/data-otservbr-global/npc/miraia.lua +++ b/data-otservbr-global/npc/miraia.lua @@ -200,8 +200,6 @@ npcConfig.shop = { { itemName = "mug of milk", clientId = 2880, buy = 5, count = 9 }, { itemName = "mug of water", clientId = 2880, buy = 2, count = 1 }, { itemName = "scarab cheese", clientId = 169, buy = 100 }, - { itemName = "vial of milk", clientId = 2874, buy = 5, count = 1, subType = 9 }, - { itemName = "vial of water", clientId = 2874, buy = 2, count = 1 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/norma.lua b/data-otservbr-global/npc/norma.lua index 769bf7b7b3f..693dc7fadbb 100644 --- a/data-otservbr-global/npc/norma.lua +++ b/data-otservbr-global/npc/norma.lua @@ -200,9 +200,6 @@ npcConfig.shop = { { itemName = "mug of milk", clientId = 2880, buy = 2, count = 6 }, { itemName = "mug of wine", clientId = 2880, buy = 3, count = 2 }, { itemName = "party cake", clientId = 6279, buy = 50 }, - { itemName = "vial of beer", clientId = 2874, buy = 3, count = 1, subType = 3 }, - { itemName = "vial of wine", clientId = 2874, buy = 3, count = 1, subType = 2 }, - { itemName = "vial of milk", clientId = 2874, buy = 2, count = 1, subType = 9 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/pugwah.lua b/data-otservbr-global/npc/pugwah.lua index e9c0874d5aa..9e9007a7667 100644 --- a/data-otservbr-global/npc/pugwah.lua +++ b/data-otservbr-global/npc/pugwah.lua @@ -61,9 +61,6 @@ npcConfig.shop = { { itemName = "mug of lemonade", clientId = 2880, buy = 2, count = 12 }, { itemName = "mug of milk", clientId = 2880, buy = 2, count = 6 }, { itemName = "mug of wine", clientId = 2880, buy = 3, count = 2 }, - { itemName = "vial of beer", clientId = 2874, buy = 3, count = 1, subType = 3 }, - { itemName = "vial of wine", clientId = 2874, buy = 4, count = 1, subType = 2 }, - { itemName = "vial of water", clientId = 2874, buy = 2, count = 1 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/ramina.lua b/data-otservbr-global/npc/ramina.lua index d88b305aaeb..f9b253c477e 100644 --- a/data-otservbr-global/npc/ramina.lua +++ b/data-otservbr-global/npc/ramina.lua @@ -64,9 +64,7 @@ npcConfig.shop = { { itemName = "mug of wine", clientId = 2880, buy = 15, count = 2 }, { itemName = "orange", clientId = 3586, buy = 12 }, { itemName = "peas", clientId = 11683, buy = 5 }, - { itemName = "vial of fruit juice", clientId = 2874, buy = 10, count = 14 }, - { itemName = "vial of water", clientId = 2874, buy = 2, count = 1 }, - { itemName = "vial of wine", clientId = 2874, buy = 15, count = 1, subType = 2 }, + { itemName = "vial of water", clientId = 2874, buy = 10, count = 1 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/red_lilly.lua b/data-otservbr-global/npc/red_lilly.lua index 357cb02775b..b744fae905d 100644 --- a/data-otservbr-global/npc/red_lilly.lua +++ b/data-otservbr-global/npc/red_lilly.lua @@ -85,7 +85,7 @@ npcConfig.shop = { { itemName = "torch", clientId = 2920, buy = 2 }, { itemName = "vial", clientId = 2874, sell = 5 }, { itemName = "vial of oil", clientId = 2874, buy = 20, count = 7 }, - { itemName = "vial of water", clientId = 2874, buy = 10, count = 1 }, + { itemName = "vial of water", clientId = 2874, buy = 20, count = 1 }, { itemName = "watch", clientId = 2906, buy = 20, sell = 6 }, { itemName = "waterskin of water", clientId = 2901, buy = 10, count = 1 }, { itemName = "wooden hammer", clientId = 3459, sell = 15 }, diff --git a/data-otservbr-global/npc/swolt.lua b/data-otservbr-global/npc/swolt.lua index 7caa8aa4160..5d8a7b48830 100644 --- a/data-otservbr-global/npc/swolt.lua +++ b/data-otservbr-global/npc/swolt.lua @@ -86,8 +86,6 @@ npcConfig.shop = { { itemName = "mug of beer", clientId = 2880, buy = 2, count = 3 }, { itemName = "mug of water", clientId = 2880, buy = 1, count = 1 }, { itemName = "terramite eggs", clientId = 10453, sell = 50 }, - { itemName = "vial of beer", clientId = 2874, buy = 2, count = 1, subType = 3 }, - { itemName = "vial of water", clientId = 2874, buy = 1, count = 1 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/tanaro.lua b/data-otservbr-global/npc/tanaro.lua index 467b70989ae..bf43db15d79 100644 --- a/data-otservbr-global/npc/tanaro.lua +++ b/data-otservbr-global/npc/tanaro.lua @@ -67,8 +67,6 @@ npcConfig.shop = { { itemName = "salmon", clientId = 3579, buy = 6 }, { itemName = "valentine's cake", clientId = 6392, buy = 100 }, { itemName = "white mushroom", clientId = 3723, buy = 6 }, - { itemName = "vial of wine", clientId = 2874, buy = 3, count = 1, subType = 2 }, - { itemName = "vial of water", clientId = 2874, buy = 2, count = 1 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data-otservbr-global/npc/urkalio.lua b/data-otservbr-global/npc/urkalio.lua index 5919a42d761..dbcbbc238b1 100644 --- a/data-otservbr-global/npc/urkalio.lua +++ b/data-otservbr-global/npc/urkalio.lua @@ -62,9 +62,6 @@ npcConfig.shop = { { itemName = "mug of lemonade", clientId = 2880, buy = 2, count = 12 }, { itemName = "mug of water", clientId = 2880, buy = 1, count = 1 }, { itemName = "mug of wine", clientId = 2880, buy = 3, count = 2 }, - { itemName = "vial of beer", clientId = 2874, buy = 2, count = 1, subType = 3 }, - { itemName = "vial of wine", clientId = 2874, buy = 3, count = 1, subType = 2 }, - { itemName = "vial of water", clientId = 2874, buy = 1, count = 1 }, } -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) diff --git a/data/scripts/lib/register_npc_type.lua b/data/scripts/lib/register_npc_type.lua index 53a5d525e59..e191bbbe240 100644 --- a/data/scripts/lib/register_npc_type.lua +++ b/data/scripts/lib/register_npc_type.lua @@ -141,25 +141,57 @@ registerNpcType.events = function(npcType, mask) end end +-- Global item tracker to track buy and sell prices across all NPCs +NpcPriceChecker = NpcPriceChecker or {} + registerNpcType.shop = function(npcType, mask) if type(mask.shop) == "table" then for _, shopItems in pairs(mask.shop) do local parent = Shop() - if shopItems.itemName or shopItems.itemname then - parent:setNameItem(shopItems.itemName or shopItems.itemname) + local itemName = shopItems.itemName or shopItems.itemname + local clientId = shopItems.clientId or shopItems.clientid + local buyPrice = shopItems.buy + local sellPrice = shopItems.sell + local npcName = npcType:getName() -- Assuming `npcType` has a `getName` method to get the NPC's name + + if itemName then + parent:setNameItem(itemName) end - if shopItems.clientId or shopItems.clientid then - parent:setId(shopItems.clientId or shopItems.clientid) + if clientId then + parent:setId(clientId) end if shopItems.subType or shopItems.subtype or shopItems.count then parent:setCount(shopItems.subType or shopItems.subtype or shopItems.count) end - if shopItems.buy then - parent:setBuyPrice(shopItems.buy) + if buyPrice then + parent:setBuyPrice(buyPrice) + end + if sellPrice then + parent:setSellPrice(sellPrice) end - if shopItems.sell then - parent:setSellPrice(shopItems.sell) + + if clientId then + if not NpcPriceChecker[clientId] then + NpcPriceChecker[clientId] = { buy = nil, sell = nil, buyNpc = nil, sellNpc = nil } + end + + if buyPrice then + NpcPriceChecker[clientId].buy = buyPrice + NpcPriceChecker[clientId].buyNpc = npcName + end + + if sellPrice then + NpcPriceChecker[clientId].sell = sellPrice + NpcPriceChecker[clientId].sellNpc = npcName + end + + if NpcPriceChecker[clientId].buy and NpcPriceChecker[clientId].sell then + if NpcPriceChecker[clientId].sell > NpcPriceChecker[clientId].buy then + logger.warn("The item {} ({}) is being sold for a value greater than the value it is purchased for by the NPCs. Buy NPC: {}, Sell NPC: {}", itemName, clientId, NpcPriceChecker[clientId].buyNpc, NpcPriceChecker[clientId].sellNpc) + end + end end + if shopItems.storageKey or shopItems.storagekey then parent:setStorageKey(shopItems.storageKey or shopItems.storagekey) end @@ -169,26 +201,26 @@ registerNpcType.shop = function(npcType, mask) if shopItems.child then for _, children in pairs(shopItems.child) do local child = Shop() - if shopItems.itemName or shopItems.itemname then - child:setNameItem(shopItems.itemName or shopItems.itemname) + if children.itemName or children.itemname then + child:setNameItem(children.itemName or children.itemname) end - if shopItems.clientId or shopItems.clientid then - child:setId(shopItems.clientId or shopItems.clientid) + if children.clientId or children.clientid then + child:setId(children.clientId or children.clientid) end - if shopItems.subType or shopItems.subtype or shopItems.count then - child:setCount(shopItems.subType or shopItems.subtype or shopItems.count) + if children.subType or children.subtype or children.count then + child:setCount(children.subType or children.subtype or children.count) end - if shopItems.buy then - child:setBuyPrice(shopItems.buy) + if children.buy then + child:setBuyPrice(children.buy) end - if shopItems.sell then - child:setSellPrice(shopItems.sell) + if children.sell then + child:setSellPrice(children.sell) end - if shopItems.storageKey or shopItems.storagekey then - child:setStorageKey(shopItems.storageKey or shopItems.storagekey) + if children.storageKey or children.storagekey then + child:setStorageKey(children.storageKey or children.storagekey) end - if shopItems.storageValue or shopItems.storagevalue then - child:setStorageValue(shopItems.storageValue or shopItems.storagevalue) + if children.storageValue or children.storagevalue then + child:setStorageValue(children.storageValue or children.storagevalue) end parent:addChildShop(child) end diff --git a/src/map/house/house.cpp b/src/map/house/house.cpp index 261147fff40..f2cb26114bf 100644 --- a/src/map/house/house.cpp +++ b/src/map/house/house.cpp @@ -871,7 +871,7 @@ void Houses::payHouses(RentPeriod_t rentPeriod) const { } std::ostringstream ss; - ss << "Warning! \nThe " << period << " rent of " << house->getRent() << " gold for your house \"" << house->getName() << "\" is payable. Have it within " << daysLeft << " days or you will lose static_self_cast() house."; + ss << "Warning! \nThe " << period << " rent of " << house->getRent() << " gold for your house \"" << house->getName() << "\" is payable. Have it within " << daysLeft << " days or you will lose this house."; letter->setAttribute(ItemAttribute_t::TEXT, ss.str()); g_game().internalAddItem(player->getInbox(), letter, INDEX_WHEREEVER, FLAG_NOLIMIT); house->setPayRentWarnings(house->getPayRentWarnings() + 1); From 0d924e8b251d8840e07022900ee83e57c11080ab Mon Sep 17 00:00:00 2001 From: Majesty <32709570+majestyotbr@users.noreply.github.com> Date: Thu, 29 Aug 2024 08:59:22 -0300 Subject: [PATCH 02/14] feat: Nimmersatt's monsters (#2831) Add Nimmersatt's monsters: - Dragolisk, - Wardragon, - Mega Dragon. - Step in Teleport. Co-authored-by: hecmo94 <63259115+hecmo94@users.noreply.github.com> --- .../monster/dragons/dragolisk.lua | 122 ++++++++++++++++ .../monster/dragons/mega_dragon.lua | 126 +++++++++++++++++ .../monster/dragons/war_dragon.lua | 125 +++++++++++++++++ .../movements/teleport/dragolisk_teleport.lua | 26 ++++ data/items/items.xml | 132 ++++++++++++++++++ 5 files changed, 531 insertions(+) create mode 100644 data-otservbr-global/monster/dragons/dragolisk.lua create mode 100644 data-otservbr-global/monster/dragons/mega_dragon.lua create mode 100644 data-otservbr-global/monster/dragons/war_dragon.lua create mode 100644 data-otservbr-global/scripts/movements/teleport/dragolisk_teleport.lua diff --git a/data-otservbr-global/monster/dragons/dragolisk.lua b/data-otservbr-global/monster/dragons/dragolisk.lua new file mode 100644 index 00000000000..26cf05df8ec --- /dev/null +++ b/data-otservbr-global/monster/dragons/dragolisk.lua @@ -0,0 +1,122 @@ +local mType = Game.createMonsterType("Dragolisk") +local monster = {} + +monster.description = "a dragolisk" +monster.experience = 5050 +monster.outfit = { + lookType = 1707, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2456 +monster.Bestiary = { + class = "Dragon", + race = BESTY_RACE_DRAGON, + toKill = 2500, + FirstUnlock = 100, + SecondUnlock = 1000, + CharmsPoints = 50, + Stars = 5, + Occurrence = 0, + Locations = "Nimmersatt's Breeding Ground", +} + +monster.health = 6180 +monster.maxHealth = 6180 +monster.race = "blood" +monster.corpse = 44654 +monster.speed = 165 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 100, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "Just Look at me!", yell = false }, + { text = "I'll stare you down", yell = false }, + { text = "Let me have a look", yell = false }, +} + +monster.loot = { + { name = "Platinum Coin", chance = 52610, minCount = 1, maxCount = 46 }, + { name = "Dragolisk Poison Gland", chance = 12600 }, + { name = "Nimmersatt's Seal", chance = 8320 }, + { id = 282, chance = 7580 }, + { name = "Dragolisk Eye", chance = 5510 }, + { name = "Green Gem", chance = 8260 }, + { name = "Dragon's Tail", chance = 1003 }, + { name = "Dragon Shield", chance = 400 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -200 }, + { name = "combat", interval = 3000, chance = 27, type = COMBAT_PHYSICALDAMAGE, minDamage = -220, maxDamage = -250, length = 7, spread = 0, effect = CONST_ME_EXPLOSIONAREA, target = false }, + { name = "combat", interval = 2500, chance = 25, type = COMBAT_PHYSICALDAMAGE, minDamage = -200, maxDamage = -250, radius = 4, effect = CONST_ME_POFF, target = true }, + { name = "death chain", interval = 2500, chance = 20, minDamage = -250, maxDamage = -300, range = 7 }, +} + +monster.defenses = { + defense = 86, + armor = 86, + mitigation = 2.11, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 15 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = -10 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = -5 }, + { type = COMBAT_HOLYDAMAGE, percent = -15 }, + { type = COMBAT_DEATHDAMAGE, percent = -10 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otservbr-global/monster/dragons/mega_dragon.lua b/data-otservbr-global/monster/dragons/mega_dragon.lua new file mode 100644 index 00000000000..66696435486 --- /dev/null +++ b/data-otservbr-global/monster/dragons/mega_dragon.lua @@ -0,0 +1,126 @@ +local mType = Game.createMonsterType("Mega Dragon") +local monster = {} + +monster.description = "a mega dragon" +monster.experience = 7810 +monster.outfit = { + lookType = 1712, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2459 +monster.Bestiary = { + class = "Dragon", + race = BESTY_RACE_DRAGON, + toKill = 2500, + FirstUnlock = 100, + SecondUnlock = 1000, + CharmsPoints = 50, + Stars = 5, + Occurrence = 0, + Locations = "Nimmersatt's Breeding Ground", +} + +monster.health = 7920 +monster.maxHealth = 7920 +monster.race = "blood" +monster.corpse = 44663 +monster.speed = 170 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 100, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "Just Look at me!", yell = false }, + { text = "I'll stare you down", yell = false }, + { text = "Let me have a look", yell = false }, +} + +monster.loot = { + { name = "Platinum Coin", chance = 50780, minCount = 1, maxCount = 48 }, + { name = "Nimmersatt's Seal", chance = 8670 }, + { id = 3039, chance = 10090 }, + { name = "Molten Dragon Essence", chance = 4720 }, + { name = "Prismatic Quartz", chance = 4460 }, + { name = "Rainbow Quartz", chance = 4430, minCount = 1, maxCount = 2 }, + { id = 3041, chance = 6790 }, + { name = "Mega Dragon Heart", chance = 1850 }, + { name = "Violet Gem", chance = 1470 }, + { name = "Dragon Slayer", chance = 130 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -450 }, + { name = "combat", interval = 2500, chance = 15, type = COMBAT_PHYSICALDAMAGE, minDamage = -250, maxDamage = -350, range = 2, effect = CONST_ME_BIG_SCRATCH, target = true }, + { name = "combat", interval = 2500, chance = 20, type = COMBAT_FIREDAMAGE, minDamage = -250, maxDamage = -300, length = 8, spread = 4, effect = CONST_ME_EXPLOSIONHIT, target = false }, + { name = "combat", interval = 2500, chance = 15, type = COMBAT_FIREDAMAGE, minDamage = -200, maxDamage = -300, range = 7, radius = 4, shootEffect = CONST_ANI_FIRE, effect = CONST_ME_FIREAREA, target = true }, + { name = "combat", interval = 3000, chance = 5, type = COMBAT_PHYSICALDAMAGE, minDamage = -200, maxDamage = -350, radius = 4, effect = CONST_ME_POFF, target = true }, + { name = "death chain", interval = 2500, chance = 15, minDamage = -200, maxDamage = -350, range = 7 }, +} + +monster.defenses = { + defense = 76, + armor = 76, + mitigation = 1.96, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = -10 }, + { type = COMBAT_EARTHDAMAGE, percent = -10 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = -10 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otservbr-global/monster/dragons/war_dragon.lua b/data-otservbr-global/monster/dragons/war_dragon.lua new file mode 100644 index 00000000000..79d04bb2c0c --- /dev/null +++ b/data-otservbr-global/monster/dragons/war_dragon.lua @@ -0,0 +1,125 @@ +local mType = Game.createMonsterType("Wardragon") +local monster = {} + +monster.description = "a wardragon" +monster.experience = 5810 +monster.outfit = { + lookType = 1708, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2458 +monster.Bestiary = { + class = "Dragon", + race = BESTY_RACE_DRAGON, + toKill = 2500, + FirstUnlock = 100, + SecondUnlock = 1000, + CharmsPoints = 50, + Stars = 5, + Occurrence = 0, + Locations = "Nimmersatt's Breeding Ground", +} + +monster.health = 6960 +monster.maxHealth = 6960 +monster.race = "blood" +monster.corpse = 44656 +monster.speed = 165 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 100, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "Just Look at me!", yell = false }, + { text = "I'll stare you down", yell = false }, + { text = "Let me have a look", yell = false }, +} + +monster.loot = { + { name = "Platinum Coin", chance = 52560, minCount = 1, maxCount = 45 }, + { name = "Wardragon Claw", chance = 14420 }, + { name = "Nimmersatt's Seal", chance = 10620 }, + { name = "Dragon Tongue", chance = 8450 }, + { name = "Wardragon Tooth", chance = 6330 }, + { name = "Gold Ingot", chance = 6007 }, + { name = "Onyx Chip", chance = 4650, minCount = 1, maxCount = 2 }, + { name = "Black Pearl", chance = 4000 }, + { name = "White Gem", chance = 1000 }, + { name = "Dragonbone Staff", chance = 740 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -450 }, + { name = "combat", interval = 2500, chance = 20, type = COMBAT_DEATHDAMAGE, minDamage = -250, maxDamage = -400, radius = 4, effect = CONST_ME_MORTAREA, target = true }, + { name = "combat", interval = 3000, chance = 25, type = COMBAT_FIREDAMAGE, minDamage = -300, maxDamage = -400, length = 8, spread = 4, effect = CONST_ME_EXPLOSIONHIT, target = false }, + { name = "combat", interval = 3000, chance = 20, type = COMBAT_PHYSICALDAMAGE, minDamage = -275, maxDamage = -400, radius = 4, effect = CONST_ME_POFF, target = true }, + { name = "combat", interval = 3000, chance = 15, type = COMBAT_PHYSICALDAMAGE, minDamage = -250, maxDamage = -300, range = 2, effect = CONST_ME_BIG_SCRATCH, target = true }, +} + +monster.defenses = { + defense = 80, + armor = 80, + mitigation = 2.19, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = -5 }, + { type = COMBAT_EARTHDAMAGE, percent = -10 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = -10 }, + { type = COMBAT_HOLYDAMAGE, percent = -5 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otservbr-global/scripts/movements/teleport/dragolisk_teleport.lua b/data-otservbr-global/scripts/movements/teleport/dragolisk_teleport.lua new file mode 100644 index 00000000000..2c04c3f2a39 --- /dev/null +++ b/data-otservbr-global/scripts/movements/teleport/dragolisk_teleport.lua @@ -0,0 +1,26 @@ +local function teleportToDestination(creature, position) + if not creature:isPlayer() then + return true + end + + if position.x == 33216 and position.y == 31126 and position.z == 14 then + creature:teleportTo(Position(33186, 31190, 7)) + creature:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + return true + elseif position.x == 33187 and position.y == 31190 and position.z == 7 then + creature:teleportTo(Position(33217, 31123, 14)) + creature:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + return true + else + return false + end +end + +local dragoliskTeleport = MoveEvent() +function dragoliskTeleport.onStepIn(creature, item, position, fromPosition) + return teleportToDestination(creature, position) +end + +dragoliskTeleport:position({ x = 33216, y = 31126, z = 14 }) +dragoliskTeleport:position({ x = 33187, y = 31190, z = 7 }) +dragoliskTeleport:register() diff --git a/data/items/items.xml b/data/items/items.xml index 4978cf0bdc4..860a148fda2 100644 --- a/data/items/items.xml +++ b/data/items/items.xml @@ -76112,5 +76112,137 @@ Granted by TibiaGoals.com"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From a15477386a542ebb6b8c15fa742d4efc31fb5196 Mon Sep 17 00:00:00 2001 From: Majesty <32709570+majestyotbr@users.noreply.github.com> Date: Thu, 29 Aug 2024 09:00:26 -0300 Subject: [PATCH 03/14] fix: data-canary errors and warnings (#2844) --- data-canary/monster/magicals/guzzlemaw.lua | 71 +++++++++--------- .../spells/monster/fury_skill_reducer.lua | 27 +++++++ data-canary/world/canary-house.xml | 2 +- data-canary/world/canary-zones.xml | 2 + data-canary/world/canary.otbm | Bin 19718929 -> 19718948 bytes 5 files changed, 65 insertions(+), 37 deletions(-) create mode 100644 data-canary/scripts/spells/monster/fury_skill_reducer.lua create mode 100644 data-canary/world/canary-zones.xml diff --git a/data-canary/monster/magicals/guzzlemaw.lua b/data-canary/monster/magicals/guzzlemaw.lua index 8e38d459779..391a85118bc 100644 --- a/data-canary/monster/magicals/guzzlemaw.lua +++ b/data-canary/monster/magicals/guzzlemaw.lua @@ -23,8 +23,7 @@ monster.Bestiary = { CharmsPoints = 50, Stars = 4, Occurrence = 0, - Locations = "Guzzlemaw Valley, and a single spawn in a tower in Upper Roshamuul \z - (south of the Depot and west of the entrance to Roshamuul Prison).", + Locations = "Guzzlemaw Valley, and a single spawn in a tower in Upper Roshamuul (south of the Depot and west of the entrance to Roshamuul Prison).", } monster.health = 6400 @@ -64,7 +63,6 @@ monster.flags = { canWalkOnEnergy = true, canWalkOnFire = true, canWalkOnPoison = true, - pet = false, } monster.light = { @@ -78,63 +76,64 @@ monster.voices = { } monster.loot = { - { id = 3031, chance = 100000, maxCount = 100 }, -- gold coin - { id = 3035, chance = 100000, maxCount = 7 }, -- platinum coin - { id = 3104, chance = 10700 }, -- banana skin - { id = 3110, chance = 10500 }, -- piece of iron - { id = 3111, chance = 9500 }, -- fishbone + { name = "gold coin", chance = 100000, maxCount = 100 }, + { name = "platinum coin", chance = 100000, maxCount = 7 }, + { name = "banana skin", chance = 10700 }, + { name = "piece of iron", chance = 10500 }, + { name = "fishbone", chance = 9500 }, { id = 3114, chance = 10400 }, -- skull { id = 3115, chance = 9200 }, -- bone { id = 3116, chance = 4500 }, -- big bone - { id = 3265, chance = 2700 }, -- two handed sword + { name = "two handed sword", chance = 2700 }, { id = 3578, chance = 7000, maxCount = 3 }, -- fish - { id = 3582, chance = 10000 }, -- ham - { id = 5880, chance = 3000 }, -- iron ore - { id = 5895, chance = 5000 }, -- fish fin - { id = 5925, chance = 5700 }, -- hardened bone + { name = "ham", chance = 10000 }, + { name = "iron ore", chance = 3000 }, + { name = "fish fin", chance = 5000 }, + { name = "hardened bone", chance = 5700 }, { id = 5951, chance = 9400 }, -- fish tail - { id = 7404, chance = 1000 }, -- assassin dagger - { id = 7407, chance = 2000 }, -- haunted blade - { id = 7418, chance = 380 }, -- nightmare blade - { id = 238, chance = 17000, maxCount = 3 }, -- great mana potion - { id = 239, chance = 18500, maxCount = 2 }, -- great health potion - { id = 10389, chance = 1200 }, -- sai - { id = 16120, chance = 3000 }, -- violet crystal shard - { id = 16123, chance = 12000, maxCount = 2 }, -- brown crystal splinter - { id = 16126, chance = 7600 }, -- red crystal fragment + { name = "assassin dagger", chance = 1000 }, + { name = "haunted blade", chance = 2000 }, + { name = "nightmare blade", chance = 380 }, + { name = "great mana potion", chance = 17000, maxCount = 3 }, + { name = "great health potion", chance = 18500, maxCount = 2 }, + { name = "sai", chance = 1200 }, + { name = "violet crystal shard", chance = 3000 }, + { name = "brown crystal splinter", chance = 12000, maxCount = 2 }, + { name = "red crystal fragment", chance = 7600 }, { id = 16279, chance = 12000 }, -- crystal rubbish - { id = 20062, chance = 920 }, -- cluster of solace - { id = 20198, chance = 15000 }, -- frazzle tongue - { id = 20199, chance = 14000 }, -- frazzle skin + { name = "cluster of solace", chance = 8920 }, + { name = "frazzle tongue", chance = 15000 }, + { name = "frazzle skin", chance = 14000 }, } monster.attacks = { { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -499 }, -- bleed - { name = "condition", type = CONDITION_BLEEDING, interval = 2000, chance = 10, minDamage = -500, maxDamage = -1000, radius = 3, target = false }, - { name = "combat", interval = 2000, chance = 10, type = COMBAT_PHYSICALDAMAGE, minDamage = 0, maxDamage = -900, length = 8, spread = 3, effect = CONST_ME_EXPLOSIONAREA, target = false }, + { name = "condition", type = CONDITION_BLEEDING, interval = 2000, chance = 10, minDamage = -500, maxDamage = -1000, radius = 3, effect = CONST_ME_DRAWBLOOD, target = false }, + { name = "combat", interval = 2000, chance = 10, type = COMBAT_LIFEDRAIN, minDamage = 0, maxDamage = -900, length = 8, spread = 0, effect = CONST_ME_EXPLOSIONAREA, target = false }, { name = "combat", interval = 2000, chance = 20, type = COMBAT_PHYSICALDAMAGE, minDamage = 0, maxDamage = -500, radius = 2, shootEffect = CONST_ANI_LARGEROCK, effect = CONST_ME_STONES, target = true }, - { name = "speed", interval = 2000, chance = 15, speedChange = -100, radius = 6, effect = CONST_ME_MAGIC_RED, target = false, duration = 15000 }, - { name = "combat", interval = 2000, chance = 10, type = COMBAT_LIFEDRAIN, minDamage = 0, maxDamage = -800, length = 8, spread = 3, effect = CONST_ME_MAGIC_RED, target = false }, + { name = "speed", interval = 2000, chance = 15, speedChange = -800, radius = 6, effect = CONST_ME_MAGIC_RED, target = false, duration = 15000 }, + { name = "combat", interval = 2000, chance = 10, type = COMBAT_LIFEDRAIN, minDamage = 0, maxDamage = -800, length = 8, spread = 0, effect = CONST_ME_MAGIC_RED, target = false }, } monster.defenses = { defense = 50, - armor = 50, + armor = 74, + mitigation = 2.31, { name = "combat", interval = 2000, chance = 20, type = COMBAT_HEALING, minDamage = 250, maxDamage = 425, effect = CONST_ME_HITBYPOISON, target = false }, } monster.elements = { - { type = COMBAT_PHYSICALDAMAGE, percent = 10 }, - { type = COMBAT_ENERGYDAMAGE, percent = 5 }, - { type = COMBAT_EARTHDAMAGE, percent = 15 }, - { type = COMBAT_FIREDAMAGE, percent = 5 }, + { type = COMBAT_PHYSICALDAMAGE, percent = 5 }, + { type = COMBAT_ENERGYDAMAGE, percent = 15 }, + { type = COMBAT_EARTHDAMAGE, percent = 20 }, + { type = COMBAT_FIREDAMAGE, percent = 10 }, { type = COMBAT_LIFEDRAIN, percent = 0 }, { type = COMBAT_MANADRAIN, percent = 0 }, { type = COMBAT_DROWNDAMAGE, percent = 0 }, { type = COMBAT_ICEDAMAGE, percent = 5 }, - { type = COMBAT_HOLYDAMAGE, percent = 0 }, - { type = COMBAT_DEATHDAMAGE, percent = 5 }, + { type = COMBAT_HOLYDAMAGE, percent = -5 }, + { type = COMBAT_DEATHDAMAGE, percent = 10 }, } monster.immunities = { diff --git a/data-canary/scripts/spells/monster/fury_skill_reducer.lua b/data-canary/scripts/spells/monster/fury_skill_reducer.lua new file mode 100644 index 00000000000..1e8acd0ae7a --- /dev/null +++ b/data-canary/scripts/spells/monster/fury_skill_reducer.lua @@ -0,0 +1,27 @@ +local combat = {} + +for i = 65, 80 do + combat[i] = Combat() + combat[i]:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SOUND_YELLOW) + + local condition = Condition(CONDITION_ATTRIBUTES) + condition:setParameter(CONDITION_PARAM_TICKS, 5000) + condition:setParameter(CONDITION_PARAM_SKILL_DEFENSEPERCENT, i) + + local area = createCombatArea(AREA_CIRCLE3X3) + combat[i]:setArea(area) + combat[i]:addCondition(condition) +end + +local spell = Spell("instant") + +function spell.onCastSpell(creature, var) + return combat[math.random(65, 80)]:execute(creature, var) +end + +spell:name("fury skill reducer") +spell:words("###2") +spell:isAggressive(true) +spell:blockWalls(true) +spell:needLearn(true) +spell:register() diff --git a/data-canary/world/canary-house.xml b/data-canary/world/canary-house.xml index adaa1a1c8f2..43d8e80d11e 100644 --- a/data-canary/world/canary-house.xml +++ b/data-canary/world/canary-house.xml @@ -1,4 +1,4 @@ - + diff --git a/data-canary/world/canary-zones.xml b/data-canary/world/canary-zones.xml new file mode 100644 index 00000000000..a9224bd3c2d --- /dev/null +++ b/data-canary/world/canary-zones.xml @@ -0,0 +1,2 @@ + + diff --git a/data-canary/world/canary.otbm b/data-canary/world/canary.otbm index 280d57a9194069ec839bb8cab86c1ffbb960ccf8..8f787811a5395efb0e0cbbb2ac7f34ec12b9e0be 100644 GIT binary patch delta 1798 zcmYk)c~}%>9KdmAVRu|tg_VI_L6l1ogtE-+rnJKp)3nm=$Sfkm)XcO?p&E}&t#qmA zpbVnI4AUV?L~x-}3%e{jtVFb$?m|)7_50gpdG?Rb_j!Kv&g{E8`>tu)WM7YsHYM5} z6EP#dD1Yw4oJF&XX3i_ID7r-v7DOTnR@k7T7wqT_2l}8d`XL%IaH2nAp&<_OhDC85 z$Wy%uexI#TGt7yuW~&-$4NhC9x(;fpB{=C71unQT00~IMKqO%h1|u0MNJSdbF$6=A zfnmr*7KS4mIXDHUVgydZ=@^MKFbZd4G|s}=I0xtAJe-dUkc$g(5yoIFF2*Ie6qjKf zF2{JIqF}W`iMRrjjKY%1<$1O}n%to~TPEX5Ou;5N*}?YIL4xD$oA%W$^LS~A+US2p+RrRv=%!ff1wIhc!i zxEJ$r9~R(#EW{!_fCupq9>ybBj3p>W2_D5`c-$ye^(l^=!-;HqNU}PP%yIbrK5NHu zd$r=Tt#!+AE!d0C@HxJ~m-q_%@HM_cE55~d_#QuCKYqkd__;Ev^+0v- z7&m#+^tKe+?!b{Y#_MhCQo9%Q_Nq1rLI+J@`2=Q4#SGn(9&N&BfvOO!9Q> zFh_>27$X|Y1|>9kIvdU6o}qZBxx8lxU(w6FCVs~s_!EEOZ~TLQ@gM$22aezOqKeV*Ummvhd}-iMVF|eaA9Ee3f^v3`UgcE}h2N&Xzpn6U2jy&5oS2$!1B&y*_?q-WI znJsF|6!%`2F-2LFx*|6`@FEdO7>r~L!BC_i6=_I^55q7V85n^~WML$-F$yPOG;(kv zPQnu^10U?y(B zjkpOn;}#U*R@{c$RbT6@x#O(?dAv5I%613t#9g=>vvCjZ#eJBA`|$uC#6x%(kKj=h z<1svrComT!D8-YQhxuxiO(}4US`^Ps_a~8klG%=MIAl3=%wBB@S<5{#96lN{-bSuVUwVCe&jywqPq7unmm}pb5?R2p{7UY{w3?;8T2t zRO* z>t0ZTm7Um&pRo`7@e6)cgO$H^&gl88Jq~TdD&t*d+WpIFwXB{&ZL8Jt96giYS0Ax7 zwO-4OGX9Ze*!@elXiD#pxm7Fc9ax#{w>M~GoceLfu!lKM(R~v7xMKXHcjd>cGaIxO zrrzzbjao_XP~I4Ep~w}bCZOSW{DA{Fh(GZc{>DG}7l&{dM-aqO9K&&RA%yUPnt*8` zBvBHxBwAvTSS2<|jKnVKD{)9-CH*A*B?BY_B~Hm8Nu0zbiI*fu+!Bw(D@l|jNd`-j oB|{`bB`K0rNtz^G;*$)M43}g`Mo2OxS(1^a%{75+)12i007O(&kpKVy From 8056ca6ee747de90bf5bb053bde6f114f42af1fc Mon Sep 17 00:00:00 2001 From: Majesty <32709570+majestyotbr@users.noreply.github.com> Date: Thu, 29 Aug 2024 19:45:55 -0300 Subject: [PATCH 04/14] fix: datapacks tools actions (#2851) --- .../scripts/actions/tools/kitchen_knife.lua | 8 -- data-canary/scripts/actions/tools/sickle.lua | 8 -- data-canary/scripts/lib/register_actions.lua | 100 ++++++++------- .../scripts/lib/register_actions.lua | 115 ++---------------- .../scripts/actions/tools/sickle.lua | 0 src/canary_server.cpp | 1 + 6 files changed, 67 insertions(+), 165 deletions(-) delete mode 100644 data-canary/scripts/actions/tools/kitchen_knife.lua delete mode 100644 data-canary/scripts/actions/tools/sickle.lua rename {data-otservbr-global => data}/scripts/actions/tools/sickle.lua (100%) diff --git a/data-canary/scripts/actions/tools/kitchen_knife.lua b/data-canary/scripts/actions/tools/kitchen_knife.lua deleted file mode 100644 index 921baf9092d..00000000000 --- a/data-canary/scripts/actions/tools/kitchen_knife.lua +++ /dev/null @@ -1,8 +0,0 @@ -local kitchenKnife = Action() - -function kitchenKnife.onUse(player, item, fromPosition, target, toPosition, isHotkey) - return ActionsLib.useKitchenKnife(player, item, fromPosition, target, toPosition, isHotkey) -end - -kitchenKnife:id(3469) -kitchenKnife:register() diff --git a/data-canary/scripts/actions/tools/sickle.lua b/data-canary/scripts/actions/tools/sickle.lua deleted file mode 100644 index a4e024dde20..00000000000 --- a/data-canary/scripts/actions/tools/sickle.lua +++ /dev/null @@ -1,8 +0,0 @@ -local sickle = Action() - -function sickle.onUse(player, item, fromPosition, target, toPosition, isHotkey) - return ActionsLib.useSickle(player, item, fromPosition, target, toPosition, isHotkey) or ActionsLib.destroyItem(player, target, toPosition) -end - -sickle:id(3293, 3306, 32595) -sickle:register() diff --git a/data-canary/scripts/lib/register_actions.lua b/data-canary/scripts/lib/register_actions.lua index 7c0aa49102e..229d385b0de 100644 --- a/data-canary/scripts/lib/register_actions.lua +++ b/data-canary/scripts/lib/register_actions.lua @@ -1,12 +1,17 @@ -local holeId = { 294, 369, 370, 385, 394, 411, 412, 413, 432, 433, 435, 8709, 594, 595, 615, 609, 610, 615, 1156, 482, 483, 868, 874, 4824, 7768, 433, 432, 413, 7767, 411, 370, 369, 7737, 7755, 7768, 7767, 7515, 7516, 7517, 7518, 7519, 7520, 7521, 7522, 7762, 8144, 8690, 8709, 12203, 12961, 17239, 19220, 23364 } -- usable rope holes, for rope spots see global.lua -local wildGrowth = { 2130, 2130, 2982, 2524, 2030, 2029, 10182 } -- wild growth destroyable by machete -local jungleGrass = { [3696] = 3695, [3702] = 3701, [17153] = 17151 } -- grass destroyable by machete -local groundIds = { 354, 355 } -- pick usable ground +local holeId = { 294, 369, 370, 385, 394, 411, 412, 413, 432, 433, 435, 482, 483, 594, 595, 609, 610, 615, 868, 874, 1156, 4824, 7515, 7516, 7517, 7518, 7519, 7520, 7521, 7522, 7737, 7755, 7762, 7767, 7768, 8144, 8690, 8709, 12203, 12961, 17239, 19220, 23364 } -- usable rope holes, for rope spots see global.lua +local wildGrowth = { 3635, 30224 } -- wild growth destroyable by machete +local jungleGrass = { -- grass destroyable by machete + [3696] = 3695, + [3702] = 3701, + [17153] = 17151, +} +local groundIds = { 354, 355 } -- pick usable grounds local sandIds = { 231 } -- desert sand -local fruits = { 2673, 2674, 2675, 2676, 2677, 2678, 2679, 2680, 2681, 2682, 2684, 2685, 5097, 8839, 8840, 8841 } -- fruits to make decorated cake with knife +local fruits = { 3584, 3585, 3586, 3587, 3588, 3589, 3590, 3591, 3592, 3593, 3595, 3596, 5096, 8011, 8012, 8013 } -- fruits to make decorated cake with knife local holes = { 593, 606, 608, 867, 21341 } -- holes opened by shovel +local ropeSpots = { 386, 421, 12935, 12936, 14238, 17238, 21501, 21965, 21966, 21967, 21968, 23363 } -function destroyItem(player, target, toPosition) +function destroyItem(player, item, fromPosition, target, toPosition, isHotkey) if type(target) ~= "userdata" or not target:isItem() then return false end @@ -40,10 +45,8 @@ function destroyItem(player, target, toPosition) end end end - target:remove(1) end - toPosition:sendMagicEffect(CONST_ME_POFF) return true end @@ -67,7 +70,7 @@ function onUseMachete(player, item, fromPosition, target, toPosition, isHotkey) player:addAchievementProgress("Nothing Can Stop Me", 100) return true end - return destroyItem(player, target, toPosition) + return destroyItem(player, item, fromPosition, target, toPosition, isHotkey) end function onUsePick(player, item, fromPosition, target, toPosition, isHotkey) @@ -82,7 +85,6 @@ function onUsePick(player, item, fromPosition, target, toPosition, isHotkey) else player:addItem(3028) -- 49% chance of getting small diamond end - player:addAchievementProgress("Petrologist", 100) target:getPosition():sendMagicEffect(CONST_ME_BLOCKHIT) target:remove(1) @@ -99,7 +101,7 @@ function onUsePick(player, item, fromPosition, target, toPosition, isHotkey) return false end - if table.contains(groundIds, ground.itemid) and ground.actionid == actionIds.pickHole then + if table.contains(groundIds, ground.itemid) and (ground:hasAttribute(ITEM_ATTRIBUTE_UNIQUEID) or ground:hasAttribute(ITEM_ATTRIBUTE_ACTIONID)) then ground:transform(394) ground:decay() toPosition:sendMagicEffect(CONST_ME_POFF) @@ -127,41 +129,28 @@ function onUseRope(player, item, fromPosition, target, toPosition, isHotkey) local ground = tile:getGround() if ground and table.contains(ropeSpots, ground:getId()) or tile:getItemById(12935) then - tile = Tile(toPosition:moveUpstairs()) - if not tile then - return false - end - - if tile:hasFlag(TILESTATE_PROTECTIONZONE) and player:isPzLocked() then + if Tile(toPosition:moveUpstairs()):hasFlag(TILESTATE_PROTECTIONZONE) and player:isPzLocked() then player:sendCancelMessage(RETURNVALUE_PLAYERISPZLOCKED) return true end - player:teleportTo(toPosition, false) return true - end - - if table.contains(holeId, target.itemid) then + elseif table.contains(holeId, target.itemid) then toPosition.z = toPosition.z + 1 tile = Tile(toPosition) - if not tile then - return false - end - - local thing = tile:getTopVisibleThing() - if not thing then - return true - end - - if thing:isPlayer() then - if Tile(toPosition:moveUpstairs()):queryAdd(thing) ~= RETURNVALUE_NOERROR then - return false + if tile then + local thing = tile:getTopVisibleThing() + if thing:isPlayer() then + if Tile(toPosition:moveUpstairs()):hasFlag(TILESTATE_PROTECTIONZONE) and thing:isPzLocked() then + return false + end + return thing:teleportTo(toPosition, false) + end + if thing:isItem() and thing:getType():isMovable() then + return thing:moveTo(toPosition:moveUpstairs()) end - - return thing:teleportTo(toPosition, false) - elseif thing:isItem() and thing:getType():isMovable() then - return thing:moveTo(toPosition:moveUpstairs()) end + player:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) return true end return false @@ -182,7 +171,13 @@ function onUseShovel(player, item, fromPosition, target, toPosition, isHotkey) if table.contains(holes, groundId) then ground:transform(groundId + 1) ground:decay() - + toPosition:moveDownstairs() + toPosition.y = toPosition.y - 1 + if Tile(toPosition):hasFlag(TILESTATE_PROTECTIONZONE) and player:isPzLocked() then + player:sendCancelMessage(RETURNVALUE_PLAYERISPZLOCKED) + return true + end + player:teleportTo(toPosition, false) toPosition.z = toPosition.z + 1 tile:relocateTo(toPosition) player:addAchievementProgress("The Undertaker", 500) @@ -192,14 +187,14 @@ function onUseShovel(player, item, fromPosition, target, toPosition, isHotkey) player:addAchievementProgress("The Undertaker", 500) elseif target.itemid == 17950 then -- swamp digging if not player:hasExhaustion("swamp-digging") then - local chance = math.random(100) - if chance >= 1 and chance <= 42 then + local chance = math.random(1, 100) + if chance <= 42 then player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You dug up a dead snake.") player:addItem(4259) elseif chance >= 43 and chance <= 79 then player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You dug up a small diamond.") player:addItem(3028) - elseif chance >= 80 then + else player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You dug up a leech.") player:addItem(17858) end @@ -209,16 +204,15 @@ function onUseShovel(player, item, fromPosition, target, toPosition, isHotkey) end elseif table.contains(sandIds, groundId) then local randomValue = math.random(1, 100) - if target.actionid == actionIds.sandHole and randomValue <= 20 then + if target.actionid == 100 and randomValue <= 20 then ground:transform(615) ground:decay() elseif randomValue == 1 then - Game.createItem(3042, 1, toPosition) + Game.createItem(3042, 1, toPosition) -- Scarab Coin player:addAchievementProgress("Gold Digger", 100) elseif randomValue > 95 then Game.createMonster("Scarab", toPosition) end - toPosition:sendMagicEffect(CONST_ME_POFF) else return false @@ -238,6 +232,13 @@ function onUseScythe(player, item, fromPosition, target, toPosition, isHotkey) player:addAchievementProgress("Happy Farmer", 200) return true end + return destroyItem(player, item, fromPosition, target, toPosition, isHotkey) +end + +function onUseSickle(player, item, fromPosition, target, toPosition, isHotkey) + if not table.contains({ 3293, 3306 }, item.itemid) then + return false + end if target.itemid == 5464 then -- burning sugar cane target:transform(5463) @@ -246,14 +247,14 @@ function onUseScythe(player, item, fromPosition, target, toPosition, isHotkey) player:addAchievementProgress("Natural Sweetener", 50) return true end - return destroyItem(player, target, toPosition) + return destroyItem(player, item, fromPosition, target, toPosition, isHotkey) end function onUseCrowbar(player, item, fromPosition, target, toPosition, isHotkey) if not table.contains({ 3304, 9598 }, item.itemid) then return false end - return destroyItem(player, target, toPosition) + return destroyItem(player, item, fromPosition, target, toPosition, isHotkey) end function onUseKitchenKnife(player, item, fromPosition, target, toPosition, isHotkey) @@ -269,3 +270,10 @@ function onUseKitchenKnife(player, item, fromPosition, target, toPosition, isHot end return false end + +function onUseSpoon(player, item, fromPosition, target, toPosition, isHotkey) + if not table.contains({ 3468, 3470 }, item.itemid) then + return false + end + return false +end diff --git a/data-otservbr-global/scripts/lib/register_actions.lua b/data-otservbr-global/scripts/lib/register_actions.lua index 6df6b96473a..b696d44c9c6 100644 --- a/data-otservbr-global/scripts/lib/register_actions.lua +++ b/data-otservbr-global/scripts/lib/register_actions.lua @@ -1,106 +1,12 @@ -local holeId = { - 294, - 369, - 370, - 385, - 394, - 411, - 412, - 413, - 432, - 433, - 435, - 8709, - 594, - 595, - 615, - 609, - 610, - 615, - 1156, - 482, - 483, - 868, - 874, - 4824, - 7768, - 433, - 432, - 413, - 7767, - 411, - 370, - 369, - 7737, - 7755, - 7768, - 7767, - 7515, - 7516, - 7517, - 7518, - 7519, - 7520, - 7521, - 7522, - 7762, - 8144, - 8690, - 8709, - 12203, - 12961, - 17239, - 19220, - 23364, -} - -local Itemsgrinder = { +local holeId = { 294, 369, 370, 385, 394, 411, 412, 413, 432, 433, 435, 482, 483, 594, 595, 609, 610, 615, 868, 874, 1156, 4824, 7515, 7516, 7517, 7518, 7519, 7520, 7521, 7522, 7737, 7755, 7762, 7767, 7768, 8144, 8690, 8709, 12203, 12961, 17239, 19220, 23364 } +local itemsGrinder = { [675] = { item_id = 30004, effect = CONST_ME_BLUE_FIREWORKS }, -- Sapphire dust [16122] = { item_id = 21507, effect = CONST_ME_GREENSMOKE }, -- Pinch of crystal dust } - -local holes = { - 593, - 606, - 608, - 867, - 21341, -} - -local JUNGLE_GRASS = { - 3696, - 3702, - 17153, -} -local WILD_GROWTH = { - 2130, - 2130, - 2982, - 2524, - 2030, - 2029, - 10182, -} - -local fruits = { - 3584, - 3585, - 3586, - 3587, - 3588, - 3589, - 3590, - 3591, - 3592, - 3593, - 3595, - 3596, - 5096, - 8011, - 8012, - 8013, -} - +local holes = { 593, 606, 608, 867, 21341 } +local jungleGrass = { 3696, 3702, 17153 } +local wildGrowth = { 3635, 30224 } +local fruits = { 3584, 3585, 3586, 3587, 3588, 3589, 3590, 3591, 3592, 3593, 3595, 3596, 5096, 8011, 8012, 8013 } local lava = { Position(32808, 32336, 11), Position(32809, 32336, 11), @@ -867,13 +773,13 @@ function onUsePick(player, item, fromPosition, target, toPosition, isHotkey) end function onUseMachete(player, item, fromPosition, target, toPosition, isHotkey) - if table.contains(JUNGLE_GRASS, target.itemid) then + if table.contains(jungleGrass, target.itemid) then target:transform(target.itemid == 17153 and 17151 or target.itemid - 1) target:decay() return true end - if table.contains(WILD_GROWTH, target.itemid) then + if table.contains(wildGrowth, target.itemid) then toPosition:sendMagicEffect(CONST_ME_POFF) target:remove() return true @@ -1035,14 +941,17 @@ function onUseScythe(player, item, fromPosition, target, toPosition, isHotkey) target:transform(5463) target:decay() Game.createItem(5466, 1, toPosition) + return true elseif target.itemid == 3653 then -- wheat target:transform(3651) target:decay() Game.createItem(3605, 1, toPosition) + return true elseif target.itemid == 30623 then -- reed target:transform(30624) target:decay() Game.createItem(30975, 1, toPosition) + return true -- The secret library elseif toPosition == Position(32177, 31925, 7) then player:teleportTo({ x = 32515, y = 32535, z = 12 }) @@ -1135,7 +1044,7 @@ function onGrindItem(player, item, fromPosition, target, toPosition) if not (target.itemid == 21573) then return false end - for index, value in pairs(Itemsgrinder) do + for index, value in pairs(itemsGrinder) do if item.itemid == index then local topParent = item:getTopParent() if topParent.isItem and (not topParent:isItem() or topParent.itemid ~= 470) then diff --git a/data-otservbr-global/scripts/actions/tools/sickle.lua b/data/scripts/actions/tools/sickle.lua similarity index 100% rename from data-otservbr-global/scripts/actions/tools/sickle.lua rename to data/scripts/actions/tools/sickle.lua diff --git a/src/canary_server.cpp b/src/canary_server.cpp index e49a86d7d9f..cca280b69fa 100644 --- a/src/canary_server.cpp +++ b/src/canary_server.cpp @@ -367,6 +367,7 @@ void CanaryServer::loadModules() { modulesLoadHelper(g_modules().loadFromXml(), "modules/modules.xml"); logger.debug("Loading datapack scripts on folder: {}/", datapackName); + modulesLoadHelper(g_scripts().loadScripts(datapackFolder + "/scripts/lib", true, false), datapackFolder + "/scripts/libs"); // Load scripts modulesLoadHelper(g_scripts().loadScripts(datapackFolder + "/scripts", false, false), datapackFolder + "/scripts"); // Load monsters From d4b1b61c6c3f982606bf0122508486159eae66fa Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Thu, 5 Sep 2024 21:16:38 -0300 Subject: [PATCH 05/14] fix: remove bakragore icons talkaction (#2828) --- .../talkactions/god/icons_functions.lua | 6 +----- src/creatures/players/player.cpp | 14 ++++++++++++++ src/creatures/players/player.hpp | 2 ++ .../creatures/player/player_functions.cpp | 19 +++++++++++++++++++ .../creatures/player/player_functions.hpp | 2 ++ 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/data/scripts/talkactions/god/icons_functions.lua b/data/scripts/talkactions/god/icons_functions.lua index 9f1ee85c0f3..059d7ac6ed9 100644 --- a/data/scripts/talkactions/god/icons_functions.lua +++ b/data/scripts/talkactions/god/icons_functions.lua @@ -78,11 +78,7 @@ function bakragoreIcon.onSay(player, words, param) end if param == "remove" then - for i = 1, 10 do - if player:hasCondition(CONDITION_BAKRAGORE, i) then - player:removeCondition(CONDITION_BAKRAGORE, i) - end - end + player:removeIconBakragore() player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Removed all Bakragore icons.") return true end diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 45f5808d633..9ba9f3b74d6 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -6209,6 +6209,20 @@ void Player::sendIconBakragore(const IconBakragore icon) { } } +void Player::removeBakragoreIcons() { + for (auto icon : magic_enum::enum_values()) { + if (hasCondition(CONDITION_BAKRAGORE, enumToValue(icon))) { + removeCondition(CONDITION_BAKRAGORE, CONDITIONID_DEFAULT, true); + } + } +} + +void Player::removeBakragoreIcon(const IconBakragore icon) { + if (hasCondition(CONDITION_BAKRAGORE, enumToValue(icon))) { + removeCondition(CONDITION_BAKRAGORE, CONDITIONID_DEFAULT, true); + } +} + void Player::sendCyclopediaCharacterAchievements(uint16_t secretsUnlocked, std::vector> achievementsUnlocked) { if (client) { client->sendCyclopediaCharacterAchievements(secretsUnlocked, achievementsUnlocked); diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 83515b2e004..5a703a1d0bf 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -1422,6 +1422,8 @@ class Player final : public Creature, public Cylinder, public Bankable { void sendClosePrivate(uint16_t channelId); void sendIcons(); void sendIconBakragore(const IconBakragore icon); + void removeBakragoreIcons(); + void removeBakragoreIcon(const IconBakragore icon); void sendClientCheck() const { if (client) { client->sendClientCheck(); diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index d360b482a7b..7bbe495f7c2 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -4412,3 +4412,22 @@ int PlayerFunctions::luaPlayerSendIconBakragore(lua_State* L) { pushBoolean(L, true); return 1; } + +int PlayerFunctions::luaPlayerRemoveIconBakragore(lua_State* L) { + // player:removeIconBakragore(iconType or nil for remove all bakragore icons) + const auto &player = getUserdataShared(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + auto iconType = getNumber(L, 2, IconBakragore::None); + if (iconType == IconBakragore::None) { + player->removeBakragoreIcons(); + } else { + player->removeBakragoreIcon(iconType); + } + + pushBoolean(L, true); + return 1; +} diff --git a/src/lua/functions/creatures/player/player_functions.hpp b/src/lua/functions/creatures/player/player_functions.hpp index 000c5f755c0..7cee43424cd 100644 --- a/src/lua/functions/creatures/player/player_functions.hpp +++ b/src/lua/functions/creatures/player/player_functions.hpp @@ -378,6 +378,7 @@ class PlayerFunctions final : LuaScriptInterface { registerMethod(L, "Player", "takeScreenshot", PlayerFunctions::luaPlayerTakeScreenshot); registerMethod(L, "Player", "sendIconBakragore", PlayerFunctions::luaPlayerSendIconBakragore); + registerMethod(L, "Player", "removeIconBakragore", PlayerFunctions::luaPlayerRemoveIconBakragore); GroupFunctions::init(L); GuildFunctions::init(L); @@ -743,6 +744,7 @@ class PlayerFunctions final : LuaScriptInterface { static int luaPlayerTakeScreenshot(lua_State* L); static int luaPlayerSendIconBakragore(lua_State* L); + static int luaPlayerRemoveIconBakragore(lua_State* L); friend class CreatureFunctions; }; From dec6ad0f04bf8cdec78917376077d0189d56572e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Morais?= Date: Fri, 6 Sep 2024 10:35:31 -0300 Subject: [PATCH 06/14] fix: container:addItem missing return (#2857) --- data-otservbr-global/monster/reptiles/two-headed_turtle.lua | 2 +- data/libs/functions/container.lua | 6 +++--- src/lua/functions/items/container_functions.cpp | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/data-otservbr-global/monster/reptiles/two-headed_turtle.lua b/data-otservbr-global/monster/reptiles/two-headed_turtle.lua index 2d1c2090b37..50b49aa8cc5 100644 --- a/data-otservbr-global/monster/reptiles/two-headed_turtle.lua +++ b/data-otservbr-global/monster/reptiles/two-headed_turtle.lua @@ -70,7 +70,7 @@ monster.loot = { { name = "two-headed turtle heads", chance = 8700 }, { name = "strong mana potion", chance = 13373 }, { name = "hydrophytes", chance = 11000 }, - { id = 1047, chance = 6388 }, -- bone + { id = 3115, chance = 6388 }, -- bone { name = "glacier shoes", chance = 4650 }, { id = 281, chance = 3582 }, -- giant shimmering pearl (green) { name = "small tropical fish", chance = 3582 }, diff --git a/data/libs/functions/container.lua b/data/libs/functions/container.lua index f932b37dfe4..824a12f5e02 100644 --- a/data/libs/functions/container.lua +++ b/data/libs/functions/container.lua @@ -20,7 +20,7 @@ function Container:addLoot(loot) local countToAdd = math.min(remainingCount, stackSize) local tmpItem = self:addItem(itemId, countToAdd, INDEX_WHEREEVER, FLAG_NOLIMIT) if not tmpItem then - logger.warn("Container:addLoot: failed to add stackable item: {}, to corpse {} with id {}", ItemType(itemId):getName(), self:getName(), self:getId()) + logger.warn("Container:addLoot: failed to add stackable item: {} with id {}, to corpse {} with id {}", ItemType(itemId):getName(), itemId, self:getName(), self:getId()) goto continue end remainingCount = remainingCount - countToAdd @@ -28,13 +28,13 @@ function Container:addLoot(loot) elseif iType:getCharges() ~= 0 then local tmpItem = self:addItem(itemId, item.count, INDEX_WHEREEVER, FLAG_NOLIMIT) if not tmpItem then - logger.warn("Container:addLoot: failed to add charge item: {}, to corpse {} with id {}", ItemType(itemId):getName(), self:getName(), self:getId()) + logger.warn("Container:addLoot: failed to add charge item: {} with id {}, to corpse {} with id {}", ItemType(itemId):getName(), itemId, self:getName(), self:getId()) end else for i = 1, item.count do local tmpItem = self:addItem(itemId, 1, INDEX_WHEREEVER, FLAG_NOLIMIT) if not tmpItem then - logger.warn("Container:addLoot: failed to add item: {}, to corpse {} with id {}", ItemType(itemId):getName(), self:getName(), self:getId()) + logger.warn("Container:addLoot: failed to add item: {} with id {}, to corpse {} with id {}", ItemType(itemId):getName(), itemId, self:getName(), self:getId()) goto continue end diff --git a/src/lua/functions/items/container_functions.cpp b/src/lua/functions/items/container_functions.cpp index a2251de641c..30eb26c6801 100644 --- a/src/lua/functions/items/container_functions.cpp +++ b/src/lua/functions/items/container_functions.cpp @@ -166,6 +166,7 @@ int ContainerFunctions::luaContainerAddItem(lua_State* L) { setItemMetatable(L, -1, item); } else { reportErrorFunc(fmt::format("Cannot add item to container, error code: '{}'", getReturnMessage(ret))); + pushBoolean(L, false); } return 1; } From b330a33d90d114eb383ecab1c389aed4e7a2131a Mon Sep 17 00:00:00 2001 From: Karin Date: Mon, 9 Sep 2024 14:22:52 -0300 Subject: [PATCH 07/14] fix: fix load unique items from map (#2881) This fix the load of unique items. Enable purchasing backpacks when the player doesn't have a main backpack --- .../startup/others/functions.lua | 19 ++++++++++++------- src/creatures/npcs/npc.cpp | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/data-otservbr-global/startup/others/functions.lua b/data-otservbr-global/startup/others/functions.lua index 8624dc8b268..362e2a38b97 100644 --- a/data-otservbr-global/startup/others/functions.lua +++ b/data-otservbr-global/startup/others/functions.lua @@ -28,7 +28,7 @@ function loadLuaMapAction(tablename) if not value.itemId == false and tile:getItemCountById(value.itemId) == 0 then logger.error("[loadLuaMapAction] - Wrong item id {} found", value.itemId) logger.warn("Action id: {}, position {}", index, tile:getPosition():toString()) - break + goto continue end if value.itemId ~= false and tile:getItemCountById(value.itemId) > 0 then @@ -49,6 +49,7 @@ function loadLuaMapAction(tablename) tile:getGround():setAttribute(ITEM_ATTRIBUTE_ACTIONID, index) end end + ::continue:: end end end @@ -64,12 +65,12 @@ function loadLuaMapUnique(tablename) if not value.itemId == false and tile:getItemCountById(value.itemId) == 0 then logger.error("[loadLuaMapUnique] - Wrong item id {} found", value.itemId) logger.warn("Unique id: {}, position {}", index, tile:getPosition():toString()) - break + goto continue end if tile:getItemCountById(value.itemId) < 1 or value.itemId == false then logger.warn("[loadLuaMapUnique] - Wrong item id {} found", value.itemId) logger.warn("Unique id: {}, position {}, item id: wrong", index, tile:getPosition():toString()) - break + goto continue end item = tile:getItemById(value.itemId) -- If he found the item, add the unique id @@ -77,6 +78,8 @@ function loadLuaMapUnique(tablename) item:setAttribute(ITEM_ATTRIBUTE_UNIQUEID, index) end end + + ::continue:: end end @@ -91,7 +94,7 @@ function loadLuaMapSign(tablename) if tile:getItemCountById(value.itemId) == 0 then logger.error("[loadLuaMapSign] - Wrong item id {} found", value.itemId) logger.warn("Sign id: {}, position {}, item id: wrong", index, tile:getPosition():toString()) - break + goto continue end if tile:getItemCountById(value.itemId) == 1 then item = tile:getItemById(value.itemId) @@ -101,6 +104,7 @@ function loadLuaMapSign(tablename) item:setAttribute(ITEM_ATTRIBUTE_TEXT, value.text) end end + ::continue:: end end @@ -137,17 +141,18 @@ function loadLuaMapBookDocument(tablename) totals[2] = totals[2] + 1 else logger.warn("[loadLuaMapBookDocument] - Item not found! Index: {}, itemId: {}", index, value.itemId) - break + goto continue end else logger.warn("[loadLuaMapBookDocument] - Container not found! Index: {}, containerId: {}", index, value.containerId) - break + goto continue end else logger.warn("[loadLuaMapBookDocument] - Tile not found! Index: {}, position: x: {} y: {} z: {}", index, value.position.x, value.position.y, value.position.z) - break + goto continue end end + ::continue:: end if totals[1] == totals[2] then logger.debug("Loaded {} books and documents in the map", totals[2]) diff --git a/src/creatures/npcs/npc.cpp b/src/creatures/npcs/npc.cpp index 18ca0dd0758..6d38524aef3 100644 --- a/src/creatures/npcs/npc.cpp +++ b/src/creatures/npcs/npc.cpp @@ -234,8 +234,8 @@ void Npc::onPlayerBuyItem(std::shared_ptr player, uint16_t itemId, uint8 return; } - // Check if the player not have empty slots - if (!ignore && player->getFreeBackpackSlots() == 0) { + // Check if the player not have empty slots or the item is not a container + if (!ignore && (player->getFreeBackpackSlots() == 0 && (player->getInventoryItem(CONST_SLOT_BACKPACK) || (!Item::items[itemId].isContainer() || !(Item::items[itemId].slotPosition & SLOTP_BACKPACK))))) { player->sendCancelMessage(RETURNVALUE_NOTENOUGHROOM); return; } From a9e48759de7ab41bc42010cd88520757b47d4da0 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Lisboa Date: Wed, 11 Sep 2024 11:10:25 -0300 Subject: [PATCH 08/14] fix: wild growth (#2885) --- data-canary/scripts/lib/register_actions.lua | 2 +- data-otservbr-global/scripts/lib/register_actions.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data-canary/scripts/lib/register_actions.lua b/data-canary/scripts/lib/register_actions.lua index 229d385b0de..e59a46b3b03 100644 --- a/data-canary/scripts/lib/register_actions.lua +++ b/data-canary/scripts/lib/register_actions.lua @@ -1,5 +1,5 @@ local holeId = { 294, 369, 370, 385, 394, 411, 412, 413, 432, 433, 435, 482, 483, 594, 595, 609, 610, 615, 868, 874, 1156, 4824, 7515, 7516, 7517, 7518, 7519, 7520, 7521, 7522, 7737, 7755, 7762, 7767, 7768, 8144, 8690, 8709, 12203, 12961, 17239, 19220, 23364 } -- usable rope holes, for rope spots see global.lua -local wildGrowth = { 3635, 30224 } -- wild growth destroyable by machete +local wildGrowth = { 2130, 3635, 30224 } -- wild growth destroyable by machete local jungleGrass = { -- grass destroyable by machete [3696] = 3695, [3702] = 3701, diff --git a/data-otservbr-global/scripts/lib/register_actions.lua b/data-otservbr-global/scripts/lib/register_actions.lua index b696d44c9c6..6118f3b837a 100644 --- a/data-otservbr-global/scripts/lib/register_actions.lua +++ b/data-otservbr-global/scripts/lib/register_actions.lua @@ -5,7 +5,7 @@ local itemsGrinder = { } local holes = { 593, 606, 608, 867, 21341 } local jungleGrass = { 3696, 3702, 17153 } -local wildGrowth = { 3635, 30224 } +local wildGrowth = { 2130, 3635, 30224 } local fruits = { 3584, 3585, 3586, 3587, 3588, 3589, 3590, 3591, 3592, 3593, 3595, 3596, 5096, 8011, 8012, 8013 } local lava = { Position(32808, 32336, 11), From 5bcbc39e5f8cb06c7af774dae86294ed73e7cf26 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Wed, 11 Sep 2024 19:32:24 -0300 Subject: [PATCH 09/14] feat: full soul war quest (#2535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remember that for this quest to work properly, it is necessary to delete the old Soul War scripts (the repository had the quest divided into parts, and I completely redid it using modern features and systems, such as BossLever, Zones, EventCallback, and others). It is also necessary to update the soul war map, the new map is in the releases I won’t document all the changes here for obvious reasons; the quest is mostly written in Lua, so there are many monster scripts and other elements that were added or modified, making this a large pull request as a result. I thought about separating the pull request into smaller commits, but I came to the conclusion that this could cause confusion, since the quest would depend on several other commits to work correctly, so I decided to keep everything in one to avoid future "problems". Added talkactions: /distanceeffect (for testing distance effects) /testtaintconditions (for testing taint icons on player inventory bar) • The "Missing configuration for identifier" log has set to debug, since in cpp there are default values ​​already to avoid putting all the values ​​in lua • Fixed some EventCallbacks that were not properly passing arguments as references (using std::ref) • Added a check for EventCallbacks with the same names, improving the resource management of callbacks. Each time a callback was called, a new element was created in the vector. Now there will only be one and it will be reused with each execution • Passed some vectors from CreatureVector for copying, thus avoiding possible crashes when changing the vectors, it was necessary due to the change of maps in ebb and flow in real time (so the player can be in the creature vector during the vector reading and be removed) • Added new callback for MonsterType (lua scripts) called "onSpawn", which will execute whenever the creature is created • Added new callback for MonsterType (lua scripts) called "onPlayerAttack", which will be executed whenever a player attacks the monster, it should be used with care as the same callback will be valid for all instances of the same monster • Added new EventCallback called "playerOnThink", thus facilitating players' onThink • Added new EventCallbacks called "creatureOnCombat" and "mapOnLoad", to facilitate the development of the quest • Added a new function argument on the CreatureEvent "executeOnPrepareDeath" called "realDamage" • The Lua function "monster:setType" now includes a new argument called "restoreHealth" (boolean). This argument determines whether the function will restore the monster's health after setting its type. • The Lua function "player:addOutfit" now accept the "outfit name" and includes new argument called "addon" for setting the outfit's addon • New Lua function "player:sendCreatureAppear" to update the creature's appearance to the client (necessary for changing the ebb and flow maps, same reason I mentioned above, the player is on the map during the change, so it is necessary to send the update to the client) • Removed the exception for the message: "The map in folder 'x' is missing or corrupted", this prevents the console from stopping loading and closing the server, as in the case of a misconfigured worldchange. Co-authored-by: Pedro Henrique Alves Cruz --- data-otservbr-global/lib/quests/quest.lua | 1 + data-otservbr-global/lib/quests/soul_war.lua | 1589 +++++++++++++++++ .../quests/soul_war/aspect_of_power.lua | 25 +- ...nia.lua => goshnar's_megalomania_blue.lua} | 84 +- .../soul_war/goshnar's_megalomania_green.lua | 163 ++ .../soul_war/goshnar's_megalomania_purple.lua | 129 ++ .../quests/soul_war/goshnars_cruelty.lua | 40 +- .../quests/soul_war/goshnars_greed.lua | 55 +- .../quests/soul_war/goshnars_hatred.lua | 28 +- .../quests/soul_war/goshnars_malice.lua | 25 +- .../quests/soul_war/goshnars_spite.lua | 11 +- .../monster/quests/soul_war/greedbeast.lua | 98 + .../monster/quests/soul_war/mirror_image.lua | 34 + .../normal_monsters}/bony_sea_devil.lua | 10 +- .../normal_monsters}/brachiodemon.lua | 10 +- .../normal_monsters}/branchy_crawler.lua | 10 +- .../ashes_of_burning_hatred.lua | 89 + .../blaze_of_burning_hatred.lua | 89 + .../flame_of_burning_hatred.lua | 89 + .../spark_of_burning_hatred.lua | 89 + .../burning_hatred/symbol_of_hatred.lua | 84 + .../normal_monsters}/capricious_phantom.lua | 6 +- .../normal_monsters}/distorted_phantom.lua | 6 +- .../normal_monsters}/druid's_apparition.lua | 6 +- .../furious_crater/a_greedy_eye.lua | 93 + .../furious_crater}/cloak_of_terror.lua | 11 +- .../furious_crater}/courage_leech.lua | 6 +- .../furious_crater/poor_soul.lua | 118 ++ .../furious_crater}/vibrant_phantom.lua | 6 +- .../{ => normal_monsters}/hateful_soul.lua | 2 +- .../normal_monsters}/infernal_demon.lua | 6 +- .../normal_monsters}/infernal_phantom.lua | 6 +- .../normal_monsters}/knight's_apparition.lua | 6 +- .../soul_war/normal_monsters}/many_faces.lua | 10 +- .../greater_splinter_of_madness.lua | 107 ++ .../lesser_splinter_of_madness.lua | 107 ++ .../mighty_splinter_of_madness.lua | 113 ++ .../megalomania_room/necromantic_focus.lua | 82 + .../normal_monsters}/mould_phantom.lua | 6 +- .../normal_monsters}/paladin's_apparition.lua | 6 +- .../normal_monsters}/rotten_golem.lua | 6 +- .../sorcerer's_apparition.lua | 6 +- .../normal_monsters}/turbulent_elemental.lua | 6 +- .../monster/quests/soul_war/powerful_soul.lua | 115 ++ .../monster/quests/soul_war/soul_cage.lua | 71 + .../monster/quests/soul_war/soul_sphere.lua | 122 ++ .../monster/quests/soul_war/soulsnatcher.lua | 115 ++ .../quests/soul_war/spiteful_spitter.lua | 4 + .../monster/quests/soul_war/strong_soul.lua | 106 ++ .../monster/quests/soul_war/weak_soul.lua | 106 ++ .../monster/quests/soul_war/weeping_soul.lua | 91 + .../monster/undeads/hazardous_phantom.lua | 6 +- data-otservbr-global/npc/flickering_soul.lua | 203 +++ .../actions/bosses_levers/goshnar_cruelty.lua | 23 - .../actions/bosses_levers/goshnar_greed.lua | 23 - .../actions/bosses_levers/goshnar_hatred.lua | 23 - .../actions/bosses_levers/goshnar_malice.lua | 23 - .../bosses_levers/goshnar_megalomania.lua | 23 - .../actions/bosses_levers/goshnar_spite.lua | 23 - .../soul_war/action-reward_soul_war.lua | 60 + .../quests/soul_war/actions_bosses_killed.lua | 24 - .../soul_war/actions_portal_megalomania.lua | 38 - .../soul_war/actions_reward_soul_war.lua | 89 - .../soul_war/actions_soulwar_entrances.lua | 63 - .../eventcallback_on_combat_taint.lua | 126 ++ .../globalevent-ebb_and_flow_change_maps.lua | 135 ++ .../moveevent-claustrophobic-inferno-raid.lua | 61 + .../soul_war/moveevent-soul_war_entrances.lua | 147 ++ ...=> moveevent-teleport_entrance_reward.lua} | 7 +- .../quests/soul_war/soul_war_mechanics.lua | 1081 +++++++++++ .../quests/soul_war/spell-eye_beam.lua | 38 + .../soul_war/spell-fire_beam_cruelty.lua | 61 + .../soul_war/spell-fire_beam_megalomania.lua | 54 + .../soul_war/spell-megalomania_blue.lua | 58 + .../quests/soul_war/spell-soulsnatcher.lua | 58 + .../iron_servant_transformation.lua | 2 +- data-otservbr-global/world/otservbr-house.xml | 2 +- .../world/otservbr-monster.xml | 70 + data-otservbr-global/world/otservbr-npc.xml | 3 + data-otservbr-global/world/otservbr-zones.xml | 2 +- .../soul_war/ebb_and_flow/ebb-flow-empty.otbm | Bin 0 -> 129452 bytes .../{inundate.otbm => ebb-flow-inundate.otbm} | Bin 325937 -> 322942 bytes .../quest/soul_war/ebb_and_flow/ebb-flow.otbm | Bin 0 -> 181627 bytes .../quest/soul_war/ebb_and_flow/empty.otbm | Bin 13957 -> 0 bytes data/items/items.xml | 101 +- data/libs/functions/boss_lever.lua | 8 +- data/libs/functions/creature.lua | 2 +- data/libs/functions/monster.lua | 4 +- data/libs/functions/monstertype.lua | 7 +- data/libs/functions/revscriptsys.lua | 8 + data/libs/systems/zones.lua | 10 +- data/scripts/actions/items/cobra_flask.lua | 2 +- data/scripts/eventcallbacks/README.md | 17 +- .../creature/on_area_combat.lua | 2 +- .../eventcallbacks/creature/on_hear.lua | 2 +- .../eventcallbacks/monster/on_spawn.lua | 2 +- .../monster/ondroploot__base.lua | 6 +- .../monster/ondroploot_boosted.lua | 4 +- .../monster/ondroploot_gem_atelier.lua | 2 +- .../monster/ondroploot_hazard.lua | 4 +- .../monster/ondroploot_prey.lua | 4 +- .../monster/ondroploot_wealth_duplex.lua | 4 +- .../monster/postdroploot_analyzer.lua | 2 +- .../eventcallbacks/party/on_disband.lua | 2 +- .../eventcallbacks/player/on_browse_field.lua | 2 +- .../scripts/eventcallbacks/player/on_look.lua | 2 +- .../eventcallbacks/player/on_look_in_shop.lua | 2 +- .../player/on_look_in_trade.lua | 2 +- .../eventcallbacks/player/on_remove_count.lua | 2 +- .../player/on_request_quest_line.lua | 2 +- .../player/on_request_quest_log.lua | 2 +- .../eventcallbacks/player/on_rotate_item.lua | 2 +- .../player/on_storage_update.lua | 2 +- .../eventcallbacks/player/on_trade_accept.lua | 2 +- data/scripts/lib/quests.lua | 2 + data/scripts/lib/register_lever_tables.lua | 4 +- data/scripts/lib/register_monster_type.lua | 4 +- data/scripts/lib/register_spells.lua | 6 + data/scripts/spells/healing/heal_malice.lua | 29 + data/scripts/systems/reward_chest.lua | 4 +- data/scripts/talkactions/gm/afk.lua | 2 +- .../talkactions/gm/distance_effect.lua | 37 + data/scripts/talkactions/gm/position.lua | 43 +- .../scripts/talkactions/god/add_condition.lua | 10 + src/config/configmanager.cpp | 2 +- src/creatures/appearance/outfit/outfit.hpp | 10 + src/creatures/combat/combat.cpp | 18 +- src/creatures/combat/condition.cpp | 32 +- src/creatures/creature.cpp | 2 +- src/creatures/creature.hpp | 10 +- src/creatures/creatures_definitions.hpp | 24 +- src/creatures/monsters/monster.cpp | 103 +- src/creatures/monsters/monster.hpp | 35 +- src/creatures/monsters/monsters.cpp | 37 +- src/creatures/monsters/monsters.hpp | 2 + .../monsters/spawns/spawn_monster.cpp | 3 +- src/creatures/players/grouping/party.cpp | 2 +- src/creatures/players/player.cpp | 18 +- src/creatures/players/player.hpp | 10 +- src/enums/player_icons.hpp | 10 +- src/game/bank/bank.cpp | 4 + src/game/game.cpp | 9 +- src/game/zones/zone.cpp | 5 + src/io/functions/iologindata_load_player.cpp | 7 +- src/io/io_bosstiary.cpp | 1 + src/io/iomap.cpp | 2 +- src/lua/callbacks/callbacks_definitions.hpp | 3 + src/lua/callbacks/event_callback.cpp | 105 +- src/lua/callbacks/event_callback.hpp | 22 +- src/lua/callbacks/events_callbacks.cpp | 31 +- src/lua/callbacks/events_callbacks.hpp | 26 +- src/lua/creature/creatureevent.cpp | 6 +- src/lua/creature/creatureevent.hpp | 2 +- src/lua/creature/events.cpp | 2 +- .../functions/core/game/bank_functions.cpp | 1 + .../functions/core/game/game_functions.cpp | 1 + src/lua/functions/core/game/lua_enums.cpp | 8 +- .../functions/core/game/zone_functions.cpp | 2 +- .../creatures/monster/monster_functions.cpp | 86 +- .../creatures/monster/monster_functions.hpp | 13 + .../monster/monster_type_functions.cpp | 4 +- .../monster/monster_type_functions.hpp | 2 + .../creatures/player/player_functions.cpp | 30 +- .../creatures/player/player_functions.hpp | 4 +- .../events/event_callback_functions.cpp | 14 +- src/map/map.cpp | 15 +- src/map/mapcache.cpp | 16 +- 167 files changed, 7124 insertions(+), 698 deletions(-) create mode 100644 data-otservbr-global/lib/quests/soul_war.lua rename data-otservbr-global/monster/quests/soul_war/{goshnars_megalomania.lua => goshnar's_megalomania_blue.lua} (63%) create mode 100644 data-otservbr-global/monster/quests/soul_war/goshnar's_megalomania_green.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/goshnar's_megalomania_purple.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/greedbeast.lua rename data-otservbr-global/monster/{undeads => quests/soul_war/normal_monsters}/bony_sea_devil.lua (95%) rename data-otservbr-global/monster/{demons => quests/soul_war/normal_monsters}/brachiodemon.lua (95%) rename data-otservbr-global/monster/{plants => quests/soul_war/normal_monsters}/branchy_crawler.lua (94%) create mode 100644 data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/ashes_of_burning_hatred.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/blaze_of_burning_hatred.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/flame_of_burning_hatred.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/spark_of_burning_hatred.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/symbol_of_hatred.lua rename data-otservbr-global/monster/{undeads => quests/soul_war/normal_monsters}/capricious_phantom.lua (97%) rename data-otservbr-global/monster/{undeads => quests/soul_war/normal_monsters}/distorted_phantom.lua (97%) rename data-otservbr-global/monster/{undeads => quests/soul_war/normal_monsters}/druid's_apparition.lua (97%) create mode 100644 data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/a_greedy_eye.lua rename data-otservbr-global/monster/{plants => quests/soul_war/normal_monsters/furious_crater}/cloak_of_terror.lua (94%) rename data-otservbr-global/monster/{extra_dimensional => quests/soul_war/normal_monsters/furious_crater}/courage_leech.lua (97%) create mode 100644 data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/poor_soul.lua rename data-otservbr-global/monster/{undeads => quests/soul_war/normal_monsters/furious_crater}/vibrant_phantom.lua (97%) rename data-otservbr-global/monster/quests/soul_war/{ => normal_monsters}/hateful_soul.lua (99%) rename data-otservbr-global/monster/{demons => quests/soul_war/normal_monsters}/infernal_demon.lua (97%) rename data-otservbr-global/monster/{undeads => quests/soul_war/normal_monsters}/infernal_phantom.lua (97%) rename data-otservbr-global/monster/{undeads => quests/soul_war/normal_monsters}/knight's_apparition.lua (97%) rename data-otservbr-global/monster/{demons => quests/soul_war/normal_monsters}/many_faces.lua (94%) create mode 100644 data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/greater_splinter_of_madness.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/lesser_splinter_of_madness.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/mighty_splinter_of_madness.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/necromantic_focus.lua rename data-otservbr-global/monster/{undeads => quests/soul_war/normal_monsters}/mould_phantom.lua (97%) rename data-otservbr-global/monster/{undeads => quests/soul_war/normal_monsters}/paladin's_apparition.lua (97%) rename data-otservbr-global/monster/{constructs => quests/soul_war/normal_monsters}/rotten_golem.lua (97%) rename data-otservbr-global/monster/{undeads => quests/soul_war/normal_monsters}/sorcerer's_apparition.lua (97%) rename data-otservbr-global/monster/{elementals => quests/soul_war/normal_monsters}/turbulent_elemental.lua (97%) create mode 100644 data-otservbr-global/monster/quests/soul_war/powerful_soul.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/soul_cage.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/soul_sphere.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/soulsnatcher.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/strong_soul.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/weak_soul.lua create mode 100644 data-otservbr-global/monster/quests/soul_war/weeping_soul.lua create mode 100644 data-otservbr-global/npc/flickering_soul.lua delete mode 100644 data-otservbr-global/scripts/actions/bosses_levers/goshnar_cruelty.lua delete mode 100644 data-otservbr-global/scripts/actions/bosses_levers/goshnar_greed.lua delete mode 100644 data-otservbr-global/scripts/actions/bosses_levers/goshnar_hatred.lua delete mode 100644 data-otservbr-global/scripts/actions/bosses_levers/goshnar_malice.lua delete mode 100644 data-otservbr-global/scripts/actions/bosses_levers/goshnar_megalomania.lua delete mode 100644 data-otservbr-global/scripts/actions/bosses_levers/goshnar_spite.lua create mode 100644 data-otservbr-global/scripts/quests/soul_war/action-reward_soul_war.lua delete mode 100644 data-otservbr-global/scripts/quests/soul_war/actions_bosses_killed.lua delete mode 100644 data-otservbr-global/scripts/quests/soul_war/actions_portal_megalomania.lua delete mode 100644 data-otservbr-global/scripts/quests/soul_war/actions_reward_soul_war.lua delete mode 100644 data-otservbr-global/scripts/quests/soul_war/actions_soulwar_entrances.lua create mode 100644 data-otservbr-global/scripts/quests/soul_war/eventcallback_on_combat_taint.lua create mode 100644 data-otservbr-global/scripts/quests/soul_war/globalevent-ebb_and_flow_change_maps.lua create mode 100644 data-otservbr-global/scripts/quests/soul_war/moveevent-claustrophobic-inferno-raid.lua create mode 100644 data-otservbr-global/scripts/quests/soul_war/moveevent-soul_war_entrances.lua rename data-otservbr-global/scripts/quests/soul_war/{actions_portal_reward_soulwar.lua => moveevent-teleport_entrance_reward.lua} (78%) create mode 100644 data-otservbr-global/scripts/quests/soul_war/soul_war_mechanics.lua create mode 100644 data-otservbr-global/scripts/quests/soul_war/spell-eye_beam.lua create mode 100644 data-otservbr-global/scripts/quests/soul_war/spell-fire_beam_cruelty.lua create mode 100644 data-otservbr-global/scripts/quests/soul_war/spell-fire_beam_megalomania.lua create mode 100644 data-otservbr-global/scripts/quests/soul_war/spell-megalomania_blue.lua create mode 100644 data-otservbr-global/scripts/quests/soul_war/spell-soulsnatcher.lua create mode 100644 data-otservbr-global/world/quest/soul_war/ebb_and_flow/ebb-flow-empty.otbm rename data-otservbr-global/world/quest/soul_war/ebb_and_flow/{inundate.otbm => ebb-flow-inundate.otbm} (80%) create mode 100644 data-otservbr-global/world/quest/soul_war/ebb_and_flow/ebb-flow.otbm delete mode 100644 data-otservbr-global/world/quest/soul_war/ebb_and_flow/empty.otbm create mode 100644 data/scripts/lib/quests.lua create mode 100644 data/scripts/spells/healing/heal_malice.lua create mode 100644 data/scripts/talkactions/gm/distance_effect.lua create mode 100644 data/scripts/talkactions/god/add_condition.lua diff --git a/data-otservbr-global/lib/quests/quest.lua b/data-otservbr-global/lib/quests/quest.lua index 21b1744fec6..392367c9d94 100644 --- a/data-otservbr-global/lib/quests/quest.lua +++ b/data-otservbr-global/lib/quests/quest.lua @@ -5,5 +5,6 @@ dofile(DATA_DIRECTORY .. "/lib/quests/killing_in_the_name_of.lua") dofile(DATA_DIRECTORY .. "/lib/quests/svargrond_arena.lua") dofile(DATA_DIRECTORY .. "/lib/quests/the_cursed_crystal.lua") dofile(DATA_DIRECTORY .. "/lib/quests/the_queen_of_the_banshees.lua") +dofile(DATA_DIRECTORY .. "/lib/quests/soul_war.lua") dofile(DATA_DIRECTORY .. "/lib/quests/their_masters_voice.lua") dofile(DATA_DIRECTORY .. "/lib/quests/the_primal_ordeal.lua") diff --git a/data-otservbr-global/lib/quests/soul_war.lua b/data-otservbr-global/lib/quests/soul_war.lua new file mode 100644 index 00000000000..a9e9d920e91 --- /dev/null +++ b/data-otservbr-global/lib/quests/soul_war.lua @@ -0,0 +1,1589 @@ +SoulWarQuest = { + -- Item ids + -- Goshnar's Hatred + bagYouDesireItemId = 34109, + goshnarsHatredSorrowId = 33793, + condensedRemorseId = 33792, + -- Goshnar's Spite + weepingSoulCorpseId = 33876, + searingFireId = 33877, + -- Goshnar's Cruelty + pulsatingEnergyId = 34005, + greedyMawId = 33890, + someMortalEssenceId = 33891, + theBloodOfCloakTerrorIds = { 33854, 34006, 34007 }, + -- Goshnar's Megalomania + deadAspectOfPowerCorpseId = 33949, + cleansedSanityItemId = 33950, + necromanticRemainsItemId = 33984, + + poolDamagePercentages = { + [33854] = 0.20, -- 20% of maximum health for the largest pool + [34006] = 0.15, -- 15% for a medium-sized pool + [34007] = 0.10, -- 10% for the smallest pool + }, + + timeToIncreaseCrueltyDefense = 15, -- In seconds, it will increase every 15 seconds if don't use mortal essence in greedy maw + useGreedMawCooldown = 30, -- In seconds + goshnarsCrueltyDefenseChange = 2, -- Defense change, the amount that will decrease or increase defense, the defense cannot decrease more than the monster's original defense amount + goshnarsCrueltyWaveInterval = 7, -- In seconds + + timeToReturnImmuneMegalomania = 70, -- In seconds + + bagYouDesireChancePerTaint = 10, -- Increases % per taint + bagYouDesireMonsters = { + "Bony Sea Devil", + "Brachiodemon", + "Branchy Crawler", + "Capricious Phantom", + "Cloak of Terror", + "Courage Leech", + "Distorted Phantom", + "Druid's Apparition", + "Infernal Demon", + "Infernal Phantom", + "Knight's Apparition", + "Many Faces", + "Mould Phantom", + "Paladin's Apparition", + "Rotten Golem", + "Sorcerer's Apparition", + "Turbulent Elemental", + "Vibrant Phantom", + "Hazardous Phantom", + "Goshnar's Cruelty", + "Goshnar's Spite", + "Goshnar's Malice", + "Goshnar's Hatred", + "Goshnar's Greed", + "Goshnar's Megalomania", + }, + + -- Goshnar's Cruelty pulsating energy monsters + pulsatingEnergyMonsters = { + "Vibrant Phantom", + "Cloak of Terror", + "Courage Leech", + }, + + miniBosses = { + ["Goshnar's Malice"] = true, + ["Goshnar's Hatred"] = true, + ["Goshnar's Spite"] = true, + ["Goshnar's Cruelty"] = true, + ["Goshnar's Greed"] = true, + }, + + finalRewards = { + { id = 34082, name = "soulcutter" }, + { id = 34083, name = "soulshredder" }, + { id = 34084, name = "soulbiter" }, + { id = 34085, name = "souleater" }, + { id = 34086, name = "soulcrusher" }, + { id = 34087, name = "soulmaimer" }, + { id = 34088, name = "soulbleeder" }, + { id = 34089, name = "soulpiercer" }, + { id = 34090, name = "soultainter" }, + { id = 34091, name = "soulhexer" }, + { id = 34092, name = "soulshanks" }, + { id = 34093, name = "soulstrider" }, + { id = 34094, name = "soulshell" }, + { id = 34095, name = "soulmantel" }, + { id = 34096, name = "soulshroud" }, + { id = 34097, name = "pair of soulwalkers" }, + { id = 34098, name = "pair of soulstalkers" }, + { id = 34099, name = "soulbastion" }, + }, + + kvSoulWar = KV.scoped("quest"):scoped("soul-war"), + -- Global KV for storage burning change form time + kvBurning = KV.scoped("quest"):scoped("soul-war"):scoped("burning-change-form"), + + rottenWastelandShrines = { + [33019] = { x = 33926, y = 31091, z = 13 }, + [33021] = { x = 33963, y = 31078, z = 13 }, + [33022] = { x = 33970, y = 30988, z = 13 }, + [33024] = { x = 33970, y = 31012, z = 13 }, + }, + + -- Lever room and teleports positions + goshnarsGreedAccessPosition = { from = { x = 33937, y = 31217, z = 11 }, to = { x = 33782, y = 31665, z = 14 } }, + goshnarsHatredAccessPosition = { from = { x = 33914, y = 31032, z = 12 }, to = { x = 33774, y = 31604, z = 14 } }, + -- Teleports from 1st/2nd/3rd floors + goshnarsCrueltyTeleportRoomPositions = { + { from = Position(33889, 31873, 3), to = Position(33830, 31881, 4), access = "first-floor-access", count = 40 }, + { from = Position(33829, 31880, 4), to = Position(33856, 31889, 5), access = "second-floor-access", count = 55 }, + { from = Position(33856, 31884, 5), to = Position(33857, 31865, 6), access = "third-floor-access", count = 70 }, + }, + + claustrophobicInfernoRaids = { + [1] = { + zoneArea = { + { x = 33985, y = 31053, z = 9 }, + { x = 34045, y = 31077, z = 9 }, + }, + sandTimerPositions = { + { x = 34012, y = 31049, z = 9 }, + { x = 34013, y = 31049, z = 9 }, + { x = 34014, y = 31049, z = 9 }, + { x = 34015, y = 31049, z = 9 }, + }, + zone = Zone("raid.first-claustrophobic-inferno"), + spawns = { + Position(33991, 31064, 9), + Position(34034, 31060, 9), + Position(34028, 31067, 9), + Position(34020, 31067, 9), + Position(34008, 31067, 9), + Position(34001, 31059, 9), + Position(33992, 31069, 9), + Position(34002, 31072, 9), + Position(34013, 31074, 9), + Position(33998, 31060, 9), + Position(34039, 31065, 9), + Position(34032, 31072, 9), + }, + exitPosition = { x = 34009, y = 31083, z = 9 }, + getZone = function() + return SoulWarQuest.claustrophobicInfernoRaids[1].zone + end, + }, + [2] = { + zoneArea = { + { x = 33988, y = 31042, z = 10 }, + { x = 34043, y = 31068, z = 10 }, + }, + sandTimerPositions = { + { x = 34012, y = 31075, z = 10 }, + { x = 34011, y = 31075, z = 10 }, + { x = 34010, y = 31075, z = 10 }, + }, + zone = Zone("raid.second-claustrophobic-inferno"), + spawns = { + Position(33999, 31046, 10), + Position(34011, 31047, 10), + Position(34015, 31052, 10), + Position(34021, 31044, 10), + Position(34029, 31054, 10), + Position(34037, 31052, 10), + Position(34037, 31060, 10), + Position(34023, 31062, 10), + Position(34012, 31061, 10), + Position(33998, 31061, 10), + Position(34005, 31052, 10), + }, + exitPosition = { x = 34011, y = 31028, z = 10 }, + getZone = function() + return SoulWarQuest.claustrophobicInfernoRaids[2].zone + end, + }, + [3] = { + zoneArea = { + { x = 33987, y = 31043, z = 11 }, + { x = 34044, y = 31076, z = 11 }, + }, + sandTimerPositions = { + { x = 34009, y = 31036, z = 11 }, + { x = 34010, y = 31036, z = 11 }, + { x = 34011, y = 31036, z = 11 }, + { x = 34012, y = 31036, z = 11 }, + { x = 34013, y = 31036, z = 11 }, + { x = 34014, y = 31036, z = 11 }, + }, + zone = Zone("raid.third-claustrophobic-inferno"), + spawns = { + Position(34005, 31049, 11), + Position(33999, 31051, 11), + Position(33995, 31055, 11), + Position(33999, 31068, 11), + Position(34016, 31068, 11), + Position(34030, 31070, 11), + Position(34038, 31066, 11), + Position(34038, 31051, 11), + Position(34033, 31051, 11), + Position(34025, 31049, 11), + Position(34013, 31058, 11), + Position(34021, 31059, 11), + Position(34027, 31063, 11), + Position(34007, 31063, 11), + Position(34004, 31059, 11), + }, + exitPosition = { x = 34014, y = 31085, z = 11 }, + getZone = function() + return SoulWarQuest.claustrophobicInfernoRaids[3].zone + end, + }, + spawnTime = 10, -- seconds + suriviveTime = 2 * 60, -- 2 minutes + timeToKick = 5, -- seconds + }, + + areaZones = { + monsters = { + ["zone.claustrophobic-inferno"] = "Brachiodemon", + ["zone.mirrored-nightmare"] = "Many Faces", + ["zone.ebb-and-flow"] = "Bony Sea Devil", + ["zone.furious-crater"] = "Cloak of Terror", + ["zone.rotten-wasteland"] = "Branchy Crawler", + ["boss.goshnar's-malice"] = "Dreadful Harvester", + ["boss.goshnar's-spite"] = "Dreadful Harvester", + ["boss.goshnar's-greed"] = "Dreadful Harvester", + ["boss.goshnar's-hatred"] = "Dreadful Harvester", + ["boss.goshnar's-cruelty"] = "Dreadful Harvester", + ["boss.goshnar's-megalomania-purple"] = "Dreadful Harvester", + }, + + claustrophobicInferno = Zone("zone.claustrophobic-inferno"), + mirroredNightmare = Zone("zone.mirrored-nightmare"), + ebbAndFlow = Zone("zone.ebb-and-flow"), + furiousCrater = Zone("zone.furious-crater"), + rottenWasteland = Zone("zone.rotten-wasteland"), + }, + + -- Levers configuration + levers = { + goshnarsMalicePosition = { x = 33678, y = 31599, z = 14 }, + goshnarsSpitePosition = { x = 33773, y = 31634, z = 14 }, + goshnarsGreedPosition = { x = 33775, y = 31665, z = 14 }, + goshnarsHatredPosition = { x = 33772, y = 31601, z = 14 }, + goshnarsCrueltyPosition = { x = 33853, y = 31854, z = 6 }, + goshnarsMegalomaniaPosition = { x = 33675, y = 31634, z = 14 }, + + -- Levers system + goshnarsSpite = { + boss = { + name = "Goshnar's Spite", + position = Position(33743, 31632, 14), + }, + requiredLevel = 250, + playerPositions = { + { pos = Position(33774, 31634, 14), teleport = Position(33742, 31639, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33775, 31634, 14), teleport = Position(33742, 31639, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33776, 31634, 14), teleport = Position(33742, 31639, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33777, 31634, 14), teleport = Position(33742, 31639, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33778, 31634, 14), teleport = Position(33742, 31639, 14), effect = CONST_ME_TELEPORT }, + }, + specPos = { + from = Position(33734, 31624, 14), + to = Position(33751, 31640, 14), + }, + onUseExtra = function(player) + local zone = Zone("boss.goshnar's-spite") + if zone then + local positions = zone:getPositions() + for _, pos in ipairs(positions) do + local tile = Tile(pos) + if tile then + local item = tile:getItemById(SoulWarQuest.weepingSoulCorpseId) + if item then + logger.debug("Weeping Soul Corpse removed from position: {}", pos) + item:remove() + end + end + end + end + end, + exit = Position(33621, 31427, 10), + timeToFightAgain = 20 * 60 * 60, -- 20 hours + }, + goshnarsMalice = { + boss = { + name = "Goshnar's Malice", + position = Position(33709, 31599, 14), + }, + requiredLevel = 250, + playerPositions = { + { pos = Position(33679, 31599, 14), teleport = Position(33710, 31605, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33680, 31599, 14), teleport = Position(33710, 31605, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33681, 31599, 14), teleport = Position(33710, 31605, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33682, 31599, 14), teleport = Position(33710, 31605, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33683, 31599, 14), teleport = Position(33710, 31605, 14), effect = CONST_ME_TELEPORT }, + }, + specPos = { + from = Position(33699, 31590, 14), + to = Position(33718, 31607, 14), + }, + onUseExtra = function(player) + addEvent(SpawnSoulCage, 23000) + end, + exit = Position(33621, 31427, 10), + timeToFightAgain = 20 * 60 * 60, -- 20 hours + }, + goshnarsGreed = { + boss = { + name = "Goshnar's Greed", + position = Position(33746, 31666, 14), + }, + requiredLevel = 250, + playerPositions = { + { pos = Position(33776, 31665, 14), teleport = Position(33747, 31671, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33777, 31665, 14), teleport = Position(33747, 31671, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33778, 31665, 14), teleport = Position(33747, 31671, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33779, 31665, 14), teleport = Position(33747, 31671, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33780, 31665, 14), teleport = Position(33747, 31671, 14), effect = CONST_ME_TELEPORT }, + }, + specPos = { + from = Position(33737, 31658, 14), + to = Position(33755, 31673, 14), + }, + timeToFightAgain = 0, -- TODO: Remove later + onUseExtra = function() + CreateGoshnarsGreedMonster("Greedbeast", Position(33744, 31666, 14)) + CreateGoshnarsGreedMonster("Soulsnatcher", Position(33747, 31668, 14)) + CreateGoshnarsGreedMonster("Weak Soul", Position(33750, 31666, 14)) + end, + exit = Position(33621, 31427, 10), + timeToFightAgain = 20 * 60 * 60, -- 20 hours + }, + goshnarsHatred = { + boss = { + name = "Goshnar's Hatred", + position = Position(33744, 31599, 14), + }, + monsters = { + { name = "Ashes of Burning Hatred", pos = { x = 33743, y = 31599, z = 14 } }, + }, + requiredLevel = 250, + playerPositions = { + { pos = Position(33773, 31601, 14), teleport = Position(33743, 31604, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33774, 31601, 14), teleport = Position(33743, 31604, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33775, 31601, 14), teleport = Position(33743, 31604, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33776, 31601, 14), teleport = Position(33743, 31604, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33777, 31601, 14), teleport = Position(33743, 31604, 14), effect = CONST_ME_TELEPORT }, + }, + specPos = { + from = Position(33735, 31592, 14), + to = Position(33751, 31606, 14), + }, + exit = Position(33621, 31427, 10), + timeToFightAgain = 20 * 60 * 60, -- 20 hours + onUseExtra = function(player) + SoulWarQuest.kvBurning:set("time", 180) + logger.trace("Goshnar's Hatred burning change form time set to: {}", 180) + player:resetGoshnarSymbolTormentCounter() + end, + }, + goshnarsCruelty = { + boss = { + name = "Goshnar's Cruelty", + position = Position(33856, 31866, 7), + }, + monsters = { + { name = "A Greedy Eye", pos = { x = 33856, y = 31858, z = 7 } }, + }, + requiredLevel = 250, + playerPositions = { + { pos = Position(33854, 31854, 6), teleport = Position(33856, 31872, 7), effect = CONST_ME_TELEPORT }, + { pos = Position(33855, 31854, 6), teleport = Position(33856, 31872, 7), effect = CONST_ME_TELEPORT }, + { pos = Position(33856, 31854, 6), teleport = Position(33856, 31872, 7), effect = CONST_ME_TELEPORT }, + { pos = Position(33857, 31854, 6), teleport = Position(33856, 31872, 7), effect = CONST_ME_TELEPORT }, + { pos = Position(33858, 31854, 6), teleport = Position(33856, 31872, 7), effect = CONST_ME_TELEPORT }, + }, + specPos = { + from = Position(33847, 31858, 7), + to = Position(33864, 31874, 7), + }, + exit = Position(33621, 31427, 10), + timeToFightAgain = 20 * 60 * 60, -- 20 hours + onUseExtra = function(player) + SoulWarQuest.kvSoulWar:remove("greedy-maw-action") + SoulWarQuest.kvSoulWar:remove("goshnars-cruelty-defense-drain") + player:soulWarQuestKV():scoped("furious-crater"):remove("greedy-maw-action") + end, + }, + goshnarsMegalomania = { + boss = { + name = "Goshnar's Megalomania Purple", + position = Position(33710, 31634, 14), + }, + monsters = { + { name = "Aspect of Power", pos = { x = 33710, y = 31635, z = 14 } }, + }, + requiredLevel = 250, + playerPositions = { + { pos = Position(33676, 31634, 14), teleport = Position(33710, 31639, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33677, 31634, 14), teleport = Position(33710, 31639, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33678, 31634, 14), teleport = Position(33710, 31639, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33679, 31634, 14), teleport = Position(33710, 31639, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33680, 31634, 14), teleport = Position(33710, 31639, 14), effect = CONST_ME_TELEPORT }, + }, + specPos = { + from = Position(33701, 31626, 14), + to = Position(33719, 31642, 14), + }, + exit = Position(33621, 31427, 10), + timeToFightAgain = 72 * 60 * 60, -- 72 hours + onUseExtra = function(player) + player:resetGoshnarSymbolTormentCounter() + SoulWarQuest.kvSoulWar:remove("cleansed-sanity-action") + player:soulWarQuestKV():scoped("furious-crater"):remove("cleansed-sanity-action") + end, + }, + }, + + -- Goshnar's Greed + apparitionNames = { + "Druid's Apparition", + "Knight's Apparition", + "Paladin's Apparition", + "Sorcerer's Apparition", + }, + + burningTransformations = { + { 180, "Ashes of Burning Hatred" }, + { 135, "Spark of Burning Hatred" }, + { 90, "Flame of Burning Hatred" }, + { 45, "Blaze of Burning Hatred" }, + }, + + burningHatredMonsters = { + "Ashes of Burning Hatred", + "Spark of Burning Hatred", + "Flame of Burning Hatred", + "Blaze of Burning Hatred", + }, + + requiredCountPerApparition = 25, + + -- Ebb and flow + ebbAndFlow = { + zone = Zone("ebb-and-flow-zone"), + -- Positions to teleport into rooms when innundate map is loaded + centerRoomPositions = { + { conor = { x = 33929, y = 31020, z = 9 }, teleportPosition = { x = 33939, y = 31021, z = 8 } }, + { conor = { x = 33929, y = 31047, z = 9 }, teleportPosition = { x = 33938, y = 31047, z = 8 } }, + { conor = { x = 33918, y = 31047, z = 9 }, teleportPosition = { x = 33903, y = 31049, z = 8 } }, + { conor = { x = 33898, y = 31054, z = 9 }, teleportPosition = { x = 33903, y = 31049, z = 8 } }, + { conor = { x = 33929, y = 31047, z = 9 }, teleportPosition = { x = 33938, y = 31047, z = 8 } }, + { conor = { x = 33940, y = 31054, z = 9 }, teleportPosition = { x = 33938, y = 31047, z = 8 } }, + { conor = { x = 33940, y = 31064, z = 9 }, teleportPosition = { x = 33937, y = 31074, z = 8 } }, + { conor = { x = 33937, y = 31086, z = 9 }, teleportPosition = { x = 33937, y = 31074, z = 8 } }, + { conor = { x = 33937, y = 31098, z = 9 }, teleportPosition = { x = 33929, y = 31109, z = 8 } }, + { conor = { x = 33933, y = 31109, z = 9 }, teleportPosition = { x = 33929, y = 31109, z = 8 } }, + { conor = { x = 33921, y = 31113, z = 9 }, teleportPosition = { x = 33929, y = 31109, z = 8 } }, + { conor = { x = 33912, y = 31113, z = 9 }, teleportPosition = { x = 33904, y = 31117, z = 8 } }, + { conor = { x = 33901, y = 31108, z = 9 }, teleportPosition = { x = 33904, y = 31117, z = 8 } }, + { conor = { x = 33901, y = 31098, z = 9 }, teleportPosition = { x = 33904, y = 31082, z = 8 } }, + { conor = { x = 33899, y = 31064, z = 9 }, teleportPosition = { x = 33904, y = 31082, z = 8 } }, + }, + mapsPath = { + empty = "data-otservbr-global/world/quest/soul_war/ebb_and_flow/ebb-flow-empty.otbm", + inundate = "data-otservbr-global/world/quest/soul_war/ebb_and_flow/ebb-flow-inundate.otbm", + ebbFlow = "data-otservbr-global/world/quest/soul_war/ebb_and_flow/ebb-flow.otbm", + }, + + -- In Minutes + intervalChangeMap = 2, + waitPosition = Position(33893, 31020, 8), + + getZone = function() + return SoulWarQuest.ebbAndFlow.zone + end, + + reloadZone = function() + SoulWarQuest.ebbAndFlow.zone:addArea({ x = 33869, y = 30991, z = 8 }, { x = 33964, y = 31147, z = 9 }) + end, + + kv = KV.scoped("quest"):scoped("soul-war"):scoped("ebb-and-flow-maps"), + isActive = function() + return SoulWarQuest.ebbAndFlow.kv:get("is-active") + end, + isLoadedEmptyMap = function() + return SoulWarQuest.ebbAndFlow.kv:get("is-loaded-empty-map") + end, + setActive = function(value) + SoulWarQuest.ebbAndFlow.kv:set("is-active", value) + end, + setLoadedEmptyMap = function(value) + SoulWarQuest.ebbAndFlow.kv:set("is-loaded-empty-map", value) + end, + + updateZonePlayers = function() + if SoulWarQuest.ebbAndFlow.zone and SoulWarQuest.ebbAndFlow.getZone():countPlayers() > 0 then + SoulWarQuest.ebbAndFlow.reloadZone() + local players = SoulWarQuest.ebbAndFlow.getZone():getPlayers() + for _, player in ipairs(players) do + logger.trace("Updating player: {}", player:getName()) + player:sendCreatureAppear() + end + end + end, + + -- Add here more positions of the pools that must transform before innundate map is loaded + poolPositions = { + { x = 33906, y = 31026, z = 9 }, + { x = 33901, y = 31026, z = 9 }, + { x = 33932, y = 31011, z = 9 }, + { x = 33941, y = 31033, z = 9 }, + { x = 33946, y = 31037, z = 9 }, + { x = 33939, y = 31056, z = 9 }, + }, + + boatId = 7272, + doorId = 33767, + smallPoolId = 33772, + MediumPoolId = 33773, + }, + + changeBlueEvent = nil, + changePurpleEvent = nil, + + changeMegalomaniaBlue = function() + local boss = Creature("Goshnar's Megalomania") + if boss then + boss:teleportTo(SoulWarQuest.levers.goshnarsMegalomania.boss.position) + boss:say("ENOUGH! I WILL MAKE YOU SUFFER FOR YOUR INSOLENCE! NOW - I - WILL - ANIHILATE - YOU!") + boss:setType("Goshnar's Megalomania Blue") + local function changeBack() + boss:setType("Goshnar's Megalomania Purple") + end + + changePurpleEvent = addEvent(changeBack, 7000) + end + end, + + -- Chance to heal the life of the monster by stepping on the corpse of "weeping soul" + goshnarsSpiteHealChance = 10, + -- Percentage that will heal by stepping and the chance is successful + goshnarsSpiteHealPercentage = 10, + + goshnarSpiteEntrancePosition = { fromPos = Position(33950, 31109, 8), toPos = Position(33780, 31634, 14) }, + + waterElementalOutfit = { + lookType = 286, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, + }, + + goshnarsSpiteFirePositions = { + -- North + { x = 33743, y = 31628, z = 14 }, + -- East + { x = 33736, y = 31632, z = 14 }, + -- West + { x = 33750, y = 31632, z = 14 }, + -- South + { x = 33742, y = 31637, z = 14 }, + }, + + -- Increased defense if the searing fire disappears + goshnarsSpiteIncreaseDefense = 10, + -- Count of monsters to kill for enter in the boss room + hardozousPanthomDeathCount = 20, + -- Time to fire created again + timeToCreateSearingFire = 14, -- In seconds + -- Time to remove the searing fire if player don't step on it + timeToRemoveSearingFire = 5, -- In seconds + cooldownToStepOnSearingFire = 56, -- In seconds (14 seconds x 4) + + -- Positions to teleport into rooms when innundate map is loaded + ebbAndFlowBoatTeleportPositions = { + -- First boat + -- Enter on boat + { register = { x = 33919, y = 31019, z = 8 }, teleportTo = { x = 33923, y = 31019, z = 8 } }, + { register = { x = 33919, y = 31020, z = 8 }, teleportTo = { x = 33923, y = 31020, z = 8 } }, + { register = { x = 33919, y = 31021, z = 8 }, teleportTo = { x = 33923, y = 31021, z = 8 } }, + { register = { x = 33919, y = 31022, z = 8 }, teleportTo = { x = 33923, y = 31022, z = 8 } }, + -- Back to innitial room + { register = { x = 33922, y = 31019, z = 8 }, teleportTo = { x = 33918, y = 31019, z = 8 } }, + { register = { x = 33922, y = 31020, z = 8 }, teleportTo = { x = 33918, y = 31020, z = 8 } }, + { register = { x = 33922, y = 31021, z = 8 }, teleportTo = { x = 33918, y = 31021, z = 8 } }, + { register = { x = 33922, y = 31022, z = 8 }, teleportTo = { x = 33918, y = 31022, z = 8 } }, + -- From boat to room + { register = { x = 33926, y = 31019, z = 8 }, teleportTo = { x = 33930, y = 31019, z = 8 } }, + { register = { x = 33926, y = 31020, z = 8 }, teleportTo = { x = 33930, y = 31020, z = 8 } }, + { register = { x = 33926, y = 31021, z = 8 }, teleportTo = { x = 33930, y = 31021, z = 8 } }, + { register = { x = 33926, y = 31022, z = 8 }, teleportTo = { x = 33930, y = 31022, z = 8 } }, + -- From room to boat + { register = { x = 33929, y = 31019, z = 8 }, teleportTo = { x = 33925, y = 31019, z = 8 } }, + { register = { x = 33929, y = 31020, z = 8 }, teleportTo = { x = 33925, y = 31020, z = 8 } }, + { register = { x = 33929, y = 31021, z = 8 }, teleportTo = { x = 33925, y = 31021, z = 8 } }, + { register = { x = 33929, y = 31022, z = 8 }, teleportTo = { x = 33925, y = 31022, z = 8 } }, + + -- Boat + -- Enter on boat + { register = { x = 33929, y = 31045, z = 8 }, teleportTo = { x = 33925, y = 31045, z = 8 } }, + { register = { x = 33929, y = 31046, z = 8 }, teleportTo = { x = 33925, y = 31046, z = 8 } }, + { register = { x = 33929, y = 31047, z = 8 }, teleportTo = { x = 33925, y = 31047, z = 8 } }, + { register = { x = 33929, y = 31048, z = 8 }, teleportTo = { x = 33925, y = 31048, z = 8 } }, + -- Back to room + { register = { x = 33926, y = 31045, z = 8 }, teleportTo = { x = 33930, y = 31045, z = 8 } }, + { register = { x = 33926, y = 31046, z = 8 }, teleportTo = { x = 33930, y = 31046, z = 8 } }, + { register = { x = 33926, y = 31047, z = 8 }, teleportTo = { x = 33930, y = 31047, z = 8 } }, + { register = { x = 33926, y = 31048, z = 8 }, teleportTo = { x = 33930, y = 31048, z = 8 } }, + -- From boat to room + { register = { x = 33922, y = 31045, z = 8 }, teleportTo = { x = 33918, y = 31045, z = 8 } }, + { register = { x = 33922, y = 31046, z = 8 }, teleportTo = { x = 33918, y = 31046, z = 8 } }, + { register = { x = 33922, y = 31047, z = 8 }, teleportTo = { x = 33918, y = 31047, z = 8 } }, + { register = { x = 33922, y = 31048, z = 8 }, teleportTo = { x = 33918, y = 31048, z = 8 } }, + -- From room to boat + { register = { x = 33919, y = 31045, z = 8 }, teleportTo = { x = 33923, y = 31045, z = 8 } }, + { register = { x = 33919, y = 31046, z = 8 }, teleportTo = { x = 33923, y = 31046, z = 8 } }, + { register = { x = 33919, y = 31047, z = 8 }, teleportTo = { x = 33923, y = 31047, z = 8 } }, + { register = { x = 33919, y = 31048, z = 8 }, teleportTo = { x = 33923, y = 31048, z = 8 } }, + + -- Boat + -- Enter on boat + { register = { x = 33896, y = 31055, z = 8 }, teleportTo = { x = 33896, y = 31059, z = 8 } }, + { register = { x = 33897, y = 31055, z = 8 }, teleportTo = { x = 33897, y = 31059, z = 8 } }, + { register = { x = 33898, y = 31055, z = 8 }, teleportTo = { x = 33898, y = 31059, z = 8 } }, + { register = { x = 33899, y = 31055, z = 8 }, teleportTo = { x = 33899, y = 31059, z = 8 } }, + { register = { x = 33900, y = 31055, z = 8 }, teleportTo = { x = 33900, y = 31059, z = 8 } }, + { register = { x = 33901, y = 31055, z = 8 }, teleportTo = { x = 33901, y = 31059, z = 8 } }, + -- Back to room + { register = { x = 33896, y = 31058, z = 8 }, teleportTo = { x = 33896, y = 31054, z = 8 } }, + { register = { x = 33897, y = 31058, z = 8 }, teleportTo = { x = 33897, y = 31054, z = 8 } }, + { register = { x = 33898, y = 31058, z = 8 }, teleportTo = { x = 33898, y = 31054, z = 8 } }, + { register = { x = 33899, y = 31058, z = 8 }, teleportTo = { x = 33899, y = 31054, z = 8 } }, + { register = { x = 33900, y = 31058, z = 8 }, teleportTo = { x = 33900, y = 31054, z = 8 } }, + { register = { x = 33901, y = 31058, z = 8 }, teleportTo = { x = 33901, y = 31054, z = 8 } }, + -- From boat to room + { register = { x = 33896, y = 31061, z = 8 }, teleportTo = { x = 33896, y = 31065, z = 8 } }, + { register = { x = 33897, y = 31061, z = 8 }, teleportTo = { x = 33897, y = 31065, z = 8 } }, + { register = { x = 33898, y = 31061, z = 8 }, teleportTo = { x = 33898, y = 31065, z = 8 } }, + { register = { x = 33899, y = 31061, z = 8 }, teleportTo = { x = 33899, y = 31065, z = 8 } }, + { register = { x = 33900, y = 31061, z = 8 }, teleportTo = { x = 33900, y = 31065, z = 8 } }, + { register = { x = 33901, y = 31061, z = 8 }, teleportTo = { x = 33901, y = 31065, z = 8 } }, + -- From room to boat + { register = { x = 33896, y = 31064, z = 8 }, teleportTo = { x = 33896, y = 31060, z = 8 } }, + { register = { x = 33897, y = 31064, z = 8 }, teleportTo = { x = 33897, y = 31060, z = 8 } }, + { register = { x = 33898, y = 31064, z = 8 }, teleportTo = { x = 33898, y = 31060, z = 8 } }, + { register = { x = 33899, y = 31064, z = 8 }, teleportTo = { x = 33899, y = 31060, z = 8 } }, + { register = { x = 33900, y = 31064, z = 8 }, teleportTo = { x = 33900, y = 31060, z = 8 } }, + { register = { x = 33901, y = 31064, z = 8 }, teleportTo = { x = 33901, y = 31060, z = 8 } }, + + -- Boat + -- Enter on boat + { register = { x = 33899, y = 31099, z = 8 }, teleportTo = { x = 33899, y = 31103, z = 8 } }, + { register = { x = 33900, y = 31099, z = 8 }, teleportTo = { x = 33900, y = 31103, z = 8 } }, + { register = { x = 33901, y = 31099, z = 8 }, teleportTo = { x = 33901, y = 31103, z = 8 } }, + { register = { x = 33902, y = 31099, z = 8 }, teleportTo = { x = 33902, y = 31103, z = 8 } }, + { register = { x = 33903, y = 31099, z = 8 }, teleportTo = { x = 33903, y = 31103, z = 8 } }, + { register = { x = 33904, y = 31099, z = 8 }, teleportTo = { x = 33904, y = 31103, z = 8 } }, + { register = { x = 33905, y = 31099, z = 8 }, teleportTo = { x = 33905, y = 31103, z = 8 } }, + -- Back from boat to room + { register = { x = 33899, y = 31102, z = 8 }, teleportTo = { x = 33899, y = 31098, z = 8 } }, + { register = { x = 33900, y = 31102, z = 8 }, teleportTo = { x = 33900, y = 31098, z = 8 } }, + { register = { x = 33901, y = 31102, z = 8 }, teleportTo = { x = 33901, y = 31098, z = 8 } }, + { register = { x = 33902, y = 31102, z = 8 }, teleportTo = { x = 33902, y = 31098, z = 8 } }, + { register = { x = 33903, y = 31102, z = 8 }, teleportTo = { x = 33903, y = 31098, z = 8 } }, + { register = { x = 33904, y = 31102, z = 8 }, teleportTo = { x = 33904, y = 31098, z = 8 } }, + { register = { x = 33905, y = 31102, z = 8 }, teleportTo = { x = 33905, y = 31098, z = 8 } }, + -- From boat to room + { register = { x = 33899, y = 31105, z = 8 }, teleportTo = { x = 33899, y = 31109, z = 8 } }, + { register = { x = 33900, y = 31105, z = 8 }, teleportTo = { x = 33900, y = 31109, z = 8 } }, + { register = { x = 33901, y = 31105, z = 8 }, teleportTo = { x = 33901, y = 31109, z = 8 } }, + { register = { x = 33902, y = 31105, z = 8 }, teleportTo = { x = 33902, y = 31109, z = 8 } }, + { register = { x = 33903, y = 31105, z = 8 }, teleportTo = { x = 33903, y = 31109, z = 8 } }, + { register = { x = 33904, y = 31105, z = 8 }, teleportTo = { x = 33904, y = 31109, z = 8 } }, + { register = { x = 33905, y = 31105, z = 8 }, teleportTo = { x = 33905, y = 31109, z = 8 } }, + -- From room to boat + { register = { x = 33899, y = 31108, z = 8 }, teleportTo = { x = 33899, y = 31104, z = 8 } }, + { register = { x = 33900, y = 31108, z = 8 }, teleportTo = { x = 33900, y = 31104, z = 8 } }, + { register = { x = 33901, y = 31108, z = 8 }, teleportTo = { x = 33901, y = 31104, z = 8 } }, + { register = { x = 33902, y = 31108, z = 8 }, teleportTo = { x = 33902, y = 31104, z = 8 } }, + { register = { x = 33903, y = 31108, z = 8 }, teleportTo = { x = 33903, y = 31104, z = 8 } }, + { register = { x = 33904, y = 31108, z = 8 }, teleportTo = { x = 33904, y = 31104, z = 8 } }, + { register = { x = 33905, y = 31108, z = 8 }, teleportTo = { x = 33905, y = 31104, z = 8 } }, + + -- Boat + -- Enter on boat + { register = { x = 33913, y = 31112, z = 8 }, teleportTo = { x = 33917, y = 31112, z = 8 } }, + { register = { x = 33913, y = 31113, z = 8 }, teleportTo = { x = 33917, y = 31113, z = 8 } }, + { register = { x = 33913, y = 31114, z = 8 }, teleportTo = { x = 33917, y = 31114, z = 8 } }, + { register = { x = 33913, y = 31115, z = 8 }, teleportTo = { x = 33917, y = 31115, z = 8 } }, + { register = { x = 33913, y = 31116, z = 8 }, teleportTo = { x = 33917, y = 31116, z = 8 } }, + -- Back to room + { register = { x = 33916, y = 31112, z = 8 }, teleportTo = { x = 33912, y = 31112, z = 8 } }, + { register = { x = 33916, y = 31113, z = 8 }, teleportTo = { x = 33912, y = 31113, z = 8 } }, + { register = { x = 33916, y = 31114, z = 8 }, teleportTo = { x = 33912, y = 31114, z = 8 } }, + { register = { x = 33916, y = 31115, z = 8 }, teleportTo = { x = 33912, y = 31115, z = 8 } }, + { register = { x = 33916, y = 31116, z = 8 }, teleportTo = { x = 33912, y = 31116, z = 8 } }, + -- From boat to room + { register = { x = 33918, y = 31112, z = 8 }, teleportTo = { x = 33922, y = 31112, z = 8 } }, + { register = { x = 33918, y = 31113, z = 8 }, teleportTo = { x = 33922, y = 31113, z = 8 } }, + { register = { x = 33918, y = 31114, z = 8 }, teleportTo = { x = 33922, y = 31114, z = 8 } }, + { register = { x = 33918, y = 31115, z = 8 }, teleportTo = { x = 33922, y = 31115, z = 8 } }, + { register = { x = 33918, y = 31116, z = 8 }, teleportTo = { x = 33922, y = 31116, z = 8 } }, + -- From room to boat + { register = { x = 33921, y = 31112, z = 8 }, teleportTo = { x = 33917, y = 31112, z = 8 } }, + { register = { x = 33921, y = 31113, z = 8 }, teleportTo = { x = 33917, y = 31113, z = 8 } }, + { register = { x = 33921, y = 31114, z = 8 }, teleportTo = { x = 33917, y = 31114, z = 8 } }, + { register = { x = 33921, y = 31115, z = 8 }, teleportTo = { x = 33917, y = 31115, z = 8 } }, + { register = { x = 33921, y = 31116, z = 8 }, teleportTo = { x = 33917, y = 31116, z = 8 } }, + + -- Boat + -- Enter on boat + { register = { x = 33936, y = 31087, z = 8 }, teleportTo = { x = 33936, y = 31091, z = 8 } }, + { register = { x = 33937, y = 31087, z = 8 }, teleportTo = { x = 33937, y = 31091, z = 8 } }, + { register = { x = 33938, y = 31087, z = 8 }, teleportTo = { x = 33938, y = 31091, z = 8 } }, + { register = { x = 33939, y = 31087, z = 8 }, teleportTo = { x = 33939, y = 31091, z = 8 } }, + { register = { x = 33940, y = 31087, z = 8 }, teleportTo = { x = 33940, y = 31091, z = 8 } }, + { register = { x = 33941, y = 31087, z = 8 }, teleportTo = { x = 33941, y = 31091, z = 8 } }, + -- Back to room + { register = { x = 33936, y = 31090, z = 8 }, teleportTo = { x = 33936, y = 31086, z = 8 } }, + { register = { x = 33937, y = 31090, z = 8 }, teleportTo = { x = 33937, y = 31086, z = 8 } }, + { register = { x = 33938, y = 31090, z = 8 }, teleportTo = { x = 33938, y = 31086, z = 8 } }, + { register = { x = 33939, y = 31090, z = 8 }, teleportTo = { x = 33939, y = 31086, z = 8 } }, + { register = { x = 33940, y = 31090, z = 8 }, teleportTo = { x = 33940, y = 31086, z = 8 } }, + { register = { x = 33941, y = 31090, z = 8 }, teleportTo = { x = 33941, y = 31086, z = 8 } }, + -- From boat to room + { register = { x = 33936, y = 31095, z = 8 }, teleportTo = { x = 33934, y = 31099, z = 8 } }, + { register = { x = 33937, y = 31095, z = 8 }, teleportTo = { x = 33935, y = 31099, z = 8 } }, + { register = { x = 33938, y = 31095, z = 8 }, teleportTo = { x = 33936, y = 31099, z = 8 } }, + { register = { x = 33939, y = 31095, z = 8 }, teleportTo = { x = 33937, y = 31099, z = 8 } }, + { register = { x = 33940, y = 31095, z = 8 }, teleportTo = { x = 33938, y = 31099, z = 8 } }, + { register = { x = 33941, y = 31095, z = 8 }, teleportTo = { x = 33939, y = 31099, z = 8 } }, + -- From room to boat + { register = { x = 33934, y = 31098, z = 8 }, teleportTo = { x = 33936, y = 31094, z = 8 } }, + { register = { x = 33935, y = 31098, z = 8 }, teleportTo = { x = 33937, y = 31094, z = 8 } }, + { register = { x = 33936, y = 31098, z = 8 }, teleportTo = { x = 33938, y = 31094, z = 8 } }, + { register = { x = 33937, y = 31098, z = 8 }, teleportTo = { x = 33939, y = 31094, z = 8 } }, + { register = { x = 33938, y = 31098, z = 8 }, teleportTo = { x = 33940, y = 31094, z = 8 } }, + { register = { x = 33939, y = 31098, z = 8 }, teleportTo = { x = 33941, y = 31094, z = 8 } }, + { register = { x = 33940, y = 31098, z = 8 }, teleportTo = { x = 33942, y = 31094, z = 8 } }, + + -- Boat + -- Enter on boat + { register = { x = 33939, y = 31064, z = 8 }, teleportTo = { x = 33939, y = 31060, z = 8 } }, + { register = { x = 33940, y = 31064, z = 8 }, teleportTo = { x = 33940, y = 31060, z = 8 } }, + { register = { x = 33941, y = 31064, z = 8 }, teleportTo = { x = 33941, y = 31060, z = 8 } }, + { register = { x = 33942, y = 31064, z = 8 }, teleportTo = { x = 33942, y = 31060, z = 8 } }, + { register = { x = 33943, y = 31064, z = 8 }, teleportTo = { x = 33943, y = 31060, z = 8 } }, + { register = { x = 33944, y = 31064, z = 8 }, teleportTo = { x = 33944, y = 31060, z = 8 } }, + -- Back to room + { register = { x = 33939, y = 31061, z = 8 }, teleportTo = { x = 33939, y = 31065, z = 8 } }, + { register = { x = 33940, y = 31061, z = 8 }, teleportTo = { x = 33940, y = 31065, z = 8 } }, + { register = { x = 33941, y = 31061, z = 8 }, teleportTo = { x = 33941, y = 31065, z = 8 } }, + { register = { x = 33942, y = 31061, z = 8 }, teleportTo = { x = 33942, y = 31065, z = 8 } }, + { register = { x = 33943, y = 31061, z = 8 }, teleportTo = { x = 33943, y = 31065, z = 8 } }, + { register = { x = 33944, y = 31061, z = 8 }, teleportTo = { x = 33944, y = 31065, z = 8 } }, + -- From boat to room + { register = { x = 33939, y = 31058, z = 8 }, teleportTo = { x = 33939, y = 31054, z = 8 } }, + { register = { x = 33940, y = 31058, z = 8 }, teleportTo = { x = 33940, y = 31054, z = 8 } }, + { register = { x = 33941, y = 31058, z = 8 }, teleportTo = { x = 33941, y = 31054, z = 8 } }, + { register = { x = 33942, y = 31058, z = 8 }, teleportTo = { x = 33942, y = 31054, z = 8 } }, + { register = { x = 33943, y = 31058, z = 8 }, teleportTo = { x = 33943, y = 31054, z = 8 } }, + { register = { x = 33944, y = 31058, z = 8 }, teleportTo = { x = 33944, y = 31054, z = 8 } }, + -- From room to boat + { register = { x = 33939, y = 31055, z = 8 }, teleportTo = { x = 33939, y = 31059, z = 8 } }, + { register = { x = 33940, y = 31055, z = 8 }, teleportTo = { x = 33940, y = 31059, z = 8 } }, + { register = { x = 33941, y = 31055, z = 8 }, teleportTo = { x = 33941, y = 31059, z = 8 } }, + { register = { x = 33942, y = 31055, z = 8 }, teleportTo = { x = 33942, y = 31059, z = 8 } }, + { register = { x = 33943, y = 31055, z = 8 }, teleportTo = { x = 33943, y = 31059, z = 8 } }, + { register = { x = 33944, y = 31055, z = 8 }, teleportTo = { x = 33944, y = 31059, z = 8 } }, + + -- Boat + -- Enter on boat + { register = { x = 33934, y = 31108, z = 8 }, teleportTo = { x = 33938, y = 31108, z = 8 } }, + { register = { x = 33934, y = 31109, z = 8 }, teleportTo = { x = 33938, y = 31109, z = 8 } }, + { register = { x = 33934, y = 31110, z = 8 }, teleportTo = { x = 33938, y = 31110, z = 8 } }, + { register = { x = 33934, y = 31111, z = 8 }, teleportTo = { x = 33938, y = 31111, z = 8 } }, + { register = { x = 33934, y = 31112, z = 8 }, teleportTo = { x = 33938, y = 31112, z = 8 } }, + -- Back to room + { register = { x = 33937, y = 31108, z = 8 }, teleportTo = { x = 33933, y = 31108, z = 8 } }, + { register = { x = 33937, y = 31109, z = 8 }, teleportTo = { x = 33933, y = 31109, z = 8 } }, + { register = { x = 33937, y = 31110, z = 8 }, teleportTo = { x = 33933, y = 31110, z = 8 } }, + { register = { x = 33937, y = 31111, z = 8 }, teleportTo = { x = 33933, y = 31111, z = 8 } }, + { register = { x = 33937, y = 31112, z = 8 }, teleportTo = { x = 33933, y = 31112, z = 8 } }, + -- From boat to room + { register = { x = 33942, y = 31108, z = 8 }, teleportTo = { x = 33946, y = 31108, z = 8 } }, + { register = { x = 33942, y = 31109, z = 8 }, teleportTo = { x = 33946, y = 31109, z = 8 } }, + { register = { x = 33942, y = 31110, z = 8 }, teleportTo = { x = 33946, y = 31110, z = 8 } }, + { register = { x = 33942, y = 31111, z = 8 }, teleportTo = { x = 33946, y = 31111, z = 8 } }, + { register = { x = 33942, y = 31112, z = 8 }, teleportTo = { x = 33946, y = 31112, z = 8 } }, + -- From room to boat + { register = { x = 33945, y = 31108, z = 8 }, teleportTo = { x = 33941, y = 31108, z = 8 } }, + { register = { x = 33945, y = 31109, z = 8 }, teleportTo = { x = 33941, y = 31109, z = 8 } }, + { register = { x = 33945, y = 31110, z = 8 }, teleportTo = { x = 33941, y = 31110, z = 8 } }, + { register = { x = 33945, y = 31111, z = 8 }, teleportTo = { x = 33941, y = 31111, z = 8 } }, + { register = { x = 33945, y = 31112, z = 8 }, teleportTo = { x = 33941, y = 31112, z = 8 } }, + }, +} + +function RegisterSoulWarBossesLevers() + -- Register levers + local goshnarsMaliceLever = BossLever(SoulWarQuest.levers.goshnarsMalice) + goshnarsMaliceLever:position(SoulWarQuest.levers.goshnarsMalicePosition) + goshnarsMaliceLever:register() + logger.debug("Registering soul war boss lever zone: {}", goshnarsMaliceLever:getZone():getName()) + + local goshnarsSpiteLever = BossLever(SoulWarQuest.levers.goshnarsSpite) + goshnarsSpiteLever:position(SoulWarQuest.levers.goshnarsSpitePosition) + goshnarsSpiteLever:register() + logger.debug("Registering soul war boss lever zone: {}", goshnarsSpiteLever:getZone():getName()) + + local goshnarsGreedLever = BossLever(SoulWarQuest.levers.goshnarsGreed) + goshnarsGreedLever:position(SoulWarQuest.levers.goshnarsGreedPosition) + goshnarsGreedLever:register() + logger.debug("Registering soul war boss lever zone: {}", goshnarsGreedLever:getZone():getName()) + + local goshnarsHatredLever = BossLever(SoulWarQuest.levers.goshnarsHatred) + goshnarsHatredLever:position(SoulWarQuest.levers.goshnarsHatredPosition) + goshnarsHatredLever:register() + logger.debug("Registering soul war boss lever zone: {}", goshnarsHatredLever:getZone():getName()) + + local goshnarsCrueltyLever = BossLever(SoulWarQuest.levers.goshnarsCruelty) + goshnarsCrueltyLever:position(SoulWarQuest.levers.goshnarsCrueltyPosition) + goshnarsCrueltyLever:register() + logger.debug("Registering soul war boss lever zone: {}", goshnarsCrueltyLever:getZone():getName()) + + local goshnarsMegalomaniaLever = BossLever(SoulWarQuest.levers.goshnarsMegalomania) + goshnarsMegalomaniaLever:position(SoulWarQuest.levers.goshnarsMegalomaniaPosition) + goshnarsMegalomaniaLever:register() + logger.debug("Registering soul war boss lever zone: {}", goshnarsMegalomaniaLever:getZone():getName()) +end + +-- Initialize ebb and flow zone area +SoulWarQuest.ebbAndFlow.zone:addArea({ x = 33869, y = 30991, z = 8 }, { x = 33964, y = 31147, z = 9 }) + +-- Initialize claustrophobic inferno raid zones and add remove destination + +for _, raid in ipairs(SoulWarQuest.claustrophobicInfernoRaids) do + local zone = raid.getZone() + zone:addArea(raid.zoneArea[1], raid.zoneArea[2]) + zone:setRemoveDestination(raid.exitPosition) +end + +-- Initialize bosses access for taint check +SoulWarQuest.areaZones.claustrophobicInferno:addArea({ x = 33982, y = 30981, z = 9 }, { x = 34051, y = 31110, z = 11 }) + +SoulWarQuest.areaZones.ebbAndFlow:addArea({ x = 33873, y = 30994, z = 8 }, { x = 33968, y = 31150, z = 9 }) + +SoulWarQuest.areaZones.furiousCrater:addArea({ x = 33814, y = 31819, z = 3 }, { x = 33907, y = 31920, z = 7 }) + +SoulWarQuest.areaZones.rottenWasteland:addArea({ x = 33980, y = 30986, z = 11 }, { x = 33901, y = 31105, z = 12 }) + +SoulWarQuest.areaZones.mirroredNightmare:addArea({ x = 33877, y = 31164, z = 9 }, { x = 33991, y = 31241, z = 13 }) + +-- Initialize safe areas (should not spawn monster, teleport, take damage from taint, etc) +SoulWarQuest.areaZones.claustrophobicInferno:subtractArea({ x = 34002, y = 31008, z = 9 }, { x = 34019, y = 31019, z = 9 }) + +SoulWarQuest.areaZones.ebbAndFlow:subtractArea({ x = 33887, y = 31015, z = 8 }, { x = 33920, y = 31024, z = 8 }) + +SoulWarQuest.areaZones.furiousCrater:subtractArea({ x = 33854, y = 31828, z = 3 }, { x = 33869, y = 31834, z = 3 }) + +SoulWarQuest.areaZones.rottenWasteland:subtractArea({ x = 33967, y = 31037, z = 11 }, { x = 33977, y = 31051, z = 11 }) + +SoulWarQuest.areaZones.mirroredNightmare:subtractArea({ x = 33884, y = 31181, z = 10 }, { x = 33892, y = 31198, z = 10 }) + +SoulCagePosition = Position(33709, 31596, 14) +TaintDurationSeconds = 14 * 24 * 60 * 60 -- 14 days +GreedbeastKills = 0 + +SoulWarReflectDamageMap = { + [COMBAT_PHYSICALDAMAGE] = 10, + [COMBAT_FIREDAMAGE] = 10, + [COMBAT_EARTHDAMAGE] = 10, + [COMBAT_ENERGYDAMAGE] = 10, + [COMBAT_ICEDAMAGE] = 10, + [COMBAT_HOLYDAMAGE] = 10, + [COMBAT_DEATHDAMAGE] = 10, +} + +local soulWarTaints = { + "taints-teleport", -- Taint 1 + "taints-spawn", -- Taint 2 + "taints-damage", -- Taint 3 + "taints-heal", -- Taint 4 + "taints-loss", -- Taint 5 +} + +GreedMonsters = { + ["Greedbeast"] = Position(33744, 31666, 14), + ["Soulsnatcher"] = Position(33747, 31668, 14), + ["Weak Soul"] = Position(33750, 31666, 14), + ["Strong Soul"] = Position(33750, 31666, 14), + ["Powerful Soul"] = Position(33750, 31666, 14), +} + +function CreateGoshnarsGreedMonster(name, position) + local function sendEffect() + position:sendMagicEffect(CONST_ME_TELEPORT) + end + + local function spawnMonster() + Game.createMonster(name, position, true, false) + logger.trace("Spawning {} in position {}", name, position:toString()) + end + + for i = 7, 9 do + addEvent(sendEffect, i * 1000) + end + + addEvent(spawnMonster, 10000) +end + +function RemoveSoulCageAndBuffMalice() + local soulCage = Creature("Soul Cage") + if soulCage then + soulCage:remove() + addEvent(SpawnSoulCage, 23000) + local malice = Creature("Goshnar's Malice") + if malice then + logger.trace("Found malice, try adding reflect and defense") + for elementType, reflectPercent in pairs(SoulWarReflectDamageMap) do + malice:addReflectElement(elementType, reflectPercent) + end + malice:addDefense(10) + end + end +end + +function SpawnSoulCage() + local tile = Tile(SoulCagePosition) + local creatures = tile:getCreatures() or {} + local soulCage = Creature("Soul Cage") + if not soulCage then + Game.createMonster("Soul Cage", SoulCagePosition, true, true) + logger.trace("Spawning Soul Cage in position {}", SoulCagePosition:toString()) + addEvent(RemoveSoulCageAndBuffMalice, 40000) + end +end + +local function shuffle(list) + for i = #list, 2, -1 do + local j = math.random(i) + list[i], list[j] = list[j], list[i] + end +end + +local function createConnectedGroup(startPos, groupPositions, groupSize) + local group = { startPos } + local lastPos = startPos + local directions = { + { x = 1, y = 0 }, + { x = -1, y = 0 }, -- Right and left + { x = 0, y = 1 }, + { x = 0, y = -1 }, -- Up and down + { x = 1, y = 1 }, + { x = -1, y = -1 }, -- Diagonals + { x = -1, y = 1 }, + { x = 1, y = -1 }, + } + + for i = 2, groupSize do + shuffle(directions) + local nextPos = nil + for _, dir in ipairs(directions) do + local potentialNextPos = Position(lastPos.x + dir.x, lastPos.y + dir.y, lastPos.z) + if table.contains(groupPositions, potentialNextPos) then + nextPos = potentialNextPos + break + end + end + + if nextPos then + table.insert(group, nextPos) + table.remove(groupPositions, table.find(groupPositions, nextPos)) + lastPos = nextPos + else + break + end + end + + return group +end + +local function generatePositionsInRange(center, range) + local positions = {} + for x = center.x - range, center.x + range do + for y = center.y - range, center.y + range do + table.insert(positions, Position(x, y, center.z)) + end + end + return positions +end + +local toRevertPositions = {} + +local tileItemIds = { + 32906, + 33066, + 33067, + 33068, + 33069, + 33070, +} + +local function revertTilesAndApplyDamage(zonePositions) + for _, pos in ipairs(zonePositions) do + local tile = Tile(pos) + if tile and tile:getGround() then + if tile:getGround():getId() ~= 409 then + local creature = tile:getTopCreature() + if creature then + local player = creature:getPlayer() + if player then + player:addHealth(-8000, COMBAT_DEATHDAMAGE) + end + end + end + + local itemFound = false + for i = 1, #tileItemIds do + local item = tile:getItemById(tileItemIds[i]) + if item then + itemFound = true + break + end + end + + if tile:getGround():getId() == 410 and not itemFound and not tile:getItemByTopOrder(1) and not tile:getItemByTopOrder(3) then + pos:sendMagicEffect(CONST_ME_REDSMOKE) + end + end + end + + for posString, itemId in pairs(toRevertPositions) do + local pos = posString:toPosition() + local tile = Tile(pos) + if tile and tile:getGround() and tile:getGround():getId() == 409 then + tile:getGround():transform(itemId) + toRevertPositions[pos:toString()] = nil + end + end +end + +function Monster:createSoulWarWhiteTiles(centerRoomPosition, zonePositions, executeInterval) + local groupPositions = generatePositionsInRange(centerRoomPosition, 7) + local totalTiles = 11 + local groupSize = 3 + local groupsCreated = 0 + + -- Run only for megalomania boss + if executeInterval then + -- Remove remains + for _, pos in ipairs(zonePositions) do + local tile = Tile(pos) + if tile and tile:getGround() then + local remains = tile:getItemById(33984) + if remains then + remains:remove() + end + end + end + end + + while #groupPositions > 0 and groupsCreated * groupSize < totalTiles do + local randomIndex = math.random(#groupPositions) + local startPos = groupPositions[randomIndex] + table.remove(groupPositions, randomIndex) + + local group = createConnectedGroup(startPos, groupPositions, groupSize) + for _, pos in ipairs(group) do + local tile = Tile(pos) + if tile then + toRevertPositions[pos:toString()] = tile:getGround():getId() + tile:getGround():transform(409) + end + end + + groupsCreated = groupsCreated + 1 + end + + addEvent(revertTilesAndApplyDamage, executeInterval or 3000, zonePositions) +end + +function MonsterType:calculateBagYouDesireChance(player, itemChance) + local playerTaintLevel = player:getTaintLevel() + if not playerTaintLevel or playerTaintLevel == 0 then + return itemChance + end + + local monsterName = self:getName() + local isMonsterValid = table.contains(SoulWarQuest.bagYouDesireMonsters, monsterName) + if not isMonsterValid then + return itemChance + end + + local soulWarQuest = player:soulWarQuestKV() + local megalomaniaKills = soulWarQuest:scoped("megalomania-kills"):get("count") or 0 + + if monsterName == "Goshnar's Megalomania" then + -- Special handling for Goshnar's Megalomania + itemChance = itemChance + megalomaniaKills * SoulWarQuest.bagYouDesireChancePerTaint + else + -- General handling for other monsters (bosses and non-bosses) + itemChance = itemChance + (playerTaintLevel * SoulWarQuest.bagYouDesireChancePerTaint) + end + + logger.info("Player {} killed {} with {} taints, loot chance {}", player:getName(), monsterName, playerTaintLevel, itemChance) + + if math.random(1, 100000) <= totalChance then + logger.debug("Player {} killed {} and got a bag you desire with drop chance {}", player:getName(), monsterName, itemChance) + if monsterName == "Goshnar's Megalomania" then + -- Reset kill count on successful drop + soulWarQuest:scoped("megalomania-kills"):set("count", 0) + end + else + if monsterName == "Goshnar's Megalomania" then + -- Increment kill count for unsuccessful attempts + soulWarQuest:scoped("megalomania-kills"):set("count", megalomaniaKills + 1) + end + end + + return itemChance +end + +local intervalBetweenExecutions = 10000 + +local accumulatedTime = 0 +local desiredInterval = 40000 +local bossSayInterval = 38000 + +function Monster:onThinkMegalomaniaWhiteTiles(interval, zonePositions, revertTime) + self:onThinkGoshnarTormentCounter(interval, 36, intervalBetweenExecutions, SoulWarQuest.levers.goshnarsMegalomania.boss.position) + + accumulatedTime = accumulatedTime + interval + + if accumulatedTime == bossSayInterval then + self:say("FEEL THE POWER OF MY WRATH!!") + end + -- Execute only after 40 seconds + if accumulatedTime >= desiredInterval then + self:createSoulWarWhiteTiles(SoulWarQuest.levers.goshnarsMegalomania.boss.position, zonePositions, revertTime) + accumulatedTime = 0 + end +end + +TaintTeleportCooldown = {} + +function Player:getTaintNameByNumber(taintNumber, skipKvCheck) + local haveTaintName = nil + local soulWarQuest = self:soulWarQuestKV() + local taintName = soulWarTaints[taintNumber] + if skipKvCheck or taintName and soulWarQuest:get(taintName) then + haveTaintName = taintName + end + + return haveTaintName +end + +function Player:addNextTaint() + local soulWarQuest = self:soulWarQuestKV() + for _, taintName in ipairs(soulWarTaints) do + if not soulWarQuest:get(taintName) then + soulWarQuest:set(taintName, true) + self:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have gained the " .. taintName .. ".") + self:setTaintIcon() + break + end + end +end + +function Player:setTaintIcon(taintId) + self:resetTaintConditions() + local condition = Condition(CONDITION_GOSHNARTAINT, CONDITIONID_DEFAULT, taintId or self:getTaintLevel()) + condition:setTicks(14 * 24 * 60 * 60 * 1000) + self:addCondition(condition) +end + +function Player:resetTaintConditions() + for i = 1, 5 do + self:removeCondition(CONDITION_GOSHNARTAINT, CONDITIONID_DEFAULT, i) + end +end + +function Player:getTaintLevel() + local taintLevel = nil + local soulWarQuest = self:soulWarQuestKV() + for i, taint in ipairs(soulWarTaints) do + if soulWarQuest:get(taint) then + taintLevel = i + end + end + + return taintLevel +end + +function Player:resetTaints(skipCheckTime) + local soulWarQuest = self:soulWarQuestKV() + local firstTaintTime = soulWarQuest:get("firstTaintTime") + if skipCheckTime or firstTaintTime and os.time() >= (firstTaintTime + TaintDurationSeconds) then + -- Reset all taints and remove condition + for _, taintName in ipairs(soulWarTaints) do + if soulWarQuest:get(taintName) then + soulWarQuest:remove(taintName) + end + end + self:resetTaintConditions() + soulWarQuest:remove("firstTaintTime") + local resetMessage = "Your Goshnar's taints have been reset." + if not skipCheckTime then + resetMessage = resetMessage .. " You didn't finish the quest in 14 days." + end + self:sendTextMessage(MESSAGE_EVENT_ADVANCE, resetMessage) + + for bossName, _ in pairs(SoulWarQuest.miniBosses) do + soulWarQuest:remove(bossName) + end + end +end + +function Monster:tryTeleportToPlayer(sayMessage) + local range = 30 + local spectators = Game.getSpectators(self:getPosition(), false, false, range, range, range, range) + local maxDistance = 0 + local farthestPlayer = nil + for i, spectator in ipairs(spectators) do + if spectator:isPlayer() then + local player = spectator:getPlayer() + if player:getTaintNameByNumber(1, true) and player:getSoulWarZoneMonster() ~= nil then + local distance = self:getPosition():getDistance(player:getPosition()) + if distance > maxDistance then + maxDistance = distance + farthestPlayer = player + logger.trace("Found player {} to teleport", player:getName()) + end + end + end + end + + if farthestPlayer and math.random(100) <= 10 then + local playerPosition = farthestPlayer:getPosition() + if TaintTeleportCooldown[farthestPlayer:getId()] then + logger.trace("Cooldown is active to player {}", farthestPlayer:getName()) + return + end + + if not TaintTeleportCooldown[farthestPlayer:getId()] then + TaintTeleportCooldown[farthestPlayer:getId()] = true + + logger.trace("Scheduling player {} to teleport", farthestPlayer:getName()) + self:getPosition():sendMagicEffect(CONST_ME_MORTAREA) + farthestPlayer:getPosition():sendMagicEffect(CONST_ME_MORTAREA) + addEvent(function(playerId, monsterId) + local monsterEvent = Monster(monsterId) + local playerEvent = Player(playerId) + if monsterEvent and playerEvent then + local destinationTile = Tile(playerPosition) + if destinationTile and not (destinationTile:hasProperty(CONST_PROP_BLOCKPROJECTILE) or destinationTile:hasProperty(CONST_PROP_MOVEABLE)) then + monsterEvent:say(sayMessage) + monsterEvent:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + monsterEvent:teleportTo(playerPosition, true) + monsterEvent:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + end + end + end, 2000, farthestPlayer:getId(), self:getId()) + + addEvent(function(playerId) + local playerEvent = Player(playerId) + if not playerEvent then + return + end + + logger.trace("Cleaning player cooldown") + TaintTeleportCooldown[playerEvent:getId()] = nil + end, 10000, farthestPlayer:getId()) + end + end +end + +function Monster:getSoulWarKV() + return SoulWarQuest.kvSoulWar:scoped("monster"):scoped(self:getName()) +end + +function Monster:getHatredDamageMultiplier() + return self:getSoulWarKV():get("burning-hatred-empowered") or 0 +end + +function Monster:increaseHatredDamageMultiplier(multiplierCount) + local attackMultiplier = self:getHatredDamageMultiplier() + self:getSoulWarKV():set("burning-hatred-empowered", attackMultiplier + multiplierCount) +end + +function Monster:resetHatredDamageMultiplier() + self:getSoulWarKV():remove("burning-hatred-empowered") +end + +function Position:increaseNecromaticMegalomaniaStrength() + local tile = Tile(self) + if tile then + local item = tile:getItemById(SoulWarQuest.necromanticRemainsId) + if item then + local boss = Creature("Goshnar's Megalomania") + if boss then + boss:increaseHatredDamageMultiplier(5) + item:remove() + logger.trace("Necromantic remains strength increased") + end + end + end +end + +local lastExecutionTime = 0 + +-- Damage 24 to 36 have a special damage +local damageTable = { + 1400, + 1600, + 1800, + 2200, + 2400, + 2600, + 3000, + 3400, + 3800, + 4200, + 4800, + 5200, + 5600, +} + +function Monster:onThinkGoshnarTormentCounter(interval, maxLimit, intervalBetweenExecutions, bossPosition) + local interval = os.time() * 1000 + if interval - lastExecutionTime < intervalBetweenExecutions then + return + end + + lastExecutionTime = interval + logger.trace("Icon time count {}", interval) + local spectators = Game.getSpectators(bossPosition, false, true, 15, 15, 15, 15) + for i = 1, #spectators do + local player = spectators[i] + local tormentCounter = player:getGoshnarSymbolTormentCounter() + local goshnarsHatred = Creature(bossName or "Goshnar's Megalomania") + if not goshnarsHatred then + player:resetGoshnarSymbolTormentCounter() + goto continue + end + + if tormentCounter <= maxLimit then + player:increaseGoshnarSymbolTormentCounter(maxLimit) + logger.trace("Player {} has {} damage counter", player:getName(), tormentCounter) + + if tormentCounter > 0 then + local damage = tormentCounter * 35 + if tormentCounter >= 24 then + damage = damageTable[tormentCounter - 23] + end + + logger.trace("Final damage {}", damage) + player:addHealth(-damage, COMBAT_DEATHDAMAGE) + player:getPosition():sendMagicEffect(CONST_ME_PINK_ENERGY_SPARK) + end + end + + if tormentCounter == 5 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The dread starts to torment you! Don't let dread level reach critical value!") + elseif tormentCounter == 15 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The dread's torment becomes unbearable!") + elseif tormentCounter == 24 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The Dread's torment begins to tear you apart!") + elseif tormentCounter == 30 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The dread's torment is killing you!") + elseif tormentCounter == 36 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The dread's torment is now lethal!") + end + + ::continue:: + end +end + +function Monster:increaseAspectOfPowerDeathCount() + local bossKV = self:getSoulWarKV() + local aspectDeathCount = bossKV:get("aspect-of-power-death-count") or 0 + local newCount = aspectDeathCount + 1 + logger.trace("Aspect of Power death count {}", newCount) + bossKV:set("aspect-of-power-death-count", newCount) + if newCount == 4 then + self:setType("Goshnar's Megalomania Green") + self:say("THE DEATH OF ASPECTS DIMINISHES GOSHNAR'S POWER AND HE TURNS VULNERABLE!") + bossKV:set("aspect-of-power-death-count", 0) + SoulWarQuest.changeBlueEvent = addEvent(SoulWarQuest.changeMegalomaniaBlue, 1 * 60 * 1000) + logger.trace("Aspect of Power defeated all and Megalomania is now vulnerable, reseting death count.") + SoulWarQuest.changePurpleEvent = addEvent(function() + local boss = Creature("Goshnar's Megalomania") + if boss and boss:getTypeName() == "Goshnar's Megalomania Green" then + boss:setType("Goshnar's Megalomania Purple") + boss:say("GOSHNAR REGAINED ENOUGH POWER TO TURN INVULNERABLE AGAIN!") + logger.trace("Megalomania is now immune again") + end + end, SoulWarQuest.timeToReturnImmuneMegalomania * 1000) + end +end + +function Monster:goshnarsDefenseIncrease(kvName) + local currentTime = os.time() + -- Gets the time when the "Greedy Maw" item was last used. + local lastItemUseTime = SoulWarQuest.kvSoulWar:get(kvName) or 0 + -- Checks if more than config time have passed since the item was last used. + if currentTime >= lastItemUseTime + SoulWarQuest.timeToIncreaseCrueltyDefense then + self:addDefense(SoulWarQuest.goshnarsCrueltyDefenseChange) + -- Register the drain callback to modify the damage for goshnar's cruelty + local newValue = SoulWarQuest.kvSoulWar:get("goshnars-cruelty-defense-drain") or SoulWarQuest.goshnarsCrueltyDefenseChange + SoulWarQuest.kvSoulWar:set("goshnars-cruelty-defense-drain", newValue + 1) -- Increment the value to track usage or modifications + + --- Updates the KV to reflect the timing of the increase to maintain control. + SoulWarQuest.kvSoulWar:set(kvName, currentTime) + else + -- If config time have not passed, logs the increase has been skipped. + logger.trace("{} skips increase cooldown due to recent item use.", self:getName()) + end +end + +function Monster:removeGoshnarsMegalomaniaMonsters(zone) + if self:getName() ~= "Goshnar's Megalomania" then + return + end + + if zone then + local creatures = zone:getCreatures() + for _, creature in ipairs(creatures) do + if creature:getMonster() then + creature:remove() + end + end + end +end + +function Player:getSoulWarZoneMonster() + local zoneMonsterName = nil + for zoneName, monsterName in pairs(SoulWarQuest.areaZones.monsters) do + local zone = Zone.getByName(zoneName) + if zone and zone:isInZone(self:getPosition()) then + zoneMonsterName = monsterName + break + end + end + + return zoneMonsterName +end + +function Player:isInBoatSpot() + -- Get ebb and flow zone and check if player is in zone + local zone = SoulWarQuest.ebbAndFlow.getZone() + local tile = Tile(self:getPosition()) + local groundId + if tile and tile:getGround() then + groundId = tile:getGround():getId() + end + if zone and zone:isInZone(self:getPosition()) and tile and groundId == SoulWarQuest.ebbAndFlow.boatId then + logger.trace("Player {} is in boat spot", self:getName()) + return true + end + + logger.trace("Player {} is not in boat spot", self:getName()) + return false +end + +function Player:soulWarQuestKV() + return self:kv():scoped("quest"):scoped("soul-war") +end + +function Player:getGoshnarSymbolTormentCounter() + local soulWarKV = self:soulWarQuestKV() + return soulWarKV:get("goshnars-hatred-torment-count") or 0 +end + +function Player:increaseGoshnarSymbolTormentCounter(maxLimit) + local soulWarKV = self:soulWarQuestKV() + local tormentCount = self:getGoshnarSymbolTormentCounter() + if tormentCount == maxLimit then + self:setIcon("goshnars-hatred-damage", CreatureIconCategory_Quests, CreatureIconQuests_RedCross, tormentCount) + return + end + + self:setIcon("goshnars-hatred-damage", CreatureIconCategory_Quests, CreatureIconQuests_RedCross, tormentCount + 1) + soulWarKV:set("goshnars-hatred-torment-count", tormentCount + 1) +end + +function Player:removeGoshnarSymbolTormentCounter(count) + local soulWarKV = self:soulWarQuestKV() + local tormentCount = self:getGoshnarSymbolTormentCounter() + if tormentCount > count then + self:setIcon("goshnars-hatred-damage", CreatureIconCategory_Quests, CreatureIconQuests_RedCross, tormentCount - count) + soulWarKV:set("goshnars-hatred-torment-count", tormentCount - count) + else + self:resetGoshnarSymbolTormentCounter() + end +end + +function Player:resetGoshnarSymbolTormentCounter() + local soulWarKV = self:soulWarQuestKV() + soulWarKV:remove("goshnars-hatred-torment-count") + self:removeIcon("goshnars-hatred-damage") +end + +function Player:furiousCraterKV() + return self:soulWarQuestKV():scoped("furius-crater") +end + +function Player:pulsatingEnergyKV() + return self:furiousCraterKV():scoped("pulsating-energy") +end + +function Zone:getRandomPlayer() + local players = self:getPlayers() + if #players == 0 then + return nil + end + + local randomIndex = math.random(#players) + return players[randomIndex] +end + +local conditionOutfit = Condition(CONDITION_OUTFIT) + +local function delayedCastSpell(cid, var, combat, targetId) + local creature = Creature(cid) + if not creature then + return + end + + local target = Player(targetId) + if target then + combat:execute(creature, positionToVariant(target:getPosition())) + target:removeCondition(conditionOutfit) + end +end + +function Creature:applyZoneEffect(var, combat, zoneName) + local outfitConfig = { + outfit = { lookType = 242, lookHead = 0, lookBody = 0, lookLegs = 0, lookFeet = 0, lookAddons = 0 }, + time = 7000, + } + + local zone = Zone.getByName(zoneName) + if not zone then + logger.error("Could not find zone '" .. zoneName .. "', you need use the 'BossLever' system") + return false + end + + local target = zone:getRandomPlayer() + if not target then + return true + end + + conditionOutfit:setTicks(outfitConfig.time) + conditionOutfit:setOutfit(outfitConfig.outfit) + target:addCondition(conditionOutfit) + target:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) + + addEvent(delayedCastSpell, SoulWarQuest.goshnarsCrueltyWaveInterval * 1000, self:getId(), var, combat, target:getId()) + + return true +end + +function string.toPosition(str) + local patterns = { + -- table format + "{%s*x%s*=%s*(%d+)%s*,%s*y%s*=%s*(%d+)%s*,%s*z%s*=%s*(%d+)%s*}", + -- Position format + "Position%s*%((%d+)%s*,%s*(%d+)%s*,%s*(%d+)%s*%)", + -- x, y, z format + "(%d+)%s*,%s*(%d+)%s*,%s*(%d+)", + } + + for _, pattern in ipairs(patterns) do + local x, y, z = string.match(str, pattern) + if x and y and z then + return Position(tonumber(x), tonumber(y), tonumber(z)) + end + end +end diff --git a/data-otservbr-global/monster/quests/soul_war/aspect_of_power.lua b/data-otservbr-global/monster/quests/soul_war/aspect_of_power.lua index 8ff012c96ef..896cb520722 100644 --- a/data-otservbr-global/monster/quests/soul_war/aspect_of_power.lua +++ b/data-otservbr-global/monster/quests/soul_war/aspect_of_power.lua @@ -4,20 +4,20 @@ local monster = {} monster.description = "an aspect of power" monster.experience = 0 monster.outfit = { - lookType = 1303, + lookType = 1306, lookHead = 0, lookBody = 0, lookLegs = 0, lookFeet = 0, - lookAddons = 1, + lookAddons = 0, lookMount = 0, } monster.health = 25000 monster.maxHealth = 25000 monster.race = "undead" -monster.corpse = 0 -monster.speed = 125 +monster.corpse = 33949 +monster.speed = 175 monster.manaCost = 0 monster.changeTarget = { @@ -25,8 +25,14 @@ monster.changeTarget = { chance = 8, } +monster.events = { + "SoulWarAspectOfPowerDeath", +} + monster.strategiesTarget = { nearest = 100, + health = 20, + damage = 30, } monster.flags = { @@ -64,16 +70,15 @@ monster.attacks = { { name = "combat", interval = 1700, chance = 15, type = COMBAT_EARTHDAMAGE, minDamage = -400, maxDamage = -950, radius = 3, shootEffect = CONST_ANI_ENVENOMEDARROW, effect = CONST_ME_HITBYPOISON, target = true }, { name = "combat", interval = 1700, chance = 25, type = COMBAT_ENERGYDAMAGE, minDamage = -300, maxDamage = -850, length = 4, spread = 0, effect = CONST_ME_ENERGYHIT, target = false }, { name = "combat", interval = 1700, chance = 35, type = COMBAT_DEATHDAMAGE, minDamage = -700, maxDamage = -1550, radius = 3, effect = CONST_ME_MORTAREA, target = false }, - { name = "outfit", interval = 1000, chance = 5, radius = 8, effect = CONST_ME_LOSEENERGY, target = false, duration = 5000, outfitMonster = "goshnar's hatred" }, - { name = "outfit", interval = 1000, chance = 5, radius = 8, effect = CONST_ME_LOSEENERGY, target = false, duration = 5000, outfitMonster = "goshnar's greed" }, - { name = "outfit", interval = 1000, chance = 5, radius = 8, effect = CONST_ME_LOSEENERGY, target = false, duration = 5000, outfitMonster = "goshnar's malice" }, - { name = "outfit", interval = 1000, chance = 5, radius = 8, effect = CONST_ME_LOSEENERGY, target = false, duration = 5000, outfitMonster = "goshnar's spite" }, } monster.defenses = { defense = 40, - armor = 0, - -- mitigation = ???, + armor = 40, + { name = "outfit", interval = 4000, chance = 30, target = false, duration = 4000, outfitMonster = "Goshnar's Malice" }, + { name = "outfit", interval = 4000, chance = 30, target = false, duration = 4000, outfitMonster = "Goshnar's Cruelty" }, + { name = "outfit", interval = 4000, chance = 30, target = false, duration = 4000, outfitMonster = "Goshnar's Hatred" }, + { name = "outfit", interval = 4000, chance = 30, target = false, duration = 4000, outfitMonster = "Goshnar's Spite" }, } monster.elements = { diff --git a/data-otservbr-global/monster/quests/soul_war/goshnars_megalomania.lua b/data-otservbr-global/monster/quests/soul_war/goshnar's_megalomania_blue.lua similarity index 63% rename from data-otservbr-global/monster/quests/soul_war/goshnars_megalomania.lua rename to data-otservbr-global/monster/quests/soul_war/goshnar's_megalomania_blue.lua index 7346e628cfc..1433d2ed7d4 100644 --- a/data-otservbr-global/monster/quests/soul_war/goshnars_megalomania.lua +++ b/data-otservbr-global/monster/quests/soul_war/goshnar's_megalomania_blue.lua @@ -1,44 +1,40 @@ -local mType = Game.createMonsterType("Goshnar's Megalomania") +local mType = Game.createMonsterType("Goshnar's Megalomania Blue") local monster = {} +monster.name = "Goshnar's Megalomania" monster.description = "Goshnar's Megalomania" -monster.experience = 200000 +monster.experience = 3000000 monster.outfit = { - lookType = 1308, - lookHead = 0, - lookBody = 0, - lookLegs = 0, - lookFeet = 0, - lookAddons = 0, - lookMount = 0, + lookType = 1337, } -monster.events = { - "SoulwarsBossDeath", -} - -monster.health = 500000 -monster.maxHealth = 500000 +monster.health = 620000 +monster.maxHealth = 620000 monster.race = "undead" monster.corpse = 33889 -monster.speed = 165 +monster.speed = 0 monster.manaCost = 0 - -monster.changeTarget = { - interval = 2000, - chance = 10, -} +monster.maxSummons = 4 monster.bosstiary = { bossRaceId = 1969, bossRace = RARITY_NEMESIS, } +monster.changeTarget = { + interval = 4000, + chance = 10, +} + monster.strategiesTarget = { - nearest = 70, + nearest = 80, health = 10, damage = 10, - random = 10, +} + +monster.events = { + "GoshnarsHatredBuff", + "MegalomaniaDeath", } monster.flags = { @@ -51,7 +47,7 @@ monster.flags = { illusionable = false, canPushItems = true, canPushCreatures = true, - staticAttackChance = 95, + staticAttackChance = 80, targetDistance = 1, runHealth = 0, healthHidden = false, @@ -67,14 +63,6 @@ monster.light = { color = 0, } -monster.summon = { - maxSummons = 4, - summons = { - { name = "dreadful harvester", chance = 40, interval = 1000, count = 2 }, - { name = "aspect of power", chance = 50, interval = 1000, count = 2 }, - }, -} - monster.voices = { interval = 5000, chance = 10, @@ -110,19 +98,14 @@ monster.loot = { } monster.attacks = { - { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -8000 }, - { name = "combat", interval = 2000, chance = 30, type = COMBAT_PHYSICALDAMAGE, minDamage = -2950, maxDamage = -4400, range = 7, radius = 3, shootEffect = CONST_ANI_DEATH, effect = CONST_ME_MORTAREA, target = true }, - { name = "combat", interval = 2000, chance = 10, type = COMBAT_DEATHDAMAGE, minDamage = -3000, maxDamage = -5500, length = 8, spread = 0, effect = CONST_ME_INSECTS, target = false }, - { name = "singlecloudchain", interval = 6000, chance = 40, minDamage = -3300, maxDamage = -5500, range = 6, effect = CONST_ME_ENERGYHIT, target = true }, - { name = "combat", interval = 2000, chance = 10, type = COMBAT_DEATHDAMAGE, minDamage = -3300, maxDamage = -5200, length = 10, spread = 0, effect = CONST_ME_BLUE_GHOST, target = false }, + { name = "melee", interval = 2000, chance = 100, minDamage = -400, maxDamage = -2225 }, + { name = "megalomania blue", interval = 6000, chance = 100, target = true }, + { name = "combat", interval = 30000, chance = 100, type = COMBAT_LIFEDRAIN, minDamage = -1000, maxDamage = -1500, length = 8, radius = 5, spread = 0, effect = CONST_ME_PINK_ENERGY_SPARK, target = true }, } monster.defenses = { - defense = 160, - armor = 160, - mitigation = 8.40, - { name = "speed", interval = 1000, chance = 20, speedChange = 500, effect = CONST_ME_MAGIC_RED, target = false, duration = 10000 }, - { name = "combat", interval = 2000, chance = 25, type = COMBAT_HEALING, minDamage = 2250, maxDamage = 4250, effect = CONST_ME_MAGIC_BLUE, target = false }, + defense = 55, + armor = 55, } monster.elements = { @@ -145,18 +128,25 @@ monster.immunities = { { type = "bleed", condition = false }, } -mType.onThink = function(monster, interval) end - mType.onAppear = function(monster, creature) if monster:getType():isRewardBoss() then monster:setReward(true) end end -mType.onDisappear = function(monster, creature) end +local intervalBetweenExecutions = 10000 -mType.onMove = function(monster, creature, fromPosition, toPosition) end +local zone = Zone.getByName("boss.goshnar's-megalomania-purple") +local zonePositions = zone:getPositions() -mType.onSay = function(monster, creature, type, message) end +mType.onThink = function(monsterCallback, interval) + monsterCallback:onThinkGoshnarTormentCounter(interval, 36, intervalBetweenExecutions, SoulWarQuest.levers.goshnarsMegalomania.boss.position) + monsterCallback:onThinkMegalomaniaWhiteTiles(interval, zonePositions, 8000) + monsterCallback:goshnarsDefenseIncrease("cleansed-sanity-action") +end + +mType.onDisappear = function(monster, creature) + creature:removeGoshnarsMegalomaniaMonsters(zone) +end mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/goshnar's_megalomania_green.lua b/data-otservbr-global/monster/quests/soul_war/goshnar's_megalomania_green.lua new file mode 100644 index 00000000000..f6d60b0b747 --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/goshnar's_megalomania_green.lua @@ -0,0 +1,163 @@ +local mType = Game.createMonsterType("Goshnar's Megalomania Green") +local monster = {} + +monster.name = "Goshnar's Megalomania" +monster.description = "Goshnar's Megalomania" +monster.experience = 3000000 +monster.outfit = { + lookType = 99, + lookHead = 95, + lookBody = 116, + lookLegs = 119, + lookFeet = 115, + lookAddons = 0, + lookMount = 0, +} + +monster.bosstiary = { + bossRaceId = 1969, + bossRace = RARITY_NEMESIS, +} + +monster.health = 620000 +monster.maxHealth = 620000 +monster.race = "undead" +monster.corpse = 33889 +monster.speed = 250 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.events = { + "GoshnarsHatredBuff", + "MegalomaniaDeath", +} + +monster.strategiesTarget = { + nearest = 80, + health = 10, + damage = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = true, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 80, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, +} + +monster.loot = { + { name = "crystal coin", chance = 55000, minCount = 70, maxCount = 75 }, + { id = 281, chance = 1150 }, -- giant shimmering pearl (green) + { name = "giant sapphire", chance = 10000, maxCount = 1 }, + { name = "giant topaz", chance = 10000, maxCount = 1 }, + { name = "violet gem", chance = 6000, maxCount = 1 }, + { name = "blue gem", chance = 10000, maxCount = 3 }, + { id = 3039, chance = 10000, maxCount = 3 }, -- red gem + { name = "green gem", chance = 10000, maxCount = 3 }, + { name = "yellow gem", chance = 10000, maxCount = 3 }, + { name = "white gem", chance = 6000, maxCount = 3 }, + { name = "dragon figurine", chance = 10000, maxCount = 1 }, + { name = "bullseye potion", chance = 15000, minCount = 10, maxCount = 25 }, + { name = "mastermind potion", chance = 15000, minCount = 10, maxCount = 25 }, + { name = "berserk potion", chance = 15000, minCount = 10, maxCount = 25 }, + { name = "ultimate mana potion", chance = 18000, minCount = 50, maxCount = 100 }, + { name = "supreme health potion", chance = 18000, minCount = 50, maxCount = 100 }, + { name = "ultimate spirit potion", chance = 18000, minCount = 50, maxCount = 100 }, + { name = "figurine of malice", chance = 400 }, + { name = "figurine of cruelty", chance = 400 }, + { name = "figurine of hatred", chance = 400 }, + { name = "figurine of greed", chance = 400 }, + { name = "figurine of spite", chance = 400 }, + { name = "figurine of megalomania", chance = 400 }, + { name = "megalomania's skull", chance = 400 }, + { name = "megalomania's essence", chance = 400 }, + { name = "bag you desire", chance = 100 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -800, maxDamage = -2500 }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_DEATHDAMAGE, minDamage = -1550, maxDamage = -2620, length = 8, spread = 0, effect = CONST_ME_MORTAREA, target = false }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_DEATHDAMAGE, minDamage = -1550, maxDamage = -2620, length = 8, spread = 0, effect = CONST_ME_BLACK_BLOOD, target = false }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_DEATHDAMAGE, minDamage = -1050, maxDamage = -2020, length = 8, spread = 3, effect = CONST_ME_GHOST_SMOKE, target = false }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_DEATHDAMAGE, minDamage = -1050, maxDamage = -2020, length = 8, spread = 3, effect = CONST_ME_SMALLCLOUDS, target = false }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_DEATHDAMAGE, minDamage = -950, maxDamage = -1400, radius = 3, effect = CONST_ME_MORTAREA, target = true }, + { name = "soulwars fear", interval = 35000, chance = 100, target = true }, + { name = "megalomania transform elemental", interval = SoulWarQuest.goshnarsCrueltyWaveInterval * 1000, chance = 50 }, + { name = "combat", interval = 30000, chance = 100, type = COMBAT_LIFEDRAIN, minDamage = -1000, maxDamage = -1500, length = 8, radius = 5, spread = 0, effect = CONST_ME_PINK_ENERGY_SPARK, target = true }, +} + +monster.defenses = { + defense = 55, + armor = 55, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType.onAppear = function(monster, creature) + if monster:getType():isRewardBoss() then + monster:setReward(true) + end +end + +local intervalBetweenExecutions = 10000 + +local zone = Zone.getByName("boss.goshnar's-megalomania-purple") +local zonePositions = zone:getPositions() + +mType.onThink = function(monsterCallback, interval) + monsterCallback:onThinkGoshnarTormentCounter(interval, 36, intervalBetweenExecutions, SoulWarQuest.levers.goshnarsMegalomania.boss.position) + monsterCallback:onThinkMegalomaniaWhiteTiles(interval, zonePositions, 8000) + monsterCallback:goshnarsDefenseIncrease("cleansed-sanity-action") +end + +mType.onDisappear = function(monster, creature) + creature:removeGoshnarsMegalomaniaMonsters(zone) +end + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/goshnar's_megalomania_purple.lua b/data-otservbr-global/monster/quests/soul_war/goshnar's_megalomania_purple.lua new file mode 100644 index 00000000000..79914af4935 --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/goshnar's_megalomania_purple.lua @@ -0,0 +1,129 @@ +local mType = Game.createMonsterType("Goshnar's Megalomania Purple") +local monster = {} + +monster.name = "Goshnar's Megalomania" +monster.description = "Goshnar's Megalomania" +monster.experience = 0 +monster.outfit = { + lookType = 1308, +} + +monster.health = 620000 +monster.maxHealth = 620000 +monster.race = "undead" +monster.corpse = 6028 +monster.speed = 250 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 80, + health = 10, + damage = 10, +} + +monster.events = { + "GoshnarsHatredBuff", +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = true, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 80, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -800, maxDamage = -2500 }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_DEATHDAMAGE, minDamage = -1550, maxDamage = -2620, length = 8, spread = 0, effect = CONST_ME_MORTAREA, target = false }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_DEATHDAMAGE, minDamage = -1550, maxDamage = -2620, length = 8, spread = 0, effect = CONST_ME_BLACK_BLOOD, target = false }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_DEATHDAMAGE, minDamage = -1050, maxDamage = -2020, length = 8, spread = 3, effect = CONST_ME_GHOST_SMOKE, target = false }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_DEATHDAMAGE, minDamage = -1050, maxDamage = -2020, length = 8, spread = 3, effect = CONST_ME_SMALLCLOUDS, target = false }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_DEATHDAMAGE, minDamage = -950, maxDamage = -1400, radius = 3, effect = CONST_ME_MORTAREA, target = true }, + { name = "soulwars fear", interval = 35000, chance = 100, target = true }, + { name = "megalomania transform elemental", interval = SoulWarQuest.goshnarsCrueltyWaveInterval * 1000, chance = 50 }, + { name = "combat", interval = 30000, chance = 100, type = COMBAT_LIFEDRAIN, minDamage = -1000, maxDamage = -1500, length = 8, radius = 5, spread = 0, effect = CONST_ME_PINK_ENERGY_SPARK, target = true }, +} + +monster.defenses = { + defense = 25, + armor = 25, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 100 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 100 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 100 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 100 }, + { type = COMBAT_HOLYDAMAGE, percent = 100 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType.onSpawn = function(monster) + if monster:getType():isRewardBoss() then + monster:setReward(true) + end + + if SoulWarQuest.changeBlueEvent then + stopEvent(SoulWarQuest.changeBlueEvent) + end + if SoulWarQuest.changePurpleEvent then + stopEvent(SoulWarQuest.changePurpleEvent) + end + + local bossKV = monster:getSoulWarKV() + bossKV:set("aspect-of-power-death-count", 0) + monster:resetHatredDamageMultiplier() +end + +local intervalBetweenExecutions = 10000 + +local zone = Zone.getByName("boss.goshnar's-megalomania-purple") +local zonePositions = zone:getPositions() + +mType.onThink = function(monsterCallback, interval) + monsterCallback:onThinkGoshnarTormentCounter(interval, 36, intervalBetweenExecutions, SoulWarQuest.levers.goshnarsMegalomania.boss.position) + monsterCallback:onThinkMegalomaniaWhiteTiles(interval, zonePositions, 8000) + monsterCallback:goshnarsDefenseIncrease("cleansed-sanity-action") +end + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/goshnars_cruelty.lua b/data-otservbr-global/monster/quests/soul_war/goshnars_cruelty.lua index 40957e0bde4..44f87cca574 100644 --- a/data-otservbr-global/monster/quests/soul_war/goshnars_cruelty.lua +++ b/data-otservbr-global/monster/quests/soul_war/goshnars_cruelty.lua @@ -14,7 +14,8 @@ monster.outfit = { } monster.events = { - "SoulwarsBossDeath", + "SoulWarBossesDeath", + "GoshnarsCrueltyBuff", } monster.health = 300000 @@ -67,14 +68,6 @@ monster.light = { color = 0, } -monster.summon = { - maxSummons = 4, - summons = { - { name = "dreadful harvester", chance = 40, interval = 1000, count = 2 }, - { name = "mean maw", chance = 30, interval = 1000, count = 2 }, - }, -} - monster.voices = { interval = 5000, chance = 10, @@ -112,6 +105,7 @@ monster.attacks = { { name = "singlecloudchain", interval = 6000, chance = 40, minDamage = -1700, maxDamage = -2500, range = 6, effect = CONST_ME_ENERGYHIT, target = true }, { name = "combat", interval = 2000, chance = 30, type = COMBAT_PHYSICALDAMAGE, minDamage = -1000, maxDamage = -2500, range = 7, radius = 4, shootEffect = CONST_ANI_EXPLOSION, effect = CONST_ME_DRAWBLOOD, target = true }, { name = "combat", interval = 2000, chance = 15, type = COMBAT_DEATHDAMAGE, minDamage = -1500, maxDamage = -3000, radius = 3, effect = CONST_ME_GROUNDSHAKER, target = false }, + { name = "cruelty transform elemental", interval = SoulWarQuest.goshnarsCrueltyWaveInterval * 1000, chance = 50 }, } monster.defenses = { @@ -142,15 +136,33 @@ monster.immunities = { { type = "bleed", condition = false }, } -mType.onThink = function(monster, interval) end +local firstTime = 0 +mType.onThink = function(monster, interval) + firstTime = firstTime + interval + -- Run only 15 seconds before creation + if firstTime >= 15000 then + monster:goshnarsDefenseIncrease("greedy-maw-action") + end +end + +mType.onAppear = function(monster, creature) end -mType.onAppear = function(monster, creature) - if monster:getType():isRewardBoss() then - monster:setReward(true) +mType.onSpawn = function(monsterCallback) + if monsterCallback:getType():isRewardBoss() then + monsterCallback:setReward(true) end + + firstTime = 0 end -mType.onDisappear = function(monster, creature) end +mType.onDisappear = function(monster, creature) + if creature:getName() == "Goshnar's Cruelty" then + local eyeCreature = Creature("A Greedy Eye") + if eyeCreature then + eyeCreature:remove() + end + end +end mType.onMove = function(monster, creature, fromPosition, toPosition) end diff --git a/data-otservbr-global/monster/quests/soul_war/goshnars_greed.lua b/data-otservbr-global/monster/quests/soul_war/goshnars_greed.lua index 171f963f5ff..f1f5284398e 100644 --- a/data-otservbr-global/monster/quests/soul_war/goshnars_greed.lua +++ b/data-otservbr-global/monster/quests/soul_war/goshnars_greed.lua @@ -14,7 +14,7 @@ monster.outfit = { } monster.events = { - "SoulwarsBossDeath", + "SoulWarBossesDeath", } monster.health = 300000 @@ -68,11 +68,7 @@ monster.light = { } monster.summon = { - maxSummons = 4, - summons = { - { name = "dreadful harvester", chance = 10, interval = 1000, count = 2 }, - { name = "hateful soul", chance = 10, interval = 1000, count = 2 }, - }, + maxSummons = 1, } monster.voices = { @@ -139,15 +135,56 @@ monster.immunities = { { type = "bleed", condition = false }, } -mType.onThink = function(monster, interval) end +local immuneTimeCount = 0 +local isImmune = nil +local createdSoulSphere = nil +mType.onThink = function(monsterCallback, interval) + if GreedbeastKills >= 5 and isImmune == nil then + isImmune = monsterCallback:immune(false) + monsterCallback:teleportTo(Position(33741, 31659, 14)) + monsterCallback:setSpeed(0) + createdSoulSphere = Game.createMonster("Soul Sphere", Position(33752, 31659, 14), true, true) + end + if isImmune ~= nil then + immuneTimeCount = immuneTimeCount + interval + logger.info("Immune time count {}", immuneTimeCount) + if immuneTimeCount >= 45000 then + monsterCallback:immune(true) + monsterCallback:setSpeed(monster.speed) + monsterCallback:teleportTo(Position(33746, 31666, 14)) + immuneTimeCount = 0 + GreedbeastKills = 0 + isImmune = nil + if createdSoulSphere then + createdSoulSphere:remove() + end + end + end +end -mType.onAppear = function(monster, creature) +mType.onSpawn = function(monster) if monster:getType():isRewardBoss() then monster:setReward(true) end + + isImmune = nil + monster:immune(true) + immuneTimeCount = 0 + GreedbeastKills = 0 end -mType.onDisappear = function(monster, creature) end +mType.onDisappear = function(monster, creature) + if creature:getName() == "Greedbeast" then + logger.debug("GreedbeastKills {}", GreedbeastKills) + end + if creature:getName() == "Goshnar's Greed" then + logger.debug("Killed goshnar's greed") + if createdSoulSphere then + logger.debug("Found soul sphere, remove it") + createdSoulSphere:remove() + end + end +end mType.onMove = function(monster, creature, fromPosition, toPosition) end diff --git a/data-otservbr-global/monster/quests/soul_war/goshnars_hatred.lua b/data-otservbr-global/monster/quests/soul_war/goshnars_hatred.lua index fa5ccf36984..47fedea4e7c 100644 --- a/data-otservbr-global/monster/quests/soul_war/goshnars_hatred.lua +++ b/data-otservbr-global/monster/quests/soul_war/goshnars_hatred.lua @@ -14,7 +14,8 @@ monster.outfit = { } monster.events = { - "SoulwarsBossDeath", + "GoshnarsHatredBuff", + "SoulWarBossesDeath", } monster.health = 300000 @@ -67,14 +68,6 @@ monster.light = { color = 0, } -monster.summon = { - maxSummons = 4, - summons = { - { name = "dreadful harvester", chance = 10, interval = 1000, count = 2 }, - { name = "hateful soul", chance = 10, interval = 1000, count = 2 }, - }, -} - monster.voices = { interval = 5000, chance = 10, @@ -143,13 +136,26 @@ monster.immunities = { mType.onThink = function(monster, interval) end -mType.onAppear = function(monster, creature) +mType.onAppear = function(monster, creature) end + +mType.onSpawn = function(monster) if monster:getType():isRewardBoss() then monster:setReward(true) end + + monster:resetHatredDamageMultiplier() end -mType.onDisappear = function(monster, creature) end +mType.onDisappear = function(monster, creature) + if creature:getName() == "Goshnar's Hatred" then + for _, monsterName in pairs(SoulWarQuest.burningHatredMonsters) do + local ashesCreature = Creature(monsterName) + if ashesCreature then + ashesCreature:remove() + end + end + end +end mType.onMove = function(monster, creature, fromPosition, toPosition) end diff --git a/data-otservbr-global/monster/quests/soul_war/goshnars_malice.lua b/data-otservbr-global/monster/quests/soul_war/goshnars_malice.lua index 5e79dcccbdd..e0b054dc10d 100644 --- a/data-otservbr-global/monster/quests/soul_war/goshnars_malice.lua +++ b/data-otservbr-global/monster/quests/soul_war/goshnars_malice.lua @@ -14,7 +14,8 @@ monster.outfit = { } monster.events = { - "SoulwarsBossDeath", + "SoulWarBossesDeath", + "Goshnar's-Malice", } monster.health = 300000 @@ -67,14 +68,6 @@ monster.light = { color = 0, } -monster.summon = { - maxSummons = 4, - summons = { - { name = "dreadful harvester", chance = 40, interval = 1000, count = 2 }, - { name = "malicious soul", chance = 30, interval = 1000, count = 2 }, - }, -} - monster.voices = { interval = 5000, chance = 10, @@ -141,7 +134,19 @@ monster.immunities = { { type = "bleed", condition = false }, } -mType.onThink = function(monster, interval) end +local zone = Zone.getByName("boss.goshnar's-malice") +local zonePositions = zone:getPositions() + +local accumulatedTime = 0 +local desiredInterval = 40000 +mType.onThink = function(monster, interval) + accumulatedTime = accumulatedTime + interval + -- Execute only after 40 seconds + if accumulatedTime >= desiredInterval then + monster:createSoulWarWhiteTiles(SoulWarQuest.levers.goshnarsMalice.boss.position, zonePositions) + accumulatedTime = 0 + end +end mType.onAppear = function(monster, creature) if monster:getType():isRewardBoss() then diff --git a/data-otservbr-global/monster/quests/soul_war/goshnars_spite.lua b/data-otservbr-global/monster/quests/soul_war/goshnars_spite.lua index 19d35cd1af7..40817c335b0 100644 --- a/data-otservbr-global/monster/quests/soul_war/goshnars_spite.lua +++ b/data-otservbr-global/monster/quests/soul_war/goshnars_spite.lua @@ -14,7 +14,7 @@ monster.outfit = { } monster.events = { - "SoulwarsBossDeath", + "SoulWarBossesDeath", } monster.health = 300000 @@ -67,14 +67,6 @@ monster.light = { color = 0, } -monster.summon = { - maxSummons = 4, - summons = { - { name = "dreadful harvester", chance = 40, interval = 1000, count = 2 }, - { name = "spiteful spitter", chance = 30, interval = 1000, count = 2 }, - }, -} - monster.voices = { interval = 5000, chance = 10, @@ -111,6 +103,7 @@ monster.attacks = { { name = "singlecloudchain", interval = 6000, chance = 40, minDamage = -1700, maxDamage = -1900, range = 6, effect = CONST_ME_ENERGYHIT, target = true }, { name = "combat", interval = 2000, chance = 30, type = COMBAT_EARTHDAMAGE, minDamage = -1200, maxDamage = -3500, range = 7, radius = 4, shootEffect = CONST_ANI_POISON, effect = CONST_ME_GREEN_RINGS, target = true }, { name = "combat", interval = 2000, chance = 10, type = COMBAT_EARTHDAMAGE, minDamage = -1400, maxDamage = -2200, length = 8, spread = 0, effect = CONST_ME_GREEN_RINGS, target = false }, + { name = "soulwars fear", interval = 2000, chance = 10, target = true }, } monster.defenses = { diff --git a/data-otservbr-global/monster/quests/soul_war/greedbeast.lua b/data-otservbr-global/monster/quests/soul_war/greedbeast.lua new file mode 100644 index 00000000000..db7076d5710 --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/greedbeast.lua @@ -0,0 +1,98 @@ +local mType = Game.createMonsterType("Greedbeast") +local monster = {} + +monster.description = "a greedbeast" +monster.experience = 0 +monster.outfit = { + lookType = 101, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 10000 +monster.maxHealth = 10000 +monster.race = "undead" +monster.corpse = 0 +monster.speed = 200 + +monster.events = { + "GreedMonsterDeath", +} + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 100, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = false, + canWalkOnFire = false, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -500, condition = { type = CONDITION_POISON, totalDamage = 100, interval = 4000 } }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_EARTHDAMAGE, minDamage = -50, maxDamage = -90, range = 7, shootEffect = CONST_ANI_POISON, effect = CONST_ME_POISONAREA, target = false }, + { name = "combat", interval = 2000, chance = 10, type = COMBAT_LIFEDRAIN, minDamage = -25, maxDamage = -47, radius = 3, effect = CONST_ME_MAGIC_RED, target = false }, + -- poison + { name = "condition", type = CONDITION_POISON, interval = 2000, chance = 10, minDamage = -200, maxDamage = -400, radius = 3, effect = CONST_ME_POISONAREA, target = false }, + -- poison + { name = "condition", type = CONDITION_POISON, interval = 2000, chance = 10, minDamage = -200, maxDamage = -400, length = 6, spread = 0, effect = CONST_ME_POISONAREA, target = false }, + { name = "speed", interval = 2000, chance = 15, speedChange = -600, target = true, duration = 13000 }, +} + +monster.defenses = { + defense = 70, + armor = 80, + mitigation = 1.15, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_HEALING, minDamage = 50, maxDamage = 60, effect = CONST_ME_HITBYPOISON, target = false }, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = -10 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = -25 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/mirror_image.lua b/data-otservbr-global/monster/quests/soul_war/mirror_image.lua index 8813ba2e93f..600550d3d78 100644 --- a/data-otservbr-global/monster/quests/soul_war/mirror_image.lua +++ b/data-otservbr-global/monster/quests/soul_war/mirror_image.lua @@ -13,6 +13,10 @@ monster.outfit = { lookMount = 0, } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 35000 monster.maxHealth = 35000 monster.race = "blood" @@ -106,4 +110,34 @@ monster.events = { "MirrorImageTransform", } +mType.onPlayerAttack = function(monster, attackerPlayer) + logger.info("Player {}, attacking monster {}", attackerPlayer:getName(), monster:getName()) + + local apparitionType = "" + + local sameVocationProbability = 70 -- 70% chance for create monster of first player attack vocation + if attackerPlayer:isDruid() then + apparitionType = "Druid's Apparition" + elseif attackerPlayer:isKnight() then + apparitionType = "Knight's Apparition" + elseif attackerPlayer:isPaladin() then + apparitionType = "Paladin's Apparition" + elseif attackerPlayer:isSorcerer() then + apparitionType = "Sorcerer's Apparition" + end + + if math.random(100) > sameVocationProbability then + repeat + local randomIndex = math.random(#SoulWarQuest.apparitionNames) + if SoulWarQuest.apparitionNames[randomIndex] ~= apparitionType then + apparitionType = SoulWarQuest.apparitionNames[randomIndex] + break + end + until false + end + + Game.createMonster(apparitionType, monster:getPosition(), true, true) + monster:remove() +end + mType:register(monster) diff --git a/data-otservbr-global/monster/undeads/bony_sea_devil.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/bony_sea_devil.lua similarity index 95% rename from data-otservbr-global/monster/undeads/bony_sea_devil.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/bony_sea_devil.lua index 1199240df4b..d6e4bcfcafe 100644 --- a/data-otservbr-global/monster/undeads/bony_sea_devil.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/bony_sea_devil.lua @@ -26,6 +26,10 @@ monster.Bestiary = { Locations = "Ebb and Flow.", } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 24000 monster.maxHealth = 24000 monster.race = "undead" @@ -96,7 +100,7 @@ monster.loot = { { name = "goblet of gloom", chance = 880 }, { name = "glacier kilt", chance = 880 }, { name = "glacial rod", chance = 1210 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { @@ -136,4 +140,8 @@ monster.immunities = { { type = "bleed", condition = false }, } +mType.onThink = function(monster, interval) + monster:tryTeleportToPlayer("Get out the way!") +end + mType:register(monster) diff --git a/data-otservbr-global/monster/demons/brachiodemon.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/brachiodemon.lua similarity index 95% rename from data-otservbr-global/monster/demons/brachiodemon.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/brachiodemon.lua index 930ecfe469f..a76c9f40ee9 100644 --- a/data-otservbr-global/monster/demons/brachiodemon.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/brachiodemon.lua @@ -26,6 +26,10 @@ monster.Bestiary = { Locations = "Claustrophobic Inferno.", } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 25000 monster.maxHealth = 25000 monster.race = "blood" @@ -99,7 +103,7 @@ monster.loot = { { name = "mastermind shield", chance = 420 }, { name = "assassin dagger", chance = 340 }, { name = "alloy legs", chance = 170 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { @@ -137,4 +141,8 @@ monster.immunities = { { type = "bleed", condition = false }, } +mType.onThink = function(monster, interval) + monster:tryTeleportToPlayer("Burn in hell!") +end + mType:register(monster) diff --git a/data-otservbr-global/monster/plants/branchy_crawler.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/branchy_crawler.lua similarity index 94% rename from data-otservbr-global/monster/plants/branchy_crawler.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/branchy_crawler.lua index d319fcde5e5..f5dccd47eb5 100644 --- a/data-otservbr-global/monster/plants/branchy_crawler.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/branchy_crawler.lua @@ -26,6 +26,10 @@ monster.Bestiary = { Locations = "Rotten Wasteland.", } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 27000 monster.maxHealth = 27000 monster.race = "blood" @@ -94,7 +98,7 @@ monster.loot = { { name = "twiceslicer", chance = 420 }, { name = "crystalline sword", chance = 390 }, { name = "ruthless axe", chance = 330 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { @@ -132,4 +136,8 @@ monster.immunities = { { type = "bleed", condition = false }, } +mType.onThink = function(monster, interval) + monster:tryTeleportToPlayer("My growth is your death!") +end + mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/ashes_of_burning_hatred.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/ashes_of_burning_hatred.lua new file mode 100644 index 00000000000..72c4082ee0e --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/ashes_of_burning_hatred.lua @@ -0,0 +1,89 @@ +local mType = Game.createMonsterType("Ashes of Burning Hatred") +local monster = {} + +monster.description = "a ashes of burning hatred" +monster.experience = 0 +monster.outfit = { + lookTypeEx = 34009, +} + +monster.health = 15000 +monster.maxHealth = 15000 +monster.race = "undead" +monster.corpse = 5993 +monster.speed = 0 +monster.manaCost = 100 +monster.maxSummons = 0 + +monster.changeTarget = { + interval = 2000, + chance = 40, +} + +monster.strategiesTarget = { + nearest = 0, + health = 0, + damage = 0, + random = 100, +} + +monster.events = { + "BurningChangeForm", +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.attacks = { + { name = "combat", interval = 2000, chance = 25, type = COMBAT_FIREDAMAGE, minDamage = -555, maxDamage = -703, range = 9, shootEffect = CONST_ANI_FIRE, target = true }, +} + +monster.defenses = { + defense = 5, + armor = 10, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 100 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 100 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 100 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 100 }, + { type = COMBAT_HOLYDAMAGE, percent = 100 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/blaze_of_burning_hatred.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/blaze_of_burning_hatred.lua new file mode 100644 index 00000000000..c0b76ce98df --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/blaze_of_burning_hatred.lua @@ -0,0 +1,89 @@ +local mType = Game.createMonsterType("Blaze of Burning Hatred") +local monster = {} + +monster.description = "a blaze of burning hatred" +monster.experience = 0 +monster.outfit = { + lookTypeEx = 34013, +} + +monster.health = 15000 +monster.maxHealth = 15000 +monster.race = "undead" +monster.corpse = 5993 +monster.speed = 0 +monster.manaCost = 100 +monster.maxSummons = 0 + +monster.changeTarget = { + interval = 2000, + chance = 40, +} + +monster.strategiesTarget = { + nearest = 0, + health = 0, + damage = 0, + random = 100, +} + +monster.events = { + "BurningChangeForm", +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.attacks = { + { name = "combat", interval = 2000, chance = 25, type = COMBAT_FIREDAMAGE, minDamage = -555, maxDamage = -703, range = 9, shootEffect = CONST_ANI_FIRE, target = true }, +} + +monster.defenses = { + defense = 5, + armor = 10, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 100 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 100 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 100 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 100 }, + { type = COMBAT_HOLYDAMAGE, percent = 100 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/flame_of_burning_hatred.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/flame_of_burning_hatred.lua new file mode 100644 index 00000000000..483c5156fb1 --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/flame_of_burning_hatred.lua @@ -0,0 +1,89 @@ +local mType = Game.createMonsterType("Flame of Burning Hatred") +local monster = {} + +monster.description = "a flame of burning hatred" +monster.experience = 0 +monster.outfit = { + lookTypeEx = 34011, +} + +monster.health = 15000 +monster.maxHealth = 15000 +monster.race = "undead" +monster.corpse = 5993 +monster.speed = 0 +monster.manaCost = 100 +monster.maxSummons = 0 + +monster.changeTarget = { + interval = 2000, + chance = 40, +} + +monster.strategiesTarget = { + nearest = 0, + health = 0, + damage = 0, + random = 100, +} + +monster.events = { + "BurningChangeForm", +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.attacks = { + { name = "combat", interval = 2000, chance = 25, type = COMBAT_FIREDAMAGE, minDamage = -555, maxDamage = -703, range = 9, shootEffect = CONST_ANI_FIRE, target = true }, +} + +monster.defenses = { + defense = 5, + armor = 10, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 100 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 100 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 100 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 100 }, + { type = COMBAT_HOLYDAMAGE, percent = 100 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/spark_of_burning_hatred.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/spark_of_burning_hatred.lua new file mode 100644 index 00000000000..dc9ff25f05e --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/spark_of_burning_hatred.lua @@ -0,0 +1,89 @@ +local mType = Game.createMonsterType("Spark of Burning Hatred") +local monster = {} + +monster.description = "a spark of burning hatred" +monster.experience = 0 +monster.outfit = { + lookTypeEx = 34010, +} + +monster.health = 15000 +monster.maxHealth = 15000 +monster.race = "undead" +monster.corpse = 5993 +monster.speed = 0 +monster.manaCost = 100 +monster.maxSummons = 0 + +monster.changeTarget = { + interval = 2000, + chance = 40, +} + +monster.strategiesTarget = { + nearest = 0, + health = 0, + damage = 0, + random = 100, +} + +monster.events = { + "BurningChangeForm", +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.attacks = { + { name = "combat", interval = 2000, chance = 25, type = COMBAT_FIREDAMAGE, minDamage = -555, maxDamage = -703, range = 9, shootEffect = CONST_ANI_FIRE, target = true }, +} + +monster.defenses = { + defense = 5, + armor = 10, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 100 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 100 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 100 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 100 }, + { type = COMBAT_HOLYDAMAGE, percent = 100 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/symbol_of_hatred.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/symbol_of_hatred.lua new file mode 100644 index 00000000000..76d403fab1a --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/burning_hatred/symbol_of_hatred.lua @@ -0,0 +1,84 @@ +local mType = Game.createMonsterType("Symbol of Hatred") +local monster = {} + +monster.description = "a symbol of hatred" +monster.experience = 0 +monster.outfit = { + lookTypeEx = 11427, +} + +monster.health = 14000 +monster.maxHealth = 14000 +monster.race = "undead" +monster.corpse = 33792 +monster.speed = 0 +monster.manaCost = 100 +monster.maxSummons = 0 + +monster.changeTarget = { + interval = 4000, + chance = 0, +} + +monster.strategiesTarget = { + nearest = 100, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = false, + canWalkOnFire = false, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.defenses = { + defense = 55, + armor = 55, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +local intervalBetweenExecutions = 3000 + +mType.onThink = function(monsterCallback, interval) + monsterCallback:onThinkGoshnarTormentCounter(interval, 30, intervalBetweenExecutions, SoulWarQuest.levers.goshnarsHatred.boss.position, "Goshnar's Hatred") +end + +mType:register(monster) diff --git a/data-otservbr-global/monster/undeads/capricious_phantom.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/capricious_phantom.lua similarity index 97% rename from data-otservbr-global/monster/undeads/capricious_phantom.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/capricious_phantom.lua index cd402486853..0b3bf5b8572 100644 --- a/data-otservbr-global/monster/undeads/capricious_phantom.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/capricious_phantom.lua @@ -26,6 +26,10 @@ monster.Bestiary = { Locations = "Ebb and Flow.", } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 30000 monster.maxHealth = 30000 monster.race = "undead" @@ -93,7 +97,7 @@ monster.loot = { { id = 23542, chance = 1180 }, -- collar of blue plasma { name = "glacial rod", chance = 940 }, { name = "ornate crossbow", chance = 940 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/monster/undeads/distorted_phantom.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/distorted_phantom.lua similarity index 97% rename from data-otservbr-global/monster/undeads/distorted_phantom.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/distorted_phantom.lua index c1fdc41461b..3f28aac16b9 100644 --- a/data-otservbr-global/monster/undeads/distorted_phantom.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/distorted_phantom.lua @@ -26,6 +26,10 @@ monster.Bestiary = { Locations = "Mirrored Nightmare.", } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 26000 monster.maxHealth = 26000 monster.race = "undead" @@ -92,7 +96,7 @@ monster.loot = { { name = "spellbook of warding", chance = 2890 }, { id = 23531, chance = 1930 }, -- ring of green plasma { name = "glacial rod", chance = 1290 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/monster/undeads/druid's_apparition.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/druid's_apparition.lua similarity index 97% rename from data-otservbr-global/monster/undeads/druid's_apparition.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/druid's_apparition.lua index 414de6155bf..957307c40eb 100644 --- a/data-otservbr-global/monster/undeads/druid's_apparition.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/druid's_apparition.lua @@ -32,6 +32,10 @@ monster.corpse = 6081 monster.speed = 235 monster.manaCost = 0 +monster.events = { + "MirroredNightmareBossAccess", +} + monster.changeTarget = { interval = 4000, chance = 0, @@ -90,7 +94,7 @@ monster.loot = { { name = "platinum amulet", chance = 1750 }, { name = "glacier robe", chance = 880 }, { id = 23544, chance = 440 }, -- collar of red plasma - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/a_greedy_eye.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/a_greedy_eye.lua new file mode 100644 index 00000000000..aed9dcfc829 --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/a_greedy_eye.lua @@ -0,0 +1,93 @@ +local mType = Game.createMonsterType("A Greedy Eye") +local monster = {} + +monster.description = "a greedy eye" +monster.experience = 0 +monster.outfit = { + lookType = 925, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 30000 +monster.maxHealth = 30000 +monster.race = "blood" +monster.corpse = 5995 +monster.speed = 0 +monster.manaCost = 0 +monster.maxSummons = 0 + +monster.changeTarget = { + interval = 4000, + chance = 20, +} + +monster.strategiesTarget = { + nearest = 100, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 70, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, +} + +monster.attacks = { + { name = "greedy eye beam", interval = 2000, chance = 100, minDamage = -1000, maxDamage = -1000 }, +} + +monster.defenses = { + defense = 55, + armor = 55, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 100 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 100 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 100 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 100 }, + { type = COMBAT_HOLYDAMAGE, percent = 100 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otservbr-global/monster/plants/cloak_of_terror.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/cloak_of_terror.lua similarity index 94% rename from data-otservbr-global/monster/plants/cloak_of_terror.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/cloak_of_terror.lua index 184ba93fab2..ff25ab1f058 100644 --- a/data-otservbr-global/monster/plants/cloak_of_terror.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/cloak_of_terror.lua @@ -26,6 +26,11 @@ monster.Bestiary = { Locations = "Furious Crater.", } +monster.events = { + "FourthTaintBossesPrepareDeath", + "CloakOfTerrorHealthLoss", +} + monster.health = 28000 monster.maxHealth = 28000 monster.race = "undead" @@ -93,7 +98,7 @@ monster.loot = { { name = "blue gem", chance = 1490 }, { name = "brooch of embracement", chance = 1490 }, { name = "wand of defiance", chance = 990 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { @@ -131,4 +136,8 @@ monster.immunities = { { type = "bleed", condition = false }, } +mType.onThink = function(monster, interval) + monster:tryTeleportToPlayer("I am your terror!") +end + mType:register(monster) diff --git a/data-otservbr-global/monster/extra_dimensional/courage_leech.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/courage_leech.lua similarity index 97% rename from data-otservbr-global/monster/extra_dimensional/courage_leech.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/courage_leech.lua index d8ad3147a8e..615e4637841 100644 --- a/data-otservbr-global/monster/extra_dimensional/courage_leech.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/courage_leech.lua @@ -26,6 +26,10 @@ monster.Bestiary = { Locations = "Furious Crater", } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 27000 monster.maxHealth = 27000 monster.race = "undead" @@ -92,7 +96,7 @@ monster.loot = { { name = "stone skin amulet", chance = 910 }, { name = "nightmare blade", chance = 1190 }, { name = "demonrage sword", chance = 600 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/poor_soul.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/poor_soul.lua new file mode 100644 index 00000000000..316959ddcd8 --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/poor_soul.lua @@ -0,0 +1,118 @@ +local mType = Game.createMonsterType("Poor Soul") +local monster = {} + +monster.description = "a poor soul" +monster.experience = 0 +monster.outfit = { + lookType = 1296, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 500 +monster.maxHealth = 500 +monster.race = "undead" +monster.corpse = 33891 +monster.speed = 140 +monster.manaCost = 0 +monster.maxSummons = 0 + +monster.changeTarget = { + interval = 4000, + chance = 5, +} + +monster.strategiesTarget = { + nearest = 60, + health = 10, + damage = 10, + random = 20, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "I have a head start.", yell = false }, + { text = "Look into my eyes! No, the other ones!", yell = false }, + { text = "The mirrors can't contain the night!", yell = false }, +} + +monster.loot = { + { name = "crystal coin", chance = 70540 }, + { name = "ultimate health potion", chance = 12220, maxCount = 7 }, + { name = "violet gem", chance = 4560 }, + { name = "green gem", chance = 5760 }, + { name = "blue gem", chance = 4960 }, + { name = "northwind rod", chance = 5920 }, + { name = "sacred tree amulet", chance = 5520 }, + { id = 33933, chance = 7920 }, -- apron + { id = 3067, chance = 7220 }, -- hailstorm rod + { name = "glacier shoes", chance = 2520 }, + { name = "glacier robe", chance = 2220 }, + { name = "stone skin amulet", chance = 5920 }, + { id = 23533, chance = 4892 }, -- ring of red plasma + { id = 33932, chance = 3520 }, -- head + { name = "glacial rod", chance = 620 }, + { id = 34024, chance = 650 }, -- gruesome fan +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -250, maxDamage = -450 }, +} + +monster.defenses = { + defense = 90, + armor = 105, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 100 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 100 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 100 }, + { type = COMBAT_DROWNDAMAGE, percent = -300 }, + { type = COMBAT_ICEDAMAGE, percent = 100 }, + { type = COMBAT_HOLYDAMAGE, percent = 100 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = true }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otservbr-global/monster/undeads/vibrant_phantom.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/vibrant_phantom.lua similarity index 97% rename from data-otservbr-global/monster/undeads/vibrant_phantom.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/vibrant_phantom.lua index 9f17bee76ee..ae4c60df095 100644 --- a/data-otservbr-global/monster/undeads/vibrant_phantom.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/furious_crater/vibrant_phantom.lua @@ -26,6 +26,10 @@ monster.Bestiary = { Locations = "Furious Crater.", } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 27000 monster.maxHealth = 27000 monster.race = "undead" @@ -94,7 +98,7 @@ monster.loot = { { name = "violet crystal shard", chance = 1080 }, { id = 23529, chance = 1080 }, -- ring of blue plasma { name = "green gem", chance = 1080 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/monster/quests/soul_war/hateful_soul.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/hateful_soul.lua similarity index 99% rename from data-otservbr-global/monster/quests/soul_war/hateful_soul.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/hateful_soul.lua index 4aad05f3f33..69c413aa242 100644 --- a/data-otservbr-global/monster/quests/soul_war/hateful_soul.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/hateful_soul.lua @@ -16,7 +16,7 @@ monster.outfit = { monster.health = 25000 monster.maxHealth = 25000 monster.race = "undead" -monster.corpse = 0 +monster.corpse = 33793 monster.speed = 125 monster.manaCost = 0 diff --git a/data-otservbr-global/monster/demons/infernal_demon.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/infernal_demon.lua similarity index 97% rename from data-otservbr-global/monster/demons/infernal_demon.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/infernal_demon.lua index 7c4a1d80302..0ec107c192f 100644 --- a/data-otservbr-global/monster/demons/infernal_demon.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/infernal_demon.lua @@ -26,6 +26,10 @@ monster.Bestiary = { Locations = "Claustrophobic Inferno.", } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 32000 monster.maxHealth = 32000 monster.race = "blood" @@ -94,7 +98,7 @@ monster.loot = { { name = "giant sword", chance = 2860 }, { name = "magma boots", chance = 2290 }, { name = "stone skin amulet", chance = 570 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/monster/undeads/infernal_phantom.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/infernal_phantom.lua similarity index 97% rename from data-otservbr-global/monster/undeads/infernal_phantom.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/infernal_phantom.lua index 0b61e291291..82b7fd2090a 100644 --- a/data-otservbr-global/monster/undeads/infernal_phantom.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/infernal_phantom.lua @@ -26,6 +26,10 @@ monster.Bestiary = { Locations = "Claustrophobic Inferno.", } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 26000 monster.maxHealth = 26000 monster.race = "undead" @@ -95,7 +99,7 @@ monster.loot = { { name = "crystal mace", chance = 1610 }, { name = "war axe", chance = 1410 }, { name = "warrior's axe", chance = 1410 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/monster/undeads/knight's_apparition.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/knight's_apparition.lua similarity index 97% rename from data-otservbr-global/monster/undeads/knight's_apparition.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/knight's_apparition.lua index 0a4415aa2fd..f763e9b4dfe 100644 --- a/data-otservbr-global/monster/undeads/knight's_apparition.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/knight's_apparition.lua @@ -33,6 +33,10 @@ monster.corpse = 111 monster.speed = 235 monster.manaCost = 0 +monster.events = { + "MirroredNightmareBossAccess", +} + monster.changeTarget = { interval = 4000, chance = 0, @@ -88,7 +92,7 @@ monster.loot = { { name = "giant sword", chance = 1720 }, { name = "stone skin amulet", chance = 1500 }, { name = "crown shield", chance = 640 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/monster/demons/many_faces.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/many_faces.lua similarity index 94% rename from data-otservbr-global/monster/demons/many_faces.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/many_faces.lua index 8b9724a6252..f60b999dbce 100644 --- a/data-otservbr-global/monster/demons/many_faces.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/many_faces.lua @@ -26,6 +26,10 @@ monster.Bestiary = { Locations = "Mirrored Nightmare.", } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 30000 monster.maxHealth = 30000 monster.race = "undead" @@ -95,7 +99,7 @@ monster.loot = { { name = "glacier robe", chance = 2130 }, { name = "gruesome fan", chance = 610 }, { name = "glacial rod", chance = 610 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { @@ -133,4 +137,8 @@ monster.immunities = { { type = "bleed", condition = false }, } +mType.onThink = function(monster, interval) + monster:tryTeleportToPlayer("Hands off my comrades!") +end + mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/greater_splinter_of_madness.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/greater_splinter_of_madness.lua new file mode 100644 index 00000000000..2193aac5447 --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/greater_splinter_of_madness.lua @@ -0,0 +1,107 @@ +local mType = Game.createMonsterType("Greater Splinter of Madness") +local monster = {} + +monster.description = "a greater splinter of madness" +monster.experience = 0 +monster.outfit = { + lookType = 1268, + lookHead = 0, + lookBody = 82, + lookLegs = 0, + lookFeet = 0, + lookAddons = 1, + lookMount = 0, +} + +monster.health = 4000 +monster.maxHealth = 4000 +monster.race = "undead" +monster.corpse = 32610 +monster.speed = 175 +monster.manaCost = 0 +monster.maxSummons = 0 + +monster.changeTarget = { + interval = 5000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 60, + health = 10, + damage = 10, + random = 20, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -100, maxDamage = -450 }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_DEATHDAMAGE, minDamage = -350, maxDamage = -800, range = 4, shootEffect = CONST_ANI_SUDDENDEATH, target = false }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_ENERGYDAMAGE, minDamage = -300, maxDamage = -750, range = 4, shootEffect = CONST_ANI_ENERGY, target = true }, +} + +monster.defenses = { + defense = 40, + armor = 79, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 100 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 100 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 100 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 100 }, + { type = COMBAT_HOLYDAMAGE, percent = 100 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType.onSpawn = function(monsterCallback) + addEvent(function(monsterId) + local eventMonster = Monster(monsterId) + if eventMonster then + eventMonster:setType("Mighty Splinter of Madness", true) + end + end, 120000, monsterCallback:getId()) +end + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/lesser_splinter_of_madness.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/lesser_splinter_of_madness.lua new file mode 100644 index 00000000000..7927ac18a26 --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/lesser_splinter_of_madness.lua @@ -0,0 +1,107 @@ +local mType = Game.createMonsterType("Lesser Splinter of Madness") +local monster = {} + +monster.description = "a lesser splinter of madness" +monster.experience = 0 +monster.outfit = { + lookType = 1268, + lookHead = 0, + lookBody = 57, + lookLegs = 0, + lookFeet = 0, + lookAddons = 1, + lookMount = 0, +} + +monster.health = 4000 +monster.maxHealth = 4000 +monster.race = "undead" +monster.corpse = 32610 +monster.speed = 175 +monster.manaCost = 0 +monster.maxSummons = 0 + +monster.changeTarget = { + interval = 5000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 60, + health = 10, + damage = 10, + random = 20, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -100, maxDamage = -350 }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_DEATHDAMAGE, minDamage = -300, maxDamage = -550, range = 4, shootEffect = CONST_ANI_SUDDENDEATH, target = false }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_ENERGYDAMAGE, minDamage = -300, maxDamage = -550, range = 4, shootEffect = CONST_ANI_ENERGY, target = true }, +} + +monster.defenses = { + defense = 40, + armor = 79, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 100 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 100 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 100 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 100 }, + { type = COMBAT_HOLYDAMAGE, percent = 100 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType.onSpawn = function(monsterCallback) + addEvent(function(monsterId) + local eventMonster = Monster(monsterId) + if eventMonster then + eventMonster:setType("Greater Splinter of Madness", true) + end + end, 120000, monsterCallback:getId()) +end + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/mighty_splinter_of_madness.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/mighty_splinter_of_madness.lua new file mode 100644 index 00000000000..18dd5825b89 --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/mighty_splinter_of_madness.lua @@ -0,0 +1,113 @@ +local mType = Game.createMonsterType("Mighty Splinter of Madness") +local monster = {} + +monster.description = "a mighty splinter of madness" +monster.experience = 0 +monster.outfit = { + lookType = 1268, + lookHead = 0, + lookBody = 93, + lookLegs = 0, + lookFeet = 0, + lookAddons = 1, + lookMount = 0, +} + +monster.health = 4000 +monster.maxHealth = 4000 +monster.race = "undead" +monster.corpse = 32610 +monster.speed = 175 +monster.manaCost = 0 +monster.maxSummons = 0 + +monster.changeTarget = { + interval = 5000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 60, + health = 10, + damage = 10, + random = 20, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -100, maxDamage = -750 }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_DEATHDAMAGE, minDamage = -450, maxDamage = -800, range = 4, shootEffect = CONST_ANI_SUDDENDEATH, target = false }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_ENERGYDAMAGE, minDamage = -400, maxDamage = -800, range = 4, shootEffect = CONST_ANI_ENERGY, target = true }, +} + +monster.defenses = { + defense = 40, + armor = 79, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 100 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 100 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 100 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 100 }, + { type = COMBAT_HOLYDAMAGE, percent = 100 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType.onSpawn = function(monsterCallback) + addEvent(function(monsterId) + local eventMonster = Monster(monsterId) + if eventMonster then + creature:say("Goshnar's Megalomania feeds on its own madness and becomes stronger!", TALKTYPE_MONSTER_SAY, 0, 0, Position(34091, 31026, 9)) + creature:remove() + local boss = Creature("Goshnar's Megalomania") + if boss then + boss:increaseHatredDamageMultiplier(5) + logger.debug("Goshnar's Megalomania has increased its damage multiplier to 5.") + end + end + end, 120000, monsterCallback:getId()) +end + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/necromantic_focus.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/necromantic_focus.lua new file mode 100644 index 00000000000..53c607ace7f --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/megalomania_room/necromantic_focus.lua @@ -0,0 +1,82 @@ +local mType = Game.createMonsterType("Necromantic Focus") +local monster = {} + +monster.description = "a necromantic focus" +monster.experience = 0 +monster.outfit = { + lookTypeEx = 7059, +} + +monster.health = 12000 +monster.maxHealth = 12000 +monster.race = "undead" +monster.corpse = 33984 +monster.speed = 0 +monster.manaCost = 100 +monster.maxSummons = 0 + +monster.events = { + "NecromanticFocusDeath", +} + +monster.changeTarget = { + interval = 4000, + chance = 0, +} + +monster.strategiesTarget = { + nearest = 100, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = false, + canWalkOnFire = false, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.defenses = { + defense = 55, + armor = 55, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otservbr-global/monster/undeads/mould_phantom.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/mould_phantom.lua similarity index 97% rename from data-otservbr-global/monster/undeads/mould_phantom.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/mould_phantom.lua index c3d7ea8ea86..1af17c877fa 100644 --- a/data-otservbr-global/monster/undeads/mould_phantom.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/mould_phantom.lua @@ -26,6 +26,10 @@ monster.Bestiary = { Locations = "Rotten Wasteland.", } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 28000 monster.maxHealth = 28000 monster.race = "undead" @@ -93,7 +97,7 @@ monster.loot = { { id = 23529, chance = 1040 }, -- ring of blue plasma { name = "ornate crossbow", chance = 840 }, { name = "crystal crossbow", chance = 620 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/monster/undeads/paladin's_apparition.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/paladin's_apparition.lua similarity index 97% rename from data-otservbr-global/monster/undeads/paladin's_apparition.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/paladin's_apparition.lua index 066266e70e3..2dc7a49555c 100644 --- a/data-otservbr-global/monster/undeads/paladin's_apparition.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/paladin's_apparition.lua @@ -33,6 +33,10 @@ monster.corpse = 111 monster.speed = 235 monster.manaCost = 0 +monster.events = { + "MirroredNightmareBossAccess", +} + monster.changeTarget = { interval = 4000, chance = 0, @@ -91,7 +95,7 @@ monster.loot = { { name = "stone skin amulet", chance = 1560 }, { id = 23542, chance = 1250 }, -- collar of blue plasma { id = 23529, chance = 1250 }, -- ring of blue plasma - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/monster/constructs/rotten_golem.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/rotten_golem.lua similarity index 97% rename from data-otservbr-global/monster/constructs/rotten_golem.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/rotten_golem.lua index edc6f909a70..f5d5808b05c 100644 --- a/data-otservbr-global/monster/constructs/rotten_golem.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/rotten_golem.lua @@ -26,6 +26,10 @@ monster.Bestiary = { Locations = "Rotten Wasteland.", } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 28000 monster.maxHealth = 28000 monster.race = "venom" @@ -91,7 +95,7 @@ monster.loot = { { name = "stone skin amulet", chance = 740 }, { name = "terra mantle", chance = 510 }, { name = "rubber cap", chance = 430 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/monster/undeads/sorcerer's_apparition.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/sorcerer's_apparition.lua similarity index 97% rename from data-otservbr-global/monster/undeads/sorcerer's_apparition.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/sorcerer's_apparition.lua index 8cf059be62c..e554c27dfd9 100644 --- a/data-otservbr-global/monster/undeads/sorcerer's_apparition.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/sorcerer's_apparition.lua @@ -33,6 +33,10 @@ monster.corpse = 6081 monster.speed = 235 monster.manaCost = 0 +monster.events = { + "MirroredNightmareBossAccess", +} + monster.changeTarget = { interval = 4000, chance = 0, @@ -92,7 +96,7 @@ monster.loot = { { name = "wand of starstorm", chance = 1310 }, { name = "stone skin amulet", chance = 1310 }, { name = "alloy legs", chance = 440 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/monster/elementals/turbulent_elemental.lua b/data-otservbr-global/monster/quests/soul_war/normal_monsters/turbulent_elemental.lua similarity index 97% rename from data-otservbr-global/monster/elementals/turbulent_elemental.lua rename to data-otservbr-global/monster/quests/soul_war/normal_monsters/turbulent_elemental.lua index 3bef5da44e3..6d7804f2207 100644 --- a/data-otservbr-global/monster/elementals/turbulent_elemental.lua +++ b/data-otservbr-global/monster/quests/soul_war/normal_monsters/turbulent_elemental.lua @@ -26,6 +26,10 @@ monster.Bestiary = { Locations = "Ebb and Flow.", } +monster.events = { + "FourthTaintBossesPrepareDeath", +} + monster.health = 28000 monster.maxHealth = 28000 monster.race = "blood" @@ -91,7 +95,7 @@ monster.loot = { { name = "crystalline armor", chance = 710 }, { name = "rubber cap", chance = 710 }, { name = "stone skin amulet", chance = 470 }, - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/monster/quests/soul_war/powerful_soul.lua b/data-otservbr-global/monster/quests/soul_war/powerful_soul.lua new file mode 100644 index 00000000000..0ece602eab5 --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/powerful_soul.lua @@ -0,0 +1,115 @@ +local mType = Game.createMonsterType("Powerful Soul") +local monster = {} + +monster.description = "a powerful soul" +monster.experience = 0 +monster.outfit = { + lookType = 568, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 30000 +monster.maxHealth = 30000 +monster.race = "undead" +monster.corpse = 0 +monster.speed = 80 +monster.manaCost = 0 + +monster.events = { + "GreedMonsterDeath", +} + +monster.changeTarget = { + interval = 1000, + chance = 0, +} + +monster.strategiesTarget = { + nearest = 100, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = false, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -2000, maxDamage = -3000 }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_LIFEDRAIN, minDamage = -2000, maxDamage = -3000, range = 1, effect = CONST_ME_MAGIC_RED, target = false }, +} + +monster.defenses = { + defense = 80, + armor = 90, + mitigation = 2, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +local transformTimeCount = 0 +mType.onThink = function(monster, interval) + transformTimeCount = transformTimeCount + interval + if transformTimeCount == 8000 then + CreateGoshnarsGreedMonster("Weak Soul", GreedMonsters[monster:getName()]) + monster:remove() + local boss = Creature("Goshnar's Greed") + if boss then + for elementType, reflectPercent in pairs(SoulWarReflectDamageMap) do + boss:addReflectElement(elementType, reflectPercent) + end + boss:addDefense(10) + boss:setMaxHealth(boss:getMaxHealth() + 10000) + boss:addHealth(10000) + end + transformTimeCount = 0 + end +end + +mType.onSpawn = function(monster) + transformTimeCount = 0 +end + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/soul_cage.lua b/data-otservbr-global/monster/quests/soul_war/soul_cage.lua new file mode 100644 index 00000000000..c6c65b32547 --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/soul_cage.lua @@ -0,0 +1,71 @@ +local mType = Game.createMonsterType("Soul Cage") +local monster = {} + +monster.description = "a soul cage" +monster.experience = 100000 +monster.outfit = { + lookType = 863, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 100000 +monster.maxHealth = 100000 +monster.race = "undead" +monster.corpse = 0 +monster.speed = 0 +monster.manaCost = 0 + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, +} + +monster.events = { + "SoulCageDeath", + "SoulCageHealthChange", +} + +monster.light = { + level = 0, + color = 0, +} + +monster.defenses = { + defense = 80, + armor = 100, + { name = "Heal Malice", interval = 2000, chance = 90, target = false }, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "invisible", condition = true }, +} + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/soul_sphere.lua b/data-otservbr-global/monster/quests/soul_war/soul_sphere.lua new file mode 100644 index 00000000000..160879ba9db --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/soul_sphere.lua @@ -0,0 +1,122 @@ +local mType = Game.createMonsterType("Soul Sphere") +local monster = {} + +monster.description = "a soul sphere" +monster.experience = 0 +monster.outfit = { + lookType = 979, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 10000 +monster.maxHealth = 10000 +monster.corpse = 0 +monster.speed = 0 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 0, +} + +monster.strategiesTarget = { + nearest = 100, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -500, maxDamage = -1000 }, +} + +monster.defenses = { + defense = 80, + armor = 90, + mitigation = 0.51, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 100 }, + { type = COMBAT_EARTHDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +local moveTimeCount = 0 +local stop = false +mType.onThink = function(monster, interval) + if stop then + return + end + + moveTimeCount = moveTimeCount + interval + if moveTimeCount >= 3000 then + local currentPos = monster:getPosition() + local newPos = Position(currentPos.x - 1, currentPos.y, currentPos.z) + + local nextTile = Tile(newPos) + if nextTile then + for _, creatureId in pairs(nextTile:getCreatures()) do + local tileMonster = Monster(creatureId) + if tileMonster and tileMonster:getName() == "Goshnar's Greed" then + tileMonster:setHealth(tileMonster:getMaxHealth()) + stop = true + return + end + end + end + + if not stop then + monster:teleportTo(newPos, true) + moveTimeCount = 0 + end + end +end + +mType.onSpawn = function(monster) + moveTimeCount = 0 + stop = false +end + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/soulsnatcher.lua b/data-otservbr-global/monster/quests/soul_war/soulsnatcher.lua new file mode 100644 index 00000000000..faa819dec6e --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/soulsnatcher.lua @@ -0,0 +1,115 @@ +local mType = Game.createMonsterType("Soulsnatcher") +local monster = {} + +monster.description = "a soulsnatcher" +monster.experience = 0 +monster.outfit = { + lookType = 1268, + lookHead = 0, + lookBody = 94, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 10000 +monster.maxHealth = 10000 +monster.race = "undead" +monster.corpse = 0 +monster.speed = 80 +monster.manaCost = 0 + +monster.events = { + "GreedMonsterDeath", +} + +monster.changeTarget = { + interval = 4000, + chance = 0, +} + +monster.strategiesTarget = { + nearest = 100, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = true, + canPushItems = true, + canPushCreatures = false, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -1000, maxDamage = -1500 }, + { name = "soulsnatcher-lifedrain-beam", interval = 2000, chance = 20, minDamage = -1000, maxDamage = -1500, target = false }, + { name = "soulsnatcher-lifedrain-missile", interval = 2000, chance = 25, minDamage = -1000, maxDamage = -1500, target = true }, + { name = "soulsnatcher-manadrain-ball", interval = 2000, chance = 30, minDamage = -500, maxDamage = -1000 }, +} + +monster.defenses = { + defense = 80, + armor = 90, + mitigation = 0.51, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +local transformTimeCount = 0 +mType.onThink = function(monster, interval) + transformTimeCount = transformTimeCount + interval + if transformTimeCount == 8000 then + transformTimeCount = 0 + local zone = Zone("boss.goshnar's-greed") + if zone then + local players = zone:getPlayers() + for _, player in ipairs(players) do + player:addHealth(-math.random(500, 1000)) + end + end + CreateGoshnarsGreedMonster(monster:getName(), GreedMonsters[monster:getName()]) + monster:remove() + end +end + +mType.onSpawn = function(monster) + transformTimeCount = 0 +end + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/spiteful_spitter.lua b/data-otservbr-global/monster/quests/soul_war/spiteful_spitter.lua index 8b8cec55ccf..d51b52e2e84 100644 --- a/data-otservbr-global/monster/quests/soul_war/spiteful_spitter.lua +++ b/data-otservbr-global/monster/quests/soul_war/spiteful_spitter.lua @@ -99,4 +99,8 @@ monster.immunities = { { type = "bleed", condition = false }, } +mType.onThink = function(monster, interval) + monster:tryTeleportToPlayer("You have been chosen for a harvest!") +end + mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/strong_soul.lua b/data-otservbr-global/monster/quests/soul_war/strong_soul.lua new file mode 100644 index 00000000000..ae3615b6eb4 --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/strong_soul.lua @@ -0,0 +1,106 @@ +local mType = Game.createMonsterType("Strong Soul") +local monster = {} + +monster.description = "a strong soul" +monster.experience = 0 +monster.outfit = { + lookType = 566, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 20000 +monster.maxHealth = 20000 +monster.race = "undead" +monster.corpse = 0 +monster.speed = 80 +monster.manaCost = 0 + +monster.events = { + "GreedMonsterDeath", +} + +monster.changeTarget = { + interval = 1000, + chance = 0, +} + +monster.strategiesTarget = { + nearest = 100, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = false, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -1000, maxDamage = -2000 }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_LIFEDRAIN, minDamage = -1000, maxDamage = -2000, range = 1, effect = CONST_ME_MAGIC_RED, target = false }, +} + +monster.defenses = { + defense = 80, + armor = 90, + mitigation = 2, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +local transformTimeCount = 0 +mType.onThink = function(monster, interval) + transformTimeCount = transformTimeCount + interval + if transformTimeCount == 8000 then + Game.createMonster("Powerful Soul", GreedMonsters[monster:getName()], true, false) + monster:remove() + transformTimeCount = 0 + end +end + +mType.onSpawn = function(monster) + transformTimeCount = 0 +end + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/weak_soul.lua b/data-otservbr-global/monster/quests/soul_war/weak_soul.lua new file mode 100644 index 00000000000..b7150efa00f --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/weak_soul.lua @@ -0,0 +1,106 @@ +local mType = Game.createMonsterType("Weak Soul") +local monster = {} + +monster.description = "a weak soul" +monster.experience = 0 +monster.outfit = { + lookType = 48, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 10000 +monster.maxHealth = 10000 +monster.race = "undead" +monster.corpse = 0 +monster.speed = 80 +monster.manaCost = 0 + +monster.events = { + "GreedMonsterDeath", +} + +monster.changeTarget = { + interval = 4000, + chance = 0, +} + +monster.strategiesTarget = { + nearest = 100, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = false, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -1000 }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_LIFEDRAIN, minDamage = -500, maxDamage = -1000, range = 1, effect = CONST_ME_MAGIC_RED, target = false }, +} + +monster.defenses = { + defense = 80, + armor = 90, + mitigation = 0.51, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +local transformTimeCount = 0 +mType.onThink = function(monster, interval) + transformTimeCount = transformTimeCount + interval + if transformTimeCount == 8000 then + Game.createMonster("Strong Soul", GreedMonsters[monster:getName()], true, false) + monster:remove() + transformTimeCount = 0 + end +end + +mType.onSpawn = function(monster) + transformTimeCount = 0 +end + +mType:register(monster) diff --git a/data-otservbr-global/monster/quests/soul_war/weeping_soul.lua b/data-otservbr-global/monster/quests/soul_war/weeping_soul.lua new file mode 100644 index 00000000000..3631fa3cb8e --- /dev/null +++ b/data-otservbr-global/monster/quests/soul_war/weeping_soul.lua @@ -0,0 +1,91 @@ +local mType = Game.createMonsterType("Weeping Soul") +local monster = {} + +monster.description = "a weeping soul" +monster.experience = 0 +monster.outfit = { + lookType = 48, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 7000 +monster.maxHealth = 7000 +monster.race = "undead" +monster.corpse = 33876 +monster.speed = 150 +monster.manaCost = 100 +monster.maxSummons = 0 + +monster.changeTarget = { + interval = 4000, + chance = 15, +} + +monster.strategiesTarget = { + nearest = 60, + health = 10, + damage = 10, + random = 20, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, + pet = false, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -220, maxDamage = -650 }, +} + +monster.defenses = { + defense = 55, + armor = 55, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 40 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otservbr-global/monster/undeads/hazardous_phantom.lua b/data-otservbr-global/monster/undeads/hazardous_phantom.lua index 29384eafe55..1cd1eb7d38c 100644 --- a/data-otservbr-global/monster/undeads/hazardous_phantom.lua +++ b/data-otservbr-global/monster/undeads/hazardous_phantom.lua @@ -20,6 +20,10 @@ monster.corpse = 34125 monster.speed = 100 monster.manaCost = 0 +monster.events = { + "HazardousPhantomDeath", +} + monster.changeTarget = { interval = 4000, chance = 0, @@ -78,7 +82,7 @@ monster.loot = { { id = 282, chance = 1570 }, -- giant shimmering pearl { name = "wand of everblazing", chance = 790 }, { id = 23542, chance = 790 }, -- collar of blue plasma - { id = 34109, chance = 20 }, -- bag you desire + { name = "bag you desire", chance = 15 }, } monster.attacks = { diff --git a/data-otservbr-global/npc/flickering_soul.lua b/data-otservbr-global/npc/flickering_soul.lua new file mode 100644 index 00000000000..2010198da6e --- /dev/null +++ b/data-otservbr-global/npc/flickering_soul.lua @@ -0,0 +1,203 @@ +local internalNpcName = "Flickering Soul" +local npcType = Game.createNpcType(internalNpcName) +local npcConfig = {} + +npcConfig.name = internalNpcName +npcConfig.description = internalNpcName + +npcConfig.health = 100 +npcConfig.maxHealth = npcConfig.health +npcConfig.walkInterval = 2000 +npcConfig.walkRadius = 2 + +npcConfig.outfit = { + lookType = 1219, + lookHead = 6, + lookBody = 26, + lookLegs = 26, + lookFeet = 6, + lookAddons = 0, + lookMount = 0, +} + +npcConfig.flags = { + floorchange = false, +} + +local keywordHandler = KeywordHandler:new() +local npcHandler = NpcHandler:new(keywordHandler) + +npcType.onThink = function(npc, interval) + npcHandler:onThink(npc, interval) +end + +npcType.onAppear = function(npc, player) + npcHandler:onAppear(npc, player) +end + +npcType.onDisappear = function(npc, player) + npcHandler:onDisappear(npc, player) +end + +npcType.onMove = function(npc, player, fromPosition, toPosition) + npcHandler:onMove(npc, player, fromPosition, toPosition) +end + +npcType.onSay = function(npc, player, type, message) + npcHandler:onSay(npc, player, type, message) +end + +npcType.onCloseChannel = function(npc, player) + npcHandler:onCloseChannel(npc, player) +end + +local function playerSayCallback(npc, player, type, message) + if not npcHandler:checkInteraction(npc, player) then + return false + end + + local soulWarQuest = player:soulWarQuestKV() + + local playerId = player:getId() + if MsgContains(message, "living") then + npcHandler:say("It has been a while since I roamed the world of the living in a mortal shell.", npc, player) + elseif MsgContains(message, "mortal") then + npcHandler:say("I had many names in my live. The one that would be the most recognizable is probably the name Goshnar. Even that was an assumed name that I took when I left my mundane past behind.", npc, player) + elseif MsgContains(message, "Goshnar") then + npcHandler:say({ + "I was once known as the necromant king. ...", + "For some it was meant as a curse, others used the name with reverence. To me it was just another stepping stone, in a life that burned with ambition.", + }, npc, player, 4000) + elseif MsgContains(message, "ambition") then + npcHandler:say({ + "My ambitions were high and knew no limits. Mastery over life and death was but a milestone that I wanted to accomplish. In the end I aspired probably somewhat like godhood. ...", + "Though in hindsight even that wouldn't have been enough. There was a hunger in me that nothing could put to rest.", + }, npc, player, 4000) + elseif MsgContains(message, "milestone") then + npcHandler:say("Everything in my life was just a tool to further my goals. The brotherhood of bones was just a tool for me. As was everyone or everything. In the path I had chosen nothing mattered but me and my ambitions.", npc, player) + elseif MsgContains(message, "everything") then + npcHandler:say("Necromancy was a passion at first, another tool while I amassed power and a crutch when my ambitions surpassed that what it could accomplish.", npc, player) + elseif MsgContains(message, "accomplish") then + npcHandler:say({ + "I was so convinced about my brilliance, my greatness, my destiny. And this hunger for more, it let me not have peace at any point in my life. I was always driven. There was no time to rest. ...", + "And there was no looking back. I never cared to remember my humble beginnings, what I had sacrificed to get where I was. All that I had left behind and that I had lost forever. ...", + "Now I see the bitter irony. I could bring back the dead, but I couldn't create second chances. I couldn't restore the truly important things that I had lost.", + }, npc, player, 4000) + elseif MsgContains(message, "dead") then + npcHandler:say("My demise did not meet me unprepared. As a powerful necromancer I had fettered my soul in the living world and the realms beyond. I had prepared for my return and was confident in my power.", npc, player) + elseif MsgContains(message, "confident") then + npcHandler:say("My soul wandered the plains of Zarganash, waiting for my wards to power up. Waiting for my soul to be slowly pulled back and manifest in the world of the living once again. What I had not taken into consideration was peace.", npc, player) + elseif MsgContains(message, "peace") then + npcHandler:say({ + "Zarganash was not a place without its dangers, but for a soul as powerful as mine, there was little threat at all. For the first time in my existence I had to stop running forward. I had to wait for things to fit into their places. ...", + "And me, who had seen things that horrible, they would have obliterated a lesser man's mind, finally took the time to look back. And what I saw was frightening in its own right. ...", + "A great tiredness overcame me. With the flames of my ambitions calming down for the first time since I could remember, all my aspirations and plans seemed to petty and futile. ...", + "Everything I had worked for and my plans for the things to come seemed pointless, and the things I had lost and never allowed myself to experience weighed heavily on my soul.", + }, npc, player, 4000) + elseif MsgContains(message, "soul") then + npcHandler:say({ + "I talked to other souls, lost in Zarganash, and most of them seemed like mirrors to myself. Their faults, their shortcomings, the things that were important to them and the things they had lost. ...", + "It was all like miniature copies of my own grand plans and losses. It made me think. And the great tiredness weighed even more heavy on me. A weariness of the world, of the hunger that drove me.", + }, npc, player, 4000) + elseif MsgContains(message, "weariness") then + npcHandler:say({ + "Then I met a wise soul. A teacher that did not lecture. I never was impressed by anything but my own accomplishments. But the inner balance and peace of this soul, it did impress me. A lot. ...", + "I, who fancied myself to have been the epitome of knowledge, learned things that were entirely new to me. But this knowledge wasn't about power. It was about me.", + }, npc, player, 4000) + elseif MsgContains(message, "knowledge") then + npcHandler:say("I recognized the extent of my folly and failure. I decided not to return to the world of the living.", npc, player) + elseif MsgContains(message, "return") then + npcHandler:say({ + "I decided to stay here, even pass on into the great beyond at some point. Yet I still feel the pull of my fetters. I can faintly hear those who think they are my followers, calling to me.", + "And I feel others, many others who crave my powers and try to bring me back for their own gain.", + }, npc, player, 4000) + elseif MsgContains(message, "fetters") then + npcHandler:say({ + "Over my time in Zarganash I split away the parts of me that my worldly fetters were bound to. Yet I had to recognize that they are still a part of me and I'm bound to them. ...", + "The fetters and the efforts to call me back are empowering them. I feel them growing in strength and gaining awareness on their own. ...", + "They are beginning to feed not only on the fetters and incarnations but also on me. As I grow weaker, they grow more powerful over time.", + }, npc, player, 4000) + elseif MsgContains(message, "powerful") then + npcHandler:say("The only way I can get rid of them is to disperse them, to 'kill' them so to say. But they are tainted parts of myself and even going near them might endanger my sanity and stability. So all I can do is to ask you to do this daunting task.", npc, player) + elseif MsgContains(message, "task") then + local soulWarQuest = player:soulWarQuestKV() + -- Checks if the boss has already been defeated + if soulWarQuest:get("goshnar's-megalomania-killed") then + npcHandler:say({ + "You did it! For the first time I can feel free from the pull of my past. Now I'm free at last. ...", + "I might stay a while and teach other souls about the inner peace, but will eventually pass on. Thank you so much, my hero. My eternal gratitude and blessings will be with you!", + }, npc, player, 2000) + npcHandler:setTopic(playerId, 2) + player:addOutfit("Revenant") + else + npcHandler:say("I'm aware I have no right to ask and I have little to offer as a payment, but I ask you nonetheless. Will you face my fettered vices and destroy them for me?", npc, player) + npcHandler:setTopic(playerId, 1) + end + elseif MsgContains(message, "yes") and npcHandler:getTopic(playerId) == 1 then + npcHandler:say("Thank you for accepting this burden.", npc, player) + soulWarQuest:set("teleport-access", true) + elseif MsgContains(message, "burden") then + npcHandler:say({ + "You will have to reach each of the negative parts of my personality that I split away. They are hidden deep in the depths of Zarganash and will have corrupted and twisted their surroundings into dangerous nightmares. ...", + "Even worse, you'll likely encounter minions of those who want to claim my soul as their prize for their own depraved reasons. You will have to destroy my shards to set me free.", + }, npc, player, 5000) + elseif MsgContains(message, "shards") then + npcHandler:say("You haven't killed Malice yet. You haven't killed Hatred yet. You haven't killed Spite yet. You haven't killed Cruelty yet. You haven't killed Greed yet.", npc, player) + elseif MsgContains(message, "hate") then + npcHandler:say({ + "I hated the world for its flaws and the reluctance of people to comply with my will. I was convinced I was destined for greatness and to change everything. Ordinary beings were far beneath me and my consideration. ...", + "All this opposition, all the wars were a nuisance on my way to greatness. I would have sacrificed the whole world to reach my goals.", + }, npc, player, 4000) + elseif MsgContains(message, "fermuba") then + npcHandler:say("My daughter was as ambitious as me, yet she lacked my intellect and my raw talent. She still was great and talented yet I sadly let her feel my disdain. One of the many errors that my way of hubris made me do.", npc, player) + elseif MsgContains(message, "ferumbras") then + npcHandler:say({ + "Even in the lands of the dead, this one caused a stir. The dead were whispering his name. It made me feel jealous and angry at first, but at some point, after much self-reflection, I could recognize my own faults in the stories about him.", + "It was almost like looking into a mirror for the first time. However, he lived way later than me, and I never met his soul here, so I can't tell more about him.", + }, npc, player, 4000) + elseif MsgContains(message, "grandson") then + npcHandler:say(" I'm not aware of the fate of my linage. Neither I'm able to relate to the mortal world in that way. Each of us is an individual, not bound by ties of blood or herritage.", npc, player) + elseif MsgContains(message, "pale worm") then + npcHandler:say("His avatar might be destroyed for now and it'd grip on Zarganash considerably weakened. Yet he burrowed to deep into the essence of this realm to be annihilated this easy.", npc, player) + elseif MsgContains(message, "necromant king") then + npcHandler:say({ + "They called me the necromant king, in an act of reverence, but to me it was always more of a slander. To limit my greatness to this insignificant aspect was an insult to my ego. But I let it slip for the greater good. ...", + "I felt it was beneath me to correct them and I went along.", + }, npc, player, 4000) + elseif MsgContains(message, "minions") or MsgContains(message, "followers") then + npcHandler:say("I despised my followers for their petty agendas and for their limited vision of my own goals and personality.", npc, player) + elseif MsgContains(message, "shards") then + local bossesYetToDefeat = {} + for bossName, _ in pairs(SoulWarQuest.miniBosses) do + if not soulWarQuest:get(bossName) then + table.insert(bossesYetToDefeat, bossName) + end + end + + local message + if #bossesYetToDefeat > 0 then + message = "You haven't killed " .. table.concat(bossesYetToDefeat, ", ") .. " yet." + else + message = "You have defeated all the Goshnar's Bosses. Your soul shines brighter with each victory." + end + npcHandler:say(message, npc, player) + elseif MsgContains(message, "taints") or MsgContains(message, "penalties") then + if player:getTaintLevel() ~= nil then + player:resetTaints(true) + npcHandler:say("I have cleansed you from the taints that you carried with you. You are now free from the burden that you should not have to bear.", npc, player) + return + end + + npcHandler:say("You are not tainted by the darkness of the world. You are pure and free from the burdens that others carry.", npc, player) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, playerSayCallback) + +npcHandler:setMessage(MESSAGE_GREET, "Be greeted, living soul!") + +npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) + +-- npcType registering the npcConfig table +npcType:register(npcConfig) diff --git a/data-otservbr-global/scripts/actions/bosses_levers/goshnar_cruelty.lua b/data-otservbr-global/scripts/actions/bosses_levers/goshnar_cruelty.lua deleted file mode 100644 index 2e01880589d..00000000000 --- a/data-otservbr-global/scripts/actions/bosses_levers/goshnar_cruelty.lua +++ /dev/null @@ -1,23 +0,0 @@ -local config = { - boss = { - name = "Goshnar's Cruelty", - position = Position(33856, 31866, 7), - }, - requiredLevel = 250, - playerPositions = { - { pos = Position(33854, 31854, 6), teleport = Position(33856, 31872, 7), effect = CONST_ME_TELEPORT }, - { pos = Position(33855, 31854, 6), teleport = Position(33856, 31872, 7), effect = CONST_ME_TELEPORT }, - { pos = Position(33856, 31854, 6), teleport = Position(33856, 31872, 7), effect = CONST_ME_TELEPORT }, - { pos = Position(33857, 31854, 6), teleport = Position(33856, 31872, 7), effect = CONST_ME_TELEPORT }, - { pos = Position(33858, 31854, 6), teleport = Position(33856, 31872, 7), effect = CONST_ME_TELEPORT }, - }, - specPos = { - from = Position(33847, 31858, 7), - to = Position(33864, 31874, 7), - }, - exit = Position(33621, 31427, 10), -} - -local lever = BossLever(config) -lever:position({ x = 33853, y = 31854, z = 6 }) -lever:register() diff --git a/data-otservbr-global/scripts/actions/bosses_levers/goshnar_greed.lua b/data-otservbr-global/scripts/actions/bosses_levers/goshnar_greed.lua deleted file mode 100644 index 522cde76845..00000000000 --- a/data-otservbr-global/scripts/actions/bosses_levers/goshnar_greed.lua +++ /dev/null @@ -1,23 +0,0 @@ -local config = { - boss = { - name = "Goshnar's Greed", - position = Position(33746, 31666, 14), - }, - requiredLevel = 250, - playerPositions = { - { pos = Position(33776, 31665, 14), teleport = Position(33747, 31671, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33777, 31665, 14), teleport = Position(33747, 31671, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33778, 31665, 14), teleport = Position(33747, 31671, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33779, 31665, 14), teleport = Position(33747, 31671, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33780, 31665, 14), teleport = Position(33747, 31671, 14), effect = CONST_ME_TELEPORT }, - }, - specPos = { - from = Position(33737, 31658, 14), - to = Position(33755, 31673, 14), - }, - exit = Position(33621, 31427, 10), -} - -local lever = BossLever(config) -lever:position({ x = 33775, y = 31665, z = 14 }) -lever:register() diff --git a/data-otservbr-global/scripts/actions/bosses_levers/goshnar_hatred.lua b/data-otservbr-global/scripts/actions/bosses_levers/goshnar_hatred.lua deleted file mode 100644 index 963404c8d85..00000000000 --- a/data-otservbr-global/scripts/actions/bosses_levers/goshnar_hatred.lua +++ /dev/null @@ -1,23 +0,0 @@ -local config = { - boss = { - name = "Goshnar's Hatred", - position = Position(33744, 31599, 14), - }, - requiredLevel = 250, - playerPositions = { - { pos = Position(33773, 31601, 14), teleport = Position(33743, 31604, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33774, 31601, 14), teleport = Position(33743, 31604, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33775, 31601, 14), teleport = Position(33743, 31604, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33776, 31601, 14), teleport = Position(33743, 31604, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33777, 31601, 14), teleport = Position(33743, 31604, 14), effect = CONST_ME_TELEPORT }, - }, - specPos = { - from = Position(33735, 31592, 14), - to = Position(33751, 31606, 14), - }, - exit = Position(33621, 31427, 10), -} - -local lever = BossLever(config) -lever:position({ x = 33772, y = 31601, z = 14 }) -lever:register() diff --git a/data-otservbr-global/scripts/actions/bosses_levers/goshnar_malice.lua b/data-otservbr-global/scripts/actions/bosses_levers/goshnar_malice.lua deleted file mode 100644 index 44d102ad598..00000000000 --- a/data-otservbr-global/scripts/actions/bosses_levers/goshnar_malice.lua +++ /dev/null @@ -1,23 +0,0 @@ -local config = { - boss = { - name = "Goshnar's Malice", - position = Position(33710, 31599, 14), - }, - requiredLevel = 250, - playerPositions = { - { pos = Position(33679, 31599, 14), teleport = Position(33710, 31605, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33680, 31599, 14), teleport = Position(33710, 31605, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33681, 31599, 14), teleport = Position(33710, 31605, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33682, 31599, 14), teleport = Position(33710, 31605, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33683, 31599, 14), teleport = Position(33710, 31605, 14), effect = CONST_ME_TELEPORT }, - }, - specPos = { - from = Position(33699, 31590, 14), - to = Position(33718, 31607, 14), - }, - exit = Position(33621, 31427, 10), -} - -local lever = BossLever(config) -lever:position({ x = 33678, y = 31599, z = 14 }) -lever:register() diff --git a/data-otservbr-global/scripts/actions/bosses_levers/goshnar_megalomania.lua b/data-otservbr-global/scripts/actions/bosses_levers/goshnar_megalomania.lua deleted file mode 100644 index 5896faa74c4..00000000000 --- a/data-otservbr-global/scripts/actions/bosses_levers/goshnar_megalomania.lua +++ /dev/null @@ -1,23 +0,0 @@ -local config = { - boss = { - name = "Goshnar's Megalomania", - position = Position(33710, 31634, 14), - }, - requiredLevel = 250, - playerPositions = { - { pos = Position(33676, 31634, 14), teleport = Position(33710, 31639, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33677, 31634, 14), teleport = Position(33710, 31639, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33678, 31634, 14), teleport = Position(33710, 31639, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33679, 31634, 14), teleport = Position(33710, 31639, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33680, 31634, 14), teleport = Position(33710, 31639, 14), effect = CONST_ME_TELEPORT }, - }, - specPos = { - from = Position(33701, 31626, 14), - to = Position(33719, 31642, 14), - }, - exit = Position(33621, 31427, 10), -} - -local lever = BossLever(config) -lever:position({ x = 33675, y = 31634, z = 14 }) -lever:register() diff --git a/data-otservbr-global/scripts/actions/bosses_levers/goshnar_spite.lua b/data-otservbr-global/scripts/actions/bosses_levers/goshnar_spite.lua deleted file mode 100644 index 0fd2396ae03..00000000000 --- a/data-otservbr-global/scripts/actions/bosses_levers/goshnar_spite.lua +++ /dev/null @@ -1,23 +0,0 @@ -local config = { - boss = { - name = "Goshnar's Spite", - position = Position(33743, 31632, 14), - }, - requiredLevel = 250, - playerPositions = { - { pos = Position(33774, 31634, 14), teleport = Position(33742, 31639, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33775, 31634, 14), teleport = Position(33742, 31639, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33776, 31634, 14), teleport = Position(33742, 31639, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33777, 31634, 14), teleport = Position(33742, 31639, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33778, 31634, 14), teleport = Position(33742, 31639, 14), effect = CONST_ME_TELEPORT }, - }, - specPos = { - from = Position(33734, 31624, 14), - to = Position(33751, 31640, 14), - }, - exit = Position(33621, 31427, 10), -} - -local lever = BossLever(config) -lever:position({ x = 33773, y = 31634, z = 14 }) -lever:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/action-reward_soul_war.lua b/data-otservbr-global/scripts/quests/soul_war/action-reward_soul_war.lua new file mode 100644 index 00000000000..fae3fc59794 --- /dev/null +++ b/data-otservbr-global/scripts/quests/soul_war/action-reward_soul_war.lua @@ -0,0 +1,60 @@ +local rewardSoulWar = Action() + +function rewardSoulWar.onUse(creature, item, fromPosition, target, toPosition, isHotkey) + local rewardItem = SoulWarQuest.finalRewards[math.random(1, #SoulWarQuest.finalRewards)] + local player = creature:getPlayer() + if not player then + return false + end + + local soulWarQuest = player:soulWarQuestKV() + if soulWarQuest:get("final-reward") then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have already received your reward.") + return true + end + + if not soulWarQuest:get("goshnar's-megalomania-killed") then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You need to defeat Goshnar's Megalomania to receive your reward.") + return true + end + + player:addItem(rewardItem.id, 1) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have found a " .. rewardItem.name .. ".") + soulWarQuest:set("final-reward", true) + return true +end + +rewardSoulWar:position({ x = 33620, y = 31400, z = 10 }) +rewardSoulWar:register() + +local phantasmalJadeMount = Action() + +function phantasmalJadeMount.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local soulWarQuest = player:soulWarQuestKV() + if soulWarQuest:get("panthasmal-jade-mount") then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You already have Phantasmal Jade mount!") + return true + end + + if table.contains({ 34072, 34073, 34074 }, item.itemid) then + if player:getItemCount(34072) >= 4 and player:getItemCount(34073) == 1 and player:getItemCount(34074) == 1 then + player:removeItem(34072, 4) + player:removeItem(34073, 1) + player:removeItem(34074, 1) + player:addMount(167) + player:addAchievement("You got Horse Power") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Congratulations! You won Phantasmal Jade mount.") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Congratulations! You won You got Horse Power achievement.") + player:getPosition():sendMagicEffect(CONST_ME_HOLYDAMAGE) + soulWarQuest:set("panthasmal-jade-mount", true) + else + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You don't have the necessary items!") + player:getPosition():sendMagicEffect(CONST_ME_POFF) + end + end + + return true +end + +phantasmalJadeMount:id(34072, 34073, 34074) +phantasmalJadeMount:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/actions_bosses_killed.lua b/data-otservbr-global/scripts/quests/soul_war/actions_bosses_killed.lua deleted file mode 100644 index de4b8ae182f..00000000000 --- a/data-otservbr-global/scripts/quests/soul_war/actions_bosses_killed.lua +++ /dev/null @@ -1,24 +0,0 @@ -local bosses = { - ["goshnar's malice"] = { storage = Storage.Quest.U12_40.SoulWar.GoshnarMaliceKilled }, - ["goshnar's hatred"] = { storage = Storage.Quest.U12_40.SoulWar.GoshnarHatredKilled }, - ["goshnar's spite"] = { storage = Storage.Quest.U12_40.SoulWar.GoshnarSpiteKilled }, - ["goshnar's cruelty"] = { storage = Storage.Quest.U12_40.SoulWar.GoshnarCrueltyKilled }, - ["goshnar's greed"] = { storage = Storage.Quest.U12_40.SoulWar.GoshnarGreedKilled }, - ["goshnar's megalomania"] = { storage = Storage.Quest.U12_40.SoulWar.GoshnarMegalomaniaKilled }, -} - -local bossesSoulWar = CreatureEvent("SoulwarsBossDeath") -function bossesSoulWar.onDeath(creature) - local bossConfig = bosses[creature:getName():lower()] - if not bossConfig then - return true - end - onDeathForDamagingPlayers(creature, function(creature, player) - if bossConfig.storage then - player:setStorageValue(bossConfig.storage, 1) - end - end) - return true -end - -bossesSoulWar:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/actions_portal_megalomania.lua b/data-otservbr-global/scripts/quests/soul_war/actions_portal_megalomania.lua deleted file mode 100644 index 41d3763c1c7..00000000000 --- a/data-otservbr-global/scripts/quests/soul_war/actions_portal_megalomania.lua +++ /dev/null @@ -1,38 +0,0 @@ -local storagesTable = { - { storage = Storage.Quest.U12_40.SoulWar.GoshnarMaliceKilled, bossName = "Goshnar's Malice" }, - { storage = Storage.Quest.U12_40.SoulWar.GoshnarHatredKilled, bossName = "Goshnar's Hatred" }, - { storage = Storage.Quest.U12_40.SoulWar.GoshnarSpiteKilled, bossName = "Goshnar's Spite" }, - { storage = Storage.Quest.U12_40.SoulWar.GoshnarCrueltyKilled, bossName = "Goshnar's Cruelty" }, - { storage = Storage.Quest.U12_40.SoulWar.GoshnarGreedKilled, bossName = "Goshnar's Greed" }, -} - -local portalMegalomania = MoveEvent() -function portalMegalomania.onStepIn(creature, item, position, fromPosition) - local player = creature:getPlayer() - if not player then - return false - end - if player:getLevel() < 250 then - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You need at least level 250 to enter.") - player:teleportTo(fromPosition, true) - player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) - return false - end - local text = "" - for value in pairs(storagesTable) do - if player:getStorageValue(storagesTable[value].storage) < 0 then - text = text .. "\n" .. storagesTable[value].bossName - end - end - if text == "" then - return true - else - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You still need to defeat:" .. text) - player:teleportTo(fromPosition, true) - return false - end -end - -portalMegalomania:type("stepin") -portalMegalomania:position({ x = 33611, y = 31430, z = 10 }) -portalMegalomania:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/actions_reward_soul_war.lua b/data-otservbr-global/scripts/quests/soul_war/actions_reward_soul_war.lua deleted file mode 100644 index 1e3c9c773e0..00000000000 --- a/data-otservbr-global/scripts/quests/soul_war/actions_reward_soul_war.lua +++ /dev/null @@ -1,89 +0,0 @@ -local rewards = { - { id = 34082, name = "Soulcutter" }, - { id = 34083, name = "Soulshredder" }, - { id = 34084, name = "Soulbiter" }, - { id = 34085, name = "Souleater" }, - { id = 34086, name = "Soulcrusher" }, - { id = 34087, name = "Soulmaimer" }, - { id = 34088, name = "Soulbleeder" }, - { id = 34089, name = "Soulpiercer" }, - { id = 34090, name = "Soultainter" }, - { id = 34091, name = "Soulhexer" }, - { id = 34092, name = "Soulshanks" }, - { id = 34093, name = "Soulstrider" }, - { id = 34094, name = "Soulshell" }, - { id = 34095, name = "Soulmantel" }, - { id = 34096, name = "Soulshroud" }, - { id = 34097, name = "Pair of Soulwalkers" }, - { id = 34098, name = "Pair of Soulstalkers" }, - { id = 34099, name = "Soulbastion" }, -} -local outfits = { 1322, 1323 } - -local function addOutfits(player) - if player:getStorageValue(Storage.Quest.U12_40.SoulWar.OutfitReward) < 0 then - player:addOutfit(outfits[1], 0) - player:addOutfit(outfits[2], 0) - player:setStorageValue(Storage.Quest.U12_40.SoulWar.OutfitReward, 1) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Congratulations you received the Revenant Outfit.") - end -end - -local rewardSoulWar = Action() -function rewardSoulWar.onUse(creature, item, fromPosition, target, toPosition, isHotkey) - local randId = math.random(1, #rewards) - local rewardItem = rewards[randId] - local player = creature:getPlayer() - if not player then - return false - end - if player:getStorageValue(Storage.Quest.U12_40.SoulWar.QuestReward) < 0 then - player:addItem(rewardItem.id, 1) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have found a " .. rewardItem.name .. ".") - player:setStorageValue(Storage.Quest.U12_40.SoulWar.QuestReward, 1) - addOutfits(player) - return true - else - addOutfits(player) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have already collected your reward") - return false - end -end - -rewardSoulWar:position({ x = 33620, y = 31400, z = 10 }) -rewardSoulWar:register() - ------------------------------ --- Phantasmal Jade Mount function - -local phantasmalJadeMount = Action() -function phantasmalJadeMount.onUse(player, item, fromPosition, target, toPosition, isHotkey) - local storage = Storage.Quest.U12_40.SoulWar.MountReward - if player:getStorageValue(storage) == 1 then - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You already have Phantasmal Jade mount!") - return false - end - - if table.contains({ 34072, 34073, 34074 }, item.itemid) then - -- check items - if player:getItemCount(34072) >= 4 and player:getItemCount(34073) == 1 and player:getItemCount(34074) == 1 then - player:removeItem(34072, 4) - player:removeItem(34073, 1) - player:removeItem(34074, 1) - player:addMount(167) - player:setStorageValue(storage, 1) - player:addAchievement("You got Horse Power") - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Congratulations! You won Phantasmal Jade mount.") - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Congratulations! You won You got Horse Power achievement.") - player:getPosition():sendMagicEffect(CONST_ME_HOLYDAMAGE) - return true - else - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You don't have the necessary items!") - player:getPosition():sendMagicEffect(CONST_ME_POFF) - return false - end - end -end - -phantasmalJadeMount:id(34072, 34073, 34074) -phantasmalJadeMount:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/actions_soulwar_entrances.lua b/data-otservbr-global/scripts/quests/soul_war/actions_soulwar_entrances.lua deleted file mode 100644 index 51f8281a04d..00000000000 --- a/data-otservbr-global/scripts/quests/soul_war/actions_soulwar_entrances.lua +++ /dev/null @@ -1,63 +0,0 @@ -local config = { - { position = { x = 33615, y = 31422, z = 10 }, destination = { x = 34009, y = 31014, z = 9 } }, -- hunt infernal demon - { position = { x = 33618, y = 31422, z = 10 }, destination = { x = 33972, y = 31041, z = 11 } }, -- hunt rotten - { position = { x = 33621, y = 31422, z = 10 }, destination = { x = 33894, y = 31019, z = 8 } }, -- hunt bony sea devil - { position = { x = 33624, y = 31422, z = 10 }, destination = { x = 33858, y = 31831, z = 3 } }, -- hunt cloak - { position = { x = 33627, y = 31422, z = 10 }, destination = { x = 33887, y = 31188, z = 10 } }, -- hunt many faces - { position = { x = 33950, y = 31109, z = 8 }, destination = { x = 33780, y = 31634, z = 14 } }, -- goshnar's spite entrance - { position = { x = 33937, y = 31217, z = 11 }, destination = { x = 33782, y = 31665, z = 14 } }, -- goshnar's greed entrance - { position = { x = 34022, y = 31091, z = 11 }, destination = { x = 33685, y = 31599, z = 14 } }, -- goshnar's malice entrance - { position = { x = 33856, y = 31884, z = 5 }, destination = { x = 33857, y = 31865, z = 6 } }, -- goshnar's cruelty entrance - { position = { x = 33889, y = 31873, z = 3 }, destination = { x = 33830, y = 31881, z = 4 } }, -- 1st to 2nd floor cloak - { position = { x = 33829, y = 31880, z = 4 }, destination = { x = 33856, y = 31889, z = 5 } }, -- 2nd to 3rd floor cloak -} - -local portal = { position = { x = 33914, y = 31032, z = 12 }, destination = { x = 33780, y = 31601, z = 14 } } -- goshnar's hatred entrance - -local soulWarEntrances = MoveEvent() -function soulWarEntrances.onStepIn(creature, item, position, fromPosition) - local player = creature:getPlayer() - if not player then - return false - end - if player:getLevel() < 250 then - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You need at least level 250 to enter.") - player:teleportTo(fromPosition, true) - player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) - return false - end - for value in pairs(config) do - if Position(config[value].position) == player:getPosition() then - player:teleportTo(Position(config[value].destination)) - player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) - return true - end - end -end - -soulWarEntrances:type("stepin") -for value in pairs(config) do - soulWarEntrances:position(config[value].position) -end -soulWarEntrances:register() - -local portalHatred = Action() -function portalHatred.onUse(creature, item, position, fromPosition) - local player = creature:getPlayer() - if not player then - return false - end - if player:getLevel() < 250 then - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You need at least level 250 to enter.") - player:teleportTo(fromPosition, true) - player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) - return false - end - doSendMagicEffect(item:getPosition(), CONST_ME_TELEPORT) - player:teleportTo(Position(portal.destination)) - player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) - return true -end - -portalHatred:position(portal.position) -portalHatred:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/eventcallback_on_combat_taint.lua b/data-otservbr-global/scripts/quests/soul_war/eventcallback_on_combat_taint.lua new file mode 100644 index 00000000000..3e5ec34f930 --- /dev/null +++ b/data-otservbr-global/scripts/quests/soul_war/eventcallback_on_combat_taint.lua @@ -0,0 +1,126 @@ +local taintCooldown = {} + +local function createTeleportEffect(position) + position:sendMagicEffect(CONST_ME_TELEPORT) +end + +local function scheduleMonsterCreation(player, monster, monsterName, spawnPosition) + addEvent(createTeleportEffect, 1000, spawnPosition) + addEvent(createTeleportEffect, 2000, spawnPosition) + addEvent(createTeleportEffect, 3000, spawnPosition) + + addEvent(function(playerId, monsterId) + local eventPlayer = Player(playerId) + if not eventPlayer then + return + end + + local eventMonster = Monster(monsterId) + if not eventMonster or eventMonster:isDead() then + return + end + + -- Only create if the player not have cooldown + if not taintCooldown[playerId] or os.time() > taintCooldown[playerId] then + taintCooldown[playerId] = os.time() + 30 + local monster = Game.createMonster(monsterName, spawnPosition, true, true) + if monster then + spawnPosition:sendMagicEffect(CONST_ME_TELEPORT) + logger.debug("Spamming monster with name {} to player {}", monsterName, eventPlayer:getName()) + end + end + end, 4000, player:getId(), monster:getId()) +end + +local function onPlayerAttackMonster(player, target) + local monster = target:getMonster() + if not monster then + return + end + + -- It will only execute if the player has the second taint + if player:getTaintNameByNumber(2) ~= nil then + local chance = math.random(1, 200) + local spawnPosition = player:getPosition() + if chance == 1 then -- 0.5% chance + local foundMonsterName = player:getSoulWarZoneMonster() + if foundMonsterName ~= nil then + scheduleMonsterCreation(player, monster, foundMonsterName, spawnPosition) + end + end + end +end + +local function onMonsterAttackPlayer(target, primaryValue, secondaryValue) + local targetPlayer = target:getPlayer() + if not targetPlayer then + return primaryValue, secondaryValue + end + + if targetPlayer:getTaintNameByNumber(3) ~= nil then + local monsterZone = targetPlayer:getSoulWarZoneMonster() + if monsterZone ~= nil then + logger.debug("Player {} have third taint, primary value {}, secondary {}", targetPlayer:getName(), primaryValue, secondaryValue) + primaryValue = primaryValue + math.ceil(primaryValue * 0.15) + secondaryValue = secondaryValue + math.ceil(secondaryValue * 0.15) + logger.debug("Primary value after {}, secondary {}", primaryValue, secondaryValue) + end + end + + return primaryValue, secondaryValue +end + +local callback = EventCallback("CreatureOnCombatTaint") + +function callback.creatureOnCombat(caster, target, primaryValue, primaryType, secondaryValue, secondaryType, origin) + if not caster or not target then + return primaryValue, primaryType, secondaryValue, secondaryType + end + + -- Second taint + local attackerPlayer = caster:getPlayer() + if attackerPlayer and target:isMonster() then + onPlayerAttackMonster(attackerPlayer, target) + end + + -- Third taint + if caster:getMonster() then + primaryValue, secondaryValue = onMonsterAttackPlayer(target, primaryValue, secondaryValue) + end + + return primaryValue, primaryType, secondaryValue, secondaryType +end + +callback:register() + +callback = EventCallback("PlayerOnThinkTaint") + +local accumulatedTime = {} + +function callback.playerOnThink(player, interval) + if not player then + return + end + + local playerId = player:getId() + if not accumulatedTime[playerId] then + accumulatedTime[playerId] = 0 + end + + accumulatedTime[playerId] = accumulatedTime[playerId] + interval + + if accumulatedTime[playerId] >= 10000 then + local soulWarQuest = player:soulWarQuestKV() + if player:getSoulWarZoneMonster() ~= nil and player:getTaintNameByNumber(5) ~= nil then + local hpLoss = math.ceil(player:getHealth() * 0.1) + local manaLoss = math.ceil(player:getMana() * 0.1) + player:addHealth(-hpLoss) + player:addMana(-manaLoss) + logger.debug("Fifth taint removing '{}' mana and '{}' health from player {}", manaLoss, hpLoss, player:getName()) + end + + accumulatedTime[playerId] = 0 + end +end + +callback:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/globalevent-ebb_and_flow_change_maps.lua b/data-otservbr-global/scripts/quests/soul_war/globalevent-ebb_and_flow_change_maps.lua new file mode 100644 index 00000000000..d13b5cf2cd6 --- /dev/null +++ b/data-otservbr-global/scripts/quests/soul_war/globalevent-ebb_and_flow_change_maps.lua @@ -0,0 +1,135 @@ +local function updateWaterPoolsSize() + for _, pos in ipairs(SoulWarQuest.ebbAndFlow.poolPositions) do + local tile = Tile(pos) + if tile then + local item = tile:getItemById(SoulWarQuest.ebbAndFlow.smallPoolId) + if item then + item:transform(SoulWarQuest.ebbAndFlow.MediumPoolId) + -- Starts another timer for filling after an additional 40 seconds + addEvent(function() + local item = tile:getItemById(SoulWarQuest.ebbAndFlow.MediumPoolId) + if item then + item:transform(SoulWarQuest.ebbAndFlow.smallPoolId) + end + end, 40000) -- 40 seconds + end + end + end +end + +local function loadMapEmpty() + if SoulWarQuest.ebbAndFlow.getZone():countPlayers() > 0 then + local players = SoulWarQuest.ebbAndFlow.getZone():getPlayers() + for _, player in ipairs(players) do + if player:getPosition().z == 8 then + if player:isInBoatSpot() then + local teleportPosition = player:getPosition() + teleportPosition.z = 9 + player:teleportTo(teleportPosition) + logger.trace("Teleporting player to down.") + end + player:sendCreatureAppear() + end + end + end + + Game.loadMap(SoulWarQuest.ebbAndFlow.mapsPath.empty) + SoulWarQuest.ebbAndFlow.setLoadedEmptyMap(true) + SoulWarQuest.ebbAndFlow.setActive(false) + + local updatePlayers = EventCallback("UpdatePlayersEmptyEbbFlowMap") + function updatePlayers.mapOnLoad(mapPath) + if mapPath ~= SoulWarQuest.ebbAndFlow.mapsPath.empty then + return + end + + SoulWarQuest.ebbAndFlow.updateZonePlayers() + end + + updatePlayers:register() + + addEvent(function() + -- Change the appearance of puddles to indicate the next filling + updateWaterPoolsSize() + end, 80000) -- 80 seconds +end + +local function getDistance(pos1, pos2) + return math.sqrt((pos1.x - pos2.x) ^ 2 + (pos1.y - pos2.y) ^ 2 + (pos1.z - pos2.z) ^ 2) +end + +local function findNearestRoomPosition(playerPosition) + local nearestPosition = nil + local smallestDistance = nil + for _, room in ipairs(SoulWarQuest.ebbAndFlow.centerRoomPositions) do + local distance = getDistance(playerPosition, room.conor) + if not smallestDistance or distance < smallestDistance then + smallestDistance = distance + nearestPosition = room.teleportPosition + end + end + return nearestPosition +end + +local function loadMapInundate() + if SoulWarQuest.ebbAndFlow.getZone():countPlayers() > 0 then + local players = SoulWarQuest.ebbAndFlow.getZone():getPlayers() + for _, player in ipairs(players) do + local playerPosition = player:getPosition() + if playerPosition.z == 9 then + if player:isInBoatSpot() then + local nearestCenterPosition = findNearestRoomPosition(playerPosition) + player:teleportTo(nearestCenterPosition) + logger.trace("Teleporting player to the near center position room and updating tile.") + else + player:teleportTo(SoulWarQuest.ebbAndFlow.waitPosition) + logger.trace("Teleporting player to wait position and updating tile.") + end + playerPosition:sendMagicEffect(CONST_ME_TELEPORT) + end + player:sendCreatureAppear() + end + end + + Game.loadMap(SoulWarQuest.ebbAndFlow.mapsPath.inundate) + SoulWarQuest.ebbAndFlow.setLoadedEmptyMap(false) + SoulWarQuest.ebbAndFlow.setActive(true) + + local updatePlayers = EventCallback("UpdatePlayersInundateEbbFlowMap") + function updatePlayers.mapOnLoad(mapPath) + if mapPath ~= SoulWarQuest.ebbAndFlow.mapsPath.inundate then + return + end + + SoulWarQuest.ebbAndFlow.updateZonePlayers() + end + + updatePlayers:register() +end + +local loadEmptyMap = GlobalEvent("SoulWarQuest.ebbAndFlow") + +function loadEmptyMap.onStartup() + Game.loadMap(SoulWarQuest.ebbAndFlow.mapsPath.ebbFlow) + loadMapEmpty() + SoulWarQuest.ebbAndFlow.updateZonePlayers() +end + +loadEmptyMap:register() + +local eddAndFlowInundate = GlobalEvent("eddAndFlowInundate") + +function eddAndFlowInundate.onThink(interval, lastExecution) + if SoulWarQuest.ebbAndFlow.isLoadedEmptyMap() then + logger.trace("Map change to empty in {} minutes.", SoulWarQuest.ebbAndFlow.intervalChangeMap) + loadMapInundate() + elseif SoulWarQuest.ebbAndFlow.isActive() then + logger.trace("Map change to inundate in {} minutes.", SoulWarQuest.ebbAndFlow.intervalChangeMap) + loadMapEmpty() + end + + return true +end + +eddAndFlowInundate:interval(SoulWarQuest.ebbAndFlow.intervalChangeMap * 60 * 1000) +eddAndFlowInundate:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/moveevent-claustrophobic-inferno-raid.lua b/data-otservbr-global/scripts/quests/soul_war/moveevent-claustrophobic-inferno-raid.lua new file mode 100644 index 00000000000..0502d53e365 --- /dev/null +++ b/data-otservbr-global/scripts/quests/soul_war/moveevent-claustrophobic-inferno-raid.lua @@ -0,0 +1,61 @@ +local firstRaid = MoveEvent() +local secondRaid = MoveEvent() +local thirdRaid = MoveEvent() + +local spawnMonsterName = "Brachiodemon" + +-- Registering encounters, stages and move events +for raidNumber, raid in ipairs(SoulWarQuest.claustrophobicInfernoRaids) do + -- Registering encounter + local raidName = string.format("Claustrophobic Inferno Raid %d", raidNumber) + local encounter = Encounter(raidName, { + zone = raid.getZone(), + timeToSpawnMonsters = "3s", + }) + + local spawnTimes = SoulWarQuest.claustrophobicInfernoRaids.suriviveTime / SoulWarQuest.claustrophobicInfernoRaids.spawnTime + + -- Registering encounter stages + for i = 1, spawnTimes do + encounter + :addSpawnMonsters({ + { + name = spawnMonsterName, + positions = raid.spawns, + }, + }) + :autoAdvance(SoulWarQuest.claustrophobicInfernoRaids.spawnTime * 1000) + end + + function encounter:onReset(position) + encounter:removeMonsters() + addEvent(function(zone) + zone:refresh() + zone:removePlayers() + end, SoulWarQuest.claustrophobicInfernoRaids.timeToKick * 1000, raid.getZone()) + logger.debug("{} has ended", raidName) + end + + encounter:register() + + -- Registering move event + local raidMoveEvent = MoveEvent() + + function raidMoveEvent.onStepIn(creature, item, position, fromPosition) + if not creature:getPlayer() then + return true + end + if fromPosition.y == position.y - (raidNumber % 2 ~= 0 and -1 or 1) then -- if player comes from the raid zone don't start the raid + return + end + logger.debug("{} has started", raidName) + encounter:start() + return true + end + + for _, pos in pairs(raid.sandTimerPositions) do + raidMoveEvent:position(pos) + end + + raidMoveEvent:register() +end diff --git a/data-otservbr-global/scripts/quests/soul_war/moveevent-soul_war_entrances.lua b/data-otservbr-global/scripts/quests/soul_war/moveevent-soul_war_entrances.lua new file mode 100644 index 00000000000..0d2bd4fffec --- /dev/null +++ b/data-otservbr-global/scripts/quests/soul_war/moveevent-soul_war_entrances.lua @@ -0,0 +1,147 @@ +local positionsTable = { + -- Hunts + [Position(33615, 31422, 10)] = Position(34009, 31014, 9), -- hunt infernal demon + [Position(33618, 31422, 10)] = Position(33972, 31041, 11), -- hunt rotten + [Position(33621, 31422, 10)] = Position(33894, 31019, 8), -- hunt bony sea devil + [Position(33624, 31422, 10)] = Position(33858, 31831, 3), -- hunt cloak + [Position(33627, 31422, 10)] = Position(33887, 31188, 10), -- hunt many faces +} + +local soul_war_entrances = MoveEvent() + +function soul_war_entrances.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player then + return + end + + if player:getLevel() < 250 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You need level 250 to enter here.") + player:teleportTo(fromPosition, true) + player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + return + end + + -- Check if player has access to teleport from Flickering Soul npc: "hi/task/yes" + local soulWarQuest = player:soulWarQuestKV() + if not soulWarQuest:get("teleport-access") then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Your soul does not yet resonate with the frequency required to enter here.") + player:teleportTo(fromPosition, true) + player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + return + end + + for position, destination in pairs(positionsTable) do + if position == player:getPosition() then + fromPosition:sendMagicEffect(CONST_ME_TELEPORT) + player:teleportTo(destination) + player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + break + end + end + + return true +end + +for key, value in pairs(positionsTable) do + soul_war_entrances:position(key) +end + +soul_war_entrances:register() + +local soul_war_megalomania_entrance = MoveEvent() + +function soul_war_megalomania_entrance.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player then + return false + end + + local soulWarQuest = player:soulWarQuestKV() + if player:getLevel() < 250 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You are not allowed to enter here.") + player:teleportTo(fromPosition, true) + player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + return false + end + + local text = "" + local soulWarCount = 0 + for bossName, completed in pairs(SoulWarQuest.miniBosses) do + if soulWarQuest:get(bossName) == completed then + soulWarCount = soulWarCount + 1 + else + text = text .. "\n" .. bossName + end + end + + if soulWarCount < 5 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You still need to defeat:" .. text) + player:teleportTo(fromPosition, true) + return false + end + + return true +end + +soul_war_megalomania_entrance:position({ x = 33611, y = 31430, z = 10 }) +soul_war_megalomania_entrance:register() + +local claustrophobicInfernoTeleportPositions = { + [Position(34022, 31091, 11)] = Position(33685, 31599, 14), +} + +local claustrophobicInfernoTeleports = MoveEvent() + +function claustrophobicInfernoTeleports.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player then + return false + end + + local soulWarQuest = player:soulWarQuestKV() + for tablePosition, toPosition in pairs(claustrophobicInfernoTeleportPositions) do + if tablePosition == position then + player:teleportTo(toPosition) + toPosition:sendMagicEffect(CONST_ME_TELEPORT) + break + end + end + + return true +end + +for key, value in pairs(claustrophobicInfernoTeleportPositions) do + claustrophobicInfernoTeleports:position(key) +end + +claustrophobicInfernoTeleports:register() + +local goshnarSpiteEntrance = MoveEvent() + +function goshnarSpiteEntrance.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player then + return false + end + + local soulWarQuest = player:soulWarQuestKV() + local killCount = soulWarQuest:get("hazardous-phantom-death") or 0 + if killCount < 20 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have killed " .. killCount .. " and need to kill 20 Hazardous Phantoms") + player:teleportTo(fromPosition, true) + player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + return false + end + + if position == SoulWarQuest.goshnarSpiteEntrancePosition.fromPos then + player:teleportTo(SoulWarQuest.goshnarSpiteEntrancePosition.toPos) + player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + return true + end + + return false +end + +goshnarSpiteEntrance:position(SoulWarQuest.goshnarSpiteEntrancePosition.fromPos) +goshnarSpiteEntrance:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/actions_portal_reward_soulwar.lua b/data-otservbr-global/scripts/quests/soul_war/moveevent-teleport_entrance_reward.lua similarity index 78% rename from data-otservbr-global/scripts/quests/soul_war/actions_portal_reward_soulwar.lua rename to data-otservbr-global/scripts/quests/soul_war/moveevent-teleport_entrance_reward.lua index cf999f89c7b..7aaff112056 100644 --- a/data-otservbr-global/scripts/quests/soul_war/actions_portal_reward_soulwar.lua +++ b/data-otservbr-global/scripts/quests/soul_war/moveevent-teleport_entrance_reward.lua @@ -1,10 +1,14 @@ local portalReward = MoveEvent() + function portalReward.onStepIn(creature, item, position, fromPosition) local player = creature:getPlayer() if not player then return false end - if player:getStorageValue(Storage.Quest.U12_40.SoulWar.GoshnarMegalomaniaKilled) < 1 then + + local soulWarQuest = player:soulWarQuestKV() + -- Checks if the boss has already been defeated + if not soulWarQuest:get("goshnar's-megalomania-killed") then player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Only warriors who defeated Goshnar's Megalomania can access this area.") player:teleportTo(fromPosition, true) return false @@ -15,6 +19,5 @@ function portalReward.onStepIn(creature, item, position, fromPosition) return true end -portalReward:type("stepin") portalReward:position({ x = 33621, y = 31416, z = 10 }) portalReward:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/soul_war_mechanics.lua b/data-otservbr-global/scripts/quests/soul_war/soul_war_mechanics.lua new file mode 100644 index 00000000000..6a60195277a --- /dev/null +++ b/data-otservbr-global/scripts/quests/soul_war/soul_war_mechanics.lua @@ -0,0 +1,1081 @@ +local login = CreatureEvent("SoulWarLogin") + +function login.onLogin(player) + player:registerEvent("GoshnarsHatredBuff") + player:resetTaints() + player:resetGoshnarSymbolTormentCounter() + return true +end + +login:register() + +-- Goshnar's Malice reflection (100%) of physical and death damage +local goshnarsMaliceReflection = CreatureEvent("Goshnar's-Malice") + +function goshnarsMaliceReflection.onHealthChange(creature, attacker, primaryDamage, primaryType, secondaryDamage, secondaryType, origin) + if not attacker then + return primaryDamage, primaryType, secondaryDamage, secondaryType + end + + local player = attacker:getPlayer() + if player then + if primaryDamage > 0 and (primaryType == COMBAT_PHYSICALDAMAGE or primaryType == COMBAT_DEATHDAMAGE) then + player:addHealth(-primaryDamage) + end + if secondaryDamage > 0 and (secondaryType == COMBAT_PHYSICALDAMAGE or secondaryType == COMBAT_DEATHDAMAGE) then + player:addHealth(-secondaryDamage) + end + end + + return primaryDamage, primaryType, secondaryDamage, secondaryType +end + +goshnarsMaliceReflection:register() + +local soulCageReflection = CreatureEvent("SoulCageHealthChange") + +function soulCageReflection.onHealthChange(creature, attacker, primaryDamage, primaryType, secondaryDamage, secondaryType, origin) + local player = attacker:getPlayer() + if player then + if primaryDamage > 0 then + player:addHealth(-primaryDamage * 0.1) + end + if secondaryDamage > 0 then + player:addHealth(-secondaryDamage * 0.1) + end + end + + return primaryDamage, primaryType, secondaryDamage, secondaryType +end + +soulCageReflection:register() + +local soulCageDeath = CreatureEvent("SoulCageDeath") + +function soulCageDeath.onDeath(creature, corpse, killer, mostDamageKiller, lastHitUnjustified, mostDamageUnjustified) + if not creature or creature:isPlayer() or creature:getMaster() then + return true + end + + addEvent(SpawnSoulCage, 23000) +end + +soulCageDeath:register() + +local fourthTaintBossesDeath = CreatureEvent("FourthTaintBossesPrepareDeath") + +function fourthTaintBossesDeath.onPrepareDeath(creature, killer, realDamage) + if not creature or not killer:getPlayer() then + return true + end + + if creature:getHealth() - realDamage < 1 then + if killer:getTaintNameByNumber(4) then + local isInZone = killer:getSoulWarZoneMonster() + if isInZone ~= nil then + -- 10% of chance to heal + if math.random(1, 10) == 1 then + creature:say("Health restored by the mystic powers of Zarganash!") + creature:addHealth(creature:getMaxHealth()) + end + end + end + end + return true +end + +fourthTaintBossesDeath:register() + +local bossesDeath = CreatureEvent("SoulWarBossesDeath") + +function bossesDeath.onDeath(creature, corpse, killer, mostDamageKiller, lastHitUnjustified, mostDamageUnjustified) + local bossName = creature:getName() + if SoulWarQuest.miniBosses[bossName] then + local killers = creature:getKillers(true) + for i, killerPlayer in ipairs(killers) do + logger.debug("Player {} killed the boss.", killerPlayer:getName()) + local soulWarQuest = killerPlayer:soulWarQuestKV() + -- Checks if the boss has already been defeated + if not soulWarQuest:get(bossName) then + local firstTaintTime = soulWarQuest:get("firstTaintTime") + if not firstTaintTime then + local currentTime = os.time() + soulWarQuest:set("firstTaintTime", currentTime) + end + + soulWarQuest:set(bossName, true) -- Mark the boss as defeated + -- Adds the next taint in the sequence that the player does not already have + killerPlayer:addNextTaint() + end + end + end +end + +bossesDeath:register() + +fourthTaintBossesDeath:register() + +local lastUse = 0 +local cooldown = 30 + +local mirrorImageCreation = Action() +function mirrorImageCreation.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local currentTime = os.time() + local timePassed = currentTime - lastUse + if timePassed >= cooldown or lastUse == 0 then + Game.createMonster("Mirror Image", player:getPosition()) + lastUse = currentTime + item:transform(33783) + else + local timeLeft = cooldown - timePassed + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You need to wait " .. timeLeft .. " second(s) to use this item again.") + end + + return true +end + +mirrorImageCreation:id(33782) +mirrorImageCreation:register() + +local mirroredNightmareApparitionDeath = CreatureEvent("MirroredNightmareBossAccess") + +function mirroredNightmareApparitionDeath.onDeath(creature, corpse, killer, mostDamageKiller, lastHitUnjustified, mostDamageUnjustified) + local creatureName = creature:getName() + if table.contains(SoulWarQuest.apparitionNames, creatureName) then + local damageMap = creature:getMonster():getDamageMap() + for key, _ in pairs(damageMap) do + local player = Player(key) + if player then + local soulWarQuest = player:soulWarQuestKV() + local currentCount = soulWarQuest:get(creatureName) or 0 + soulWarQuest:set(creatureName, currentCount + 1) + end + end + end +end + +mirroredNightmareApparitionDeath:register() + +-- Check mirrored nightmare boss access +local goshnarGreedEntrance = MoveEvent() + +function goshnarGreedEntrance.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player then + return false + end + + local soulWarQuest = player:soulWarQuestKV() + local hasAccess = true + local message = "Progress towards Mirrored Nightmare boss access:\n" + + for _, apparitionName in pairs(SoulWarQuest.apparitionNames) do + local count = soulWarQuest:get(apparitionName) or 0 + if count < SoulWarQuest.requiredCountPerApparition then + hasAccess = false + message = message .. apparitionName .. ": " .. count .. "/" .. SoulWarQuest.requiredCountPerApparition .. " kills\n" + else + message = message .. apparitionName .. ": Access achieved!\n" + end + end + + if not hasAccess then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, message) + player:teleportTo(fromPosition) + return false + end + + player:teleportTo(SoulWarQuest.goshnarsGreedAccessPosition.to) + player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + return true +end + +goshnarGreedEntrance:position(SoulWarQuest.goshnarsGreedAccessPosition.from) +goshnarGreedEntrance:register() + +local greedMonsterDeath = CreatureEvent("GreedMonsterDeath") + +function greedMonsterDeath.onDeath(creature, corpse, killer, mostDamageKiller, lastHitUnjustified, mostDamageUnjustified) + local createMonsterPosition = GreedMonsters[creature:getName()] + if creature:getName() == "Greedbeast" then + GreedbeastKills = GreedbeastKills + 1 + end + + CreateGoshnarsGreedMonster(creature:getName(), createMonsterPosition) +end + +greedMonsterDeath:register() + +local checkTaint = TalkAction("!checktaint") + +function checkTaint.onSay(player, words, param) + local taintLevel = player:getTaintLevel() + local taintName = player:getTaintNameByNumber(taintLevel) + if taintLevel ~= nil and taintName ~= nil then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Your current taint level is: " .. taintLevel .. " name: " .. taintName) + else + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You currently have no taint.") + end + + return true +end + +checkTaint:groupType("normal") +checkTaint:register() + +local setTaint = TalkAction("/settaint") + +function setTaint.onSay(player, words, param) + local split = param:split(",") + local target = Player(split[1]) + if not target then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player is offline") + return false + end + + local taintLevel = split[2]:trim():lower() + local taintName = player:getTaintNameByNumber(tonumber(taintLevel), true) + if taintName ~= nil then + target:resetTaints(true) + target:soulWarQuestKV():set(taintName, true) + target:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You new taint level is: " .. taintLevel .. ", name: " .. taintName) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Added taint level: " .. taintLevel .. ", name: " .. taintName .. " to player: " .. target:getName()) + target:setTaintIcon() + end +end + +setTaint:separator(" ") +setTaint:groupType("god") +setTaint:register() + +local goshnarGreedTeleport = MoveEvent() + +function goshnarGreedTeleport.onStepIn(creature, item, position, fromPosition) + local creatureName = creature:getName() + if creatureName == "Greedbeast" then + return + end + + local foundCreaturePosition = GreedMonsters[creatureName] + if not foundCreaturePosition then + return false + end + + if item:getId() == 33791 then + creature:remove() + item:transform(33790) + position:sendMagicEffect(CONST_ME_MORTAREA) + CreateGoshnarsGreedMonster(creatureName, foundCreaturePosition) + end + + return true +end + +goshnarGreedTeleport:id(33790, 33791) +goshnarGreedTeleport:register() + +local setTaint = TalkAction("/removetaint") + +function setTaint.onSay(player, words, param) + local split = param:split(",") + local target = Player(split[1]) + if not target then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player is offline") + return false + end + + local taintLevel = split[2]:trim():lower() + local taintName = player:getTaintNameByNumber(tonumber(taintLevel)) + if taintName ~= nil then + target:soulWarQuestKV():remove(taintName) + target:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You lose taint level: " .. taintLevel .. ", name: " .. taintName) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Removed taint level: " .. taintLevel .. ", name: " .. taintName .. " from player: " .. target:getName()) + end +end + +setTaint:separator(" ") +setTaint:groupType("god") +setTaint:register() + +local changeMap = TalkAction("/changeflowmap") + +function changeMap.onSay(player, words, param) + if param == "empty" then + Game.loadMap(SoulWarQuest.ebbAndFlow.mapsPath.empty) + elseif param == "inundate" then + Game.loadMap(SoulWarQuest.ebbAndFlow.mapsPath.inundate) + elseif param == "ebb" then + Game.loadMap(SoulWarQuest.ebbAndFlowmapsPath.ebbFlow) + end +end + +changeMap:separator(" ") +changeMap:groupType("god") +changeMap:register() + +local hazardousPhantomDeath = CreatureEvent("HazardousPhantomDeath") + +function hazardousPhantomDeath.onDeath(creature, corpse, killer, mostDamageKiller, lastHitUnjustified, mostDamageUnjustified) + local killers = creature:getKillers(true) + for i, killerPlayer in ipairs(killers) do + -- Checks if the killer is a player + if killerPlayer:isPlayer() then + local soulWarQuest = killerPlayer:soulWarQuestKV() + local deathCount = soulWarQuest:get("hazardous-phantom-death") or 0 + -- Checks that the death count has not yet reached the limit + if deathCount < SoulWarQuest.hardozousPanthomDeathCount then + -- Increases death count + soulWarQuest:set("hazardous-phantom-death", deathCount + 1) + -- Send the count for the player + killerPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You killed " .. (deathCount + 1) .. " of " .. SoulWarQuest.hardozousPanthomDeathCount .. " Hazardous Panthom.") + end + + if deathCount + 1 == SoulWarQuest.hardozousPanthomDeathCount then + killerPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You can now access the boss room.") + end + end + end +end + +hazardousPhantomDeath:register() + +local weepingSoulCorpse = MoveEvent() + +local condition = Condition(CONDITION_OUTFIT) +condition:setOutfit(SoulWarQuest.waterElementalOutfit) +condition:setTicks(14000) + +function weepingSoulCorpse.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player then + return + end + + if player:hasCondition(CONDITION_OUTFIT) then + return + end + + local monster = Creature("Goshnar's Spite") + if monster then + local chance = math.random(100) + if chance <= SoulWarQuest.goshnarsSpiteHealChance then + local healAmount = math.floor(monster:getMaxHealth() * (SoulWarQuest.goshnarsSpiteHealPercentage / 100)) + -- Heal percentage of the maximum health + monster:addHealth(healAmount) + logger.debug("Goshnar's Spite was healed to 10% of its maximum health.") + end + end + + item:remove() + player:addCondition(condition) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You are soaked by tears of the weeping soul!") + return true +end + +weepingSoulCorpse:id(SoulWarQuest.weepingSoulCorpseId) +weepingSoulCorpse:register() + +local function removeSearingFire(position) + local tile = Tile(position) + if tile then + local fire = tile:getItemById(SoulWarQuest.searingFireId) + if fire then + local monster = Creature("Goshnar's Spite") + if monster then + monster:addDefense(SoulWarQuest.goshnarsSpiteIncreaseDefense) + logger.debug("Found Goshnar's Spite on boss zone, adding defense.") + end + fire:remove() + end + end +end + +local goshnarSpiteFire = GlobalEvent("CreateGoshnarSpiteFire") + +function goshnarSpiteFire.onThink(interval) + local randomIndex = math.random(#SoulWarQuest.goshnarsSpiteFirePositions) -- Choose a random index + local firePosition = SoulWarQuest.goshnarsSpiteFirePositions[randomIndex] -- Get the corresponding position + local tile = Tile(firePosition) + if tile then + local fire = Game.createItem(SoulWarQuest.searingFireId, 1, firePosition) + if fire then + addEvent(removeSearingFire, SoulWarQuest.timeToRemoveSearingFire * 1000, firePosition) + end + end + + return true +end + +goshnarSpiteFire:interval(SoulWarQuest.timeToCreateSearingFire * 1000) +goshnarSpiteFire:register() + +local goshnarSpiteSoulFire = MoveEvent() + +function goshnarSpiteSoulFire.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player then + return + end + + local tile = Tile(position) + if not tile then + return + end + + local searingFire = tile:getItemById(SoulWarQuest.searingFireId) + if not searingFire then + return + end + + local soulWarQuest = player:soulWarQuestKV() + local lastSteppedTime = soulWarQuest:get("goshnar-spite-fire") or 0 + local currentTime = os.time() + + if lastSteppedTime + SoulWarQuest.cooldownToStepOnSearingFire > currentTime then + local remainingTime = lastSteppedTime + SoulWarQuest.cooldownToStepOnSearingFire - currentTime + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "His soul won't need to recover again! You need wait " .. remainingTime .. " seconds.") + return true + end + + addEvent(function(playerId) + local eventPlayer = Player(playerId) + if eventPlayer then + eventPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Your soul has recovered!") + end + end, SoulWarQuest.cooldownToStepOnSearingFire * 1000, player:getId()) + + soulWarQuest:set("goshnar-spite-fire", currentTime) + searingFire:remove() + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The soul fire was stomped out in time! Your soul will now have to recover before you can do this again.") + + return true +end + +for _, pos in pairs(SoulWarQuest.goshnarsSpiteFirePositions) do + goshnarSpiteSoulFire:position(pos) +end + +goshnarSpiteSoulFire:register() + +local ebbAndFlowBoatTeleports = MoveEvent() + +function ebbAndFlowBoatTeleports.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player or not SoulWarQuest.ebbAndFlow.isActive() then + return + end + + for _, pos in pairs(SoulWarQuest.ebbAndFlowBoatTeleportPositions) do + if Position(pos.register) == position then + player:teleportTo(pos.teleportTo) + player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + return true + end + end +end + +for _, pos in pairs(SoulWarQuest.ebbAndFlowBoatTeleportPositions) do + ebbAndFlowBoatTeleports:position(pos.register) +end +ebbAndFlowBoatTeleports:register() + +local ebbAndFlowDoor = Action() + +function ebbAndFlowDoor.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if SoulWarQuest.ebbAndFlow.isActive() then + return false + end + + -- Determines whether the player is north or south of the door + local playerPosition = player:getPosition() + local destination = Position(toPosition.x, toPosition.y, toPosition.z) + if playerPosition.y < toPosition.y then + -- Player is north, move south + destination.y = toPosition.y + 1 + else + -- Player is south (or at the same y position), moves north + destination.y = toPosition.y - 1 + end + + player:teleportTo(destination) + destination:sendMagicEffect(CONST_ME_TELEPORT) + return true +end + +ebbAndFlowDoor:id(SoulWarQuest.ebbAndFlow.doorId) +ebbAndFlowDoor:register() + +local rottenWastelandShrines = Action() + +function rottenWastelandShrines.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local soulWarQuest = player:soulWarQuestKV() + local shrineUsed = soulWarQuest:get("rotten-wasterland-activated-shrine-id") or 0 + if shrineUsed == item:getId() then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have already activated this shrine.") + return true + end + + local activatedShrinesCount = soulWarQuest:get("rotten-wasterland-activated-shrine-count") or 0 + if activatedShrinesCount >= 4 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have already activated all the shrines.") + return true + end + + soulWarQuest:set("rotten-wasterland-activated-shrine-id", item:getId()) + + soulWarQuest:set("rotten-wasterland-activated-shrine-count", activatedShrinesCount + 1) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have activated this shrine.") + return true +end + +for itemId, position in pairs(SoulWarQuest.rottenWastelandShrines) do + rottenWastelandShrines:id(itemId) +end + +rottenWastelandShrines:register() + +local goshnarsHatredAccess = Action() + +function goshnarsHatredAccess.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local soulWarQuest = player:soulWarQuestKV() + local activatedShrineCount = soulWarQuest:get("rotten-wasterland-activated-shrine-count") or 0 + if activatedShrineCount < 4 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You still need to activate all the shrines.") + return true + end + + player:teleportTo(SoulWarQuest.goshnarsHatredAccessPosition.to) + player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + return true +end + +goshnarsHatredAccess:position(SoulWarQuest.goshnarsHatredAccessPosition.from) +goshnarsHatredAccess:register() + +local goshnarsHatredSorrow = Action() + +function goshnarsHatredSorrow.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if not target then + return + end + + if not table.contains(SoulWarQuest.burningHatredMonsters, target:getName()) then + logger.error("Player {} tried to use the item on a non-burning hatred monster.", player:getName()) + return + end + + item:remove() + local actualTime = SoulWarQuest.kvBurning:get("time") or 0 + SoulWarQuest.kvBurning:set("time", actualTime + 10) + logger.debug("Player {} used the item on the monster {}, oldTime {}, newTime {}.", player:getName(), target:getName(), actualTime, actualTime + 10) + player:say("The flame of hatred is doused!", TALKTYPE_MONSTER_SAY, 0, 0, target:getPosition()) + return true +end + +goshnarsHatredSorrow:id(SoulWarQuest.goshnarsHatredSorrowId) +goshnarsHatredSorrow:register() + +local burningChangeForm = CreatureEvent("BurningChangeForm") + +function burningChangeForm.onThink(creature) + if not creature or not creature:getMonster() then + return true + end + + local monster = creature:getMonster() + local currentTime = SoulWarQuest.kvBurning:get("time") or 0 + if currentTime == 0 then + SoulWarQuest.kvBurning:set("time", 180) + return true + end + + SoulWarQuest.kvBurning:set("time", currentTime - 1) + + logger.debug("Burning transformation decreased to time : {}", currentTime) + for _, transformation in ipairs(SoulWarQuest.burningTransformations) do + local timeTransformation, newType = unpack(transformation) + if currentTime == timeTransformation and monster:getName() ~= newType then + monster:setType(newType, true) + logger.debug("Changing monster to {} on currentTime {}.", newType, currentTime) + + if newType == "Ashes of Burning Hatred" then + monster:say("The fire of hatred fuels and empowers Goshnar's Hate!", TALKTYPE_MONSTER_SAY, 0, 0, monster:getPosition()) + local boss = Creature("Goshnar's Hatred") + if boss then + logger.debug("Increasing hatred damage multiplier.") + boss:increaseHatredDamageMultiplier(10) + end + logger.debug("Beginning of the burning transformation cycle.") + end + break + end + end + + return true +end + +burningChangeForm:register() + +local goshnarsHatredBuff = CreatureEvent("GoshnarsHatredBuff") + +function goshnarsHatredBuff.onHealthChange(creature, attacker, primaryDamage, primaryType, secondaryDamage, secondaryType, origin) + -- Ensure both attacker and creature are valid and the creature is "Goshnar's Hatred" + if creature then + -- Check if the attacker is a player and the creature is being hit + if attacker and creature:isMonster() and attacker:isPlayer() and (creature:getName() == "Goshnar's Hatred" or creature:getName() == "Goshnar's Megalomania") then + local defenseMultiplier = creature:getHatredDamageMultiplier() + if defenseMultiplier > 0 then + -- Apply the defense multiplier + creature:addDefense(defenseMultiplier) + logger.debug("Adding defense to {}.", creature:getName()) + end + -- Check if the attacker is a monster and the player is being hit + elseif attacker and creature:isPlayer() and attacker:isMonster() and (attacker:getName() == "Goshnar's Hatred" or creature:getName() == "Goshnar's Megalomania") then + local damageMultiplier = attacker:getHatredDamageMultiplier() + if damageMultiplier > 0 then + local multip = 1 + (damageMultiplier / 100) + logger.debug("Adding damage: {} to {}.", multip, attacker:getName()) + -- Return modified damage values + return primaryDamage * multip, primaryType, secondaryDamage, secondaryType + end + end + end + + -- Return original damage values if no conditions are met + return primaryDamage, primaryType, secondaryDamage, secondaryType +end + +goshnarsHatredBuff:register() + +local condensedRemorse = MoveEvent() + +function condensedRemorse.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player then + return false + end + + local soulWarKV = player:soulWarQuestKV() + local remorseCount = soulWarKV:get("condensed-remorse") or 0 + soulWarKV:set("condensed-remorse", remorseCount + 1) + if remorseCount + 1 == 2 then + player:resetGoshnarSymbolTormentCounter() + player:say("The remorse calms your dread!", TALKTYPE_MONSTER_SAY, 0, 0, item:getPosition()) + player:getPosition():sendMagicEffect(CONST_ME_HOLYAREA) + soulWarKV:remove("condensed-remorse") + end + + item:remove() + return true +end + +condensedRemorse:id(SoulWarQuest.condensedRemorseId) +condensedRemorse:register() + +local furiousCraterAccess = EventCallback("FuriousCraterAccessDropLoot") + +function furiousCraterAccess.monsterOnDropLoot(monster, corpse) + if not monster or not corpse then + return + end + + local player = Player(corpse:getCorpseOwner()) + if not player or not player:canReceiveLoot() then + return + end + + local mType = monster:getType() + if not mType then + return + end + + if not table.contains(SoulWarQuest.pulsatingEnergyMonsters, mType:getName()) then + return + end + + Game.createItem(SoulWarQuest.pulsatingEnergyId, 1, monster:getPosition()) +end + +furiousCraterAccess:register() + +local pulsatingEnergy = MoveEvent() + +function pulsatingEnergy.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player then + return false + end + + local kv = player:pulsatingEnergyKV() + local energyCount = kv:get("access-counter") or 0 + energyCount = energyCount + 1 + kv:set("access-counter", energyCount) + + logger.debug("Player {} stepped on a pulsating energy, current count: {}", player:getName(), energyCount) + + local firstFloorAccess = kv:get("first-floor-access") or false + local secondFloorAccess = kv:get("second-floor-access") or false + local thirdFloorAccess = kv:get("third-floor-access") or false + if thirdFloorAccess then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You've already gained access to fight with the Goshnar's Cruelty.") + return true + end + + if energyCount >= 40 and not firstFloorAccess then + kv:set("access-counter", 0) + kv:set("first-floor-access", true) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You've gained access to the first floor. Continue collecting Pulsating Energies to gain further access.") + end + + if energyCount >= 55 and not secondFloorAccess then + kv:set("access-counter", 0) + kv:set("second-floor-access", true) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You've gained access to the second floor. Continue collecting Pulsating Energies to gain further access.") + end + + if energyCount >= 70 and not thirdFloorAccess then + kv:set("access-counter", 0) + kv:set("third-floor-access", true) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You've gained access to the third floor. You can now fight with the Goshnar's Cruelty.") + end + + item:remove() + return true +end + +pulsatingEnergy:id(SoulWarQuest.pulsatingEnergyId) +pulsatingEnergy:register() + +local pulsatingEnergyTeleportAccess = MoveEvent() + +function pulsatingEnergyTeleportAccess.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player then + return false + end + + for _, posData in pairs(SoulWarQuest.goshnarsCrueltyTeleportRoomPositions) do + if posData.from == position then + local kv = player:pulsatingEnergyKV() + local hasAccess = kv:get(posData.access) or false + local energyCount = kv:get("access-counter") or 0 + local energiesNeeded = posData.count - energyCount + if not hasAccess then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You don't have access to this floor yet. You have collected " .. energyCount .. "/" .. posData.count .. ", and need " .. energiesNeeded .. " more pulsating energies to gain access.") + player:teleportTo(fromPosition, true) + fromPosition:sendMagicEffect(CONST_ME_TELEPORT) + else + player:teleportTo(posData.to) + posData.to:sendMagicEffect(CONST_ME_TELEPORT) + end + + break + end + end + + return true +end + +for _, positions in pairs(SoulWarQuest.goshnarsCrueltyTeleportRoomPositions) do + pulsatingEnergyTeleportAccess:position(positions.from) +end + +pulsatingEnergyTeleportAccess:register() + +local cloakOfTerrorHealthLoss = CreatureEvent("CloakOfTerrorHealthLoss") + +function cloakOfTerrorHealthLoss.onHealthChange(creature, attacker, primaryDamage, primaryType, secondaryDamage, secondaryType, origin) + if not creature or not attacker then + return primaryDamage, primaryType, secondaryDamage, secondaryType + end + + if attacker:getPlayer() and primaryDamage > 0 or secondaryDamage > 0 then + local position = creature:getPosition() + local tile = Tile(position) + if tile then + if not tile:getItemById(SoulWarQuest.theBloodOfCloakTerrorIds[1]) then + Game.createItem(SoulWarQuest.theBloodOfCloakTerrorIds[1], 1, position) + end + end + end + + return primaryDamage, primaryType, secondaryDamage, secondaryType +end + +cloakOfTerrorHealthLoss:register() + +local theBloodOfCloakStep = MoveEvent() + +function theBloodOfCloakStep.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + -- If a player steps in blood, it takes damage + if player then + local damagePercentage = SoulWarQuest.poolDamagePercentages[item:getId()] or 0 + local maxHealth = player:getMaxHealth() + local damage = maxHealth * damagePercentage + + player:addHealth(-damage, COMBAT_ENERGYDAMAGE) + end + + -- If a "Cloak of Terror" monster steps in blood, it heals itself + local monster = creature:getMonster() + if monster and monster:getName() == "Cloak of Terror" then + local healAmount = math.random(1500, 2000) + monster:addHealth(healAmount) + end + + item:remove() + + return true +end + +for _, itemId in pairs(SoulWarQuest.theBloodOfCloakTerrorIds) do + theBloodOfCloakStep:id(itemId) +end + +theBloodOfCloakStep:register() + +local greedyMaw = Action() + +function greedyMaw.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if not item or not target then + logger.error("Greedy Maw action failed, item or target is nil.") + return false + end + + if target:getId() == SoulWarQuest.greedyMawId then + local kv = player:soulWarQuestKV():scoped("furious-crater") + local cooldown = kv:get("greedy-maw-action") or 0 + local currentTime = os.time() + if cooldown + SoulWarQuest.useGreedMawCooldown > currentTime then + local timeLeft = cooldown + SoulWarQuest.useGreedMawCooldown - currentTime + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You need to wait " .. timeLeft .. " more seconds before using the greedy maw again.") + return true + end + + kv:set("greedy-maw-action", currentTime) + local timeToIncreaseDefense = SoulWarQuest.timeToIncreaseCrueltyDefense + SoulWarQuest.kvSoulWar:set("greedy-maw-action", currentTime + timeToIncreaseDefense) + target:getPosition():sendMagicEffect(CONST_ME_DRAWBLOOD) + item:remove() + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Use the item again within " .. timeToIncreaseDefense .. " seconds, or the monster's defense will increase by 2 every " .. timeToIncreaseDefense .. " seconds.") + local goshnarsCruelty = Creature("Goshnar's Cruelty") + if goshnarsCruelty then + local mtype = goshnarsCruelty:getType() + if not mtype then + logger.error("Greedy Maw action failed, Goshnar's Cruelty has no type.") + return false + end + + -- If the defense of Goshnar's Cruelty is higher than the default defense, decrease it by 2 + if goshnarsCruelty:getDefense() > mtype:defense() then + logger.debug("Greedy Maw used on Goshnar's Cruelty, old defense {}", goshnarsCruelty:getDefense()) + goshnarsCruelty:addDefense(-SoulWarQuest.goshnarsCrueltyDefenseChange) + logger.debug("Greedy Maw used on Goshnar's Cruelty, new defense {}", goshnarsCruelty:getDefense()) + end + + local defenseDrainValue = SoulWarQuest.kvSoulWar:get("goshnars-cruelty-defense-drain") or 0 + if defenseDrainValue > 0 then + SoulWarQuest.kvSoulWar:set("goshnars-cruelty-defense-drain", defenseDrainValue - 1) + end + end + return true + end + + return false +end + +greedyMaw:id(SoulWarQuest.someMortalEssenceId) +greedyMaw:register() + +local soulWarAspectOfPowerDeath = CreatureEvent("SoulWarAspectOfPowerDeath") + +function soulWarAspectOfPowerDeath.onDeath(creature) + local targetMonster = creature:getMonster() + if not targetMonster or targetMonster:getMaster() then + return + end + + logger.debug("Aspect of Power died, checking if all are dead.") + local boss = Creature("Goshnar's Megalomania") + if boss and boss:getTypeName() == "Goshnar's Megalomania Purple" then + boss:increaseAspectOfPowerDeathCount() + end + + local position = boss and boss:getPosition() or creature:getPosition() + addEvent(function(position) + local aspectMonster = Game.createMonster("Aspect of Power", position) + if aspectMonster then + local outfit = aspectMonster:getOutfit() + outfit.lookType = math.random(1303, 1307) + aspectMonster:setOutfit(outfit) + end + end, 5000, position) + + return true +end + +soulWarAspectOfPowerDeath:register() + +local madnessReduce = MoveEvent() + +function madnessReduce.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + item:getPosition():sendMagicEffect(CONST_ME_HOLYAREA) + item:remove() + if player and player:getGoshnarSymbolTormentCounter() > 0 then + player:resetGoshnarSymbolTormentCounter() + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The ooze calms your dread but leaves you vulnerable to phantasmal attacks!") + return true + end + + local creatureName = creature:getName() + if creatureName == "Lesser Splinter of Madness" or creatureName == "Greater Splinter of Madness" or creatureName == "Mighty Splinter of Madness" then + creature:remove() + item:transform(SoulWarQuest.cleansedSanityItemId) + end + + return true +end + +madnessReduce:id(SoulWarQuest.deadAspectOfPowerCorpseId) +madnessReduce:register() + +local cleansedSanity = Action() + +function cleansedSanity.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if not item or not target then + logger.error("Cleansed action failed, item or target is nil.") + return false + end + + local kv = player:soulWarQuestKV():scoped("furious-crater") + local cooldown = kv:get("cleansed-sanity-action") or 0 + local currentTime = os.time() + if cooldown + SoulWarQuest.useGreedMawCooldown > currentTime then + local timeLeft = cooldown + SoulWarQuest.useGreedMawCooldown - currentTime + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You need to wait " .. timeLeft .. " more seconds before using the cleansed again.") + return true + end + + kv:set("cleansed-sanity-action", currentTime) + if target:getId() == SoulWarQuest.greedyMawId then + local timeToIncreaseDefense = SoulWarQuest.timeToIncreaseCrueltyDefense + SoulWarQuest.kvSoulWar:set("cleansed-sanity-action", currentTime + timeToIncreaseDefense) + target:getPosition():sendMagicEffect(CONST_ME_DRAWBLOOD) + item:remove() + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Use the item again within " .. timeToIncreaseDefense .. " seconds, or the monster's defense will increase every " .. timeToIncreaseDefense .. " seconds.") + local boss = Creature("Goshnar's Megalomania") + if boss then + local mtype = boss:getType() + if not mtype then + logger.error("Cleansed action failed, Goshnar's Megalomania has no type.") + return false + end + + -- If the defense of Goshnar's Megalomania is higher than the default defense, decrease it by 2 + if boss:getDefense() > mtype:defense() then + logger.debug("Cleansed used on Goshnar's Megalomania, old defense {}", boss:getDefense()) + boss:addDefense(-SoulWarQuest.goshnarsCrueltyDefenseChange) + logger.debug("Cleansed used on Goshnar's Megalomania, new defense {}", boss:getDefense()) + end + end + return true + end + + return false +end + +cleansedSanity:id(SoulWarQuest.cleansedSanityItemId) +cleansedSanity:register() + +local necromanticRemainsReduce = MoveEvent() + +function necromanticRemainsReduce.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player then + return + end + + player:removeGoshnarSymbolTormentCounter(5) + item:remove() + position:sendMagicEffect(CONST_ME_HOLYAREA) + return true +end + +necromanticRemainsReduce:id(SoulWarQuest.necromanticRemainsItemId) +necromanticRemainsReduce:register() + +local necromanticFocusDeath = CreatureEvent("NecromanticFocusDeath") + +function necromanticFocusDeath.onDeath(creature) + local targetMonster = creature:getMonster() + if not targetMonster or targetMonster:getMaster() then + return + end + + local position = targetMonster:getPosition() + addEvent(function() + position:increaseNecromaticMegalomaniaStrength() + end, 5 * 60 * 1000) + + return true +end + +necromanticFocusDeath:register() + +local megalomaniaDeath = CreatureEvent("MegalomaniaDeath") + +function megalomaniaDeath.onDeath(creature, corpse, killer, mostDamageKiller, lastHitUnjustified, mostDamageUnjustified) + local killers = creature:getKillers(true) + for i, killerPlayer in ipairs(killers) do + local soulWarQuest = killerPlayer:soulWarQuestKV() + -- Checks if the boss has already been defeated + if not soulWarQuest:get("goshnar's-megalomania-killed") then + soulWarQuest:set("goshnar's-megalomania-killed", true) + killerPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have defeated Goshnar's Megalomania. Report the 'task' to Flickering Soul and earn your outfit.") + end + end + return true +end + +megalomaniaDeath:register() + +local teleportStepRemoveIcon = MoveEvent() + +function teleportStepRemoveIcon.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player then + return + end + + player:resetGoshnarSymbolTormentCounter() + return true +end + +local teleportPositions = { + Position(33713, 31642, 14), + Position(33743, 31606, 14), +} + +for _, pos in pairs(teleportPositions) do + teleportStepRemoveIcon:position(pos) +end + +teleportStepRemoveIcon:register() + +local goshnarsCrueltyBuff = CreatureEvent("GoshnarsCrueltyBuff") + +function goshnarsCrueltyBuff.onHealthChange(creature, attacker, primaryDamage, primaryType, secondaryDamage, secondaryType, origin) + if creature and creature:isMonster() and attacker:isPlayer() and creature:getName() == "Goshnar's Cruelty" then + local newValue = SoulWarQuest.kvSoulWar:get("goshnars-cruelty-defense-drain") or SoulWarQuest.goshnarsCrueltyDefenseChange + if newValue ~= 0 then + local multiplier = math.max(0, 1 - (newValue / 100)) + return primaryDamage * multiplier, primaryType, secondaryDamage * multiplier, secondaryType + end + end + + return primaryDamage, primaryType, secondaryDamage, secondaryType +end + +goshnarsCrueltyBuff:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/spell-eye_beam.lua b/data-otservbr-global/scripts/quests/soul_war/spell-eye_beam.lua new file mode 100644 index 00000000000..7a73979930a --- /dev/null +++ b/data-otservbr-global/scripts/quests/soul_war/spell-eye_beam.lua @@ -0,0 +1,38 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DROWNDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SMALLCLOUDS) + +combat:setArea(createCombatArea({ + { 1 }, + { 1 }, + { 3 }, +})) + +function onTargetTile(cid, pos) + local tile = Tile(pos) + local target = tile:getTopCreature() + if tile then + if target then + if target:isMonster() and target:getName() == "Poor Soul" then + target:addHealth(-1000) + end + end + end + return true +end + +combat:setCallback(CALLBACK_PARAM_TARGETTILE, "onTargetTile") + +local spell = Spell("instant") + +function spell.onCastSpell(creature, var) + return combat:execute(creature, var) +end + +spell:name("greedy eye beam") +spell:words("greedy eye beam") +spell:isAggressive(true) +spell:blockWalls(true) +spell:needLearn(true) +spell:needDirection(true) +spell:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/spell-fire_beam_cruelty.lua b/data-otservbr-global/scripts/quests/soul_war/spell-fire_beam_cruelty.lua new file mode 100644 index 00000000000..2bc321f24e0 --- /dev/null +++ b/data-otservbr-global/scripts/quests/soul_war/spell-fire_beam_cruelty.lua @@ -0,0 +1,61 @@ +local areaSpell = { + { 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0 }, + { 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 }, + { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 }, + { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 }, + { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 }, + { 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1 }, + { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 }, + { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 }, + { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 }, + { 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 }, + { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 }, +} + +local area = createCombatArea(areaSpell) + +local combat = Combat() +combat:setArea(area) +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREAREA) + +function onTargetTile(cid, pos) + local tile = Tile(pos) + if tile then + local target = tile:getTopCreature() + if target and target:isPlayer() then + target:addHealth(math.random(2300, 3000)) + end + end + return true +end + +combat:setCallback(CALLBACK_PARAM_TARGETTILE, "onTargetTile") + +local function delayedCastSpell(cid, var, targetId) + local creature = Creature(cid) + if not creature then + return + end + + local target = Player(targetId) + if target then + combat:execute(creature, positionToVariant(target:getPosition())) + end +end + +local spell = Spell("instant") + +function spell.onCastSpell(creature, var, isHotkey) + return creature:applyZoneEffect(var, combat, "boss.goshnar's-cruelty") +end + +spell:name("cruelty transform elemental") +spell:words("cruelty transform elemental") +spell:isAggressive(true) +spell:blockWalls(true) +spell:needLearn(true) +spell:needDirection(true) +spell:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/spell-fire_beam_megalomania.lua b/data-otservbr-global/scripts/quests/soul_war/spell-fire_beam_megalomania.lua new file mode 100644 index 00000000000..2906f213d74 --- /dev/null +++ b/data-otservbr-global/scripts/quests/soul_war/spell-fire_beam_megalomania.lua @@ -0,0 +1,54 @@ +local areaSpell = { + { 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0 }, + { 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 }, + { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 }, + { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 }, + { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 }, + { 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1 }, + { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 }, + { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 }, + { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 }, + { 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 }, + { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 }, +} + +local area = createCombatArea(areaSpell) + +local combat = Combat() +combat:setArea(area) +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREAREA) + +function onTargetTile(cid, pos) + local tile = Tile(pos) + if tile then + local target = tile:getTopCreature() + if target and target:isPlayer() then + target:addHealth(math.random(2300, 3000)) + end + end + return true +end + +combat:setCallback(CALLBACK_PARAM_TARGETTILE, "onTargetTile") + +local config = { + outfit = { lookType = 242, lookHead = 0, lookBody = 0, lookLegs = 0, lookFeet = 0, lookAddons = 0 }, + time = 7000, +} + +local spell = Spell("instant") + +function spell.onCastSpell(creature, var, isHotkey) + return creature:applyZoneEffect(var, combat, "boss.goshnar's-megalomania-purple") +end + +spell:name("megalomania transform elemental") +spell:words("megalomania transform elemental") +spell:isAggressive(true) +spell:blockWalls(true) +spell:needLearn(true) +spell:needDirection(true) +spell:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/spell-megalomania_blue.lua b/data-otservbr-global/scripts/quests/soul_war/spell-megalomania_blue.lua new file mode 100644 index 00000000000..d8a9c8e9875 --- /dev/null +++ b/data-otservbr-global/scripts/quests/soul_war/spell-megalomania_blue.lua @@ -0,0 +1,58 @@ +local area = { + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }, +} + +local createArea = createCombatArea(area) + +local combat = Combat() +combat:setArea(createArea) + +local zone = Zone.getByName("boss.goshnar's-megalomania-purple") +local zonePositions = zone:getPositions() + +function onTargetTile(creature, pos) + for _, pos in ipairs(zonePositions) do + local tile = Tile(pos) + if tile and tile:getGround() and tile:getGround():getId() ~= 409 then + local creature = tile:getTopCreature() + if creature then + local player = creature:getPlayer() + if player then + player:addHealth(-6000, COMBAT_DEATHDAMAGE) + end + end + end + end + + pos:sendMagicEffect(CONST_ME_BLACKSMOKE) + return true +end + +combat:setCallback(CALLBACK_PARAM_TARGETTILE, "onTargetTile") + +local spell = Spell("instant") + +function spell.onCastSpell(creature, var) + return combat:execute(creature, positionToVariant(creature:getPosition())) +end + +spell:name("megalomania blue") +spell:words("megalomania blue") +spell:isAggressive(true) +spell:blockWalls(false) +spell:needLearn(true) +spell:register() diff --git a/data-otservbr-global/scripts/quests/soul_war/spell-soulsnatcher.lua b/data-otservbr-global/scripts/quests/soul_war/spell-soulsnatcher.lua new file mode 100644 index 00000000000..9292f10530f --- /dev/null +++ b/data-otservbr-global/scripts/quests/soul_war/spell-soulsnatcher.lua @@ -0,0 +1,58 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HOLYDAMAGE) + +combat:setArea(createCombatArea(CrossBeamArea3X2)) + +local spell = Spell("instant") + +function spell.onCastSpell(creature, var) + return combat:execute(creature, var) +end + +spell:name("soulsnatcher-lifedrain-beam") +spell:words("soulsnatcher-lifedrain-beam") +spell:isAggressive(true) +spell:blockWalls(true) +spell:needLearn(true) +spell:needDirection(true) +spell:register() + +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HOLYAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_HOLY) + +spell = Spell("instant") + +function spell.onCastSpell(creature, var) + return combat:execute(creature, var) +end + +spell:name("soulsnatcher-lifedrain-missile") +spell:words("soulsnatcher-lifedrain-missile") +spell:isAggressive(true) +spell:blockWalls(true) +spell:needLearn(true) +spell:needTarget(true) +spell:register() + +-- Mana drain ball +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYAREA) + +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +spell = Spell("instant") + +function spell.onCastSpell(creature, var) + return combat:execute(creature, var) +end + +spell:name("soulsnatcher-manadrain-ball") +spell:words("soulsnatcher-manadrain-ball") +spell:isAggressive(true) +spell:blockWalls(true) +spell:needLearn(true) +spell:register() diff --git a/data-otservbr-global/scripts/world_changes/iron_servant_transformation.lua b/data-otservbr-global/scripts/world_changes/iron_servant_transformation.lua index 65e6af5a3b2..7da5f5d1b99 100644 --- a/data-otservbr-global/scripts/world_changes/iron_servant_transformation.lua +++ b/data-otservbr-global/scripts/world_changes/iron_servant_transformation.lua @@ -1,4 +1,4 @@ -local ironServantTransformation = EventCallback() +local ironServantTransformation = EventCallback("IronServantTransformationOnSpawn") ironServantTransformation.monsterOnSpawn = function(monster, position) if monster:getName():lower() ~= "iron servant replica" then diff --git a/data-otservbr-global/world/otservbr-house.xml b/data-otservbr-global/world/otservbr-house.xml index f7e5cd52370..7eff23b4606 100644 --- a/data-otservbr-global/world/otservbr-house.xml +++ b/data-otservbr-global/world/otservbr-house.xml @@ -284,7 +284,7 @@ - + diff --git a/data-otservbr-global/world/otservbr-monster.xml b/data-otservbr-global/world/otservbr-monster.xml index ef003deee1a..80c5f5d4ff3 100644 --- a/data-otservbr-global/world/otservbr-monster.xml +++ b/data-otservbr-global/world/otservbr-monster.xml @@ -34097,6 +34097,9 @@ + + + @@ -34129,6 +34132,9 @@ + + + @@ -34214,6 +34220,9 @@ + + + @@ -161998,9 +162007,15 @@ + + + + + + @@ -162027,6 +162042,15 @@ + + + + + + + + + @@ -162036,6 +162060,12 @@ + + + + + + @@ -162050,6 +162080,9 @@ + + + @@ -162062,6 +162095,9 @@ + + + @@ -162083,6 +162119,12 @@ + + + + + + @@ -162156,6 +162198,9 @@ + + + @@ -162168,12 +162213,22 @@ + + + + + + + + + + @@ -162185,6 +162240,15 @@ + + + + + + + + + @@ -162194,6 +162258,9 @@ + + + @@ -162201,6 +162268,9 @@ + + + diff --git a/data-otservbr-global/world/otservbr-npc.xml b/data-otservbr-global/world/otservbr-npc.xml index 2011d333ad4..2b3f88a00d7 100644 --- a/data-otservbr-global/world/otservbr-npc.xml +++ b/data-otservbr-global/world/otservbr-npc.xml @@ -2460,6 +2460,9 @@ + + + diff --git a/data-otservbr-global/world/otservbr-zones.xml b/data-otservbr-global/world/otservbr-zones.xml index 4740d50385c..44cff897a42 100644 --- a/data-otservbr-global/world/otservbr-zones.xml +++ b/data-otservbr-global/world/otservbr-zones.xml @@ -1,4 +1,4 @@ - + diff --git a/data-otservbr-global/world/quest/soul_war/ebb_and_flow/ebb-flow-empty.otbm b/data-otservbr-global/world/quest/soul_war/ebb_and_flow/ebb-flow-empty.otbm new file mode 100644 index 0000000000000000000000000000000000000000..9d3fa897425709c4d254f1ac0b598f26e2cbe0bd GIT binary patch literal 129452 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

PUx6Z9|8{7uB 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 delta 6727 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 diff --git a/data-otservbr-global/world/quest/soul_war/ebb_and_flow/ebb-flow.otbm b/data-otservbr-global/world/quest/soul_war/ebb_and_flow/ebb-flow.otbm new file mode 100644 index 0000000000000000000000000000000000000000..5a8d5d16fefe7f43e2bd163cb1f86eac75068fc0 GIT binary patch literal 181627 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 Q|Mc-c{nN)k*zN%TKhH4k761SM literal 0 HcmV?d00001 diff --git a/data-otservbr-global/world/quest/soul_war/ebb_and_flow/empty.otbm b/data-otservbr-global/world/quest/soul_war/ebb_and_flow/empty.otbm deleted file mode 100644 index 70036d063b14a9694c6166b1bd317cf887aeb285..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13957 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 HU=#R%=4!6# diff --git a/data/items/items.xml b/data/items/items.xml index 860a148fda2..8cec8f05ec6 100644 --- a/data/items/items.xml +++ b/data/items/items.xml @@ -62032,9 +62032,7 @@ hands of its owner. Granted by TibiaRoyal.com"/> - - - + @@ -62476,6 +62474,7 @@ hands of its owner. Granted by TibiaRoyal.com"/> + @@ -62891,28 +62890,25 @@ hands of its owner. Granted by TibiaRoyal.com"/> - + + + + - - - - - - - - - - - + + + + + - + @@ -63066,6 +63062,10 @@ hands of its owner. Granted by TibiaRoyal.com"/> + + + + @@ -63161,6 +63161,11 @@ hands of its owner. Granted by TibiaRoyal.com"/> + + + + + @@ -63180,6 +63185,7 @@ hands of its owner. Granted by TibiaRoyal.com"/> + @@ -63379,6 +63385,10 @@ hands of its owner. Granted by TibiaRoyal.com"/> + + + + @@ -63525,6 +63535,19 @@ hands of its owner. Granted by TibiaRoyal.com"/> + + + + + + + + + + + + + @@ -64262,7 +64285,7 @@ hands of its owner. Granted by TibiaRoyal.com"/> - + @@ -74750,12 +74773,31 @@ Granted by TibiaGoals.com"/> + + + + + + + + + + + + + + + + + + + @@ -74790,6 +74832,22 @@ Granted by TibiaGoals.com"/> + + + + + + + + + + + + + + + + @@ -74808,6 +74866,15 @@ Granted by TibiaGoals.com"/> + + + + + + + + + diff --git a/data/libs/functions/boss_lever.lua b/data/libs/functions/boss_lever.lua index b95bf7211b0..9cd577ee911 100644 --- a/data/libs/functions/boss_lever.lua +++ b/data/libs/functions/boss_lever.lua @@ -144,6 +144,7 @@ end ---@param player Player ---@return boolean function BossLever:onUse(player) + local monsterName = MonsterType(self.name):getName() local isParticipant = false for _, v in ipairs(self.playerPositions) do if Position(v.pos) == player:getPosition() then @@ -161,7 +162,7 @@ function BossLever:onUse(player) local zone = self:getZone() if zone:countPlayers(IgnoredByMonsters) > 0 then - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "There's already someone fighting with " .. self.name .. ".") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "There's already someone fighting with " .. monsterName .. ".") return true end @@ -173,14 +174,15 @@ function BossLever:onUse(player) return true end - if creature:getLevel() < self.requiredLevel then + local isAccountNormal = creature:getAccountType() == ACCOUNT_TYPE_NORMAL + if isAccountNormal and creature:getLevel() < self.requiredLevel then local message = "All players need to be level " .. self.requiredLevel .. " or higher." creature:sendTextMessage(MESSAGE_EVENT_ADVANCE, message) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, message) return false end - if creature:getGroup():getId() < GROUP_TYPE_GOD and self:lastEncounterTime(creature) > os.time() then + if creature:getGroup():getId() < GROUP_TYPE_GOD and isAccountNormal and self:lastEncounterTime(creature) > os.time() then local infoPositions = lever:getInfoPositions() for _, posInfo in pairs(infoPositions) do local currentPlayer = posInfo.creature diff --git a/data/libs/functions/creature.lua b/data/libs/functions/creature.lua index be95a45144c..376f828c5b0 100644 --- a/data/libs/functions/creature.lua +++ b/data/libs/functions/creature.lua @@ -208,7 +208,7 @@ end function Creature.getKillers(self, onlyPlayers) local killers = {} local inFightTicks = configManager.getNumber(configKeys.PZ_LOCKED) - local timeNow = os.mtime() + local timeNow = systemTime() local getCreature = onlyPlayers and Player or Creature for cid, cb in pairs(self:getDamageMap()) do local creature = getCreature(cid) diff --git a/data/libs/functions/monster.lua b/data/libs/functions/monster.lua index f26f2a5b9b4..0327daab3b3 100644 --- a/data/libs/functions/monster.lua +++ b/data/libs/functions/monster.lua @@ -204,7 +204,7 @@ do return table.contains(equipmentTypes, t) end - function MonsterType.getBossReward(self, lootFactor, topScore, equipmentOnly, lootTable) + function MonsterType.getBossReward(self, lootFactor, topScore, equipmentOnly, lootTable, player) if configManager.getNumber(configKeys.RATE_LOOT) <= 0 then return lootTable or {} end @@ -221,6 +221,6 @@ do end return true end, - }, lootTable) + }, lootTable, player) end end diff --git a/data/libs/functions/monstertype.lua b/data/libs/functions/monstertype.lua index 168cab13109..a9f6fff59f7 100644 --- a/data/libs/functions/monstertype.lua +++ b/data/libs/functions/monstertype.lua @@ -1,7 +1,7 @@ -- return a dictionary of itemId => { count, gut } ---@param config { factor: number, gut: boolean, filter?: fun(itemType: ItemType, unique: boolean): boolean } ---@return LootItems -function MonsterType:generateLootRoll(config, resultTable) +function MonsterType:generateLootRoll(config, resultTable, player) if configManager.getNumber(configKeys.RATE_LOOT) <= 0 then return resultTable or {} end @@ -28,6 +28,11 @@ function MonsterType:generateLootRoll(config, resultTable) end local chance = item.chance + if iType:getId() == SoulWarQuest.bagYouDesireItemId then + result[item.itemId].chance = self:calculateBagYouDesireChance(player, chance) + logger.debug("Final chance for bag you desire: {}, original chance: {}", result[item.itemId].chance, chance) + end + if config.gut and iType:getType() == ITEM_TYPE_CREATUREPRODUCT then chance = math.ceil((chance * GLOBAL_CHARM_GUT) / 100) end diff --git a/data/libs/functions/revscriptsys.lua b/data/libs/functions/revscriptsys.lua index 515522d6443..7026410c5e4 100644 --- a/data/libs/functions/revscriptsys.lua +++ b/data/libs/functions/revscriptsys.lua @@ -290,6 +290,14 @@ do self:eventType(MONSTERS_EVENT_SAY) self:onSay(value) return + elseif key == "onPlayerAttack" then + self:eventType(MONSTERS_EVENT_ATTACKED_BY_PLAYER) + self:onPlayerAttack(value) + return + elseif key == "onSpawn" then + self:eventType(MONSTERS_EVENT_ON_SPAWN) + self:onSpawn(value) + return end rawset(self, key, value) end diff --git a/data/libs/systems/zones.lua b/data/libs/systems/zones.lua index ff37af5b16a..698a464fe87 100644 --- a/data/libs/systems/zones.lua +++ b/data/libs/systems/zones.lua @@ -100,7 +100,7 @@ setmetatable(ZoneEvent, { function ZoneEvent:register() if self.beforeEnter then - local beforeEnter = EventCallback() + local beforeEnter = EventCallback("ZoneEventBeforeEnter", true) function beforeEnter.zoneBeforeCreatureEnter(zone, creature) if zone ~= self.zone then return true @@ -112,7 +112,7 @@ function ZoneEvent:register() end if self.beforeLeave then - local beforeLeave = EventCallback() + local beforeLeave = EventCallback("ZoneEventBeforeLeave", true) function beforeLeave.zoneBeforeCreatureLeave(zone, creature) if zone ~= self.zone then return true @@ -124,7 +124,7 @@ function ZoneEvent:register() end if self.afterEnter then - local afterEnter = EventCallback() + local afterEnter = EventCallback("ZoneEventAfterEnter", true) function afterEnter.zoneAfterCreatureEnter(zone, creature) if zone ~= self.zone then return true @@ -136,7 +136,7 @@ function ZoneEvent:register() end if self.afterLeave then - local afterLeave = EventCallback() + local afterLeave = EventCallback("ZoneEventAfterLeave", true) function afterLeave.zoneAfterCreatureLeave(zone, creature) if zone ~= self.zone then return true @@ -148,7 +148,7 @@ function ZoneEvent:register() end if self.onSpawn then - local afterEnter = EventCallback() + local afterEnter = EventCallback("ZoneEventAfterEnterOnSpawn", true) function afterEnter.zoneAfterCreatureEnter(zone, creature) if zone ~= self.zone then return true diff --git a/data/scripts/actions/items/cobra_flask.lua b/data/scripts/actions/items/cobra_flask.lua index 095a8c39a96..c36d42983c5 100644 --- a/data/scripts/actions/items/cobra_flask.lua +++ b/data/scripts/actions/items/cobra_flask.lua @@ -1,4 +1,4 @@ -local applyCobraFlaskEffectOnMonsterSpawn = EventCallback() +local applyCobraFlaskEffectOnMonsterSpawn = EventCallback("CobraFlaskEffectOnMonsterSpawn") applyCobraFlaskEffectOnMonsterSpawn.monsterOnSpawn = function(monster, position) if table.contains({ "cobra scout", "cobra vizier", "cobra assassin" }, monster:getName():lower()) then diff --git a/data/scripts/eventcallbacks/README.md b/data/scripts/eventcallbacks/README.md index ae5de046bd2..6e0bacfcd6e 100644 --- a/data/scripts/eventcallbacks/README.md +++ b/data/scripts/eventcallbacks/README.md @@ -18,6 +18,7 @@ Event callbacks are available for several categories of game entities, such as ` - `(ReturnValue)` `creatureOnTargetCombat` - `(void)` `creatureOnHear` - `(void)` `creatureOnDrainHealth` +- `(void)` `creatureOnCombat` - `(bool)` `partyOnJoin` - `(bool)` `partyOnLeave` - `(bool)` `partyOnDisband` @@ -62,7 +63,7 @@ Below are examples for each category of game entities: ### Creature Callback ```lua -local callback = EventCallback() +local callback = EventCallback("UniqueCallbackName") function callback.creatureOnAreaCombat(creature, tile, isAggressive) -- custom behavior when a creature enters combat area @@ -75,7 +76,7 @@ callback:register() ### Player Callback ```lua -local callback = EventCallback() +local callback = EventCallback("UniqueCallbackName") function callback.playerOnLook(player, position, thing, stackpos, lookDistance) -- custom behavior when a player looks at something @@ -87,7 +88,7 @@ callback:register() ### Party Callback ```lua -local callback = EventCallback() +local callback = EventCallback("UniqueCallbackName") function callback.partyOnJoin(party, player) -- custom behavior when a player joins a party @@ -99,7 +100,7 @@ callback:register() ### Monster Callback ```lua -local callback = EventCallback() +local callback = EventCallback("UniqueCallbackName") function callback.monsterOnSpawn(monster, position) -- custom behavior when a monster spawns @@ -111,7 +112,7 @@ callback:register() ### Npc Callback ```lua -local callback = EventCallback() +local callback = EventCallback("UniqueCallbackName") function callback.npcOnSpawn(npc, position) -- custom behavior when a npc spawns @@ -129,7 +130,7 @@ If the callback returns `false`, the execution of the associated function on the Here is an example of a boolean event callback: ```lua -local callback = EventCallback() +local callback = EventCallback("UniqueCallbackName") function callback.playerOnMoveItem(player, item, count, fromPos, toPos, fromCylinder, toCylinder) if item:getId() == ITEM_PARCEL then @@ -180,7 +181,7 @@ Here is an example of defining multiple callbacks for the creatureOnAreaCombat e #### Example 1 ```lua -local example1 = EventCallback() +local example1 = EventCallback("UniqueCallbackName") function example1.creatureOnAreaCombat(creature, tile, isAggressive) -- custom behavior 1 when a creature enters combat area @@ -192,7 +193,7 @@ example1:register() #### Example 2 ```lua -local example2 = EventCallback() +local example2 = EventCallback("UniqueCallbackName") function example2.creatureOnAreaCombat(creature, tile, isAggressive) -- custom behavior 2 when a creature enters combat area diff --git a/data/scripts/eventcallbacks/creature/on_area_combat.lua b/data/scripts/eventcallbacks/creature/on_area_combat.lua index f68cc95ccad..cd8720b25ad 100644 --- a/data/scripts/eventcallbacks/creature/on_area_combat.lua +++ b/data/scripts/eventcallbacks/creature/on_area_combat.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("CreatureOnAreaCombatBaseEvent") function callback.creatureOnAreaCombat(creature, tile, isAggressive) return RETURNVALUE_NOERROR diff --git a/data/scripts/eventcallbacks/creature/on_hear.lua b/data/scripts/eventcallbacks/creature/on_hear.lua index 871f6456c9a..2954c81c8fb 100644 --- a/data/scripts/eventcallbacks/creature/on_hear.lua +++ b/data/scripts/eventcallbacks/creature/on_hear.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("CreatureOnHearBaseEvent") function callback.creatureOnHear(creature, speaker, words, type) end diff --git a/data/scripts/eventcallbacks/monster/on_spawn.lua b/data/scripts/eventcallbacks/monster/on_spawn.lua index e40778aafce..5a490a8edac 100644 --- a/data/scripts/eventcallbacks/monster/on_spawn.lua +++ b/data/scripts/eventcallbacks/monster/on_spawn.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("MonsterOnSpawnBase") function callback.monsterOnSpawn(monster, position) if not monster then diff --git a/data/scripts/eventcallbacks/monster/ondroploot__base.lua b/data/scripts/eventcallbacks/monster/ondroploot__base.lua index cb7bc4207e8..0f724be9f67 100644 --- a/data/scripts/eventcallbacks/monster/ondroploot__base.lua +++ b/data/scripts/eventcallbacks/monster/ondroploot__base.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("MonsterOnDropLootBaseEvent") function Player:canReceiveLoot() return self:getStamina() > 840 @@ -15,14 +15,14 @@ function callback.monsterOnDropLoot(monster, corpse) end local mType = monster:getType() if not mType then - logger.warning("monsterOnDropLoot: monster has no type") + logger.warn("monsterOnDropLoot: monster '{}' has no type", monster:getName()) return end local charm = player and player:getCharmMonsterType(CHARM_GUT) local gut = charm and charm:raceId() == mType:raceId() - local lootTable = mType:generateLootRoll({ factor = factor, gut = gut }, {}) + local lootTable = mType:generateLootRoll({ factor = factor, gut = gut }, {}, player) corpse:addLoot(lootTable) for _, item in ipairs(lootTable) do if item.gut then diff --git a/data/scripts/eventcallbacks/monster/ondroploot_boosted.lua b/data/scripts/eventcallbacks/monster/ondroploot_boosted.lua index 1d94033a572..3bb43256772 100644 --- a/data/scripts/eventcallbacks/monster/ondroploot_boosted.lua +++ b/data/scripts/eventcallbacks/monster/ondroploot_boosted.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("MonsterOnDropLootBoosted") function callback.monsterOnDropLoot(monster, corpse) if not monster or not corpse then @@ -22,7 +22,7 @@ function callback.monsterOnDropLoot(monster, corpse) local factor = 1.0 local msgSuffix = " (boosted loot)" - corpse:addLoot(mType:generateLootRoll({ factor = factor, gut = false }, {})) + corpse:addLoot(mType:generateLootRoll({ factor = factor, gut = false }, {}, player)) local existingSuffix = corpse:getAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX) or "" corpse:setAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX, existingSuffix .. msgSuffix) diff --git a/data/scripts/eventcallbacks/monster/ondroploot_gem_atelier.lua b/data/scripts/eventcallbacks/monster/ondroploot_gem_atelier.lua index d1fb6ad38b1..4305e60e086 100644 --- a/data/scripts/eventcallbacks/monster/ondroploot_gem_atelier.lua +++ b/data/scripts/eventcallbacks/monster/ondroploot_gem_atelier.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("MonsterOnDropLootGemAtelier") function callback.monsterOnDropLoot(monster, corpse) if not monster or not corpse then diff --git a/data/scripts/eventcallbacks/monster/ondroploot_hazard.lua b/data/scripts/eventcallbacks/monster/ondroploot_hazard.lua index ddcaeaee782..78851d186e6 100644 --- a/data/scripts/eventcallbacks/monster/ondroploot_hazard.lua +++ b/data/scripts/eventcallbacks/monster/ondroploot_hazard.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("MonsterOnDropLootHazard") function callback.monsterOnDropLoot(monster, corpse) if not monster:hazard() then @@ -31,7 +31,7 @@ function callback.monsterOnDropLoot(monster, corpse) local lootTable = {} for _ = 1, rolls do - lootTable = mType:generateLootRoll({ factor = factor, gut = false }, lootTable) + lootTable = mType:generateLootRoll({ factor = factor, gut = false }, lootTable, player) end corpse:addLoot(lootTable) diff --git a/data/scripts/eventcallbacks/monster/ondroploot_prey.lua b/data/scripts/eventcallbacks/monster/ondroploot_prey.lua index 1f732f173d2..eb4657ccc4f 100644 --- a/data/scripts/eventcallbacks/monster/ondroploot_prey.lua +++ b/data/scripts/eventcallbacks/monster/ondroploot_prey.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("MonsterOnDropLootPrey") function callback.monsterOnDropLoot(monster, corpse) local player = Player(corpse:getCorpseOwner()) @@ -42,7 +42,7 @@ function callback.monsterOnDropLoot(monster, corpse) msgSuffix = msgSuffix .. " (active prey bonus)" end - corpse:addLoot(mType:generateLootRoll({ factor = factor, gut = false }, {})) + corpse:addLoot(mType:generateLootRoll({ factor = factor, gut = false }, {}, player)) local existingSuffix = corpse:getAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX) or "" corpse:setAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX, existingSuffix .. msgSuffix) end diff --git a/data/scripts/eventcallbacks/monster/ondroploot_wealth_duplex.lua b/data/scripts/eventcallbacks/monster/ondroploot_wealth_duplex.lua index 20202c0b5cc..a4f3c67fe9c 100644 --- a/data/scripts/eventcallbacks/monster/ondroploot_wealth_duplex.lua +++ b/data/scripts/eventcallbacks/monster/ondroploot_wealth_duplex.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("MonsterOnDropLootWealthDuplex") function callback.monsterOnDropLoot(monster, corpse) local player = Player(corpse:getCorpseOwner()) @@ -59,7 +59,7 @@ function callback.monsterOnDropLoot(monster, corpse) local lootTable = {} for _ = 1, rolls do - lootTable = mType:generateLootRoll({ factor = factor, gut = false }, lootTable) + lootTable = mType:generateLootRoll({ factor = factor, gut = false }, lootTable, player) end corpse:addLoot(lootTable) diff --git a/data/scripts/eventcallbacks/monster/postdroploot_analyzer.lua b/data/scripts/eventcallbacks/monster/postdroploot_analyzer.lua index 8214ac98ea1..4c1e53e6f77 100644 --- a/data/scripts/eventcallbacks/monster/postdroploot_analyzer.lua +++ b/data/scripts/eventcallbacks/monster/postdroploot_analyzer.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("MonsterPostDropLootAnalyzer") function callback.monsterPostDropLoot(monster, corpse) local player = Player(corpse:getCorpseOwner()) diff --git a/data/scripts/eventcallbacks/party/on_disband.lua b/data/scripts/eventcallbacks/party/on_disband.lua index 60838962395..93bb1578fd1 100644 --- a/data/scripts/eventcallbacks/party/on_disband.lua +++ b/data/scripts/eventcallbacks/party/on_disband.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("PartyOnDisbandEventBaseEvent") function callback.partyOnDisband(party) local members = party:getMembers() diff --git a/data/scripts/eventcallbacks/player/on_browse_field.lua b/data/scripts/eventcallbacks/player/on_browse_field.lua index a4f00341202..3b674a608a7 100644 --- a/data/scripts/eventcallbacks/player/on_browse_field.lua +++ b/data/scripts/eventcallbacks/player/on_browse_field.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("PlayerOnBrowseFieldBaseEvent") function callback.playerOnBrowseField(player, position) return true diff --git a/data/scripts/eventcallbacks/player/on_look.lua b/data/scripts/eventcallbacks/player/on_look.lua index 022aebbcc36..6b4be92553a 100644 --- a/data/scripts/eventcallbacks/player/on_look.lua +++ b/data/scripts/eventcallbacks/player/on_look.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("PlayerOnLookBaseEvent") function callback.playerOnLook(player, thing, position, distance) local description = "You see " diff --git a/data/scripts/eventcallbacks/player/on_look_in_shop.lua b/data/scripts/eventcallbacks/player/on_look_in_shop.lua index bd96296624d..0e724009b5e 100644 --- a/data/scripts/eventcallbacks/player/on_look_in_shop.lua +++ b/data/scripts/eventcallbacks/player/on_look_in_shop.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("PlayerOnLookInShopBaseEvent") function callback.playerOnLookInShop(player, itemType, count) return true diff --git a/data/scripts/eventcallbacks/player/on_look_in_trade.lua b/data/scripts/eventcallbacks/player/on_look_in_trade.lua index 77711c5b95d..c82dd9905f0 100644 --- a/data/scripts/eventcallbacks/player/on_look_in_trade.lua +++ b/data/scripts/eventcallbacks/player/on_look_in_trade.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("PlayerOnLookInTradeBaseEvent") function callback.playerOnLookInTrade(player, partner, item, distance) player:sendTextMessage(MESSAGE_LOOK, "You see " .. item:getDescription(distance)) diff --git a/data/scripts/eventcallbacks/player/on_remove_count.lua b/data/scripts/eventcallbacks/player/on_remove_count.lua index 4629d0a1b12..f39438c24ba 100644 --- a/data/scripts/eventcallbacks/player/on_remove_count.lua +++ b/data/scripts/eventcallbacks/player/on_remove_count.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("PlayerOnRemoveCountBaseEvent") function callback.playerOnRemoveCount(player, item) player:sendWaste(item:getId()) diff --git a/data/scripts/eventcallbacks/player/on_request_quest_line.lua b/data/scripts/eventcallbacks/player/on_request_quest_line.lua index c233279cfb8..ad5737d3246 100644 --- a/data/scripts/eventcallbacks/player/on_request_quest_line.lua +++ b/data/scripts/eventcallbacks/player/on_request_quest_line.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("PlayerOnRequestQuestLineBaseEvent") function callback.playerOnRequestQuestLine(player, questId) player:sendQuestLine(questId) diff --git a/data/scripts/eventcallbacks/player/on_request_quest_log.lua b/data/scripts/eventcallbacks/player/on_request_quest_log.lua index b6bcbbc3a64..15cfbd34719 100644 --- a/data/scripts/eventcallbacks/player/on_request_quest_log.lua +++ b/data/scripts/eventcallbacks/player/on_request_quest_log.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("PlayerOnRequestQuestLogBaseEvent") function callback.playerOnRequestQuestLog(player) player:sendQuestLog() diff --git a/data/scripts/eventcallbacks/player/on_rotate_item.lua b/data/scripts/eventcallbacks/player/on_rotate_item.lua index 3692fcdc655..61bb2a99b2a 100644 --- a/data/scripts/eventcallbacks/player/on_rotate_item.lua +++ b/data/scripts/eventcallbacks/player/on_rotate_item.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("PlayerOnRotateItemBaseEvent") function callback.playerOnRotateItem(player, item, position) if item:getActionId() == IMMOVABLE_ACTION_ID then diff --git a/data/scripts/eventcallbacks/player/on_storage_update.lua b/data/scripts/eventcallbacks/player/on_storage_update.lua index 8b3006b1d54..0f4233e51d2 100644 --- a/data/scripts/eventcallbacks/player/on_storage_update.lua +++ b/data/scripts/eventcallbacks/player/on_storage_update.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("PlayerOnStorageUpdateBaseEvent") function callback.playerOnStorageUpdate(player, key, value, oldValue, currentFrameTime) player:updateStorage(key, value, oldValue, currentFrameTime) diff --git a/data/scripts/eventcallbacks/player/on_trade_accept.lua b/data/scripts/eventcallbacks/player/on_trade_accept.lua index 3d1eba7171b..8425bdc10b3 100644 --- a/data/scripts/eventcallbacks/player/on_trade_accept.lua +++ b/data/scripts/eventcallbacks/player/on_trade_accept.lua @@ -1,4 +1,4 @@ -local callback = EventCallback() +local callback = EventCallback("PlayerOnTradeAcceptBaseEvent") function callback.playerOnTradeAccept(player, target, item, targetItem) player:closeForge() diff --git a/data/scripts/lib/quests.lua b/data/scripts/lib/quests.lua new file mode 100644 index 00000000000..08005e34ae2 --- /dev/null +++ b/data/scripts/lib/quests.lua @@ -0,0 +1,2 @@ +-- We need to register the variables beforehand to avoid accessing null values. +RegisterSoulWarBossesLevers() diff --git a/data/scripts/lib/register_lever_tables.lua b/data/scripts/lib/register_lever_tables.lua index 66a78f42a11..de3cba6649e 100644 --- a/data/scripts/lib/register_lever_tables.lua +++ b/data/scripts/lib/register_lever_tables.lua @@ -7,8 +7,8 @@ AscendingFerumbrasConfig = { centerRoom = Position(33392, 31473, 14), -- Center Room exitPosition = Position(33266, 31479, 14), -- Exit Position newPos = Position(33392, 31479, 14), -- Player Position on room - days = 3, + days = 5, range = 20, time = 60, -- time in minutes to remove the player - vortex = 23726, + vortex = 20121, } diff --git a/data/scripts/lib/register_monster_type.lua b/data/scripts/lib/register_monster_type.lua index e42bf4e6ca1..cfb0a6bfaaa 100644 --- a/data/scripts/lib/register_monster_type.lua +++ b/data/scripts/lib/register_monster_type.lua @@ -940,8 +940,8 @@ function readSpell(incomingLua, mtype) if incomingLua.effect then spell:setCombatEffect(incomingLua.effect) end - if incomingLua.shootEffect then - spell:setCombatShootEffect(incomingLua.shootEffect) + if incomingLua.shootEffect or incomingLua.shooteffect then + spell:setCombatShootEffect(incomingLua.shootEffect or incomingLua.shooteffect) end end diff --git a/data/scripts/lib/register_spells.lua b/data/scripts/lib/register_spells.lua index 49a5d7aec2f..8c549859640 100644 --- a/data/scripts/lib/register_spells.lua +++ b/data/scripts/lib/register_spells.lua @@ -393,6 +393,12 @@ AREA_RING1_BURST3 = { { 0, 0, 0, 1, 1, 1, 0, 0, 0 }, } +CrossBeamArea3X2 = { + { 1, 1, 1 }, + { 0, 1, 0 }, + { 0, 3, 0 }, +} + -- The numbered-keys represents the damage values, and their table -- contains the minimum and maximum number of rounds of those damage values. RANGE = { diff --git a/data/scripts/spells/healing/heal_malice.lua b/data/scripts/spells/healing/heal_malice.lua new file mode 100644 index 00000000000..67211a58341 --- /dev/null +++ b/data/scripts/spells/healing/heal_malice.lua @@ -0,0 +1,29 @@ +function onTargetCreature(creature, target) + if target:getName() == "Goshnar's Malice" then + logger.debug("Monster {} Healing {}", creature:getName(), target:getName()) + local min = 15000 + local max = 30000 + doTargetCombatHealth(target, target, COMBAT_HEALING, min, max) + end + return true +end + +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_RED) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, 0) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) +combat:setCallback(CALLBACK_PARAM_TARGETCREATURE, "onTargetCreature") + +local spell = Spell("instant") + +function spell.onCastSpell(creature, var) + return combat:execute(creature, var) +end + +spell:name("Heal Malice") +spell:words("#####462") +spell:blockWalls(true) +spell:needLearn(true) +spell:needDirection(true) +spell:register() diff --git a/data/scripts/systems/reward_chest.lua b/data/scripts/systems/reward_chest.lua index 384115af0b0..07ec9b512a9 100644 --- a/data/scripts/systems/reward_chest.lua +++ b/data/scripts/systems/reward_chest.lua @@ -103,9 +103,9 @@ function bossDeath.onDeath(creature, corpse, killer, mostDamageKiller, lastHitUn end local playerLoot = creature:generateGemAtelierLoot() - playerLoot = monsterType:getBossReward(lootFactor, _ == 1, false, playerLoot) + playerLoot = monsterType:getBossReward(lootFactor, _ == 1, false, playerLoot, player) for _ = 2, rolls do - playerLoot = monsterType:getBossReward(lootFactor, false, true, playerLoot) + playerLoot = monsterType:getBossReward(lootFactor, false, true, playerLoot, player) end -- Add droped items to reward container diff --git a/data/scripts/talkactions/gm/afk.lua b/data/scripts/talkactions/gm/afk.lua index 6167a2b6068..f5342362113 100644 --- a/data/scripts/talkactions/gm/afk.lua +++ b/data/scripts/talkactions/gm/afk.lua @@ -69,7 +69,7 @@ afkEffect:interval(5000) afkEffect:register() ------------------ Stop AFK Message when moves ------------------ -local callback = EventCallback() +local callback = EventCallback("PlayerOnWalk") function callback.playerOnWalk(player, creature, creaturePos, toPos) local isAfk = checkIsAFK(player:getId()) if isAfk.afk then diff --git a/data/scripts/talkactions/gm/distance_effect.lua b/data/scripts/talkactions/gm/distance_effect.lua new file mode 100644 index 00000000000..4c637972cb7 --- /dev/null +++ b/data/scripts/talkactions/gm/distance_effect.lua @@ -0,0 +1,37 @@ +local magicEffect = TalkAction("/distanceeffect") + +function magicEffect.onSay(player, words, param) + -- create log + logCommand(player, words, param) + + if param == "" then + player:sendCancelMessage("Command param required.") + return true + end + + local effect = tonumber(param) + if effect ~= nil and effect > 0 then + local playerPos = player:getPosition() + local direction = player:getDirection() + local targetPos = Position(playerPos.x, playerPos.y, playerPos.z) + + local distance = 7 + if direction == DIRECTION_NORTH then + targetPos.y = targetPos.y - distance + elseif direction == DIRECTION_EAST then + targetPos.x = targetPos.x + distance + elseif direction == DIRECTION_SOUTH then + targetPos.y = targetPos.y + distance + elseif direction == DIRECTION_WEST then + targetPos.x = targetPos.x - distance + end + + player:getPosition():sendDistanceEffect(targetPos, effect) + end + + return true +end + +magicEffect:separator(" ") +magicEffect:groupType("gamemaster") +magicEffect:register() diff --git a/data/scripts/talkactions/gm/position.lua b/data/scripts/talkactions/gm/position.lua index 1869b109c5f..dc95552d5c2 100644 --- a/data/scripts/talkactions/gm/position.lua +++ b/data/scripts/talkactions/gm/position.lua @@ -1,23 +1,5 @@ local position = TalkAction("/pos", "!pos") -local function extractCoordinates(input) - local patterns = { - -- table format - "{%s*x%s*=%s*(%d+)%s*,%s*y%s*=%s*(%d+)%s*,%s*z%s*=%s*(%d+)%s*}", - -- Position format - "Position%s*%((%d+)%s*,%s*(%d+)%s*,%s*(%d+)%s*%)", - -- x, y, z format - "(%d+)%s*,%s*(%d+)%s*,%s*(%d+)", - } - - for _, pattern in ipairs(patterns) do - local x, y, z = string.match(input, pattern) - if x and y and z then - return tonumber(x), tonumber(y), tonumber(z) - end - end -end - function position.onSay(player, words, param) -- create log logCommand(player, words, param) @@ -28,18 +10,21 @@ function position.onSay(player, words, param) return end - local x, y, z = extractCoordinates(param) - if x and y and z then - local teleportPosition = Position(x, y, z) - local tile = Tile(teleportPosition) - if not tile then - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Invalid tile or position. Send a valid position.") - return - end - - player:teleportTo(teleportPosition) - else + local teleportPosition = param:toPosition() + if not teleportPosition then player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Invalid position format. Use one of the following formats: \n/pos {x = ..., y = ..., z = ...}\n/pos Position(..., ..., ...)\n/pos x, y, z.") + return + end + + local tile = Tile(teleportPosition) + if not tile then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Invalid tile or position. Send a valid position.") + return + end + + player:teleportTo(teleportPosition) + if not player:isInGhostMode() then + teleportPosition:sendMagicEffect(CONST_ME_TELEPORT) end end diff --git a/data/scripts/talkactions/god/add_condition.lua b/data/scripts/talkactions/god/add_condition.lua new file mode 100644 index 00000000000..7d92cbc0a09 --- /dev/null +++ b/data/scripts/talkactions/god/add_condition.lua @@ -0,0 +1,10 @@ +local talkaction = TalkAction("/testtaintconditions") + +function talkaction.onSay(player, words, param) + player:setTaintIcon() + return false +end + +talkaction:separator(" ") +talkaction:groupType("god") +talkaction:register() diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index 6390d0699b3..80126ddac93 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -379,7 +379,7 @@ bool ConfigManager::reload() { } void ConfigManager::missingConfigWarning(const char* identifier) { - g_logger().warn("[{}]: Missing configuration for identifier: {}", __FUNCTION__, identifier); + g_logger().debug("[{}]: Missing configuration for identifier: {}", __FUNCTION__, identifier); } std::string ConfigManager::loadStringConfig(lua_State* L, const ConfigKey_t &key, const char* identifier, const std::string &defaultValue) { diff --git a/src/creatures/appearance/outfit/outfit.hpp b/src/creatures/appearance/outfit/outfit.hpp index 0d89a2c932d..c4d49a7f1a2 100644 --- a/src/creatures/appearance/outfit/outfit.hpp +++ b/src/creatures/appearance/outfit/outfit.hpp @@ -54,6 +54,16 @@ class Outfits { return outfits[sex]; } + std::shared_ptr getOutfitByName(PlayerSex_t sex, const std::string &name) const { + for (const auto &outfit : outfits[sex]) { + if (outfit->name == name) { + return outfit; + } + } + + return nullptr; + } + private: std::vector> outfits[PLAYERSEX_LAST + 1]; }; diff --git a/src/creatures/combat/combat.cpp b/src/creatures/combat/combat.cpp index 8e3de0da1bd..d65d10e4e6f 100644 --- a/src/creatures/combat/combat.cpp +++ b/src/creatures/combat/combat.cpp @@ -12,6 +12,8 @@ #include "declarations.hpp" #include "creatures/combat/combat.hpp" #include "lua/creature/events.hpp" +#include "lua/callbacks/event_callback.hpp" +#include "lua/callbacks/events_callbacks.hpp" #include "creatures/players/wheel/player_wheel.hpp" #include "game/game.hpp" #include "game/scheduling/dispatcher.hpp" @@ -595,10 +597,15 @@ void Combat::CombatHealthFunc(std::shared_ptr caster, std::shared_ptr< targetPlayer = target->getPlayer(); } + g_logger().trace("[{}] (old) eventcallback: 'creatureOnCombat', damage primary: '{}', secondary: '{}'", __FUNCTION__, damage.primary.value, damage.secondary.value); + g_callbacks().executeCallback(EventCallback_t::creatureOnCombat, &EventCallback::creatureOnCombat, caster, target, std::ref(damage)); + g_logger().trace("[{}] (new) eventcallback: 'creatureOnCombat', damage primary: '{}', secondary: '{}'", __FUNCTION__, damage.primary.value, damage.secondary.value); + if (attackerPlayer) { std::shared_ptr item = attackerPlayer->getWeapon(); damage = applyImbuementElementalDamage(attackerPlayer, item, damage); g_events().eventPlayerOnCombat(attackerPlayer, target, item, damage); + g_callbacks().executeCallback(EventCallback_t::playerOnCombat, &EventCallback::playerOnCombat, attackerPlayer, target, item, std::ref(damage)); if (targetPlayer && targetPlayer->getSkull() != SKULL_BLACK) { if (damage.primary.type != COMBAT_HEALING) { @@ -624,6 +631,9 @@ void Combat::CombatHealthFunc(std::shared_ptr caster, std::shared_ptr< damage.primary.value += static_cast(std::ceil((damage.primary.value * slot->bonusPercentage) / 100)); damage.secondary.value += static_cast(std::ceil((damage.secondary.value * slot->bonusPercentage) / 100)); } + + // Monster type onPlayerAttack event + targetMonster->onAttackedByPlayer(attackerPlayer); } // Monster attacking player @@ -1157,7 +1167,9 @@ void Combat::CombatFunc(std::shared_ptr caster, const Position &origin if (CreatureVector* creatures = tile->getCreatures()) { const std::shared_ptr topCreature = tile->getTopCreature(); - for (auto &creature : *creatures) { + // A copy of the tile's creature list is made because modifications to this vector, such as adding or removing creatures through a Lua callback, may occur during the iteration within the for loop. + CreatureVector creaturesCopy = *creatures; + for (auto &creature : creaturesCopy) { if (params.targetCasterOrTopMost) { if (caster && caster->getTile() == tile) { if (creature != caster) { @@ -1212,7 +1224,9 @@ void Combat::CombatFunc(std::shared_ptr caster, const Position &origin if (CreatureVector* creatures = tile->getCreatures()) { const std::shared_ptr topCreature = tile->getTopCreature(); - for (auto &creature : *creatures) { + // A copy of the tile's creature list is made because modifications to this vector, such as adding or removing creatures through a Lua callback, may occur during the iteration within the for loop. + CreatureVector creaturesCopy = *creatures; + for (auto &creature : creaturesCopy) { if (params.targetCasterOrTopMost) { if (caster && caster->getTile() == tile) { if (creature != caster) { diff --git a/src/creatures/combat/condition.cpp b/src/creatures/combat/condition.cpp index c06d9a792d5..6784e8786f3 100644 --- a/src/creatures/combat/condition.cpp +++ b/src/creatures/combat/condition.cpp @@ -257,6 +257,8 @@ std::shared_ptr Condition::createCondition(ConditionId_t id, Conditio return std::make_shared(id, type, ticks, buff, subId); case CONDITION_BAKRAGORE: return std::make_shared(id, type, ticks, buff, subId, isPersistent); + case CONDITION_GOSHNARTAINT: + return std::make_shared(id, type, ticks, buff, subId); default: return nullptr; @@ -345,7 +347,14 @@ bool Condition::isRemovableOnDeath() const { return false; } - if (conditionType == CONDITION_SPELLCOOLDOWN || conditionType == CONDITION_SPELLGROUPCOOLDOWN || conditionType == CONDITION_MUTED) { + static const std::unordered_set nonRemovableConditions = { + CONDITION_SPELLCOOLDOWN, + CONDITION_SPELLGROUPCOOLDOWN, + CONDITION_MUTED, + CONDITION_GOSHNARTAINT + }; + + if (nonRemovableConditions.find(conditionType) != nonRemovableConditions.end()) { return false; } @@ -418,7 +427,26 @@ std::unordered_set ConditionGeneric::getIcons() const { case CONDITION_ROOTED: icons.insert(PlayerIcon::Rooted); break; - + case CONDITION_GOSHNARTAINT: + switch (subId) { + case 1: + icons.insert(PlayerIcon::GoshnarTaint1); + break; + case 2: + icons.insert(PlayerIcon::GoshnarTaint2); + break; + case 3: + icons.insert(PlayerIcon::GoshnarTaint3); + break; + case 4: + icons.insert(PlayerIcon::GoshnarTaint4); + break; + case 5: + icons.insert(PlayerIcon::GoshnarTaint5); + break; + default: + break; + } default: break; } diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index 6c15f6cf1fe..a5cd66a629f 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -1804,7 +1804,7 @@ void Creature::handleLostSummon(bool teleportSummons) { g_game().addMagicEffect(getPosition(), CONST_ME_POFF); } -int32_t Creature::getReflectPercent(CombatType_t combatType, bool useCharges /*= false*/) const { +double_t Creature::getReflectPercent(CombatType_t combatType, bool useCharges /*= false*/) const { try { return reflectPercent.at(combatTypeToIndex(combatType)); } catch (const std::out_of_range &e) { diff --git a/src/creatures/creature.hpp b/src/creatures/creature.hpp index 138e8b364c8..a274b49364c 100644 --- a/src/creatures/creature.hpp +++ b/src/creatures/creature.hpp @@ -255,6 +255,10 @@ class Creature : virtual public Thing, public SharedObject { return creatureIcons.at(key); } + bool hasIcon(const std::string &key) const { + return creatureIcons.contains(key); + } + void setIcon(const std::string &key, CreatureIcon icon) { creatureIcons[key] = icon; iconChanged(); @@ -609,7 +613,7 @@ class Creature : virtual public Thing, public SharedObject { * @param useCharges Indicates whether charges should be considered. * @return The reflection percentage for the specified combat type. */ - virtual int32_t getReflectPercent(CombatType_t combatType, bool useCharges = false) const; + virtual double_t getReflectPercent(CombatType_t combatType, bool useCharges = false) const; /** * @brief Retrieves the flat reflection value for a given combat type. @@ -707,6 +711,10 @@ class Creature : virtual public Thing, public SharedObject { return false; } + virtual bool isDead() const { + return false; + } + static constexpr int32_t mapWalkWidth = MAP_MAX_VIEW_PORT_X * 2 + 1; static constexpr int32_t mapWalkHeight = MAP_MAX_VIEW_PORT_Y * 2 + 1; static constexpr int32_t maxWalkCacheWidth = (mapWalkWidth - 1) / 2; diff --git a/src/creatures/creatures_definitions.hpp b/src/creatures/creatures_definitions.hpp index 410349a67fb..9594a704eed 100644 --- a/src/creatures/creatures_definitions.hpp +++ b/src/creatures/creatures_definitions.hpp @@ -113,15 +113,11 @@ enum ConditionType_t : uint8_t { CONDITION_LESSERHEX = 31, CONDITION_INTENSEHEX = 32, CONDITION_GREATERHEX = 33, - CONDITION_GOSHNAR1 = 34, - CONDITION_GOSHNAR2 = 35, - CONDITION_GOSHNAR3 = 36, - CONDITION_GOSHNAR4 = 37, - CONDITION_GOSHNAR5 = 38, - CONDITION_BAKRAGORE = 39, + CONDITION_BAKRAGORE = 34, + CONDITION_GOSHNARTAINT = 35, // Need the last ever - CONDITION_COUNT = 39 + CONDITION_COUNT }; // constexpr definiting suppressible conditions @@ -492,12 +488,14 @@ enum BestiaryType_t : uint8_t { }; enum MonstersEvent_t : uint8_t { - MONSTERS_EVENT_NONE = 0, - MONSTERS_EVENT_THINK = 1, - MONSTERS_EVENT_APPEAR = 2, - MONSTERS_EVENT_DISAPPEAR = 3, - MONSTERS_EVENT_MOVE = 4, - MONSTERS_EVENT_SAY = 5, + MONSTERS_EVENT_NONE, + MONSTERS_EVENT_THINK, + MONSTERS_EVENT_APPEAR, + MONSTERS_EVENT_DISAPPEAR, + MONSTERS_EVENT_MOVE, + MONSTERS_EVENT_SAY, + MONSTERS_EVENT_ATTACKED_BY_PLAYER, + MONSTERS_EVENT_ON_SPAWN, }; enum NpcsEvent_t : uint8_t { diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index 3ae73f8dc7c..1a364ece2ba 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -108,15 +108,57 @@ bool Monster::canWalkOnFieldType(CombatType_t combatType) const { } } -int32_t Monster::getReflectPercent(CombatType_t reflectType, bool useCharges) const { - int32_t result = Creature::getReflectPercent(reflectType, useCharges); +double_t Monster::getReflectPercent(CombatType_t reflectType, bool useCharges) const { + // Monster type reflect + auto result = Creature::getReflectPercent(reflectType, useCharges); + if (result != 0) { + g_logger().debug("[{}] before mtype reflect element {}, percent {}", __FUNCTION__, fmt::underlying(reflectType), result); + } auto it = mType->info.reflectMap.find(reflectType); if (it != mType->info.reflectMap.end()) { result += it->second; } + + if (result != 0) { + g_logger().debug("[{}] after mtype reflect element {}, percent {}", __FUNCTION__, fmt::underlying(reflectType), result); + } + + // Monster reflect + auto monsterReflectIt = m_reflectElementMap.find(reflectType); + if (monsterReflectIt != m_reflectElementMap.end()) { + result += monsterReflectIt->second; + } + + if (result != 0) { + g_logger().debug("[{}] (final) after monster reflect element {}, percent {}", __FUNCTION__, fmt::underlying(reflectType), result); + } + return result; } +void Monster::addReflectElement(CombatType_t combatType, int32_t percent) { + g_logger().debug("[{}] added reflect element {}, percent {}", __FUNCTION__, fmt::underlying(combatType), percent); + m_reflectElementMap[combatType] += percent; +} + +int32_t Monster::getDefense() const { + auto mtypeDefense = mType->info.defense; + if (mtypeDefense != 0) { + g_logger().trace("[{}] old defense {}", __FUNCTION__, mtypeDefense); + } + mtypeDefense += m_defense; + if (mtypeDefense != 0) { + g_logger().trace("[{}] new defense {}", __FUNCTION__, mtypeDefense); + } + return mtypeDefense * getDefenseMultiplier(); +} + +void Monster::addDefense(int32_t defense) { + g_logger().trace("[{}] adding defense {}", __FUNCTION__, defense); + m_defense += defense; + g_logger().trace("[{}] new defense {}", __FUNCTION__, m_defense); +} + uint32_t Monster::getHealingCombatValue(CombatType_t healingType) const { auto it = mType->info.healingMap.find(healingType); if (it != mType->info.healingMap.end()) { @@ -315,6 +357,57 @@ void Monster::onCreatureSay(std::shared_ptr creature, SpeakClasses typ } } +void Monster::onAttackedByPlayer(std::shared_ptr attackerPlayer) { + if (mType->info.monsterAttackedByPlayerEvent != -1) { + // onPlayerAttack(self, attackerPlayer) + LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + if (!scriptInterface->reserveScriptEnv()) { + g_logger().error("Monster {} creature {}] Call stack overflow. Too many lua " + "script calls being nested.", + getName(), this->getName()); + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(mType->info.monsterAttackedByPlayerEvent, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(mType->info.monsterAttackedByPlayerEvent); + + LuaScriptInterface::pushUserdata(L, getMonster()); + LuaScriptInterface::setMetatable(L, -1, "Monster"); + + LuaScriptInterface::pushUserdata(L, attackerPlayer); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + scriptInterface->callVoidFunction(2); + } +} + +void Monster::onSpawn() { + if (mType->info.spawnEvent != -1) { + // onSpawn(self) + LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + if (!scriptInterface->reserveScriptEnv()) { + g_logger().error("Monster {} creature {}] Call stack overflow. Too many lua " + "script calls being nested.", + getName(), this->getName()); + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(mType->info.spawnEvent, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(mType->info.spawnEvent); + + LuaScriptInterface::pushUserdata(L, getMonster()); + LuaScriptInterface::setMetatable(L, -1, "Monster"); + + scriptInterface->callVoidFunction(1); + } +} + void Monster::addFriend(const std::shared_ptr &creature) { if (creature == getMonster()) { g_logger().error("[{}]: adding creature is same of monster", __FUNCTION__); @@ -1921,6 +2014,8 @@ void Monster::death(std::shared_ptr) { if (mType) { g_game().sendSingleSoundEffect(static_self_cast()->getPosition(), mType->info.deathSound, getMonster()); } + + setDead(true); } std::shared_ptr Monster::getCorpse(std::shared_ptr lastHitCreature, std::shared_ptr mostDamageCreature) { @@ -2122,11 +2217,11 @@ bool Monster::changeTargetDistance(int32_t distance, uint32_t duration /* = 1200 } bool Monster::isImmune(ConditionType_t conditionType) const { - return mType->info.m_conditionImmunities[static_cast(conditionType)]; + return m_isImmune || mType->info.m_conditionImmunities[static_cast(conditionType)]; } bool Monster::isImmune(CombatType_t combatType) const { - return mType->info.m_damageImmunities[combatTypeToIndex(combatType)]; + return m_isImmune || mType->info.m_damageImmunities[combatTypeToIndex(combatType)]; } void Monster::getPathSearchParams(const std::shared_ptr &creature, FindPathParams &fpp) { diff --git a/src/creatures/monsters/monster.hpp b/src/creatures/monsters/monster.hpp index 061ad2b9879..155ba1e5c87 100644 --- a/src/creatures/monsters/monster.hpp +++ b/src/creatures/monsters/monster.hpp @@ -77,9 +77,9 @@ class Monster final : public Creature { int32_t getArmor() const override { return mType->info.armor * getDefenseMultiplier(); } - int32_t getDefense() const override { - return mType->info.defense * getDefenseMultiplier(); - } + int32_t getDefense() const override; + + void addDefense(int32_t defense); Faction_t getFaction() const override { auto master = getMaster(); @@ -134,9 +134,11 @@ class Monster final : public Creature { this->spawnMonster = newSpawnMonster; } - int32_t getReflectPercent(CombatType_t combatType, bool useCharges = false) const override; + double_t getReflectPercent(CombatType_t combatType, bool useCharges = false) const override; uint32_t getHealingCombatValue(CombatType_t healingType) const; + void addReflectElement(CombatType_t combatType, int32_t percent); + bool canWalkOnFieldType(CombatType_t combatType) const; void onAttackedCreatureDisappear(bool isLogout) override; @@ -144,6 +146,8 @@ class Monster final : public Creature { void onRemoveCreature(std::shared_ptr creature, bool isLogout) override; void onCreatureMove(const std::shared_ptr &creature, const std::shared_ptr &newTile, const Position &newPos, const std::shared_ptr &oldTile, const Position &oldPos, bool teleport) override; void onCreatureSay(std::shared_ptr creature, SpeakClasses type, const std::string &text) override; + void onAttackedByPlayer(std::shared_ptr attackerPlayer); + void onSpawn(); void drainHealth(std::shared_ptr attacker, int32_t damage) override; void changeHealth(int32_t healthChange, bool sendHealthChange = true) override; @@ -333,6 +337,12 @@ class Monster final : public Creature { bool isImmune(ConditionType_t conditionType) const override; bool isImmune(CombatType_t combatType) const override; + void setImmune(bool immune) { + m_isImmune = immune; + } + bool isImmune() const { + return m_isImmune; + } float getAttackMultiplier() const { float multiplier = mType->getAttackMultiplier(); @@ -347,6 +357,14 @@ class Monster final : public Creature { return multiplier * std::pow(1.02f, getForgeStack()); } + bool isDead() const override { + return m_isDead; + } + + void setDead(bool isDead) { + m_isDead = isDead; + } + private: auto getTargetIterator(const std::shared_ptr &creature) { return std::ranges::find_if(targetList.begin(), targetList.end(), [id = creature->getID()](const std::weak_ptr &ref) { @@ -372,6 +390,8 @@ class Monster final : public Creature { int64_t lastMeleeAttack = 0; + uint16_t totalPlayersOnScreen = 0; + uint32_t attackTicks = 0; uint32_t targetChangeTicks = 0; uint32_t defenseTicks = 0; @@ -385,8 +405,10 @@ class Monster final : public Creature { int32_t stepDuration = 0; int32_t targetDistance = 1; int32_t challengeMeleeDuration = 0; - uint16_t totalPlayersOnScreen = 0; int32_t runAwayHealth = 0; + int32_t m_defense = 0; + + std::unordered_map m_reflectElementMap; Position masterPos; @@ -402,6 +424,9 @@ class Monster final : public Creature { bool hazardDamageBoost = false; bool hazardDefenseBoost = false; + bool m_isDead = false; + bool m_isImmune = false; + void onCreatureEnter(std::shared_ptr creature); void onCreatureLeave(std::shared_ptr creature); void onCreatureFound(std::shared_ptr creature, bool pushFront = false); diff --git a/src/creatures/monsters/monsters.cpp b/src/creatures/monsters/monsters.cpp index 78358f69d87..7e192a79575 100644 --- a/src/creatures/monsters/monsters.cpp +++ b/src/creatures/monsters/monsters.cpp @@ -278,17 +278,34 @@ bool MonsterType::loadCallback(LuaScriptInterface* scriptInterface) { } info.scriptInterface = scriptInterface; - if (info.eventType == MONSTERS_EVENT_THINK) { - info.thinkEvent = id; - } else if (info.eventType == MONSTERS_EVENT_APPEAR) { - info.creatureAppearEvent = id; - } else if (info.eventType == MONSTERS_EVENT_DISAPPEAR) { - info.creatureDisappearEvent = id; - } else if (info.eventType == MONSTERS_EVENT_MOVE) { - info.creatureMoveEvent = id; - } else if (info.eventType == MONSTERS_EVENT_SAY) { - info.creatureSayEvent = id; + + switch (info.eventType) { + case MONSTERS_EVENT_THINK: + info.thinkEvent = id; + break; + case MONSTERS_EVENT_APPEAR: + info.creatureAppearEvent = id; + break; + case MONSTERS_EVENT_DISAPPEAR: + info.creatureDisappearEvent = id; + break; + case MONSTERS_EVENT_MOVE: + info.creatureMoveEvent = id; + break; + case MONSTERS_EVENT_SAY: + info.creatureSayEvent = id; + break; + case MONSTERS_EVENT_ATTACKED_BY_PLAYER: + info.monsterAttackedByPlayerEvent = id; + break; + case MONSTERS_EVENT_ON_SPAWN: + info.spawnEvent = id; + break; + default: + g_logger().error("[MonsterType::loadCallback] - Unknown event type"); + return false; } + return true; } diff --git a/src/creatures/monsters/monsters.hpp b/src/creatures/monsters/monsters.hpp index 171bef68e38..88d25e195c6 100644 --- a/src/creatures/monsters/monsters.hpp +++ b/src/creatures/monsters/monsters.hpp @@ -120,7 +120,9 @@ class MonsterType { int32_t creatureDisappearEvent = -1; int32_t creatureMoveEvent = -1; int32_t creatureSayEvent = -1; + int32_t monsterAttackedByPlayerEvent = -1; int32_t thinkEvent = -1; + int32_t spawnEvent = -1; int32_t targetDistance = 1; int32_t runAwayHealth = 0; int32_t health = 100; diff --git a/src/creatures/monsters/spawns/spawn_monster.cpp b/src/creatures/monsters/spawns/spawn_monster.cpp index ca8f4584775..846068e67b7 100644 --- a/src/creatures/monsters/spawns/spawn_monster.cpp +++ b/src/creatures/monsters/spawns/spawn_monster.cpp @@ -177,7 +177,7 @@ bool SpawnMonster::spawnMonster(uint32_t spawnMonsterId, spawnBlock_t &sb, const return false; } } else { - g_logger().debug("[SpawnMonster] Spawning {} at {}", monsterType->name, sb.pos.toString()); + g_logger().trace("[SpawnMonster] Spawning {} at {}", monsterType->name, sb.pos.toString()); if (!g_game().placeCreature(monster, sb.pos, false, true)) { return false; } @@ -190,6 +190,7 @@ bool SpawnMonster::spawnMonster(uint32_t spawnMonsterId, spawnBlock_t &sb, const spawnedMonsterMap[spawnMonsterId] = monster; sb.lastSpawn = OTSYS_TIME(); g_events().eventMonsterOnSpawn(monster, sb.pos); + monster->onSpawn(); g_callbacks().executeCallback(EventCallback_t::monsterOnSpawn, &EventCallback::monsterOnSpawn, monster, sb.pos); return true; } diff --git a/src/creatures/players/grouping/party.cpp b/src/creatures/players/grouping/party.cpp index 76f1d955f10..b56e0c68605 100644 --- a/src/creatures/players/grouping/party.cpp +++ b/src/creatures/players/grouping/party.cpp @@ -454,7 +454,7 @@ void Party::shareExperience(uint64_t experience, std::shared_ptr targe uint64_t shareExperience = experience; g_events().eventPartyOnShareExperience(getParty(), shareExperience); - g_callbacks().executeCallback(EventCallback_t::partyOnShareExperience, &EventCallback::partyOnShareExperience, getParty(), shareExperience); + g_callbacks().executeCallback(EventCallback_t::partyOnShareExperience, &EventCallback::partyOnShareExperience, getParty(), std::ref(shareExperience)); for (const auto &member : getMembers()) { member->onGainSharedExperience(shareExperience, target); diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 9ba9f3b74d6..6f7e33eb7f9 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -677,7 +677,7 @@ void Player::addSkillAdvance(skills_t skill, uint64_t count) { } g_events().eventPlayerOnGainSkillTries(static_self_cast(), skill, count); - g_callbacks().executeCallback(EventCallback_t::playerOnGainSkillTries, &EventCallback::playerOnGainSkillTries, getPlayer(), skill, count); + g_callbacks().executeCallback(EventCallback_t::playerOnGainSkillTries, &EventCallback::playerOnGainSkillTries, getPlayer(), std::ref(skill), std::ref(count)); if (count == 0) { return; } @@ -2236,6 +2236,8 @@ void Player::onThink(uint32_t interval) { // Wheel of destiny major spells wheel()->onThink(); + + g_callbacks().executeCallback(EventCallback_t::playerOnThink, &EventCallback::playerOnThink, getPlayer(), interval); } uint32_t Player::isMuted() const { @@ -2369,7 +2371,7 @@ void Player::addExperience(std::shared_ptr target, uint64_t exp, bool return; } - g_callbacks().executeCallback(EventCallback_t::playerOnGainExperience, &EventCallback::playerOnGainExperience, getPlayer(), target, exp, rawExp); + g_callbacks().executeCallback(EventCallback_t::playerOnGainExperience, &EventCallback::playerOnGainExperience, getPlayer(), target, std::ref(exp), std::ref(rawExp)); g_events().eventPlayerOnGainExperience(static_self_cast(), target, exp, rawExp); if (exp == 0) { @@ -2485,7 +2487,7 @@ void Player::removeExperience(uint64_t exp, bool sendText /* = false*/) { } g_events().eventPlayerOnLoseExperience(static_self_cast(), exp); - g_callbacks().executeCallback(EventCallback_t::playerOnLoseExperience, &EventCallback::playerOnLoseExperience, getPlayer(), exp); + g_callbacks().executeCallback(EventCallback_t::playerOnLoseExperience, &EventCallback::playerOnLoseExperience, getPlayer(), std::ref(exp)); if (exp == 0) { return; } @@ -5564,15 +5566,15 @@ int32_t Player::getMagicShieldCapacityPercent(bool useCharges) const { return result; } -int32_t Player::getReflectPercent(CombatType_t combat, bool useCharges) const { - int32_t result = reflectPercent[combatTypeToIndex(combat)]; - for (const auto &item : getEquippedItems()) { +double_t Player::getReflectPercent(CombatType_t combat, bool useCharges) const { + double_t result = reflectPercent[combatTypeToIndex(combat)]; + for (const auto item : getEquippedItems()) { const ItemType &itemType = Item::items[item->getID()]; if (!itemType.abilities) { continue; } - int32_t reflectPercent = itemType.abilities->reflectPercent[combatTypeToIndex(combat)]; + double_t reflectPercent = itemType.abilities->reflectPercent[combatTypeToIndex(combat)]; if (reflectPercent != 0) { result += reflectPercent; uint16_t charges = item->getCharges(); @@ -5991,7 +5993,7 @@ bool Player::addOfflineTrainingTries(skills_t skill, uint64_t tries) { oldPercentToNextLevel = static_cast(manaSpent * 100) / nextReqMana; g_events().eventPlayerOnGainSkillTries(static_self_cast(), SKILL_MAGLEVEL, tries); - g_callbacks().executeCallback(EventCallback_t::playerOnGainSkillTries, &EventCallback::playerOnGainSkillTries, getPlayer(), SKILL_MAGLEVEL, tries); + g_callbacks().executeCallback(EventCallback_t::playerOnGainSkillTries, &EventCallback::playerOnGainSkillTries, getPlayer(), SKILL_MAGLEVEL, std::ref(tries)); uint32_t currMagLevel = magLevel; while ((manaSpent + tries) >= nextReqMana) { diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 5a703a1d0bf..124ad59ed6b 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -417,7 +417,7 @@ class Player final : public Creature, public Cylinder, public Bankable { magicShieldCapacityPercent += value; } - int32_t getReflectPercent(CombatType_t combat, bool useCharges = false) const override; + double_t getReflectPercent(CombatType_t combat, bool useCharges = false) const override; int32_t getReflectFlat(CombatType_t combat, bool useCharges = false) const override; @@ -2944,7 +2944,7 @@ class Player final : public Creature, public Cylinder, public Bankable { bool marketMenu = false; // Menu option 'show in market' bool exerciseTraining = false; bool moved = false; - bool dead = false; + bool m_isDead = false; bool imbuementTrackerWindowOpen = false; // Hazard system @@ -3014,10 +3014,10 @@ class Player final : public Creature, public Cylinder, public Bankable { void getPathSearchParams(const std::shared_ptr &creature, FindPathParams &fpp) override; void setDead(bool isDead) { - dead = isDead; + m_isDead = isDead; } - bool isDead() const { - return dead; + bool isDead() const override { + return m_isDead; } void triggerMomentum(); diff --git a/src/enums/player_icons.hpp b/src/enums/player_icons.hpp index c289144bd7b..7878d9e5037 100644 --- a/src/enums/player_icons.hpp +++ b/src/enums/player_icons.hpp @@ -35,11 +35,11 @@ enum class PlayerIcon : uint8_t { GreaterHex = 18, Rooted = 19, Feared = 20, - Goshnar1 = 21, - Goshnar2 = 22, - Goshnar3 = 23, - Goshnar4 = 24, - Goshnar5 = 25, + GoshnarTaint1 = 21, + GoshnarTaint2 = 22, + GoshnarTaint3 = 23, + GoshnarTaint4 = 24, + GoshnarTaint5 = 25, NewManaShield = 26, Agony = 27, diff --git a/src/game/bank/bank.cpp b/src/game/bank/bank.cpp index d9a056396ce..b6f3b14c6e5 100644 --- a/src/game/bank/bank.cpp +++ b/src/game/bank/bank.cpp @@ -130,6 +130,10 @@ bool Bank::transferTo(const std::shared_ptr destination, uint64_t amount) } bool Bank::withdraw(std::shared_ptr player, uint64_t amount) { + if (!player) { + return false; + } + if (!debit(amount)) { return false; } diff --git a/src/game/game.cpp b/src/game/game.cpp index df1b401bf48..7b60220ecf9 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -1165,6 +1165,7 @@ bool Game::removeCreature(std::shared_ptr creature, bool isLogout /* = size_t i = 0; for (const auto &spectator : playersSpectators) { if (const auto &player = spectator->getPlayer()) { + player->sendMagicEffect(tilePosition, CONST_ME_POFF); player->sendRemoveTileThing(tilePosition, oldStackPosVector[i++]); } } @@ -1476,6 +1477,10 @@ ReturnValue Game::internalMoveCreature(std::shared_ptr creature, Direc return RETURNVALUE_NOTPOSSIBLE; } + if (creature->getBaseSpeed() == 0) { + return RETURNVALUE_NOTMOVABLE; + } + creature->setLastPosition(creature->getPosition()); const Position ¤tPos = creature->getPosition(); Position destPos = getNextPosition(direction, currentPos); @@ -7109,7 +7114,7 @@ bool Game::combatChangeHealth(std::shared_ptr attacker, std::shared_pt if (!isEvent) { g_events().eventCreatureOnDrainHealth(target, attacker, damage.primary.type, damage.primary.value, damage.secondary.type, damage.secondary.value, message.primary.color, message.secondary.color); - g_callbacks().executeCallback(EventCallback_t::creatureOnDrainHealth, &EventCallback::creatureOnDrainHealth, target, attacker, damage.primary.type, damage.primary.value, damage.secondary.type, damage.secondary.value, message.primary.color, message.secondary.color); + g_callbacks().executeCallback(EventCallback_t::creatureOnDrainHealth, &EventCallback::creatureOnDrainHealth, target, attacker, std::ref(damage.primary.type), std::ref(damage.primary.value), std::ref(damage.secondary.type), std::ref(damage.secondary.value), std::ref(message.primary.color), std::ref(message.secondary.color)); } if (damage.origin != ORIGIN_NONE && attacker && damage.primary.type != COMBAT_HEALING) { damage.primary.value *= attacker->getBuff(BUFF_DAMAGEDEALT) / 100.; @@ -7297,7 +7302,7 @@ bool Game::combatChangeHealth(std::shared_ptr attacker, std::shared_pt return true; } else if (realDamage >= targetHealth) { for (const auto &creatureEvent : target->getCreatureEvents(CREATURE_EVENT_PREPAREDEATH)) { - if (!creatureEvent->executeOnPrepareDeath(target, attacker)) { + if (!creatureEvent->executeOnPrepareDeath(target, attacker, std::ref(realDamage))) { return false; } } diff --git a/src/game/zones/zone.cpp b/src/game/zones/zone.cpp index b6c1191aa78..7ffa4b594f4 100644 --- a/src/game/zones/zone.cpp +++ b/src/game/zones/zone.cpp @@ -15,6 +15,7 @@ #include "creatures/npcs/npc.hpp" #include "creatures/players/player.hpp" #include "utils/pugicast.hpp" +#include "kv/kv.hpp" phmap::parallel_flat_hash_map> Zone::zones = {}; phmap::parallel_flat_hash_map> Zone::zonesByID = {}; @@ -122,6 +123,10 @@ std::vector> Zone::getItems() { void Zone::removePlayers() { for (const auto &player : getPlayers()) { g_game().internalTeleport(player, getRemoveDestination(player)); + // Remove icon from player (soul war quest) + if (player->hasIcon("goshnars-hatred-damage")) { + player->removeIcon("goshnars-hatred-damage"); + } } } diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index 764d4641469..63ca7939c46 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -829,7 +829,12 @@ void IOLoginDataLoad::loadPlayerForgeHistory(std::shared_ptr player, DBR } void IOLoginDataLoad::loadPlayerBosstiary(std::shared_ptr player, DBResult_ptr result) { - if (!result || !player) { + if (!result) { + g_logger().warn("[IOLoginData::loadPlayer] - Result nullptr: {}", __FUNCTION__); + return; + } + + if (!player) { g_logger().warn("[IOLoginData::loadPlayer] - Player or Result nullptr: {}", __FUNCTION__); return; } diff --git a/src/io/io_bosstiary.cpp b/src/io/io_bosstiary.cpp index 2f625cdb7cc..7c7c8108ce7 100644 --- a/src/io/io_bosstiary.cpp +++ b/src/io/io_bosstiary.cpp @@ -249,6 +249,7 @@ std::vector IOBosstiary::getBosstiaryFinished(const std::shared_ptrflush(); - g_logger().info("Map Loaded {} ({}x{}) in {} milliseconds", map->path.filename().string(), map->width, map->height, bm_mapLoad.duration()); + g_logger().debug("Map Loaded {} ({}x{}) in {} milliseconds", map->path.filename().string(), map->width, map->height, bm_mapLoad.duration()); } void IOMap::parseMapDataAttributes(FileStream &stream, Map* map) { diff --git a/src/lua/callbacks/callbacks_definitions.hpp b/src/lua/callbacks/callbacks_definitions.hpp index 3b8016f5f5b..6c19cc809c9 100644 --- a/src/lua/callbacks/callbacks_definitions.hpp +++ b/src/lua/callbacks/callbacks_definitions.hpp @@ -25,6 +25,7 @@ enum class EventCallback_t : uint16_t { creatureOnTargetCombat, creatureOnHear, creatureOnDrainHealth, + creatureOnCombat, // Party partyOnJoin, partyOnLeave, @@ -57,6 +58,7 @@ enum class EventCallback_t : uint16_t { playerOnInventoryUpdate, playerOnRotateItem, playerOnWalk, + playerOnThink, // Monster monsterOnDropLoot, monsterPostDropLoot, @@ -68,4 +70,5 @@ enum class EventCallback_t : uint16_t { zoneBeforeCreatureLeave, zoneAfterCreatureEnter, zoneAfterCreatureLeave, + mapOnLoad, }; diff --git a/src/lua/callbacks/event_callback.cpp b/src/lua/callbacks/event_callback.cpp index 38e7654d8d5..8280bad6d2b 100644 --- a/src/lua/callbacks/event_callback.cpp +++ b/src/lua/callbacks/event_callback.cpp @@ -25,8 +25,16 @@ * * @see Script */ -EventCallback::EventCallback(LuaScriptInterface* scriptInterface) : - Script(scriptInterface) { +EventCallback::EventCallback(LuaScriptInterface* scriptInterface, const std::string &callbackName, bool skipDuplicationCheck) : + Script(scriptInterface), m_callbackName(callbackName), m_skipDuplicationCheck(skipDuplicationCheck) { +} + +std::string EventCallback::getName() const { + return m_callbackName; +} + +bool EventCallback::skipDuplicationCheck() const { + return m_skipDuplicationCheck; } std::string EventCallback::getScriptTypeName() const { @@ -225,6 +233,58 @@ void EventCallback::creatureOnDrainHealth(std::shared_ptr creature, st getScriptInterface()->resetScriptEnv(); } +void EventCallback::creatureOnCombat(std::shared_ptr attacker, std::shared_ptr target, CombatDamage &damage) const { + if (!getScriptInterface()->reserveScriptEnv()) { + g_logger().error("[{} - " + "Creature {} target {}] " + "Call stack overflow. Too many lua script calls being nested.", + __FUNCTION__, attacker->getName(), target->getName()); + return; + } + + ScriptEnvironment* scriptEnvironment = getScriptInterface()->getScriptEnv(); + scriptEnvironment->setScriptId(getScriptId(), getScriptInterface()); + + lua_State* L = getScriptInterface()->getLuaState(); + getScriptInterface()->pushFunction(getScriptId()); + + LuaScriptInterface::pushUserdata(L, attacker); + LuaScriptInterface::setCreatureMetatable(L, -1, attacker); + + LuaScriptInterface::pushUserdata(L, target); + LuaScriptInterface::setCreatureMetatable(L, -1, target); + + LuaScriptInterface::pushCombatDamage(L, damage); + + if (getScriptInterface()->protectedCall(L, 7, 4) != 0) { + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L)); + } else { + damage.primary.value = std::abs(LuaScriptInterface::getNumber(L, -4)); + damage.primary.type = LuaScriptInterface::getNumber(L, -3); + damage.secondary.value = std::abs(LuaScriptInterface::getNumber(L, -2)); + damage.secondary.type = LuaScriptInterface::getNumber(L, -1); + + lua_pop(L, 4); + if (damage.primary.type != COMBAT_HEALING) { + damage.primary.value = -damage.primary.value; + damage.secondary.value = -damage.secondary.value; + } + /* + Only EK with dealing physical damage will get elemental damage on skill + */ + if (damage.origin == ORIGIN_SPELL && attacker) { + const auto &player = attacker->getPlayer(); + if (player && player->getVocationId() != 4 && player->getVocationId() != 8) { + damage.primary.value = damage.primary.value + damage.secondary.value; + damage.secondary.type = COMBAT_NONE; + damage.secondary.value = 0; + } + } + } + + getScriptInterface()->resetScriptEnv(); +} + // Party bool EventCallback::partyOnJoin(std::shared_ptr party, std::shared_ptr player) const { if (!getScriptInterface()->reserveScriptEnv()) { @@ -852,7 +912,7 @@ void EventCallback::playerOnCombat(std::shared_ptr player, std::shared_p if (target) { LuaScriptInterface::pushUserdata(L, target); - LuaScriptInterface::setMetatable(L, -1, "Creature"); + LuaScriptInterface::setCreatureMetatable(L, -1, target); } else { lua_pushnil(L); } @@ -1034,6 +1094,26 @@ void EventCallback::playerOnStorageUpdate(std::shared_ptr player, const getScriptInterface()->callVoidFunction(5); } +void EventCallback::playerOnThink(std::shared_ptr player, uint32_t interval) const { + if (!getScriptInterface()->reserveScriptEnv()) { + g_logger().error("[{}] player {}. Call stack overflow. Too many lua script calls being nested.", __FUNCTION__, player->getName()); + return; + } + + ScriptEnvironment* scriptEnvironment = getScriptInterface()->getScriptEnv(); + scriptEnvironment->setScriptId(getScriptId(), getScriptInterface()); + + lua_State* L = getScriptInterface()->getLuaState(); + getScriptInterface()->pushFunction(getScriptId()); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + lua_pushnumber(L, interval); + + getScriptInterface()->callVoidFunction(2); +} + // Monster void EventCallback::monsterOnDropLoot(std::shared_ptr monster, std::shared_ptr corpse) const { if (!getScriptInterface()->reserveScriptEnv()) { @@ -1235,3 +1315,22 @@ void EventCallback::zoneAfterCreatureLeave(std::shared_ptr zone, std::shar getScriptInterface()->callVoidFunction(2); } + +void EventCallback::mapOnLoad(const std::string &mapFullPath) const { + if (!getScriptInterface()->reserveScriptEnv()) { + g_logger().error("[{} - " + "Call stack overflow. Too many lua script calls being nested.", + __FUNCTION__); + return; + } + + ScriptEnvironment* scriptEnvironment = getScriptInterface()->getScriptEnv(); + scriptEnvironment->setScriptId(getScriptId(), getScriptInterface()); + + lua_State* L = getScriptInterface()->getLuaState(); + getScriptInterface()->pushFunction(getScriptId()); + + LuaScriptInterface::pushString(L, mapFullPath); + + getScriptInterface()->callVoidFunction(1); +} diff --git a/src/lua/callbacks/event_callback.hpp b/src/lua/callbacks/event_callback.hpp index 9141235a028..9e65480c246 100644 --- a/src/lua/callbacks/event_callback.hpp +++ b/src/lua/callbacks/event_callback.hpp @@ -35,13 +35,27 @@ class EventCallback : public Script { private: EventCallback_t m_callbackType = EventCallback_t::none; ///< The type of the event callback. std::string m_scriptTypeName; ///< The name associated with the script type. + std::string m_callbackName; ///< The name of the callback. + bool m_skipDuplicationCheck = false; ///< Whether the callback is silent error for already registered log error. public: /** * @brief Constructor that initializes the EventCallback with a given script interface. * @param scriptInterface Pointer to the LuaScriptInterface object. */ - explicit EventCallback(LuaScriptInterface* scriptInterface); + explicit EventCallback(LuaScriptInterface* scriptInterface, const std::string &callbackName, bool silentAlreadyRegistered); + + /** + * @brief Retrieves the callback name. + * @return The callback name as a string. + */ + std::string getName() const; + + /** + * @brief Retrieves the skip registration status of the callback. + * @return True if the callback is true for skip duplication check and register again the event, false otherwise. + */ + bool skipDuplicationCheck() const; /** * @brief Retrieves the script type name. @@ -84,6 +98,7 @@ class EventCallback : public Script { ReturnValue creatureOnTargetCombat(std::shared_ptr creature, std::shared_ptr target) const; void creatureOnHear(std::shared_ptr creature, std::shared_ptr speaker, const std::string &words, SpeakClasses type) const; void creatureOnDrainHealth(std::shared_ptr creature, std::shared_ptr attacker, CombatType_t &typePrimary, int32_t &damagePrimary, CombatType_t &typeSecondary, int32_t &damageSecondary, TextColor_t &colorPrimary, TextColor_t &colorSecondary) const; + void creatureOnCombat(std::shared_ptr attacker, std::shared_ptr target, CombatDamage &damage) const; // Party bool partyOnJoin(std::shared_ptr party, std::shared_ptr player) const; @@ -117,6 +132,7 @@ class EventCallback : public Script { void playerOnInventoryUpdate(std::shared_ptr player, std::shared_ptr item, Slots_t slot, bool equip) const; bool playerOnRotateItem(std::shared_ptr player, std::shared_ptr item, const Position &position) const; void playerOnWalk(std::shared_ptr player, Direction &dir) const; + void playerOnThink(std::shared_ptr player, uint32_t interval) const; // Monster void monsterOnDropLoot(std::shared_ptr monster, std::shared_ptr corpse) const; @@ -132,7 +148,5 @@ class EventCallback : public Script { void zoneAfterCreatureEnter(std::shared_ptr zone, std::shared_ptr creature) const; void zoneAfterCreatureLeave(std::shared_ptr zone, std::shared_ptr creature) const; - /** - * @note here end the lua binder functions } - */ + void mapOnLoad(const std::string &mapFullPath) const; }; diff --git a/src/lua/callbacks/events_callbacks.cpp b/src/lua/callbacks/events_callbacks.cpp index 4a1830f80fb..13a42baa15a 100644 --- a/src/lua/callbacks/events_callbacks.cpp +++ b/src/lua/callbacks/events_callbacks.cpp @@ -12,6 +12,8 @@ #include "lua/callbacks/events_callbacks.hpp" #include "lua/callbacks/event_callback.hpp" +#include "game/game.hpp" +#include "lib/di/container.hpp" /** * @class EventsCallbacks @@ -28,22 +30,37 @@ EventsCallbacks &EventsCallbacks::getInstance() { return inject(); } -void EventsCallbacks::addCallback(const std::shared_ptr callback) { - m_callbacks.push_back(callback); +bool EventsCallbacks::isCallbackRegistered(const std::shared_ptr &callback) { + if (g_game().getGameState() == GAME_STATE_STARTUP && !callback->skipDuplicationCheck() && m_callbacks.find(callback->getName()) != m_callbacks.end()) { + return true; + } + + return false; +} + +void EventsCallbacks::addCallback(const std::shared_ptr &callback) { + if (m_callbacks.find(callback->getName()) != m_callbacks.end() && !callback->skipDuplicationCheck()) { + g_logger().trace("Event callback already registered: {}", callback->getName()); + return; + } + + g_logger().trace("Registering event callback: {}", callback->getName()); + + m_callbacks[callback->getName()] = callback; } -std::vector> EventsCallbacks::getCallbacks() const { +std::unordered_map> EventsCallbacks::getCallbacks() const { return m_callbacks; } -std::vector> EventsCallbacks::getCallbacksByType(EventCallback_t type) const { - std::vector> eventCallbacks; - for (auto callback : getCallbacks()) { +std::unordered_map> EventsCallbacks::getCallbacksByType(EventCallback_t type) const { + std::unordered_map> eventCallbacks; + for (auto [name, callback] : getCallbacks()) { if (callback->getType() != type) { continue; } - eventCallbacks.push_back(callback); + eventCallbacks[name] = callback; } return eventCallbacks; diff --git a/src/lua/callbacks/events_callbacks.hpp b/src/lua/callbacks/events_callbacks.hpp index f71103047e6..dff5cec30c5 100644 --- a/src/lua/callbacks/events_callbacks.hpp +++ b/src/lua/callbacks/events_callbacks.hpp @@ -45,24 +45,35 @@ class EventsCallbacks { */ static EventsCallbacks &getInstance(); + /** + * @brief Checks if an event callback is already registered. + * + * @details Determines if the game state is at startup and if a callback with the same name already exists. + * @details If both conditions are met, logs an error and indicates the callback is already registered. + * + * @param callback Shared pointer to the event callback being checked. + * @return True if the callback already exists during the game startup state, otherwise false. + */ + bool isCallbackRegistered(const std::shared_ptr &callback); + /** * @brief Adds a new event callback to the list. * @param callback Pointer to the EventCallback object to add. */ - void addCallback(const std::shared_ptr callback); + void addCallback(const std::shared_ptr &callback); /** * @brief Gets all registered event callbacks. * @return Vector of pointers to EventCallback objects. */ - std::vector> getCallbacks() const; + std::unordered_map> getCallbacks() const; /** * @brief Gets event callbacks by their type. * @param type The type of callbacks to retrieve. * @return Vector of pointers to EventCallback objects of the specified type. */ - std::vector> getCallbacksByType(EventCallback_t type) const; + std::unordered_map> getCallbacksByType(EventCallback_t type) const; /** * @brief Clears all registered event callbacks. @@ -77,7 +88,7 @@ class EventsCallbacks { */ template void executeCallback(EventCallback_t eventType, CallbackFunc callbackFunc, Args &&... args) { - for (const auto &callback : getCallbacksByType(eventType)) { + for (const auto &[name, callback] : getCallbacksByType(eventType)) { auto argsCopy = std::make_tuple(args...); if (callback && callback->isLoadedCallback()) { std::apply( @@ -86,6 +97,7 @@ class EventsCallbacks { }, argsCopy ); + g_logger().trace("Executed callback: {}", name); } } } @@ -99,7 +111,7 @@ class EventsCallbacks { template ReturnValue checkCallbackWithReturnValue(EventCallback_t eventType, CallbackFunc callbackFunc, Args &&... args) { ReturnValue res = RETURNVALUE_NOERROR; - for (const auto &callback : getCallbacksByType(eventType)) { + for (const auto &[name, callback] : getCallbacksByType(eventType)) { auto argsCopy = std::make_tuple(args...); if (callback && callback->isLoadedCallback()) { ReturnValue callbackResult = std::apply( @@ -127,7 +139,7 @@ class EventsCallbacks { bool checkCallback(EventCallback_t eventType, CallbackFunc callbackFunc, Args &&... args) { bool allCallbacksSucceeded = true; - for (const auto &callback : getCallbacksByType(eventType)) { + for (const auto &[name, callback] : getCallbacksByType(eventType)) { auto argsCopy = std::make_tuple(args...); if (callback && callback->isLoadedCallback()) { bool callbackResult = std::apply( @@ -144,7 +156,7 @@ class EventsCallbacks { private: // Container for storing registered event callbacks. - std::vector> m_callbacks; + std::unordered_map> m_callbacks; }; constexpr auto g_callbacks = EventsCallbacks::getInstance; diff --git a/src/lua/creature/creatureevent.cpp b/src/lua/creature/creatureevent.cpp index 09dbdf086ce..90d09d2b7f9 100644 --- a/src/lua/creature/creatureevent.cpp +++ b/src/lua/creature/creatureevent.cpp @@ -231,7 +231,7 @@ bool CreatureEvent::executeOnThink(std::shared_ptr creature, uint32_t return getScriptInterface()->callFunction(2); } -bool CreatureEvent::executeOnPrepareDeath(std::shared_ptr creature, std::shared_ptr killer) const { +bool CreatureEvent::executeOnPrepareDeath(std::shared_ptr creature, std::shared_ptr killer, int realDamage) const { // onPrepareDeath(creature, killer) if (!getScriptInterface()->reserveScriptEnv()) { g_logger().error("[CreatureEvent::executeOnPrepareDeath - Creature {} killer {}" @@ -257,7 +257,9 @@ bool CreatureEvent::executeOnPrepareDeath(std::shared_ptr creature, st lua_pushnil(L); } - return getScriptInterface()->callFunction(2); + lua_pushnumber(L, realDamage); + + return getScriptInterface()->callFunction(3); } bool CreatureEvent::executeOnDeath(std::shared_ptr creature, std::shared_ptr corpse, std::shared_ptr killer, std::shared_ptr mostDamageKiller, bool lastHitUnjustified, bool mostDamageUnjustified) const { diff --git a/src/lua/creature/creatureevent.hpp b/src/lua/creature/creatureevent.hpp index 8a208343e11..e34bd8eb75e 100644 --- a/src/lua/creature/creatureevent.hpp +++ b/src/lua/creature/creatureevent.hpp @@ -46,7 +46,7 @@ class CreatureEvent final : public Script { bool executeOnLogin(std::shared_ptr player) const; bool executeOnLogout(std::shared_ptr player) const; bool executeOnThink(std::shared_ptr creature, uint32_t interval) const; - bool executeOnPrepareDeath(std::shared_ptr creature, std::shared_ptr killer) const; + bool executeOnPrepareDeath(std::shared_ptr creature, std::shared_ptr killer, int realDamage) const; bool executeOnDeath(std::shared_ptr creature, std::shared_ptr corpse, std::shared_ptr killer, std::shared_ptr mostDamageKiller, bool lastHitUnjustified, bool mostDamageUnjustified) const; void executeOnKill(std::shared_ptr creature, std::shared_ptr target, bool lastHit) const; bool executeAdvance(std::shared_ptr player, skills_t, uint32_t, uint32_t) const; diff --git a/src/lua/creature/events.cpp b/src/lua/creature/events.cpp index 8bdd53465b3..144c188e900 100644 --- a/src/lua/creature/events.cpp +++ b/src/lua/creature/events.cpp @@ -1159,7 +1159,7 @@ void Events::eventPlayerOnCombat(std::shared_ptr player, std::shared_ptr if (target) { LuaScriptInterface::pushUserdata(L, target); - LuaScriptInterface::setMetatable(L, -1, "Creature"); + LuaScriptInterface::setCreatureMetatable(L, -1, target); } else { lua_pushnil(L); } diff --git a/src/lua/functions/core/game/bank_functions.cpp b/src/lua/functions/core/game/bank_functions.cpp index f6732b8bf70..cc76bf4c091 100644 --- a/src/lua/functions/core/game/bank_functions.cpp +++ b/src/lua/functions/core/game/bank_functions.cpp @@ -81,6 +81,7 @@ int BankFunctions::luaBankTransferToGuild(lua_State* L) { reportErrorFunc("Source is nullptr"); return 1; } + std::shared_ptr destination = getBank(L, 2, true /* isGuild */); if (destination == nullptr) { reportErrorFunc("Destination is nullptr"); diff --git a/src/lua/functions/core/game/game_functions.cpp b/src/lua/functions/core/game/game_functions.cpp index 83ecd091850..10a6379781a 100644 --- a/src/lua/functions/core/game/game_functions.cpp +++ b/src/lua/functions/core/game/game_functions.cpp @@ -449,6 +449,7 @@ int GameFunctions::luaGameCreateMonster(lua_State* L) { if (g_game().placeCreature(monster, position, extended, force)) { g_events().eventMonsterOnSpawn(monster, position); g_callbacks().executeCallback(EventCallback_t::monsterOnSpawn, &EventCallback::monsterOnSpawn, monster, position); + monster->onSpawn(); const auto &mtype = monster->getMonsterType(); if (mtype && mtype->info.raceid > 0 && mtype->info.bosstiaryRace == BosstiaryRarity_t::RARITY_ARCHFOE) { for (const auto &spectator : Spectators().find(monster->getPosition(), true)) { diff --git a/src/lua/functions/core/game/lua_enums.cpp b/src/lua/functions/core/game/lua_enums.cpp index 05fc72c019f..e4dc252be0d 100644 --- a/src/lua/functions/core/game/lua_enums.cpp +++ b/src/lua/functions/core/game/lua_enums.cpp @@ -1206,11 +1206,9 @@ void LuaEnums::initReloadTypeEnums(lua_State* L) { void LuaEnums::initCreaturesEventEnums(lua_State* L) { // Monsters - registerEnum(L, MONSTERS_EVENT_THINK); - registerEnum(L, MONSTERS_EVENT_APPEAR); - registerEnum(L, MONSTERS_EVENT_DISAPPEAR); - registerEnum(L, MONSTERS_EVENT_MOVE); - registerEnum(L, MONSTERS_EVENT_SAY); + for (auto value : magic_enum::enum_values()) { + registerMagicEnum(L, value); + } // Npcs registerEnum(L, NPCS_EVENT_THINK); diff --git a/src/lua/functions/core/game/zone_functions.cpp b/src/lua/functions/core/game/zone_functions.cpp index 5471d01895a..0f19c94f789 100644 --- a/src/lua/functions/core/game/zone_functions.cpp +++ b/src/lua/functions/core/game/zone_functions.cpp @@ -136,7 +136,7 @@ int ZoneFunctions::luaZoneGetCreatures(lua_State* L) { for (auto creature : creatures) { index++; pushUserdata(L, creature); - setMetatable(L, -1, "Creature"); + setCreatureMetatable(L, -1, creature); lua_rawseti(L, -2, index); } return 1; diff --git a/src/lua/functions/creatures/monster/monster_functions.cpp b/src/lua/functions/creatures/monster/monster_functions.cpp index 5578477d6ba..917e90f85f4 100644 --- a/src/lua/functions/creatures/monster/monster_functions.cpp +++ b/src/lua/functions/creatures/monster/monster_functions.cpp @@ -60,7 +60,8 @@ int MonsterFunctions::luaMonsterGetType(lua_State* L) { } int MonsterFunctions::luaMonsterSetType(lua_State* L) { - // monster:setType(name or raceid) + // monster:setType(name or raceid, restoreHealth = false) + bool restoreHealth = getBoolean(L, 3, false); std::shared_ptr monster = getUserdataShared(L, 1); if (monster) { std::shared_ptr mType = nullptr; @@ -81,8 +82,14 @@ int MonsterFunctions::luaMonsterSetType(lua_State* L) { monster->defaultOutfit = mType->info.outfit; monster->currentOutfit = mType->info.outfit; monster->skull = mType->info.skull; - monster->health = mType->info.health * mType->getHealthMultiplier(); - monster->healthMax = mType->info.healthMax * mType->getHealthMultiplier(); + if (restoreHealth) { + auto multiplier = mType->getHealthMultiplier(); + monster->health = mType->info.health * multiplier; + monster->healthMax = mType->info.healthMax * multiplier; + } else { + monster->health = monster->getHealth(); + monster->healthMax = monster->getMaxHealth(); + } monster->baseSpeed = mType->getBaseSpeed(); monster->internalLight = mType->info.light; monster->hiddenHealth = mType->info.hiddenHealth; @@ -630,3 +637,76 @@ int MonsterFunctions::luaMonsterHazardDefenseBoost(lua_State* L) { } return 1; } + +int MonsterFunctions::luaMonsterAddReflectElement(lua_State* L) { + // monster:addReflectElement(type, percent) + const auto &monster = getUserdataShared(L, 1); + if (!monster) { + reportErrorFunc(getErrorDesc(LUA_ERROR_MONSTER_NOT_FOUND)); + pushBoolean(L, false); + return 0; + } + + CombatType_t element = getNumber(L, 2); + monster->addReflectElement(element, getNumber(L, 3)); + pushBoolean(L, true); + return 1; +} + +int MonsterFunctions::luaMonsterAddDefense(lua_State* L) { + // monster:addDefense(defense) + const auto &monster = getUserdataShared(L, 1); + if (!monster) { + reportErrorFunc(getErrorDesc(LUA_ERROR_MONSTER_NOT_FOUND)); + pushBoolean(L, false); + return 0; + } + + monster->addDefense(getNumber(L, 2)); + pushBoolean(L, true); + return 1; +} + +int MonsterFunctions::luaMonsterGetDefense(lua_State* L) { + // monster:getDefense(defense) + const auto &monster = getUserdataShared(L, 1); + if (!monster) { + reportErrorFunc(getErrorDesc(LUA_ERROR_MONSTER_NOT_FOUND)); + pushBoolean(L, false); + return 0; + } + + lua_pushnumber(L, monster->getDefense()); + return 1; +} + +int MonsterFunctions::luaMonsterIsDead(lua_State* L) { + // monster:isDead() + const auto &monster = getUserdataShared(L, 1); + if (!monster) { + reportErrorFunc(getErrorDesc(LUA_ERROR_MONSTER_NOT_FOUND)); + pushBoolean(L, false); + return 0; + } + + pushBoolean(L, monster->isDead()); + return 1; +} + +int MonsterFunctions::luaMonsterImmune(lua_State* L) { + // to get: isImmune = monster:immune() + // to set and get: newImmuneBool = monster:immune(newImmuneBool) + const auto &monster = getUserdataShared(L, 1); + if (!monster) { + reportErrorFunc(getErrorDesc(LUA_ERROR_MONSTER_NOT_FOUND)); + pushBoolean(L, false); + return 0; + } + + if (lua_gettop(L) > 1) { + monster->setImmune(getBoolean(L, 2)); + } + + pushBoolean(L, monster->isImmune()); + return 1; +} diff --git a/src/lua/functions/creatures/monster/monster_functions.hpp b/src/lua/functions/creatures/monster/monster_functions.hpp index dd1c3827344..90f2b526ca4 100644 --- a/src/lua/functions/creatures/monster/monster_functions.hpp +++ b/src/lua/functions/creatures/monster/monster_functions.hpp @@ -64,6 +64,13 @@ class MonsterFunctions final : LuaScriptInterface { registerMethod(L, "Monster", "hazardDamageBoost", MonsterFunctions::luaMonsterHazardDamageBoost); registerMethod(L, "Monster", "hazardDefenseBoost", MonsterFunctions::luaMonsterHazardDefenseBoost); + registerMethod(L, "Monster", "addReflectElement", MonsterFunctions::luaMonsterAddReflectElement); + registerMethod(L, "Monster", "addDefense", MonsterFunctions::luaMonsterAddDefense); + registerMethod(L, "Monster", "getDefense", MonsterFunctions::luaMonsterGetDefense); + + registerMethod(L, "Monster", "isDead", MonsterFunctions::luaMonsterIsDead); + registerMethod(L, "Monster", "immune", MonsterFunctions::luaMonsterImmune); + CharmFunctions::init(L); LootFunctions::init(L); MonsterSpellFunctions::init(L); @@ -124,6 +131,12 @@ class MonsterFunctions final : LuaScriptInterface { static int luaMonsterHazardDodge(lua_State* L); static int luaMonsterHazardDamageBoost(lua_State* L); static int luaMonsterHazardDefenseBoost(lua_State* L); + static int luaMonsterAddReflectElement(lua_State* L); + static int luaMonsterAddDefense(lua_State* L); + static int luaMonsterGetDefense(lua_State* L); + + static int luaMonsterIsDead(lua_State* L); + static int luaMonsterImmune(lua_State* L); friend class CreatureFunctions; }; diff --git a/src/lua/functions/creatures/monster/monster_type_functions.cpp b/src/lua/functions/creatures/monster/monster_type_functions.cpp index 757337bfe3c..2a6e3d1b581 100644 --- a/src/lua/functions/creatures/monster/monster_type_functions.cpp +++ b/src/lua/functions/creatures/monster/monster_type_functions.cpp @@ -1055,6 +1055,8 @@ int MonsterTypeFunctions::luaMonsterTypeEventOnCallback(lua_State* L) { // monsterType:onDisappear(callback) // monsterType:onMove(callback) // monsterType:onSay(callback) + // monsterType:onPlayerAttack(callback) + // monsterType:onSpawn(callback) const auto monsterType = getUserdataShared(L, 1); if (monsterType) { if (monsterType->loadCallback(&g_scripts().getScriptInterface())) { @@ -1602,7 +1604,7 @@ int MonsterTypeFunctions::luaMonsterTypeBossRaceId(lua_State* L) { } else { auto raceId = getNumber(L, 2, 0); monsterType->info.raceid = raceId; - g_ioBosstiary().addBosstiaryMonster(raceId, monsterType->name); + g_ioBosstiary().addBosstiaryMonster(raceId, monsterType->typeName); pushBoolean(L, true); } diff --git a/src/lua/functions/creatures/monster/monster_type_functions.hpp b/src/lua/functions/creatures/monster/monster_type_functions.hpp index cdbd0c4158a..6923740db41 100644 --- a/src/lua/functions/creatures/monster/monster_type_functions.hpp +++ b/src/lua/functions/creatures/monster/monster_type_functions.hpp @@ -98,6 +98,8 @@ class MonsterTypeFunctions final : LuaScriptInterface { registerMethod(L, "MonsterType", "onDisappear", MonsterTypeFunctions::luaMonsterTypeEventOnCallback); registerMethod(L, "MonsterType", "onMove", MonsterTypeFunctions::luaMonsterTypeEventOnCallback); registerMethod(L, "MonsterType", "onSay", MonsterTypeFunctions::luaMonsterTypeEventOnCallback); + registerMethod(L, "MonsterType", "onPlayerAttack", MonsterTypeFunctions::luaMonsterTypeEventOnCallback); + registerMethod(L, "MonsterType", "onSpawn", MonsterTypeFunctions::luaMonsterTypeEventOnCallback); registerMethod(L, "MonsterType", "getSummonList", MonsterTypeFunctions::luaMonsterTypeGetSummonList); registerMethod(L, "MonsterType", "addSummon", MonsterTypeFunctions::luaMonsterTypeAddSummon); diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index 7bbe495f7c2..421ceefcb0d 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -2272,10 +2272,23 @@ int PlayerFunctions::luaPlayerGetParty(lua_State* L) { } int PlayerFunctions::luaPlayerAddOutfit(lua_State* L) { - // player:addOutfit(lookType) + // player:addOutfit(lookType or name, addon = 0) std::shared_ptr player = getUserdataShared(L, 1); if (player) { - player->addOutfit(getNumber(L, 2), 0); + auto addon = getNumber(L, 3, 0); + if (lua_isnumber(L, 2)) { + player->addOutfit(getNumber(L, 2), addon); + } else if (lua_isstring(L, 2)) { + const std::string &outfitName = getString(L, 2); + const auto &outfit = Outfits::getInstance().getOutfitByName(player->getSex(), outfitName); + if (!outfit) { + reportErrorFunc("Outfit not found"); + return 1; + } + + player->addOutfit(outfit->lookType, addon); + } + pushBoolean(L, true); } else { lua_pushnil(L); @@ -4431,3 +4444,16 @@ int PlayerFunctions::luaPlayerRemoveIconBakragore(lua_State* L) { pushBoolean(L, true); return 1; } + +int PlayerFunctions::luaPlayerSendCreatureAppear(lua_State* L) { + auto player = getUserdataShared(L, 1); + if (!player) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + return 1; + } + + bool isLogin = getBoolean(L, 2, false); + player->sendCreatureAppear(player, player->getPosition(), isLogin); + pushBoolean(L, true); + return 1; +} diff --git a/src/lua/functions/creatures/player/player_functions.hpp b/src/lua/functions/creatures/player/player_functions.hpp index 7cee43424cd..aa4db5857f7 100644 --- a/src/lua/functions/creatures/player/player_functions.hpp +++ b/src/lua/functions/creatures/player/player_functions.hpp @@ -376,9 +376,9 @@ class PlayerFunctions final : LuaScriptInterface { registerMethod(L, "Player", "createTransactionSummary", PlayerFunctions::luaPlayerCreateTransactionSummary); registerMethod(L, "Player", "takeScreenshot", PlayerFunctions::luaPlayerTakeScreenshot); - registerMethod(L, "Player", "sendIconBakragore", PlayerFunctions::luaPlayerSendIconBakragore); registerMethod(L, "Player", "removeIconBakragore", PlayerFunctions::luaPlayerRemoveIconBakragore); + registerMethod(L, "Player", "sendCreatureAppear", PlayerFunctions::luaPlayerSendCreatureAppear); GroupFunctions::init(L); GuildFunctions::init(L); @@ -746,5 +746,7 @@ class PlayerFunctions final : LuaScriptInterface { static int luaPlayerSendIconBakragore(lua_State* L); static int luaPlayerRemoveIconBakragore(lua_State* L); + static int luaPlayerSendCreatureAppear(lua_State* L); + friend class CreatureFunctions; }; diff --git a/src/lua/functions/events/event_callback_functions.cpp b/src/lua/functions/events/event_callback_functions.cpp index 54457f4367a..ef1c348c113 100644 --- a/src/lua/functions/events/event_callback_functions.cpp +++ b/src/lua/functions/events/event_callback_functions.cpp @@ -34,7 +34,14 @@ void EventCallbackFunctions::init(lua_State* luaState) { } int EventCallbackFunctions::luaEventCallbackCreate(lua_State* luaState) { - const auto eventCallback = std::make_shared(getScriptEnv()->getScriptInterface()); + const auto &callbackName = getString(luaState, 2); + if (callbackName.empty()) { + reportErrorFunc("Invalid callback name"); + return 1; + } + + bool skipDuplicationCheck = getBoolean(luaState, 3, false); + const auto eventCallback = std::make_shared(getScriptEnv()->getScriptInterface(), callbackName, skipDuplicationCheck); pushUserdata(luaState, eventCallback); setMetatable(luaState, -1, "EventCallback"); return 1; @@ -82,6 +89,11 @@ int EventCallbackFunctions::luaEventCallbackRegister(lua_State* luaState) { return 0; } + if (g_callbacks().isCallbackRegistered(callback)) { + reportErrorFunc(fmt::format("EventCallback is duplicated for event with name: {}", callback->getName())); + return 0; + } + g_callbacks().addCallback(callback); pushBoolean(luaState, true); return 1; diff --git a/src/map/map.cpp b/src/map/map.cpp index 85e7de50484..b1706be0759 100644 --- a/src/map/map.cpp +++ b/src/map/map.cpp @@ -12,6 +12,8 @@ #include "map.hpp" #include "utils/astarnodes.hpp" +#include "lua/callbacks/event_callback.hpp" +#include "lua/callbacks/events_callbacks.hpp" #include "creatures/monsters/monster.hpp" #include "game/game.hpp" #include "game/zones/zone.hpp" @@ -25,11 +27,7 @@ void Map::load(const std::string &identifier, const Position &pos) { path = identifier; IOMap::loadMap(this, pos); } catch (const std::exception &e) { - throw IOMapException(fmt::format( - "\n[Map::load] - The map in folder {} is missing or corrupted" - "\n - {}", - identifier, e.what() - )); + g_logger().warn("[Map::load] - The map in folder {} is missing or corrupted", identifier); } } @@ -97,6 +95,10 @@ void Map::loadMap(const std::string &identifier, bool mainMap /*= false*/, bool housefile.clear(); npcfile.clear(); } + + if (!mainMap) { + g_callbacks().executeCallback(EventCallback_t::mapOnLoad, &EventCallback::mapOnLoad, path.string()); + } } void Map::loadMapCustom(const std::string &mapName, bool loadHouses, bool loadMonsters, bool loadNpcs, bool loadZones, int customMapIndex) { @@ -192,8 +194,7 @@ std::shared_ptr Map::getTile(uint16_t x, uint16_t y, uint8_t z) { return nullptr; } - const auto tile = floor->getTile(x, y); - return tile ? tile : getOrCreateTileFromCache(floor, x, y); + return getOrCreateTileFromCache(floor, x, y); } void Map::refreshZones(uint16_t x, uint16_t y, uint8_t z) { diff --git a/src/map/mapcache.cpp b/src/map/mapcache.cpp index e6e2f79b806..f4cdb524ec9 100644 --- a/src/map/mapcache.cpp +++ b/src/map/mapcache.cpp @@ -104,8 +104,9 @@ std::shared_ptr MapCache::createItem(const std::shared_ptr &Bas std::shared_ptr MapCache::getOrCreateTileFromCache(const std::unique_ptr &floor, uint16_t x, uint16_t y) { const auto cachedTile = floor->getTileCache(x, y); + const auto oldTile = floor->getTile(x, y); if (!cachedTile) { - return floor->getTile(x, y); + return oldTile; } std::unique_lock l(floor->getMutex()); @@ -114,6 +115,15 @@ std::shared_ptr MapCache::getOrCreateTileFromCache(const std::unique_ptr(this); + std::vector> oldCreatureList; + if (oldTile) { + if (CreatureVector* creatures = oldTile->getCreatures()) { + for (const auto &creature : *creatures) { + oldCreatureList.emplace_back(creature); + } + } + } + std::shared_ptr tile = nullptr; if (cachedTile->isHouse()) { const auto house = map->houses.getHouse(cachedTile->houseId); @@ -127,6 +137,10 @@ std::shared_ptr MapCache::getOrCreateTileFromCache(const std::unique_ptrinternalAddThing(creature); + } + if (cachedTile->ground != nullptr) { tile->internalAddThing(createItem(cachedTile->ground, pos)); } From 687aba591eb9b1f8a7d6ea9095d9b76facaad47b Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Thu, 12 Sep 2024 23:40:07 -0300 Subject: [PATCH 10/14] fix: float precision in config retrieval (#2889) Description: This addresses the issue where floating point values retrieved from the configuration were not being rounded properly, leading to slight inaccuracies. This fix ensures that all float values are correctly rounded to two decimal places before being passed to Lua, thereby ensuring consistent behavior and data accuracy. Expected behavior: With the implemented changes, when floating point values are retrieved, they will now be rounded to two decimal places. For example, a configured value of `1.15` will correctly be returned as `1.15` in Lua scripts. --- .../functions/core/game/config_functions.cpp | 13 +++++++-- .../functions/core/game/config_functions.hpp | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/lua/functions/core/game/config_functions.cpp b/src/lua/functions/core/game/config_functions.cpp index d0e77a69352..b839f720057 100644 --- a/src/lua/functions/core/game/config_functions.cpp +++ b/src/lua/functions/core/game/config_functions.cpp @@ -70,12 +70,21 @@ int ConfigFunctions::luaConfigManagerGetBoolean(lua_State* L) { } int ConfigFunctions::luaConfigManagerGetFloat(lua_State* L) { - auto key = getNumber(L, -1); + // configManager.getFloat(key, shouldRound = true) + + // Ensure the first argument (key) is provided and is a valid enum + auto key = getNumber(L, 1); if (!key) { reportErrorFunc("Wrong enum"); return 1; } - lua_pushnumber(L, g_configManager().getFloat(key, __FUNCTION__)); + // Check if the second argument (shouldRound) is provided and is a boolean; default to true if not provided + bool shouldRound = getBoolean(L, 2, true); + float value = g_configManager().getFloat(key, __FUNCTION__); + double finalValue = shouldRound ? static_cast(std::round(value * 100.0) / 100.0) : value; + + g_logger().debug("[{}] key: {}, finalValue: {}, shouldRound: {}", __METHOD_NAME__, magic_enum::enum_name(key), finalValue, shouldRound); + lua_pushnumber(L, finalValue); return 1; } diff --git a/src/lua/functions/core/game/config_functions.hpp b/src/lua/functions/core/game/config_functions.hpp index ae4952e9643..9806a35f426 100644 --- a/src/lua/functions/core/game/config_functions.hpp +++ b/src/lua/functions/core/game/config_functions.hpp @@ -17,6 +17,33 @@ class ConfigFunctions final : LuaScriptInterface { static void init(lua_State* L); private: + /** + * @brief Retrieves a float configuration value from the configuration manager, with an optional rounding. + * + * This function is a Lua binding used to get a float value from the configuration manager. It requires + * a key as the first argument, which should be a valid enumeration. An optional second boolean argument + * specifies whether the retrieved float should be rounded to two decimal places. + * + * @param L Pointer to the Lua state. The first argument must be a valid enum key, and the second argument (optional) + * can be a boolean indicating whether to round the result. + * + * @return Returns 1 after pushing the result onto the Lua stack, indicating the number of return values. + * + * @exception reportErrorFunc Throws an error if the first argument is not a valid enum. + * + * Usage: + * local result = ConfigManager.getFloat(ConfigKey.SomeKey) + * local result_rounded = ConfigManager.getFloat(ConfigKey.SomeKey, false) + * + * Detailed behavior: + * 1. Extracts the key from the first Lua stack argument as an enumeration of type `ConfigKey_t`. + * 2. Checks if the second argument is provided; if not, defaults to true for rounding. + * 3. Retrieves the float value associated with the key from the configuration manager. + * 4. If rounding is requested, rounds the value to two decimal places. + * 5. Logs the method call and the obtained value using the debug logger. + * 6. Pushes the final value (rounded or original) back onto the Lua stack. + * 7. Returns 1 to indicate a single return value. + */ static int luaConfigManagerGetFloat(lua_State* L); static int luaConfigManagerGetBoolean(lua_State* L); static int luaConfigManagerGetNumber(lua_State* L); From e4f0cdbde025abe83d66f8fd66d83ed10e93193a Mon Sep 17 00:00:00 2001 From: Luan Luciano Date: Sat, 14 Sep 2024 15:59:58 -0300 Subject: [PATCH 11/14] fix: field doesn`t display the condition (#2882) --- data/items/items.xml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/data/items/items.xml b/data/items/items.xml index 8cec8f05ec6..0afe5ebe858 100644 --- a/data/items/items.xml +++ b/data/items/items.xml @@ -4027,6 +4027,9 @@ + + + @@ -4342,6 +4345,9 @@ + + + @@ -20590,6 +20596,9 @@ + + + @@ -26310,6 +26319,9 @@ + + + @@ -26319,6 +26331,9 @@ + + + @@ -26328,6 +26343,9 @@ + + + @@ -45774,6 +45792,9 @@ hands of its owner. Granted by TibiaRoyal.com"/> + + + @@ -45781,6 +45802,9 @@ hands of its owner. Granted by TibiaRoyal.com"/> + + + @@ -45788,6 +45812,9 @@ hands of its owner. Granted by TibiaRoyal.com"/> + + + @@ -45796,6 +45823,9 @@ hands of its owner. Granted by TibiaRoyal.com"/> + + + @@ -58245,6 +58275,9 @@ hands of its owner. Granted by TibiaRoyal.com"/> + + + From b3b19a68b8e38ad8ed7b350a6f34f17ab99f028c Mon Sep 17 00:00:00 2001 From: Pedro Cruz Date: Sat, 14 Sep 2024 17:58:40 -0300 Subject: [PATCH 12/14] fix: login into another accounts (#2853) Fixes the login to other account when injecting a custom login.php --- src/account/account_repository.hpp | 2 ++ src/account/account_repository_db.cpp | 10 +++++++ src/account/account_repository_db.hpp | 2 ++ src/io/iologindata.cpp | 7 ++++- src/io/iologindata.hpp | 2 +- src/server/network/protocol/protocolgame.cpp | 2 +- .../account/in_memory_account_repository.hpp | 11 ++++++++ tests/unit/account/account_test.cpp | 28 +++++++++++++++++++ 8 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/account/account_repository.hpp b/src/account/account_repository.hpp index 0d4dcc7abcf..6dfe2c65668 100644 --- a/src/account/account_repository.hpp +++ b/src/account/account_repository.hpp @@ -27,6 +27,8 @@ class AccountRepository { virtual bool loadBySession(const std::string &email, AccountInfo &acc) = 0; virtual bool save(const AccountInfo &accInfo) = 0; + virtual bool getCharacterByAccountIdAndName(const uint32_t &id, const std::string &name) = 0; + virtual bool getPassword(const uint32_t &id, std::string &password) = 0; virtual bool getCoins(const uint32_t &id, const uint8_t &type, uint32_t &coins) = 0; diff --git a/src/account/account_repository_db.cpp b/src/account/account_repository_db.cpp index b150a636a97..81da30c08c5 100644 --- a/src/account/account_repository_db.cpp +++ b/src/account/account_repository_db.cpp @@ -64,6 +64,16 @@ bool AccountRepositoryDB::save(const AccountInfo &accInfo) { return successful; }; +bool AccountRepositoryDB::getCharacterByAccountIdAndName(const uint32_t &id, const std::string &name) { + auto result = g_database().storeQuery(fmt::format("SELECT `id` FROM `players` WHERE `account_id` = {} AND `name` = {}", id, g_database().escapeString(name))); + if (!result) { + g_logger().error("Failed to get character: [{}] from account: [{}]!", name, id); + return false; + } + + return result->countResults() == 1; +} + bool AccountRepositoryDB::getPassword(const uint32_t &id, std::string &password) { auto result = g_database().storeQuery(fmt::format("SELECT * FROM `accounts` WHERE `id` = {}", id)); if (!result) { diff --git a/src/account/account_repository_db.hpp b/src/account/account_repository_db.hpp index 651600e3bc4..e34d864a090 100644 --- a/src/account/account_repository_db.hpp +++ b/src/account/account_repository_db.hpp @@ -20,6 +20,8 @@ class AccountRepositoryDB final : public AccountRepository { bool loadBySession(const std::string &esseionKey, AccountInfo &acc) override; bool save(const AccountInfo &accInfo) override; + bool getCharacterByAccountIdAndName(const uint32_t &id, const std::string &name) override; + bool getPassword(const uint32_t &id, std::string &password) override; bool getCoins(const uint32_t &id, const uint8_t &type, uint32_t &coins) override; diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index 132cead6412..fce5d0dc293 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -19,7 +19,7 @@ #include "enums/account_type.hpp" #include "enums/account_errors.hpp" -bool IOLoginData::gameWorldAuthentication(const std::string &accountDescriptor, const std::string &password, std::string &characterName, uint32_t &accountId, bool oldProtocol) { +bool IOLoginData::gameWorldAuthentication(const std::string &accountDescriptor, const std::string &password, std::string &characterName, uint32_t &accountId, bool oldProtocol, const uint32_t ip) { Account account(accountDescriptor); account.setProtocolCompat(oldProtocol); @@ -38,6 +38,11 @@ bool IOLoginData::gameWorldAuthentication(const std::string &accountDescriptor, } } + if (!g_accountRepository().getCharacterByAccountIdAndName(account.getID(), characterName)) { + g_logger().warn("IP [{}] trying to connect into another account character", convertIPToString(ip)); + return false; + } + if (AccountErrors_t::Ok != enumFromValue(account.load())) { g_logger().error("Failed to load account [{}]", accountDescriptor); return false; diff --git a/src/io/iologindata.hpp b/src/io/iologindata.hpp index 1451cf89778..79fa3b59ad7 100644 --- a/src/io/iologindata.hpp +++ b/src/io/iologindata.hpp @@ -17,7 +17,7 @@ using ItemBlockList = std::list>>; class IOLoginData { public: - static bool gameWorldAuthentication(const std::string &accountDescriptor, const std::string &sessionOrPassword, std::string &characterName, uint32_t &accountId, bool oldProcotol); + static bool gameWorldAuthentication(const std::string &accountDescriptor, const std::string &sessionOrPassword, std::string &characterName, uint32_t &accountId, bool oldProcotol, const uint32_t ip); static uint8_t getAccountType(uint32_t accountId); static void updateOnlineStatus(uint32_t guid, bool login); static bool loadPlayerById(std::shared_ptr player, uint32_t id, bool disableIrrelevantInfo = true); diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 032b9b512d0..aab3f88bd47 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -841,7 +841,7 @@ void ProtocolGame::onRecvFirstMessage(NetworkMessage &msg) { } uint32_t accountId; - if (!IOLoginData::gameWorldAuthentication(accountDescriptor, password, characterName, accountId, oldProtocol)) { + if (!IOLoginData::gameWorldAuthentication(accountDescriptor, password, characterName, accountId, oldProtocol, getIP())) { ss.str(std::string()); if (authType == "session") { ss << "Your session has expired. Please log in again."; diff --git a/tests/fixture/account/in_memory_account_repository.hpp b/tests/fixture/account/in_memory_account_repository.hpp index 40dbda38e08..8a294992274 100644 --- a/tests/fixture/account/in_memory_account_repository.hpp +++ b/tests/fixture/account/in_memory_account_repository.hpp @@ -120,6 +120,17 @@ namespace tests { return true; } + bool getCharacterByAccountIdAndName(const uint32_t &id, const std::string &name) final { + for (auto it = accounts.begin(); it != accounts.end(); ++it) { + if (it->second.id == id) { + if (it->second.players.find(name) != it->second.players.end()) { + return true; + } + } + } + return false; + } + InMemoryAccountRepository &reset() { accounts.clear(); coins_.clear(); diff --git a/tests/unit/account/account_test.cpp b/tests/unit/account/account_test.cpp index 9259703c77c..c0c3ebbb832 100644 --- a/tests/unit/account/account_test.cpp +++ b/tests/unit/account/account_test.cpp @@ -592,4 +592,32 @@ suite<"account"> accountTest = [] { expect(acc.load() == enumToValue(AccountErrors_t::Ok)); expect(acc.authenticate()); }; + + test("Account::getCharacterByAccountIdAndName using an account with the given character.") = [&injectionFixture] { + auto [accountRepository] = injectionFixture.get(); + + Account acc { 1 }; + accountRepository.addAccount( + "session-key", + AccountInfo { 1, 1, 1, AccountType::ACCOUNT_TYPE_GOD, { { "Canary", 1 }, { "Canary2", 2 } }, false, getTimeNow() + 24 * 60 * 60 * 1000 } + ); + + const auto hasCharacter = accountRepository.getCharacterByAccountIdAndName(1, "Canary"); + + expect(hasCharacter); + }; + + test("Account::getCharacterByAccountIdAndName using an account without the given character.") = [&injectionFixture] { + auto [accountRepository] = injectionFixture.get(); + + Account acc { 1 }; + accountRepository.addAccount( + "session-key", + AccountInfo { 1, 1, 1, AccountType::ACCOUNT_TYPE_GOD, { { "Canary", 1 }, { "Canary2", 2 } }, false, getTimeNow() + 24 * 60 * 60 * 1000 } + ); + + const auto hasCharacter = accountRepository.getCharacterByAccountIdAndName(1, "Invalid"); + + expect(!hasCharacter); + }; }; From 563b0f7512baf538dd79e52d80290c276afda571 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Wed, 18 Sep 2024 22:51:17 -0300 Subject: [PATCH 13/14] fix: qodana linter (#2902) --- qodana.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qodana.yml b/qodana.yml index 1621c979a9d..4857043fe5d 100644 --- a/qodana.yml +++ b/qodana.yml @@ -3,6 +3,8 @@ version: "1.0" profile: name: qodana.recommended +linter: jetbrains/qodana-clang:latest + bootstrap: | set -e sudo apt-get update && sudo apt-get -y dist-upgrade From ae3968295a5ba78fa98389e1e36f4afeb9d20288 Mon Sep 17 00:00:00 2001 From: Karin Date: Thu, 19 Sep 2024 02:01:25 -0300 Subject: [PATCH 14/14] fix: crash in use with creature (add nullptr check) (#2899) --- src/game/game.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/game.cpp b/src/game/game.cpp index 7b60220ecf9..5e99dda81fa 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -3909,7 +3909,7 @@ void Game::playerUseWithCreature(uint32_t playerId, const Position &fromPos, uin } const std::shared_ptr monster = creature->getMonster(); - if (monster && monster->isFamiliar() && creature->getMaster()->getPlayer() == player && (it.isRune() || it.type == ITEM_TYPE_POTION)) { + if (monster && monster->isFamiliar() && creature->getMaster() && creature->getMaster()->getPlayer() == player && (it.isRune() || it.type == ITEM_TYPE_POTION)) { player->setNextPotionAction(OTSYS_TIME() + g_configManager().getNumber(EX_ACTIONS_DELAY_INTERVAL, __FUNCTION__)); if (it.isMultiUse()) {