Skip to content

External C# scripts

Tirlititi edited this page Feb 17, 2025 · 7 revisions

Table of contents

  1. Introduction
  2. Sections that are involved
  3. Base C# code
    1. Ability effects (menu)
    2. Status effects
    3. Overloadable methods
  4. Examples

Introduction

Memoria can externalise the C# code of specific parts of the game's code. It allows to define custom effects and formulas for abilities, custom effects for status alterations, custom conditions for the automatic appearance of Eiko's Rebirth Flame... Each Mod folder can contain a modded version of the DLL Memoria.Scripts.{ModName}.dll, allowing changes in the code of the non-modded version (the base code).

A drawback of this system is that if the modded DLL and the Memoria version of Assembly-CSharp.dll are incompatible, it will be dysfunctional. In this situation, an error message will be shown in place of damage when using an ability and error informations will be printed in Memoria.log. This happens when the Memoria.Scripts.{ModName}.dll was compiled with a version of the Assembly-CSharp.dll that has a different API. Recompiling Memoria.Scripts.{ModName}.dll normally fixes the issue, provided that the source code of that DLL is available.

In order to compile custom scripts for a Mod folder:

  1. Create a folder {ModFolder}/StreamingAssets/Scripts/Sources/
  2. Place C# files .cs inside containing the code for your custom scripts (their filenames don't matter)
  3. Run the compiler StreamingAssets/Scripts/Compiler/Memoria.Compiler.exe: it should automatically recognise that your Mod folder has custom scripts and allow to compile them (navigate with the arrow keys and press C, or press A to re-compile the base scripts and all the custom scripts in Mod folders)
  4. Hopefully, it will print a success message: your custom scripts are compiled in the DLL {ModFolder}/StreamingAssets/Scripts/Memoria.Scripts.{ModFolder}.dll and are ready to be used in-game.

MemoriaCompiler.png

A good practice when using custom C# scripts is to have 1 file per class and to enclose that class in a namespace specific for your modded DLL. For example:

using System;
using Memoria.Data;

namespace Memoria.MyMod
{
  public class ...
  {
    ...
  }
}

Sections that are involved

There can be custom C# scripts for modding:

These other key methods are called "Overloadable methods" and modding them requires to declare a class with an interface type amongst those in IOverloadableMethod.cs.

Base C# code

Ability effects (battle)

