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