Skip to content
Felucca Online

Docs

Effect sprites (VFX sheets)

Pixel-art effects of the build system (#147–#150): projectiles, hit bursts, ground zones, states over mobiles, reactions, escapes, triggers, auras and formations. They are painted with the pxl skill (.agents/skills/pxl-pixel-art), packed into flipbook sheets in assets/vfx/, and played by the client's VfxPlugin (crates/uo-client/src/vfx.rs) as quads in the 3D scene. This page is the contract of docs/builds-implementation.md § 3.3 in detail: streams V1 and V2 paint in it, stream U plays sheets on combat events.

Files

tools/pixel/vfx/_palette.px.js   the shared palette (included by every source; `// glow` marks tones that give light)
tools/pixel/vfx/*.px.js          V1's sources (one anim per sheet, `// @vfx` lines give the sheet's options)
tools/pixel/vfx2/*.px.js         V2's sources, same rules
tools/pixel/vfx/pack.py          builds the sources with pxl and writes the sheets and the index
assets/vfx/<name>.png            the sheet: RGBA, frames left to right, one row per facing variant
assets/vfx/<name>.json           its options (below)
assets/vfx/vfx.json              the index: every sheet's options by name (generated, never edited)

Build: PXL=<path to pxl.js> python3 tools/pixel/vfx/pack.py (all sources of tools/pixel/vfx*/), --only proj_fire,hit_fire for some sheets, --index to rewrite only vfx.json from the per-sheet JSONs (after a merge, when two streams both changed the index: never merge vfx.json by hand). Without PXL the script looks for the skill in the repository and its worktrees.

Sheet options (<name>.json)

{ "frames": 8, "fps": 16, "w": 16, "h": 16, "anchor": [8, 8], "loop": true, "emissive": 2.5,
  "plane": "billboard", "rows": 2, "glow": ["#fff4d0", "#ffd27a"], "nudge": 0.4, "mount": "body" }
