Skip to content

Fighter methods

For objective-based victories and defeats, see fighter:end_round. It requires a declared controller rule and queues a round result through the normal offline result flow.

These methods are supplied as the fighter argument to supported combat callbacks. Use a colon (:), which passes the fighter as the method’s first argument. They are not global sf2 functions.

The examples below run inside a callback. A fighter reference must not be saved and used after that callback returns. Missing capability declarations, expired references, invalid values, or an unavailable fighter raise Lua errors. Most mutation methods return nil on success; exceptions are documented below. Observation methods return detached data.

For native collision-rig rectangle queries and owned arena warning art, see Arena regions and markers.

Read fresh combat observations, including both fighters and the engine’s elapsed fight clock. Use this when making a health or distance decision; the older fighter.health field is captured at callback entry.

Signature: fighter:snapshot()

Returns: A CombatSnapshot table, or nil if the fighter cannot be observed.

When: Inside any supported combat behavior callback, including battle rules, perks, enchantments, warrior behaviors and attached actor behaviors. Each call samples the current state. The callable reference expires when that callback returns; the returned data may be retained as an observation, but will not update itself.

Requires: No additional capability. Observing the opponent does not require combat.target; changing the opponent still requires the normal capabilities.

Field Meaning
self The fighter receiving this callback, even when it is the enemy.
opponent This fighter’s current native target snapshot, or nil when unavailable. It can be a main fighter or an independent actor.
frame Nonnegative elapsed active-fight frame count, shared by both sides.
seconds frame / 60, using the recovered engine’s simulation rate.
round_active Whether the engine is processing the round at capture time.
difficulty The fight’s difficulty, described below, or nil when the fight has none.

Both fighter snapshots contain health, max_health, health_bars, and position = { x, y, z }. Health uses the game’s life units, the same units as fighter.health, damage and fighter:change_health: a normal fighter’s full health is 1. A raid boss stores its whole shield in these units, so its max_health equals its warrior’s health_bars (for example 20), and a hit of 0.25 removes 0.25 of it. health_bars is that authored shield total (at least one), not the number of bars remaining or the number drawn on the HUD. Divide health by max health for a fraction, guarding against a zero maximum.

difficulty is the band the map’s difficulty bar shows for this encounter, worked out the same way from the player’s and the current opponent’s equipment and rating. It is a table with two fields:

Difficulty field Meaning
level Integer band: 0 very easy, 1 easy, 2 normal, 3 hard, 4 very hard.
ratio The opponent-to-player rating ratio the band is chosen from; above 0. Bands start above 0.82, 1.3, 2.6 and 5.2 in the shipped settings.

It is measured once for each opponent of a fight, so it does not change as health or charges change, and it is the same for both fighters’ callbacks. It is nil in fights that have no difficulty rating. Use it to scale your own rules, for example a computer opponent’s chance to use an ability:

local combat = fighter:snapshot()
local level = combat and combat.difficulty and combat.difficulty.level or 2
local chance = level * 0.1 -- 0% on very easy, 40% on very hard

Each fighter snapshot may also have an actor table when the observed root is an initialized independent fighter. Main fighters omit it. This applies to actor snapshots and Lua AI observations.

Actor field Meaning
id Live fight-local actor instance ID, such as a1; changes when a replacement is spawned.
definition Qualified actor definition ID, such as my.mod:actors/helper.
owner Mod that owns this actor instance.
team Absolute "player" or "opponent" team, irrespective of who receives this observation.

These are copied observations, not actor handles or permissions. Editing them cannot retarget, transfer ownership or change a team. Retained snapshots remain historical data; reacquire a live scoped actor reference for commands.

Each fighter snapshot also has an optional animation table. It is nil when the native controller is absent, stopped, or cannot provide a valid bounded observation. The rest of the fighter snapshot remains available.

Animation field Meaning
name Native name of the currently playing animation, including authored namespaced moves.
type Native authored classification: "none", "move", or "attack".
facing Native orientation sign: 1 along positive model X, -1 along negative model X.
frame The animation’s current frame number, as a perk’s <CurrentAnimation Min Max> condition compares it. Use it to act on a particular frame of a move. While the fighter is in a physics reaction (for example falling), it is the physics frame instead.
intervals Array of currently active { name, type } interval observations, in native order. May be empty; at most 256 entries.

