Skip to content

Visuals and mod settings

Mods can change how fights look in two ways:

  • Building blocks (sf2.fx) let you design your own effects: particle emitters with your own sprites or built-in shapes (including bursts at the point of each hit), overlays placed at a chosen depth or on the screen, trails on weapons or any two fighter nodes, contact shadows, blade glints, lights carried by weapons and magic, fixed in the stage or flashed by hits, floor stains, and screen grading with film effects (split toning, god rays, heat haze, chromatic aberration, cinema bars, camera shake) that can also fire on hits, knockouts, round starts, at random intervals, or when your Lua says so. Any number of mods can add these, and they stack.
  • Presets (sf2.visuals) are nine ready-made effects: deeper background parallax, weapon trails, depth haze, a rim light on the fighters, bloom, ambient particles, a short impact effect on heavy hits, a restyled fight HUD and a close-up on the winner after a knockout. You only tune their numbers, and each preset has one owner.

Everything is off until a mod asks for it. The engine does the drawing; a mod describes effects with typed tables when it loads. A combat callback can fire one of its own screen grades with sf2.fx.play, and a mod can change its own effects while fights run: sf2.fx.on_frame calls your Lua a few times a second with a read-only view of the fight, and sf2.fx.set changes an effect’s numbers or switches it off.

Mod settings are simple on/off switches that appear under Options > Mod settings, grouped under a header with your mod’s name. Link an effect to a switch and players can turn it off without disabling your mod.

Things to know first:

  • Capabilities. sf2.fx.* and sf2.visuals.* require presentation.visuals. sf2.settings.toggle requires ui.settings. sf2.settings.get needs no extra capability.
  • When. Every function except sf2.settings.get is a registration: call it while your mod loads (at the top level of your scripts), not from callbacks.
  • Stacking. A mod may add up to 128 sf2.fx effects, and effects from different mods all run together. Each preset has one owner: if two enabled mods configure the same preset, the second fails to load with a conflict error.
  • Presentation only. Effects and setting values are never written to the player’s profile and are not part of the content fingerprint, so adding or removing a visuals mod never affects saves. Setting values are stored per installation.
  • Where they apply. Fighter effects (trails, glints, node particles, the rim light) can also run on fighter previews in menus such as the shop and profile; for sf2.fx effects set scenes = "everywhere". The shop renders the selected dojo’s animated layers, location overlays, ambient particles and depth haze, and supports continuous screen grades and bloom on its world camera. Location match/exclude filters use that dojo’s name. Screen-space shop controls remain outside the world-camera effects. Background parallax responds to the moving fight camera; the shop uses a fixed view. Hit effects, contact shadows, stains and combat-triggered grades still require their fight events.
  • Triggers. Most effects run continuously. Hit particles, stains and screen grades with a trigger fire once per matching hit instead: hit is any unblocked hit, critical a critical hit, block a blocked hit (particles only) and ko the hit that empties a fighter’s health. Screen grades and screen overlays also accept round, which fires as each round begins, and outro and outro_land, which fire as the knockout close-up begins and as its zoom arrives. A screen grade with trigger = "interval" fires by itself every few seconds (lightning, for example), and one with trigger = "script" fires only when your Lua calls sf2.fx.play. Hit lights flash on hit, critical, block or ko.
  • Motion triggers. Contact particles and screen grades can also fire from how a fighter moves: land when a fall ends on the floor, knockdown when the body hits the floor, slide repeatedly while the feet skid fast along the floor (contact particles only) and wall when a fighter plays a wall-hit recoil. See contact particles.
  • Accessibility. The impact effect is also scaled by the player’s Critical hit shake accessibility slider on critical hits and by the Hit shake slider on other hits.
  • Defaults. Every numeric field is optional. Omitted fields use the defaults shown. Out-of-range values and unknown fields raise an error.

Complete examples ship with Eclipse. Final Blow (Mods/final-blow) is a kill cam built only from screen grades with zoom. Shadow Clones (Mods/shadow-clones) and Umbra, the Last Eclipse (Mods/umbra) fire script-triggered grades from Lua with sf2.fx.play. See Examples. Custom FX Showcase (Mods/example.custom-fx) builds weapon sparks, kick trails, ground fog, a dojo light wash and a screen grade from building blocks. Chiaroscuro - Cinematic Visuals (Mods/chiaroscuro) builds weapon trails, light shafts with drifting dust, a slow-motion knockout fade, film halation and grain, floor stains, and light from weapons and magic from building blocks. It uses presets for background depth, depth haze and the rim light, and gives each effect its own switch. Chiaroscuro 2 (Mods/chiaroscuro2) shows off nearly everything on this page: about a hundred effects behind 30 switches, including weather and fog chosen by stage, inked weapon trails with crisp edges and thrust streaks, god rays, heat haze, lightning storms, stage and hit lights that shade the fighters, a knockout kill cam, a fight that reacts through sf2.fx.on_frame, and the lacquer HUD. It owns the same background depth, depth haze and rim light presets as Chiaroscuro, so enable one or the other.

Every sf2.fx function takes one table and returns the effect’s qualified name, <mod-id>.<id>. These fields work on every building block:

Field Required/default Meaning
id Required 1–64 lowercase ASCII letters, digits, _ or -, unique among your mod’s effects.
setting nil A handle from sf2.settings.toggle. The effect runs only while the switch is on.
tags Every stage Up to 16 stage tags. The effect runs only on stages with one of them, such as { "fire" } or { "day", "dusk" }. This is the usual way to pick stages.
exclude_tags None Up to 16 stage tags. The effect never runs on a stage with one of them, even if tags also fits.
match Every location Up to 32 lowercase words found in the stage’s name. Names are split on _, -, :, /, spaces and digits, so new_year_24_china_dojo gives new, year, china and dojo. A name says which stage it is, not what it looks like, so prefer tags; use match to pick out one particular stage.
exclude None Up to 64 lowercase words. The effect never runs in a location whose name contains one of them.

When an effect gives more than one of these, a stage has to pass all of them: one of the tags (when given), one of the match words (when given), and none of the exclusions. Every one of the game’s stages is tagged; see Stage tags for the list and for tagging your own stages.

-- Embers over every burning stage, and fireflies among trees, but not in the snow.
sf2.fx.particles { id = "embers", tags = { "fire" }, placement = "front", blend = "additive",
shape = "glow", color = "#FFB050", count = 60 }
sf2.fx.particles { id = "fireflies", tags = { "foliage" }, exclude_tags = { "snow" },
placement = "front", blend = "additive", shape = "glow", color = "#D8FF8AE0", count = 30 }

Colours are #RRGGBB or #RRGGBBAA strings. blend = "additive" makes an effect glow (it adds light); the default "alpha" paints over what is behind. Distances and speeds are in location units, where a fighter is roughly 300 units tall; speeds are units per second.

Fighter nodes. Trails and node particles name points on a fighter. Useful names, taken from the game’s own move data:

Node Where
NPivot Centre of the body
NTop, NNeck, NChest, NStomach Head top, neck, chest, stomach
NWrist_1, NWrist_2, NKnuckles_1, NKnuckles_2 Wrists and fists (_1 main side, _2 other side)
NKnee_1, NKnee_2, NHeel_1, NHeel_2, NToe_2 Legs and feet
Weapon-Node2_1, Weapon-Node2_2 Near the main-hand and off-hand weapon
Magic-Node2_1, Ranged-Node2_1 Magic and ranged attachment points

A node a fighter does not have is skipped for that fighter.

Signature: sf2.fx.particles { id = "...", placement = "behind", color = "#RRGGBBAA", count = 100, ... }

Returns: The effect’s name, <mod-id>.<id>.

When: During loading.

Requires: presentation.visuals. A sprite must be a handle from sf2.assets.sprite.

Adds a particle emitter. With placement = "background", "behind" or "front" the particles fill the location; with placement = "node" (or just node = "...") they stream from one node of each selected fighter, and keep flowing behind it as it moves. With placement = "hit" nothing plays until a fighter is struck: then count particles burst from the contact point, fly outward at speed_min–speed_max, fall with gravity and fade out. For hit particles, fighters selects the fighter being struck. With placement = "contact" the particles burst where a fighter meets the floor or a wall; see contact particles.

