diff --git a/FFXIVClassic Map Server/actors/chara/Character.cs b/FFXIVClassic Map Server/actors/chara/Character.cs index d143629d..bbf0d87b 100644 --- a/FFXIVClassic Map Server/actors/chara/Character.cs +++ b/FFXIVClassic Map Server/actors/chara/Character.cs @@ -1109,7 +1109,7 @@ namespace FFXIVClassic_Map_Server.Actors lua.LuaEngine.CallLuaBattleCommandFunction(this, command, folder, "onSkillFinish", this, chara, command, action, actions); //cached script //skill.CallLuaFunction(owner, "onSkillFinish", this, chara, command, action, actions); - if (action.hitType > HitType.Evade && action.hitType != HitType.Resist) + if (action.ActionLanded()) { hitTarget = true; hitCount++; diff --git a/FFXIVClassic Map Server/actors/chara/ai/utils/BattleUtils.cs b/FFXIVClassic Map Server/actors/chara/ai/utils/BattleUtils.cs index bfdf9eed..7e0a4e69 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/utils/BattleUtils.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/utils/BattleUtils.cs @@ -17,37 +17,55 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils static class BattleUtils { - public static Dictionary SingleHitTypeTextIds = new Dictionary() + public static Dictionary PhysicalHitTypeTextIds = new Dictionary() { { HitType.Miss, 30311 }, { HitType.Evade, 30310 }, { HitType.Parry, 30308 }, { HitType.Block, 30306 }, - { HitType.Resist, 30310 }, //Resists seem to use the evade text id { HitType.Hit, 30301 }, { HitType.Crit, 30302 } }; + public static Dictionary MagicalHitTypeTextIds = new Dictionary() + { + { HitType.SingleResist,30318 }, + { HitType.DoubleResist,30317 }, + { HitType.TripleResist, 30316 },//Triple Resists seem to use the same text ID as full resists + { HitType.FullResist,30316 }, + { HitType.Hit, 30319 }, + { HitType.Crit, 30392 } //Unsure why crit is separated from the rest of the ids + }; + public static Dictionary MultiHitTypeTextIds = new Dictionary() { { HitType.Miss, 30449 }, //The attack misses. { HitType.Evade, 0 }, //Evades were removed before multi hit skills got their own messages, so this doesnt exist { HitType.Parry, 30448 }, //[Target] parries, taking x points of damage. { HitType.Block, 30447 }, //[Target] blocks, taking x points of damage. - { HitType.Resist, 0 }, //No spells are multi-hit, so this doesn't exist { HitType.Hit, 30443 }, //[Target] tales x points of damage { HitType.Crit, 30444 } //Critical! [Target] takes x points of damage. }; - public static Dictionary HitTypeEffects = new Dictionary() + public static Dictionary HitTypeEffectsPhysical = new Dictionary() { { HitType.Miss, 0 }, { HitType.Evade, HitEffect.Evade }, { HitType.Parry, HitEffect.Parry }, { HitType.Block, HitEffect.Block }, - { HitType.Resist, HitEffect.RecoilLv1 },//Probably don't need this, resists are handled differently to the rest { HitType.Hit, HitEffect.Hit }, - { HitType.Crit, HitEffect.Crit } + { HitType.Crit, HitEffect.Crit | HitEffect.CriticalHit } + }; + + //Magic attacks can't miss, be blocked, or parried. Resists are technically evades + public static Dictionary HitTypeEffectsMagical = new Dictionary() + { + { HitType.SingleResist, HitEffect.WeakResist }, + { HitType.DoubleResist, HitEffect.WeakResist }, + { HitType.TripleResist, HitEffect.WeakResist }, + { HitType.FullResist, HitEffect.FullResist }, + { HitType.Hit, HitEffect.NoResist }, + { HitType.Crit, HitEffect.Crit } }; public static Dictionary KnockbackEffects = new Dictionary() @@ -206,7 +224,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils //Or we could have HitTypes for DoubleResist, TripleResist, and FullResist that get used here. public static void CalculateResistDamage(Character attacker, Character defender, BattleCommand skill, CommandResult action) { - double percentResist = 0.5; + //Every tier of resist is a 25% reduction in damage. ie SingleResist is 25% damage taken down, Double is 50% damage taken down, etc + double percentResist = 0.25 * (action.hitType - HitType.SingleResist + 1); action.amountMitigated = (ushort)(action.amount * (1 - percentResist)); action.amount = (ushort)(action.amount * percentResist); @@ -360,11 +379,26 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils return false; } + //This probably isn't totally correct but it's close enough for now. + //Full Resists seem to be calculated in a different way because the resist rates don't seem to line up with kanikan's testing (their tests didn't show any full resists) + //Non-spells with elemental damage can be resisted, it just doesnt say in the chat that they were. As far as I can tell, all mob-specific attacks are considered not to be spells public static bool TryResist(Character attacker, Character defender, BattleCommand skill, CommandResult action) { - if ((Program.Random.NextDouble() * 100) <= action.resistRate) + //The rate degrades for each check. Meaning with 100% resist, the attack will always be resisted, but it won't necessarily be a triple or full resist + //Rates beyond 100 still increase the chance for higher resist tiers though + double rate = action.resistRate; + + int i = -1; + + while ((Program.Random.NextDouble() * 100) <= rate && i < 4) { - action.hitType = HitType.Resist; + rate /= 2; + i++; + } + + if (i != -1) + { + action.hitType = (HitType) ((int) HitType.SingleResist + i); CalculateResistDamage(attacker, defender, skill, action); return true; } @@ -455,7 +489,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils } //Actions have different text ids depending on whether they're a part of a multi-hit ws or not. - Dictionary textIds = SingleHitTypeTextIds; + Dictionary textIds = PhysicalHitTypeTextIds; //If this is the first hit of a multi hit command, add the "You use [command] on [target]" action //Needs to be done here because certain buff messages appear before it. @@ -484,17 +518,20 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils public static void FinishActionSpell(Character attacker, Character defender, BattleCommand skill, CommandResult action, CommandResultContainer actionContainer = null) { + //I'm assuming that like physical attacks stoneskin is taken into account before mitigation + HandleStoneskin(defender, action); + //Determine the hit type of the action - if (!TryMiss(attacker, defender, skill, action)) + //Spells don't seem to be able to miss, instead magic acc/eva is used for resists (which are generally called evades in game) + //Unlike blocks and parries, crits do not go through resists. + if (!TryResist(attacker, defender, skill, action)) { - HandleStoneskin(defender, action); if (!TryCrit(attacker, defender, skill, action)) - if (!TryResist(attacker, defender, skill, action)) - action.hitType = HitType.Hit; + action.hitType = HitType.Hit; } - //There are no multi-hit spells - action.worldMasterTextId = SingleHitTypeTextIds[action.hitType]; + //There are no multi-hit spells, so we don't need to take that into account + action.worldMasterTextId = MagicalHitTypeTextIds[action.hitType]; //Set the hit effect SetHitEffectSpell(attacker, defender, skill, action); @@ -547,10 +584,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils hitEffect |= HitEffect.RecoilLv3; } - hitEffect |= HitTypeEffects[hitType]; + hitEffect |= HitTypeEffectsPhysical[hitType]; //For combos that land, add the combo effect - if (skill != null && skill.isCombo && hitType > HitType.Evade && hitType != HitType.Evade && !skill.comboEffectAdded) + if (skill != null && skill.isCombo && action.ActionLanded() && !skill.comboEffectAdded) { hitEffect |= (HitEffect)(skill.comboStep << 15); skill.comboEffectAdded = true; @@ -560,7 +597,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils if (hitType >= HitType.Parry) { //Protect / Shell only show on physical/ magical attacks respectively. - if (defender.statusEffects.HasStatusEffect(StatusEffectId.Protect)) + if (defender.statusEffects.HasStatusEffect(StatusEffectId.Protect) || defender.statusEffects.HasStatusEffect(StatusEffectId.Protect2)) if (action != null) hitEffect |= HitEffect.Protect; @@ -577,20 +614,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils var hitEffect = HitEffect.MagicEffectType; HitType hitType = action.hitType; - //Recoil levels for spells are a bit different than physical. Recoil levels are used for resists. - //Lv1 is for larger resists, Lv2 is for smaller resists and Lv3 is for no resists. Crit is still used for crits - if (hitType == HitType.Resist) - { - //todo: calculate resist levels and figure out what the difference between Lv1 and 2 in retail was. For now assuming a full resist with 0 damage dealt is Lv1, all other resists Lv2 - if (action.amount == 0) - hitEffect |= HitEffect.RecoilLv1; - else - hitEffect |= HitEffect.RecoilLv2; - } - else - hitEffect |= HitEffect.RecoilLv3; - hitEffect |= HitTypeEffects[hitType]; + hitEffect |= HitTypeEffectsMagical[hitType]; if (skill != null && skill.isCombo && !skill.comboEffectAdded) { @@ -599,16 +624,15 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils } //if attack hit the target, take into account protective status effects - if (hitType >= HitType.Block) + if (action.ActionLanded()) { //Protect / Shell only show on physical/ magical attacks respectively. + //The magic hit effect category only has a flag for shell (and another shield effect that seems unused) + //Even though traited protect gives magic defense, the shell effect doesn't play on attacks + //This also means stoneskin doesnt show, but it does reduce damage if (defender.statusEffects.HasStatusEffect(StatusEffectId.Shell)) if (action != null) - hitEffect |= HitEffect.Shell; - - if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin)) - if (action != null) - hitEffect |= HitEffect.Stoneskin; + hitEffect |= HitEffect.MagicShell; } action.effectId = (uint)hitEffect; } @@ -659,7 +683,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils double rand = Program.Random.NextDouble(); //Statuses only land for non-resisted attacks and attacks that hit - if (skill != null && skill.statusId != 0 && (action.hitType > HitType.Evade && action.hitType != HitType.Resist) && rand < skill.statusChance) + if (skill != null && skill.statusId != 0 && (action.ActionLanded()) && rand < skill.statusChance) { StatusEffect effect = Server.GetWorldManager().GetStatusEffect(skill.statusId); //Because combos might change duration or tier diff --git a/FFXIVClassic Map Server/packets/send/Actor/battle/CommandResult.cs b/FFXIVClassic Map Server/packets/send/Actor/battle/CommandResult.cs index 212a493d..df5b962f 100644 --- a/FFXIVClassic Map Server/packets/send/Actor/battle/CommandResult.cs +++ b/FFXIVClassic Map Server/packets/send/Actor/battle/CommandResult.cs @@ -195,9 +195,12 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle Evade = 1, Parry = 2, Block = 3, - Resist = 4, - Hit = 5, - Crit = 6 + SingleResist = 4, + DoubleResist = 5, + TripleResist = 6, + FullResist = 7, + Hit = 8, + Crit = 9 } //Type of action @@ -387,5 +390,11 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle { return (ushort)hitType; } + + //Whether this action didn't miss, and wasn't evaded or resisted + public bool ActionLanded() + { + return hitType > HitType.Evade && hitType != HitType.SingleResist && hitType != HitType.DoubleResist && hitType != HitType.FullResist; + } } }