mirror of
https://bitbucket.org/Ioncannon/project-meteor-server.git
synced 2025-07-23 02:56:02 +02:00
Combat additions
Added formulas for base EXP gain and chain experience Added basic scripts for most player abilities and effects Added stat gains for some abilities Changed status flags Fixed bug with player death Fixed bug where auto attacks didnt work when not locked on Added traits
This commit is contained in:
parent
b8d6a943aa
commit
c5ce2ec771
239 changed files with 5125 additions and 1237 deletions
|
@ -8,6 +8,7 @@ using FFXIVClassic_Map_Server.Actors;
|
|||
using FFXIVClassic_Map_Server.packets.send.actor;
|
||||
using FFXIVClassic_Map_Server.packets.send.actor.battle;
|
||||
using FFXIVClassic_Map_Server.actors.chara.player;
|
||||
using FFXIVClassic_Map_Server.actors.chara.npc;
|
||||
using FFXIVClassic_Map_Server.dataobjects;
|
||||
using FFXIVClassic.Common;
|
||||
|
||||
|
@ -16,17 +17,28 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
static class BattleUtils
|
||||
{
|
||||
|
||||
public static Dictionary<HitType, ushort> HitTypeTextIds = new Dictionary<HitType, ushort>()
|
||||
public static Dictionary<HitType, ushort> SingleHitTypeTextIds = new Dictionary<HitType, ushort>()
|
||||
{
|
||||
{ HitType.Miss, 30311 },
|
||||
{ HitType.Evade, 30310 },
|
||||
{ HitType.Parry, 30308 },
|
||||
{ HitType.Block, 30306 },
|
||||
{ HitType.Resist, 30306 },//I can't find what the actual textid for resists is
|
||||
{ HitType.Resist, 30310 }, //Resists seem to use the evade text id
|
||||
{ HitType.Hit, 30301 },
|
||||
{ HitType.Crit, 30302 }
|
||||
};
|
||||
|
||||
public static Dictionary<HitType, ushort> MultiHitTypeTextIds = new Dictionary<HitType, ushort>()
|
||||
{
|
||||
{ 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<HitType, HitEffect> HitTypeEffects = new Dictionary<HitType, HitEffect>()
|
||||
{
|
||||
{ HitType.Miss, 0 },
|
||||
|
@ -38,6 +50,13 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
{ HitType.Crit, HitEffect.Crit }
|
||||
};
|
||||
|
||||
//Most of these numbers I'm fairly certain are correct. The repeated numbers at levels 23 and 48 I'm less sure about but they do match some weird spots in the EXP graph
|
||||
|
||||
public static ushort[] BASEEXP = {150, 150, 150, 150, 150, 150, 150, 150, 150, 150, //Level <= 10
|
||||
150, 150, 150, 150, 150, 150, 150, 150, 160, 170, //Level <= 20
|
||||
180, 190, 190, 200, 210, 220, 230, 240, 250, 260, //Level <= 30
|
||||
270, 280, 290, 300, 310, 320, 330, 340, 350, 360, //Level <= 40
|
||||
370, 380, 380, 390, 400, 410, 420, 430, 430, 440}; //Level <= 50
|
||||
|
||||
public static bool TryAttack(Character attacker, Character defender, BattleAction action, ref BattleAction error)
|
||||
{
|
||||
|
@ -65,53 +84,61 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
//Damage calculations
|
||||
//Calculate damage of action
|
||||
//We could probably just do this when determining the action's hit type
|
||||
public static void CalculateDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
public static void CalculatePhysicalDamageTaken(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
|
||||
short dlvl = (short)(defender.GetLevel() - attacker.GetLevel());
|
||||
|
||||
switch (action.hitType)
|
||||
{
|
||||
//Misses and evades deal no damage.
|
||||
case (HitType.Miss):
|
||||
case (HitType.Evade):
|
||||
action.amount = 0;
|
||||
break;
|
||||
// todo: figure out parry damage reduction. For now assume 25% reduction
|
||||
case (HitType.Parry):
|
||||
CalculateParryDamage(attacker, defender, skill, action);
|
||||
break;
|
||||
case (HitType.Block):
|
||||
CalculateBlockDamage(attacker, defender, skill, action);
|
||||
break;
|
||||
//There are 3 (or 4?) tiers of resists, each decreasing damage dealt by 25%. For now just assume level 2 resist (50% reduction)
|
||||
// todo: figure out resist tiers
|
||||
case (HitType.Resist):
|
||||
CalculateResistDamage(attacker, defender, skill, action);
|
||||
break;
|
||||
case (HitType.Crit):
|
||||
CalculateCritDamage(attacker, defender, skill, action);
|
||||
break;
|
||||
}
|
||||
ushort finalAmount = action.amount;
|
||||
// todo: physical resistances
|
||||
|
||||
//dlvl, Defense, and Vitality all effect how much damage is taken after hittype takes effect
|
||||
//player attacks cannot do more than 9999 damage.
|
||||
action.amount = (ushort) (finalAmount - CalculateDlvlModifier(dlvl) * (defender.GetMod((uint)Modifier.Defense) + 0.67 * defender.GetMod((uint)Modifier.Vitality))).Clamp(0, 9999);
|
||||
|
||||
|
||||
//VIT is turned into Defense at a 3:2 ratio in calculatestats, so don't need to do that here
|
||||
double damageTakenPercent = 1 - (defender.GetMod(Modifier.DamageTakenDown) / 100.0);
|
||||
action.amount = (ushort)(action.amount - CalculateDlvlModifier(dlvl) * (defender.GetMod((uint)Modifier.Defense))).Clamp(0, 9999);
|
||||
action.amount = (ushort)(action.amount * damageTakenPercent).Clamp(0, 9999);
|
||||
}
|
||||
|
||||
|
||||
public static void CalculateSpellDamageTaken(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
short dlvl = (short)(defender.GetLevel() - attacker.GetLevel());
|
||||
|
||||
// todo: elemental resistances
|
||||
//Patch 1.19:
|
||||
//Magic Defense has been abolished and no longer appears in equipment attributes.
|
||||
//The effect of elemental attributes has been changed to that of reducing damage from element-based attacks.
|
||||
|
||||
//http://kanican.livejournal.com/55370.html:
|
||||
//elemental resistance stats are not actually related to resists (except for status effects), instead they impact damage taken
|
||||
|
||||
|
||||
//dlvl, Defense, and Vitality all effect how much damage is taken after hittype takes effect
|
||||
//player attacks cannot do more than 9999 damage.
|
||||
double damageTakenPercent = 1 - (defender.GetMod(Modifier.DamageTakenDown) / 100.0);
|
||||
action.amount = (ushort)(action.amount - CalculateDlvlModifier(dlvl) * (defender.GetMod((uint)Modifier.Defense) + 0.67 * defender.GetMod((uint)Modifier.Vitality))).Clamp(0, 9999);
|
||||
action.amount = (ushort)(action.amount * damageTakenPercent).Clamp(0, 9999);
|
||||
}
|
||||
|
||||
|
||||
public static void CalculateBlockDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
double percentBlocked = defender.GetMod((uint)Modifier.Block) * .2;//Every point of Block adds .2% to how much is blocked
|
||||
percentBlocked += defender.GetMod((uint)Modifier.Vitality) * .1;//Every point of vitality adds .1% to how much is blocked
|
||||
double percentBlocked;
|
||||
|
||||
percentBlocked = 1 - percentBlocked;
|
||||
action.amount = (ushort)(action.amount * percentBlocked);
|
||||
//Aegis boon forces a full block
|
||||
if (defender.statusEffects.HasStatusEffect(StatusEffectId.AegisBoon))
|
||||
percentBlocked = 1.0;
|
||||
else
|
||||
{
|
||||
//Is this a case where VIT gives Block?
|
||||
percentBlocked = defender.GetMod((uint)Modifier.Block) * 0.002;//Every point of Block adds .2% to how much is blocked
|
||||
percentBlocked += defender.GetMod((uint)Modifier.Vitality) * 0.001;//Every point of vitality adds .1% to how much is blocked
|
||||
}
|
||||
|
||||
action.amountMitigated = (ushort)(action.amount * percentBlocked);
|
||||
action.amount = (ushort)(action.amount * (1.0 - percentBlocked));
|
||||
}
|
||||
|
||||
//don't know crit formula
|
||||
//don't know exact crit bonus formula
|
||||
public static void CalculateCritDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
short dlvl = (short)(defender.GetLevel() - attacker.GetLevel());
|
||||
|
@ -124,14 +151,15 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
// - Crit resilience
|
||||
//bonus -= attacker.GetMod((uint)Modifier.CriticalResilience) * potencyModifier;
|
||||
|
||||
//need to add something for bonus potency as a part of skill (ie thundara)
|
||||
//need to add something for bonus potency as a part of skill (ie thundara, which breaks the cap)
|
||||
action.amount = (ushort)(action.amount * bonus.Clamp(1.15, 1.75));//min bonus of 115, max bonus of 175
|
||||
}
|
||||
|
||||
public static void CalculateParryDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
double percentParry = .75;
|
||||
double percentParry = 0.75;
|
||||
|
||||
action.amountMitigated = (ushort)(action.amount * (1 - percentParry));
|
||||
action.amount = (ushort)(action.amount * percentParry);
|
||||
}
|
||||
|
||||
|
@ -140,80 +168,31 @@ 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, BattleAction action)
|
||||
{
|
||||
double percentResist = .5;
|
||||
double percentResist = 0.5;
|
||||
|
||||
action.amountMitigated = (ushort)(action.amount * (1 - percentResist));
|
||||
action.amount = (ushort)(action.amount * percentResist);
|
||||
}
|
||||
|
||||
//Used for attacks and abilities like Jump that deal damage
|
||||
public static ushort CalculateAttackDamage(Character attacker, Character defender, BattleAction action)
|
||||
//It's weird that stoneskin is handled in C# and all other buffs are in scripts right now
|
||||
//But it's because stoneskin acts like both a preaction and postaction buff in that it falls off after damage is dealt but impacts how much damage is dealt
|
||||
public static void HandleStoneskin(Character defender, BattleAction action)
|
||||
{
|
||||
ushort damage = (ushort)100;
|
||||
var mitigation = Math.Min(action.amount, defender.GetMod(Modifier.Stoneskin));
|
||||
|
||||
if (attacker is Player p)
|
||||
{
|
||||
var weapon = p.GetEquipment().GetItemAtSlot(Equipment.SLOT_MAINHAND);
|
||||
if (weapon != null)
|
||||
{
|
||||
var weaponData = Server.GetItemGamedata(weapon.itemId);
|
||||
|
||||
//just some numbers from https://www.bluegartr.com/threads/107403-Stats-and-how-they-work/page24
|
||||
damage += (ushort) (2.225 * (weaponData as WeaponItem).damagePower + (attacker.GetMod((uint) Modifier.Attack) * .38));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// todo: handle all other crap before protect/stoneskin
|
||||
|
||||
// todo: handle crit etc
|
||||
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Protect) || defender.statusEffects.HasStatusEffect(StatusEffectId.Protect2))
|
||||
{
|
||||
if (action != null)
|
||||
action.effectId |= (uint)HitEffect.Protect;
|
||||
}
|
||||
|
||||
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin))
|
||||
{
|
||||
if (action != null)
|
||||
action.effectId |= (uint)HitEffect.Stoneskin;
|
||||
}
|
||||
return damage;
|
||||
action.amount = (ushort) (action.amount - mitigation).Clamp(0, 9999);
|
||||
defender.SubtractMod((uint)Modifier.Stoneskin, mitigation);
|
||||
}
|
||||
|
||||
public static ushort GetCriticalHitDamage(Character attacker, Character defender, BattleAction action)
|
||||
{
|
||||
ushort damage = action.amount;
|
||||
|
||||
// todo:
|
||||
//
|
||||
// action.effectId |= (uint)HitEffect.Critical;
|
||||
//
|
||||
return damage;
|
||||
}
|
||||
|
||||
public static ushort CalculateSpellDamage(Character attacker, Character defender, BattleAction action)
|
||||
{
|
||||
ushort damage = 0;
|
||||
|
||||
// todo: handle all other crap before shell/stoneskin
|
||||
|
||||
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Shell))
|
||||
{
|
||||
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Shell))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin))
|
||||
{
|
||||
}
|
||||
return damage;
|
||||
}
|
||||
|
||||
public static void DamageTarget(Character attacker, Character defender, BattleAction action, DamageTakenType type, bool sendBattleAction = false)
|
||||
public static void DamageTarget(Character attacker, Character defender, BattleAction action, BattleActionContainer actionContainer= null)
|
||||
{
|
||||
if (defender != null)
|
||||
{
|
||||
|
||||
defender.DelHP((short)action.amount);
|
||||
attacker.OnDamageDealt(defender, action, actionContainer);
|
||||
defender.OnDamageTaken(attacker, action, actionContainer);
|
||||
|
||||
// todo: other stuff too
|
||||
if (defender is BattleNpc)
|
||||
{
|
||||
|
@ -225,66 +204,75 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
bnpc.hateContainer.UpdateHate(attacker, action.enmity);
|
||||
bnpc.lastAttacker = attacker;
|
||||
}
|
||||
defender.DelHP((short) action.amount);
|
||||
defender.OnDamageTaken(attacker, action, type);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DoAction(Character user, Character receiver, BattleAction action, DamageTakenType type = DamageTakenType.None)
|
||||
public static void HealTarget(Character caster, Character target, BattleAction action, BattleActionContainer actionContainer = null)
|
||||
{
|
||||
switch(action.battleActionType)
|
||||
if (target != null)
|
||||
{
|
||||
//split attack into phys/mag?
|
||||
case (BattleActionType.AttackMagic)://not sure if these use different damage taken formulas
|
||||
case (BattleActionType.AttackPhysical):
|
||||
DamageTarget(user, receiver, action, type, false);
|
||||
break;
|
||||
case (BattleActionType.Heal):
|
||||
receiver.AddHP(action.amount);
|
||||
break;
|
||||
}
|
||||
target.AddHP(action.amount);
|
||||
|
||||
|
||||
if ((type == DamageTakenType.Ability || type == DamageTakenType.Attack) && action.amount != 0)
|
||||
{
|
||||
receiver.AddTP(150);
|
||||
user.AddTP(200);
|
||||
target.statusEffects.CallLuaFunctionByFlag((uint) StatusEffectFlags.ActivateOnHealed, "onHealed", caster, target, action, actionContainer);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Rate functions
|
||||
*/
|
||||
|
||||
#region Rate Functions
|
||||
|
||||
//How is accuracy actually calculated?
|
||||
public static double GetHitRate(Character attacker, Character defender, BattleCommand skill)
|
||||
public static double GetHitRate(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
double hitRate = .80;
|
||||
//Certain skills have lower or higher accuracy rates depending on position/combo
|
||||
return hitRate * (skill != null ? skill.accuracyModifier : 1);
|
||||
double hitRate = 80.0;
|
||||
|
||||
//Add raw hit rate buffs, subtract raw evade buffs, take into account skill's accuracy modifier.
|
||||
double hitBuff = attacker.GetMod(Modifier.RawHitRate);
|
||||
double evadeBuff = defender.GetMod(Modifier.RawEvadeRate);
|
||||
float modifier = skill != null ? skill.accuracyModifier : 0;
|
||||
hitRate += (hitBuff + modifier).Clamp(0, 100.0);
|
||||
hitRate -= evadeBuff;
|
||||
return hitRate.Clamp(0, 100.0);
|
||||
}
|
||||
|
||||
//Whats the parry formula?
|
||||
public static double GetParryRate(Character attacker, Character defender, BattleCommand skill)
|
||||
public static double GetParryRate(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
//Can't parry with shield, must be facing attacker
|
||||
if (defender.GetMod((uint)Modifier.HasShield) > 0 || !defender.IsFacing(attacker))
|
||||
//Can't parry with shield, can't parry rear attacks
|
||||
if (defender.GetMod((uint)Modifier.HasShield) != 0 || action.param == (byte) HitDirection.Rear)
|
||||
return 0;
|
||||
|
||||
return .10;
|
||||
double parryRate = 10.0;
|
||||
|
||||
parryRate += defender.GetMod(Modifier.Parry) * 0.1;//.1% rate for every point of Parry
|
||||
|
||||
return parryRate + (defender.GetMod(Modifier.RawParryRate));
|
||||
}
|
||||
|
||||
public static double GetCritRate(Character attacker, Character defender, BattleCommand skill)
|
||||
public static double GetCritRate(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
double critRate = 10;// .0016 * attacker.GetMod((uint)Modifier.CritRating);//Crit rating adds .16% per point
|
||||
return Math.Min(critRate, .20);//Crit rate is capped at 20%
|
||||
if (action.actionType == ActionType.Status)
|
||||
return 0.0;
|
||||
|
||||
//using 10.0 for now since gear isn't working
|
||||
double critRate = 10.0;// 0.16 * attacker.GetMod((uint)Modifier.CritRating);//Crit rating adds .16% per point
|
||||
|
||||
//Add additional crit rate from skill
|
||||
//Should this be a raw percent or a flat crit raitng? the wording on skills/buffs isn't clear.
|
||||
critRate += 0.16 * (skill != null ? skill.bonusCritRate : 0);
|
||||
|
||||
return critRate + attacker.GetMod(Modifier.RawCritRate);
|
||||
}
|
||||
|
||||
//http://kanican.livejournal.com/55370.html
|
||||
// todo: figure that out
|
||||
public static double GetResistRate(Character attacker, Character defender, BattleCommand skill)
|
||||
public static double GetResistRate(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
// todo: add elemental stuff
|
||||
//Can only resist spells?
|
||||
if (action.commandType != CommandType.Spell && action.actionProperty <= ActionProperty.Projectile)
|
||||
return 0.0;
|
||||
|
||||
return .95;
|
||||
return 15.0 + defender.GetMod(Modifier.RawResistRate);
|
||||
}
|
||||
|
||||
//Block Rate follows 4 simple rules:
|
||||
|
@ -292,30 +280,34 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
//(2) Every point in "Block Rate" gives +0.2% rate
|
||||
//(3) True block proc rate is capped at 75%. No clue on a possible floor.
|
||||
//(4) The baseline rate is based on dLVL only(mob stats play no role). The baseline rate is summarized in this raw data sheet: https://imgbox.com/aasLyaJz
|
||||
public static double GetBlockRate(Character attacker, Character defender, BattleCommand skill)
|
||||
public static double GetBlockRate(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
//Shields are required to block.
|
||||
if (defender.GetMod((uint)Modifier.HasShield) == 0)//|| !defender.IsFacing(attacker))
|
||||
//Shields are required to block and can't block from rear.
|
||||
if (defender.GetMod((uint)Modifier.HasShield) == 0 || action.param == (byte)HitDirection.Rear)
|
||||
return 0;
|
||||
|
||||
short dlvl = (short) (attacker.GetLevel() - defender.GetLevel());
|
||||
double blockRate = (-2.5 * dlvl) - 5; // Base block rate
|
||||
blockRate += attacker.GetMod((uint) Modifier.Dexterity) * .1;// .1% for every dex
|
||||
blockRate += attacker.GetMod((uint) Modifier.BlockRate) * .2;// .2% for every block rate
|
||||
return Math.Min(blockRate, 25);
|
||||
short dlvl = (short)(defender.GetLevel() - attacker.GetLevel());
|
||||
double blockRate = (2.5 * dlvl) + 5; // Base block rate
|
||||
|
||||
//Is this one of those thing where DEX gives block rate and this would be taking DEX into account twice?
|
||||
blockRate += defender.GetMod((uint)Modifier.Dexterity) * 0.1;// .1% for every dex
|
||||
blockRate += defender.GetMod((uint)Modifier.BlockRate) * 0.2;// .2% for every block rate
|
||||
|
||||
return Math.Min(blockRate, 25.0) + defender.GetMod((uint)Modifier.RawBlockRate);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* HitType helpers. Used for determining if attacks are hits, crits, blocks, etc. and changing their damage based on that
|
||||
*/
|
||||
#endregion
|
||||
|
||||
public static bool TryCrit(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
if (Program.Random.NextDouble() < GetCritRate(attacker, defender, skill))
|
||||
if ((Program.Random.NextDouble() * 100) <= action.critRate)
|
||||
{
|
||||
action.hitType = HitType.Crit;
|
||||
CalculateCritDamage(attacker, defender, skill, action);
|
||||
|
||||
if(skill != null)
|
||||
skill.actionCrit = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -324,7 +316,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
|
||||
public static bool TryResist(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
if (Program.Random.NextDouble() < GetResistRate(attacker, defender, skill))
|
||||
if ((Program.Random.NextDouble() * 100) <= action.resistRate)
|
||||
{
|
||||
action.hitType = HitType.Resist;
|
||||
CalculateResistDamage(attacker, defender, skill, action);
|
||||
|
@ -336,10 +328,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
|
||||
public static bool TryBlock(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
if (Program.Random.NextDouble() < GetBlockRate(attacker, defender, skill))
|
||||
if ((Program.Random.NextDouble() * 100) <= action.blockRate)
|
||||
{
|
||||
action.hitType = HitType.Block;
|
||||
defender.SetProc((int)HitType.Block);
|
||||
CalculateBlockDamage(attacker, defender, skill, action);
|
||||
return true;
|
||||
}
|
||||
|
@ -349,10 +340,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
|
||||
public static bool TryParry(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
if (Program.Random.NextDouble() < GetParryRate(attacker, defender, skill))
|
||||
if ((Program.Random.NextDouble() * 100) <= action.parryRate)
|
||||
{
|
||||
action.hitType = HitType.Parry;
|
||||
defender.SetProc((int)HitType.Parry);
|
||||
CalculateParryDamage(attacker, defender, skill, action);
|
||||
return true;
|
||||
}
|
||||
|
@ -363,12 +353,12 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
//TryMiss instead of tryHit because hits are the default and don't change damage
|
||||
public static bool TryMiss(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
if (Program.Random.NextDouble() > GetHitRate(attacker, defender, skill))
|
||||
if ((Program.Random.NextDouble() * 100) >= GetHitRate(attacker, defender, skill, action))
|
||||
{
|
||||
action.hitType = HitType.Miss;
|
||||
action.hitType = (ushort)HitType.Miss;
|
||||
//On misses, the entire amount is considered mitigated
|
||||
action.amountMitigated = action.amount;
|
||||
action.amount = 0;
|
||||
defender.SetProc((int)HitType.Evade);
|
||||
attacker.SetProc((int)HitType.Miss);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -377,82 +367,167 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
/*
|
||||
* Hit Effecthelpers. Different types of hit effects hits use some flags for different things, so they're split into physical, magical, heal, and status
|
||||
*/
|
||||
public static void CalcHitType(Character caster, Character target, BattleCommand skill, BattleAction action)
|
||||
public static void DoAction(Character caster, Character target, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer = null)
|
||||
{
|
||||
//Might be a simpler way to do this?
|
||||
switch(action.battleActionType)
|
||||
switch (action.actionType)
|
||||
{
|
||||
case (BattleActionType.AttackPhysical):
|
||||
SetHitEffectPhysical(caster, target, skill, action);
|
||||
case (ActionType.Physical):
|
||||
FinishActionPhysical(caster, target, skill, action, actionContainer);
|
||||
break;
|
||||
case (BattleActionType.AttackMagic):
|
||||
SetHitEffectMagical(caster, target, skill, action);
|
||||
case (ActionType.Magic):
|
||||
FinishActionSpell(caster, target, skill, action, actionContainer);
|
||||
break;
|
||||
case (BattleActionType.Heal):
|
||||
SetHitEffectHeal(caster, target, skill, action);
|
||||
case (ActionType.Heal):
|
||||
FinishActionHeal(caster, target, skill, action, actionContainer);
|
||||
break;
|
||||
case (BattleActionType.Status):
|
||||
SetHitEffectStatus(caster, target, skill, action);
|
||||
case (ActionType.Status):
|
||||
FinishActionStatus(caster, target, skill, action, actionContainer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetHitEffectPhysical(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
//Determine the hit type, set the hit effect, modify damage based on stoneskin and hit type, hit target
|
||||
public static void FinishActionPhysical(Character attacker, Character defender, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer = null)
|
||||
{
|
||||
//Determine the hittype of the action and change amount of damage it does based on that
|
||||
//Figure out the hit type and change damage depending on hit type
|
||||
if (!TryMiss(attacker, defender, skill, action))
|
||||
if (!TryCrit(attacker, defender, skill, action))
|
||||
if (!TryBlock(attacker, defender, skill, action))
|
||||
TryParry(attacker, defender, skill, action);
|
||||
{
|
||||
//Handle Stoneskin here because it seems like stoneskin mitigates damage done before taking into consideration crit/block/parry damage reductions.
|
||||
//This is based on the fact that a 0 damage attack due to stoneskin will heal for 0 with Aegis Boon, meaning Aegis Boon didn't mitigate any damage
|
||||
HandleStoneskin(defender, action);
|
||||
|
||||
//Crits can't be blocked (is this true for Aegis Boon and Divine Veil?) or parried so they are checked first.
|
||||
if (!TryCrit(attacker, defender, skill, action))
|
||||
//Block and parry order don't really matter because if you can block you can't parry and vice versa
|
||||
if (!TryBlock(attacker, defender, skill, action))
|
||||
if(!TryParry(attacker, defender, skill, action))
|
||||
//Finally if it's none of these, the attack was a hit
|
||||
action.hitType = HitType.Hit;
|
||||
}
|
||||
|
||||
//Actions have different text ids depending on whether they're a part of a multi-hit ws or not.
|
||||
Dictionary<HitType, ushort> textIds = SingleHitTypeTextIds;
|
||||
|
||||
//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.
|
||||
if (skill != null && skill.numHits > 1)
|
||||
{
|
||||
if (action.hitNum == 1)
|
||||
actionContainer?.AddAction(new BattleAction(attacker.actorId, 30441, 0));
|
||||
|
||||
textIds = MultiHitTypeTextIds;
|
||||
}
|
||||
|
||||
//Set the correct textId
|
||||
action.worldMasterTextId = textIds[action.hitType];
|
||||
|
||||
//Set the hit effect
|
||||
SetHitEffectPhysical(attacker, defender, skill, action, actionContainer);
|
||||
|
||||
//Modify damage based on defender's stats
|
||||
CalculatePhysicalDamageTaken(attacker, defender, skill, action);
|
||||
|
||||
actionContainer.AddAction(action);
|
||||
action.enmity = (ushort) (action.enmity * (skill != null ? skill.enmityModifier : 1));
|
||||
//Damage the target
|
||||
DamageTarget(attacker, defender, action, actionContainer);
|
||||
}
|
||||
|
||||
public static void FinishActionSpell(Character attacker, Character defender, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer = null)
|
||||
{
|
||||
//Determine the hit type of the action
|
||||
if (!TryMiss(attacker, defender, skill, action))
|
||||
{
|
||||
HandleStoneskin(defender, action);
|
||||
if (!TryCrit(attacker, defender, skill, action))
|
||||
if (!TryResist(attacker, defender, skill, action))
|
||||
action.hitType = HitType.Hit;
|
||||
}
|
||||
|
||||
//There are no multi-hit spells
|
||||
action.worldMasterTextId = SingleHitTypeTextIds[action.hitType];
|
||||
|
||||
//Set the hit effect
|
||||
SetHitEffectSpell(attacker, defender, skill, action);
|
||||
|
||||
HandleStoneskin(defender, action);
|
||||
|
||||
CalculateSpellDamageTaken(attacker, defender, skill, action);
|
||||
|
||||
actionContainer.AddAction(action);
|
||||
|
||||
DamageTarget(attacker, defender, action, actionContainer);
|
||||
}
|
||||
|
||||
public static void FinishActionHeal(Character attacker, Character defender, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer = null)
|
||||
{
|
||||
//Set the hit effect
|
||||
SetHitEffectHeal(attacker, defender, skill, action);
|
||||
|
||||
actionContainer.AddAction(action);
|
||||
|
||||
HealTarget(attacker, defender, action, actionContainer);
|
||||
}
|
||||
|
||||
public static void FinishActionStatus(Character attacker, Character defender, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer = null)
|
||||
{
|
||||
//Set the hit effect
|
||||
SetHitEffectStatus(attacker, defender, skill, action);
|
||||
|
||||
TryStatus(attacker, defender, skill, action, actionContainer, false);
|
||||
|
||||
actionContainer.AddAction(action);
|
||||
}
|
||||
|
||||
public static void SetHitEffectPhysical(Character attacker, Character defender, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer)
|
||||
{
|
||||
var hitEffect = HitEffect.HitEffectType;
|
||||
HitType hitType = action.hitType;
|
||||
|
||||
//Don't know what recoil is actually based on, just guessing
|
||||
//Crit is 2 and 3 together
|
||||
if (action.hitType == HitType.Crit)
|
||||
if (hitType == HitType.Crit)
|
||||
hitEffect |= HitEffect.CriticalHit;
|
||||
else
|
||||
{
|
||||
//It's not clear what recoil level is based on for physical attacks
|
||||
double percentDealt = (100.0 * (action.amount / defender.GetMaxHP()));
|
||||
if (percentDealt > 5.0)
|
||||
hitEffect |= HitEffect.RecoilLv2;
|
||||
else if(percentDealt > 10)
|
||||
else if (percentDealt > 10)
|
||||
hitEffect |= HitEffect.RecoilLv3;
|
||||
}
|
||||
action.worldMasterTextId = HitTypeTextIds[action.hitType];
|
||||
hitEffect |= HitTypeEffects[action.hitType];
|
||||
|
||||
if (skill != null && skill.isCombo && action.hitType > HitType.Evade)
|
||||
hitEffect |= HitTypeEffects[hitType];
|
||||
|
||||
//For combos that land, add the combo effect
|
||||
if (skill != null && skill.isCombo && hitType > HitType.Evade && hitType != HitType.Evade)
|
||||
hitEffect |= (HitEffect)(skill.comboStep << 15);
|
||||
|
||||
//if attack hit the target, take into account protective status effects
|
||||
if (action.hitType >= HitType.Parry)
|
||||
if (hitType >= HitType.Parry)
|
||||
{
|
||||
//Protect / Shell only show on physical/ magical attacks respectively.
|
||||
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Protect))
|
||||
if (action != null)
|
||||
hitEffect |= HitEffect.Protect;
|
||||
|
||||
|
||||
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin))
|
||||
if (action != null)
|
||||
hitEffect |= HitEffect.Stoneskin;
|
||||
}
|
||||
action.effectId = (uint) hitEffect;
|
||||
|
||||
action.effectId = (uint)hitEffect;
|
||||
}
|
||||
|
||||
public static void SetHitEffectMagical(Character attacker, Character defender, BattleCommand skill, BattleAction action)
|
||||
public static void SetHitEffectSpell(Character attacker, Character defender, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer = null)
|
||||
{
|
||||
//Determine the hit type of the action
|
||||
if (!TryMiss(attacker, defender, skill, action))
|
||||
if (!TryCrit(attacker, defender, skill, action))
|
||||
TryResist(attacker, defender, skill, action);
|
||||
|
||||
var hitEffect = HitEffect.MagicEffectType;
|
||||
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 (action.hitType == HitType.Resist)
|
||||
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)
|
||||
|
@ -460,26 +535,22 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
else
|
||||
hitEffect |= HitEffect.RecoilLv2;
|
||||
}
|
||||
else if (action.hitType == HitType.Crit)
|
||||
hitEffect |= HitEffect.Crit;
|
||||
else
|
||||
hitEffect |= HitEffect.RecoilLv3;
|
||||
|
||||
action.worldMasterTextId = HitTypeTextIds[action.hitType];
|
||||
hitEffect |= HitTypeEffects[action.hitType];
|
||||
hitEffect |= HitTypeEffects[hitType];
|
||||
|
||||
if (skill != null && skill.isCombo)
|
||||
hitEffect |= (HitEffect)(skill.comboStep << 15);
|
||||
|
||||
//if attack hit the target, take into account protective status effects
|
||||
if (action.hitType >= HitType.Block)
|
||||
if (hitType >= HitType.Block)
|
||||
{
|
||||
//Protect / Shell only show on physical/ magical attacks respectively.
|
||||
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Shell))
|
||||
if (action != null)
|
||||
hitEffect |= HitEffect.Shell;
|
||||
|
||||
|
||||
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin))
|
||||
if (action != null)
|
||||
hitEffect |= HitEffect.Stoneskin;
|
||||
|
@ -501,14 +572,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
|
||||
public static void SetHitEffectStatus(Character caster, Character receiver, BattleCommand skill, BattleAction action)
|
||||
{
|
||||
var hitEffect = (uint) HitEffect.StatusEffectType | skill.statusId;
|
||||
var hitEffect = (uint)HitEffect.StatusEffectType | skill.statusId;
|
||||
action.effectId = hitEffect;
|
||||
}
|
||||
|
||||
public static int CalculateSpellDamage(Character attacker, Character defender, BattleCommand spell)
|
||||
{
|
||||
// todo: spell formulas and stuff (stoneskin, mods, stats, etc)
|
||||
return 69;
|
||||
action.hitType = HitType.Hit;
|
||||
}
|
||||
|
||||
public static uint CalculateSpellCost(Character caster, Character target, BattleCommand spell)
|
||||
|
@ -518,7 +585,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
// todo: calculate cost for mob/player
|
||||
if (caster is BattleNpc)
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -527,12 +594,56 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
return scaledCost;
|
||||
}
|
||||
|
||||
|
||||
//IsAdditional is needed because additional actions may be required for some actions' effects
|
||||
//For instance, Goring Blade's bleed effect requires another action so the first action can still show damage numbers
|
||||
//Sentinel doesn't require an additional action because it doesn't need to show those numbers
|
||||
//this is stupid
|
||||
public static void TryStatus(Character caster, Character target, BattleCommand skill, BattleAction action, BattleActionContainer battleActions, bool isAdditional = true)
|
||||
{
|
||||
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)
|
||||
{
|
||||
StatusEffect effect = Server.GetWorldManager().GetStatusEffect(skill.statusId);
|
||||
//Because combos might change duration or tier
|
||||
if (effect != null)
|
||||
{
|
||||
effect.SetDuration(skill.statusDuration);
|
||||
effect.SetTier(skill.statusTier);
|
||||
effect.SetMagnitude(skill.statusMagnitude);
|
||||
effect.SetOwner(target);
|
||||
if (target.statusEffects.AddStatusEffect(effect, caster))
|
||||
{
|
||||
//If we need an extra action to show the status text
|
||||
if (isAdditional)
|
||||
battleActions.AddAction(target.actorId, 30328, skill.statusId | (uint) HitEffect.StatusEffectType);
|
||||
}
|
||||
else
|
||||
action.worldMasterTextId = 32002;//Is this right?
|
||||
}
|
||||
else
|
||||
{
|
||||
//until all effects are scripted and added to db just doing this
|
||||
if (target.statusEffects.AddStatusEffect(skill.statusId, skill.statusTier, skill.statusMagnitude, skill.statusDuration, 3000))
|
||||
{
|
||||
//If we need an extra action to show the status text
|
||||
if (isAdditional)
|
||||
battleActions.AddAction(target.actorId, 30328, skill.statusId | (uint) HitEffect.StatusEffectType);
|
||||
}
|
||||
else
|
||||
action.worldMasterTextId = 32002;//Is this right?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Convert a HitDirection to a BattleCommandPositionBonus. Basically just combining left/right into flank
|
||||
public static BattleCommandPositionBonus ConvertHitDirToPosition(HitDirection hitDir)
|
||||
{
|
||||
BattleCommandPositionBonus position = BattleCommandPositionBonus.None;
|
||||
|
||||
switch(hitDir)
|
||||
switch (hitDir)
|
||||
{
|
||||
case (HitDirection.Front):
|
||||
position = BattleCommandPositionBonus.Front;
|
||||
|
@ -548,44 +659,168 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
|
|||
return position;
|
||||
}
|
||||
|
||||
//IsAdditional is needed because additional actions may be required for some actions' effects
|
||||
//For instance, Goring Blade's bleed effect requires another action so the first action can still show damage numbers
|
||||
//Sentinel doesn't require an additional action because it doesn't need to show those numbers
|
||||
public static void TryStatus(Character caster, Character target, BattleCommand skill, BattleAction action, bool isAdditional = true)
|
||||
|
||||
#region experience helpers
|
||||
//See 1.19 patch notes for exp info.
|
||||
public static ushort GetBaseEXP(Player player, BattleNpc mob)
|
||||
{
|
||||
double rand = Program.Random.NextDouble();
|
||||
(caster as Player).SendMessage(0x20, "", rand.ToString());
|
||||
if (skill != null && action.amount < target.GetHP() && skill.statusId != 0 && action.hitType > HitType.Evade && rand < skill.statusChance)
|
||||
//The way EXP seems to work for most enemies is that it gets the lower character's level, gets the base exp for that level, then uses dlvl to modify that exp
|
||||
//Less than -19 dlvl gives 0 exp and no message is sent.
|
||||
//This equation doesn't seem to work for certain bosses or NMs.
|
||||
//Some enemies might give less EXP? Unsure on this. It seems like there might have been a change in base exp amounts after 1.19
|
||||
|
||||
//Example:
|
||||
//Level 50 in a party kills a level 45 enemy
|
||||
//Base exp is 400, as that's the base EXP for level 45
|
||||
//That's multiplied by the dlvl modifier for -5, which is 0.5625, which gives 225
|
||||
//That's then multiplied by the party modifier, which seems to be 0.667 regardless of party size, which gives 150
|
||||
//150 is then modified by bonus experience from food, rested exp, links, and chains
|
||||
|
||||
int dlvl = mob.GetLevel() - player.GetLevel();
|
||||
if (dlvl <= -20)
|
||||
return 0;
|
||||
|
||||
int baseLevel = Math.Min(player.GetLevel(), mob.GetLevel());
|
||||
ushort baseEXP = BASEEXP[baseLevel];
|
||||
|
||||
double dlvlModifier = 1.0;
|
||||
|
||||
//There's 2 functions depending on if the dlvl is positive or negative.
|
||||
if (dlvl >= 0)
|
||||
//I'm not sure if this caps out at some point. This is correct up to at least +9 dlvl though.
|
||||
dlvlModifier += 0.2 * dlvl;
|
||||
else
|
||||
//0.1x + 0.0025x^2
|
||||
dlvlModifier += 0.1 * dlvl + 0.0025 * (dlvl * dlvl);
|
||||
|
||||
//The party modifier isn't clear yet. It seems like it might just be 0.667 for any number of members in a group, but the 1.19 notes say it's variable
|
||||
//There also seem to be some cases where it simply doesn't apply but it isn't obvious if that's correct or when it applies if it is correct
|
||||
double partyModifier = player.currentParty.GetMemberCount() == 1 ? 1.0 : 0.667;
|
||||
|
||||
baseEXP = (ushort) (baseEXP * dlvlModifier * partyModifier);
|
||||
|
||||
return baseEXP;
|
||||
}
|
||||
|
||||
//Gets the EXP bonus when enemies link
|
||||
public static byte GetLinkBonus(ushort linkCount)
|
||||
{
|
||||
byte bonus = 0;
|
||||
|
||||
switch (linkCount)
|
||||
{
|
||||
StatusEffect effect = Server.GetWorldManager().GetStatusEffect(skill.statusId);
|
||||
//Because combos might change duration or tier
|
||||
case (0):
|
||||
break;
|
||||
case (1):
|
||||
bonus = 25;
|
||||
break;
|
||||
case (2):
|
||||
bonus = 50;
|
||||
break;
|
||||
case (3):
|
||||
bonus = 75;
|
||||
break;
|
||||
case (4):
|
||||
default:
|
||||
bonus = 100;
|
||||
break;
|
||||
}
|
||||
|
||||
return bonus;
|
||||
}
|
||||
|
||||
//Gets EXP chain bonus for Attacker fighting Defender
|
||||
//Official text on EXP Chains: An EXP Chain occurs when players consecutively defeat enemies of equal or higher level than themselves within a specific amount of time.
|
||||
//Assuming this means that there is no bonus for enemies below player's level and EXP chains are specific to the person, not party
|
||||
public static byte GetChainBonus(ushort tier)
|
||||
{
|
||||
byte bonus = 0;
|
||||
|
||||
switch (tier)
|
||||
{
|
||||
case (0):
|
||||
break;
|
||||
case (1):
|
||||
bonus = 20;
|
||||
break;
|
||||
case (2):
|
||||
bonus = 25;
|
||||
break;
|
||||
case (3):
|
||||
bonus = 30;
|
||||
break;
|
||||
case (4):
|
||||
bonus = 40;
|
||||
break;
|
||||
default:
|
||||
bonus = 50;
|
||||
break;
|
||||
}
|
||||
return bonus;
|
||||
}
|
||||
|
||||
public static byte GetChainTimeLimit(ushort tier)
|
||||
{
|
||||
byte timeLimit = 0;
|
||||
|
||||
switch (tier)
|
||||
{
|
||||
case (0):
|
||||
timeLimit = 100;
|
||||
break;
|
||||
case (1):
|
||||
timeLimit = 80;
|
||||
break;
|
||||
case (2):
|
||||
timeLimit = 60;
|
||||
break;
|
||||
case (3):
|
||||
timeLimit = 20;
|
||||
break;
|
||||
default:
|
||||
timeLimit = 10;
|
||||
break;
|
||||
}
|
||||
|
||||
return timeLimit;
|
||||
}
|
||||
|
||||
//Calculates bonus EXP for Links and Chains
|
||||
public static void AddBattleBonusEXP(Player attacker, BattleNpc defender, BattleActionContainer actionContainer)
|
||||
{
|
||||
ushort baseExp = GetBaseEXP(attacker, defender);
|
||||
|
||||
//Only bother calculating the rest if there's actually exp to be gained.
|
||||
//0 exp sends no message
|
||||
if (baseExp > 0)
|
||||
{
|
||||
int totalBonus = 0;//GetMod(Modifier.bonusEXP)
|
||||
|
||||
var linkCount = defender.GetMobMod(MobModifier.LinkCount);
|
||||
totalBonus += GetLinkBonus((byte)Math.Min(linkCount, 255));
|
||||
|
||||
StatusEffect effect = attacker.statusEffects.GetStatusEffectById((uint)StatusEffectId.EXPChain);
|
||||
ushort expChainNumber = 0;
|
||||
uint timeLimit = 100;
|
||||
if (effect != null)
|
||||
{
|
||||
effect.SetDuration(skill.statusDuration);
|
||||
effect.SetTier(skill.statusTier);
|
||||
effect.SetOwner(target);
|
||||
if (target.statusEffects.AddStatusEffect(effect, caster))
|
||||
{
|
||||
//If we need an extra action to show the status text
|
||||
if (isAdditional)
|
||||
action.AddStatusAction(target.actorId, skill.statusId);
|
||||
}
|
||||
else
|
||||
action.worldMasterTextId = 32002;//Is this right?
|
||||
}
|
||||
else
|
||||
{
|
||||
if (target.statusEffects.AddStatusEffect(skill.statusId, 1, 3000, skill.statusDuration, skill.statusTier))
|
||||
{
|
||||
//If we need an extra action to show the status text
|
||||
if (isAdditional)
|
||||
action.AddStatusAction(target.actorId, skill.statusId);
|
||||
}
|
||||
else
|
||||
action.worldMasterTextId = 32002;//Is this right?
|
||||
expChainNumber = effect.GetTier();
|
||||
timeLimit = (uint)(GetChainTimeLimit(expChainNumber));
|
||||
actionContainer?.AddEXPAction(new BattleAction(attacker.actorId, 33919, 0, expChainNumber, (byte)timeLimit));
|
||||
}
|
||||
|
||||
totalBonus += GetChainBonus(expChainNumber);
|
||||
|
||||
StatusEffect newChain = Server.GetWorldManager().GetStatusEffect((uint)StatusEffectId.EXPChain);
|
||||
|
||||
newChain.SetDuration(timeLimit);
|
||||
newChain.SetTier((byte)(Math.Min(expChainNumber + 1, 255)));
|
||||
attacker.statusEffects.AddStatusEffect(newChain, attacker, true, true);
|
||||
|
||||
actionContainer?.AddEXPActions(attacker.AddExp(baseExp, (byte)attacker.GetClass(), (byte)(totalBonus.Min(255))));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue