3
\$\begingroup\$

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.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ What language is this, C#? I'm not sure why you picked the reinventing-the-wheel tag here, but there's definitely a language tag missing. \$\endgroup\$
    – Mast
    Commented Jan 9 at 11:30
  • \$\begingroup\$ Yes. This is C#. I’ve added lang tag. Tag was chosen due to fact that there are libraries/project which already do have mentioned functionality. “If you are deliberately reimplementing a common library function as an exercise, you should tag it as (reinventing-the-wheel)” \$\endgroup\$ Commented Jan 9 at 12:36

0

Browse other questions tagged or ask your own question.