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); }