The base code for the effects of abilities in battle can be found inside the folder StreamingAssets/Scripts/Sources/Battle (ie. the same place as where you need to put your custom C# scripts, except they are outside of any mod folder). So they are not reproduced here.

Ability effects (menu)

A custom ability effect for the menu must be a class based on FieldAbilityScriptBase and using the attribute [FieldAbilityScript(ID)], where ID is the ability script ID. By default, only a few script IDs have an effect in menus; most abilities are meant to be used in battles only.

The base codes of each script ID are the followings:

[FieldAbilityScript(10)]
public class MenuMagicRecovery : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (context.CanBeHealed(true, false))
    {
      context.SetupSpellHeal();
      context.ApplyConcentrate();
      context.ApplyMultiTarget();
      context.HealHp();
    }
  }
}
[FieldAbilityScript(12)]
public class MenuMagicCureStatus : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    context.CureActionStatuses();
  }
}
[FieldAbilityScript(13)]
public class MenuRevive : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (context.CanBeRevived())
      context.ReviveSpell();
  }
}
[FieldAbilityScript(15)]
public class MenuDrainMP : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (context.CanBeHealed(context.Caster, false, true) && context.CanBeMPDamaged(context.Target))
    {
      context.SetupSpellHeal();
      context.ApplyConcentrate();
      context.DrainMp();
    }
  }
}
[FieldAbilityScript(16)]
public class MenuDrainHP : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (context.CanBeHealed(context.Caster, true, false) && context.CanBeDamaged(context.Target))
    {
      context.SetupSpellHeal();
      context.ApplyConcentrate();
      context.DrainHp();
    }
  }
}
[FieldAbilityScript(30)]
public class MenuWhiteWind : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (context.CanBeHealed(true, false))
    {
      if (context.Action.Ref.Power == 0) // Value in vanilla
        context.TargetRecoverHp = (Int32)context.Caster.max.hp / 3;
      else
        context.TargetRecoverHp = (Int32)context.Caster.max.hp * context.Action.Ref.Power / 100;
    }
  }
}
[FieldAbilityScript(37)]
public class MenuChakra : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (context.CanBeHealed(true, true))
    {
      context.TargetRecoverHp = (Int32)context.Target.max.hp * context.Action.Ref.Power / 100;
      context.TargetRecoverMp = (Int32)context.Target.max.mp * context.Action.Ref.Power / 100;
    }
  }
}
[FieldAbilityScript(50)]
public class MenuSixDragons : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (context.CanBeHealed(true, true) && context.CanBeDamaged() && context.CanBeMPDamaged())
    {
      Int32 percent = GameRandom.Next16() % 100;
      if (percent < 10)
      {
        context.TargetRecoverHp = (Int32)context.Target.max.hp;
        context.TargetRecoverMp = (Int32)context.Target.max.mp;
      }
      else if (percent < 30)
      {
        context.TargetRecoverHp = (Int32)context.Target.max.hp;
      }
      else if (percent < 50)
      {
        context.TargetRecoverMp = (Int32)context.Target.max.mp;
      }
      else if (percent < 65)
      {
        context.TargetRecoverHp = (Int32)(1 - context.Target.cur.hp);
      }
      else if (percent < 80)
      {
        context.TargetRecoverMp = (Int32)(1 - context.Target.cur.mp);
      }
      else
      {
        context.TargetRecoverHp = (Int32)(1 - context.Target.cur.hp);
        context.TargetRecoverMp = (Int32)(1 - context.Target.cur.mp);
      }
    }
  }
}
[FieldAbilityScript(62)]
public class MenuItemSoft : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    context.CureActionStatuses();
  }
}
[FieldAbilityScript(73)]
public class MenuItemCureStatus : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    context.CureActionStatuses();
  }
}
[FieldAbilityScript(69)]
public class MenuItemPotion : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (context.CanBeHealed(true, false))
    {
      context.SetupItemHeal();
      context.HealHp();
    }
  }
}
[FieldAbilityScript(70)]
public class MenuItemEther : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (context.CanBeHealed(false, true))
    {
      context.SetupItemHeal();
      context.HealMp();
    }
  }
}
[FieldAbilityScript(71)]
public class MenuItemElixir : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (context.CanBeHealed(true, true))
      context.HealFull();
  }
}
[FieldAbilityScript(72)]
public class MenuItemPhoenix : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (context.CanBeRevived())
      context.ReviveLow();
  }
}
[FieldAbilityScript(74)]
public class MenuItemGem : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (context.CanBeHealed(true, false))
    {
      context.SetupJewelHeal();
      context.HealHp();
    }
  }
}
[FieldAbilityScript(76)]
public class MenuItemTent : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (context.CanBeHealed(true, true))
      context.RecoverHalfHpMp();
  }
}
[FieldAbilityScript(89)]
public class MenuHPSwitching : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    Boolean success = false;
    if (context.Caster.cur.hp > context.Target.cur.hp)
      success = context.CanBeHealed(context.Target, true, false) && context.CanBeDamaged(context.Caster);
    else if (context.Caster.cur.hp < context.Target.cur.hp)
      success = context.CanBeHealed(context.Caster, true, false) && context.CanBeDamaged(context.Target);
    if (success)
    {
        context.CasterRecoverHp = (Int32)(context.Target.cur.hp - context.Caster.cur.hp);
        context.TargetRecoverHp = (Int32)(context.Caster.cur.hp - context.Target.cur.hp);
    }
  }
}
[FieldAbilityScript(103)]
public class MenuPositiveStatus : FieldAbilityScriptBase
{
  public override void Apply(FieldCalculator context)
  {
    if (FieldCalculator.AlterStatuses(context.Target, context.Action.Status) < 2)
      context.Flags = BattleCalcFlags.Miss;
  }
}

Status effects

All the status effect codes must be a class derived from StatusScriptBase with possibly one or several interfaces from IStatusScript.cs. They must also declare the attribute [StatusScript(STATUS_ID)] to determine which status it codes.

