In the battle system, the characters on screen are called actors. This includes enemies, bosses, and even objects like Gulpit Rocks. The name comes from the theatre metaphor: battles take place on a stage, and the characters that perform on it are actors.
Each actor is defined in its own C file under src/battle/actor/. These files are compiled as overlays, meaning they're loaded into memory only when needed. An actor file exports a blueprint that defines the actor's stats and appearance, along with four scripts that control its behaviour:
- EVS_Init — runs when the actor spawns, binds the other three scripts
- EVS_Idle — runs continuously while the actor is waiting for its turn
- EVS_HandleEvent — responds to taking damage, status effects, and death
- EVS_TakeTurn — the actor's AI, runs when it's the actor's turn to attack
This page walks through creating a simple enemy called frost_goomba.
Creating the actor file
Create a new file at src/battle/actor/frost_goomba.c. Every actor file needs a few key pieces: a defense table, a status table, parts, animations, the blueprint, and four scripts.
Here's a minimal template:
#include "battle/battle.h"
#include "script_api/battle.h"
#include "sprite/npc/Goomba.h"
extern EvtScript EVS_Init;
extern EvtScript EVS_Idle;
extern EvtScript EVS_TakeTurn;
extern EvtScript EVS_HandleEvent;
extern s32 DefaultAnims[];
enum ActorPartIDs {
PRT_MAIN = 1,
};
s32 DefenseTable[] = {
ELEMENT_NORMAL, 0,
ELEMENT_END,
};
s32 StatusTable[] = {
STATUS_KEY_NORMAL, 0,
STATUS_KEY_DEFAULT, 0,
STATUS_KEY_SLEEP, 70,
STATUS_KEY_POISON, 100,
STATUS_KEY_FROZEN, 100,
STATUS_KEY_DIZZY, 80,
STATUS_KEY_UNUSED, 100,
STATUS_KEY_STATIC, 100,
STATUS_KEY_PARALYZE, 90,
STATUS_KEY_SHRINK, 80,
STATUS_KEY_STOP, 90,
STATUS_TURN_MOD_DEFAULT, 0,
STATUS_TURN_MOD_SLEEP, 0,
STATUS_TURN_MOD_POISON, 0,
STATUS_TURN_MOD_FROZEN, 0,
STATUS_TURN_MOD_DIZZY, 0,
STATUS_TURN_MOD_UNUSED, 0,
STATUS_TURN_MOD_STATIC, 0,
STATUS_TURN_MOD_PARALYZE, 0,
STATUS_TURN_MOD_SHRINK, 0,
STATUS_TURN_MOD_STOP, 0,
STATUS_END,
};
ActorPartBlueprint ActorParts[] = {
{
.flags = ACTOR_PART_FLAG_PRIMARY_TARGET,
.index = PRT_MAIN,
.posOffset = { 0, 0, 0 },
.targetOffset = { 0, 20 },
.opacity = 255,
.idleAnimations = DefaultAnims,
.defenseTable = DefenseTable,
.eventFlags = 0,
.elementImmunityFlags = 0,
.projectileTargetOffset = { 0, -10 },
},
};
export ActorBlueprint blueprint = {
.flags = 0,
.type = ACTOR_TYPE_GOOMBA,
.level = ACTOR_LEVEL_GOOMBA,
.maxHP = 5,
.partCount = ARRAY_COUNT(ActorParts),
.partsData = ActorParts,
.initScript = &EVS_Init,
.statusTable = StatusTable,
.escapeChance = 70,
.airLiftChance = 90,
.hurricaneChance = 85,
.spookChance = 80,
.upAndAwayChance = 95,
.spinSmashReq = 0,
.powerBounceChance = 100,
.coinReward = 1,
.size = { 24, 24 },
.healthBarOffset = { 0, 0 },
.statusIconOffset = { -10, 20 },
.statusTextOffset = { 10, 20 },
};
s32 DefaultAnims[] = {
STATUS_KEY_NORMAL, ANIM_Goomba_Idle,
STATUS_KEY_STONE, ANIM_Goomba_Still,
STATUS_KEY_SLEEP, ANIM_Goomba_Sleep,
STATUS_KEY_POISON, ANIM_Goomba_Idle,
STATUS_KEY_STOP, ANIM_Goomba_Still,
STATUS_KEY_STATIC, ANIM_Goomba_Idle,
STATUS_KEY_PARALYZE, ANIM_Goomba_Still,
STATUS_KEY_DIZZY, ANIM_Goomba_Dizzy,
STATUS_KEY_UNUSED, ANIM_Goomba_Dizzy,
STATUS_END,
};
EvtScript EVS_Init = {
Call(BindTakeTurn,actorID ACTOR_SELF,script Ref(EVS_TakeTurn))
Call(BindIdle,actorID ACTOR_SELF,script Ref(EVS_Idle))
Call(BindHandleEvent,actorID ACTOR_SELF,script Ref(EVS_HandleEvent))
Return
End
};
EvtScript EVS_Idle = {
Return
End
};
EvtScript EVS_HandleEvent = {
Call(UseIdleAnimation,actorID ACTOR_SELF,useIdle false)
Call(EnableIdleScript,actorID ACTOR_SELF,mode IDLE_SCRIPT_DISABLE)
Call(GetLastEvent,actorID ACTOR_SELF,outEvent LVar0)
Switch(LVar0)
CaseOrEq(EVENT_HIT_COMBO)
CaseOrEq(EVENT_HIT)
SetConst(LVar0, PRT_MAIN)
SetConst(LVar1, ANIM_Goomba_Hurt)
ExecWait(EVS_Enemy_Hit)
EndCaseGroup
CaseEq(EVENT_DEATH)
SetConst(LVar0, PRT_MAIN)
SetConst(LVar1, ANIM_Goomba_Hurt)
ExecWait(EVS_Enemy_Hit)
Wait(10)
SetConst(LVar0, PRT_MAIN)
SetConst(LVar1, ANIM_Goomba_Dead)
ExecWait(EVS_Enemy_Death)
Return
CaseOrEq(EVENT_ZERO_DAMAGE)
CaseOrEq(EVENT_IMMUNE)
SetConst(LVar0, PRT_MAIN)
SetConst(LVar1, ANIM_Goomba_Idle)
ExecWait(EVS_Enemy_NoDamageHit)
EndCaseGroup
CaseDefault
EndSwitch
Call(EnableIdleScript,actorID ACTOR_SELF,mode IDLE_SCRIPT_ENABLE)
Call(UseIdleAnimation,actorID ACTOR_SELF,useIdle true)
Return
End
};
EvtScript EVS_TakeTurn = {
Call(UseIdleAnimation,actorID ACTOR_SELF,useIdle false)
Call(EnableIdleScript,actorID ACTOR_SELF,mode IDLE_SCRIPT_DISABLE)
Call(SetTargetActor,attackerID ACTOR_SELF,defenderID ACTOR_PLAYER)
Call(SetGoalToTarget,actorID ACTOR_SELF)
Call(EnemyTestTarget,
actorID ACTOR_SELF,outResult LVar0,damageType 0,debuffType 0,
damageAmount 1,flagsModifier BS_FLAGS1_INCLUDE_POWER_UPS)
Switch(LVar0)
CaseOrEq(HIT_RESULT_MISS)
CaseOrEq(HIT_RESULT_LUCKY)
Call(YieldTurn)
Call(EnableIdleScript,actorID ACTOR_SELF,mode IDLE_SCRIPT_ENABLE)
Call(UseIdleAnimation,actorID ACTOR_SELF,useIdle true)
Return
EndCaseGroup
EndSwitch
Call(EnemyDamageTarget,
actorID ACTOR_SELF,outResult LVar0,damageType 0,suppressEventFlags 0,
debuffType 0,damageAmount 1,flagsModifier BS_FLAGS1_TRIGGER_EVENTS)
Call(YieldTurn)
Call(EnableIdleScript,actorID ACTOR_SELF,mode IDLE_SCRIPT_ENABLE)
Call(UseIdleAnimation,actorID ACTOR_SELF,useIdle true)
Return
End
};This template creates a working actor with 5 HP that deals 1 damage. For a much more complete example with real attack animations, idle shuffling, and full event handling, see src/battle/actor/gloomba.c.
Adding the actor to a battle
Actors appear in battles through formations defined in area files. Use the OVL_ACTOR_BY_IDX macro to place your actor at a predefined position:
Formation MyFormation = {
OVL_ACTOR_BY_IDX("frost_goomba", BTL_POS_GROUND_B, 10),
OVL_ACTOR_BY_IDX("frost_goomba", BTL_POS_GROUND_C, 9),
};The third argument is priority — actors with higher priority take their turns first.
For custom positions, use OVL_ACTOR_BY_POS with a position vector:
Vec3i FrostGoombaPos = { 50, 0, -10 };
Formation MyFormation = {
OVL_ACTOR_BY_POS("frost_goomba", FrostGoombaPos, 10),
};Battle position constants
| Constant | Tier |
|---|---|
BTL_POS_GROUND_A | Ground (front) |
BTL_POS_GROUND_B | Ground |
BTL_POS_GROUND_C | Ground |
BTL_POS_GROUND_D | Ground (back) |
BTL_POS_AIR_A | Air (front) |
BTL_POS_AIR_B | Air |
BTL_POS_AIR_C | Air |
BTL_POS_AIR_D | Air (back) |
BTL_POS_HIGH_A | High (front) |
BTL_POS_HIGH_B | High (back) |
Testing
By this point, you should have the following files:
Directorysrc
Directorybattle
Directoryactor
- frost_goomba.c
Directoryarea
Directoryyour_area
- area.c Edited
Build your mod and use the debug menu to Load Battle and test your formation.