Interval type is one of "none", "unstable", "uninterrupt", "self_uninterrupt", "attack", "block", "invulnerable", or "invisible". name preserves the authored interval name, or is empty when the native interval has no name. Custom named intervals may have type "none". These are observations from the controller’s latest interval update, not the animation’s complete list of future windows. An active attack interval does not guarantee contact; an interval label does not override other native combat rules.

For example, inside a callback you can detect an opponent’s active attack window:

local combat = fighter:snapshot()
local animation = combat and combat.opponent and combat.opponent.animation
local attacking = false
if animation then
for _, interval in ipairs(animation.intervals) do
if interval.type == "attack" then attacking = true; break end
end
end
-- Use attacking in your own Lua rule logic.

Animation and interval tables are detached values: editing them cannot start/stop an animation, turn a fighter, or add/remove an interval. They can be retained as historical observations, but never used as an AI candidate or an engine operation handle.

Positions use arena model coordinates, not screen pixels or metres. The clock counts active fight frames across rounds, excluding round transitions and frames when fight processing is stopped; it is neither wall time nor the remaining round timer. Capture a baseline in on_round_begin to measure a round-local duration. This method does not schedule callbacks or grant control of the clock.

on_damage_resolving = function(self, fighter, event)
local combat = fighter:snapshot()
if not combat or combat.self.max_health <= 0 then return end
local fraction = combat.self.health / combat.self.max_health
if fraction < 0.25 then
fighter:scale_incoming_damage(0.5) -- requires combat.modify_hit
end
end

Each result is a detached Lua table. Editing it affects only your copy and cannot move a fighter, change health, alter later snapshots, or change the engine clock. Call again after a health operation to obtain a fresh observation. Query failure does not fabricate zero health or a default position.

Scale the current attacker’s pending hit.

Signature: fighter:scale_outgoing_damage(multiplier)

Returns: nil on success; invalid values, missing capability, unavailable hit or expired references raise a Lua error.

When: Inside on_damage_dealing, and inside attacker-side on_post_hit where event.target == "opponent". It is absent from on_hit_post_crit and incoming on_post_hit callbacks.

Requires: combat.modify_outgoing_hit.

on_damage_dealing = function(parameters, fighter, event)
fighter:scale_outgoing_damage(1.25)
end

The multiplier must be finite and between 0 and 16 inclusive. The resulting damage must remain finite, nonnegative, and representable as a single-precision number. Invalid scaling leaves the pending value unchanged. Calls multiply the current value; zero remains zero. Invulnerability, shields, incoming modifiers, and the normal health path run afterward. This does not bypass defense or change the shared weapon/stat economy. Earlier successful operations remain applied if a later handler fails. Retained methods cannot modify a later hit.

Add normalized health damage to the current attacker’s pending hit.

Signature: fighter:add_outgoing_damage(amount)

Requires: combat.modify_outgoing_hit.

When: Inside on_damage_dealing, and inside attacker-side on_post_hit where event.target == "opponent". It is absent from on_hit_post_crit and incoming post-hit callbacks.

Returns: nil on success; invalid values, missing capability, unavailable hit or expired references raise a Lua error.

amount must be finite and from 0 through 1 normalized health units. It is added to the current pending damage value; the resulting value must remain finite, nonnegative and representable as a single-precision number. Defender rules and normal health application still run afterward.

fighter:add_outgoing_damage(0.04)

Add or subtract health through the game’s normal health-change path.

Signature: fighter:change_health(amount)

Requires: combat.change_life.

When: A supported callback with a live fighter capability.

Returns: nil.

amount is a finite number representable by a 32-bit float. Positive values heal; negative values damage. Values use the game’s life units rather than a separate percentage-conversion API. A fighter whose health has already reached zero cannot be revived with this operation.

fighter:change_health(0.05)

Use small values and test them against the relevant opponent and health-bar configuration: -1 removes a normal fighter’s whole health but only 1 of the 20 units of a health_bars = 20 raid shield. Healing after on_damage_received does not reverse a lethal hit.

Add to the fighter’s magic charge and let the game normalize it.

Signature: fighter:add_magic_charge(amount)

Requires: combat.magic_charge.

When: A supported callback with a fighter capability.