The base codes of statuses can be found there. Here are a couple of them that are representative of different kinds of statuses:

// Auto-Life, a status that triggers an effect on KO
using System;
using Memoria.Data;
using Object = System.Object;

namespace Memoria.MyMod
{
  [StatusScript(BattleStatusId.AutoLife)]
  public class AutoLifeStatusScript : StatusScriptBase, IDeathChangerStatusScript
  {
    public Int32 HPRestore = 1; // public fields can be accessed by NCalc formulas

    // The "parameters" can be used to apply statuses in a more precise way
    // Here for example, a call like that from another script would apply an Auto-Life that restores half of HP on revival:
    // btl_stat.AlterStatus(target, BattleStatusId.AutoLife, parameters: [target.MaximumHp / 2]);
    public override UInt32 Apply(BattleUnit target, BattleUnit inflicter, params Object[] parameters)
    {
      base.Apply(target, inflicter, parameters);
      HPRestore = Math.Max(HPRestore, parameters.Length > 0 ? Convert.ToInt32(parameters[0]) : 1);
      return btl_stat.ALTER_SUCCESS;
    }

    public override Boolean Remove()
    {
      return true;
    }

    public Boolean OnDeath()
    {
      btl_stat.RemoveStatus(Target, BattleStatusId.AutoLife);
      if (HPRestore > 0)
      {
        Target.CurrentHp = Math.Min((UInt32)HPRestore, Target.MaximumHp);
        btl_stat.RemoveStatus(Target, BattleStatusId.Death);
      }
      BattleVoice.TriggerOnStatusChange(Target, "Used", BattleStatusId.AutoLife); // These are useful for a Voice Acting mod
      return true;
    }
  }
}
// Berserk, a status that triggers auto-attacks
using System;
using Memoria.Data;
using FF9;
using Object = System.Object;

namespace Memoria.MyMod
{
  [StatusScript(BattleStatusId.Berserk)]
  public class BerserkStatusScript : StatusScriptBase, IAutoAttackStatusScript
  {
    public override UInt32 Apply(BattleUnit target, BattleUnit inflicter, params Object[] parameters)
    {
      base.Apply(target, inflicter, parameters);
      if (!target.CanUseTheAttackCommand)
        return btl_stat.ALTER_RESIST; // "ALTER_RESIST" prevents the status to be added and most likely display "Guard"
      return btl_stat.ALTER_SUCCESS;
    }

    public override Boolean Remove()
    {
      btl_stat.StatusCommandCancel(Target);
      return true;
    }

    public Boolean OnATB()
    {
      if (!Target.CanUseTheAttackCommand)
      {
        btl_stat.RemoveStatus(Target, BattleStatusId.Berserk);
        return false;
      }
      if (Target.IsPlayer) // Player characters should use "btl_cmd.SetCommand"...
        btl_cmd.SetCommand(Target.ATBCommand, BattleCommandId.Attack, (Int32)BattleAbilityId.Attack, btl_util.GetRandomBtlID(0), 0u);
      else // ... while enemies should use "btl_cmd.SetEnemyCommand"
        btl_cmd.SetEnemyCommand(Target, BattleCommandId.EnemyAtk, Target.EnemyType.p_atk_no, btl_util.GetRandomBtlID(1));
      if (Configuration.VoiceActing.Enabled)
        Target.AddDelayedModifier(WaitForAutoAttack, TriggerUsageForBattleVoice);
      return true;
    }

    // This delayed modifier waits for the queued auto-attack to start...
    private Boolean WaitForAutoAttack(BattleUnit unit)
    {
      return btl_cmd.CheckCommandQueued(unit.ATBCommand);
    }

    // ... and send the signal to the Voice Acting system
    private void TriggerUsageForBattleVoice(BattleUnit unit)
    {
      if (unit.ATBCommand.ExecutionStep != command_mode_index.CMD_MODE_INSPECTION)
        BattleVoice.TriggerOnStatusChange(unit, "Used", BattleStatusId.Berserk);
    }
  }
}
// Doom, a status using a countdown displayed above the target's head and an Opr effect (an effect that applies on a regular basis)
using System;
using UnityEngine;
using Memoria.Data;
using Object = System.Object;

