Skip to content

External Battle scripts

Tirlititi edited this page Nov 16, 2022 · 5 revisions

Introduction

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.

Application Programming Interface (API)

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.

A basic example: MagicAttackScript

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;
		}
	}
}

Most useful methods

To be complete

Delayed effects

To be complete

Script examples

Quick

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);
}

Monster Transfom

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
}

Mind Control

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));
	}
}