Field Default Meaning
placement "behind" ("node" when node is set) background: on the background layer nearest depth. behind: just behind the fighters. front: in front of the fighters. node: at a fighter node. hit: a burst at each matching hit. contact: a burst at each matching landing, knockdown, skid or wall impact.
node — Required for node placement. See the node table.
fighters "both" Node placement only: both, player or opponent. In menu previews the fighter counts as the player.
scenes "fights" Node placement only: everywhere also runs on menu previews.
sprite Soft dot Your particle image.
color, end_color White, nil Start colour, and the colour each particle fades to by the end of its life.
blend "alpha" alpha or additive.
count 100 1–1000 particles alive at once.
lifetime_min, lifetime_max 6, 10 0.05–60 seconds; min must not exceed max.
size_min, size_max 3, 6 0.1–400 units.
velocity_x_min, velocity_x_max -10, 10 Horizontal speed range, −2000 to 2000; positive is right.
velocity_y_min, velocity_y_max -10, 10 Vertical speed range; positive is up.
noise 5 0–500 random wandering.
spin 0 0–1: random start rotation, 1 is a full turn.
depth 0.3 0–1 for background placement: 0 nearest the fighters, 1 the farthest layer.
area_width, area_height 1.1, 1.1 0–2: emission area as a fraction of the location size.
radius 20 0–500: emission radius around a node, hit or contact point.
x, y 0, 0 Location placements: centre of the emission area, as an offset from the location centre in location units; up is positive. A stage is about 2300 × 512 units, with the floor about 80 units above its bottom edge.
trigger "hit" for hit placement, "land" for contact placement Hit placement: hit, critical, block or ko. Contact placement: land, knockdown, slide or wall. Other placements run continuously and do not accept a trigger.
speed_min, speed_max 0, 0 0–5000: outward burst speed of hit and contact particles; min must not exceed max. The velocity_* ranges are added on top.
gravity 0 −5000 to 5000: downward pull on hit and contact particles in units per second squared; negative values rise.
shape "dot" Built-in art when there is no sprite: dot (a soft dot), glow, ring, star (a four-pointed glint), smoke (an uneven puff) or shard (a thin splinter). A particle effect takes a sprite or a shape, not both.
stretch 0 0–4: draws each particle stretched along its velocity, for sparks and rain.
grow 1 0–8: each particle’s size at the end of its life as a multiple of its start size. 3 makes smoke billow and rings expand into shockwaves; below 1 shrinks.
interval 0 0–30 seconds, location and node particles only: instead of a steady stream, count particles burst out every interval seconds (breath, puffs). Each emitter starts at its own point in the cycle.
facing 0 Node particles only. 1 makes velocity_x point the way the fighter faces (positive is forward), so breath leaves the mouth whichever way the fighter turns. 0 keeps it pointing right on the screen.

Location particles also appear in the dojo and the shop’s selected-dojo backdrop. Node particles appear on menu previews with scenes = "everywhere"; hit/contact particles require fight events. Every particle fades in and out.

sf2.fx.particles {
id = "blade_sparks", node = "Weapon-Node2_1", scenes = "everywhere",
blend = "additive", color = "#FFC060", end_color = "#FF400000",
count = 40, lifetime_min = 0.3, lifetime_max = 0.7,
velocity_y_min = 10, velocity_y_max = 60, radius = 6,
}
-- Sparks thrown from the contact point of every unblocked hit.
sf2.fx.particles {
id = "hit_sparks", placement = "hit", trigger = "hit", blend = "additive",
color = "#FFD27A", end_color = "#FF5A1E00", count = 10,
lifetime_min = 0.18, lifetime_max = 0.35, size_min = 2, size_max = 5,
speed_min = 250, speed_max = 700, gravity = 900, radius = 4,
}
-- A shockwave: one ring at the contact point of each critical hit, expanding
-- to six times its size as it fades.
sf2.fx.particles {
id = "critical_ring", placement = "hit", trigger = "critical", blend = "additive",
shape = "ring", color = "#FFE8C8B0", end_color = "#FFE8C800", count = 1, radius = 0,
lifetime_min = 0.35, lifetime_max = 0.35, size_min = 44, size_max = 44, grow = 6,
}
-- Breath: a small puff from each fighter's neck every 2.2 seconds.
sf2.fx.particles {
id = "breath", node = "NNeck", match = { "snowy", "ice" }, shape = "smoke",
color = "#EEF4FA55", end_color = "#EEF4FA00", count = 4, radius = 5,
lifetime_min = 0.9, lifetime_max = 1.5, size_min = 5, size_max = 10, grow = 3.2,
velocity_y_min = 12, velocity_y_max = 35, interval = 2.2,
}

Contact particles (placement = "contact") are bursts that play where a fighter meets the arena: dust from the floor, or grit knocked off a wall. Nothing plays until the fighter’s movement matches the trigger, then count particles burst at the contact point, just in front of the fighter, and behave like hit particles (speed_*, gravity, velocity_*, radius, fading out).

Trigger Fires when Where
land (default) A fall from more than about 45 units above the floor ends with the feet on the floor. Small hops do not count. On the floor under the lowest part of the body.
knockdown The body’s centre drops below about 60 units above the floor while falling fast. At most once every 0.6 seconds per fighter. On the floor under the body’s centre.
slide The feet are on the floor while the body moves along it faster than about 420 units per second: skids, pushback and dashes. Repeats about every 0.07 seconds, so keep count small. On the floor under the feet.
wall The fighter starts a wall-hit recoil: the WallHit or WallHitFall move the game plays when a fighter is knocked into the arena wall. Walking or being pushed against a wall does not count. At the edge of the body facing the nearer wall.

A standing fighter is about 300 units tall, for scale. fighters selects the fighter that is moving. Contact particles run in fights; with scenes = "everywhere" landings, knockdowns and skids also play on menu previews, but wall needs a fight’s arena walls.

The triggers are read from the fighter’s animated pose, not from the fight’s rules, so they only add presentation: they never change the fight. They use game time, so nothing fires while the game is paused. A fighter placed at a new position (such as at the start of a round) does not count as sliding.

-- Dust from heavy landings, and debris from wall impacts.
sf2.fx.particles {
id = "landing_dust", placement = "contact", trigger = "land",
color = "#C9B89A66", end_color = "#C9B89A00", count = 12, radius = 16,
lifetime_min = 0.45, lifetime_max = 0.9, size_min = 16, size_max = 34,
speed_min = 10, speed_max = 40, velocity_x_min = -140, velocity_x_max = 140,
velocity_y_min = 15, velocity_y_max = 60, noise = 15,
}
sf2.fx.particles {
id = "wall_debris", placement = "contact", trigger = "wall",
color = "#5E4E3EFF", end_color = "#5E4E3E00", count = 16, radius = 20, spin = 1,
lifetime_min = 0.5, lifetime_max = 0.9, size_min = 3, size_max = 8,
speed_min = 200, speed_max = 480, gravity = 900,
}

Signature: sf2.fx.overlay { id = "...", placement = "background", depth = 0.3, sprite = handle, color = "#RRGGBB", alpha = 1, ... }

Returns: The effect’s name, <mod-id>.<id>.

When: During loading.

Requires: presentation.visuals.

Places an image or a solid colour in the location. In the background it moves with the background layer nearest depth, so it shares that layer’s parallax; in front it moves with the fighters’ layer and sits in front of them. With placement = "screen" it sits on the screen itself, above the fight and below the HUD, and can appear on a trigger; see screen overlays.

Field Default Meaning
placement "background" background, front or screen.
depth 0.3 0–1 for background placement: 0 nearest the fighters, 1 the farthest layer.
sprite Solid colour Your image. Without one the overlay uses shape.
shape "rect" Built-in art used when there is no sprite: rect is a flat colour, shaft a soft vertical light beam that is brightest at the top, glow a soft round light and band a soft horizontal strip (fog banks, light pooled on the floor). A shaft or glow without a size is a quarter of the stage wide (a shaft is also the full stage height); a band without a size is 1.4 stage widths by a quarter of the stage height. Screen overlays add slash and wash.
color White Multiplies the image, or is the flat colour.
alpha 1 0–1 extra opacity.
blend "alpha" alpha or additive.
x, y 0, 0 Offset from the location centre; up is positive.
width, height 0 Size in location units. 0 for either covers the whole view (see shape for beams and glows).
angle 0 −180 to 180 degrees; positive turns counter-clockwise. Tilt a shaft to slant the light.
flicker 0 0–1: how far the opacity wavers, like a flame.
flicker_speed 6 0.1–30: how fast the flicker moves.
sway, sway_speed 0, 0.1 0–2000 units the overlay drifts to either side, and 0.01–10 sways per second.
pulse, pulse_speed 0, 0.5 0–1 slow breathing of its size (1 is ±25%), and 0.01–10 breaths per second.
trigger, hold, duration, reveal, slide — Screen overlays only; see below.
sf2.fx.overlay {
id = "dojo_glow", match = { "dojo" }, placement = "background", depth = 0.8,
color = "#FFB060", blend = "additive", alpha = 0.18,
}
-- A fog bank drifting slowly in front of the fighters' feet.
sf2.fx.overlay {
id = "fog_bank", match = { "swamp" }, placement = "front", shape = "band",
color = "#D8E2EA", y = -215, width = 3600, height = 150, alpha = 0.16,
sway = 160, sway_speed = 0.025, pulse = 0.2, pulse_speed = 0.04,
}

