diff --git a/config.lua.dist b/config.lua.dist
index 8f7422da26a..10105287115 100644
--- a/config.lua.dist
+++ b/config.lua.dist
@@ -88,6 +88,17 @@ maxItem = 5000
maxContainer = 500
maxContainerDepth = 200
+-- Animus Mastery - SoulPit (Get more info in: https://github.com/opentibiabr/canary/pull/3230)
+-- NOTE: animusMasteryMaxMonsterXpMultiplier is the maximum experience the multiplier can be.
+-- NOTE: animusMasteryMonsterXpMultiplier is the monster experience multiplier that has the animus mastery unlocked.
+-- NOTE: animusMasteryMonstersXpMultiplier is the multiplier for each 'animusMasteryMonstersToIncreaseXpMultiplier' monsters that
+-- the player has the animus mastery unlocked.
+-- NOTE: animusMasteryMonstersToIncreaseXpMultiplier is the amount of monster to increase the experience multiplier by 'animusMasteryMonstersXpMultiplier'.
+animusMasteryMaxMonsterXpMultiplier = 4.0
+animusMasteryMonsterXpMultiplier = 2.0
+animusMasteryMonstersXpMultiplier = 0.1
+animusMasteryMonstersToIncreaseXpMultiplier = 10
+
-- Augments System (Get more info in: https://github.com/opentibiabr/canary/pull/2602)
-- NOTE: the following values are for all weapons and equipments that have type of "increase damage", "powerful impact" and "strong impact".
-- To customize the percentage of a particular item with these augment types, please add to the item "augments" section on items.xml as the example above.
diff --git a/data-otservbr-global/lib/others/load.lua b/data-otservbr-global/lib/others/load.lua
index 031c8fb2026..3422819b9f8 100644
--- a/data-otservbr-global/lib/others/load.lua
+++ b/data-otservbr-global/lib/others/load.lua
@@ -1 +1,2 @@
dofile(DATA_DIRECTORY .. "/lib/others/dawnport.lua")
+dofile(DATA_DIRECTORY .. "/lib/others/soulpit.lua")
diff --git a/data-otservbr-global/lib/others/soulpit.lua b/data-otservbr-global/lib/others/soulpit.lua
new file mode 100644
index 00000000000..b677b10fd9c
--- /dev/null
+++ b/data-otservbr-global/lib/others/soulpit.lua
@@ -0,0 +1,174 @@
+SoulPit = {
+ SoulCoresConfiguration = {
+ chanceToGetSameMonsterSoulCore = 15, -- 15%
+ chanceToDropSoulCore = 5, -- 5%
+ chanceToGetOminousSoulCore = 2, -- 2%
+ chanceToDropSoulPrism = 4, -- 4%
+ monsterVariationsSoulCore = {
+ ["Horse"] = "horse soul core (taupe)",
+ ["Brown Horse"] = "horse soul core (brown)",
+ ["Grey Horse"] = "horse soul core (gray)",
+ ["Nomad"] = "nomad soul core (basic)",
+ ["Nomad Blue"] = "nomad soul core (blue)",
+ ["Nomad Female"] = "nomad soul core (female)",
+ ["Purple Butterfly"] = "butterfly soul core (purple)",
+ ["Butterfly"] = "butterfly soul core (blue)",
+ ["Blue Butterfly"] = "butterfly soul core (blue)",
+ ["Red Butterfly"] = "butterfly soul core (red)",
+ },
+ monstersDifficulties = {
+ ["Harmless"] = 1,
+ ["Trivial"] = 2,
+ ["Easy"] = 3,
+ ["Medium"] = 4,
+ ["Hard"] = 5,
+ ["Challenge"] = 6,
+ },
+ },
+ encounter = nil,
+ kickEvent = nil,
+ soulCores = Game.getSoulCoreItems(),
+ requiredLevel = 8,
+ playerPositions = {
+ {
+ pos = Position(32375, 31158, 8),
+ teleport = Position(32373, 31151, 8),
+ effect = CONST_ME_TELEPORT,
+ },
+ {
+ pos = Position(32375, 31159, 8),
+ teleport = Position(32374, 31151, 8),
+ effect = CONST_ME_TELEPORT,
+ },
+ {
+ pos = Position(32375, 31160, 8),
+ teleport = Position(32375, 31151, 8),
+ effect = CONST_ME_TELEPORT,
+ },
+ {
+ pos = Position(32375, 31161, 8),
+ teleport = Position(32376, 31151, 8),
+ effect = CONST_ME_TELEPORT,
+ },
+ {
+ pos = Position(32375, 31162, 8),
+ teleport = Position(32377, 31151, 8),
+ effect = CONST_ME_TELEPORT,
+ },
+ },
+ waves = {
+ [1] = {
+ stacks = {
+ [1] = 7,
+ },
+ },
+ [2] = {
+ stacks = {
+ [1] = 4,
+ [5] = 3,
+ },
+ },
+ [3] = {
+ stacks = {
+ [1] = 5,
+ [15] = 2,
+ },
+ },
+ [4] = {
+ stacks = {
+ [1] = 3,
+ [5] = 3,
+ [40] = 1,
+ },
+ },
+ },
+ effects = {
+ [1] = CONST_ME_TELEPORT,
+ [5] = CONST_ME_ORANGETELEPORT,
+ [15] = CONST_ME_REDTELEPORT,
+ [40] = CONST_ME_PURPLETELEPORT,
+ },
+ possibleAbilities = {
+ "overpowerSoulPit",
+ "enrageSoulPit",
+ "opressorSoulPit",
+ },
+ bossAbilities = {
+ overpowerSoulPit = {
+ criticalChance = 50, -- 50%
+ criticalDamage = 25, -- 25%
+ apply = function(monster)
+ monster:criticalChance(SoulPit.bossAbilities.overpowerSoulPit.criticalChance)
+ monster:criticalDamage(SoulPit.bossAbilities.overpowerSoulPit.criticalDamage)
+ end,
+ },
+ enrageSoulPit = {
+ bounds = {
+ [{ 0.8, 0.6 }] = 0.9, -- 10% damage reduction
+ [{ 0.6, 0.4 }] = 0.75, -- 25% damage reduction
+ [{ 0.4, 0.2 }] = 0.6, -- 40% damage reduction
+ [{ 0.0, 0.2 }] = 0.4, -- 60% damage reduction
+ },
+ apply = function(monster)
+ monster:registerEvent("enrageSoulPit")
+ end,
+ },
+ opressorSoulPit = {
+ spells = {
+ { name = "soulpit opressor", interval = 2000, chance = 25, minDamage = 0, maxDamage = 0 },
+ { name = "soulpit powerless", interval = 2000, chance = 30, minDamage = 0, maxDamage = 0 },
+ { name = "soulpit intensehex", interval = 2000, chance = 15, minDamage = 0, maxDamage = 0 },
+ },
+ apply = function(monster)
+ -- Applying spells
+ for _, spell in pairs(SoulPit.bossAbilities.opressorSoulPit.spells) do
+ monster:addAttackSpell(readSpell(spell, monster:getType()))
+ end
+
+ return true
+ end,
+ },
+ },
+ timeToKick = 10 * 60 * 1000, -- 10 minutes
+ checkMonstersDelay = 4.5 * 1000, -- 4.5 seconds | The check delay should never be less than the timeToSpawnMonsters.
+ timeToSpawnMonsters = 4 * 1000, -- 4 seconds
+ totalMonsters = 7,
+ obeliskActive = 47379,
+ obeliskInactive = 47367,
+ obeliskPosition = Position(32375, 31157, 8),
+ bossPosition = Position(32376, 31144, 8),
+ exit = Position(32373, 31158, 8),
+ zone = Zone("soulpit"),
+
+ getMonsterVariationNameBySoulCore = function(searchName)
+ for mTypeName, soulCoreName in pairs(SoulPit.SoulCoresConfiguration.monsterVariationsSoulCore) do
+ if soulCoreName == searchName then
+ return mTypeName
+ end
+ end
+
+ return nil
+ end,
+ getSoulCoreMonster = function(name)
+ return name:match("^(.-) soul core")
+ end,
+ onFuseSoulCores = function(player, item, target)
+ local itemName = item:getName()
+ local targetItemName = target:getName()
+
+ if SoulPit.getSoulCoreMonster(itemName) and SoulPit.getSoulCoreMonster(targetItemName) then
+ local randomSoulCore = SoulPit.soulCores[math.random(#SoulPit.soulCores)]
+ player:addItem(randomSoulCore:getId(), 1)
+ player:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE)
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, string.format("You have received a %s soul core.", randomSoulCore:getName()))
+ item:remove(1)
+ target:remove(1)
+ return true
+ end
+
+ return false
+ end,
+}
+
+SoulPit.zone:addArea(Position(32362, 31132, 8), Position(32390, 31153, 8))
+SoulPit.zone:setRemoveDestination(SoulPit.exit)
diff --git a/data-otservbr-global/migrations/49.lua b/data-otservbr-global/migrations/49.lua
new file mode 100644
index 00000000000..2f2f2460d10
--- /dev/null
+++ b/data-otservbr-global/migrations/49.lua
@@ -0,0 +1,5 @@
+function onUpdateDatabase()
+ logger.info("Updating database to version 49 (feat: animus mastery (soulpit))")
+
+ db.query("ALTER TABLE `players` ADD `animus_mastery` mediumblob DEFAULT NULL;")
+end
diff --git a/data-otservbr-global/scripts/actions/soulpit/soulpit_arena_exit.lua b/data-otservbr-global/scripts/actions/soulpit/soulpit_arena_exit.lua
new file mode 100644
index 00000000000..21304d0cfb5
--- /dev/null
+++ b/data-otservbr-global/scripts/actions/soulpit/soulpit_arena_exit.lua
@@ -0,0 +1,17 @@
+local soulpitArenaExitConfig = {
+ arenaExit = Position(32375, 31153, 8),
+ destination = Position(32373, 31158, 8),
+}
+
+local soulpitArenaExit = Action()
+
+function soulpitArenaExit.onUse(player, item, fromPosition, target, toPosition, isHotkey)
+ if not player then
+ return false
+ end
+ player:getPosition():sendMagicEffect(CONST_ME_TELEPORT)
+ player:teleportTo(soulpitArenaExitConfig.destination)
+end
+
+soulpitArenaExit:position(soulpitArenaExitConfig.arenaExit)
+soulpitArenaExit:register()
diff --git a/data-otservbr-global/scripts/actions/soulpit/soulpit_entrance.lua b/data-otservbr-global/scripts/actions/soulpit/soulpit_entrance.lua
new file mode 100644
index 00000000000..d31f63fe386
--- /dev/null
+++ b/data-otservbr-global/scripts/actions/soulpit/soulpit_entrance.lua
@@ -0,0 +1,57 @@
+local config = {
+ entrance = {
+ positions = {
+ Position(32350, 31030, 3),
+ Position(32349, 31030, 3),
+ },
+ destination = Position(32374, 31171, 8),
+ },
+ exit = {
+ position = Position(32374, 31173, 8),
+ destination = Position(32349, 31032, 3),
+ },
+}
+
+local soulpitEntrance = MoveEvent()
+
+function soulpitEntrance.onStepIn(creature, item, position, fromPosition)
+ local player = creature:getPlayer()
+ if not player then
+ return true
+ end
+
+ if not config.entrance.destination then
+ return true
+ end
+
+ player:teleportTo(config.entrance.destination)
+ position:sendMagicEffect(CONST_ME_TELEPORT)
+ return true
+end
+
+soulpitEntrance:type("stepin")
+for value in pairs(config.entrance.positions) do
+ soulpitEntrance:position(config.entrance.positions[value])
+end
+soulpitEntrance:register()
+
+local soulpitExit = MoveEvent()
+
+function soulpitExit.onStepIn(creature, item, position, fromPosition)
+ local player = creature:getPlayer()
+ if not player then
+ return true
+ end
+
+ if not config.exit then
+ return true
+ end
+
+ player:teleportTo(config.exit.destination)
+ position:sendMagicEffect(CONST_ME_TELEPORT)
+ return true
+end
+
+soulpitExit:type("stepin")
+soulpitExit:position(config.exit.position)
+soulpitExit:register()
diff --git a/data-otservbr-global/scripts/quests/soulpit/exalted_core.lua b/data-otservbr-global/scripts/quests/soulpit/exalted_core.lua
new file mode 100644
index 00000000000..dadb7b7fd0d
--- /dev/null
+++ b/data-otservbr-global/scripts/quests/soulpit/exalted_core.lua
@@ -0,0 +1,92 @@
+local exaltedCore = Action()
+
+local function getPreviousDifficultyLevel(currentLevel)
+ for level, value in pairs(SoulPit.SoulCoresConfiguration.monstersDifficulties) do
+ if value == currentLevel - 1 then
+ return level
+ end
+ end
+ return nil
+end
+
+local function getSoulCoreItemForMonster(monsterName)
+ local lowerMonsterName = monsterName:lower()
+ local soulCoreName = SoulPit.SoulCoresConfiguration.monsterVariationsSoulCore[monsterName]
+
+ if soulCoreName then
+ local newSoulCoreId = getItemIdByName(soulCoreName)
+ if newSoulCoreId then
+ return newSoulCoreId
+ end
+ else
+ local newMonsterSoulCore = string.format("%s soul core", monsterName)
+ local newSoulCoreId = getItemIdByName(newMonsterSoulCore)
+ if newSoulCoreId then
+ return newSoulCoreId
+ end
+ end
+
+ return false
+end
+
+function exaltedCore.onUse(player, item, fromPosition, target, toPosition, isHotkey)
+ local itemName = target:getName()
+ local monsterName = SoulPit.getSoulCoreMonster(itemName)
+
+ if not monsterName then
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You can only use Exalted Core with a Soul Core.")
+ player:getPosition():sendMagicEffect(CONST_ME_POFF)
+ return false
+ end
+
+ local monsterType = MonsterType(monsterName)
+ if not monsterType then
+ player:sendTextMessage(MESSAGE_GAME_HIGHLIGHT, "Invalid monster type. Please contact an administrator.")
+ player:getPosition():sendMagicEffect(CONST_ME_POFF)
+ return false
+ end
+
+ local currentDifficulty = monsterType:BestiaryStars()
+ local previousDifficultyLevel = getPreviousDifficultyLevel(currentDifficulty)
+ local previousDifficultyMonsters = nil
+
+ if previousDifficultyLevel then
+ previousDifficultyMonsters = Game.getMonstersByBestiaryStars(SoulPit.SoulCoresConfiguration.monstersDifficulties[previousDifficultyLevel])
+ else
+ previousDifficultyLevel = currentDifficulty
+ previousDifficultyMonsters = Game.getMonstersByBestiaryStars(SoulPit.SoulCoresConfiguration.monstersDifficulties[currentDifficulty])
+ end
+
+ if #previousDifficultyMonsters == 0 then
+ player:sendTextMessage(MESSAGE_GAME_HIGHLIGHT, "No monsters available for the previous difficulty level.")
+ player:getPosition():sendMagicEffect(CONST_ME_POFF)
+ return false
+ end
+
+ local newMonsterType = previousDifficultyMonsters[math.random(#previousDifficultyMonsters)]
+ local newSoulCoreItem = getSoulCoreItemForMonster(newMonsterType:getName())
+ if not newSoulCoreItem then -- Retry a second time.
+ newSoulCoreItem = getSoulCoreItemForMonster(newMonsterType:getName())
+ if not newSoulCoreItem then
+ player:sendTextMessage(MESSAGE_GAME_HIGHLIGHT, "Failed to generate a Soul Core.")
+ player:getPosition():sendMagicEffect(CONST_ME_POFF)
+ return false
+ end
+ end
+
+ if player:getFreeCapacity() < ItemType(newSoulCoreItem):getWeight() then
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You do not have enough capacity.")
+ player:getPosition():sendMagicEffect(CONST_ME_POFF)
+ return false
+ end
+
+ player:addItem(newSoulCoreItem, 1)
+ target:remove(1)
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, string.format("You have received a %s soul core.", newMonsterType:getName()))
+ item:remove(1)
+ player:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE)
+ return true
+end
+
+exaltedCore:id(37110)
+exaltedCore:register()
diff --git a/data-otservbr-global/scripts/quests/soulpit/ondroploot_soul_core.lua b/data-otservbr-global/scripts/quests/soulpit/ondroploot_soul_core.lua
new file mode 100644
index 00000000000..9ddb479c117
--- /dev/null
+++ b/data-otservbr-global/scripts/quests/soulpit/ondroploot_soul_core.lua
@@ -0,0 +1,58 @@
+local callback = EventCallback("MonsterOnDropLootSoulCore")
+
+function callback.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
+ if monster:getMonsterForgeClassification() ~= FORGE_FIENDISH_MONSTER then
+ return
+ end
+
+ local soulCoreId = nil
+ local trySameMonsterSoulCore = math.random(100) <= SoulPit.SoulCoresConfiguration.chanceToGetSameMonsterSoulCore
+ local mType = monster:getType()
+ local lootTable = {}
+
+ if math.random(100) < SoulPit.SoulCoresConfiguration.chanceToDropSoulCore then
+ if trySameMonsterSoulCore then
+ local itemName = monster:getName():lower() .. " soul core"
+ soulCoreId = getItemIdByName(itemName)
+ end
+
+ if not soulCoreId and not trySameMonsterSoulCore then
+ local race = mType:Bestiaryrace()
+ local monstersInCategory = Game.getMonstersByRace(race)
+
+ if monstersInCategory and #monstersInCategory > 0 then
+ local randomMonster = monstersInCategory[math.random(#monstersInCategory)]
+ local itemName = randomMonster:name():lower() .. " soul core"
+ soulCoreId = getItemIdByName(itemName)
+ logger.info("soulcoreId: " .. soulCoreId)
+ end
+ end
+
+ if soulCoreId then
+ lootTable[soulCoreId] = {
+ count = 1,
+ }
+ else
+ return {}
+ end
+ end
+
+ if math.random(100) < SoulPit.SoulCoresConfiguration.chanceToDropSoulPrism then
+ local soulPrismId = getItemIdByName("soul prism")
+ if soulPrismId then
+ lootTable[soulPrismId] = {
+ count = 1,
+ }
+ end
+ end
+ corpse:addLoot(mType:generateLootRoll({}, lootTable, player))
+end
+
+callback:register()
diff --git a/data-otservbr-global/scripts/quests/soulpit/soul_prism.lua b/data-otservbr-global/scripts/quests/soulpit/soul_prism.lua
new file mode 100644
index 00000000000..326bba11e4d
--- /dev/null
+++ b/data-otservbr-global/scripts/quests/soulpit/soul_prism.lua
@@ -0,0 +1,106 @@
+local soulPrism = Action()
+
+local function getNextDifficultyLevel(currentLevel)
+ for level, value in pairs(SoulPit.SoulCoresConfiguration.monstersDifficulties) do
+ if value == currentLevel + 1 then
+ return level
+ end
+ end
+ return nil
+end
+
+local function getPreviousDifficultyLevel(currentLevel)
+ for level, value in pairs(SoulPit.SoulCoresConfiguration.monstersDifficulties) do
+ if value == currentLevel - 1 then
+ return level
+ end
+ end
+ return nil
+end
+
+local function getSoulCoreItemForMonster(monsterName)
+ local lowerMonsterName = monsterName:lower()
+ local soulCoreName = SoulPit.SoulCoresConfiguration.monsterVariationsSoulCore[monsterName]
+
+ if soulCoreName then
+ local newSoulCoreId = getItemIdByName(soulCoreName)
+ if newSoulCoreId then
+ return newSoulCoreId
+ end
+ else
+ local newMonsterSoulCore = string.format("%s soul core", monsterName)
+ local newSoulCoreId = getItemIdByName(newMonsterSoulCore)
+ if newSoulCoreId then
+ return newSoulCoreId
+ end
+ end
+
+ return false
+end
+
+function soulPrism.onUse(player, item, fromPosition, target, toPosition, isHotkey)
+ local itemName = target:getName()
+ local monsterName = SoulPit.getSoulCoreMonster(itemName)
+
+ if not monsterName then
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You can only use Soul Prism with a Soul Core.")
+ player:getPosition():sendMagicEffect(CONST_ME_POFF)
+ return false
+ end
+
+ local monsterType = MonsterType(monsterName)
+ if not monsterType then
+ player:sendTextMessage(MESSAGE_GAME_HIGHLIGHT, "Invalid monster type. Please contact an administrator.")
+ player:getPosition():sendMagicEffect(CONST_ME_POFF)
+ return false
+ end
+
+ local currentDifficulty = monsterType:BestiaryStars()
+ local nextDifficultyLevel = getNextDifficultyLevel(currentDifficulty)
+ local nextDifficultyMonsters = nil
+
+ if nextDifficultyLevel then
+ nextDifficultyMonsters = Game.getMonstersByBestiaryStars(SoulPit.SoulCoresConfiguration.monstersDifficulties[nextDifficultyLevel])
+ else
+ nextDifficultyLevel = currentDifficulty
+ nextDifficultyMonsters = Game.getMonstersByBestiaryStars(SoulPit.SoulCoresConfiguration.monstersDifficulties[currentDifficulty])
+ end
+
+ if #nextDifficultyMonsters == 0 then
+ player:sendTextMessage(MESSAGE_GAME_HIGHLIGHT, "No monsters available for the next difficulty level.")
+ player:getPosition():sendMagicEffect(CONST_ME_POFF)
+ return false
+ end
+
+ local newMonsterType = nextDifficultyMonsters[math.random(#nextDifficultyMonsters)]
+ local newSoulCoreItem = getSoulCoreItemForMonster(newMonsterType:getName())
+ if not newSoulCoreItem then -- Retry a second time.
+ newSoulCoreItem = getSoulCoreItemForMonster(newMonsterType:getName())
+ if not newSoulCoreItem then
+ player:sendTextMessage(MESSAGE_GAME_HIGHLIGHT, "Failed to generate a Soul Core.")
+ player:getPosition():sendMagicEffect(CONST_ME_POFF)
+ return false
+ end
+ end
+
+ if player:getFreeCapacity() < ItemType(newSoulCoreItem):getWeight() then
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You do not have enough capacity.")
+ player:getPosition():sendMagicEffect(CONST_ME_POFF)
+ return false
+ end
+
+ if math.random(100) <= SoulPit.SoulCoresConfiguration.chanceToGetOminousSoulCore then
+ player:addItem(49163, 1)
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have received an Ominous Soul Core.")
+ else
+ player:addItem(newSoulCoreItem, 1)
+ target:remove(1)
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, string.format("You have received a %s soul core.", newMonsterType:getName()))
+ end
+ item:remove(1)
+ player:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE)
+ return true
+end
+
+soulPrism:id(49164)
+soulPrism:register()
diff --git a/data-otservbr-global/scripts/quests/soulpit/soulpit_creatureevents.lua b/data-otservbr-global/scripts/quests/soulpit/soulpit_creatureevents.lua
new file mode 100644
index 00000000000..118a190bf51
--- /dev/null
+++ b/data-otservbr-global/scripts/quests/soulpit/soulpit_creatureevents.lua
@@ -0,0 +1,17 @@
+local enrage = CreatureEvent("enrageSoulPit")
+function enrage.onHealthChange(creature, attacker, primaryDamage, primaryType, secondaryDamage, secondaryType)
+ if not creature or not creature:isMonster() and creature:getMaster() then
+ return true
+ end
+
+ local healthPercentage = creature:getHealth() / creature:getMaxHealth()
+
+ for bounds, reduction in pairs(SoulPit.bossAbilities.enrageSoulPit.bounds) do
+ if healthPercentage > bounds[2] and healthPercentage <= bounds[1] then
+ return primaryDamage * reduction, primaryType, secondaryDamage * reduction, secondaryType
+ end
+ end
+
+ return primaryDamage, primaryType, secondaryDamage, secondaryType
+end
+enrage:register()
diff --git a/data-otservbr-global/scripts/quests/soulpit/soulpit_fight.lua b/data-otservbr-global/scripts/quests/soulpit/soulpit_fight.lua
new file mode 100644
index 00000000000..c607a0de9a9
--- /dev/null
+++ b/data-otservbr-global/scripts/quests/soulpit/soulpit_fight.lua
@@ -0,0 +1,163 @@
+SoulPit.zone:blockFamiliars()
+
+local zoneEvent = ZoneEvent(SoulPit.zone)
+function zoneEvent.afterLeave(zone, creature)
+ local player = creature:getPlayer()
+ if not player then
+ return false
+ end
+
+ if table.empty(zone:getPlayers()) then
+ if SoulPit.encounter then
+ SoulPit.encounter:reset()
+ SoulPit.encounter = nil
+ end
+ if SoulPit.kickEvent then
+ stopEvent(SoulPit.kickEvent)
+ SoulPit.obeliskPosition:transformItem(SoulPit.obeliskActive, SoulPit.obeliskInactive)
+ end
+ end
+end
+zoneEvent:register()
+
+local soulPitAction = Action()
+function soulPitAction.onUse(player, item, fromPosition, target, toPosition, isHotkey)
+ if SoulPit.onFuseSoulCores(player, item, target) then
+ return true
+ end
+
+ if target and target:getId() == SoulPit.obeliskActive then
+ creature:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Someone is fighting in the soulpit!")
+ return false
+ end
+ if not target or target:getId() ~= SoulPit.obeliskInactive then
+ return false
+ end
+
+ local isParticipant = false
+ for _, v in ipairs(SoulPit.playerPositions) do
+ if Position(v.pos) == player:getPosition() then
+ isParticipant = true
+ end
+ end
+
+ if not isParticipant then
+ return false
+ end
+
+ local lever = Lever()
+ lever:setPositions(SoulPit.playerPositions)
+ lever:setCondition(function(creature)
+ if not creature or not creature:isPlayer() then
+ return true
+ end
+
+ local isAccountNormal = creature:getAccountType() < ACCOUNT_TYPE_GAMEMASTER
+ if isAccountNormal and creature:getLevel() < SoulPit.requiredLevel then
+ local message = string.format("All players need to be level %s or higher.", SoulPit.requiredLevel)
+ creature:sendTextMessage(MESSAGE_EVENT_ADVANCE, message)
+ return false
+ end
+
+ return true
+ end)
+
+ lever:checkPositions()
+ if not lever:checkConditions() then
+ return true
+ end
+
+ item:remove(1)
+
+ if SoulPit.kickEvent then
+ stopEvent(SoulPit.kickEvent)
+ end
+
+ lever:teleportPlayers()
+ SoulPit.obeliskPosition:transformItem(SoulPit.obeliskInactive, SoulPit.obeliskActive)
+ SoulPit.kickEvent = addEvent(function()
+ SoulPit.kickEvent = nil
+ SoulPit.encounter = nil
+ SoulPit.zone:removePlayers()
+ SoulPit.obeliskPosition:transformItem(SoulPit.obeliskActive, SoulPit.obeliskInactive)
+ end, SoulPit.timeToKick)
+
+ local monsterName = string.gsub(item:getName(), " soul core", "")
+ local monsterVariationName = SoulPit.getMonsterVariationNameBySoulCore(item:getName())
+ monsterName = monsterVariationName and monsterVariationName or monsterName
+
+ SoulPit.zone:removeMonsters()
+
+ if SoulPit.encounter ~= nil then
+ SoulPit.encounter:reset()
+ end
+
+ local encounter = Encounter("Soulpit", {
+ zone = SoulPit.zone,
+ })
+
+ function encounter:onReset(position)
+ SoulPit.zone:removeMonsters()
+
+ for _, player in pairs(SoulPit.zone:getPlayers()) do
+ player:addAnimusMastery(monsterName)
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, string.format("You have defeated the core of the %s soul and unlocked its animus mastery!", monsterName))
+ end
+
+ SoulPit.zone:removePlayers()
+ end
+
+ SoulPit.encounter = encounter
+
+ local function waveStart()
+ for stack, amount in pairs(SoulPit.waves[encounter.currentStage].stacks) do
+ for i = 1, amount do
+ local position = stack ~= 40 and SoulPit.zone:randomPosition() or SoulPit.bossPosition
+ for i = 1, SoulPit.timeToSpawnMonsters / 1000 do
+ encounter:addEvent(function(position)
+ local effect = SoulPit.effects[stack]
+ if effect then
+ position:sendMagicEffect(effect)
+ end
+ end, i * 1000, position)
+ end
+
+ local randomAbility = SoulPit.possibleAbilities[math.random(1, #SoulPit.possibleAbilities)]
+ local chosenBossAbility = SoulPit.bossAbilities[randomAbility]
+
+ encounter:addEvent(function(name, stack, position, bossAbilityName, bossAbility)
+ local monster = Game.createSoulPitMonster(name, position, stack)
+ if not monster then
+ return false
+ end
+
+ if stack ~= 40 then
+ return false
+ end
+
+ bossAbility.apply(monster)
+ end, SoulPit.timeToSpawnMonsters, monsterName, stack, position, randomAbility, chosenBossAbility)
+ end
+ end
+ end
+
+ for i = 1, #SoulPit.waves do
+ encounter
+ :addStage({
+ start = waveStart,
+ })
+ :autoAdvance({ delay = SoulPit.checkMonstersDelay, monstersKilled = true })
+ end
+
+ encounter:start()
+ encounter:register()
+
+ return true
+end
+
+for _, itemType in pairs(SoulPit.soulCores) do
+ if itemType:getId() ~= 49164 then -- Exclude soul prism
+ soulPitAction:id(itemType:getId())
+ end
+end
+soulPitAction:register()
diff --git a/data-otservbr-global/scripts/quests/soulpit/soulpit_intensehex.lua b/data-otservbr-global/scripts/quests/soulpit/soulpit_intensehex.lua
new file mode 100644
index 00000000000..9dc9f54dfbe
--- /dev/null
+++ b/data-otservbr-global/scripts/quests/soulpit/soulpit_intensehex.lua
@@ -0,0 +1,23 @@
+local combat = Combat()
+combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_STUN)
+combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_NONE)
+
+local condition = Condition(CONDITION_INTENSEHEX)
+condition:setParameter(CONDITION_PARAM_BUFF_DAMAGEDEALT, 50)
+condition:setParameter(CONDITION_PARAM_BUFF_HEALINGRECEIVED, 50)
+condition:setParameter(CONDITION_PARAM_TICKS, 3000)
+combat:addCondition(condition)
+
+local spell = Spell("instant")
+
+function spell.onCastSpell(creature, var)
+ return combat:execute(creature, var)
+end
+
+spell:name("soulpit intensehex")
+spell:words("###940")
+spell:blockWalls(true)
+spell:needTarget(true)
+spell:needLearn(true)
+spell:isAggressive(true)
+spell:register()
diff --git a/data-otservbr-global/scripts/quests/soulpit/soulpit_opressor.lua b/data-otservbr-global/scripts/quests/soulpit/soulpit_opressor.lua
new file mode 100644
index 00000000000..0b29b4944b5
--- /dev/null
+++ b/data-otservbr-global/scripts/quests/soulpit/soulpit_opressor.lua
@@ -0,0 +1,42 @@
+-- ROOT
+local combatRoot = Combat()
+combatRoot:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ROOTS)
+
+local area = createCombatArea(AREA_ROOT_OPRESSOR)
+combatRoot:setArea(area)
+
+local condition = Condition(CONDITION_ROOTED)
+condition:setParameter(CONDITION_PARAM_TICKS, 3000)
+combatRoot:addCondition(condition)
+
+-- FEAR
+
+local combatFear = Combat()
+combatFear:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_BLUE_GHOST)
+
+local area = createCombatArea(AREA_FEAR_OPRESSOR)
+combatFear:setArea(area)
+
+local condition = Condition(CONDITION_FEARED)
+condition:setParameter(CONDITION_PARAM_TICKS, 3000)
+combatFear:addCondition(condition)
+
+local spell = Spell("instant")
+
+local combats = { combatRoot, combatFear }
+
+function spell.onCastSpell(creature, var)
+ for _, combat in pairs(combats) do
+ combat:execute(creature, var)
+ end
+
+ return true
+end
+
+spell:name("soulpit opressor")
+spell:words("###938")
+spell:blockWalls(true)
+spell:needTarget(false)
+spell:needLearn(true)
+spell:isAggressive(true)
+spell:register()
diff --git a/data-otservbr-global/scripts/quests/soulpit/soulpit_powerless.lua b/data-otservbr-global/scripts/quests/soulpit/soulpit_powerless.lua
new file mode 100644
index 00000000000..36af89a9860
--- /dev/null
+++ b/data-otservbr-global/scripts/quests/soulpit/soulpit_powerless.lua
@@ -0,0 +1,21 @@
+local combat = Combat()
+combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_EXPLOSIONHIT)
+combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_NONE)
+
+local condition = Condition(CONDITION_POWERLESS)
+condition:setParameter(CONDITION_PARAM_TICKS, 3000)
+combat:addCondition(condition)
+
+local spell = Spell("instant")
+
+function spell.onCastSpell(creature, var)
+ return combat:execute(creature, var)
+end
+
+spell:name("soulpit powerless")
+spell:words("###939")
+spell:blockWalls(true)
+spell:needTarget(true)
+spell:needLearn(true)
+spell:isAggressive(true)
+spell:register()
diff --git a/data/items/items.xml b/data/items/items.xml
index 44476042d8b..9b228d3882a 100644
--- a/data/items/items.xml
+++ b/data/items/items.xml
@@ -80328,5 +80328,15 @@ Granted by TibiaGoals.com"/>
+ -
+
+
+
+ -
+
+
+ -
+
+
diff --git a/data/libs/functions/lever.lua b/data/libs/functions/lever.lua
index 39802af49d2..651b1e9e908 100644
--- a/data/libs/functions/lever.lua
+++ b/data/libs/functions/lever.lua
@@ -172,7 +172,7 @@ function Lever.teleportPlayers(self) -- It will teleport all players to the posi
local player = v.creature
if player then
player:teleportTo(v.teleport)
- player:getPosition():sendMagicEffect(v.effect)
+ player:getPosition():sendMagicEffect(v.effect or CONST_ME_TELEPORT)
self:getTeleportPlayerFunc(player)
end
end
diff --git a/data/scripts/lib/register_spells.lua b/data/scripts/lib/register_spells.lua
index f2e7cacee3d..3d945661ae4 100644
--- a/data/scripts/lib/register_spells.lua
+++ b/data/scripts/lib/register_spells.lua
@@ -399,6 +399,32 @@ CrossBeamArea3X2 = {
{ 0, 3, 0 },
}
+AREA_FEAR_OPRESSOR = {
+ { 0, 1, 1, 1, 0 },
+ { 1, 1, 1, 1, 1 },
+ { 1, 1, 3, 1, 1 },
+ { 1, 1, 1, 1, 1 },
+ { 0, 1, 1, 1, 0 },
+}
+
+AREA_ROOT_OPRESSOR = {
+ { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 },
+ { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 },
+ { 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 },
+ { 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 },
+ { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
+ { 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0 },
+ { 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0 },
+ { 1, 1, 1, 1, 1, 0, 0, 3, 0, 0, 1, 1, 1, 1, 1 },
+ { 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0 },
+ { 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0 },
+ { 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0 },
+ { 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0 },
+ { 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0 },
+ { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 },
+ { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 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/schema.sql b/schema.sql
index 6fe1f21cbb8..9d8f3522f69 100644
--- a/schema.sql
+++ b/schema.sql
@@ -149,6 +149,7 @@ CREATE TABLE IF NOT EXISTS `players` (
`forge_dust_level` bigint(21) NOT NULL DEFAULT '100',
`randomize_mount` tinyint(1) NOT NULL DEFAULT '0',
`boss_points` int NOT NULL DEFAULT '0',
+ `animus_mastery` mediumblob DEFAULT NULL,
INDEX `account_id` (`account_id`),
INDEX `vocation` (`vocation`),
CONSTRAINT `players_pk` PRIMARY KEY (`id`),
diff --git a/src/config/config_enums.hpp b/src/config/config_enums.hpp
index 16b91289480..85a685cef3e 100644
--- a/src/config/config_enums.hpp
+++ b/src/config/config_enums.hpp
@@ -16,6 +16,10 @@ enum ConfigKey_t : uint16_t {
AIMBOT_HOTKEY_ENABLED,
ALLOW_CHANGEOUTFIT,
ALLOW_RELOAD,
+ ANIMUS_MASTERY_MAX_MONSTER_XP_MULTIPLIER,
+ ANIMUS_MASTERY_MONSTER_XP_MULTIPLIER,
+ ANIMUS_MASTERY_MONSTERS_XP_MULTIPLIER,
+ ANIMUS_MASTERY_MONSTERS_TO_INCREASE_XP_MULTIPLIER,
AUGMENT_INCREASED_DAMAGE_PERCENT,
AUGMENT_POWERFUL_IMPACT_PERCENT,
AUGMENT_STRONG_IMPACT_PERCENT,
diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp
index e0eefc98298..af7c72c24bc 100644
--- a/src/config/configmanager.cpp
+++ b/src/config/configmanager.cpp
@@ -199,6 +199,9 @@ bool ConfigManager::load() {
loadFloatConfig(L, TRANSCENDANCE_CHANCE_FORMULA_A, "transcendanceChanceFormulaA", 0.0127);
loadFloatConfig(L, TRANSCENDANCE_CHANCE_FORMULA_B, "transcendanceChanceFormulaB", 0.1070);
loadFloatConfig(L, TRANSCENDANCE_CHANCE_FORMULA_C, "transcendanceChanceFormulaC", 0.0073);
+ loadFloatConfig(L, ANIMUS_MASTERY_MAX_MONSTER_XP_MULTIPLIER, "animusMasteryMaxMonsterXpMultiplier", 4.0);
+ loadFloatConfig(L, ANIMUS_MASTERY_MONSTER_XP_MULTIPLIER, "animusMasteryMonsterXpMultiplier", 2.0);
+ loadFloatConfig(L, ANIMUS_MASTERY_MONSTERS_XP_MULTIPLIER, "animusMasteryMonstersXpMultiplier", 0.1);
loadIntConfig(L, ACTIONS_DELAY_INTERVAL, "timeBetweenActions", 200);
loadIntConfig(L, ADVENTURERSBLESSING_LEVEL, "adventurersBlessingLevel", 21);
@@ -345,6 +348,7 @@ bool ConfigManager::load() {
loadIntConfig(L, AUGMENT_INCREASED_DAMAGE_PERCENT, "augmentIncreasedDamagePercent", 5);
loadIntConfig(L, AUGMENT_POWERFUL_IMPACT_PERCENT, "augmentPowerfulImpactPercent", 10);
loadIntConfig(L, AUGMENT_STRONG_IMPACT_PERCENT, "augmentStrongImpactPercent", 7);
+ loadIntConfig(L, ANIMUS_MASTERY_MONSTERS_TO_INCREASE_XP_MULTIPLIER, "animusMasteryMonstersToIncreaseXpMultiplier", 10);
loadStringConfig(L, CORE_DIRECTORY, "coreDirectory", "data");
loadStringConfig(L, DATA_DIRECTORY, "dataPackDirectory", "data-otservbr-global");
diff --git a/src/creatures/CMakeLists.txt b/src/creatures/CMakeLists.txt
index c87a45a0aa0..b48b9c42844 100644
--- a/src/creatures/CMakeLists.txt
+++ b/src/creatures/CMakeLists.txt
@@ -12,6 +12,7 @@ target_sources(${PROJECT_NAME}_lib PRIVATE
npcs/npc.cpp
npcs/npcs.cpp
npcs/spawns/spawn_npc.cpp
+ players/animus_mastery/animus_mastery.cpp
players/grouping/familiars.cpp
players/grouping/groups.cpp
players/grouping/guild.cpp
diff --git a/src/creatures/combat/combat.cpp b/src/creatures/combat/combat.cpp
index 6b7667a71a5..273eb3d382d 100644
--- a/src/creatures/combat/combat.cpp
+++ b/src/creatures/combat/combat.cpp
@@ -641,6 +641,10 @@ void Combat::CombatHealthFunc(const std::shared_ptr &caster, const std
}
}
+ if (targetPlayer && damage.primary.type == COMBAT_HEALING) {
+ damage.primary.value *= targetPlayer->getBuff(BUFF_HEALINGRECEIVED) / 100.;
+ }
+
damage.damageMultiplier += attackerPlayer->wheel()->getMajorStatConditional("Divine Empowerment", WheelMajor_t::DAMAGE);
g_logger().trace("Wheel Divine Empowerment damage multiplier {}", damage.damageMultiplier);
}
@@ -2250,7 +2254,8 @@ void Combat::applyExtensions(const std::shared_ptr &caster, const std:
}
}
} else if (monster) {
- chance = monster->critChance() * 100;
+ chance = monster->getCriticalChance() * 100;
+ bonus = monster->getCriticalDamage() * 100;
}
bonus += damage.criticalDamage;
diff --git a/src/creatures/combat/condition.cpp b/src/creatures/combat/condition.cpp
index 4d4796ec2d5..0b769ce6d91 100644
--- a/src/creatures/combat/condition.cpp
+++ b/src/creatures/combat/condition.cpp
@@ -237,6 +237,9 @@ std::shared_ptr Condition::createCondition(ConditionId_t id, Conditio
case CONDITION_SOUL:
return ObjectPool::allocateShared(id, type, ticks, buff, subId);
+ case CONDITION_LESSERHEX:
+ case CONDITION_INTENSEHEX:
+ case CONDITION_GREATERHEX:
case CONDITION_ATTRIBUTES:
return ObjectPool::allocateShared(id, type, ticks, buff, subId);
@@ -261,6 +264,7 @@ std::shared_ptr Condition::createCondition(ConditionId_t id, Conditio
case CONDITION_MUTED:
case CONDITION_CHANNELMUTEDTICKS:
case CONDITION_YELLTICKS:
+ case CONDITION_POWERLESS:
case CONDITION_PACIFIED:
return ObjectPool::allocateShared(id, type, ticks, buff, subId);
@@ -464,6 +468,23 @@ std::unordered_set ConditionGeneric::getIcons() const {
case CONDITION_ROOTED:
icons.insert(PlayerIcon::Rooted);
break;
+
+ case CONDITION_LESSERHEX:
+ icons.insert(PlayerIcon::LesserHex);
+ break;
+
+ case CONDITION_INTENSEHEX:
+ icons.insert(PlayerIcon::IntenseHex);
+ break;
+
+ case CONDITION_GREATERHEX:
+ icons.insert(PlayerIcon::GreaterHex);
+ break;
+
+ case CONDITION_POWERLESS:
+ icons.insert(PlayerIcon::Powerless);
+ break;
+
case CONDITION_GOSHNARTAINT:
switch (subId) {
case 1:
@@ -1004,6 +1025,11 @@ bool ConditionAttributes::setParam(ConditionParam_t param, int32_t value) {
return true;
}
+ case CONDITION_PARAM_BUFF_HEALINGRECEIVED: {
+ buffsPercent[BUFF_HEALINGRECEIVED] = std::max(0, value);
+ return true;
+ }
+
case CONDITION_PARAM_BUFF_DAMAGEDEALT: {
buffsPercent[BUFF_DAMAGEDEALT] = std::max(0, value);
return true;
diff --git a/src/creatures/combat/spells.cpp b/src/creatures/combat/spells.cpp
index f522eac86a6..2d611f3bfa6 100644
--- a/src/creatures/combat/spells.cpp
+++ b/src/creatures/combat/spells.cpp
@@ -931,7 +931,7 @@ void Spell::addVocMap(uint16_t vocationId, bool b) {
vocSpellMap[vocationId] = b;
}
-SpellGroup_t Spell::getGroup() {
+SpellGroup_t Spell::getGroup() const {
return group;
}
@@ -1058,6 +1058,10 @@ bool InstantSpell::playerCastInstant(const std::shared_ptr &player, std:
return false;
}
+ if (player->hasCondition(CONDITION_POWERLESS) && getGroup() == SPELLGROUP_ATTACK) {
+ return false;
+ }
+
LuaVariant var;
var.instantName = getName();
std::shared_ptr playerTarget = nullptr;
@@ -1379,6 +1383,10 @@ bool RuneSpell::executeUse(const std::shared_ptr &player, const std::sha
return false;
}
+ if (player->hasCondition(CONDITION_POWERLESS) && getGroup() == SPELLGROUP_ATTACK) {
+ return false;
+ }
+
LuaVariant var;
var.runeName = getName();
diff --git a/src/creatures/combat/spells.hpp b/src/creatures/combat/spells.hpp
index 56ff1a330de..e7b8a2a6183 100644
--- a/src/creatures/combat/spells.hpp
+++ b/src/creatures/combat/spells.hpp
@@ -150,7 +150,7 @@ class Spell : public BaseSpell {
[[nodiscard]] const VocSpellMap &getVocMap() const;
void addVocMap(uint16_t vocationId, bool b);
- SpellGroup_t getGroup();
+ SpellGroup_t getGroup() const;
void setGroup(SpellGroup_t g);
SpellGroup_t getSecondaryGroup();
void setSecondaryGroup(SpellGroup_t g);
diff --git a/src/creatures/creatures_definitions.hpp b/src/creatures/creatures_definitions.hpp
index dd554c086b3..017a8db7c62 100644
--- a/src/creatures/creatures_definitions.hpp
+++ b/src/creatures/creatures_definitions.hpp
@@ -116,6 +116,7 @@ enum ConditionType_t : uint8_t {
CONDITION_GREATERHEX = 33,
CONDITION_BAKRAGORE = 34,
CONDITION_GOSHNARTAINT = 35,
+ CONDITION_POWERLESS = 36,
// Need the last ever
CONDITION_COUNT
@@ -222,6 +223,7 @@ enum ConditionParam_t {
CONDITION_PARAM_INCREASE_MANADRAINPERCENT = 80,
CONDITION_PARAM_INCREASE_DROWNPERCENT = 81,
CONDITION_PARAM_CHARM_CHANCE_MODIFIER = 82,
+ CONDITION_PARAM_BUFF_HEALINGRECEIVED = 83,
};
enum stats_t {
@@ -1332,6 +1334,7 @@ enum class CreatureIconModifications_t {
Influenced,
Fiendish,
ReducedHealth,
+ ReducedHealthExclamation,
};
enum class CreatureIconQuests_t {
diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp
index 3535cd0897c..c3c5b4975fd 100644
--- a/src/creatures/monsters/monster.cpp
+++ b/src/creatures/monsters/monster.cpp
@@ -48,6 +48,8 @@ Monster::Monster(const std::shared_ptr &mType) :
internalLight = mType->info.light;
hiddenHealth = mType->info.hiddenHealth;
targetDistance = mType->info.targetDistance;
+ attackSpells = mType->info.attackSpells;
+ defenseSpells = mType->info.defenseSpells;
// Register creature events
for (const std::string &scriptName : mType->info.scripts) {
@@ -249,8 +251,20 @@ bool Monster::canSeeInvisibility() const {
return isImmune(CONDITION_INVISIBLE);
}
-uint16_t Monster::critChance() const {
- return mType->info.critChance;
+void Monster::setCriticalDamage(uint16_t damage) {
+ criticalDamage = damage;
+}
+
+uint16_t Monster::getCriticalDamage() const {
+ return criticalDamage;
+}
+
+void Monster::setCriticalChance(uint16_t chance) {
+ criticalChance = chance;
+}
+
+uint16_t Monster::getCriticalChance() const {
+ return mType->info.critChance + criticalChance;
}
uint32_t Monster::getManaCost() const {
@@ -692,7 +706,8 @@ bool Monster::isOpponent(const std::shared_ptr &creature) const {
}
uint64_t Monster::getLostExperience() const {
- return skillLoss ? mType->info.experience : 0;
+ float extraExperience = forgeStack <= 15 ? (forgeStack + 10) / 10 : 28;
+ return skillLoss ? static_cast(std::round(mType->info.experience * extraExperience)) : 0;
}
uint16_t Monster::getLookCorpse() const {
@@ -1132,7 +1147,7 @@ void Monster::doAttacking(uint32_t interval) {
const Position &myPos = getPosition();
const Position &targetPos = attackedCreature->getPosition();
- for (const spellBlock_t &spellBlock : mType->info.attackSpells) {
+ for (const spellBlock_t &spellBlock : attackSpells) {
bool inRange = false;
if (spellBlock.spell == nullptr || (spellBlock.isMelee && isFleeing())) {
@@ -1184,7 +1199,7 @@ bool Monster::canUseAttack(const Position &pos, const std::shared_ptr
if (isHostile()) {
const Position &targetPos = target->getPosition();
uint32_t distance = std::max(Position::getDistanceX(pos, targetPos), Position::getDistanceY(pos, targetPos));
- for (const spellBlock_t &spellBlock : mType->info.attackSpells) {
+ for (const spellBlock_t &spellBlock : attackSpells) {
if (spellBlock.range != 0 && distance <= spellBlock.range) {
return g_game().isSightClear(pos, targetPos, true);
}
@@ -1279,7 +1294,7 @@ void Monster::onThinkDefense(uint32_t interval) {
bool resetTicks = true;
defenseTicks += interval;
- for (const spellBlock_t &spellBlock : mType->info.defenseSpells) {
+ for (const spellBlock_t &spellBlock : defenseSpells) {
if (spellBlock.speed > defenseTicks) {
resetTicks = false;
continue;
@@ -1329,13 +1344,15 @@ void Monster::onThinkDefense(uint32_t interval) {
}
const auto &summon = Monster::createMonster(summonName);
- if (summon) {
- if (g_game().placeCreature(summon, getPosition(), false, summonForce)) {
- summon->setMaster(static_self_cast(), true);
- g_game().addMagicEffect(getPosition(), CONST_ME_MAGIC_BLUE);
- g_game().addMagicEffect(summon->getPosition(), CONST_ME_TELEPORT);
- g_game().sendSingleSoundEffect(summon->getPosition(), SoundEffect_t::MONSTER_SPELL_SUMMON, getMonster());
+ if (summon && g_game().placeCreature(summon, getPosition(), false, summonForce)) {
+ if (getSoulPit()) {
+ const auto stack = getForgeStack();
+ summon->setSoulPitStack(stack, true);
}
+ summon->setMaster(static_self_cast(), true);
+ g_game().addMagicEffect(getPosition(), CONST_ME_MAGIC_BLUE);
+ g_game().addMagicEffect(summon->getPosition(), CONST_ME_TELEPORT);
+ g_game().sendSingleSoundEffect(summon->getPosition(), SoundEffect_t::MONSTER_SPELL_SUMMON, getMonster());
}
}
}
@@ -2199,6 +2216,24 @@ void Monster::setHazardSystemDefenseBoost(bool value) {
hazardDefenseBoost = value;
}
+bool Monster::getSoulPit() const {
+ return soulPit;
+}
+
+void Monster::setSoulPit(bool value) {
+ soulPit = value;
+}
+
+void Monster::setSoulPitStack(uint8_t stack, bool isSummon /* = false */) {
+ const bool isBoss = stack == 40;
+ const CreatureIconModifications_t icon = isBoss ? CreatureIconModifications_t::ReducedHealthExclamation : CreatureIconModifications_t::ReducedHealth;
+ setForgeStack(stack);
+ setIcon("soulpit", CreatureIcon(icon, isBoss ? 0 : stack));
+ setSoulPit(true);
+ setDropLoot(false);
+ setSkillLoss(isBoss && !isSummon);
+}
+
bool Monster::canWalkTo(Position pos, Direction moveDirection) {
pos = getNextPosition(moveDirection, pos);
if (isInSpawnRange(pos)) {
@@ -2523,6 +2558,15 @@ void Monster::getPathSearchParams(const std::shared_ptr &creature, Fin
}
}
+void Monster::applyStacks() {
+ // Change health based in stacks
+ const auto percentToIncrement = 1 + (15 * forgeStack + 35) / 100.f;
+ auto newHealth = static_cast(std::ceil(static_cast(healthMax) * percentToIncrement));
+
+ healthMax = newHealth;
+ health = newHealth;
+}
+
void Monster::configureForgeSystem() {
if (!canBeForgeMonster()) {
return;
@@ -2539,13 +2583,6 @@ void Monster::configureForgeSystem() {
g_game().updateCreatureIcon(static_self_cast());
}
- // Change health based in stacks
- const auto percentToIncrement = 1 + (15 * forgeStack + 35) / 100.f;
- auto newHealth = static_cast(std::ceil(static_cast(healthMax) * percentToIncrement));
-
- healthMax = newHealth;
- health = newHealth;
-
// Event to give Dusts
const std::string &Eventname = "ForgeSystemMonster";
registerCreatureEvent(Eventname);
@@ -2571,6 +2608,7 @@ uint16_t Monster::getForgeStack() const {
void Monster::setForgeStack(uint16_t stack) {
forgeStack = stack;
+ applyStacks();
}
ForgeClassifications_t Monster::getMonsterForgeClassification() const {
diff --git a/src/creatures/monsters/monster.hpp b/src/creatures/monsters/monster.hpp
index 3661eea9bef..89305e7f5f7 100644
--- a/src/creatures/monsters/monster.hpp
+++ b/src/creatures/monsters/monster.hpp
@@ -77,7 +77,6 @@ class Monster final : public Creature {
bool isHostile() const;
bool isFamiliar() const;
bool canSeeInvisibility() const override;
- uint16_t critChance() const;
uint32_t getManaCost() const;
RespawnType getRespawnType() const;
void setSpawnMonster(const std::shared_ptr &newSpawnMonster);
@@ -179,6 +178,10 @@ class Monster final : public Creature {
void setHazardSystemDefenseBoost(bool value);
// Hazard end
+ bool getSoulPit() const;
+ void setSoulPit(bool value);
+ void setSoulPitStack(uint8_t stack, bool isSummon = false);
+
void updateTargetList();
void clearTargetList();
void clearFriendList();
@@ -187,6 +190,8 @@ class Monster final : public Creature {
static uint32_t monsterAutoID;
+ void applyStacks();
+
void configureForgeSystem();
bool canBeForgeMonster() const;
@@ -225,6 +230,12 @@ class Monster final : public Creature {
void setDead(bool isDead);
+ void setCriticalChance(uint16_t chance);
+ uint16_t getCriticalChance() const;
+
+ void setCriticalDamage(uint16_t damage);
+ uint16_t getCriticalDamage() const;
+
protected:
void onExecuteAsyncTasks() override;
@@ -258,6 +269,9 @@ class Monster final : public Creature {
uint16_t totalPlayersOnScreen = 0;
+ uint16_t criticalChance = 0;
+ uint16_t criticalDamage = 0;
+
uint32_t attackTicks = 0;
uint32_t targetChangeTicks = 0;
uint32_t defenseTicks = 0;
@@ -276,6 +290,9 @@ class Monster final : public Creature {
std::unordered_map m_reflectElementMap;
+ std::vector attackSpells;
+ std::vector defenseSpells;
+
Position masterPos;
bool isWalkingBack = false;
@@ -290,6 +307,8 @@ class Monster final : public Creature {
bool hazardDamageBoost = false;
bool hazardDefenseBoost = false;
+ bool soulPit = false;
+
bool m_isDead = false;
bool m_isImmune = false;
diff --git a/src/creatures/monsters/monsters.cpp b/src/creatures/monsters/monsters.cpp
index cd33e12e248..fd269da1709 100644
--- a/src/creatures/monsters/monsters.cpp
+++ b/src/creatures/monsters/monsters.cpp
@@ -382,3 +382,29 @@ bool Monsters::tryAddMonsterType(const std::string &name, const std::shared_ptr<
monsters[lowerName] = mType;
return true;
}
+
+std::vector> Monsters::getMonstersByRace(BestiaryType_t race) const {
+ std::vector> monstersByRace;
+ const auto &bestiaryList = g_game().getBestiaryList();
+
+ for (const auto &[raceId, name] : bestiaryList) {
+ const auto &monsterType = g_monsters().getMonsterType(name);
+ if (monsterType && monsterType->info.bestiaryRace == race) {
+ monstersByRace.emplace_back(monsterType);
+ }
+ }
+ return monstersByRace;
+}
+
+std::vector> Monsters::getMonstersByBestiaryStars(uint8_t stars) const {
+ std::vector> monstersByStars;
+ const auto &bestiaryList = g_game().getBestiaryList();
+
+ for (const auto &[raceId, name] : bestiaryList) {
+ const auto &monsterType = g_monsters().getMonsterType(name);
+ if (monsterType && monsterType->info.bestiaryStars == stars) {
+ monstersByStars.emplace_back(monsterType);
+ }
+ }
+ return monstersByStars;
+}
diff --git a/src/creatures/monsters/monsters.hpp b/src/creatures/monsters/monsters.hpp
index 35c4d73050d..146f53e604c 100644
--- a/src/creatures/monsters/monsters.hpp
+++ b/src/creatures/monsters/monsters.hpp
@@ -30,22 +30,6 @@ class Loot {
class BaseSpell;
struct spellBlock_t {
- constexpr spellBlock_t() = default;
- ~spellBlock_t() = default;
- spellBlock_t(const spellBlock_t &other) = delete;
- spellBlock_t &operator=(const spellBlock_t &other) = delete;
- spellBlock_t(spellBlock_t &&other) noexcept :
- spell(std::move(other.spell)),
- chance(other.chance),
- speed(other.speed),
- range(other.range),
- minCombatValue(other.minCombatValue),
- maxCombatValue(other.maxCombatValue),
- combatSpell(other.combatSpell),
- isMelee(other.isMelee) {
- other.spell = nullptr;
- }
-
std::shared_ptr spell = nullptr;
uint32_t chance = 100;
uint32_t speed = 2000;
@@ -94,6 +78,8 @@ class MonsterType {
uint32_t maxSummons = 0;
uint32_t changeTargetSpeed = 0;
+ uint32_t soulCore = 0;
+
std::bitset m_conditionImmunities;
std::bitset m_damageImmunities;
@@ -265,6 +251,8 @@ class Monsters {
std::shared_ptr getMonsterTypeByRaceId(uint16_t raceId, bool isBoss = false) const;
bool tryAddMonsterType(const std::string &name, const std::shared_ptr &mType);
bool deserializeSpell(const std::shared_ptr &spell, spellBlock_t &sb, const std::string &description = "") const;
+ std::vector> getMonstersByRace(BestiaryType_t race) const;
+ std::vector> getMonstersByBestiaryStars(uint8_t stars) const;
std::unique_ptr scriptInterface;
std::map> monsters;
diff --git a/src/creatures/players/animus_mastery/animus_mastery.cpp b/src/creatures/players/animus_mastery/animus_mastery.cpp
new file mode 100644
index 00000000000..595c98ac81e
--- /dev/null
+++ b/src/creatures/players/animus_mastery/animus_mastery.cpp
@@ -0,0 +1,72 @@
+/**
+ * Canary - A free and open-source MMORPG server emulator
+ * Copyright (©) 2019-2024 OpenTibiaBR
+ * Repository: https://github.com/opentibiabr/canary
+ * License: https://github.com/opentibiabr/canary/blob/main/LICENSE
+ * Contributors: https://github.com/opentibiabr/canary/graphs/contributors
+ * Website: https://docs.opentibiabr.com/
+ */
+
+// Player.hpp already includes the animus mastery
+#include "creatures/players/player.hpp"
+
+#include "config/configmanager.hpp"
+#include "io/fileloader.hpp"
+#include "utils/tools.hpp"
+
+AnimusMastery::AnimusMastery(Player &player) :
+ m_player(player) {
+ maxMonsterXpMultiplier = g_configManager().getFloat(ANIMUS_MASTERY_MAX_MONSTER_XP_MULTIPLIER);
+ monsterXpMultiplier = g_configManager().getFloat(ANIMUS_MASTERY_MONSTER_XP_MULTIPLIER);
+ monstersXpMultiplier = g_configManager().getFloat(ANIMUS_MASTERY_MONSTERS_XP_MULTIPLIER);
+ monstersAmountToMultiply = std::clamp(g_configManager().getNumber(ANIMUS_MASTERY_MONSTERS_TO_INCREASE_XP_MULTIPLIER), 1, std::numeric_limits::max());
+}
+
+void AnimusMastery::add(const std::string &addMonsterType) {
+ if (!has(addMonsterType)) {
+ const std::string &lowerMonsterName = asLowerCaseString(addMonsterType);
+ animusMasteries.emplace_back(lowerMonsterName);
+ }
+}
+
+void AnimusMastery::remove(const std::string &removeMonsterType) {
+ const std::string &lowerMonsterName = asLowerCaseString(removeMonsterType);
+ std::erase_if(animusMasteries, [lowerMonsterName](const std::string &monsterType) {
+ return asLowerCaseString(monsterType) == lowerMonsterName;
+ });
+}
+
+bool AnimusMastery::has(const std::string &searchMonsterType) const {
+ const std::string &lowerMonsterName = asLowerCaseString(searchMonsterType);
+ auto it = std::ranges::find(animusMasteries, lowerMonsterName);
+
+ return it != animusMasteries.end();
+}
+
+float AnimusMastery::getExperienceMultiplier() const {
+ uint16_t monsterAmountMultiplier = animusMasteries.size() / monstersAmountToMultiply;
+
+ return std::min(maxMonsterXpMultiplier, 1 + (monsterXpMultiplier + (monsterAmountMultiplier * monstersXpMultiplier)) / 100);
+}
+
+uint16_t AnimusMastery::getPoints() const {
+ return animusMasteries.size();
+}
+
+const std::vector &AnimusMastery::getAnimusMasteries() const {
+ return animusMasteries;
+}
+
+void AnimusMastery::serialize(PropWriteStream &propWriteStream) const {
+ std::ranges::for_each(animusMasteries, [&propWriteStream](const std::string &monsterName) {
+ propWriteStream.writeString(monsterName);
+ });
+}
+
+bool AnimusMastery::unserialize(PropStream &propStream) {
+ std::string monsterName;
+ while (propStream.readString(monsterName)) {
+ animusMasteries.emplace_back(monsterName);
+ }
+ return true;
+}
diff --git a/src/creatures/players/animus_mastery/animus_mastery.hpp b/src/creatures/players/animus_mastery/animus_mastery.hpp
new file mode 100644
index 00000000000..e0b9625b4f2
--- /dev/null
+++ b/src/creatures/players/animus_mastery/animus_mastery.hpp
@@ -0,0 +1,43 @@
+/**
+ * Canary - A free and open-source MMORPG server emulator
+ * Copyright (©) 2019-2024 OpenTibiaBR
+ * Repository: https://github.com/opentibiabr/canary
+ * License: https://github.com/opentibiabr/canary/blob/main/LICENSE
+ * Contributors: https://github.com/opentibiabr/canary/graphs/contributors
+ * Website: https://docs.opentibiabr.com/
+ */
+
+#pragma once
+
+class Player;
+class PropStream;
+class PropWriteStream;
+
+class AnimusMastery {
+public:
+ explicit AnimusMastery(Player &player);
+
+ void add(const std::string &addMonsterType);
+ void remove(const std::string &removeMonsterType);
+
+ bool has(const std::string &searchMonsterType) const;
+
+ float getExperienceMultiplier() const;
+
+ uint16_t getPoints() const;
+
+ const std::vector &getAnimusMasteries() const;
+
+ void serialize(PropWriteStream &propWriteStream) const;
+ bool unserialize(PropStream &propStream);
+
+private:
+ Player &m_player;
+
+ float maxMonsterXpMultiplier = 4.0;
+ float monsterXpMultiplier = 2.0;
+ float monstersXpMultiplier = 0.1;
+ uint16_t monstersAmountToMultiply = 10;
+
+ std::vector animusMasteries;
+};
diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp
index a6fb54646e6..d55cb8f5022 100644
--- a/src/creatures/players/player.cpp
+++ b/src/creatures/players/player.cpp
@@ -19,6 +19,7 @@
#include "creatures/monsters/monster.hpp"
#include "creatures/monsters/monsters.hpp"
#include "creatures/npcs/npc.hpp"
+#include "creatures/players/animus_mastery/animus_mastery.hpp"
#include "creatures/players/wheel/player_wheel.hpp"
#include "creatures/players/wheel/wheel_gems.hpp"
#include "creatures/players/achievement/player_achievement.hpp"
@@ -71,7 +72,8 @@ Player::Player(std::shared_ptr p) :
lastPing(OTSYS_TIME()),
lastPong(lastPing),
inbox(std::make_shared(ITEM_INBOX)),
- client(std::move(p)) {
+ client(std::move(p)),
+ m_animusMastery(*this) {
m_playerVIP = std::make_unique(*this);
m_wheelPlayer = std::make_unique(*this);
m_playerAchievement = std::make_unique(*this);
@@ -3134,6 +3136,14 @@ void Player::addExperience(const std::shared_ptr &target, uint64_t exp
exp += (exp * (1.75 * getHazardSystemPoints() * g_configManager().getFloat(HAZARD_EXP_BONUS_MULTIPLIER))) / 100.;
}
+ const bool handleAnimusMastery = monster && animusMastery().has(monster->getMonsterType()->name);
+ float animusMasteryMultiplier = 0;
+
+ if (handleAnimusMastery) {
+ animusMasteryMultiplier = animusMastery().getExperienceMultiplier();
+ exp *= animusMasteryMultiplier;
+ }
+
experience += exp;
if (sendText) {
@@ -3145,6 +3155,10 @@ void Player::addExperience(const std::shared_ptr &target, uint64_t exp
}
}
+ if (handleAnimusMastery) {
+ expString = fmt::format("{} (animus mastery bonus {:.1f}%)", expString, (animusMasteryMultiplier - 1) * 100);
+ }
+
TextMessage message(MESSAGE_EXPERIENCE, "You gained " + expString + (handleHazardExperience ? " (Hazard)" : ""));
message.position = position;
message.primary.value = exp;
@@ -5854,9 +5868,11 @@ bool Player::onKilledMonster(const std::shared_ptr &monster) {
g_logger().error("[{}] Monster type is null.", __FUNCTION__);
return false;
}
- addHuntingTaskKill(mType);
- addBestiaryKill(mType);
- addBosstiaryKill(mType);
+ if (!monster->getSoulPit()) {
+ addHuntingTaskKill(mType);
+ addBestiaryKill(mType);
+ addBosstiaryKill(mType);
+ }
return false;
}
@@ -10367,6 +10383,15 @@ const std::unique_ptr &Player::title() const {
return m_playerTitle;
}
+// Cyclopedia interface
+std::unique_ptr &Player::cyclopedia() {
+ return m_playerCyclopedia;
+}
+
+const std::unique_ptr &Player::cyclopedia() const {
+ return m_playerCyclopedia;
+}
+
// VIP interface
std::unique_ptr &Player::vip() {
return m_playerVIP;
@@ -10376,13 +10401,13 @@ const std::unique_ptr &Player::vip() const {
return m_playerVIP;
}
-// Cyclopedia
-std::unique_ptr &Player::cyclopedia() {
- return m_playerCyclopedia;
+// Animus Mastery interface
+AnimusMastery &Player::animusMastery() {
+ return m_animusMastery;
}
-const std::unique_ptr &Player::cyclopedia() const {
- return m_playerCyclopedia;
+const AnimusMastery &Player::animusMastery() const {
+ return m_animusMastery;
}
void Player::sendLootMessage(const std::string &message) const {
diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp
index 8f99f9d1821..28ef63ec8ef 100644
--- a/src/creatures/players/player.hpp
+++ b/src/creatures/players/player.hpp
@@ -16,7 +16,9 @@
#include "items/cylinder.hpp"
#include "game/movement/position.hpp"
#include "creatures/creatures_definitions.hpp"
+#include "creatures/players/animus_mastery/animus_mastery.hpp"
+class AnimusMastery;
class House;
class NetworkMessage;
class Weapon;
@@ -1270,7 +1272,7 @@ class Player final : public Creature, public Cylinder, public Bankable {
std::unique_ptr &title();
const std::unique_ptr &title() const;
- // Player summary interface
+ // Player cyclopedia interface
std::unique_ptr &cyclopedia();
const std::unique_ptr &cyclopedia() const;
@@ -1278,6 +1280,10 @@ class Player final : public Creature, public Cylinder, public Bankable {
std::unique_ptr &vip();
const std::unique_ptr &vip() const;
+ // Player animusMastery interface
+ AnimusMastery &animusMastery();
+ const AnimusMastery &animusMastery() const;
+
void sendLootMessage(const std::string &message) const;
std::shared_ptr getLootPouch();
@@ -1654,6 +1660,7 @@ class Player final : public Creature, public Cylinder, public Bankable {
std::unique_ptr m_playerCyclopedia;
std::unique_ptr m_playerTitle;
std::unique_ptr m_playerVIP;
+ AnimusMastery m_animusMastery;
std::mutex quickLootMutex;
diff --git a/src/enums/player_icons.hpp b/src/enums/player_icons.hpp
index 7878d9e5037..ecab8a5d8ce 100644
--- a/src/enums/player_icons.hpp
+++ b/src/enums/player_icons.hpp
@@ -42,6 +42,7 @@ enum class PlayerIcon : uint8_t {
GoshnarTaint5 = 25,
NewManaShield = 26,
Agony = 27,
+ Powerless = 28,
// Must always be the last
Count
diff --git a/src/game/game.cpp b/src/game/game.cpp
index 09fa4346773..d755c05bb9e 100644
--- a/src/game/game.cpp
+++ b/src/game/game.cpp
@@ -7649,8 +7649,12 @@ void Game::buildMessageAsTarget(
const std::string &damageString
) const {
ss.str({});
- auto attackMsg = damage.critical ? "critical " : "";
- auto article = damage.critical ? "a" : "an";
+ const auto &monster = attacker ? attacker->getMonster() : nullptr;
+ bool handleSoulPit = monster ? monster->getSoulPit() && monster->getForgeStack() == 40 : false;
+
+ std::string attackMsg = damage.critical && !handleSoulPit ? "critical " : "";
+ std::string article = damage.critical && !handleSoulPit ? "a" : "an";
+
ss << "You lose " << damageString;
if (!attacker) {
ss << '.';
@@ -7662,6 +7666,9 @@ void Game::buildMessageAsTarget(
if (damage.extension) {
ss << " " << damage.exString;
}
+ if (handleSoulPit && damage.critical) {
+ ss << " (Soulpit Crit)";
+ }
message.type = MESSAGE_DAMAGE_RECEIVED;
message.text = ss.str();
}
diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp
index 54d7135edf0..9410a5535cc 100644
--- a/src/io/functions/iologindata_load_player.cpp
+++ b/src/io/functions/iologindata_load_player.cpp
@@ -14,6 +14,7 @@
#include "creatures/combat/condition.hpp"
#include "database/database.hpp"
#include "creatures/monsters/monsters.hpp"
+#include "creatures/players/animus_mastery/animus_mastery.hpp"
#include "creatures/players/achievement/player_achievement.hpp"
#include "creatures/players/cyclopedia/player_badge.hpp"
#include "creatures/players/cyclopedia/player_cyclopedia.hpp"
@@ -275,6 +276,20 @@ void IOLoginDataLoad::loadPlayerConditions(const std::shared_ptr &player
}
}
+void IOLoginDataLoad::loadPlayerAnimusMastery(const std::shared_ptr &player, const DBResult_ptr &result) {
+ if (!result || !player) {
+ g_logger().warn("[{}] - Player or Result nullptr", __FUNCTION__);
+ return;
+ }
+
+ unsigned long attrSize;
+ const char* attr = result->getStream("animus_mastery", attrSize);
+ PropStream propStream;
+ propStream.init(attr, attrSize);
+
+ player->animusMastery().unserialize(propStream);
+}
+
void IOLoginDataLoad::loadPlayerDefaultOutfit(const std::shared_ptr &player, const DBResult_ptr &result) {
if (!result || !player) {
g_logger().warn("[{}] - Player or Result nullptr", __FUNCTION__);
diff --git a/src/io/functions/iologindata_load_player.hpp b/src/io/functions/iologindata_load_player.hpp
index ca05bdcb1b8..08d31e93072 100644
--- a/src/io/functions/iologindata_load_player.hpp
+++ b/src/io/functions/iologindata_load_player.hpp
@@ -22,6 +22,7 @@ class IOLoginDataLoad : public IOLoginData {
static void loadPlayerExperience(const std::shared_ptr &player, const DBResult_ptr &result);
static void loadPlayerBlessings(const std::shared_ptr &player, const DBResult_ptr &result);
static void loadPlayerConditions(const std::shared_ptr &player, const DBResult_ptr &result);
+ static void loadPlayerAnimusMastery(const std::shared_ptr &player, const DBResult_ptr &result);
static void loadPlayerDefaultOutfit(const std::shared_ptr &player, const DBResult_ptr &result);
static void loadPlayerSkullSystem(const std::shared_ptr &player, const DBResult_ptr &result);
static void loadPlayerSkill(const std::shared_ptr &player, const DBResult_ptr &result);
diff --git a/src/io/functions/iologindata_save_player.cpp b/src/io/functions/iologindata_save_player.cpp
index 613fdac1cb0..11888ee6660 100644
--- a/src/io/functions/iologindata_save_player.cpp
+++ b/src/io/functions/iologindata_save_player.cpp
@@ -10,6 +10,7 @@
#include "io/functions/iologindata_save_player.hpp"
#include "config/configmanager.hpp"
+#include "creatures/players/animus_mastery/animus_mastery.hpp"
#include "creatures/combat/condition.hpp"
#include "creatures/monsters/monsters.hpp"
#include "game/game.hpp"
@@ -246,6 +247,14 @@ bool IOLoginDataSave::savePlayerFirst(const std::shared_ptr &player) {
query << "`conditions` = " << db.escapeBlob(attributes, static_cast(attributesSize)) << ",";
+ // serialize animus mastery
+ PropWriteStream propAnimusMasteryStream;
+ player->animusMastery().serialize(propAnimusMasteryStream);
+ size_t animusMasterySize;
+ const char* animusMastery = propAnimusMasteryStream.getStream(animusMasterySize);
+
+ query << "`animus_mastery` = " << db.escapeBlob(animusMastery, static_cast(animusMasterySize)) << ",";
+
if (g_game().getWorldType() != WORLD_TYPE_PVP_ENFORCED) {
int64_t skullTime = 0;
diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp
index 90c6a99c62e..0df4de64e2b 100644
--- a/src/io/iologindata.cpp
+++ b/src/io/iologindata.cpp
@@ -113,6 +113,9 @@ bool IOLoginData::loadPlayer(const std::shared_ptr &player, const DBResu
// load conditions
IOLoginDataLoad::loadPlayerConditions(player, result);
+ // load animus mastery
+ IOLoginDataLoad::loadPlayerAnimusMastery(player, result);
+
// load default outfit
IOLoginDataLoad::loadPlayerDefaultOutfit(player, result);
diff --git a/src/items/functions/item/item_parse.hpp b/src/items/functions/item/item_parse.hpp
index 6fe6fbcccd3..43a4f79433c 100644
--- a/src/items/functions/item/item_parse.hpp
+++ b/src/items/functions/item/item_parse.hpp
@@ -179,7 +179,7 @@ const phmap::flat_hash_map ItemTypesMap = {
{ "food", ITEM_TYPE_FOOD },
{ "valuable", ITEM_TYPE_VALUABLE },
{ "potion", ITEM_TYPE_POTION },
-
+ { "soulcore", ITEM_TYPE_SOULCORES },
{ "ladder", ITEM_TYPE_LADDER },
{ "dummy", ITEM_TYPE_DUMMY },
};
diff --git a/src/items/items.hpp b/src/items/items.hpp
index b977f0bf7a9..f4eae14df27 100644
--- a/src/items/items.hpp
+++ b/src/items/items.hpp
@@ -415,6 +415,10 @@ class Items {
return items.size();
}
+ std::vector &getItems() {
+ return items;
+ }
+
NameMap nameToItems;
void addLadderId(uint16_t newId) {
diff --git a/src/lua/functions/core/game/game_functions.cpp b/src/lua/functions/core/game/game_functions.cpp
index eec876eee7c..eff01af0313 100644
--- a/src/lua/functions/core/game/game_functions.cpp
+++ b/src/lua/functions/core/game/game_functions.cpp
@@ -66,6 +66,7 @@ void GameFunctions::init(lua_State* L) {
Lua::registerMethod(L, "Game", "createItem", GameFunctions::luaGameCreateItem);
Lua::registerMethod(L, "Game", "createContainer", GameFunctions::luaGameCreateContainer);
Lua::registerMethod(L, "Game", "createMonster", GameFunctions::luaGameCreateMonster);
+ Lua::registerMethod(L, "Game", "createSoulPitMonster", GameFunctions::luaGameCreateSoulPitMonster);
Lua::registerMethod(L, "Game", "createNpc", GameFunctions::luaGameCreateNpc);
Lua::registerMethod(L, "Game", "generateNpc", GameFunctions::luaGameGenerateNpc);
Lua::registerMethod(L, "Game", "createTile", GameFunctions::luaGameCreateTile);
@@ -107,6 +108,11 @@ void GameFunctions::init(lua_State* L) {
Lua::registerMethod(L, "Game", "getSecretAchievements", GameFunctions::luaGameGetSecretAchievements);
Lua::registerMethod(L, "Game", "getPublicAchievements", GameFunctions::luaGameGetPublicAchievements);
Lua::registerMethod(L, "Game", "getAchievements", GameFunctions::luaGameGetAchievements);
+
+ Lua::registerMethod(L, "Game", "getSoulCoreItems", GameFunctions::luaGameGetSoulCoreItems);
+
+ Lua::registerMethod(L, "Game", "getMonstersByRace", GameFunctions::luaGameGetMonstersByRace);
+ Lua::registerMethod(L, "Game", "getMonstersByBestiaryStars", GameFunctions::luaGameGetMonstersByBestiaryStars);
}
// Game
@@ -546,6 +552,41 @@ int GameFunctions::luaGameCreateMonster(lua_State* L) {
return 1;
}
+int GameFunctions::luaGameCreateSoulPitMonster(lua_State* L) {
+ // Game.createSoulPitMonster(monsterName, position, [stack = 1, [, extended = false[, force = false[, master = nil]]]])
+ const auto &monster = Monster::createMonster(Lua::getString(L, 1));
+ if (!monster) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ bool isSummon = false;
+ if (lua_gettop(L) >= 6) {
+ if (const auto &master = Lua::getCreature(L, 6)) {
+ monster->setMaster(master, true);
+ isSummon = true;
+ }
+ }
+
+ const Position &position = Lua::getPosition(L, 2);
+ const uint8_t stack = Lua::getNumber(L, 3, 1);
+ const bool extended = Lua::getBoolean(L, 4, false);
+ const bool force = Lua::getBoolean(L, 5, false);
+ if (g_game().placeCreature(monster, position, extended, force)) {
+ monster->setSoulPitStack(stack);
+ monster->onSpawn(position);
+
+ Lua::pushUserdata(L, monster);
+ Lua::setMetatable(L, -1, "Monster");
+ } else {
+ if (isSummon) {
+ monster->setMaster(nullptr);
+ }
+ lua_pushnil(L);
+ }
+ return 1;
+}
+
int GameFunctions::luaGameGenerateNpc(lua_State* L) {
// Game.generateNpc(npcName)
const auto &npc = Npc::createNpc(Lua::getString(L, 1));
@@ -994,3 +1035,55 @@ int GameFunctions::luaGameGetAchievements(lua_State* L) {
}
return 1;
}
+
+int GameFunctions::luaGameGetSoulCoreItems(lua_State* L) {
+ // Game.getSoulCoreItems()
+ std::vector soulCoreItems;
+
+ for (const auto &itemType : Item::items.getItems()) {
+ if (itemType.m_primaryType == "SoulCores" || itemType.type == ITEM_TYPE_SOULCORES) {
+ soulCoreItems.emplace_back(&itemType);
+ }
+ }
+
+ lua_createtable(L, soulCoreItems.size(), 0);
+
+ int index = 0;
+ for (const auto* itemType : soulCoreItems) {
+ Lua::pushUserdata(L, itemType);
+ Lua::setMetatable(L, -1, "ItemType");
+ lua_rawseti(L, -2, ++index);
+ }
+
+ return 1;
+}
+
+int GameFunctions::luaGameGetMonstersByRace(lua_State* L) {
+ // Game.getMonstersByRace(race)
+ const BestiaryType_t race = Lua::getNumber(L, 1);
+ const auto monstersByRace = g_monsters().getMonstersByRace(race);
+
+ lua_createtable(L, monstersByRace.size(), 0);
+ int index = 0;
+ for (const auto &monsterType : monstersByRace) {
+ Lua::pushUserdata(L, monsterType);
+ Lua::setMetatable(L, -1, "MonsterType");
+ lua_rawseti(L, -2, ++index);
+ }
+ return 1;
+}
+
+int GameFunctions::luaGameGetMonstersByBestiaryStars(lua_State* L) {
+ // Game.getMonstersByBestiaryStars(stars)
+ const uint8_t stars = Lua::getNumber(L, 1);
+ const auto monstersByStars = g_monsters().getMonstersByBestiaryStars(stars);
+
+ lua_createtable(L, monstersByStars.size(), 0);
+ int index = 0;
+ for (const auto &monsterType : monstersByStars) {
+ Lua::pushUserdata(L, monsterType);
+ Lua::setMetatable(L, -1, "MonsterType");
+ lua_rawseti(L, -2, ++index);
+ }
+ return 1;
+}
diff --git a/src/lua/functions/core/game/game_functions.hpp b/src/lua/functions/core/game/game_functions.hpp
index 6d332face9f..b24b960368c 100644
--- a/src/lua/functions/core/game/game_functions.hpp
+++ b/src/lua/functions/core/game/game_functions.hpp
@@ -46,6 +46,7 @@ class GameFunctions {
static int luaGameCreateItem(lua_State* L);
static int luaGameCreateContainer(lua_State* L);
static int luaGameCreateMonster(lua_State* L);
+ static int luaGameCreateSoulPitMonster(lua_State* L);
static int luaGameGenerateNpc(lua_State* L);
static int luaGameCreateNpc(lua_State* L);
static int luaGameCreateTile(lua_State* L);
@@ -88,4 +89,9 @@ class GameFunctions {
static int luaGameGetSecretAchievements(lua_State* L);
static int luaGameGetPublicAchievements(lua_State* L);
static int luaGameGetAchievements(lua_State* L);
+
+ static int luaGameGetSoulCoreItems(lua_State* L);
+
+ static int luaGameGetMonstersByRace(lua_State* L);
+ static int luaGameGetMonstersByBestiaryStars(lua_State* L);
};
diff --git a/src/lua/functions/creatures/monster/monster_functions.cpp b/src/lua/functions/creatures/monster/monster_functions.cpp
index 3f5d0c25ea8..7d73d7bbea9 100644
--- a/src/lua/functions/creatures/monster/monster_functions.cpp
+++ b/src/lua/functions/creatures/monster/monster_functions.cpp
@@ -66,6 +66,8 @@ void MonsterFunctions::init(lua_State* L) {
Lua::registerMethod(L, "Monster", "hazardDamageBoost", MonsterFunctions::luaMonsterHazardDamageBoost);
Lua::registerMethod(L, "Monster", "hazardDefenseBoost", MonsterFunctions::luaMonsterHazardDefenseBoost);
+ Lua::registerMethod(L, "Monster", "soulPit", MonsterFunctions::luaMonsterSoulPit);
+
Lua::registerMethod(L, "Monster", "addReflectElement", MonsterFunctions::luaMonsterAddReflectElement);
Lua::registerMethod(L, "Monster", "addDefense", MonsterFunctions::luaMonsterAddDefense);
Lua::registerMethod(L, "Monster", "getDefense", MonsterFunctions::luaMonsterGetDefense);
@@ -73,6 +75,12 @@ void MonsterFunctions::init(lua_State* L) {
Lua::registerMethod(L, "Monster", "isDead", MonsterFunctions::luaMonsterIsDead);
Lua::registerMethod(L, "Monster", "immune", MonsterFunctions::luaMonsterImmune);
+ Lua::registerMethod(L, "Monster", "criticalChance", MonsterFunctions::luaMonsterCriticalChance);
+ Lua::registerMethod(L, "Monster", "criticalDamage", MonsterFunctions::luaMonsterCriticalDamage);
+
+ Lua::registerMethod(L, "Monster", "addAttackSpell", MonsterFunctions::luaMonsterAddAttackSpell);
+ Lua::registerMethod(L, "Monster", "addDefenseSpell", MonsterFunctions::luaMonsterAddDefenseSpell);
+
CharmFunctions::init(L);
LootFunctions::init(L);
MonsterSpellFunctions::init(L);
@@ -700,6 +708,23 @@ int MonsterFunctions::luaMonsterHazardDefenseBoost(lua_State* L) {
return 1;
}
+int MonsterFunctions::luaMonsterSoulPit(lua_State* L) {
+ // get: monster:soulPit() ; set: monster:soulPit(hazard)
+ const auto &monster = Lua::getUserdataShared(L, 1);
+ const bool soulPit = Lua::getBoolean(L, 2, false);
+ if (monster) {
+ if (lua_gettop(L) == 1) {
+ Lua::pushBoolean(L, monster->getSoulPit());
+ } else {
+ monster->setSoulPit(soulPit);
+ Lua::pushBoolean(L, monster->getSoulPit());
+ }
+ } else {
+ lua_pushnil(L);
+ }
+ return 1;
+}
+
int MonsterFunctions::luaMonsterAddReflectElement(lua_State* L) {
// monster:addReflectElement(type, percent)
const auto &monster = Lua::getUserdataShared(L, 1);
@@ -772,3 +797,81 @@ int MonsterFunctions::luaMonsterImmune(lua_State* L) {
Lua::pushBoolean(L, monster->isImmune());
return 1;
}
+
+int MonsterFunctions::luaMonsterCriticalChance(lua_State* L) {
+ // get: monster:criticalChance(); set: monster:criticalChance(critical)
+ const auto &monster = Lua::getUserdataShared(L, 1);
+ const auto critical = Lua::getNumber(L, 2, 0);
+ if (monster) {
+ if (lua_gettop(L) == 1) {
+ Lua::pushBoolean(L, monster->getCriticalChance());
+ } else {
+ monster->setCriticalChance(critical);
+ Lua::pushBoolean(L, monster->getCriticalChance());
+ }
+ } else {
+ lua_pushnil(L);
+ }
+ return 1;
+}
+
+int MonsterFunctions::luaMonsterCriticalDamage(lua_State* L) {
+ // get: monster:criticalDamage(); set: monster:criticalDamage(damage)
+ const auto &monster = Lua::getUserdataShared(L, 1);
+ const auto damage = Lua::getNumber(L, 2, 0);
+ if (monster) {
+ if (lua_gettop(L) == 1) {
+ Lua::pushBoolean(L, monster->getCriticalDamage());
+ } else {
+ monster->setCriticalDamage(damage);
+ Lua::pushBoolean(L, monster->getCriticalDamage());
+ }
+ } else {
+ lua_pushnil(L);
+ }
+ return 1;
+}
+
+int MonsterFunctions::luaMonsterAddAttackSpell(lua_State* L) {
+ // monster:addAttackSpell(monsterspell)
+ const auto &monster = Lua::getUserdataShared(L, 1);
+ if (monster) {
+ const auto &spell = Lua::getUserdataShared(L, 2);
+ if (spell) {
+ spellBlock_t sb;
+ const auto &monsterName = monster->getName();
+ if (g_monsters().deserializeSpell(spell, sb, monsterName)) {
+ monster->attackSpells.push_back(std::move(sb));
+ } else {
+ g_logger().warn("Monster: {}, cant load spell: {}", monsterName, spell->name);
+ }
+ } else {
+ lua_pushnil(L);
+ }
+ } else {
+ lua_pushnil(L);
+ }
+ return 1;
+}
+
+int MonsterFunctions::luaMonsterAddDefenseSpell(lua_State* L) {
+ // monster:addDefenseSpell(monsterspell)
+ const auto &monster = Lua::getUserdataShared(L, 1);
+ if (monster) {
+ const auto &spell = Lua::getUserdataShared(L, 2);
+ if (spell) {
+ spellBlock_t sb;
+ const auto &monsterName = monster->getName();
+ if (g_monsters().deserializeSpell(spell, sb, monsterName)) {
+ monster->defenseSpells.push_back(std::move(sb));
+ } else {
+ g_logger().warn("Monster: {}, Cant load spell: {}", monsterName, spell->name);
+ }
+ } else {
+ lua_pushnil(L);
+ }
+ } else {
+ lua_pushnil(L);
+ }
+ return 1;
+}
diff --git a/src/lua/functions/creatures/monster/monster_functions.hpp b/src/lua/functions/creatures/monster/monster_functions.hpp
index 4ba696e941b..a7c6a5269e1 100644
--- a/src/lua/functions/creatures/monster/monster_functions.hpp
+++ b/src/lua/functions/creatures/monster/monster_functions.hpp
@@ -76,8 +76,15 @@ class MonsterFunctions {
static int luaMonsterAddDefense(lua_State* L);
static int luaMonsterGetDefense(lua_State* L);
+ static int luaMonsterSoulPit(lua_State* L);
+
static int luaMonsterIsDead(lua_State* L);
static int luaMonsterImmune(lua_State* L);
+ static int luaMonsterCriticalChance(lua_State* L);
+ static int luaMonsterCriticalDamage(lua_State* L);
+
+ static int luaMonsterAddAttackSpell(lua_State* L);
+ static int luaMonsterAddDefenseSpell(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 8b2f4a5f314..03ade91f273 100644
--- a/src/lua/functions/creatures/monster/monster_type_functions.cpp
+++ b/src/lua/functions/creatures/monster/monster_type_functions.cpp
@@ -151,6 +151,8 @@ void MonsterTypeFunctions::init(lua_State* L) {
Lua::registerMethod(L, "MonsterType", "deathSound", MonsterTypeFunctions::luaMonsterTypedeathSound);
Lua::registerMethod(L, "MonsterType", "variant", MonsterTypeFunctions::luaMonsterTypeVariant);
+ Lua::registerMethod(L, "MonsterType", "getMonstersByRace", MonsterTypeFunctions::luaMonsterTypeGetMonstersByRace);
+ Lua::registerMethod(L, "MonsterType", "getMonstersByBestiaryStars", MonsterTypeFunctions::luaMonsterTypeGetMonstersByBestiaryStars);
}
void MonsterTypeFunctions::createMonsterTypeLootLuaTable(lua_State* L, const std::vector &lootList) {
@@ -658,6 +660,22 @@ int MonsterTypeFunctions::luaMonsterTypeRaceid(lua_State* L) {
return 1;
}
+int MonsterTypeFunctions::luaMonsterTypeSoulCore(lua_State* L) {
+ // get: monsterType:luaMonsterTypeSoulCore() set: monsterType:luaMonsterTypeSoulCore(id)
+ const auto &monsterType = Lua::getUserdataShared(L, 1);
+ if (monsterType) {
+ if (lua_gettop(L) == 1) {
+ lua_pushnumber(L, monsterType->info.soulCore);
+ } else {
+ monsterType->info.soulCore = Lua::getNumber(L, 2);
+ Lua::pushBoolean(L, true);
+ }
+ } else {
+ lua_pushnil(L);
+ }
+ return 1;
+}
+
int MonsterTypeFunctions::luaMonsterTypeBestiarytoKill(lua_State* L) {
// get: monsterType:BestiarytoKill() set: monsterType:BestiarytoKill(value)
const auto &monsterType = Lua::getUserdataShared(L, 1);
@@ -1857,3 +1875,33 @@ int MonsterTypeFunctions::luaMonsterTypeVariant(lua_State* L) {
return 1;
}
+
+int MonsterTypeFunctions::luaMonsterTypeGetMonstersByRace(lua_State* L) {
+ // monsterType:getMonstersByRace(race)
+ const BestiaryType_t race = Lua::getNumber(L, 1);
+ const auto monstersByRace = g_monsters().getMonstersByRace(race);
+
+ lua_createtable(L, monstersByRace.size(), 0);
+ int index = 0;
+ for (const auto &monsterType : monstersByRace) {
+ Lua::pushUserdata(L, monsterType);
+ Lua::setMetatable(L, -1, "MonsterType");
+ lua_rawseti(L, -2, ++index);
+ }
+ return 1;
+}
+
+int MonsterTypeFunctions::luaMonsterTypeGetMonstersByBestiaryStars(lua_State* L) {
+ // monsterType:getMonstersByBestiaryStars(stars)
+ const uint8_t stars = Lua::getNumber(L, 1);
+ const auto monstersByStars = g_monsters().getMonstersByBestiaryStars(stars);
+
+ lua_createtable(L, monstersByStars.size(), 0);
+ int index = 0;
+ for (const auto &monsterType : monstersByStars) {
+ Lua::pushUserdata(L, monsterType);
+ Lua::setMetatable(L, -1, "MonsterType");
+ lua_rawseti(L, -2, ++index);
+ }
+ return 1;
+}
diff --git a/src/lua/functions/creatures/monster/monster_type_functions.hpp b/src/lua/functions/creatures/monster/monster_type_functions.hpp
index e272ea45cd7..0994edaed23 100644
--- a/src/lua/functions/creatures/monster/monster_type_functions.hpp
+++ b/src/lua/functions/creatures/monster/monster_type_functions.hpp
@@ -131,6 +131,8 @@ class MonsterTypeFunctions {
static int luaMonsterTypeBossRace(lua_State* L);
static int luaMonsterTypeBossRaceId(lua_State* L);
+ static int luaMonsterTypeSoulCore(lua_State* L);
+
static int luaMonsterTypeSoundChance(lua_State* L);
static int luaMonsterTypeSoundSpeedTicks(lua_State* L);
static int luaMonsterTypeAddSound(lua_State* L);
@@ -139,4 +141,6 @@ class MonsterTypeFunctions {
static int luaMonsterTypeCritChance(lua_State* L);
static int luaMonsterTypeVariant(lua_State* L);
+ static int luaMonsterTypeGetMonstersByRace(lua_State* L);
+ static int luaMonsterTypeGetMonstersByBestiaryStars(lua_State* L);
};
diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp
index 46fdebd016e..97d3a07f876 100644
--- a/src/lua/functions/creatures/player/player_functions.cpp
+++ b/src/lua/functions/creatures/player/player_functions.cpp
@@ -15,6 +15,7 @@
#include "creatures/creature.hpp"
#include "creatures/interactions/chat.hpp"
#include "creatures/monsters/monsters.hpp"
+#include "creatures/players/animus_mastery/animus_mastery.hpp"
#include "creatures/players/achievement/player_achievement.hpp"
#include "creatures/players/cyclopedia/player_cyclopedia.hpp"
#include "creatures/players/cyclopedia/player_title.hpp"
@@ -404,6 +405,10 @@ void PlayerFunctions::init(lua_State* L) {
Lua::registerMethod(L, "Player", "removeIconBakragore", PlayerFunctions::luaPlayerRemoveIconBakragore);
Lua::registerMethod(L, "Player", "sendCreatureAppear", PlayerFunctions::luaPlayerSendCreatureAppear);
+ Lua::registerMethod(L, "Player", "addAnimusMastery", PlayerFunctions::luaPlayerAddAnimusMastery);
+ Lua::registerMethod(L, "Player", "removeAnimusMastery", PlayerFunctions::luaPlayerRemoveAnimusMastery);
+ Lua::registerMethod(L, "Player", "hasAnimusMastery", PlayerFunctions::luaPlayerHasAnimusMastery);
+
GroupFunctions::init(L);
GuildFunctions::init(L);
MountFunctions::init(L);
@@ -4870,3 +4875,42 @@ int PlayerFunctions::luaPlayerSendCreatureAppear(lua_State* L) {
Lua::pushBoolean(L, true);
return 1;
}
+
+int PlayerFunctions::luaPlayerAddAnimusMastery(lua_State* L) {
+ auto player = Lua::getUserdataShared(L, 1);
+ if (!player) {
+ Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND));
+ return 1;
+ }
+
+ const std::string &monsterType = Lua::getString(L, 2);
+ player->animusMastery().add(monsterType);
+
+ return 1;
+}
+int PlayerFunctions::luaPlayerRemoveAnimusMastery(lua_State* L) {
+ auto player = Lua::getUserdataShared(L, 1);
+ if (!player) {
+ Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND));
+ return 1;
+ }
+
+ const std::string &monsterType = Lua::getString(L, 2);
+ player->animusMastery().remove(monsterType);
+
+ return 1;
+}
+int PlayerFunctions::luaPlayerHasAnimusMastery(lua_State* L) {
+ auto player = Lua::getUserdataShared(L, 1);
+ if (!player) {
+ Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND));
+ return 1;
+ }
+
+ const std::string &monsterType = Lua::getString(L, 2);
+
+ bool has = player->animusMastery().has(monsterType);
+ Lua::pushBoolean(L, has);
+
+ return 1;
+}
diff --git a/src/lua/functions/creatures/player/player_functions.hpp b/src/lua/functions/creatures/player/player_functions.hpp
index 8e4f1381b7b..54e57d56155 100644
--- a/src/lua/functions/creatures/player/player_functions.hpp
+++ b/src/lua/functions/creatures/player/player_functions.hpp
@@ -384,5 +384,9 @@ class PlayerFunctions {
static int luaPlayerSendCreatureAppear(lua_State* L);
+ static int luaPlayerAddAnimusMastery(lua_State* L);
+ static int luaPlayerRemoveAnimusMastery(lua_State* L);
+ static int luaPlayerHasAnimusMastery(lua_State* L);
+
friend class CreatureFunctions;
};
diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp
index 9d385be58c7..4cdc5ecbdf1 100644
--- a/src/server/network/protocol/protocolgame.cpp
+++ b/src/server/network/protocol/protocolgame.cpp
@@ -19,6 +19,7 @@
#include "creatures/monsters/monster.hpp"
#include "creatures/monsters/monsters.hpp"
#include "creatures/npcs/npc.hpp"
+#include "creatures/players/animus_mastery/animus_mastery.hpp"
#include "creatures/players/achievement/player_achievement.hpp"
#include "creatures/players/cyclopedia/player_badge.hpp"
#include "creatures/players/cyclopedia/player_cyclopedia.hpp"
@@ -2390,9 +2391,13 @@ void ProtocolGame::parseBestiarysendMonsterData(NetworkMessage &msg) {
newmsg.addByte(currentLevel);
- newmsg.add(0); // Animus Mastery Bonus
- newmsg.add(0); // Animus Mastery Points
-
+ if (player->animusMastery().has(mtype->name)) {
+ newmsg.add(static_cast(std::round((player->animusMastery().getExperienceMultiplier() - 1) * 1000))); // Animus Mastery Bonus
+ newmsg.add(player->animusMastery().getPoints()); // Animus Mastery Points
+ } else {
+ newmsg.add(0);
+ newmsg.add(0);
+ }
newmsg.add(killCounter);
newmsg.add(mtype->info.bestiaryFirstUnlock);
@@ -3026,10 +3031,15 @@ void ProtocolGame::parseBestiarysendCreatures(NetworkMessage &msg) {
newmsg.addByte(0);
}
- newmsg.add(0); // Creature Animous Bonus
+ const auto monsterType = g_monsters().getMonsterType(it_.second);
+ if (monsterType && player->animusMastery().has(it_.second)) {
+ newmsg.add(static_cast(std::round((player->animusMastery().getExperienceMultiplier() - 1) * 1000))); // Animus Mastery Bonus
+ } else {
+ newmsg.add(0);
+ }
}
- newmsg.add(0); // Animus Mastery Points
+ newmsg.add(player->animusMastery().getPoints()); // Animus Mastery Points
writeToOutputBuffer(newmsg);
}