Returns: nil.

amount must be a finite 32-bit-float value. The example adds 0.35 charge through the normal magic-charge path. The player needs a usable magic item to observe it.

fighter:add_magic_charge(0.35)

Apply the health-change operation to the opposing fighter.

Signature: fighter.opponent:change_health(amount)

Requires: Both combat.target and combat.change_life.

When: A supported callback where fighter.opponent is available.

Returns: nil; the same numeric and no-revival rules as change_health apply.

if fighter.opponent then
fighter.opponent:change_health(-0.05)
end

Adjust the opposing fighter’s magic charge.

Signature: fighter.opponent:add_magic_charge(amount)

Requires: Both combat.target and combat.magic_charge.

When: A supported callback where the opponent is available.

Returns: nil; accepts a finite 32-bit-float number.

if fighter.opponent then
fighter.opponent:add_magic_charge(0.1)
end

The opponent target also exposes move_by and play_move. There are no opponent shield or opponent pending-hit methods on this target object.

Multiply this fighter’s pending incoming damage before it is applied.

Signature: fighter:scale_incoming_damage(multiplier)

Requires: combat.modify_hit.

When: Only inside on_damage_resolving.

Returns: nil.

multiplier is finite and between 0 and 1 inclusive. 0 prevents this hit’s damage, 0.5 halves it, and 1 leaves it unchanged. Values above 1 are rejected. Multiple active behaviors multiply in dispatch order.

fighter:scale_incoming_damage(0.5)

Calling this after damage has resolved is too late; the method is not supplied in those callbacks.

Decide whether the hit being classified counts as blocked. Use it to make your attacks unblockable for a while, or to let a guard stop a hit that would land.

Signature: fighter:set_hit_blocked(blocked)

Requires: combat.modify_outgoing_hit when event.target == "opponent" (this fighter is the attacker), or combat.modify_hit when event.target == "self" (this fighter is being hit).

When: Only inside on_hit_pre_crit.

Returns: nil. A non-boolean argument, a missing capability or a call after the callback returned raises a Lua error.

Argument Type Meaning
blocked Boolean false lets the hit through as unblocked; true counts it as blocked.
-- Inside on_hit_pre_crit: the opponent's guard does not stop this fighter's hits.
if event.target == "opponent" and event.blocked then
fighter:set_hit_blocked(false)
end

This works like a perk’s <SetHit Block="0"> in a <HitPreCrit> trigger. The game then rolls the critical and calculates damage from your decision: an unblocked hit can be critical and deals full, unblocked damage. Several handlers can change the decision; the last call wins, and later handlers see it in event.blocked.

Apply temporary damage reduction to future hits.

Signature: fighter:add_damage_shield(key, fraction, frames)

Requires: combat.effects.

When: A supported callback with fighter effect operations.

Returns: nil.

Argument Type and bounds Meaning
key 1–64 letters, digits, or underscores Name used to refresh/remove this behavior’s shield.
fraction Finite number, 0–1 Fraction of damage reduced; 0.25 means 25% reduction.
frames Integer, 1–3600 Duration in simulation frames; 60 frames is one combat second.
fighter:add_damage_shield("opening_ward", 0.25, 180)

This requests 25% reduction for three combat seconds. Reusing a key refreshes that behavior’s shield; different keys can compound. At most 128 shields are active per fighter. The timer pauses with combat and shields clear at round/fight cleanup. They do not add native buff icons or extra boss health bars.

Remove a shield created under this behavior’s key.

Signature: fighter:remove_damage_shield(key)

Requires: combat.effects.

When: A supported callback with fighter effect operations.

Returns: nil.

key uses the same naming rules as add_damage_shield. It is scoped by the behavior identity; this does not remove arbitrary native effects.

fighter:remove_damage_shield("opening_ward")

Create a native combat flag owned by this behavior instance.

Signature: fighter:set_flag(key, frames?)

Requires: combat.effects.

When: A supported callback during an active round, with a currently registered fighter. The method expires when the callback returns.

Returns: The qualified native flag name as a string. Setting an existing key does nothing and returns the same name. Invalid input or unavailable support raises a Lua error.

key is 1–64 ASCII letters, digits or underscores. The native name is <qualified behavior ID>:<key> and must fit within 128 characters in total. For behavior example.spell:behaviors/cast, key pending becomes example.spell:behaviors/cast:pending.