A screen overlay (placement = "screen") is drawn on its own canvas above the fight and below the HUD. Its position and size are in HUD units: the screen is always 1536 units tall, and its width follows the aspect ratio (2731 at 16:9). x and y offset it from the centre of the screen, angle turns it, and a width or height of 0 fits the screen; the built-in slash (a long dry-brush ink stroke) is 1.1 screens wide and 0.24 tall by default, and wash (a screen-filling ink wash with a ragged edge on its right) is 1.6 screens by 1.25. Screen overlays are not graded by sf2.fx.screen, so they keep their colours through a black-and-white knockout.

With a trigger (hit, critical, ko, land, knockdown, wall or round) the overlay is hidden until the trigger fires, then shows at full strength for hold seconds and leaves over duration seconds, in real time.

Field Default Meaning
trigger "always" When it appears.
hold 0 0–10 seconds at full strength.
duration 0.6 0.02–10 seconds to leave.
reveal 0 0–5 seconds to draw it on from its left end.
slide 0 0–1: 1 makes it sweep off to the left instead of fading; between 0 and 1 mixes the two.
-- An ink stroke tearing across the screen on the knockout.
sf2.fx.overlay {
id = "knockout_slash", placement = "screen", trigger = "ko", shape = "slash",
color = "#0A0606", alpha = 0.92, angle = -10, y = 40,
reveal = 0.12, hold = 0.55, duration = 0.9,
}
-- An ink wash that covers each round's reset, then sweeps away.
sf2.fx.overlay {
id = "round_wipe", placement = "screen", trigger = "round", shape = "wash",
color = "#0A0707", x = 120, hold = 0.15, duration = 0.55, slide = 1,
}

Signature: sf2.fx.trail { id = "...", weapon = true, color = "#RRGGBB", ... } or sf2.fx.trail { id = "...", nodes = { "NKnee_2", "NHeel_2" }, ... }

Returns: The effect’s name, <mod-id>.<id>.

When: During loading.

Requires: presentation.visuals. Give exactly one of weapon = true or two nodes.

Draws a fading ribbon swept by a line on the fighter. weapon = true follows the weapon’s blade (each hand separately). nodes gives the inner end first and the moving end second. The ribbon only appears while the moving end is faster than min_speed, at full strength from full_speed.

Every trail, including the weapon trail preset, behaves the same way:

  • Fight time. lifetime and speeds are measured in fight time. In slow motion a trail lasts longer and still appears on slowed swings. It holds still during hit-stop and pause, then continues. Menu previews use real time.
  • Shape. Between frames the line is swept as an arc around its inner end, so fast swings look round at any frame rate. Older parts of the ribbon narrow toward the moving end, giving a crescent, and fade out smoothly.
  • Overlaps. Where a swing reverses and the ribbon folds over itself, the overlap is drawn once, so it does not turn darker or brighter. The same holds between trails on one fighter with the same blend: where they overlap, the trail registered first is drawn and the others are hidden. See layering trails.
  • Jumps. A fighter that teleports, resets for a new round or snaps to a new facing starts a fresh ribbon instead of drawing a streak across the screen.
Field Default Meaning
weapon false Follow the weapon blade.
nodes — Two node names. See the node table.
weapons — Up to 32 lowercase words. Needs weapon = true. The trail then runs only while the fighter has a weapon equipped whose item name or subtype contains one of the words, for example { "staff", "blunt" }.
fighters "both" both, player or opponent.
scenes "fights" everywhere also runs on menu previews.
color Fighter colour Ribbon colour. Without it the trail follows the fighter’s colour, including perk tints.
end_color — #RRGGBB or #RRGGBBAA. The trail’s colour shifts from color to this colour as it ages.
blend "alpha" alpha or additive.
lifetime 0.11 0.02–1 seconds.
min_speed, full_speed 900, 2600 Speeds of the moving end; full_speed must be greater.
alpha 0.55 0–1 at the moving end.
start_alpha 0.35 0–1, fraction of alpha at the inner end.

The fields below are optional. At their defaults the trail looks exactly like the plain ribbon described above, so you can add only the ones you need.

Field Default Range Meaning
falloff 1 0.25–4 How the ribbon’s opacity rises from the inner end (start_alpha) to the moving end. 1 is a straight ramp. Higher values keep the ribbon faint until close to the moving end, so a swing reads as a cut instead of a smoky fan.
ribbon 1 0–1 Multiplies the ribbon’s opacity. Use 0 for a trail that draws only an edge or streaks.
edge 0 0–1 Opacity of a crisp line along the path of the moving end. 0 turns it off.
edge_width 0.08 0.01–0.5 Depth of the edge line, as a fraction of the line’s length.
edge_color Trail colour — #RRGGBB or #RRGGBBAA colour of the edge line.
thrust 0 0–200 Width of a soft streak drawn where the moving end travels along the line, as in a stab or lunge. A plain ribbon has no width there. Its opacity follows alpha. 0 turns it off.
hit_flash 0 0–2 Opacity added to the whole trail when this fighter lands an unblocked hit with its own body or weapon (not a projectile). It fades over about a tenth of a second. It also works with alpha = 0, for a trail that appears only on hits.
hit_linger 0 0–1 Extra seconds the newest part of the trail (the last 0.1 seconds) stays when such a hit lands.

The edges, ribbons and streaks of all trails on a fighter are drawn in that order. Where they overlap, the earlier one wins, so an edge line stays crisp on top of a ribbon.

All of these fields are numbers, except the colours and weapons. You can change the numbers while a fight runs with sf2.fx.set.

sf2.fx.trail {
id = "kick_trail", nodes = { "NKnee_2", "NHeel_2" }, fighters = "player",
color = "#6FB8FF", blend = "additive", lifetime = 0.15, min_speed = 700,
}
-- A blade trail that gathers toward the tip, cools to red and leaves a crisp edge.
sf2.fx.trail {
id = "katana_cut", weapon = true, weapons = { "katana" },
falloff = 2.2, end_color = "#6A1A10", edge = 0.85, edge_width = 0.07,
}

Because each trail can have its own setting, you can build a look from several trails and give each part its own switch under Options > Mod settings. Set ribbon = 0 on the layers that should add only an edge or streaks:

local sf2 = require("sf2")
local trails = sf2.settings.toggle { id = "trails", label = "Weapon trails", default = true }
local edges = sf2.settings.toggle { id = "edges", label = "Blade edges", default = true }
sf2.fx.trail { id = "blade", setting = trails, weapon = true, falloff = 2 }
sf2.fx.trail { id = "blade_edge", setting = edges, weapon = true, ribbon = 0, edge = 0.85, edge_width = 0.07 }

Signature: sf2.fx.screen { id = "...", trigger = "always", saturation = 1, contrast = 1, brightness = 0, tint = "#RRGGBB", tint_strength = 0, vignette = 0, ... }

Returns: The effect’s name, <mod-id>.<id>.

When: During loading.

Requires: presentation.visuals.

Grades the whole fight picture, in the locations its match and exclude allow. When several grades are active they combine: saturation and contrast multiply, brightness adds, tints layer in load order, halation adds, and the strongest vignette, grain, accent, chromatic aberration, radial blur, cinema bars, sharpening and camera shake win. For split toning, god rays and distortion the strongest grade wins together with its colours and settings.

With a trigger other than always, the grade is off until a matching hit or, for land, knockdown and wall, a matching movement by either fighter (see contact particles for when they fire). round fires as each round begins. outro fires as the knockout close-up begins and outro_land as its zoom arrives; both end when the next round starts, so a long hold keeps an outro grade on for the whole close-up. With trigger = "interval" the grade fires by itself every interval seconds, varied by up to interval_jitter of that either way; the first firing comes one interval into the fight, and paused games and title-screen sparring wait. With trigger = "script", it waits for your Lua to call sf2.fx.play. It then applies fully for hold seconds and eases smoothly back to nothing over duration seconds. Each new matching hit or movement restarts it. Hold and duration are measured in real time, so they are not stretched by slow motion.

