PR pixi-reels
All recipes

Big symbols with Spine

Production-shape big-symbols recipe. a 2x2 wild placed at an anchor cell, with each cell a Spine skeleton that idles, lands, and plays a win animation. Same engine mechanic as the CardSymbol version, real art on top.

Loading recipe…

Same big-symbols mechanic as the CardSymbol recipe, but every cell is a Spine 2D skeleton. idle plays on land, landing plays as the squash-and-stretch on touchdown, win fires when WinPresenter highlights a payline, and destroy is wired up for cascades. The 2x2 bigWild reuses the regular wild skeleton; Spine scales it to the larger cell box without losing crispness.

Boot the atlas first

SpineReelSymbol looks up its skeleton by alias on activate, so the assets need to be in PIXI’s Assets cache before .build() runs:

import { loadGeneratedSpines, buildSpineMap } from '../../shared/generatedSpineLoader';

await loadGeneratedSpines();   // idempotent. safe to call from every boot

The default base path is '/generated-symbols/'. Both examples/assets/ (publicDir) and the docs site (apps/site/public/generated-symbols/) serve the same 10 JSONs + atlas + page from there.

Mapping ids to skeletons

The bundle ships ten generic skeletons (low_a, low_k, low_q, low_j, mid_1, mid_2, mid_3, high_1, wild, scatter). Map your slot’s symbol ids onto whichever skeleton you want for each:

const SPINE_MAP = {
  '9':     'low_a',
  '10':    'low_k',
  J:       'low_q',
  Q:       'low_j',
  K:       'mid_1',
  A:       'high_1',
  wild:    'wild',
  bigWild: 'wild',   // reuse the wild rig at 2x2 scale
};

builder.symbols((registry) => {
  const spineMap = buildSpineMap(SPINE_MAP);
  for (const id of Object.keys(SPINE_MAP)) {
    registry.register(id, SpineReelSymbol, {
      spineMap,
      autoPlayLanding: true,   // run the landing anim on touchdown
    });
  }
});

buildSpineMap produces the Record<symbolId, { skeleton, atlas }> shape SpineReelSymbol wants and throws loud if you reference a skeleton name that isn’t in the bundle.

Big-symbol metadata

Same shape as the CardSymbol recipe. the engine doesn’t care what renders the cell; size and weight are what make a symbol “big”:

.symbolData({
  wild:    { weight: 3, zIndex: 4 },
  bigWild: { weight: 0, zIndex: 5, size: { w: 2, h: 2 } },
})

weight: 0 is mandatory for anything bigger than 1x1: random fill must never place a 2x2 because it has no concept of block geometry. The server places it at an anchor cell only, and the engine paints OCCUPIED across the rest of the block. The bigWild registration’s SpineReelSymbol only renders at the anchor; the OCCUPIED cells are rendered by the engine’s invisible stub class.

Animations

The bundled skeletons all expose the canonical four-animation set:

AnimWhen it playsBehavior
idleOn activate (every land)Loops; gentle vertical float + micro rotation on the icon bone; frame stays stationary
landingOn touchdown when autoPlayLanding: trueSquash-and-stretch on root; ends at scale 1
winWhen WinPresenter calls playWin()Anticipation dip → punch to 1.22x → bounce settle to 1.0
destroyWhen playOut() is called (cascades)Burst to 1.55x while alpha drives to 0

idle explicitly resets root.scale and icon.rotate to baseline across its loop, so a destroy’s lingering 1.55x scale is overwritten on the next activate. win and landing settle at scale 1 / alpha ff so they never leave a symbol invisible after their animation ends.

Authoring your own

These ten skeletons are generated by the bun pipeline at tools/symbol-gen/. The animations are authored in a typed DSL, compiled to Spine 4.x JSON, and packed against a shared atlas:

// tools/symbol-gen/scripts/animations/win.ts
export const win = anim('win')
  .bone('root', (b) => b
    .scale(1.0)
    .scaleTo(0.94, frames(4),  'easeOut')   // anticipation dip
    .scaleTo(1.22, frames(8),  'easeOut')   // punch
    .scaleTo(0.96, frames(10), 'easeInOut')
    .scaleTo(1.04, frames(8),  'easeInOut')
    .scaleTo(1.0,  frames(8),  'easeInOut') // settle
    .scaleTo(1.0,  frames(16))              // hold
  )
  // ...
  .build();

Run bun run build in tools/symbol-gen/ to regenerate. Output drops in tools/symbol-gen/out/; copy to examples/assets/generated-symbols/ and apps/site/public/generated-symbols/ to deploy.