Skip to content

Creating an Actor

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
ConstantTier
BTL_POS_GROUND_AGround (front)
BTL_POS_GROUND_BGround
BTL_POS_GROUND_CGround
BTL_POS_GROUND_DGround (back)
BTL_POS_AIR_AAir (front)
BTL_POS_AIR_BAir
BTL_POS_AIR_CAir
BTL_POS_AIR_DAir (back)
BTL_POS_HIGH_AHigh (front)
BTL_POS_HIGH_BHigh (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.