namespace Memoria.MyMod
{
  [StatusScript(BattleStatusId.Doom)]
  public class DoomStatusScript : StatusScriptBase, IOprStatusScript
  {
    public HUDMessageChild Message = null; // Non-integer fields cannot be accessed by NCalc formula but can be by other C# scripts
    public Int32 InitialCounter;
    public Int32 Counter;

    public override UInt32 Apply(BattleUnit target, BattleUnit inflicter, params Object[] parameters)
    {
      base.Apply(target, inflicter, parameters);
      btl2d.GetIconPosition(target, btl2d.ICON_POS_NUMBER, out Transform attachTransf, out Vector3 iconOff);
      InitialCounter = parameters.Length > 0 ? Convert.ToInt32(parameters[0]) : 10;
      Counter = InitialCounter;
      Message = Singleton<HUDMessage>.Instance.Show(attachTransf, $"{Counter}", HUDMessage.MessageStyle.DEATH_SENTENCE, new Vector3(0f, iconOff.y), 0);
      btl2d.StatusMessages.Add(Message);
      return btl_stat.ALTER_SUCCESS;
    }

    public override Boolean Remove()
    {
      btl2d.StatusMessages.Remove(Message);
      Singleton<HUDMessage>.Instance.ReleaseObject(Message);
      return true;
    }

    // Defining a non-null "SetupOpr" allows to choose a custom formula for the tick frequencies
    // It takes priority over the INI option "[Battle] StatusTickFormula"
    public IOprStatusScript.SetupOprMethod SetupOpr => SetupDoomOpr;
    public Int32 SetupDoomOpr()
    {
      // Use the duration "ContiCnt" of Doom even if it is not registered as BattleStatusConst.ContiCount
      return (Int32)(Target.StatusDurationFactor[BattleStatusId.Doom] * BattleStatusId.Doom.GetStatData().ContiCnt * (60 - Target.Will << 3) / 10);
    }

    public Boolean OnOpr()
    {
      Counter--;
      if (Counter > 0)
      {
        Message.Label = $"{Counter}";
        return false;
      }
      if (btl_stat.AlterStatus(Target, BattleStatusId.Death, Inflicter) == btl_stat.ALTER_SUCCESS)
        BattleVoice.TriggerOnStatusChange(Target, "Used", BattleStatusId.Doom);
      btl2d.Btl2dReq(Target);
      return true;
    }
  }
}
// Mini, a status that changes the unit size and that removes itself when applied twice
using System;
using Memoria.Data;
using FF9;
using Object = System.Object;

namespace Memoria.MyMod
{
  [StatusScript(BattleStatusId.Mini)]
  public class MiniStatusScript : StatusScriptBase
  {
    public override UInt32 Apply(BattleUnit target, BattleUnit inflicter, params Object[] parameters)
    {
      base.Apply(target, inflicter, parameters);
      if ((target.PermanentStatus & BattleStatus.Mini) != 0)
        return btl_stat.ALTER_INVALID; // "ALTER_INVALID" prevents the status to be added and most likely display "Miss"
      if ((target.CurrentStatus & BattleStatus.Mini) != 0)
      {
        btl_stat.RemoveStatus(target, BattleStatusId.Mini);
        return btl_stat.ALTER_SUCCESS_NO_SET; // "ALTER_SUCCESS_NO_SET": the status is not added (in fact it is even removed here) but is not considered to have missed
      }
      target.ModelStatusScale *= 0.5f;
      geo.geoScaleUpdate(target, true);
      return btl_stat.ALTER_SUCCESS;
    }

    public override Boolean Remove()
    {
      Target.ModelStatusScale *= 2f;
      geo.geoScaleUpdate(Target, true);
      return true;
    }
  }
}
// Trouble, a status that triggers an effect (damage spreading) at the "figure point" of attacks
using System;
using Memoria.Data;
using FF9;
using Object = System.Object;

