-
Notifications
You must be signed in to change notification settings - Fork 51
External Battle scripts
Memoria externalises the C# code of battle spell effects.
It allows to define custom effects, damage formulas, hit rate formulas, hitting conditions, etc... within an external DLL, the StreamingAssets/Scripts/Memoria.Scripts.dll
.
A Mod folder can contain a modded version of that DLL, allowing tweaks in existing spell effects or coding of brand new effects.
A drawback of this system is that if the Memoria.Scripts.dll
and the Assembly-CSharp.dll
are incompatible, some attacks or spells have no effect in battles (no damage is dealt, no "Miss" message is displayed...).
This happens when the Memoria.Scripts.dll
was compiled with a version of the Assembly-CSharp.dll
that has a different API.
Recompiling Memoria.Scripts.dll
normally fixes the issue, provided that the source code of that DLL is available.
In order to compile Memoria.Scripts.dll
, one needs to place its source code in the folder StreamingAssets/Scripts/Sources/
(out of any mod folder) and then run StreamingAssets/Scripts/Compiler/Memoria.Compiler.exe
.
Warning: the newly compiled DLL will replace the existing StreamingAssets/Scripts/Memoria.Scripts.dll
.
Theoretically, almost all the public classes and methods of Memoria's C# source code could be used by custom scripts. In practice, the most useful API for battle effects can be found in the different classes of this folder, in particular BattleCalculator.cs.
It is a good idea to read at the native code of existing spell effects. For example, the native code of most common damaging magic spells (eg. Fire, Thunder, Bio, Flare, Doomsday...) is the following:
using System;
using Memoria.Data;
namespace Memoria.Scripts.Battle
{
[BattleScript(Id)]
// IBattleScript: required
// IEstimateBattleScript: optional. It means the class contains a method RateTarget that is used for selecting the most useful target by default
public sealed class MagicAttackScript : IBattleScript, IEstimateBattleScript
{
public const Int32 Id = 0009; // ID of the effect: it is the spell's "scriptId" given in "StreamingAssets/Data/Battle/Actions.csv"
// These setups should be in each battle script classes
private readonly BattleCalculator _v;
public MagicAttackScript(BattleCalculator v)
{
_v = v;
}
// The main method: what happens when the spell hits its target(s)
// It runs once per "Effect Point" of the spell's sequence and per different target
public void Perform()
{
_v.NormalMagicParams(); // Setup the base power of the spell; it expands to the following code:
/*
_v.Context.AttackPower = _v.Command.Power;
_v.Context.Attack = (Int16)(_v.Caster.Magic + Comn.random16() % (1 + (_v.Caster.Level + _v.Caster.Magic >> 3)));
_v.Context.DefensePower = _v.Target.MagicDefence;
*/
_v.Caster.PenaltyMini(); // Apply a penalty in the Attack if the caster is under Mini
_v.Target.PenaltyShellAttack(); // Apply a penalty in the Attack if the target is under Shell
_v.PenaltyCommandDividedAttack(); // Apply a penalty in the Attack if the spell is a Multi-Single targeting spell and it was casted on multiple targets
_v.CasterCommand.BonusElement(); // Apply the bonus if the spell has an elemental property that the caster boosts (eg. with an elemental-boosting equipment)
if (_v.CanAttackMagic()) // Apply the elemental property of the spell, returning "false" if the target is guarded against that element
{
_v.CalcHpDamage(); // Convert the AttackPower, Attack and DefensePower into HP damage; it roughly expands to the following code:
/*
_v.Target.HpDamage = _v.Context.Attack * (_v.Context.AttackPower - _v.Context.DefensePower);
*/
_v.TryAlterMagicStatuses(); // Apply the spell's status if there is any, with "_v.Command.HitRate"% chances to hit
}
}
public Single RateTarget()
{
_v.NormalMagicParams();
_v.Caster.PenaltyMini();
_v.Target.PenaltyShellAttack();
_v.PenaltyCommandDividedAttack();
_v.CasterCommand.BonusElement();
if (!_v.CanAttackMagic())
return 0;
if (_v.Target.IsUnderAnyStatus(BattleStatus.Reflect) && !_v.Command.IsReflectNull)
return 0;
_v.CalcHpDamage();
Single rate = Math.Min(_v.Target.HpDamage, _v.Target.CurrentHp); // Rate the spell with the damage it could deal
if ((_v.Target.Flags & CalcFlag.HpRecovery) == CalcFlag.HpRecovery) // Rate it negatively if it would heal the enemy target (eg. the spell's element is absorbed)
rate *= -1;
if (_v.Target.IsPlayer) // Rate it negatively on player characters (which are allies of the caster since "RateTarget" is only used for rating spells casted by player characters)
rate *= -1;
return rate;
}
}
}
To be complete
To be complete
This spell effect makes its target act twice for their next action: the next command used is duplicated (including its target) and added as a counter.
public void Perform()
{
if (_v.Target.IsUnderAnyStatus(BattleStatus.CannotAct)) // Missing condition
{
_v.Context.Flags |= BattleCalcFlags.Miss;
return;
}
_v.Target.AddDelayedModifier(
target => // Wait until the target dies or uses a command that can be duplicated
{
if (target.IsUnderAnyStatus(BattleStatus.Jump | BattleStatus.Death | BattleStatus.Petrify | BattleStatus.Venom))
return false;
CMD_DATA cmd;
if (btl_util.IsBtlUsingCommand(target.GetData, out cmd))
return !IsCommandValid(cmd);
return true;
},
target =>
{
if (target.IsUnderAnyStatus(BattleStatus.CannotAct))
return;
CMD_DATA cmd;
if (!btl_util.IsBtlUsingCommand(target.GetData, out cmd))
return;
// If there is a valid command to duplicate, add it as a counter
if (IsCommandValid(cmd))
BattleState.EnqueueConter(target, cmd.cmd_no, (BattleAbilityId)cmd.sub_no, cmd.tar_id);
}
);
}
private Boolean IsCommandValid(CMD_DATA cmd)
{
// A command is valid if (1) it is a main command, not a counter or a reaction, (2) its effect is not Quick, (3) it's not one of the special commands Defend/Change/Item/Throw and (4) it is not a system command
return cmd.regist != null && cmd == cmd.regist.cmd[0] && cmd.ScriptId != QuickScript.Id && cmd.cmd_no != BattleCommandId.Defend && cmd.cmd_no != BattleCommandId.Change && cmd.cmd_no != BattleCommandId.Item && cmd.cmd_no != BattleCommandId.Throw && cmd.cmd_no != BattleCommandId.AutoPotion && (cmd.cmd_no < BattleCommandId.SysEscape || cmd.cmd_no > BattleCommandId.SysStone);
}
Use the feature ChangeToMonster to turn a player character into an enemy. The pool of enemies changes depending on the scenario counter.
private class ShapePossibility
{
public String BattleName;
public Int32 TypeIndex;
public Byte Probability;
public Single ScaleFactor;
public ShapePossibility(String name, Int32 index, Byte prob, Single scale = 1.0f)
{
BattleName = name;
TypeIndex = index;
Probability = prob;
ScaleFactor = scale;
}
}
private ShapePossibility[] Shapes = new ShapePossibility[]
{
new ShapePossibility( "CH_E093", 0, 0, 0.35f ), // Ozma
new ShapePossibility( "CW_E066", 0, 0, 0.28f ), // Deathguise
new ShapePossibility( "WM_1000", 0, 0, 0.73f ), // Grand Dragon
new ShapePossibility( "BB_R001", 0, 0, 0.9f ), // Ring Leader
new ShapePossibility( "IP_R010", 1, 0, 0.8f ), // Agares
new ShapePossibility( "GV_R006", 0, 0, 0.9f ), // Wraith (Ice)
new ShapePossibility( "GV_R006", 1, 0, 0.9f ), // Wraith (Fire)
new ShapePossibility( "UV_R000", 0, 0, 0.7f ), // Ogre
new ShapePossibility( "WM_1550", 0, 0, 0.8f ), // Jabberwock
new ShapePossibility( "WM_1520", 0, 0, 0.8f ), // Armstrong
new ShapePossibility( "FR_R012", 0, 0, 0.8f ), // Abomination
new ShapePossibility( "WM_0300", 0, 25, 0.8f ), // Lizard Man
new ShapePossibility( "KM_R003", 0, 35, 0.8f ), // Anemone
new ShapePossibility( "KM_R002", 0, 100, 0.8f ) // Gigan Toad
};
public void Perform()
{
if (_v.Target.IsMonsterTransform || _v.Target.IsUnderAnyStatus(BattleStatus.Death))
{
_v.Context.Flags |= BattleCalcFlags.Miss;
return;
}
// By default, there are 25% chances to transform into Lizard Man, 35% in Anemone and the rest (40%) in Gigan Toad
// With advancements in the scenario, these probabilities are adjusted, mostly by adding probabilities to transform into stronger monsters
UInt16 scenarioCounter = (UInt16)(GameState.GeneralVariable[0] | (GameState.GeneralVariable[1] << 8));
Boolean deathguiseAvailable = scenarioCounter >= 11770 // Deathguise vanquished
&& (GameState.GeneralVariable[453] & 0x80) != 0 // Hades vanquished
&& GameState.Frogs >= 99; // Quale vanquished or currently fighting
Boolean ozmaAvailable = deathguiseAvailable
&& (GameState.GeneralVariable[199] & 0x8) != 0; // Ozma vanquished
if (scenarioCounter >= 4990) // Cleyra destroyed
{
Shapes[10].Probability += 25;
Shapes[12].Probability -= 10;
}
if (scenarioCounter >= 9600) // Forgotten Continent reached
{
Shapes[7].Probability += 10;
Shapes[8].Probability += 10;
Shapes[9].Probability += 20;
}
if (scenarioCounter >= 10400) // Hilda Garde III obtained
{
Shapes[4].Probability += 15;
Shapes[5].Probability += 10;
Shapes[6].Probability += 10;
}
if (scenarioCounter >= 10800) // Terra reached
{
Shapes[3].Probability += 15;
}
if (scenarioCounter >= 11760) // Crystal World reached
{
Shapes[2].Probability += 15;
Shapes[5].Probability -= 2;
Shapes[6].Probability -= 3;
}
if (deathguiseAvailable)
{
Shapes[1].Probability += 10;
Shapes[3].Probability -= 5;
Shapes[4].Probability -= 5;
}
if (ozmaAvailable)
{
Shapes[0].Probability += 10;
Shapes[1].Probability += 5;
Shapes[5].Probability -= 3;
Shapes[6].Probability -= 2;
}
UInt16 rand = (UInt16)(GameRandom.Next16() % 100);
UInt16 step = 0;
UInt32 choice = (UInt32)(Shapes.Length - 1);
for (UInt32 i = 0; i < Shapes.Length; i++)
{
if (rand < Shapes[i].Probability + step)
{
choice = i;
break;
}
step += Shapes[i].Probability;
}
List<BattleCommandId> disable = new List<BattleCommandId>();
disable.Add(BattleCommandId.Defend); // Disable the "Defend" command when transformed
if (choice <= 2)
disable.Add(BattleCommandId.Change); // Disable the "Change" command when transformed if the enemy is a big one
// With these settings, the command "Item" is replaced by "YellowMagic1", the transformation cancels on death, the HP/MP/Stats of the character are kept, and the monster's defences and elemental affinities are used
_v.Target.ChangeToMonster(Shapes[choice].BattleName, Shapes[choice].TypeIndex, BattleCommandId.Item, BattleCommandId.YellowMagic1, true, false, false, true, true, disable);
if (choice <= 2)
{
// Displace the transformed character to somewhere behind the normal character row
_v.Target.BattlePosX = 0;
_v.Target.BattlePosZ = -2600;
}
if (choice <= 1)
_v.Target.BattlePosY = -300;
_v.Target.ScaleModel((Int32)Math.Round(4096 * Shapes[choice].ScaleFactor), true); // Rescale the character
}
This spell effect can be used by the enemies on player characters to force them to act according to the enemies' interests.
private UInt16 ListToBtlID(IEnumerable<BattleUnit> list)
{
UInt16 btlId = 0;
foreach (BattleUnit unit in list)
btlId |= unit.Id;
return btlId;
}
private UInt16 ListToRandomBtlID(IEnumerable<BattleUnit> list)
{
UInt16 btlId = ListToBtlID(list);
return (UInt16)Comn.randomID(btlId);
}
public void Perform()
{
if (!_v.Target.IsPlayer || _v.Target.IsUnderAnyStatus(BattleStatus.CannotAct))
{
_v.Context.Flags |= BattleCalcFlags.Miss;
return;
}
List<BattleUnit> playerTeam = new List<BattleUnit>(BattleState.EnumerateUnits()).FindAll(btl => btl.IsPlayer);
List<BattleUnit> playerValidTargets = playerTeam.FindAll(btl => !btl.IsUnderAnyStatus(BattleStatus.Petrify | BattleStatus.Death | BattleStatus.Jump));
List<BattleUnit> enemyTargets = new List<BattleUnit>(BattleState.EnumerateUnits()).FindAll(btl => !btl.IsPlayer);
List<BattleUnit> enemyHealable = enemyTargets.FindAll(btl => !btl.IsUnderAnyStatus(BattleStatus.Zombie));
if (_v.Target.PlayerIndex == CharacterId.Zidane) // Force Zidane to use a Skill command
{
if (enemyHealable.Exists(btl => btl.CurrentMp < 200))
BattleState.EnqueueConter(_v.Target, BattleCommandId.RushAttack, BattleAbilityId.Sacrifice, ListToBtlID(enemyHealable)); // Heal the enemies
else
BattleState.EnqueueConter(_v.Target, BattleCommandId.RushAttack, BattleAbilityId.Thievery, ListToRandomBtlID(playerValidTargets)); // Damage an ally
}
else if (_v.Target.PlayerIndex == CharacterId.Vivi) // Force Vivi to use a Black Magic
{
if (_v.Target.IsUnderAnyStatus(BattleStatus.Silence)) // ...unless he's silenced
BattleState.EnqueueConter(_v.Target, BattleCommandId.RushAttack, BattleAbilityId.Attack, ListToRandomBtlID(playerValidTargets));
else if (playerValidTargets.FindAll(btl => !btl.IsUnderAnyStatus(BattleStatus.Petrify | BattleStatus.Death | BattleStatus.Sleep | BattleStatus.Reflect | BattleStatus.Jump) && (btl.ResistStatus & BattleStatus.Sleep) == 0).Count > 2)
BattleState.EnqueueConter(_v.Target, BattleCommandId.RushAttack, BattleAbilityId.Sleep, ListToBtlID(playerTeam)); // Sleep an ally
else
BattleState.EnqueueConter(_v.Target, BattleCommandId.RushAttack, BattleAbilityId.Comet, ListToRandomBtlID(playerValidTargets)); // Damage an ally
}
// Etc...
else
{
// The default case, when the target player is none of the above handled cases
BattleState.EnqueueConter(_v.Target, BattleCommandId.RushAttack, BattleAbilityId.Attack, ListToRandomBtlID(playerValidTargets));
}
}