Combat effect recipes
FXRecipe2D describes one named effect. One event starts its particles, sprite, light flash, sound, directional shake and camera flash together. The renderer receives the same world-space primitives on Metal and OpenGL.
The shipped names are slash, heavy_slash, fire_hit, ice_hit, lightning, dust_land, dash_burst, coin_pickup, heal, level_up, explosion and magic_circle. Editable copies live in Lab/fxpro/presets/.
Frame events use fx:NAME, for example:
anim_2d { events ["attack@1=fx:heavy_slash", "attack@1=slash:arc_wide", "attack@0=trail:tip"] }
Scripts register a JSON object and invoke its name. game.effectRecipe returns true when the recipe was accepted, false when its fields are invalid. The last argument of game.burst is horizontal direction: -1 left, 1 right.
game.effectRecipe({
name: "green_hit",
particles: [{kind: "ichor", count: 18, speed: 220, spread: 100,
gravity: 700, bounce: 0, life: 0.8, size: 5,
color: [0.2, 0.6, 0.1], additive: false}],
shake: 2, shakeTime: 0.1
});
game.burst("green_hit", [x, y], 1);
self.weaponTrail("tip", 0.3, 14, {color: [0.2, 0.7, 1], glow: 0.8, fade: 0.18});
self.rig.lookAt([x, y]); // world point; null releases the target
The trail arguments are socket name, sampling duration, width in world units, and optional style. Existing three-argument calls keep their defaults. Style defaults are color: [0.55, 0.85, 1], glow: 0.6, and fade: 0.14 seconds per sample. RGB clamps to 0...1, glow to 0...2, and fade to 0.01...1 second. Malformed or non-finite style fields use the defaults.
{
"name": "heavy_slash",
"particles": [
{"kind": "spark", "count": 32, "speed": 480, "spread": 90,
"gravity": 700, "bounce": 0.35, "life": 0.55, "size": 5,
"color": [1, 0.8, 0.3], "additive": true}
],
"sprite": {"image": "builtin:sparkle", "size": 100, "life": 0.22,
"growth": 1.5, "color": [1, 0.85, 0.5], "additive": true},
"sound": "sword_heavy",
"shake": 5, "shakeTime": 0.2,
"light": 2.5, "lightTime": 0.1, "lightRadius": 160,
"color": [1, 0.8, 0.4],
"chromatic": 0.35, "radialBlur": 0.3, "postTime": 0.1
}
Distances and particle sizes are world units; speed is units per second; gravity is downward acceleration in units per second squared. spread is the full angle in degrees around the hit direction. bounce is the retained normal speed before contact friction. life and cue times are seconds. growth is the final sprite size divided by its initial size. Colors are RGB values from 0 to 1. An omitted field uses its documented Swift default; omitted particles, sprite, sound, light and camera effects stay off.
Particles can be spark, chip, dust, orb, blood, ichor or oil. Spark rectangles turn to face their velocity; chips spin. Blood, ichor and oil leave a flattened decal when they reach upward-facing solid ground. Decals remain for 30 seconds, fade over the last three, and the oldest is replaced when the decal budget fills. They are presentation state and do not change a save file. Their position stays in world space; they do not ride a moving platform. The session draws them on layer 1, so a level's opaque ground art must sit below that layer. Choose a dark red, green or charcoal particle color and turn additive off for these materials.
Swift integration:
for recipe in try FXRecipe2D.decode(jsonData) { try effects.register(recipe) }
let cue = effects.emit("heavy_slash", at: contactPoint, direction: hitDirection)
effects.step(presentationDelta) { from, to in
// Return the first solid tile/body contact, excluding characters.
solidCollision(from, to)
}
The session routes frame events and script bursts into this same emission path. A recipe's slowMotion (0.05 to 1, default 1) and slowTime (up to two seconds) are optional cues for a final boss hit. They affect gameplay timing only when the session explicitly accepts the cue; merely loading JSON has no effect. The pool keeps running on presentation time during hit-stop.
Input is limited to 128 KB, 64 recipes per load, eight particle groups per recipe, and 200 requested particles per group. Invalid colors, non-finite numbers, unknown particle kinds and duplicate recipe names are rejected. The default runtime reserves 256 particles, 32 sprites and 64 decals. Excess particles and sprites are counted and dropped. Emission never grows these budgets, executes code, writes files or mutates collision bodies.
CombatEffects2DTests checks recipe round trips, invalid input, deterministic direction, limits and cleanup, fast swept bounces, persistent decals and a 200-particle CPU sample. That last sample excludes trail generation and GPU drawing; it is not the complete effects frame-cost acceptance test. See the worktree STATUS.md for tests actually run, captures and remaining validation.