A triggered screen grade responds to gameplay fights. Background title-screen sparring does not trigger screen grades, their sounds, slow motion, or audio muffle; menu music and UI sounds continue normally. After eight seconds without input on the Home screen, the menu fades away to showcase the CPU fight and native fight sounds fade in at the player’s sound volume. Keyboard, mouse, or controller input restores the menu and silences those fight sounds; the waking input does not activate a menu choice. This does not enable mod screen-grade triggers, their sound effects, or combat callbacks in title-screen sparring.

A triggered grade can also carry sound. sound plays once each time the trigger fires, at sound_volume times the player’s sound volume (silent when sound is muted). Give it an array to pick one sound at random each time, and give each choice its own volume to even out sounds of different loudness. Sounds start on the frame the trigger fires, since every effect sound is loaded when a fight starts. They play in real time, so slow motion does not stretch them. Trim silence from the start of your files, because it delays the sound. muffle puts a low-pass filter over every other game sound while the grade is active. The filter follows the grade’s strength, so the fight sounds muffled at full strength and clears again as the grade fades. Effect sounds skip the filter, which keeps a knockout sting clear while the fight goes dull underneath. When several grades muffle at once, the strongest wins.

Field Default Meaning
saturation 1 0–2. 0 is black and white.
contrast 1 0–2.
brightness 0 −1 to 1.
tint, tint_strength nil, 0 A colour multiplied into the picture, and how strongly (0–1).
vignette 0 0–1 darkening toward the edges.
vignette_x, vignette_y 0, 0 −1 to 1: moves the vignette’s centre; -0.35, 0.25 centres it up and to the left, so the lower right is darkest.
trigger "always" always, hit, critical, ko, land, knockdown, wall, round, outro, outro_land, interval or script. slide is not accepted because it repeats while a fighter skids. script fires only from sf2.fx.play. Only screen grades accept script and interval.
interval, interval_jitter 8, 0.5 trigger = "interval" only: 0.5–120 seconds between firings, and 0–1 random variation of that interval either way.
duration 0.25 0.02–10 seconds for a triggered grade to fade out.
hold 0 0–10 seconds a triggered grade stays at full strength first.
time_scale 1 0.05–1, triggered grades only: game speed while the grade is at full strength. Speed returns to normal as the grade fades, so slow motion lasts exactly as long as the grade. The slowest active grade wins. It never overrides a pause or another speed change already in effect.
grain 0 0–1 fine moving film grain.
halation, halation_threshold 0, 0.75 0–2 glow strength around bright areas, and the brightness (0–2) where the glow starts.
halation_color "#FF9E6B" Colour of the halation glow.
flicker, flicker_speed 0, 6 0–1 irregular brightness wobble like firelight, and its speed (0.1–30).
accent, accent_strength, accent_width nil, 0, 0.08 A colour whose hue keeps its saturation when the grade removes colour, how strongly (0–1), and how close a hue must be (0.01–0.5 of the colour wheel).
sound nil Triggered grades only. A native sound name such as "snd_time_shift" (letters, digits, _, - and /), an audio handle from sf2.assets.audio for a sound your mod ships, a table { sound = <name or handle>, volume = 0..1 }, or an array of 1–16 of these to pick one from at random. A choice’s volume (default 1) multiplies sound_volume. A native name that a mod has replaced plays the replacement. A missing sound is skipped with one warning in the log.
sound_volume 1 0–1 volume of sound, multiplied by the player’s sound volume.
muffle 0 0–1 low-pass filter over the other game sounds while the grade is active. 1 leaves only a dull thud. Works on always-on grades too, for example an underwater location.
zoom 1 1–3, triggered grades only: the camera pushes in by this factor while the grade is active. See camera push-in.
zoom_offset_y 0 −400 to 400, triggered grades only: camera pan during the push-in, like the camera API’s offset_y. Positive values raise the framing so heads stay in view; negative values lower it.
shake, shake_speed 0, 22 Triggered grades only: 0–1 camera shake (1 is about 26 units), and 1–60 for how fast it moves. The shake stays inside the arena art.
letterbox 0 0–0.3: height of each cinema bar as a fraction of the screen.
chromatic 0 0–1 colour fringing that grows toward the edges of the frame.
radial_blur 0 0–1 zoom blur toward the centre (it shares the impact preset’s blur; the stronger wins).
split_tone, shadow_tint, highlight_tint 0, cool teal, warm amber 0–1 split toning: shadows take shadow_tint and highlights highlight_tint, as overlay colours (mid grey changes nothing).
god_rays 0 0–2: bright areas stream light toward a point on the screen.
god_rays_x, god_rays_y -0.6, 0.8 −1 to 1: where the light comes from, from the left/bottom (−1) to the right/top (1).
god_rays_follow 0 0–1: how far the rays turn toward the stage’s own light, read from its art (the sun, the moon, a glowing window). At 1 they stream from that light, and take its colour unless god_rays_color is given. god_rays_x and god_rays_y are used where a stage’s light cannot be read.
god_rays_threshold, god_rays_length, god_rays_color 0.7, 0.6, warm white 0–2 brightness at which areas start to stream, 0.1–1 how far the rays reach, and their colour.
distortion, distortion_speed, distortion_height 0, 3, 1 0–1 heat-haze or underwater wobble, 0.1–20 its speed, and 0.05–1 how much of the screen from the bottom it covers: 1 is the whole frame evenly, 0.45 hot air rising off the floor.
sharpen 0 0–1: crisps edges, useful on upscaled art.
monochrome 0 0–1: turns the picture black and white after every other colour change (tints, split toning, kept accent colours), so no other grade can colour it again. With several grades the strongest wins.
light_wrap, light_wrap_width 0, 8 0–1: bright picture around a fighter spills over the edge of the silhouette, as it does through a real lens, so a fighter against a bright sky or a fire looks lit by it rather than cut out. The inside of the body stays dark. light_wrap_width (1–32) is how far it reaches into the silhouette, in pixels of a 1080-line picture. With several grades the strongest wins. It costs one copy of the screen each frame while it is on.
sf2.fx.screen {
id = "cinema_grade", saturation = 0.9, contrast = 1.08,
tint = "#E8F0FF", tint_strength = 0.3, vignette = 0.35,
}
-- On the knockout hit, slow down and drain every colour but red, hold, then return.
sf2.fx.screen {
id = "knockout_fade", trigger = "ko", saturation = 0, contrast = 1.15,
accent = "#B01010", accent_strength = 1, accent_width = 0.06,
hold = 1.2, duration = 1.6, time_scale = 0.3, -- slow motion while grey
sound = "snd_time_shift", sound_volume = 0.8, -- a clear swell over...
muffle = 0.75, -- ...a muffled fight
}
-- A kill cam: on the knockout hit the camera slams in on the fallen fighter
-- while time nearly stops, then drifts back out as the speed returns.
sf2.fx.screen {
id = "kill_cam", trigger = "ko", zoom = 1.65, zoom_offset_y = -70,
hold = 0.7, duration = 2.2, time_scale = 0.12, vignette = 0.5,
}
-- A film look: teal shadows, warm highlights, a little halation and grain.
sf2.fx.screen {
id = "film", split_tone = 0.35, shadow_tint = "#3C6E8C", highlight_tint = "#E0A070",
halation = 0.15, halation_threshold = 0.8, grain = 0.18, vignette = 0.3, sharpen = 0.15,
}
-- God rays streaming from each daylit stage's own light, and hot air over lava.
sf2.fx.screen { id = "sun_rays", tags = { "day" }, god_rays = 0.3, god_rays_follow = 1 }
sf2.fx.screen { id = "heat_haze", tags = { "fire" }, distortion = 0.35, distortion_speed = 4, distortion_height = 0.45 }
-- Lightning with thunder, every nine seconds or so over rainy stages.
sf2.fx.screen {
id = "lightning", match = { "flooded", "swamp" }, trigger = "interval", interval = 9, interval_jitter = 0.6,
brightness = 0.55, contrast = 1.3, saturation = 0.6, hold = 0.04, duration = 0.45,
sound = { "snd_hermit_lightning", "snd_hermit_lightning2" },
}
-- A sound the mod ships, on every critical hit, with no change to the picture.
local sting = sf2.assets.audio("audio/crit_sting")
sf2.fx.screen { id = "crit_sting", trigger = "critical", sound = sting, sound_volume = 0.6 }
-- One of three impacts at random on the knockout hit. The heavy one is much
-- louder than the others, so its own volume brings it down to match.
sf2.fx.screen {
id = "knockout_impact", trigger = "ko",
sound = {
{ sound = sf2.assets.audio("audio/heavy_impact"), volume = 0.3 },
sf2.assets.audio("audio/impact"),
"snd_super_hit1",
},
}