namespace Memoria.MyMod
{
  [StatusScript(BattleStatusId.Trouble)]
  public class TroubleStatusScript : StatusScriptBase, IFigurePointStatusScript
  {
    public override UInt32 Apply(BattleUnit target, BattleUnit inflicter, params Object[] parameters)
    {
      base.Apply(target, inflicter, parameters);
      return btl_stat.ALTER_SUCCESS;
    }

    public override Boolean Remove()
    {
      return true;
    }

    public void OnFigurePoint(ref UInt16 fig_info, ref Int32 fig, ref Int32 m_fig)
    {
      if ((fig_info & Param.FIG_INFO_TROUBLE) == 0) // Only on "physical" single-target attacks
        return;
      if ((fig_info & Param.FIG_INFO_DISP_HP) == 0)
        return;
      if ((fig_info & (Param.FIG_INFO_HP_RECOVER | Param.FIG_INFO_GUARD | Param.FIG_INFO_MISS | Param.FIG_INFO_DEATH)) != 0)
        return;
      Int32 dmg = fig >> 1; // Half of the original hp damage
      foreach (BattleUnit unit in FF9StateSystem.Battle.FF9Battle.EnumerateBattleUnits())
      {
        if (unit.IsPlayer == Target.IsPlayer && unit.Id != Target.Id && unit.IsTargetable)
        {
          btl_para.SetDamage(unit, dmg, 0, requestFigureNow: true);
          BattleVoice.TriggerOnStatusChange(Target, "Used", BattleStatusId.Trouble);
        }
      }
    }
  }
}

Overloadable methods

  • IOverloadPlayerUIScript determines UI elements of player details when seen in various menus:
using System;
using Assets.Sources.Scripts.UI.Common;

namespace Memoria.MyMod
{
  public class OverloadedPlayerUI : IOverloadPlayerUIScript
  {
    public IOverloadPlayerUIScript.Result UpdatePointStatus(PLAYER player)
    {
      IOverloadPlayerUIScript.Result result = new IOverloadPlayerUIScript.Result();
      result.ColorHP = (player.cur.hp == 0) ? FF9TextTool.Red
                     : (player.cur.hp <= player.max.hp / 6) ? FF9TextTool.Yellow : FF9TextTool.White;
      result.ColorMP = (player.cur.mp <= player.max.mp / 6) ? FF9TextTool.Yellow : FF9TextTool.White;
      result.ColorMagicStone = (player.cur.capa == 0) ? FF9TextTool.Yellow : FF9TextTool.White;
      return result;
    }
  }
}
  • IOverloadUnitCheckPointScript runs every ATB tick to update the status of units depending on their state:
using System;
using Assets.Sources.Scripts.UI.Common;
using Memoria.Data;

namespace Memoria.MyMod
{
  public class OverloadedUnitCheckPoint : IOverloadUnitCheckPointScript
  {
    public BattleStatus UpdatePointStatus(BattleUnit unit)
    {
      Boolean isLowHP = unit.IsPlayer && unit.CurrentHp * 6 <= unit.MaximumHp;
      if (isLowHP)
      {
        unit.UIColorHP = FF9TextTool.Yellow;
        if (!btl_stat.CheckStatus(unit, BattleStatus.LowHP))
          btl_stat.AlterStatus(unit, BattleStatusId.LowHP);
      }
      else
      {
        unit.UIColorHP = FF9TextTool.White;
        btl_stat.RemoveStatus(unit, BattleStatusId.LowHP);
      }
      unit.UIColorMP = unit.CurrentMp <= unit.MaximumMp / 6f ? FF9TextTool.Yellow : FF9TextTool.White;
      return isLowHP ? BattleStatus.LowHP : 0;
    }
  }
}
  • IOverloadOnBattleInitScript runs a script at the start of each battle, right after the initialisation of all battle units:
using System;

namespace Memoria.MyMod
{
  public class OverloadedBattleInit : IOverloadOnBattleInitScript
  {
    public void OnBattleInit()
    {
      // This can add all kinds of initialisation code
    }
  }
}
  • IOverloadOnBattleScriptStartScript runs a script before all the scripts of external battle scripts, possibly skipping it:
using System;
using Memoria.Data;

