diff --git a/cmake/modules/BaseConfig.cmake b/cmake/modules/BaseConfig.cmake index 14dc6e22153..cf3c40b2603 100644 --- a/cmake/modules/BaseConfig.cmake +++ b/cmake/modules/BaseConfig.cmake @@ -61,7 +61,7 @@ endif() # ***************************************************************************** # Options # ***************************************************************************** -option(TOGGLE_BIN_FOLDER "Use build/bin folder for generate compilation files" ON) +option(TOGGLE_BIN_FOLDER "Use build/bin folder for generate compilation files" OFF) option(OPTIONS_ENABLE_OPENMP "Enable Open Multi-Processing support." ON) option(DEBUG_LOG "Enable Debug Log" OFF) option(ASAN_ENABLED "Build this target with AddressSanitizer" OFF) diff --git a/config.lua.dist b/config.lua.dist index 0c8d7dd4cd0..48d498c133f 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -177,11 +177,15 @@ momentumChanceFormulaA = 0.05 momentumChanceFormulaB = 1.9 momentumChanceFormulaC = 0.05 -transcendanceChanceFormulaA = 0.0127 -transcendanceChanceFormulaB = 0.1070 -transcendanceChanceFormulaC = 0.0073 +transcendenceChanceFormulaA = 0.0127 +transcendenceChanceFormulaB = 0.1070 +transcendenceChanceFormulaC = 0.0073 -transcendanceAvatarDuration = 7000 +amplificationChanceFormulaA = 0.4 +amplificationChanceFormulaB = 1.7 +amplificationChanceFormulaC = 0.4 + +transcendenceAvatarDuration = 7000 -- Bestiary & Bosstiary system -- NOTE: bestiaryKillMultiplier, multiplier value of monster killed, default 1 @@ -258,8 +262,6 @@ tibiadromeConcoctionTickType = "online" -- "online" | "experience" onlyPremiumAccount = false -- Customs --- NOTE: stashMoving = true, stow an container inside your stash --- NOTE: stashItemCount, the maximum items quantity in stash -- NOTE: depotChest, the non-stackable items will be moved to the selected depot chest(I - XVIII). -- NOTE: autoBank = true, the dropped coins from monsters will be automatically deposited to your bank account. -- NOTE: toggleGoldPouchAllowAnything will allow players to move items or gold to gold pouch @@ -276,8 +278,6 @@ onlyPremiumAccount = false -- NOTE: if showLootsInBestiary is true, will cause all loots to be shown in the bestiary even if the player has not reached the required number of kills -- NOTE: minTownIdToBankTransferFromMain blocks towns less than defined from receiving money transfers -- NOTE: enableSupportOutfit enable GODS and GMS to select support outfit (gamemaster, customer support or community manager) -stashMoving = false -stashItemCount = 5000 depotChest = 4 autoLoot = false autoBank = false @@ -298,6 +298,11 @@ showLootsInBestiary = false minTownIdToBankTransferFromMain = 4 enableSupportOutfit = true +-- NOTE: stashMoving = true, stow an container inside your stash +-- NOTE: stashManageAmount = max items add/remove from stash at once +stashMoving = false +stashManageAmount = 100000 + -- Teleport summon -- Set to true will never remove the summon teleportSummons = false diff --git a/data-otservbr-global/migrations/50.lua b/data-otservbr-global/migrations/50.lua new file mode 100644 index 00000000000..dc40c208fa0 --- /dev/null +++ b/data-otservbr-global/migrations/50.lua @@ -0,0 +1,58 @@ +function onUpdateDatabase() + logger.info("Updating database to version 50 (feat: support to 14.12)") + + db.query([[ + ALTER TABLE `player_charms` + DROP `rune_wound`, + DROP `rune_enflame`, + DROP `rune_poison`, + DROP `rune_freeze`, + DROP `rune_zap`, + DROP `rune_curse`, + DROP `rune_cripple`, + DROP `rune_parry`, + DROP `rune_dodge`, + DROP `rune_adrenaline`, + DROP `rune_numb`, + DROP `rune_cleanse`, + DROP `rune_bless`, + DROP `rune_scavenge`, + DROP `rune_gut`, + DROP `rune_low_blow`, + DROP `rune_divine`, + DROP `rune_vamp`, + DROP `rune_void` + ]]) + + db.query([[ + ALTER TABLE `player_charms` + ADD `minor_charm_echoes` SMALLINT NOT NULL DEFAULT '0', + ADD `max_charm_points` SMALLINT NOT NULL DEFAULT '0', + ADD `max_minor_charm_echoes` SMALLINT NOT NULL DEFAULT '0', + ADD `charms` BLOB NULL + ]]) + + db.query([[ + ALTER TABLE `player_charms` + MODIFY COLUMN `charm_points` SMALLINT NOT NULL DEFAULT '0', + MODIFY COLUMN `UsedRunesBit` INT NOT NULL DEFAULT '0', + MODIFY COLUMN `UnlockedRunesBit` INT NOT NULL DEFAULT '0', + MODIFY COLUMN `charm_expansion` BOOLEAN NOT NULL DEFAULT 0, + CHANGE COLUMN `player_guid` `player_id` int(11) NOT NULL + ]]) + + db.query([[ + ALTER TABLE player_charms + ADD CONSTRAINT player_charms_players_fk + FOREIGN KEY (player_id) REFERENCES players (id) + ]]) + + db.query([[ + UPDATE `player_charms` pc + JOIN `players` p ON pc.player_id = p.id + SET + pc.minor_charm_echoes = 100, + pc.max_minor_charm_echoes = 100 + WHERE p.vocation >= 5 + ]]) +end diff --git a/data-otservbr-global/monster/constructs/diamond_servant_replica.lua b/data-otservbr-global/monster/constructs/diamond_servant_replica.lua index c7ca4102fbd..228b63c3a55 100644 --- a/data-otservbr-global/monster/constructs/diamond_servant_replica.lua +++ b/data-otservbr-global/monster/constructs/diamond_servant_replica.lua @@ -2,7 +2,7 @@ local mType = Game.createMonsterType("Diamond Servant Replica") local monster = {} monster.description = "a diamond servant replica" -monster.experience = 700 +monster.experience = 1400 monster.outfit = { lookType = 397, lookHead = 0, @@ -80,7 +80,7 @@ monster.voices = { monster.loot = { { id = 9655, chance = 5040 }, -- gear crystal { id = 8775, chance = 5070 }, -- gear wheel - { id = 3031, chance = 94130, maxCount = 179 }, -- gold coin + { id = 3031, chance = 94130, maxCount = 358 }, -- gold coin { id = 5944, chance = 44990 }, -- soul orb { id = 3061, chance = 9150 }, -- life crystal { id = 237, chance = 5980 }, -- strong mana potion @@ -108,7 +108,7 @@ monster.defenses = { defense = 45, armor = 25, mitigation = 0.83, - { name = "combat", interval = 2000, chance = 11, type = COMBAT_HEALING, minDamage = 50, maxDamage = 150, effect = CONST_ME_MAGIC_BLUE, target = false }, + { name = "combat", interval = 2000, chance = 11, type = COMBAT_HEALING, minDamage = 50, maxDamage = 130, effect = CONST_ME_MAGIC_BLUE, target = false }, { name = "combat", interval = 2000, chance = 10, type = COMBAT_HEALING, effect = CONST_ME_YELLOWENERGY, target = false }, } diff --git a/data-otservbr-global/monster/constructs/golden_servant_replica.lua b/data-otservbr-global/monster/constructs/golden_servant_replica.lua index f5086d79354..de1225e952f 100644 --- a/data-otservbr-global/monster/constructs/golden_servant_replica.lua +++ b/data-otservbr-global/monster/constructs/golden_servant_replica.lua @@ -2,7 +2,7 @@ local mType = Game.createMonsterType("Golden Servant Replica") local monster = {} monster.description = "a golden servant replica" -monster.experience = 450 +monster.experience = 1250 monster.outfit = { lookType = 396, lookHead = 0, @@ -80,7 +80,7 @@ monster.voices = { monster.loot = { { id = 3732, chance = 1450 }, -- green mushroom { id = 8775, chance = 940 }, -- gear wheel - { id = 3031, chance = 85180, maxCount = 140 }, -- gold coin + { id = 3031, chance = 85180, maxCount = 270 }, -- gold coin { id = 266, chance = 4930 }, -- health potion { id = 268, chance = 4950 }, -- mana potion { id = 3269, chance = 3030 }, -- halberd diff --git a/data-otservbr-global/monster/constructs/iron_servant_replica.lua b/data-otservbr-global/monster/constructs/iron_servant_replica.lua index a0fa2c74c74..19915a02d0c 100644 --- a/data-otservbr-global/monster/constructs/iron_servant_replica.lua +++ b/data-otservbr-global/monster/constructs/iron_servant_replica.lua @@ -2,7 +2,7 @@ local mType = Game.createMonsterType("Iron Servant Replica") local monster = {} monster.description = "an iron servant replica" -monster.experience = 210 +monster.experience = 600 monster.outfit = { lookType = 395, lookHead = 0, @@ -75,7 +75,7 @@ monster.voices = { monster.loot = { { id = 8775, chance = 4840 }, -- gear wheel - { id = 3031, chance = 82190, maxCount = 55 }, -- gold coin + { id = 3031, chance = 82190, maxCount = 130 }, -- gold coin { id = 266, chance = 1980 }, -- health potion { id = 3269, chance = 1000 }, -- halberd { id = 12601, chance = 310 }, -- slime mould diff --git a/data-otservbr-global/monster/giants/orclops_doomhauler.lua b/data-otservbr-global/monster/giants/orclops_doomhauler.lua index b963069abc0..d4042fb1ce4 100644 --- a/data-otservbr-global/monster/giants/orclops_doomhauler.lua +++ b/data-otservbr-global/monster/giants/orclops_doomhauler.lua @@ -2,7 +2,7 @@ local mType = Game.createMonsterType("Orclops Doomhauler") local monster = {} monster.description = "an orclops doomhauler" -monster.experience = 1200 +monster.experience = 1450 monster.outfit = { lookType = 934, lookHead = 0, diff --git a/data-otservbr-global/monster/humanoids/broken_shaper.lua b/data-otservbr-global/monster/humanoids/broken_shaper.lua index 9ed950bbc70..12434236f51 100644 --- a/data-otservbr-global/monster/humanoids/broken_shaper.lua +++ b/data-otservbr-global/monster/humanoids/broken_shaper.lua @@ -2,7 +2,7 @@ local mType = Game.createMonsterType("Broken Shaper") local monster = {} monster.description = "a broken shaper" -monster.experience = 1600 +monster.experience = 1800 monster.outfit = { lookType = 932, lookHead = 94, diff --git a/data-otservbr-global/monster/humanoids/twisted_shaper.lua b/data-otservbr-global/monster/humanoids/twisted_shaper.lua index 564d71cb077..5bd840d06d9 100644 --- a/data-otservbr-global/monster/humanoids/twisted_shaper.lua +++ b/data-otservbr-global/monster/humanoids/twisted_shaper.lua @@ -2,7 +2,7 @@ local mType = Game.createMonsterType("Twisted Shaper") local monster = {} monster.description = "a twisted shaper" -monster.experience = 1750 +monster.experience = 2050 monster.outfit = { lookType = 932, lookHead = 105, @@ -99,7 +99,7 @@ monster.attacks = { { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -200 }, { name = "combat", interval = 2000, chance = 10, type = COMBAT_PHYSICALDAMAGE, minDamage = -50, maxDamage = -100, range = 7, shootEffect = CONST_ANI_ENERGY, effect = CONST_ME_ENERGYHIT, target = true }, { name = "combat", interval = 2000, chance = 35, type = COMBAT_LIFEDRAIN, minDamage = 0, maxDamage = -100, length = 5, spread = 0, effect = CONST_ME_MAGIC_RED, target = false }, - { name = "combat", interval = 2000, chance = 10, type = COMBAT_MANADRAIN, minDamage = -50, maxDamage = -100, radius = 7, effect = CONST_ME_MAGIC_BLUE, target = false }, + { name = "combat", interval = 2000, chance = 8, type = COMBAT_MANADRAIN, minDamage = -50, maxDamage = -100, radius = 7, effect = CONST_ME_MAGIC_BLUE, target = false }, { name = "speed", interval = 2000, chance = 9, speedChange = -440, effect = CONST_ME_GIANTICE, target = true, duration = 7000 }, } diff --git a/data-otservbr-global/monster/magicals/feversleep.lua b/data-otservbr-global/monster/magicals/feversleep.lua index dae681d08d6..e7b1edde5f7 100644 --- a/data-otservbr-global/monster/magicals/feversleep.lua +++ b/data-otservbr-global/monster/magicals/feversleep.lua @@ -95,8 +95,8 @@ monster.attacks = { { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -450 }, -- poison { name = "condition", type = CONDITION_POISON, interval = 2000, chance = 20, minDamage = -800, maxDamage = -1000, radius = 7, effect = CONST_ME_YELLOW_RINGS, target = false }, - { name = "combat", interval = 2000, chance = 10, type = COMBAT_MANADRAIN, minDamage = -70, maxDamage = -100, radius = 5, effect = CONST_ME_MAGIC_RED, target = false }, - { name = "feversleep skill reducer", interval = 2000, chance = 10, target = false }, + -- { name = "combat", interval = 2000, chance = 10, type = COMBAT_MANADRAIN, minDamage = -70, maxDamage = -100, radius = 5, effect = CONST_ME_MAGIC_RED, target = false }, + -- { name = "feversleep skill reducer", interval = 2000, chance = 10, target = false }, { name = "combat", interval = 2000, chance = 10, type = COMBAT_LIFEDRAIN, minDamage = -250, maxDamage = -300, length = 6, spread = 0, effect = CONST_ME_YELLOWENERGY, target = true }, { name = "combat", interval = 2000, chance = 15, type = COMBAT_DEATHDAMAGE, minDamage = -150, maxDamage = -300, radius = 1, shootEffect = CONST_ANI_SUDDENDEATH, effect = CONST_ME_MORTAREA, target = true }, } @@ -105,19 +105,19 @@ monster.defenses = { defense = 45, armor = 73, mitigation = 1.10, - { name = "combat", interval = 2000, chance = 20, type = COMBAT_HEALING, minDamage = 250, maxDamage = 425, effect = CONST_ME_MAGIC_BLUE, target = false }, - { name = "invisible", interval = 2000, chance = 10, effect = CONST_ME_HITAREA }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_HEALING, minDamage = 225, maxDamage = 350, effect = CONST_ME_MAGIC_BLUE, target = false }, + { name = "invisible", interval = 2000, chance = 8, effect = CONST_ME_HITAREA }, } monster.elements = { { type = COMBAT_PHYSICALDAMAGE, percent = 15 }, - { type = COMBAT_ENERGYDAMAGE, percent = 10 }, + { type = COMBAT_ENERGYDAMAGE, percent = -5 }, { type = COMBAT_EARTHDAMAGE, percent = 100 }, { type = COMBAT_FIREDAMAGE, percent = 35 }, { type = COMBAT_LIFEDRAIN, percent = 0 }, { type = COMBAT_MANADRAIN, percent = 0 }, { type = COMBAT_DROWNDAMAGE, percent = 0 }, - { type = COMBAT_ICEDAMAGE, percent = 20 }, + { type = COMBAT_ICEDAMAGE, percent = 5 }, { type = COMBAT_HOLYDAMAGE, percent = -10 }, { type = COMBAT_DEATHDAMAGE, percent = 55 }, } diff --git a/data-otservbr-global/monster/magicals/shiversleep.lua b/data-otservbr-global/monster/magicals/shiversleep.lua index c81c741eddc..391411a4c86 100644 --- a/data-otservbr-global/monster/magicals/shiversleep.lua +++ b/data-otservbr-global/monster/magicals/shiversleep.lua @@ -77,14 +77,14 @@ monster.defenses = { monster.elements = { { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, - { type = COMBAT_ENERGYDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, { type = COMBAT_EARTHDAMAGE, percent = 100 }, - { type = COMBAT_FIREDAMAGE, percent = -10 }, + { type = COMBAT_FIREDAMAGE, percent = 35 }, { type = COMBAT_LIFEDRAIN, percent = 100 }, { type = COMBAT_MANADRAIN, percent = 0 }, { type = COMBAT_DROWNDAMAGE, percent = 0 }, - { type = COMBAT_ICEDAMAGE, percent = -10 }, - { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 10 }, + { type = COMBAT_HOLYDAMAGE, percent = -10 }, { type = COMBAT_DEATHDAMAGE, percent = 0 }, } diff --git a/data-otservbr-global/monster/magicals/terrorsleep.lua b/data-otservbr-global/monster/magicals/terrorsleep.lua index 531081d1cdd..7749ab01513 100644 --- a/data-otservbr-global/monster/magicals/terrorsleep.lua +++ b/data-otservbr-global/monster/magicals/terrorsleep.lua @@ -101,11 +101,11 @@ monster.loot = { } monster.attacks = { - { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -450 }, + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -400 }, -- poison { name = "condition", type = CONDITION_POISON, interval = 2000, chance = 20, minDamage = -1000, maxDamage = -1500, radius = 7, effect = CONST_ME_YELLOW_RINGS, target = false }, { name = "combat", interval = 2000, chance = 10, type = COMBAT_MANADRAIN, minDamage = -100, maxDamage = -300, radius = 5, effect = CONST_ME_MAGIC_RED, target = false }, - { name = "feversleep skill reducer", interval = 2000, chance = 10, target = false }, + { name = "feversleep skill reducer", interval = 2000, chance = 7, target = false }, { name = "combat", interval = 2000, chance = 10, type = COMBAT_LIFEDRAIN, minDamage = -350, maxDamage = -500, length = 6, spread = 0, effect = CONST_ME_YELLOWENERGY, target = true }, { name = "combat", interval = 2000, chance = 20, type = COMBAT_DEATHDAMAGE, minDamage = -200, maxDamage = -450, radius = 1, shootEffect = CONST_ANI_SUDDENDEATH, effect = CONST_ME_MORTAREA, target = true }, } @@ -113,20 +113,20 @@ monster.attacks = { monster.defenses = { defense = 50, armor = 50, - { name = "combat", interval = 2000, chance = 15, type = COMBAT_HEALING, minDamage = 350, maxDamage = 600, effect = CONST_ME_MAGIC_BLUE, target = false }, - { name = "invisible", interval = 2000, chance = 15, effect = CONST_ME_HITAREA }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_HEALING, minDamage = 300, maxDamage = 500, effect = CONST_ME_MAGIC_BLUE, target = false }, + -- { name = "invisible", interval = 2000, chance = 15, effect = CONST_ME_HITAREA }, { name = "speed", interval = 2000, chance = 15, speedChange = 300, effect = CONST_ME_MAGIC_RED, target = false, duration = 5000 }, } monster.elements = { { type = COMBAT_PHYSICALDAMAGE, percent = 15 }, - { type = COMBAT_ENERGYDAMAGE, percent = 10 }, + { type = COMBAT_ENERGYDAMAGE, percent = -5 }, { type = COMBAT_EARTHDAMAGE, percent = 100 }, { type = COMBAT_FIREDAMAGE, percent = 35 }, { type = COMBAT_LIFEDRAIN, percent = 0 }, { type = COMBAT_MANADRAIN, percent = 0 }, { type = COMBAT_DROWNDAMAGE, percent = 0 }, - { type = COMBAT_ICEDAMAGE, percent = 20 }, + { type = COMBAT_ICEDAMAGE, percent = 5 }, { type = COMBAT_HOLYDAMAGE, percent = -10 }, { type = COMBAT_DEATHDAMAGE, percent = 55 }, } diff --git a/data-otservbr-global/monster/mammals/exotic_bat.lua b/data-otservbr-global/monster/mammals/exotic_bat.lua index 162e70f9d58..5454ea332cd 100644 --- a/data-otservbr-global/monster/mammals/exotic_bat.lua +++ b/data-otservbr-global/monster/mammals/exotic_bat.lua @@ -83,14 +83,15 @@ monster.loot = { } monster.attacks = { - { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -100 }, - { name = "combat", interval = 2000, chance = 15, type = COMBAT_EARTHDAMAGE, minDamage = -80, maxDamage = -150, length = 5, spread = 2, effect = CONST_ME_GREEN_RINGS, target = false }, - { name = "combat", interval = 2000, chance = 10, type = COMBAT_PHYSICALDAMAGE, minDamage = -60, maxDamage = -150, range = 7, radius = 3, effect = CONST_ME_YELLOW_RINGS, target = true }, + { name = "melee", interval = 2000, chance = 100, minDamage = -10, maxDamage = -130 }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_EARTHDAMAGE, minDamage = -90, maxDamage = -170, length = 5, spread = 2, effect = CONST_ME_GREEN_RINGS, target = false }, + { name = "combat", interval = 2000, chance = 10, type = COMBAT_PHYSICALDAMAGE, minDamage = -70, maxDamage = -170, range = 7, radius = 3, effect = CONST_ME_YELLOW_RINGS, target = true }, } monster.defenses = { defense = 40, armor = 40, + mitigation = 1.18, } monster.elements = { diff --git a/data-otservbr-global/monster/reptiles/crape_man.lua b/data-otservbr-global/monster/reptiles/crape_man.lua index 83097403b9c..cd09cf2feba 100644 --- a/data-otservbr-global/monster/reptiles/crape_man.lua +++ b/data-otservbr-global/monster/reptiles/crape_man.lua @@ -76,7 +76,7 @@ monster.voices = { } monster.loot = { - { name = "platinum coin", chance = 71540, maxCount = 28 }, + { name = "platinum coin", chance = 71540, maxCount = 25 }, { name = "crab man claws", chance = 5210, maxCount = 2 }, { name = "green gem", chance = 3010 }, { name = "great health potion", chance = 2000, maxCount = 5 }, diff --git a/data-otservbr-global/npc/eruaran.lua b/data-otservbr-global/npc/eruaran.lua index f90524535a9..038ad5a0669 100644 --- a/data-otservbr-global/npc/eruaran.lua +++ b/data-otservbr-global/npc/eruaran.lua @@ -445,7 +445,7 @@ local function creatureSayCallback(npc, creature, type, message) end end npcHandler:removeInteraction(npc, creature) - npcHandler:resetNpc() + npcHandler:resetNpc(creature) end end diff --git a/data-otservbr-global/npc/frosty.lua b/data-otservbr-global/npc/frosty.lua index 31b73c3ea8e..998394c52ab 100644 --- a/data-otservbr-global/npc/frosty.lua +++ b/data-otservbr-global/npc/frosty.lua @@ -58,7 +58,7 @@ local function creatureSayCallback(npc, creature, type, message) if sleightInfo[message] ~= nil then if getPlayerStorageValue(creature, sleightInfo[message].storageID) ~= -1 then npcHandler:say("You already have this sleigh!", npc, creature) - npcHandler:resetNpc() + npcHandler:resetNpc(player) else local itemsTable = sleightInfo[message].items local items_list = "" @@ -112,20 +112,20 @@ local function creatureSayCallback(npc, creature, type, message) end rtnt[playerId] = nil talkState[playerId] = 0 - npcHandler:resetNpc() + npcHandler:resetNpc(player) return true end elseif MsgContains(message, "mount") or MsgContains(message, "mounts") or MsgContains(message, "sleigh") or MsgContains(message, "sleighs") then npcHandler:say("I can give you one of the following sleighs: {" .. table.concat(monsterName, "}, {") .. "}.", npc, creature) rtnt[playerId] = nil talkState[playerId] = 0 - npcHandler:resetNpc() + npcHandler:resetNpc(player) return true elseif MsgContains(message, "help") then npcHandler:say("Just tell me which {sleigh} you want to know more about.", npc, creature) rtnt[playerId] = nil talkState[playerId] = 0 - npcHandler:resetNpc() + npcHandler:resetNpc(player) return true else if talkState[playerId] ~= nil then @@ -133,7 +133,7 @@ local function creatureSayCallback(npc, creature, type, message) npcHandler:say("Come back when you get these items.", npc, creature) rtnt[playerId] = nil talkState[playerId] = 0 - npcHandler:resetNpc() + npcHandler:resetNpc(player) return true end end diff --git a/data-otservbr-global/npc/hireling.lua b/data-otservbr-global/npc/hireling.lua index 670ceeb7bce..0825acba8ce 100644 --- a/data-otservbr-global/npc/hireling.lua +++ b/data-otservbr-global/npc/hireling.lua @@ -708,10 +708,57 @@ function createHirelingType(HirelingName) npcHandler:setTopic(playerId, TOPIC.SERVICES) local servicesMsg = getHirelingServiceString(creature) npcHandler:say(servicesMsg, npc, creature) - elseif MsgContains(message, "lamp") then - npcHandler:setTopic(playerId, TOPIC.LAMP) - if player:getGuid() ~= hireling:getOwnerId() then - return false + elseif npcHandler:getTopic(playerId) == TOPIC.SERVICES then + if MsgContains(message, "bank") then + local bankerSkillName = HIRELING_SKILLS.BANKER[2] + if hireling:hasSkill(bankerSkillName) then + npcHandler:setTopic(playerId, TOPIC.BANK) + count[playerId], transfer[playerId] = nil, nil + npcHandler:say(GREETINGS.BANK, npc, creature) + else + sendSkillNotLearned(npc, creature, bankerSkillName) + end + elseif MsgContains(message, "food") then + local bankerSkillName = HIRELING_SKILLS.COOKING[2] + if hireling:hasSkill(bankerSkillName) then + npcHandler:setTopic(playerId, TOPIC.FOOD) + npcHandler:say(GREETINGS.FOOD, npc, creature) + else + sendSkillNotLearned(npc, creature, bankerSkillName) + end + elseif MsgContains(message, "stash") then + local bankerSkillName = HIRELING_SKILLS.STEWARD[2] + if hireling:hasSkill(bankerSkillName) then + npcHandler:say(GREETINGS.STASH, npc, creature) + player:setSpecialContainersAvailable(true) + player:openStash(true) + player:sendTextMessage(MESSAGE_FAILURE, "Your stash contains " .. player:getStashCount() .. " item" .. (player:getStashCount() > 1 and "s." or ".")) + else + sendSkillNotLearned(npc, creature, bankerSkillName) + end + elseif MsgContains(message, "goods") then + local string + if not hireling:hasSkill(HIRELING_SKILLS.TRADER[2]) then + string = "While I'm not a trader, I still have a collection of {various} items to sell if you like!" + else + string = "I sell a selection of {various} items, {exercise weapons}, {equipment}, " .. "{distance} weapons, {wands} and {rods}, {potions}, {runes}, " .. "{supplies}, {tools} and {postal} goods. Just ask!" + end + npcHandler:setTopic(playerId, TOPIC.GOODS) + npcHandler:say(string, npc, creature) + elseif MsgContains(message, "lamp") then + npcHandler:setTopic(playerId, TOPIC.LAMP) + if player:getGuid() ~= hireling:getOwnerId() then + return false + end + + npcHandler:say("Are you sure you want me to go back to my lamp?", npc, creature) + elseif MsgContains(message, "outfit") then + if player:getGuid() ~= hireling:getOwnerId() then + return false + end + + hireling:requestOutfitChange() + npcHandler:say("As you wish!", npc, creature) end npcHandler:say("Are you sure you want me to go back to my lamp?", npc, creature) elseif npcHandler:getTopic(playerId) == TOPIC.LAMP then diff --git a/data-otservbr-global/scripts/actions/tools/skinning.lua b/data-otservbr-global/scripts/actions/tools/skinning.lua index a35929db054..c3c74de5fe0 100644 --- a/data-otservbr-global/scripts/actions/tools/skinning.lua +++ b/data-otservbr-global/scripts/actions/tools/skinning.lua @@ -201,7 +201,9 @@ function skinning.onUse(player, item, fromPosition, target, toPosition, isHotkey if charmMType then local charmCorpse = charmMType:getCorpseId() if charmCorpse == target.itemid or ItemType(charmCorpse):getDecayId() == target.itemid then - chanceRange = chanceRange * GLOBAL_CHARM_SCAVENGE / 100 + local charmChance = player:getCharmChance(CHARM_SCAVENGE) + charmChance = (charmChance == 0 and 1 or charmChance) -- Guarantee that the chance will neve be 0 + chanceRange = chanceRange * charmChance / 100 end end diff --git a/data-otservbr-global/scripts/game_migrations/20251737599334_reset_old_charms.lua b/data-otservbr-global/scripts/game_migrations/20251737599334_reset_old_charms.lua new file mode 100644 index 00000000000..8cf1483b1ab --- /dev/null +++ b/data-otservbr-global/scripts/game_migrations/20251737599334_reset_old_charms.lua @@ -0,0 +1,15 @@ +local migration = Migration("20251737599334_reset_charms") + +function migration:onExecute() + local totalPlayers = 0 + + logger.info("[Migration] Resetting old charms for all players. This may take some time...") + self:forEachPlayer(function(player) + player:resetOldCharms() + totalPlayers = totalPlayers + 1 + end) + + logger.info("[Migration] Successfully reset charms for {} players.", totalPlayers) +end + +migration:register() diff --git a/data/XML/mounts.xml b/data/XML/mounts.xml index 7af3063e200..472c4ed036e 100644 --- a/data/XML/mounts.xml +++ b/data/XML/mounts.xml @@ -231,4 +231,8 @@ + + + + diff --git a/data/XML/outfits.xml b/data/XML/outfits.xml index 9cb5279e49b..8e87161d52f 100644 --- a/data/XML/outfits.xml +++ b/data/XML/outfits.xml @@ -122,6 +122,8 @@ + + @@ -245,4 +247,6 @@ + + diff --git a/data/global.lua b/data/global.lua index 85987d559d0..562ce8d33b2 100644 --- a/data/global.lua +++ b/data/global.lua @@ -52,10 +52,6 @@ SERVER_MOTD = configManager.getString(configKeys.SERVER_MOTD) AUTH_TYPE = configManager.getString(configKeys.AUTH_TYPE) --- Bestiary charm -GLOBAL_CHARM_GUT = 120 -- 20% more chance to get creature products from looting -GLOBAL_CHARM_SCAVENGE = 125 -- 25% more chance to get creature products from skinning - -- Event Schedule SCHEDULE_LOOT_RATE = 100 SCHEDULE_EXP_RATE = 100 diff --git a/data/items/appearances.dat b/data/items/appearances.dat index 8b9c47a9d32..e9aafb81542 100644 Binary files a/data/items/appearances.dat and b/data/items/appearances.dat differ diff --git a/data/items/items.xml b/data/items/items.xml index d2fe1e17e42..4b1dcb76c81 100644 --- a/data/items/items.xml +++ b/data/items/items.xml @@ -80971,4 +80971,3 @@ Granted by TibiaGoals.com"/> - diff --git a/data/libs/functions/monstertype.lua b/data/libs/functions/monstertype.lua index ee0d6429fbc..d9739f993fe 100644 --- a/data/libs/functions/monstertype.lua +++ b/data/libs/functions/monstertype.lua @@ -37,10 +37,12 @@ function MonsterType:generateLootRoll(config, resultTable, player) end local dynamicFactor = factor * (math.random(95, 105) / 100) - local adjustedChance = item.chance * dynamicFactor + local adjustedChance = chance * dynamicFactor + local originalChance = chance if config.gut and iType:getType() == ITEM_TYPE_CREATUREPRODUCT then - adjustedChance = math.ceil((adjustedChance * GLOBAL_CHARM_GUT) / 100) + local charmChance = player:getCharmChance(CHARM_GUT) + adjustedChance = adjustedChance + math.ceil((adjustedChance * charmChance / 100)) end local randValue = getLootRandom() @@ -59,8 +61,10 @@ function MonsterType:generateLootRoll(config, resultTable, player) count = 1 end + local gutTriggered = randValue < chance and randValue > originalChance + result[item.itemId].count = result[item.itemId].count + count - result[item.itemId].gut = config.gut and iType:getType() == ITEM_TYPE_CREATUREPRODUCT + result[item.itemId].gut = config.gut and iType:getType() == ITEM_TYPE_CREATUREPRODUCT and gutTriggered result[item.itemId].unique = item.unique result[item.itemId].subType = item.subType result[item.itemId].text = item.text diff --git a/data/libs/functions/player.lua b/data/libs/functions/player.lua index 8dae1cb7fe5..969338f2f43 100644 --- a/data/libs/functions/player.lua +++ b/data/libs/functions/player.lua @@ -448,10 +448,10 @@ end ---@param monster Monster ---@return {factor: number, msgSuffix: string} function Player:calculateLootFactor(monster) - if self:getStamina() <= 840 then + if not self:canReceiveLoot() then return { factor = 0.0, - msgSuffix = " (due to low stamina)", + msgSuffix = "due to low stamina", } end @@ -482,7 +482,7 @@ function Player:calculateLootFactor(monster) factor = factor * (1 + vipBoost) end if vipBoost > 0 then - suffix = suffix .. (" (vip bonus: %d%%)"):format(math.floor(vipBoost * 100 + 0.5)) + suffix = string.format("vip bonus %d%%", math.floor(vipBoost * 100 + 0.5)) end return { diff --git a/data/libs/functions/quests.lua b/data/libs/functions/quests.lua index 02487c55031..ad98aa37fa8 100644 --- a/data/libs/functions/quests.lua +++ b/data/libs/functions/quests.lua @@ -59,6 +59,7 @@ function Player.resetTrackedMissions(self, missions) if questName and questId and missionIndex then if self:missionIsStarted(questId, missionIndex) then local data = { + questId = questId, missionId = missionId, questName = questName, missionName = self:getMissionName(questId, missionIndex), @@ -297,8 +298,8 @@ function Player.sendQuestLog(self) for questId = 1, #Quests do if self:questIsStarted(questId) then msg:addU16(questId) - msg:addString(Quests[questId].name .. (self:questIsCompleted(questId) and " (completed)" or ""), "Player.sendQuestLog") - msg:addByte(self:questIsCompleted(questId)) + msg:addString(Quests[questId].name, "Player.sendQuestLog") + msg:addByte(self:questIsCompleted(questId) and 0x01 or 0x00) end end msg:sendToPlayer(self) @@ -337,6 +338,7 @@ function Player.sendTrackedQuests(self, remainingQuests, missions) msg:addByte(remainingQuests) msg:addByte(#missions) for _, mission in ipairs(missions) do + msg:addU16(mission.questId) msg:addU16(mission.missionId) msg:addString(mission.questName, "Player.sendTrackedQuests - mission.questName") msg:addString(mission.missionName, "Player.sendTrackedQuests - mission.missionName") @@ -350,7 +352,9 @@ function Player.sendUpdateTrackedQuest(self, mission) local msg = NetworkMessage() msg:addByte(0xD0) msg:addByte(0x00) + msg:addU16(mission.questId) msg:addU16(mission.missionId) + msg:addString(mission.questName) msg:addString(mission.missionName, "Player.sendUpdateTrackedQuest - mission.missionName") msg:addString(mission.missionDesc, "Player.sendUpdateTrackedQuest - mission.missionDesc") msg:sendToPlayer(self) diff --git a/data/libs/tables/doors.lua b/data/libs/tables/doors.lua index aa9c9ddf95f..db28349f57b 100644 --- a/data/libs/tables/doors.lua +++ b/data/libs/tables/doors.lua @@ -71,6 +71,8 @@ KeyDoorTable = { { lockedDoor = 30774, closedDoor = 30775, openDoor = 30777 }, { lockedDoor = 37982, closedDoor = 37981, openDoor = 37985 }, { lockedDoor = 37984, closedDoor = 37983, openDoor = 37986 }, + { lockedDoor = 44914, closedDoor = 44913, openDoor = 44917 }, + { lockedDoor = 44916, closedDoor = 44915, openDoor = 44918 }, } -- These are the common doors, the ones that just open and close without any special requirements. @@ -164,10 +166,6 @@ CustomDoorTable = { { closedDoor = 22504, openDoor = 22505 }, { closedDoor = 39660, openDoor = 39666 }, { closedDoor = 39661, openDoor = 39667 }, - { closedDoor = 44913, openDoor = 44917 }, - { closedDoor = 44914, openDoor = 44917 }, - { closedDoor = 44915, openDoor = 44918 }, - { closedDoor = 44916, openDoor = 44918 }, { closedDoor = 48495, openDoor = 48497 }, { closedDoor = 48496, openDoor = 48498 }, { closedDoor = 48499, openDoor = 48501 }, diff --git a/data/modules/scripts/gamestore/gamestore.lua b/data/modules/scripts/gamestore/gamestore.lua index ed17c456c0d..77ca7041f98 100644 --- a/data/modules/scripts/gamestore/gamestore.lua +++ b/data/modules/scripts/gamestore/gamestore.lua @@ -2256,6 +2256,30 @@ GameStore.Categories = { description = "{character}\n{speedboost}\n\nBadgers have been a staple of the Tibian fauna for a long time, and finally some daring souls have braved the challenge to tame some exceptional specimens - and succeeded! While the common badger you can encounter during your travels might seem like a rather unassuming creature, the Battle Badger, the Ether Badger, and the Zaoan Badger are fierce and mighty beasts, which are at your beck and call.", type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, }, + { + icons = { "Night_Locust.png" }, + name = "Night Locust", + price = 750, + id = 233, + description = "{character}\n{speedboost}\n\nBorn from the buzzing chaos of nature's most untamed corners, the Night Locust, Leaf Locust, and Pearl Locust are said to be harbingers of fortune for their allies and heralds of despair for their foes. With their vibrant wings and shimmering shells, these eerie yet majestic creatures are exceptional mounts for adventurers who thrive in the wilds.", + type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, + }, + { + icons = { "Leaf_Locust.png" }, + name = "Leaf Locust", + price = 750, + id = 234, + description = "{character}\n{speedboost}\n\nBorn from the buzzing chaos of nature's most untamed corners, the Night Locust, Leaf Locust, and Pearl Locust are said to be harbingers of fortune for their allies and heralds of despair for their foes. With their vibrant wings and shimmering shells, these eerie yet majestic creatures are exceptional mounts for adventurers who thrive in the wilds.", + type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, + }, + { + icons = { "Pearl_Locust.png" }, + name = "Pearl Locust", + price = 750, + id = 235, + description = "{character}\n{speedboost}\n\nBorn from the buzzing chaos of nature's most untamed corners, the Night Locust, Leaf Locust, and Pearl Locust are said to be harbingers of fortune for their allies and heralds of despair for their foes. With their vibrant wings and shimmering shells, these eerie yet majestic creatures are exceptional mounts for adventurers who thrive in the wilds.", + type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, + }, }, }, -- Cosmetics ~ Outfits (base outfit has addon = 0 or no defined addon. By default addon is set to 0) diff --git a/data/npclib/npc_system/modules.lua b/data/npclib/npc_system/modules.lua index 08027a67d2b..e82e9e96227 100644 --- a/data/npclib/npc_system/modules.lua +++ b/data/npclib/npc_system/modules.lua @@ -111,6 +111,7 @@ if Modules == nil then else npcHandler:say(parameters.text, npc, player) player:setVocation(promotion) + player:addMinorCharmEchoes(100) player:kv():set("promoted", true) end else diff --git a/data/scripts/eventcallbacks/monster/ondroploot__base.lua b/data/scripts/eventcallbacks/monster/ondroploot__base.lua index 0f724be9f67..c7437e89b04 100644 --- a/data/scripts/eventcallbacks/monster/ondroploot__base.lua +++ b/data/scripts/eventcallbacks/monster/ondroploot__base.lua @@ -1,9 +1,5 @@ local callback = EventCallback("MonsterOnDropLootBaseEvent") -function Player:canReceiveLoot() - return self:getStamina() > 840 -end - function callback.monsterOnDropLoot(monster, corpse) local player = Player(corpse:getCorpseOwner()) local factor = 1.0 @@ -19,17 +15,19 @@ function callback.monsterOnDropLoot(monster, corpse) return end - local charm = player and player:getCharmMonsterType(CHARM_GUT) - local gut = charm and charm:raceId() == mType:raceId() + local mTypeCharm = player and player:getCharmMonsterType(CHARM_GUT) + local gut = mTypeCharm and mTypeCharm:raceId() == mType:raceId() local lootTable = mType:generateLootRoll({ factor = factor, gut = gut }, {}, player) corpse:addLoot(lootTable) - for _, item in ipairs(lootTable) do - if item.gut then - msgSuffix = msgSuffix .. " (active charm bonus)" + local charmMessage = false + local existingSuffix = corpse:getAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX) or "" + for _, item in pairs(lootTable) do + if item.gut and not charmMessage then + charmMessage = true + msgSuffix = msgSuffix .. (string.len(msgSuffix) > 0 and ", gut charm" or "gut charm") end end - local existingSuffix = corpse:getAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX) or "" corpse:setAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX, existingSuffix .. msgSuffix) end diff --git a/data/scripts/eventcallbacks/monster/ondroploot_boosted.lua b/data/scripts/eventcallbacks/monster/ondroploot_boosted.lua index 3bb43256772..c13a100ee38 100644 --- a/data/scripts/eventcallbacks/monster/ondroploot_boosted.lua +++ b/data/scripts/eventcallbacks/monster/ondroploot_boosted.lua @@ -21,10 +21,10 @@ function callback.monsterOnDropLoot(monster, corpse) end local factor = 1.0 - local msgSuffix = " (boosted loot)" - corpse:addLoot(mType:generateLootRoll({ factor = factor, gut = false }, {}, player)) - local existingSuffix = corpse:getAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX) or "" + local msgSuffix = string.len(existingSuffix) > 0 and ", boosted loot" or "boosted loot" + + corpse:addLoot(mType:generateLootRoll({ factor = factor, gut = false }, {}, player)) corpse:setAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX, existingSuffix .. msgSuffix) end diff --git a/data/scripts/eventcallbacks/monster/ondroploot_hazard.lua b/data/scripts/eventcallbacks/monster/ondroploot_hazard.lua index 78851d186e6..47f55913267 100644 --- a/data/scripts/eventcallbacks/monster/ondroploot_hazard.lua +++ b/data/scripts/eventcallbacks/monster/ondroploot_hazard.lua @@ -23,10 +23,12 @@ function callback.monsterOnDropLoot(monster, corpse) rolls = math.floor(rolls) end + local existingSuffix = corpse:getAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX) or "" + if configManager.getBoolean(configKeys.PARTY_SHARE_LOOT_BOOSTS) and rolls > 1 then - msgSuffix = msgSuffix .. " (hazard system, " .. rolls .. " extra rolls)" + msgSuffix = string.len(existingSuffix) > 0 and string.format(", hazard system %s extra rolls", rolls) or string.format("hazard system %s extra rolls", rolls) elseif rolls == 1 then - msgSuffix = msgSuffix .. " (hazard system)" + msgSuffix = string.len(existingSuffix) > 0 and ", hazard system" or "hazard system" end local lootTable = {} @@ -34,8 +36,6 @@ function callback.monsterOnDropLoot(monster, corpse) lootTable = mType:generateLootRoll({ factor = factor, gut = false }, lootTable, player) end corpse:addLoot(lootTable) - - local existingSuffix = corpse:getAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX) or "" corpse:setAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX, existingSuffix .. msgSuffix) end diff --git a/data/scripts/eventcallbacks/monster/ondroploot_prey.lua b/data/scripts/eventcallbacks/monster/ondroploot_prey.lua index eb4657ccc4f..9445b8b5489 100644 --- a/data/scripts/eventcallbacks/monster/ondroploot_prey.lua +++ b/data/scripts/eventcallbacks/monster/ondroploot_prey.lua @@ -36,14 +36,15 @@ function callback.monsterOnDropLoot(monster, corpse) return end + local existingSuffix = corpse:getAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX) or "" + if configManager.getBoolean(configKeys.PARTY_SHARE_LOOT_BOOSTS) then - msgSuffix = msgSuffix .. " (active prey bonus for " .. table.concat(preyActivators, ", ") .. ")" + msgSuffix = string.len(existingSuffix) > 0 and string.format(", active prey bonus for %s", table.concat(preyActivators, ", ")) or string.format("active prey bonus for %s", table.concat(preyActivators, ", ")) else - msgSuffix = msgSuffix .. " (active prey bonus)" + msgSuffix = string.len(existingSuffix) > 0 and ", active prey bonus" or "active prey bonus" end corpse:addLoot(mType:generateLootRoll({ factor = factor, gut = false }, {}, player)) - local existingSuffix = corpse:getAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX) or "" corpse:setAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX, existingSuffix .. msgSuffix) end diff --git a/data/scripts/eventcallbacks/monster/ondroploot_wealth_duplex.lua b/data/scripts/eventcallbacks/monster/ondroploot_wealth_duplex.lua index a4f3c67fe9c..23e2602e389 100644 --- a/data/scripts/eventcallbacks/monster/ondroploot_wealth_duplex.lua +++ b/data/scripts/eventcallbacks/monster/ondroploot_wealth_duplex.lua @@ -51,10 +51,12 @@ function callback.monsterOnDropLoot(monster, corpse) return end + local existingSuffix = corpse:getAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX) or "" + if configManager.getBoolean(configKeys.PARTY_SHARE_LOOT_BOOSTS) and rolls > 1 then - msgSuffix = msgSuffix .. " (active wealth duplex, " .. rolls .. " extra rolls)" + msgSuffix = string.len(existingSuffix) > 0 and string.format(", active wealth duplex %s extra rolls", rolls) or string.format("active wealth duplex %s extra rolls", rolls) else - msgSuffix = msgSuffix .. " (active wealth duplex)" + msgSuffix = string.len(existingSuffix) > 0 and ", active wealth duplex" or "active wealth duplex" end local lootTable = {} @@ -62,8 +64,6 @@ function callback.monsterOnDropLoot(monster, corpse) lootTable = mType:generateLootRoll({ factor = factor, gut = false }, lootTable, player) end corpse:addLoot(lootTable) - - local existingSuffix = corpse:getAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX) or "" corpse:setAttribute(ITEM_ATTRIBUTE_LOOTMESSAGE_SUFFIX, existingSuffix .. msgSuffix) end diff --git a/data/scripts/lib/register_bestiary_charm.lua b/data/scripts/lib/register_bestiary_charm.lua index c0daffff9f9..d05465a2d16 100644 --- a/data/scripts/lib/register_bestiary_charm.lua +++ b/data/scripts/lib/register_bestiary_charm.lua @@ -34,6 +34,12 @@ registerCharm.sounds = function(charm, mask) end end +registerCharm.category = function(charm, mask) + if mask.type then + charm:category(mask.category) + end +end + registerCharm.type = function(charm, mask) if mask.type then charm:type(mask.type) diff --git a/data/scripts/movements/special_tiles.lua b/data/scripts/movements/special_tiles.lua index bccdf5b82f0..5e6e4737c66 100644 --- a/data/scripts/movements/special_tiles.lua +++ b/data/scripts/movements/special_tiles.lua @@ -16,7 +16,7 @@ local function checkAndSendDepotMessage(player) end local depotMessage = string.format("Your depot contains %d item%s", depotItems, depotItems ~= 1 and "s." or ".") - local stashMessage = string.format("Your supply stash contains %d item%s", player:getStashCount(), player:getStashCount() ~= 1 and "s." or ".") + local stashMessage = string.format("Your stash contains %d item%s", player:getStashCount(), player:getStashCount() ~= 1 and "s." or ".") player:sendTextMessage(MESSAGE_STATUS, string.format("%s %s", depotMessage, stashMessage)) player:setSpecialContainersAvailable(true, true, true) diff --git a/data/scripts/systems/bestiary_charms.lua b/data/scripts/systems/bestiary_charms.lua index 951b935dc6d..4f997ef53e0 100644 --- a/data/scripts/systems/bestiary_charms.lua +++ b/data/scripts/systems/bestiary_charms.lua @@ -4,212 +4,283 @@ local charms = { name = "Wound", description = "Triggers on a creature with a certain chance and deals 5% \z of its initial hit points as physical damage once.", + category = CHARM_MAJOR, type = CHARM_OFFENSIVE, damageType = COMBAT_PHYSICALDAMAGE, percent = 5, - chance = 10, - messageCancel = "You wounded the monster.", - messageServerLog = "[Wound charm]", + chance = { 5, 10, 11 }, + messageServerLog = true, effect = CONST_ME_HITAREA, - points = 600, + points = { 240, 360, 1200 }, }, -- Enflame charm [2] = { name = "Enflame", description = "Triggers on a creature with a certain chance and deals 5% \z of its initial hit points as fire damage once.", + category = CHARM_MAJOR, type = CHARM_OFFENSIVE, damageType = COMBAT_FIREDAMAGE, percent = 5, - chance = 10, - messageCancel = "You enflamed the monster.", - messageServerLog = "[Enflame charm]", + chance = { 5, 10, 11 }, + messageServerLog = true, effect = CONST_ME_HITBYFIRE, - points = 1000, + points = { 400, 600, 2000 }, }, -- Poison charm [3] = { name = "Poison", description = "Triggers on a creature with a certain chance and deals 5% \z of its initial hit points as earth damage once.", + category = CHARM_MAJOR, type = CHARM_OFFENSIVE, damageType = COMBAT_EARTHDAMAGE, percent = 5, - chance = 10, - messageCancel = "You poisoned the monster.", - messageServerLog = "[Poison charm]", + chance = { 5, 10, 11 }, + messageServerLog = true, effect = CONST_ME_GREEN_RINGS, - points = 600, + points = { 240, 360, 1200 }, }, -- Freeze charm [4] = { name = "Freeze", description = "Triggers on a creature with a certain chance and deals 5% \z of its initial hit points as ice damage once.", + category = CHARM_MAJOR, type = CHARM_OFFENSIVE, damageType = COMBAT_ICEDAMAGE, percent = 5, - chance = 10, - messageCancel = "You frozen the monster.", - messageServerLog = "[Freeze charm]", + chance = { 5, 10, 11 }, + messageServerLog = true, effect = CONST_ME_ICEATTACK, - points = 800, + points = { 320, 480, 1600 }, }, - --Zap charm + -- Zap charm [5] = { name = "Zap", description = "Triggers on a creature with a certain chance and deals 5% \z of its initial hit points as energy damage once.", + category = CHARM_MAJOR, type = CHARM_OFFENSIVE, damageType = COMBAT_ENERGYDAMAGE, percent = 5, - chance = 10, - messageCancel = "You eletrocuted the monster.", - messageServerLog = "[Zap charm]", + chance = { 5, 10, 11 }, + messageServerLog = true, effect = CONST_ME_ENERGYHIT, - points = 800, + points = { 320, 480, 1600 }, }, - --Curse charm + -- Curse charm [6] = { name = "Curse", description = "Triggers on a creature with a certain chance and deals 5% \z of its initial hit points as death damage once.", + category = CHARM_MAJOR, type = CHARM_OFFENSIVE, damageType = COMBAT_DEATHDAMAGE, percent = 5, - chance = 10, - messageCancel = "You curse the monster.", - messageServerLog = "[Curse charm]", + chance = { 5, 10, 11 }, + messageServerLog = true, effect = CONST_ME_SMALLCLOUDS, - points = 900, + points = { 360, 540, 1800 }, }, -- Cripple charm [7] = { name = "Cripple", description = "Cripples the creature with a certain chance and paralyzes it for 10 seconds.", + category = CHARM_MINOR, type = CHARM_OFFENSIVE, - chance = 10, - messageCancel = "You cripple the monster.", - points = 500, + chance = { 6, 9, 12 }, + messageCancel = "You crippled a monster. (cripple charm)", + points = { 100, 150, 225 }, }, -- Parry charm [8] = { name = "Parry", description = "Any damage taken is reflected to the aggressor with a certain chance.", + category = CHARM_MAJOR, type = CHARM_DEFENSIVE, damageType = COMBAT_PHYSICALDAMAGE, - chance = 10, - messageCancel = "You parry the attack.", - messageServerLog = "[Parry charm]", + chance = { 5, 10, 11 }, + messageCancel = "You parried an attack. (parry charm)", effect = CONST_ME_EXPLOSIONAREA, - points = 1000, + points = { 400, 600, 2000 }, }, -- Dodge charm [9] = { name = "Dodge", description = "Dodges an attack with a certain chance without taking any damage at all.", + category = CHARM_MAJOR, type = CHARM_DEFENSIVE, - chance = 10, - messageCancel = "You dodge the attack.", + chance = { 5, 10, 11 }, + messageCancel = "You dodged an attack. (dodge charm)", effect = CONST_ME_POFF, - points = 600, + points = { 240, 360, 1200 }, }, - -- Adrenaline burst charm + -- Adrenaline Burst charm [10] = { name = "Adrenaline Burst", description = "Bursts of adrenaline enhance your reflexes with a certain chance \z after you get hit and let you move faster for 10 seconds.", + category = CHARM_MINOR, type = CHARM_DEFENSIVE, - chance = 10, - messageCancel = "Your movements where bursted.", - points = 500, + chance = { 6, 9, 12 }, + messageCancel = "Your movements where bursted. (adrenaline burst charm)", + points = { 100, 150, 225 }, }, -- Numb charm [11] = { name = "Numb", description = "Numbs the creature with a certain chance after its attack and paralyzes the creature for 10 seconds.", + category = CHARM_MINOR, type = CHARM_DEFENSIVE, - chance = 10, - messageCancel = "You numb the monster.", - points = 500, + chance = { 6, 9, 12 }, + messageCancel = "You numbed a monster. (numb charm)", + points = { 100, 150, 225 }, }, -- Cleanse charm [12] = { name = "Cleanse", description = "Cleanses you from within with a certain chance after you get hit and \z removes one random active negative status effect and temporarily makes you immune against it.", + category = CHARM_MINOR, type = CHARM_DEFENSIVE, - chance = 10, - messageCancel = "You purified the attack.", - points = 700, + chance = { 6, 9, 12 }, + messageCancel = "You purified an attack. (cleanse charm)", + points = { 100, 150, 225 }, }, -- Bless charm [13] = { name = "Bless", description = "Blesses you and reduces skill and xp loss by 10% when killed by the chosen creature.", + category = CHARM_MINOR, type = CHARM_PASSIVE, percent = 10, - chance = 100, - points = 800, + chance = { 6, 9, 12 }, + points = { 100, 150, 225 }, }, -- Scavenge charm [14] = { name = "Scavenge", description = "Enhances your chances to successfully skin/dust a skinnable/dustable creature.", + category = CHARM_MINOR, type = CHARM_PASSIVE, - percent = 25, - chance = 100, - points = 800, + chance = { 60, 90, 120 }, + points = { 100, 150, 225 }, }, -- Gut charm [15] = { name = "Gut", description = "Gutting the creature yields 20% more creature products.", + category = CHARM_MINOR, type = CHARM_PASSIVE, - percent = 20, - chance = 100, - points = 800, + chance = { 6, 9, 12 }, + points = { 100, 150, 225 }, }, -- Low blow charm [16] = { name = "Low Blow", description = "Adds 8% critical hit chance to attacks with critical hit weapons.", + category = CHARM_MAJOR, type = CHARM_PASSIVE, - percent = 8, - chance = 100, - points = 2000, + chance = { 4, 8, 9 }, + points = { 800, 1200, 4000 }, }, - -- Divine wrath charm + -- Divine Wrath charm [17] = { name = "Divine Wrath", description = "Triggers on a creature with a certain chance and deals 5% \z of its initial hit points as holy damage once.", + category = CHARM_MAJOR, type = CHARM_OFFENSIVE, damageType = COMBAT_HOLYDAMAGE, percent = 5, - chance = 10, - messageCancel = "You divine the monster.", - messageServerLog = "[Divine charm]", + chance = { 5, 10, 11 }, + messageServerLog = true, effect = CONST_ME_HOLYDAMAGE, - points = 1500, + points = { 600, 900, 3000 }, }, - -- Vampiric embrace charm + -- Vampiric Embrace charm [18] = { name = "Vampiric Embrace", description = "Adds 4% Life Leech to attacks if wearing equipment that provides life leech.", + category = CHARM_MINOR, type = CHARM_PASSIVE, - percent = 400, - chance = 100, - points = 1500, + chance = { 1.6, 2.4, 3.2 }, + points = { 100, 150, 225 }, }, - -- Void's call charm + -- Void's Call charm [19] = { name = "Void's Call", description = "Adds 2% Mana Leech to attacks if wearing equipment that provides mana leech.", + category = CHARM_MINOR, type = CHARM_PASSIVE, - percent = 200, - chance = 100, - points = 1500, + chance = { 0.8, 1.2, 1.6 }, + points = { 100, 150, 225 }, + }, + -- Savage Blow charm + [20] = { + name = "Savage Blow", + description = "Adds critical extra damage to attacks with critical hit weapons.", + category = CHARM_MAJOR, + type = CHARM_PASSIVE, + chance = { 20, 40, 44 }, + points = { 800, 1200, 4000 }, + }, + -- Fatal Hold charm + [21] = { + name = "Fatal Hold", + description = "Prevents creatures from fleeing due to low health for 30 seconds.", + category = CHARM_MINOR, + type = CHARM_PASSIVE, + chance = { 30, 45, 60 }, + messageCancel = "Your enemy is not able to flee now for 30 \z + seconds. (fatal hold charm)", + points = { 100, 150, 225 }, + }, + -- Void Inversion charm + [22] = { + name = "Void Inversion", + description = "Chance to gain mana instead of losing it when taking Mana Drain damage.", + category = CHARM_MINOR, + type = CHARM_PASSIVE, + chance = { 20, 30, 40 }, + points = { 100, 150, 225 }, + }, + -- Carnage charm + [23] = { + name = "Carnage", + description = "Killing a monster deals physical damage to others in a small radius.", + category = CHARM_MAJOR, + type = CHARM_OFFENSIVE, + damageType = COMBAT_NEUTRALDAMAGE, + percent = 15, + chance = { 10, 20, 22 }, + messageServerLog = true, + points = { 600, 900, 3000 }, + }, + -- Overpower charm + [24] = { + name = "Overpower", + description = "Deals physical damage based on your maximum health.", + category = CHARM_MAJOR, + type = CHARM_OFFENSIVE, + damageType = COMBAT_NEUTRALDAMAGE, + percent = 5, + chance = { 5, 10, 11 }, + messageServerLog = true, + points = { 600, 900, 3000 }, + }, + -- Overflux charm + [25] = { + name = "Overflux", + description = "Deals physical damage based on your maximum mana.", + category = CHARM_MAJOR, + type = CHARM_OFFENSIVE, + damageType = COMBAT_NEUTRALDAMAGE, + percent = 2.5, + chance = { 5, 10, 11 }, + messageServerLog = true, + points = { 600, 900, 3000 }, }, } @@ -224,6 +295,9 @@ for charmId, charmsTable in ipairs(charms) do if charmsTable.description then charmConfig.description = charmsTable.description end + if charmsTable.category then + charmConfig.category = charmsTable.category + end if charmsTable.type then charmConfig.type = charmsTable.type end @@ -246,7 +320,7 @@ for charmId, charmsTable in ipairs(charms) do charmConfig.effect = charmsTable.effect end if charmsTable.points then - charmConfig.points = math.ceil(charmsTable.points * bestiaryRateCharmShopPrice) + charmConfig.points = charmsTable.points end -- Create charm and egister charmConfig table diff --git a/data/scripts/talkactions/god/charms.lua b/data/scripts/talkactions/god/charms.lua index 238bce7614f..4a4cc3e8570 100644 --- a/data/scripts/talkactions/god/charms.lua +++ b/data/scripts/talkactions/god/charms.lua @@ -143,7 +143,6 @@ function setBestiary.onSay(player, words, param) end local monsterName = split[2] - -- If "all" is specified, iterate through all monsters if monsterName:lower() == "all" then local monsterList = Game.getMonsterTypes() -- Retrieves all available monsters diff --git a/data/scripts/weapons/dawnport_weapons.lua b/data/scripts/weapons/dawnport_weapons.lua deleted file mode 100644 index 90a64d5656a..00000000000 --- a/data/scripts/weapons/dawnport_weapons.lua +++ /dev/null @@ -1,47 +0,0 @@ --- the chille -local dawnportWeapon = Weapon(WEAPON_WAND) - -local combat = Combat() -combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ICEDAMAGE) -combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ICE) - -function onGetFormulaValues(player, level, maglevel) - local min = (level / 5) + (maglevel * 0.4) + 3 - local max = (level / 5) + (maglevel * 0.7) + 7 - return -min, -max -end - -combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") - -dawnportWeapon.onUseWeapon = function(player, variant) - return combat:execute(player, variant) -end - -dawnportWeapon:id(21350) -dawnportWeapon:mana(1) -dawnportWeapon:range(3) -dawnportWeapon:register() - --- the scorcher -local dawnportWeapon = Weapon(WEAPON_WAND) - -local combat = Combat() -combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) -combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) - -function onGetFormulaValues(player, level, maglevel) - local min = (level / 5) + (maglevel * 0.4) + 3 - local max = (level / 5) + (maglevel * 0.7) + 7 - return -min, -max -end - -combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") - -dawnportWeapon.onUseWeapon = function(player, variant) - return combat:execute(player, variant) -end - -dawnportWeapon:id(21348) -dawnportWeapon:mana(1) -dawnportWeapon:range(3) -dawnportWeapon:register() diff --git a/schema.sql b/schema.sql index 10efc5dce00..834a1245708 100644 --- a/schema.sql +++ b/schema.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS `server_config` ( CONSTRAINT `server_config_pk` PRIMARY KEY (`config`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '49'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); +INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '50'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); -- Table structure `accounts` CREATE TABLE IF NOT EXISTS `accounts` ( @@ -552,31 +552,18 @@ CREATE TABLE IF NOT EXISTS `players_online` ( -- Table structure `player_charm` CREATE TABLE IF NOT EXISTS `player_charms` ( - `player_guid` INT(250) NOT NULL, - `charm_points` VARCHAR(250) NULL, - `charm_expansion` BOOLEAN NULL, - `rune_wound` INT(250) NULL, - `rune_enflame` INT(250) NULL, - `rune_poison` INT(250) NULL, - `rune_freeze` INT(250) NULL, - `rune_zap` INT(250) NULL, - `rune_curse` INT(250) NULL, - `rune_cripple` INT(250) NULL, - `rune_parry` INT(250) NULL, - `rune_dodge` INT(250) NULL, - `rune_adrenaline` INT(250) NULL, - `rune_numb` INT(250) NULL, - `rune_cleanse` INT(250) NULL, - `rune_bless` INT(250) NULL, - `rune_scavenge` INT(250) NULL, - `rune_gut` INT(250) NULL, - `rune_low_blow` INT(250) NULL, - `rune_divine` INT(250) NULL, - `rune_vamp` INT(250) NULL, - `rune_void` INT(250) NULL, - `UsedRunesBit` VARCHAR(250) NULL, - `UnlockedRunesBit` VARCHAR(250) NULL, - `tracker list` BLOB NULL + `player_id` int(11) NOT NULL, + `charm_points` SMALLINT NOT NULL DEFAULT '0', + `minor_charm_echoes` SMALLINT NOT NULL DEFAULT '0', + `max_charm_points` SMALLINT NOT NULL DEFAULT '0', + `max_minor_charm_echoes` SMALLINT NOT NULL DEFAULT '0', + `charm_expansion` BOOLEAN NOT NULL DEFAULT FALSE, + `UsedRunesBit` INT NOT NULL DEFAULT '0', + `UnlockedRunesBit` INT NOT NULL DEFAULT '0', + `charms` BLOB NULL, + `tracker list` BLOB NULL, + CONSTRAINT `player_charms_players_fk` + FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) ) ENGINE = InnoDB DEFAULT CHARSET=utf8; -- Table structure `player_deaths` diff --git a/src/config/config_enums.hpp b/src/config/config_enums.hpp index 5895c119042..f24e94d28ba 100644 --- a/src/config/config_enums.hpp +++ b/src/config/config_enums.hpp @@ -270,8 +270,8 @@ enum ConfigKey_t : uint16_t { STAMINA_TRAINER_GAIN, STAMINA_TRAINER, START_STREAK_LEVEL, - STASH_ITEMS, STASH_MOVING, + STASH_MANAGE_AMOUNT, STATUS_PORT, STATUSQUERY_TIMEOUT, STORE_COIN_PACKET, @@ -310,10 +310,10 @@ enum ConfigKey_t : uint16_t { TOGGLE_SERVER_IS_RETRO, TOGGLE_TRAVELS_FREE, TOGGLE_WHEELSYSTEM, - TRANSCENDANCE_AVATAR_DURATION, - TRANSCENDANCE_CHANCE_FORMULA_A, - TRANSCENDANCE_CHANCE_FORMULA_B, - TRANSCENDANCE_CHANCE_FORMULA_C, + TRANSCENDENCE_AVATAR_DURATION, + TRANSCENDENCE_CHANCE_FORMULA_A, + TRANSCENDENCE_CHANCE_FORMULA_B, + TRANSCENDENCE_CHANCE_FORMULA_C, URL, USE_ANY_DATAPACK_FOLDER, VIP_AUTOLOOT_VIP_ONLY, @@ -335,5 +335,8 @@ enum ConfigKey_t : uint16_t { WHEEL_POINTS_PER_LEVEL, WHITE_SKULL_TIME, WORLD_TYPE, - XP_DISPLAY_MODE + XP_DISPLAY_MODE, + AMPLIFICATION_CHANCE_FORMULA_A, + AMPLIFICATION_CHANCE_FORMULA_B, + AMPLIFICATION_CHANCE_FORMULA_C }; diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index 4bb74113266..8833ec7958b 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -60,7 +60,6 @@ bool ConfigManager::load() { loadIntConfig(L, MARKET_REFRESH_PRICES, "marketRefreshPricesInterval", 30); loadIntConfig(L, PREMIUM_DEPOT_LIMIT, "premiumDepotLimit", 8000); loadIntConfig(L, SQL_PORT, "mysqlPort", 3306); - loadIntConfig(L, STASH_ITEMS, "stashItemCount", 5000); loadIntConfig(L, STATUS_PORT, "statusProtocolPort", 7171); loadStringConfig(L, AUTH_TYPE, "authType", "password"); @@ -129,6 +128,7 @@ bool ConfigManager::load() { loadBoolConfig(L, STAMINA_SYSTEM, "staminaSystem", true); loadBoolConfig(L, STAMINA_TRAINER, "staminaTrainer", false); loadBoolConfig(L, STASH_MOVING, "stashMoving", false); + loadIntConfig(L, STASH_MANAGE_AMOUNT, "stashManageAmount", 100000); loadBoolConfig(L, TASK_HUNTING_ENABLED, "taskHuntingSystemEnabled", true); loadBoolConfig(L, TASK_HUNTING_FREE_THIRD_SLOT, "taskHuntingFreeThirdSlot", false); loadBoolConfig(L, TELEPORT_PLAYER_TO_VOCATION_ROOM, "teleportPlayerToVocationRoom", true); @@ -197,9 +197,13 @@ bool ConfigManager::load() { loadFloatConfig(L, RUSE_CHANCE_FORMULA_A, "ruseChanceFormulaA", 0.0307576); loadFloatConfig(L, RUSE_CHANCE_FORMULA_B, "ruseChanceFormulaB", 0.440697); loadFloatConfig(L, RUSE_CHANCE_FORMULA_C, "ruseChanceFormulaC", 0.026); - 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, TRANSCENDENCE_CHANCE_FORMULA_A, "transcendanceChanceFormulaA", 0.0127); + loadFloatConfig(L, TRANSCENDENCE_CHANCE_FORMULA_B, "transcendanceChanceFormulaB", 0.1070); + loadFloatConfig(L, TRANSCENDENCE_CHANCE_FORMULA_C, "transcendanceChanceFormulaC", 0.0073); + loadFloatConfig(L, AMPLIFICATION_CHANCE_FORMULA_A, "amplificationChanceFormulaA", 0.4); + loadFloatConfig(L, AMPLIFICATION_CHANCE_FORMULA_B, "amplificationChanceFormulaB", 1.7); + loadFloatConfig(L, AMPLIFICATION_CHANCE_FORMULA_C, "amplificationChanceFormulaC", 0.4); + 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); @@ -333,7 +337,7 @@ bool ConfigManager::load() { loadIntConfig(L, TASK_HUNTING_SELECTION_LIST_PRICE, "taskHuntingSelectListPrice", 5); loadIntConfig(L, TIBIADROME_CONCOCTION_COOLDOWN, "tibiadromeConcoctionCooldown", 24 * 60 * 60); loadIntConfig(L, TIBIADROME_CONCOCTION_DURATION, "tibiadromeConcoctionDuration", 1 * 60 * 60); - loadIntConfig(L, TRANSCENDANCE_AVATAR_DURATION, "transcendanceAvatarDuration", 7000); + loadIntConfig(L, TRANSCENDENCE_AVATAR_DURATION, "transcendenceAvatarDuration", 7000); loadIntConfig(L, VIP_BONUS_EXP, "vipBonusExp", 0); loadIntConfig(L, VIP_BONUS_LOOT, "vipBonusLoot", 0); loadIntConfig(L, VIP_BONUS_SKILL, "vipBonusSkill", 0); diff --git a/src/core.hpp b/src/core.hpp index c27c5e15752..3b7b2e15356 100644 --- a/src/core.hpp +++ b/src/core.hpp @@ -15,7 +15,7 @@ static constexpr auto AUTHENTICATOR_PERIOD = 30U; // SERVER_MAJOR_VERSION is the actual full version of the server, including minor and patch numbers. // This is intended for internal use to identify the exact state of the server (release) software. static constexpr auto SERVER_RELEASE_VERSION = "3.2.0"; -static constexpr auto CLIENT_VERSION = 1405; +static constexpr auto CLIENT_VERSION = 1412; #define CLIENT_VERSION_UPPER (CLIENT_VERSION / 100) #define CLIENT_VERSION_LOWER (CLIENT_VERSION % 100) diff --git a/src/creatures/combat/combat.cpp b/src/creatures/combat/combat.cpp index be14872c90b..fe96dc4a703 100644 --- a/src/creatures/combat/combat.cpp +++ b/src/creatures/combat/combat.cpp @@ -343,7 +343,13 @@ ReturnValue Combat::canDoCombat(const std::shared_ptr &attacker, const if (attacker) { const auto &attackerMaster = attacker->getMaster(); + const auto &masterAttackerPlayer = attackerMaster ? attackerMaster->getPlayer() : nullptr; + const auto &masterAttackerMonster = attackerMaster ? attackerMaster->getMonster() : nullptr; const auto &attackerPlayer = attacker->getPlayer(); + const auto &attackerMonster = attacker->getMonster(); + const auto &targetMonster = target ? target->getMonster() : nullptr; + const auto &targetMaster = target ? target->getMaster() : nullptr; + const auto &targetMasterPlayer = targetMaster ? targetMaster->getPlayer() : nullptr; if (targetPlayer) { if (targetPlayer->hasFlag(PlayerFlags_t::CannotBeAttacked)) { return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER; @@ -373,7 +379,7 @@ ReturnValue Combat::canDoCombat(const std::shared_ptr &attacker, const } if (attackerMaster) { - if (const auto &masterAttackerPlayer = attackerMaster->getPlayer()) { + if (masterAttackerPlayer) { if (masterAttackerPlayer->hasFlag(PlayerFlags_t::CannotAttackPlayer)) { return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER; } @@ -388,13 +394,13 @@ ReturnValue Combat::canDoCombat(const std::shared_ptr &attacker, const } } - if (attacker->getMonster() && (!attackerMaster || attackerMaster->getMonster())) { - if (attacker->getFaction() != FACTION_DEFAULT && !attacker->getMonster()->isEnemyFaction(targetPlayer->getFaction())) { + if (attackerMonster && (!attackerMaster || masterAttackerMonster)) { + if (attacker->getFaction() != FACTION_DEFAULT && !attackerMonster->isEnemyFaction(targetPlayer->getFaction())) { return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER; } } - } else if (target && target->getMonster()) { - if (attacker->getFaction() != FACTION_DEFAULT && attacker->getFaction() != FACTION_PLAYER && attacker->getMonster() && !attacker->getMonster()->isEnemyFaction(target->getFaction())) { + } else if (targetMonster) { + if (attacker->getFaction() != FACTION_DEFAULT && attacker->getFaction() != FACTION_PLAYER && attackerMonster && !attackerMonster->isEnemyFaction(target->getFaction())) { return RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE; } @@ -403,14 +409,12 @@ ReturnValue Combat::canDoCombat(const std::shared_ptr &attacker, const return RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE; } - if (target->isSummon() && target->getMaster()->getPlayer() && target->getZoneType() == ZONE_NOPVP) { + if (target->isSummon() && targetMasterPlayer && target->getZoneType() == ZONE_NOPVP) { return RETURNVALUE_ACTIONNOTPERMITTEDINANOPVPZONE; } - } else if (attacker->getMonster()) { - const auto &targetMaster = target->getMaster(); - - if ((!targetMaster || !targetMaster->getPlayer()) && attacker->getFaction() == FACTION_DEFAULT) { - if (!attackerMaster || !attackerMaster->getPlayer()) { + } else if (attackerMonster) { + if ((!targetMaster || !targetMasterPlayer) && attacker->getFaction() == FACTION_DEFAULT) { + if (!attackerMaster || !masterAttackerPlayer) { return RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE; } } @@ -420,14 +424,14 @@ ReturnValue Combat::canDoCombat(const std::shared_ptr &attacker, const } if (g_game().getWorldType() == WORLD_TYPE_NO_PVP) { - if (attacker->getPlayer() || (attackerMaster && attackerMaster->getPlayer())) { + if (attackerPlayer || masterAttackerPlayer) { if (targetPlayer) { if (!isInPvpZone(attacker, target)) { return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER; } } - if (target && target->isSummon() && target->getMaster()->getPlayer()) { + if (target && target->isSummon() && targetMasterPlayer) { if (!isInPvpZone(attacker, target)) { return RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE; } @@ -691,6 +695,31 @@ void Combat::CombatHealthFunc(const std::shared_ptr &caster, const std if (g_game().combatChangeHealth(caster, target, damage)) { CombatConditionFunc(caster, target, params, &damage); CombatDispelFunc(caster, target, params, nullptr); + + if (!targetMonster || !attackerPlayer) { + return; + } + + const uint16_t playerCharmRaceid = attackerPlayer->parseRacebyCharm(CHARM_FATAL); + if (playerCharmRaceid == 0) { + return; + } + + const auto &mType = g_monsters().getMonsterType(targetMonster->getName()); + if (!mType || playerCharmRaceid != mType->info.raceid) { + return; + } + + const auto &charm = g_iobestiary().getBestiaryCharm(CHARM_FATAL); + if (!charm) { + return; + } + + if (charm->chance[attackerPlayer->getCharmTier(CHARM_FATAL)] <= normal_random(0, 100)) { + return; + } + + g_iobestiary().parseCharmCombat(charm, attackerPlayer, targetMonster); } } @@ -788,35 +817,45 @@ void Combat::CombatConditionFunc(const std::shared_ptr &caster, const return; } - for (const auto &condition : params.conditionList) { - std::shared_ptr player = nullptr; - if (target) { - player = target->getPlayer(); - } - if (player) { - // Cleanse charm rune (target as player) - if (player->isImmuneCleanse(condition->getType())) { - player->sendCancelMessage("You are still immune against this spell."); - return; - } else if (caster && caster->getMonster()) { - uint16_t playerCharmRaceid = player->parseRacebyCharm(CHARM_CLEANSE, false, 0); - if (playerCharmRaceid != 0) { - const auto &mType = g_monsters().getMonsterType(caster->getName()); - if (mType && playerCharmRaceid == mType->info.raceid) { - const auto charm = g_iobestiary().getBestiaryCharm(CHARM_CLEANSE); - if (charm && (charm->chance > normal_random(0, 100))) { - if (player->hasCondition(condition->getType())) { - player->removeCondition(condition->getType()); - } - player->setImmuneCleanse(condition->getType()); - player->sendCancelMessage(charm->cancelMsg); - return; + const auto &targetPlayer = target ? target->getPlayer() : nullptr; + const auto &casterMonster = caster ? caster->getMonster() : nullptr; + + if (targetPlayer && casterMonster) { + const auto &cleansableConditions = targetPlayer->getCleansableConditions(); + + if (!cleansableConditions.empty()) { + uint16_t playerCharmRaceid = targetPlayer->parseRacebyCharm(CHARM_CLEANSE); + if (playerCharmRaceid != 0) { + const auto &mType = casterMonster->getMonsterType(); + if (mType && playerCharmRaceid == mType->info.raceid) { + const auto &charm = g_iobestiary().getBestiaryCharm(CHARM_CLEANSE); + const auto charmTier = targetPlayer->getCharmTier(CHARM_CLEANSE); + if (charm && (charm->chance[charmTier] >= normal_random(0, 10000) / 100.0)) { + uint16_t conditionIndex = uniform_random(0, cleansableConditions.size() - 1); + const auto &condition = cleansableConditions[conditionIndex]; + const auto conditionType = condition->getType(); + if (targetPlayer->hasCondition(conditionType)) { + targetPlayer->removeCondition(conditionType); + } + targetPlayer->setImmuneCleanse(conditionType); + if (!charm->cancelMessage.empty()) { + targetPlayer->onCleanseCondition(conditionType); } + + return; } } } + } + } - if (condition->getType() == CONDITION_FEARED && !checkFearConditionAffected(player)) { + for (const auto &condition : params.conditionList) { + if (targetPlayer) { + if (condition->getType() != CONDITION_FEARED && targetPlayer->isImmuneCleanse(condition->getType())) { + return; + } + + if (condition->getType() == CONDITION_FEARED && !checkFearConditionAffected(targetPlayer)) { return; } } @@ -1172,20 +1211,22 @@ bool Combat::doCombat(const std::shared_ptr &caster, const Position &p return true; } -void Combat::CombatFunc(const std::shared_ptr &caster, const Position &origin, const Position &pos, const std::unique_ptr &area, const CombatParams ¶ms, const CombatFunction &func, CombatDamage* data) { +void Combat::CombatFunc(const std::shared_ptr &caster, const Position &origin, const Position &toPos, const std::unique_ptr &area, const CombatParams ¶ms, const CombatFunction &func, CombatDamage* data) { std::vector> tileList; + const std::shared_ptr &casterPlayer = caster ? caster->getPlayer() : nullptr; + if (caster) { - getCombatArea(caster->getPosition(), pos, area, tileList); + getCombatArea(caster->getPosition(), toPos, area, tileList); } else { - getCombatArea(pos, pos, area, tileList); + getCombatArea(toPos, toPos, area, tileList); } uint32_t maxX = 0; uint32_t maxY = 0; + std::vector> affectedTargets; - const std::shared_ptr &casterPlayer = caster ? caster->getPlayer() : nullptr; - // calculate the max viewable range + // Calculate the max viewable range and affected creatures for (const auto &tile : tileList) { // If the caster is a player and the world is no pvp, we need to check if there are more than one player in the tile and skip the combat if (casterPlayer && g_game().getWorldType() == WORLD_TYPE_NO_PVP && tile->getPosition() == origin) { @@ -1198,33 +1239,21 @@ void Combat::CombatFunc(const std::shared_ptr &caster, const Position const Position &tilePos = tile->getPosition(); - uint32_t diff = Position::getDistanceX(tilePos, pos); + uint32_t diff = Position::getDistanceX(tilePos, toPos); if (diff > maxX) { maxX = diff; } - - diff = Position::getDistanceY(tilePos, pos); + diff = Position::getDistanceY(tilePos, toPos); if (diff > maxY) { maxY = diff; } - } - - const int32_t rangeX = maxX + MAP_MAX_VIEW_PORT_X; - const int32_t rangeY = maxY + MAP_MAX_VIEW_PORT_Y; - std::vector> affectedTargets; - CombatDamage tmpDamage; - if (data) { - tmpDamage = *data; - } - for (const auto &tile : tileList) { if (canDoCombat(caster, tile, params.aggressive) != RETURNVALUE_NOERROR) { continue; } if (CreatureVector* creatures = tile->getCreatures()) { const auto &topCreature = tile->getTopCreature(); - // A copy of the tile's creature list is made because modifications to this vector, such as adding or removing creatures through a Lua callback, may occur during the iteration within the for loop. CreatureVector creaturesCopy = *creatures; for (const auto &creature : creaturesCopy) { if (params.targetCasterOrTopMost) { @@ -1244,10 +1273,16 @@ void Combat::CombatFunc(const std::shared_ptr &caster, const Position } } - applyExtensions(caster, affectedTargets, tmpDamage, params); + const int32_t rangeX = maxX + MAP_MAX_VIEW_PORT_X; + const int32_t rangeY = maxY + MAP_MAX_VIEW_PORT_Y; + + CombatDamage tmpDamage; + if (data) { + tmpDamage = *data; + } // Wheel of destiny get beam affected total - auto spectators = Spectators().find(pos, true, rangeX, rangeX, rangeY, rangeY); + auto spectators = Spectators().find(toPos, true, rangeX, rangeX, rangeY, rangeY); uint8_t beamAffectedTotal = casterPlayer ? casterPlayer->wheel().getBeamAffectedTotal(tmpDamage) : 0; uint8_t beamAffectedCurrent = 0; @@ -1305,7 +1340,7 @@ void Combat::CombatFunc(const std::shared_ptr &caster, const Position combatTileEffects(spectators.data(), caster, tile, params); } - postCombatEffects(caster, origin, pos, params); + postCombatEffects(caster, origin, toPos, params); } void Combat::doCombatHealth(const std::shared_ptr &caster, const std::shared_ptr &target, CombatDamage &damage, const CombatParams ¶ms) { @@ -2273,7 +2308,8 @@ void Combat::applyExtensions(const std::shared_ptr &caster, const std: baseChance = player->getSkillLevel(SKILL_CRITICAL_HIT_CHANCE); baseBonus = player->getSkillLevel(SKILL_CRITICAL_HIT_DAMAGE); - uint16_t lowBlowRaceid = player->parseRacebyCharm(CHARM_LOW, false, 0); + uint16_t lowBlowRaceid = player->parseRacebyCharm(CHARM_LOW); + uint16_t savageBlowRaceid = player->parseRacebyCharm(CHARM_SAVAGE); baseBonus += damage.criticalDamage; baseChance += static_cast(damage.criticalChance); @@ -2285,13 +2321,17 @@ void Combat::applyExtensions(const std::shared_ptr &caster, const std: bool canApplyFatal = false; if (const auto &playerWeapon = player->getInventoryItem(CONST_SLOT_LEFT); playerWeapon && playerWeapon->getTier() > 0) { double fatalChance = playerWeapon->getFatalChance(); + if (const auto &playerBoots = player->getInventoryItem(CONST_SLOT_FEET); playerBoots && playerBoots->getTier()) { + fatalChance *= 1 + (playerBoots->getAmplificationChance() / 100); + } canApplyFatal = (fatalChance > 0 && uniform_random(0, 10000) / 100.0 < fatalChance); } if (!canApplyCritical && lowBlowRaceid != 0) { const auto &charm = g_iobestiary().getBestiaryCharm(CHARM_LOW); if (charm) { - uint16_t lowBlowChance = baseChance + charm->percent; + auto charmTier = player->getCharmTier(CHARM_LOW); + uint16_t lowBlowChance = baseChance + (charm->chance[charmTier] * 100); for (const auto &target : targets) { const auto &targetMonster = target->getMonster(); @@ -2315,6 +2355,15 @@ void Combat::applyExtensions(const std::shared_ptr &caster, const std: } } + int32_t savageBlowBonus = baseBonus; + if (savageBlowRaceid != 0) { + const auto &charm = g_iobestiary().getBestiaryCharm(CHARM_SAVAGE); + if (charm) { + auto charmTier = player->getCharmTier(CHARM_SAVAGE); + savageBlowBonus += charm->chance[charmTier] * 100; + } + } + bool isSingleCombat = targets.size() == 1; for (const auto &targetCreature : targets) { CombatDamage targetDamage = damage; @@ -2333,6 +2382,10 @@ void Combat::applyExtensions(const std::shared_ptr &caster, const std: if (!canApplyCritical && lowBlowCrits.contains(raceId) && lowBlowCrits[raceId]) { isTargetCritical = true; } + + if (raceId == savageBlowRaceid) { + finalBonus = savageBlowBonus; + } } double targetMultiplier = 1.0 + static_cast(finalBonus) / 10000.0; diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index 4f4a66a0bcd..50d51cf6d07 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -698,14 +698,13 @@ bool Creature::dropCorpse(const std::shared_ptr &lastHitCreature, cons if (corpseContainer && player && !disallowedCorpses) { const auto &monster = getMonster(); if (monster && !monster->isRewardBoss()) { - std::ostringstream lootMessage; auto collorMessage = player->getProtocolVersion() > 1200; - lootMessage << "Loot of " << getNameDescription() << ": " << corpseContainer->getContentDescription(collorMessage) << "."; auto suffix = corpseContainer->getAttribute(ItemAttribute_t::LOOTMESSAGE_SUFFIX); + std::string lootMessage = fmt::format("Loot of {}: {}", getNameDescription(), corpseContainer->getContentDescription(collorMessage)); if (!suffix.empty()) { - lootMessage << suffix; + lootMessage = fmt::format("{} ({})", lootMessage, suffix); } - player->sendLootMessage(lootMessage.str()); + player->sendLootMessage(fmt::format("{}.", lootMessage)); } FindPathParams fpp; @@ -1384,6 +1383,30 @@ std::shared_ptr Creature::getCondition(ConditionType_t type, Conditio return nullptr; } +std::vector> Creature::getCleansableConditions() const { + std::vector> cleansableConditions; + for (const auto &condition : conditions) { + switch (condition->getType()) { + case CONDITION_POISON: + case CONDITION_FIRE: + case CONDITION_ENERGY: + case CONDITION_FREEZING: + case CONDITION_CURSED: + case CONDITION_DAZZLED: + case CONDITION_BLEEDING: + case CONDITION_PARALYZE: + case CONDITION_ROOTED: + case CONDITION_FEARED: + cleansableConditions.emplace_back(condition); + break; + + default: + break; + } + } + return cleansableConditions; +} + std::vector> Creature::getConditionsByType(ConditionType_t type) const { std::vector> conditionsVec; for (const auto &condition : conditions) { diff --git a/src/creatures/creature.hpp b/src/creatures/creature.hpp index c76890a9ba1..62ae644361e 100644 --- a/src/creatures/creature.hpp +++ b/src/creatures/creature.hpp @@ -83,10 +83,10 @@ class Creature : virtual public Thing, public SharedObject { Creature(const Creature &) = delete; Creature &operator=(const Creature &) = delete; - std::shared_ptr getCreature() override final { + std::shared_ptr getCreature() final { return static_self_cast(); } - std::shared_ptr getCreature() const override final { + std::shared_ptr getCreature() const final { return static_self_cast(); } std::shared_ptr getPlayer() override { @@ -168,13 +168,13 @@ class Creature : virtual public Thing, public SharedObject { directionLocked = locked; } - int32_t getThrowRange() const override final { + int32_t getThrowRange() const final { return 1; } bool isPushable() override { return getWalkDelay() <= 0; } - bool isRemoved() override final { + bool isRemoved() final { return isInternalRemoved; } virtual bool canSeeInvisibility() const { @@ -400,13 +400,13 @@ class Creature : virtual public Thing, public SharedObject { virtual float getMitigation() const { return 0; } - virtual int32_t getDefense() const { + virtual int32_t getDefense(bool = false) const { return 0; } virtual float getAttackFactor() const { return 1.0f; } - virtual float getDefenseFactor() const { + virtual float getDefenseFactor(bool = false) const { return 1.0f; } @@ -422,6 +422,7 @@ class Creature : virtual public Thing, public SharedObject { void removeCombatCondition(ConditionType_t type); std::shared_ptr getCondition(ConditionType_t type) const; std::shared_ptr getCondition(ConditionType_t type, ConditionId_t conditionId, uint32_t subId = 0) const; + std::vector> getCleansableConditions() const; std::vector> getConditionsByType(ConditionType_t type) const; void executeConditions(uint32_t interval); bool hasCondition(ConditionType_t type, uint32_t subId = 0) const; @@ -566,7 +567,7 @@ class Creature : virtual public Thing, public SharedObject { void setParent(std::weak_ptr cylinder) final; - const Position &getPosition() override final { + const Position &getPosition() final { return position; } diff --git a/src/creatures/creatures_definitions.hpp b/src/creatures/creatures_definitions.hpp index 9f04b4978cd..5963d6393b4 100644 --- a/src/creatures/creatures_definitions.hpp +++ b/src/creatures/creatures_definitions.hpp @@ -286,13 +286,6 @@ enum CallBackParam_t { CALLBACK_PARAM_CHAINPICKER, }; -enum charm_t { - CHARM_UNDEFINED = 0, - CHARM_OFFENSIVE = 1, - CHARM_DEFENSIVE = 2, - CHARM_PASSIVE = 3, -}; - enum SpeechBubble_t { SPEECHBUBBLE_NONE = 0, SPEECHBUBBLE_NORMAL = 1, @@ -348,6 +341,19 @@ enum Slots_t : uint8_t { CONST_SLOT_LAST = CONST_SLOT_STORE_INBOX, }; +enum charmCategory_t { + CHARM_ALL = 0, + CHARM_MAJOR = 1, + CHARM_MINOR = 2, +}; + +enum charm_t { + CHARM_UNDEFINED = 0, + CHARM_OFFENSIVE = 1, + CHARM_DEFENSIVE = 2, + CHARM_PASSIVE = 3, +}; + enum charmRune_t : int8_t { CHARM_NONE = -1, CHARM_WOUND = 0, @@ -369,8 +375,12 @@ enum charmRune_t : int8_t { CHARM_DIVINE = 16, CHARM_VAMP = 17, CHARM_VOID = 18, - - CHARM_LAST = CHARM_VOID, + CHARM_SAVAGE = 19, + CHARM_FATAL = 20, + CHARM_VOIDINVERSION = 21, + CHARM_CARNAGE = 22, + CHARM_OVERPOWER = 23, + CHARM_OVERFLUX = 24, }; enum ConditionId_t : int8_t { @@ -1566,6 +1576,7 @@ struct CombatDamage { bool extension = false; std::string exString; bool fatal = false; + bool hazardDodge = false; int32_t criticalDamage = 0; int32_t criticalChance = 0; diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index 988665b8999..e53c86d2f10 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -15,6 +15,7 @@ #include "creatures/players/player.hpp" #include "game/game.hpp" #include "game/scheduling/dispatcher.hpp" +#include "io/iobestiary.hpp" #include "items/tile.hpp" #include "lua/callbacks/event_callback.hpp" #include "lua/callbacks/events_callbacks.hpp" @@ -37,7 +38,7 @@ std::shared_ptr Monster::createMonster(const std::string &name) { Monster::Monster(const std::shared_ptr &mType) : m_lowerName(asLowerCaseString(mType->name)), nameDescription(asLowerCaseString(mType->nameDescription)), - mType(mType) { + m_monsterType(mType) { defaultOutfit = mType->info.outfit; currentOutfit = mType->info.outfit; skull = mType->info.skull; @@ -85,7 +86,7 @@ void Monster::removeList() { const std::string &Monster::getName() const { if (name.empty()) { - return mType->name; + return m_monsterType->name; } return name; } @@ -110,12 +111,12 @@ void Monster::setName(const std::string &name) { // Real monster name, set on monster creation "createMonsterType(typeName)" const std::string &Monster::getTypeName() const { - return mType->typeName; + return m_monsterType->typeName; } const std::string &Monster::getNameDescription() const { if (nameDescription.empty()) { - return mType->nameDescription; + return m_monsterType->nameDescription; } return nameDescription; } @@ -143,11 +144,11 @@ void Monster::setMasterPos(Position pos) { bool Monster::canWalkOnFieldType(CombatType_t combatType) const { switch (combatType) { case COMBAT_ENERGYDAMAGE: - return mType->info.canWalkOnEnergy; + return m_monsterType->info.canWalkOnEnergy; case COMBAT_FIREDAMAGE: - return mType->info.canWalkOnFire; + return m_monsterType->info.canWalkOnFire; case COMBAT_EARTHDAMAGE: - return mType->info.canWalkOnPoison; + return m_monsterType->info.canWalkOnPoison; default: return true; } @@ -159,8 +160,8 @@ double_t Monster::getReflectPercent(CombatType_t reflectType, bool useCharges) c if (result != 0) { g_logger().debug("[{}] before mtype reflect element {}, percent {}", __FUNCTION__, fmt::underlying(reflectType), result); } - auto it = mType->info.reflectMap.find(reflectType); - if (it != mType->info.reflectMap.end()) { + auto it = m_monsterType->info.reflectMap.find(reflectType); + if (it != m_monsterType->info.reflectMap.end()) { result += it->second; } @@ -186,8 +187,8 @@ void Monster::addReflectElement(CombatType_t combatType, int32_t percent) { m_reflectElementMap[combatType] += percent; } -int32_t Monster::getDefense() const { - auto mtypeDefense = mType->info.defense; +int32_t Monster::getDefense(bool) const { + auto mtypeDefense = m_monsterType->info.defense; if (mtypeDefense != 0) { g_logger().trace("[{}] old defense {}", __FUNCTION__, mtypeDefense); } @@ -208,7 +209,7 @@ Faction_t Monster::getFaction() const { if (const auto &master = getMaster()) { return master->getFaction(); } - return mType->info.faction; + return m_monsterType->info.faction; } bool Monster::isEnemyFaction(Faction_t faction) const { @@ -216,35 +217,35 @@ bool Monster::isEnemyFaction(Faction_t faction) const { if (master && master->getMonster()) { return master->getMonster()->isEnemyFaction(faction); } - return mType->info.enemyFactions.empty() ? false : mType->info.enemyFactions.contains(faction); + return m_monsterType->info.enemyFactions.empty() ? false : m_monsterType->info.enemyFactions.contains(faction); } bool Monster::isPushable() { - return mType->info.pushable && baseSpeed != 0; + return m_monsterType->info.pushable && baseSpeed != 0; } bool Monster::isAttackable() const { - return mType->info.isAttackable; + return m_monsterType->info.isAttackable; } bool Monster::canPushItems() const { - return mType->info.canPushItems; + return m_monsterType->info.canPushItems; } bool Monster::canPushCreatures() const { - return mType->info.canPushCreatures; + return m_monsterType->info.canPushCreatures; } bool Monster::isRewardBoss() const { - return mType->info.isRewardBoss; + return m_monsterType->info.isRewardBoss; } bool Monster::isHostile() const { - return mType->info.isHostile; + return m_monsterType->info.isHostile; } bool Monster::isFamiliar() const { - return mType->info.isFamiliar; + return m_monsterType->info.isFamiliar; } bool Monster::canSeeInvisibility() const { @@ -264,15 +265,15 @@ void Monster::setCriticalChance(uint16_t chance) { } uint16_t Monster::getCriticalChance() const { - return mType->info.critChance + criticalChance; + return m_monsterType->info.critChance + criticalChance; } uint32_t Monster::getManaCost() const { - return mType->info.manaCost; + return m_monsterType->info.manaCost; } RespawnType Monster::getRespawnType() const { - return mType->info.respawnType; + return m_monsterType->info.respawnType; } void Monster::setSpawnMonster(const std::shared_ptr &newSpawnMonster) { @@ -280,8 +281,8 @@ void Monster::setSpawnMonster(const std::shared_ptr &newSpawnMonst } uint32_t Monster::getHealingCombatValue(CombatType_t healingType) const { - auto it = mType->info.healingMap.find(healingType); - if (it != mType->info.healingMap.end()) { + auto it = m_monsterType->info.healingMap.find(healingType); + if (it != m_monsterType->info.healingMap.end()) { return it->second; } return 0; @@ -295,9 +296,9 @@ void Monster::onAttackedCreatureDisappear(bool) { void Monster::onCreatureAppear(const std::shared_ptr &creature, bool isLogin) { Creature::onCreatureAppear(creature, isLogin); - if (mType->info.creatureAppearEvent != -1) { + if (m_monsterType->info.creatureAppearEvent != -1) { // onCreatureAppear(self, creature) - LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + LuaScriptInterface* scriptInterface = m_monsterType->info.scriptInterface; if (!LuaScriptInterface::reserveScriptEnv()) { g_logger().error("[Monster::onCreatureAppear - Monster {} creature {}] " "Call stack overflow. Too many lua script calls being nested.", @@ -306,10 +307,10 @@ void Monster::onCreatureAppear(const std::shared_ptr &creature, bool i } ScriptEnvironment* env = LuaScriptInterface::getScriptEnv(); - env->setScriptId(mType->info.creatureAppearEvent, scriptInterface); + env->setScriptId(m_monsterType->info.creatureAppearEvent, scriptInterface); lua_State* L = scriptInterface->getLuaState(); - scriptInterface->pushFunction(mType->info.creatureAppearEvent); + scriptInterface->pushFunction(m_monsterType->info.creatureAppearEvent); LuaScriptInterface::pushUserdata(L, getMonster()); LuaScriptInterface::setMetatable(L, -1, "Monster"); @@ -335,9 +336,9 @@ void Monster::onCreatureAppear(const std::shared_ptr &creature, bool i void Monster::onRemoveCreature(const std::shared_ptr &creature, bool isLogout) { Creature::onRemoveCreature(creature, isLogout); - if (mType->info.creatureDisappearEvent != -1) { + if (m_monsterType->info.creatureDisappearEvent != -1) { // onCreatureDisappear(self, creature) - LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + LuaScriptInterface* scriptInterface = m_monsterType->info.scriptInterface; if (!LuaScriptInterface::reserveScriptEnv()) { g_logger().error("[Monster::onCreatureDisappear - Monster {} creature {}] " "Call stack overflow. Too many lua script calls being nested.", @@ -346,10 +347,10 @@ void Monster::onRemoveCreature(const std::shared_ptr &creature, bool i } ScriptEnvironment* env = LuaScriptInterface::getScriptEnv(); - env->setScriptId(mType->info.creatureDisappearEvent, scriptInterface); + env->setScriptId(m_monsterType->info.creatureDisappearEvent, scriptInterface); lua_State* L = scriptInterface->getLuaState(); - scriptInterface->pushFunction(mType->info.creatureDisappearEvent); + scriptInterface->pushFunction(m_monsterType->info.creatureDisappearEvent); LuaScriptInterface::pushUserdata(L, getMonster()); LuaScriptInterface::setMetatable(L, -1, "Monster"); @@ -378,9 +379,9 @@ void Monster::onRemoveCreature(const std::shared_ptr &creature, bool i void Monster::onCreatureMove(const std::shared_ptr &creature, const std::shared_ptr &newTile, const Position &newPos, const std::shared_ptr &oldTile, const Position &oldPos, bool teleport) { Creature::onCreatureMove(creature, newTile, newPos, oldTile, oldPos, teleport); - if (mType->info.creatureMoveEvent != -1) { + if (m_monsterType->info.creatureMoveEvent != -1) { // onCreatureMove(self, creature, oldPosition, newPosition) - LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + LuaScriptInterface* scriptInterface = m_monsterType->info.scriptInterface; if (!LuaScriptInterface::reserveScriptEnv()) { g_logger().error("[Monster::onCreatureMove - Monster {} creature {}] " "Call stack overflow. Too many lua script calls being nested.", @@ -389,10 +390,10 @@ void Monster::onCreatureMove(const std::shared_ptr &creature, const st } ScriptEnvironment* env = LuaScriptInterface::getScriptEnv(); - env->setScriptId(mType->info.creatureMoveEvent, scriptInterface); + env->setScriptId(m_monsterType->info.creatureMoveEvent, scriptInterface); lua_State* L = scriptInterface->getLuaState(); - scriptInterface->pushFunction(mType->info.creatureMoveEvent); + scriptInterface->pushFunction(m_monsterType->info.creatureMoveEvent); LuaScriptInterface::pushUserdata(L, getMonster()); LuaScriptInterface::setMetatable(L, -1, "Monster"); @@ -431,7 +432,7 @@ void Monster::onCreatureMove(const std::shared_ptr &creature, const st int32_t offset_x = Position::getDistanceX(followPosition, pos); int32_t offset_y = Position::getDistanceY(followPosition, pos); - if ((offset_x > 1 || offset_y > 1) && mType->info.changeTargetChance > 0) { + if ((offset_x > 1 || offset_y > 1) && m_monsterType->info.changeTargetChance > 0) { Direction dir = getDirectionTo(pos, followPosition); const auto &checkPosition = getNextPosition(dir, pos); @@ -460,9 +461,9 @@ void Monster::onCreatureMove(const std::shared_ptr &creature, const st void Monster::onCreatureSay(const std::shared_ptr &creature, SpeakClasses type, const std::string &text) { Creature::onCreatureSay(creature, type, text); - if (mType->info.creatureSayEvent != -1) { + if (m_monsterType->info.creatureSayEvent != -1) { // onCreatureSay(self, creature, type, message) - LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + LuaScriptInterface* scriptInterface = m_monsterType->info.scriptInterface; if (!LuaScriptInterface::reserveScriptEnv()) { g_logger().error("Monster {} creature {}] Call stack overflow. Too many lua " "script calls being nested.", @@ -471,10 +472,10 @@ void Monster::onCreatureSay(const std::shared_ptr &creature, SpeakClas } ScriptEnvironment* env = LuaScriptInterface::getScriptEnv(); - env->setScriptId(mType->info.creatureSayEvent, scriptInterface); + env->setScriptId(m_monsterType->info.creatureSayEvent, scriptInterface); lua_State* L = scriptInterface->getLuaState(); - scriptInterface->pushFunction(mType->info.creatureSayEvent); + scriptInterface->pushFunction(m_monsterType->info.creatureSayEvent); LuaScriptInterface::pushUserdata(L, getMonster()); LuaScriptInterface::setMetatable(L, -1, "Monster"); @@ -490,9 +491,9 @@ void Monster::onCreatureSay(const std::shared_ptr &creature, SpeakClas } void Monster::onAttackedByPlayer(const std::shared_ptr &attackerPlayer) { - if (mType->info.monsterAttackedByPlayerEvent != -1) { + if (m_monsterType->info.monsterAttackedByPlayerEvent != -1) { // onPlayerAttack(self, attackerPlayer) - LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + LuaScriptInterface* scriptInterface = m_monsterType->info.scriptInterface; if (!scriptInterface->reserveScriptEnv()) { g_logger().error("Monster {} creature {}] Call stack overflow. Too many lua " "script calls being nested.", @@ -501,10 +502,10 @@ void Monster::onAttackedByPlayer(const std::shared_ptr &attackerPlayer) } ScriptEnvironment* env = scriptInterface->getScriptEnv(); - env->setScriptId(mType->info.monsterAttackedByPlayerEvent, scriptInterface); + env->setScriptId(m_monsterType->info.monsterAttackedByPlayerEvent, scriptInterface); lua_State* L = scriptInterface->getLuaState(); - scriptInterface->pushFunction(mType->info.monsterAttackedByPlayerEvent); + scriptInterface->pushFunction(m_monsterType->info.monsterAttackedByPlayerEvent); LuaScriptInterface::pushUserdata(L, getMonster()); LuaScriptInterface::setMetatable(L, -1, "Monster"); @@ -517,9 +518,9 @@ void Monster::onAttackedByPlayer(const std::shared_ptr &attackerPlayer) } void Monster::onSpawn(const Position &position) { - if (mType->info.spawnEvent != -1) { + if (m_monsterType->info.spawnEvent != -1) { // onSpawn(self, spawnPosition) - LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + LuaScriptInterface* scriptInterface = m_monsterType->info.scriptInterface; if (!scriptInterface->reserveScriptEnv()) { g_logger().error("Monster {} creature {}] Call stack overflow. Too many lua " "script calls being nested.", @@ -528,10 +529,10 @@ void Monster::onSpawn(const Position &position) { } ScriptEnvironment* env = scriptInterface->getScriptEnv(); - env->setScriptId(mType->info.spawnEvent, scriptInterface); + env->setScriptId(m_monsterType->info.spawnEvent, scriptInterface); lua_State* L = scriptInterface->getLuaState(); - scriptInterface->pushFunction(mType->info.spawnEvent); + scriptInterface->pushFunction(m_monsterType->info.spawnEvent); LuaScriptInterface::pushUserdata(L, getMonster()); LuaScriptInterface::setMetatable(L, -1, "Monster"); @@ -707,11 +708,11 @@ bool Monster::isOpponent(const std::shared_ptr &creature) const { uint64_t Monster::getLostExperience() const { float extraExperience = forgeStack <= 15 ? (forgeStack + 10) / 10 : 28; - return skillLoss ? static_cast(std::round(mType->info.experience * extraExperience)) : 0; + return skillLoss ? static_cast(std::round(m_monsterType->info.experience * extraExperience)) : 0; } uint16_t Monster::getLookCorpse() const { - return mType->info.lookcorpse; + return m_monsterType->info.lookcorpse; } void Monster::onCreatureLeave(const std::shared_ptr &creature) { @@ -735,14 +736,14 @@ bool Monster::searchTarget(TargetSearchType_t searchType /*= TARGETSEARCH_DEFAUL searchType = TARGETSEARCH_NEAREST; - int32_t sum = this->mType->info.strategiesTargetNearest; + int32_t sum = this->m_monsterType->info.strategiesTargetNearest; if (rnd > sum) { searchType = TARGETSEARCH_HP; - sum += this->mType->info.strategiesTargetHealth; + sum += this->m_monsterType->info.strategiesTargetHealth; if (rnd > sum) { searchType = TARGETSEARCH_DAMAGE; - sum += this->mType->info.strategiesTargetDamage; + sum += this->m_monsterType->info.strategiesTargetDamage; if (rnd > sum) { searchType = TARGETSEARCH_RANDOM; } @@ -880,11 +881,11 @@ void Monster::onFollowCreatureComplete(const std::shared_ptr &creature } RaceType_t Monster::getRace() const { - return mType->info.race; + return m_monsterType->info.race; } float Monster::getMitigation() const { - float mitigation = mType->info.mitigation * getDefenseMultiplier(); + float mitigation = m_monsterType->info.mitigation * getDefenseMultiplier(); if (g_configManager().getBoolean(DISABLE_MONSTER_ARMOR)) { mitigation += std::ceil(static_cast(getDefense() + getArmor()) / 100.f) * getDefenseMultiplier() * 2.f; } @@ -892,7 +893,7 @@ float Monster::getMitigation() const { } int32_t Monster::getArmor() const { - return mType->info.armor * getDefenseMultiplier(); + return m_monsterType->info.armor * getDefenseMultiplier(); } BlockType_t Monster::blockHit(const std::shared_ptr &attacker, const CombatType_t &combatType, int32_t &damage, bool checkDefense /* = false*/, bool checkArmor /* = false*/, bool /* field = false */) { @@ -900,8 +901,8 @@ BlockType_t Monster::blockHit(const std::shared_ptr &attacker, const C if (damage != 0) { int32_t elementMod = 0; - auto it = mType->info.elementMap.find(combatType); - if (it != mType->info.elementMap.end()) { + auto it = m_monsterType->info.elementMap.find(combatType); + if (it != m_monsterType->info.elementMap.end()) { elementMod = it->second; } @@ -941,8 +942,12 @@ bool Monster::isTarget(const std::shared_ptr &creature) { return true; } +void Monster::setFatalHoldDuration(int32_t value) { + fatalHoldDuration = value; +} + bool Monster::isFleeing() const { - return !isSummon() && getHealth() <= runAwayHealth && challengeFocusDuration <= 0 && challengeMeleeDuration <= 0; + return !isSummon() && getHealth() <= runAwayHealth && challengeFocusDuration <= 0 && challengeMeleeDuration <= 0 && fatalHoldDuration <= 0; } bool Monster::selectTarget(const std::shared_ptr &creature) { @@ -1036,9 +1041,9 @@ void Monster::onEndCondition(ConditionType_t type) { void Monster::onThink(uint32_t interval) { Creature::onThink(interval); - if (mType->info.thinkEvent != -1) { + if (m_monsterType->info.thinkEvent != -1) { // onThink(self, interval) - LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + LuaScriptInterface* scriptInterface = m_monsterType->info.scriptInterface; if (!LuaScriptInterface::reserveScriptEnv()) { g_logger().error("Monster {} Call stack overflow. Too many lua script calls " "being nested.", @@ -1047,10 +1052,10 @@ void Monster::onThink(uint32_t interval) { } ScriptEnvironment* env = LuaScriptInterface::getScriptEnv(); - env->setScriptId(mType->info.thinkEvent, scriptInterface); + env->setScriptId(m_monsterType->info.thinkEvent, scriptInterface); lua_State* L = scriptInterface->getLuaState(); - scriptInterface->pushFunction(mType->info.thinkEvent); + scriptInterface->pushFunction(m_monsterType->info.thinkEvent); LuaScriptInterface::pushUserdata(L, getMonster()); LuaScriptInterface::setMetatable(L, -1, "Monster"); @@ -1066,12 +1071,12 @@ void Monster::onThink(uint32_t interval) { challengeMeleeDuration -= interval; if (challengeMeleeDuration <= 0) { challengeMeleeDuration = 0; - targetDistance = mType->info.targetDistance; + targetDistance = m_monsterType->info.targetDistance; g_game().updateCreatureIcon(static_self_cast()); } } - if (!mType->canSpawn(position)) { + if (!m_monsterType->canSpawn(position)) { g_game().removeCreature(static_self_cast()); } @@ -1243,7 +1248,7 @@ bool Monster::canUseSpell(const Position &pos, const Position &targetPos, const void Monster::onThinkTarget(uint32_t interval) { if (!isSummon()) { - if (mType->info.changeTargetSpeed != 0) { + if (m_monsterType->info.changeTargetSpeed != 0) { bool canChangeTarget = true; if (challengeFocusDuration > 0) { @@ -1255,12 +1260,20 @@ void Monster::onThinkTarget(uint32_t interval) { } } + if (fatalHoldDuration > 0 && runAwayHealth > 0) { + fatalHoldDuration -= interval; + + if (fatalHoldDuration <= 0) { + fatalHoldDuration = 0; + } + } + if (m_targetChangeCooldown > 0) { m_targetChangeCooldown -= interval; if (m_targetChangeCooldown <= 0) { m_targetChangeCooldown = 0; - targetChangeTicks = mType->info.changeTargetSpeed; + targetChangeTicks = m_monsterType->info.changeTargetSpeed; } else { canChangeTarget = false; } @@ -1269,16 +1282,16 @@ void Monster::onThinkTarget(uint32_t interval) { if (canChangeTarget) { targetChangeTicks += interval; - if (targetChangeTicks >= mType->info.changeTargetSpeed) { + if (targetChangeTicks >= m_monsterType->info.changeTargetSpeed) { targetChangeTicks = 0; - m_targetChangeCooldown = mType->info.changeTargetSpeed; + m_targetChangeCooldown = m_monsterType->info.changeTargetSpeed; if (challengeFocusDuration > 0) { challengeFocusDuration = 0; } - if (mType->info.changeTargetChance >= uniform_random(1, 100)) { - if (mType->info.targetDistance <= 1) { + if (m_monsterType->info.changeTargetChance >= uniform_random(1, 100)) { + if (m_monsterType->info.targetDistance <= 1) { searchTarget(TARGETSEARCH_RANDOM); } else { searchTarget(TARGETSEARCH_NEAREST); @@ -1312,14 +1325,14 @@ void Monster::onThinkDefense(uint32_t interval) { } } - if (!isSummon() && m_summons.size() < mType->info.maxSummons && hasFollowPath) { - for (const auto &[summonName, summonChance, summonSpeed, summonCount, summonForce] : mType->info.summons) { + if (!isSummon() && m_summons.size() < m_monsterType->info.maxSummons && hasFollowPath) { + for (const auto &[summonName, summonChance, summonSpeed, summonCount, summonForce] : m_monsterType->info.summons) { if (summonSpeed > defenseTicks) { resetTicks = false; continue; } - if (m_summons.size() >= mType->info.maxSummons) { + if (m_summons.size() >= m_monsterType->info.maxSummons) { continue; } @@ -1363,17 +1376,17 @@ void Monster::onThinkDefense(uint32_t interval) { } void Monster::onThinkYell(uint32_t interval) { - if (mType->info.yellSpeedTicks == 0) { + if (m_monsterType->info.yellSpeedTicks == 0) { return; } yellTicks += interval; - if (yellTicks >= mType->info.yellSpeedTicks) { + if (yellTicks >= m_monsterType->info.yellSpeedTicks) { yellTicks = 0; - if (!mType->info.voiceVector.empty() && (mType->info.yellChance >= static_cast(uniform_random(1, 100)))) { - const uint32_t index = uniform_random(0, mType->info.voiceVector.size() - 1); - const auto &[text, yellText] = mType->info.voiceVector[index]; + if (!m_monsterType->info.voiceVector.empty() && (m_monsterType->info.yellChance >= static_cast(uniform_random(1, 100)))) { + const uint32_t index = uniform_random(0, m_monsterType->info.voiceVector.size() - 1); + const auto &[text, yellText] = m_monsterType->info.voiceVector[index]; if (yellText) { g_game().internalCreatureSay(static_self_cast(), TALKTYPE_MONSTER_YELL, text, false); @@ -1385,17 +1398,17 @@ void Monster::onThinkYell(uint32_t interval) { } void Monster::onThinkSound(uint32_t interval) { - if (mType->info.soundSpeedTicks == 0) { + if (m_monsterType->info.soundSpeedTicks == 0) { return; } soundTicks += interval; - if (soundTicks >= mType->info.soundSpeedTicks) { + if (soundTicks >= m_monsterType->info.soundSpeedTicks) { soundTicks = 0; - if (!mType->info.soundVector.empty() && (mType->info.soundChance >= static_cast(uniform_random(1, 100)))) { - int64_t index = uniform_random(0, static_cast(mType->info.soundVector.size() - 1)); - g_game().sendSingleSoundEffect(static_self_cast()->getPosition(), mType->info.soundVector[index], getMonster()); + if (!m_monsterType->info.soundVector.empty() && (m_monsterType->info.soundChance >= static_cast(uniform_random(1, 100)))) { + int64_t index = uniform_random(0, static_cast(m_monsterType->info.soundVector.size() - 1)); + g_game().sendSingleSoundEffect(static_self_cast()->getPosition(), m_monsterType->info.soundVector[index], getMonster()); } } } @@ -1574,7 +1587,7 @@ void Monster::doFollowCreature(uint32_t &flags, Direction &nextDirection, bool & if (attackedCreature && attackedCreature == followCreature) { if (isFleeing()) { result = getDanceStep(getPosition(), nextDirection, false, false); - } else if (mType->info.staticAttackChance < static_cast(uniform_random(1, 100))) { + } else if (m_monsterType->info.staticAttackChance < static_cast(uniform_random(1, 100))) { result = getDanceStep(getPosition(), nextDirection); } } @@ -2172,7 +2185,7 @@ bool Monster::getIgnoreFieldDamage() const { } uint16_t Monster::getRaceId() const { - return mType->info.raceid; + return m_monsterType->info.raceid; } // Hazard system @@ -2249,6 +2262,8 @@ void Monster::death(const std::shared_ptr &) { if (monsterForgeClassification > ForgeClassifications_t::FORGE_NORMAL_MONSTER) { g_game().removeForgeMonster(getID(), monsterForgeClassification, true); } + const auto &attackedCreature = getAttackedCreature(); + const auto &targetPlayer = attackedCreature ? attackedCreature->getPlayer() : nullptr; setAttackedCreature(nullptr); for (const auto &summon : m_summons) { @@ -2264,11 +2279,26 @@ void Monster::death(const std::shared_ptr &) { clearFriendList(); onIdleStatus(); - if (mType) { - g_game().sendSingleSoundEffect(static_self_cast()->getPosition(), mType->info.deathSound, getMonster()); + setDead(true); + + if (!m_monsterType) { + return; } - setDead(true); + g_game().sendSingleSoundEffect(static_self_cast()->getPosition(), m_monsterType->info.deathSound, getMonster()); + + if (!targetPlayer) { + return; + } + + auto [activeCharm, _] = g_iobestiary().getCharmFromTarget(targetPlayer, m_monsterType); + if (activeCharm == CHARM_CARNAGE) { + const auto &charm = g_iobestiary().getBestiaryCharm(activeCharm); + const auto charmTier = targetPlayer->getCharmTier(activeCharm); + if (charm && charm->chance[charmTier] >= normal_random(1, 10000) / 100.0) { + g_iobestiary().parseCharmCombat(charm, targetPlayer, getMonster()); + } + } } std::shared_ptr Monster::getCorpse(const std::shared_ptr &lastHitCreature, const std::shared_ptr &mostDamageCreature) { @@ -2405,7 +2435,7 @@ void Monster::dropLoot(const std::shared_ptr &corpse, const std::shar } void Monster::setNormalCreatureLight() { - internalLight = mType->info.light; + internalLight = m_monsterType->info.light; } void Monster::drainHealth(const std::shared_ptr &attacker, int32_t damage) { @@ -2421,9 +2451,9 @@ void Monster::drainHealth(const std::shared_ptr &attacker, int32_t dam } void Monster::changeHealth(int32_t healthChange, bool sendHealthChange /* = true*/) { - if (mType && !mType->info.soundVector.empty() && mType->info.soundChance >= static_cast(uniform_random(1, 100))) { - auto index = uniform_random(0, mType->info.soundVector.size() - 1); - g_game().sendSingleSoundEffect(static_self_cast()->getPosition(), mType->info.soundVector[index], getMonster()); + if (m_monsterType && !m_monsterType->info.soundVector.empty() && m_monsterType->info.soundChance >= static_cast(uniform_random(1, 100))) { + auto index = uniform_random(0, m_monsterType->info.soundVector.size() - 1); + g_game().sendSingleSoundEffect(static_self_cast()->getPosition(), m_monsterType->info.soundVector[index], getMonster()); } // In case a player with ignore flag set attacks the monster @@ -2454,11 +2484,11 @@ bool Monster::changeTargetDistance(int32_t distance, uint32_t duration /* = 1200 return false; } - if (mType->info.isRewardBoss) { + if (m_monsterType->info.isRewardBoss) { return false; } - bool shouldUpdate = mType->info.targetDistance > distance ? true : false; + bool shouldUpdate = m_monsterType->info.targetDistance > distance ? true : false; challengeMeleeDuration = duration; targetDistance = distance; @@ -2479,7 +2509,7 @@ std::vector Monster::getIcons() const { } using enum CreatureIconModifications_t; - if (challengeMeleeDuration > 0 && mType->info.targetDistance > targetDistance) { + if (challengeMeleeDuration > 0 && m_monsterType->info.targetDistance > targetDistance) { return { CreatureIcon(TurnedMelee) }; } else if (varBuffs[BUFF_DAMAGERECEIVED] > 100) { return { CreatureIcon(HigherDamageReceived) }; @@ -2490,11 +2520,11 @@ std::vector Monster::getIcons() const { } bool Monster::isImmune(ConditionType_t conditionType) const { - return m_isImmune || mType->info.m_conditionImmunities[static_cast(conditionType)]; + return m_isImmune || m_monsterType->info.m_conditionImmunities[static_cast(conditionType)]; } bool Monster::isImmune(CombatType_t combatType) const { - return m_isImmune || mType->info.m_damageImmunities[combatTypeToIndex(combatType)]; + return m_isImmune || m_monsterType->info.m_damageImmunities[combatTypeToIndex(combatType)]; } void Monster::setImmune(bool immune) { @@ -2506,7 +2536,7 @@ bool Monster::isImmune() const { } float Monster::getAttackMultiplier() const { - float multiplier = mType->getAttackMultiplier(); + float multiplier = m_monsterType->getAttackMultiplier(); if (auto stacks = getForgeStack(); stacks > 0) { multiplier *= (1.35 + (stacks - 1) * 0.1); } @@ -2514,7 +2544,7 @@ float Monster::getAttackMultiplier() const { } float Monster::getDefenseMultiplier() const { - float multiplier = mType->getDefenseMultiplier(); + float multiplier = m_monsterType->getDefenseMultiplier(); if (auto stacks = getForgeStack(); stacks > 0) { multiplier *= (1 + (0.1 * stacks)); } @@ -2595,11 +2625,11 @@ bool Monster::canBeForgeMonster() const { } bool Monster::isForgeCreature() const { - return mType->info.isForgeCreature; + return m_monsterType->info.isForgeCreature; } void Monster::setForgeMonster(bool forge) const { - mType->info.isForgeCreature = forge; + m_monsterType->info.isForgeCreature = forge; } uint16_t Monster::getForgeStack() const { @@ -2628,7 +2658,7 @@ time_t Monster::getTimeToChangeFiendish() const { } std::shared_ptr Monster::getMonsterType() const { - return mType; + return m_monsterType; } void Monster::clearFiendishStatus() { @@ -2636,8 +2666,8 @@ void Monster::clearFiendishStatus() { forgeStack = 0; monsterForgeClassification = ForgeClassifications_t::FORGE_NORMAL_MONSTER; - health = mType->info.health * mType->getHealthMultiplier(); - healthMax = mType->info.healthMax * mType->getHealthMultiplier(); + health = m_monsterType->info.health * m_monsterType->getHealthMultiplier(); + healthMax = m_monsterType->info.healthMax * m_monsterType->getHealthMultiplier(); removeIcon("forge"); g_game().updateCreatureIcon(static_self_cast()); @@ -2645,7 +2675,7 @@ void Monster::clearFiendishStatus() { } bool Monster::canDropLoot() const { - return !mType->info.lootItems.empty(); + return !m_monsterType->info.lootItems.empty(); } std::vector> Monster::getPushItemLocationOptions(const Direction &direction) { @@ -2692,8 +2722,7 @@ bool Monster::checkCanApplyCharm(const std::shared_ptr &player, charmRun uint16_t playerCharmRaceid = player->parseRacebyCharm(charmRune, false, 0); if (playerCharmRaceid != 0) { - const auto &monsterType = g_monsters().getMonsterType(getName()); - if (monsterType && playerCharmRaceid == monsterType->info.raceid) { + if (m_monsterType && playerCharmRaceid == m_monsterType->info.raceid) { const auto &charm = g_iobestiary().getBestiaryCharm(charmRune); if (charm) { return true; diff --git a/src/creatures/monsters/monster.hpp b/src/creatures/monsters/monster.hpp index 6942d992baa..417137b87a9 100644 --- a/src/creatures/monsters/monster.hpp +++ b/src/creatures/monsters/monster.hpp @@ -61,7 +61,7 @@ class Monster final : public Creature { RaceType_t getRace() const override; float getMitigation() const override; int32_t getArmor() const override; - int32_t getDefense() const override; + int32_t getDefense(bool = false) const override; void addDefense(int32_t defense); @@ -154,6 +154,8 @@ class Monster final : public Creature { bool isTarget(const std::shared_ptr &creature); bool isFleeing() const; + void setFatalHoldDuration(int32_t value); + bool getDistanceStep(const Position &targetPos, Direction &direction, bool flee = false); bool isTargetNearby() const; bool isIgnoringFieldDamage() const; @@ -263,7 +265,7 @@ class Monster final : public Creature { std::string m_lowerName; std::string nameDescription; - std::shared_ptr mType; + std::shared_ptr m_monsterType; std::shared_ptr spawnMonster = nullptr; int64_t lastMeleeAttack = 0; @@ -282,6 +284,7 @@ class Monster final : public Creature { int32_t minCombatValue = 0; int32_t maxCombatValue = 0; int32_t m_targetChangeCooldown = 0; + int32_t fatalHoldDuration = 0; int32_t challengeFocusDuration = 0; int32_t stepDuration = 0; int32_t targetDistance = 1; @@ -296,6 +299,7 @@ class Monster final : public Creature { Position masterPos; + bool canFlee = false; bool isWalkingBack = false; bool isIdle = true; bool extraMeleeAttack = false; diff --git a/src/creatures/players/components/player_title.cpp b/src/creatures/players/components/player_title.cpp index 1abb2b3adbd..f33f73ba422 100644 --- a/src/creatures/players/components/player_title.cpp +++ b/src/creatures/players/components/player_title.cpp @@ -210,10 +210,10 @@ bool PlayerTitle::checkHighscore(uint8_t skill) const { switch (static_cast(skill)) { case HighscoreCategories_t::CHARMS: query = fmt::format( - "SELECT `pc`.`player_guid`, `pc`.`charm_points`, `p`.`group_id` FROM `player_charms` pc JOIN `players` p ON `pc`.`player_guid` = `p`.`id` WHERE `p`.`group_id` < {} ORDER BY `pc`.`charm_points` DESC LIMIT 1", + "SELECT `pc`.`player_id`, `pc`.`charm_points`, `p`.`group_id` FROM `player_charms` pc JOIN `players` p ON `pc`.`player_id` = `p`.`id` WHERE `p`.`group_id` < {} ORDER BY `pc`.`charm_points` DESC LIMIT 1", static_cast(GROUP_TYPE_GAMEMASTER) ); - fieldCheck = "player_guid"; + fieldCheck = "player_id"; break; case HighscoreCategories_t::DROME: // todo check if player is in the top 5 for the previous rota of the Tibiadrome. diff --git a/src/creatures/players/components/wheel/player_wheel.cpp b/src/creatures/players/components/wheel/player_wheel.cpp index e61a4aa4831..eb6ea475bc4 100644 --- a/src/creatures/players/components/wheel/player_wheel.cpp +++ b/src/creatures/players/components/wheel/player_wheel.cpp @@ -1859,7 +1859,6 @@ bool PlayerWheel::saveDBPlayerSlotPointsOnLogout() const { uint16_t PlayerWheel::getExtraPoints() const { if (m_player.getLevel() < 51) { - g_logger().error("Character level must be above 50."); return 0; } @@ -3813,15 +3812,15 @@ float PlayerWheel::calculateMitigation() const { float distanceFactor = 1.0f; switch (m_player.fightMode) { case FIGHTMODE_ATTACK: { - fightFactor = 0.67f; + fightFactor = 0.8f; break; } case FIGHTMODE_BALANCED: { - fightFactor = 0.84f; + fightFactor = 1.0f; break; } case FIGHTMODE_DEFENSE: { - fightFactor = 1.0f; + fightFactor = 1.2f; break; } default: diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 7eacacb97c4..fc7f810ee35 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -389,6 +389,147 @@ int32_t Player::getWeaponSkill(const std::shared_ptr &item) const { return attackSkill; } +uint16_t Player::getDistanceAttackSkill(const int32_t attackSkill, const int32_t weaponAttack) const { + // Correct calculation of getWeaponSkill function (getMaxWeaponDamage) for Paladins + const double skillFactor = (attackSkill + 4) / 28.; + return weaponAttack * skillFactor - weaponAttack; +} + +uint16_t Player::getAttackSkill(const std::shared_ptr &item) const { + if (!item) { + return getSkillLevel(SKILL_FIST); + } + + int32_t attackSkill; + + const WeaponType_t &weaponType = item->getWeaponType(); + switch (weaponType) { + case WEAPON_SWORD: { + attackSkill = getSkillLevel(SKILL_SWORD); + break; + } + + case WEAPON_CLUB: { + attackSkill = getSkillLevel(SKILL_CLUB); + break; + } + + case WEAPON_AXE: { + attackSkill = getSkillLevel(SKILL_AXE); + break; + } + + case WEAPON_MISSILE: + case WEAPON_DISTANCE: { + attackSkill = getSkillLevel(SKILL_DISTANCE); + break; + } + + default: { + attackSkill = 0; + break; + } + } + + // Correct calculation of getWeaponSkill function (getMaxWeaponDamage) + const double skillFactor = (attackSkill + 4) / 28.; + const auto weaponAttack = item->getAttack(); + return weaponAttack * skillFactor - weaponAttack; +} + +uint8_t Player::getWeaponSkillId(const std::shared_ptr &item) const { + uint8_t skillId; + const WeaponType_t &weaponType = item->getWeaponType(); + switch (weaponType) { + case WEAPON_SWORD: { + skillId = 8; + break; + } + + case WEAPON_CLUB: { + skillId = 9; + break; + } + + case WEAPON_AXE: { + skillId = 10; + break; + } + + default: { + skillId = 11; + break; + } + } + + return skillId; +} + +uint16_t Player::calculateFlatDamageHealing() const { + uint16_t constA = 0; + uint16_t constB = 0; + double constC = 0; + if (level < 500) { + constA = 0; + constB = 0; + constC = 0.2f; + } else if (level >= 500 && level <= 1100) { + constA = 100; + constB = 500; + constC = 0.1667f; + } else if (level >= 1100 && level <= 1800) { + constA = 200; + constB = 1101; + constC = 0.1429f; + } else if (level >= 1800 && level <= 2600) { + constA = 300; + constB = 1800; + constC = 0.1250f; + } else if (level > 2600) { + constA = 400; + constB = 2600; + constC = 0.111f; + } + + return constA + (level - constB) * constC; +} + +uint16_t Player::attackTotal(uint16_t flatBonus, uint16_t equipment, uint16_t skill) const { + double fightFactor = 0; + switch (fightMode) { + case FIGHTMODE_ATTACK: { + fightFactor = 1.2f * equipment; + break; + } + + case FIGHTMODE_BALANCED: { + fightFactor = 1.0f * equipment; + break; + } + + case FIGHTMODE_DEFENSE: { + fightFactor = 0.6f * equipment; + break; + } + + default: { + fightFactor = 1.0f * equipment; + break; + } + } + + fightFactor = std::floor(fightFactor); + + const double skillFactor = (skill + 4) / 28.; + + return flatBonus + (fightFactor * skillFactor); +} + +uint16_t Player::attackRawTotal(uint16_t flatBonus, uint16_t equipment, uint16_t skill) const { + const double skillFactor = (skill + 4) / 28.; + return flatBonus + (equipment * skillFactor); +} + int32_t Player::getArmor() const { int32_t armor = 0; @@ -435,16 +576,34 @@ float Player::getMitigation() const { return wheel().calculateMitigation(); } -int32_t Player::getDefense() const { +double Player::getCombatTacticsMitigation() const { + double fightFactor = 0.0; + switch (fightMode) { + case FIGHTMODE_ATTACK: { + fightFactor = 0.8f; + break; + } + case FIGHTMODE_BALANCED: { + fightFactor = 1.0f; + break; + } + case FIGHTMODE_DEFENSE: { + fightFactor = 1.2f; + break; + } + default: + break; + } + + return fightFactor; +} + +int32_t Player::getDefense(bool sendToClient /* = false*/) const { int32_t defenseSkill = getSkillLevel(SKILL_FIST); int32_t defenseValue = 7; std::shared_ptr weapon; std::shared_ptr shield; - try { - getShieldAndWeapon(shield, weapon); - } catch (const std::exception &e) { - g_logger().error("{} got exception {}", getName(), e.what()); - } + getShieldAndWeapon(shield, weapon); if (weapon) { defenseValue = weapon->getDefense() + weapon->getExtraDefense(); @@ -452,7 +611,9 @@ int32_t Player::getDefense() const { } if (shield) { - defenseValue = weapon != nullptr ? shield->getDefense() + weapon->getExtraDefense() : shield->getDefense(); + defenseValue = (weapon != nullptr) + ? shield->getDefense() + weapon->getExtraDefense() + : shield->getDefense(); // Wheel of destiny - Combat Mastery if (shield->getDefense() > 0) { defenseValue += wheel().getMajorStatConditional("Combat Mastery", WheelMajor_t::DEFENSE); @@ -465,13 +626,34 @@ int32_t Player::getDefense() const { case FIGHTMODE_ATTACK: case FIGHTMODE_BALANCED: return 1; - case FIGHTMODE_DEFENSE: return 2; } } - return (defenseSkill / 4. + 2.23) * defenseValue * 0.15 * getDefenseFactor() * vocation->defenseMultiplier; + auto defenseScalingFactor = shield ? 0.16f : (weapon && weapon->getDefense() > 0 ? 0.146f : 0.15f); + + return ((defenseSkill / 4.0 + 2.23) * defenseValue * getDefenseFactor(sendToClient) * defenseScalingFactor) * vocation->defenseMultiplier; +} + +uint16_t Player::getDefenseEquipment() const { + uint16_t defenseValue = 6; + std::shared_ptr weapon; + std::shared_ptr shield; + getShieldAndWeapon(shield, weapon); + + if (weapon) { + defenseValue = weapon->getDefense() + weapon->getExtraDefense(); + } + + if (shield) { + defenseValue = weapon != nullptr ? shield->getDefense() + weapon->getExtraDefense() : shield->getDefense(); + if (shield->getDefense() > 0) { + defenseValue += wheel().getMajorStatConditional("Combat Mastery", WheelMajor_t::DEFENSE); + } + } + + return defenseValue; } float Player::getAttackFactor() const { @@ -487,11 +669,19 @@ float Player::getAttackFactor() const { } } -float Player::getDefenseFactor() const { +float Player::getDefenseFactor(bool sendToClient /* = false*/) const { switch (fightMode) { case FIGHTMODE_ATTACK: + if (sendToClient) { + return 0.5f; + } + return (OTSYS_TIME() - lastAttack) < getAttackSpeed() ? 0.5f : 1.0f; case FIGHTMODE_BALANCED: + if (sendToClient) { + return 0.75f; + } + return (OTSYS_TIME() - lastAttack) < getAttackSpeed() ? 0.75f : 1.0f; case FIGHTMODE_DEFENSE: return 1.0f; @@ -500,6 +690,29 @@ float Player::getDefenseFactor() const { } } +std::vector Player::getDamageAccuracy(const ItemType &it) const { + std::vector accuracy = {}; + const auto distanceValue = getSkillLevel(SKILL_DISTANCE); + if (it.ammoType == AMMO_BOLT || it.ammoType == AMMO_ARROW) { + accuracy.push_back(std::min(90, (1.20f * (distanceValue + 1)))); + accuracy.push_back(std::min(90, (3.20f * distanceValue))); + accuracy.push_back(std::min(90, (2.00f * distanceValue))); + accuracy.push_back(std::min(90, (1.55f * distanceValue))); + accuracy.push_back(std::min(90, (1.20f * (distanceValue + 1)))); + accuracy.push_back(std::min(90, distanceValue)); + } else { + accuracy.push_back(std::min(75, distanceValue + 1)); + accuracy.push_back(std::min(75, 2.40f * (distanceValue + 8))); + accuracy.push_back(std::min(75, 1.55f * (distanceValue + 6))); + accuracy.push_back(std::min(75, 1.25f * (distanceValue + 3))); + accuracy.push_back(std::min(75, distanceValue + 1)); + accuracy.push_back(std::min(75, 0.80f * (distanceValue + 3))); + accuracy.push_back(std::min(75, 0.70f * (distanceValue + 2))); + } + + return accuracy; +} + void Player::setLastWalkthroughAttempt(int64_t walkthroughAttempt) { lastWalkthroughAttempt = walkthroughAttempt; } @@ -1146,10 +1359,17 @@ bool Player::canWalkthrough(const std::shared_ptr &creature) { const auto &player = creature->getPlayer(); const auto &monster = creature->getMonster(); const auto &npc = creature->getNpc(); - if (monster) { - if (!monster->isFamiliar()) { - return false; + bool noPvpThroughAtSummon = false; + // Allow players to walk through summons in no pvp worlds + if (g_game().getWorldType() == WORLD_TYPE_NO_PVP) { + const auto &monsterMaster = monster ? monster->getMaster() : nullptr; + const auto &monsterMasterPlayer = monsterMaster ? monsterMaster->getPlayer() : nullptr; + if (monsterMasterPlayer) { + noPvpThroughAtSummon = true; } + } + + if (monster && (monster->isFamiliar() || noPvpThroughAtSummon)) { return true; } @@ -1179,8 +1399,8 @@ bool Player::canWalkthrough(const std::shared_ptr &creature) { return true; } else if (npc) { const auto &tile = npc->getTile(); - const auto &houseTile = std::dynamic_pointer_cast(tile); - return (houseTile != nullptr); + const auto &house = tile ? tile->getHouse() : nullptr; + return (house != nullptr); } return false; @@ -1523,6 +1743,41 @@ void Player::setCharmPoints(uint32_t points) { charmPoints = points; } +uint32_t Player::getMinorCharmEchoes() const { + return minorCharmEchoes; +} + +void Player::setMinorCharmEchoes(uint32_t points) { + minorCharmEchoes = points; +} + +uint32_t Player::getMaxCharmPoints() const { + return maxCharmPoints; +} + +void Player::setMaxCharmPoints(uint32_t points) { + maxCharmPoints = points; +} + +uint32_t Player::getMaxMinorCharmEchoes() const { + return maxMinorCharmEchoes; +} + +void Player::setMaxMinorCharmEchoes(uint32_t points) { + maxMinorCharmEchoes = points; +} + +uint8_t Player::getCharmTier(charmRune_t charmId) const { + if (charmId == CHARM_NONE || charmId > charmsArray.size()) { + return 0; + } + return charmsArray[charmId].tier; +} + +void Player::setCharmTier(charmRune_t charmId, uint8_t newTier) { + charmsArray[charmId].tier = newTier; +} + bool Player::hasCharmExpansion() const { return charmExpansion; } @@ -1548,22 +1803,41 @@ int32_t Player::getUnlockedRunesBit() const { } void Player::setImmuneCleanse(ConditionType_t conditiontype) { - cleanseCondition.first = conditiontype; - cleanseCondition.second = OTSYS_TIME() + 10000; + if (conditiontype == CONDITION_FEARED) { + setImmuneFear(11000); + } else { + for (auto &[type, time] : cleanseConditions) { + if (type != conditiontype) { + continue; + } + + time = OTSYS_TIME() + 11000; + return; + } + cleanseConditions.emplace_back(conditiontype, OTSYS_TIME() + 11000); + } } bool Player::isImmuneCleanse(ConditionType_t conditiontype) const { const uint64_t timenow = OTSYS_TIME(); - if ((cleanseCondition.first == conditiontype) - && (timenow <= cleanseCondition.second)) { + if (conditiontype == CONDITION_FEARED) { + return isImmuneFear(); + } + + for (const auto &[type, time] : cleanseConditions) { + if (type != conditiontype || timenow > time) { + continue; + } + return true; } + return false; } -void Player::setImmuneFear() { +void Player::setImmuneFear(uint32_t immuneTime /* = 10000 */) { m_fearCondition.first = CONDITION_FEARED; - m_fearCondition.second = OTSYS_TIME() + 10000; + m_fearCondition.second = OTSYS_TIME() + immuneTime; } bool Player::isImmuneFear() const { @@ -1571,140 +1845,38 @@ bool Player::isImmuneFear() const { return (m_fearCondition.first == CONDITION_FEARED) && (timenow <= m_fearCondition.second); } -uint16_t Player::parseRacebyCharm(charmRune_t charmId, bool set, uint16_t newRaceid) { +uint16_t Player::parseRacebyCharm(charmRune_t charmId, bool set /*= false*/, uint16_t newRaceid /*= 0*/) { uint16_t raceid = 0; switch (charmId) { case CHARM_WOUND: - if (set) { - charmRuneWound = newRaceid; - } else { - raceid = charmRuneWound; - } - break; case CHARM_ENFLAME: - if (set) { - charmRuneEnflame = newRaceid; - } else { - raceid = charmRuneEnflame; - } - break; case CHARM_POISON: - if (set) { - charmRunePoison = newRaceid; - } else { - raceid = charmRunePoison; - } - break; case CHARM_FREEZE: - if (set) { - charmRuneFreeze = newRaceid; - } else { - raceid = charmRuneFreeze; - } - break; case CHARM_ZAP: - if (set) { - charmRuneZap = newRaceid; - } else { - raceid = charmRuneZap; - } - break; case CHARM_CURSE: - if (set) { - charmRuneCurse = newRaceid; - } else { - raceid = charmRuneCurse; - } - break; case CHARM_CRIPPLE: - if (set) { - charmRuneCripple = newRaceid; - } else { - raceid = charmRuneCripple; - } - break; case CHARM_PARRY: - if (set) { - charmRuneParry = newRaceid; - } else { - raceid = charmRuneParry; - } - break; case CHARM_DODGE: - if (set) { - charmRuneDodge = newRaceid; - } else { - raceid = charmRuneDodge; - } - break; case CHARM_ADRENALINE: - if (set) { - charmRuneAdrenaline = newRaceid; - } else { - raceid = charmRuneAdrenaline; - } - break; case CHARM_NUMB: - if (set) { - charmRuneNumb = newRaceid; - } else { - raceid = charmRuneNumb; - } - break; case CHARM_CLEANSE: - if (set) { - charmRuneCleanse = newRaceid; - } else { - raceid = charmRuneCleanse; - } - break; case CHARM_BLESS: - if (set) { - charmRuneBless = newRaceid; - } else { - raceid = charmRuneBless; - } - break; case CHARM_SCAVENGE: - if (set) { - charmRuneScavenge = newRaceid; - } else { - raceid = charmRuneScavenge; - } - break; case CHARM_GUT: - if (set) { - charmRuneGut = newRaceid; - } else { - raceid = charmRuneGut; - } - break; case CHARM_LOW: - if (set) { - charmRuneLowBlow = newRaceid; - } else { - raceid = charmRuneLowBlow; - } - break; case CHARM_DIVINE: - if (set) { - charmRuneDivine = newRaceid; - } else { - raceid = charmRuneDivine; - } - break; case CHARM_VAMP: - if (set) { - charmRuneVamp = newRaceid; - } else { - raceid = charmRuneVamp; - } - break; case CHARM_VOID: + case CHARM_SAVAGE: + case CHARM_FATAL: + case CHARM_VOIDINVERSION: + case CHARM_CARNAGE: + case CHARM_OVERPOWER: + case CHARM_OVERFLUX: if (set) { - charmRuneVoid = newRaceid; + charmsArray[charmId].raceId = newRaceid; } else { - raceid = charmRuneVoid; + raceid = charmsArray[charmId].raceId; } break; default: @@ -1768,17 +1940,17 @@ std::shared_ptr Player::getDepotLocker(uint32_t depotId) { return it->second; } - // We need to make room for supply stash on 12+ protocol versions and remove it for 10x. - const bool createSupplyStash = !client->oldProtocol; + // We need to make room for stash on 12+ protocol versions and remove it for 10x. + const bool createStash = !client->oldProtocol; - auto depotLocker = std::make_shared(ITEM_LOCKER, createSupplyStash ? 4 : 3); + auto depotLocker = std::make_shared(ITEM_LOCKER, createStash ? 4 : 3); depotLocker->setDepotId(depotId); const auto &marketItem = Item::CreateItem(ITEM_MARKET); depotLocker->internalAddThing(marketItem); depotLocker->internalAddThing(inbox); - if (createSupplyStash) { - const auto &supplyStashPtr = Item::CreateItem(ITEM_SUPPLY_STASH); - depotLocker->internalAddThing(supplyStashPtr); + if (createStash) { + const auto &stashPtr = Item::CreateItem(ITEM_STASH); + depotLocker->internalAddThing(stashPtr); } const auto &depotChest = Item::CreateItemAsContainer(ITEM_DEPOT, static_cast(g_configManager().getNumber(DEPOT_BOXES))); for (uint32_t i = g_configManager().getNumber(DEPOT_BOXES); i > 0; i--) { @@ -2367,7 +2539,7 @@ void Player::onApplyImbuement(const Imbuement* imbuement, const std::shared_ptr< const ItemType &itemType = Item::items[key]; - withdrawItemMessage << "Using " << mathItemCount << "x " << itemType.name << " from your supply stash. "; + withdrawItemMessage << "Using " << mathItemCount << "x " << itemType.name << " from your stash. "; withdrawItem(itemType.id, mathItemCount); sendTextMessage(MESSAGE_STATUS, withdrawItemMessage.str()); } @@ -3597,10 +3769,14 @@ void Player::death(const std::shared_ptr &lastHitCreature) { double deathLossPercent = getLostPercent() * (unfairFightReduction / 100.); // Charm bless bestiary - if (lastHitCreature && lastHitCreature->getMonster() && charmRuneBless != 0) { + const auto charmBless = charmsArray[CHARM_BLESS]; + const auto charmBlessRaceId = charmBless.raceId; + const auto &charm = g_iobestiary().getBestiaryCharm(CHARM_BLESS); + if (charm && lastHitCreature && lastHitCreature->getMonster() && charmBlessRaceId != 0) { const auto &mType = g_monsters().getMonsterType(lastHitCreature->getName()); - if (mType && mType->info.raceid == charmRuneBless) { - deathLossPercent = (deathLossPercent * 90) / 100; + if (mType && mType->info.raceid == charmBlessRaceId) { + const auto percentReduction = charm->chance[charmBless.tier] / 100; + deathLossPercent -= deathLossPercent * percentReduction; } } @@ -4619,6 +4795,7 @@ uint32_t Player::getItemTypeCount(uint16_t itemId, int32_t subType /*= -1*/) con } void Player::stashContainer(const StashContainerList &itemDict) { + const auto &selfPlayer = static_self_cast(); StashItemList stashItemDict; // ItemID - Count for (const auto &[item, itemCount] : itemDict) { if (!item) { @@ -4628,7 +4805,7 @@ void Player::stashContainer(const StashContainerList &itemDict) { stashItemDict[item->getID()] = itemCount; } - for (const auto &[itemId, itemCount] : stashItems) { + for (const auto &[itemId, itemCount] : getStashItems()) { if (!stashItemDict[itemId]) { stashItemDict[itemId] = itemCount; } else { @@ -4636,33 +4813,80 @@ void Player::stashContainer(const StashContainerList &itemDict) { } } - if (getStashSize(stashItemDict) > g_configManager().getNumber(STASH_ITEMS)) { - sendCancelMessage("You don't have capacity in the Supply Stash to stow all this item->"); - return; - } - - uint32_t totalStowed = 0; - std::ostringstream retString; uint16_t refreshDepotSearchOnItem = 0; - for (const auto &[item, itemCount] : itemDict) { + + auto processItem = [&](const std::shared_ptr &item, uint16_t itemCount) { if (!item) { - continue; + return false; + } + + if (!item->isItemStorable()) { + return false; + } + + for (int i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) { + const auto &inventoryItem = inventory[i]; + if (!inventoryItem) { + continue; + } + + if (inventoryItem == item) { + g_moveEvents().onPlayerDeEquip(selfPlayer, item, static_cast(i)); + } } + const uint16_t iteratorCID = item->getID(); - if (g_game().internalRemoveItem(item, itemCount) == RETURNVALUE_NOERROR) { + bool success = false; + + if (const auto &player = item->getHoldingPlayer()) { + if (player == selfPlayer) { + success = (removeItem(item, itemCount) == RETURNVALUE_NOERROR); + } + } else { + if (const auto &parent = item->getParent()) { + const auto &parentItem = parent->getItem(); + if (parentItem && parentItem->getID() == ITEM_BROWSEFIELD) { + const auto &parentTile = parent->getTile(); + if (parentTile) { + parentTile->removeThing(item, itemCount); + } + } else { + parent->removeThing(item, itemCount); + } + success = true; + } + } + + if (success) { addItemOnStash(iteratorCID, itemCount); - totalStowed += itemCount; if (isDepotSearchOpenOnItem(iteratorCID)) { refreshDepotSearchOnItem = iteratorCID; } } + return success; + }; + + uint32_t totalStowed = 0; + for (const auto &[item, itemCount] : itemDict) { + if (!item) { + continue; + } + if (processItem(item, itemCount)) { + totalStowed += itemCount; + } } + updateState(); + if (totalStowed == 0) { sendCancelMessage("Sorry, not possible."); return; } + sendStats(); + sendInventoryIds(); + + std::ostringstream retString; retString << "Stowed " << totalStowed << " object" << (totalStowed > 1 ? "s." : "."); if (moved) { retString << " Moved " << movedItems << " object" << (movedItems > 1 ? "s." : "."); @@ -4862,7 +5086,7 @@ uint32_t Player::getFreeCapacity() const { } } -ItemsTierCountList Player::getInventoryItemsId(bool ignoreStoreInbox /* false */) const { +ItemsTierCountList Player::getInventoryItemsId(bool ignoreStoreInbox /*= false */) const { ItemsTierCountList itemMap; for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; i++) { const auto &item = inventory[i]; @@ -4989,6 +5213,7 @@ void Player::parseAttackDealtHazardSystem(CombatDamage &damage, const std::share if (chance <= stage) { damage.primary.value = 0; damage.secondary.value = 0; + damage.hazardDodge = true; return; } } @@ -5525,6 +5750,9 @@ void Player::setChaseMode(bool mode) { void Player::setFightMode(FightMode_t mode) { fightMode = mode; + + sendStats(); + sendSkills(); } void Player::setSecureMode(bool mode) { @@ -5610,6 +5838,26 @@ void Player::onAddCondition(ConditionType_t type) { sendIcons(); } +void Player::onCleanseCondition(ConditionType_t type) const { + static const std::unordered_map conditionMessages = { + { CONDITION_POISON, "poisoned" }, + { CONDITION_FIRE, "burning" }, + { CONDITION_ENERGY, "electrified" }, + { CONDITION_FREEZING, "freezing" }, + { CONDITION_CURSED, "cursed" }, + { CONDITION_DAZZLED, "dazzled" }, + { CONDITION_BLEEDING, "bleeding" }, + { CONDITION_PARALYZE, "paralyzed" }, + { CONDITION_ROOTED, "rooted" }, + { CONDITION_FEARED, "feared" } + }; + + auto it = conditionMessages.find(type); + if (it != conditionMessages.end()) { + sendTextMessage(MESSAGE_PARTY, fmt::format("You are no longer {}. (cleanse charm)", it->second)); + } +} + void Player::onAddCombatCondition(ConditionType_t type) { if (IsConditionSuppressible(type)) { updateLastConditionTime(type); @@ -6047,7 +6295,7 @@ void Player::genReservedStorageRange() { } } -void Player::setSpecialMenuAvailable(bool supplyStashBool, bool marketMenuBool, bool depotSearchBool) { +void Player::setSpecialMenuAvailable(bool stashBool, bool marketMenuBool, bool depotSearchBool) { // Closing depot search when player have special container disabled and it's still open. if (isDepotSearchOpen() && !depotSearchBool && depotSearch) { depotSearchOnItem = { 0, 0 }; @@ -6057,7 +6305,7 @@ void Player::setSpecialMenuAvailable(bool supplyStashBool, bool marketMenuBool, // Menu option 'stow, stow container ...' // Menu option 'show in market' // Menu option to open depot search - supplyStash = supplyStashBool; + m_isStashMenuAvailable = stashBool; marketMenu = marketMenuBool; depotSearch = depotSearchBool; if (client) { @@ -7508,12 +7756,6 @@ void Player::sendCyclopediaCharacterGeneralStats() const { } } -void Player::sendCyclopediaCharacterCombatStats() const { - if (client) { - client->sendCyclopediaCharacterCombatStats(); - } -} - void Player::sendCyclopediaCharacterRecentDeaths(uint16_t page, uint16_t pages, const std::vector &entries) const { if (client) { client->sendCyclopediaCharacterRecentDeaths(page, pages, entries); @@ -7532,9 +7774,9 @@ void Player::sendCyclopediaCharacterAchievements(uint16_t secretsUnlocked, const } } -void Player::sendCyclopediaCharacterItemSummary(const ItemsTierCountList &inventoryItems, const ItemsTierCountList &storeInboxItems, const StashItemList &supplyStashItems, const ItemsTierCountList &depotBoxItems, const ItemsTierCountList &inboxItems) const { +void Player::sendCyclopediaCharacterItemSummary(const ItemsTierCountList &inventoryItems, const ItemsTierCountList &storeInboxItems, const StashItemList &stashItems, const ItemsTierCountList &depotBoxItems, const ItemsTierCountList &inboxItems) const { if (client) { - client->sendCyclopediaCharacterItemSummary(inventoryItems, storeInboxItems, supplyStashItems, depotBoxItems, inboxItems); + client->sendCyclopediaCharacterItemSummary(inventoryItems, storeInboxItems, stashItems, depotBoxItems, inboxItems); } } @@ -7568,6 +7810,24 @@ void Player::sendCyclopediaCharacterTitles() const { } } +void Player::sendCyclopediaCharacterOffenceStats() const { + if (client) { + client->sendCyclopediaCharacterOffenceStats(); + } +} + +void Player::sendCyclopediaCharacterDefenceStats() const { + if (client) { + client->sendCyclopediaCharacterDefenceStats(); + } +} + +void Player::sendCyclopediaCharacterMiscStats() const { + if (client) { + client->sendCyclopediaCharacterMiscStats(); + } +} + void Player::sendHighscoresNoData() const { if (client) { client->sendHighscoresNoData(); @@ -7637,8 +7897,8 @@ void Player::onThink(uint32_t interval) { addMessageBuffer(); } - // Transcendance (avatar trigger) - triggerTranscendance(); + // Transcendence (avatar trigger) + triggerTranscendence(); // Momentum (cooldown resets) triggerMomentum(); const auto &playerTile = getTile(); @@ -8197,25 +8457,275 @@ bool Player::isGuildMate(const std::shared_ptr &player) const { return guild == player->guild; } -bool Player::addItemFromStash(uint16_t itemId, uint32_t itemCount) { - const uint32_t stackCount = 100u; +ReturnValue Player::addItemFromStash(uint16_t itemId, uint32_t itemCount) { + const auto &itemType = Item::items[itemId]; + if (!itemType.id) { + return RETURNVALUE_NOTPOSSIBLE; + } + + double availableCapacity = getFreeCapacity(); + double itemWeight = itemType.weight; + + auto maxRetrievableByWeight = static_cast(availableCapacity / itemWeight); + uint32_t retrievableCount = std::min(maxRetrievableByWeight, itemCount); + + if (retrievableCount == 0) { + sendMessageDialog("Not enough capacity. You could not retrieve any items."); + return RETURNVALUE_NOTENOUGHROOM; + } + + const auto &thisPtr = static_self_cast(); + std::vector> containersCache; + size_t cacheIndex = 0; + bool fallbackConsumed = false; + uint32_t freeStackSpace = 0; + std::vector> stackableItemsCache; + const auto &mainBackpack = getBackpack(); + auto objectCategory = g_game().getObjectCategory(itemType); + const auto &obtainContainer = g_game().findManagedContainer(thisPtr, fallbackConsumed, objectCategory, false); + if (obtainContainer) { + if (obtainContainer->capacity() > obtainContainer->size()) { + containersCache.emplace_back(obtainContainer); + } + + for (const auto &item : obtainContainer->getItems(true)) { + const auto &subContainer = item->getContainer(); + if (subContainer && subContainer->capacity() > subContainer->size()) { + containersCache.emplace_back(subContainer); + } + + if (item && item->getID() == itemId && item->isStackable()) { + uint32_t availableSpace = item->getStackSize() - item->getItemCount(); + if (availableSpace > 0) { + stackableItemsCache.emplace_back(item); + freeStackSpace += availableSpace; + } + } + } + } else { + containersCache = getAllContainers(); + } + + uint32_t maxBySlots = 0; + if (itemType.stackable) { + uint32_t freeSlots = getFreeBackpackSlots(); + maxBySlots = freeStackSpace + (freeSlots * itemType.stackSize); + } else { + maxBySlots = getFreeBackpackSlots(); + } + + uint32_t finalRetrievable = std::min({ maxBySlots, retrievableCount }); + + // Check if there is enough space to add the items + bool canAddItems = false; + if (itemType.stackable) { + // For stackable items, check space in existing stacks and free slots + uint32_t totalSpace = freeStackSpace; + for (const auto &container : containersCache) { + uint32_t freeContainerSlots = container->capacity() - container->size(); + totalSpace += freeContainerSlots * itemType.stackSize; + } + canAddItems = totalSpace >= finalRetrievable; + } else { + // For non-stackable items, check free slots + uint32_t totalFreeSlots = 0; + for (const auto &container : containersCache) { + totalFreeSlots += container->capacity() - container->size(); + } + canAddItems = totalFreeSlots >= finalRetrievable; + } + + if (!canAddItems) { + sendMessageDialog("Not enough space. You could not retrieve any items."); + return RETURNVALUE_NOTENOUGHROOM; + } + + // Remove the item from the stash if have enough space + if (!withdrawItem(itemId, finalRetrievable)) { + g_logger().warn("Failed to remove itemId: {} from stash, to player: {}, requested: {}", itemId, getName(), finalRetrievable); + return RETURNVALUE_NOTPOSSIBLE; + } + + uint32_t addedItemCount = 0; + uint32_t remainingToRetrieve = finalRetrievable; + + if (itemType.stackable) { + while (remainingToRetrieve > 0 && availableCapacity >= itemWeight) { + uint32_t addValue = std::min(itemType.stackSize, remainingToRetrieve); + remainingToRetrieve -= addValue; + + bool itemAdded = false; + + for (auto it = stackableItemsCache.begin(); it != stackableItemsCache.end();) { + auto &stackableItem = *it; + if (addValue == 0) { + break; + } + + uint32_t spaceInStack = stackableItem->getStackSize() - stackableItem->getItemCount(); + uint32_t stackableCount = std::min(spaceInStack, addValue); + + if (stackableCount > 0) { + stackableItem->getParent()->updateThing( + stackableItem, stackableItem->getID(), + stackableItem->getItemCount() + stackableCount + ); + addValue -= stackableCount; + addedItemCount += stackableCount; + itemAdded = true; + availableCapacity -= stackableCount * itemWeight; + + if (stackableItem->getItemCount() >= stackableItem->getStackSize()) { + it = stackableItemsCache.erase(it); + continue; + } + } + ++it; + } + + while (addValue > 0 && cacheIndex < containersCache.size()) { + const auto &targetContainer = containersCache[cacheIndex]; + if (!targetContainer) { + ++cacheIndex; + continue; + } + + if (targetContainer->capacity() > targetContainer->size()) { + uint32_t toCreate = std::min(addValue, itemType.stackSize); + if (availableCapacity < toCreate * itemWeight) { + break; + } + + const auto &newItem = Item::createItemBatch(itemId, toCreate); + if (!newItem) { + g_logger().warn("[addItemFromStash] Failed to create new stackable itemId: {} for player {}", itemId, getName()); + break; + } + + targetContainer->addThing(newItem); + onSendContainer(targetContainer); + addedItemCount += toCreate; + availableCapacity -= toCreate * itemWeight; + addValue -= toCreate; + itemAdded = true; + } + + if (targetContainer->capacity() <= targetContainer->size()) { + ++cacheIndex; + } + } + + if (!itemAdded && addValue > 0) { + g_logger().warn("No more space available for itemId: {}, remaining: {}", itemId, addValue); + break; + } + } + } + + if (!itemType.stackable) { + while (finalRetrievable > 0 && cacheIndex < containersCache.size()) { + auto &targetContainer = containersCache[cacheIndex]; + if (!targetContainer) { + ++cacheIndex; + continue; + } + + if (targetContainer->capacity() > targetContainer->size()) { + const auto &newItem = Item::createItemBatch(itemId, 1); + if (!newItem) { + g_logger().warn("[addItemFromStash] Failed to create new itemId: {} for player {}", itemId, getName()); + break; + } - while (itemCount > 0) { - const auto addValue = itemCount > stackCount ? stackCount : itemCount; - itemCount -= addValue; - const auto &newItem = Item::CreateItem(itemId, addValue); + targetContainer->addThing(newItem); + onSendContainer(targetContainer); + addedItemCount += 1; + finalRetrievable -= 1; + } - if (!g_game().tryRetrieveStashItems(static_self_cast(), newItem)) { - g_game().internalPlayerAddItem(static_self_cast(), newItem, true); + if (targetContainer->capacity() <= targetContainer->size()) { + ++cacheIndex; + } } } - // This check is necessary because we need to block it when we retrieve an item from depot search. + std::string itemName = itemType.name + (addedItemCount > 1 ? "s" : ""); + sendTextMessage(MESSAGE_STATUS, fmt::format("Retrieved {}x {}.", addedItemCount, itemName)); + if (!isDepotSearchOpenOnItem(itemId)) { sendOpenStash(); } - return true; + if (addedItemCount > 0) { + updateState(); + } + + availableCapacity = getFreeCapacity(); + bool limitedByCapacity = (addedItemCount < itemCount) && (availableCapacity < itemWeight); + bool limitedBySlots = (addedItemCount < retrievableCount) && !limitedByCapacity; + + if (limitedByCapacity) { + sendMessageDialog("Not enough capacity. You could not retrieve all items."); + } else if (limitedBySlots) { + sendMessageDialog("Not enough space. You could not retrieve all items."); + } + + return addedItemCount > 0 ? RETURNVALUE_NOERROR : RETURNVALUE_NOTENOUGHROOM; +} + +std::vector> Player::getAllContainers(bool onlyFromMainBackpack) const { + std::vector> containersCache; + + // Add main backpack to the cache + if (onlyFromMainBackpack) { + const auto &mainBp = getBackpack(); + if (mainBp) { + containersCache.emplace_back(mainBp); + } + } + + // Gather all containers from player inventory + for (uint32_t slot = CONST_SLOT_FIRST; slot <= CONST_SLOT_AMMO; ++slot) { + // Skip slots check if onlyFromMainBackpack is true + if (onlyFromMainBackpack) { + break; + } + + const auto &slotItem = getInventoryItem(static_cast(slot)); + if (!slotItem) { + continue; + } + + if (auto container = slotItem->getContainer()) { + containersCache.emplace_back(container); + } + } + + // Add all nested containers to the cache + for (size_t i = 0; i < containersCache.size(); i++) { + const auto &container = containersCache[i]; + if (!container) { + continue; + } + + for (const auto &item : container->getItemList()) { + if (auto subContainer = item->getContainer()) { + containersCache.emplace_back(subContainer); + } + } + } + + return containersCache; +} + +std::shared_ptr Player::getBackpack() const { + const auto &item = getInventoryItem(CONST_SLOT_BACKPACK); + if (!item) { + return nullptr; + } + + const auto &container = item->getContainer(); + return container; } ReturnValue Player::addItemBatchToPaginedContainer( @@ -8306,72 +8816,112 @@ ReturnValue Player::removeItem(const std::shared_ptr &item, uint32_t count return RETURNVALUE_NOERROR; } -void sendStowItems(const std::shared_ptr &item, const std::shared_ptr &stowItem, StashContainerList &itemDict) { +uint32_t sendStowItems(const std::shared_ptr &item, const std::shared_ptr &stowItem, StashContainerList &itemDict, uint32_t totalItemsToStow, uint32_t maxItemsToStow) { + uint32_t itemsAdded = 0; + if (stowItem->getID() == item->getID()) { - itemDict.emplace_back(stowItem, stowItem->getItemCount()); + uint32_t stowableToAdd = std::min(stowItem->getItemAmount(), maxItemsToStow - totalItemsToStow); + itemDict.emplace_back(stowItem, stowableToAdd); + itemsAdded += stowableToAdd; } if (const auto &container = stowItem->getContainer()) { - std::ranges::copy_if(container->getStowableItems(), std::back_inserter(itemDict), [&item](const auto &stowable_it) { - return stowable_it.first->getID() == item->getID(); - }); + for (const auto &[stowableItem, stowableCount] : container->getStowableItems()) { + if (totalItemsToStow + itemsAdded >= maxItemsToStow) { + break; + } + + if (stowableItem->getID() != item->getID()) { + continue; + } + + uint32_t stowableToAdd = std::min(stowableCount, maxItemsToStow - (totalItemsToStow + itemsAdded)); + itemDict.emplace_back(stowableItem, stowableToAdd); + itemsAdded += stowableToAdd; + } } + + return itemsAdded; } void Player::stowItem(const std::shared_ptr &item, uint32_t count, bool allItems) { - if (!item || (!item->isItemStorable() && item->getID() != ITEM_GOLD_POUCH)) { + if (!item || !item->isItemStorable() && item->getID() != ITEM_GOLD_POUCH) { sendCancelMessage("This item cannot be stowed here."); return; } + if (!item->isItemStorable() && item->getID() != ITEM_GOLD_POUCH) { + if (!item->getParent()) { + sendCancelMessage("This item cannot be stowed here."); + return; + } + if (!item->getParent()->getItem()) { + sendCancelMessage("This item cannot be stowed here."); + return; + } + if (item->getParent()->getItem()->getID() != ITEM_GOLD_POUCH) { + sendCancelMessage("This item cannot be stowed here."); + return; + } + } + StashContainerList itemDict; + uint32_t totalItemsToStow = 0; + uint32_t maxItemsToStow = g_configManager().getNumber(STASH_MANAGE_AMOUNT); + if (allItems) { + if (item->getContainer()) { + sendCancelMessage("You cannot stow containers."); + return; + } + if (!item->isInsideDepot(true)) { - // Stow "all items" from player backpack - if (const auto &backpack = getInventoryItem(CONST_SLOT_BACKPACK)) { - sendStowItems(item, backpack, itemDict); + // Stow items from player backpack + if (const auto &backpack = getBackpack()) { + totalItemsToStow += sendStowItems(item, backpack, itemDict, totalItemsToStow, maxItemsToStow); } - // Stow "all items" from loot pouch - const auto &itemParent = item->getParent(); - const auto &lootPouch = itemParent->getItem(); - if (itemParent && lootPouch && lootPouch->getID() == ITEM_GOLD_POUCH) { - sendStowItems(item, lootPouch, itemDict); + // Stow items from loot pouch + if (const auto &itemParent = item->getParent()) { + if (const auto &lootPouch = itemParent->getItem(); lootPouch && lootPouch->getID() == ITEM_GOLD_POUCH) { + totalItemsToStow += sendStowItems(item, lootPouch, itemDict, totalItemsToStow, maxItemsToStow); + } } } - // Stow locker items + // Stow items from depot locker const auto &depotLocker = getDepotLocker(getLastDepotId()); const auto &[itemVector, itemMap] = requestLockerItems(depotLocker); for (const auto &lockerItem : itemVector) { - if (lockerItem == nullptr) { - break; - } - - if (item->isInsideDepot(true)) { - sendStowItems(item, lockerItem, itemDict); + if (lockerItem && item->isInsideDepot(true)) { + totalItemsToStow += sendStowItems(item, lockerItem, itemDict, totalItemsToStow, maxItemsToStow); } } - } else if (item->getContainer()) { - itemDict = item->getContainer()->getStowableItems(); - for (const std::shared_ptr &containerItem : item->getContainer()->getItems(true)) { - uint32_t depotChest = g_configManager().getNumber(DEPOTCHEST); - bool validDepot = depotChest > 0 && depotChest < 21; - if (g_configManager().getBoolean(STASH_MOVING) && containerItem && !containerItem->isStackable() && validDepot) { - g_game().internalMoveItem(containerItem->getParent(), getDepotChest(depotChest, true), INDEX_WHEREEVER, containerItem, containerItem->getItemCount(), nullptr); - movedItems++; - moved = true; + } else if (const auto &container = item->getContainer()) { + for (const auto &[stowableItem, stowableCount] : container->getStowableItems()) { + if (totalItemsToStow >= maxItemsToStow) { + break; } + + uint32_t stowableToAdd = std::min(stowableCount, maxItemsToStow - totalItemsToStow); + itemDict.emplace_back(stowableItem, stowableToAdd); + totalItemsToStow += stowableToAdd; } } else { - itemDict.emplace_back(item, count); + uint32_t stowableToAdd = std::min(count, maxItemsToStow - totalItemsToStow); + itemDict.emplace_back(item, stowableToAdd); + totalItemsToStow += stowableToAdd; } if (itemDict.empty()) { - sendCancelMessage("There is no stowable items on this container."); + sendCancelMessage("There are no stowable items in this container."); return; } + if (totalItemsToStow >= maxItemsToStow) { + sendTextMessage(MESSAGE_EVENT_ADVANCE, fmt::format("You have reached the maximum stow limit of {} items. Try to stow again.", maxItemsToStow)); + } + stashContainer(itemDict); } @@ -8714,22 +9264,23 @@ void Player::requestDepotItems() { for (const auto &[itemId, itemCount] : getStashItems()) { auto itemMap_it = itemMap.find(itemId); - // Stackable items not have upgrade classification - if (Item::items[itemId].upgradeClassification > 0) { - g_logger().error("{} - Player {} have wrong item with id {} on stash with upgrade classification", __FUNCTION__, getName(), itemId); + // Stash items must have market flag + if (Item::items[itemId].wareId <= 0) { + g_logger().error("{} - Player {} have wrong item with id {} on stash without market flag", __FUNCTION__, getName(), itemId); continue; } + uint8_t itemTier = Item::items[itemId].upgradeClassification > 0 ? 1 : 0; if (itemMap_it == itemMap.end()) { std::map itemTierMap; - itemTierMap[0] = itemCount; + itemTierMap[itemTier] = itemCount; itemMap[itemId] = itemTierMap; count++; - } else if (auto itemTier_it = itemMap[itemId].find(0); itemTier_it == itemMap[itemId].end()) { - itemMap[itemId][0] = itemCount; + } else if (auto itemTier_it = itemMap[itemId].find(itemTier); itemTier_it == itemMap[itemId].end()) { + itemMap[itemId][itemTier] = itemCount; count++; } else { - itemMap[itemId][0] += itemCount; + itemMap[itemId][itemTier] += itemCount; } } @@ -8745,7 +9296,7 @@ void Player::requestDepotSearchItem(uint16_t itemId, uint8_t tier) { uint32_t stashCount = 0; if (const ItemType &iType = Item::items[itemId]; - iType.stackable && iType.wareId > 0) { + iType.wareId > 0 && tier == 0) { stashCount = getStashItemCount(itemId); } @@ -9034,9 +9585,20 @@ bool Player::isDead() const { } void Player::triggerMomentum() { - double_t chance = 0; - if (const auto &item = getInventoryItem(CONST_SLOT_HEAD)) { - chance += item->getMomentumChance(); + const auto &item = getInventoryItem(CONST_SLOT_HEAD); + if (!item) { + return; + } + + if (!item->getTier()) { + return; + } + + double_t chance = item->getMomentumChance(); + const auto &playerBoots = getInventoryItem(CONST_SLOT_FEET); + if (playerBoots && playerBoots->getTier()) { + double_t amplificationChange = playerBoots->getAmplificationChance() / 100; + chance *= 1 + amplificationChange; } chance += m_wheelPlayer.getBonusData().momentum; @@ -9087,7 +9649,7 @@ void Player::clearCooldowns() { } } -void Player::triggerTranscendance() { +void Player::triggerTranscendence() { if (wheel().getOnThinkTimer(WheelOnThink_t::AVATAR_FORGE) > OTSYS_TIME()) { return; } @@ -9097,10 +9659,20 @@ void Player::triggerTranscendance() { return; } - const double_t chance = item->getTranscendenceChance(); + if (!item->getTier()) { + return; + } + + double_t chance = item->getTranscendenceChance(); + const auto &playerBoots = getInventoryItem(CONST_SLOT_FEET); + if (playerBoots && playerBoots->getTier()) { + double_t amplificationChange = playerBoots->getAmplificationChance() / 100; + chance *= 1 + amplificationChange; + } + const double_t randomChance = uniform_random(0, 10000) / 100.; if (getZoneType() != ZONE_PROTECTION && checkLastAggressiveActionWithin(2000) && ((OTSYS_TIME() / 1000) % 2) == 0 && chance > 0 && randomChance < chance) { - int64_t duration = g_configManager().getNumber(TRANSCENDANCE_AVATAR_DURATION); + int64_t duration = g_configManager().getNumber(TRANSCENDENCE_AVATAR_DURATION); const auto &outfitCondition = Condition::createCondition(CONDITIONID_COMBAT, CONDITION_OUTFIT, duration, 0)->static_self_cast(); Outfit_t outfit; outfit.lookType = getVocation()->getAvatarLookType(); @@ -9113,9 +9685,9 @@ void Player::triggerTranscendance() { sendStats(); sendBasicData(); - sendTextMessage(MESSAGE_ATTENTION, "Transcendance was triggered."); + sendTextMessage(MESSAGE_ATTENTION, "Transcendence was triggered."); - // Send player data after transcendance timer expire + // Send player data after transcendence timer expire const auto &task = createPlayerTask( std::max(SCHEDULER_MINTICKS, duration), [playerId = getID()] { @@ -10649,13 +11221,21 @@ bool Player::hasPermittedConditionInPZ() const { } uint16_t Player::getDodgeChance() const { - uint16_t chance = 0; - if (const auto &playerArmor = getInventoryItem(CONST_SLOT_ARMOR); - playerArmor != nullptr && playerArmor->getTier()) { - chance += static_cast(playerArmor->getDodgeChance() * 100); + const auto &playerArmor = getInventoryItem(CONST_SLOT_ARMOR); + const auto wheelDodge = m_wheelPlayer.getStat(WheelStat_t::DODGE); + if (!playerArmor || playerArmor->getTier() == 0) { + return wheelDodge; } - chance += m_wheelPlayer.getStat(WheelStat_t::DODGE); + auto chance = static_cast(playerArmor->getDodgeChance() * 100); + const auto &playerBoots = getInventoryItem(CONST_SLOT_FEET); + if (playerBoots && playerBoots->getTier() > 0) { + double amplificationChance = playerBoots->getAmplificationChance() / 100.0; + double_t amplValue = chance * amplificationChance; + chance += static_cast(amplValue); + } + + chance += wheelDodge; return chance; } @@ -10674,9 +11254,15 @@ void Player::sendFYIBox(const std::string &message) const { } } -void Player::BestiarysendCharms() const { +void Player::parseBestiarySendRaces() const { if (client) { - client->BestiarysendCharms(); + client->parseBestiarySendRaces(); + } +} + +void Player::sendBestiaryCharms() const { + if (client) { + client->sendBestiaryCharms(); } } @@ -10876,3 +11462,37 @@ bool Player::isFirstOnStack() const { const auto &bottomPlayer = bottomCreature ? bottomCreature->getPlayer() : nullptr; return !bottomPlayer || this == bottomPlayer.get(); } + +void Player::resetOldCharms() { + const auto &bestiaryList = g_game().getBestiaryList(); + const auto &charmList = g_game().getCharmList(); + uint16_t unlockedCharms = 0; + for (const auto &charm : charmList) { + if (g_iobestiary().hasCharmUnlockedRuneBit(charm, getUnlockedRunesBit())) { + ++unlockedCharms; + } + } + + uint16_t totalRefund = 0; + for (const auto &[raceId, monsterName] : bestiaryList) { + const auto &mtype = g_monsters().getMonsterType(monsterName); + if (mtype && getBestiaryKillCount(raceId) >= mtype->info.bestiaryToUnlock) { + totalRefund += mtype->info.bestiaryCharmsPoints; + unlockedCharms++; + } + } + + bool unlockedAllCharms = unlockedCharms == 19; + if (unlockedAllCharms) { + totalRefund += 17400; + g_logger().info("Player: {}, has all charms unlocked. Bonus points: 17400", getName()); + } + + uint32_t myCharms = getCharmPoints(); + totalRefund += myCharms; + + setMaxCharmPoints(totalRefund); + setCharmPoints(totalRefund); + + g_logger().info("Player: {}, recalculated charm points based on unlocked bestiary: {}", getName(), totalRefund); +} diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 536755fc687..a3f68b0d2e5 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -91,6 +91,11 @@ using UsersMap = std::map>; using InvitedMap = std::map>; using HouseMap = std::map>; +struct CharmInfo { + uint16_t raceId = 0; + uint8_t tier = 0; +}; + struct ForgeHistory { ForgeAction_t actionType = ForgeAction_t::FUSION; uint8_t tier = 0; @@ -215,7 +220,8 @@ class Player final : public Creature, public Cylinder, public Bankable { void sendFYIBox(const std::string &message) const; - void BestiarysendCharms() const; + void parseBestiarySendRaces() const; + void sendBestiaryCharms() const; void addBestiaryKillCount(uint16_t raceid, uint32_t amount); uint32_t getBestiaryKillCount(uint16_t raceid) const; @@ -405,7 +411,7 @@ class Player final : public Creature, public Cylinder, public Bankable { bool isInMarket() const { return inMarket; } - void setSpecialMenuAvailable(bool supplyStashBool, bool marketMenuBool, bool depotSearchBool); + void setSpecialMenuAvailable(bool stashBool, bool marketMenuBool, bool depotSearchBool); bool isDepotSearchOpen() const { return depotSearchOnItem.first != 0; } @@ -418,8 +424,8 @@ class Player final : public Creature, public Cylinder, public Bankable { bool isDepotSearchAvailable() const { return depotSearch; } - bool isSupplyStashMenuAvailable() const { - return supplyStash; + bool isStashMenuAvailable() const { + return m_isStashMenuAvailable; } bool isMarketMenuAvailable() const { return marketMenu; @@ -636,7 +642,7 @@ class Player final : public Creature, public Cylinder, public Bankable { static bool lastHitIsPlayer(const std::shared_ptr &lastHitCreature); // stash functions - bool addItemFromStash(uint16_t itemId, uint32_t itemCount); + ReturnValue addItemFromStash(uint16_t itemId, uint32_t itemCount); void stowItem(const std::shared_ptr &item, uint32_t count, bool allItems); ReturnValue addItemBatchToPaginedContainer( @@ -647,6 +653,8 @@ class Player final : public Creature, public Cylinder, public Bankable { uint32_t flags = 0, uint8_t tier = 0 ); + std::vector> getAllContainers(bool onlyFromMainBackpack = true) const; + std::shared_ptr getBackpack() const; ReturnValue removeItem(const std::shared_ptr &item, uint32_t count = 0); @@ -691,6 +699,15 @@ class Player final : public Creature, public Cylinder, public Bankable { WeaponType_t getWeaponType() const; int32_t getWeaponSkill(const std::shared_ptr &item) const; void getShieldAndWeapon(std::shared_ptr &shield, std::shared_ptr &weapon) const; + uint16_t calculateFlatDamageHealing() const; + uint16_t attackTotal(uint16_t flatBonus, uint16_t equipment, uint16_t skill) const; + uint16_t attackRawTotal(uint16_t flatBonus, uint16_t equipment, uint16_t skill) const; + uint16_t getDistanceAttackSkill(const int32_t attackSkill, const int32_t weaponAttack) const; + uint16_t getAttackSkill(const std::shared_ptr &item) const; + uint8_t getWeaponSkillId(const std::shared_ptr &item) const; + uint16_t getDefenseEquipment() const; + double getCombatTacticsMitigation() const; + std::vector getDamageAccuracy(const ItemType &it) const; void drainHealth(const std::shared_ptr &attacker, int32_t damage) override; void drainMana(const std::shared_ptr &attacker, int32_t manaLoss) override; @@ -698,9 +715,9 @@ class Player final : public Creature, public Cylinder, public Bankable { void addSkillAdvance(skills_t skill, uint64_t count); int32_t getArmor() const override; - int32_t getDefense() const override; + int32_t getDefense(bool sendToClient = false) const override; float getAttackFactor() const override; - float getDefenseFactor() const override; + float getDefenseFactor(bool sendToClient) const override; float getMitigation() const override; void addInFightTicks(bool pzlock = false); @@ -708,6 +725,7 @@ class Player final : public Creature, public Cylinder, public Bankable { uint64_t getGainedExperience(const std::shared_ptr &attacker) const override; // combat event functions + void onCleanseCondition(ConditionType_t type) const; void onAddCondition(ConditionType_t type) override; void onAddCombatCondition(ConditionType_t type) override; void onEndCondition(ConditionType_t type) override; @@ -932,16 +950,18 @@ class Player final : public Creature, public Cylinder, public Bankable { void sendCyclopediaCharacterNoData(CyclopediaCharacterInfoType_t characterInfoType, uint8_t errorCode) const; void sendCyclopediaCharacterBaseInformation() const; void sendCyclopediaCharacterGeneralStats() const; - void sendCyclopediaCharacterCombatStats() const; void sendCyclopediaCharacterRecentDeaths(uint16_t page, uint16_t pages, const std::vector &entries) const; void sendCyclopediaCharacterRecentPvPKills(uint16_t page, uint16_t pages, const std::vector &entries) const; void sendCyclopediaCharacterAchievements(uint16_t secretsUnlocked, const std::vector> &achievementsUnlocked) const; - void sendCyclopediaCharacterItemSummary(const ItemsTierCountList &inventoryItems, const ItemsTierCountList &storeInboxItems, const StashItemList &supplyStashItems, const ItemsTierCountList &depotBoxItems, const ItemsTierCountList &inboxItems) const; + void sendCyclopediaCharacterItemSummary(const ItemsTierCountList &inventoryItems, const ItemsTierCountList &storeInboxItems, const StashItemList &stashItems, const ItemsTierCountList &depotBoxItems, const ItemsTierCountList &inboxItems) const; void sendCyclopediaCharacterOutfitsMounts() const; void sendCyclopediaCharacterStoreSummary() const; void sendCyclopediaCharacterInspection() const; void sendCyclopediaCharacterBadges() const; void sendCyclopediaCharacterTitles() const; + void sendCyclopediaCharacterOffenceStats() const; + void sendCyclopediaCharacterDefenceStats() const; + void sendCyclopediaCharacterMiscStats() const; void sendHighscoresNoData() const; void sendHighscores(const std::vector &characters, uint8_t categoryId, uint32_t vocationId, uint16_t page, uint16_t pages, uint32_t updateTimer) const; void addAsyncOngoingTask(uint64_t flags); @@ -1065,6 +1085,14 @@ class Player final : public Creature, public Cylinder, public Bankable { void setItemCustomPrice(uint16_t itemId, uint64_t price); uint32_t getCharmPoints() const; void setCharmPoints(uint32_t points); + uint32_t getMinorCharmEchoes() const; + void setMinorCharmEchoes(uint32_t points); + uint32_t getMaxCharmPoints() const; + void setMaxCharmPoints(uint32_t points); + uint32_t getMaxMinorCharmEchoes() const; + void setMaxMinorCharmEchoes(uint32_t points); + uint8_t getCharmTier(charmRune_t charmId) const; + void setCharmTier(charmRune_t charmId, uint8_t newTier); bool hasCharmExpansion() const; void setCharmExpansion(bool onOff); void setUsedRunesBit(int32_t bit); @@ -1073,9 +1101,9 @@ class Player final : public Creature, public Cylinder, public Bankable { int32_t getUnlockedRunesBit() const; void setImmuneCleanse(ConditionType_t conditiontype); bool isImmuneCleanse(ConditionType_t conditiontype) const; - void setImmuneFear(); + void setImmuneFear(uint32_t immuneTime = 10000); bool isImmuneFear() const; - uint16_t parseRacebyCharm(charmRune_t charmId, bool set, uint16_t newRaceid); + uint16_t parseRacebyCharm(charmRune_t charmId, bool set = false, uint16_t newRaceid = 0); uint64_t getItemCustomPrice(uint16_t itemId, bool buyPrice = false) const; uint16_t getFreeBackpackSlots() const; @@ -1323,6 +1351,7 @@ class Player final : public Creature, public Cylinder, public Bankable { void sendPlayerTyping(const std::shared_ptr &creature, uint8_t typing) const; bool isFirstOnStack() const; + void resetOldCharms(); private: friend class PlayerLock; @@ -1551,29 +1580,16 @@ class Player final : public Creature, public Cylinder, public Bankable { // Bestiary bool charmExpansion = false; - uint16_t charmRuneWound = 0; - uint16_t charmRuneEnflame = 0; - uint16_t charmRunePoison = 0; - uint16_t charmRuneFreeze = 0; - uint16_t charmRuneZap = 0; - uint16_t charmRuneCurse = 0; - uint16_t charmRuneCripple = 0; - uint16_t charmRuneParry = 0; - uint16_t charmRuneDodge = 0; - uint16_t charmRuneAdrenaline = 0; - uint16_t charmRuneNumb = 0; - uint16_t charmRuneCleanse = 0; - uint16_t charmRuneBless = 0; - uint16_t charmRuneScavenge = 0; - uint16_t charmRuneGut = 0; - uint16_t charmRuneLowBlow = 0; - uint16_t charmRuneDivine = 0; - uint16_t charmRuneVamp = 0; - uint16_t charmRuneVoid = 0; + + std::array() + 1> charmsArray = {}; uint32_t charmPoints = 0; + uint32_t minorCharmEchoes = 0; + uint32_t maxCharmPoints = 0; + uint32_t maxMinorCharmEchoes = 0; int32_t UsedRunesBit = 0; int32_t UnlockedRunesBit = 0; - std::pair cleanseCondition = { CONDITION_NONE, 0 }; + + std::vector> cleanseConditions; std::pair m_fearCondition = { CONDITION_NONE, 0 }; @@ -1604,7 +1620,7 @@ class Player final : public Creature, public Cylinder, public Bankable { bool logged = false; bool scheduledSaleUpdate = false; bool inEventMovePush = false; - bool supplyStash = false; // Menu option 'stow, stow container ...' + bool m_isStashMenuAvailable = false; // Menu option 'stow, stow container ...' bool marketMenu = false; // Menu option 'show in market' bool exerciseTraining = false; bool moved = false; @@ -1656,7 +1672,7 @@ class Player final : public Creature, public Cylinder, public Bankable { void triggerMomentum(); void clearCooldowns(); - void triggerTranscendance(); + void triggerTranscendence(); friend class Game; friend class SaveManager; diff --git a/src/game/game.cpp b/src/game/game.cpp index 582542e16ba..0411b20871d 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -1908,7 +1908,7 @@ void Game::playerMoveItem(const std::shared_ptr &player, const Position } bool Game::isTryingToStow(const Position &toPos, const std::shared_ptr &toCylinder) const { - return toCylinder->getContainer() && toCylinder->getItem()->getID() == ITEM_LOCKER && toPos.getZ() == ITEM_SUPPLY_STASH_INDEX; + return toCylinder->getContainer() && toCylinder->getItem()->getID() == ITEM_LOCKER && toPos.getZ() == ITEM_STASH_INDEX; } ReturnValue Game::checkMoveItemToCylinder(const std::shared_ptr &player, const std::shared_ptr &fromCylinder, const std::shared_ptr &toCylinder, const std::shared_ptr &item, Position toPos) { @@ -3193,6 +3193,16 @@ ReturnValue Game::internalCollectManagedItems(const std::shared_ptr &pla } } + if (!player->quickLootListItemIds.empty()) { + uint16_t itemId = item->getID(); + bool isInList = std::ranges::find(player->quickLootListItemIds, itemId) != player->quickLootListItemIds.end(); + if (player->quickLootFilter == QuickLootFilter_t::QUICKLOOTFILTER_ACCEPTEDLOOT && !isInList) { + return RETURNVALUE_NOTPOSSIBLE; + } else if (player->quickLootFilter == QuickLootFilter_t::QUICKLOOTFILTER_SKIPPEDLOOT && isInList) { + return RETURNVALUE_NOTPOSSIBLE; + } + } + bool fallbackConsumed = false; std::shared_ptr lootContainer = findManagedContainer(player, fallbackConsumed, category, isLootContainer); if (!lootContainer) { @@ -4849,56 +4859,23 @@ void Game::playerStashWithdraw(uint32_t playerId, uint16_t itemId, uint32_t coun return; } - uint16_t freeSlots = player->getFreeBackpackSlots(); - auto stashContainer = player->getManagedContainer(getObjectCategory(it), false); - if (stashContainer && !(player->quickLootFallbackToMainContainer)) { - freeSlots = stashContainer->getFreeSlots(); - } - - if (freeSlots == 0) { - player->sendCancelMessage(RETURNVALUE_NOTENOUGHROOM); - return; - } - if (player->getFreeCapacity() < 100) { player->sendCancelMessage(RETURNVALUE_NOTENOUGHCAPACITY); return; } - int32_t NDSlots = ((freeSlots) - (count < it.stackSize ? 1 : (count / it.stackSize))); - uint32_t SlotsWith = count; - uint32_t noSlotsWith = 0; - - if (NDSlots <= 0) { - SlotsWith = (freeSlots * it.stackSize); - noSlotsWith = (count - SlotsWith); - } - - uint32_t capWith = count; - uint32_t noCapWith = 0; - if (player->getFreeCapacity() < (count * it.weight)) { - capWith = (player->getFreeCapacity() / it.weight); - noCapWith = (count - capWith); + auto maxWithdrawLimit = static_cast(g_configManager().getNumber(STASH_MANAGE_AMOUNT)); + if (count > maxWithdrawLimit) { + std::stringstream limitMessage; + limitMessage << "You can only withdraw up to " << maxWithdrawLimit << " items at a time from the stash."; + player->sendTextMessage(MESSAGE_EVENT_ADVANCE, limitMessage.str()); + count = maxWithdrawLimit; } - std::stringstream ss; - uint32_t WithdrawCount = (SlotsWith > capWith ? capWith : SlotsWith); - uint32_t NoWithdrawCount = (noSlotsWith < noCapWith ? noCapWith : noSlotsWith); - const char* NoWithdrawMsg = (noSlotsWith < noCapWith ? "capacity" : "slots"); - - if (WithdrawCount != count) { - ss << "Retrieved " << WithdrawCount << "x " << it.name << ".\n"; - ss << NoWithdrawCount << "x are impossible to retrieve due to insufficient inventory " << NoWithdrawMsg << "."; - } else { - ss << "Retrieved " << WithdrawCount << "x " << it.name << '.'; - } - - player->sendTextMessage(MESSAGE_STATUS, ss.str()); - - if (player->withdrawItem(itemId, WithdrawCount)) { - player->addItemFromStash(it.id, WithdrawCount); - } else { - player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + auto ret = player->addItemFromStash(itemId, count); + if (ret != RETURNVALUE_NOERROR) { + g_logger().warn("[{}] failed to retrieve item: {}, to player: {}, from the stash", __FUNCTION__, itemId, player->getName()); + player->sendCancelMessage(ret); } // Refresh depot search window if necessary @@ -6771,7 +6748,8 @@ bool Game::combatBlockHit(CombatDamage &damage, const std::shared_ptr return true; } - if (target->getPlayer() && target->isInGhostMode()) { + const auto &targetPlayer = target->getPlayer(); + if (targetPlayer && targetPlayer->isInGhostMode()) { return true; } @@ -6780,9 +6758,9 @@ bool Game::combatBlockHit(CombatDamage &damage, const std::shared_ptr } // Skill dodge (ruse) - if (std::shared_ptr targetPlayer = target->getPlayer()) { + if (targetPlayer) { auto chance = targetPlayer->getDodgeChance(); - if (chance > 0 && uniform_random(0, 10000) < chance) { + if (chance > 0 && uniform_random(0, 10000) < chance || damage.hazardDodge) { InternalGame::sendBlockEffect(BLOCK_DODGE, damage.primary.type, target->getPosition(), attacker); targetPlayer->sendTextMessage(MESSAGE_ATTENTION, "You dodged an attack."); return true; @@ -6801,8 +6779,6 @@ bool Game::combatBlockHit(CombatDamage &damage, const std::shared_ptr CombatParams damageReflectedParams; BlockType_t primaryBlockType, secondaryBlockType; - std::shared_ptr targetPlayer = target->getPlayer(); - if (damage.primary.type != COMBAT_NONE) { damage.primary.value = -damage.primary.value; // Damage healing primary @@ -6830,15 +6806,16 @@ bool Game::combatBlockHit(CombatDamage &damage, const std::shared_ptr InternalGame::sendBlockEffect(primaryBlockType, damage.primary.type, target->getPosition(), attacker); // Damage reflection primary if (!damage.extension && attacker) { - std::shared_ptr attackerMonster = attacker->getMonster(); + const auto &attackerMonster = attacker->getMonster(); if (attackerMonster && targetPlayer && damage.primary.type != COMBAT_HEALING) { // Charm rune (target as player) const auto &mType = attackerMonster->getMonsterType(); if (mType) { - charmRune_t activeCharm = g_iobestiary().getCharmFromTarget(targetPlayer, mType); + auto [activeCharm, _] = g_iobestiary().getCharmFromTarget(targetPlayer, mType); if (activeCharm == CHARM_PARRY) { - const auto charm = g_iobestiary().getBestiaryCharm(activeCharm); - if (charm && charm->type == CHARM_DEFENSIVE && (charm->chance > normal_random(0, 100))) { + const auto &charm = g_iobestiary().getBestiaryCharm(activeCharm); + const auto charmTier = targetPlayer->getCharmTier(activeCharm); + if (charm && charm->type == CHARM_DEFENSIVE && (charm->chance[charmTier] >= normal_random(1, 10000) / 100.0)) { g_iobestiary().parseCharmCombat(charm, targetPlayer, attacker, (damage.primary.value + damage.secondary.value)); } } @@ -7426,7 +7403,7 @@ bool Game::combatChangeHealth(const std::shared_ptr &attacker, const s } else if (attackerPlayer && targetMonster) { handleHazardSystemAttack(damage, attackerPlayer, targetMonster, true); - if (damage.primary.value == 0 && damage.secondary.value == 0) { + if (damage.primary.value == 0 && damage.secondary.value == 0 || damage.hazardDodge) { notifySpectators(spectators.data(), targetPos, attackerPlayer, targetMonster); return true; } @@ -7440,11 +7417,23 @@ bool Game::combatChangeHealth(const std::shared_ptr &attacker, const s if (!damage.extension && attackerMonster && targetPlayer) { // Charm rune (target as player) - if (charmRune_t activeCharm = g_iobestiary().getCharmFromTarget(targetPlayer, g_monsters().getMonsterTypeByRaceId(attackerMonster->getRaceId())); - activeCharm != CHARM_NONE && activeCharm != CHARM_CLEANSE) { - if (const auto charm = g_iobestiary().getBestiaryCharm(activeCharm); - charm->type == CHARM_DEFENSIVE && charm->chance > normal_random(0, 100) && g_iobestiary().parseCharmCombat(charm, targetPlayer, attacker, (damage.primary.value + damage.secondary.value))) { - return false; // Dodge charm + auto [major, minor] = g_iobestiary().getCharmFromTarget(targetPlayer, attackerMonster->getMonsterType()); + if (minor != CHARM_NONE && minor != CHARM_CLEANSE) { + const auto &charm = g_iobestiary().getBestiaryCharm(minor); + const auto charmTier = targetPlayer->getCharmTier(minor); + if (charm && charm->type == CHARM_DEFENSIVE && charm->chance[charmTier] >= normal_random(1, 10000) / 100.0) { + g_iobestiary().parseCharmCombat(charm, targetPlayer, attacker, (damage.primary.value + damage.secondary.value)); + } + } + + if (major != CHARM_NONE) { + const auto &charm = g_iobestiary().getBestiaryCharm(major); + const auto charmTier = targetPlayer->getCharmTier(major); + if (charm && charm->type == CHARM_DEFENSIVE && charm->chance[charmTier] >= normal_random(1, 10000) / 100.0) { + g_iobestiary().parseCharmCombat(charm, targetPlayer, attacker, (damage.primary.value + damage.secondary.value)); + if (charm->id == CHARM_DODGE) { + return true; + } } } } @@ -7705,7 +7694,9 @@ void Game::sendMessages( } if (tmpPlayer == attackerPlayer && attackerPlayer != targetPlayer) { - buildMessageAsAttacker(target, damage, message, ss, damageString, attackerPlayer); + const auto &boots = tmpPlayer->getInventoryItem(CONST_SLOT_FEET); + bool amplifiedFatal = boots ? boots->getTier() > 0 : false; + buildMessageAsAttacker(target, damage, message, ss, damageString, amplifiedFatal, attackerPlayer); } else if (tmpPlayer == targetPlayer) { buildMessageAsTarget(attacker, damage, attackerPlayer, targetPlayer, message, ss, damageString); } else { @@ -7780,10 +7771,26 @@ void Game::buildMessageAsTarget( void Game::buildMessageAsAttacker( const std::shared_ptr &target, const CombatDamage &damage, TextMessage &message, - std::stringstream &ss, const std::string &damageString, const std::shared_ptr &attackerPlayer + std::stringstream &ss, const std::string &damageString, bool amplified, const std::shared_ptr &attackerPlayer ) const { ss.str({}); ss << ucfirst(target->getNameDescription()) << " loses " << damageString << " due to your " << (damage.critical ? "critical " : " ") << "attack."; + + if (damage.critical && target->getMonster() && attackerPlayer) { + const auto &targetMonster = target->getMonster(); + static const std::pair charms[] = { + { CHARM_LOW, " (low blow charm)" }, + { CHARM_SAVAGE, " (savage blow charm)" } + }; + + for (const auto &[charmType, charmText] : charms) { + if (targetMonster->checkCanApplyCharm(attackerPlayer, charmType)) { + ss << charmText; + break; + } + } + } + if (damage.extension) { ss << " " << damage.exString; } @@ -7796,7 +7803,7 @@ void Game::buildMessageAsAttacker( } if (damage.fatal) { - ss << " (Onslaught)"; + ss << (amplified ? " (Amplified Onslaught)" : " (Onslaught)"); } message.type = MESSAGE_DAMAGE_DEALT; message.text = ss.str(); @@ -7828,12 +7835,18 @@ void Game::applyCharmRune( if (!targetMonster || !attackerPlayer) { return; } - if (charmRune_t activeCharm = g_iobestiary().getCharmFromTarget(attackerPlayer, g_monsters().getMonsterTypeByRaceId(targetMonster->getRaceId())); - activeCharm != CHARM_NONE) { - const auto charm = g_iobestiary().getBestiaryCharm(activeCharm); - int8_t chance = charm->id == CHARM_CRIPPLE ? charm->chance : charm->chance + attackerPlayer->getCharmChanceModifier(); - g_logger().debug("charm chance: {}, base: {}, bonus: {}", chance, charm->chance, attackerPlayer->getCharmChanceModifier()); - if (charm->type == CHARM_OFFENSIVE && (chance >= normal_random(0, 100))) { + + auto [major, minor] = g_iobestiary().getCharmFromTarget(attackerPlayer, targetMonster->getMonsterType()); + for (auto charmType : { major, minor }) { + if (charmType == CHARM_NONE) { + continue; + } + + const auto &charm = g_iobestiary().getBestiaryCharm(charmType); + const auto charmTier = attackerPlayer->getCharmTier(charmType); + int8_t chance = charm->chance[charmTier] + (charm->id == CHARM_CRIPPLE ? 0 : attackerPlayer->getCharmChanceModifier()); + + if (charm->type == CHARM_OFFENSIVE && (chance >= normal_random(1, 10000) / 100.0)) { g_iobestiary().parseCharmCombat(charm, attackerPlayer, target, realDamage); } } @@ -7853,14 +7866,12 @@ void Game::applyManaLeech( return; } // Void charm rune - if (targetMonster) { - if (uint16_t playerCharmRaceidVoid = attackerPlayer->parseRacebyCharm(CHARM_VOID, false, 0); - playerCharmRaceidVoid != 0 && playerCharmRaceidVoid == targetMonster->getRaceId()) { - if (const auto charm = g_iobestiary().getBestiaryCharm(CHARM_VOID)) { - manaSkill += charm->percent; - } + if (targetMonster && attackerPlayer->parseRacebyCharm(CHARM_VOID) == targetMonster->getRaceId()) { + if (const auto &charm = g_iobestiary().getBestiaryCharm(CHARM_VOID)) { + manaSkill += charm->chance[attackerPlayer->getCharmTier(CHARM_VOID)] * 100; } } + CombatParams tmpParams; CombatDamage tmpDamage; @@ -7884,14 +7895,12 @@ void Game::applyLifeLeech( if (normal_random(0, 100) >= lifeChance) { return; } - if (targetMonster) { - if (uint16_t playerCharmRaceidVamp = attackerPlayer->parseRacebyCharm(CHARM_VAMP, false, 0); - playerCharmRaceidVamp != 0 && playerCharmRaceidVamp == targetMonster->getRaceId()) { - if (const auto lifec = g_iobestiary().getBestiaryCharm(CHARM_VAMP)) { - lifeSkill += lifec->percent; - } + if (targetMonster && attackerPlayer->parseRacebyCharm(CHARM_VAMP) == targetMonster->getRaceId()) { + if (const auto &charm = g_iobestiary().getBestiaryCharm(CHARM_VAMP)) { + lifeSkill += charm->chance[attackerPlayer->getCharmTier(CHARM_VAMP)] * 100; } } + CombatParams tmpParams; CombatDamage tmpDamage; @@ -7919,8 +7928,28 @@ bool Game::combatChangeMana(const std::shared_ptr &attacker, const std } else { attackerPlayer = nullptr; } + } - auto targetPlayer = target->getPlayer(); + const auto &targetPlayer = target ? target->getPlayer() : nullptr; + const auto &attackerMonster = attacker ? attacker->getMonster() : nullptr; + const auto &attackerPlayer = attacker ? attacker->getPlayer() : nullptr; + if (targetPlayer && attackerMonster) { + uint16_t playerCharmRaceid = targetPlayer->parseRacebyCharm(CHARM_VOIDINVERSION); + if (playerCharmRaceid != 0) { + const auto &mType = g_monsters().getMonsterType(attackerMonster->getName()); + if (mType && playerCharmRaceid == mType->info.raceid) { + const auto &charm = g_iobestiary().getBestiaryCharm(CHARM_VOIDINVERSION); + const auto charmTier = targetPlayer->getCharmTier(CHARM_VOIDINVERSION); + if (charm && (charm->chance[charmTier] > normal_random(0, 100)) && manaChange < 0) { + damage.primary.value = damage.primary.type == COMBAT_MANADRAIN ? -damage.primary.value : damage.primary.value; + damage.secondary.value = damage.secondary.type == COMBAT_MANADRAIN ? -damage.secondary.value : damage.secondary.value; + manaChange = damage.primary.value + damage.secondary.value; + } + } + } + } + + if (manaChange > 0) { if (attackerPlayer && targetPlayer && attackerPlayer->getSkull() == SKULL_BLACK && attackerPlayer->getSkullClient(targetPlayer) == SKULL_NONE) { return false; } @@ -7996,14 +8025,6 @@ bool Game::combatChangeMana(const std::shared_ptr &attacker, const std return false; } - std::shared_ptr attackerPlayer; - if (attacker) { - attackerPlayer = attacker->getPlayer(); - } else { - attackerPlayer = nullptr; - } - - auto targetPlayer = target->getPlayer(); if (attackerPlayer && targetPlayer && attackerPlayer->getSkull() == SKULL_BLACK && attackerPlayer->getSkullClient(targetPlayer) == SKULL_NONE) { return false; } @@ -8030,19 +8051,31 @@ bool Game::combatChangeMana(const std::shared_ptr &attacker, const std } } - if (targetPlayer && attacker && attacker->getMonster()) { - // Charm rune (target as player) - const auto mType = g_monsters().getMonsterType(attacker->getName()); - if (mType) { - charmRune_t activeCharm = g_iobestiary().getCharmFromTarget(targetPlayer, mType); - if (activeCharm != CHARM_NONE && activeCharm != CHARM_CLEANSE) { - const auto charm = g_iobestiary().getBestiaryCharm(activeCharm); - if (charm && charm->type == CHARM_DEFENSIVE && (charm->chance > normal_random(0, 100))) { - if (g_iobestiary().parseCharmCombat(charm, targetPlayer, attacker, manaChange)) { - sendDoubleSoundEffect(targetPlayer->getPosition(), charm->soundCastEffect, charm->soundImpactEffect, targetPlayer); - return false; // Dodge charm - } - } + std::shared_ptr mType = nullptr; + if (attackerMonster) { + mType = g_monsters().getMonsterType(attackerMonster->getName()); + } + if (targetPlayer && attacker && mType) { + auto [major, minor] = g_iobestiary().getCharmFromTarget(targetPlayer, mType); + for (auto charmType : { major, minor }) { + if (charmType == CHARM_NONE || charmType == CHARM_CLEANSE) { + continue; + } + + const auto &charm = g_iobestiary().getBestiaryCharm(charmType); + if (!charm || charm->type != CHARM_DEFENSIVE) { + continue; + } + + const auto charmTier = targetPlayer->getCharmTier(charmType); + if (charm->chance[charmTier] < normal_random(1, 10000) / 100.0) { + continue; + } + + g_iobestiary().parseCharmCombat(charm, targetPlayer, attacker, manaChange); + + if (charm->id == CHARM_DODGE) { + return false; // Dodge charm } } } @@ -8635,8 +8668,8 @@ void Game::playerFriendSystemAction(const std::shared_ptr &player, uint8 } void Game::playerCyclopediaCharacterInfo(const std::shared_ptr &player, uint32_t characterID, CyclopediaCharacterInfoType_t characterInfoType, uint16_t entriesPerPage, uint16_t page) { - uint32_t playerGUID = player->getGUID(); - if (characterID != playerGUID) { + uint32_t playerID = player->getID(); + if (playerID != characterID) { // For now allow viewing only our character since we don't have tournaments supported player->sendCyclopediaCharacterNoData(characterInfoType, 2); return; @@ -8649,9 +8682,6 @@ void Game::playerCyclopediaCharacterInfo(const std::shared_ptr &player, case CYCLOPEDIA_CHARACTERINFO_GENERALSTATS: player->sendCyclopediaCharacterGeneralStats(); break; - case CYCLOPEDIA_CHARACTERINFO_COMBATSTATS: - player->sendCyclopediaCharacterCombatStats(); - break; case CYCLOPEDIA_CHARACTERINFO_RECENTDEATHS: player->cyclopedia().loadDeathHistory(page, entriesPerPage); break; @@ -8664,11 +8694,11 @@ void Game::playerCyclopediaCharacterInfo(const std::shared_ptr &player, case CYCLOPEDIA_CHARACTERINFO_ITEMSUMMARY: { const ItemsTierCountList &inventoryItems = player->getInventoryItemsId(true); const ItemsTierCountList &storeInboxItems = player->getStoreInboxItemsId(); - const StashItemList &supplyStashItems = player->getStashItems(); + const StashItemList &stashItems = player->getStashItems(); const ItemsTierCountList &depotBoxItems = player->getDepotChestItemsId(); const ItemsTierCountList &inboxItems = player->getDepotInboxItemsId(); - player->sendCyclopediaCharacterItemSummary(inventoryItems, storeInboxItems, supplyStashItems, depotBoxItems, inboxItems); + player->sendCyclopediaCharacterItemSummary(inventoryItems, storeInboxItems, stashItems, depotBoxItems, inboxItems); break; } case CYCLOPEDIA_CHARACTERINFO_OUTFITSMOUNTS: @@ -8686,6 +8716,18 @@ void Game::playerCyclopediaCharacterInfo(const std::shared_ptr &player, case CYCLOPEDIA_CHARACTERINFO_TITLES: player->sendCyclopediaCharacterTitles(); break; + case CYCLOPEDIA_CHARACTERINFO_WHEEL: + playerOpenWheel(playerID, characterID); + break; + case CYCLOPEDIA_CHARACTERINFO_OFFENCESTATS: + player->sendCyclopediaCharacterOffenceStats(); + break; + case CYCLOPEDIA_CHARACTERINFO_DEFENCESTATS: + player->sendCyclopediaCharacterDefenceStats(); + break; + case CYCLOPEDIA_CHARACTERINFO_MISCSTATS: + player->sendCyclopediaCharacterMiscStats(); + break; default: player->sendCyclopediaCharacterNoData(characterInfoType, 1); break; diff --git a/src/game/game.hpp b/src/game/game.hpp index 5261848a668..1918ed66184 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -694,6 +694,20 @@ class Game { void playerSetTyping(uint32_t playerId, uint8_t typing); void refreshItem(const std::shared_ptr &item); + /** + * @brief Finds the managed container for loot or obtain based on the given parameters. + * + * @param player Pointer to the player object. + * @param fallbackConsumed Reference to a boolean flag indicating whether a fallback has been consumed. + * @param category The category of the object. + * + * @note If it's enabled in config.lua to use the gold pouch to store any item, then the system will check whether the player has a loot pouch. + * @note If the player does have one, the loot pouch will be used instead of the managed containers. + * + * @return Pointer to the managed container or nullptr if not found. + */ + std::shared_ptr findManagedContainer(const std::shared_ptr &player, bool &fallbackConsumed, ObjectCategory_t category, bool isLootContainer); + private: std::map m_achievements; std::map m_achievementsNameToId; @@ -719,20 +733,6 @@ class Game { void playerSpeakToNpc(const std::shared_ptr &player, const std::string &text); std::shared_ptr createPlayerTask(uint32_t delay, std::function f, const std::string &context) const; - /** - * @brief Finds the managed container for loot or obtain based on the given parameters. - * - * @param player Pointer to the player object. - * @param fallbackConsumed Reference to a boolean flag indicating whether a fallback has been consumed. - * @param category The category of the object. - * - * @note If it's enabled in config.lua to use the gold pouch to store any item, then the system will check whether the player has a loot pouch. - * @note If the player does have one, the loot pouch will be used instead of the managed containers. - * - * @return Pointer to the managed container or nullptr if not found. - */ - std::shared_ptr findManagedContainer(const std::shared_ptr &player, bool &fallbackConsumed, ObjectCategory_t category, bool isLootContainer); - /** * @brief Finds the next available sub-container within a container. * @@ -907,7 +907,7 @@ class Game { void buildMessageAsAttacker( const std::shared_ptr &target, const CombatDamage &damage, TextMessage &message, - std::stringstream &ss, const std::string &damageString, const std::shared_ptr &attackerPlayer + std::stringstream &ss, const std::string &damageString, bool amplified = false, const std::shared_ptr &attackerPlayer = nullptr ) const; void buildMessageAsTarget( diff --git a/src/game/game_definitions.hpp b/src/game/game_definitions.hpp index 50fa1307764..b07a0cf841d 100644 --- a/src/game/game_definitions.hpp +++ b/src/game/game_definitions.hpp @@ -64,7 +64,7 @@ enum LightState_t : uint8_t { enum CyclopediaCharacterInfoType_t : uint8_t { CYCLOPEDIA_CHARACTERINFO_BASEINFORMATION = 0, CYCLOPEDIA_CHARACTERINFO_GENERALSTATS = 1, - CYCLOPEDIA_CHARACTERINFO_COMBATSTATS = 2, + CYCLOPEDIA_CHARACTERINFO_COMBATSTATS = 2, // unused CYCLOPEDIA_CHARACTERINFO_RECENTDEATHS = 3, CYCLOPEDIA_CHARACTERINFO_RECENTPVPKILLS = 4, CYCLOPEDIA_CHARACTERINFO_ACHIEVEMENTS = 5, @@ -73,7 +73,11 @@ enum CyclopediaCharacterInfoType_t : uint8_t { CYCLOPEDIA_CHARACTERINFO_STORESUMMARY = 8, CYCLOPEDIA_CHARACTERINFO_INSPECTION = 9, CYCLOPEDIA_CHARACTERINFO_BADGES = 10, - CYCLOPEDIA_CHARACTERINFO_TITLES = 11 + CYCLOPEDIA_CHARACTERINFO_TITLES = 11, + CYCLOPEDIA_CHARACTERINFO_WHEEL = 12, + CYCLOPEDIA_CHARACTERINFO_OFFENCESTATS = 13, + CYCLOPEDIA_CHARACTERINFO_DEFENCESTATS = 14, + CYCLOPEDIA_CHARACTERINFO_MISCSTATS = 15 }; enum CyclopediaCharacterInfo_RecentKillStatus_t : uint8_t { diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index f7a81af5364..06bee954c02 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -444,7 +444,19 @@ void IOLoginDataLoad::loadPlayerStashItems(const std::shared_ptr &player query << "SELECT `item_count`, `item_id` FROM `player_stash` WHERE `player_id` = " << player->getGUID(); if ((result = db.storeQuery(query.str()))) { do { - player->addItemOnStash(result->getNumber("item_id"), result->getNumber("item_count")); + auto itemId = result->getNumber("item_id"); + const ItemType &itemType = Item::items[itemId]; + if (itemType.decayTo >= 0 && itemType.decayTime > 0) { + continue; + } + + auto wareId = itemType.wareId; + if (wareId > 0 && wareId != itemType.id) { + g_logger().warn("[{}] - Item ID {} is a ware item, for player: {}, skipping.", __FUNCTION__, itemId, player->getName()); + continue; + } + + player->addItemOnStash(itemId, result->getNumber("item_count")); } while (result->next()); } } @@ -457,32 +469,34 @@ void IOLoginDataLoad::loadPlayerBestiaryCharms(const std::shared_ptr &pl Database &db = Database::getInstance(); std::ostringstream query; - query << "SELECT * FROM `player_charms` WHERE `player_guid` = " << player->getGUID(); + query << "SELECT * FROM `player_charms` WHERE `player_id` = " << player->getGUID(); if ((result = db.storeQuery(query.str()))) { player->charmPoints = result->getNumber("charm_points"); + player->minorCharmEchoes = result->getNumber("minor_charm_echoes"); + player->maxCharmPoints = result->getNumber("max_charm_points"); + player->maxMinorCharmEchoes = result->getNumber("max_minor_charm_echoes"); player->charmExpansion = result->getNumber("charm_expansion"); - player->charmRuneWound = result->getNumber("rune_wound"); - player->charmRuneEnflame = result->getNumber("rune_enflame"); - player->charmRunePoison = result->getNumber("rune_poison"); - player->charmRuneFreeze = result->getNumber("rune_freeze"); - player->charmRuneZap = result->getNumber("rune_zap"); - player->charmRuneCurse = result->getNumber("rune_curse"); - player->charmRuneCripple = result->getNumber("rune_cripple"); - player->charmRuneParry = result->getNumber("rune_parry"); - player->charmRuneDodge = result->getNumber("rune_dodge"); - player->charmRuneAdrenaline = result->getNumber("rune_adrenaline"); - player->charmRuneNumb = result->getNumber("rune_numb"); - player->charmRuneCleanse = result->getNumber("rune_cleanse"); - player->charmRuneBless = result->getNumber("rune_bless"); - player->charmRuneScavenge = result->getNumber("rune_scavenge"); - player->charmRuneGut = result->getNumber("rune_gut"); - player->charmRuneLowBlow = result->getNumber("rune_low_blow"); - player->charmRuneDivine = result->getNumber("rune_divine"); - player->charmRuneVamp = result->getNumber("rune_vamp"); - player->charmRuneVoid = result->getNumber("rune_void"); player->UsedRunesBit = result->getNumber("UsedRunesBit"); player->UnlockedRunesBit = result->getNumber("UnlockedRunesBit"); + unsigned long size; + const auto attribute = result->getStream("charms", size); + PropStream charmsStream; + charmsStream.init(attribute, size); + for (uint8_t id = magic_enum::enum_value(1); id <= magic_enum::enum_count(); id++) { + uint16_t raceId; + uint8_t tier; + + if (!charmsStream.read(raceId) || !charmsStream.read(tier)) { + continue; + } + + player->charmsArray[id].raceId = raceId; + player->charmsArray[id].tier = tier; + + g_logger().debug("Player {} loaded charm Id {} with raceId {} and tier {}", player->name, id, raceId, tier); + } + unsigned long attrBestSize; const char* Bestattr = result->getStream("tracker list", attrBestSize); PropStream propBestStream; @@ -497,7 +511,7 @@ void IOLoginDataLoad::loadPlayerBestiaryCharms(const std::shared_ptr &pl } } else { query.str(""); - query << "INSERT INTO `player_charms` (`player_guid`) VALUES (" << player->getGUID() << ')'; + query << "INSERT INTO `player_charms` (`player_id`) VALUES (" << player->getGUID() << ')'; Database::getInstance().executeQuery(query.str()); } } diff --git a/src/io/functions/iologindata_save_player.cpp b/src/io/functions/iologindata_save_player.cpp index 11888ee6660..5b96c320b76 100644 --- a/src/io/functions/iologindata_save_player.cpp +++ b/src/io/functions/iologindata_save_player.cpp @@ -346,6 +346,17 @@ bool IOLoginDataSave::savePlayerStash(const std::shared_ptr &player) { DBInsert stashQuery("INSERT INTO `player_stash` (`player_id`,`item_id`,`item_count`) VALUES "); for (const auto &[itemId, itemCount] : player->getStashItems()) { + const ItemType &itemType = Item::items[itemId]; + if (itemType.decayTo >= 0 && itemType.decayTime > 0) { + continue; + } + + auto wareId = itemType.wareId; + if (wareId > 0 && wareId != itemType.id) { + g_logger().warn("[{}] - Item ID {} is a ware item, for player: {}, skipping.", __FUNCTION__, itemId, player->getName()); + continue; + } + query << player->getGUID() << ',' << itemId << ',' << itemCount; if (!stashQuery.addRow(query)) { return false; @@ -427,29 +438,24 @@ bool IOLoginDataSave::savePlayerBestiarySystem(const std::shared_ptr &pl std::ostringstream query; query << "UPDATE `player_charms` SET "; query << "`charm_points` = " << player->charmPoints << ","; - query << "`charm_expansion` = " << ((player->charmExpansion) ? 1 : 0) << ","; - query << "`rune_wound` = " << player->charmRuneWound << ","; - query << "`rune_enflame` = " << player->charmRuneEnflame << ","; - query << "`rune_poison` = " << player->charmRunePoison << ","; - query << "`rune_freeze` = " << player->charmRuneFreeze << ","; - query << "`rune_zap` = " << player->charmRuneZap << ","; - query << "`rune_curse` = " << player->charmRuneCurse << ","; - query << "`rune_cripple` = " << player->charmRuneCripple << ","; - query << "`rune_parry` = " << player->charmRuneParry << ","; - query << "`rune_dodge` = " << player->charmRuneDodge << ","; - query << "`rune_adrenaline` = " << player->charmRuneAdrenaline << ","; - query << "`rune_numb` = " << player->charmRuneNumb << ","; - query << "`rune_cleanse` = " << player->charmRuneCleanse << ","; - query << "`rune_bless` = " << player->charmRuneBless << ","; - query << "`rune_scavenge` = " << player->charmRuneScavenge << ","; - query << "`rune_gut` = " << player->charmRuneGut << ","; - query << "`rune_low_blow` = " << player->charmRuneLowBlow << ","; - query << "`rune_divine` = " << player->charmRuneDivine << ","; - query << "`rune_vamp` = " << player->charmRuneVamp << ","; - query << "`rune_void` = " << player->charmRuneVoid << ","; + query << "`minor_charm_echoes` = " << player->minorCharmEchoes << ","; + query << "`max_charm_points` = " << player->maxCharmPoints << ","; + query << "`max_minor_charm_echoes` = " << player->maxMinorCharmEchoes << ","; + query << "`charm_expansion` = " << (player->charmExpansion ? 1 : 0) << ","; query << "`UsedRunesBit` = " << player->UsedRunesBit << ","; query << "`UnlockedRunesBit` = " << player->UnlockedRunesBit << ","; + PropWriteStream charmsStream; + for (uint8_t id = magic_enum::enum_value(1); id <= magic_enum::enum_count(); id++) { + const auto &charm = player->charmsArray[id]; + charmsStream.write(charm.raceId); + charmsStream.write(charm.tier); + g_logger().debug("Player {} saved raceId {} and tier {} to charm Id {}", player->name, charm.raceId, charm.tier, id); + } + size_t size; + const char* charmsList = charmsStream.getStream(size); + query << " `charms` = " << db.escapeBlob(charmsList, static_cast(size)) << ","; + PropWriteStream propBestiaryStream; for (const auto &trackedType : player->getCyclopediaMonsterTrackerSet(false)) { propBestiaryStream.write(trackedType->info.raceid); @@ -457,7 +463,7 @@ bool IOLoginDataSave::savePlayerBestiarySystem(const std::shared_ptr &pl size_t trackerSize; const char* trackerList = propBestiaryStream.getStream(trackerSize); query << " `tracker list` = " << db.escapeBlob(trackerList, static_cast(trackerSize)); - query << " WHERE `player_guid` = " << player->getGUID(); + query << " WHERE `player_id` = " << player->getGUID(); if (!db.executeQuery(query.str())) { g_logger().warn("[IOLoginData::savePlayer] - Error saving bestiary data from player: {}", player->getName()); diff --git a/src/io/iobestiary.cpp b/src/io/iobestiary.cpp index b18ed181467..86423de6b91 100644 --- a/src/io/iobestiary.cpp +++ b/src/io/iobestiary.cpp @@ -11,6 +11,7 @@ #include "creatures/combat/combat.hpp" #include "creatures/combat/condition.hpp" +#include "creatures/monsters/monster.hpp" #include "creatures/monsters/monsters.hpp" #include "creatures/players/player.hpp" #include "game/game.hpp" @@ -18,81 +19,208 @@ SoftSingleton IOBestiary::instanceTracker("IOBestiary"); -bool IOBestiary::parseCharmCombat(const std::shared_ptr &charm, const std::shared_ptr &player, const std::shared_ptr &target, int32_t realDamage, bool dueToPotion, bool checkArmor) { - if (!charm || !player || !target) { - return false; +void IOBestiary::parseOffensiveCharmCombatDamage(const std::shared_ptr &charm, int32_t damage, CombatDamage &charmDamage, CombatParams &charmParams) { + charmDamage.primary.type = charm->damageType; + charmDamage.primary.value = damage; + charmDamage.extension = true; + if (!charmDamage.exString.empty()) { + charmDamage.exString += ", "; } - CombatParams charmParams; + if (charm->logMessage) { + charmDamage.exString += fmt::format("({} charm)", asLowerCaseString(charm->name)); + } + + charmParams.impactEffect = charm->effect; + charmParams.combatType = charmDamage.primary.type; + charmParams.aggressive = true; + + charmParams.soundImpactEffect = charm->soundImpactEffect; + charmParams.soundCastEffect = charm->soundCastEffect; +} + +void IOBestiary::parseCharmCarnage(const std::shared_ptr &charm, const std::shared_ptr &player, const std::shared_ptr &target, int32_t damage) { CombatDamage charmDamage; - if (charm->type == CHARM_OFFENSIVE) { - if (charm->id == CHARM_CRIPPLE) { - std::shared_ptr cripple = Condition::createCondition(CONDITIONID_COMBAT, CONDITION_PARALYZE, 10000, 0)->static_self_cast(); + CombatParams charmParams; + + parseOffensiveCharmCombatDamage(charm, damage, charmDamage, charmParams); + + Combat::doCombatHealth(player, target, charmDamage, charmParams); +} + +bool IOBestiary::parseOffensiveCharmCombat(const std::shared_ptr &charm, const std::shared_ptr &player, const std::shared_ptr &target, CombatDamage &charmDamage, CombatParams &charmParams) { + static double_t maxHealthLimit = 0.08; // 8% max health (max damage) + static uint8_t maxLevelsLimit = 2; // 2x level (max damage) + static constexpr std::array, 4> offsets = { { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } } }; + + int32_t value = 0; + const auto &targetPosition = target->getPosition(); + const auto &monster = target->getMonster(); + + switch (charm->id) { + case CHARM_WOUND: + case CHARM_ENFLAME: + case CHARM_POISON: + case CHARM_FREEZE: + case CHARM_ZAP: + case CHARM_CURSE: + case CHARM_DIVINE: + value = std::min(std::ceil(player->getLevel() * maxLevelsLimit), std::ceil(target->getMaxHealth() * (charm->percent / 100.0))); + break; + case CHARM_CARNAGE: + if (!monster || !monster->isDead()) { + return false; + } + + maxLevelsLimit = 6; + value = target->getMaxHealth(); + + for (const auto &[dx, dy] : offsets) { + Position damagePosition(targetPosition.x + dx, targetPosition.y + dy, targetPosition.z); + const auto &tile = g_game().map.getTile(damagePosition); + + if (!tile) { + continue; + } + + g_game().addMagicEffect(damagePosition, CONST_ME_DRAWBLOOD); + + const auto &topCreature = tile->getTopCreature(); + if (topCreature && topCreature->getType() == CREATURETYPE_MONSTER) { + int32_t damage = std::min( + std::ceil(value * (charm->percent / 100.0)), + player->getLevel() * maxLevelsLimit + ); + parseCharmCarnage(charm, player, topCreature, -damage); + } + } + return false; + + case CHARM_OVERPOWER: + value = std::min(std::ceil(target->getMaxHealth() * maxHealthLimit), std::ceil(player->getMaxHealth() * (charm->percent / 100.0))); + break; + case CHARM_OVERFLUX: + value = std::min(std::ceil(target->getMaxHealth() * maxHealthLimit), std::ceil(player->getMaxMana() * (charm->percent / 100.0))); + break; + case CHARM_CRIPPLE: { + const auto &cripple = std::static_pointer_cast(Condition::createCondition(CONDITIONID_COMBAT, CONDITION_PARALYZE, 10000, 0)); cripple->setFormulaVars(-1, 0, -1, 0); target->addCondition(cripple); - player->sendCancelMessage(charm->cancelMsg); return false; } - int32_t maxHealth = target->getMaxHealth(); - charmDamage.primary.type = charm->dmgtype; - charmDamage.primary.value = ((-maxHealth * (charm->percent)) / 100); - charmDamage.extension = true; - if (!charmDamage.exString.empty()) { - charmDamage.exString += ", "; - } - charmDamage.exString += charm->logMsg + (dueToPotion ? " due to active charm upgrade" : ""); - - charmParams.impactEffect = charm->effect; - charmParams.combatType = charmDamage.primary.type; - charmParams.aggressive = true; - - charmParams.soundImpactEffect = charm->soundImpactEffect; - charmParams.soundCastEffect = charm->soundCastEffect; - - player->sendCancelMessage(charm->cancelMsg); - } else if (charm->type == CHARM_DEFENSIVE) { - switch (charm->id) { - case CHARM_PARRY: { - charmDamage.primary.type = COMBAT_NEUTRALDAMAGE; - charmDamage.primary.value = -realDamage; - charmDamage.extension = true; - if (!charmDamage.exString.empty()) { - charmDamage.exString += ", "; - } - charmDamage.exString += charm->logMsg + (dueToPotion ? " due to active charm upgrade" : ""); - charmParams.aggressive = true; - charmParams.blockedByArmor = checkArmor; - break; - } - case CHARM_DODGE: { - const Position &targetPos = target->getPosition(); - player->sendCancelMessage(charm->cancelMsg); - g_game().addMagicEffect(targetPos, charm->effect); - return true; + default: + g_logger().warn("[{}] - No handler found for offensive charm id {}.", __FUNCTION__, charm->id); + return false; + } + + // This will be handled if any switch statement be break. + parseOffensiveCharmCombatDamage(charm, -value, charmDamage, charmParams); + + return true; +} + +bool IOBestiary::parseDefensiveCharmCombat(const std::shared_ptr &charm, const std::shared_ptr &player, const std::shared_ptr &target, int32_t realDamage, bool checkArmor, CombatDamage &charmDamage, CombatParams &charmParams) { + switch (charm->id) { + case CHARM_PARRY: { + charmDamage.primary.type = COMBAT_NEUTRALDAMAGE; + charmDamage.primary.value = -realDamage; + charmDamage.extension = true; + if (!charmDamage.exString.empty()) { + charmDamage.exString += ", "; } - case CHARM_ADRENALINE: { - std::shared_ptr adrenaline = Condition::createCondition(CONDITIONID_COMBAT, CONDITION_HASTE, 10000, 0)->static_self_cast(); - adrenaline->setFormulaVars(2.5, 40, 2.5, 40); - player->addCondition(adrenaline); - player->sendCancelMessage(charm->cancelMsg); - return false; + if (charm->logMessage) { + charmDamage.exString += fmt::format("({} charm)", asLowerCaseString(charm->name)); } - case CHARM_NUMB: { - std::shared_ptr numb = Condition::createCondition(CONDITIONID_COMBAT, CONDITION_PARALYZE, 10000, 0)->static_self_cast(); - numb->setFormulaVars(-1, 0, -1, 0); - target->addCondition(numb); - player->sendCancelMessage(charm->cancelMsg); - return false; + charmParams.aggressive = true; + charmParams.blockedByArmor = checkArmor; + return true; + } + case CHARM_DODGE: { + const Position &targetPos = target->getPosition(); + g_game().addMagicEffect(targetPos, charm->effect); + break; + } + case CHARM_ADRENALINE: { + const auto &adrenaline = std::static_pointer_cast(Condition::createCondition(CONDITIONID_COMBAT, CONDITION_HASTE, 10000, 0)); + adrenaline->setFormulaVars(2.5, 40, 2.5, 40); + player->addCondition(adrenaline); + break; + } + case CHARM_NUMB: { + const auto &numb = std::static_pointer_cast(Condition::createCondition(CONDITIONID_COMBAT, CONDITION_PARALYZE, 10000, 0)); + numb->setFormulaVars(-1, 0, -1, 0); + target->addCondition(numb); + break; + } + + default: + g_logger().warn("[{}] - No handler found for defensive charm id {}.", __FUNCTION__, charm->id); + break; + } + + return false; +} + +bool IOBestiary::parsePassiveCharmCombat(const std::shared_ptr &charm, const std::shared_ptr &player, const std::shared_ptr &target, int32_t value, CombatDamage &charmDamage, CombatParams &charmParams) { + const auto &monster = target->getMonster(); + switch (charm->id) { + case CHARM_BLESS: + case CHARM_GUT: + case CHARM_LOW: + case CHARM_SAVAGE: + case CHARM_SCAVENGE: + case CHARM_VAMP: + case CHARM_VOID: + case CHARM_VOIDINVERSION: + // All the charms above are being handled separately. + break; + case CHARM_FATAL: + if (monster) { + constexpr int32_t preventFleeDuration = 30000; // Fatal Hold prevents fleeing for 30 seconds. + monster->setFatalHoldDuration(preventFleeDuration); } + break; + default: + g_logger().warn("[{}] - No handler found for passive charm id {}.", __FUNCTION__, charm->id); + break; + } - default: - return false; - } - player->sendCancelMessage(charm->cancelMsg); - } else { + return false; +} + +bool IOBestiary::parseCharmCombat(const std::shared_ptr &charm, const std::shared_ptr &player, const std::shared_ptr &target, int32_t realDamage, bool checkArmor) { + if (!charm || !player || !target) { return false; } - Combat::doCombatHealth(player, target, charmDamage, charmParams); - return false; + + CombatDamage charmDamage; + CombatParams charmParams; + + bool callCombat = false; + + switch (charm->type) { + case CHARM_OFFENSIVE: + callCombat = parseOffensiveCharmCombat(charm, player, target, charmDamage, charmParams); + break; + case CHARM_DEFENSIVE: + callCombat = parseDefensiveCharmCombat(charm, player, target, realDamage, checkArmor, charmDamage, charmParams); + break; + case CHARM_PASSIVE: + callCombat = parsePassiveCharmCombat(charm, player, target, realDamage, charmDamage, charmParams); + break; + default: + g_logger().warn("[{}] - No handler found for charm type {}.", __FUNCTION__, static_cast(charm->type)); + return false; + } + + if (!charm->cancelMessage.empty()) { + player->sendCancelMessage(charm->cancelMessage); + } + + if (callCombat) { + Combat::doCombatHealth(player, target, charmDamage, charmParams); + } + + return true; } IOBestiary &IOBestiary::getInstance() { @@ -161,6 +289,21 @@ void IOBestiary::resetCharmRuneCreature(const std::shared_ptr &player, c player->parseRacebyCharm(charm->id, true, 0); } +void IOBestiary::resetAllCharmRuneCreatures(const std::shared_ptr &player) const { + if (!player) { + return; + } + + const auto charmList = g_game().getCharmList(); + for (const auto &charm : charmList) { + if (!charm) { + continue; + } + player->parseRacebyCharm(charm->id, true, 0); + player->setCharmTier(charm->id, 0); + } +} + void IOBestiary::setCharmRuneCreature(const std::shared_ptr &player, const std::shared_ptr &charm, uint16_t raceid) const { if (!player || !charm) { return; @@ -204,7 +347,7 @@ uint16_t IOBestiary::getBestiaryRaceUnlocked(const std::shared_ptr &play return count; } -void IOBestiary::addCharmPoints(const std::shared_ptr &player, uint16_t amount, bool negative /*= false*/) { +void IOBestiary::addCharmPoints(const std::shared_ptr &player, uint32_t amount, bool negative /*= false*/) { if (!player) { return; } @@ -214,10 +357,28 @@ void IOBestiary::addCharmPoints(const std::shared_ptr &player, uint16_t myCharms -= amount; } else { myCharms += amount; + const auto maxCharmPoints = player->getMaxCharmPoints(); + player->setMaxCharmPoints(maxCharmPoints + amount); } player->setCharmPoints(myCharms); } +void IOBestiary::addMinorCharmEchoes(const std::shared_ptr &player, uint32_t amount, bool negative /*= false*/) { + if (!player) { + return; + } + + uint32_t myCharms = player->getMinorCharmEchoes(); + if (negative) { + myCharms -= amount; + } else { + myCharms += amount; + const auto maxCharmPoints = player->getMaxMinorCharmEchoes(); + player->setMaxMinorCharmEchoes(maxCharmPoints + amount); + } + player->setMinorCharmEchoes(myCharms); +} + void IOBestiary::addBestiaryKill(const std::shared_ptr &player, const std::shared_ptr &mtype, uint32_t amount /*= 1*/) { uint16_t raceid = mtype->info.raceid; if (raceid == 0 || !player || !mtype) { @@ -245,21 +406,26 @@ void IOBestiary::addBestiaryKill(const std::shared_ptr &player, const st player->refreshCyclopediaMonsterTracker(); } -charmRune_t IOBestiary::getCharmFromTarget(const std::shared_ptr &player, const std::shared_ptr &mtype) { +PlayerCharmsByMonster IOBestiary::getCharmFromTarget(const std::shared_ptr &player, const std::shared_ptr &mtype, charmCategory_t category /* = CHARM_ALL */) { + PlayerCharmsByMonster playerCharmByMonster; if (!player || !mtype) { - return CHARM_NONE; + return {}; } uint16_t bestiaryEntry = mtype->info.raceid; std::list usedRunes = getCharmUsedRuneBitAll(player); for (charmRune_t it : usedRunes) { - const auto charm = getBestiaryCharm(it); - if (bestiaryEntry == player->parseRacebyCharm(charm->id, false, 0)) { - return charm->id; + const auto &charm = getBestiaryCharm(it); + if (bestiaryEntry == player->parseRacebyCharm(charm->id) && (category == CHARM_ALL || charm->category == category)) { + if (charm->category == CHARM_MAJOR) { + playerCharmByMonster.major = charm->id; + } else if (charm->category == CHARM_MINOR) { + playerCharmByMonster.minor = charm->id; + } } } - return CHARM_NONE; + return playerCharmByMonster; } bool IOBestiary::hasCharmUnlockedRuneBit(const std::shared_ptr &charm, int32_t input) const { @@ -287,66 +453,121 @@ int32_t IOBestiary::bitToggle(int32_t input, const std::shared_ptr &charm } } -void IOBestiary::sendBuyCharmRune(const std::shared_ptr &player, charmRune_t runeID, uint8_t action, uint16_t raceid) { - const auto charm = getBestiaryCharm(runeID); - if (!player || !charm) { +void IOBestiary::sendBuyCharmRune(const std::shared_ptr &player, uint8_t action, charmRune_t charmId, uint16_t raceId) { + const auto &charm = getBestiaryCharm(charmId); + if (!player || (action != 3 && !charm)) { return; } if (action == 0) { - std::ostringstream ss; + if (charm->category == CHARM_MAJOR) { + auto charmTier = player->getCharmTier(charm->id); + if (charmTier > 2) { + player->sendFYIBox("Charm at max level."); + return; + } + + if (player->getCharmPoints() < charm->points[charmTier]) { + player->sendFYIBox("You don't have enough charm points to unlock this rune."); + return; + } + + addCharmPoints(player, charm->points[charmTier], true); + addMinorCharmEchoes(player, 25 * charmTier * charmTier + 25 * charmTier + 50); + player->setCharmTier(charm->id, charmTier + 1); + } else if (charm->category == CHARM_MINOR) { + auto charmTier = player->getCharmTier(charm->id); + if (charmTier > 2) { + player->sendFYIBox("Charm at max level."); + return; + } - if (player->getCharmPoints() < charm->points) { - ss << "You don't have enough charm points to unlock this rune."; - player->sendFYIBox(ss.str()); - player->BestiarysendCharms(); + if (player->getMinorCharmEchoes() < charm->points[charmTier]) { + player->sendFYIBox("You don't have enough minor charm echoes to unlock this rune."); + return; + } + + addMinorCharmEchoes(player, charm->points[charmTier], true); + player->setCharmTier(charm->id, charmTier + 1); + } else { return; } - ss << "You successfully unlocked '" << charm->name << "' for " << charm->points << " charm points."; - player->sendFYIBox(ss.str()); - addCharmPoints(player, charm->points, true); - int32_t value = bitToggle(player->getUnlockedRunesBit(), charm, true); player->setUnlockedRunesBit(value); } else if (action == 1) { std::list usedRunes = getCharmUsedRuneBitAll(player); - uint16_t limitRunes; + uint16_t limitRunes = 2; if (player->isPremium()) { - if (player->hasCharmExpansion()) { - limitRunes = 100; - } else { - limitRunes = 6; - } - } else { - limitRunes = 3; + limitRunes = player->hasCharmExpansion() ? 25 : 6; } if (limitRunes <= usedRunes.size()) { player->sendFYIBox("You don't have any charm slots available."); - player->BestiarysendCharms(); return; } - setCharmRuneCreature(player, charm, raceid); - player->sendFYIBox("Creature has been set! You are Premium player, so you benefit from up to 6 runes! Charm Expansion allow you to set creatures to all runes at once!"); + const auto &mType = g_monsters().getMonsterTypeByRaceId(raceId); + if (mType && charm->category == CHARM_MAJOR) { + const uint32_t killedAmount = player->getBestiaryKillCount(raceId); + if (killedAmount < mType->info.bestiaryToUnlock) { + return; + } + } + + auto [major, minor] = getCharmFromTarget(player, mType); + if ((charm->category == CHARM_MAJOR && major != CHARM_NONE) || (charm->category == CHARM_MINOR && minor != CHARM_NONE)) { + player->sendFYIBox(fmt::format("You already have this monster set on another {} Charm!", charm->category == CHARM_MAJOR ? "Major" : "Minor")); + return; + } + + setCharmRuneCreature(player, charm, raceId); + if (!player->isPremium()) { + player->sendFYIBox("Creature has been set! You are a Free player, so you benefit from up to 2 runes! Premium players benefit from 6 and Charm Expansion allow you to set creatures to all runes at once!."); + } else if (!player->hasCharmExpansion()) { + player->sendFYIBox("Creature has been set! You are a Premium player, so you benefit from up to 6 runes! Charm Expansion allow you to set creatures to all runes at once!"); + } } else if (action == 2) { int32_t fee = player->getLevel() * 100; if (player->hasCharmExpansion()) { fee = (fee * 75) / 100; } - if (g_game().removeMoney(player, fee, 0, true)) { - resetCharmRuneCreature(player, charm); - player->sendFYIBox("You successfully removed the creature."); - player->BestiarysendCharms(); - g_metrics().addCounter("balance_decrease", fee, { { "player", player->getName() }, { "context", "charm_removal" } }); + if (!g_game().removeMoney(player, fee, 0, true)) { + player->sendFYIBox("You don't have enough gold."); + return; + } + + resetCharmRuneCreature(player, charm); + g_metrics().addCounter("balance_decrease", fee, { { "player", player->getName() }, { "context", "charm_removal" } }); + } else if (action == 3) { + const auto playerLevel = player->getLevel(); + uint64_t resetAllCharmsCost = 100000 + (playerLevel > 100 ? playerLevel * 11000 : 0); + if (player->hasCharmExpansion()) { + resetAllCharmsCost = (resetAllCharmsCost * 75) / 100; + } + + if (!g_game().removeMoney(player, resetAllCharmsCost, 0, true)) { + player->sendFYIBox("You don't have enough gold."); return; } - player->sendFYIBox("You don't have enough gold."); + + resetAllCharmRuneCreatures(player); + const auto maxCharmPoints = player->getMaxCharmPoints(); + player->setCharmPoints(maxCharmPoints); + uint16_t echoesResetValue = 0; + const auto isPromoted = player->kv()->get("promoted"); + if (isPromoted) { + echoesResetValue = 100; + } + player->setMinorCharmEchoes(echoesResetValue); + player->setMaxMinorCharmEchoes(echoesResetValue); + player->setUsedRunesBit(0); + player->setUnlockedRunesBit(0); + g_metrics().addCounter("balance_decrease", resetAllCharmsCost, { { "player", player->getName() }, { "context", "charm_removal" } }); } - player->BestiarysendCharms(); + player->sendBestiaryCharms(); } std::map IOBestiary::getMonsterElements(const std::shared_ptr &mtype) const { @@ -416,6 +637,23 @@ std::vector IOBestiary::getBestiaryFinished(const std::shared_ptr IOBestiary::getBestiaryStageTwo(const std::shared_ptr &player) const { + const auto &bestiaryMap = g_game().getBestiaryList(); + + stdext::vector_set stageTwoMonsters; + stageTwoMonsters.reserve(bestiaryMap.size()); + + for (const auto &[monsterTypeRaceId, monsterTypeName] : bestiaryMap) { + const auto &mtype = g_monsters().getMonsterType(monsterTypeName); + const uint32_t thisKilled = player->getBestiaryKillCount(monsterTypeRaceId); + + if (mtype && thisKilled >= mtype->info.bestiarySecondUnlock) { + stageTwoMonsters.insert(monsterTypeRaceId); + } + } + return stageTwoMonsters.data(); +} + int8_t IOBestiary::calculateDifficult(uint32_t chance) const { float chanceInPercent = chance / 1000; diff --git a/src/io/iobestiary.hpp b/src/io/iobestiary.hpp index 4eb576327fe..385dedeedcb 100644 --- a/src/io/iobestiary.hpp +++ b/src/io/iobestiary.hpp @@ -20,30 +20,46 @@ class SoftSingletonGuard; class MonsterType; class Creature; +struct CombatDamage; +struct CombatParams; + +struct PlayerCharmsByMonster { + charmRune_t major = CHARM_NONE; + charmRune_t minor = CHARM_NONE; +}; + class Charm { public: Charm() = default; - Charm(std::string initname, charmRune_t initcharmRune_t, std::string initdescription, charm_t inittype, uint16_t initpoints, int32_t initbinary) : - name(std::move(initname)), id(initcharmRune_t), description(std::move(initdescription)), type(inittype), points(initpoints), binary(initbinary) { } + Charm(std::string initname, charmRune_t initcharmRune_t, std::string initdescription, charmCategory_t initCategory, charm_t inittype, const std::vector &initpoints, int32_t initbinary) : + name(std::move(initname)), id(initcharmRune_t), description(std::move(initdescription)), category(initCategory), type(inittype), points(initpoints), binary(initbinary) { } virtual ~Charm() = default; + double getChance(uint8_t tier) const { + if (tier < chance.size()) { + return chance[tier]; + } + return 0.0; + } + std::string name; charmRune_t id = CHARM_NONE; std::string description; + charmCategory_t category {}; charm_t type {}; - uint16_t points = 0; + std::vector points; int32_t binary = 0; - std::string cancelMsg; - std::string logMsg; + std::string cancelMessage; + bool logMessage; - CombatType_t dmgtype = COMBAT_NONE; + CombatType_t damageType = COMBAT_NONE; uint16_t effect = 0; SoundEffect_t soundImpactEffect = SoundEffect_t::SILENCE; SoundEffect_t soundCastEffect = SoundEffect_t::SILENCE; - uint16_t percent = 0; - int8_t chance = 0; + float percent = 0; + std::vector chance; }; class IOBestiary { @@ -58,11 +74,18 @@ class IOBestiary { std::shared_ptr getBestiaryCharm(charmRune_t activeCharm, bool force = false) const; void addBestiaryKill(const std::shared_ptr &player, const std::shared_ptr &mtype, uint32_t amount = 1); - bool parseCharmCombat(const std::shared_ptr &charm, const std::shared_ptr &player, const std::shared_ptr &target, int32_t realDamage, bool dueToPotion = false, bool checkArmor = false); - void addCharmPoints(const std::shared_ptr &player, uint16_t amount, bool negative = false); - void sendBuyCharmRune(const std::shared_ptr &player, charmRune_t runeID, uint8_t action, uint16_t raceid); + void parseOffensiveCharmCombatDamage(const std::shared_ptr &charm, int32_t damage, CombatDamage &charmDamage, CombatParams &charmParams); + void parseCharmCarnage(const std::shared_ptr &charm, const std::shared_ptr &player, const std::shared_ptr &target, int32_t damage); + bool parseOffensiveCharmCombat(const std::shared_ptr &charm, const std::shared_ptr &player, const std::shared_ptr &target, CombatDamage &charmDamage, CombatParams &charmParams); + bool parseDefensiveCharmCombat(const std::shared_ptr &charm, const std::shared_ptr &player, const std::shared_ptr &target, int32_t realDamage, bool checkArmor, CombatDamage &charmDamage, CombatParams &charmParams); + bool parsePassiveCharmCombat(const std::shared_ptr &charm, const std::shared_ptr &player, const std::shared_ptr &target, int32_t realDamage, CombatDamage &charmDamage, CombatParams &charmParams); + bool parseCharmCombat(const std::shared_ptr &charm, const std::shared_ptr &player, const std::shared_ptr &target, int32_t realDamage = 0, bool checkArmor = false); + void addCharmPoints(const std::shared_ptr &player, uint32_t amount, bool negative = false); + void addMinorCharmEchoes(const std::shared_ptr &player, uint32_t amount, bool negative = false); + void sendBuyCharmRune(const std::shared_ptr &player, uint8_t action, charmRune_t charmId, uint16_t raceId); void setCharmRuneCreature(const std::shared_ptr &player, const std::shared_ptr &charm, uint16_t raceid) const; void resetCharmRuneCreature(const std::shared_ptr &player, const std::shared_ptr &charm) const; + void resetAllCharmRuneCreatures(const std::shared_ptr &player) const; int8_t calculateDifficult(uint32_t chance) const; uint8_t getKillStatus(const std::shared_ptr &mtype, uint32_t killAmount) const; @@ -75,8 +98,9 @@ class IOBestiary { std::list getCharmUsedRuneBitAll(const std::shared_ptr &player); std::vector getBestiaryFinished(const std::shared_ptr &player) const; + std::vector getBestiaryStageTwo(const std::shared_ptr &player) const; - charmRune_t getCharmFromTarget(const std::shared_ptr &player, const std::shared_ptr &mtype); + PlayerCharmsByMonster getCharmFromTarget(const std::shared_ptr &player, const std::shared_ptr &mtype, charmCategory_t category = CHARM_ALL); std::map getBestiaryKillCountByMonsterIDs(const std::shared_ptr &player, const std::map &mtype_list) const; std::map getMonsterElements(const std::shared_ptr &mtype) const; diff --git a/src/items/containers/container.cpp b/src/items/containers/container.cpp index 0a1b8cba532..5d91f20684a 100644 --- a/src/items/containers/container.cpp +++ b/src/items/containers/container.cpp @@ -152,16 +152,13 @@ void Container::addItem(const std::shared_ptr &item) { item->setParent(getContainer()); } -StashContainerList Container::getStowableItems() const { +StashContainerList Container::getStowableItems() { StashContainerList toReturnList; - for (const auto &item : itemlist) { - if (item->getContainer() != nullptr) { - const auto &subContainer = item->getContainer()->getStowableItems(); - for (const auto &key : subContainer | std::views::keys) { - const auto &containerItem = key; - toReturnList.emplace_back(containerItem, static_cast(containerItem->getItemCount())); - } - } else if (item->isItemStorable()) { + + for (ContainerIterator it = iterator(); it.hasNext(); it.advance()) { + const auto &item = *it; + const auto &itemType = Item::items.getItemType(item->getID()); + if (item->isItemStorable() && !itemType.isContainer()) { toReturnList.emplace_back(item, static_cast(item->getItemCount())); } } @@ -241,13 +238,9 @@ uint32_t Container::getWeight() const { return Item::getWeight() + totalWeight; } -std::string Container::getContentDescription(bool oldProtocol) { - std::ostringstream os; - return getContentDescription(os, oldProtocol).str(); -} +std::string Container::getContentDescription(bool sendColoredMessage) { + std::vector descriptions; -std::ostringstream &Container::getContentDescription(std::ostringstream &os, bool sendColoredMessage) { - bool firstitem = true; for (ContainerIterator it = iterator(); it.hasNext(); it.advance()) { const auto &item = *it; if (!item) { @@ -259,23 +252,14 @@ std::ostringstream &Container::getContentDescription(std::ostringstream &os, boo continue; } - if (firstitem) { - firstitem = false; - } else { - os << ", "; - } - if (sendColoredMessage) { - os << "{" << item->getID() << "|" << item->getNameDescription() << "}"; + descriptions.push_back(fmt::format("{{{}|{}}}", item->getID(), item->getNameDescription())); } else { - os << item->getNameDescription(); + descriptions.push_back(item->getNameDescription()); } } - if (firstitem) { - os << "nothing"; - } - return os; + return descriptions.empty() ? "nothing" : fmt::format("{}", fmt::join(descriptions, ", ")); } uint32_t Container::getMaxCapacity() const { diff --git a/src/items/containers/container.hpp b/src/items/containers/container.hpp index f7e1b5c42a3..1be5c4e77de 100644 --- a/src/items/containers/container.hpp +++ b/src/items/containers/container.hpp @@ -194,7 +194,7 @@ class Container : public Item, public Cylinder { Attr_ReadValue readAttr(AttrTypes_t attr, PropStream &propStream) override; bool unserializeItemNode(OTB::Loader &loader, const OTB::Node &node, PropStream &propStream, Position &itemPosition) override; - std::string getContentDescription(bool oldProtocol); + std::string getContentDescription(bool sendColoredMessage); uint32_t getMaxCapacity() const; @@ -224,7 +224,7 @@ class Container : public Item, public Cylinder { bool countsToLootAnalyzerBalance() const; bool hasParent(); void addItem(const std::shared_ptr &item); - StashContainerList getStowableItems() const; + StashContainerList getStowableItems(); bool isStoreInbox() const; bool isStoreInboxFiltered() const; std::deque> getStoreInboxFilteredItems() const; @@ -286,8 +286,6 @@ class Container : public Item, public Cylinder { bool isInsideContainerWithId(uint16_t id); protected: - std::ostringstream &getContentDescription(std::ostringstream &os, bool oldProtocol); - uint32_t m_maxItems {}; uint32_t maxSize {}; uint32_t totalWeight {}; diff --git a/src/items/functions/item/item_parse.cpp b/src/items/functions/item/item_parse.cpp index d2b2b58cdc3..142b8561ca1 100644 --- a/src/items/functions/item/item_parse.cpp +++ b/src/items/functions/item/item_parse.cpp @@ -629,6 +629,8 @@ CombatType_t ItemParse::parseFieldCombatType(pugi::xml_attribute valueAttribute) return COMBAT_DROWNDAMAGE; } else if (lowerStringValue == "physical") { return COMBAT_PHYSICALDAMAGE; + } else if (lowerStringValue == "agony") { + return COMBAT_AGONYDAMAGE; } else { g_logger().warn("[Items::parseItemNode] Unknown field value {}", valueAttribute.as_string()); } diff --git a/src/items/item.cpp b/src/items/item.cpp index d3f832d7af7..d9b2077885b 100644 --- a/src/items/item.cpp +++ b/src/items/item.cpp @@ -215,9 +215,21 @@ double Item::getTranscendenceChance() const { return 0; } return quadraticPoly( - g_configManager().getFloat(TRANSCENDANCE_CHANCE_FORMULA_A), - g_configManager().getFloat(TRANSCENDANCE_CHANCE_FORMULA_B), - g_configManager().getFloat(TRANSCENDANCE_CHANCE_FORMULA_C), + g_configManager().getFloat(TRANSCENDENCE_CHANCE_FORMULA_A), + g_configManager().getFloat(TRANSCENDENCE_CHANCE_FORMULA_B), + g_configManager().getFloat(TRANSCENDENCE_CHANCE_FORMULA_C), + getTier() + ); +} + +double Item::getAmplificationChance() const { + if (getTier() == 0) { + return 0; + } + return quadraticPoly( + g_configManager().getFloat(AMPLIFICATION_CHANCE_FORMULA_A), + g_configManager().getFloat(AMPLIFICATION_CHANCE_FORMULA_B), + g_configManager().getFloat(AMPLIFICATION_CHANCE_FORMULA_C), getTier() ); } @@ -508,8 +520,8 @@ std::shared_ptr Item::getHoldingPlayer() { return nullptr; } -bool Item::isItemStorable() const { - if (isStoreItem() || hasOwner()) { +bool Item::isItemStorable() { + if (isStoreItem() || hasOwner() || canDecay()) { return false; } const auto isContainerAndHasSomethingInside = (getContainer() != nullptr) && (!getContainer()->getItemList().empty()); @@ -1360,7 +1372,7 @@ Item::getDescriptions(const ItemType &it, const std::shared_ptr &item /*= ss << ", "; } - ss << fmt::format("{} {:+}%", getCombatName(indexToCombatType(i)), it.abilities->fieldAbsorbPercent[i]); + ss << fmt::format("{} {:+}%", getCombatName(indexToCombatType(i)), it.abilities->absorbPercent[i]); protection = true; } if (protection) { @@ -1501,16 +1513,6 @@ Item::getDescriptions(const ItemType &it, const std::shared_ptr &item /*= descriptions.emplace_back("Effect", ss.str()); } - for (size_t i = 0; i < COMBAT_COUNT; ++i) { - if (it.abilities->absorbPercent[i] == 0) { - continue; - } - - ss.str(""); - ss << getCombatName(indexToCombatType(i)) << ' ' - << std::showpos << it.abilities->absorbPercent[i] << std::noshowpos << '%'; - descriptions.emplace_back("Protection", ss.str()); - } for (size_t i = 0; i < COMBAT_COUNT; ++i) { if (it.abilities->fieldAbsorbPercent[i] == 0) { continue; @@ -1547,7 +1549,7 @@ Item::getDescriptions(const ItemType &it, const std::shared_ptr &item /*= } if (it.upgradeClassification > 0) { - descriptions.emplace_back("Tier", std::to_string(item->getTier())); + descriptions.emplace_back("Tier", getTierEffectDescription(item)); } std::string slotName; @@ -2161,23 +2163,45 @@ SoundEffect_t Item::getMovementSound(const std::shared_ptr &toCylinder } std::string Item::parseClassificationDescription(const std::shared_ptr &item) { - std::ostringstream string; if (item && item->getClassification() >= 1) { - string << std::endl - << "Classification: " << std::to_string(item->getClassification()) << " Tier: " << std::to_string(item->getTier()); - if (item->getTier() != 0) { - if (Item::items[item->getID()].weaponType != WEAPON_NONE) { - string << fmt::format(" ({:.2f}% Onslaught).", item->getFatalChance()); - } else if (g_game().getObjectCategory(item) == OBJECTCATEGORY_HELMETS) { - string << fmt::format(" ({:.2f}% Momentum).", item->getMomentumChance()); - } else if (g_game().getObjectCategory(item) == OBJECTCATEGORY_ARMORS) { - string << fmt::format(" ({:.2f}% Ruse).", item->getDodgeChance()); - } else if (g_game().getObjectCategory(item) == OBJECTCATEGORY_LEGS) { - string << fmt::format(" ({:.2f}% Transcendence).", item->getTranscendenceChance()); - } - } + return fmt::format("\nClassification: {} Tier: {}", item->getClassification(), getTierEffectDescription(item)); + } + return ""; +} + +std::string Item::getTierEffectDescription(const std::shared_ptr &item) { + if (!item) { + return ""; } - return string.str(); + + auto itemTier = item->getTier(); + if (itemTier == 0) { + return "0"; + } + + std::string effectDescription; + if (Item::items[item->getID()].weaponType != WEAPON_NONE) { + effectDescription = fmt::format(" ({:.2f}% Onslaught)", item->getFatalChance()); + } else { + switch (g_game().getObjectCategory(item)) { + case OBJECTCATEGORY_HELMETS: + effectDescription = fmt::format(" ({:.2f}% Momentum)", item->getMomentumChance()); + break; + case OBJECTCATEGORY_ARMORS: + effectDescription = fmt::format(" ({:.2f}% Ruse)", item->getDodgeChance()); + break; + case OBJECTCATEGORY_LEGS: + effectDescription = fmt::format(" ({:.2f}% Transcendence)", item->getTranscendenceChance()); + break; + case OBJECTCATEGORY_BOOTS: + effectDescription = fmt::format(" ({:.2f}% Amplification)", item->getAmplificationChance()); + break; + default: + break; + } + } + + return fmt::format("{}{}", itemTier, effectDescription); } std::string Item::parseShowDurationSpeed(int32_t speed, bool &begin) { diff --git a/src/items/item.hpp b/src/items/item.hpp index d24333b2ef7..31d79c6df67 100644 --- a/src/items/item.hpp +++ b/src/items/item.hpp @@ -294,6 +294,7 @@ class Item : virtual public Thing, public ItemProperties, public SharedObject { static std::string parseShowDuration(const std::shared_ptr &item); static std::string parseShowAttributesDescription(const std::shared_ptr &item, uint16_t itemId); static std::string parseClassificationDescription(const std::shared_ptr &item); + static std::string getTierEffectDescription(const std::shared_ptr &item); static std::vector> getDescriptions(const ItemType &it, const std::shared_ptr &item = nullptr); static std::string getDescription(const ItemType &it, int32_t lookDistance, const std::shared_ptr &item = nullptr, int32_t subType = -1, bool addArticle = true); @@ -463,7 +464,9 @@ class Item : virtual public Thing, public ItemProperties, public SharedObject { return items[id].stackable; } bool isStowable() const { - return items[id].stackable && items[id].wareId > 0; + const auto &itemType = items[id]; + auto wareId = itemType.wareId; + return hasMarketAttributes() && !getTier() && wareId > 0 && !itemType.isContainer() && wareId == itemType.id; } bool isAlwaysOnTop() const { return items[id].alwaysOnTopOrder != 0; @@ -597,7 +600,7 @@ class Item : virtual public Thing, public ItemProperties, public SharedObject { void setDefaultSubtype(); uint16_t getSubType() const; - bool isItemStorable() const; + bool isItemStorable(); void setSubType(uint16_t n); void addUniqueId(uint16_t uniqueId); @@ -703,6 +706,8 @@ class Item : virtual public Thing, public ItemProperties, public SharedObject { double getTranscendenceChance() const; + double getAmplificationChance() const; + uint8_t getTier() const; void setTier(uint8_t tier); uint8_t getClassification() const { diff --git a/src/items/items.hpp b/src/items/items.hpp index 890ae04576c..5b267bf5ba7 100644 --- a/src/items/items.hpp +++ b/src/items/items.hpp @@ -203,6 +203,9 @@ class ItemType { bool isLegs() const { return slotPosition & SLOTP_LEGS; } + bool isBoots() const { + return slotPosition & SLOTP_FEET; + } bool isRanged() const { return weaponType == WEAPON_DISTANCE && weaponType != WEAPON_NONE; } diff --git a/src/items/weapons/weapons.cpp b/src/items/weapons/weapons.cpp index b47c5f8a482..c238fe77d3f 100644 --- a/src/items/weapons/weapons.cpp +++ b/src/items/weapons/weapons.cpp @@ -78,7 +78,7 @@ int32_t Weapons::getMaxMeleeDamage(int32_t attackSkill, int32_t attackValue) { // Players int32_t Weapons::getMaxWeaponDamage(uint32_t level, int32_t attackSkill, int32_t attackValue, float attackFactor, bool isMelee) { if (isMelee) { - return static_cast(std::round((0.085 * attackFactor * attackValue * attackSkill) + (level / 5))); + return attackValue > 0 ? static_cast(std::round((0.085 * attackFactor * attackValue * attackSkill) + (level / 5))) : 0; } else { return static_cast(std::round((0.09 * attackFactor * attackValue * attackSkill) + (level / 5))); } @@ -629,7 +629,7 @@ int32_t WeaponMelee::getWeaponDamage(const std::shared_ptr &player, cons const auto maxValue = static_cast(Weapons::getMaxWeaponDamage(level, attackSkill, combinedAttack, attackFactor, true) * player->getVocation()->meleeDamageMultiplier); - const int32_t minValue = level / 5; + const int32_t minValue = physicalAttack > 0 ? level / 5 : 0; if (maxDamage) { return -maxValue; diff --git a/src/lua/creature/talkaction.cpp b/src/lua/creature/talkaction.cpp index 798f47ae28a..0ec336b9dd0 100644 --- a/src/lua/creature/talkaction.cpp +++ b/src/lua/creature/talkaction.cpp @@ -14,6 +14,7 @@ #include "creatures/players/player.hpp" #include "lua/scripts/scripts.hpp" #include "lib/di/container.hpp" +#include "enums/account_type.hpp" #include "enums/account_group_type.hpp" TalkActions::TalkActions() = default; @@ -41,30 +42,28 @@ bool TalkActions::checkWord(const std::shared_ptr &player, SpeakClasses return false; } - // Helper lambda that maps an account type to the maximum allowed group type - auto allowedGroupLevelForAccount = [](AccountType account) -> uint8_t { - switch (account) { - case ACCOUNT_TYPE_NORMAL: - return GROUP_TYPE_NORMAL; - case ACCOUNT_TYPE_TUTOR: - return GROUP_TYPE_TUTOR; - case ACCOUNT_TYPE_SENIORTUTOR: - return GROUP_TYPE_SENIORTUTOR; - case ACCOUNT_TYPE_GAMEMASTER: - // Allow both GAMEMASTER and COMMUNITYMANAGER talk actions. - return GROUP_TYPE_COMMUNITYMANAGER; // COMMUNITYMANAGER = 5 - case ACCOUNT_TYPE_GOD: - return GROUP_TYPE_GOD; - default: - return GROUP_TYPE_NONE; - } + // Map of allowed group levels for each account type + static const std::unordered_map allowedGroupLevels = { + { ACCOUNT_TYPE_NORMAL, GROUP_TYPE_NORMAL }, + { ACCOUNT_TYPE_TUTOR, GROUP_TYPE_TUTOR }, + { ACCOUNT_TYPE_SENIORTUTOR, GROUP_TYPE_SENIORTUTOR }, + { ACCOUNT_TYPE_GAMEMASTER, GROUP_TYPE_COMMUNITYMANAGER }, // GAMEMASTER -> COMMUNITYMANAGER (5) + { ACCOUNT_TYPE_GOD, GROUP_TYPE_GOD } }; - if (player->getAccountType() != ACCOUNT_TYPE_GOD) { - // Compare the talk action's required group level to the allowed maximum for the account. - if (talkActionPtr->getGroupType() > allowedGroupLevelForAccount(static_cast(player->getAccountType()))) { - return false; + // Helper lambda to get the allowed group level for an account + auto allowedGroupLevelForAccount = [](AccountType account) { + if (auto it = allowedGroupLevels.find(account); it != allowedGroupLevels.end()) { + return it->second; } + + g_logger().warn("[TalkActions::checkWord] Invalid account type: {}", account); + return GROUP_TYPE_NONE; + }; + + // Check if player has permission for the talk action + if (player->getAccountType() != ACCOUNT_TYPE_GOD && talkActionPtr->getGroupType() > allowedGroupLevelForAccount(static_cast(player->getAccountType()))) { + return false; } std::string param; diff --git a/src/lua/functions/core/game/lua_enums.cpp b/src/lua/functions/core/game/lua_enums.cpp index d8e2194d65c..74408ae4f62 100644 --- a/src/lua/functions/core/game/lua_enums.cpp +++ b/src/lua/functions/core/game/lua_enums.cpp @@ -131,6 +131,8 @@ void LuaEnums::initOthersEnums(lua_State* L) { registerEnum(L, CHARM_OFFENSIVE); registerEnum(L, CHARM_DEFENSIVE); registerEnum(L, CHARM_PASSIVE); + registerEnum(L, CHARM_MAJOR); + registerEnum(L, CHARM_MINOR); registerEnum(L, CHARM_GUT); registerEnum(L, CHARM_SCAVENGE); diff --git a/src/lua/functions/creatures/monster/charm_functions.cpp b/src/lua/functions/creatures/monster/charm_functions.cpp index 872a58612f3..0613837d481 100644 --- a/src/lua/functions/creatures/monster/charm_functions.cpp +++ b/src/lua/functions/creatures/monster/charm_functions.cpp @@ -19,6 +19,7 @@ void CharmFunctions::init(lua_State* L) { Lua::registerMethod(L, "Charm", "name", CharmFunctions::luaCharmName); Lua::registerMethod(L, "Charm", "description", CharmFunctions::luaCharmDescription); + Lua::registerMethod(L, "Charm", "category", CharmFunctions::luaCharmCategory); Lua::registerMethod(L, "Charm", "type", CharmFunctions::luaCharmType); Lua::registerMethod(L, "Charm", "points", CharmFunctions::luaCharmPoints); Lua::registerMethod(L, "Charm", "damageType", CharmFunctions::luaCharmDamageType); @@ -73,6 +74,18 @@ int CharmFunctions::luaCharmDescription(lua_State* L) { return 1; } +int CharmFunctions::luaCharmCategory(lua_State* L) { + // get: charm:category() set: charm:category(charmCategory_t) + const auto &charm = Lua::getUserdataShared(L, 1, "Charm"); + if (lua_gettop(L) == 1) { + lua_pushnumber(L, charm->category); + } else { + charm->category = Lua::getNumber(L, 2); + Lua::pushBoolean(L, true); + } + return 1; +} + int CharmFunctions::luaCharmType(lua_State* L) { // get: charm:type() set: charm:type(charm_t) const auto &charm = Lua::getUserdataShared(L, 1, "Charm"); @@ -89,10 +102,25 @@ int CharmFunctions::luaCharmPoints(lua_State* L) { // get: charm:points() set: charm:points(value) const auto &charm = Lua::getUserdataShared(L, 1, "Charm"); if (lua_gettop(L) == 1) { - lua_pushnumber(L, charm->points); - } else { - charm->points = Lua::getNumber(L, 2); + lua_createtable(L, charm->points.size(), 0); + int index = 0; + for (const auto &pointsValue : charm->points) { + lua_pushnumber(L, pointsValue); + lua_rawseti(L, -2, ++index); + } + } else if (lua_istable(L, 2)) { + charm->points.clear(); + lua_pushnil(L); + while (lua_next(L, 2)) { + if (lua_isnumber(L, -1)) { + charm->points.push_back(static_cast(lua_tonumber(L, -1))); + } + lua_pop(L, 1); + } Lua::pushBoolean(L, true); + } else { + lua_pushstring(L, "Expected a table for points."); + lua_error(L); } return 1; } @@ -101,9 +129,9 @@ int CharmFunctions::luaCharmDamageType(lua_State* L) { // get: charm:damageType() set: charm:damageType(type) const auto &charm = Lua::getUserdataShared(L, 1, "Charm"); if (lua_gettop(L) == 1) { - lua_pushnumber(L, charm->dmgtype); + lua_pushnumber(L, charm->damageType); } else { - charm->dmgtype = Lua::getNumber(L, 2); + charm->damageType = Lua::getNumber(L, 2); Lua::pushBoolean(L, true); } return 1; @@ -115,7 +143,7 @@ int CharmFunctions::luaCharmPercentage(lua_State* L) { if (lua_gettop(L) == 1) { lua_pushnumber(L, charm->percent); } else { - charm->percent = Lua::getNumber(L, 2); + charm->percent = Lua::getNumber(L, 2); Lua::pushBoolean(L, true); } return 1; @@ -125,10 +153,26 @@ int CharmFunctions::luaCharmChance(lua_State* L) { // get: charm:chance() set: charm:chance(value) const auto &charm = Lua::getUserdataShared(L, 1, "Charm"); if (lua_gettop(L) == 1) { - lua_pushnumber(L, charm->chance); - } else { - charm->chance = Lua::getNumber(L, 2); + lua_createtable(L, charm->chance.size(), 0); + int index = 0; + for (const auto &chanceValue : charm->chance) { + lua_pushnumber(L, chanceValue); + lua_rawseti(L, -2, ++index); + } + } else if (lua_istable(L, 2)) { + charm->chance.clear(); + lua_pushnil(L); + charm->chance.emplace_back(0); + while (lua_next(L, 2)) { + if (lua_isnumber(L, -1)) { + charm->chance.emplace_back(static_cast(lua_tonumber(L, -1))); + } + lua_pop(L, 1); + } Lua::pushBoolean(L, true); + } else { + lua_pushstring(L, "Expected a table for chance."); + lua_error(L); } return 1; } @@ -137,9 +181,9 @@ int CharmFunctions::luaCharmMessageCancel(lua_State* L) { // get: charm:messageCancel() set: charm:messageCancel(string) const auto &charm = Lua::getUserdataShared(L, 1, "Charm"); if (lua_gettop(L) == 1) { - Lua::pushString(L, charm->cancelMsg); + Lua::pushString(L, charm->cancelMessage); } else { - charm->cancelMsg = Lua::getString(L, 2); + charm->cancelMessage = Lua::getString(L, 2); Lua::pushBoolean(L, true); } return 1; @@ -149,9 +193,9 @@ int CharmFunctions::luaCharmMessageServerLog(lua_State* L) { // get: charm:messageServerLog() set: charm:messageServerLog(string) const auto &charm = Lua::getUserdataShared(L, 1, "Charm"); if (lua_gettop(L) == 1) { - Lua::pushString(L, charm->logMsg); + Lua::pushBoolean(L, charm->logMessage); } else { - charm->logMsg = Lua::getString(L, 2); + charm->logMessage = Lua::getBoolean(L, 2); Lua::pushBoolean(L, true); } return 1; diff --git a/src/lua/functions/creatures/monster/charm_functions.hpp b/src/lua/functions/creatures/monster/charm_functions.hpp index 10b9f96f124..b2c6c68fd9f 100644 --- a/src/lua/functions/creatures/monster/charm_functions.hpp +++ b/src/lua/functions/creatures/monster/charm_functions.hpp @@ -17,6 +17,7 @@ class CharmFunctions { static int luaCharmCreate(lua_State* L); static int luaCharmName(lua_State* L); static int luaCharmDescription(lua_State* L); + static int luaCharmCategory(lua_State* L); static int luaCharmType(lua_State* L); static int luaCharmPoints(lua_State* L); static int luaCharmDamageType(lua_State* L); diff --git a/src/lua/functions/creatures/monster/monster_functions.cpp b/src/lua/functions/creatures/monster/monster_functions.cpp index bbf0b683176..48d086374f6 100644 --- a/src/lua/functions/creatures/monster/monster_functions.cpp +++ b/src/lua/functions/creatures/monster/monster_functions.cpp @@ -121,7 +121,7 @@ int MonsterFunctions::luaMonsterGetType(lua_State* L) { // monster:getType() const auto &monster = Lua::getUserdataShared(L, 1, "Monster"); if (monster) { - Lua::pushUserdata(L, monster->mType); + Lua::pushUserdata(L, monster->m_monsterType); Lua::setMetatable(L, -1, "MonsterType"); } else { lua_pushnil(L); @@ -141,7 +141,7 @@ int MonsterFunctions::luaMonsterSetType(lua_State* L) { mType = g_monsters().getMonsterType(Lua::getString(L, 2)); } // Unregister creature events (current MonsterType) - for (const std::string &scriptName : monster->mType->info.scripts) { + for (const std::string &scriptName : monster->m_monsterType->info.scripts) { if (!monster->unregisterCreatureEvent(scriptName)) { g_logger().warn("[Warning - MonsterFunctions::luaMonsterSetType] Unknown event name: {}", scriptName); } @@ -151,7 +151,7 @@ int MonsterFunctions::luaMonsterSetType(lua_State* L) { g_game().updateMonster(monster, mType); // Assign new MonsterType - monster->mType = mType; + monster->m_monsterType = mType; monster->nameDescription = asLowerCaseString(mType->nameDescription); monster->defaultOutfit = mType->info.outfit; monster->currentOutfit = mType->info.outfit; @@ -446,7 +446,7 @@ int MonsterFunctions::luaMonsterSetSpawnPosition(lua_State* L) { const auto &spawnMonster = g_game().map.spawnsMonster.getspawnMonsterList().emplace_back(std::make_shared(pos, 5)); uint32_t interval = Lua::getNumber(L, 2, 90) * 1000 * 100 / std::max((uint32_t)1, (g_configManager().getNumber(RATE_SPAWN) * eventschedule)); - spawnMonster->addMonster(monster->mType->typeName, pos, DIRECTION_NORTH, static_cast(interval)); + spawnMonster->addMonster(monster->m_monsterType->typeName, pos, DIRECTION_NORTH, static_cast(interval)); spawnMonster->startSpawnMonsterCheck(); Lua::pushBoolean(L, true); diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index 4544169d5e1..fb1bd868c34 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -42,7 +42,11 @@ void PlayerFunctions::init(lua_State* L) { Lua::registerMethod(L, "Player", "resetCharmsBestiary", PlayerFunctions::luaPlayerResetCharmsMonsters); Lua::registerMethod(L, "Player", "unlockAllCharmRunes", PlayerFunctions::luaPlayerUnlockAllCharmRunes); - Lua::registerMethod(L, "Player", "addCharmPoints", PlayerFunctions::luaPlayeraddCharmPoints); + Lua::registerMethod(L, "Player", "addCharmPoints", PlayerFunctions::luaPlayerAddCharmPoints); + Lua::registerMethod(L, "Player", "addMinorCharmEchoes", PlayerFunctions::luaPlayerAddMinorCharmEchoes); + Lua::registerMethod(L, "Player", "getCharmTier", PlayerFunctions::luaPlayerGetCharmTier); + Lua::registerMethod(L, "Player", "getCharmChance", PlayerFunctions::luaPlayerGetCharmChance); + Lua::registerMethod(L, "Player", "resetOldCharms", PlayerFunctions::luaPlayerResetOldCharms); Lua::registerMethod(L, "Player", "isPlayer", PlayerFunctions::luaPlayerIsPlayer); Lua::registerMethod(L, "Player", "getGuid", PlayerFunctions::luaPlayerGetGuid); @@ -176,6 +180,8 @@ void PlayerFunctions::init(lua_State* L) { Lua::registerMethod(L, "Player", "getStashCount", PlayerFunctions::luaPlayerGetStashCounter); Lua::registerMethod(L, "Player", "openStash", PlayerFunctions::luaPlayerOpenStash); + Lua::registerMethod(L, "Player", "canReceiveLoot", PlayerFunctions::luaPlayerCanReceiveLoot); + Lua::registerMethod(L, "Player", "getStamina", PlayerFunctions::luaPlayerGetStamina); Lua::registerMethod(L, "Player", "setStamina", PlayerFunctions::luaPlayerSetStamina); @@ -548,7 +554,7 @@ int PlayerFunctions::luaPlayerResetCharmsMonsters(lua_State* L) { player->setCharmExpansion(false); player->setUsedRunesBit(0); player->setUnlockedRunesBit(0); - for (int8_t i = CHARM_WOUND; i <= CHARM_LAST; i++) { + for (int8_t i = magic_enum::enum_value(1); i <= magic_enum::enum_count(); i++) { player->parseRacebyCharm(static_cast(i), true, 0); } Lua::pushBoolean(L, true); @@ -562,7 +568,7 @@ int PlayerFunctions::luaPlayerUnlockAllCharmRunes(lua_State* L) { // player:unlockAllCharmRunes() const auto &player = Lua::getUserdataShared(L, 1, "Player"); if (player) { - for (int8_t i = CHARM_WOUND; i <= CHARM_LAST; i++) { + for (int8_t i = magic_enum::enum_value(1); i <= magic_enum::enum_count(); i++) { const auto charm = g_iobestiary().getBestiaryCharm(static_cast(i)); if (charm) { const int32_t value = g_iobestiary().bitToggle(player->getUnlockedRunesBit(), charm, true); @@ -576,7 +582,7 @@ int PlayerFunctions::luaPlayerUnlockAllCharmRunes(lua_State* L) { return 1; } -int PlayerFunctions::luaPlayeraddCharmPoints(lua_State* L) { +int PlayerFunctions::luaPlayerAddCharmPoints(lua_State* L) { // player:addCharmPoints() const auto &player = Lua::getUserdataShared(L, 1, "Player"); if (player) { @@ -594,6 +600,62 @@ int PlayerFunctions::luaPlayeraddCharmPoints(lua_State* L) { return 1; } +int PlayerFunctions::luaPlayerGetCharmTier(lua_State* L) { + // player:getCharmTier(charmId) + const auto &player = Lua::getUserdataShared(L, 1, "Player"); + if (!player) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + return 1; + } + + charmRune_t charmId = Lua::getNumber(L, 2); + Lua::pushNumber(L, player->getCharmTier(charmId)); + return 1; +} + +int PlayerFunctions::luaPlayerGetCharmChance(lua_State* L) { + // player:getCharmChance(charmId) + const auto &player = Lua::getUserdataShared(L, 1, "Player"); + if (!player) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + return 1; + } + + charmRune_t charmId = Lua::getNumber(L, 2); + const auto &charm = g_iobestiary().getBestiaryCharm(charmId); + if (!charm) { + Lua::pushNumber(L, 0); + return 1; + } + + uint8_t charmTier = player->getCharmTier(charmId); + + double chance = charm->getChance(charmTier); + + Lua::pushNumber(L, chance); + + return 1; +} + +int PlayerFunctions::luaPlayerAddMinorCharmEchoes(lua_State* L) { + // player:addMinorCharmEchoes() + const auto &player = Lua::getUserdataShared(L, 1, "Player"); + if (!player) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + return 1; + } + + int16_t charms = Lua::getNumber(L, 2); + if (charms >= 0) { + g_iobestiary().addMinorCharmEchoes(player, static_cast(charms)); + } else { + charms = -charms; + g_iobestiary().addMinorCharmEchoes(player, static_cast(charms), true); + } + Lua::pushBoolean(L, true); + return 1; +} + int PlayerFunctions::luaPlayerIsPlayer(lua_State* L) { // player:isPlayer() Lua::pushBoolean(L, Lua::getUserdataShared(L, 1, "Player") != nullptr); @@ -739,7 +801,7 @@ int PlayerFunctions::luaPlayergetCharmMonsterType(lua_State* L) { const auto &player = Lua::getUserdataShared(L, 1, "Player"); if (player) { const charmRune_t charmid = Lua::getNumber(L, 2); - const uint16_t raceid = player->parseRacebyCharm(charmid, false, 0); + const uint16_t raceid = player->parseRacebyCharm(charmid); if (raceid > 0) { const auto &mtype = g_monsters().getMonsterTypeByRaceId(raceid); if (mtype) { @@ -2009,12 +2071,12 @@ int PlayerFunctions::luaPlayerSetGroup(lua_State* L) { int PlayerFunctions::luaPlayerSetSpecialContainersAvailable(lua_State* L) { // player:setSpecialContainersAvailable(stashMenu, marketMenu, depotSearchMenu) - const bool supplyStashMenu = Lua::getBoolean(L, 2, false); + const bool stashMenu = Lua::getBoolean(L, 2, false); const bool marketMenu = Lua::getBoolean(L, 3, false); const bool depotSearchMenu = Lua::getBoolean(L, 4, false); const auto &player = Lua::getUserdataShared(L, 1, "Player"); if (player) { - player->setSpecialMenuAvailable(supplyStashMenu, marketMenu, depotSearchMenu); + player->setSpecialMenuAvailable(stashMenu, marketMenu, depotSearchMenu); Lua::pushBoolean(L, true); } else { lua_pushnil(L); @@ -2022,6 +2084,18 @@ int PlayerFunctions::luaPlayerSetSpecialContainersAvailable(lua_State* L) { return 1; } +int PlayerFunctions::luaPlayerCanReceiveLoot(lua_State* L) { + // player:canReceiveLoot() + const auto &player = Lua::getUserdataShared(L, 1, "Player"); + if (!player) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + return 1; + } + + lua_pushboolean(L, player->getStaminaMinutes() > 840); + return 1; +} + int PlayerFunctions::luaPlayerGetStamina(lua_State* L) { // player:getStamina() const auto &player = Lua::getUserdataShared(L, 1, "Player"); @@ -4987,3 +5061,16 @@ int PlayerFunctions::luaPlayerRemoveCustomOutfit(lua_State* L) { Lua::pushBoolean(L, player->attachedEffects().removeCustomOutfit(type, idOrName)); return 1; } + +int PlayerFunctions::luaPlayerResetOldCharms(lua_State* L) { + // player:resetOldCharms() + const auto &player = Lua::getUserdataShared(L, 1, "Player"); + if (!player) { + Lua::reportErrorFunc(Lua::getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + return 1; + } + + player->resetOldCharms(); + Lua::pushBoolean(L, true); + return 1; +} diff --git a/src/lua/functions/creatures/player/player_functions.hpp b/src/lua/functions/creatures/player/player_functions.hpp index 04e44ec1695..816692338e6 100644 --- a/src/lua/functions/creatures/player/player_functions.hpp +++ b/src/lua/functions/creatures/player/player_functions.hpp @@ -25,7 +25,11 @@ class PlayerFunctions { static int luaPlayerUnlockAllCharmRunes(lua_State* L); static int luaPlayerResetCharmsMonsters(lua_State* L); - static int luaPlayeraddCharmPoints(lua_State* L); + static int luaPlayerAddCharmPoints(lua_State* L); + static int luaPlayerAddMinorCharmEchoes(lua_State* L); + static int luaPlayerGetCharmTier(lua_State* L); + static int luaPlayerGetCharmChance(lua_State* L); + static int luaPlayerResetOldCharms(lua_State* L); static int luaPlayerIsPlayer(lua_State* L); static int luaPlayerGetGuid(lua_State* L); @@ -155,11 +159,13 @@ class PlayerFunctions { static int luaPlayerGetGroup(lua_State* L); static int luaPlayerSetGroup(lua_State* L); - static int luaPlayerIsSupplyStashAvailable(lua_State* L); + static int luaPlayerIsStashAvailable(lua_State* L); static int luaPlayerGetStashCounter(lua_State* L); static int luaPlayerOpenStash(lua_State* L); static int luaPlayerSetSpecialContainersAvailable(lua_State* L); + static int luaPlayerCanReceiveLoot(lua_State* L); + static int luaPlayerGetStamina(lua_State* L); static int luaPlayerSetStamina(lua_State* L); diff --git a/src/server/network/connection/connection.cpp b/src/server/network/connection/connection.cpp index cb60eef3aa8..5f642eafd50 100644 --- a/src/server/network/connection/connection.cpp +++ b/src/server/network/connection/connection.cpp @@ -135,7 +135,7 @@ void Connection::parseProxyIdentification(const std::error_code &error) { if (error || connectionState == CONNECTION_STATE_CLOSED) { if (error != asio::error::operation_aborted && error != asio::error::eof && error != asio::error::connection_reset) { - g_logger().error("[Connection::parseProxyIdentification] - Read error: {}", error.message()); + g_logger().debug("[Connection::parseProxyIdentification] - Read error: {}", error.message()); } close(FORCE_CLOSE); return; @@ -239,7 +239,7 @@ void Connection::parsePacket(const std::error_code &error) { if (error || connectionState == CONNECTION_STATE_CLOSED) { if (error) { - g_logger().error("[Connection::parsePacket] - Read error: {}", error.message()); + g_logger().debug("[Connection::parsePacket] - Read error: {}", error.message()); } close(FORCE_CLOSE); return; diff --git a/src/server/network/message/networkmessage.cpp b/src/server/network/message/networkmessage.cpp index 4f203dc4551..69d197cfed8 100644 --- a/src/server/network/message/networkmessage.cpp +++ b/src/server/network/message/networkmessage.cpp @@ -168,7 +168,7 @@ void NetworkMessage::addString(const std::string &value, const std::source_locat info.length += stringLen; } -void NetworkMessage::addDouble(double value, uint8_t precision /*= 2*/) { +void NetworkMessage::addDouble(double value, uint8_t precision /*= 4*/) { addByte(precision); add((value * std::pow(static_cast(SCALING_BASE), precision)) + std::numeric_limits::max()); } diff --git a/src/server/network/message/networkmessage.hpp b/src/server/network/message/networkmessage.hpp index 5e250c520a5..97dbfc66f74 100644 --- a/src/server/network/message/networkmessage.hpp +++ b/src/server/network/message/networkmessage.hpp @@ -137,7 +137,7 @@ class NetworkMessage { */ void addString(const std::string &value, const std::source_location &location = std::source_location::current(), const std::string &function = ""); - void addDouble(double value, uint8_t precision = 2); + void addDouble(double value, uint8_t precision = 4); double getDouble(); // write functions for complex types diff --git a/src/server/network/protocol/protocol.cpp b/src/server/network/protocol/protocol.cpp index 0a0d4730302..aa8eb8147ca 100644 --- a/src/server/network/protocol/protocol.cpp +++ b/src/server/network/protocol/protocol.cpp @@ -201,11 +201,16 @@ void Protocol::XTEA_encrypt(OutputMessage &outputMessage) const { bool Protocol::XTEA_decrypt(NetworkMessage &msg) const { uint16_t msgLength = msg.getLength() - (checksumMethod == CHECKSUM_METHOD_NONE ? 2 : 6); + uint8_t* buffer = msg.getBuffer() + msg.getBufferPosition(); if ((msgLength % 8) != 0) { + g_logger().error("XTEA_decrypt Failed - invalid block size: {}", msgLength); + for (int i = 0; i < msgLength; ++i) { + fmt::print("{:02X} ", buffer[i]); + } + fmt::print("\n"); return false; } - uint8_t* buffer = msg.getBuffer() + msg.getBufferPosition(); size_t messageLength = msgLength; XTEA_transform(buffer, messageLength, false); @@ -213,10 +218,11 @@ bool Protocol::XTEA_decrypt(NetworkMessage &msg) const { uint8_t paddingSize = msg.getByte(); uint16_t innerLength = messageLength - paddingSize; if (innerLength + paddingSize > msgLength) { + g_logger().error("XTEA_decrypt Failed - invalid inner length: {} + {} > {}", innerLength, paddingSize, msgLength); return false; } - msg.setLength(innerLength); + msg.setLength(messageLength - paddingSize); return true; } diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index db08229a36f..a2d933ef99f 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -118,7 +118,7 @@ namespace { */ void handleImbuementDamage(NetworkMessage &msg, const std::shared_ptr &player) { bool imbueDmg = false; - std::shared_ptr weapon = player->getWeapon(); + const auto &weapon = player->getWeapon(); if (weapon) { uint8_t slots = Item::items[weapon->getID()].imbuementSlot; if (slots > 0) { @@ -130,8 +130,9 @@ namespace { if (imbuementInfo.duration > 0) { auto imbuement = *imbuementInfo.imbuement; - if (imbuement.combatType != COMBAT_NONE) { - msg.addByte(static_cast(imbuement.elementDamage)); + bool hasValidCombat = imbuement.combatType != COMBAT_NONE && imbuement.combatType < COMBAT_COUNT; + if (hasValidCombat) { + msg.addDouble(imbuement.elementDamage / 100.); msg.addByte(getCipbiaElement(imbuement.combatType)); imbueDmg = true; break; @@ -140,8 +141,9 @@ namespace { } } } + if (!imbueDmg) { - msg.addByte(0); + msg.addDouble(0); msg.addByte(0); } } @@ -154,7 +156,7 @@ namespace { * * @param[in] player The pointer to the player whose equipped items are considered. */ - void calculateAbsorbValues(const std::shared_ptr &player, NetworkMessage &msg, uint8_t &combats) { + void calculateAbsorbValues(const std::shared_ptr &player, NetworkMessage &msg, uint8_t &combats, bool fromPlayerSkills = false) { alignas(16) uint16_t damageModifiers[COMBAT_COUNT] = { 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000 }; for (int32_t slot = CONST_SLOT_FIRST; slot <= CONST_SLOT_LAST; ++slot) { @@ -210,10 +212,13 @@ namespace { } if (damageModifiers[i] != 10000) { - int16_t clientModifier = std::clamp(10000 - static_cast(damageModifiers[i]), -10000, 10000); + double clientModifier = (10000 - static_cast(damageModifiers[i])) / 10000.; g_logger().debug("[{}] CombatType: {}, Damage Modifier: {}, Resulting Client Modifier: {}", __FUNCTION__, i, damageModifiers[i], clientModifier); + if (!fromPlayerSkills) { + msg.addByte(0x04); + } msg.addByte(getCipbiaElement(indexToCombatType(i))); - msg.add(clientModifier); + msg.addDouble(clientModifier); ++combats; } } @@ -238,11 +243,51 @@ namespace { continue; } - g_logger().debug("Sendding category number '{}', category name '{}'", static_cast(value), magic_enum::enum_name(value).data()); + g_logger().debug("Sending category number '{}', category name '{}'", static_cast(value), magic_enum::enum_name(value).data()); msg.addByte(static_cast(value)); msg.addString(toStartCaseWithSpace(magic_enum::enum_name(value).data())); } } + + /** + * @brief Calculates and adds the values for different skills based on the player's equipped items and other factors. + * + * This function calculates the total, equipment-based, imbuement-based, and wheel-based contributions to a specific skill + * of the player. These values are then added to the provided `NetworkMessage` object. + * + * @param[in] player The pointer to the player whose skills and equipment are considered. + * @param[in] msg The network message to which the calculated skill values will be added. + * @param[in] skill The specific skill to calculate (e.g., Life Leech, Mana Leech, Critical Hit Damage, etc.). + */ + void addCyclopediaSkills(std::shared_ptr &player, NetworkMessage &msg, skills_t skill) { + const auto skillTotal = player->getSkillLevel(skill); + const auto &playerItem = player->getInventoryItem(CONST_SLOT_LEFT); + double skillEquipment = 0.0; + if (playerItem) { + skillEquipment = playerItem->getSkill(skill); + } + + double skillWheel = 0.0; + const auto &playerWheel = player->wheel(); + if (skill == SKILL_LIFE_LEECH_AMOUNT) { + skillWheel = playerWheel.getStat(WheelStat_t::LIFE_LEECH); + } else if (skill == SKILL_MANA_LEECH_AMOUNT) { + skillWheel = playerWheel.getStat(WheelStat_t::MANA_LEECH); + } else if (skill == SKILL_CRITICAL_HIT_DAMAGE) { + skillWheel = playerWheel.getStat(WheelStat_t::CRITICAL_DAMAGE); + skillWheel += playerWheel.getMajorStatConditional("Combat Mastery", WheelMajor_t::CRITICAL_DMG_2); + skillWheel += playerWheel.getMajorStatConditional("Ballistic Mastery", WheelMajor_t::CRITICAL_DMG); + skillWheel += playerWheel.checkAvatarSkill(WheelAvatarSkill_t::CRITICAL_DAMAGE); + } + + double skillImbuement = skillTotal - skillEquipment - skillWheel; + + msg.addDouble(skillTotal / 10000.); + msg.addDouble(skillEquipment / 10000.); + msg.addDouble(skillImbuement / 10000.); + msg.addDouble(skillWheel / 10000.); + msg.addDouble(0.00); + } } // namespace ProtocolGame::ProtocolGame(const Connection_ptr &initConnection) : @@ -757,7 +802,13 @@ void ProtocolGame::onRecvFirstMessage(NetworkMessage &msg) { return; } - std::array key = { msg.get(), msg.get(), msg.get(), msg.get() }; + std::array key = { + msg.get(), + msg.get(), + msg.get(), + msg.get() + }; + enableXTEAEncryption(); setXTEAKey(key.data()); @@ -1325,10 +1376,10 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage &msg, uint8_t recvby parseVipGroupActions(msg); break; case 0xE1: - parseBestiarysendRaces(); + parseBestiarySendRaces(); break; case 0xE2: - parseBestiarysendCreatures(msg); + parseBestiarySendCreatures(msg); break; case 0xE3: parseBestiarysendMonsterData(msg); @@ -2185,7 +2236,7 @@ void ProtocolGame::parseCyclopediaCharacterInfo(NetworkMessage &msg) { page = std::max(1, msg.get()); } if (characterID == 0) { - characterID = player->getGUID(); + characterID = player->getID(); } g_game().playerCyclopediaCharacterInfo(player, characterID, characterInfoType, entriesPerPage, page); } @@ -2341,7 +2392,7 @@ void ProtocolGame::parseRuleViolationReport(NetworkMessage &msg) { g_game().playerReportRuleViolationReport(player->getID(), targetName, reportType, reportReason, comment, translation); } -void ProtocolGame::parseBestiarysendRaces() { +void ProtocolGame::parseBestiarySendRaces() { if (oldProtocol) { return; } @@ -2370,7 +2421,7 @@ void ProtocolGame::parseBestiarysendRaces() { } writeToOutputBuffer(msg); - player->BestiarysendCharms(); + player->sendBestiaryCharms(); } void ProtocolGame::sendBestiaryEntryChanged(uint16_t raceid) { @@ -2500,18 +2551,6 @@ void ProtocolGame::parseBestiarysendMonsterData(NetworkMessage &msg) { newmsg.addString(mtype->info.bestiaryLocations); } - if (currentLevel > 3) { - charmRune_t mType_c = g_iobestiary().getCharmFromTarget(player, mtype); - if (mType_c != CHARM_NONE) { - newmsg.addByte(1); - newmsg.addByte(mType_c); - newmsg.add(player->getLevel() * 100); - } else { - newmsg.addByte(0); - newmsg.addByte(1); - } - } - writeToOutputBuffer(newmsg); } @@ -2898,10 +2937,10 @@ void ProtocolGame::parseSendBuyCharmRune(NetworkMessage &msg) { return; } - auto runeID = static_cast(msg.getByte()); uint8_t action = msg.getByte(); - auto raceid = msg.get(); - g_iobestiary().sendBuyCharmRune(player, runeID, action, raceid); + auto charmId = static_cast(msg.getByte()); + uint16_t raceId = msg.get(); + g_iobestiary().sendBuyCharmRune(player, action, charmId, raceId); } void ProtocolGame::refreshCyclopediaMonsterTracker(const std::unordered_set> &trackerSet, bool isBoss) { @@ -2946,61 +2985,91 @@ void ProtocolGame::refreshCyclopediaMonsterTracker(const std::unordered_setgetLevel() * 100; + const auto playerLevel = player->getLevel(); + uint64_t resetAllCharmsCost = 100000 + (playerLevel > 100 ? playerLevel * 11000 : 0); if (player->hasCharmExpansion()) { - removeRuneCost = (removeRuneCost * 75) / 100; + resetAllCharmsCost = (resetAllCharmsCost * 75) / 100; } + NetworkMessage msg; msg.addByte(0xD8); - msg.add(player->getCharmPoints()); + msg.add(resetAllCharmsCost); - const auto charmList = g_game().getCharmList(); + const auto &charmList = g_game().getCharmList(); msg.addByte(charmList.size()); for (const auto &c_type : charmList) { msg.addByte(c_type->id); - msg.addString(c_type->name); - msg.addString(c_type->description); - msg.addByte(0); // Unknown - msg.add(c_type->points); + const auto &charmPoints = c_type->points; if (g_iobestiary().hasCharmUnlockedRuneBit(c_type, player->getUnlockedRunesBit())) { - msg.addByte(1); - uint16_t raceid = player->parseRacebyCharm(c_type->id, false, 0); - if (raceid > 0) { - msg.addByte(1); - msg.add(raceid); - msg.add(removeRuneCost); + const auto charmTier = player->getCharmTier(c_type->id); + msg.addByte(charmTier); + uint16_t raceId = player->parseRacebyCharm(c_type->id); + if (raceId > 0) { + msg.addByte(0x01); + msg.add(raceId); + uint32_t removeCharmCost = player->getLevel() * 100; + if (player->hasCharmExpansion()) { + removeCharmCost = (removeCharmCost * 75) / 100; + } + msg.add(removeCharmCost); } else { - msg.addByte(0); + msg.addByte(0x00); } } else { - msg.addByte(0); - msg.addByte(0); + msg.addByte(0x00); + msg.addByte(0x00); } } - msg.addByte(4); // Unknown - auto finishedMonstersSet = g_iobestiary().getBestiaryFinished(player); - for (charmRune_t charmRune : g_iobestiary().getCharmUsedRuneBitAll(player)) { - const auto tmpCharm = g_iobestiary().getBestiaryCharm(charmRune); - uint16_t tmp_raceid = player->parseRacebyCharm(tmpCharm->id, false, 0); + std::list usedCharms = g_iobestiary().getCharmUsedRuneBitAll(player); + uint8_t availableCharmSlots; + if (player->isPremium() && player->hasCharmExpansion()) { + availableCharmSlots = 0xFF; + } else { + uint8_t totalCharmSlots = player->isPremium() ? 6 : 2; + availableCharmSlots = totalCharmSlots - usedCharms.size(); + } + + msg.addByte(availableCharmSlots); - std::erase(finishedMonstersSet, tmp_raceid); + auto finishedMonstersSet = g_iobestiary().getBestiaryStageTwo(player); + std::unordered_map charmsAssigned; + for (charmRune_t charmRune : usedCharms) { + const auto &tmpCharm = g_iobestiary().getBestiaryCharm(charmRune); + if (!tmpCharm) { + continue; + } + + uint16_t tmpRaceId = player->parseRacebyCharm(tmpCharm->id); + charmsAssigned[tmpRaceId]++; + } + + for (const auto &[raceId, amount] : charmsAssigned) { + if (amount >= 2) { + std::erase(finishedMonstersSet, raceId); + } } msg.add(finishedMonstersSet.size()); - for (uint16_t raceid_tmp : finishedMonstersSet) { - msg.add(raceid_tmp); + for (uint16_t tmpRaceId : finishedMonstersSet) { + msg.add(tmpRaceId); } writeToOutputBuffer(msg); + sendCharmResourcesBalance( + player->getCharmPoints(), + player->getMinorCharmEchoes(), + player->getMaxCharmPoints(), + player->getMaxMinorCharmEchoes() + ); } -void ProtocolGame::parseBestiarysendCreatures(NetworkMessage &msg) { +void ProtocolGame::parseBestiarySendCreatures(NetworkMessage &msg) { if (!player || oldProtocol) { return; } @@ -3132,6 +3201,13 @@ void ProtocolGame::parseSendResourceBalance() { sliverCount, coreCount ); + + sendCharmResourcesBalance( + player->getCharmPoints(), + player->getMinorCharmEchoes(), + player->getMaxCharmPoints(), + player->getMaxMinorCharmEchoes() + ); } void ProtocolGame::parseInviteToParty(NetworkMessage &msg) { @@ -3584,162 +3660,6 @@ void ProtocolGame::sendCyclopediaCharacterGeneralStats() { writeToOutputBuffer(msg); } -void ProtocolGame::sendCyclopediaCharacterCombatStats() { - if (!player || oldProtocol) { - return; - } - - NetworkMessage msg; - msg.addByte(0xDA); - msg.addByte(CYCLOPEDIA_CHARACTERINFO_COMBATSTATS); - msg.addByte(0x00); - for (uint8_t i = SKILL_CRITICAL_HIT_CHANCE; i <= SKILL_LAST; ++i) { - if (i == SKILL_LIFE_LEECH_CHANCE || i == SKILL_MANA_LEECH_CHANCE) { - continue; - } - auto skill = static_cast(i); - msg.add(std::min(player->getSkillLevel(skill), std::numeric_limits::max())); - msg.add(0); - } - - // Version 12.81 new skill (Fatal, Dodge and Momentum) - sendForgeSkillStats(msg); - - // Cleave (12.70) - msg.add(static_cast(player->getCleavePercent())); - // Magic shield capacity (12.70) - msg.add(static_cast(player->getMagicShieldCapacityFlat())); // Direct bonus - msg.add(static_cast(player->getMagicShieldCapacityPercent())); // Percentage bonus - - // Perfect shot range (12.70) - for (uint8_t range = 1; range <= 5; range++) { - msg.add(static_cast(player->getPerfectShotDamage(range))); - } - - // Damage reflection (12.70) - msg.add(static_cast(player->getReflectFlat(COMBAT_PHYSICALDAMAGE))); - - uint8_t haveBlesses = 0; - for (auto bless : magic_enum::enum_values()) { - if (player->hasBlessing(enumToValue(bless))) { - ++haveBlesses; - } - } - - msg.addByte(haveBlesses); - msg.addByte(magic_enum::enum_count()); - - std::shared_ptr weapon = player->getWeapon(); - if (weapon) { - const ItemType &it = Item::items[weapon->getID()]; - if (it.weaponType == WEAPON_WAND) { - msg.add(it.maxHitChance); - msg.addByte(getCipbiaElement(it.combatType)); - msg.addByte(0); - msg.addByte(0); - } else if (it.weaponType == WEAPON_DISTANCE || it.weaponType == WEAPON_AMMO || it.weaponType == WEAPON_MISSILE) { - int32_t attackValue = weapon->getAttack(); - if (it.weaponType == WEAPON_AMMO) { - std::shared_ptr weaponItem = player->getWeapon(true); - if (weaponItem) { - attackValue += weaponItem->getAttack(); - } - } - - int32_t attackSkill = player->getSkillLevel(SKILL_DISTANCE); - float attackFactor = player->getAttackFactor(); - int32_t maxDamage = static_cast(Weapons::getMaxWeaponDamage(player->getLevel(), attackSkill, attackValue, attackFactor, true) * player->getVocation()->distDamageMultiplier); - if (it.abilities && it.abilities->elementType != COMBAT_NONE) { - maxDamage += static_cast(Weapons::getMaxWeaponDamage(player->getLevel(), attackSkill, attackValue - weapon->getAttack() + it.abilities->elementDamage, attackFactor, true) * player->getVocation()->distDamageMultiplier); - } - msg.add(maxDamage >> 1); - msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL); - if (it.abilities && it.abilities->elementType != COMBAT_NONE) { - if (attackValue) { - msg.addByte(static_cast(it.abilities->elementDamage) * 100 / attackValue); - } else { - msg.addByte(0); - } - msg.addByte(getCipbiaElement(it.abilities->elementType)); - } else { - handleImbuementDamage(msg, player); - } - } else { - int32_t attackValue = std::max(0, weapon->getAttack()); - int32_t attackSkill = player->getWeaponSkill(weapon); - float attackFactor = player->getAttackFactor(); - int32_t maxDamage = static_cast(Weapons::getMaxWeaponDamage(player->getLevel(), attackSkill, attackValue, attackFactor, true) * player->getVocation()->meleeDamageMultiplier); - if (it.abilities && it.abilities->elementType != COMBAT_NONE) { - maxDamage += static_cast(Weapons::getMaxWeaponDamage(player->getLevel(), attackSkill, it.abilities->elementDamage, attackFactor, true) * player->getVocation()->meleeDamageMultiplier); - } - msg.add(maxDamage >> 1); - msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL); - if (it.abilities && it.abilities->elementType != COMBAT_NONE) { - if (attackValue) { - msg.addByte(static_cast(it.abilities->elementDamage) * 100 / attackValue); - } else { - msg.addByte(0); - } - msg.addByte(getCipbiaElement(it.abilities->elementType)); - } else { - handleImbuementDamage(msg, player); - } - } - } else { - float attackFactor = player->getAttackFactor(); - int32_t attackSkill = player->getSkillLevel(SKILL_FIST); - int32_t attackValue = 7; - - int32_t maxDamage = Weapons::getMaxWeaponDamage(player->getLevel(), attackSkill, attackValue, attackFactor, true); - msg.add(maxDamage >> 1); - msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL); - msg.addByte(0); - msg.addByte(0); - } - - msg.add(player->getArmor()); - msg.add(player->getDefense()); - // Wheel of destiny mitigation - if (g_configManager().getBoolean(TOGGLE_WHEELSYSTEM)) { - msg.addDouble(player->getMitigation()); - } else { - msg.addDouble(0); - } - - // Store the "combats" to increase in absorb values function and send to client later - uint8_t combats = 0; - auto startCombats = msg.getBufferPosition(); - msg.skipBytes(1); - - // Calculate and parse the combat absorbs values - calculateAbsorbValues(player, msg, combats); - - // Now set the buffer position skiped and send the total combats count - auto endCombats = msg.getBufferPosition(); - msg.setBufferPosition(startCombats); - msg.addByte(combats); - msg.setBufferPosition(endCombats); - - // Concoctions potions (12.70) - auto startConcoctions = msg.getBufferPosition(); - msg.skipBytes(1); - auto activeConcoctions = player->getActiveConcoctions(); - uint8_t concoctions = 0; - for (const auto &concoction : activeConcoctions) { - if (concoction.second == 0) { - continue; - } - msg.add(concoction.first); - msg.add(concoction.second); - ++concoctions; - } - - msg.setBufferPosition(startConcoctions); - msg.addByte(concoctions); - - writeToOutputBuffer(msg); -} - void ProtocolGame::sendCyclopediaCharacterRecentDeaths(uint16_t page, uint16_t pages, const std::vector &entries) { if (!player || oldProtocol) { return; @@ -3808,7 +3728,7 @@ void ProtocolGame::sendCyclopediaCharacterAchievements(uint16_t secretsUnlocked, writeToOutputBuffer(msg); } -void ProtocolGame::sendCyclopediaCharacterItemSummary(const ItemsTierCountList &inventoryItems, const ItemsTierCountList &storeInboxItems, const StashItemList &supplyStashItems, const ItemsTierCountList &depotBoxItems, const ItemsTierCountList &inboxItems) { +void ProtocolGame::sendCyclopediaCharacterItemSummary(const ItemsTierCountList &inventoryItems, const ItemsTierCountList &storeInboxItems, const StashItemList &stashItems, const ItemsTierCountList &depotBoxItems, const ItemsTierCountList &inboxItems) { if (!player || oldProtocol) { return; } @@ -3866,10 +3786,14 @@ void ProtocolGame::sendCyclopediaCharacterItemSummary(const ItemsTierCountList & msg.setBufferPosition(endStoreInbox); - msg.add(supplyStashItems.size()); + msg.add(stashItems.size()); - for (const auto &[itemId, itemCount] : supplyStashItems) { + for (const auto &[itemId, itemCount] : stashItems) { + const ItemType &it = Item::items[itemId]; msg.add(itemId); + if (it.upgradeClassification > 0) { + msg.addByte(0x00); + } msg.add(itemCount); } @@ -4303,6 +4227,276 @@ void ProtocolGame::sendCyclopediaCharacterTitles() { writeToOutputBuffer(msg); } +void ProtocolGame::sendCyclopediaCharacterOffenceStats() { + if (!player || oldProtocol) { + return; + } + + NetworkMessage msg; + msg.addByte(0xDA); + msg.addByte(CYCLOPEDIA_CHARACTERINFO_OFFENCESTATS); + msg.addByte(0x00); // 0x00 Here means 'no error' + + msg.addDouble(player->getSkillLevel(SKILL_CRITICAL_HIT_CHANCE) / 10000.); // Crit Chance Total + msg.addDouble(0.00); + msg.addDouble(0.00); + msg.addDouble(0.00); + msg.addDouble(0.00); + + addCyclopediaSkills(player, msg, SKILL_CRITICAL_HIT_DAMAGE); + addCyclopediaSkills(player, msg, SKILL_LIFE_LEECH_AMOUNT); + addCyclopediaSkills(player, msg, SKILL_MANA_LEECH_AMOUNT); + + msg.addDouble(getForgeSkillStat(CONST_SLOT_LEFT)); // Onslaught Total + msg.addDouble(getForgeSkillStat(CONST_SLOT_LEFT, false)); + msg.addDouble(getForgeSkillStat(CONST_SLOT_LEFT) - getForgeSkillStat(CONST_SLOT_LEFT, false)); + msg.addDouble(0.00); + + msg.addDouble(player->getCleavePercent() / 100.); + + // Perfect shot range (12.70) + for (uint8_t range = 1; range <= 5; range++) { + msg.add(static_cast(player->getPerfectShotDamage(range))); + } + + const auto flatBonus = player->calculateFlatDamageHealing(); + msg.add(flatBonus); // Flat Damage and Healing Total + msg.add(flatBonus); + msg.add(0x00); + + const auto &weapon = player->getWeapon(); + if (weapon) { + const ItemType &it = Item::items[weapon->getID()]; + if (it.weaponType == WEAPON_WAND) { + msg.add(it.maxHitChance); + msg.add(0); + msg.add(0); + msg.addByte(0x00); + msg.add(0); + msg.add(0); + msg.addByte(getCipbiaElement(it.combatType)); + msg.addDouble(0.0); + msg.addByte(0x00); + msg.addByte(0x00); + } else if (it.weaponType == WEAPON_DISTANCE || it.weaponType == WEAPON_AMMO || it.weaponType == WEAPON_MISSILE) { + int32_t physicalAttack = std::max(0, weapon->getAttack()); + int32_t elementalAttack = 0; + if (it.abilities && it.abilities->elementType != COMBAT_NONE) { + elementalAttack = std::max(0, it.abilities->elementDamage); + } + int32_t attackValue = physicalAttack + elementalAttack; + if (it.weaponType == WEAPON_AMMO) { + std::shared_ptr weaponItem = player->getWeapon(true); + if (weaponItem) { + attackValue += weaponItem->getAttack(); + } + } + + int32_t distanceValue = player->getSkillLevel(SKILL_DISTANCE); + int32_t attackSkill = player->getDistanceAttackSkill(distanceValue, attackValue); + const auto attackRawTotal = player->attackRawTotal(flatBonus, attackValue, distanceValue); + const auto attackTotal = player->attackTotal(flatBonus, attackValue, distanceValue); + + msg.add(attackTotal); + msg.add(flatBonus); + msg.add(static_cast(attackValue)); + msg.addByte(0x07); + msg.add(attackSkill); + msg.add(attackTotal - attackRawTotal); + msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL); + + // Converted Damage + if (it.abilities && it.abilities->elementType != COMBAT_NONE) { + if (physicalAttack) { + msg.addDouble(elementalAttack / static_cast(attackValue)); + } else { + msg.addDouble(0.0); + } + msg.addByte(getCipbiaElement(it.abilities->elementType)); + } else { + handleImbuementDamage(msg, player); + } + + const auto distanceAccuracy = player->getDamageAccuracy(it); + const auto distanceAccuracySize = distanceAccuracy.size(); + msg.addByte(distanceAccuracy.size()); + for (uint8_t i = 0; i < distanceAccuracySize; ++i) { + msg.addByte(i + 1); + msg.addDouble(distanceAccuracy[i] / 100.); + } + } else { + int32_t physicalAttack = std::max(0, weapon->getAttack()); + int32_t elementalAttack = 0; + if (it.abilities && it.abilities->elementType != COMBAT_NONE) { + elementalAttack = std::max(0, it.abilities->elementDamage); + } + int32_t weaponAttack = physicalAttack + elementalAttack; + int32_t weaponSkill = player->getWeaponSkill(weapon); + int32_t attackSkill = player->getAttackSkill(weapon); + uint8_t skillId = player->getWeaponSkillId(weapon); + const auto attackRawTotal = player->attackRawTotal(flatBonus, weaponAttack, weaponSkill); + const auto attackTotal = player->attackTotal(flatBonus, weaponAttack, weaponSkill); + + msg.add(attackTotal); + msg.add(flatBonus); + msg.add(static_cast(weaponAttack)); + msg.addByte(skillId); + msg.add(attackSkill); + msg.add(attackTotal - attackRawTotal); + msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL); + + // Converted Damage + if (it.abilities && it.abilities->elementType != COMBAT_NONE) { + if (physicalAttack) { + msg.addDouble(elementalAttack / static_cast(weaponAttack)); + } else { + msg.addDouble(0); + } + msg.addByte(getCipbiaElement(it.abilities->elementType)); + } else { + handleImbuementDamage(msg, player); + } + msg.addByte(0x00); + } + } else { + uint16_t attackValue = 7; + int32_t fistValue = player->getSkillLevel(SKILL_FIST); + int32_t attackSkill = player->getDistanceAttackSkill(fistValue, attackValue); + const auto attackRawTotal = player->attackRawTotal(flatBonus, attackValue, fistValue); + const auto attackTotal = player->attackTotal(flatBonus, attackValue, fistValue); + + msg.add(attackTotal); + msg.add(flatBonus); + msg.add(attackValue); + msg.addByte(11); + msg.add(attackSkill); + msg.add(attackTotal - attackRawTotal); + msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL); + + msg.addDouble(0.0); + msg.addByte(0x00); + msg.addByte(0x00); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCyclopediaCharacterDefenceStats() { + if (!player || oldProtocol) { + return; + } + + NetworkMessage msg; + msg.addByte(0xDA); + msg.addByte(CYCLOPEDIA_CHARACTERINFO_DEFENCESTATS); + msg.addByte(0x00); // 0x00 Here means 'no error' + + const double dodgeTotal = getForgeSkillStat(CONST_SLOT_ARMOR) + player->wheel().getStat(WheelStat_t::DODGE); + msg.addDouble(dodgeTotal); + msg.addDouble(getForgeSkillStat(CONST_SLOT_ARMOR, false)); + msg.addDouble(getForgeSkillStat(CONST_SLOT_ARMOR) - getForgeSkillStat(CONST_SLOT_ARMOR, false)); + msg.addDouble(0.00); + msg.addDouble(player->wheel().getStat(WheelStat_t::DODGE)); + + msg.add(player->getMagicShieldCapacityFlat() * (1 + player->getMagicShieldCapacityPercent())); + msg.add(static_cast(player->getMagicShieldCapacityFlat())); // Direct bonus + msg.addDouble(player->getMagicShieldCapacityPercent()); // Percentage bonus + + msg.add(static_cast(player->getReflectFlat(COMBAT_PHYSICALDAMAGE))); + + msg.add(player->getArmor()); + + const auto shieldingSkill = player->getSkillLevel(SKILL_SHIELD); + const uint16_t defenseWheel = player->wheel().getMajorStatConditional("Combat Mastery", WheelMajor_t::DEFENSE); + msg.add(player->getDefense(true)); + msg.add(player->getDefenseEquipment()); + msg.addByte(0x06); + msg.add(shieldingSkill); + msg.add(defenseWheel); + msg.add(0); + + const auto wheelMultiplier = player->wheel().getMitigationMultiplier(); + msg.addDouble(player->getMitigation() / 100.); + msg.addDouble(0.0); + msg.addDouble(player->getDefenseEquipment() / 10000.); + msg.addDouble(player->getSkillLevel(SKILL_SHIELD) * player->getVocation()->mitigationFactor / 10000.); + msg.addDouble(wheelMultiplier / 100.); + msg.addDouble(player->getCombatTacticsMitigation()); + + // Store the "combats" to increase in absorb values function and send to client later + uint8_t combats = 0; + auto startCombats = msg.getBufferPosition(); + msg.skipBytes(1); + + // Calculate and parse the combat absorbs values + calculateAbsorbValues(player, msg, combats); + + // Now set the buffer position skiped and send the total combats count + auto endCombats = msg.getBufferPosition(); + msg.setBufferPosition(startCombats); + msg.addByte(combats); + msg.setBufferPosition(endCombats); + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCyclopediaCharacterMiscStats() { + if (!player || oldProtocol) { + return; + } + + NetworkMessage msg; + msg.addByte(0xDA); + msg.addByte(CYCLOPEDIA_CHARACTERINFO_MISCSTATS); + msg.addByte(0x00); // 0x00 Here means 'no error' + + const double momentumTotal = getForgeSkillStat(CONST_SLOT_HEAD) + player->wheel().getBonusData().momentum; + msg.addDouble(momentumTotal); + msg.addDouble(getForgeSkillStat(CONST_SLOT_HEAD, false)); + msg.addDouble(getForgeSkillStat(CONST_SLOT_HEAD) - getForgeSkillStat(CONST_SLOT_HEAD, false)); + msg.addDouble(player->wheel().getBonusData().momentum); + msg.addDouble(0.00); + + msg.addDouble(getForgeSkillStat(CONST_SLOT_LEGS)); + msg.addDouble(getForgeSkillStat(CONST_SLOT_LEGS), false); + msg.addDouble(getForgeSkillStat(CONST_SLOT_LEGS) - getForgeSkillStat(CONST_SLOT_LEGS, false)); + msg.addDouble(0.09); + + msg.addDouble(getForgeSkillStat(CONST_SLOT_FEET, false)); + msg.addDouble(getForgeSkillStat(CONST_SLOT_FEET, false)); + msg.addDouble(0.00); + + uint8_t haveBlesses = 0; + for (auto bless : magic_enum::enum_values()) { + if (bless == Blessings::TwistOfFate) { + continue; + } + + if (player->hasBlessing(enumToValue(bless))) { + ++haveBlesses; + } + } + + msg.addByte(haveBlesses); + msg.addByte(magic_enum::enum_count() - 1); // Skip Twist of Fate + + auto activeConcoctions = player->getActiveConcoctions(); + msg.addByte(activeConcoctions.size()); + for (const auto &concoction : activeConcoctions) { + if (concoction.second == 0) { + continue; + } + msg.add(concoction.first); + msg.addByte(0x00); + msg.addByte(0x00); + msg.add(concoction.second); + } + + msg.addByte(0x00); + + writeToOutputBuffer(msg); +} + void ProtocolGame::sendReLoginWindow(uint8_t unfairFightReduction) { NetworkMessage msg; msg.addByte(0x28); @@ -5004,6 +5198,27 @@ void ProtocolGame::sendResourceBalance(Resource_t resourceType, uint64_t value) writeToOutputBuffer(msg); } +void ProtocolGame::sendCharmResourcesBalance(uint32_t charm /*= 0*/, uint32_t minorCharm /*= 0*/, uint32_t maxCharm /*= 0*/, uint32_t maxMinorCharm /*= 0*/) { + sendCharmResourceBalance(RESOURCE_CHARM, charm); + sendCharmResourceBalance(RESOURCE_MINOR_CHARM, minorCharm); + sendCharmResourceBalance(RESOURCE_MAX_CHARM, maxCharm); + sendCharmResourceBalance(RESOURCE_MAX_MINOR_CHARM, maxMinorCharm); +}; + +void ProtocolGame::sendCharmResourceBalance(CharmResource_t resourceType, uint32_t value) { + if (oldProtocol) { + return; + } + + NetworkMessage msg; + msg.addByte(0xEE); + + msg.addByte(resourceType); + msg.add(value); + + writeToOutputBuffer(msg); +} + void ProtocolGame::sendSaleItemList(const std::vector &shopVector, const std::map &inventoryMap) { sendResourceBalance(RESOURCE_BANK, player->getBankBalance()); @@ -6140,7 +6355,7 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { msg.add(0x00); // Magic shield capacity msg.add(0x00); - // Damage reflection modifie + // Damage reflection modifier msg.add(0x00); // Perfect shot modifier msg.add(0x00); @@ -6153,14 +6368,20 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { double chance; if (it.isWeapon()) { - chance = 0.5 * tier + 0.05 * ((tier - 1) * (tier - 1)); + chance = (0.05 * tier * tier) + (0.4 * tier) + 0.05; ss << fmt::format("{} ({:.2f}% Onslaught)", static_cast(tier), chance); } else if (it.isHelmet()) { - chance = 2 * tier + 0.05 * ((tier - 1) * (tier - 1)); + chance = (0.05 * tier * tier) + (1.9 * tier) + 0.05; ss << fmt::format("{} ({:.2f}% Momentum)", static_cast(tier), chance); } else if (it.isArmor()) { chance = (0.0307576 * tier * tier) + (0.440697 * tier) + 0.026; ss << fmt::format("{} ({:.2f}% Ruse)", static_cast(tier), chance); + } else if (it.isLegs()) { + chance = (0.0127 * tier * tier) + (0.1070 * tier) + 0.0073; + ss << fmt::format("{} ({:.2f}% Transcendence)", static_cast(tier), chance); + } else if (it.isBoots()) { + chance = (0.4 * tier * tier) + (1.7 * tier) + 0.4; + ss << fmt::format("{} ({:.2f}% Amplification)", static_cast(tier), chance); } msg.addString(ss.str()); } else if (it.upgradeClassification > 0 && tier == 0) { @@ -7997,25 +8218,123 @@ void ProtocolGame::AddPlayerSkills(NetworkMessage &msg) { } } - for (uint8_t i = SKILL_CRITICAL_HIT_CHANCE; i <= SKILL_LAST; ++i) { - if (!oldProtocol && (i == SKILL_LIFE_LEECH_CHANCE || i == SKILL_MANA_LEECH_CHANCE)) { - continue; + // 13.10 List (U8 + U16) + msg.addByte(0); + + // Used for Imbuement (Feather) + msg.add(player->getCapacity()); // Total Capacity + msg.add(player->getBaseCapacity()); // Base Total Capacity + + const auto flatBonus = player->calculateFlatDamageHealing(); + msg.add(flatBonus); // Flat Damage and Healing Total + + const auto &weapon = player->getWeapon(); + if (weapon) { + const ItemType &it = Item::items[weapon->getID()]; + if (it.weaponType == WEAPON_WAND) { + msg.add(it.maxHitChance); + msg.addByte(getCipbiaElement(it.combatType)); + msg.addDouble(0.0); + msg.addByte(0x00); + } else if (it.weaponType == WEAPON_DISTANCE || it.weaponType == WEAPON_AMMO || it.weaponType == WEAPON_MISSILE) { + int32_t physicalAttack = std::max(0, weapon->getAttack()); + int32_t elementalAttack = 0; + if (it.abilities && it.abilities->elementType != COMBAT_NONE) { + elementalAttack = std::max(0, it.abilities->elementDamage); + } + int32_t attackValue = physicalAttack + elementalAttack; + if (it.weaponType == WEAPON_AMMO) { + std::shared_ptr weaponItem = player->getWeapon(true); + if (weaponItem) { + attackValue += weaponItem->getAttack(); + } + } + + int32_t distanceValue = player->getSkillLevel(SKILL_DISTANCE); + const auto attackTotal = player->attackTotal(flatBonus, attackValue, distanceValue); + + msg.add(attackTotal); + msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL); + + // Converted Damage + if (it.abilities && it.abilities->elementType != COMBAT_NONE) { + if (physicalAttack) { + msg.addDouble(elementalAttack / static_cast(attackValue)); + } else { + msg.addDouble(0.0); + } + msg.addByte(getCipbiaElement(it.abilities->elementType)); + } else { + handleImbuementDamage(msg, player); + } + } else { + int32_t physicalAttack = std::max(0, weapon->getAttack()); + int32_t elementalAttack = 0; + if (it.abilities && it.abilities->elementType != COMBAT_NONE) { + elementalAttack = std::max(0, it.abilities->elementDamage); + } + int32_t weaponAttack = physicalAttack + elementalAttack; + int32_t weaponSkill = player->getWeaponSkill(weapon); + const auto attackTotal = player->attackTotal(flatBonus, weaponAttack, weaponSkill); + + msg.add(attackTotal); + msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL); + + // Converted Damage + if (it.abilities && it.abilities->elementType != COMBAT_NONE) { + if (physicalAttack) { + msg.addDouble(elementalAttack / static_cast(weaponAttack)); + } else { + msg.addDouble(0); + } + msg.addByte(getCipbiaElement(it.abilities->elementType)); + } else { + handleImbuementDamage(msg, player); + } } - auto skill = static_cast(i); - msg.add(std::min(player->getSkillLevel(skill), std::numeric_limits::max())); - msg.add(player->getBaseSkill(skill)); - } + } else { + uint16_t attackValue = 7; + int32_t fistValue = player->getSkillLevel(SKILL_FIST); + const auto attackTotal = player->attackTotal(flatBonus, attackValue, fistValue); - if (!oldProtocol) { - // 13.10 list (U8 + U16) - msg.addByte(0); - // Version 12.81 new skill (Fatal, Dodge and Momentum) - sendForgeSkillStats(msg); + msg.add(attackTotal); + msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL); - // used for imbuement (Feather) - msg.add(player->getCapacity()); // total capacity - msg.add(player->getBaseCapacity()); // base total capacity + msg.addDouble(0.0); + msg.addByte(0x00); } + + // Imbuements + msg.addDouble(player->getSkillLevel(SKILL_LIFE_LEECH_AMOUNT) / 10000.); // Life Leech + msg.addDouble(player->getSkillLevel(SKILL_MANA_LEECH_AMOUNT) / 10000.); // Mana Leech + msg.addDouble(player->getSkillLevel(SKILL_CRITICAL_HIT_CHANCE) / 10000.); // Crit Chance + msg.addDouble(player->getSkillLevel(SKILL_CRITICAL_HIT_DAMAGE) / 10000.); // Crit Extra Damage + msg.addDouble(getForgeSkillStat(CONST_SLOT_LEFT)); // Onslaught + + msg.add(player->getDefense(true)); + msg.add(player->getArmor()); + msg.addDouble(player->getMitigation() / 100.); // Mitigation + msg.addDouble(getForgeSkillStat(CONST_SLOT_ARMOR)); // Dodge (Ruse) + msg.add(static_cast(player->getReflectFlat(COMBAT_PHYSICALDAMAGE))); // Damage Reflection + + // Store the "combats" to increase in absorb values function and send to client later + uint8_t combats = 0; + auto startCombats = msg.getBufferPosition(); + msg.skipBytes(1); + + // Calculate and parse the combat absorbs values + calculateAbsorbValues(player, msg, combats, true); + + // Now set the buffer position skiped and send the total combats count + auto endCombats = msg.getBufferPosition(); + msg.setBufferPosition(startCombats); + msg.addByte(combats); + msg.setBufferPosition(endCombats); + + // Forge Bonus + msg.addDouble(getForgeSkillStat(CONST_SLOT_HEAD)); // Momentum + msg.addDouble(getForgeSkillStat(CONST_SLOT_LEGS)); // Transcedence + msg.addDouble(getForgeSkillStat(CONST_SLOT_FEET, false)); // Amplification } void ProtocolGame::AddOutfit(NetworkMessage &msg, const Outfit_t &outfit, bool addMount /* = true*/) { @@ -8168,7 +8487,7 @@ void ProtocolGame::sendSpecialContainersAvailable() { NetworkMessage msg; msg.addByte(0x2A); - msg.addByte(player->isSupplyStashMenuAvailable() ? 0x01 : 0x00); + msg.addByte(player->isStashMenuAvailable() ? 0x01 : 0x00); msg.addByte(player->isMarketMenuAvailable() ? 0x01 : 0x00); writeToOutputBuffer(msg); } @@ -8710,8 +9029,8 @@ void ProtocolGame::parseStashWithdraw(NetworkMessage &msg) { return; } - if (!player->isAccessPlayer() && !player->isSupplyStashMenuAvailable()) { - player->sendCancelMessage("You can't use supply stash right now."); + if (!player->isAccessPlayer() && !player->isStashMenuAvailable()) { + player->sendCancelMessage("You can't use stash right now."); return; } @@ -8720,9 +9039,9 @@ void ProtocolGame::parseStashWithdraw(NetworkMessage &msg) { return; } - auto action = static_cast(msg.getByte()); + auto action = static_cast(msg.getByte()); switch (action) { - case SUPPLY_STASH_ACTION_STOW_ITEM: { + case STASH_ACTION_STOW_ITEM: { Position pos = msg.getPosition(); auto itemId = msg.get(); uint8_t stackpos = msg.getByte(); @@ -8730,21 +9049,21 @@ void ProtocolGame::parseStashWithdraw(NetworkMessage &msg) { g_game().playerStowItem(player->getID(), pos, itemId, stackpos, count, false); break; } - case SUPPLY_STASH_ACTION_STOW_CONTAINER: { + case STASH_ACTION_STOW_CONTAINER: { Position pos = msg.getPosition(); auto itemId = msg.get(); uint8_t stackpos = msg.getByte(); g_game().playerStowItem(player->getID(), pos, itemId, stackpos, 0, false); break; } - case SUPPLY_STASH_ACTION_STOW_STACK: { + case STASH_ACTION_STOW_STACK: { Position pos = msg.getPosition(); auto itemId = msg.get(); uint8_t stackpos = msg.getByte(); g_game().playerStowItem(player->getID(), pos, itemId, stackpos, 0, true); break; } - case SUPPLY_STASH_ACTION_WITHDRAW: { + case STASH_ACTION_WITHDRAW: { auto itemId = msg.get(); auto count = msg.get(); uint8_t stackpos = msg.getByte(); @@ -8752,7 +9071,7 @@ void ProtocolGame::parseStashWithdraw(NetworkMessage &msg) { break; } default: - g_logger().error("Unknown 'supply stash' action switch: {}", fmt::underlying(action)); + g_logger().error("Unknown 'stash' action switch: {}", fmt::underlying(action)); break; } @@ -8937,20 +9256,26 @@ void ProtocolGame::sendForgeSkillStats(NetworkMessage &msg) const { std::vector slots { CONST_SLOT_LEFT, CONST_SLOT_ARMOR, CONST_SLOT_HEAD, CONST_SLOT_LEGS }; for (const auto &slot : slots) { double_t skill = 0; - if (const auto &item = player->getInventoryItem(slot); item) { - const ItemType &it = Item::items[item->getID()]; - if (it.isWeapon()) { - skill = item->getFatalChance() * 100; - } - if (it.isArmor()) { - skill = item->getDodgeChance() * 100; - } - if (it.isHelmet()) { - skill = item->getMomentumChance() * 100; - } - if (it.isLegs()) { - skill = item->getTranscendenceChance() * 100; - } + const auto &item = player->getInventoryItem(slot); + if (!item) { + continue; + } + + const ItemType &it = Item::items[item->getID()]; + if (it.isWeapon()) { + skill = item->getFatalChance() * 100; + } + if (it.isArmor()) { + skill = item->getDodgeChance() * 100; + } + if (it.isHelmet()) { + skill = item->getMomentumChance() * 100; + } + if (it.isLegs()) { + skill = item->getTranscendenceChance() * 100; + } + if (it.isBoots()) { + skill = item->getAmplificationChance(); } auto skillCast = static_cast(skill); @@ -8959,6 +9284,41 @@ void ProtocolGame::sendForgeSkillStats(NetworkMessage &msg) const { } } +double ProtocolGame::getForgeSkillStat(Slots_t slot, bool applyAmplification /*= true*/) const { + if (oldProtocol) { + return 0; + } + + double skill = 0; + if (const auto &item = player->getInventoryItem(slot); item) { + const ItemType &it = Item::items[item->getID()]; + if (it.isWeapon()) { + skill = item->getFatalChance(); + } + if (it.isArmor()) { + skill = item->getDodgeChance(); + } + if (it.isHelmet()) { + skill = item->getMomentumChance(); + } + if (it.isLegs()) { + skill = item->getTranscendenceChance(); + } + if (it.isBoots()) { + skill = item->getAmplificationChance(); + } + } + + if (applyAmplification) { + const auto &boots = player->getInventoryItem(CONST_SLOT_FEET); + if (slot != CONST_SLOT_FEET && boots) { + skill *= 1 + (boots->getAmplificationChance() / 100); + } + } + + return skill / 100; +} + void ProtocolGame::sendBosstiaryData() { if (oldProtocol) { return; diff --git a/src/server/network/protocol/protocolgame.hpp b/src/server/network/protocol/protocolgame.hpp index 413dbb21b7d..b0d3e045e23 100644 --- a/src/server/network/protocol/protocolgame.hpp +++ b/src/server/network/protocol/protocolgame.hpp @@ -26,6 +26,7 @@ enum Resource_t : uint8_t; enum class VipStatus_t : uint8_t; enum SpellGroup_t : uint8_t; enum Slots_t : uint8_t; +enum skills_t : int8_t; enum CombatType_t : uint8_t; enum SoundEffect_t : uint16_t; enum class SourceEffect_t : uint8_t; @@ -181,9 +182,9 @@ class ProtocolGame final : public Protocol { void parseSendResourceBalance(); void parseRuleViolationReport(NetworkMessage &msg); - void parseBestiarysendRaces(); - void parseBestiarysendCreatures(NetworkMessage &msg); - void BestiarysendCharms(); + void parseBestiarySendRaces(); + void parseBestiarySendCreatures(NetworkMessage &msg); + void sendBestiaryCharms(); void sendBestiaryEntryChanged(uint16_t raceid); void refreshCyclopediaMonsterTracker(const std::unordered_set> &trackerSet, bool isBoss); void sendTeamFinderList(); @@ -295,6 +296,7 @@ class ProtocolGame final : public Protocol { void sendForgeResult(ForgeAction_t actionType, uint16_t leftItemId, uint8_t leftTier, uint16_t rightItemId, uint8_t rightTier, bool success, uint8_t bonus, uint8_t coreCount, bool convergence); void sendForgeHistory(uint8_t page); void sendForgeSkillStats(NetworkMessage &msg) const; + double getForgeSkillStat(Slots_t slot, bool applyAmplification = true) const; void sendBosstiaryData(); void parseSendBosstiary(); @@ -344,16 +346,18 @@ class ProtocolGame final : public Protocol { void sendCyclopediaCharacterNoData(CyclopediaCharacterInfoType_t characterInfoType, uint8_t errorCode); void sendCyclopediaCharacterBaseInformation(); void sendCyclopediaCharacterGeneralStats(); - void sendCyclopediaCharacterCombatStats(); void sendCyclopediaCharacterRecentDeaths(uint16_t page, uint16_t pages, const std::vector &entries); void sendCyclopediaCharacterRecentPvPKills(uint16_t page, uint16_t pages, const std::vector &entries); void sendCyclopediaCharacterAchievements(uint16_t secretsUnlocked, const std::vector> &achievementsUnlocked); - void sendCyclopediaCharacterItemSummary(const ItemsTierCountList &inventoryItems, const ItemsTierCountList &storeInboxItems, const StashItemList &supplyStashItems, const ItemsTierCountList &depotBoxItems, const ItemsTierCountList &inboxItems); + void sendCyclopediaCharacterItemSummary(const ItemsTierCountList &inventoryItems, const ItemsTierCountList &storeInboxItems, const StashItemList &stashItems, const ItemsTierCountList &depotBoxItems, const ItemsTierCountList &inboxItems); void sendCyclopediaCharacterOutfitsMounts(); void sendCyclopediaCharacterStoreSummary(); void sendCyclopediaCharacterInspection(); void sendCyclopediaCharacterBadges(); void sendCyclopediaCharacterTitles(); + void sendCyclopediaCharacterOffenceStats(); + void sendCyclopediaCharacterDefenceStats(); + void sendCyclopediaCharacterMiscStats(); void sendHousesInfo(); void parseCyclopediaHouseAuction(NetworkMessage &msg); @@ -372,6 +376,8 @@ class ProtocolGame final : public Protocol { void sendGameNews(); void sendResourcesBalance(uint64_t money = 0, uint64_t bank = 0, uint64_t preyCards = 0, uint64_t taskHunting = 0, uint64_t forgeDust = 0, uint64_t forgeSliver = 0, uint64_t forgeCores = 0); void sendResourceBalance(Resource_t resourceType, uint64_t value); + void sendCharmResourcesBalance(uint32_t charm = 0, uint32_t minorCharm = 0, uint32_t maxCharm = 0, uint32_t maxMinorCharm = 0); + void sendCharmResourceBalance(CharmResource_t resourceType, uint32_t value); void sendSaleItemList(const std::vector &shopVector, const std::map &inventoryMap); void sendMarketEnter(uint32_t depotId); void updateCoinBalance(); diff --git a/src/server/server_definitions.hpp b/src/server/server_definitions.hpp index 2a8d55e2c89..92068266db0 100644 --- a/src/server/server_definitions.hpp +++ b/src/server/server_definitions.hpp @@ -72,6 +72,13 @@ enum Resource_t : uint8_t { RESOURCE_WHEEL_OF_DESTINY = 0x56 }; +enum CharmResource_t : uint8_t { + RESOURCE_CHARM = 0x1E, + RESOURCE_MINOR_CHARM = 0x1F, + RESOURCE_MAX_CHARM = 0x20, + RESOURCE_MAX_MINOR_CHARM = 0x21 +}; + enum InspectObjectTypes : uint8_t { INSPECT_NORMALOBJECT = 0, INSPECT_NPCTRADE = 1, @@ -110,11 +117,11 @@ enum ImpactAnalyzerAndTracker_t : uint8_t { ANALYZER_DAMAGE_RECEIVED = 2 }; -enum Supply_Stash_Actions_t : uint8_t { - SUPPLY_STASH_ACTION_STOW_ITEM = 0, - SUPPLY_STASH_ACTION_STOW_CONTAINER = 1, - SUPPLY_STASH_ACTION_STOW_STACK = 2, - SUPPLY_STASH_ACTION_WITHDRAW = 3 +enum Stash_Actions_t : uint8_t { + STASH_ACTION_STOW_ITEM = 0, + STASH_ACTION_STOW_CONTAINER = 1, + STASH_ACTION_STOW_STACK = 2, + STASH_ACTION_WITHDRAW = 3 }; struct HighscoreCharacter { diff --git a/src/utils/utils_definitions.hpp b/src/utils/utils_definitions.hpp index 90dbf09f5eb..0ce48f6662e 100644 --- a/src/utils/utils_definitions.hpp +++ b/src/utils/utils_definitions.hpp @@ -491,7 +491,7 @@ enum NameEval_t : uint8_t { enum ItemID_t : uint16_t { ITEM_BROWSEFIELD = 470, // for internal use - ITEM_SUPPLY_STASH_INDEX = 1, // for internal use + ITEM_STASH_INDEX = 1, // for internal use ITEM_DEPOT_NULL = 22796, // for internal use - Actual Item ID: 168 ITEM_DECORATION_KIT = 23398, // For internal use (wrap item) ITEM_DOCUMENT_RO = 2834, // Read-only @@ -562,7 +562,7 @@ enum ItemID_t : uint16_t { ITEM_INBOX = 12902, ITEM_MARKET = 12903, ITEM_STORE_INBOX = 23396, - ITEM_SUPPLY_STASH = 28750, + ITEM_STASH = 28750, ITEM_MALE_CORPSE = 4240, ITEM_FEMALE_CORPSE = 4247,