A triggered grade with zoom above 1 or a nonzero zoom_offset_y moves the fight camera while it is active. The push follows the grade’s strength: full during hold, then easing back to the normal view over duration. The view also moves toward the fighter involved: the struck fighter for hit, critical and ko, the moving fighter for land, knockdown and wall, and the x passed to sf2.fx.play for script (without x the centre stays where it is). When several grades push at once, the strongest wins; zooms do not multiply each other.

The push always keeps both main fighters in view. If they stand far apart (after a knockback, say) the zoom stops at whatever still fits them both, with room for a body on either side, and the centre only moves toward the focus as far as it can without dropping either fighter. Independent actors are not part of this framing.

The push multiplies whatever zoom the camera already has, including a mod that owns the camera through camera control. The arena’s edge limits still apply, so the view never shows past the art, and a native rule that forces the minimum zoom turns the push off. Strong zoom crops the top of the picture, and a high jump can still leave the frame vertically; zoom_offset_y around −40 to −70 keeps heads in view at 1.3–1.7. An always-on grade cannot push the camera.

Signature: sf2.fx.play(effect, options?)

Returns: true when the grade started, or false when it cannot show: its setting switch is off, its match/exclude words rule out the current location, or no game presentation is running (as in a headless check).

When: Any time after loading, normally inside a combat callback such as on_tick or on_animation_start. Calling it again restarts the grade, its sound and its slow motion from full strength. Hold and duration use real time.

Requires: presentation.visuals. effect is the name a screen grade with trigger = "script" registered by this mod returned, or its local id. Another mod’s effect, or a grade with any other trigger, raises an error.

Option Default Meaning
x nil Arena X (−10000 to 10000) that the grade’s camera push-in frames, for example a fighter’s snapshot().self.position.x.

This is how Lua makes a moment happen: a boss changing phase, a summon, a bolt of lightning. The grade carries everything else: colour, slow motion, sound, muffle and zoom. It is presentation only and never changes the fight.

-- During loading:
local stall = sf2.fx.screen {
id = "stall", trigger = "script",
saturation = 0.2, contrast = 1.4, vignette = 0.75,
hold = 0.3, duration = 1.1, time_scale = 0.3, zoom = 1.3, zoom_offset_y = -40,
sound = "snd_time_shift",
}
-- Inside a combat callback, when your ability fires:
local view = fighter:snapshot()
if view then sf2.fx.play(stall, { x = view.self.position.x }) end

Signature: sf2.fx.shadow { id = "...", alpha = 0.45, width = 150, height = 28, fade_height = 350, min_scale = 0.35, color = "#000000", fighters = "both" }

Returns: The effect’s name, <mod-id>.<id>.

When: During loading.

Requires: presentation.visuals.

Draws a soft oval on the floor under each selected fighter, just behind the fighter’s body. As the fighter’s lowest point rises above the floor, the shadow shrinks toward min_scale and fades out, reaching nothing at fade_height. The floor is the lowest point any fighter’s skeleton has reached in the fight, so it settles as soon as the fighters stand. Shadows run in fights only.

Field Default Meaning
alpha 0.45 0–1 opacity on the ground.
width, height 150, 28 Oval size in fighter units (1–2000 and 1–1000). A standing fighter is about 300 units tall.
fade_height 350 1–5000: height above the floor at which the shadow has faded out.
min_scale 0.35 0–1: shadow size at fade_height, as a fraction of full size.
color Black Shadow colour; its alpha multiplies alpha.
fighters "both" both, player or opponent.
sf2.fx.shadow { id = "contact_shadow", alpha = 0.42, width = 150, height = 26, fade_height = 320 }

Signature: sf2.fx.glint { id = "...", interval = 3, duration = 0.35, size = 22, alpha = 0.9, max_speed = 250, color = "#FFF7E6", fighters = "both", scenes = "fights" }

Returns: The effect’s name, <mod-id>.<id>.

When: During loading.

Requires: presentation.visuals.

Every interval seconds or so, a small four-pointed star of light runs along one of the fighter’s weapon blades, from near the grip to the tip. It swells and fades over duration. A glint starts only while the blade’s tip is moving slower than max_speed, so it appears on a held weapon, not during swings. It uses the same blades as weapon trails; bare hands have none. It draws in front of the fighter with additive light.

Field Default Meaning
interval 3 0.2–60 seconds between glints (varied by ±30%).
duration 0.35 0.05–3 seconds for one glint to travel and fade.
size 22 1–400: length of the star’s arms in fighter units.
alpha 0.9 0–1 peak brightness.
max_speed 250 0–20000: fastest tip speed at which a glint may start.
color Warm white Glint colour.
fighters "both" both, player or opponent.
scenes "fights" everywhere also runs on menu previews.
sf2.fx.glint { id = "blade_glint", scenes = "everywhere", interval = 4, duration = 0.4, size = 26 }

Signature: sf2.fx.light { id = "...", source = "weapon", weapons = { "fire" }, color = "#FFB061", radius = 320, intensity = 1, fighter_light = 0.8, glow = 0.3, glow_size = 380, flicker = 0.15, flicker_speed = 8, fighters = "both", scenes = "fights", tags = { ... } }

Returns: The effect’s name, <mod-id>.<id>.

When: During loading.

Requires: presentation.visuals.

Makes something carry a light. There are five sources: weapons, magic, a fixed point of the stage, the stage’s own light, and the hits themselves. With source = "weapon", every fighter whose equipped weapon matches one of the weapons words carries a light at each weapon hand (Weapon-Node2_1 and Weapon-Node2_2). A word matches when it appears anywhere in the weapon’s item name or subtype, ignoring case: "fire" matches WEAPON_FIRE_BATONS and the FireBatons subtype. With source = "magic", a fighter casting magic (any move with the native MagicPlayer template) carries a light at its magic hand, and every magic projectile in flight carries one at its centre. A projectile’s light goes out the moment it strikes something (hit or blocked) or starts its end animation, and fighters it was lighting lose that light at once. With source = "location", the light sits at x, y in the stage (offsets from its centre, up positive), like the sun, the moon or a fire below the floor. With source = "stage", the light is the stage’s own key light, read from its background art when the fight loads: it sits where the brightest light in the picture is (the sun, the moon, a glowing gate, a lantern-lit lake), takes that light’s colour unless you give color, and is as strong as that light stands out from the rest of the picture (from about a third to full intensity). One light then suits every stage, including other mods’ stages. With source = "sky", the light falls from above: it shades the fighters’ bodies only (set body_light), from nothing at the floor to full strength height units above it, so heads and shoulders lift out of the black while the feet stay dark. It takes the stage’s light colour unless you give color. With source = "hit", a light flashes at the contact point of every matching hit (trigger = hit, critical, block or ko, default hit) and fades out over duration seconds of real time.

Each light does two things:

  • It lights the stage. A soft additive glow of glow_size is drawn just behind the fighter at the light.
  • It lights the fighters. Every fighter within radius, including the one carrying it, gets a lit edge on the side facing the light, in the light’s colour. The strength falls off with the square of the distance. Several lights blend by strength. This uses the rim-light edge, so it works with or without sf2.visuals.rim_light; with the preset, the lit edge turns from its usual side toward the light.

Fighters are lit by the lights from the previous rendered frame, so every fighter sees every light regardless of update order.

With body_light above 0, a light also shades the fighters’ whole bodies, not just their edge: each pixel of a silhouette brightens toward the light’s colour with distance falloff, so a fire baton warms the arm and chest nearest to it. Every part of a fighter is lit the same way, so overlapping limbs and clothing stay one silhouette. Up to eight such lights count at once, the strongest first.

With aim above 0, a light also turns the fighters’ lit edge to face it, however bright it is: at 1 the edge sits fully on the side toward the light and swings round smoothly as a fighter moves past it. A light with intensity = 0 and aim = 1 steers the rim light without lighting anything itself. The edge only shows where the rim light or another light colours it.