namespace Memoria.MyMod
{
  public class OverloadedBattleScriptStart : IOverloadOnBattleScriptStartScript
  {
    public Boolean OnBattleScriptStart(BattleCalculator calc)
    {
      if ((calc.Command.AbilityCategory & 8) != 0 && calc.Target.TryKillFrozen()) // Physical attacks shatter frozen targets
        return true;
      return false;
    }
  }
}
  • IOverloadOnCommandRunScript runs a script when a command changes from being queued to being performed, possibly preventing it:
using System;
using Memoria.Data;

namespace Memoria.MyMod
{
  public class OverloadedCommandRun : IOverloadOnCommandRunScript
  {
    public Boolean OnCommandRun(BattleCommand cmd)
    {
      // Kill units under Heat that try to act
      if (cmd.Id < BattleCommandId.EnemyReaction || cmd.Id > BattleCommandId.BoundaryUpperCheck)
      {
        BattleUnit caster = cmd.Caster;
        if (caster.IsUnderAnyStatus(BattleStatus.Heat))
        {
          if (caster.IsUnderAnyStatus(BattleStatus.EasyKill))
          {
            caster.CurrentHp = 0;
            BattleVoice.TriggerOnStatusChange(caster, "Used", BattleStatusId.Heat);
            btl_cmd.KillCommand(cmd);
            if (caster.CurrentHp == 0 && !caster.IsPlayer) // Prevent dying animation for enemies
            {
              BattleEnemy enemy = caster.Enemy;
              if (!enemy.AttackOnDeath)
              {
                btl_util.SetEnemyDieSound(caster, enemy.Data.et.die_snd_no);
                caster.Data.die_seq = 3;
              }
            }
            return true;
          }
          else if (btl_stat.AlterStatus(caster, BattleStatusId.Death) == btl_stat.ALTER_SUCCESS)
          {
            BattleVoice.TriggerOnStatusChange(caster, "Used", BattleStatusId.Heat);
            btl_cmd.KillCommand(cmd);
            return true;
          }
        }
      }
      return false;
    }
  }
}
  • IOverloadOnGameOverScript runs a script when a Game Over is about to happen, possibly preventing it:
using System;
using System.Linq;
using Memoria.Data;

namespace Memoria.MyMod
{
  public class OverloadedGameOver : IOverloadOnGameOverScript
  {
    public Boolean OnGameOver(FF9StateBattleSystem state, BattleUnit dyingPC)
    {
      BattleUnit eiko = BattleState.EnumerateUnits().FirstOrDefault(unit => unit.PlayerIndex == CharacterId.Eiko);
      if (eiko == null || eiko.IsUnderAnyStatus(BattleStatusConst.NoRebirthFlame))
        return false;
      if (btl_cmd.CheckSpecificCommand(eiko, BattleCommandId.SysLastPhoenix))
        return true;
      Boolean procRebirthFlame = GameState.ItemCount(RegularItem.PhoenixPinion) > GameRandom.Next8();
      if (procRebirthFlame)
      {
        UIManager.Battle.FF9BMenu_EnableMenu(true);
        btl_cmd.SetCommand(eiko.ATBCommand, BattleCommandId.SysLastPhoenix, (Int32)BattleAbilityId.RebirthFlame, btl_scrp.GetBattleID(0u), 1u);
        return true;
      }
      return false;
    }
  }
}
  • IOverloadOnFleeScript runs a script when fleeing:
using System;
using Memoria.Data;

namespace Memoria.MyMod
{
  public class OverloadedFlee : IOverloadOnFleeScript
  {
    public void OnFlee(FF9StateGlobal state)
    {
      if ((state.btl_flag & battle.BTL_FLAG_ABILITY_FLEE) != 0)
      {
        // Retrieve the total gil of the battle: those of enemies already defeated and those of enemies not yet defeated
        UInt32 gil = (UInt32)battle.btl_bonus.gil;
        foreach (BattleUnit unit in BattleState.EnumerateUnits())
          if (!unit.IsPlayer)
            gil += unit.Enemy.BonusGil;
        UInt32 gilLost = gil / 10u;
        if (state.party.gil > gilLost)
        {
          state.party.gil -= gilLost;
        }
        else
        {
          gilLost = state.party.gil;
          state.party.gil = 0u;
        }
        UIManager.Battle.SetBattleFollowMessage(BattleMesages.DroppedGil, gilLost);
      }
    }
  }
}
  • IOverloadDamageModifierScript runs a script whenever the DamageModifierCount of an attack is modified:
