From 4dc5020dad29250b9464570a134a571b75391e84 Mon Sep 17 00:00:00 2001 From: MrSent Date: Thu, 12 Feb 2026 23:23:58 +0000 Subject: [PATCH 1/5] Update Summoner and Pet functionality - Update pets.lua and mob_pool_mods.sql: Add Atomos spawn logic - Update Pet claiming and failed BP re-engage --- .../actions/abilities/pets/chronoshift.lua | 50 +++++++++++++++++++ .../actions/abilities/pets/deconstruction.lua | 42 ++++++++++++++++ scripts/actions/spells/summoning/atomos.lua | 18 +++++++ scripts/enum/msg.lua | 2 + scripts/globals/job_utils/summoner.lua | 1 + scripts/globals/pets.lua | 15 ++++++ sql/abilities.sql | 4 +- sql/mob_family_system.sql | 4 +- sql/mob_pools.sql | 4 +- sql/mob_resistances.sql | 3 +- sql/mob_skill_lists.sql | 3 ++ sql/pet_list.sql | 2 +- sql/pet_skills.sql | 4 +- sql/spell_list.sql | 2 +- src/map/ai/controllers/mob_controller.cpp | 5 +- src/map/ai/controllers/pet_controller.cpp | 1 + src/map/ai/states/magic_state.cpp | 19 ++++--- src/map/ai/states/petskill_state.cpp | 14 ++++++ src/map/ai/states/petskill_state.h | 1 + src/map/entities/battleentity.cpp | 34 +++++++++++-- src/map/entities/charentity.cpp | 4 +- src/map/entities/petentity.cpp | 2 +- src/map/utils/battleutils.cpp | 17 +++++-- src/map/utils/charutils.cpp | 6 +-- src/map/utils/petutils.cpp | 6 +++ 25 files changed, 227 insertions(+), 36 deletions(-) create mode 100644 scripts/actions/abilities/pets/chronoshift.lua create mode 100644 scripts/actions/abilities/pets/deconstruction.lua create mode 100644 scripts/actions/spells/summoning/atomos.lua diff --git a/scripts/actions/abilities/pets/chronoshift.lua b/scripts/actions/abilities/pets/chronoshift.lua new file mode 100644 index 00000000000..617c6160b01 --- /dev/null +++ b/scripts/actions/abilities/pets/chronoshift.lua @@ -0,0 +1,50 @@ +----------------------------------- +-- Chronoshift +----------------------------------- +---@type TAbilityPet +local abilityObject = {} + +abilityObject.onAbilityCheck = function(player, target, ability) + return 0, 0 +end + +abilityObject.onPetAbility = function(target, pet, petskill, summoner, action) + if summoner == nil then + return 0 + end + + local effect = nil + local effectID = 0 + local effectCount = 0 + + -- Retrive the absorbed effect's ID + effectID = pet:getLocalVar('aEffectID') + -- Find that effect on Atomos + effect = pet:getStatusEffect(effectID) + + if effect then + local effectIcon = effect:getIcon() + local power = effect:getPower() + local duration = effect:getDuration() / 1000 + -- Delete old effect if on target already + target:delStatusEffectSilent(effectID) + -- Add the stolen effect to the party + target:addStatusEffectEx(effectID, effectIcon, power, 0, duration, true) + petskill:setMsg(xi.msg.basic.RECEIVE_MAGICAL_EFFECT) + effectCount = 1 + else + petskill:setMsg(xi.msg.basic.NO_EFFECT) + end + + target:addEnmity(pet, 1, 60) + + pet:timer(5000, function() + if summoner then + summoner:despawnPet() + end + end) + + return effectCount +end + +return abilityObject diff --git a/scripts/actions/abilities/pets/deconstruction.lua b/scripts/actions/abilities/pets/deconstruction.lua new file mode 100644 index 00000000000..c889ea2cfb9 --- /dev/null +++ b/scripts/actions/abilities/pets/deconstruction.lua @@ -0,0 +1,42 @@ +----------------------------------- +-- Deconstruction +----------------------------------- +---@type TAbilityPet +local abilityObject = {} + +abilityObject.onAbilityCheck = function(player, target, ability) + return 0, 0 +end + +abilityObject.onPetAbility = function(target, pet, petskill, summoner, action) + if summoner == nil then + return 0 + end + + local effectCount = 0 + local effectID = 0 + + effectID = pet:stealStatusEffect(target) + + local newStatus = pet:getStatusEffect(effectID) + + if newStatus then + -- Store the stolen effect ID for Chronoshift so we know what effect to transfer. + pet:setLocalVar('aEffectID', effectID) + target:delStatusEffectSilent(effectID) + petskill:setMsg(xi.msg.basic.MAGIC_EFFECT_DRAINED) + effectCount = 1 + else + petskill:setMsg(xi.msg.basic.JA_NO_EFFECT_2) + end + + target:addEnmity(pet, 1, 60) + + if summoner then + target:addEnmity(summoner, 1, 0) -- this is to ensure you cannot cheese mobs with this, mob goes passive if not added + end + + return effectCount +end + +return abilityObject diff --git a/scripts/actions/spells/summoning/atomos.lua b/scripts/actions/spells/summoning/atomos.lua new file mode 100644 index 00000000000..fc217c8adc9 --- /dev/null +++ b/scripts/actions/spells/summoning/atomos.lua @@ -0,0 +1,18 @@ +----------------------------------- +-- Spell: Atomos +-- Summons Atomos to fight by your side +----------------------------------- +---@type TSpell +local spellObject = {} + +spellObject.onMagicCastingCheck = function(caster, target, spell) + return xi.pet.onCastingCheck(caster, target, spell) +end + +spellObject.onSpellCast = function(caster, target, spell) + xi.pet.spawnPet(caster, xi.petId.ATOMOS, spell, target) + + return 0 +end + +return spellObject diff --git a/scripts/enum/msg.lua b/scripts/enum/msg.lua index 0b66057aa24..3b7215dab21 100644 --- a/scripts/enum/msg.lua +++ b/scripts/enum/msg.lua @@ -236,6 +236,8 @@ xi.msg.basic = STEAL_EFFECT = 453, -- uses . steals the effect of from . REQUIRES_COMBAT = 525, -- .. can only be performed during battle. STATUS_PREVENTS = 569, -- Your current status prevents you from using that ability. + MAGIC_EFFECT_DRAINED = 737, -- of the 's magic effects is drained. most likely only used for Atomos (can only absorb 1 effect) + RECEIVE_MAGICAL_EFFECT = 739, -- receives magical effect. most likely only used for Atomos (can only absorb 1 effect) OBTAINED_KEY_ITEM = 758, -- Obtained key item: . ALREADY_HAVE_KEY_ITEM = 759, -- You already have key item: . diff --git a/scripts/globals/job_utils/summoner.lua b/scripts/globals/job_utils/summoner.lua index 13c8c586432..e06010900be 100644 --- a/scripts/globals/job_utils/summoner.lua +++ b/scripts/globals/job_utils/summoner.lua @@ -216,6 +216,7 @@ xi.job_utils.summoner.onUseBloodPact = function(target, petskill, summoner, acti if target:isMob() then target:addBaseEnmity(summoner) + --target:addEnmity(summoner, 1, 60) end if summoner:hasStatusEffect(xi.effect.APOGEE) then diff --git a/scripts/globals/pets.lua b/scripts/globals/pets.lua index e4b3f9f8557..33a8a043c8a 100644 --- a/scripts/globals/pets.lua +++ b/scripts/globals/pets.lua @@ -118,6 +118,21 @@ xi.pet.spawnPet = function(caster, petID, state, target) elseif petID == xi.petId.ODIN then if target then caster:petAttack(target) + --pet:timer(5000, function() + -- pet:usePetAbility(xi.jobAbility.ZANTETSUKEN, target) + --end) + end + elseif petID == xi.petId.ATOMOS then + if target then + -- Use Deconstruction on the target 3 seconds after spawning. + local pet = caster:getPet() + if pet then + pet:setAutoAttackEnabled(false) -- stops claiming, Atomos does not claim mobs on retail. + pet:setMobMod(xi.mobMod.NO_MOVE, 1) -- Atomos is stubborn and will still chase a mob unless this is set. + -- Timed sequence after spawning, wait -> Deconstruction -> wait -> Chronoshift (despawn pet after complete) + pet:timer(3000, function() pet:usePetAbility(xi.jobAbility.DECONSTRUCTION, target) end) + pet:timer(10000, function() pet:usePetAbility(xi.jobAbility.CHRONOSHIFT, pet) end) + end end end end diff --git a/sql/abilities.sql b/sql/abilities.sql index e982b0132bd..31d6ad1e06a 100644 --- a/sql/abilities.sql +++ b/sql/abilities.sql @@ -521,8 +521,8 @@ INSERT INTO `abilities` VALUES (664,'ruinous_omen',15,1,4,60,173,0,0,94,2000,0,6 INSERT INTO `abilities` VALUES (665,'night_terror',15,80,4,60,173,0,0,94,2000,0,6,5,0,0,1,60,0,0,NULL); INSERT INTO `abilities` VALUES (666,'pavor_nocturnus',15,98,4,60,174,0,0,94,2000,0,6,12,0,0,1,60,0,0,NULL); INSERT INTO `abilities` VALUES (667,'blindside',15,99,4,60,173,0,0,94,2000,0,6,10,0,0,1,60,0,0,NULL); --- INSERT INTO `abilities` VALUES (668,'deconstruction',22,1,1,0,300,0,0,???,2000,0,6,20,0,0,450,900,0,0,NULL); --- INSERT INTO `abilities` VALUES (669,'chronoshift',22,1,1,0,300,0,0,???,2000,0,6,0,0,14,450,900,0,0,NULL); +INSERT INTO `abilities` VALUES (668,'deconstruction',15,75,4,0,0,0,0,0,0,0,6,20,0,0,0,0,0,0,NULL); +INSERT INTO `abilities` VALUES (669,'chronoshift',15,75,4,0,0,0,0,0,0,0,6,0,0,14,0,0,0,0,NULL); -- INSERT INTO `abilities` VALUES (670,'zantetsuken',22,1,1,0,300,0,0,???,2000,0,6,10,0,10,450,900,0,0,NULL); INSERT INTO `abilities` VALUES (671,'perfect_defense',15,75,1,0,300,0,0,152,2000,0,6,0,0,10,0,0,0,2,NULL); INSERT INTO `abilities` VALUES (672,'foot_kick',9,25,257,1,102,0,0,0,2000,0,6,3,0,0,1,60,0,0,NULL); diff --git a/sql/mob_family_system.sql b/sql/mob_family_system.sql index 615035e0443..9aec928e614 100644 --- a/sql/mob_family_system.sql +++ b/sql/mob_family_system.sql @@ -533,8 +533,8 @@ INSERT INTO `mob_family_system` VALUES (506,'Meeble',190,'Meeble',7,'Beastmen',4 INSERT INTO `mob_family_system` VALUES (507,'Quasilumin',15,'Luminian',0,'Unclassified',25,100,100,5,5,5,5,5,5,5,5,5,5,5,0.0,2,0); INSERT INTO `mob_family_system` VALUES (411,'Pet-Siren',16,'Siren',5,'Avatar',40,100,120,3,3,3,3,3,3,3,1,3,1,3,0.0,41,0); - --- Family IDs 10,22,50,96,317-318,405,412-434,439-443 available for use +INSERT INTO `mob_family_system` VALUES (412,'Pet-Atomos',11,'Atomos',5,'Avatar',40,100,120,3,3,3,3,6,6,3,1,3,1,3,0.0,41,0); +-- Family IDs 10,22,50,96,317-318,405,413-434,439-443 available for use /*!40000 ALTER TABLE `mob_family_system` ENABLE KEYS */; UNLOCK TABLES; diff --git a/sql/mob_pools.sql b/sql/mob_pools.sql index b5abee836aa..619ed5d5a6b 100644 --- a/sql/mob_pools.sql +++ b/sql/mob_pools.sql @@ -7388,9 +7388,7 @@ INSERT INTO `mob_pools` VALUES (7293,'Generic_Doll_XLarge','Generic_Doll',85,0x0 INSERT INTO `mob_pools` VALUES (7294,'Lilisette_WoE_2','Lilisette',484,0x00008F0800000000000000000000000000000000,19,19,2,240,100,0,0,0,0,16,0,0,3,1,0,0,0,0,0,484,484,0,17); INSERT INTO `mob_pools` VALUES (7295,'Flesh_Eater_WS_Large','Flesh_Eater_WS',258,0x0000A80100000000000000000000000000000000,4,5,7,240,100,0,1,0,1,16,0,0,7,131,0,0,9,0,64,258,258,2,14); INSERT INTO `mob_pools` VALUES (7296,'Savage_Ruszor_Large','Savage_Ruszor',211,0x0000AF0800000000000000000000000000000000,1,1,0,240,100,0,0,0,0,0,0,0,556,129,4,0,0,0,0,211,211,2,34); - -INSERT INTO `mob_pools` VALUES (7297,'Pet_Atomos','Pet_Atomos',32,0x00001D0000000000000000000000000000000000,4,4,12,320,100,0,0,0,0,0,0,32,0,0,0,0,0,0,0,32,32,0,15); -- TODO: Only model and hitboxes are verified - +INSERT INTO `mob_pools` VALUES (7297,'Pet_Atomos','Pet_Atomos',412,0x00001D0000000000000000000000000000000000,4,4,12,320,100,2,0,0,0,0,0,32,0,0,0,0,0,0,0,2092,412,0,15); INSERT INTO `mob_pools` VALUES (7298,'Boodlix_Large','Boodlix',133,0x0000BE0200000000000000000000000000000000,1,1,3,240,100,0,1,1,1,32,0,32,0,157,0,0,0,0,0,133,133,2,20); INSERT INTO `mob_pools` VALUES (7299,'Chigoe_Caedarva','Chigoe',64,0x0000D30600000000000000000000000000000000,6,6,2,150,100,0,1,0,0,16,0,0,2593,641,8,0,0,0,1024,64,64,1,6); INSERT INTO `mob_pools` VALUES (7300,'Coral_Crab_Dangruf','Coral_Crab',77,0x0000640100000000000000000000000000000000,7,7,4,240,100,0,1,0,0,4,0,0,0,129,8,0,0,0,0,77,77,1,13); diff --git a/sql/mob_resistances.sql b/sql/mob_resistances.sql index 4bfc032d61c..fd29d5ad515 100644 --- a/sql/mob_resistances.sql +++ b/sql/mob_resistances.sql @@ -450,7 +450,8 @@ INSERT INTO `mob_resistances` VALUES (408,'Quadav-Seed',0,2500,0,0,0,0,0,0,0,0,0 INSERT INTO `mob_resistances` VALUES (409,'Yagudo-Seed',0,0,0,0,0,0,0,0,0,0,0,0,0,-2,-3,0,0,-2,-2,-2,-2,-3,-3,0,0,-2,-2,-2,-2); INSERT INTO `mob_resistances` VALUES (410,'Goblin-Seed',0,0,0,0,0,0,0,0,0,0,0,0,0,-2,-2,-2,-2,-2,-2,-3,0,-2,-2,-2,-2,-2,-3,0,0); INSERT INTO `mob_resistances` VALUES (411,'Pet-Siren',0,0,0,0,0,0,0,0,0,0,0,0,0,4,2,11,11,4,4,11,2,2,2,11,11,4,11,2,2); --- 412 to 434 free +INSERT INTO `mob_resistances` VALUES (412,'Pet-Atomos',0,0,0,0,0,0,0,0,0,0,0,0,0,5,7,5,7,5,7,4,11,7,7,5,7,7,4,11,11); +-- 413 to 434 free INSERT INTO `mob_resistances` VALUES (435,'Giant_Gnat',0,0,0,0,0,0,0,0,0,0,0,0,0,-1,0,3,0,0,0,-2,4,0,0,3,0,0,-2,4,4); INSERT INTO `mob_resistances` VALUES (436,'Gnat-Bloodlapper',0,0,0,0,0,0,0,0,0,0,0,0,0,-1,0,3,0,0,0,-2,4,0,0,3,0,0,-2,4,4); INSERT INTO `mob_resistances` VALUES (437,'Sapling-Ghillie_Dhu',0,0,0,0,0,0,0,0,0,0,0,0,0,-3,-2,-2,0,-2,0,0,-3,-2,-2,-2,0,0,0,-3,-3); diff --git a/sql/mob_skill_lists.sql b/sql/mob_skill_lists.sql index 284534d8f42..380d632a3e5 100644 --- a/sql/mob_skill_lists.sql +++ b/sql/mob_skill_lists.sql @@ -4278,6 +4278,9 @@ INSERT INTO `mob_skill_lists` VALUES ('Hecteyes_Expansion',2091,438); -- Hex Eye INSERT INTO `mob_skill_lists` VALUES ('Hecteyes_Expansion',2091,439); -- Catharsis INSERT INTO `mob_skill_lists` VALUES ('Hecteyes_Expansion',2091,440); -- Petro Gaze +INSERT INTO `mob_skill_lists` VALUES ('Pet-Atomos',2092,668); -- Deconstruction +INSERT INTO `mob_skill_lists` VALUES ('Pet-Atomos',2092,669); -- Chronoshift + -- Next ID : 2092 -- ------------------------------------------------------------ -- Start of Ambuscade section diff --git a/sql/pet_list.sql b/sql/pet_list.sql index 0e749049dce..099d75bdb65 100644 --- a/sql/pet_list.sql +++ b/sql/pet_list.sql @@ -49,7 +49,7 @@ INSERT INTO `pet_list` VALUES (15,'Ramuh',4587,1,99,0,5,2); INSERT INTO `pet_list` VALUES (16,'Diabolos',4590,1,99,0,8,2); INSERT INTO `pet_list` VALUES (17,'Alexander',4589,1,99,0,7,2); INSERT INTO `pet_list` VALUES (18,'Odin',4591,1,99,0,8,2); -INSERT INTO `pet_list` VALUES (19,'Atomos',7297,1,99,0,0,2); +INSERT INTO `pet_list` VALUES (19,'Atomos',7297,1,99,0,8,2); INSERT INTO `pet_list` VALUES (20,'Cait Sith',5775,1,99,0,7,2); INSERT INTO `pet_list` VALUES (21,'SheepFamiliar',4598,23,35,3600,0,3); INSERT INTO `pet_list` VALUES (22,'HareFamiliar',4641,23,35,5400,0,3); diff --git a/sql/pet_skills.sql b/sql/pet_skills.sql index a29c6a88663..181b4206a66 100644 --- a/sql/pet_skills.sql +++ b/sql/pet_skills.sql @@ -185,8 +185,8 @@ INSERT INTO `pet_skills` VALUES (664,0,149,'ruinous_omen',1,10,5,2000,1000,4,317 INSERT INTO `pet_skills` VALUES (665,0,153,'night_terror',0,0,5,2000,1000,4,317,@SKILLFLAG_SPECIAL | @SKILLFLAG_BLOODPACT_RAGE,0,13,0,0,0,0); -- No LUA yet INSERT INTO `pet_skills` VALUES (666,0,155,'pavor_nocturnus',0,0,12,2000,1000,4,646,@SKILLFLAG_SPECIAL | @SKILLFLAG_BLOODPACT_WARD,0,13,0,0,0,0); -- No LUA yet INSERT INTO `pet_skills` VALUES (667,0,173,'blindside',0,0,10,2000,1000,4,317,@SKILLFLAG_SPECIAL | @SKILLFLAG_BLOODPACT_RAGE,0,13,0,@SC_GRAVITATION,@SC_TRANSFIXION,0); -- No LUA yet --- 668: Deconstruction 20y (Radius 0y) --- 669: Chronoshift 0y (Radius 14y) +INSERT INTO `pet_skills` VALUES (668,0,171,'deconstruction',0,0,21,2000,1000,4,323,@SKILLFLAG_SPECIAL | @SKILLFLAG_BLOODPACT_RAGE,0,13,0,0,0,0); +INSERT INTO `pet_skills` VALUES (669,0,172,'chronoshift',1,14,0,2000,1000,3,283,@SKILLFLAG_SPECIAL | @SKILLFLAG_BLOODPACT_WARD,0,13,0,0,0,0); -- 670: Zantetsuken 10y (Radius 10y) INSERT INTO `pet_skills` VALUES (671,0,152,'perfect_defense',1,10,0,2000,1000,3,316,@SKILLFLAG_ASTRAL_FLOW | @SKILLFLAG_SPECIAL | @SKILLFLAG_BLOODPACT_WARD,0,13,0,0,0,0); INSERT INTO `pet_skills` VALUES (672,3840,1,'foot_kick',0,0,3,2000,1500,4,185,@SKILLFLAG_NONE,0,11,0,@SC_REVERBERATION,0,0); diff --git a/sql/spell_list.sql b/sql/spell_list.sql index cf9ec64ab63..66c3ac75ae3 100644 --- a/sql/spell_list.sql +++ b/sql/spell_list.sql @@ -881,7 +881,7 @@ INSERT INTO `spell_list` VALUES (843,'frazzle',0x000000002A000000000000000000000 INSERT INTO `spell_list` VALUES (844,'frazzle_ii',0x000000005C0000000000000000000000000000000000,2,155,@ELEMENT_DARK,0,4,@SKILL_ENFEEBLING,64,3000,10000,0,0,936,4000,0,0,1.00,1,320,0,200,0,'SOA'); INSERT INTO `spell_list` VALUES (845,'flurry',0x00000000300000000000000000000000000000000000,6,156,@ELEMENT_WIND,0,91,@SKILL_ENHANCING,40,3000,20000,230,0,931,2000,0,0,1.00,1,300,0,200,0,'SOA'); INSERT INTO `spell_list` VALUES (846,'flurry_ii',0x00000000600000000000000000000000000000000000,6,156,@ELEMENT_WIND,0,91,@SKILL_ENHANCING,80,3000,20000,230,0,932,2000,0,0,1.00,1,300,0,200,0,'SOA'); --- INSERT INTO `spell_list` VALUES (847,'atomos',0x00000000000000000000000000004B00000000000000,5,?,@ELEMENT_DARK,128,4,@SKILL_SUMMONING,0,1000,60000,0,0,288,1000,0,0,1.00,0,0,0,200,0,'SOA'); +INSERT INTO `spell_list` VALUES (847,'atomos',0x00000000000000000000000000004B00000000000000,5,86,@ELEMENT_DARK,128,4,@SKILL_SUMMONING,50,1000,60000,0,0,288,1000,0,0,1.00,0,0,0,200,0,'SOA'); INSERT INTO `spell_list` VALUES (848,'reraise_iv',0x00000000000000000000000000000000000000000000,6,41,@ELEMENT_LIGHT,0,1,@SKILL_HEALING,150,7000,60000,0,0,979,4000,0,0,1.00,1,80,4,0,0,'SOA'); INSERT INTO `spell_list` VALUES (849,'fire_vi',0x00000000000000000000000000000000000000000000,2,42,@ELEMENT_FIRE,0,4,@SKILL_ELEMENTAL,339,10500,60000,2,252,957,4000,0,35,1.00,0,0,0,200,0,'SOA'); INSERT INTO `spell_list` VALUES (850,'blizzard_vi',0x00000000000000000000000000000000000000000000,2,43,@ELEMENT_ICE,0,4,@SKILL_ELEMENTAL,386,10500,60000,2,252,958,4000,0,35,1.00,0,0,0,200,0,'SOA'); diff --git a/src/map/ai/controllers/mob_controller.cpp b/src/map/ai/controllers/mob_controller.cpp index 1d77caa5aaf..248750ef5e8 100644 --- a/src/map/ai/controllers/mob_controller.cpp +++ b/src/map/ai/controllers/mob_controller.cpp @@ -189,7 +189,7 @@ void CMobController::TryLink() // handle pet behavior on the targets behalf (faster than in ai_pet_dummy) // Avatars defend masters by attacking mobs if the avatar isn't attacking anything currently (bodyguard behavior) - // Alexander and Odin are passive and do not protect the master. + // Alexander, Odin and Atomos are passive and do not protect the master. if (PTarget->PPet != nullptr && PTarget->PPet->GetBattleTargetID() == 0) { if (PTarget->PPet->objtype == TYPE_PET) @@ -197,7 +197,8 @@ void CMobController::TryLink() const auto PPetEntity = static_cast(PTarget->PPet); if (PPetEntity->getPetType() == PET_TYPE::AVATAR && PPetEntity->m_PetID != PETID_ALEXANDER && - PPetEntity->m_PetID != PETID_ODIN) + PPetEntity->m_PetID != PETID_ODIN && + PPetEntity->m_PetID != PETID_ATOMOS) { if (PTarget->objtype == TYPE_PC) { diff --git a/src/map/ai/controllers/pet_controller.cpp b/src/map/ai/controllers/pet_controller.cpp index 15318ec0efd..81b92451b4e 100644 --- a/src/map/ai/controllers/pet_controller.cpp +++ b/src/map/ai/controllers/pet_controller.cpp @@ -34,6 +34,7 @@ const std::set immobilePets = { PETID_LUOPAN, PETID_ALEXANDER, PETID_ODIN, + PETID_ATOMOS, }; } diff --git a/src/map/ai/states/magic_state.cpp b/src/map/ai/states/magic_state.cpp index fdb5dc98bae..5fbb19ce579 100644 --- a/src/map/ai/states/magic_state.cpp +++ b/src/map/ai/states/magic_state.cpp @@ -520,15 +520,20 @@ void CMagicState::ApplyEnmity(CBattleEntity* PTarget, int ce, int ve) mob->m_DropItemTime = m_PSpell->getAnimationTime(); } - if (!(m_PSpell->isHeal()) || m_PSpell->tookEffect()) // can't claim mob with cure unless it does damage + // Skip enmity generation for summoning magic + // Retail does not generate enmity for summoning pets on a target and does not claim. + if (m_PSpell->getSkillType() != SKILL_SUMMONING_MAGIC) { - mob->PEnmityContainer->UpdateEnmity(m_PEntity, ce, ve); - enmityApplied = true; - if (PTarget->isDead()) - { // claim mob only on death (for aoe) - battleutils::ClaimMob(PTarget, m_PEntity); + if (!(m_PSpell->isHeal()) || m_PSpell->tookEffect()) // can't claim mob with cure unless it does damage + { + mob->PEnmityContainer->UpdateEnmity(m_PEntity, ce, ve); + enmityApplied = true; + if (PTarget->isDead()) + { // claim mob only on death (for aoe) + battleutils::ClaimMob(PTarget, m_PEntity); + } + battleutils::DirtyExp(PTarget, m_PEntity); } - battleutils::DirtyExp(PTarget, m_PEntity); } } } diff --git a/src/map/ai/states/petskill_state.cpp b/src/map/ai/states/petskill_state.cpp index 133b74c076d..7c15daec42a 100644 --- a/src/map/ai/states/petskill_state.cpp +++ b/src/map/ai/states/petskill_state.cpp @@ -120,6 +120,7 @@ bool CPetSkillState::Update(timer::time_point tick) // Only send packet if action was populated (e.g. interrupts return early) if (!action.targets.empty()) { + m_skillSuccess = true; m_PEntity->loc.zone->PushPacket(m_PEntity, CHAR_INRANGE_SELF, std::make_unique(action)); } m_finishTime = tick + m_PSkill->getAnimationTime(); @@ -152,6 +153,19 @@ bool CPetSkillState::Update(timer::time_point tick) power += levelGained; PSummoner->StatusEffectContainer->GetStatusEffect(EFFECT_AVATARS_FAVOR)->SetPower(power > 11 ? power : 11); } + + if (m_skillSuccess && PTarget && m_PEntity->getPetType() == PET_TYPE::AVATAR && (m_PEntity->m_PetID != PETID_ALEXANDER && m_PEntity->m_PetID != PETID_ATOMOS)) + { + auto* PBattleTarget = dynamic_cast(PTarget); + if (PBattleTarget && + PBattleTarget->isAlive() && + PBattleTarget->objtype == TYPE_MOB && + PBattleTarget->allegiance != m_PEntity->allegiance) + { + // Re-engage the target after blood pact + m_PEntity->PAI->Engage(PTarget->targid); + } + } } return true; } diff --git a/src/map/ai/states/petskill_state.h b/src/map/ai/states/petskill_state.h index 5171d6dd1f6..173d0ce12fc 100644 --- a/src/map/ai/states/petskill_state.h +++ b/src/map/ai/states/petskill_state.h @@ -62,6 +62,7 @@ class CPetSkillState : public CState timer::time_point m_finishTime; timer::duration m_castTime{}; int16 m_spentTP; + bool m_skillSuccess{ false }; }; #endif diff --git a/src/map/entities/battleentity.cpp b/src/map/entities/battleentity.cpp index d262d5a0a03..a1d9aaa76e2 100644 --- a/src/map/entities/battleentity.cpp +++ b/src/map/entities/battleentity.cpp @@ -2424,12 +2424,27 @@ void CBattleEntity::OnCastFinished(CMagicState& state, action_t& action) battleutils::handleSecondaryTargetEnmity(this, PActionTarget); } } + if ((!(PSpell->isHeal()) || PSpell->tookEffect()) && PActionTarget->isAlive()) { - if (objtype != TYPE_PET) + // Current logic for magic claiming: + // Spell is not a heal. + // spell took effect (i.e. it wasn't fully resisted). + // The target is alive after the spell damage/effects are applied. + // Target must be a MOB. + // Allegiance must be different from the caster. + // Must NOT be Summoning Magic (Atomos and Odin are cast on a mob). + // If the caster is a PET, it must NOT be an AUTOMATON. + + bool isDifferentAllegianceMob = (PActionTarget->objtype == TYPE_MOB && this->allegiance != PActionTarget->allegiance); + bool isNotSummoning = (PSpell->getSkillType() != SKILL_SUMMONING_MAGIC); + bool isNotAutomaton = (this->objtype != TYPE_PET || static_cast(this)->getPetType() != PET_TYPE::AUTOMATON); + + if (isDifferentAllegianceMob && isNotSummoning && isNotAutomaton) { + ShowWarning("%s (Type: %d) claiming %s using spell %s\n", this->name, this->objtype, PActionTarget->name, PSpell->getName()); battleutils::ClaimMob(PActionTarget, this); - } + } } // TODO: Pixies will probably break here, once they're added. @@ -2760,9 +2775,20 @@ void CBattleEntity::OnMobSkillFinished(CMobSkillState& state, action_t& action) if (PTarget) { - if (PTarget->objtype == TYPE_MOB && (PTarget->isDead() || (objtype == TYPE_PET && static_cast(this)->getPetType() == PET_TYPE::AVATAR))) + if (PTarget->objtype == TYPE_MOB && this->allegiance != PTarget->allegiance) { - battleutils::ClaimMob(PTarget, this); + // Current logic for claiming with Mob Skills: + // Target must be a MOB. + // Allegiance must be different from the mob using the mob skill. + // If the mob is a PET, it must NOT be an AUTOMATON. + + bool isInvalidPet = (this->objtype == TYPE_PET && static_cast(this)->getPetType() == PET_TYPE::AUTOMATON); + + if (!isInvalidPet) + { + ShowWarning("%s claiming mob %s using mob skill %s\n", this->name, PTarget->name, PSkill->getName()); + battleutils::ClaimMob(PTarget, this); + } } battleutils::DirtyExp(PTarget, this); } diff --git a/src/map/entities/charentity.cpp b/src/map/entities/charentity.cpp index 285d10a3d4b..2513b7307f6 100644 --- a/src/map/entities/charentity.cpp +++ b/src/map/entities/charentity.cpp @@ -589,9 +589,9 @@ void CCharEntity::setPetZoningInfo() petZoningInfo.jugDuration = PPetEntity->getJugDuration(); [[fallthrough]]; case PET_TYPE::AVATAR: - if (PPetEntity->m_PetID == PETID_ALEXANDER || PPetEntity->m_PetID == PETID_ODIN) + if (PPetEntity->m_PetID == PETID_ALEXANDER || PPetEntity->m_PetID == PETID_ODIN || PPetEntity->m_PetID == PETID_ATOMOS) { - // Alexander and Odin cannot persist through zoning. + // Alexander, Odin and Atomos cannot persist through zoning. break; } [[fallthrough]]; diff --git a/src/map/entities/petentity.cpp b/src/map/entities/petentity.cpp index 7561ab018b8..9d410fe08db 100644 --- a/src/map/entities/petentity.cpp +++ b/src/map/entities/petentity.cpp @@ -607,7 +607,7 @@ void CPetEntity::OnPetSkillFinished(CPetSkillState& state, action_t& action) if (PTarget) { - if (PTarget->objtype == TYPE_MOB && (PTarget->isDead() || (this->getPetType() == PET_TYPE::AVATAR))) + if (PTarget->objtype == TYPE_MOB && PTarget->isDead() || (this->getPetType() == PET_TYPE::AVATAR && PSkill->getID() != ABILITY_DECONSTRUCTION)) // Atomos is an exception, Deconstruction does not claim. { battleutils::ClaimMob(PTarget, this); } diff --git a/src/map/utils/battleutils.cpp b/src/map/utils/battleutils.cpp index e5e94f57e1a..b910417db37 100644 --- a/src/map/utils/battleutils.cpp +++ b/src/map/utils/battleutils.cpp @@ -2022,7 +2022,11 @@ int32 TakePhysicalDamage(CBattleEntity* PAttacker, CBattleEntity* PDefender, PHY damage = -corrected; } - battleutils::ClaimMob(PDefender, PAttacker); + // Only claim mob if the allegiance is mob. This prevents claiming on players and trusts, but allows claiming when charmed. + if (PDefender->objtype == TYPE_MOB && PDefender->allegiance != PAttacker->allegiance) + { + battleutils::ClaimMob(PDefender, PAttacker); + } if (damage > 0) { @@ -4488,10 +4492,13 @@ void ClaimMob(CBattleEntity* PDefender, CBattleEntity* PAttacker, bool passing) { TracyZoneScoped; - if (PDefender == nullptr || - (PDefender && PDefender->objtype != ENTITYTYPE::TYPE_MOB) || // Do not try to claim anything but mobs (trusts, pets, players don't count) - (PDefender && PDefender->objtype == ENTITYTYPE::TYPE_MOB && PDefender->allegiance == ALLEGIANCE_TYPE::PLAYER)) // Added mobs that are in allied with player - { + if (PDefender == nullptr || (PDefender && PDefender->objtype != TYPE_MOB)) + {// Do not try to claim anything but mobs (trusts, pets, players don't count) + return; + } + + if (PDefender && PDefender->objtype == TYPE_MOB && PDefender->allegiance == PAttacker->allegiance) + { // mobs that are allied with the attacker do not need to be claimed and will not update enmity return; } diff --git a/src/map/utils/charutils.cpp b/src/map/utils/charutils.cpp index 454a24563b3..6020290b7b3 100644 --- a/src/map/utils/charutils.cpp +++ b/src/map/utils/charutils.cpp @@ -3416,7 +3416,7 @@ void BuildingCharPetAbilityTable(CCharEntity* PChar, CPetEntity* PPet, uint32 Pe std::memset(&PChar->m_PetCommands, 0, sizeof(PChar->m_PetCommands)); - if (PetID == 0) + if (PetID == 0) { // technically Fire Spirit but we're using this to null the abilities shown PChar->pushPacket(PChar); return; @@ -6752,9 +6752,9 @@ auto CheckAbilityAddtype(CCharEntity* PChar, const CAbility* PAbility) -> bool return false; } - // Alexander and Odin grant no abilities (Assault, Release...) to the master. + // Alexander, Odin and Atomos grant no abilities (Assault, Release...) to the master. const auto* petEntity = static_cast(PChar->PPet); - if (petEntity->m_PetID == PETID_ALEXANDER || petEntity->m_PetID == PETID_ODIN) + if (petEntity->m_PetID == PETID_ALEXANDER || petEntity->m_PetID == PETID_ODIN || petEntity->m_PetID == PETID_ATOMOS) { return false; } diff --git a/src/map/utils/petutils.cpp b/src/map/utils/petutils.cpp index 978de5f5705..d16842a8878 100644 --- a/src/map/utils/petutils.cpp +++ b/src/map/utils/petutils.cpp @@ -1815,6 +1815,12 @@ void LoadPet(CBattleEntity* PMaster, uint32 PetID, bool spawningFromZone) // m_ActionOffsetPos is a combination of targets pos + action offset pos PPet->loc.p = static_cast(PMaster)->m_ActionOffsetPos; } + else if (PetID == PETID_ALEXANDER || PetID == PETID_ODIN || PetID == PETID_ATOMOS) + { + // spawn at master's position + // nearPosition with 0 distance to ensure correct placement and avoid any potential issues with pet collision on spawn + PPet->loc.p = nearPosition(PMaster->loc.p, 0, PMaster->loc.p.rotation); + } else { // spawn me randomly around master From 0a2870b165da6f131f2438ba23122e49c3a97bb0 Mon Sep 17 00:00:00 2001 From: MrSent Date: Sat, 14 Feb 2026 01:00:09 +0000 Subject: [PATCH 2/5] Fixed pup and cleaned up a bit --- scripts/commands/changejob.lua | 6 ++++++ scripts/commands/changesjob.lua | 6 ++++++ scripts/globals/job_utils/summoner.lua | 1 - src/map/ai/states/mobskill_state.cpp | 9 +++++++-- src/map/ai/states/mobskill_state.h | 2 +- src/map/ai/states/petskill_state.cpp | 12 +++++++++--- src/map/enmity_container.cpp | 2 +- src/map/entities/automatonentity.cpp | 21 +++++++++++++++++++++ src/map/entities/battleentity.cpp | 18 ++++++------------ src/map/entities/petentity.cpp | 11 +++++++++-- src/map/utils/battleutils.cpp | 6 +++--- src/map/utils/charutils.cpp | 2 +- 12 files changed, 70 insertions(+), 26 deletions(-) diff --git a/scripts/commands/changejob.lua b/scripts/commands/changejob.lua index 70a205dd821..5f281c98547 100644 --- a/scripts/commands/changejob.lua +++ b/scripts/commands/changejob.lua @@ -56,6 +56,12 @@ commandObj.onTrigger = function(player, jobId, level, master) jobNameByNum[v] = k end + -- if the player has a pet despawn it, clean up pet. + local pet = player:getPet() + if pet then + player:despawnPet() + end + -- output new job to player local masterStr = masterJob and ' (Mastered)' or '' player:printToPlayer(string.format('You are now a %s%i/%s%i%s.', jobNameByNum[player:getMainJob()], player:getMainLvl(), jobNameByNum[player:getSubJob()], player:getSubLvl(), masterStr)) diff --git a/scripts/commands/changesjob.lua b/scripts/commands/changesjob.lua index c7259261692..2a051c9df1e 100644 --- a/scripts/commands/changesjob.lua +++ b/scripts/commands/changesjob.lua @@ -49,6 +49,12 @@ commandObj.onTrigger = function(player, jobId, level) jobNameByNum[v] = k end + -- if the player has a pet despawn it, clean up pet. + local pet = player:getPet() + if pet then + player:despawnPet() + end + -- output new job to player player:printToPlayer(string.format('You are now a %s%i/%s%i.', jobNameByNum[player:getMainJob()], player:getMainLvl(), jobNameByNum[player:getSubJob()], player:getSubLvl())) end diff --git a/scripts/globals/job_utils/summoner.lua b/scripts/globals/job_utils/summoner.lua index e06010900be..13c8c586432 100644 --- a/scripts/globals/job_utils/summoner.lua +++ b/scripts/globals/job_utils/summoner.lua @@ -216,7 +216,6 @@ xi.job_utils.summoner.onUseBloodPact = function(target, petskill, summoner, acti if target:isMob() then target:addBaseEnmity(summoner) - --target:addEnmity(summoner, 1, 60) end if summoner:hasStatusEffect(xi.effect.APOGEE) then diff --git a/src/map/ai/states/mobskill_state.cpp b/src/map/ai/states/mobskill_state.cpp index b3a2b93195d..6d8c5f0e208 100644 --- a/src/map/ai/states/mobskill_state.cpp +++ b/src/map/ai/states/mobskill_state.cpp @@ -157,6 +157,9 @@ void CMobSkillState::SpendCost() bool CMobSkillState::Update(timer::time_point tick) { + // Reset the state for the current skill attempt + m_skillSuccess = false; + // Rotate towards target during ability // TODO : add force param to turnTowardsTarget on certain TP moves like Petro Eyes if (m_castTime > 0s && tick < GetEntryTime() + m_castTime) { @@ -189,6 +192,7 @@ bool CMobSkillState::Update(timer::time_point tick) // Only send packet if action was populated (e.g. interrupts return early) if (!action.targets.empty()) { + m_skillSuccess = true; m_PEntity->loc.zone->PushPacket(m_PEntity, CHAR_INRANGE_SELF, std::make_unique(action)); } @@ -205,9 +209,10 @@ bool CMobSkillState::Update(timer::time_point tick) if (IsCompleted() && tick > m_finishTime) { auto* PTarget = GetTarget(); - if (PTarget && PTarget->objtype == TYPE_MOB && PTarget != m_PEntity && m_PEntity->allegiance == ALLEGIANCE_TYPE::PLAYER) + if (m_skillSuccess && PTarget && PTarget->objtype == TYPE_MOB && PTarget != m_PEntity && m_PEntity->allegiance == ALLEGIANCE_TYPE::PLAYER) { - static_cast(PTarget)->PEnmityContainer->UpdateEnmity(m_PEntity, 0, 0); + bool withMaster = m_PEntity->objtype == TYPE_PET || (m_PEntity->objtype == TYPE_MOB && m_PEntity->isCharmed); + static_cast(PTarget)->PEnmityContainer->UpdateEnmity(m_PEntity, 0, 0, withMaster); } if (m_PEntity->objtype == TYPE_PET && m_PEntity->PMaster && m_PEntity->PMaster->objtype == TYPE_PC && (m_PSkill->isBloodPactRage() || m_PSkill->isBloodPactWard())) diff --git a/src/map/ai/states/mobskill_state.h b/src/map/ai/states/mobskill_state.h index f1bef6e1e06..af6df8d7a95 100644 --- a/src/map/ai/states/mobskill_state.h +++ b/src/map/ai/states/mobskill_state.h @@ -62,7 +62,7 @@ class CMobSkillState : public CState timer::time_point m_finishTime; timer::duration m_castTime{}; int16 m_spentTP; - + bool m_skillSuccess{ false }; void reduceTpOnInterrupt() const; }; diff --git a/src/map/ai/states/petskill_state.cpp b/src/map/ai/states/petskill_state.cpp index 7c15daec42a..48afd067578 100644 --- a/src/map/ai/states/petskill_state.cpp +++ b/src/map/ai/states/petskill_state.cpp @@ -113,6 +113,9 @@ void CPetSkillState::SpendCost() bool CPetSkillState::Update(timer::time_point tick) { + // Reset the state for the current skill attempt + m_skillSuccess = false; + if (m_PEntity && m_PEntity->isAlive() && (tick > GetEntryTime() + m_castTime && !IsCompleted())) { action_t action{}; @@ -136,9 +139,12 @@ bool CPetSkillState::Update(timer::time_point tick) if (IsCompleted() && tick > m_finishTime) { auto* PTarget = GetTarget(); - if (PTarget && PTarget->objtype == TYPE_MOB && PTarget != m_PEntity && m_PEntity->allegiance == ALLEGIANCE_TYPE::PLAYER) + if (m_skillSuccess && PTarget && PTarget->objtype == TYPE_MOB && PTarget != m_PEntity && m_PEntity->allegiance != PTarget->allegiance) { - static_cast(PTarget)->PEnmityContainer->UpdateEnmity(m_PEntity, 0, 0); + // This generates enmity for the master when using a pet skill, excluding Automatons. + // All player pets will generate base enmity for the master, which is retail accurate. + bool withMaster = m_PEntity->objtype == TYPE_PET; + static_cast(PTarget)->PEnmityContainer->UpdateEnmity(m_PEntity, 0, 0, withMaster); } m_PEntity->PAI->EventHandler.triggerListener("WEAPONSKILL_STATE_EXIT", m_PEntity, m_PSkill->getID()); @@ -154,7 +160,7 @@ bool CPetSkillState::Update(timer::time_point tick) PSummoner->StatusEffectContainer->GetStatusEffect(EFFECT_AVATARS_FAVOR)->SetPower(power > 11 ? power : 11); } - if (m_skillSuccess && PTarget && m_PEntity->getPetType() == PET_TYPE::AVATAR && (m_PEntity->m_PetID != PETID_ALEXANDER && m_PEntity->m_PetID != PETID_ATOMOS)) + if (PTarget && m_PEntity->getPetType() == PET_TYPE::AVATAR && (m_PEntity->m_PetID != PETID_ALEXANDER && m_PEntity->m_PetID != PETID_ATOMOS)) { auto* PBattleTarget = dynamic_cast(PTarget); if (PBattleTarget && diff --git a/src/map/enmity_container.cpp b/src/map/enmity_container.cpp index 41635fd85f1..a02f2797891 100644 --- a/src/map/enmity_container.cpp +++ b/src/map/enmity_container.cpp @@ -122,7 +122,7 @@ void CEnmityContainer::AddBaseEnmity(CBattleEntity* PChar) { return; } - m_EnmityList.emplace(PChar->id, EnmityObject_t{ PChar, 0, 0, false }); + m_EnmityList.emplace(PChar->id, EnmityObject_t{ PChar, 0, 0, true }); PChar->PNotorietyContainer->add(m_EnmityHolder); } diff --git a/src/map/entities/automatonentity.cpp b/src/map/entities/automatonentity.cpp index be27c22d3cf..fe844c28ce1 100644 --- a/src/map/entities/automatonentity.cpp +++ b/src/map/entities/automatonentity.cpp @@ -30,6 +30,7 @@ #include "common/utils.h" #include "recast_container.h" #include "status_effect_container.h" +#include "enmity_container.h" #include "utils/mobutils.h" #include "utils/puppetutils.h" @@ -185,6 +186,16 @@ void CAutomatonEntity::OnCastFinished(CMagicState& state, action_t& action) if (PSpell->tookEffect()) { puppetutils::TrySkillUP(this, SKILL_AUTOMATON_MAGIC, PTarget->GetMLevel()); + + if (PTarget && PTarget->objtype == TYPE_MOB && PTarget->allegiance != ALLEGIANCE_TYPE::PLAYER) + { + auto* PMob = static_cast(PTarget); + auto* PMaster = static_cast(this->PMaster); + if (PMaster && PMaster->objtype == TYPE_PC) + { + PMob->PEnmityContainer->AddBaseEnmity(PMaster); + } + } } } @@ -195,6 +206,16 @@ void CAutomatonEntity::OnMobSkillFinished(CMobSkillState& state, action_t& actio auto* PSkill = state.GetSkill(); auto* PTarget = static_cast(state.GetTarget()); + if (PTarget && PTarget->objtype == TYPE_MOB && PTarget->allegiance != ALLEGIANCE_TYPE::PLAYER) + { + auto* PMob = static_cast(PTarget); + auto* PMaster = static_cast(this->PMaster); + if (PMaster && PMaster->objtype == TYPE_PC) + { + PMob->PEnmityContainer->AddBaseEnmity(PMaster); + } + } + // Ranged attack skill up if (PSkill->getID() == 1949 && !PSkill->hasMissMsg()) { diff --git a/src/map/entities/battleentity.cpp b/src/map/entities/battleentity.cpp index a1d9aaa76e2..b8415c6bff2 100644 --- a/src/map/entities/battleentity.cpp +++ b/src/map/entities/battleentity.cpp @@ -2436,15 +2436,14 @@ void CBattleEntity::OnCastFinished(CMagicState& state, action_t& action) // Must NOT be Summoning Magic (Atomos and Odin are cast on a mob). // If the caster is a PET, it must NOT be an AUTOMATON. - bool isDifferentAllegianceMob = (PActionTarget->objtype == TYPE_MOB && this->allegiance != PActionTarget->allegiance); + bool isDifferentAllegianceMob = (PActionTarget->objtype == TYPE_MOB && PActionTarget->allegiance != this->allegiance); bool isNotSummoning = (PSpell->getSkillType() != SKILL_SUMMONING_MAGIC); bool isNotAutomaton = (this->objtype != TYPE_PET || static_cast(this)->getPetType() != PET_TYPE::AUTOMATON); if (isDifferentAllegianceMob && isNotSummoning && isNotAutomaton) { - ShowWarning("%s (Type: %d) claiming %s using spell %s\n", this->name, this->objtype, PActionTarget->name, PSpell->getName()); battleutils::ClaimMob(PActionTarget, this); - } + } } // TODO: Pixies will probably break here, once they're added. @@ -2775,18 +2774,13 @@ void CBattleEntity::OnMobSkillFinished(CMobSkillState& state, action_t& action) if (PTarget) { - if (PTarget->objtype == TYPE_MOB && this->allegiance != PTarget->allegiance) + if (PTarget->objtype == TYPE_MOB && this->allegiance == ALLEGIANCE_TYPE::PLAYER) { - // Current logic for claiming with Mob Skills: - // Target must be a MOB. - // Allegiance must be different from the mob using the mob skill. - // If the mob is a PET, it must NOT be an AUTOMATON. - - bool isInvalidPet = (this->objtype == TYPE_PET && static_cast(this)->getPetType() == PET_TYPE::AUTOMATON); + bool isAvatar = (this->objtype == TYPE_PET && static_cast(this)->getPetType() == PET_TYPE::AVATAR); // this is here to prevent other pet types from calling ClaimMob. + bool isKillShot = PTarget->isDead(); - if (!isInvalidPet) + if (isAvatar || isKillShot) { - ShowWarning("%s claiming mob %s using mob skill %s\n", this->name, PTarget->name, PSkill->getName()); battleutils::ClaimMob(PTarget, this); } } diff --git a/src/map/entities/petentity.cpp b/src/map/entities/petentity.cpp index 9d410fe08db..98e79ed7adb 100644 --- a/src/map/entities/petentity.cpp +++ b/src/map/entities/petentity.cpp @@ -607,9 +607,16 @@ void CPetEntity::OnPetSkillFinished(CPetSkillState& state, action_t& action) if (PTarget) { - if (PTarget->objtype == TYPE_MOB && PTarget->isDead() || (this->getPetType() == PET_TYPE::AVATAR && PSkill->getID() != ABILITY_DECONSTRUCTION)) // Atomos is an exception, Deconstruction does not claim. + if (PTarget->objtype == TYPE_MOB && PTarget->allegiance != this->allegiance) { - battleutils::ClaimMob(PTarget, this); + bool isAvatar = (this->getPetType() == PET_TYPE::AVATAR); + bool isAtomosSkill = (PSkill->getID() == ABILITY_DECONSTRUCTION); + bool isDead = PTarget->isDead(); + + if (isDead || (isAvatar && !isAtomosSkill)) + { + battleutils::ClaimMob(PTarget, this); + } } battleutils::DirtyExp(PTarget, this); } diff --git a/src/map/utils/battleutils.cpp b/src/map/utils/battleutils.cpp index b910417db37..6069c4bf54b 100644 --- a/src/map/utils/battleutils.cpp +++ b/src/map/utils/battleutils.cpp @@ -2022,7 +2022,7 @@ int32 TakePhysicalDamage(CBattleEntity* PAttacker, CBattleEntity* PDefender, PHY damage = -corrected; } - // Only claim mob if the allegiance is mob. This prevents claiming on players and trusts, but allows claiming when charmed. + // Only claim a mob and if the allegiance is not PLAYER. This prevents mobs from calling ClaimMob on other mobs or themselves. if (PDefender->objtype == TYPE_MOB && PDefender->allegiance != PAttacker->allegiance) { battleutils::ClaimMob(PDefender, PAttacker); @@ -4493,10 +4493,10 @@ void ClaimMob(CBattleEntity* PDefender, CBattleEntity* PAttacker, bool passing) TracyZoneScoped; if (PDefender == nullptr || (PDefender && PDefender->objtype != TYPE_MOB)) - {// Do not try to claim anything but mobs (trusts, pets, players don't count) + { // Do not try to claim anything but mobs (trusts, pets, players don't count) return; } - + if (PDefender && PDefender->objtype == TYPE_MOB && PDefender->allegiance == PAttacker->allegiance) { // mobs that are allied with the attacker do not need to be claimed and will not update enmity return; diff --git a/src/map/utils/charutils.cpp b/src/map/utils/charutils.cpp index 6020290b7b3..ee369102cd9 100644 --- a/src/map/utils/charutils.cpp +++ b/src/map/utils/charutils.cpp @@ -3416,7 +3416,7 @@ void BuildingCharPetAbilityTable(CCharEntity* PChar, CPetEntity* PPet, uint32 Pe std::memset(&PChar->m_PetCommands, 0, sizeof(PChar->m_PetCommands)); - if (PetID == 0) + if (PetID == 0) { // technically Fire Spirit but we're using this to null the abilities shown PChar->pushPacket(PChar); return; From 6242807e0b9623d220ca7d4b6d9759ee7f91e9ce Mon Sep 17 00:00:00 2001 From: MrSent Date: Mon, 16 Feb 2026 00:35:20 +0000 Subject: [PATCH 3/5] Review Amendments --- .../actions/abilities/pets/chronoshift.lua | 13 ++--------- .../actions/abilities/pets/deconstruction.lua | 16 +++----------- scripts/globals/pets.lua | 11 ++++++---- src/map/ai/states/magic_state.cpp | 22 ++++++++++--------- src/map/ai/states/mobskill_state.h | 1 + src/map/entities/automatonentity.cpp | 10 ++++----- src/map/entities/battleentity.cpp | 4 ++-- src/map/entities/petentity.cpp | 6 ++--- 8 files changed, 35 insertions(+), 48 deletions(-) diff --git a/scripts/actions/abilities/pets/chronoshift.lua b/scripts/actions/abilities/pets/chronoshift.lua index 617c6160b01..53353108fe8 100644 --- a/scripts/actions/abilities/pets/chronoshift.lua +++ b/scripts/actions/abilities/pets/chronoshift.lua @@ -9,18 +9,9 @@ abilityObject.onAbilityCheck = function(player, target, ability) end abilityObject.onPetAbility = function(target, pet, petskill, summoner, action) - if summoner == nil then - return 0 - end - - local effect = nil - local effectID = 0 local effectCount = 0 - - -- Retrive the absorbed effect's ID - effectID = pet:getLocalVar('aEffectID') - -- Find that effect on Atomos - effect = pet:getStatusEffect(effectID) + local effectID = pet:getLocalVar('aEffectID') or 0 -- Retrive the absorbed effect's ID + local effect = pet:getStatusEffect(effectID) or nil -- Find that effect on Atomos if effect then local effectIcon = effect:getIcon() diff --git a/scripts/actions/abilities/pets/deconstruction.lua b/scripts/actions/abilities/pets/deconstruction.lua index c889ea2cfb9..39c7f5781b8 100644 --- a/scripts/actions/abilities/pets/deconstruction.lua +++ b/scripts/actions/abilities/pets/deconstruction.lua @@ -9,16 +9,9 @@ abilityObject.onAbilityCheck = function(player, target, ability) end abilityObject.onPetAbility = function(target, pet, petskill, summoner, action) - if summoner == nil then - return 0 - end - local effectCount = 0 - local effectID = 0 - - effectID = pet:stealStatusEffect(target) - - local newStatus = pet:getStatusEffect(effectID) + local effectID = pet:stealStatusEffect(target) or 0 + local newStatus = pet:getStatusEffect(effectID) or nil if newStatus then -- Store the stolen effect ID for Chronoshift so we know what effect to transfer. @@ -31,10 +24,7 @@ abilityObject.onPetAbility = function(target, pet, petskill, summoner, action) end target:addEnmity(pet, 1, 60) - - if summoner then - target:addEnmity(summoner, 1, 0) -- this is to ensure you cannot cheese mobs with this, mob goes passive if not added - end + target:addEnmity(summoner, 1, 0) -- this is to ensure you cannot cheese mobs with this, mob goes passive if not added return effectCount end diff --git a/scripts/globals/pets.lua b/scripts/globals/pets.lua index 33a8a043c8a..bb5d4296013 100644 --- a/scripts/globals/pets.lua +++ b/scripts/globals/pets.lua @@ -127,11 +127,14 @@ xi.pet.spawnPet = function(caster, petID, state, target) -- Use Deconstruction on the target 3 seconds after spawning. local pet = caster:getPet() if pet then - pet:setAutoAttackEnabled(false) -- stops claiming, Atomos does not claim mobs on retail. - pet:setMobMod(xi.mobMod.NO_MOVE, 1) -- Atomos is stubborn and will still chase a mob unless this is set. -- Timed sequence after spawning, wait -> Deconstruction -> wait -> Chronoshift (despawn pet after complete) - pet:timer(3000, function() pet:usePetAbility(xi.jobAbility.DECONSTRUCTION, target) end) - pet:timer(10000, function() pet:usePetAbility(xi.jobAbility.CHRONOSHIFT, pet) end) + pet:timer(3000, function() + pet:usePetAbility(xi.jobAbility.DECONSTRUCTION, target) + end) + + pet:timer(10000, function() + pet:usePetAbility(xi.jobAbility.CHRONOSHIFT, pet) + end) end end end diff --git a/src/map/ai/states/magic_state.cpp b/src/map/ai/states/magic_state.cpp index 5fbb19ce579..2b6d5956589 100644 --- a/src/map/ai/states/magic_state.cpp +++ b/src/map/ai/states/magic_state.cpp @@ -522,18 +522,20 @@ void CMagicState::ApplyEnmity(CBattleEntity* PTarget, int ce, int ve) // Skip enmity generation for summoning magic // Retail does not generate enmity for summoning pets on a target and does not claim. - if (m_PSpell->getSkillType() != SKILL_SUMMONING_MAGIC) + if (m_PSpell->getSpellGroup() == SPELLGROUP_SUMMONING) { - if (!(m_PSpell->isHeal()) || m_PSpell->tookEffect()) // can't claim mob with cure unless it does damage - { - mob->PEnmityContainer->UpdateEnmity(m_PEntity, ce, ve); - enmityApplied = true; - if (PTarget->isDead()) - { // claim mob only on death (for aoe) - battleutils::ClaimMob(PTarget, m_PEntity); - } - battleutils::DirtyExp(PTarget, m_PEntity); + return; + } + + if (!(m_PSpell->isHeal()) || m_PSpell->tookEffect()) // can't claim mob with cure unless it does damage + { + mob->PEnmityContainer->UpdateEnmity(m_PEntity, ce, ve); + enmityApplied = true; + if (PTarget->isDead()) + { // claim mob only on death (for aoe) + battleutils::ClaimMob(PTarget, m_PEntity); } + battleutils::DirtyExp(PTarget, m_PEntity); } } } diff --git a/src/map/ai/states/mobskill_state.h b/src/map/ai/states/mobskill_state.h index af6df8d7a95..96424b7e3c2 100644 --- a/src/map/ai/states/mobskill_state.h +++ b/src/map/ai/states/mobskill_state.h @@ -63,6 +63,7 @@ class CMobSkillState : public CState timer::duration m_castTime{}; int16 m_spentTP; bool m_skillSuccess{ false }; + void reduceTpOnInterrupt() const; }; diff --git a/src/map/entities/automatonentity.cpp b/src/map/entities/automatonentity.cpp index fe844c28ce1..91d261daccf 100644 --- a/src/map/entities/automatonentity.cpp +++ b/src/map/entities/automatonentity.cpp @@ -28,9 +28,9 @@ #include "ai/states/mobskill_state.h" #include "common/tracy.h" #include "common/utils.h" +#include "enmity_container.h" #include "recast_container.h" #include "status_effect_container.h" -#include "enmity_container.h" #include "utils/mobutils.h" #include "utils/puppetutils.h" @@ -189,8 +189,8 @@ void CAutomatonEntity::OnCastFinished(CMagicState& state, action_t& action) if (PTarget && PTarget->objtype == TYPE_MOB && PTarget->allegiance != ALLEGIANCE_TYPE::PLAYER) { - auto* PMob = static_cast(PTarget); - auto* PMaster = static_cast(this->PMaster); + auto* PMob = static_cast(PTarget); + auto* PMaster = dynamic_cast(this->PMaster); if (PMaster && PMaster->objtype == TYPE_PC) { PMob->PEnmityContainer->AddBaseEnmity(PMaster); @@ -208,8 +208,8 @@ void CAutomatonEntity::OnMobSkillFinished(CMobSkillState& state, action_t& actio if (PTarget && PTarget->objtype == TYPE_MOB && PTarget->allegiance != ALLEGIANCE_TYPE::PLAYER) { - auto* PMob = static_cast(PTarget); - auto* PMaster = static_cast(this->PMaster); + auto* PMob = static_cast(PTarget); + auto* PMaster = dynamic_cast(this->PMaster); if (PMaster && PMaster->objtype == TYPE_PC) { PMob->PEnmityContainer->AddBaseEnmity(PMaster); diff --git a/src/map/entities/battleentity.cpp b/src/map/entities/battleentity.cpp index b8415c6bff2..ab13ca0db76 100644 --- a/src/map/entities/battleentity.cpp +++ b/src/map/entities/battleentity.cpp @@ -2776,8 +2776,8 @@ void CBattleEntity::OnMobSkillFinished(CMobSkillState& state, action_t& action) { if (PTarget->objtype == TYPE_MOB && this->allegiance == ALLEGIANCE_TYPE::PLAYER) { - bool isAvatar = (this->objtype == TYPE_PET && static_cast(this)->getPetType() == PET_TYPE::AVATAR); // this is here to prevent other pet types from calling ClaimMob. - bool isKillShot = PTarget->isDead(); + bool isAvatar = (this->objtype == TYPE_PET && static_cast(this)->getPetType() == PET_TYPE::AVATAR); // this is here to prevent other pet types from calling ClaimMob. + bool isKillShot = PTarget->isDead(); if (isAvatar || isKillShot) { diff --git a/src/map/entities/petentity.cpp b/src/map/entities/petentity.cpp index 98e79ed7adb..69b558d0b4c 100644 --- a/src/map/entities/petentity.cpp +++ b/src/map/entities/petentity.cpp @@ -609,9 +609,9 @@ void CPetEntity::OnPetSkillFinished(CPetSkillState& state, action_t& action) { if (PTarget->objtype == TYPE_MOB && PTarget->allegiance != this->allegiance) { - bool isAvatar = (this->getPetType() == PET_TYPE::AVATAR); - bool isAtomosSkill = (PSkill->getID() == ABILITY_DECONSTRUCTION); - bool isDead = PTarget->isDead(); + bool isAvatar = (this->getPetType() == PET_TYPE::AVATAR); + bool isAtomosSkill = (PSkill->getID() == ABILITY_DECONSTRUCTION); + bool isDead = PTarget->isDead(); if (isDead || (isAvatar && !isAtomosSkill)) { From af29063bce3c0c10214e60e44615ccee2da1f1c9 Mon Sep 17 00:00:00 2001 From: MrSent Date: Tue, 17 Feb 2026 00:31:13 +0000 Subject: [PATCH 4/5] blocked player charmed mobs and mobs claiming with magic --- src/map/ai/states/magic_state.cpp | 9 ++++++++- src/map/entities/battleentity.cpp | 10 ++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/map/ai/states/magic_state.cpp b/src/map/ai/states/magic_state.cpp index 2b6d5956589..38455db05b5 100644 --- a/src/map/ai/states/magic_state.cpp +++ b/src/map/ai/states/magic_state.cpp @@ -529,9 +529,16 @@ void CMagicState::ApplyEnmity(CBattleEntity* PTarget, int ce, int ve) if (!(m_PSpell->isHeal()) || m_PSpell->tookEffect()) // can't claim mob with cure unless it does damage { + bool isMob = (m_PEntity->objtype == TYPE_MOB); + + if (isMob && !m_PEntity->isCharmed) + { + return; + } + mob->PEnmityContainer->UpdateEnmity(m_PEntity, ce, ve); enmityApplied = true; - if (PTarget->isDead()) + if (PTarget->isDead() && (!isMob || (isMob && m_PEntity->isCharmed))) { // claim mob only on death (for aoe) battleutils::ClaimMob(PTarget, m_PEntity); } diff --git a/src/map/entities/battleentity.cpp b/src/map/entities/battleentity.cpp index ab13ca0db76..c670b4cd9af 100644 --- a/src/map/entities/battleentity.cpp +++ b/src/map/entities/battleentity.cpp @@ -2435,12 +2435,14 @@ void CBattleEntity::OnCastFinished(CMagicState& state, action_t& action) // Allegiance must be different from the caster. // Must NOT be Summoning Magic (Atomos and Odin are cast on a mob). // If the caster is a PET, it must NOT be an AUTOMATON. + // No mobs should claim with magic not even charmed mobs. - bool isDifferentAllegianceMob = (PActionTarget->objtype == TYPE_MOB && PActionTarget->allegiance != this->allegiance); - bool isNotSummoning = (PSpell->getSkillType() != SKILL_SUMMONING_MAGIC); - bool isNotAutomaton = (this->objtype != TYPE_PET || static_cast(this)->getPetType() != PET_TYPE::AUTOMATON); + bool isTargetValidMob = (PActionTarget->objtype == TYPE_MOB && PActionTarget->allegiance != this->allegiance); + bool isNotSummoning = (PSpell->getSkillType() != SKILL_SUMMONING_MAGIC); + bool isAutomaton = (this->objtype == TYPE_PET && static_cast(this)->getPetType() == PET_TYPE::AUTOMATON); + bool isMob = (this->objtype == TYPE_MOB); - if (isDifferentAllegianceMob && isNotSummoning && isNotAutomaton) + if (isTargetValidMob && !isMob && isNotSummoning && !isAutomaton) { battleutils::ClaimMob(PActionTarget, this); } From 04dc6c25ba69a51f52f33a38611769cd9533e56e Mon Sep 17 00:00:00 2001 From: MrSent Date: Mon, 23 Feb 2026 20:28:47 +0000 Subject: [PATCH 5/5] convert chronoshift to use copyStatusEffect --- scripts/actions/abilities/pets/chronoshift.lua | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/scripts/actions/abilities/pets/chronoshift.lua b/scripts/actions/abilities/pets/chronoshift.lua index 53353108fe8..b72ce87dfcf 100644 --- a/scripts/actions/abilities/pets/chronoshift.lua +++ b/scripts/actions/abilities/pets/chronoshift.lua @@ -14,21 +14,16 @@ abilityObject.onPetAbility = function(target, pet, petskill, summoner, action) local effect = pet:getStatusEffect(effectID) or nil -- Find that effect on Atomos if effect then - local effectIcon = effect:getIcon() - local power = effect:getPower() - local duration = effect:getDuration() / 1000 -- Delete old effect if on target already target:delStatusEffectSilent(effectID) -- Add the stolen effect to the party - target:addStatusEffectEx(effectID, effectIcon, power, 0, duration, true) + target:copyStatusEffect(effect) petskill:setMsg(xi.msg.basic.RECEIVE_MAGICAL_EFFECT) effectCount = 1 else petskill:setMsg(xi.msg.basic.NO_EFFECT) end - target:addEnmity(pet, 1, 60) - pet:timer(5000, function() if summoner then summoner:despawnPet()