Field Default Meaning
source "weapon" weapon, magic, location, stage, sky or hit.
weapons — Required for weapon lights, not allowed for other sources: up to 32 lowercase words found in glowing weapons’ names.
x, y 0, 0 Location lights only: position in the stage, as an offset from its centre in location units; up is positive.
trigger, duration "hit", 0.12 Hit lights only: which hits flash (hit, critical, block or ko), and 0.02–2 seconds to fade.
body_light 0 0–1: how strongly it shades the fighters’ whole bodies. 0 lights only their edge. A sky light needs it above 0 to show.
aim 0 0–1: how far fighters within radius turn their lit edge to face this light, whatever its brightness.
height 260 Sky lights only: 10–2000, how far above the floor the light reaches full strength, in fighter units.
color Warm orange Light colour. Stage lights default to the stage’s own light colour.
radius 320 10–5000: how far the light reaches fighters, in fighter units (a standing fighter is about 300 tall).
intensity 1 0–2 overall strength.
fighter_light 0.8 0–1: how strongly it lights fighters. 0 only lights the stage.
glow, glow_size 0.3, 380 0–1 opacity and 1–5000 size of the stage glow. glow = 0 draws none.
flicker, flicker_speed 0.15, 8 0–1 irregular waver of the strength, and its speed (0.1–30).
fighters "both" Whose weapons or magic carry the light. A projectile counts as the fighter who cast it.
scenes "fights" Weapon lights only: everywhere also lights menu previews.

A light without glow lights only the fighters; with glow, location and hit lights also draw a glow on the stage at their position.

sf2.fx.light {
id = "fire_weapons", source = "weapon", scenes = "everywhere",
weapons = { "fire", "flame", "lava" }, color = "#FF8A3A",
radius = 340, glow = 0.3, flicker = 0.25, flicker_speed = 9,
}
sf2.fx.light { id = "magic_light", source = "magic", color = "#C8B8FF", radius = 420, intensity = 1.2 }
-- The moon lighting the fighters from the upper right on night stages.
sf2.fx.light {
id = "moonlight", source = "location", tags = { "night" }, x = 900, y = 600,
color = "#9DB8FF", radius = 3200, intensity = 0.8, fighter_light = 0.55, body_light = 0.08, glow = 0,
}
-- Every stage lights the fighters from wherever its own light is, in its colour.
sf2.fx.light {
id = "stage_light", source = "stage", exclude_tags = { "fire" },
radius = 3600, intensity = 0.85, fighter_light = 0.55, body_light = 0.08, glow = 0, flicker = 0,
}
-- The rim light faces each stage's own light. This light adds no light of its own.
sf2.fx.light { id = "rim_follows_light", source = "stage", radius = 5000, intensity = 0, glow = 0, flicker = 0, aim = 1 }
-- A faint light from above in the stage's colour: heads catch it, feet stay in shadow.
sf2.fx.light { id = "top_light", source = "sky", body_light = 0.14, height = 280, glow = 0, flicker = 0 }
-- A warm flash at every clean hit, lighting both fighters for an instant.
sf2.fx.light {
id = "hit_light", source = "hit", trigger = "hit", color = "#FFD7A0", radius = 520,
intensity = 1.2, fighter_light = 0.9, body_light = 0.35, glow = 0.3, glow_size = 240,
flicker = 0, duration = 0.12,
}

Signature: sf2.fx.stain { id = "...", trigger = "hit", color = "#590808", alpha = 0.8, count = 3, size_min = 10, size_max = 26, spread = 40, flatten = 0.35, limit = 60, speed_min = 90, speed_max = 210, gravity = 900, lift = 80, merge_radius = 16, max_pool_size = 120, sprite = handle, blend = "alpha", fighters = "both" }

Returns: The effect’s name, <mod-id>.<id>.

When: During loading.

Requires: presentation.visuals. A sprite must be a handle from sf2.assets.sprite.

Leaves splats on the floor under matching hits. With the default speed_max = 0, count splats appear immediately below the contact point, scattered up to spread to either side. Set a positive speed_max to launch visible droplets instead. Droplets follow the strike’s impulse direction with a small random angular spread, an upward lift, and downward gravity. Critical hits multiply launch speed by 1.25; knockouts by 1.5 (these multipliers do not stack). No damage or physics changes. For droplets, spread adds random horizontal speed in arena units per second; the splat appears where the droplet crosses the floor, without another random offset. A zero impulse launches upward. Airborne drops use 75% of their eventual splat width, with height equal to 80% of that airborne width, so the spray stays visible at normal fight zoom. Flight follows game time, including slow motion, and freezes while the fight is paused.

Landed splats are flattened by flatten and stay in arena coordinates until the next round begins. They draw over foreground floor artwork, with fighter silhouettes masked out so blood does not paint over feet. The built-in renderer needs a stencil buffer for this mask. Blocked hits never stain. Without a sprite, three built-in splat shapes are used at random. A custom sprite is used for both drops and splats. The floor is sampled from fighter skeletons as for sf2.fx.shadow; this is a flat arena plane, not collision against the floor sprite’s pixels. Stains run in fights only.

Set merge_radius above zero to enable accumulation. A landing merges into the nearest unsaturated pool of the same effect ID within that horizontal distance. Its area and opacity increase, keeping its original position and irregular shape. Width stops at max_pool_size; subsequent landings create new pools once nearby pools are full. Different effect IDs never merge, even within the same mod. max_pool_size must be at least size_max when merging is enabled.

limit counts landed pools, removing the oldest when necessary. The arena also caps all stain effects together at 128 airborne droplets and 512 landed pools, recycling the oldest at capacity. Droplets still airborne after eight game seconds are discarded. Renderers are reused, with at most 128 inactive renderers cached. Turning off an effect’s setting removes its droplets and pools; round reset and arena teardown clear them too. Existing definitions keep instant, separate stains unless they opt into the new fields.

Field Default Meaning
trigger "hit" hit, critical or ko.
fighters "both" Which struck fighter leaves stains.
color Dark red Stain colour; its alpha multiplies alpha.
alpha 0.8 0–1 opacity (each splat varies slightly).
count 3 1–12 splats per hit.
size_min, size_max 10, 26 1–400 splat width in fighter units; min must not exceed max.
spread 40 0–400: instant splat scatter in arena units, or extra droplet horizontal speed in arena units/s.
flatten 0.35 0.1–1: splat height as a fraction of its width.
limit 60 1–200 landed pools of this effect kept at once.
sprite, blend Built-in, "alpha" Your own splat image, and alpha or additive.
speed_min, speed_max 0, 0 Launch speed, 0-2000 arena units/s before critical/KO scaling; minimum must not exceed maximum. Zero maximum keeps instant stains.
gravity 900 Downward acceleration, 50-5000 arena units/s squared.
lift 80 Upward launch velocity added to the spray, 0-1000 arena units/s.
merge_radius 0 Horizontal centre distance for accumulation, 0-400 arena units. Zero disables merging.
max_pool_size 120 Maximum accumulated width, 1-1600 arena units; at least size_max when merging.
detail 0 0–1: above 0 uses the built-in detailed art instead of the simple splats: flat ink splashes with spikes, flung drops and speckle, and teardrops in flight. Ignored with a sprite.
stretch 0 0–4, detailed art: a drop still moving sideways when it lands becomes a directional splash, this much longer, with its head toward the throw.
spread_time 0 0–10 seconds for a landed pool to spread from a third of its size to full size.
age_time, age_color 0, dried brown 0–300 seconds over which pools darken toward age_color. Fresh blood landing on an old pool brings back some of its colour.
sheen 0 0–1 wet highlight in the stage’s light colour that dulls as the pool dries.
sf2.fx.stain {
id = "hit_stains", color = "#4A0606", count = 3,
size_min = 8, size_max = 20, limit = 50,
speed_min = 90, speed_max = 210, gravity = 900, lift = 90,
merge_radius = 16, max_pool_size = 70,
}
sf2.fx.stain { id = "knockout_stain", trigger = "ko", count = 5, size_min = 20, size_max = 42 }

Signature: sf2.fx.on_frame(handler, options?)

Returns: Nothing.

When: During loading, once per mod. The engine then calls handler(view) during gameplay fights, options.rate times a second (1–30, default 10). It is not called while the game is paused or during title-screen sparring.

Requires: presentation.visuals.

This is how a visuals mod reacts to the fight: read the copied view and change your own effects with sf2.fx.set, or fire a script grade with sf2.fx.play. The view is a fresh table each call; changing it changes nothing.

Field Meaning
round The round number.
time Seconds of fight time so far.
round_active true while the round is being fought.
outro How far the knockout close-up has come: 0 when there is none, rising to 1 as it frames the winner. Use it to change effects with sf2.fx.set as the camera arrives.
location The location’s name.
player, opponent health, max_health, health_fraction (0–1) and arena x of each main fighter.
distance Horizontal distance between the two fighters.

The handler has the same instruction budget as a combat callback. If it raises an error or runs out of budget it is switched off for the session, with a warning in the log.