using System;
using Memoria.Data;

namespace Memoria.MyMod
{
  public class OverloadedDamageModifier : IOverloadDamageModifierScript
  {
    // The usual bonuses and penalties, such as elemental boost, weakness and resistance, defences like Shell / Protect, Bird Killer, etc...
    public void OnDamageModifierChange(BattleCalculator v, Int32 previousValue, Int32 bonus)
    {
      if (bonus >= 0)
      {
        for (Int32 i = 0; i < bonus; i++)
          v.Context.Attack = v.Context.Attack * 3 >> 1;
      }
      else
      {
        Int32 malus = -bonus;
        for (Int32 i = 0; i < malus; i++)
          v.Context.Attack >>= 1;
      }
    }

    // The heavy damage reduction (physical attacks under Mini)
    public void OnDamageDrasticReduction(BattleCalculator v)
    {
      v.Context.Attack = 1;
    }

    // The last potential modifications of damage
    public void OnDamageFinalChanges(BattleCalculator v)
    {
      if (v.Target.Flags == 0)
        return;
      Int32 reflectMultiplier = v.Command.GetReflectMultiplierOnTarget(v.Target.Id);
      if ((v.Target.Flags & CalcFlag.HpAlteration) != 0)
        v.Target.HpDamage *= reflectMultiplier;
      if ((v.Target.Flags & CalcFlag.MpAlteration) != 0)
        v.Target.MpDamage *= reflectMultiplier;
    }
  }
}

Examples

Items providing permanent stat bonus

The strategy for this is to use "Memoria Dictionary" entries dedicated to stat bonus points, and apply the bonus themselves in a global supporting ability feature.

using System;
using System.Collections.Generic;
using Memoria;
using Memoria.Data;

namespace Memoria.MyMod
{
  // An effect for items / spells that can be used out of battles:
  // Items with effect "300" increase the Max HP of the target
  // The bonus is specified by the item's Power

  [FieldAbilityScript(300)]
  public class MenuIncreaseHP : FieldAbilityScriptBase
  {
    public const Int32 DICT_ENTRY_ID = 500;

    public override void Apply(FieldCalculator context)
    {
      // Don't allow to use it on KO / Stoned characters
      if (context.Target.cur.hp != 0 && !FieldCalculator.CheckStatus(context.Target, BattleStatusConst.CannotHealInMenu))
      {
        if (!FF9StateSystem.EventState.gScriptDictionary.TryGetValue(DICT_ENTRY_ID, out Dictionary<Int32, Int32> dict))
        {
          dict = new Dictionary<Int32, Int32>();
          FF9StateSystem.EventState.gScriptDictionary[DICT_ENTRY_ID] = dict;
        }
        Int32 characterIndex = (Int32)context.Target.Index;
        if (!dict.TryGetValue(characterIndex, out Int32 currentBonus))
          currentBonus = 0;

        // There, Max HP bonus is stored in "FF9StateSystem.EventState.gScriptDictionary[500][characterIndex]"
        // It can be accessed in NCalc formulas with "GetMemoriaDictionary(500, CharacterIndex)"
        currentBonus += context.Action.Ref.Power;
        dict[characterIndex] = currentBonus;

        // Update the player's characteristics now, so the effect takes effect immediatly
        ff9play.FF9Play_Update(context.Target);
      }
    }
  }
}

And the actual bonus is coded by these lines in AbilityFeatures.txt:

>SA GlobalLast+ Permanent HP Bonus
Permanent
[code=MaxHP] MaxHP + GetMemoriaDictionary(500, CharacterIndex) [/code]

Note that using GlobalLast+ here makes the bonus apply after all the other bonuses, so the bonus given by items are not affected by supporting abilities like HP+20%. You can use Global+ instead if you want that behaviour.

Items that give a bonus in Max HP are the ones with effect 300 (as specified in the C# code) and give as much HP as their Power.

Clone this wiki locally