Fight rules
Rule functions create definitions. Add the returned handles to a fight’s rules
array; calling a rule function does not immediately change the current fight.
Every function below requires content.register, runs in the entrypoint, and
returns a rule handle. Referenced items/perks must exist and belong to your
mod or a declared dependency. Unknown fields are rejected.
Shared rule fields
Section titled “Shared rule fields”| Field | Default | Meaning |
|---|---|---|
id |
Required string | Stable local rule ID. |
target |
sf2.rules.ALL |
Which fighter the rule applies to: PLAYER, OPPONENT, or ALL for both, on functions that accept it. |
mode |
sf2.rules.BOTH |
Which campaign mode the rule applies in: NORMAL, ECLIPSE, or BOTH. |
rounds |
Empty integer array | Restrict the rule to listed rounds; omitted means no round filter. |
Examples assume sf2 is loaded. weapon and perk below are previously
registered or looked-up handles. Each example creates a different local rule ID.
sf2.rules.no_perks
Section titled “sf2.rules.no_perks”Disable perks matching the rule’s target and optional name filter.
Signature: sf2.rules.no_perks { id, target?, mode?, rounds?, name? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight.
Returns: A rule handle.
name is an optional string, default ""; omit it for an unfiltered no-perks rule. Common field defaults are listed above.
local rule = sf2.rules.no_perks { id = "no_perks", target = sf2.rules.OPPONENT,}sf2.rules.require_item
Section titled “sf2.rules.require_item”Require an item as a fight-entry/equipment condition.
Signature: sf2.rules.require_item { id, item, minimum_level?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight.
Returns: A rule handle.
item is required; minimum_level is a nonnegative integer defaulting to 0. This rule has no target field. Common field defaults are listed above.
local rule = sf2.rules.require_item { id = "require_blade", item = weapon, minimum_level = 1,}sf2.rules.equip_item
Section titled “sf2.rules.equip_item”Apply a specified item to the target for this fight.
Signature: sf2.rules.equip_item { id, item, minimum_level?, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight.
Returns: A rule handle.
item is required; minimum_level is a nonnegative integer defaulting to 0. Fight equipment replacement is not a permanent inventory grant. Common field defaults are listed above.
local rule = sf2.rules.equip_item { id = "equip_blade", item = weapon, target = sf2.rules.PLAYER,}sf2.rules.avatar
Section titled “sf2.rules.avatar”Override the target’s fight portrait reference.
Signature: sf2.rules.avatar { id, name, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight.
Returns: A rule handle.
name is a required existing portrait identifier string. It is not a sprite handle; use a verified portrait name from core content. Common field defaults are listed above.
local rule = sf2.rules.avatar { id = "portrait", name = "avatar_hero", target = sf2.rules.PLAYER,}avatar_hero is an existing core avatar name.
sf2.rules.name
Section titled “sf2.rules.name”Override the target’s displayed name during the fight.
Signature: sf2.rules.name { id, name, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight.
Returns: A rule handle.
name is a required string, typically a qualified localization reference. Common field defaults are listed above.
local rule = sf2.rules.name { id = "fighter_name", name = "my.mod:localization/opponent.name", target = sf2.rules.OPPONENT,}sf2.rules.no_button
Section titled “sf2.rules.no_button”Disable a named combat button through the native fight-rule system.
Signature: sf2.rules.no_button { id, name, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight.
Returns: A rule handle.
name is a required native button identifier. It is not arbitrary UI text; use an identifier verified for the intended button. Common field defaults are listed above.
local rule = sf2.rules.no_button { id = "restricted_button", name = "Punch", target = sf2.rules.PLAYER,}Supported native identifiers are "Punch", "Kick", "Ranged", "Magic", and
"RaidCharge". Active rules hide the relevant control and reject player-one
touch, keyboard and gamepad presses through their shared input path. A held
control is released when blocked and must return to neutral before a fresh press
can work after the rule ends. Availability refreshes cannot show a blocked button;
removing a rule does not make unavailable equipment usable.
This native rule controls the player’s input, not AI move selection. The recovered
runtime does not interpret its target as an opponent-input restriction; use
PLAYER for clarity. Round changes clear and reapply the active restrictions.
sf2.rules.perk
Section titled “sf2.rules.perk”Apply a perk rule to the selected target.
Signature: sf2.rules.perk { id, perk, aspect?, parameters?, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight.
Returns: A rule handle.
perk must be a perk handle, not a behavior or enchantment handle. aspect is an
optional finite number from 0 through 2147483647; omit it to keep the native
perk rule’s normal aspect/default behavior. Common field defaults are listed above.
parameters optionally sets other native parameters the perk reads, with the
same rules as warrior perk rows: up to 32 names mapped to finite numbers, and the
dedicated names such as Aspect, Chance and Frames rejected. See
warrior perk settings.
local rule = sf2.rules.perk { id = "opponent_perk", perk = perk, aspect = 100000, target = sf2.rules.OPPONENT,}local bleeding = sf2.rules.perk { id = "bleeding_hits", perk = perk, aspect = 100000, target = sf2.rules.OPPONENT, parameters = { AttributeBleeding = 0.2 },}sf2.rules.hot_ground
Section titled “sf2.rules.hot_ground”Create the native timed hot-ground rule used by challenge fights.
Signature: sf2.rules.hot_ground { id, frames, nodes, animations?, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight or encounter plan.
Returns: A rule handle.
frames is required and must be an integer from 1 through 216000. target
defaults to sf2.rules.PLAYER; only PLAYER or OPPONENT are valid for this
rule. ALL is rejected because the recovered native copy path loses the
hot-ground runtime rule type when applying it to both sides. nodes is
a required dense array of 1–32 tables. Each node requires a nonempty native node
name of at most 128 characters and axis = "X" or "Y". At least one of
min or max is required; supplied bounds are finite single-precision numbers,
and when both are present they must satisfy min < max. Duplicate name + axis
pairs are rejected. animations is optional and defaults to an empty list; when
supplied it is a dense array of at most 64 distinct nonempty native animation
names, each at most 128 characters.
local rule = sf2.rules.hot_ground { id = "burning_floor", frames = 300, nodes = { { name = "root", axis = "Y", max = 0 }, }, target = sf2.rules.PLAYER,}sf2.rules.ring_out
Section titled “sf2.rules.ring_out”Create a native ring-out boundary rule.
Signature: sf2.rules.ring_out { id, node, axis, min, max, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight or encounter plan.
Returns: A rule handle.
node is a required nonempty native node name of at most 128 characters. axis
is exactly "X" or "Y". min and max are required finite single-precision
numbers and must satisfy min < max. Shared target/mode/round filters use the
defaults above.
local rule = sf2.rules.ring_out { id = "ring_out", node = "root", axis = "X", min = -180, max = 180,}sf2.rules.regeneration
Section titled “sf2.rules.regeneration”Create the native regeneration or degeneration rule.
Signature: sf2.rules.regeneration { id, rate, frames_after_hit, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight or encounter plan.
Returns: A rule handle.
rate is a required finite number from -1 through 1. Negative values retain
their native negative behavior rather than being clamped away. frames_after_hit
is a required integer from 0 through 216000.
local rule = sf2.rules.regeneration { id = "enemy_regen", rate = 0.001, frames_after_hit = 180, target = sf2.rules.OPPONENT,}sf2.rules.no_animation
Section titled “sf2.rules.no_animation”Disable one named native animation for both fighters.
Signature: sf2.rules.no_animation { id, name, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight or encounter plan.
Returns: A rule handle.
name is a required nonempty native animation name of at most 128 characters.
This rule intentionally has no target field: the native rule applies to both
fighters. mode and rounds use the shared defaults.
local rule = sf2.rules.no_animation { id = "no_jump", name = "Jump" }sf2.rules.remove_interval
Section titled “sf2.rules.remove_interval”Remove one native animation interval class from the selected target.
Signature: sf2.rules.remove_interval { id, type, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight or encounter plan.
Returns: A rule handle.
type is required and is exactly one of Attack, Block, Invulnerable,
SelfUninterrupt, Uninterrupt, or Unstable.
local rule = sf2.rules.remove_interval { id = "no_block", type = "Block", target = sf2.rules.PLAYER,}These native rule constructors expose typed, evidence-backed challenge payloads.
They do not expose raw XML or arbitrary rule names. Use them in a fight’s rules
array or in an encounter plan’s dynamic rules replacement.
sf2.rules.recharge_magic_each_round
Section titled “sf2.rules.recharge_magic_each_round”Request normal magic recharge at round boundaries.
Signature: sf2.rules.recharge_magic_each_round { id, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight.
Returns: A rule handle.
The target needs a usable magic item for this rule to have a visible effect. Common field defaults are listed above.
local rule = sf2.rules.recharge_magic_each_round { id = "recharge", target = sf2.rules.ALL,}sf2.rules.attributes
Section titled “sf2.rules.attributes”Apply a map of native fight attribute values.
Signature: sf2.rules.attributes { id, values, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight.
Returns: A rule handle.
values is a required string-to-finite-number table. Names are native attribute identifiers; unknown names do not implement new gameplay logic. Common field defaults are listed above.
local rule = sf2.rules.attributes { id = "attributes", values = { WeaponDamage = 100 }, target = sf2.rules.OPPONENT,}sf2.rules.no_health_bar
Section titled “sf2.rules.no_health_bar”Hide the health bar of the selected fighters.
Signature: sf2.rules.no_health_bar { id, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight.
Returns: A rule handle.
This is the native “no health bar” fight rule: damage still counts, the player just cannot see how much health is left. Shared field defaults are listed above.
local blind = sf2.rules.no_health_bar { id = "hidden_health", target = sf2.rules.ALL }sf2.rules.invert_joystick
Section titled “sf2.rules.invert_joystick”Reverse the player’s left and right movement input.
Signature: sf2.rules.invert_joystick { id, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight.
Returns: A rule handle.
This is the native inverted-controls rule. It affects the controls of the player’s
fighter; set target = sf2.rules.PLAYER to make that explicit.
local mirror = sf2.rules.invert_joystick { id = "mirror_controls", target = sf2.rules.PLAYER }sf2.rules.random_area
Section titled “sf2.rules.random_area”Show the native random area: a band of the arena that appears at a random place, stays for a while, fades away and returns somewhere else.
Signature: sf2.rules.random_area { id, image, icon?, width, fade_in, frames_on, fade_out, frames_off, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight.
Returns: A rule handle.
| Field | Type | Meaning |
|---|---|---|
image |
String, required | Area texture name in the game’s Textures/fight/rules/randomarea/ folder, such as "ra_bleed". A plain name of at most 128 characters; paths are rejected. |
icon |
String | Optional indicator texture from the same folder. |
width |
Number, required | Area width in arena units, greater than 0 and at most 100,000. |
fade_in, fade_out |
Integers, required | Fade durations, 1–36,000 frames. |
frames_on, frames_off |
Integers, required | Time fully shown and fully hidden, 0–36,000 frames. |
Frames are native simulation frames (60 per second). The cycle repeats for the
whole fight. The area is a location the native rule tracks; pair it with a rule or
perk that reacts to it (the Underworld bosses combine it with perk rules in a
sf2.rules.group). It is not a custom damage zone by itself.
local area = sf2.rules.random_area { id = "bleeding_ground", image = "ra_bleed", icon = "ra_bleed_icon", width = 500, fade_in = 60, frames_on = 180, fade_out = 60, frames_off = 240,}sf2.rules.light_in_the_darkness
Section titled “sf2.rules.light_in_the_darkness”Darken the arena except for a spotlight that follows a fighter. This is a visual rule: it does not change damage, movement, or which attacks can connect.
Signature: sf2.rules.light_in_the_darkness { id, radius, shape?, target?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, before attaching the rule to a fight. The spotlight is created at round setup, follows the fighter every render frame, and is removed when the rule stops.
Returns: A rule handle.
radius is required and must be greater than 0 and at most 1. It is a fraction
of the rule’s 2,048-unit presentation square; 0.20 lights about 410 arena
units around the fighter. The recovered fights use 0.13 or 0.20.
shape is optional, from 0 (square) to 1 (circle), and defaults to 1. The
recovered fights use 1. target defaults to all, which follows the player
for this single-spotlight rule; use sf2.rules.OPPONENT to follow the enemy.
The shared mode and rounds fields are described above. A rule group can
pair the spotlight with a localized description. The radius and shape are part
of the fight graph’s save compatibility fingerprint, so changing either is a
content change for existing saves.
local light = sf2.rules.light_in_the_darkness { id = "follow_the_light", radius = 0.20, shape = 1, target = sf2.rules.PLAYER,}-- Include light in a fight's rules array.sf2.rules.group
Section titled “sf2.rules.group”Combine several rules into one native rule group with a single description.
Signature: sf2.rules.group { id, rules, description?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, after registering the rules it contains.
Returns: A rule handle.
rules is a dense array of 1–64 distinct rule handles, registered earlier by
your mod or a declared dependency. The group applies all of them together.
description is an optional localization handle shown for the group in the
fight’s rule list, instead of listing each child. sf2.rules.behavior rules
cannot be placed in a group; attach them to the fight directly. A group can hold
other groups and sf2.rules.random rules.
local label = sf2.localization.register { id = "rules.cursed_ground", language = "eng", value = "Cursed ground" }local cursed = sf2.rules.group { id = "cursed_ground", description = label, rules = { area, rule } }sf2.rules.random
Section titled “sf2.rules.random”Pick one rule at random from a list, for each round or for the whole fight.
Signature: sf2.rules.random { id, rules, refresh?, no_doubles?, mode?, rounds? }
Requires: content.register.
When: Entrypoint, after registering the rules it chooses from.
Returns: A rule handle.
rules takes the same dense array of 1–64 rule handles as sf2.rules.group,
with the same restriction on behavior rules. refresh is "each_fight" (the
default: one pick for the whole fight) or "each_round" (a new pick every
round). no_doubles defaults to false; true asks the native rule not to pick
the same child twice in a row. The game makes the random choice, so a script
cannot predict it.
local jump = sf2.rules.no_animation { id = "no_jumping", name = "Jump" }local twist = sf2.rules.random { id = "round_twist", refresh = "each_round", no_doubles = true, rules = { jump, blind },}sf2.rules.behavior
Section titled “sf2.rules.behavior”Attach executable Lua behavior directly to a fight, without creating a perk or requiring an equipped item.
Signature: sf2.rules.behavior { id, behavior, parameters?, target?, mode?, rounds?, controls_outcome?, every_fight? }
Returns: A rule handle; put it in sf2.fights.register { rules = { rule } },
or set every_fight = true to run it in every fight.
When: Register in the entrypoint. Attached handlers run only at the supported
combat callback boundaries of the selected fight, or of every offline fight for an
every_fight rule.
Requires: content.register. Each fighter operation still requires its own
combat capability, declared by the mod that owns the behavior.
behavior is a handle returned by sf2.behaviors.register. parameters is a
map matching that behavior’s schema; required values and types are validated at
registration. target, mode, and rounds use the shared defaults above.
Unknown fields, missing behaviors, undeclared dependencies, and saved-lifetime
behaviors are rejected. The behavior must use fight or round state (or no
state); use sf2.state explicitly for longer-lived mod progression.
controls_outcome is a boolean defaulting to false. Setting it to true
requires combat.round_outcome at registration and declares this rule as the
exclusive round controller in its mode/round scope. Its behavior can request
fighter:end_round("win") or "loss" during active simulation callbacks.
Different targets do not divide authority; overlapping controllers conflict.
See custom round objectives for timing, precedence and examples.
every_fight is a boolean defaulting to false. Setting it to true runs the
rule in every offline fight while your mod is enabled: campaign and event
fights, other mods’ fights and fights without any rules of their own. You do not
add the handle to a fight’s rules, and listing it there as well still runs it
once. In each fight, every-fight rules run after that fight’s own rules, in mod
load and registration order. target, mode and rounds still filter it, so
target = sf2.rules.OPPONENT runs only on the computer opponent’s side. It cannot
be combined with controls_outcome = true; a rule that runs everywhere does not
decide round results. Like every rule, it does not run in Local or online Versus,
in title-screen sparring or in the dojo.
-- Log the start of every round, in every fight.local announcer = sf2.behaviors.register { id = "announcer", on_round_begin = function(_, fighter, event) sf2.log.info(fighter.side .. " starts round " .. tostring(event.round)) end,}sf2.rules.behavior { id = "announcer", behavior = announcer, every_fight = true }Use it for gameplay that should apply wherever the player fights, such as a new
mechanic or an opponent habit. Keep its callbacks cheap: on_tick then runs in
every fight. To limit it to some encounters, attach an ordinary rule to those
fights instead, or return early in the callback.
local guard = sf2.behaviors.register { id = "guard", parameters = { every = sf2.behaviors.INTEGER }, state = { lifetime = "round", fields = { hits = { type = sf2.behaviors.INTEGER, default = 0 } }, }, on_damage_resolving = function(self, fighter, event) if event.damage <= 0 then return end self.state.hits = self.state.hits + 1 if self.state.hits % self.params.every ~= 0 then fighter:scale_incoming_damage(0) end end,}local rule = sf2.rules.behavior { id = "third_strike", behavior = guard, parameters = { every = 3 }, target = sf2.rules.OPPONENT,}-- Add rules = { rule } to your fight definition.The example needs combat.modify_hit. See the complete
programmable-rules guide.
State is isolated per attached rule and fighter, even when two rules reuse one
behavior. An ALL rule gets separate player and opponent state. Fight state
survives rounds; round state resets on the first callback in each new round.
There is no mid-fight save/resume contract. Rule state never enters equipment or
profile XML. The rule ID is available as fighter.rule_id, with
fighter.source == "rule".
Each side runs rules in the fight’s declared rules order, before that side’s
perk/enchantment callbacks. A duplicate rule handle runs once. These rules are
independent of no_perks. Filters are checked for every callback: a rule limited
to round two does not receive on_fight_begin in round one. It receives only the
callbacks that occur while its filters match. No omitted callback is synthesized.
Handlers share the existing execution budget, error isolation, and expiring fighter handles. One rule’s failure does not stop subsequent rules; already performed gameplay operations are not rolled back. Nested combat notifications from an operation inside a handler are suppressed, preventing recursive loops. Shield keys are isolated by rule ID, so two rules reusing one behavior do not replace each other’s shield on the same fighter.
Behavior rules support simulation ticks and can update owned HUDs through the documented combat and UI callbacks. Custom victory conditions, general animation control and additional simultaneous fighters remain outside this contract. The available operations remain the documented fighter methods. Rules can be attached to new fights or appended to or replace the rules of existing encounters through fight patches.
Attacker-side scaling is available through on_damage_dealing and
fighter:scale_outgoing_damage with combat.modify_outgoing_hit.
For conditional action restrictions, attach a player-targeted behavior and call
fighter:set_control_blocked from
on_round_begin after reading your mod’s condition. This requires combat.effects.
The claim belongs to that rule instance; releasing it cannot undo native
no_button rules or another instance’s restrictions. It does not grant equipment,
charges or an ability button, and must be reapplied each round.