-- A vignette that darkens and pulses like a heartbeat below a third of health.
local danger = sf2.fx.screen { id = "danger", vignette = 0, accent = "#B01010", accent_width = 0.06 }
sf2.fx.on_frame(function(view)
if not view.player then return end
local peril = math.max(0, 1 - view.player.health_fraction / 0.33)
local beat = math.max(0, math.sin(view.time * (1.2 + peril * 1.4) * math.pi * 2)) ^ 6
sf2.fx.set(danger, { vignette = math.min(1, peril * (0.45 + beat * 0.25)), accent_strength = peril })
end, { rate = 10 })

Signature: sf2.fx.set(effect, values)

Returns: Nothing.

When: Any time after the effect was registered, usually inside an sf2.fx.on_frame handler or a combat callback.

Requires: presentation.visuals. effect is the name an sf2.fx function returned, or its local id, and must belong to this mod.

Changes an effect’s numbers while the game runs, or switches it off and on. values holds any numeric fields of the effect, checked against the same ranges as at registration, plus enabled (true or false). Colours, shapes, triggers and sprites cannot change. Changed values last until sf2.fx.reset or the start of the next fight. Screen grades, lights and overlays pick up new numbers at once, and so do trails; particle emitters and stains read most of theirs only when they are built, but enabled works on every effect. A grade that is always on still cannot be given zoom, shake or a time_scale below 1.

-- Fragment: "fog_bank" and "rain_near" are effects this mod registered earlier.
sf2.fx.set("fog_bank", { alpha = 0.3 }) -- thicker fog
sf2.fx.set("rain_near", { enabled = false }) -- stop the rain

Signature: sf2.fx.reset(effect)

Returns: Nothing.

When: Any time after the effect was registered.

Requires: presentation.visuals. The effect must belong to this mod.

Puts back every value sf2.fx.set changed and switches the effect on again. Every fight also starts from the registered values.

sf2.fx.reset("fog_bank")

Signature: sf2.settings.toggle { id = "...", label = "...", description = "...", default = false, group = "...", excludes = other }

Returns: A setting handle. Pass it as setting to a visuals function or to sf2.settings.get.

When: During loading. Register switches before the effects that use them.

Requires: ui.settings.

Field Required/default Meaning
id Required 1–64 lowercase ASCII letters, digits, _ or -, unique within your mod. It keeps the player’s choice stable across versions, so do not rename it.
label Required 1–48 characters shown in the Options row, under your mod’s name header.
description nil Up to 160 characters describing the switch. Shown in the note beside Back while the switch has focus.
default false Value used until the player changes it.
group nil 1–32 characters: the heading this switch is listed under on your mod’s settings page, such as "Lighting". Groups appear in the order their first switch is registered; switches without a group come first, without a heading.
excludes nil A setting handle, or a list of up to 8, for switches of your mod registered earlier that can never be on together with this one. Turning either on turns the other off. Two switches that exclude each other cannot both default to true. If a player’s saved choices have both on (say, from before you added excludes), the earlier switch stays on and this one counts as off.

A mod may register up to 32 switches. Options > Mod settings first lists every enabled mod that has switches, with how many of them are on. Choosing a mod opens its own page: its switches under their group headings (in two columns on desktop, one scrolling list on phones), and an All switches row that turns every switch on, then off, then back to its defaults. Back returns to the list of mods. Give a mod with many switches a few groups so players can find them.

local sf2 = require("sf2")
local trails = sf2.settings.toggle {
id = "weapon_trails", label = "Weapon trails",
description = "Short trails behind fast weapon swings.", default = true,
}
-- Listed under a "Lighting" heading on the mod's settings page.
local rays = sf2.settings.toggle { id = "god_rays", label = "God rays", group = "Lighting", default = true }
-- Two looks that should never stack: turning one on turns the other off.
local rim = sf2.settings.toggle { id = "rim_light", label = "Rim light", default = true }
local wrap = sf2.settings.toggle { id = "light_wrap", label = "Light wrap", excludes = rim }

Signature: sf2.settings.get(setting)

Returns: true or false, the switch’s current value.

When: Any time after the switch was registered, including from callbacks. The value can change while the game runs.

Requires: A handle from sf2.settings.toggle created by the same script context. No extra capability.

if sf2.settings.get(trails) then
-- The player has trails on.
end

The presets below are quicker to set up but fixed in shape. Use them as-is or as a starting point, and build anything else from the blocks above.

Signature: sf2.visuals.background_depth { strength = 0.6, setting = handle }

Returns: Nothing.

When: During loading.

Requires: presentation.visuals.

Background layers already move at their own parallax factor from the location file (for example 0.1 for far layers and 1 for the floor). This effect uses factor ^ (1 + strength) instead, so far layers move proportionally less and the scene reads deeper. A layer never moves further than its original factor allows, so the art never runs out at the edges. Layers with factor 0 or 1 and above, the fighters’ own layer and everything in front of it keep their original movement.

Field Default Range
strength 0.6 0–2
setting nil A setting handle; without one the effect is always on.
sf2.visuals.background_depth { strength = 0.6, setting = depth }

Signature: sf2.visuals.weapon_trails { lifetime = 0.11, min_speed = 900, full_speed = 2600, alpha = 0.55, color = "#RRGGBBAA", setting = handle }

Returns: Nothing.

When: During loading.

Requires: presentation.visuals.

Draws a short ribbon from the main-hand weapon’s grip to its tip. Each part of the ribbon fades out over lifetime seconds, and only appears when the tip is moving faster than min_speed (model units per second), at full strength from full_speed. Fists and weapons shorter than a small blade length draw no trail. The ribbon runs on fight time, sweeps round arcs and tapers. See sf2.fx.trail for details.

Field Default Range
lifetime 0.11 0.02–0.5 seconds
min_speed 900 0–20000
full_speed 2600 1–40000, must be greater than min_speed
alpha 0.55 0–1
color Fighter colour #RRGGBB or #RRGGBBAA. Without it the trail follows the fighter’s colour, including perk tints.
setting nil A setting handle.
sf2.visuals.weapon_trails { lifetime = 0.12, alpha = 0.5, setting = trails }

Signature: sf2.visuals.depth_haze { strength = 0.4, setting = handle }

Returns: Nothing.

When: During loading.

Requires: presentation.visuals.

Fades background layers toward the colour of the location’s farthest layer. The farther a layer is, the stronger its haze (strength × (1 − factor)). The fighters’ layer and the foreground are never hazed.

Field Default Range
strength 0.4 0–1
setting nil A setting handle.
sf2.visuals.depth_haze { strength = 0.35, setting = haze }

Signature: sf2.visuals.rim_light { offset = 2.5, alpha = 0.85, lighten = 0.35, warmth = 0, softness = 0, key_light = 0, ink = 0, ink_color = "#12081C", setting = handle }

Returns: Nothing.

When: During loading.

Requires: presentation.visuals.

Draws a thin lit edge on each fighter’s upper-left side. In fights its colour is the location’s background colour (the average colour of its farthest layer); with key_light it turns toward the stage’s own key light instead, the colour of the brightest light in the stage’s art, so fighters are rimmed orange in a burning town and pink by lanterns. In menu previews such as the shop and profile it is a warm neutral light. Either way it is brightened toward white by lighten. With ink above 0, a fighter’s rim eases toward ink_color while that fighter casts magic (any move with the native MagicPlayer template), so the lit edge becomes a dark outline, then eases back.

warmth blends the background-derived light toward warm ivory while preserving its peak RGB value and configured alpha. Nearby weapon/magic lights and casting ink still apply their own colors afterward. softness adds a smooth outer fringe in screen pixels, leaving the solid rim’s color and intensity intact. Zero keeps the original hard edge. The fringe follows mesh boundary edges, including in menu previews; it does not blur the fighter or its internal triangle edges. These options default to zero for existing mods. Native visual validation is pending.

Field Default Range
offset 2.5 0–12 screen pixels
alpha 0.85 0–1
lighten 0.35 0–1
warmth 0 0–1: blend toward warm ivory, preserving peak brightness.
softness 0 0–3 screen pixels of outer feathering.
key_light 0 0–1: how far the fight colour turns from the background’s average colour to the stage’s key light. 0 keeps the average colour.
ink 0 0–1: how far the rim turns to ink_color during magic casts.
ink_color "#12081C" #RRGGBB or #RRGGBBAA ink colour.
setting nil A setting handle.
sf2.visuals.rim_light { offset = 2.5, alpha = 0.85, lighten = 0.35,
warmth = 0.65, softness = 1.25, ink = 0.85, ink_color = "#1A0C26", setting = rim }
-- Rim each fighter in the colour of the stage's own light.
sf2.visuals.rim_light { offset = 2.75, alpha = 0.9, lighten = 0.4, warmth = 0.25, key_light = 1, setting = rim }