on_animation_start = function(parameters, fighter, event)
if event.target == "self"
and event.animation_name == sf2.mod.id .. ":moves/cast" then
fighter:set_flag("pending")
end
end,

frames is optional: an integer from 1 to 216,000 native simulation frames (60 per second). With it, the flag expires on its own after that time, exactly like a perk’s timed <ModFlag>: native expiry removes it and emits its ModExpires event, and a recharge indicator can show its remaining time. Without it, the flag has no timer: clear it explicitly or let native round/fight cleanup remove it. Setting a key that is already active does nothing, even with a new duration; clear it first to restart it:

fighter:clear_flag("cooldown")
fighter:set_flag("cooldown", 600) -- ten seconds

Flags are not saved in the profile. They participate in native modifier lists, mod_exists conditions, expiry notifications and fighter form transfers. There are at most 64 flag-owning instances per fighter registration and 64 active flags per instance; exceeding either limit raises an error.

Ownership includes both behavior identity and equipped/rule instance. One instance cannot clear another instance’s flags, even if both use the same key. Native move conditions see the qualified name, so they match any active instance using that name. This method cannot create or overwrite a core flag such as Stun.

Check this behavior instance’s flag without changing it.

Signature: fighter:has_flag(key)

Requires: combat.effects.

When: A supported callback during an active round with a registered fighter.

Returns: true if this instance owns the flag, otherwise false. Invalid keys, expired callback references or unavailable support raise a Lua error.

if fighter:has_flag("pending") then
sf2.log.info("This cast still owns its pending flag")
end

The key and length rules are the same as set_flag. This does not query arbitrary native modifiers or another instance’s state.

Remove this behavior instance’s flag through native modifier expiry handling.

Signature: fighter:clear_flag(key)

Requires: combat.effects.

When: A supported callback during an active round with a registered fighter.

Returns: nil. Clearing an absent key succeeds without emitting another expiry event. Invalid keys, expired methods or unavailable support raise an error.

fighter:clear_flag("pending")

Clearing an existing flag removes it from mod_exists observations and emits the native ModExpires event for its qualified name. A move can listen using events = { { type = "mod_expires", name = "example.spell:behaviors/cast:pending" } }. Normal move conditions, locks and priority still decide whether that move runs. Clearing one instance emits its expiry even if another instance retains a flag with the same qualified name. For purely Lua bookkeeping with no native move interaction, prefer declared behavior state.

Restrict one player action for this behavior instance.

Signature: fighter:set_control_blocked(control, blocked)

Returns: nil. Invalid arguments, expired references, unavailable support, or exceeding the source limit raise a Lua error.

When: A behavior callback for the player during an active round, including on_fight_begin and on_round_begin setup before combat starts. Unavailable for opponents, after a round ends, and in local versus.

Requires: combat.effects.

control is one of the case-sensitive strings "punch", "kick", "ranged", "magic", or "raid_charge". blocked must be a boolean.

local restriction = sf2.behaviors.register {
id = "restricted_magic",
on_round_begin = function(parameters, fighter)
fighter:set_control_blocked("magic", true)
end,
}
sf2.rules.behavior {
id = "restricted_magic", behavior = restriction, target = sf2.rules.PLAYER,
}
-- Attach the returned rule to a fight's rules list to apply it.

true hides and blocks the action through the shared touch, keyboard and gamepad gate. Blocking a held action releases it; resuming requires neutral input followed by a fresh press. false releases only this behavior instance’s restriction. It cannot override another instance, a native no_button rule, or ordinary equipment and charge availability, and does not force a hidden ability button to appear.

Restrictions persist across callbacks and pause/resume. They clear when the next round is prepared or the fight ends; reapply them in on_round_begin when needed. Repeated calls are idempotent. Ownership includes the behavior and its attached perk/rule instance. At most 256 instances can hold restrictions per controller; each can block all five actions. Releasing an instance’s last block frees its slot.

Start a recharge animation on one of this fighter’s combat buttons.

Signature: fighter:set_button_cooldown(control, frames)

Requires: combat.effects.

When: A supported callback during an active round, with a currently registered fighter. The method expires when the callback returns.

