-
Notifications
You must be signed in to change notification settings - Fork 85
External C# scripts
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:
- Create a folder
{ModFolder}/StreamingAssets/Scripts/Sources/ - Place C# files
.csinside containing the code for your custom scripts (their filenames don't matter) - 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 pressC, or pressAto re-compile the base scripts and all the custom scripts in Mod folders) - Hopefully, it will print a success message: your custom scripts are compiled in the DLL
{ModFolder}/StreamingAssets/Scripts/Memoria.Scripts.{ModFolder}.dlland are ready to be used in-game.

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 ...
{
...
}
}There can be custom C# scripts for modding:
- The effects of abilities in battle
- The effects of abilities used from the menu
- The effects of status alterations
- A couple of other key methods, mostly in battle
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.
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.
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;
}
}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);
}
}
}
}
}-
IOverloadPlayerUIScriptdetermines 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;
}
}
}-
IOverloadUnitCheckPointScriptruns 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;
}
}
}-
IOverloadOnBattleInitScriptruns 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
}
}
}-
IOverloadOnBattleScriptStartScriptruns 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;
}
}
}-
IOverloadOnCommandRunScriptruns 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;
}
}
}-
IOverloadOnGameOverScriptruns 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;
}
}
}-
IOverloadOnFleeScriptruns 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);
}
}
}
}-
IOverloadDamageModifierScriptruns a script whenever theDamageModifierCountof 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;
}
}
}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.