Signature: sf2.visuals.bloom { threshold = 0.82, knee = 0.12, intensity = 0.7, setting = handle }

Returns: Nothing.

When: During loading.

Requires: presentation.visuals.

Makes bright pixels glow. Pixels brighter than threshold bloom, with knee softening the cut-off. This works on the whole picture, so very bright background art also glows; raise threshold to limit it to effects.

Field Default Range
threshold 0.82 0–2
knee 0.12 0–1
intensity 0.7 0–4
setting nil A setting handle.
sf2.visuals.bloom { threshold = 0.85, intensity = 0.6, setting = bloom }

Signature: sf2.visuals.ambient_particles { density = 1, default_style = "dust", locations = { { tags = { "snow" }, match = { "word", ... }, style = "snow" } }, setting = handle }

Returns: Nothing.

When: During loading.

Requires: presentation.visuals.

Adds drifting particles behind the fighters. The first rule that fits the stage picks the style: a rule fits when the stage has one of its stage tags, or when one of its match words is in the stage’s name. Names are lowercased and split on _, -, :, /, spaces and digits, so new_year_24_china_dojo gives the words new, year, china and dojo. Locations that fit no rule use default_style.

Field Default Meaning
density 1 0–4, multiplies the number of particles. 0 draws none.
default_style "dust" none, dust, snow, embers or petals.
locations {} Up to 32 rules. Each has style and at least one of tags (stage tags) and match (lowercase name words using a-z, 0-9, _ or -).
setting nil A setting handle.
sf2.visuals.ambient_particles {
default_style = "dust",
locations = {
{ tags = { "snow" }, style = "snow" },
{ tags = { "fire" }, style = "embers" },
-- One particular stage, by name.
{ match = { "autumn" }, style = "petals" },
},
setting = particles,
}

Signature: sf2.visuals.impact { critical = 1, head = 0.6, shock = 0.4, duration = 0.3, setting = handle }

Returns: Nothing.

When: During loading.

Requires: presentation.visuals.

Adds a short radial blur and colour split when a native hit effect plays: critical for critical hits (also scaled by the player’s Critical hit shake slider), head for head hits and shock for shocks (both scaled by the Hit shake slider). It fades out over duration seconds.

Field Default Range
critical 1 0–1
head 0.6 0–1
shock 0.4 0–1
duration 0.3 0.05–2 seconds
setting nil A setting handle.
sf2.visuals.impact { critical = 1, head = 0, shock = 0, setting = impact }

Signature: sf2.visuals.ink_hud { quiet = 0.45, quiet_delay = 3, low_health = 0.3, timer_warning = 10, knockout_wash = 0.85, setting = handle }

Returns: Nothing.

When: During loading.

Requires: presentation.visuals.

Restyles the fight HUD in black lacquer and gold leaf, in place, while it is active:

  • Health bars become gold-framed lacquer plates with a glossy vermilion fill; lost health lingers as warm ivory, raid bosses are indigo, stacked bars keep their native grey bar below, and in local versus the recoverable stretch is jade.
  • The style meter is the same plate with a fill that deepens from ivory through amber, orange, vermilion and crimson to gold.
  • Round markers become gold-rimmed beads; a won round is vermilion and thumps in.
  • Names and the timer turn warm ivory with a crisp outline; the timer sits on a soft shade and pulses red in its last timer_warning seconds.
  • Style and hit texts stamp in.
  • A fighter on their last bar below low_health gets a frame that pulses red.
  • After quiet_delay seconds without hits, the style meter and perk icons settle to quiet opacity (1 keeps them at full).
  • During the knockout the whole HUD and the on-screen controls fade by knockout_wash, then return.

Portraits, control buttons and perk icon art are never replaced, and the bars keep the native fill timing, drain direction and smoothing. Switching the preset off (or its setting) restores the native HUD at once, mid-fight included.

Field Default Range
quiet 0.45 0–1
quiet_delay 3 0.5–30 seconds
low_health 0.3 0–0.9 of the last bar
timer_warning 10 0–60 seconds
knockout_wash 0.85 0–1
setting nil A setting handle.
local hud = sf2.settings.toggle { id = "ink_hud", label = "Lacquer HUD", default = true }
sf2.visuals.ink_hud { quiet = 0.55, setting = hud }

Signature: sf2.visuals.knockout_outro { hold = 0.5, lead = 0.3, zoom = 1.7, zoom_time = 0.8, offset_y = -60, snap = 0, setting = handle }

Returns: Nothing.

When: During loading.

Requires: presentation.visuals.

Turns the end of a knockout round into a short cinematic. Normally the winner starts its win pose as soon as its finishing move can be interrupted. With this preset:

  1. The winner finishes its move and returns to its idle stance. If it has not settled after 3 seconds, the outro carries on anyway.
  2. The winner holds the idle stance for hold seconds, then starts its win pose.
  3. lead seconds into the win pose, the camera starts closing in on the winner, so the pose is what draws it in. It arrives over zoom_time seconds and frames the winner alone, so the knocked-out fighter may leave the picture. zoom is how far the camera closes in, as a multiple of its normal scale. offset_y pans the view at the same time; positive values raise the framing (more above the winner’s head shows), negative values lower it. A small change goes a long way: tens of units, not hundreds. snap sets how the camera moves. At 0 the zoom and pan ease evenly in and out. At 1 the camera whips onto the winner (most of the pan lands in the first tenth of zoom_time) and the zoom punches about 5% past zoom before settling back, for a hard cinematic cut. Values in between blend the two. Screen effects with trigger = "outro" start with the close-up and ones with "outro_land" as its zoom arrives (at the snap’s peak, or at the end of an even ease), and the frame view’s outro value follows it. The whole stage comes closer with the camera. Background layers, which normally keep their size while the fight camera zooms, grow by the same amount during the close-up, and offset_y moves every layer alike, so layers drawn to line up with each other still do. Parallax stays sideways only, as in a normal fight. The camera stays close until the next round starts, then every layer returns to its normal size. When the knockout ends the fight, the result screen opens over the close-up: the picture freezes where the camera framed it, with both fighters still in place, instead of the empty stage a normal ending leaves behind.

A short pose such as a bow can end before the camera arrives; lower lead or zoom_time if the close-up misses it.

Only rounds won by knockout get the outro. Rounds that end on time, by ring-out or by a fight rule keep the normal ending. The round timer is stopped throughout, and the result is decided before the outro starts, so it never changes who wins. All times are fight time, so the outro stretches with slow motion, for example from a knockout screen grade. Local and online versus matches keep the normal ending.

Field Default Range
hold 0.5 0–3 seconds
zoom 1.7 1–3
zoom_time 0.8 0.05–3 seconds
lead 0.3 0–3 seconds of win pose before the camera moves
focus_time 0.25 0–3; legacy, accepted for older mods and has no effect
offset_y -60 -400 to 400 arena units
snap 0 0–1: 0 eases evenly; 1 whips onto the winner and punches the zoom past its target before settling.
hide_hud 0 1 fades the fight HUD and the on-screen controls out as the close-up begins, and back in when the next round starts.
setting nil A setting handle.
local outro = sf2.settings.toggle { id = "outro", label = "Knockout close-up", default = true }
sf2.visuals.knockout_outro { hold = 0.6, zoom = 1.8, setting = outro }
-- A snappier close-up that whips onto the winner, as a clean finishing shot.
sf2.visuals.knockout_outro { hold = 0.1, zoom = 1.5, zoom_time = 0.6, snap = 1, hide_hud = 1, setting = outro }
-- Cinema bars for the whole close-up, and a flash and jolt as the camera lands.
sf2.fx.screen { id = "outro_bars", setting = outro, trigger = "outro", letterbox = 0.11, vignette = 0.6, hold = 10, duration = 0.6 }
sf2.fx.screen { id = "outro_hit", setting = outro, trigger = "outro_land", brightness = 0.45, shake = 0.55, duration = 0.28 }

The API contract (fields, defaults, ranges, triggers, shapes, capabilities, conflicts, sf2.fx.play, sf2.fx.on_frame, sf2.fx.set and the shipped Chiaroscuro, Chiaroscuro 2, Final Blow, Shadow Clones and Umbra packages) is checked headlessly. The rendering itself, including hit timing, floor detection for shadows and glint placement, and when the motion triggers (land, knockdown, slide, wall) fire, the camera push-in’s framing, the film effects, body lighting, screen overlays, the lacquer HUD and the knockout outro’s timing and framing, is only verified by playing the game. The land, knockdown and slide thresholds may be tuned.