FieldMeaning
framesframes per row, left to right, each w x h px
fpsplayback rate
w, hone frame, in game texels: 1 tile = 32 px wide on the ground (#34), 1 unit (1 m) up = 22.6 px on screen
anchorthe pixel of a frame that sits on the spawn point (x right, y down, from the frame's top-left corner)
looptrue loops until the effect is despawned; false plays once and despawns itself
emissive0..4: how strongly the glowing pixels give light into the HDR frame (0 = lit like a prop, never glows)
planebillboard: a camera-facing quad (image plane), drawn 1:1; ground: lying on the ground, drawn 1:1 too (the oblique view shows the ground without foreshortening)
rowsoptional, 1 or 2. 2 = the sheet can face 8 ways: row 0 is drawn heading screen-right, row 1 heading screen up-right (45°); the client turns them by quarter turns (frames must be square). Without it the sheet never turns
glowoptional: the colours that glow (sRGB hex). Absent = every pixel glows (when emissive > 0). pack.py fills it from the palette's // glow tones found in the sheet
nudgeoptional, units towards the camera (default 0.35 billboard, 0.04 ground): drawn in front of the mobile or ground it sits on without moving on screen
mountoptional hint for whoever spawns it: feet (the mobile's ground point), body (its chest, ≈ 1.2 units up on a human), head (above its head, ≈ 2 units up on a human), ground (a tile or zone centre), hand (a projectile's start)
withoptional: other sheets spawned together with this one at the same point (a zone's standing part)
radiusoptional, zones: the tile radius the sheet is painted for (1: 3x3 tiles ≈ 96 px wide, 2: 5x5 ≈ 160 px)

Pixels are opaque (alpha 255) or clear; partly clear pixels are allowed (thin mist, ice) and blend over the scene. Non-glowing pixels are shaded like props (toon bands, sun, night); glowing pixels add colour x emissive on top, so fire and holy light read at night.

Names

proj_<element>, hit_<element>, zone_<kind> (radius 1) and zone_<kind>_r2 (radius 2), each with an optional _air companion (the standing part, a billboard, listed in with), state_<state> (a loop over a mobile), and for V2 react_<reaction>, esc_<escape kind or key>, trig_<trigger>, aura_<type>, form_<formation>, impact_<physical type>. Elements are the catalogue's (docs/builds/catalog.py TAGS): slash, pierce, crush (Physical), fire, cold, lightning, poison, holy, arcane. States are the catalogue's STATES keys.

Writing a source

include("_palette.px.js")
// @vfx proj_fire {"emissive": 2.5, "rows": 2}
anim("proj_fire", { w: 16, h: 16, frames: 8, fps: 16 }, (c) => { ... heading right ... })
anim("proj_fire__r1", { w: 16, h: 16, frames: 8, fps: 16 }, (c) => { ... heading up-right ... })
  • One anim per sheet named like the sheet; the second row of a rows: 2 sheet is the anim <name>__r1 with the same size and frame count. Helper parts start with _ and are not sheets.
  • // @vfx <name> {json} on its own line declares a sheet; the JSON overrides the defaults by prefix: proj_ billboard loop, centre anchor, mount hand; hit_ billboard once, centre anchor, mount body; zone_ ground loop, centre anchor, mount ground (_air: billboard, bottom anchor at the zone's front edge, nudge radius + 1); state_ billboard loop, bottom-centre anchor, mount feet; others billboard once, centre anchor. frames, fps, w, h come from the anim.
  • Use the palette's keys only; add a tone to _palette.px.js (with // glow if it gives light) rather than a hex colour in a source, so the element colours stay one set (§ Palette).
  • Size in texels: a human is about 41 px tall (1.8 m), a tile 32 px wide; keep loops over a mobile within its silhouette's width (≈ 40 px) and overhead marks small (≤ 24 px) so a crowd stays readable.

Palette

The calm warm look (#62): 2–4 tones per material, the hue turning as it darkens, light cores near white but tinted, no pure black. Element colours (core → rim):

ElementTonesGlows
Fire#fff4d0 #ffd27a #ec8a3a #b04a2c #6e2a26yes
Smoke#a89ca4 #7a6e80 #4e4658 #322c3ano
Cold#f2fbff #b6e2f0 #78afd6 #4a6ea6 #2e4470faintly
Lightning#fdfcff #dfe6ff #9cb0ff #6464c8yes
Poison#e6f2a0 #a8cc58 #6c963c #3e5e34no
Holy#fffbe8 #ffeaa0 #f0c460 #b8883cyes
Arcane#f4e4ff #cfa0f0 #9a66d2 #5c3c94yes
Steel (slash, pierce)#f4f0e6 #c8c2b8 #8c8680 #4e4a4csparks only
Earth (crush)#e2cc9e #b8946a #86684a #584232no
Blood#d65040 #94282c #5a1a22no
Water#e4f4fc #a6d4ec #5e9cc4 #36648ano
Nature#a6c66a #6a9444 #3e6234no
Dark#3c2e52 #241c32 (glints arcane)glints

The exact keys are in tools/pixel/vfx/_palette.px.js; icons (§ 3.4) take their element colours from this table.

Playing sheets (client)

VfxPlugin loads assets/vfx/vfx.json at start; a sheet's texture and frame meshes are made the first time it is spawned.

fn system(mut vfx: Vfx) {
    // game space: x east, y south, z up (like SpriteView::pos)
    let e = vfx.spawn("hit_fire", at, Vec3::ZERO, 1.0, None);
    let p = vfx.spawn("proj_cold", from, to - from, 1.0, None); // move it: VfxInstance::at
    let s = vfx.spawn("state_burning", Vec3::ZERO, Vec3::ZERO, 1.0, Some(mobile)); // `at` = offset
    vfx.despawn(s);
}
  • at: game-space point of the anchor, or with follow an offset from the followed entity (its SpriteView position, else its transform).
  • facing: a game-space direction (zero = as drawn); rows: 2 sheets snap it to the nearest of 8 screen directions, others ignore it.
  • scale: 1 draws texel for texel; use whole numbers (2 for a big creature) to keep pixels square.
  • follow: the effect moves with that entity and despawns with it.
  • Loops play until despawn; one-shots despawn after their last frame. VfxInstance::at, facing and scale may be changed at any time (a flying projectile).

Rendering: the toon mesh material (#38/#62) with the sheet as base colour and the glow mask as emission, alpha blended in the transparent pass (depth-tested, no depth write, no shadows, no outline), nearest sampling; each quad is snapped to whole screen texels.

cargo run --release -p uo-client -- --vfx-gallery lays every sheet out on the demo ground with its name: states over a figure, zones on the ground, projectiles turning through their 8 facings, hits replayed; --time 23 shows the night. UO_VFX_FRAME=n holds every sheet at frame n (captures).

3D effects (decision #152)

The pixel sheets above are being replaced by 3D effects: bold meshes built in the world, so the texel-resolution render and its nearest upscale give them the game's pixel look, lit by the same toon bands, with coloured light thrown on the ground and the characters. The user picked the look from concept paint-overs over a real capture (docs/concepts/kie/vfx3d/, git-ignored: bold, 3–4 flat tones per element from a white-hot core, glowing cores, smooth translucency, no dithering).

Code: crates/uo-client/src/vfx3d/ (mod.rs runtime and API, defs.rs descriptors, shapes.rs built-in meshes and the shape registry, shapes_v1.rs stream V1's custom shapes, coverage.rs which catalogue keys still lack an effect, stage.rs the gallery). Data: every assets/vfx3d/*.json is loaded and merged (V2's hand-tuned prototypes effects.json and its generated v2.json, V1's effects_v1.json; a key defined twice keeps the first file's, with a warning; UO_VFX3D=path reads another folder or one file).

Splitting the work between streams: each stream keeps its own JSON file and its own shapes file. A layer whose kind isn't built in is a custom shape: its builder is looked up by name in the ShapeRegistry, filled by shapes_v1::register (r.add("orb", orb); a builder gets the layer's JSON and the frame's Build and returns a Geo built with shapes.rs's public helpers).

Materials: both are the toon mesh material (mesh.rs) with vertex colours.

  • Glow layers are alpha-blended, depth-tested and don't write depth. They emit their vertex colour (emission, HDR units), cast no shadows and stay out of the outline.
  • Solid layers ("solid": true: ice spikes, debris, shards) are opaque and toon-lit. They are tagged as objects in the normal prepass, so the sel-out outline draws round them like props.

Lights: each effect may spawn short-lived point lights on the lit layers. These are dynamic, not GI. Under the toon bands they give the stepped pools of light that sell an impact.

An effect is a palette (four tones, core to rim, linear) and a list of layers and lights:

FieldMeaning
shapearc (swept blade trail), bolt (jagged tube with forks), star (camera-facing burst), disc (flash), ring (ground ring with a lip), pillar (beam from the sky), flames (flickering tongues), crystal (a closed block or a ring of shards), rays (ground light rays), particles (sparks, debris, motes: cubes with speed, gravity, bounce), streak (a projectile or trail along a hop, lobbed), puffs (billows: smoke, dust, steam; solid puffs are outlined toon smoke), helix (two strands winding up), slash (camera-facing cut marks), dome (wards, shields, nets: ribs, rings and a see-through shell, any span), wedge (a ground sector: cones, telegraphs), strip (a ground band: lines, fissures)
atorigin (the caster), target (the first target), targets (one copy each), chain (one copy per hop: caster → target 1 → target 2 …), fan (a hop from the caster to every target: volleys), hop (the caster to the first target only: a dash's path), rest (every target after the first)
height, forwardunits over the anchor's ground point and towards the facing (the first target)
start, stagger, lifeseconds; stagger delays each further copy (chain hops, targets)
scale, alpha, progresscurves [[t, v], …] over the layer's life (t 0..1); progress sweeps arcs, grows bolts, drops pillars; flat_scale scales across only
spin, reshapedegrees a second round the vertical; seconds between re-shapes (new lightning)
staystays where it was spawned; otherwise a layer follows its mobile (caster or target) every frame, chain hops stretch with them
palette, solid, emissionthe layer's own palette, look and glow
lightsat, tone (0 core .. 3 rim), height, start, stagger, life, range, intensity (lumens curve), flicker

An effect may also have:

FieldMeaning
likeanother effect it starts from: the base's layers then its own layers, the base's lights unless it has its own, the base's palette unless it names one (one family's look reused by its abilities)
size, speedsize over the base (multiplies every spawn's scale) and how many times faster it plays (every start, stagger, life and beat divided by it)
hitstop{ "at": s, "secs": s }: game time freezes for weight (combat::melee_fx::Hitstop)
shake{ "at": s, "secs": s, "amp": px }: a camera kick (melee_fx::Kick), grown with the spawn's scale
repeatabove 0, the effect starts over every repeat s until its root is despawned (auras, formations, state loops)

An effect without a target faces the way its caster's SpriteView faces (stances, wards).

Units are tiles (1 = 1 m; a human is 1.8 tall, chest ≈ 1.0, a hand ≈ 1.1).

Coverage: the gallery logs, per stream and group, how many of the catalogue's keys already have an effect and which are missing (vfx3d coverage: lines): the physical families' abilities, the escapes, reactions, clothing and jewel triggers, keystones, auras, formations and impacts and the armour D and R abilities (V2), the staff families' spells and the state loops (V1).

API (stream U):

fn system(mut vfx: vfx3d::Vfx3d) {
    use vfx3d::{Place, VfxOpts};
    // from a caster to targets (entities with a SpriteView, or game-space points), in order
    vfx.spawn("react_electrocute", Place::On(caster), &[Place::On(a), Place::On(b)], VfxOpts::default());
    vfx.spawn("cleave", Place::On(hero), &[Place::On(foe)], VfxOpts { delay: 0.55, ..default() });
    vfx.spawn("react_freeze", Place::At(tile), &[], VfxOpts { scale: 1.5, palette: Some("frost".into()), ..default() });
}

spawn returns the effect's root entity: despawn it to cut it short. Layers on entities follow them while the effect plays (unless stay). VfxOpts.delay syncs the effect with a clip's impact frame (assets/models/characters/ability_clips.json); scale grows it, for tiers and big creatures; palette swaps the element. Positions are read once, at spawn.

Stream V2's effects (230 keys: the 11 physical families' abilities, the 12 boots escapes, the 20 helmet D and body armour R abilities, the 10 reactions, 53 clothing and jewel triggers, 10 keystones, 10 cloak auras, 6 formations and 7 impacts) are generated by tools/vfx3d/v2_effects.py into assets/vfx3d/v2.json (--check fails when the file is stale). The script builds them from a few blocks (swings, slashes, thrusts, slams, projectiles, stances, shouts, buffs, marks) so each family keeps one look: swords steel, axes crimson with blood, daggers violet shadow, spears long azure thrusts, maces gold impacts and stun stars, hammers earth slams and rubble, fighting staves jade, polearms wide steel arcs, fists amber, bows wooden arrows, crossbows iron bolts. Tests (coverage::every_v2_key_has_an_effect, coverage::every_ability_has_an_effect: all 187 abilities, staves included) keep every key covered.

How the game spawns them (t = 0 is the clip's impact frame or the moment the shot leaves; pass the clip's impact time as delay):

Groupfromto
weapon abilities, reactions, impactsthe userthe struck, in order (chains hop through them; Ally abilities: the ally)
armour D and R abilitiesthe wearerchallenge, blind: the target; rally, war_cry, guardians_oath: the allies reached; insight: the enemies revealed; the rest nothing
moving abilities (dash, leap, escapes, weapon escapes)the mover (Place::On: layers follow it, trails stay)the landing tile (Place::At) first, then the enemies it strikes or marks
clothing and jewel triggersthe wearerwhom the trigger touches (the attacker, the struck, the healed ally)
keystonesthe wearerallies or enemies it touches, if any
auras (aura_<cloak>), formations (form_<drill>)each wearer (formations: every wearer in it)nothing; they loop (repeat) until the root is despawned

Prototype (four effects, the first use of the engine): cleave (a swept arc, crit burst, sparks, ground slam ring, debris, light), react_electrocute (a chain through every target, splash rings, sparks, flickering light), react_freeze (a frost ring, ice spikes and a see-through faceted block, shattering at 1.5 s), react_holy_fire (a pillar from the sky, flames, ground rays, a ring, rising motes, a golden light).

cargo run --release -p uo-client -- --vfx3d-gallery
cargo run --release -p uo-client -- --vfx3d-gallery weapon:hammer
cargo run --release -p uo-client -- --vfx3d-gallery trigger,page:3 --record trig3.mp4 --seconds 9 --zoom 3

The gallery plays effects in a loop on real characters and creature models, on a levelled field south of the village, nine to a screen, each named. What to show: effect keys, prefix*, groups (weapon, weapon:<family>, escape, reaction, trigger, keystone, aura, formation, impact, spell, spell:<family>, state, armour), page:N; nothing shows the four prototypes. Each vignette is built from the catalogue: a weapon ability's user holds its family's first weapon in leathers, plays the ability's clip and the effect starts on its impact frame; targets stand where the ability's shape reaches and flinch; escapes really dash, leap, blink or swap; auras and formations loop. Plays start with a recording's first frame. --record out.mp4 --seconds 9 films a page, --time 22 at night.

Staff spells, staff bolts and states (stream V1)

Every spell of the seven staff families (fire, frost, arcane, holy, nature, curse, earth: 63, keyed by the catalogue's ability keys), each staff's auto-attack bolt (bolt_<family>) and the twenty state loops (state_<state>). The data is assets/vfx3d/effects_v1.json, generated by tools/vfx3d/gen_effects_v1.py (edit the script: shared builders keep the families alike, e.g. every projectile leaves the hand with a flash, flies with a trail and lands with its element's burst). It adds the palettes venom, nature, leaf, curse, plague, drain, stone, dusk, ash, mana, ember and rot.

Shapes (vfx3d/shapes_v1.rs, custom kinds): orb (a projectile head: banded discs with a wobbling rim and a tail), shard (a faceted spike in flight), sphere (a bubble, clear in the middle and bright at the rim, squashed in height so it reads round in the oblique view; dome, bands), cloud (see-through puffs lit on top), spiral (a vortex on the ground), slabs (wall pieces across the way or in a ring, rising with progress), sigil (a rune circle on the ground or upright), rain (streaks or flakes), swirl (motes or leafs winding up, or inward), blaze (flame tongues over a circle, line or cone), cracks (glowing fissures, radiating or in a line), wings, tether (a hop with pulses running back to the caster), spikes (ice or rock out of the ground, in a ring or a line), rock (a tumbling boulder, or count of them rolling out), plates (shards circling a body).

Motion (vfx3d/motion_v1.rs), read from a custom layer's shape JSON:

FieldMeaning
travelcurve: the layer flies from where it stands to the first target over its life (projectiles)
arclob height at mid-flight
liftcurve scaling its height over the ground (a meteor falling from 9 up)
repeaton any layer: the effect loops (every layer and light starts over) until the game despawns it: states
kind trailno mesh; sheds rate bits a second where it is (sparks behind a bolt, embers off a burning body)
kind tintno mesh; the struck mobile's own materials take a palette tone (tone, amount), glow (emission) and may turn see-through (alpha) while it plays: Frozen goes icy, Hidden half-transparent
kind loopno mesh; a marker carrying repeat

For the game (stream U): spells spawn from the caster to their targets on the clip's impact frame (ability_clips.json); projectiles fly a fixed 0.45 s whatever the distance. States spawn once with the mobile as both caster and target and are despawned when the state ends; marks over the head sit at 2.2 units, scale them for big creatures. Zone effects take the zone's centre as the target.

Gallery: the catalogue gallery above shows them: --vfx3d-gallery spell:fire (or another family), state, bolt_*:

cargo run --release -p uo-client -- --vfx3d-gallery spell:fire --no-hud --record fire.mp4 --seconds 9 --zoom 2