Returns: nil. Unknown controls, out-of-range frames, an inactive round or an expired fighter raise a Lua error.

Argument Type and bounds Meaning
control String "punch", "kick", "ranged" or "raid_charge".
frames Integer, 1–3600 Native combat frames (60 per second) for the button’s progress ring to refill.
-- After the ability is used, show a ten-second recharge on the RaidCharge button.
on_animation_start = function(self, fighter, event)
if event.target == "self" and event.animation_name == "SummonAssistant" then
fighter:set_button_cooldown("raid_charge", 600)
end
end,

This is the same presentation as the native SetCooldown perk action: the button empties and refills over frames. It only changes what the button shows. It does not stop the move from being used; gate the move itself with a timed flag and a not_mod move condition. Calling it again restarts the ring. Only the player’s buttons are drawn, so an opponent’s call has no visible effect. The magic button’s ring follows the magic charge and is not supported here.

Show the RaidCharge button in a fight that would otherwise hide it.

Signature: fighter:set_control_visible(control, visible)

Requires: combat.effects.

When: A supported callback during an active round, with a currently registered fighter. The method expires when the callback returns.

Returns: nil. An unsupported control, a non-boolean visible, an inactive round or an expired fighter raises a Lua error.

Argument Type Meaning
control String "raid_charge". Other buttons follow their equipment and are not supported.
visible Boolean true shows the button, false hides it again.
-- An equipped ability uses the RaidCharge input, so show its button.
on_tick = function(self, fighter, event)
if not self.state.shown then
self.state.shown = true
fighter:set_control_visible("raid_charge", true)
end
end,

The game normally shows the RaidCharge button only in raid fights where the player carries raid charges. Use this when your perk or enchantment adds a move on the RaidCharge input in other fights. The raid charge count is not shown on a button made visible this way. A control restriction still hides a blocked button. Only the player’s button is drawn: for an opponent or in Local Versus the call succeeds and changes nothing. The next fight starts with the game’s own visibility again.

Show or refresh a transient status icon owned by this behavior instance.

Signature: fighter:show_status_icon(key, sprite, frames, stacks?)

Requires: combat.effects.

When: A supported behavior callback with a live fighter capability.

Returns: nil; invalid arguments, unavailable native support or an expired fighter method raise a Lua error.

Argument Type and bounds Meaning
key Valid behavior field name Local key used to refresh or clear this behavior instance’s icon.
sprite Sprite handle Captured sprite handle from the current mod context.
frames Integer, 1–3600 Native-frame lifetime. 0 is rejected.
stacks Integer, 0–10000 Optional stack count; defaults to 0.
local icon = sf2.assets.sprite("core:ui/skills/IconCrackedApple_Blue")
fighter:show_status_icon("relentless", icon, 300, 5)

The native key includes the behavior instance identity, so matching Lua keys from different behavior instances are isolated. The method expires when the callback returns. This documents the scripting contract only; it does not claim rendered HUD acceptance for a particular sprite.

Clear a status icon addressed by this behavior instance.

Signature: fighter:clear_status_icon(key)

Requires: combat.effects.

When: A supported behavior callback with a live fighter capability.

Returns: nil; invalid keys, unavailable native support or expired references raise a Lua error.

fighter:clear_status_icon("relentless")

The key uses the same validation and behavior-instance isolation as show_status_icon; it does not clear arbitrary status entries owned elsewhere.

Tint this fighter’s whole model, including weapons, for a number of frames. An alpha below FF makes the fighter see-through.

Signature: fighter:set_model_color(color, frames)

Requires: combat.effects.

When: A supported behavior callback with a live fighter capability, during an active round.

Returns: nil; an invalid color or frame count, or an expired fighter method, raises a Lua error.

Argument Type and bounds Meaning
color "#RRGGBB" or "#RRGGBBAA" Multiplies the model’s colors. #FFFFFF leaves the colors unchanged; the alpha defaults to FF (opaque).
frames Integer, 1–216000 How long the tint lasts, in simulation frames (60 per second).
-- Half-transparent for 650 frames, as DE's Smoke Bomb does.
fighter:set_model_color("#FFFFFF80", 650)

