Skip to content

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.

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.

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

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

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

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.

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

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.

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

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

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

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

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

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.

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

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

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 }

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 }

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

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.

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

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

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.