I am re-implementing the damage calculations for the game Final Fantasy 9, based on the documentation provided in the GameFaqs Battle Mechanics Guide. Currently, I have two main methods for performing calculations: BattleEngine.Attack
and BattleEngine.Magic
.
BattleEngine.Attack
public AttackResult Attack(Unit attacker, Unit target)
{
var result = new AttackResult
{
Attacker = attacker,
Target = target
};
int rnd = _randomProvider.Next();
// Calculate if the attacker has critical hit
bool isCritical = rnd % Math.Floor(attacker.Spr / 4.0) > rnd % 100;
result.IsCritical = isCritical;
// Check 1: attacker has to hit a target to deal damage
// This is where Confuse, Darkness, Defend, Distract and Vanish get
// involved.
var acc = 100;
acc -= attacker.Statuses.HasFlag(Statuses.Confuse) ? acc / 2 : 0;
acc -= attacker.Statuses.HasFlag(Statuses.Darkness) ? acc / 2 : 0;
acc -= target.Statuses.HasFlag(Statuses.Defend) ? acc / 2 : 0;
acc -= target.Statuses.HasFlag(Statuses.Distract) ? acc / 2 : 0;
acc -= target.Statuses.HasFlag(Statuses.Vanish) ? acc : 0;
// Normalize just in case we get negative value
acc = Math.Max(acc, 0);
result.IsMiss = rnd % 100 >= acc;
if (result.IsMiss)
{
return result;
}
// Check 2: target can evade the attack
result.IsEvaded = rnd % 100 < target.Eva;
if (result.IsEvaded)
{
return result;
}
// The Sword Magic deals no damage if the enemy nullifies that element,
// and heals the enemy if it absorbs that element.
if (target.IsImmuneTo(attacker.Equipment.Weapon.ElementalAffix))
{
return result;
}
(int @base, int bonus) = CalculateAttackDamageParts(attacker, target);
if (target.AbsorbsElemental(attacker.Equipment.Weapon.ElementalAffix))
{
result.TargetAbsorbed = true;
result.Damage = @base * bonus;
return result;
}
bonus = ApplyMultiplier(attacker, target, bonus, isCritical);
bonus = CalculateElementalBonus(bonus, attacker, target);
if (attacker.Statuses.HasFlag(Statuses.Mini))
{
bonus = 1;
}
if (attacker.Equipment.Weapon.CanInflictStatuses)
{
int accuracy = attacker.Equipment.Weapon.StatusAccuracy;
if (accuracy > rnd % 100)
{
result.ApplicableStatuses |=
attacker.Equipment.Weapon.StatusAffix;
}
}
result.Damage = @base * bonus;
// Calculate if the target will counter attack
bool isCounterAttack = target.Statuses.HasFlag(Statuses.Eye4Eye)
? target.Spr * 2 >= rnd % 100
: target.Spr >= rnd % 100;
result.IsCounterAttack = isCounterAttack;
if (attacker.Statuses.HasFlag(Statuses.Trance))
{
result.TranceDecrease =
(int)(Math.Floor((300.0 - attacker.Lvl) / attacker.Spr)
* 10.0 % 256);
}
if (target.IsAi == false)
{
result.TranceIncrease = CalculateTranceIncrease(target);
}
return result;
}
BattleEngine.Magic
/// <summary>
/// Allows to cast any type of Magic onto target. Spell will be casted
/// on a single target.
/// </summary>
/// <param name="attacker">Source of the spell</param>
/// <param name="target">Target of the spell</param>
/// <param name="spellName">Name of the spell to cast.</param>
/// <returns>Result of the casting magic on the target.</returns>
public AttackResult Magic(Unit attacker, Unit target, string spellName)
{
return Magic(attacker, target, spellName, false);
}
private AttackResult Magic(Unit attacker,
Unit target,
string spellName,
bool isMultiTarget)
{
int rnd = _randomProvider.Next();
SpellBase spell = FindSpell(spellName);
var result = new AttackResult
{
Attacker = attacker,
Target = target
};
// Determine if target has Reflect2x status before we do any changes
// to the AttackResult instance.
bool targetHasReflect2X = target.Statuses.HasFlag(Statuses.Reflect2x);
// If target has Reflect status then the spell will be
// reflected back to someone from opposing team.
if (target.Statuses.HasFlag(Statuses.Reflect)
|| targetHasReflect2X)
{
result.IsReflected = true;
// If target reflects spell to a party/enemy group which has only
// one alive member then is is no longer a multi target spell
// and such penalty can be lifted.
int aliveCount;
IEnumerable<Unit> group;
if (attacker.IsAi)
{
group = Enemies.Members;
aliveCount = Enemies.AliveCount;
}
else
{
// This is player attacking own units.
if (attacker.IsAi == false && target.IsAi == false)
{
group = Enemies.Members;
aliveCount = Enemies.AliveCount;
}
else // this is Ai attacking player controllable units.
{
group = Party.Members;
aliveCount = Party.AliveCount;
}
}
Unit reflectTo;
if (aliveCount == 1)
{
reflectTo = group.First();
isMultiTarget = false;
}
else
{
int index = rnd % aliveCount;
reflectTo = group.ToArray()[index];
}
result.RefelectedTo = reflectTo;
target = reflectTo;
}
spell.UpdateDamageParts(ref result, _randomProvider, isMultiTarget);
if (targetHasReflect2X)
{
result.Bonus *= 2;
}
if (result.IsMiss)
{
result.IsMiss = true;
result.Damage = 0;
}
else if (target.IsImmuneTo(spell.ElementalAffix))
{
result.Damage = 0;
}
// When target absorbs elemental damage is should
// be healed by the amount indicated in the damage.
else if (target.AbsorbsElemental(spell.ElementalAffix))
{
result.TargetAbsorbed = true;
result.Damage = result.Base * result.Bonus;
}
else
{
result.Damage = result.Base * result.Bonus;
}
if (attacker.Statuses.HasFlag(Statuses.Trance))
{
result.TranceDecrease =
(int)(Math.Floor((300.0 - attacker.Lvl) / attacker.Spr)
* 10.0 % 256);
}
if (target.IsAi == false)
{
result.TranceIncrease = CalculateTranceIncrease(target);
}
return result;
}
Neither of these methods modifies any other objects. They both return an AttackResult
object that contains information about the side-effects that must be applied by different layers of the game.
AttackResult
public class AttackResult
{
public bool IsCritical { get; set; }
public bool IsMiss { get; set; }
public bool IsEvaded { get; set; }
public int Damage { get; set; }
public bool IsCounterAttack { get; set; }
public Statuses ApplicableStatuses { get; set; }
public bool TargetAbsorbed { get; set; }
public Unit Attacker { get; set; }
public Unit Target { get; set; }
public bool IsReflected { get; set; }
public Unit? RefelectedTo { get; set; }
/// <summary>
/// Indicates the amount of which Trance bar should be increased by.
/// </summary>
public int TranceIncrease { get; set; }
/// <summary>
/// Indicate the amount of which Trance bar should be decreased by.
/// </summary>
public int TranceDecrease { get; set; }
public int Bonus { get; set; }
public int Base { get; set; }
public bool IsMpRestored { get; set; }
public int RestoredMp { get; set; }
public bool IsHpRestored { get; set; }
public int HpRestored { get; set; }
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"Is miss: {IsMiss}");
sb.AppendLine($"Is evaded: {IsEvaded}");
sb.AppendLine($"Damage: {Damage}");
sb.AppendLine($"Is critical: {IsCritical}");
sb.AppendLine($"Will target counter-attack: {IsCounterAttack}");
sb.AppendLine($"Applicable statuses: {ApplicableStatuses}");
sb.AppendLine($"Has target absorbed damage: {TargetAbsorbed}");
return sb.ToString();
}
public readonly List<(Statuses Status, Unit Unit)> InflictStatus = [];
}
The advantage of this approach is that it makes testing easier. I only need to examine the AttackResult
object, rather than checking the properties of Unit
. This approach also minimizes the potential for side-effects, as any changes to the game's state will be made elsewhere based on the AttackResult
. I cannot see any disadvantages to this approach, other than the need to write more code to handle the AttackResult
object.