This is the game’s own <ChangeModelColor> perk effect. Each behavior instance owns one tint: calling again replaces it and restarts its timer. When the frames run out, when you call clear_model_color, or when the round resets, the fighter returns to its normal colors. A fighter shows one tint at a time: the most recent tint from any perk or mod is shown, and removing any tint returns the fighter to its normal colors. This changes only how the fighter looks; it does not hide the fighter from the opponent or its AI.

Remove the tint this behavior instance set with set_model_color before its frames run out.

Signature: fighter:clear_model_color()

Requires: combat.effects.

When: A supported behavior callback with a live fighter capability, during an active round.

Returns: nil. Clearing when this behavior has no tint does nothing.

fighter:clear_model_color()

Turn this fighter into refracting glass for a number of frames: the stage behind shows through the body, blurred and bent toward the edges, with a slow shimmer and a bright rim. Use it for invisibility and stealth abilities.

Signature: fighter:set_cloak(frames)

Requires: combat.effects.

When: A supported behavior callback with a live fighter capability, during an active round.

Returns: nil; an invalid frame count or an expired fighter method raises a Lua error.

Argument Type and bounds Meaning
frames Integer, 1–216000 How long the cloak lasts, in simulation frames (60 per second).
-- Glass for 650 frames, as DE128's Smoke Bomb does.
fighter:set_cloak(650)

The cloak flashes as it appears, flashes briefly whenever this fighter lands a hit, and ends with a short white flash before the fighter is drawn normally again. The whole body, including weapons, is drawn as one shape, so overlapping parts do not show through each other the way a transparent set_model_color does.

Each behavior instance owns one cloak: calling again restarts its timer. It ends when the frames run out, when you call clear_cloak, or when the round resets. Cloaks from several behaviors add up; the fighter stays cloaked while any is active.

This changes only how the fighter looks. Hit detection, the opponent’s AI and every combat rule see the fighter exactly as before; pair it with set_hit_blocked or other effects for gameplay. If the device cannot run the cloak shaders, the fighter is drawn normally.

End the cloak this behavior instance started with set_cloak before its frames run out.

Signature: fighter:clear_cloak()

Requires: combat.effects.

When: A supported behavior callback with a live fighter capability, during an active round.

Returns: nil. Clearing when this behavior has no cloak does nothing.

fighter:clear_cloak()

Queue an additive displacement of the current main fighter.

Signature: fighter:move_by(x, y, z?)

Returns: true, nil for an accepted request; false, reason when the native host cannot accept it. Invalid arguments, missing capabilities, expired methods or forbidden callback timing raise a Lua error. Acceptance is not a completion receipt and does not change a snapshot taken in the same callback.

When: An active simulation callback with a living main fighter in an offline fight. Unavailable in on_fight_begin, on_round_begin, on_round_end and on_fight_end, while paused, after round ending, in training/title sparring, local versus, legacy PvP and online raids. Offline mod raid encounters are eligible.

Requires: combat.motion.

-- Inside on_tick, when your ability becomes ready:
local accepted, reason = fighter:move_by(80, 0)
if not accepted then sf2.log.warn(reason) end

x and y are required finite numbers; z is optional and defaults to 0 (including an explicit nil). Each axis must be within -1000..1000 native rig units. Positive X is independent of facing, positive Y is downward (negative Y moves upward); these are relative offsets, not pixels, destinations or velocities. Only two or three arguments are accepted. Both colon and dot-call syntax are supported.

Accepted requests add together for each body and apply once after model, collision and animation processing, before round arbitration and interpolation capture. The running sum must stay within the same per-axis bound; at most 32 nonzero requests can be pending per body per step across all behaviors/mods. A rejection preserves earlier accepted requests. Zero offsets consume no slot.

The rig’s current/previous points, derived geometry, keyframes and animation buffers translate together. The native constraint/friction solver can correct rig positions and velocity, including configured wall/floor constraints. This does not create a hit or sweep collisions along the path. The API does not validate a safe destination; native collision processing continues on subsequent steps. A stale round, body, script session or dead fighter discards pending motion; pause retains it until simulation resumes. There is no completion callback. See Create a movement ability for the complete example and composition rules.

Queue an additive displacement of the opposing main fighter.

Signature: fighter.opponent:move_by(x, y, z?)

