From f5ea5a3774b62a6029bf307b32f0a51f2909c029 Mon Sep 17 00:00:00 2001 From: MrSent Date: Sat, 23 May 2026 03:48:01 +0100 Subject: [PATCH] Passive trust movement/animation gambits/auras --- scripts/actions/spells/trust/brygid.lua | 22 +++ scripts/actions/spells/trust/cornelia.lua | 49 +++++ scripts/actions/spells/trust/kupofried.lua | 22 +++ .../actions/spells/trust/kuyin_hathdenna.lua | 43 +++- scripts/actions/spells/trust/matsui-p.lua | 27 +++ scripts/actions/spells/trust/moogle.lua | 31 +-- scripts/actions/spells/trust/sakura.lua | 37 ++-- scripts/actions/spells/trust/star_sibyl.lua | 30 ++- scripts/effects/trust_aura_acc.lua | 27 +++ scripts/effects/trust_aura_chr.lua | 26 +++ scripts/effects/trust_aura_exp.lua | 19 ++ scripts/effects/trust_aura_haste.lua | 28 +++ scripts/effects/trust_aura_magic_attack.lua | 24 +++ scripts/effects/trust_aura_refresh.lua | 23 +++ scripts/effects/trust_aura_regen.lua | 23 +++ scripts/enum/effect.lua | 12 +- scripts/globals/gambits.lua | 15 +- scripts/globals/trust.lua | 28 ++- scripts/specs/core/CBaseEntity.lua | 1 + settings/default/main.lua | 2 + sql/mob_pools.sql | 4 +- sql/spell_list.sql | 4 +- sql/status_effects.sql | 7 + src/map/ai/controllers/trust_controller.cpp | 184 ++++++++++++++---- src/map/ai/controllers/trust_controller.h | 4 +- src/map/ai/helpers/gambits_container.cpp | 100 +++++++++- src/map/ai/helpers/gambits_container.h | 19 +- src/map/enums/key_items.h | 3 + src/map/lua/lua_baseentity.cpp | 2 + src/map/lua/lua_statuseffect.cpp | 21 ++ src/map/lua/lua_statuseffect.h | 2 + src/map/status_effect.cpp | 19 +- src/map/status_effect.h | 19 +- src/map/status_effect_container.cpp | 19 +- src/map/utils/charutils.cpp | 22 +++ 35 files changed, 817 insertions(+), 101 deletions(-) create mode 100644 scripts/actions/spells/trust/cornelia.lua create mode 100644 scripts/actions/spells/trust/matsui-p.lua create mode 100644 scripts/effects/trust_aura_acc.lua create mode 100644 scripts/effects/trust_aura_chr.lua create mode 100644 scripts/effects/trust_aura_exp.lua create mode 100644 scripts/effects/trust_aura_haste.lua create mode 100644 scripts/effects/trust_aura_magic_attack.lua create mode 100644 scripts/effects/trust_aura_refresh.lua create mode 100644 scripts/effects/trust_aura_regen.lua diff --git a/scripts/actions/spells/trust/brygid.lua b/scripts/actions/spells/trust/brygid.lua index 5c6021e2eed..53286c06a55 100644 --- a/scripts/actions/spells/trust/brygid.lua +++ b/scripts/actions/spells/trust/brygid.lua @@ -14,6 +14,28 @@ end spellObject.onMobSpawn = function(mob) xi.trust.message(mob, xi.trust.messageOffset.SPAWN) + + mob:setMobMod(xi.mobMod.TRUST_DISTANCE, xi.trust.movementType.NON_COMBAT) + + mob:addMod(xi.mod.AURA_SIZE, 600) -- Trust have a 12 yalm aura 6 base + 6 from mod + + local effectParams = + { + power = 6, + origin = mob, + tick = 3, + subType = xi.effect.TRUST_AURA_CHR, + subPower = mob:getMainLvl(), + subIcon = xi.effect.GEO_CHR_BOOST, + tier = xi.auraTarget.ALLIES, + flag = xi.effectFlag.AURA + } + + mob:addStatusEffect(xi.effect.COLURE_ACTIVE, effectParams) + + mob:addGambit(ai.t.SELF, { { ai.c.TIMER, 5 }, { ai.c.RANDOM, 45 } }, { ai.r.ANIM_STRING, ai.s.RANDOM_ANIMATION, 3 }) + + mob:setAutoAttackEnabled(false) end spellObject.onMobDespawn = function(mob) diff --git a/scripts/actions/spells/trust/cornelia.lua b/scripts/actions/spells/trust/cornelia.lua new file mode 100644 index 00000000000..d870749ce43 --- /dev/null +++ b/scripts/actions/spells/trust/cornelia.lua @@ -0,0 +1,49 @@ +----------------------------------- +-- Trust: Cornelia +----------------------------------- +---@type TSpellTrust +local spellObject = {} + +spellObject.onMagicCastingCheck = function(caster, target, spell) + return xi.trust.canCast(caster, spell) +end + +spellObject.onSpellCast = function(caster, target, spell) + return xi.trust.spawn(caster, spell) +end + +spellObject.onMobSpawn = function(mob) + xi.trust.message(mob, xi.trust.messageOffset.SPAWN) + + mob:setMobMod(xi.mobMod.TRUST_DISTANCE, xi.trust.movementType.NON_COMBAT) + + mob:addMod(xi.mod.AURA_SIZE, 600) -- Trust have a 12 yalm aura 6 base + 6 from mod + + local effectParams = + { + power = 6, + origin = mob, + tick = 3, + subType = xi.effect.TRUST_AURA_HASTE, + subPower = mob:getMainLvl(), + subIcon = xi.effect.GEO_HASTE, + tier = xi.auraTarget.ALLIES, + flag = xi.effectFlag.AURA + } + + mob:addStatusEffect(xi.effect.COLURE_ACTIVE, effectParams) + + mob:addGambit(ai.t.SELF, { { ai.c.TIMER, 5 }, { ai.c.RANDOM, 45 } }, { ai.r.ANIM_STRING, ai.s.RANDOM_ANIMATION, 4 }) + + mob:setAutoAttackEnabled(false) +end + +spellObject.onMobDespawn = function(mob) + xi.trust.message(mob, xi.trust.messageOffset.DESPAWN) +end + +spellObject.onMobDeath = function(mob) + xi.trust.message(mob, xi.trust.messageOffset.DEATH) +end + +return spellObject diff --git a/scripts/actions/spells/trust/kupofried.lua b/scripts/actions/spells/trust/kupofried.lua index 0f9e9694fcb..e2b658d4e07 100644 --- a/scripts/actions/spells/trust/kupofried.lua +++ b/scripts/actions/spells/trust/kupofried.lua @@ -14,6 +14,28 @@ end spellObject.onMobSpawn = function(mob) xi.trust.message(mob, xi.trust.messageOffset.SPAWN) + + mob:setMobMod(xi.mobMod.TRUST_DISTANCE, xi.trust.movementType.NON_COMBAT) + + mob:addMod(xi.mod.AURA_SIZE, 600) -- Trust have a 12 yalm aura 6 base + 6 from mod + + local effectParams = + { + power = 6, + origin = mob, + tick = 3, + subType = xi.effect.TRUST_AURA_EXP, + subPower = 20, + subIcon = xi.effect.DEDICATION, + tier = xi.auraTarget.ALLIES, + flag = xi.effectFlag.AURA + } + + mob:addStatusEffect(xi.effect.COLURE_ACTIVE, effectParams) + + mob:addGambit(ai.t.SELF, { { ai.c.TIMER, 5 }, { ai.c.RANDOM, 45 } }, { ai.r.ANIM_STRING, ai.s.RANDOM_ANIMATION, 4 }) + + mob:setAutoAttackEnabled(false) end spellObject.onMobDespawn = function(mob) diff --git a/scripts/actions/spells/trust/kuyin_hathdenna.lua b/scripts/actions/spells/trust/kuyin_hathdenna.lua index 8b90c21efca..bc915501002 100644 --- a/scripts/actions/spells/trust/kuyin_hathdenna.lua +++ b/scripts/actions/spells/trust/kuyin_hathdenna.lua @@ -13,7 +13,48 @@ spellObject.onSpellCast = function(caster, target, spell) end spellObject.onMobSpawn = function(mob) - xi.trust.message(mob, xi.trust.messageOffset.SPAWN) + local master = mob:getMaster() + local hasTitle = false + local titleBonus = 0 + + if master then + hasTitle = master:hasTitle(xi.title.KIT_EMPATHIZER) + end + + if hasTitle then + -- Kyuin only says this line when the master has the title "Kit Empathizer" from the mog garden quest to enable her employment. + xi.trust.message(mob, xi.trust.messageOffset.SPAWN) + titleBonus = 1 + else + -- No title: I'd like you to gimme a job. + xi.trust.message(mob, xi.trust.messageOffset.TEAMWORK_1) + end + + mob:setMobMod(xi.mobMod.TRUST_DISTANCE, xi.trust.movementType.NON_COMBAT) + + mob:addMod(xi.mod.AURA_SIZE, 600) -- Trust have a 12 yalm aura 6 base + 6 from mod + + local employmentBonus = 0 -- TODO: Add dex boost from mog garden employment (not implimented yet). + -- +1 Bonus when Kuyin is employed in your Mog Garden. + -- +1 Bonus when the player has the title "Kit Empathizer". + + local effectParams = + { + power = 6, + origin = mob, + tick = 3, + subType = xi.effect.TRUST_AURA_ACC, + subPower = mob:getMainLvl() + employmentBonus + titleBonus, -- the effect script will adjust the power using xi.trust.trustAuraValue in trust.lua + subIcon = xi.effect.GEO_ACCURACY_BOOST, + tier = xi.auraTarget.ALLIES, + flag = xi.effectFlag.AURA + } + + mob:addStatusEffect(xi.effect.COLURE_ACTIVE, effectParams) + + mob:addGambit(ai.t.SELF, { { ai.c.TIMER, 5 }, { ai.c.RANDOM, 45 } }, { ai.r.ANIM_STRING, ai.s.RANDOM_ANIMATION, 3 }) + + mob:setAutoAttackEnabled(false) end spellObject.onMobDespawn = function(mob) diff --git a/scripts/actions/spells/trust/matsui-p.lua b/scripts/actions/spells/trust/matsui-p.lua new file mode 100644 index 00000000000..c25081c0500 --- /dev/null +++ b/scripts/actions/spells/trust/matsui-p.lua @@ -0,0 +1,27 @@ +----------------------------------- +-- Trust: Matsui-P +----------------------------------- +---@type TSpellTrust +local spellObject = {} + +spellObject.onMagicCastingCheck = function(caster, target, spell) + return xi.trust.canCast(caster, spell) +end + +spellObject.onSpellCast = function(caster, target, spell) + return xi.trust.spawn(caster, spell) +end + +spellObject.onMobSpawn = function(mob) + xi.trust.message(mob, xi.trust.messageOffset.SPAWN) +end + +spellObject.onMobDespawn = function(mob) + xi.trust.message(mob, xi.trust.messageOffset.DESPAWN) +end + +spellObject.onMobDeath = function(mob) + xi.trust.message(mob, xi.trust.messageOffset.DEATH) +end + +return spellObject diff --git a/scripts/actions/spells/trust/moogle.lua b/scripts/actions/spells/trust/moogle.lua index efd5d84cc95..fc3931996d7 100644 --- a/scripts/actions/spells/trust/moogle.lua +++ b/scripts/actions/spells/trust/moogle.lua @@ -17,17 +17,26 @@ spellObject.onMobSpawn = function(mob) [xi.magic.spell.FABLINIX] = xi.trust.messageOffset.TEAMWORK_1, }) - local mlvl = mob:getMainLvl() - local tick_amount - if mlvl == 99 then - tick_amount = 3 - elseif mlvl < 99 and mlvl > 58 then - tick_amount = 2 - else - tick_amount = 1 - end - - mob:addStatusEffect(xi.effect.COLURE_ACTIVE, { power = 6, origin = mob, tick = 3, subType = xi.effect.GEO_REFRESH, subPower = tick_amount, tier = xi.auraTarget.ALLIES, flag = xi.effectFlag.AURA }) + mob:setMobMod(xi.mobMod.TRUST_DISTANCE, xi.trust.movementType.NON_COMBAT) + + mob:addMod(xi.mod.AURA_SIZE, 600) -- Trust have a 12 yalm aura 6 base + 6 from mod + + local effectParams = + { + power = 6, + origin = mob, + tick = 3, + subType = xi.effect.TRUST_AURA_REFRESH, + subPower = mob:getMainLvl(), + subIcon = xi.effect.GEO_REFRESH, + tier = xi.auraTarget.ALLIES, + flag = xi.effectFlag.AURA + } + + mob:addStatusEffect(xi.effect.COLURE_ACTIVE, effectParams) + + mob:addGambit(ai.t.SELF, { { ai.c.TIMER, 5 }, { ai.c.RANDOM, 45 } }, { ai.r.ANIM_STRING, ai.s.RANDOM_ANIMATION, 5 }) + mob:setAutoAttackEnabled(false) end diff --git a/scripts/actions/spells/trust/sakura.lua b/scripts/actions/spells/trust/sakura.lua index 22cc57b2d78..fc9332ddcf2 100644 --- a/scripts/actions/spells/trust/sakura.lua +++ b/scripts/actions/spells/trust/sakura.lua @@ -15,23 +15,26 @@ end spellObject.onMobSpawn = function(mob) xi.trust.message(mob, xi.trust.messageOffset.SPAWN) - local mlvl = mob:getMainLvl() - local tick_amount - if mlvl == 99 then - tick_amount = 6 - elseif mlvl < 99 then - tick_amount = 5 - elseif mlvl <= 87 then - tick_amount = 4 - elseif mlvl <= 73 then - tick_amount = 3 - elseif mlvl <= 51 then - tick_amount = 2 - else - tick_amount = 1 - end - - mob:addStatusEffect(xi.effect.COLURE_ACTIVE, { power = 6, origin = mob, tick = 3, subType = xi.effect.GEO_REGEN, subPower = tick_amount, tier = xi.auraTarget.ALLIES, flag = xi.effectFlag.AURA }) + mob:setMobMod(xi.mobMod.TRUST_DISTANCE, xi.trust.movementType.NON_COMBAT) + + mob:addMod(xi.mod.AURA_SIZE, 600) -- Trust have a 12 yalm aura 6 base + 6 from mod + + local effectParams = + { + power = 6, + origin = mob, + tick = 3, + subType = xi.effect.TRUST_AURA_REGEN, + subPower = mob:getMainLvl(), + subIcon = xi.effect.GEO_REGEN, + tier = xi.auraTarget.ALLIES, + flag = xi.effectFlag.AURA + } + + mob:addStatusEffect(xi.effect.COLURE_ACTIVE, effectParams) + + mob:addGambit(ai.t.SELF, { { ai.c.TIMER, 5 }, { ai.c.RANDOM, 45 } }, { ai.r.ANIM_STRING, ai.s.RANDOM_ANIMATION, 4 }) + mob:setAutoAttackEnabled(false) end diff --git a/scripts/actions/spells/trust/star_sibyl.lua b/scripts/actions/spells/trust/star_sibyl.lua index 4eedd8cdc71..e343994ad15 100644 --- a/scripts/actions/spells/trust/star_sibyl.lua +++ b/scripts/actions/spells/trust/star_sibyl.lua @@ -13,7 +13,35 @@ spellObject.onSpellCast = function(caster, target, spell) end spellObject.onMobSpawn = function(mob) - xi.trust.message(mob, xi.trust.messageOffset.SPAWN) + xi.trust.teamworkMessage(mob, { + [xi.magic.spell.AJIDO_MARUJIDO] = xi.trust.messageOffset.TEAMWORK_1, + [xi.magic.spell.SHANTOTTO] = xi.trust.messageOffset.TEAMWORK_2, + [xi.magic.spell.KUPIPI] = xi.trust.messageOffset.TEAMWORK_3, + [xi.magic.spell.KARAHA_BARUHA] = xi.trust.messageOffset.TEAMWORK_4, + [xi.magic.spell.SEMIH_LAFIHNA] = xi.trust.messageOffset.TEAMWORK_5, + }) + + mob:setMobMod(xi.mobMod.TRUST_DISTANCE, xi.trust.movementType.NON_COMBAT) + + mob:addMod(xi.mod.AURA_SIZE, 600) -- Trust have a 12 yalm aura 6 base + 6 from mod + + local effectParams = + { + power = 6, + origin = mob, + tick = 3, + subType = xi.effect.TRUST_AURA_MAGIC_ATTACK, + subPower = mob:getMainLvl(), + subIcon = xi.effect.GEO_MAGIC_ATK_BOOST, + tier = xi.auraTarget.ALLIES, + flag = xi.effectFlag.AURA + } + + mob:addStatusEffect(xi.effect.COLURE_ACTIVE, effectParams) + + mob:addGambit(ai.t.SELF, { { ai.c.TIMER, 5 }, { ai.c.RANDOM, 45 } }, { ai.r.ANIM_STRING, ai.s.RANDOM_ANIMATION, 3 }) + + mob:setAutoAttackEnabled(false) end spellObject.onMobDespawn = function(mob) diff --git a/scripts/effects/trust_aura_acc.lua b/scripts/effects/trust_aura_acc.lua new file mode 100644 index 00000000000..606b4e576ac --- /dev/null +++ b/scripts/effects/trust_aura_acc.lua @@ -0,0 +1,27 @@ +----------------------------------- +-- Kuyin Hathdenna's Accuracy Aura +-- DEX Boost: +4 at level 99 with no title or Kuyin employed +-- Accuracy Boost: +24 at level 99 with no title or Kuyin employed +-- Ranged accuracy Boost: +24 at level 99 with no title or Kuyin employed +-- Boost values: +1 Bonus when Kuyin is employed in your Mog Garden, +1 Bonus when the player has the title "Kit Empathizer", rank may be involved +-- Stacks with player Indi/Geo version +----------------------------------- +---@type TEffect +local effectObject = {} + +effectObject.onEffectGain = function(target, effect) + local lvl = effect:getPower() + local modVal = xi.trust.auraValue(lvl, 24) + + effect:addMod(xi.mod.DEX, xi.trust.auraValue(lvl, 4)) + effect:addMod(xi.mod.ACC, modVal) + effect:addMod(xi.mod.RACC, modVal) +end + +effectObject.onEffectTick = function(target, effect) +end + +effectObject.onEffectLose = function(target, effect) +end + +return effectObject diff --git a/scripts/effects/trust_aura_chr.lua b/scripts/effects/trust_aura_chr.lua new file mode 100644 index 00000000000..acf08355546 --- /dev/null +++ b/scripts/effects/trust_aura_chr.lua @@ -0,0 +1,26 @@ +----------------------------------- +-- Brygid's CHR Aura +-- CHR Boost: static +5 at 99 +-- Defense Bonus: static +10% at 99 +-- Magic Defense Bonus: +5% at 99 +-- Stacks with player Indi/Geo version +----------------------------------- +---@type TEffect +local effectObject = {} + +effectObject.onEffectGain = function(target, effect) + local lvl = effect:getPower() + local modVal = xi.trust.auraValue(lvl, 5) + + effect:addMod(xi.mod.DEFP, xi.trust.auraValue(lvl, 9.7)) + effect:addMod(xi.mod.CHR, modVal) + effect:addMod(xi.mod.MDEF, modVal) +end + +effectObject.onEffectTick = function(target, effect) +end + +effectObject.onEffectLose = function(target, effect) +end + +return effectObject diff --git a/scripts/effects/trust_aura_exp.lua b/scripts/effects/trust_aura_exp.lua new file mode 100644 index 00000000000..42f98c130b3 --- /dev/null +++ b/scripts/effects/trust_aura_exp.lua @@ -0,0 +1,19 @@ +----------------------------------- +-- Kupofried's EXP Aura +-- EXP_BONUS: +20% static value +-- Stacks with other forms of dedication +----------------------------------- +---@type TEffect +local effectObject = {} + +effectObject.onEffectGain = function(target, effect) + effect:addMod(xi.mod.EXP_BONUS, effect:getPower()) +end + +effectObject.onEffectTick = function(target, effect) +end + +effectObject.onEffectLose = function(target, effect) +end + +return effectObject diff --git a/scripts/effects/trust_aura_haste.lua b/scripts/effects/trust_aura_haste.lua new file mode 100644 index 00000000000..adfc06fcb97 --- /dev/null +++ b/scripts/effects/trust_aura_haste.lua @@ -0,0 +1,28 @@ +----------------------------------- +-- Cornelia's HASTE Aura +-- Haste: +20% static value +-- Accuracy Boost: max 30 at 99 +-- Ranged Accuracy Boost: max 30 at 99 +-- Magic Accuracy Boost: max 30 at 99 +-- stacks with player Indi/Geo version +----------------------------------- +---@type TEffect +local effectObject = {} + +effectObject.onEffectGain = function(target, effect) + local lvl = effect:getPower() + local modVal = xi.trust.auraValue(lvl, 30) + + effect:addMod(xi.mod.HASTE_MAGIC, 2000) -- This is a static 20% + effect:addMod(xi.mod.ACC, modVal) + effect:addMod(xi.mod.RACC, modVal) + effect:addMod(xi.mod.MACC, modVal) +end + +effectObject.onEffectTick = function(target, effect) +end + +effectObject.onEffectLose = function(target, effect) +end + +return effectObject diff --git a/scripts/effects/trust_aura_magic_attack.lua b/scripts/effects/trust_aura_magic_attack.lua new file mode 100644 index 00000000000..aec6f6dc9a0 --- /dev/null +++ b/scripts/effects/trust_aura_magic_attack.lua @@ -0,0 +1,24 @@ +----------------------------------- +-- Star Sibyl's Magic Attack Aura +-- Magic Attack Boost: max +19 at level 99 +-- Magic Accuracy boost: max +19 at level 99 +-- Stacks with player Indi/Geo version +----------------------------------- +---@type TEffect +local effectObject = {} + +effectObject.onEffectGain = function(target, effect) + local lvl = effect:getPower() + local modVal = xi.trust.auraValue(lvl, 6) + + effect:addMod(xi.mod.MATT, modVal) + effect:addMod(xi.mod.MACC, modVal) +end + +effectObject.onEffectTick = function(target, effect) +end + +effectObject.onEffectLose = function(target, effect) +end + +return effectObject diff --git a/scripts/effects/trust_aura_refresh.lua b/scripts/effects/trust_aura_refresh.lua new file mode 100644 index 00000000000..db5ba5b1c9a --- /dev/null +++ b/scripts/effects/trust_aura_refresh.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Moogle's REFRESH Aura +-- Refresh: 3 MP/tick at lv. 99 +-- Magical skill gain rate: static +20%, same as the magic skillup food (Stuffed Pitaru) +-- Stacks with player Indi/Geo version +----------------------------------- +---@type TEffect +local effectObject = {} + +effectObject.onEffectGain = function(target, effect) + local lvl = effect:getPower() + + effect:addMod(xi.mod.REFRESH, xi.trust.auraValue(lvl, 3)) + effect:addMod(xi.mod.MAGIC_SKILLUP_RATE, 20) +end + +effectObject.onEffectTick = function(target, effect) +end + +effectObject.onEffectLose = function(target, effect) +end + +return effectObject diff --git a/scripts/effects/trust_aura_regen.lua b/scripts/effects/trust_aura_regen.lua new file mode 100644 index 00000000000..5766ed40356 --- /dev/null +++ b/scripts/effects/trust_aura_regen.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Sakura's REGEN Aura +-- Regen: 6 MP/tick at lv. 99 +-- Physical skill gain rate: static +20%, same as the physical skillup food (Saltena) +-- Stacks with player Indi/Geo version +----------------------------------- +---@type TEffect +local effectObject = {} + +effectObject.onEffectGain = function(target, effect) + local lvl = effect:getPower() + + effect:addMod(xi.mod.REGEN, xi.trust.auraValue(lvl, 6)) + effect:addMod(xi.mod.COMBAT_SKILLUP_RATE, 20) +end + +effectObject.onEffectTick = function(target, effect) +end + +effectObject.onEffectLose = function(target, effect) +end + +return effectObject diff --git a/scripts/enum/effect.lua b/scripts/enum/effect.lua index 075373ce479..8e684c20f6c 100644 --- a/scripts/enum/effect.lua +++ b/scripts/enum/effect.lua @@ -680,7 +680,17 @@ xi.effect = TOMAHAWK = 805, -- Silent status effect inflicted by a Warrior using the "Tomahawk" job ability NUKE_WALL = 806, -- Custom effect for NM type mobs only. + -- TRUST Aura Effects + TRUST_AURA_CHR = 807, -- CHR Aura, +9.7% Defense Bonus, +5 Magic Defense Bonus and +5 CHR at lv. 99, stacks with player Indi/Geo CHR. + TRUST_AURA_HASTE = 808, -- HASTE Aura, Haste +20%, Accuracy +30, Ranged Accuracy +30 and Magic Accuracy +30 at lvl 99, stacks with player Indi/Geo HASTE. + TRUST_AURA_EXP = 809, -- EXP Aura, +20% dedication effect for Experience Points and Capacity Points, stacks with other forms of dedication. + TRUST_AURA_ACC = 810, -- ACC Aura, Accuracy+24, Ranged accuracy+24, and DEX+5 at lvl 99, stacks with player Indi/Geo PRECISION. + TRUST_AURA_REFRESH = 811, -- REFRESH Aura, 3 MP/tick at lvl 99 stacks with player Indi/Geo REFRESH, also grants an increase to magical skill gain rate. + TRUST_AURA_REGEN = 812, -- REGEN Aura, 6 HP/tick at lvl 99 stacks with player Indi/Geo REGEN. also grants an increase to physical combat skill gain rate. + TRUST_AURA_MAGIC_ATTACK = 813, -- MATT Aura, Magic Attack Boost +19 and +19 Magic Accuracy boost at lvl 99, stacks with player Indi/Geo ACUMEN. + -- End of Trust Aura Effects + -- 789 - -- 807-1022 + -- 813-1022 -- PLACEHOLDER = 1023 -- The client dat file seems to have only this many "slots", results of exceeding that are untested. } diff --git a/scripts/globals/gambits.lua b/scripts/globals/gambits.lua index 43af96e0e29..6f30174abe5 100644 --- a/scripts/globals/gambits.lua +++ b/scripts/globals/gambits.lua @@ -96,18 +96,20 @@ ai.condition = SUB_ANIMATION = 36, JA_ON_COOLDOWN = 37, VAL_URIEL_CHECK = 38, + TIMER = 39, -- argument in seconds } ai.c = ai.condition -- Reaction ai.reaction = { - ATTACK = 0, - RATTACK = 1, - MA = 2, - JA = 3, - WS = 4, - MS = 5, + ATTACK = 0, + RATTACK = 1, + MA = 2, + JA = 3, + WS = 4, + MS = 5, + ANIM_STRING = 6, } ai.r = ai.reaction @@ -133,6 +135,7 @@ ai.select = HELIX_MOB_WEAKNESS = 16, DEF_BAR_ELEMENT = 17, RUNE_DAY = 18, + RANDOM_ANIMATION = 19, } ai.s = ai.select diff --git a/scripts/globals/trust.lua b/scripts/globals/trust.lua index c39f71abee6..9566cfb22ab 100644 --- a/scripts/globals/trust.lua +++ b/scripts/globals/trust.lua @@ -18,6 +18,7 @@ xi.trust.movementType = -- : Will set the combat distance the trust tries to stick to to 20' -- NOTE: If a Trust doesn't immediately sprint to a certain distance at the start of battle, it's probably NO_MOVE or MELEE. NO_MOVE = -1, -- Will stand still providing they're within casting distance of their master and target when the fight starts. Otherwise will reposition to be within 9.0' of both + NON_COMBAT = -2, -- Will follow the master if first trust in party and will follow the trust in front if lower in the list. MELEE = 0, -- Default: will continually reposition to stay within melee range of the target MID_RANGE = 6, -- Will path at the start of battle to 6' away from the target, and try to stay at that distance LONG_RANGE = 12, -- Will path at the start of battle to 12' away from the target, and try to stay at that distance @@ -172,6 +173,8 @@ local poolIDToMessagePageOffset = [5997] = 110, -- Iroha [5998] = 118, -- Ygnas [5999] = 120, -- Monberaux + [6002] = 119, -- Cornelia + [6003] = 121, -- Matsui-P [6004] = 52, -- Excenmille [S] [6005] = 63, -- Ayame UC [6006] = 64, -- Maat UC @@ -413,13 +416,32 @@ end -- 1.2 13 56 99 -- 1.0 10 50 99 -- NOTE: This does take into account iLevel, iLevel is different and trust get much more of an aggressive curve. -xi.trust.modGrowthValMax = function(mob, maxVal) +xi.trust.modGrowthValMax = function(mob, maxVal, optionalCurve) local lvl = math.max(mob:getMainLvl(), 1) -- Ensure lvl is at least 1 - local curve = 1.5 -- Gentle curve: starts increasing around lvl 20, this needs testing more, but seems to work well at this value. + local curve = optionalCurve or 1.5 -- Gentle curve: starts increasing around lvl 20, this needs testing more, but seems to work well at this value. local progress = (lvl - 1) / 98 -- Normalize level to 0.0 - 1.0 range (98 is the span between 1 and 99) local exponentGrowth = math.pow(progress, curve) + local val = math.floor(maxVal * exponentGrowth) - return math.floor(maxVal * exponentGrowth) + return math.max(val, 1) +end + +-- Work out the value of aura power for passive trust, default is 1.0 (linear curve) +xi.trust.auraValue = function(lvl, maxVal, optionalCurve) + local bonus = 0 + local trustLevel = math.max(lvl, 1) -- Ensure lvl is at least 1 + + if trustLevel > 99 then + bonus = trustLevel - 99 + trustLevel = 99 + end + + local curve = optionalCurve or 1.0 -- linear curve + local progress = (trustLevel - 1) / 98 -- Normalize level to 0.0 - 1.0 range (98 is the span between 1 and 99) + local exponentGrowth = math.pow(progress, curve) + local val = math.floor(maxVal * exponentGrowth) + + return math.max(val + bonus, 1) end -- pageOffset is: (summon_message_id - 1) / 100 diff --git a/scripts/specs/core/CBaseEntity.lua b/scripts/specs/core/CBaseEntity.lua index 7ab5d9727ba..8f71637b205 100644 --- a/scripts/specs/core/CBaseEntity.lua +++ b/scripts/specs/core/CBaseEntity.lua @@ -2889,6 +2889,7 @@ end ---@field icon xi.effect? Defaults to effectId if not set ---@field subType integer? ---@field subPower number? +---@field subIcon? xi.effect? Defaults to effectId if not set ---@field tier integer? ---@field flag xi.effectFlag? ---@field sourceType xi.effectSourceType? diff --git a/settings/default/main.lua b/settings/default/main.lua index e9a519ac46f..7764aa52fc7 100644 --- a/settings/default/main.lua +++ b/settings/default/main.lua @@ -170,6 +170,8 @@ xi.settings.main = ENABLE_TRUST_QUESTS = 1, ENABLE_TRUST_CUSTOM_ENGAGEMENT = 0, + ENABLE_LIMITED_TIME_TRUST = 0, -- 0 = disabled, 1 = Cornelia, 2 = Matsui-P, will get automatically added to players trust list if the player has a trust permit KI. + ENABLE_TRUST_ALTER_EGO_EXTRAVAGANZA = 0, -- 0 = disabled, 1 = summer/ny, 2 = spring/autumn, 3 = both ENABLE_TRUST_ALTER_EGO_EXTRAVAGANZA_ANNOUNCE = 0, -- 0 = disabled, 1 = add announcement to player login ENABLE_TRUST_ALTER_EGO_EXPO = 0, -- 0 = disabled, 1 = expo - HPP/MPP/Status Resistance, 2 = expo plus (not implemented) diff --git a/sql/mob_pools.sql b/sql/mob_pools.sql index 14e35c3ba8e..70df71d97bf 100644 --- a/sql/mob_pools.sql +++ b/sql/mob_pools.sql @@ -6059,8 +6059,8 @@ INSERT INTO `mob_pools` VALUES (5998,'ygnas','Ygnas',342,0x00002E0C0000000000000 INSERT INTO `mob_pools` VALUES (5999,'monberaux','Monberaux',293,0x0000300C00000000000000000000000000000000,7,22,3,240,100,0,0,0,0,0,0,32,0,3,0,0,0,0,0,1114,145,1,20); -- Reserved for future Trust 6000 -- Reserved for future Trust 6001 --- Reserved for future Trust 6002 --- Reserved for future Trust 6003 +INSERT INTO `mob_pools` VALUES (6002,'cornelia','Cornelia',295,0x00002F0C00000000000000000000000000000000,21,10,0,240,100,0,0,0,0,0,0,32,0,3,0,0,0,0,0,0,0,0,12); +INSERT INTO `mob_pools` VALUES (6003,'matsui-p','Matsui-P',297,0x0000310C00000000000000000000000000000000,13,4,1,240,100,0,0,0,0,0,0,32,0,3,0,0,0,0,0,0,153,1,12); INSERT INTO `mob_pools` VALUES (6004,'excenmille_s','Excenmille',293,0x0000EC0B00000000000000000000000000000000,1,0,3,240,100,0,0,0,0,0,0,32,0,3,0,0,0,0,0,1119,145,NULL,NULL); INSERT INTO `mob_pools` VALUES (6005,'ayame_uc','Ayame',295,0x0000F70B00000000000000000000000000000000,12,0,3,240,100,0,0,0,0,0,0,32,0,3,0,0,0,0,0,1120,149,NULL,NULL); INSERT INTO `mob_pools` VALUES (6006,'maat_uc','Maat',295,0x0000F80B00000000000000000000000000000000,2,1,3,240,100,0,0,0,0,0,0,32,0,3,0,0,0,0,0,1121,149,NULL,NULL); diff --git a/sql/spell_list.sql b/sql/spell_list.sql index 5cd7de14f62..56ac53c7cc6 100644 --- a/sql/spell_list.sql +++ b/sql/spell_list.sql @@ -1038,8 +1038,8 @@ INSERT INTO `spell_list` VALUES (998,'ygnas',0x010101010101010101010101010101010 INSERT INTO `spell_list` VALUES (999,'monberaux',0x01010101010101010101010101010101010101010101,8,0,@ELEMENT_LIGHT,0,1,@SKILL_NONE,0,2000,240000,0,0,939,1500,0,0,1.00,0,0,0,0,0,NULL); -- 1000 -- 1001 --- 1002 --- 1003 +INSERT INTO `spell_list` VALUES (1002,'cornelia',0x01010101010101010101010101010101010101010101,8,0,@ELEMENT_LIGHT,0,1,@SKILL_NONE,0,2000,240000,0,0,939,1500,0,0,1.00,0,0,0,0,0,NULL); +INSERT INTO `spell_list` VALUES (1003,'matsui-p',0x01010101010101010101010101010101010101010101,8,0,@ELEMENT_LIGHT,0,1,@SKILL_NONE,0,2000,240000,0,0,939,1500,0,0,1.00,0,0,0,0,0,NULL); INSERT INTO `spell_list` VALUES (1004,'excenmille_s',0x01010101010101010101010101010101010101010101,8,0,@ELEMENT_LIGHT,0,1,@SKILL_NONE,0,2000,240000,0,0,939,1500,0,0,1.00,0,0,0,0,0,NULL); INSERT INTO `spell_list` VALUES (1005,'ayame_uc',0x01010101010101010101010101010101010101010101,8,0,@ELEMENT_LIGHT,0,1,@SKILL_NONE,0,2000,240000,0,0,939,1500,0,0,1.00,0,0,0,0,0,NULL); INSERT INTO `spell_list` VALUES (1006,'maat_uc',0x01010101010101010101010101010101010101010101,8,0,@ELEMENT_LIGHT,0,1,@SKILL_NONE,0,2000,240000,0,0,939,1500,0,0,1.00,0,0,0,0,0,NULL); diff --git a/sql/status_effects.sql b/sql/status_effects.sql index 8fb604106e8..d5383339c08 100644 --- a/sql/status_effects.sql +++ b/sql/status_effects.sql @@ -728,6 +728,13 @@ INSERT INTO `status_effects` VALUES (801,'meditate',@FLAG_DEATH | @FLAG_ON_ZONE, INSERT INTO `status_effects` VALUES (802,'elemental_resistance_down',@FLAG_DEATH | @FLAG_ON_ZONE | @FLAG_NO_LOSS_MESSAGE | @FLAG_NO_CANCEL,0,0,0,0,0,0,0,0,NULL); INSERT INTO `status_effects` VALUES (803,'full_speed_ahead',@FLAG_ON_ZONE | @FLAG_NO_LOSS_MESSAGE,0,0,0,0,0,0,0,0,NULL); INSERT INTO `status_effects` VALUES (805,'tomahawk',@FLAG_DEATH | @FLAG_NO_LOSS_MESSAGE,0,0,0,0,0,0,0,0,NULL); +INSERT INTO `status_effects` VALUES (807,'trust_aura_chr',@FLAG_DEATH | @FLAG_NO_CANCEL | @FLAG_HIDE_TIMER,0,0,0,0,0,0,0,0,NULL); +INSERT INTO `status_effects` VALUES (808,'trust_aura_haste',@FLAG_DEATH | @FLAG_NO_CANCEL | @FLAG_HIDE_TIMER,0,0,0,0,0,0,0,0,NULL); +INSERT INTO `status_effects` VALUES (809,'trust_aura_exp',@FLAG_DEATH | @FLAG_NO_CANCEL,0,0,0,0,0,0,0,0,NULL); +INSERT INTO `status_effects` VALUES (810,'trust_aura_acc',@FLAG_DEATH | @FLAG_NO_CANCEL | @FLAG_HIDE_TIMER,0,0,0,0,0,0,0,0,NULL); +INSERT INTO `status_effects` VALUES (811,'trust_aura_refresh',@FLAG_DEATH | @FLAG_NO_CANCEL | @FLAG_HIDE_TIMER,0,0,0,0,0,0,0,0,NULL); +INSERT INTO `status_effects` VALUES (812,'trust_aura_regen',@FLAG_DEATH | @FLAG_NO_CANCEL | @FLAG_HIDE_TIMER,0,0,0,0,0,0,0,0,NULL); +INSERT INTO `status_effects` VALUES (813,'trust_aura_magic_attack',@FLAG_DEATH | @FLAG_NO_CANCEL | @FLAG_HIDE_TIMER,0,0,0,0,0,0,0,0,NULL); /*!40000 ALTER TABLE `status_effects` ENABLE KEYS */; UNLOCK TABLES; diff --git a/src/map/ai/controllers/trust_controller.cpp b/src/map/ai/controllers/trust_controller.cpp index 24309f32696..c2bb18d583d 100644 --- a/src/map/ai/controllers/trust_controller.cpp +++ b/src/map/ai/controllers/trust_controller.cpp @@ -50,6 +50,7 @@ enum TRUST_MOVEMENT_TYPE : int8 // : Will set the combat distance the trust tries to stick to to 20' // NOTE: If a Trust doesn't immediately sprint to a certain distance at the start of battle, it's probably NO_MOVE or MELEE. NO_MOVE = -1, // Will stand still providing they're within casting distance of their master and target when the fight starts. Otherwise will reposition to be within 9.0' of both + NON_COMBAT = -2, // Will follow the master if first trust in party and will follow the trust in front if lower in the list. MELEE = 0, // Default: will continually reposition to stay within melee range of the target MID_RANGE = 6, // Will path at the start of battle to 6' away from the target, and try to stay at that distance LONG_RANGE = 12, // Will path at the start of battle to 12' away from the target, and try to stay at that distance @@ -92,17 +93,26 @@ auto CTrustController::Tick(timer::time_point tick) -> Task m_Tick = tick; - if (!POwner->PMaster) + auto* PTrust = static_cast(POwner); + + if (!PTrust->PMaster) { co_return; } - if (POwner->PMaster->isCharmed) + if (PTrust->PMaster->isCharmed) { this->Despawn(); + co_return; } - if (POwner->PAI->IsEngaged()) + const bool nonCombatFollowTrust = PTrust->getMobMod(MOBMOD_TRUST_DISTANCE) == TRUST_MOVEMENT_TYPE::NON_COMBAT; + + if (PTrust->PMaster->PAI->IsEngaged() && nonCombatFollowTrust) + { + co_await DoNonCombatTick(tick); + } + else if (POwner->PAI->IsEngaged()) { co_await DoCombatTick(tick); } @@ -190,6 +200,11 @@ auto CTrustController::DoCombatTick(timer::time_point tick) -> Task } break; } + case TRUST_MOVEMENT_TYPE::NON_COMBAT: + { + // Non-combat followers should not use target-distance positioning. + break; + } case TRUST_MOVEMENT_TYPE::MELEE: { std::unique_ptr err; @@ -238,6 +253,100 @@ auto CTrustController::DoCombatTick(timer::time_point tick) -> Task } } +auto CTrustController::DoNonCombatTick(timer::time_point tick) -> Task +{ + TracyZoneScoped; + + auto* PTrust = static_cast(POwner); + auto* PMaster = static_cast(POwner->PMaster); + + if (!PMaster) + { + co_return; + } + + // Keep COMBAT_TICK target valid for listeners/gambits. + PTarget = PMaster->GetBattleTarget(); + + // Non-combat trust follow order: + // - first trust follows master + // - others follow the trust directly in front of them + uint8 currentPartyPos = GetPartyPosition(); + + CBattleEntity* PFollowTarget = PMaster; + if (currentPartyPos > 0 && static_cast(currentPartyPos - 1) < PMaster->PTrusts.size()) + { + if (auto* PLeadTrust = PMaster->PTrusts.at(currentPartyPos - 1); PLeadTrust && PLeadTrust != PTrust) + { + PFollowTarget = PLeadTrust; + } + } + + // First trust keeps a bit more space from master. + constexpr float FirstTrustFollowDistance = 3.0f; // tune as needed + const float desiredFollowDistance = (currentPartyPos == 0) ? FirstTrustFollowDistance : RoamDistance; + + float currentDistance = distance(PTrust->loc.p, PFollowTarget->loc.p); + + // Simple declump so non-combat trusts don't stack on each other. + for (auto* POtherTrust : PMaster->PTrusts) + { + if (POtherTrust != PTrust && + distance(POtherTrust->loc.p, PTrust->loc.p) < 1.0f && + !PTrust->PAI->PathFind->IsFollowingPath()) + { + auto diff_angle = worldAngle(PTrust->loc.p, POtherTrust->loc.p) + 64; + auto amount = (currentPartyPos % 2) ? 1.0f : -1.0f; + + position_t new_pos = { + PTrust->loc.p.x - (cosf(rotationToRadian(diff_angle)) * amount), + POtherTrust->loc.p.y, + PTrust->loc.p.z + (sinf(rotationToRadian(diff_angle)) * amount), + 0, + 0, + }; + + if (PTrust->PAI->PathFind->ValidPosition(new_pos) && + PTrust->PAI->PathFind->PathAround(new_pos, desiredFollowDistance, PATHFLAG_RUN | PATHFLAG_WALLHACK)) + { + PTrust->PAI->PathFind->FollowPath(m_Tick); + } + break; + } + } + + if (currentDistance > WarpDistance) + { + PTrust->PAI->PathFind->WarpTo(PFollowTarget->loc.p); + } + else if (currentDistance > desiredFollowDistance) + { + if (currentDistance < desiredFollowDistance * 3.0f && + PTrust->PAI->PathFind->PathAround(PFollowTarget->loc.p, desiredFollowDistance, PATHFLAG_RUN | PATHFLAG_WALLHACK)) + { + PTrust->PAI->PathFind->FollowPath(m_Tick); + } + else if (PTrust->GetSpeed() > 0) + { + PTrust->PAI->PathFind->StepTo(PFollowTarget->loc.p, true); + } + } + + if (PTrust->PAI->PathFind->IsFollowingPath()) + { + PTrust->PAI->PathFind->FollowPath(m_Tick); + } + + // Keep gambits active in combat, but only while stationary. + if (PMaster->PAI->IsEngaged() && !PTrust->PAI->PathFind->IsFollowingPath()) + { + co_await m_GambitsContainer->Tick(tick); + PTrust->PAI->EventHandler.triggerListener("COMBAT_TICK", PTrust, PMaster, PTarget); + } + + co_return; +} + auto CTrustController::DoRoamTick(timer::time_point tick) -> Task { TracyZoneScoped; @@ -265,7 +374,9 @@ auto CTrustController::DoRoamTick(timer::time_point tick) -> Task } } - if (PMaster->PAI->IsEngaged() && trustEngageCondition) + const uint16 modelID_Cornelia = 3119; // Cornielia does not have an Attack Schedule so do not engage. + + if (PMaster->PAI->IsEngaged() && trustEngageCondition && POwner->GetModelId() != modelID_Cornelia) { POwner->PAI->Internal_Engage(PMaster->GetBattleTargetID()); } @@ -274,45 +385,50 @@ auto CTrustController::DoRoamTick(timer::time_point tick) -> Task CBattleEntity* PFollowTarget = (GetPartyPosition() > 0) ? (CBattleEntity*)PMaster->PTrusts.at(currentPartyPos - 1) : POwner->PMaster; float currentDistance = distance(POwner->loc.p, PFollowTarget->loc.p); - for (auto* POtherTrust : PMaster->PTrusts) - { - if (POtherTrust != POwner && distance(POtherTrust->loc.p, POwner->loc.p) < 1.0f && !POwner->PAI->PathFind->IsFollowingPath()) - { - auto diff_angle = worldAngle(POwner->loc.p, POtherTrust->loc.p) + 64; - auto amount = (currentPartyPos % 2) ? 1.0f : -1.0f; + // Formation following thresholds (in yalms) + // First trust follows master more closely than other trusts follow each other + bool isFirstTrust = (currentPartyPos == 0); - // clang-format off - position_t new_pos = - { - POwner->loc.p.x - (cosf(rotationToRadian(diff_angle)) * amount), - POtherTrust->loc.p.y, - POwner->loc.p.z + (sinf(rotationToRadian(diff_angle)) * amount), - 0, - 0, - }; - // clang-format on + float declumpDistance = isFirstTrust ? 1.0f : 1.5f; // Too close, need to move away + float followMax = isFirstTrust ? 2.0f : 3.5f; // Maximum follow distance before moving closer + float followTarget = isFirstTrust ? 1.5f : 3.0f; // Ideal follow distance - if (POwner->PAI->PathFind->ValidPosition(new_pos) && POwner->PAI->PathFind->PathAround(new_pos, RoamDistance, PATHFLAG_RUN | PATHFLAG_WALLHACK)) + // Handle formation movement based on distance thresholds + if (currentDistance < declumpDistance) + { + // Too close to follow target - push away to maintain formation spacing + if (PFollowTarget && POwner->PAI->PathFind->PathAround(PFollowTarget->loc.p, followTarget + 0.5f, PATHFLAG_RUN | PATHFLAG_WALLHACK)) + { + POwner->PAI->PathFind->FollowPath(m_Tick); + } + } + else if (currentDistance > followMax) + { + // Too far from follow target - move closer to maintain formation + if (currentDistance > WarpDistance) + { + // Warp if extremely too far + POwner->PAI->PathFind->WarpTo(PFollowTarget->loc.p); + } + else + { + // Path or step closer to follow target + if (currentDistance < RoamDistance * 3.0f && POwner->PAI->PathFind->PathAround(PFollowTarget->loc.p, followTarget, PATHFLAG_RUN | PATHFLAG_WALLHACK)) { POwner->PAI->PathFind->FollowPath(m_Tick); } - break; + else if (POwner->GetSpeed() > 0) + { + POwner->PAI->PathFind->StepTo(PFollowTarget->loc.p, true); + } } } - - if (currentDistance > WarpDistance) - { - POwner->PAI->PathFind->WarpTo(PFollowTarget->loc.p); - } - else if (currentDistance > RoamDistance) + else { - if (currentDistance < RoamDistance * 3.0f && POwner->PAI->PathFind->PathAround(PFollowTarget->loc.p, RoamDistance, PATHFLAG_RUN | PATHFLAG_WALLHACK)) - { - POwner->PAI->PathFind->FollowPath(m_Tick); - } - else if (POwner->GetSpeed() > 0) + // In formation range - stop pathfinding to prevent circling + if (POwner->PAI->PathFind->IsFollowingPath()) { - POwner->PAI->PathFind->StepTo(PFollowTarget->loc.p, true); + POwner->PAI->PathFind->Clear(); } } diff --git a/src/map/ai/controllers/trust_controller.h b/src/map/ai/controllers/trust_controller.h index a172c71df74..fd047ffd4ba 100644 --- a/src/map/ai/controllers/trust_controller.h +++ b/src/map/ai/controllers/trust_controller.h @@ -50,7 +50,7 @@ class CTrustController : public CMobController bool RangedAttack(uint16 targid) override; - static constexpr float RoamDistance = { 2.0f }; + static constexpr float RoamDistance = { 3.0f }; static constexpr float SpawnDistance = { 3.0f }; static constexpr float CastingDistance = { 15.0f }; static constexpr float WarpDistance = { 30.0f }; @@ -64,7 +64,7 @@ class CTrustController : public CMobController private: auto DoCombatTick(timer::time_point tick) -> Task override; auto DoRoamTick(timer::time_point tick) -> Task override; - + auto DoNonCombatTick(timer::time_point tick) -> Task; void Declump(CCharEntity* PMaster, CBattleEntity* PTarget); void PathOutToDistance(CBattleEntity* PTarget, float amount); diff --git a/src/map/ai/helpers/gambits_container.cpp b/src/map/ai/helpers/gambits_container.cpp index 83866abfede..66c82ea2fdb 100644 --- a/src/map/ai/helpers/gambits_container.cpp +++ b/src/map/ai/helpers/gambits_container.cpp @@ -39,6 +39,8 @@ #include "ai/controllers/player_controller.h" #include "ai/controllers/trust_controller.h" +#include "packets/s2c/0x038_schedulor.h" + #include #include @@ -93,11 +95,18 @@ void CGambitsContainer::RemoveGambit(const std::string& id) return gambit.identifier == id; }), gambits.end()); + + const auto prefix = fmt::format("{}:", id); + std::erase_if(m_timerConditionLastTrigger, [&](const auto& kv) + { + return kv.first.rfind(prefix, 0) == 0; + }); } void CGambitsContainer::RemoveAllGambits() { gambits.clear(); + m_timerConditionLastTrigger.clear(); } auto CGambitsContainer::Tick(timer::time_point tick) -> Task @@ -292,9 +301,10 @@ auto CGambitsContainer::Tick(timer::time_point tick) -> Task { // All predicate groups must resolve successfully for the target to be considered bool targetMatchAllPredicates = true; - for (auto& predicateGroup : gambit.predicate_groups) + for (size_t predicateGroupIndex = 0; predicateGroupIndex < gambit.predicate_groups.size(); ++predicateGroupIndex) { - if (!CheckTrigger(potentialTarget, predicateGroup)) + auto& predicateGroup = gambit.predicate_groups[predicateGroupIndex]; + if (!CheckTrigger(potentialTarget, gambit, predicateGroupIndex, predicateGroup)) { targetMatchAllPredicates = false; } @@ -944,6 +954,57 @@ auto CGambitsContainer::Tick(timer::time_point tick) -> Task executedAnyAction = true; } } + else if (action.reaction == G_REACTION::ANIM_STRING) + { + static constexpr std::array animationList = { + "sp00", + "sp10", + "sp20", + "sp30", + "sp40" + }; + + if (POwner->loc.zone == nullptr || !POwner->PMaster || POwner->PAI->PathFind->IsFollowingPath()) + { + continue; + } + + if (action.select == G_SELECT::RANDOM_ANIMATION) + { + // action.select_arg is the number of animations to include from the animationList. + // Clamp to [1, animationList.size()]. + size_t usableCount = static_cast(action.select_arg); + usableCount = std::clamp(usableCount, 1, animationList.size()); + + const auto randomIndex = xirand::GetRandomNumber(0, usableCount); + const auto* animString = animationList[randomIndex]; + + POwner->loc.zone->PushPacket( + POwner, + CHAR_INRANGE, + std::make_unique(POwner, target, animString)); + + executedAnyAction = true; + } + else if (action.select == G_SELECT::SPECIFIC) + { + size_t animIndex = static_cast(action.select_arg); + + if (animIndex >= animationList.size()) + { + animIndex = xirand::GetRandomNumber(0, 3); + } + + const auto* animString = animationList[animIndex]; + + POwner->loc.zone->PushPacket( + POwner, + CHAR_INRANGE, + std::make_unique(POwner, target, animString)); + + executedAnyAction = true; + } + } } // If we executed any action and the gambit has a retry_delay, set last_used @@ -954,7 +1015,7 @@ auto CGambitsContainer::Tick(timer::time_point tick) -> Task } } -bool CGambitsContainer::CheckTrigger(const CBattleEntity* triggerTarget, PredicateGroup_t& predicateGroup) +bool CGambitsContainer::CheckTrigger(const CBattleEntity* triggerTarget, const Gambit_t& gambit, size_t predicateGroupIndex, PredicateGroup_t& predicateGroup) { TracyZoneScoped; @@ -967,8 +1028,9 @@ bool CGambitsContainer::CheckTrigger(const CBattleEntity* triggerTarget, Predica std::vector predicateResults; // Iterate and collect results from all predicates in the group - for (auto& predicate : predicateGroup.predicates) + for (size_t predicateIndex = 0; predicateIndex < predicateGroup.predicates.size(); ++predicateIndex) { + auto& predicate = predicateGroup.predicates[predicateIndex]; switch (predicate.condition) { case G_CONDITION::ALWAYS: @@ -1016,6 +1078,36 @@ bool CGambitsContainer::CheckTrigger(const CBattleEntity* triggerTarget, Predica predicateResults.push_back(!triggerTarget->StatusEffectContainer->HasStatusEffect(static_cast(predicate.condition_arg))); continue; } + case G_CONDITION::TIMER: + { + if (predicate.condition_arg == 0) + { + predicateResults.push_back(true); + continue; + } + + const auto key = fmt::format("{}:{}:{}", gambit.identifier, predicateGroupIndex, predicateIndex); + const auto interval = std::chrono::seconds(predicate.condition_arg); + const auto now = timer::now(); + + auto [it, inserted] = m_timerConditionLastTrigger.try_emplace(key, now); + + if (inserted) + { + // first evaluation is true, then gated by interval + predicateResults.push_back(true); + } + else if (now - it->second >= interval) + { + it->second = now; + predicateResults.push_back(true); + } + else + { + predicateResults.push_back(false); + } + continue; + } case G_CONDITION::JA_ON_COOLDOWN: { auto* PAbility = ability::GetAbility(static_cast(predicate.condition_arg)); diff --git a/src/map/ai/helpers/gambits_container.h b/src/map/ai/helpers/gambits_container.h index 53458ca3adb..83717445520 100644 --- a/src/map/ai/helpers/gambits_container.h +++ b/src/map/ai/helpers/gambits_container.h @@ -101,16 +101,18 @@ enum class G_CONDITION : uint16 SUB_ANIMATION = 36, JA_ON_COOLDOWN = 37, VAL_URIEL_CHECK = 38, + TIMER = 39, // condition_arg in seconds }; enum class G_REACTION : uint16 { - ATTACK = 0, - RATTACK = 1, - MA = 2, - JA = 3, - WS = 4, - MS = 5, + ATTACK = 0, + RATTACK = 1, + MA = 2, + JA = 3, + WS = 4, + MS = 5, + ANIM_STRING = 6, }; enum class G_SELECT : uint16 @@ -134,6 +136,7 @@ enum class G_SELECT : uint16 HELIX_MOB_WEAKNESS = 16, DEF_BAR_ELEMENT = 17, RUNE_DAY = 18, + RANDOM_ANIMATION = 19, }; enum class G_TP_TRIGGER : uint16 @@ -304,7 +307,7 @@ class CGambitsContainer uint16 tp_value; private: - bool CheckTrigger(const CBattleEntity* triggerTarget, PredicateGroup_t& predicateGroup); + bool CheckTrigger(const CBattleEntity* triggerTarget, const Gambit_t& gambit, size_t predicateGroupIndex, PredicateGroup_t& predicateGroup); bool TryTrustSkill(); bool PartyHasHealer(); bool PartyHasTank(); @@ -313,6 +316,8 @@ class CGambitsContainer timer::time_point m_lastAction; std::vector gambits; + std::unordered_map m_timerConditionLastTrigger; + std::set melee_jobs = { JOB_WAR, JOB_MNK, diff --git a/src/map/enums/key_items.h b/src/map/enums/key_items.h index f399737e728..0d3f2b882d5 100644 --- a/src/map/enums/key_items.h +++ b/src/map/enums/key_items.h @@ -56,6 +56,9 @@ enum class KeyItem : uint16_t FROG_FISHING = 1976, SERPENT_RUMORS = 1977, MOOCHING = 1978, + WINDURST_TRUST_PERMIT = 2497, + BASTOK_TRUST_PERMIT = 2499, + SAN_DORIA_TRUST_PERMIT = 2501, JOB_BREAKER = 2544, IMPERIAL_CHAIR = 2826, DECORATIVE_CHAIR = 2827, diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index ab7c9843892..a8254869e40 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -13706,6 +13706,7 @@ auto CLuaBaseEntity::addStatusEffect(const EFFECT effectId, sol::table params) c const auto icon = params["icon"].get_or(static_cast(effectId)); const auto subType = params["subType"].get_or(0u); const auto subPower = static_cast(params["subPower"].get_or(0.0)); + const auto subIcon = params["subIcon"].get_or(0u); const auto tier = params["tier"].get_or(0); const auto flag = params["flag"].get_or(0u); const auto sourceType = params["sourceType"].get_or(0); @@ -13720,6 +13721,7 @@ auto CLuaBaseEntity::addStatusEffect(const EFFECT effectId, sol::table params) c std::chrono::milliseconds(static_cast(duration * 1000)), subType, subPower, + subIcon, tier, flag); diff --git a/src/map/lua/lua_statuseffect.cpp b/src/map/lua/lua_statuseffect.cpp index cb1d82e2bde..477a54b8188 100644 --- a/src/map/lua/lua_statuseffect.cpp +++ b/src/map/lua/lua_statuseffect.cpp @@ -159,6 +159,20 @@ void CLuaStatusEffect::setSubPower(uint16 subpower) m_PLuaStatusEffect->SetSubPower(subpower); } +/************************************************************************ + * * + * Sets the icon used by the sub effect of auras etc * + * Will default to the main icon if not set * + * * + ************************************************************************/ + +void CLuaStatusEffect::setSubIcon(uint16 subIcon) +{ + m_PLuaStatusEffect->SetSubIcon(subIcon); +} + +//======================================================// + void CLuaStatusEffect::setTier(uint16 tier) { m_PLuaStatusEffect->SetTier(tier); @@ -236,6 +250,11 @@ uint16 CLuaStatusEffect::getIcon() return m_PLuaStatusEffect->GetIcon(); } +uint16 CLuaStatusEffect::getSubIcon() +{ + return m_PLuaStatusEffect->GetSubIcon(); +} + uint16 CLuaStatusEffect::getSourceType() { return m_PLuaStatusEffect->GetSourceType(); @@ -276,6 +295,7 @@ void CLuaStatusEffect::Register() SOL_REGISTER("addMod", CLuaStatusEffect::addMod); SOL_REGISTER("getSubPower", CLuaStatusEffect::getSubPower); SOL_REGISTER("setSubPower", CLuaStatusEffect::setSubPower); + SOL_REGISTER("setSubIcon", CLuaStatusEffect::setSubIcon); SOL_REGISTER("getTier", CLuaStatusEffect::getTier); SOL_REGISTER("setTier", CLuaStatusEffect::setTier); SOL_REGISTER("getTick", CLuaStatusEffect::getTick); @@ -287,6 +307,7 @@ void CLuaStatusEffect::Register() SOL_REGISTER("delEffectFlag", CLuaStatusEffect::delEffectFlag); SOL_REGISTER("hasEffectFlag", CLuaStatusEffect::hasEffectFlag); SOL_REGISTER("getIcon", CLuaStatusEffect::getIcon); + SOL_REGISTER("getSubIcon", CLuaStatusEffect::getSubIcon); } std::ostream& operator<<(std::ostream& os, const CLuaStatusEffect& effect) diff --git a/src/map/lua/lua_statuseffect.h b/src/map/lua/lua_statuseffect.h index e252b0f39a1..244a0f32cc3 100644 --- a/src/map/lua/lua_statuseffect.h +++ b/src/map/lua/lua_statuseffect.h @@ -48,6 +48,7 @@ class CLuaStatusEffect auto getOriginID() -> uint32; uint16 getPower(); uint16 getSubPower(); + uint16 getSubIcon(); uint16 getTier(); uint32 getDuration(); uint32 getStartTime(); @@ -62,6 +63,7 @@ class CLuaStatusEffect auto setOriginID(uint32 originid) -> void; void setPower(uint16 power); void setSubPower(uint16 subpower); + void setSubIcon(uint16 subIcon); void setTier(uint16 tier); void setDuration(uint32 duration); void setTick(uint32 tick); diff --git a/src/map/status_effect.cpp b/src/map/status_effect.cpp index dfbecbf78b9..f5f649c5485 100644 --- a/src/map/status_effect.cpp +++ b/src/map/status_effect.cpp @@ -27,12 +27,13 @@ #include "status_effect_container.h" #include -CStatusEffect::CStatusEffect(EFFECT id, uint16 icon, uint16 power, timer::duration tick, timer::duration duration, uint32 subid, uint16 subPower, uint16 tier, uint32 flags, uint16 sourceType, uint32 sourceTypeParam, uint32 originID) +CStatusEffect::CStatusEffect(EFFECT id, uint16 icon, uint16 power, timer::duration tick, timer::duration duration, uint32 subid, uint16 subPower, uint16 subIcon, uint16 tier, uint32 flags, uint16 sourceType, uint32 sourceTypeParam, uint32 originID) : m_StatusID(id) , m_SubID(subid) , m_Icon(icon) , m_Power(power) , m_SubPower(subPower) +, m_SubIcon(subIcon) , m_Tier(tier) , m_Flags(flags) , m_OriginID(originID) @@ -114,6 +115,11 @@ uint16 CStatusEffect::GetSubPower() const return m_SubPower; } +uint16 CStatusEffect::GetSubIcon() const +{ + return m_SubIcon; +} + uint16 CStatusEffect::GetTier() const { return m_Tier; @@ -180,6 +186,17 @@ void CStatusEffect::SetIcon(uint16 Icon) m_POwner->StatusEffectContainer->UpdateStatusIcons(); } +void CStatusEffect::SetSubIcon(uint16 subIcon) +{ + if (m_POwner == nullptr) + { + ShowWarning("m_POwner was null."); + return; + } + m_SubIcon = subIcon; + m_POwner->StatusEffectContainer->UpdateStatusIcons(); +} + auto CStatusEffect::SetSource(uint16 sourceType, uint32 sourceTypeParam) -> void { m_SourceType = sourceType; diff --git a/src/map/status_effect.h b/src/map/status_effect.h index 40333336f58..df59625695c 100644 --- a/src/map/status_effect.h +++ b/src/map/status_effect.h @@ -752,11 +752,21 @@ enum EFFECT : uint16 EFFECT_TOMAHAWK = 805, // Silent status effect inflicted by a Warrior using the "Tomahawk" job ability EFFECT_NUKE_WALL = 806, // Custom effect for NM type mobs only. Applied by elemental magic damage sources + // TRUST Aura Effects + EFFECT_TRUST_AURA_CHR = 807, // CHR Aura, + 9.7 % Defense Bonus, + 5 Magic Defense Bonus and +5 CHR at lv.99, stacks with player Indi / Geo CHR. + EFFECT_TRUST_AURA_HASTE = 808, // HASTE Aura, Haste + 20 %, Accuracy + 30, Ranged Accuracy + 30 and Magic Accuracy + 30 at lvl 99, stacks with player Indi / Geo HASTE. + EFFECT_TRUST_AURA_EXP = 809, // EXP Aura, +20% dedication effect for Experience Points and Capacity Points, stacks with other forms of dedication. + EFFECT_TRUST_AURA_ACC = 810, // ACC Aura, Accuracy + 24, Ranged accuracy + 24, and DEX + 5 at lvl 99, stacks with player Indi / Geo PRECISION. + EFFECT_TRUST_AURA_REFRESH = 811, // REFRESH Aura, 3 MP / tick at lvl 99 stacks with player Indi / Geo REFRESH, also grants an increase to magical skill gain rate. + EFFECT_TRUST_AURA_REGEN = 812, // REGEN Aura, 6 HP / tick at lvl 99 stacks with player Indi / Geo REGEN.also grants an increase to physical combat skill gain rate. + EFFECT_TRUST_AURA_MAGIC_ATTACK = 813, // MATT Aura, Magic Attack Boost + 19 and +19 Magic Accuracy boost at lvl 99, stacks with player Indi / Geo ACUMEN. + // End of Trust Aura Effects + // 789 - // 807-1022 + // 813-1022 // EFFECT_PLACEHOLDER = 1023 // The client dat file seems to have only this many "slots", results of exceeding that are untested. }; -#define MAX_EFFECTID 807 // 768 real + 39 custom +#define MAX_EFFECTID 814 // 768 real + 46 custom DECLARE_FORMAT_AS_UNDERLYING(EFFECT); /************************************************************************ @@ -791,6 +801,7 @@ class CStatusEffect uint16 GetIcon() const; uint16 GetPower() const; uint16 GetSubPower() const; + uint16 GetSubIcon() const; uint16 GetTier() const; uint32 GetEffectFlags() const; uint16 GetEffectType() const; @@ -812,6 +823,7 @@ class CStatusEffect auto SetSource(uint16 sourceType, uint32 sourceTypeParam) -> void; void SetPower(uint16 Power); void SetSubPower(uint16 subPower); + void SetSubIcon(uint16 subIcon); void SetTier(uint16 tier); void SetDuration(timer::duration Duration); void SetOwner(CBattleEntity* Owner); @@ -831,7 +843,7 @@ class CStatusEffect std::vector modList; // List of modifiers bool deleted{ false }; - CStatusEffect(EFFECT id, uint16 icon, uint16 power, timer::duration tick, timer::duration duration, uint32 subid = 0, uint16 subPower = 0, uint16 tier = 0, uint32 flags = 0, uint16 sourceType = EffectSourceType::SOURCE_NONE, uint32 sourceTypeParam = 0, uint32 originID = 0); + CStatusEffect(EFFECT id, uint16 icon, uint16 power, timer::duration tick, timer::duration duration, uint32 subid = 0, uint16 subPower = 0, uint16 subIcon = 0, uint16 tier = 0, uint32 flags = 0, uint16 sourceType = EffectSourceType::SOURCE_NONE, uint32 sourceTypeParam = 0, uint32 originID = 0); ~CStatusEffect(); @@ -843,6 +855,7 @@ class CStatusEffect uint16 m_Icon{ 0 }; // Effect icon uint16 m_Power{ 0 }; // Strength of effect uint16 m_SubPower{ 0 }; // Secondary power of the effect + uint16 m_SubIcon{ 0 }; // Secondary icon for the sub effect (used for things like setting an Aura sub effect icon) uint16 m_Tier{ 0 }; // Tier of the effect uint32 m_Flags{ 0 }; // Effect flags (conditions for its disappearance) uint32 m_OriginID{ 0 }; // The effect's origin ID. (This is usually the ID of the entity that created the effect) diff --git a/src/map/status_effect_container.cpp b/src/map/status_effect_container.cpp index e42a85bf3d6..dfb65f00b9d 100644 --- a/src/map/status_effect_container.cpp +++ b/src/map/status_effect_container.cpp @@ -1876,14 +1876,13 @@ void CStatusEffectContainer::HandleAura(CStatusEffect* PStatusEffect) CBattleEntity* PEntity = m_POwner; AURA_TARGET auraTarget = static_cast(PStatusEffect->GetTier()); + float aura_range = 6.0f + (PEntity->getMod(Mod::AURA_SIZE) / 100.0f); // Adding to this mod should be the value you want * 100 if (PEntity->objtype == TYPE_PET || PEntity->objtype == TYPE_TRUST) { PEntity = PEntity->PMaster; } - float aura_range = 6.0f + (PEntity->getMod(Mod::AURA_SIZE) / 100.0f); // Adding to this mod should be the value you want * 100 - if (PEntity->objtype == TYPE_PC) { auto* PChar = static_cast(PEntity); @@ -1918,8 +1917,10 @@ void CStatusEffectContainer::HandleAura(CStatusEffect* PStatusEffect) } else { + uint16 icon = PStatusEffect->GetSubIcon() > 0 ? PStatusEffect->GetSubIcon() : PStatusEffect->GetSubID(); + PEffect = new CStatusEffect(static_cast(PStatusEffect->GetSubID()), // Effect ID - PStatusEffect->GetSubID(), // Effect Icon (Associated with ID) + icon, // Effect Icon PStatusEffect->GetSubPower(), // Power 3s, // Tick 4s); // Duration @@ -1960,8 +1961,10 @@ void CStatusEffectContainer::HandleAura(CStatusEffect* PStatusEffect) } else { + uint16 icon = PStatusEffect->GetSubIcon() > 0 ? PStatusEffect->GetSubIcon() : PStatusEffect->GetSubID(); + PEffect = new CStatusEffect(static_cast(PStatusEffect->GetSubID()), // Effect ID - PStatusEffect->GetSubID(), // Effect Icon (Associated with ID) + icon, // Effect Icon PStatusEffect->GetSubPower(), // Power 3s, // Tick 4s); // Duration @@ -2005,8 +2008,10 @@ void CStatusEffectContainer::HandleAura(CStatusEffect* PStatusEffect) } else { + uint16 icon = PStatusEffect->GetSubIcon() > 0 ? PStatusEffect->GetSubIcon() : PStatusEffect->GetSubID(); + PEffect = new CStatusEffect(static_cast(PStatusEffect->GetSubID()), // Effect ID - PStatusEffect->GetSubID(), // Effect Icon (Associated with ID) + icon, // Effect Icon PStatusEffect->GetSubPower(), // Power 3s, // Tick 4s); // Duration @@ -2050,8 +2055,10 @@ void CStatusEffectContainer::HandleAura(CStatusEffect* PStatusEffect) } else { + uint16 icon = PStatusEffect->GetSubIcon() > 0 ? PStatusEffect->GetSubIcon() : PStatusEffect->GetSubID(); + PEffect = new CStatusEffect(static_cast(PStatusEffect->GetSubID()), // Effect ID - PStatusEffect->GetSubID(), // Effect Icon (Associated with ID) + icon, // Effect Icon PStatusEffect->GetSubPower(), // Power 3s, // Tick 4s); // Duration diff --git a/src/map/utils/charutils.cpp b/src/map/utils/charutils.cpp index 64e129b1080..9f7dc5acd38 100644 --- a/src/map/utils/charutils.cpp +++ b/src/map/utils/charutils.cpp @@ -1033,6 +1033,28 @@ void LoadSpells(CCharEntity* PChar) } } } + + // Handle trust spells that are enabled via settings. + bool hasTrustPermit = + charutils::hasKeyItem(PChar, KeyItem::WINDURST_TRUST_PERMIT) || + charutils::hasKeyItem(PChar, KeyItem::BASTOK_TRUST_PERMIT) || + charutils::hasKeyItem(PChar, KeyItem::SAN_DORIA_TRUST_PERMIT); + + if (hasTrustPermit) + { + static const std::unordered_map trustSpells = { + { 1, 1002 }, // Cornelia + { 2, 1003 }, // Matsui-P + }; // This can be expanded if more trust spells are added as settings options. + + uint8 trustSetting = settings::get("main.ENABLE_LIMITED_TIME_TRUST"); + + auto it = trustSpells.find(trustSetting); + if (it != trustSpells.end()) + { + PChar->m_SpellList.set(it->second); + } + } } /************************************************************************