Returns: true, nil for an accepted request; false, reason for native rejection. Invalid arguments, missing capabilities, expired methods and forbidden callback timing raise a Lua error. Acceptance does not guarantee application.

When: The same active simulation callbacks and offline fight eligibility as fighter:move_by, with a living opposing main fighter. The opponent table may be absent; check it before calling. Begin/end callbacks cannot move either fighter.

Requires: combat.motion and combat.target.

-- Inside an active combat callback:
if fighter.opponent then
local accepted, reason = fighter.opponent:move_by(-60, 0, 0)
if not accepted then sf2.log.warn(reason) end
end

The self method’s coordinate, argument, displacement, request-count, deferred application and lifetime limits also apply. The opponent shares its pending queue with requests it receives through other behaviors. Moving both participants requires two independent calls; they are not a transaction. A call cannot target arbitrary child actors or retained fighters from earlier callbacks.

Request explicit playback of an authored move on this main fighter.

Signature: fighter:play_move(move)

Returns: A PlayMoveRequest table with status ("queued", "applied" or "failed") and optional error. Native rejection immediately returns failed; accepted work starts queued and the host updates the receipt at its simulation boundary. applied means playback started, not that the move completed or hit. Invalid arguments, missing capabilities, expired references and forbidden callback timing raise Lua errors.

When: Active simulation callbacks with a living main fighter in an offline round. Unavailable in on_fight_begin, on_round_begin, on_round_end, on_fight_end, while paused, in training/title sparring, local versus, legacy PvP or online raids. Offline mod raids are eligible. Requests during the native playback boundary reject to prevent recursive start/end chains.

Requires: combat.animation. move must be a handle returned by this mod’s sf2.moves.register or sf2.moves.replace; raw names, forged handles and another script context’s handles are rejected.

-- strike is a move handle registered by this script during loading.
-- Inside an active callback, request once when the ability activates:
local request = fighter:play_move(strike)
-- You may retain this receipt and poll it in later ticks, unlike fighter handles.
if request.status == "failed" then sf2.log.warn(request.error) end

The move must exist in the current fighter’s native animation cache, which is built for its rig/equipment/locks. Availability is checked at queuing and again at application. This API deliberately uses explicit named playback, rather than input/AI selection: it does not evaluate move selection events/conditions, key bindings, priorities, cancel windows or player control restrictions. It can interrupt a running animation. Native model readiness/physics checks still apply. Define ability costs, readiness, cancellation and cooldown policy in Lua; add attack windows, damage, effects and transitions to the declarative move. Test the binary’s rig compatibility. A cached move alone is not proof that it is appropriate to play during every state.

Only one request per body can be pending in a simulation step, shared across mods. The first accepted request wins; another returns a failed receipt without overwriting it, even for the same move. Each participant has a separate slot. An accepted request remains queued if its handler later raises an error; combat operations are not a transaction. The host applies motion first, then playback, after normal model/collision/ animation processing and before round arbitration/interpolation capture. Snapshots inside the requesting callback still show the old animation.

Pause retains already queued work until simulation resumes. A dead/replaced/ detached body, unavailable move, stale round/session or round/fight teardown fails its receipt and never replays the request. Native start failure also sets failed, without suppressing the other participant’s request. Receipt updates do not invoke a Lua completion callback. The receipt is transient; do not persist it in saved state. Editing its fields cannot control the host.

See Active Strike for a complete HUD ability using a registered punch and an applied receipt before starting cooldown.

Request explicit playback of a registered move on the opposing main fighter.

Signature: fighter.opponent:play_move(move)

Returns: A PlayMoveRequest with the same queued/applied/failed/error contract as the self method. applied confirms startup only, not completion or damage.

When: The same active simulation callbacks and offline-round eligibility as the self method, with a living opponent. Check that the opponent table exists; begin/end callbacks cannot start a move on either participant.

Requires: combat.animation and combat.target; a registered move handle owned by this script context.

-- Inside an active callback; reaction was registered during loading.
if fighter.opponent then
local request = fighter.opponent:play_move(reaction)
if request.status == "failed" then sf2.log.warn(request.error) end
end

All self-method availability, argument, explicit-selection, queue, receipt and lifetime rules apply. The opponent’s slot is shared with requests it receives through its own behaviors. Two calls are independent, not an atomic pair; this operation cannot address arbitrary child actors.