pixi-reels 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/symbols/PerspectiveCell.ts","../src/symbols/SpriteSymbol.ts","../src/symbols/AnimatedSpriteSymbol.ts","../src/symbols/EmptySymbol.ts","../src/snapshot/SpinTextureCache.ts","../src/snapshot/StaticSpinSymbol.ts","../src/symbols/CardSymbol.ts","../src/spin/anticipationRecipes.ts","../src/spin/modes/CascadeMode.ts","../src/spin/modes/ImmediateMode.ts","../src/board/BoardGrid.ts","../src/board/HwTypes.ts","../src/board/HoldAndWinState.ts","../src/board/HoldAndWinBoard.ts","../src/board/HoldAndWinBuilder.ts","../src/wins/Win.ts","../src/wins/WinPresenter.ts","../src/utils/gsapTicker.ts"],"sourcesContent":["import { PerspectiveMesh, type Container, type Texture } from 'pixi.js';\nimport type { ReelCellInset, ReelCellQuad } from '../config/types.js';\n\n/**\n * The slice of its cell a texture's opaque art really occupies.\n *\n * TexturePacker trims the transparent margin off each frame, so a 300x300\n * symbol whose art is a 152x152 blob at (74, 74) ships as a 152x152 frame. A\n * `Sprite` puts that back; a mesh does not, so the reel has to be told or it\n * projects - and inflates - the whole cell.\n *\n * `null` for an untrimmed texture, which needs no correction.\n */\nexport function textureCellInset(texture: Texture): ReelCellInset | null {\n const trim = texture.trim;\n const orig = texture.orig;\n if (!trim || orig.width <= 0 || orig.height <= 0) return null;\n if (trim.width === orig.width && trim.height === orig.height) return null;\n return {\n left: trim.x / orig.width,\n top: trim.y / orig.height,\n right: (trim.x + trim.width) / orig.width,\n bottom: (trim.y + trim.height) / orig.height,\n };\n}\n\n/**\n * Whether a texture can go through the perspective mesh.\n *\n * Only textures owning their whole source qualify. An atlas SUB-frame does\n * not: the mesh addresses its source with plain 0..1 UVs, and remapping them\n * onto the frame - via `texture.uvs`, via `textureMatrix`, or leaving it to\n * PixiJS - has not produced a correct draw here. Unmapped, the cell shows the\n * whole sheet; mapped by hand, a magnified crop of the right frame, which says\n * the mapping is applied somewhere else too. Rather than ship either, a\n * sub-frame falls back to the affine fit in `ReelSymbol.applyCellQuad()` -\n * correct, just not keystoned.\n *\n * Generated textures, single loaded images and render textures pass. Atlas\n * frames - the common production case - do not, yet.\n */\nexport function canProjectTexture(texture: Texture): boolean {\n const source = texture.source;\n if (!source) return false;\n if (texture.rotate !== 0) return false;\n const { frame, orig } = texture;\n if (frame.x !== 0 || frame.y !== 0) return false;\n if (frame.width !== source.width || frame.height !== source.height) return false;\n return orig.width === frame.width && orig.height === frame.height;\n}\n\n/**\n * Tessellation of the projected quad. PixiJS builds the perspective by\n * interpolating UVs across a grid, so this is the quality knob: too few and a\n * keystoned cell reads as a plain stretch, too many and every symbol on the\n * board costs a pile of vertices. Slot cells are small and their trapezoids are\n * mild, so the PixiJS default of 10 is more than enough.\n */\nconst VERTICES = 10;\n\n/**\n * Draws a texture through a real perspective quad.\n *\n * What makes a curved reel a projection rather than a squash. A `Container`\n * transform is affine - it can scale a cell, never turn it into a trapezoid -\n * so a cell rotated away comes out a smaller rectangle instead of one with a\n * narrower far edge. `PerspectiveMesh` maps the texture onto four arbitrary\n * corners with perspective-correct interpolation, and costs no render pass\n * because the symbol already owns a texture.\n *\n * Lazy: a game that never calls `curve()` allocates no mesh and keeps\n * rendering through its plain sprite.\n *\n * @internal Shared by `SpriteSymbol` and `AnimatedSpriteSymbol`. Use it the\n * same way from a custom symbol whose content is a single texture.\n */\nexport class PerspectiveCell {\n private _mesh: PerspectiveMesh | null = null;\n private _active = false;\n private _uvScratch: Float32Array | null = null;\n private _uvSource: Texture | null = null;\n\n /**\n * @param _view the symbol's own view, which the mesh is parented to\n * @param _flat the display object used when the reel is flat. hidden while\n * the mesh is driving, so the two never draw on top of each other\n */\n constructor(\n private readonly _view: Container,\n private readonly _flat: Container,\n ) {}\n\n /** True while the perspective mesh is the thing being drawn. */\n get isActive(): boolean {\n return this._active;\n }\n\n /**\n * The mesh, once one exists. Animate THIS, not the symbol's view, for a win\n * pulse on a curved reel - it is pivoted on the quad's centre, so a scale\n * tween pulses in place instead of swinging out from the cell corner.\n */\n get mesh(): PerspectiveMesh | null {\n return this._mesh;\n }\n\n /**\n * Project the cell, or hand rendering back to the flat display object when\n * `quad` is `null`.\n *\n * @param quad the projected footprint, view-local\n * @param texture what to draw. re-read on every call so an animated symbol\n * can swap frames without telling us\n */\n apply(quad: ReelCellQuad | null, texture: Texture): boolean {\n if (quad === null || !canProjectTexture(texture)) {\n if (!this._active) return false;\n this._active = false;\n this._flat.visible = true;\n if (this._mesh) this._mesh.visible = false;\n return false;\n }\n\n const mesh = this._ensureMesh(texture);\n if (mesh.texture !== texture) mesh.texture = texture;\n this._syncUvs(mesh, texture);\n mesh.setCorners(quad.x0, quad.y0, quad.x1, quad.y1, quad.x2, quad.y2, quad.x3, quad.y3);\n // Pivot and position on the quad's centre: renders unchanged at scale 1,\n // but a scale tween pulses about the middle of the cell.\n const cx = (quad.x0 + quad.x1 + quad.x2 + quad.x3) / 4;\n const cy = (quad.y0 + quad.y1 + quad.y2 + quad.y3) / 4;\n mesh.pivot.set(cx, cy);\n mesh.position.set(cx, cy);\n\n if (!this._active) {\n this._active = true;\n this._flat.visible = false;\n mesh.visible = true;\n }\n return true;\n }\n\n /** Reset the mesh's own transform. Call from `stopAnimation` / `onDeactivate`. */\n resetTransform(): void {\n if (this._mesh) this._mesh.scale.set(1, 1);\n }\n\n /**\n * Push a new texture through without re-projecting. For the pooled swap in\n * `onActivate`, where the identity changes but the quad has not.\n */\n syncTexture(texture: Texture): void {\n if (this._active && this._mesh && this._mesh.texture !== texture) {\n this._mesh.texture = texture;\n this._syncUvs(this._mesh, texture);\n }\n }\n\n destroy(): void {\n if (!this._mesh) return;\n this._mesh.destroy();\n this._mesh = null;\n this._active = false;\n\n this._uvScratch = null;\n this._uvSource = null;\n }\n\n private _ensureMesh(texture: Texture): PerspectiveMesh {\n if (this._mesh) return this._mesh;\n this._mesh = new PerspectiveMesh({\n texture,\n verticesX: VERTICES,\n verticesY: VERTICES,\n });\n // `PerspectivePlaneGeometry` warps VERTEX POSITIONS and leaves the UVs\n // alone, so the atlas mapping is ours to own. One scratch buffer, rewritten\n // from the grid on every texture swap rather than remapped in place, which\n // would compound.\n this._uvScratch = new Float32Array(VERTICES * VERTICES * 2);\n this._view.addChild(this._mesh);\n return this._mesh;\n }\n\n /**\n * Fold the texture's atlas frame into the mesh's UVs.\n *\n * A `Sprite` gets this free; a `Mesh` does not. Its 0..1 UVs address the\n * whole SHEET, so an unmapped mesh draws the entire atlas squeezed into one\n * cell.\n *\n * `texture.uvs` carries the frame's corners in source space and PixiJS keeps\n * it current, so interpolating between them is exact. `textureMatrix` is not\n * necessarily updated for this texture yet, and yields out-of-range UVs that\n * sample whatever is next door on the sheet.\n */\n private _syncUvs(mesh: PerspectiveMesh, texture: Texture): void {\n if (this._uvSource === texture) return;\n const out = this._uvScratch;\n if (!out) return;\n this._uvSource = texture;\n\n // Grid coordinates from the vertex index, not read back off the geometry:\n // the UV buffer is not necessarily populated at construction, and a\n // snapshot of zeros points every vertex at the frame's first texel - a\n // cell of flat colour. Row-major, matching PixiJS's own plane transform.\n const { x0, y0, x1, y1, x2, y2, x3, y3 } = texture.uvs;\n const span = VERTICES - 1;\n for (let k = 0; k < out.length / 2; k++) {\n const u = (k % VERTICES) / span;\n const v = Math.floor(k / VERTICES) / span;\n const tl = (1 - u) * (1 - v);\n const tr = u * (1 - v);\n const br = u * v;\n const bl = (1 - u) * v;\n out[k * 2] = tl * x0 + tr * x1 + br * x2 + bl * x3;\n out[k * 2 + 1] = tl * y0 + tr * y1 + br * y2 + bl * y3;\n }\n mesh.geometry.uvs = out;\n }\n}\n","import { Sprite, type Texture } from 'pixi.js';\nimport type { gsap } from 'gsap';\n\nimport type { ReelCellInset, ReelCellQuad } from '../config/types.js';\nimport { ReelSymbol } from './ReelSymbol.js';\nimport { PerspectiveCell, textureCellInset } from './PerspectiveCell.js';\n\nexport interface SpriteSymbolOptions {\n /** Map of symbolId → Texture. */\n textures: Record<string, Texture>;\n /** Anchor point. Default: { x: 0.5, y: 0.5 }. */\n anchor?: { x: number; y: number };\n}\n\n/**\n * Symbol implementation using a simple PixiJS Sprite.\n * Swaps texture on activate. Win animation is a scale pulse via GSAP.\n *\n * On a curved reel it draws the same texture through a `PerspectiveMesh`\n * instead, so the cell is a real projected trapezoid rather than a rectangle\n * that has been scaled down. See {@link ReelSymbol.applyCellQuad}.\n */\nexport class SpriteSymbol extends ReelSymbol {\n private _sprite: Sprite;\n private _textures: Record<string, Texture>;\n private _winTween: gsap.core.Tween | null = null;\n private _perspective: PerspectiveCell;\n\n constructor(options: SpriteSymbolOptions) {\n super();\n this._textures = options.textures;\n const anchor = options.anchor ?? { x: 0, y: 0 };\n this._sprite = new Sprite();\n this._sprite.anchor.set(anchor.x, anchor.y);\n this.view.addChild(this._sprite);\n this._perspective = new PerspectiveCell(this.view, this._sprite);\n }\n\n protected onActivate(symbolId: string): void {\n const texture = this._textures[symbolId];\n if (texture) {\n this._sprite.texture = texture;\n // The pool hands this instance a new identity without re-projecting, so\n // the mesh has to be told about the swap or it keeps drawing the old one.\n this._perspective.syncTexture(texture);\n }\n }\n\n protected onDeactivate(): void {\n this._killWinTween();\n this._sprite.scale.set(1, 1);\n this._perspective.resetTransform();\n }\n\n override get cellInset(): ReelCellInset | null {\n return textureCellInset(this._sprite.texture);\n }\n\n override applyCellQuad(quad: ReelCellQuad | null): void {\n if (this._perspective.apply(quad, this._sprite.texture)) {\n // The mesh's own corners carry the whole projection, so the view must\n // stay at identity. Anything else would apply the curve twice.\n this.view.scale.set(1, 1);\n this.view.pivot.set(0, 0);\n return;\n }\n // The mesh declined this texture (today: any atlas sub-frame). Take the\n // base class's uniform-scale fit so the cell is still on the drum.\n super.applyCellQuad(quad);\n }\n\n async playWin(): Promise<void> {\n this._killWinTween();\n // Pulse whichever object is actually on screen. The mesh is pivoted on the\n // quad's centre, so it swells in place the same way the sprite does.\n const target = this._perspective.isActive ? this._perspective.mesh : this._sprite;\n if (!target) return;\n return new Promise<void>((resolve) => {\n this._winTween = this.gsap.to(target.scale, {\n x: 1.15,\n y: 1.15,\n duration: 0.15,\n yoyo: true,\n repeat: 1,\n ease: 'power2.inOut',\n onComplete: resolve,\n });\n });\n }\n\n stopAnimation(): void {\n this._killWinTween();\n this._sprite.scale.set(1, 1);\n this._perspective.resetTransform();\n }\n\n resize(width: number, height: number): void {\n this._sprite.width = width;\n this._sprite.height = height;\n }\n\n protected override onDestroy(): void {\n this._killWinTween();\n this._perspective.destroy();\n }\n\n private _killWinTween(): void {\n if (this._winTween) {\n this._winTween.kill();\n this._winTween = null;\n }\n }\n}\n","import { AnimatedSprite, type Texture } from 'pixi.js';\nimport type { ReelCellInset, ReelCellQuad } from '../config/types.js';\nimport { ReelSymbol } from './ReelSymbol.js';\nimport { PerspectiveCell, textureCellInset } from './PerspectiveCell.js';\n\nexport interface AnimatedSpriteSymbolOptions {\n /** Map of symbolId → array of frame textures. */\n frames: Record<string, Texture[]>;\n /** Playback speed (frames per second multiplier). Default: 1. */\n animationSpeed?: number;\n /** Anchor point. Default: { x: 0.5, y: 0.5 }. */\n anchor?: { x: number; y: number };\n}\n\n/**\n * Symbol implementation using PixiJS AnimatedSprite.\n * Swaps frame arrays on activate. Win animation plays the full sequence.\n */\nexport class AnimatedSpriteSymbol extends ReelSymbol {\n private _animSprite: AnimatedSprite;\n private _frames: Record<string, Texture[]>;\n private _animationSpeed: number;\n private _winResolve: (() => void) | null = null;\n private _perspective: PerspectiveCell;\n\n constructor(options: AnimatedSpriteSymbolOptions) {\n super();\n this._frames = options.frames;\n this._animationSpeed = options.animationSpeed ?? 1;\n const anchor = options.anchor ?? { x: 0, y: 0 };\n\n const firstFrames = Object.values(this._frames)[0] ?? [];\n this._animSprite = new AnimatedSprite(firstFrames.length > 0 ? firstFrames : []);\n this._animSprite.anchor.set(anchor.x, anchor.y);\n this._animSprite.animationSpeed = this._animationSpeed;\n this._animSprite.loop = false;\n this.view.addChild(this._animSprite);\n this._perspective = new PerspectiveCell(this.view, this._animSprite);\n // On a curved reel the mesh, not the sprite, is what draws. It has to be\n // handed each new frame or a playing win animation freezes on frame 0.\n this._animSprite.onFrameChange = () => {\n this._perspective.syncTexture(this._animSprite.texture);\n };\n }\n\n override get cellInset(): ReelCellInset | null {\n return textureCellInset(this._animSprite.texture);\n }\n\n override applyCellQuad(quad: ReelCellQuad | null): void {\n if (this._perspective.apply(quad, this._animSprite.texture)) {\n // The mesh's own corners carry the whole projection, so the view must\n // stay at identity. Anything else would apply the curve twice.\n this.view.scale.set(1, 1);\n this.view.pivot.set(0, 0);\n return;\n }\n // The mesh declined this texture (today: any atlas sub-frame). Take the\n // base class's uniform-scale fit so the cell is still on the drum.\n super.applyCellQuad(quad);\n }\n\n protected onActivate(symbolId: string): void {\n const frames = this._frames[symbolId];\n if (frames && frames.length > 0) {\n this._animSprite.textures = frames;\n this._animSprite.gotoAndStop(0);\n }\n }\n\n protected onDeactivate(): void {\n this._animSprite.stop();\n this._winResolve = null;\n }\n\n async playWin(): Promise<void> {\n return new Promise<void>((resolve) => {\n this._winResolve = resolve;\n this._animSprite.loop = false;\n this._animSprite.onComplete = () => {\n this._winResolve = null;\n this._animSprite.onComplete = undefined;\n // Return to frame 0 so the cell settles on its idle look instead\n // of holding the last frame of the win sequence (which for\n // generated pixel-art sequences often ends mid-action. muted,\n // shifted, or otherwise not the neutral base pose).\n this._animSprite.gotoAndStop(0);\n resolve();\n };\n this._animSprite.gotoAndPlay(0);\n });\n }\n\n stopAnimation(): void {\n this._animSprite.stop();\n this._animSprite.gotoAndStop(0);\n if (this._winResolve) {\n this._winResolve();\n this._winResolve = null;\n }\n }\n\n resize(width: number, height: number): void {\n this._animSprite.width = width;\n this._animSprite.height = height;\n // Position the sprite to match its anchor so it fills the cell.\n // Without this, an anchor of (0.5, 0.5) would render the sprite with\n // its centre at the cell's top-left corner. only the bottom-right\n // quadrant visible inside the mask.\n this._animSprite.x = width * this._animSprite.anchor.x;\n this._animSprite.y = height * this._animSprite.anchor.y;\n }\n\n protected override onDestroy(): void {\n this._animSprite.onFrameChange = undefined;\n this._perspective.destroy();\n }\n}\n","import { ReelSymbol } from './ReelSymbol.js';\n\n/**\n * A {@link ReelSymbol} that renders nothing and never animates.\n *\n * Register it for an id that needs to occupy a grid slot without producing\n * any visual - the blank rest state of a {@link HoldAndWinBoard} cell, a\n * cascade \"hole\", a dry-run symbol-set placeholder. The {@link HoldAndWinBuilder}\n * auto-registers one under its `emptyId` so callers never have to.\n */\nexport class EmptySymbol extends ReelSymbol {\n protected onActivate(_symbolId: string): void {}\n protected onDeactivate(): void {}\n async playWin(): Promise<void> {}\n stopAnimation(): void {}\n resize(_width: number, _height: number): void {}\n}\n","import { BlurFilter, Container, Rectangle, Sprite, type Texture } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport type { ReelSymbol } from '../symbols/ReelSymbol.js';\n\n/**\n * The slice of a PixiJS renderer the cache needs. `Renderer` (WebGL or\n * WebGPU) satisfies it structurally; tests can pass a stub.\n */\nexport interface SnapshotRenderer {\n generateTexture(options: {\n target: Container;\n frame?: Rectangle;\n resolution?: number;\n antialias?: boolean;\n }): Texture;\n}\n\n/** Tuning for the baked motion-blur variant of a snapshot. */\nexport interface MotionBlurOptions {\n /**\n * Smear axis — the reel's travel direction. `StaticSpinSymbol` defaults\n * this to the owning set's orientation (`'y'` vertical, `'x'` horizontal),\n * so it is only worth setting to override that, or when calling the cache\n * directly. The other axis is never blurred.\n */\n axis?: 'y' | 'x';\n /**\n * Blur strength in pixels along the axis. Default: 20% of the cell's\n * span on that axis (`cellHeight * 0.2` for `'y'`, `cellWidth * 0.2`\n * for `'x'`).\n */\n strength?: number;\n /** BlurFilter quality (number of passes). Default: 4. */\n quality?: number;\n /**\n * Extra transparent pixels added on both sides of the cell along the\n * axis so the smear isn't clipped at the texture edge.\n * Default: `ceil(strength)`.\n */\n padding?: number;\n}\n\nexport interface SpinTextureCacheOptions {\n /** The renderer used to generate snapshot textures (`app.renderer`). */\n renderer: SnapshotRenderer;\n /** Resolution for generated textures. Default: the renderer's default. */\n resolution?: number;\n /** Default motion-blur tuning for `captureBlurred`. */\n blur?: MotionBlurOptions;\n}\n\ninterface CacheEntry {\n texture: Texture;\n /** true = generated by this cache (destroyed on invalidate), false = user-provided. */\n owned: boolean;\n width: number;\n height: number;\n /** Smear axis the blurred variant was baked for (blurred entries only). */\n axis?: 'y' | 'x';\n}\n\n/**\n * Per-symbolId cache of \"spin textures\". flat snapshots a reel shows\n * instead of the live symbol (Spine skeleton, animated sprite, ...) while\n * it spins.\n *\n * Two flavors per symbolId:\n *\n * - **static**. the symbol rendered once into a RenderTexture at cell size.\n * - **blurred**. the static snapshot smeared vertically through a one-time\n * BlurFilter pass into a taller, padded RenderTexture. At spin time it's\n * an ordinary sprite texture: zero filters per frame.\n *\n * Textures come from two sources, user-provided always winning:\n *\n * - `setStatic(id, tex)` / `setBlurred(id, tex)`. hand-authored art from\n * your atlas. Never destroyed by the cache.\n * - `captureStatic` / `captureBlurred`. generated on demand from a live\n * symbol view. Owned by the cache and destroyed on `invalidate` /\n * `clear` / `destroy`.\n *\n * Captures are keyed by symbolId and remembered together with the cell size\n * they were taken at; capturing again at a different size regenerates.\n * Share ONE cache across all reels/symbols of a reel set. that's the point.\n */\nexport class SpinTextureCache implements Disposable {\n private _renderer: SnapshotRenderer;\n private _resolution: number | undefined;\n private _blurDefaults: MotionBlurOptions;\n private _static = new Map<string, CacheEntry>();\n private _blurred = new Map<string, CacheEntry>();\n private _isDestroyed = false;\n\n constructor(options: SpinTextureCacheOptions) {\n this._renderer = options.renderer;\n this._resolution = options.resolution;\n this._blurDefaults = options.blur ?? {};\n }\n\n // -- User-provided textures ----------------------------------------------\n\n /** Provide a hand-authored static spin texture. Wins over captures. */\n setStatic(symbolId: string, texture: Texture): void {\n this._put(this._static, symbolId, { texture, owned: false, width: 0, height: 0 });\n }\n\n /** Provide a hand-authored motion-blur texture. Wins over captures. */\n setBlurred(symbolId: string, texture: Texture): void {\n this._put(this._blurred, symbolId, { texture, owned: false, width: 0, height: 0 });\n }\n\n // -- Lookups ---------------------------------------------------------------\n\n getStatic(symbolId: string): Texture | null {\n return this._static.get(symbolId)?.texture ?? null;\n }\n\n getBlurred(symbolId: string): Texture | null {\n return this._blurred.get(symbolId)?.texture ?? null;\n }\n\n hasStatic(symbolId: string): boolean {\n return this._static.has(symbolId);\n }\n\n hasBlurred(symbolId: string): boolean {\n return this._blurred.has(symbolId);\n }\n\n // -- Capture ---------------------------------------------------------------\n\n /**\n * Snapshot `source` (a symbol's `view`, already activated and resized to\n * the cell) into a `width`x`height` texture and cache it under `symbolId`.\n * Returns the cached texture if one already exists at this size; a\n * user-provided texture always short-circuits.\n */\n captureStatic(symbolId: string, source: Container, width: number, height: number): Texture {\n const existing = this._static.get(symbolId);\n if (existing && (!existing.owned || (existing.width === width && existing.height === height))) {\n return existing.texture;\n }\n const texture = this._renderer.generateTexture({\n target: source,\n frame: new Rectangle(0, 0, width, height),\n resolution: this._resolution,\n antialias: true,\n });\n this._put(this._static, symbolId, { texture, owned: true, width, height });\n return texture;\n }\n\n /**\n * Bake a motion-blurred variant of the static snapshot for `symbolId`\n * (which must exist. call `captureStatic` or `setStatic` first) and\n * cache it. The smear runs along `blur.axis` — the reel's travel\n * direction (`'y'` default when called directly; `StaticSpinSymbol`\n * derives it from the set's orientation) — and the\n * result is `2 * padding` larger than the cell on that axis only. Draw\n * it center-anchored at the cell center and the smear extends evenly\n * past the cell on both sides.\n */\n captureBlurred(\n symbolId: string,\n width: number,\n height: number,\n blur?: MotionBlurOptions,\n ): Texture {\n const axis = blur?.axis ?? this._blurDefaults.axis ?? 'y';\n const existing = this._blurred.get(symbolId);\n if (\n existing &&\n (!existing.owned ||\n (existing.width === width && existing.height === height && existing.axis === axis))\n ) {\n return existing.texture;\n }\n const staticTex = this.getStatic(symbolId);\n if (!staticTex) {\n throw new Error(\n `SpinTextureCache.captureBlurred('${symbolId}'): no static texture to blur. ` +\n `Call captureStatic() or setStatic() for this symbolId first.`,\n );\n }\n\n const strength =\n blur?.strength ?? this._blurDefaults.strength ?? (axis === 'y' ? height : width) * 0.2;\n const quality = blur?.quality ?? this._blurDefaults.quality ?? 4;\n const padding = blur?.padding ?? this._blurDefaults.padding ?? Math.ceil(strength);\n\n const wrap = new Container();\n const sprite = new Sprite(staticTex);\n // Fit the source into the cell box in case it's a user-provided static\n // texture with different dimensions.\n sprite.width = width;\n sprite.height = height;\n if (axis === 'y') {\n sprite.y = padding;\n sprite.filters = [new BlurFilter({ strengthX: 0, strengthY: strength, quality })];\n } else {\n sprite.x = padding;\n sprite.filters = [new BlurFilter({ strengthX: strength, strengthY: 0, quality })];\n }\n wrap.addChild(sprite);\n\n const texture = this._renderer.generateTexture({\n target: wrap,\n frame: new Rectangle(\n 0,\n 0,\n width + (axis === 'x' ? padding * 2 : 0),\n height + (axis === 'y' ? padding * 2 : 0),\n ),\n resolution: this._resolution,\n antialias: true,\n });\n // Destroy the scratch scene but not the cached static texture.\n wrap.destroy({ children: true });\n\n this._put(this._blurred, symbolId, { texture, owned: true, width, height, axis });\n return texture;\n }\n\n // -- Invalidation ------------------------------------------------------------\n\n /** Drop (and destroy, if cache-generated) both textures for one symbolId. */\n invalidate(symbolId: string): void {\n this._drop(this._static, symbolId);\n this._drop(this._blurred, symbolId);\n }\n\n /** Drop everything. Call when the cell size changes (responsive relayout). */\n clear(): void {\n for (const id of [...this._static.keys()]) this._drop(this._static, id);\n for (const id of [...this._blurred.keys()]) this._drop(this._blurred, id);\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n this.clear();\n this._isDestroyed = true;\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n // -- Internals ----------------------------------------------------------------\n\n private _put(map: Map<string, CacheEntry>, id: string, entry: CacheEntry): void {\n this._drop(map, id);\n map.set(id, entry);\n }\n\n private _drop(map: Map<string, CacheEntry>, id: string): void {\n const existing = map.get(id);\n if (existing?.owned) existing.texture.destroy(true);\n map.delete(id);\n }\n}\n\nexport interface PrewarmSpinTexturesOptions {\n /** The shared cache to fill. */\n cache: SpinTextureCache;\n /** Every symbolId to bake. */\n ids: string[];\n /**\n * Factory for a scratch symbol used as the render source. Typically the\n * same factory you registered (e.g. `() => new SpineReelSymbol(opts)`).\n * It is activated per id, snapshotted, and destroyed at the end.\n */\n createSymbol: () => ReelSymbol;\n /** Cell width the reels will use. */\n width: number;\n /** Cell height the reels will use. */\n height: number;\n /** Also bake the motion-blur variants. Default: true. */\n blurred?: boolean;\n /** Motion-blur tuning override (defaults to the cache's). */\n blur?: MotionBlurOptions;\n}\n\n/**\n * Bake spin textures for every symbolId up front (at load or between\n * rounds) so the first spin never pays a capture hitch. Ids that already\n * have a user-provided or captured texture are skipped by the cache.\n *\n * ```ts\n * prewarmSpinTextures({\n * cache,\n * ids: ['cherry', 'lemon', 'seven'],\n * createSymbol: () => new SpineReelSymbol(spineOpts),\n * width: 150,\n * height: 150,\n * });\n * ```\n */\nexport function prewarmSpinTextures(options: PrewarmSpinTexturesOptions): void {\n const { cache, ids, width, height } = options;\n const blurred = options.blurred ?? true;\n const symbol = options.createSymbol();\n try {\n for (const id of ids) {\n if (symbol.symbolId !== id) symbol.activate(id);\n symbol.resize(width, height);\n cache.captureStatic(id, symbol.view, width, height);\n if (blurred) cache.captureBlurred(id, width, height, options.blur);\n }\n } finally {\n symbol.destroy();\n }\n}\n","import { Sprite } from 'pixi.js';\nimport type { gsap } from 'gsap';\n\nimport { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { MotionBlurOptions, SpinTextureCache } from './SpinTextureCache.js';\n\nexport interface StaticSpinSymbolOptions {\n /**\n * Factory for the wrapped \"live\" symbol. any ReelSymbol subclass\n * (SpineReelSymbol, AnimatedSpriteSymbol, a custom class). The wrapper\n * owns the instance: it activates it while the reel is at rest and\n * deactivates it while the reel spins.\n */\n createInner: () => ReelSymbol;\n /** Shared spin-texture cache. one per reel set. */\n cache: SpinTextureCache;\n /**\n * What to show while spinning. `'blurred'` (default) crossfades the\n * static snapshot into the baked motion-blur variant; `'static'` shows\n * the crisp snapshot only and never touches the blur pipeline.\n */\n spinTexture?: 'blurred' | 'static';\n /**\n * Crossfade duration (ms) from crisp snapshot to blurred snapshot at\n * spin start. matches the reel's acceleration ramp. `0` = instant.\n * Default: 120. Ignored when `spinTexture: 'static'`.\n */\n blurRampMs?: number;\n /**\n * Motion-blur tuning forwarded to `cache.captureBlurred`. `axis` defaults\n * to the owning set's travel axis, so a horizontal set smears sideways\n * without being told; pass it explicitly only to override that. The\n * sideways travel (and snapshots fit the cell by height instead of width).\n */\n blur?: MotionBlurOptions;\n}\n\n/**\n * Wraps any `ReelSymbol` and swaps it for a cached snapshot texture while\n * the reel spins. \"spin static, not Spine.\"\n *\n * At rest the inner symbol is live (idle loops, win animations, cascade\n * destruction all delegate to it). When the reel starts, the wrapper\n * snapshots the inner symbol through the shared {@link SpinTextureCache}\n * (or reuses a cached / user-provided texture), **deactivates** the inner\n * symbol so it costs nothing, and spins a plain Sprite instead. Symbols\n * that wrap in mid-spin only retarget that sprite's texture. no inner\n * symbol is ever created or ticked for them. On land the inner symbol is\n * reactivated on the final symbolId.\n *\n * With `spinTexture: 'blurred'` the sprite crossfades from the crisp\n * snapshot to a pre-baked motion-blur variant over `blurRampMs`, then\n * spins the blurred texture. The smear follows the reel's travel axis,\n * derived from the set's orientation (override with `blur.axis`). No\n * filter runs during the spin; the blur is baked once per symbolId and\n * cached.\n *\n * Register it like any other symbol:\n *\n * ```ts\n * const cache = new SpinTextureCache({ renderer: app.renderer });\n * builder.symbols((r) => {\n * for (const id of ids) {\n * r.register(id, StaticSpinSymbol, {\n * createInner: () => new SpineReelSymbol(spineOpts),\n * cache,\n * });\n * }\n * });\n * ```\n *\n * Prefer calling {@link prewarmSpinTextures} at load time so the first\n * spin doesn't pay the one-time capture cost.\n */\nexport class StaticSpinSymbol extends ReelSymbol {\n private _inner: ReelSymbol;\n private _cache: SpinTextureCache;\n private _mode: 'blurred' | 'static';\n private _rampMs: number;\n private _blurOpts: MotionBlurOptions | undefined;\n\n private _staticSprite: Sprite;\n private _blurSprite: Sprite;\n private _spinning = false;\n private _anticipating = false;\n private _cellW = 0;\n private _cellH = 0;\n private _rampTween: gsap.core.Tween | null = null;\n\n constructor(options: StaticSpinSymbolOptions) {\n super();\n this._inner = options.createInner();\n this._cache = options.cache;\n this._mode = options.spinTexture ?? 'blurred';\n this._rampMs = options.blurRampMs ?? 120;\n this._blurOpts = options.blur;\n\n this.view.addChild(this._inner.view);\n this._staticSprite = new Sprite();\n this._staticSprite.anchor.set(0.5, 0.5);\n this._staticSprite.visible = false;\n this._blurSprite = new Sprite();\n this._blurSprite.anchor.set(0.5, 0.5);\n this._blurSprite.visible = false;\n this.view.addChild(this._staticSprite, this._blurSprite);\n }\n\n /** The wrapped live symbol. for type-specific access (`inner as SpineReelSymbol`). */\n get inner(): ReelSymbol {\n return this._inner;\n }\n\n /** True while the wrapper is showing a snapshot instead of the live symbol. */\n get isShowingSnapshot(): boolean {\n return this._spinning;\n }\n\n protected onActivate(symbolId: string): void {\n if (this._spinning) {\n // Wrapped in mid-spin: never touch the inner symbol, just retarget\n // the snapshot sprite (instantly blurred. the ramp belongs to spin\n // start, not to cells joining a reel already at speed).\n this._showSnapshot(symbolId, { instant: true });\n return;\n }\n if (this._inner.symbolId !== symbolId) this._inner.activate(symbolId);\n this._hideSnapshot();\n }\n\n protected onDeactivate(): void {\n this._killRamp();\n this._spinning = false;\n this._anticipating = false;\n this._staticSprite.visible = false;\n this._blurSprite.visible = false;\n if (this._inner.symbolId !== '') this._inner.deactivate();\n }\n\n override onReelSpinStart(joinedMidSpin = false): void {\n if (this._spinning) {\n // Idempotent: a repeat notification (same-id fast path during a\n // wrap) only refreshes the snapshot target.\n this._showSnapshot(this.symbolId, { instant: true });\n return;\n }\n this._spinning = true;\n this._anticipating = false;\n // Capture while the inner symbol is still live so a cache miss can\n // snapshot it in place, then put the inner symbol to sleep for the\n // duration of the spin.\n this._showSnapshot(this.symbolId, { instant: joinedMidSpin || this._rampMs <= 0 });\n if (this._inner.symbolId !== '') this._inner.deactivate();\n }\n\n /**\n * The reel entered its anticipation tease: it is slow enough to read, so\n * crossfade the baked blur back out and ride the crisp snapshot until\n * the land. Mid-tease installs arrive blurred (`_showSnapshot` instant)\n * and immediately relax through this same hook.\n */\n override onReelAnticipationStart(): void {\n if (!this._spinning || this._anticipating) return;\n this._anticipating = true;\n if (this._mode === 'static') return;\n\n this._killRamp();\n this._staticSprite.visible = true;\n this._staticSprite.alpha = 1;\n if (!this._blurSprite.visible || this._rampMs <= 0) {\n this._blurSprite.visible = false;\n return;\n }\n this._rampTween = this.gsap.to(this._blurSprite, {\n alpha: 0,\n duration: this._rampMs / 1000,\n ease: 'power1.out',\n onComplete: () => {\n this._rampTween = null;\n this._blurSprite.visible = false;\n },\n });\n }\n\n override onReelSpinEnd(): void {\n if (!this._spinning) return;\n this._spinning = false;\n this._anticipating = false;\n this._killRamp();\n this._staticSprite.visible = false;\n this._blurSprite.visible = false;\n if (this._inner.symbolId !== this.symbolId) {\n this._inner.activate(this.symbolId);\n this._inner.resize(this._cellW, this._cellH);\n }\n }\n\n override onReelLanded(): void {\n if (!this._spinning) this._inner.onReelLanded();\n }\n\n async playWin(): Promise<void> {\n return this._inner.playWin();\n }\n\n stopAnimation(): void {\n if (this._inner.symbolId !== '') this._inner.stopAnimation();\n }\n\n override async playDestroy(opts?: { delay?: number; signal?: AbortSignal }): Promise<void> {\n // Landed symbols delegate so art-specific destruction (Spine `out`\n // tracks etc.) plays; a snapshot mid-spin falls back to the generic\n // implode on the wrapper view.\n if (!this._spinning && this._inner.symbolId !== '') return this._inner.playDestroy(opts);\n return super.playDestroy(opts);\n }\n\n resize(width: number, height: number): void {\n this._cellW = width;\n this._cellH = height;\n this._staticSprite.position.set(width / 2, height / 2);\n this._blurSprite.position.set(width / 2, height / 2);\n this._fitSprites();\n this._inner.resize(width, height);\n }\n\n protected override onDestroy(): void {\n this._killRamp();\n // Detach before destroying so the base class's view.destroy({children})\n // doesn't double-destroy the inner view.\n if (this._inner.view.parent === this.view) this.view.removeChild(this._inner.view);\n this._inner.destroy();\n }\n\n // -- Internals -------------------------------------------------------------\n\n /**\n * Point the snapshot sprites at `symbolId`'s cached textures, capturing\n * on miss. With `instant`, the end state is applied directly (blurred\n * fully visible); otherwise the crisp→blur crossfade ramp starts.\n */\n private _showSnapshot(symbolId: string, opts: { instant: boolean }): void {\n const staticTex = this._ensureStatic(symbolId);\n this._staticSprite.texture = staticTex;\n\n if (this._mode === 'blurred') {\n this._blurSprite.texture = this._cache.captureBlurred(\n symbolId,\n this._cellW,\n this._cellH,\n this._resolvedBlur(),\n );\n }\n this._fitSprites();\n\n if (this._mode === 'static') {\n this._staticSprite.visible = true;\n this._staticSprite.alpha = 1;\n return;\n }\n\n if (opts.instant) {\n // Anticipating: the tease shows the crisp snapshot. never bring the\n // blur back for re-notifications or same-id wraps mid-tease.\n if (this._anticipating) {\n this._killRamp();\n this._blurSprite.visible = false;\n this._staticSprite.visible = true;\n this._staticSprite.alpha = 1;\n return;\n }\n // Already ramping? Let the running crossfade finish on the new\n // textures instead of snapping mid-fade.\n if (this._rampTween) return;\n this._staticSprite.visible = false;\n this._blurSprite.visible = true;\n this._blurSprite.alpha = 1;\n return;\n }\n\n this._killRamp();\n this._staticSprite.visible = true;\n this._staticSprite.alpha = 1;\n this._blurSprite.visible = true;\n this._blurSprite.alpha = 0;\n this._rampTween = this.gsap.to(this._blurSprite, {\n alpha: 1,\n duration: this._rampMs / 1000,\n ease: 'power1.in',\n onComplete: () => {\n this._rampTween = null;\n this._staticSprite.visible = false;\n },\n });\n }\n\n /**\n * Static texture for `symbolId`, capturing from the inner symbol on a\n * cache miss. If the inner symbol currently holds a different identity\n * (mid-spin wrap), it is briefly activated offscreen for the capture and\n * deactivated again. one-time cost per symbolId; avoid it entirely with\n * `prewarmSpinTextures`.\n */\n private _ensureStatic(symbolId: string) {\n const cached = this._cache.getStatic(symbolId);\n if (cached) return cached;\n\n const needsTempActivation = this._inner.symbolId !== symbolId;\n if (needsTempActivation) {\n this._inner.activate(symbolId);\n this._inner.resize(this._cellW, this._cellH);\n }\n const tex = this._cache.captureStatic(symbolId, this._inner.view, this._cellW, this._cellH);\n if (needsTempActivation && this._spinning) this._inner.deactivate();\n return tex;\n }\n\n private _hideSnapshot(): void {\n this._killRamp();\n this._staticSprite.visible = false;\n this._blurSprite.visible = false;\n }\n\n /**\n * Uniformly fit both sprites to the cell along the axis the blur does\n * NOT pad (keeps aspect; the padded axis then centers out evenly past\n * the cell). Vertical reels fit by width; horizontal strips fit by\n * height.\n */\n private _fitSprites(): void {\n if (this._cellW <= 0 || this._cellH <= 0) return;\n const fitByHeight = this._resolvedBlur().axis === 'x';\n for (const sprite of [this._staticSprite, this._blurSprite]) {\n const tw = sprite.texture.width;\n const th = sprite.texture.height;\n if (tw <= 0 || th <= 0) continue;\n sprite.scale.set(fitByHeight ? this._cellH / th : this._cellW / tw);\n }\n }\n\n /**\n * Blur options with the smear axis resolved.\n *\n * The smear must follow the strip, so the default comes from the owning\n * set's orientation rather than being hardcoded to `'y'`. Before this,\n * a horizontal set using `StaticSpinSymbol` smeared vertically - across\n * the direction of travel - and nothing said so. An explicit\n * `blur.axis` still wins, for art that wants a deliberate cross-smear.\n */\n private _resolvedBlur(): MotionBlurOptions {\n return { ...this._blurOpts, axis: this._blurOpts?.axis ?? this.mainAxis };\n }\n\n private _killRamp(): void {\n if (this._rampTween) {\n this._rampTween.kill();\n this._rampTween = null;\n }\n }\n}\n","import { Graphics, Text } from 'pixi.js';\nimport { ReelSymbol } from './ReelSymbol.js';\n\n/**\n * **Debug / prototyping symbol - NOT for production.**\n *\n * `CardSymbol` is a flat-rectangle-plus-centered-text `ReelSymbol` drawn\n * entirely with `PIXI.Graphics`. It exists to give recipes, demos, and\n * mechanic tests a no-asset, no-loader, infinitely-resizable visual that\n * always renders crisply at any cell size, showing how the engine treats\n * cell space across MultiWays reshapes, big-symbol blocks, and pyramid\n * layouts.\n *\n * In a real game, ship one of:\n *\n * - `SpriteSymbol` - pre-rendered art at one resolution. Cheapest.\n * - `AnimatedSpriteSymbol` - frame-by-frame win/idle animation.\n * - `SpineSymbol` (via `pixi-reels/spine`) - vector skeletal animation\n * that scales without quality loss. Best for MultiWays where cells\n * resize across spins.\n * - Or a custom `ReelSymbol` subclass for game-specific visuals.\n *\n * Ships from the package so a prototype runs with no art at all. It is\n * deliberately plain `Graphics`: swap it for real art before you ship.\n *\n * ```ts\n * import { CardSymbol, CARD_DECK } from 'pixi-reels';\n *\n * builder.symbols((registry) => {\n * for (const card of CARD_DECK) {\n * registry.register(card.id, CardSymbol, { color: card.color, label: card.label });\n * }\n * });\n * ```\n */\nexport interface CardSymbolOptions {\n /** Hex fill color (e.g. `0xc0392b`). */\n color: number;\n /** Centered label text (e.g. `'A'`, `'10'`, `'WILD'`). */\n label: string;\n /** Text fill color. Defaults to white. */\n textColor?: number;\n}\n\nexport class CardSymbol extends ReelSymbol {\n private _color: number;\n private _label: string;\n private _textColor: number;\n private _gfx: Graphics;\n private _text: Text;\n\n constructor(opts: CardSymbolOptions) {\n super();\n this._color = opts.color;\n this._label = opts.label;\n this._textColor = opts.textColor ?? 0xffffff;\n this._gfx = new Graphics();\n this._text = new Text({\n text: this._label,\n style: {\n // Roboto Condensed if installed (the site loads it via @fontsource);\n // falls through to a system condensed sans-serif on plain pages.\n fontFamily:\n '\"Roboto Condensed\", \"Arial Narrow\", \"Helvetica Neue Condensed\", \"Liberation Sans Narrow\", system-ui, sans-serif',\n fontSize: 32,\n fontWeight: '700',\n fill: this._textColor,\n align: 'center',\n },\n });\n this._text.anchor.set(0.5);\n this.view.addChild(this._gfx);\n this.view.addChild(this._text);\n }\n\n protected onActivate(_symbolId: string): void {\n // No-op - this symbol's visual identity is set at construction.\n }\n\n protected onDeactivate(): void {\n // No-op - leave _gfx/_text in their last state until next activate.\n }\n\n async playWin(): Promise<void> {\n return new Promise((resolve) => {\n // Animate only the glyph - the card body stays put. Useful for big\n // symbols (the rectangle is the cell footprint) and any setting\n // where movement of the cell border is distracting (cascade refills,\n // MultiWays adjust). The text is anchored at (0.5, 0.5) so its\n // scale tween pivots about the centre.\n this.gsap.killTweensOf(this._text);\n this.gsap.killTweensOf(this._text.scale);\n const originalFill = this._textColor;\n const tl = this.gsap.timeline({\n onComplete: () => {\n this._text.scale.set(1, 1);\n this._text.rotation = 0;\n this._text.style.fill = originalFill;\n resolve();\n },\n });\n tl.to(this._text.scale, { x: 1.4, y: 1.4, duration: 0.18, ease: 'back.out(2)' }, 0)\n .to(this._text, { rotation: -0.12, duration: 0.09, ease: 'sine.inOut' }, 0)\n .to(this._text, { rotation: 0.12, duration: 0.18, ease: 'sine.inOut' }, 0.09)\n .to(this._text, { rotation: 0, duration: 0.09, ease: 'sine.inOut' }, 0.27)\n .to(this._text.scale, { x: 1, y: 1, duration: 0.18, ease: 'power2.out' }, 0.32);\n // Glyph fill flashes warm gold mid-pulse - `Text.style.fill` isn't\n // a tweenable target on every Pixi version, so we set it discretely.\n this.gsap.delayedCall(0.06, () => (this._text.style.fill = 0xffe168));\n this.gsap.delayedCall(0.42, () => (this._text.style.fill = originalFill));\n });\n }\n\n stopAnimation(): void {\n this.gsap.killTweensOf(this._text);\n this.gsap.killTweensOf(this._text.scale);\n this._text.scale.set(1, 1);\n this._text.rotation = 0;\n this._text.style.fill = this._textColor;\n this._gfx.alpha = 1;\n }\n\n resize(width: number, height: number): void {\n this._gfx.clear();\n this._gfx.rect(0, 0, width, height).fill({ color: this._color });\n // Inner darker stroke so adjacent cells are visually separated even\n // when they share the same color.\n this._gfx.rect(1, 1, width - 2, height - 2).stroke({ color: 0x000000, width: 2, alpha: 0.25 });\n this._text.x = width / 2;\n this._text.y = height / 2;\n // Font size constrained by both height (~38%) and label width (so\n // multi-char labels like '10' or 'WILD' don't overflow narrow cells).\n // Smaller than half-cell to leave breathing room - looks less crowded.\n const labelLen = Math.max(1, this._label.length);\n const fitH = height * 0.38;\n // Roboto Condensed glyphs are narrower (~0.45 avg ratio) than serif fonts.\n const fitW = (width * 0.7) / (labelLen * 0.45);\n this._text.style.fontSize = Math.max(7, Math.floor(Math.min(fitH, fitW)));\n }\n}\n\n/**\n * Standard high-card deck for prototyping. Each card has its own color\n * so a full grid is visually unambiguous at a glance.\n */\nexport const CARD_DECK: ReadonlyArray<{ id: string; color: number; label: string }> = [\n { id: '7', color: 0xc0392b, label: '7' }, // crimson\n { id: '8', color: 0xe67e22, label: '8' }, // orange\n { id: '9', color: 0xf1c40f, label: '9' }, // amber\n { id: '10', color: 0x27ae60, label: '10' }, // green\n { id: 'J', color: 0x16a085, label: 'J' }, // teal\n { id: 'Q', color: 0x2980b9, label: 'Q' }, // blue\n { id: 'K', color: 0x8e44ad, label: 'K' }, // purple\n { id: 'A', color: 0x2c3e50, label: 'A' }, // navy\n];\n\n/** A pale-yellow `WILD` card for sticky/expanding-wild prototypes. */\nexport const WILD_CARD = {\n id: 'wild',\n color: 0xfff3a0,\n label: 'WILD',\n textColor: 0x6b5400,\n} as const;\n","import type { ColumnTarget } from '../frame/ColumnTarget.js';\n\nexport interface ScatterAnticipationOptions {\n /** The trigger symbol id to scan for (e.g. `'SCAT'`). */\n symbol: string;\n /**\n * How many of `symbol` must be showing before anticipation kicks in.\n * Anticipation begins on the reel AFTER the one that lands the\n * `trigger`-th symbol. Default `2` (the classic \"2 scatters and the rest\n * of the reels start teasing\").\n */\n trigger?: number;\n /**\n * Which of the remaining reels should tease:\n * - `'all-remaining'` (default): every reel after the trigger reel teases,\n * whether or not it actually holds the symbol. Maximum tension.\n * - `'scatter-only'`: only reels that actually contain the symbol tease.\n * Use when a result can't complete the feature on some reels and you\n * don't want to fake tension on reels that don't matter (e.g. a grid\n * with only 3 scatters total shouldn't slow the empty reels between/after\n * them).\n */\n mode?: 'all-remaining' | 'scatter-only';\n}\n\n/**\n * Decide which reels should show anticipation, straight from a result grid.\n *\n * Scans reels left→right counting `symbol` occurrences; once the running total\n * reaches `trigger`, the reels AFTER that point become the anticipation set\n * (per `mode`). Feed the return value straight into\n * `reelSet.setAnticipation(...)`.\n *\n * Returns an empty array when the grid never reaches `trigger` (no tease).\n *\n * `grid` is the same `ColumnTarget[]` you pass to `reelSet.setResult(...)`, so\n * you can hand the result straight through. Only visible cells are scanned.\n * buffer symbols are off-screen and never trigger tension.\n *\n * @example\n * // Result has SCAT on reels 0 and 1 → tease reels [2,3,4].\n * const grid = [{ visible: ['A','SCAT','B'] }, { visible: ['SCAT','A','B'] }, ...];\n * const reels = anticipationForScatters(grid, { symbol: 'SCAT', trigger: 2 });\n * reelSet.setResult(grid);\n * reelSet.setAnticipation(reels, { stagger: 'sequential', slowdown: { from: 0.4, to: 0.1 } });\n *\n * @example\n * // Only 3 scatters total (reels 0,1,3): 'scatter-only' teases just reel [3],\n * // leaving reels 2 and 4 to stop normally.\n * const reels = anticipationForScatters(grid, { symbol: 'SCAT', mode: 'scatter-only' });\n */\nexport function anticipationForScatters(\n grid: ColumnTarget[],\n opts: ScatterAnticipationOptions,\n): number[] {\n const trigger = opts.trigger ?? 2;\n const mode = opts.mode ?? 'all-remaining';\n\n // Count the trigger symbol per reel. Only visible cells are scanned; buffer\n // symbols are off-screen and must not trigger tension.\n const perReelCount = grid.map(\n (reel) => reel.visible.filter((id) => id === opts.symbol).length,\n );\n\n // Find the reel at which the running total first reaches `trigger`.\n let running = 0;\n let triggerReel = -1;\n for (let i = 0; i < perReelCount.length; i++) {\n running += perReelCount[i];\n if (running >= trigger) {\n triggerReel = i;\n break;\n }\n }\n if (triggerReel === -1) return [];\n\n const result: number[] = [];\n for (let i = triggerReel + 1; i < grid.length; i++) {\n if (mode === 'all-remaining' || perReelCount[i] > 0) result.push(i);\n }\n return result;\n}\n","import type { SpinningMode } from './SpinningMode.js';\n\n/**\n * Cascade/tumble spinning mode.\n * Symbols fall from above with gravity-like acceleration,\n * used for tumble/avalanche mechanics.\n */\nexport class CascadeMode implements SpinningMode {\n readonly name = 'cascade';\n\n private _gravity: number;\n\n /**\n * @param gravity - Gravity acceleration factor. Default: 1.5.\n */\n constructor(gravity: number = 1.5) {\n this._gravity = gravity;\n }\n\n computeDelta(slotPitch: number, speed: number, deltaMs: number): number {\n const raw = (slotPitch * speed * this._gravity * deltaMs) / 1000;\n // Full-slot cap. The old wrap-skip risk (contract L7) is gone now that\n // ReelMotion derives rotation from total travel, so this only bounds the\n // per-frame step; cascade speed is never negative.\n return Math.min(raw, slotPitch);\n }\n}\n","import type { SpinningMode } from './SpinningMode.js';\n\n/**\n * Immediate placement mode for super-turbo skip.\n * Symbols snap to position instantly. no actual spinning animation.\n */\nexport class ImmediateMode implements SpinningMode {\n readonly name = 'immediate';\n\n computeDelta(_slotPitch: number, _speed: number, _deltaMs: number): number {\n // No movement. placement is handled directly by the phase\n return 0;\n }\n}\n","import { Container, Graphics } from 'pixi.js';\nimport type { Ticker } from 'pixi.js';\nimport { ReelSetBuilder } from '../core/ReelSetBuilder.js';\nimport type { Direction, Orientation } from '../core/ReelAxis.js';\nimport type { ReelSet } from '../core/ReelSet.js';\nimport { SharedRectMaskStrategy } from '../core/ReelViewport.js';\nimport type { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { SymbolRegistry } from '../symbols/SymbolRegistry.js';\nimport { EmptySymbol } from '../symbols/EmptySymbol.js';\nimport { SpeedPresets } from '../config/SpeedPresets.js';\nimport type { SpeedProfile, SymbolData } from '../config/types.js';\nimport type { Disposable } from '../utils/Disposable.js';\n\n/** A cell coordinate in the grid. */\nexport interface BoardCell {\n reel: number;\n cell: number;\n}\n\n/** A landing target: spin `cell` and stop it showing `id`. */\nexport interface BoardSpinTarget {\n cell: BoardCell;\n id: string;\n}\n\n/** A speed profile, or a per-cell function of one (e.g. a stagger wave). */\nexport type BoardProfile = SpeedProfile | ((cell: BoardCell) => SpeedProfile);\n\nexport interface BoardGridOptions {\n /** Grid dimensions. */\n cols: number;\n rows: number;\n /** Cell edge length in pixels. */\n cellSize: number;\n /** Gap between cells. Default 4. */\n gap?: number;\n /** Id a cell shows when blank - also placed in the off-window buffers. Default `'empty'`. */\n emptyId?: string;\n /** Register symbol classes, exactly like `ReelSetBuilder.symbols`. Applied to every cell. */\n symbols: (registry: SymbolRegistry) => void;\n /** Strip weights during the spin. */\n weights?: Record<string, number>;\n /** Per-symbol engine overrides, exactly like `ReelSetBuilder.symbolData`. */\n symbolData?: Record<string, Partial<SymbolData>>;\n /** Injected RNG for the spin strips (deterministic demos / tests). */\n rng?: () => number;\n /** Drives every cell's reel - required. */\n ticker: Ticker;\n /** Per-cell background, drawn behind each reel. */\n chrome?: (g: Graphics, size: number) => void;\n /**\n * Which way each cell's own strip travels while it spins. Every cell is a\n * 1x1 reel set, so this changes the direction a symbol scrolls in from, not\n * the board layout - `cols` and `rows` stay board dimensions either way.\n * Defaults to the engine default (vertical / forward), i.e. symbols drop in\n * from above.\n */\n orientation?: Orientation;\n direction?: Direction;\n /**\n * Named speed profiles, each registered on every cell and selected by name\n * via {@link BoardGrid.setProfile}. A value may be a flat profile or a\n * per-cell function (for stagger waves). Defaults to a single `'default'`\n * profile, which is the active one until you `setProfile` otherwise.\n */\n profiles?: Record<string, BoardProfile>;\n}\n\nconst key = (c: BoardCell): string => `${c.reel},${c.cell}`;\nconst DEFAULT_PROFILE = 'default';\n\n/**\n * A grid of cells that each spin **independently** - the generic \"board of\n * reels\" primitive. Every cell is its own 1×1 {@link ReelSet}, so it inherits\n * the engine's phases, speed modes and pooling rather than a parallel lighter\n * reel.\n *\n * Deliberately mechanism-only: it knows nothing about coins, locks, respins,\n * value or any game rule. It lays the grid out, hands back per-cell geometry\n * and live symbol instances, places symbols instantly, and spins a\n * **caller-chosen** set of cells to caller-chosen results. Build your own\n * feature on top by owning the rules in your own code; {@link HoldAndWinBoard}\n * is one such opinionated layer, built entirely on this public surface.\n *\n * ```ts\n * const grid = new BoardGrid({\n * cols: 3, rows: 3, cellSize: 80,\n * symbols: (r) => r.register('prize', PrizeSymbol, {}),\n * weights: { prize: 1, empty: 4 },\n * ticker: app.ticker,\n * });\n * app.stage.addChild(grid.container);\n *\n * await grid.spinCells(\n * grid.cells().map((cell) => ({ cell, id: pick() })), // you decide each result\n * (cell, id) => console.log('landed', cell, id), // react as each settles\n * );\n * ```\n */\nexport class BoardGrid implements Disposable {\n readonly container: Container;\n readonly cols: number;\n readonly rows: number;\n readonly cellSize: number;\n readonly gap: number;\n readonly emptyId: string;\n\n private readonly _reels = new Map<string, ReelSet>();\n private readonly _cells: BoardCell[] = [];\n private _destroyed = false;\n\n constructor(opts: BoardGridOptions) {\n if (!opts.ticker) throw new Error('BoardGrid: a ticker is required.');\n this.cols = opts.cols;\n this.rows = opts.rows;\n this.cellSize = opts.cellSize;\n this.gap = opts.gap ?? 4;\n this.emptyId = opts.emptyId ?? 'empty';\n this.container = new Container();\n\n const profiles: Record<string, BoardProfile> =\n opts.profiles && Object.keys(opts.profiles).length > 0\n ? opts.profiles\n : { [DEFAULT_PROFILE]: { ...SpeedPresets.NORMAL, minimumSpinTime: 320 } };\n const profileNames = Object.keys(profiles);\n const profileFor = (p: BoardProfile, cell: BoardCell): SpeedProfile =>\n typeof p === 'function' ? p(cell) : p;\n\n for (let reel = 0; reel < opts.cols; reel++) {\n for (let rowIdx = 0; rowIdx < opts.rows; rowIdx++) {\n const cell: BoardCell = { reel, cell: rowIdx };\n const origin = this._origin(cell);\n if (opts.chrome) {\n const bg = new Graphics();\n opts.chrome(bg, this.cellSize);\n bg.position.set(origin.x, origin.y);\n this.container.addChild(bg);\n }\n\n const builder = new ReelSetBuilder()\n .reels(1)\n .visibleCells(1)\n .symbolSize(this.cellSize, this.cellSize)\n .symbolGap(0, 0)\n // Spine symbols overrun the default per-reel rect mask; a shared rect\n // keeps buffer-cell art from painting over neighbouring cells.\n .maskStrategy(new SharedRectMaskStrategy())\n .symbols((registry) => {\n opts.symbols(registry);\n if (!registry.has(this.emptyId)) registry.register(this.emptyId, EmptySymbol, {});\n })\n .initialFrame([\n { visible: [this.emptyId], bufferStart: [this.emptyId], bufferEnd: [this.emptyId] },\n ])\n .ticker(opts.ticker)\n .orientation(opts.orientation ?? 'vertical')\n .direction(opts.direction ?? 'forward')\n // The active profile defaults to the engine's 'normal'; point it at\n // the first registered name so any profile vocabulary works.\n .initialSpeed(profileNames[0]);\n for (const [name, profile] of Object.entries(profiles)) {\n builder.speed(name, profileFor(profile, cell));\n }\n if (opts.weights) builder.weights(opts.weights);\n if (opts.symbolData) builder.symbolData(opts.symbolData);\n if (opts.rng) builder.rng(opts.rng);\n\n const reelSet = builder.build();\n reelSet.position.set(origin.x, origin.y);\n this.container.addChild(reelSet);\n this._reels.set(key(cell), reelSet);\n this._cells.push(cell);\n }\n }\n }\n\n /** Every cell coordinate, reel-major: (0,0), (0,1), ... then (1,0). */\n cells(): BoardCell[] {\n return this._cells.map((c) => ({ reel: c.reel, cell: c.cell }));\n }\n\n /** Board-local bounds of a cell. `container.toGlobal` for stage space. */\n cellBounds(cell: BoardCell): { x: number; y: number; width: number; height: number } {\n const origin = this._origin(cell);\n return { x: origin.x, y: origin.y, width: this.cellSize, height: this.cellSize };\n }\n\n /** Board-local center of a cell - flight / trail start and end points. */\n cellCenter(cell: BoardCell): { x: number; y: number } {\n const origin = this._origin(cell);\n return { x: origin.x + this.cellSize / 2, y: origin.y + this.cellSize / 2 };\n }\n\n /** Live symbol instance currently shown in a cell. */\n symbolAt(cell: BoardCell): ReelSymbol {\n return this._reel(cell).getReel(0).getSymbolAt(0);\n }\n\n /** The cell's underlying 1×1 ReelSet, for driving one cell directly. */\n reelAt(cell: BoardCell): ReelSet {\n return this._reel(cell);\n }\n\n /** Select a registered speed profile by name for one cell. */\n setProfile(cell: BoardCell, name: string): void {\n this._reel(cell).speed.set(name);\n }\n\n /** Place a symbol instantly (no spin), with blank off-window buffers. */\n place(cell: BoardCell, id: string): void {\n // Explicit empties in both buffers keep the rest state from\n // random-filling past the mask.\n this._reel(cell).getReel(0).placeSymbols({\n visible: [id],\n bufferStart: [this.emptyId],\n bufferEnd: [this.emptyId],\n });\n }\n\n /**\n * Spin each target cell and stop it showing its `id`; `onLanded` fires per\n * cell as it settles, in stagger order. The caller selects which cells spin\n * and to what - this layer applies no lock/free policy of its own. Set\n * profiles via {@link setProfile} first.\n *\n * `onLanded` may be **async**: if it returns a promise, that cell's task\n * awaits it, so the returned promise resolves only once every cell has landed\n * *and* its after-land work has finished. Cells still run concurrently, so an\n * early cell's reveal overlaps with later cells still spinning.\n */\n async spinCells(\n targets: BoardSpinTarget[],\n onLanded: (cell: BoardCell, id: string) => void | Promise<void> = () => {},\n ): Promise<void> {\n await Promise.all(\n targets.map(async ({ cell, id }) => {\n const reelSet = this._reel(cell);\n const settle = reelSet.spin();\n // Buffers land empty too: off-window art can paint over neighbours.\n reelSet.setResult([\n { visible: [id], bufferStart: [this.emptyId], bufferEnd: [this.emptyId] },\n ]);\n await settle;\n await onLanded(cell, id);\n }),\n );\n }\n\n /** Slam every in-flight cell to its landed position. Returns the count. */\n skipSpinning(): number {\n let inFlight = 0;\n for (const reelSet of this._reels.values()) {\n if (reelSet.isSpinning) {\n inFlight += 1;\n try {\n reelSet.skipSpin();\n } catch {\n /* result not provided yet - nothing to skip to; ignore */\n }\n }\n }\n return inFlight;\n }\n\n get isDestroyed(): boolean {\n return this._destroyed;\n }\n\n destroy(): void {\n if (this._destroyed) return;\n this._destroyed = true;\n for (const reelSet of this._reels.values()) reelSet.destroy();\n this._reels.clear();\n this._cells.length = 0;\n this.container.destroy({ children: true });\n }\n\n private _origin(cell: BoardCell): { x: number; y: number } {\n return {\n x: cell.reel * (this.cellSize + this.gap),\n y: cell.cell * (this.cellSize + this.gap),\n };\n }\n\n private _reel(cell: BoardCell): ReelSet {\n const reelSet = this._reels.get(key(cell));\n if (!reelSet) {\n throw new Error(`BoardGrid: cell ${key(cell)} is outside the ${this.cols}x${this.rows} grid.`);\n }\n return reelSet;\n }\n}\n","/**\n * Public types for the Hold & Win board. Kept in one leaf module so the\n * reducer ({@link HoldAndWinState}), the driver ({@link HoldAndWinBoard}) and\n * the builder can share them without import cycles.\n */\n\n/** Grid coordinate of a board cell. */\nexport interface HwCell {\n reel: number;\n cell: number;\n}\n\n/**\n * A coin somewhere on the board. `id` selects the registered symbol art;\n * `data` is an opaque game-layer payload the board never interprets.\n *\n * **Ownership contract:** `cell` and `id` belong to the board - treat them as\n * read-only; the board keys its ledger by cell and mutating them is undefined.\n * `data` is yours: mutate its contents freely (a doubler does `coin.data.value\n * *= 2`), that is the intended way to carry live game state on a locked coin.\n */\nexport interface HwCoin<TData = unknown> {\n readonly cell: HwCell;\n readonly id: string;\n data?: TData;\n}\n\n/** Why the respin counter changed - disambiguates a `respins:changed` event. */\nexport type HwRespinReason = 'seed' | 'hit-reset' | 'miss';\n\n/** Resolution of one {@link HoldAndWinBoard.respin} wave. */\nexport interface HwRespinResult<TData = unknown> {\n round: number;\n hits: HwCoin<TData>[];\n respinsLeft: number;\n full: boolean;\n /** True when the feature is over (counter exhausted or board full). */\n done: boolean;\n}\n\nexport type HoldAndWinBoardEvents<TData = unknown> = {\n 'feature:enter': [{ seed: HwCoin<TData>[]; respins: number }];\n 'respin:start': [{ round: number; respinsLeft: number; spinning: HwCell[] }];\n /** Fires per cell, in landing (stagger) order. `coin` is null on a miss. */\n 'cell:landed': [{ cell: HwCell; coin: HwCoin<TData> | null }];\n 'coin:locked': [{ coin: HwCoin<TData>; locked: number; capacity: number }];\n /** Fired by `release()` - the collect / fly-away moment. */\n 'coin:released': [{ coin: HwCoin<TData>; remaining: number }];\n 'respins:changed': [{ value: number; reason: HwRespinReason }];\n 'respin:end': [{ round: number; hits: HwCoin<TData>[]; respinsLeft: number }];\n 'board:full': [{ coins: HwCoin<TData>[] }];\n /** Fired by `skip()` so the game layer can cut its own flights / collect short. */\n 'feature:skip': [{ inFlight: number }];\n /**\n * Fired by `reset()` - a hard clear back to idle. Distinct from `coin:released`\n * (which means \"collect this coin\"); listeners that maintain derived state\n * from events (HUD totals, meters) clear it here without triggering collect.\n */\n 'feature:reset': [{ clearedCoins: number }];\n 'feature:end': [{ coins: HwCoin<TData>[]; rounds: number; full: boolean }];\n};\n\n/**\n * One state-change the reducer ({@link HoldAndWinState}) decided, ready for the\n * driver to emit. A tagged pair of `{ type, payload }` for every board event the\n * reducer owns - the driver replays them onto its emitter and keys visual side\n * effects (e.g. `playWin()` on `coin:locked`) off the type. `respin:start` and\n * `feature:skip` are driver-owned (they describe in-flight reels, not ledger\n * state) and are not produced here.\n */\nexport type HwEffect<TData = unknown> = {\n [K in keyof HoldAndWinBoardEvents<TData>]: {\n type: K;\n payload: HoldAndWinBoardEvents<TData>[K][0];\n };\n}[keyof HoldAndWinBoardEvents<TData>];\n\nexport interface HwCellSizeOptions {\n gap?: number;\n}\n\n/** `reel,cell` string key for the board's cell-indexed maps. */\nexport const cellKey = (c: HwCell): string => `${c.reel},${c.cell}`;\n","import type { HwCell, HwCoin, HwEffect, HwRespinReason } from './HwTypes.js';\nimport { cellKey } from './HwTypes.js';\n\nexport type HwPhase = 'idle' | 'active' | 'spinning';\n\n/**\n * The pure Hold & Win state machine - the single source of truth for board\n * state, with **zero** PixiJS. It owns the locked-coin ledger, the respin\n * counter, the round number and the feature {@link HwPhase}; every derived\n * value (`freeCells`, `isFull`) is computed from the one ledger, never stored\n * in parallel, so nothing can drift out of sync.\n *\n * It is a *reducer, not a cache*: {@link HoldAndWinBoard} drives the reels and\n * reports each landing here; the methods mutate the ledger and **return the\n * ordered list of {@link HwEffect}s to emit**, which the driver replays onto its\n * event emitter (and uses to fire visual side effects like `playWin()`). Locks\n * happen progressively as cells land in stagger order, so this is an\n * *incremental* reducer ({@link beginWave} → N×{@link land} → {@link endWave}),\n * not a one-shot `reduce(state, hits)`.\n *\n * Being PixiJS-free, it is fully unit-testable: assert the returned effect\n * sequence for hit / miss / full / release / reset / error paths.\n */\nexport class HoldAndWinState<TData = unknown> {\n private readonly _locked = new Map<string, HwCoin<TData>>();\n private readonly _cellSet: Set<string>;\n private readonly _allCells: HwCell[];\n private readonly _defaultRespins: number;\n private _respinsLeft = 0;\n private _round = 0;\n private _phase: HwPhase = 'idle';\n private _waveLanded: HwCoin<TData>[] = [];\n\n constructor(allCells: HwCell[], defaultRespins: number) {\n this._allCells = allCells;\n this._cellSet = new Set(allCells.map(cellKey));\n this._defaultRespins = defaultRespins;\n }\n\n // ── Queries ──────────────────────────────────────────────────────────\n\n get phase(): HwPhase {\n return this._phase;\n }\n get respinsLeft(): number {\n return this._respinsLeft;\n }\n get round(): number {\n return this._round;\n }\n get capacity(): number {\n return this._allCells.length;\n }\n get isFull(): boolean {\n return this._locked.size === this.capacity;\n }\n\n lockedCoins(): HwCoin<TData>[] {\n return [...this._locked.values()];\n }\n\n freeCells(): HwCell[] {\n return this._allCells.filter((c) => !this._locked.has(cellKey(c)));\n }\n\n isLocked(cell: HwCell): boolean {\n return this._locked.has(cellKey(cell));\n }\n\n coinAt(cell: HwCell): HwCoin<TData> | undefined {\n return this._locked.get(cellKey(cell));\n }\n\n // ── Transitions (mutate + return effects to emit) ───────────────────\n\n /** Seed the trigger coins and arm the counter. Idle → active. */\n enter(seed: HwCoin<TData>[]): HwEffect<TData>[] {\n if (this._phase !== 'idle') {\n throw new Error('HoldAndWinBoard: enter() while a feature is active — call reset() first.');\n }\n const placed: HwCoin<TData>[] = [];\n const seen = new Set<string>();\n for (const coin of seed) {\n const k = cellKey(coin.cell);\n this._assertInGrid(coin.cell, 'enter');\n if (seen.has(k)) throw new Error(`HoldAndWinBoard: enter() seeds cell ${k} twice.`);\n seen.add(k);\n const stored = this._freeze(coin.cell, coin.id, coin.data);\n this._locked.set(k, stored);\n placed.push(stored);\n }\n this._round = 0;\n this._phase = 'active';\n return [\n this._setRespins(this._defaultRespins, 'seed'),\n { type: 'feature:enter', payload: { seed: placed, respins: this._respinsLeft } },\n ];\n }\n\n /**\n * Open a wave: validate the hits, mark every free cell as spinning, bump the\n * round. Returns the data the driver needs to emit `respin:start` and to land\n * each cell. Active → spinning.\n */\n beginWave(hits: HwCoin<TData>[]): {\n round: number;\n spinning: HwCell[];\n hitByKey: Map<string, HwCoin<TData>>;\n } {\n if (this._phase === 'idle') {\n throw new Error('HoldAndWinBoard: respin() before enter().');\n }\n if (this._phase === 'spinning') {\n throw new Error('HoldAndWinBoard: respin() while a wave is in flight.');\n }\n const hitByKey = new Map<string, HwCoin<TData>>();\n for (const hit of hits) {\n const k = cellKey(hit.cell);\n this._assertInGrid(hit.cell, 'respin');\n if (this._locked.has(k)) throw new Error(`HoldAndWinBoard: hit targets locked cell ${k}.`);\n // A free cell can only land once per wave, so a duplicate hit is always a\n // malformed result. Fail loud rather than silently dropping the first coin.\n if (hitByKey.has(k)) throw new Error(`HoldAndWinBoard: respin() targets cell ${k} twice.`);\n hitByKey.set(k, hit);\n }\n this._waveLanded = [];\n this._phase = 'spinning';\n this._round += 1;\n return { round: this._round, spinning: this.freeCells(), hitByKey };\n }\n\n /** Record one cell's landing. `coin` is null on a miss. */\n land(cell: HwCell, coin: HwCoin<TData> | null): HwEffect<TData>[] {\n // Outside a wave this is a stray landing - a sibling reel settling after\n // the wave was aborted on error, or a cell landing after reset(). Drop it\n // so it can't re-lock a coin into a cleared ledger or resurrect a feature.\n if (this._phase !== 'spinning') return [];\n if (!coin) {\n return [{ type: 'cell:landed', payload: { cell, coin: null } }];\n }\n const stored = this._freeze(cell, coin.id, coin.data);\n this._locked.set(cellKey(cell), stored);\n this._waveLanded.push(stored);\n return [\n { type: 'cell:landed', payload: { cell, coin: stored } },\n {\n type: 'coin:locked',\n payload: { coin: stored, locked: this._locked.size, capacity: this.capacity },\n },\n ];\n }\n\n /** Close the wave: resolve the counter, detect full / feature end. */\n endWave(): { effects: HwEffect<TData>[]; landed: HwCoin<TData>[] } {\n // The wave was aborted or reset out from under us (see land()'s guard).\n // Closing it would re-arm the counter and flip a finished feature back to\n // active off a wave that no longer exists. Do nothing.\n if (this._phase !== 'spinning') return { effects: [], landed: [] };\n const landed = this._waveLanded;\n const effects: HwEffect<TData>[] = [];\n effects.push(\n landed.length > 0\n ? this._setRespins(this._defaultRespins, 'hit-reset')\n : this._setRespins(this._respinsLeft - 1, 'miss'),\n );\n this._phase = 'active';\n effects.push({\n type: 'respin:end',\n payload: { round: this._round, hits: [...landed], respinsLeft: this._respinsLeft },\n });\n const full = this.isFull;\n if (full) effects.push({ type: 'board:full', payload: { coins: this.lockedCoins() } });\n const done = full || this._respinsLeft <= 0;\n if (done) {\n this._phase = 'idle';\n effects.push({\n type: 'feature:end',\n payload: { coins: this.lockedCoins(), rounds: this._round, full },\n });\n }\n // Hand back a caller-owned copy, not the live `_waveLanded` reference, so a\n // consumer that mutates `respin().hits` can't reach into reducer state. The\n // `respin:end` event payload above is already copied for the same reason.\n return { effects, landed: [...landed] };\n }\n\n /**\n * Abandon an in-flight wave after a driver error: restore the phase from\n * `spinning` back to `active` so a thrown spin doesn't strand the board (every\n * later `beginWave` would otherwise throw \"wave in flight\"). Cells that already\n * landed stay locked; the caller decides whether to retry or `reset`.\n */\n abortWave(): void {\n if (this._phase !== 'spinning') return;\n this._phase = 'active';\n this._waveLanded = [];\n }\n\n /** Remove locked coins - the collect moment. */\n release(cells: HwCell[]): { effects: HwEffect<TData>[]; released: HwCoin<TData>[] } {\n if (this._phase === 'spinning') {\n throw new Error('HoldAndWinBoard: release() while a wave is in flight — await respin() first.');\n }\n const effects: HwEffect<TData>[] = [];\n const released: HwCoin<TData>[] = [];\n for (const cell of cells) {\n const k = cellKey(cell);\n const coin = this._locked.get(k);\n if (!coin) continue;\n this._locked.delete(k);\n released.push(coin);\n effects.push({ type: 'coin:released', payload: { coin, remaining: this._locked.size } });\n }\n return { effects, released };\n }\n\n /**\n * Rewrite a **locked** cell's coin identity in place (coin → jackpot, mini →\n * major). Throws on a free cell - placing a brand-new tracked coin out of a\n * spin is `enter`/`respin`'s job; for purely decorative art on a free cell use\n * the cell's reel directly.\n */\n swap(cell: HwCell, id: string, data: TData | undefined): void {\n if (this._phase === 'spinning') {\n throw new Error('HoldAndWinBoard: setSymbolAt() while a wave is in flight — await respin() first.');\n }\n const k = cellKey(cell);\n const prev = this._locked.get(k);\n if (!prev) {\n throw new Error(\n `HoldAndWinBoard: setSymbolAt(${k}) on a non-locked cell — setSymbolAt rewrites a locked coin's identity.`,\n );\n }\n this._locked.set(k, this._freeze(cell, id, data ?? prev.data));\n }\n\n /** Hard clear back to idle. Fires `feature:reset`, never `coin:released`. */\n reset(): HwEffect<TData>[] {\n const clearedCoins = this._locked.size;\n this._locked.clear();\n this._waveLanded = [];\n this._round = 0;\n this._respinsLeft = 0;\n this._phase = 'idle';\n return [{ type: 'feature:reset', payload: { clearedCoins } }];\n }\n\n // ── Internals ────────────────────────────────────────────────────────\n\n private _setRespins(value: number, reason: HwRespinReason): HwEffect<TData> {\n this._respinsLeft = Math.max(0, value);\n return { type: 'respins:changed', payload: { value: this._respinsLeft, reason } };\n }\n\n /**\n * Store a coin with a board-owned, frozen `cell` so the ledger key can never\n * be corrupted by a game mutating the coin it was handed. `data` is left\n * mutable by reference - that is the supported way to carry live value.\n */\n private _freeze(cell: HwCell, id: string, data: TData | undefined): HwCoin<TData> {\n return { cell: Object.freeze({ reel: cell.reel, cell: cell.cell }), id, data };\n }\n\n private _assertInGrid(cell: HwCell, op: string): void {\n if (!this._cellSet.has(cellKey(cell))) {\n throw new Error(`HoldAndWinBoard: ${op}() targets cell ${cellKey(cell)} outside the grid.`);\n }\n }\n}\n","import type { Container, Graphics, Ticker } from 'pixi.js';\nimport { EventEmitter } from '../events/EventEmitter.js';\nimport type { ReelSet } from '../core/ReelSet.js';\nimport type { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { SymbolRegistry } from '../symbols/SymbolRegistry.js';\nimport type { SpeedProfile, SymbolData } from '../config/types.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { BoardGrid } from './BoardGrid.js';\nimport type { Direction, Orientation } from '../core/ReelAxis.js';\nimport { HoldAndWinState } from './HoldAndWinState.js';\nimport type { HwPhase } from './HoldAndWinState.js';\nimport { cellKey } from './HwTypes.js';\nimport type {\n HoldAndWinBoardEvents,\n HwCell,\n HwCoin,\n HwEffect,\n HwRespinResult,\n} from './HwTypes.js';\n\n/** Internal config produced by {@link HoldAndWinBuilder.build}. */\nexport interface HoldAndWinBoardConfig<TData> {\n cols: number;\n rows: number;\n cell: number;\n gap: number;\n emptyId: string;\n respins: number;\n configurator: (registry: SymbolRegistry) => void;\n weights: Record<string, number> | null;\n symbolData: Record<string, Partial<SymbolData>> | null;\n baseProfile: SpeedProfile;\n stagger: (reel: number, cell: number) => number;\n anticipateWhen:\n | ((state: { locked: number; capacity: number; respinsLeft: number }) => boolean)\n | null;\n chrome: ((g: Graphics, size: number) => void) | null;\n /** Travel axis for each cell's own strip. See `HoldAndWinBuilder.axis`. */\n orientation?: Orientation;\n direction?: Direction;\n ticker: Ticker;\n rng: (() => number) | null;\n}\n\n/** Extra spin time (ms) a tense wave adds on top of the normal profile. */\nconst TENSION_EXTRA_MS = 1100;\n\n/**\n * A Hold & Win board: a grid of independently spinning cells plus the round\n * choreography every H&W game repeats - spin the free cells, lock the hits,\n * reset-or-decrement the respin counter, detect the full board.\n *\n * It composes two collaborators: a `BoardGrid` (the generic \"board of reels\"\n * mechanism - geometry, instances, spinning) and a `HoldAndWinState` (the pure\n * single-source reducer - ledger, counter, phase). The board is the\n * mediator: it drives the reels, reports each landing to the reducer, and\n * replays the reducer's decided effects onto {@link events}.\n *\n * It deliberately owns nothing about *value*. Coins are opaque `{ cell, id, data }`\n * - `id` picks the registered art, `data` is the game layer's to read and mutate.\n * Adders, doublers, collectors and flights are game design, expressed through\n * three openings rather than board features: {@link events}, {@link symbolAt}\n * (the live `ReelSymbol` instance) and {@link cellBounds}/{@link cellCenter}\n * (pixel geometry for flights).\n *\n * ```ts\n * const board = new HoldAndWinBuilder<{ value: number }>()\n * .grid(5, 3).cellSize(72, { gap: 4 })\n * .symbols((r) => r.register('coin', CoinSymbol, COIN_TRIGGER))\n * .weights({ coin: 1, empty: 3 }).respins(3).ticker(app.ticker)\n * .build();\n *\n * board.events.on('coin:locked', ({ coin }) => hud.add(coin.data.value));\n * board.enter(triggerCoins);\n * while (true) {\n * const round = await server.respin(board.lockedCoins);\n * const result = await board.respin(round.hits);\n * if (result.done) break; // game animates between rounds\n * }\n * ```\n */\nexport class HoldAndWinBoard<TData = unknown> implements Disposable {\n readonly events = new EventEmitter<HoldAndWinBoardEvents<TData>>();\n readonly cols: number;\n readonly rows: number;\n\n private readonly _grid: BoardGrid;\n private readonly _state: HoldAndWinState<TData>;\n private readonly _emptyId: string;\n private readonly _anticipateWhen: HoldAndWinBoardConfig<TData>['anticipateWhen'];\n\n constructor(cfg: HoldAndWinBoardConfig<TData>) {\n this.cols = cfg.cols;\n this.rows = cfg.rows;\n this._emptyId = cfg.emptyId;\n this._anticipateWhen = cfg.anticipateWhen;\n\n const base = (cell: HwCell): number =>\n (cfg.baseProfile.minimumSpinTime ?? 320) + cfg.stagger(cell.reel, cell.cell);\n this._grid = new BoardGrid({\n cols: cfg.cols,\n rows: cfg.rows,\n cellSize: cfg.cell,\n gap: cfg.gap,\n emptyId: cfg.emptyId,\n symbols: cfg.configurator,\n weights: cfg.weights ?? undefined,\n symbolData: cfg.symbolData ?? undefined,\n chrome: cfg.chrome ?? undefined,\n orientation: cfg.orientation,\n direction: cfg.direction,\n ticker: cfg.ticker,\n rng: cfg.rng ?? undefined,\n profiles: {\n normal: (cell) => ({ ...cfg.baseProfile, minimumSpinTime: base(cell) }),\n tension: (cell) => ({ ...cfg.baseProfile, minimumSpinTime: base(cell) + TENSION_EXTRA_MS }),\n },\n });\n this._state = new HoldAndWinState<TData>(this._grid.cells(), cfg.respins);\n }\n\n // ── State (delegated to the single-source reducer) ───────────────────\n\n get container(): Container {\n return this._grid.container;\n }\n get capacity(): number {\n return this._state.capacity;\n }\n get respinsLeft(): number {\n return this._state.respinsLeft;\n }\n get lockedCoins(): HwCoin<TData>[] {\n return this._state.lockedCoins();\n }\n get isFull(): boolean {\n return this._state.isFull;\n }\n get freeCells(): HwCell[] {\n return this._state.freeCells();\n }\n /** Where the feature is right now: idle (no feature), active, or spinning. */\n get phase(): HwPhase {\n return this._state.phase;\n }\n\n // ── Geometry & instances (the game layer's openings) ────────────────\n\n cellBounds(cell: HwCell): { x: number; y: number; width: number; height: number } {\n return this._grid.cellBounds(cell);\n }\n cellCenter(cell: HwCell): { x: number; y: number } {\n return this._grid.cellCenter(cell);\n }\n /** Live symbol instance currently shown in a cell. */\n symbolAt(cell: HwCell): ReelSymbol {\n return this._grid.symbolAt(cell);\n }\n /** The cell's underlying 1×1 ReelSet, for driving one cell directly. */\n reelAt(cell: HwCell): ReelSet {\n return this._grid.reelAt(cell);\n }\n\n /**\n * Rewrite a **locked** cell's coin in place - coin → jackpot, mini → major,\n * raise a tier - without disturbing any other cell. The ledger entry is\n * rewritten so `lockedCoins` and totals stay correct. Throws on a free cell.\n * Returns the new live symbol instance.\n *\n * Throws if called while a wave is in flight - `await respin()` first. To\n * upgrade a coin in reaction to its own `coin:locked`, defer the swap until\n * the awaited `respin()` resolves rather than swapping inside the listener.\n */\n setSymbolAt(cell: HwCell, id: string, data?: TData): ReelSymbol {\n this._state.swap(cell, id, data);\n this._grid.place(cell, id);\n return this._grid.symbolAt(cell);\n }\n\n // ── Round choreography ───────────────────────────────────────────────\n\n /** Activate the feature with the trigger coins. Seeds land locked, instantly. */\n enter(seed: HwCoin<TData>[]): void {\n const effects = this._state.enter(seed); // validates first; throws before any visual\n for (const coin of seed) this._grid.place(coin.cell, coin.id);\n this._apply(effects);\n }\n\n /**\n * Spin every free cell; `hits` land (and lock) their coins, all other spinning\n * cells land empty. Resolves once the wave has landed and the counter is\n * resolved. The game layer drives pacing between rounds.\n */\n async respin(hits: HwCoin<TData>[]): Promise<HwRespinResult<TData>> {\n const { round, spinning, hitByKey } = this._state.beginWave(hits);\n try {\n const tense = this._anticipating() && spinning.length > 0;\n for (const cell of spinning) this._grid.setProfile(cell, tense ? 'tension' : 'normal');\n this.events.emit('respin:start', { round, respinsLeft: this._state.respinsLeft, spinning });\n\n const targets = spinning.map((cell) => ({\n cell,\n id: hitByKey.get(cellKey(cell))?.id ?? this._emptyId,\n }));\n await this._grid.spinCells(targets, (cell) => {\n this._apply(this._state.land(cell, hitByKey.get(cellKey(cell)) ?? null));\n });\n\n const { effects, landed } = this._state.endWave();\n this._apply(effects);\n return {\n round,\n hits: landed,\n respinsLeft: this._state.respinsLeft,\n full: this._state.isFull,\n done: this._state.phase === 'idle',\n };\n } catch (err) {\n // A synchronous throw between beginWave and endWave - most plausibly a\n // game-layer event listener (respin:start / cell:landed / coin:locked)\n // throwing - must not strand the board. (An unregistered symbol id does\n // NOT land here: the engine logs and slams the reel internally, so the\n // spin resolves rather than rejecting.) abortWave() restores the reducer\n // phase, else every later respin() throws \"wave in flight\"; skipSpinning()\n // lands any cell still in flight, else the next respin() throws \"already\n // spinning\" on it. Stray landings from those slams are dropped by the\n // reducer's not-spinning guard. Rethrow so the caller still sees it.\n this._state.abortWave();\n this._grid.skipSpinning();\n throw err;\n }\n }\n\n /**\n * Remove locked coins - the collect moment. Clears the cells (they become\n * free again) and returns the released coins; the flight itself is game-layer\n * animation, started from `cellCenter()` or the `coin:released` event.\n */\n release(cells: HwCell[]): HwCoin<TData>[] {\n const { effects, released } = this._state.release(cells);\n for (const coin of released) this._grid.place(coin.cell, this._emptyId);\n this._apply(effects);\n return released;\n }\n\n /**\n * Fast-forward whatever is spinning: every in-flight cell is slammed to its\n * landed position, then `feature:skip` fires so the game layer can cut its own\n * flights short. The normal landing → `coin:locked` → `feature:end` flow still\n * resolves; this only removes the waiting. Returns the number of cells that\n * were in flight.\n */\n skip(): number {\n const inFlight = this._grid.skipSpinning();\n this.events.emit('feature:skip', { inFlight });\n return inFlight;\n }\n\n /** Clear the board back to idle. Fires `feature:reset` (not `coin:released`). */\n reset(): void {\n const effects = this._state.reset();\n for (const cell of this._grid.cells()) this._grid.place(cell, this._emptyId);\n this._apply(effects);\n }\n\n get isDestroyed(): boolean {\n return this._grid.isDestroyed;\n }\n\n destroy(): void {\n if (this._grid.isDestroyed) return;\n this.events.removeAllListeners();\n this._grid.destroy();\n }\n\n // ── Internals ────────────────────────────────────────────────────────\n\n /** Emit each reducer-decided effect and fire the visual side effects. */\n private _apply(effects: HwEffect<TData>[]): void {\n for (const fx of effects) {\n // Correlated union: `fx.type` and `fx.payload` are paired by construction\n // in the reducer, but TS can't carry that correlation through `emit`'s\n // generic. One local cast keeps every other call site fully typed.\n (this.events.emit as (type: string, payload: unknown) => void)(fx.type, fx.payload);\n if (fx.type === 'coin:locked') {\n // playWin is presentation; a hiccup must not break the feature flow, but\n // it must not vanish silently either - log it like the rest of the engine.\n void this.symbolAt(fx.payload.coin.cell)\n .playWin()\n .catch((err) => console.warn('HoldAndWinBoard: coin win animation failed.', err));\n }\n }\n }\n\n private _anticipating(): boolean {\n if (!this._anticipateWhen) return false;\n return this._anticipateWhen({\n locked: this._state.lockedCoins().length,\n capacity: this._state.capacity,\n respinsLeft: this._state.respinsLeft,\n });\n }\n}\n","import type { Graphics, Ticker } from 'pixi.js';\nimport { SpeedPresets } from '../config/SpeedPresets.js';\nimport type { SpeedProfile, SymbolData } from '../config/types.js';\nimport type { SymbolRegistry } from '../symbols/SymbolRegistry.js';\nimport { HoldAndWinBoard } from './HoldAndWinBoard.js';\nimport type { Direction, Orientation } from '../core/ReelAxis.js';\nimport type { HwCellSizeOptions } from './HwTypes.js';\n\n/**\n * Fluent builder for {@link HoldAndWinBoard}.\n *\n * A Hold & Win board is a W×H grid of cells that spin **independently** - the\n * mechanic's atomic unit is the cell, the engine's is the column, so each cell\n * is its own 1×1 ReelSet. This builder wires that grid plus the round\n * choreography; everything value-shaped stays in the game layer (see\n * {@link HoldAndWinBoard}).\n *\n * `TData` types the opaque payload carried on each coin's `data`.\n */\nexport class HoldAndWinBuilder<TData = unknown> {\n private _cols = 5;\n private _rows = 3;\n private _cell = 72;\n private _gap = 4;\n private _emptyId = 'empty';\n private _respins = 3;\n private _configurator: ((registry: SymbolRegistry) => void) | null = null;\n private _weights: Record<string, number> | null = null;\n private _symbolData: Record<string, Partial<SymbolData>> | null = null;\n private _baseProfile: SpeedProfile = { ...SpeedPresets.NORMAL, minimumSpinTime: 320 };\n private _stagger: (reel: number, cell: number) => number = (reel, cell) => (reel + cell) * 70;\n private _anticipateWhen:\n | ((state: { locked: number; capacity: number; respinsLeft: number }) => boolean)\n | null = null;\n private _chrome: ((g: Graphics, size: number) => void) | null = null;\n private _orientation: Orientation = 'vertical';\n private _direction: Direction = 'forward';\n private _ticker: Ticker | null = null;\n private _rng: (() => number) | null = null;\n\n grid(cols: number, rows: number): this {\n this._cols = cols;\n this._rows = rows;\n return this;\n }\n\n cellSize(size: number, opts: HwCellSizeOptions = {}): this {\n this._cell = size;\n this._gap = opts.gap ?? this._gap;\n return this;\n }\n\n /**\n * Register coin symbol classes, exactly like `ReelSetBuilder.symbols`. Applied\n * to every cell. An {@link EmptySymbol} is auto-registered under {@link emptyId}\n * unless the configurator registers one itself.\n */\n symbols(configurator: (registry: SymbolRegistry) => void): this {\n this._configurator = configurator;\n return this;\n }\n\n /** Strip weights during the spin (how often coins flash past empties). */\n weights(weights: Record<string, number>): this {\n this._weights = weights;\n return this;\n }\n\n /** Symbol id a cell shows when it holds no coin. Default `'empty'`. */\n emptyId(id: string): this {\n this._emptyId = id;\n return this;\n }\n\n /**\n * Per-symbol engine overrides, exactly like `ReelSetBuilder.symbolData`. The\n * headline use is `{ unmask: true }` for coins whose lock/reveal animations\n * expand past the cell. Safe only for server-placed ids (weight 0): unmasked\n * strip symbols mis-track vertically while the reel spins.\n */\n symbolData(overrides: Record<string, Partial<SymbolData>>): this {\n this._symbolData = { ...(this._symbolData ?? {}), ...overrides };\n return this;\n }\n\n /** Respins granted on enter and restored on every hit. Default 3. */\n respins(count: number): this {\n this._respins = count;\n return this;\n }\n\n /** Base spin feel for every cell. Default: NORMAL with a 320ms floor. */\n speedProfile(profile: SpeedProfile): this {\n this._baseProfile = profile;\n return this;\n }\n\n /**\n * Extra milliseconds of spin per cell on top of the base minimum spin time.\n * Default `(reel + cell) * 70` - the diagonal landing wave. Return 0 for\n * simultaneous landings.\n */\n stagger(fn: (reel: number, cell: number) => number): this {\n this._stagger = fn;\n return this;\n }\n\n /**\n * When the predicate returns true for a wave, **every** spinning cell uses a\n * drawn-out tension profile - the \"one cell left for Grand\" moment. Evaluated\n * once per wave for the whole board (not per cell), against the pre-wave state.\n */\n anticipateWhen(\n fn: (state: { locked: number; capacity: number; respinsLeft: number }) => boolean,\n ): this {\n this._anticipateWhen = fn;\n return this;\n }\n\n /** Per-cell background, drawn behind each mini reel. */\n cellChrome(draw: (g: Graphics, size: number) => void): this {\n this._chrome = draw;\n return this;\n }\n\n /**\n * Which way each cell's strip travels while it spins. Cells are 1x1 reel\n * sets, so this picks the edge a coin scrolls in from; the board's own\n * `cols` x `rows` layout is unaffected. Defaults to vertical / forward.\n */\n axis(orientation: Orientation, direction: Direction = 'forward'): this {\n this._orientation = orientation;\n this._direction = direction;\n return this;\n }\n\n ticker(ticker: Ticker): this {\n this._ticker = ticker;\n return this;\n }\n\n /** Injected RNG for the spin strips (deterministic demos / tests). */\n rng(fn: () => number): this {\n this._rng = fn;\n return this;\n }\n\n build(): HoldAndWinBoard<TData> {\n if (!this._configurator) {\n throw new Error('HoldAndWinBuilder: .symbols(...) is required — register at least one coin id.');\n }\n if (!this._ticker) {\n throw new Error('HoldAndWinBuilder: .ticker(...) is required.');\n }\n return new HoldAndWinBoard<TData>({\n cols: this._cols,\n rows: this._rows,\n cell: this._cell,\n gap: this._gap,\n emptyId: this._emptyId,\n respins: this._respins,\n configurator: this._configurator,\n weights: this._weights,\n symbolData: this._symbolData,\n baseProfile: this._baseProfile,\n stagger: this._stagger,\n anticipateWhen: this._anticipateWhen,\n chrome: this._chrome,\n orientation: this._orientation,\n direction: this._direction,\n ticker: this._ticker,\n rng: this._rng,\n });\n }\n}\n","import type { SymbolPosition, Win } from '../config/types.js';\n\nexport type { Win, SymbolPosition };\n\n/** Return a new array sorted by `value` descending (non-mutating). Missing values sort as 0. */\nexport function sortByValueDesc<T extends { value?: number }>(wins: readonly T[]): T[] {\n return [...wins].sort((a, b) => (b.value ?? 0) - (a.value ?? 0));\n}\n","import type { Win } from '../config/types.js';\nimport type { ReelSet } from '../core/ReelSet.js';\nimport type { SymbolPosition } from '../events/ReelEvents.js';\nimport type { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { sortByValueDesc } from './Win.js';\n\n/**\n * What to play on each cell being highlighted.\n *\n * - `'win'`. default. Calls `symbol.playWin()` (your subclass's hook).\n * - `string`. a named animation. If the symbol exposes a\n * `playAnimation(name)` method (e.g. SpineSymbol), it's invoked; else\n * it falls back to `playWin()`.\n * - `(symbol, cell, win) => Promise<void>`. drive the animation\n * yourself. Good for GSAP bounces, line-specific pulses, etc.\n */\nexport type WinSymbolAnim =\n | 'win'\n | string\n | ((symbol: ReelSymbol, cell: SymbolPosition, win: Win) => Promise<void>);\n\nexport interface WinPresenterOptions {\n /**\n * Fade non-winning symbols to this alpha while a win is active.\n * - `true` (default) → alpha 0.35\n * - number → that alpha\n * - `false` → don't touch non-winners\n *\n * Restored on `win:end`.\n */\n dimLosers?: boolean | { alpha?: number };\n /** See {@link WinSymbolAnim}. Default `'win'`. */\n symbolAnim?: WinSymbolAnim;\n /**\n * Delay between cells *within* a single win (ms).\n * - `0` (default) → all cells animate simultaneously\n * - `> 0` → cells start one after another in array order (e.g. a\n * left-to-right sweep across a payline's cells)\n */\n stagger?: number;\n /** Delay between successive wins in the sequence (ms). Default 400. */\n cycleGap?: number;\n /** Number of full cycles through the wins list. `-1` for infinite. Default 1. */\n cycles?: number;\n /** Sort wins by `value` descending before cycling. Default true. */\n sortByValue?: boolean;\n}\n\ninterface ResolvedOptions {\n dimAlpha: number | null;\n symbolAnim: WinSymbolAnim;\n stagger: number;\n cycleGap: number;\n cycles: number;\n sortByValue: boolean;\n}\n\n/**\n * Highlights winning cells on a reel set. One job: animate the symbols.\n *\n * The presenter doesn't draw lines, outlines, or any per-win visual. it\n * emits `win:start` / `win:group` / `win:symbol` / `win:end` events so\n * your code can hook anything it wants (polylines, Spine line rigs,\n * popup numbers, sound cues) by subscribing and using\n * `reelSet.getCellBounds(reel, cell)` to place graphics.\n *\n * Two knobs cover the common presentation modes:\n *\n * - `stagger: 0` → all cells in a win pulse together\n * - `stagger: 60` → cells start one after another. a left-to-right\n * sweep if you pass cells in reel order\n *\n * ```ts\n * const presenter = new WinPresenter(reelSet, { stagger: 80 });\n *\n * reelSet.events.on('spin:complete', async () => {\n * const wins = await server.wins(result); // your wins, your shape\n * await presenter.show(wins);\n * });\n * reelSet.events.on('spin:start', () => presenter.abort());\n * ```\n *\n * Cascades: drive `presenter.show([{ cells: winners }])` from\n * `runCascade`'s `onWinnersVanish` hook. cluster pops and payline hits\n * are the same shape to the presenter.\n */\nexport class WinPresenter implements Disposable {\n private _reelSet: ReelSet;\n private _options: ResolvedOptions;\n private _abort: AbortController | null = null;\n private _isActive = false;\n private _isDestroyed = false;\n\n constructor(reelSet: ReelSet, options: WinPresenterOptions = {}) {\n this._reelSet = reelSet;\n this._options = WinPresenter._resolve(options);\n }\n\n get isActive(): boolean {\n return this._isActive;\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n /**\n * Present the given wins. Cancels any in-flight sequence first.\n * Resolves when all cycles complete or when `abort()` is called.\n *\n * Empty input resolves immediately without firing any events.\n */\n async show(wins: readonly Win[]): Promise<void> {\n this.abort();\n if (this._isDestroyed) return;\n if (wins.length === 0) return;\n\n const ordered = this._options.sortByValue\n ? sortByValueDesc(wins)\n : [...wins];\n\n const abort = new AbortController();\n this._abort = abort;\n this._isActive = true;\n this._reelSet.events.emit('win:start', ordered);\n\n let loop = 0;\n try {\n while (this._options.cycles === -1 || loop < this._options.cycles) {\n for (const win of ordered) {\n if (abort.signal.aborted) return;\n await this._showOne(win, abort.signal);\n if (abort.signal.aborted) return;\n await this._wait(this._options.cycleGap, abort.signal);\n }\n loop++;\n }\n } finally {\n this._restoreAlpha();\n const wasAborted = abort.signal.aborted;\n if (this._abort === abort) this._abort = null;\n this._isActive = false;\n this._reelSet.events.emit('win:end', wasAborted ? 'aborted' : 'complete');\n }\n }\n\n /** Abort any in-flight `show()`. */\n abort(): void {\n if (this._abort) this._abort.abort();\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n this._isDestroyed = true;\n this.abort();\n }\n\n private async _showOne(win: Win, signal: AbortSignal): Promise<void> {\n const cells = [...win.cells];\n if (cells.length === 0) return;\n\n // Apply dim before firing win:group so listeners observe the live\n // visual state (e.g. a UI snapshot sees losers already faded).\n this._applyDim(cells);\n this._reelSet.events.emit('win:group', win, cells);\n\n const stagger = this._options.stagger;\n const animPromises: Promise<void>[] = [];\n\n for (let i = 0; i < cells.length; i++) {\n if (signal.aborted) break;\n if (i > 0 && stagger > 0) await this._wait(stagger, signal);\n if (signal.aborted) break;\n const cell = cells[i];\n const reel = this._reelSet.getReel(cell.reelIndex);\n if (!reel) continue;\n const symbol = reel.getSymbolAt(cell.cellIndex);\n if (!symbol) continue;\n this._reelSet.events.emit('win:symbol', symbol, cell, win);\n animPromises.push(this._playAnim(symbol, cell, win));\n }\n\n await Promise.all(animPromises);\n }\n\n private async _playAnim(\n symbol: ReelSymbol,\n cell: SymbolPosition,\n win: Win,\n ): Promise<void> {\n const anim = this._options.symbolAnim;\n if (typeof anim === 'function') return anim(symbol, cell, win);\n if (anim === 'win') return symbol.playWin();\n const withPlay = symbol as unknown as { playAnimation?: (name: string) => Promise<void> };\n if (typeof withPlay.playAnimation === 'function') return withPlay.playAnimation(anim);\n return symbol.playWin();\n }\n\n private _applyDim(winCells: readonly SymbolPosition[]): void {\n const alpha = this._options.dimAlpha;\n if (alpha === null) return;\n\n const winKeys = new Set<string>();\n for (const c of winCells) winKeys.add(`${c.reelIndex}:${c.cellIndex}`);\n\n const reels = this._reelSet.reels;\n for (let r = 0; r < reels.length; r++) {\n const reel = reels[r];\n for (let cell = 0; cell < reel.visibleCells; cell++) {\n const view = reel.getSymbolAt(cell).view;\n view.alpha = winKeys.has(`${r}:${cell}`) ? 1 : alpha;\n }\n }\n }\n\n private _restoreAlpha(): void {\n const reels = this._reelSet.reels;\n for (const reel of reels) {\n for (let cell = 0; cell < reel.visibleCells; cell++) {\n reel.getSymbolAt(cell).view.alpha = 1;\n }\n }\n }\n\n private _wait(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal.aborted) return resolve();\n const t = setTimeout(resolve, ms);\n signal.addEventListener(\n 'abort',\n () => {\n clearTimeout(t);\n resolve();\n },\n { once: true },\n );\n });\n }\n\n private static _resolve(opts: WinPresenterOptions): ResolvedOptions {\n let dimAlpha: number | null;\n if (opts.dimLosers === false) {\n dimAlpha = null;\n } else if (typeof opts.dimLosers === 'object' && opts.dimLosers !== null) {\n dimAlpha = opts.dimLosers.alpha ?? 0.35;\n } else {\n dimAlpha = 0.35;\n }\n\n return {\n dimAlpha,\n symbolAnim: opts.symbolAnim ?? 'win',\n stagger: Math.max(0, opts.stagger ?? 0),\n cycleGap: opts.cycleGap ?? 400,\n cycles: opts.cycles ?? 1,\n sortByValue: opts.sortByValue ?? true,\n };\n }\n}\n","import type { Ticker } from 'pixi.js';\nimport { DEFAULT_GSAP, type Gsap } from './gsap.js';\n\n/**\n * Drive GSAP from a PixiJS ticker instead of its own requestAnimationFrame\n * loop, and return a disposer that restores GSAP's default loop.\n *\n * **Why this matters:** GSAP's default ticker runs on `requestAnimationFrame`,\n * which browsers throttle — and often freeze entirely — in hidden tabs and many\n * iframes. Casino lobbies and iframed clients are routinely backgrounded. When\n * that happens GSAP stalls while the PixiJS ticker keeps running, so every\n * engine animation (spin easing, the landing bounce, spotlight pulses) freezes\n * mid-flight. Pinning GSAP to the same ticker keeps animation and rendering in\n * lockstep everywhere, foreground or background.\n *\n * This is the one line every integration has to remember\n * (`gsap.ticker.remove(gsap.updateRoot); ticker.add(...)`); calling this\n * function instead removes that footgun.\n *\n * Pass the SAME instance you gave `ReelSetBuilder.gsap(...)`. gsap is held per\n * reel set in v2, not process-wide, so this function cannot look it up for you\n * -- and driving a different instance than the engine's tweens live on is the\n * dual-instance trap all over again. Omit it only if you never called\n * `.gsap(...)` either.\n *\n * Call it **once per app**, after creating the PixiJS `Application` and before\n * the first spin. Do NOT also detach `gsap.updateRoot` yourself — driving GSAP\n * from two sources advances it twice per frame (animations run at double speed).\n *\n * @param ticker - The PixiJS ticker to drive GSAP from (usually `app.ticker`).\n * @param instance - The gsap instance to drive. Defaults to the one resolved\n * at lib-load time; pass the one you handed `ReelSetBuilder.gsap(...)`.\n * @returns A disposer that detaches the driver and restores GSAP's own ticker.\n *\n * @example\n * import { Application } from 'pixi.js';\n * import { driveGsapWithTicker } from 'pixi-reels';\n *\n * const app = new Application();\n * await app.init({ ... });\n * const stopGsapSync = driveGsapWithTicker(app.ticker);\n * // ...on teardown:\n * stopGsapSync();\n */\nexport function driveGsapWithTicker(ticker: Ticker, instance: Gsap = DEFAULT_GSAP): () => void {\n const gsap = instance;\n gsap.ticker.remove(gsap.updateRoot);\n const driver = (): void => {\n gsap.updateRoot(ticker.lastTime / 1000);\n };\n ticker.add(driver);\n\n let disposed = false;\n return () => {\n if (disposed) return;\n disposed = true;\n ticker.remove(driver);\n gsap.ticker.add(gsap.updateRoot);\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/symbols/PerspectiveCell.ts","../src/symbols/SpriteSymbol.ts","../src/symbols/AnimatedSpriteSymbol.ts","../src/symbols/EmptySymbol.ts","../src/snapshot/SpinTextureCache.ts","../src/snapshot/StaticSpinSymbol.ts","../src/symbols/CardSymbol.ts","../src/spin/anticipationRecipes.ts","../src/spin/modes/CascadeMode.ts","../src/spin/modes/ImmediateMode.ts","../src/board/BoardGrid.ts","../src/board/HwTypes.ts","../src/board/HoldAndWinState.ts","../src/board/HoldAndWinBoard.ts","../src/board/HoldAndWinBuilder.ts","../src/wins/Win.ts","../src/wins/WinPresenter.ts","../src/utils/gsapTicker.ts"],"sourcesContent":["import { PerspectiveMesh, type Container, type Texture } from 'pixi.js';\nimport type { ReelCellInset, ReelCellQuad } from '../config/types.js';\n\n/**\n * The slice of its cell a texture's opaque art really occupies.\n *\n * TexturePacker trims the transparent margin off each frame, so a 300x300\n * symbol whose art is a 152x152 blob at (74, 74) ships as a 152x152 frame. A\n * `Sprite` puts that back; a mesh does not, so the reel has to be told or it\n * projects - and inflates - the whole cell.\n *\n * `null` for an untrimmed texture, which needs no correction.\n */\nexport function textureCellInset(texture: Texture): ReelCellInset | null {\n const trim = texture.trim;\n const orig = texture.orig;\n if (!trim || orig.width <= 0 || orig.height <= 0) return null;\n if (trim.width === orig.width && trim.height === orig.height) return null;\n return {\n left: trim.x / orig.width,\n top: trim.y / orig.height,\n right: (trim.x + trim.width) / orig.width,\n bottom: (trim.y + trim.height) / orig.height,\n };\n}\n\n/**\n * Whether a texture can go through the perspective mesh.\n *\n * Only textures owning their whole source qualify. An atlas SUB-frame does\n * not: the mesh addresses its source with plain 0..1 UVs, and remapping them\n * onto the frame - via `texture.uvs`, via `textureMatrix`, or leaving it to\n * PixiJS - has not produced a correct draw here. Unmapped, the cell shows the\n * whole sheet; mapped by hand, a magnified crop of the right frame, which says\n * the mapping is applied somewhere else too. Rather than ship either, a\n * sub-frame falls back to the affine fit in `ReelSymbol.applyCellQuad()` -\n * correct, just not keystoned.\n *\n * Generated textures, single loaded images and render textures pass. Atlas\n * frames - the common production case - do not, yet.\n */\nexport function canProjectTexture(texture: Texture): boolean {\n const source = texture.source;\n if (!source) return false;\n if (texture.rotate !== 0) return false;\n const { frame, orig } = texture;\n if (frame.x !== 0 || frame.y !== 0) return false;\n if (frame.width !== source.width || frame.height !== source.height) return false;\n return orig.width === frame.width && orig.height === frame.height;\n}\n\n/**\n * Tessellation of the projected quad. PixiJS builds the perspective by\n * interpolating UVs across a grid, so this is the quality knob: too few and a\n * keystoned cell reads as a plain stretch, too many and every symbol on the\n * board costs a pile of vertices. Slot cells are small and their trapezoids are\n * mild, so the PixiJS default of 10 is more than enough.\n */\nconst VERTICES = 10;\n\n/**\n * Draws a texture through a real perspective quad.\n *\n * What makes a curved reel a projection rather than a squash. A `Container`\n * transform is affine - it can scale a cell, never turn it into a trapezoid -\n * so a cell rotated away comes out a smaller rectangle instead of one with a\n * narrower far edge. `PerspectiveMesh` maps the texture onto four arbitrary\n * corners with perspective-correct interpolation, and costs no render pass\n * because the symbol already owns a texture.\n *\n * Lazy: a game that never calls `curve()` allocates no mesh and keeps\n * rendering through its plain sprite.\n *\n * @internal Shared by `SpriteSymbol` and `AnimatedSpriteSymbol`. Use it the\n * same way from a custom symbol whose content is a single texture.\n */\nexport class PerspectiveCell {\n private _mesh: PerspectiveMesh | null = null;\n private _active = false;\n private _uvScratch: Float32Array | null = null;\n private _uvSource: Texture | null = null;\n\n /**\n * @param _view the symbol's own view, which the mesh is parented to\n * @param _flat the display object used when the reel is flat. hidden while\n * the mesh is driving, so the two never draw on top of each other\n */\n constructor(\n private readonly _view: Container,\n private readonly _flat: Container,\n ) {}\n\n /** True while the perspective mesh is the thing being drawn. */\n get isActive(): boolean {\n return this._active;\n }\n\n /**\n * The mesh, once one exists. Animate THIS, not the symbol's view, for a win\n * pulse on a curved reel - it is pivoted on the quad's centre, so a scale\n * tween pulses in place instead of swinging out from the cell corner.\n */\n get mesh(): PerspectiveMesh | null {\n return this._mesh;\n }\n\n /**\n * Project the cell, or hand rendering back to the flat display object when\n * `quad` is `null`.\n *\n * @param quad the projected footprint, view-local\n * @param texture what to draw. re-read on every call so an animated symbol\n * can swap frames without telling us\n */\n apply(quad: ReelCellQuad | null, texture: Texture): boolean {\n if (quad === null || !canProjectTexture(texture)) {\n if (!this._active) return false;\n this._active = false;\n this._flat.visible = true;\n if (this._mesh) this._mesh.visible = false;\n return false;\n }\n\n const mesh = this._ensureMesh(texture);\n if (mesh.texture !== texture) mesh.texture = texture;\n this._syncUvs(mesh, texture);\n mesh.setCorners(quad.x0, quad.y0, quad.x1, quad.y1, quad.x2, quad.y2, quad.x3, quad.y3);\n // Pivot and position on the quad's centre: renders unchanged at scale 1,\n // but a scale tween pulses about the middle of the cell.\n const cx = (quad.x0 + quad.x1 + quad.x2 + quad.x3) / 4;\n const cy = (quad.y0 + quad.y1 + quad.y2 + quad.y3) / 4;\n mesh.pivot.set(cx, cy);\n mesh.position.set(cx, cy);\n\n if (!this._active) {\n this._active = true;\n this._flat.visible = false;\n mesh.visible = true;\n }\n return true;\n }\n\n /** Reset the mesh's own transform. Call from `stopAnimation` / `onDeactivate`. */\n resetTransform(): void {\n if (this._mesh) this._mesh.scale.set(1, 1);\n }\n\n /**\n * Push a new texture through without re-projecting. For the pooled swap in\n * `onActivate`, where the identity changes but the quad has not.\n */\n syncTexture(texture: Texture): void {\n if (this._active && this._mesh && this._mesh.texture !== texture) {\n this._mesh.texture = texture;\n this._syncUvs(this._mesh, texture);\n }\n }\n\n destroy(): void {\n if (!this._mesh) return;\n this._mesh.destroy();\n this._mesh = null;\n this._active = false;\n\n this._uvScratch = null;\n this._uvSource = null;\n }\n\n private _ensureMesh(texture: Texture): PerspectiveMesh {\n if (this._mesh) return this._mesh;\n this._mesh = new PerspectiveMesh({\n texture,\n verticesX: VERTICES,\n verticesY: VERTICES,\n });\n // `PerspectivePlaneGeometry` warps VERTEX POSITIONS and leaves the UVs\n // alone, so the atlas mapping is ours to own. One scratch buffer, rewritten\n // from the grid on every texture swap rather than remapped in place, which\n // would compound.\n this._uvScratch = new Float32Array(VERTICES * VERTICES * 2);\n this._view.addChild(this._mesh);\n return this._mesh;\n }\n\n /**\n * Fold the texture's atlas frame into the mesh's UVs.\n *\n * A `Sprite` gets this free; a `Mesh` does not. Its 0..1 UVs address the\n * whole SHEET, so an unmapped mesh draws the entire atlas squeezed into one\n * cell.\n *\n * `texture.uvs` carries the frame's corners in source space and PixiJS keeps\n * it current, so interpolating between them is exact. `textureMatrix` is not\n * necessarily updated for this texture yet, and yields out-of-range UVs that\n * sample whatever is next door on the sheet.\n */\n private _syncUvs(mesh: PerspectiveMesh, texture: Texture): void {\n if (this._uvSource === texture) return;\n const out = this._uvScratch;\n if (!out) return;\n this._uvSource = texture;\n\n // Grid coordinates from the vertex index, not read back off the geometry:\n // the UV buffer is not necessarily populated at construction, and a\n // snapshot of zeros points every vertex at the frame's first texel - a\n // cell of flat colour. Row-major, matching PixiJS's own plane transform.\n const { x0, y0, x1, y1, x2, y2, x3, y3 } = texture.uvs;\n const span = VERTICES - 1;\n for (let k = 0; k < out.length / 2; k++) {\n const u = (k % VERTICES) / span;\n const v = Math.floor(k / VERTICES) / span;\n const tl = (1 - u) * (1 - v);\n const tr = u * (1 - v);\n const br = u * v;\n const bl = (1 - u) * v;\n out[k * 2] = tl * x0 + tr * x1 + br * x2 + bl * x3;\n out[k * 2 + 1] = tl * y0 + tr * y1 + br * y2 + bl * y3;\n }\n mesh.geometry.uvs = out;\n }\n}\n","import { Sprite, type Texture } from 'pixi.js';\nimport type { gsap } from 'gsap';\n\nimport type { ReelCellInset, ReelCellQuad } from '../config/types.js';\nimport { ReelSymbol } from './ReelSymbol.js';\nimport { PerspectiveCell, textureCellInset } from './PerspectiveCell.js';\n\nexport interface SpriteSymbolOptions {\n /** Map of symbolId → Texture. */\n textures: Record<string, Texture>;\n /** Anchor point. Default: { x: 0.5, y: 0.5 }. */\n anchor?: { x: number; y: number };\n}\n\n/**\n * Symbol implementation using a simple PixiJS Sprite.\n * Swaps texture on activate. Win animation is a scale pulse via GSAP.\n *\n * On a curved reel it draws the same texture through a `PerspectiveMesh`\n * instead, so the cell is a real projected trapezoid rather than a rectangle\n * that has been scaled down. See {@link ReelSymbol.applyCellQuad}.\n */\nexport class SpriteSymbol extends ReelSymbol {\n private _sprite: Sprite;\n private _textures: Record<string, Texture>;\n private _winTween: gsap.core.Tween | null = null;\n private _perspective: PerspectiveCell;\n\n constructor(options: SpriteSymbolOptions) {\n super();\n this._textures = options.textures;\n const anchor = options.anchor ?? { x: 0, y: 0 };\n this._sprite = new Sprite();\n this._sprite.anchor.set(anchor.x, anchor.y);\n this.view.addChild(this._sprite);\n this._perspective = new PerspectiveCell(this.view, this._sprite);\n }\n\n protected onActivate(symbolId: string): void {\n const texture = this._textures[symbolId];\n if (texture) {\n this._sprite.texture = texture;\n // The pool hands this instance a new identity without re-projecting, so\n // the mesh has to be told about the swap or it keeps drawing the old one.\n this._perspective.syncTexture(texture);\n }\n }\n\n protected onDeactivate(): void {\n this._killWinTween();\n this._sprite.scale.set(1, 1);\n this._perspective.resetTransform();\n }\n\n override get cellInset(): ReelCellInset | null {\n return textureCellInset(this._sprite.texture);\n }\n\n override applyCellQuad(quad: ReelCellQuad | null): void {\n if (this._perspective.apply(quad, this._sprite.texture)) {\n // The mesh's own corners carry the whole projection, so the view must\n // stay at identity. Anything else would apply the curve twice.\n this.view.scale.set(1, 1);\n this.view.pivot.set(0, 0);\n return;\n }\n // The mesh declined this texture (today: any atlas sub-frame). Take the\n // base class's uniform-scale fit so the cell is still on the drum.\n super.applyCellQuad(quad);\n }\n\n async playWin(): Promise<void> {\n this._killWinTween();\n // Pulse whichever object is actually on screen. The mesh is pivoted on the\n // quad's centre, so it swells in place the same way the sprite does.\n const target = this._perspective.isActive ? this._perspective.mesh : this._sprite;\n if (!target) return;\n return new Promise<void>((resolve) => {\n this._winTween = this.gsap.to(target.scale, {\n x: 1.15,\n y: 1.15,\n duration: 0.15,\n yoyo: true,\n repeat: 1,\n ease: 'power2.inOut',\n onComplete: resolve,\n });\n });\n }\n\n stopAnimation(): void {\n this._killWinTween();\n this._sprite.scale.set(1, 1);\n this._perspective.resetTransform();\n }\n\n resize(width: number, height: number): void {\n this._sprite.width = width;\n this._sprite.height = height;\n }\n\n protected override onDestroy(): void {\n this._killWinTween();\n this._perspective.destroy();\n }\n\n private _killWinTween(): void {\n if (this._winTween) {\n this._winTween.kill();\n this._winTween = null;\n }\n }\n}\n","import { AnimatedSprite, type Texture } from 'pixi.js';\nimport type { ReelCellInset, ReelCellQuad } from '../config/types.js';\nimport { ReelSymbol } from './ReelSymbol.js';\nimport { PerspectiveCell, textureCellInset } from './PerspectiveCell.js';\n\nexport interface AnimatedSpriteSymbolOptions {\n /** Map of symbolId → array of frame textures. */\n frames: Record<string, Texture[]>;\n /** Playback speed (frames per second multiplier). Default: 1. */\n animationSpeed?: number;\n /** Anchor point. Default: { x: 0.5, y: 0.5 }. */\n anchor?: { x: number; y: number };\n}\n\n/**\n * Symbol implementation using PixiJS AnimatedSprite.\n * Swaps frame arrays on activate. Win animation plays the full sequence.\n */\nexport class AnimatedSpriteSymbol extends ReelSymbol {\n private _animSprite: AnimatedSprite;\n private _frames: Record<string, Texture[]>;\n private _animationSpeed: number;\n private _winResolve: (() => void) | null = null;\n private _perspective: PerspectiveCell;\n\n constructor(options: AnimatedSpriteSymbolOptions) {\n super();\n this._frames = options.frames;\n this._animationSpeed = options.animationSpeed ?? 1;\n const anchor = options.anchor ?? { x: 0, y: 0 };\n\n const firstFrames = Object.values(this._frames)[0] ?? [];\n this._animSprite = new AnimatedSprite(firstFrames.length > 0 ? firstFrames : []);\n this._animSprite.anchor.set(anchor.x, anchor.y);\n this._animSprite.animationSpeed = this._animationSpeed;\n this._animSprite.loop = false;\n this.view.addChild(this._animSprite);\n this._perspective = new PerspectiveCell(this.view, this._animSprite);\n // On a curved reel the mesh, not the sprite, is what draws. It has to be\n // handed each new frame or a playing win animation freezes on frame 0.\n this._animSprite.onFrameChange = () => {\n this._perspective.syncTexture(this._animSprite.texture);\n };\n }\n\n override get cellInset(): ReelCellInset | null {\n return textureCellInset(this._animSprite.texture);\n }\n\n override applyCellQuad(quad: ReelCellQuad | null): void {\n if (this._perspective.apply(quad, this._animSprite.texture)) {\n // The mesh's own corners carry the whole projection, so the view must\n // stay at identity. Anything else would apply the curve twice.\n this.view.scale.set(1, 1);\n this.view.pivot.set(0, 0);\n return;\n }\n // The mesh declined this texture (today: any atlas sub-frame). Take the\n // base class's uniform-scale fit so the cell is still on the drum.\n super.applyCellQuad(quad);\n }\n\n protected onActivate(symbolId: string): void {\n const frames = this._frames[symbolId];\n if (frames && frames.length > 0) {\n this._animSprite.textures = frames;\n this._animSprite.gotoAndStop(0);\n }\n }\n\n protected onDeactivate(): void {\n this._animSprite.stop();\n this._winResolve = null;\n }\n\n async playWin(): Promise<void> {\n return new Promise<void>((resolve) => {\n this._winResolve = resolve;\n this._animSprite.loop = false;\n this._animSprite.onComplete = () => {\n this._winResolve = null;\n this._animSprite.onComplete = undefined;\n // Return to frame 0 so the cell settles on its idle look instead\n // of holding the last frame of the win sequence (which for\n // generated pixel-art sequences often ends mid-action. muted,\n // shifted, or otherwise not the neutral base pose).\n this._animSprite.gotoAndStop(0);\n resolve();\n };\n this._animSprite.gotoAndPlay(0);\n });\n }\n\n stopAnimation(): void {\n this._animSprite.stop();\n this._animSprite.gotoAndStop(0);\n if (this._winResolve) {\n this._winResolve();\n this._winResolve = null;\n }\n }\n\n resize(width: number, height: number): void {\n this._animSprite.width = width;\n this._animSprite.height = height;\n // Position the sprite to match its anchor so it fills the cell.\n // Without this, an anchor of (0.5, 0.5) would render the sprite with\n // its centre at the cell's top-left corner. only the bottom-right\n // quadrant visible inside the mask.\n this._animSprite.x = width * this._animSprite.anchor.x;\n this._animSprite.y = height * this._animSprite.anchor.y;\n }\n\n protected override onDestroy(): void {\n this._animSprite.onFrameChange = undefined;\n this._perspective.destroy();\n }\n}\n","import { ReelSymbol } from './ReelSymbol.js';\n\n/**\n * A {@link ReelSymbol} that renders nothing and never animates.\n *\n * Register it for an id that needs to occupy a grid slot without producing\n * any visual - the blank rest state of a {@link HoldAndWinBoard} cell, a\n * cascade \"hole\", a dry-run symbol-set placeholder. The {@link HoldAndWinBuilder}\n * auto-registers one under its `emptyId` so callers never have to.\n */\nexport class EmptySymbol extends ReelSymbol {\n protected onActivate(_symbolId: string): void {}\n protected onDeactivate(): void {}\n async playWin(): Promise<void> {}\n stopAnimation(): void {}\n resize(_width: number, _height: number): void {}\n}\n","import { BlurFilter, Container, Rectangle, Sprite, type Texture } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport type { ReelSymbol } from '../symbols/ReelSymbol.js';\n\n/**\n * The slice of a PixiJS renderer the cache needs. `Renderer` (WebGL or\n * WebGPU) satisfies it structurally; tests can pass a stub.\n */\nexport interface SnapshotRenderer {\n generateTexture(options: {\n target: Container;\n frame?: Rectangle;\n resolution?: number;\n antialias?: boolean;\n }): Texture;\n}\n\n/** Tuning for the baked motion-blur variant of a snapshot. */\nexport interface MotionBlurOptions {\n /**\n * Smear axis — the reel's travel direction. `StaticSpinSymbol` defaults\n * this to the owning set's orientation (`'y'` vertical, `'x'` horizontal),\n * so it is only worth setting to override that, or when calling the cache\n * directly. The other axis is never blurred.\n */\n axis?: 'y' | 'x';\n /**\n * Blur strength in pixels along the axis. Default: 20% of the cell's\n * span on that axis (`cellHeight * 0.2` for `'y'`, `cellWidth * 0.2`\n * for `'x'`).\n */\n strength?: number;\n /** BlurFilter quality (number of passes). Default: 4. */\n quality?: number;\n /**\n * Extra transparent pixels added on both sides of the cell along the\n * axis so the smear isn't clipped at the texture edge.\n * Default: `ceil(strength)`.\n */\n padding?: number;\n}\n\nexport interface SpinTextureCacheOptions {\n /** The renderer used to generate snapshot textures (`app.renderer`). */\n renderer: SnapshotRenderer;\n /** Resolution for generated textures. Default: the renderer's default. */\n resolution?: number;\n /** Default motion-blur tuning for `captureBlurred`. */\n blur?: MotionBlurOptions;\n}\n\ninterface CacheEntry {\n texture: Texture;\n /** true = generated by this cache (destroyed on invalidate), false = user-provided. */\n owned: boolean;\n width: number;\n height: number;\n /** Smear axis the blurred variant was baked for (blurred entries only). */\n axis?: 'y' | 'x';\n}\n\n/**\n * Per-symbolId cache of \"spin textures\". flat snapshots a reel shows\n * instead of the live symbol (Spine skeleton, animated sprite, ...) while\n * it spins.\n *\n * Two flavors per symbolId:\n *\n * - **static**. the symbol rendered once into a RenderTexture at cell size.\n * - **blurred**. the static snapshot smeared vertically through a one-time\n * BlurFilter pass into a taller, padded RenderTexture. At spin time it's\n * an ordinary sprite texture: zero filters per frame.\n *\n * Textures come from two sources, user-provided always winning:\n *\n * - `setStatic(id, tex)` / `setBlurred(id, tex)`. hand-authored art from\n * your atlas. Never destroyed by the cache.\n * - `captureStatic` / `captureBlurred`. generated on demand from a live\n * symbol view. Owned by the cache and destroyed on `invalidate` /\n * `clear` / `destroy`.\n *\n * Captures are keyed by symbolId and remembered together with the cell size\n * they were taken at; capturing again at a different size regenerates.\n * Share ONE cache across all reels/symbols of a reel set. that's the point.\n */\nexport class SpinTextureCache implements Disposable {\n private _renderer: SnapshotRenderer;\n private _resolution: number | undefined;\n private _blurDefaults: MotionBlurOptions;\n private _static = new Map<string, CacheEntry>();\n private _blurred = new Map<string, CacheEntry>();\n private _isDestroyed = false;\n\n constructor(options: SpinTextureCacheOptions) {\n this._renderer = options.renderer;\n this._resolution = options.resolution;\n this._blurDefaults = options.blur ?? {};\n }\n\n // -- User-provided textures ----------------------------------------------\n\n /** Provide a hand-authored static spin texture. Wins over captures. */\n setStatic(symbolId: string, texture: Texture): void {\n this._put(this._static, symbolId, { texture, owned: false, width: 0, height: 0 });\n }\n\n /** Provide a hand-authored motion-blur texture. Wins over captures. */\n setBlurred(symbolId: string, texture: Texture): void {\n this._put(this._blurred, symbolId, { texture, owned: false, width: 0, height: 0 });\n }\n\n // -- Lookups ---------------------------------------------------------------\n\n getStatic(symbolId: string): Texture | null {\n return this._static.get(symbolId)?.texture ?? null;\n }\n\n getBlurred(symbolId: string): Texture | null {\n return this._blurred.get(symbolId)?.texture ?? null;\n }\n\n hasStatic(symbolId: string): boolean {\n return this._static.has(symbolId);\n }\n\n hasBlurred(symbolId: string): boolean {\n return this._blurred.has(symbolId);\n }\n\n // -- Capture ---------------------------------------------------------------\n\n /**\n * Snapshot `source` (a symbol's `view`, already activated and resized to\n * the cell) into a `width`x`height` texture and cache it under `symbolId`.\n * Returns the cached texture if one already exists at this size; a\n * user-provided texture always short-circuits.\n */\n captureStatic(symbolId: string, source: Container, width: number, height: number): Texture {\n const existing = this._static.get(symbolId);\n if (existing && (!existing.owned || (existing.width === width && existing.height === height))) {\n return existing.texture;\n }\n const texture = this._renderer.generateTexture({\n target: source,\n frame: new Rectangle(0, 0, width, height),\n resolution: this._resolution,\n antialias: true,\n });\n this._put(this._static, symbolId, { texture, owned: true, width, height });\n return texture;\n }\n\n /**\n * Bake a motion-blurred variant of the static snapshot for `symbolId`\n * (which must exist. call `captureStatic` or `setStatic` first) and\n * cache it. The smear runs along `blur.axis` — the reel's travel\n * direction (`'y'` default when called directly; `StaticSpinSymbol`\n * derives it from the set's orientation) — and the\n * result is `2 * padding` larger than the cell on that axis only. Draw\n * it center-anchored at the cell center and the smear extends evenly\n * past the cell on both sides.\n */\n captureBlurred(\n symbolId: string,\n width: number,\n height: number,\n blur?: MotionBlurOptions,\n ): Texture {\n const axis = blur?.axis ?? this._blurDefaults.axis ?? 'y';\n const existing = this._blurred.get(symbolId);\n if (\n existing &&\n (!existing.owned ||\n (existing.width === width && existing.height === height && existing.axis === axis))\n ) {\n return existing.texture;\n }\n const staticTex = this.getStatic(symbolId);\n if (!staticTex) {\n throw new Error(\n `SpinTextureCache.captureBlurred('${symbolId}'): no static texture to blur. ` +\n `Call captureStatic() or setStatic() for this symbolId first.`,\n );\n }\n\n const strength =\n blur?.strength ?? this._blurDefaults.strength ?? (axis === 'y' ? height : width) * 0.2;\n const quality = blur?.quality ?? this._blurDefaults.quality ?? 4;\n const padding = blur?.padding ?? this._blurDefaults.padding ?? Math.ceil(strength);\n\n const wrap = new Container();\n const sprite = new Sprite(staticTex);\n // Fit the source into the cell box in case it's a user-provided static\n // texture with different dimensions.\n sprite.width = width;\n sprite.height = height;\n if (axis === 'y') {\n sprite.y = padding;\n sprite.filters = [new BlurFilter({ strengthX: 0, strengthY: strength, quality })];\n } else {\n sprite.x = padding;\n sprite.filters = [new BlurFilter({ strengthX: strength, strengthY: 0, quality })];\n }\n wrap.addChild(sprite);\n\n const texture = this._renderer.generateTexture({\n target: wrap,\n frame: new Rectangle(\n 0,\n 0,\n width + (axis === 'x' ? padding * 2 : 0),\n height + (axis === 'y' ? padding * 2 : 0),\n ),\n resolution: this._resolution,\n antialias: true,\n });\n // Destroy the scratch scene but not the cached static texture.\n wrap.destroy({ children: true });\n\n this._put(this._blurred, symbolId, { texture, owned: true, width, height, axis });\n return texture;\n }\n\n // -- Invalidation ------------------------------------------------------------\n\n /** Drop (and destroy, if cache-generated) both textures for one symbolId. */\n invalidate(symbolId: string): void {\n this._drop(this._static, symbolId);\n this._drop(this._blurred, symbolId);\n }\n\n /** Drop everything. Call when the cell size changes (responsive relayout). */\n clear(): void {\n for (const id of [...this._static.keys()]) this._drop(this._static, id);\n for (const id of [...this._blurred.keys()]) this._drop(this._blurred, id);\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n this.clear();\n this._isDestroyed = true;\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n // -- Internals ----------------------------------------------------------------\n\n private _put(map: Map<string, CacheEntry>, id: string, entry: CacheEntry): void {\n this._drop(map, id);\n map.set(id, entry);\n }\n\n private _drop(map: Map<string, CacheEntry>, id: string): void {\n const existing = map.get(id);\n if (existing?.owned) existing.texture.destroy(true);\n map.delete(id);\n }\n}\n\nexport interface PrewarmSpinTexturesOptions {\n /** The shared cache to fill. */\n cache: SpinTextureCache;\n /** Every symbolId to bake. */\n ids: string[];\n /**\n * Factory for a scratch symbol used as the render source. Typically the\n * same factory you registered (e.g. `() => new SpineReelSymbol(opts)`).\n * It is activated per id, snapshotted, and destroyed at the end.\n */\n createSymbol: () => ReelSymbol;\n /** Cell width the reels will use. */\n width: number;\n /** Cell height the reels will use. */\n height: number;\n /** Also bake the motion-blur variants. Default: true. */\n blurred?: boolean;\n /** Motion-blur tuning override (defaults to the cache's). */\n blur?: MotionBlurOptions;\n}\n\n/**\n * Bake spin textures for every symbolId up front (at load or between\n * rounds) so the first spin never pays a capture hitch. Ids that already\n * have a user-provided or captured texture are skipped by the cache.\n *\n * ```ts\n * prewarmSpinTextures({\n * cache,\n * ids: ['cherry', 'lemon', 'seven'],\n * createSymbol: () => new SpineReelSymbol(spineOpts),\n * width: 150,\n * height: 150,\n * });\n * ```\n */\nexport function prewarmSpinTextures(options: PrewarmSpinTexturesOptions): void {\n const { cache, ids, width, height } = options;\n const blurred = options.blurred ?? true;\n const symbol = options.createSymbol();\n try {\n for (const id of ids) {\n if (symbol.symbolId !== id) symbol.activate(id);\n symbol.resize(width, height);\n cache.captureStatic(id, symbol.view, width, height);\n if (blurred) cache.captureBlurred(id, width, height, options.blur);\n }\n } finally {\n symbol.destroy();\n }\n}\n","import { Sprite } from 'pixi.js';\nimport type { gsap } from 'gsap';\n\nimport { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { MotionBlurOptions, SpinTextureCache } from './SpinTextureCache.js';\n\nexport interface StaticSpinSymbolOptions {\n /**\n * Factory for the wrapped \"live\" symbol. any ReelSymbol subclass\n * (SpineReelSymbol, AnimatedSpriteSymbol, a custom class). The wrapper\n * owns the instance: it activates it while the reel is at rest and\n * deactivates it while the reel spins.\n */\n createInner: () => ReelSymbol;\n /** Shared spin-texture cache. one per reel set. */\n cache: SpinTextureCache;\n /**\n * What to show while spinning. `'blurred'` (default) crossfades the\n * static snapshot into the baked motion-blur variant; `'static'` shows\n * the crisp snapshot only and never touches the blur pipeline.\n */\n spinTexture?: 'blurred' | 'static';\n /**\n * Crossfade duration (ms) from crisp snapshot to blurred snapshot at\n * spin start. matches the reel's acceleration ramp. `0` = instant.\n * Default: 120. Ignored when `spinTexture: 'static'`.\n */\n blurRampMs?: number;\n /**\n * Motion-blur tuning forwarded to `cache.captureBlurred`. `axis` defaults\n * to the owning set's travel axis, so a horizontal set smears sideways\n * without being told; pass it explicitly only to override that. The\n * sideways travel (and snapshots fit the cell by height instead of width).\n */\n blur?: MotionBlurOptions;\n}\n\n/**\n * Wraps any `ReelSymbol` and swaps it for a cached snapshot texture while\n * the reel spins. \"spin static, not Spine.\"\n *\n * At rest the inner symbol is live (idle loops, win animations, cascade\n * destruction all delegate to it). When the reel starts, the wrapper\n * snapshots the inner symbol through the shared {@link SpinTextureCache}\n * (or reuses a cached / user-provided texture), **deactivates** the inner\n * symbol so it costs nothing, and spins a plain Sprite instead. Symbols\n * that wrap in mid-spin only retarget that sprite's texture. no inner\n * symbol is ever created or ticked for them. On land the inner symbol is\n * reactivated on the final symbolId.\n *\n * With `spinTexture: 'blurred'` the sprite crossfades from the crisp\n * snapshot to a pre-baked motion-blur variant over `blurRampMs`, then\n * spins the blurred texture. The smear follows the reel's travel axis,\n * derived from the set's orientation (override with `blur.axis`). No\n * filter runs during the spin; the blur is baked once per symbolId and\n * cached.\n *\n * Register it like any other symbol:\n *\n * ```ts\n * const cache = new SpinTextureCache({ renderer: app.renderer });\n * builder.symbols((r) => {\n * for (const id of ids) {\n * r.register(id, StaticSpinSymbol, {\n * createInner: () => new SpineReelSymbol(spineOpts),\n * cache,\n * });\n * }\n * });\n * ```\n *\n * Prefer calling {@link prewarmSpinTextures} at load time so the first\n * spin doesn't pay the one-time capture cost.\n */\nexport class StaticSpinSymbol extends ReelSymbol {\n private _inner: ReelSymbol;\n private _cache: SpinTextureCache;\n private _mode: 'blurred' | 'static';\n private _rampMs: number;\n private _blurOpts: MotionBlurOptions | undefined;\n\n private _staticSprite: Sprite;\n private _blurSprite: Sprite;\n private _spinning = false;\n private _anticipating = false;\n private _cellW = 0;\n private _cellH = 0;\n private _rampTween: gsap.core.Tween | null = null;\n\n constructor(options: StaticSpinSymbolOptions) {\n super();\n this._inner = options.createInner();\n this._cache = options.cache;\n this._mode = options.spinTexture ?? 'blurred';\n this._rampMs = options.blurRampMs ?? 120;\n this._blurOpts = options.blur;\n\n this.view.addChild(this._inner.view);\n this._staticSprite = new Sprite();\n this._staticSprite.anchor.set(0.5, 0.5);\n this._staticSprite.visible = false;\n this._blurSprite = new Sprite();\n this._blurSprite.anchor.set(0.5, 0.5);\n this._blurSprite.visible = false;\n this.view.addChild(this._staticSprite, this._blurSprite);\n }\n\n /** The wrapped live symbol. for type-specific access (`inner as SpineReelSymbol`). */\n get inner(): ReelSymbol {\n return this._inner;\n }\n\n /** True while the wrapper is showing a snapshot instead of the live symbol. */\n get isShowingSnapshot(): boolean {\n return this._spinning;\n }\n\n protected onActivate(symbolId: string): void {\n if (this._spinning) {\n // Wrapped in mid-spin: never touch the inner symbol, just retarget\n // the snapshot sprite (instantly blurred. the ramp belongs to spin\n // start, not to cells joining a reel already at speed).\n this._showSnapshot(symbolId, { instant: true });\n return;\n }\n if (this._inner.symbolId !== symbolId) this._inner.activate(symbolId);\n this._hideSnapshot();\n }\n\n protected onDeactivate(): void {\n this._killRamp();\n this._spinning = false;\n this._anticipating = false;\n this._staticSprite.visible = false;\n this._blurSprite.visible = false;\n if (this._inner.symbolId !== '') this._inner.deactivate();\n }\n\n override onReelSpinStart(joinedMidSpin = false): void {\n if (this._spinning) {\n // Idempotent: a repeat notification (same-id fast path during a\n // wrap) only refreshes the snapshot target.\n this._showSnapshot(this.symbolId, { instant: true });\n return;\n }\n this._spinning = true;\n this._anticipating = false;\n // Capture while the inner symbol is still live so a cache miss can\n // snapshot it in place, then put the inner symbol to sleep for the\n // duration of the spin.\n this._showSnapshot(this.symbolId, { instant: joinedMidSpin || this._rampMs <= 0 });\n if (this._inner.symbolId !== '') this._inner.deactivate();\n }\n\n /**\n * The reel entered its anticipation tease: it is slow enough to read, so\n * crossfade the baked blur back out and ride the crisp snapshot until\n * the land. Mid-tease installs arrive blurred (`_showSnapshot` instant)\n * and immediately relax through this same hook.\n */\n override onReelAnticipationStart(): void {\n if (!this._spinning || this._anticipating) return;\n this._anticipating = true;\n if (this._mode === 'static') return;\n\n this._killRamp();\n this._staticSprite.visible = true;\n this._staticSprite.alpha = 1;\n if (!this._blurSprite.visible || this._rampMs <= 0) {\n this._blurSprite.visible = false;\n return;\n }\n this._rampTween = this.gsap.to(this._blurSprite, {\n alpha: 0,\n duration: this._rampMs / 1000,\n ease: 'power1.out',\n onComplete: () => {\n this._rampTween = null;\n this._blurSprite.visible = false;\n },\n });\n }\n\n override onReelSpinEnd(): void {\n if (!this._spinning) return;\n this._spinning = false;\n this._anticipating = false;\n this._killRamp();\n this._staticSprite.visible = false;\n this._blurSprite.visible = false;\n if (this._inner.symbolId !== this.symbolId) {\n this._inner.activate(this.symbolId);\n this._inner.resize(this._cellW, this._cellH);\n }\n }\n\n override onReelLanded(): void {\n if (!this._spinning) this._inner.onReelLanded();\n }\n\n async playWin(): Promise<void> {\n return this._inner.playWin();\n }\n\n stopAnimation(): void {\n if (this._inner.symbolId !== '') this._inner.stopAnimation();\n }\n\n override async playDestroy(opts?: { delay?: number; signal?: AbortSignal }): Promise<void> {\n // Landed symbols delegate so art-specific destruction (Spine `out`\n // tracks etc.) plays; a snapshot mid-spin falls back to the generic\n // implode on the wrapper view.\n if (!this._spinning && this._inner.symbolId !== '') return this._inner.playDestroy(opts);\n return super.playDestroy(opts);\n }\n\n resize(width: number, height: number): void {\n this._cellW = width;\n this._cellH = height;\n this._staticSprite.position.set(width / 2, height / 2);\n this._blurSprite.position.set(width / 2, height / 2);\n this._fitSprites();\n this._inner.resize(width, height);\n }\n\n protected override onDestroy(): void {\n this._killRamp();\n // Detach before destroying so the base class's view.destroy({children})\n // doesn't double-destroy the inner view.\n if (this._inner.view.parent === this.view) this.view.removeChild(this._inner.view);\n this._inner.destroy();\n }\n\n // -- Internals -------------------------------------------------------------\n\n /**\n * Point the snapshot sprites at `symbolId`'s cached textures, capturing\n * on miss. With `instant`, the end state is applied directly (blurred\n * fully visible); otherwise the crisp→blur crossfade ramp starts.\n */\n private _showSnapshot(symbolId: string, opts: { instant: boolean }): void {\n const staticTex = this._ensureStatic(symbolId);\n this._staticSprite.texture = staticTex;\n\n if (this._mode === 'blurred') {\n this._blurSprite.texture = this._cache.captureBlurred(\n symbolId,\n this._cellW,\n this._cellH,\n this._resolvedBlur(),\n );\n }\n this._fitSprites();\n\n if (this._mode === 'static') {\n this._staticSprite.visible = true;\n this._staticSprite.alpha = 1;\n return;\n }\n\n if (opts.instant) {\n // Anticipating: the tease shows the crisp snapshot. never bring the\n // blur back for re-notifications or same-id wraps mid-tease.\n if (this._anticipating) {\n this._killRamp();\n this._blurSprite.visible = false;\n this._staticSprite.visible = true;\n this._staticSprite.alpha = 1;\n return;\n }\n // Already ramping? Let the running crossfade finish on the new\n // textures instead of snapping mid-fade.\n if (this._rampTween) return;\n this._staticSprite.visible = false;\n this._blurSprite.visible = true;\n this._blurSprite.alpha = 1;\n return;\n }\n\n this._killRamp();\n this._staticSprite.visible = true;\n this._staticSprite.alpha = 1;\n this._blurSprite.visible = true;\n this._blurSprite.alpha = 0;\n this._rampTween = this.gsap.to(this._blurSprite, {\n alpha: 1,\n duration: this._rampMs / 1000,\n ease: 'power1.in',\n onComplete: () => {\n this._rampTween = null;\n this._staticSprite.visible = false;\n },\n });\n }\n\n /**\n * Static texture for `symbolId`, capturing from the inner symbol on a\n * cache miss. If the inner symbol currently holds a different identity\n * (mid-spin wrap), it is briefly activated offscreen for the capture and\n * deactivated again. one-time cost per symbolId; avoid it entirely with\n * `prewarmSpinTextures`.\n */\n private _ensureStatic(symbolId: string) {\n const cached = this._cache.getStatic(symbolId);\n if (cached) return cached;\n\n const needsTempActivation = this._inner.symbolId !== symbolId;\n if (needsTempActivation) {\n this._inner.activate(symbolId);\n this._inner.resize(this._cellW, this._cellH);\n }\n const tex = this._cache.captureStatic(symbolId, this._inner.view, this._cellW, this._cellH);\n if (needsTempActivation && this._spinning) this._inner.deactivate();\n return tex;\n }\n\n private _hideSnapshot(): void {\n this._killRamp();\n this._staticSprite.visible = false;\n this._blurSprite.visible = false;\n }\n\n /**\n * Uniformly fit both sprites to the cell along the axis the blur does\n * NOT pad (keeps aspect; the padded axis then centers out evenly past\n * the cell). Vertical reels fit by width; horizontal strips fit by\n * height.\n */\n private _fitSprites(): void {\n if (this._cellW <= 0 || this._cellH <= 0) return;\n const fitByHeight = this._resolvedBlur().axis === 'x';\n for (const sprite of [this._staticSprite, this._blurSprite]) {\n const tw = sprite.texture.width;\n const th = sprite.texture.height;\n if (tw <= 0 || th <= 0) continue;\n sprite.scale.set(fitByHeight ? this._cellH / th : this._cellW / tw);\n }\n }\n\n /**\n * Blur options with the smear axis resolved.\n *\n * The smear must follow the strip, so the default comes from the owning\n * set's orientation rather than being hardcoded to `'y'`. Before this,\n * a horizontal set using `StaticSpinSymbol` smeared vertically - across\n * the direction of travel - and nothing said so. An explicit\n * `blur.axis` still wins, for art that wants a deliberate cross-smear.\n */\n private _resolvedBlur(): MotionBlurOptions {\n return { ...this._blurOpts, axis: this._blurOpts?.axis ?? this.mainAxis };\n }\n\n private _killRamp(): void {\n if (this._rampTween) {\n this._rampTween.kill();\n this._rampTween = null;\n }\n }\n}\n","import { Graphics, Text } from 'pixi.js';\nimport { ReelSymbol } from './ReelSymbol.js';\n\n/**\n * **Debug / prototyping symbol - NOT for production.**\n *\n * `CardSymbol` is a flat-rectangle-plus-centered-text `ReelSymbol` drawn\n * entirely with `PIXI.Graphics`. It exists to give recipes, demos, and\n * mechanic tests a no-asset, no-loader, infinitely-resizable visual that\n * always renders crisply at any cell size, showing how the engine treats\n * cell space across MultiWays reshapes, big-symbol blocks, and pyramid\n * layouts.\n *\n * In a real game, ship one of:\n *\n * - `SpriteSymbol` - pre-rendered art at one resolution. Cheapest.\n * - `AnimatedSpriteSymbol` - frame-by-frame win/idle animation.\n * - `SpineSymbol` (via `pixi-reels/spine`) - vector skeletal animation\n * that scales without quality loss. Best for MultiWays where cells\n * resize across spins.\n * - Or a custom `ReelSymbol` subclass for game-specific visuals.\n *\n * Ships from the package so a prototype runs with no art at all. It is\n * deliberately plain `Graphics`: swap it for real art before you ship.\n *\n * ```ts\n * import { CardSymbol, CARD_DECK } from 'pixi-reels';\n *\n * builder.symbols((registry) => {\n * for (const card of CARD_DECK) {\n * registry.register(card.id, CardSymbol, { color: card.color, label: card.label });\n * }\n * });\n * ```\n */\nexport interface CardSymbolOptions {\n /** Hex fill color (e.g. `0xc0392b`). */\n color: number;\n /** Centered label text (e.g. `'A'`, `'10'`, `'WILD'`). */\n label: string;\n /** Text fill color. Defaults to white. */\n textColor?: number;\n}\n\nexport class CardSymbol extends ReelSymbol {\n private _color: number;\n private _label: string;\n private _textColor: number;\n private _gfx: Graphics;\n private _text: Text;\n\n constructor(opts: CardSymbolOptions) {\n super();\n this._color = opts.color;\n this._label = opts.label;\n this._textColor = opts.textColor ?? 0xffffff;\n this._gfx = new Graphics();\n this._text = new Text({\n text: this._label,\n style: {\n // Roboto Condensed if installed (the site loads it via @fontsource);\n // falls through to a system condensed sans-serif on plain pages.\n fontFamily:\n '\"Roboto Condensed\", \"Arial Narrow\", \"Helvetica Neue Condensed\", \"Liberation Sans Narrow\", system-ui, sans-serif',\n fontSize: 32,\n fontWeight: '700',\n fill: this._textColor,\n align: 'center',\n },\n });\n this._text.anchor.set(0.5);\n this.view.addChild(this._gfx);\n this.view.addChild(this._text);\n }\n\n protected onActivate(_symbolId: string): void {\n // No-op - this symbol's visual identity is set at construction.\n }\n\n protected onDeactivate(): void {\n // No-op - leave _gfx/_text in their last state until next activate.\n }\n\n async playWin(): Promise<void> {\n return new Promise((resolve) => {\n // Animate only the glyph - the card body stays put. Useful for big\n // symbols (the rectangle is the cell footprint) and any setting\n // where movement of the cell border is distracting (cascade refills,\n // MultiWays adjust). The text is anchored at (0.5, 0.5) so its\n // scale tween pivots about the centre.\n this.gsap.killTweensOf(this._text);\n this.gsap.killTweensOf(this._text.scale);\n const originalFill = this._textColor;\n const tl = this.gsap.timeline({\n onComplete: () => {\n this._text.scale.set(1, 1);\n this._text.rotation = 0;\n this._text.style.fill = originalFill;\n resolve();\n },\n });\n tl.to(this._text.scale, { x: 1.4, y: 1.4, duration: 0.18, ease: 'back.out(2)' }, 0)\n .to(this._text, { rotation: -0.12, duration: 0.09, ease: 'sine.inOut' }, 0)\n .to(this._text, { rotation: 0.12, duration: 0.18, ease: 'sine.inOut' }, 0.09)\n .to(this._text, { rotation: 0, duration: 0.09, ease: 'sine.inOut' }, 0.27)\n .to(this._text.scale, { x: 1, y: 1, duration: 0.18, ease: 'power2.out' }, 0.32);\n // Glyph fill flashes warm gold mid-pulse - `Text.style.fill` isn't\n // a tweenable target on every Pixi version, so we set it discretely.\n this.gsap.delayedCall(0.06, () => (this._text.style.fill = 0xffe168));\n this.gsap.delayedCall(0.42, () => (this._text.style.fill = originalFill));\n });\n }\n\n stopAnimation(): void {\n this.gsap.killTweensOf(this._text);\n this.gsap.killTweensOf(this._text.scale);\n this._text.scale.set(1, 1);\n this._text.rotation = 0;\n this._text.style.fill = this._textColor;\n this._gfx.alpha = 1;\n }\n\n resize(width: number, height: number): void {\n this._gfx.clear();\n this._gfx.rect(0, 0, width, height).fill({ color: this._color });\n // Inner darker stroke so adjacent cells are visually separated even\n // when they share the same color.\n this._gfx.rect(1, 1, width - 2, height - 2).stroke({ color: 0x000000, width: 2, alpha: 0.25 });\n this._text.x = width / 2;\n this._text.y = height / 2;\n // Font size constrained by both height (~38%) and label width (so\n // multi-char labels like '10' or 'WILD' don't overflow narrow cells).\n // Smaller than half-cell to leave breathing room - looks less crowded.\n const labelLen = Math.max(1, this._label.length);\n const fitH = height * 0.38;\n // Roboto Condensed glyphs are narrower (~0.45 avg ratio) than serif fonts.\n const fitW = (width * 0.7) / (labelLen * 0.45);\n this._text.style.fontSize = Math.max(7, Math.floor(Math.min(fitH, fitW)));\n }\n}\n\n/**\n * Standard high-card deck for prototyping. Each card has its own color\n * so a full grid is visually unambiguous at a glance.\n */\nexport const CARD_DECK: ReadonlyArray<{ id: string; color: number; label: string }> = [\n { id: '7', color: 0xc0392b, label: '7' }, // crimson\n { id: '8', color: 0xe67e22, label: '8' }, // orange\n { id: '9', color: 0xf1c40f, label: '9' }, // amber\n { id: '10', color: 0x27ae60, label: '10' }, // green\n { id: 'J', color: 0x16a085, label: 'J' }, // teal\n { id: 'Q', color: 0x2980b9, label: 'Q' }, // blue\n { id: 'K', color: 0x8e44ad, label: 'K' }, // purple\n { id: 'A', color: 0x2c3e50, label: 'A' }, // navy\n];\n\n/** A pale-yellow `WILD` card for sticky/expanding-wild prototypes. */\nexport const WILD_CARD = {\n id: 'wild',\n color: 0xfff3a0,\n label: 'WILD',\n textColor: 0x6b5400,\n} as const;\n","import type { ColumnTarget } from '../frame/ColumnTarget.js';\n\nexport interface ScatterAnticipationOptions {\n /** The trigger symbol id to scan for (e.g. `'SCAT'`). */\n symbol: string;\n /**\n * How many of `symbol` must be showing before anticipation kicks in.\n * Anticipation begins on the reel AFTER the one that lands the\n * `trigger`-th symbol. Default `2` (the classic \"2 scatters and the rest\n * of the reels start teasing\").\n */\n trigger?: number;\n /**\n * Which of the remaining reels should tease:\n * - `'all-remaining'` (default): every reel after the trigger reel teases,\n * whether or not it actually holds the symbol. Maximum tension.\n * - `'scatter-only'`: only reels that actually contain the symbol tease.\n * Use when a result can't complete the feature on some reels and you\n * don't want to fake tension on reels that don't matter (e.g. a grid\n * with only 3 scatters total shouldn't slow the empty reels between/after\n * them).\n */\n mode?: 'all-remaining' | 'scatter-only';\n}\n\n/**\n * Decide which reels should show anticipation, straight from a result grid.\n *\n * Scans reels left→right counting `symbol` occurrences; once the running total\n * reaches `trigger`, the reels AFTER that point become the anticipation set\n * (per `mode`). Feed the return value straight into\n * `reelSet.setAnticipation(...)`.\n *\n * Returns an empty array when the grid never reaches `trigger` (no tease).\n *\n * `grid` is the same `ColumnTarget[]` you pass to `reelSet.setResult(...)`, so\n * you can hand the result straight through. Only visible cells are scanned.\n * buffer symbols are off-screen and never trigger tension.\n *\n * @example\n * // Result has SCAT on reels 0 and 1 → tease reels [2,3,4].\n * const grid = [{ visible: ['A','SCAT','B'] }, { visible: ['SCAT','A','B'] }, ...];\n * const reels = anticipationForScatters(grid, { symbol: 'SCAT', trigger: 2 });\n * reelSet.setResult(grid);\n * reelSet.setAnticipation(reels, { stagger: 'sequential', slowdown: { from: 0.4, to: 0.1 } });\n *\n * @example\n * // Only 3 scatters total (reels 0,1,3): 'scatter-only' teases just reel [3],\n * // leaving reels 2 and 4 to stop normally.\n * const reels = anticipationForScatters(grid, { symbol: 'SCAT', mode: 'scatter-only' });\n */\nexport function anticipationForScatters(\n grid: ColumnTarget[],\n opts: ScatterAnticipationOptions,\n): number[] {\n const trigger = opts.trigger ?? 2;\n const mode = opts.mode ?? 'all-remaining';\n\n // Count the trigger symbol per reel. Only visible cells are scanned; buffer\n // symbols are off-screen and must not trigger tension.\n const perReelCount = grid.map(\n (reel) => reel.visible.filter((id) => id === opts.symbol).length,\n );\n\n // Find the reel at which the running total first reaches `trigger`.\n let running = 0;\n let triggerReel = -1;\n for (let i = 0; i < perReelCount.length; i++) {\n running += perReelCount[i];\n if (running >= trigger) {\n triggerReel = i;\n break;\n }\n }\n if (triggerReel === -1) return [];\n\n const result: number[] = [];\n for (let i = triggerReel + 1; i < grid.length; i++) {\n if (mode === 'all-remaining' || perReelCount[i] > 0) result.push(i);\n }\n return result;\n}\n","import type { SpinningMode } from './SpinningMode.js';\n\n/**\n * Cascade/tumble spinning mode.\n * Symbols fall from above with gravity-like acceleration,\n * used for tumble/avalanche mechanics.\n */\nexport class CascadeMode implements SpinningMode {\n readonly name = 'cascade';\n\n private _gravity: number;\n\n /**\n * @param gravity - Gravity acceleration factor. Default: 1.5.\n */\n constructor(gravity: number = 1.5) {\n this._gravity = gravity;\n }\n\n computeDelta(slotPitch: number, speed: number, deltaMs: number): number {\n const raw = (slotPitch * speed * this._gravity * deltaMs) / 1000;\n // Full-slot cap. The old wrap-skip risk (contract L7) is gone now that\n // ReelMotion derives rotation from total travel, so this only bounds the\n // per-frame step; cascade speed is never negative.\n return Math.min(raw, slotPitch);\n }\n}\n","import type { SpinningMode } from './SpinningMode.js';\n\n/**\n * Immediate placement mode for super-turbo skip.\n * Symbols snap to position instantly. no actual spinning animation.\n */\nexport class ImmediateMode implements SpinningMode {\n readonly name = 'immediate';\n\n computeDelta(_slotPitch: number, _speed: number, _deltaMs: number): number {\n // No movement. placement is handled directly by the phase\n return 0;\n }\n}\n","import { Container, Graphics } from 'pixi.js';\nimport type { Ticker } from 'pixi.js';\nimport { ReelSetBuilder } from '../core/ReelSetBuilder.js';\nimport type { Direction, Orientation } from '../core/ReelAxis.js';\nimport type { ReelSet } from '../core/ReelSet.js';\nimport { SharedRectMaskStrategy } from '../core/ReelViewport.js';\nimport type { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { SymbolRegistry } from '../symbols/SymbolRegistry.js';\nimport { EmptySymbol } from '../symbols/EmptySymbol.js';\nimport { SpeedPresets } from '../config/SpeedPresets.js';\nimport type { SpeedProfile, SymbolData } from '../config/types.js';\nimport type { Disposable } from '../utils/Disposable.js';\n\n/** A cell coordinate in the grid. */\nexport interface BoardCell {\n reel: number;\n cell: number;\n}\n\n/** A landing target: spin `cell` and stop it showing `id`. */\nexport interface BoardSpinTarget {\n cell: BoardCell;\n id: string;\n}\n\n/** A speed profile, or a per-cell function of one (e.g. a stagger wave). */\nexport type BoardProfile = SpeedProfile | ((cell: BoardCell) => SpeedProfile);\n\nexport interface BoardGridOptions {\n /** Grid dimensions. */\n cols: number;\n rows: number;\n /** Cell edge length in pixels. */\n cellSize: number;\n /** Gap between cells. Default 4. */\n gap?: number;\n /** Id a cell shows when blank - also placed in the off-window buffers. Default `'empty'`. */\n emptyId?: string;\n /** Register symbol classes, exactly like `ReelSetBuilder.symbols`. Applied to every cell. */\n symbols: (registry: SymbolRegistry) => void;\n /** Strip weights during the spin. */\n weights?: Record<string, number>;\n /** Per-symbol engine overrides, exactly like `ReelSetBuilder.symbolData`. */\n symbolData?: Record<string, Partial<SymbolData>>;\n /** Injected RNG for the spin strips (deterministic demos / tests). */\n rng?: () => number;\n /** Drives every cell's reel - required. */\n ticker: Ticker;\n /** Per-cell background, drawn behind each reel. */\n chrome?: (g: Graphics, size: number) => void;\n /**\n * Which way each cell's own strip travels while it spins. Every cell is a\n * 1x1 reel set, so this changes the direction a symbol scrolls in from, not\n * the board layout - `cols` and `rows` stay board dimensions either way.\n * Defaults to the engine default (vertical / forward), i.e. symbols drop in\n * from above.\n */\n orientation?: Orientation;\n direction?: Direction;\n /**\n * Named speed profiles, each registered on every cell and selected by name\n * via {@link BoardGrid.setProfile}. A value may be a flat profile or a\n * per-cell function (for stagger waves). Defaults to a single `'default'`\n * profile, which is the active one until you `setProfile` otherwise.\n */\n profiles?: Record<string, BoardProfile>;\n}\n\nconst key = (c: BoardCell): string => `${c.reel},${c.cell}`;\nconst DEFAULT_PROFILE = 'default';\n\n/**\n * A grid of cells that each spin **independently** - the generic \"board of\n * reels\" primitive. Every cell is its own 1×1 {@link ReelSet}, so it inherits\n * the engine's phases, speed modes and pooling rather than a parallel lighter\n * reel.\n *\n * Deliberately mechanism-only: it knows nothing about coins, locks, respins,\n * value or any game rule. It lays the grid out, hands back per-cell geometry\n * and live symbol instances, places symbols instantly, and spins a\n * **caller-chosen** set of cells to caller-chosen results. Build your own\n * feature on top by owning the rules in your own code; {@link HoldAndWinBoard}\n * is one such opinionated layer, built entirely on this public surface.\n *\n * ```ts\n * const grid = new BoardGrid({\n * cols: 3, rows: 3, cellSize: 80,\n * symbols: (r) => r.register('prize', PrizeSymbol, {}),\n * weights: { prize: 1, empty: 4 },\n * ticker: app.ticker,\n * });\n * app.stage.addChild(grid.container);\n *\n * await grid.spinCells(\n * grid.cells().map((cell) => ({ cell, id: pick() })), // you decide each result\n * (cell, id) => console.log('landed', cell, id), // react as each settles\n * );\n * ```\n */\nexport class BoardGrid implements Disposable {\n readonly container: Container;\n readonly cols: number;\n readonly rows: number;\n readonly cellSize: number;\n readonly gap: number;\n readonly emptyId: string;\n\n private readonly _reels = new Map<string, ReelSet>();\n private readonly _cells: BoardCell[] = [];\n private _destroyed = false;\n\n constructor(opts: BoardGridOptions) {\n if (!opts.ticker) throw new Error('BoardGrid: a ticker is required.');\n this.cols = opts.cols;\n this.rows = opts.rows;\n this.cellSize = opts.cellSize;\n this.gap = opts.gap ?? 4;\n this.emptyId = opts.emptyId ?? 'empty';\n this.container = new Container();\n\n const profiles: Record<string, BoardProfile> =\n opts.profiles && Object.keys(opts.profiles).length > 0\n ? opts.profiles\n : { [DEFAULT_PROFILE]: { ...SpeedPresets.NORMAL, minimumSpinTime: 320 } };\n const profileNames = Object.keys(profiles);\n const profileFor = (p: BoardProfile, cell: BoardCell): SpeedProfile =>\n typeof p === 'function' ? p(cell) : p;\n\n for (let reel = 0; reel < opts.cols; reel++) {\n for (let rowIdx = 0; rowIdx < opts.rows; rowIdx++) {\n const cell: BoardCell = { reel, cell: rowIdx };\n const origin = this._origin(cell);\n if (opts.chrome) {\n const bg = new Graphics();\n opts.chrome(bg, this.cellSize);\n bg.position.set(origin.x, origin.y);\n this.container.addChild(bg);\n }\n\n const builder = new ReelSetBuilder()\n .reels(1)\n .visibleCells(1)\n .symbolSize(this.cellSize, this.cellSize)\n .symbolGap(0, 0)\n // Spine symbols overrun the default per-reel rect mask; a shared rect\n // keeps buffer-cell art from painting over neighbouring cells.\n .maskStrategy(new SharedRectMaskStrategy())\n .symbols((registry) => {\n opts.symbols(registry);\n if (!registry.has(this.emptyId)) registry.register(this.emptyId, EmptySymbol, {});\n })\n .initialFrame([\n { visible: [this.emptyId], bufferStart: [this.emptyId], bufferEnd: [this.emptyId] },\n ])\n .ticker(opts.ticker)\n .orientation(opts.orientation ?? 'vertical')\n .direction(opts.direction ?? 'forward')\n // The active profile defaults to the engine's 'normal'; point it at\n // the first registered name so any profile vocabulary works.\n .initialSpeed(profileNames[0]);\n for (const [name, profile] of Object.entries(profiles)) {\n builder.speed(name, profileFor(profile, cell));\n }\n if (opts.weights) builder.weights(opts.weights);\n if (opts.symbolData) builder.symbolData(opts.symbolData);\n if (opts.rng) builder.rng(opts.rng);\n\n const reelSet = builder.build();\n reelSet.position.set(origin.x, origin.y);\n this.container.addChild(reelSet);\n this._reels.set(key(cell), reelSet);\n this._cells.push(cell);\n }\n }\n }\n\n /** Every cell coordinate, reel-major: (0,0), (0,1), ... then (1,0). */\n cells(): BoardCell[] {\n return this._cells.map((c) => ({ reel: c.reel, cell: c.cell }));\n }\n\n /** Board-local bounds of a cell. `container.toGlobal` for stage space. */\n cellBounds(cell: BoardCell): { x: number; y: number; width: number; height: number } {\n const origin = this._origin(cell);\n return { x: origin.x, y: origin.y, width: this.cellSize, height: this.cellSize };\n }\n\n /** Board-local center of a cell - flight / trail start and end points. */\n cellCenter(cell: BoardCell): { x: number; y: number } {\n const origin = this._origin(cell);\n return { x: origin.x + this.cellSize / 2, y: origin.y + this.cellSize / 2 };\n }\n\n /** Live symbol instance currently shown in a cell. */\n symbolAt(cell: BoardCell): ReelSymbol {\n return this._reel(cell).getReel(0).getSymbolAt(0);\n }\n\n /** The cell's underlying 1×1 ReelSet, for driving one cell directly. */\n reelAt(cell: BoardCell): ReelSet {\n return this._reel(cell);\n }\n\n /** Select a registered speed profile by name for one cell. */\n setProfile(cell: BoardCell, name: string): void {\n this._reel(cell).speed.set(name);\n }\n\n /** Place a symbol instantly (no spin), with blank off-window buffers. */\n place(cell: BoardCell, id: string): void {\n // Explicit empties in both buffers keep the rest state from\n // random-filling past the mask.\n this._reel(cell).getReel(0).placeSymbols({\n visible: [id],\n bufferStart: [this.emptyId],\n bufferEnd: [this.emptyId],\n });\n }\n\n /**\n * Spin each target cell and stop it showing its `id`; `onLanded` fires per\n * cell as it settles, in stagger order. The caller selects which cells spin\n * and to what - this layer applies no lock/free policy of its own. Set\n * profiles via {@link setProfile} first.\n *\n * `onLanded` may be **async**: if it returns a promise, that cell's task\n * awaits it, so the returned promise resolves only once every cell has landed\n * *and* its after-land work has finished. Cells still run concurrently, so an\n * early cell's reveal overlaps with later cells still spinning.\n */\n async spinCells(\n targets: BoardSpinTarget[],\n onLanded: (cell: BoardCell, id: string) => void | Promise<void> = () => {},\n ): Promise<void> {\n await Promise.all(\n targets.map(async ({ cell, id }) => {\n const reelSet = this._reel(cell);\n const settle = reelSet.spin();\n // Buffers land empty too: off-window art can paint over neighbours.\n reelSet.setResult([\n { visible: [id], bufferStart: [this.emptyId], bufferEnd: [this.emptyId] },\n ]);\n await settle;\n await onLanded(cell, id);\n }),\n );\n }\n\n /** Slam every in-flight cell to its landed position. Returns the count. */\n skipSpinning(): number {\n let inFlight = 0;\n for (const reelSet of this._reels.values()) {\n if (reelSet.isSpinning) {\n inFlight += 1;\n try {\n reelSet.skipSpin();\n } catch {\n /* result not provided yet - nothing to skip to; ignore */\n }\n }\n }\n return inFlight;\n }\n\n get isDestroyed(): boolean {\n return this._destroyed;\n }\n\n destroy(): void {\n if (this._destroyed) return;\n this._destroyed = true;\n for (const reelSet of this._reels.values()) reelSet.destroy();\n this._reels.clear();\n this._cells.length = 0;\n this.container.destroy({ children: true });\n }\n\n private _origin(cell: BoardCell): { x: number; y: number } {\n return {\n x: cell.reel * (this.cellSize + this.gap),\n y: cell.cell * (this.cellSize + this.gap),\n };\n }\n\n private _reel(cell: BoardCell): ReelSet {\n const reelSet = this._reels.get(key(cell));\n if (!reelSet) {\n throw new Error(`BoardGrid: cell ${key(cell)} is outside the ${this.cols}x${this.rows} grid.`);\n }\n return reelSet;\n }\n}\n","/**\n * Public types for the Hold & Win board. Kept in one leaf module so the\n * reducer ({@link HoldAndWinState}), the driver ({@link HoldAndWinBoard}) and\n * the builder can share them without import cycles.\n */\n\n/** Grid coordinate of a board cell. */\nexport interface HwCell {\n reel: number;\n cell: number;\n}\n\n/**\n * A coin somewhere on the board. `id` selects the registered symbol art;\n * `data` is an opaque game-layer payload the board never interprets.\n *\n * **Ownership contract:** `cell` and `id` belong to the board - treat them as\n * read-only; the board keys its ledger by cell and mutating them is undefined.\n * `data` is yours: mutate its contents freely (a doubler does `coin.data.value\n * *= 2`), that is the intended way to carry live game state on a locked coin.\n */\nexport interface HwCoin<TData = unknown> {\n readonly cell: HwCell;\n readonly id: string;\n data?: TData;\n}\n\n/** Why the respin counter changed - disambiguates a `respins:changed` event. */\nexport type HwRespinReason = 'seed' | 'hit-reset' | 'miss';\n\n/** Resolution of one {@link HoldAndWinBoard.respin} wave. */\nexport interface HwRespinResult<TData = unknown> {\n round: number;\n hits: HwCoin<TData>[];\n respinsLeft: number;\n full: boolean;\n /** True when the feature is over (counter exhausted or board full). */\n done: boolean;\n}\n\nexport type HoldAndWinBoardEvents<TData = unknown> = {\n 'feature:enter': [{ seed: HwCoin<TData>[]; respins: number }];\n 'respin:start': [{ round: number; respinsLeft: number; spinning: HwCell[] }];\n /** Fires per cell, in landing (stagger) order. `coin` is null on a miss. */\n 'cell:landed': [{ cell: HwCell; coin: HwCoin<TData> | null }];\n 'coin:locked': [{ coin: HwCoin<TData>; locked: number; capacity: number }];\n /** Fired by `release()` - the collect / fly-away moment. */\n 'coin:released': [{ coin: HwCoin<TData>; remaining: number }];\n 'respins:changed': [{ value: number; reason: HwRespinReason }];\n 'respin:end': [{ round: number; hits: HwCoin<TData>[]; respinsLeft: number }];\n 'board:full': [{ coins: HwCoin<TData>[] }];\n /** Fired by `skip()` so the game layer can cut its own flights / collect short. */\n 'feature:skip': [{ inFlight: number }];\n /**\n * Fired by `reset()` - a hard clear back to idle. Distinct from `coin:released`\n * (which means \"collect this coin\"); listeners that maintain derived state\n * from events (HUD totals, meters) clear it here without triggering collect.\n */\n 'feature:reset': [{ clearedCoins: number }];\n 'feature:end': [{ coins: HwCoin<TData>[]; rounds: number; full: boolean }];\n};\n\n/**\n * One state-change the reducer ({@link HoldAndWinState}) decided, ready for the\n * driver to emit. A tagged pair of `{ type, payload }` for every board event the\n * reducer owns - the driver replays them onto its emitter and keys visual side\n * effects (e.g. `playWin()` on `coin:locked`) off the type. `respin:start` and\n * `feature:skip` are driver-owned (they describe in-flight reels, not ledger\n * state) and are not produced here.\n */\nexport type HwEffect<TData = unknown> = {\n [K in keyof HoldAndWinBoardEvents<TData>]: {\n type: K;\n payload: HoldAndWinBoardEvents<TData>[K][0];\n };\n}[keyof HoldAndWinBoardEvents<TData>];\n\nexport interface HwCellSizeOptions {\n gap?: number;\n}\n\n/** `reel,cell` string key for the board's cell-indexed maps. */\nexport const cellKey = (c: HwCell): string => `${c.reel},${c.cell}`;\n","import type { HwCell, HwCoin, HwEffect, HwRespinReason } from './HwTypes.js';\nimport { cellKey } from './HwTypes.js';\n\nexport type HwPhase = 'idle' | 'active' | 'spinning';\n\n/**\n * The pure Hold & Win state machine - the single source of truth for board\n * state, with **zero** PixiJS. It owns the locked-coin ledger, the respin\n * counter, the round number and the feature {@link HwPhase}; every derived\n * value (`freeCells`, `isFull`) is computed from the one ledger, never stored\n * in parallel, so nothing can drift out of sync.\n *\n * It is a *reducer, not a cache*: {@link HoldAndWinBoard} drives the reels and\n * reports each landing here; the methods mutate the ledger and **return the\n * ordered list of {@link HwEffect}s to emit**, which the driver replays onto its\n * event emitter (and uses to fire visual side effects like `playWin()`). Locks\n * happen progressively as cells land in stagger order, so this is an\n * *incremental* reducer ({@link beginWave} → N×{@link land} → {@link endWave}),\n * not a one-shot `reduce(state, hits)`.\n *\n * Being PixiJS-free, it is fully unit-testable: assert the returned effect\n * sequence for hit / miss / full / release / reset / error paths.\n */\nexport class HoldAndWinState<TData = unknown> {\n private readonly _locked = new Map<string, HwCoin<TData>>();\n private readonly _cellSet: Set<string>;\n private readonly _allCells: HwCell[];\n private readonly _defaultRespins: number;\n private _respinsLeft = 0;\n private _round = 0;\n private _phase: HwPhase = 'idle';\n private _waveLanded: HwCoin<TData>[] = [];\n\n constructor(allCells: HwCell[], defaultRespins: number) {\n this._allCells = allCells;\n this._cellSet = new Set(allCells.map(cellKey));\n this._defaultRespins = defaultRespins;\n }\n\n // ── Queries ──────────────────────────────────────────────────────────\n\n get phase(): HwPhase {\n return this._phase;\n }\n get respinsLeft(): number {\n return this._respinsLeft;\n }\n get round(): number {\n return this._round;\n }\n get capacity(): number {\n return this._allCells.length;\n }\n get isFull(): boolean {\n return this._locked.size === this.capacity;\n }\n\n lockedCoins(): HwCoin<TData>[] {\n return [...this._locked.values()];\n }\n\n freeCells(): HwCell[] {\n return this._allCells.filter((c) => !this._locked.has(cellKey(c)));\n }\n\n isLocked(cell: HwCell): boolean {\n return this._locked.has(cellKey(cell));\n }\n\n coinAt(cell: HwCell): HwCoin<TData> | undefined {\n return this._locked.get(cellKey(cell));\n }\n\n // ── Transitions (mutate + return effects to emit) ───────────────────\n\n /** Seed the trigger coins and arm the counter. Idle → active. */\n enter(seed: HwCoin<TData>[]): HwEffect<TData>[] {\n if (this._phase !== 'idle') {\n throw new Error('HoldAndWinBoard: enter() while a feature is active — call reset() first.');\n }\n const placed: HwCoin<TData>[] = [];\n const seen = new Set<string>();\n for (const coin of seed) {\n const k = cellKey(coin.cell);\n this._assertInGrid(coin.cell, 'enter');\n if (seen.has(k)) throw new Error(`HoldAndWinBoard: enter() seeds cell ${k} twice.`);\n seen.add(k);\n const stored = this._freeze(coin.cell, coin.id, coin.data);\n this._locked.set(k, stored);\n placed.push(stored);\n }\n this._round = 0;\n this._phase = 'active';\n return [\n this._setRespins(this._defaultRespins, 'seed'),\n { type: 'feature:enter', payload: { seed: placed, respins: this._respinsLeft } },\n ];\n }\n\n /**\n * Open a wave: validate the hits, mark every free cell as spinning, bump the\n * round. Returns the data the driver needs to emit `respin:start` and to land\n * each cell. Active → spinning.\n */\n beginWave(hits: HwCoin<TData>[]): {\n round: number;\n spinning: HwCell[];\n hitByKey: Map<string, HwCoin<TData>>;\n } {\n if (this._phase === 'idle') {\n throw new Error('HoldAndWinBoard: respin() before enter().');\n }\n if (this._phase === 'spinning') {\n throw new Error('HoldAndWinBoard: respin() while a wave is in flight.');\n }\n const hitByKey = new Map<string, HwCoin<TData>>();\n for (const hit of hits) {\n const k = cellKey(hit.cell);\n this._assertInGrid(hit.cell, 'respin');\n if (this._locked.has(k)) throw new Error(`HoldAndWinBoard: hit targets locked cell ${k}.`);\n // A free cell can only land once per wave, so a duplicate hit is always a\n // malformed result. Fail loud rather than silently dropping the first coin.\n if (hitByKey.has(k)) throw new Error(`HoldAndWinBoard: respin() targets cell ${k} twice.`);\n hitByKey.set(k, hit);\n }\n this._waveLanded = [];\n this._phase = 'spinning';\n this._round += 1;\n return { round: this._round, spinning: this.freeCells(), hitByKey };\n }\n\n /** Record one cell's landing. `coin` is null on a miss. */\n land(cell: HwCell, coin: HwCoin<TData> | null): HwEffect<TData>[] {\n // Outside a wave this is a stray landing - a sibling reel settling after\n // the wave was aborted on error, or a cell landing after reset(). Drop it\n // so it can't re-lock a coin into a cleared ledger or resurrect a feature.\n if (this._phase !== 'spinning') return [];\n if (!coin) {\n return [{ type: 'cell:landed', payload: { cell, coin: null } }];\n }\n const stored = this._freeze(cell, coin.id, coin.data);\n this._locked.set(cellKey(cell), stored);\n this._waveLanded.push(stored);\n return [\n { type: 'cell:landed', payload: { cell, coin: stored } },\n {\n type: 'coin:locked',\n payload: { coin: stored, locked: this._locked.size, capacity: this.capacity },\n },\n ];\n }\n\n /** Close the wave: resolve the counter, detect full / feature end. */\n endWave(): { effects: HwEffect<TData>[]; landed: HwCoin<TData>[] } {\n // The wave was aborted or reset out from under us (see land()'s guard).\n // Closing it would re-arm the counter and flip a finished feature back to\n // active off a wave that no longer exists. Do nothing.\n if (this._phase !== 'spinning') return { effects: [], landed: [] };\n const landed = this._waveLanded;\n const effects: HwEffect<TData>[] = [];\n effects.push(\n landed.length > 0\n ? this._setRespins(this._defaultRespins, 'hit-reset')\n : this._setRespins(this._respinsLeft - 1, 'miss'),\n );\n this._phase = 'active';\n effects.push({\n type: 'respin:end',\n payload: { round: this._round, hits: [...landed], respinsLeft: this._respinsLeft },\n });\n const full = this.isFull;\n if (full) effects.push({ type: 'board:full', payload: { coins: this.lockedCoins() } });\n const done = full || this._respinsLeft <= 0;\n if (done) {\n this._phase = 'idle';\n effects.push({\n type: 'feature:end',\n payload: { coins: this.lockedCoins(), rounds: this._round, full },\n });\n }\n // Hand back a caller-owned copy, not the live `_waveLanded` reference, so a\n // consumer that mutates `respin().hits` can't reach into reducer state. The\n // `respin:end` event payload above is already copied for the same reason.\n return { effects, landed: [...landed] };\n }\n\n /**\n * Abandon an in-flight wave after a driver error: restore the phase from\n * `spinning` back to `active` so a thrown spin doesn't strand the board (every\n * later `beginWave` would otherwise throw \"wave in flight\"). Cells that already\n * landed stay locked; the caller decides whether to retry or `reset`.\n */\n abortWave(): void {\n if (this._phase !== 'spinning') return;\n this._phase = 'active';\n this._waveLanded = [];\n }\n\n /** Remove locked coins - the collect moment. */\n release(cells: HwCell[]): { effects: HwEffect<TData>[]; released: HwCoin<TData>[] } {\n if (this._phase === 'spinning') {\n throw new Error('HoldAndWinBoard: release() while a wave is in flight — await respin() first.');\n }\n const effects: HwEffect<TData>[] = [];\n const released: HwCoin<TData>[] = [];\n for (const cell of cells) {\n const k = cellKey(cell);\n const coin = this._locked.get(k);\n if (!coin) continue;\n this._locked.delete(k);\n released.push(coin);\n effects.push({ type: 'coin:released', payload: { coin, remaining: this._locked.size } });\n }\n return { effects, released };\n }\n\n /**\n * Rewrite a **locked** cell's coin identity in place (coin → jackpot, mini →\n * major). Throws on a free cell - placing a brand-new tracked coin out of a\n * spin is `enter`/`respin`'s job; for purely decorative art on a free cell use\n * the cell's reel directly.\n */\n swap(cell: HwCell, id: string, data: TData | undefined): void {\n if (this._phase === 'spinning') {\n throw new Error('HoldAndWinBoard: setSymbolAt() while a wave is in flight — await respin() first.');\n }\n const k = cellKey(cell);\n const prev = this._locked.get(k);\n if (!prev) {\n throw new Error(\n `HoldAndWinBoard: setSymbolAt(${k}) on a non-locked cell — setSymbolAt rewrites a locked coin's identity.`,\n );\n }\n this._locked.set(k, this._freeze(cell, id, data ?? prev.data));\n }\n\n /** Hard clear back to idle. Fires `feature:reset`, never `coin:released`. */\n reset(): HwEffect<TData>[] {\n const clearedCoins = this._locked.size;\n this._locked.clear();\n this._waveLanded = [];\n this._round = 0;\n this._respinsLeft = 0;\n this._phase = 'idle';\n return [{ type: 'feature:reset', payload: { clearedCoins } }];\n }\n\n // ── Internals ────────────────────────────────────────────────────────\n\n private _setRespins(value: number, reason: HwRespinReason): HwEffect<TData> {\n this._respinsLeft = Math.max(0, value);\n return { type: 'respins:changed', payload: { value: this._respinsLeft, reason } };\n }\n\n /**\n * Store a coin with a board-owned, frozen `cell` so the ledger key can never\n * be corrupted by a game mutating the coin it was handed. `data` is left\n * mutable by reference - that is the supported way to carry live value.\n */\n private _freeze(cell: HwCell, id: string, data: TData | undefined): HwCoin<TData> {\n return { cell: Object.freeze({ reel: cell.reel, cell: cell.cell }), id, data };\n }\n\n private _assertInGrid(cell: HwCell, op: string): void {\n if (!this._cellSet.has(cellKey(cell))) {\n throw new Error(`HoldAndWinBoard: ${op}() targets cell ${cellKey(cell)} outside the grid.`);\n }\n }\n}\n","import type { Container, Graphics, Ticker } from 'pixi.js';\nimport { EventEmitter } from '../events/EventEmitter.js';\nimport type { ReelSet } from '../core/ReelSet.js';\nimport type { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { SymbolRegistry } from '../symbols/SymbolRegistry.js';\nimport type { SpeedProfile, SymbolData } from '../config/types.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { BoardGrid } from './BoardGrid.js';\nimport type { Direction, Orientation } from '../core/ReelAxis.js';\nimport { HoldAndWinState } from './HoldAndWinState.js';\nimport type { HwPhase } from './HoldAndWinState.js';\nimport { cellKey } from './HwTypes.js';\nimport { noticeWarn } from '../utils/notify.js';\nimport type {\n HoldAndWinBoardEvents,\n HwCell,\n HwCoin,\n HwEffect,\n HwRespinResult,\n} from './HwTypes.js';\n\n/** Internal config produced by {@link HoldAndWinBuilder.build}. */\nexport interface HoldAndWinBoardConfig<TData> {\n cols: number;\n rows: number;\n cell: number;\n gap: number;\n emptyId: string;\n respins: number;\n configurator: (registry: SymbolRegistry) => void;\n weights: Record<string, number> | null;\n symbolData: Record<string, Partial<SymbolData>> | null;\n baseProfile: SpeedProfile;\n stagger: (reel: number, cell: number) => number;\n anticipateWhen:\n | ((state: { locked: number; capacity: number; respinsLeft: number }) => boolean)\n | null;\n chrome: ((g: Graphics, size: number) => void) | null;\n /** Travel axis for each cell's own strip. See `HoldAndWinBuilder.axis`. */\n orientation?: Orientation;\n direction?: Direction;\n ticker: Ticker;\n rng: (() => number) | null;\n}\n\n/** Extra spin time (ms) a tense wave adds on top of the normal profile. */\nconst TENSION_EXTRA_MS = 1100;\n\n/**\n * A Hold & Win board: a grid of independently spinning cells plus the round\n * choreography every H&W game repeats - spin the free cells, lock the hits,\n * reset-or-decrement the respin counter, detect the full board.\n *\n * It composes two collaborators: a `BoardGrid` (the generic \"board of reels\"\n * mechanism - geometry, instances, spinning) and a `HoldAndWinState` (the pure\n * single-source reducer - ledger, counter, phase). The board is the\n * mediator: it drives the reels, reports each landing to the reducer, and\n * replays the reducer's decided effects onto {@link events}.\n *\n * It deliberately owns nothing about *value*. Coins are opaque `{ cell, id, data }`\n * - `id` picks the registered art, `data` is the game layer's to read and mutate.\n * Adders, doublers, collectors and flights are game design, expressed through\n * three openings rather than board features: {@link events}, {@link symbolAt}\n * (the live `ReelSymbol` instance) and {@link cellBounds}/{@link cellCenter}\n * (pixel geometry for flights).\n *\n * ```ts\n * const board = new HoldAndWinBuilder<{ value: number }>()\n * .grid(5, 3).cellSize(72, { gap: 4 })\n * .symbols((r) => r.register('coin', CoinSymbol, COIN_TRIGGER))\n * .weights({ coin: 1, empty: 3 }).respins(3).ticker(app.ticker)\n * .build();\n *\n * board.events.on('coin:locked', ({ coin }) => hud.add(coin.data.value));\n * board.enter(triggerCoins);\n * while (true) {\n * const round = await server.respin(board.lockedCoins);\n * const result = await board.respin(round.hits);\n * if (result.done) break; // game animates between rounds\n * }\n * ```\n */\nexport class HoldAndWinBoard<TData = unknown> implements Disposable {\n readonly events = new EventEmitter<HoldAndWinBoardEvents<TData>>();\n readonly cols: number;\n readonly rows: number;\n\n private readonly _grid: BoardGrid;\n private readonly _state: HoldAndWinState<TData>;\n private readonly _emptyId: string;\n private readonly _anticipateWhen: HoldAndWinBoardConfig<TData>['anticipateWhen'];\n\n constructor(cfg: HoldAndWinBoardConfig<TData>) {\n this.cols = cfg.cols;\n this.rows = cfg.rows;\n this._emptyId = cfg.emptyId;\n this._anticipateWhen = cfg.anticipateWhen;\n\n const base = (cell: HwCell): number =>\n (cfg.baseProfile.minimumSpinTime ?? 320) + cfg.stagger(cell.reel, cell.cell);\n this._grid = new BoardGrid({\n cols: cfg.cols,\n rows: cfg.rows,\n cellSize: cfg.cell,\n gap: cfg.gap,\n emptyId: cfg.emptyId,\n symbols: cfg.configurator,\n weights: cfg.weights ?? undefined,\n symbolData: cfg.symbolData ?? undefined,\n chrome: cfg.chrome ?? undefined,\n orientation: cfg.orientation,\n direction: cfg.direction,\n ticker: cfg.ticker,\n rng: cfg.rng ?? undefined,\n profiles: {\n normal: (cell) => ({ ...cfg.baseProfile, minimumSpinTime: base(cell) }),\n tension: (cell) => ({ ...cfg.baseProfile, minimumSpinTime: base(cell) + TENSION_EXTRA_MS }),\n },\n });\n this._state = new HoldAndWinState<TData>(this._grid.cells(), cfg.respins);\n }\n\n // ── State (delegated to the single-source reducer) ───────────────────\n\n get container(): Container {\n return this._grid.container;\n }\n get capacity(): number {\n return this._state.capacity;\n }\n get respinsLeft(): number {\n return this._state.respinsLeft;\n }\n get lockedCoins(): HwCoin<TData>[] {\n return this._state.lockedCoins();\n }\n get isFull(): boolean {\n return this._state.isFull;\n }\n get freeCells(): HwCell[] {\n return this._state.freeCells();\n }\n /** Where the feature is right now: idle (no feature), active, or spinning. */\n get phase(): HwPhase {\n return this._state.phase;\n }\n\n // ── Geometry & instances (the game layer's openings) ────────────────\n\n cellBounds(cell: HwCell): { x: number; y: number; width: number; height: number } {\n return this._grid.cellBounds(cell);\n }\n cellCenter(cell: HwCell): { x: number; y: number } {\n return this._grid.cellCenter(cell);\n }\n /** Live symbol instance currently shown in a cell. */\n symbolAt(cell: HwCell): ReelSymbol {\n return this._grid.symbolAt(cell);\n }\n /** The cell's underlying 1×1 ReelSet, for driving one cell directly. */\n reelAt(cell: HwCell): ReelSet {\n return this._grid.reelAt(cell);\n }\n\n /**\n * Rewrite a **locked** cell's coin in place - coin → jackpot, mini → major,\n * raise a tier - without disturbing any other cell. The ledger entry is\n * rewritten so `lockedCoins` and totals stay correct. Throws on a free cell.\n * Returns the new live symbol instance.\n *\n * Throws if called while a wave is in flight - `await respin()` first. To\n * upgrade a coin in reaction to its own `coin:locked`, defer the swap until\n * the awaited `respin()` resolves rather than swapping inside the listener.\n */\n setSymbolAt(cell: HwCell, id: string, data?: TData): ReelSymbol {\n this._state.swap(cell, id, data);\n this._grid.place(cell, id);\n return this._grid.symbolAt(cell);\n }\n\n // ── Round choreography ───────────────────────────────────────────────\n\n /** Activate the feature with the trigger coins. Seeds land locked, instantly. */\n enter(seed: HwCoin<TData>[]): void {\n const effects = this._state.enter(seed); // validates first; throws before any visual\n for (const coin of seed) this._grid.place(coin.cell, coin.id);\n this._apply(effects);\n }\n\n /**\n * Spin every free cell; `hits` land (and lock) their coins, all other spinning\n * cells land empty. Resolves once the wave has landed and the counter is\n * resolved. The game layer drives pacing between rounds.\n */\n async respin(hits: HwCoin<TData>[]): Promise<HwRespinResult<TData>> {\n const { round, spinning, hitByKey } = this._state.beginWave(hits);\n try {\n const tense = this._anticipating() && spinning.length > 0;\n for (const cell of spinning) this._grid.setProfile(cell, tense ? 'tension' : 'normal');\n this.events.emit('respin:start', { round, respinsLeft: this._state.respinsLeft, spinning });\n\n const targets = spinning.map((cell) => ({\n cell,\n id: hitByKey.get(cellKey(cell))?.id ?? this._emptyId,\n }));\n await this._grid.spinCells(targets, (cell) => {\n this._apply(this._state.land(cell, hitByKey.get(cellKey(cell)) ?? null));\n });\n\n const { effects, landed } = this._state.endWave();\n this._apply(effects);\n return {\n round,\n hits: landed,\n respinsLeft: this._state.respinsLeft,\n full: this._state.isFull,\n done: this._state.phase === 'idle',\n };\n } catch (err) {\n // A synchronous throw between beginWave and endWave - most plausibly a\n // game-layer event listener (respin:start / cell:landed / coin:locked)\n // throwing - must not strand the board. (An unregistered symbol id does\n // NOT land here: the engine logs and slams the reel internally, so the\n // spin resolves rather than rejecting.) abortWave() restores the reducer\n // phase, else every later respin() throws \"wave in flight\"; skipSpinning()\n // lands any cell still in flight, else the next respin() throws \"already\n // spinning\" on it. Stray landings from those slams are dropped by the\n // reducer's not-spinning guard. Rethrow so the caller still sees it.\n this._state.abortWave();\n this._grid.skipSpinning();\n throw err;\n }\n }\n\n /**\n * Remove locked coins - the collect moment. Clears the cells (they become\n * free again) and returns the released coins; the flight itself is game-layer\n * animation, started from `cellCenter()` or the `coin:released` event.\n */\n release(cells: HwCell[]): HwCoin<TData>[] {\n const { effects, released } = this._state.release(cells);\n for (const coin of released) this._grid.place(coin.cell, this._emptyId);\n this._apply(effects);\n return released;\n }\n\n /**\n * Fast-forward whatever is spinning: every in-flight cell is slammed to its\n * landed position, then `feature:skip` fires so the game layer can cut its own\n * flights short. The normal landing → `coin:locked` → `feature:end` flow still\n * resolves; this only removes the waiting. Returns the number of cells that\n * were in flight.\n */\n skip(): number {\n const inFlight = this._grid.skipSpinning();\n this.events.emit('feature:skip', { inFlight });\n return inFlight;\n }\n\n /** Clear the board back to idle. Fires `feature:reset` (not `coin:released`). */\n reset(): void {\n const effects = this._state.reset();\n for (const cell of this._grid.cells()) this._grid.place(cell, this._emptyId);\n this._apply(effects);\n }\n\n get isDestroyed(): boolean {\n return this._grid.isDestroyed;\n }\n\n destroy(): void {\n if (this._grid.isDestroyed) return;\n this.events.removeAllListeners();\n this._grid.destroy();\n }\n\n // ── Internals ────────────────────────────────────────────────────────\n\n /** Emit each reducer-decided effect and fire the visual side effects. */\n private _apply(effects: HwEffect<TData>[]): void {\n for (const fx of effects) {\n // Correlated union: `fx.type` and `fx.payload` are paired by construction\n // in the reducer, but TS can't carry that correlation through `emit`'s\n // generic. One local cast keeps every other call site fully typed.\n (this.events.emit as (type: string, payload: unknown) => void)(fx.type, fx.payload);\n if (fx.type === 'coin:locked') {\n // playWin is presentation; a hiccup must not break the feature flow, but\n // it must not vanish silently either - log it like the rest of the engine.\n void this.symbolAt(fx.payload.coin.cell)\n .playWin()\n .catch((err) => noticeWarn('hw-coin-win-failed', 'HoldAndWinBoard: coin win animation failed.', err));\n }\n }\n }\n\n private _anticipating(): boolean {\n if (!this._anticipateWhen) return false;\n return this._anticipateWhen({\n locked: this._state.lockedCoins().length,\n capacity: this._state.capacity,\n respinsLeft: this._state.respinsLeft,\n });\n }\n}\n","import type { Graphics, Ticker } from 'pixi.js';\nimport { SpeedPresets } from '../config/SpeedPresets.js';\nimport type { SpeedProfile, SymbolData } from '../config/types.js';\nimport type { SymbolRegistry } from '../symbols/SymbolRegistry.js';\nimport { HoldAndWinBoard } from './HoldAndWinBoard.js';\nimport type { Direction, Orientation } from '../core/ReelAxis.js';\nimport type { HwCellSizeOptions } from './HwTypes.js';\n\n/**\n * Fluent builder for {@link HoldAndWinBoard}.\n *\n * A Hold & Win board is a W×H grid of cells that spin **independently** - the\n * mechanic's atomic unit is the cell, the engine's is the column, so each cell\n * is its own 1×1 ReelSet. This builder wires that grid plus the round\n * choreography; everything value-shaped stays in the game layer (see\n * {@link HoldAndWinBoard}).\n *\n * `TData` types the opaque payload carried on each coin's `data`.\n */\nexport class HoldAndWinBuilder<TData = unknown> {\n private _cols = 5;\n private _rows = 3;\n private _cell = 72;\n private _gap = 4;\n private _emptyId = 'empty';\n private _respins = 3;\n private _configurator: ((registry: SymbolRegistry) => void) | null = null;\n private _weights: Record<string, number> | null = null;\n private _symbolData: Record<string, Partial<SymbolData>> | null = null;\n private _baseProfile: SpeedProfile = { ...SpeedPresets.NORMAL, minimumSpinTime: 320 };\n private _stagger: (reel: number, cell: number) => number = (reel, cell) => (reel + cell) * 70;\n private _anticipateWhen:\n | ((state: { locked: number; capacity: number; respinsLeft: number }) => boolean)\n | null = null;\n private _chrome: ((g: Graphics, size: number) => void) | null = null;\n private _orientation: Orientation = 'vertical';\n private _direction: Direction = 'forward';\n private _ticker: Ticker | null = null;\n private _rng: (() => number) | null = null;\n\n grid(cols: number, rows: number): this {\n this._cols = cols;\n this._rows = rows;\n return this;\n }\n\n cellSize(size: number, opts: HwCellSizeOptions = {}): this {\n this._cell = size;\n this._gap = opts.gap ?? this._gap;\n return this;\n }\n\n /**\n * Register coin symbol classes, exactly like `ReelSetBuilder.symbols`. Applied\n * to every cell. An {@link EmptySymbol} is auto-registered under {@link emptyId}\n * unless the configurator registers one itself.\n */\n symbols(configurator: (registry: SymbolRegistry) => void): this {\n this._configurator = configurator;\n return this;\n }\n\n /** Strip weights during the spin (how often coins flash past empties). */\n weights(weights: Record<string, number>): this {\n this._weights = weights;\n return this;\n }\n\n /** Symbol id a cell shows when it holds no coin. Default `'empty'`. */\n emptyId(id: string): this {\n this._emptyId = id;\n return this;\n }\n\n /**\n * Per-symbol engine overrides, exactly like `ReelSetBuilder.symbolData`. The\n * headline use is `{ unmask: true }` for coins whose lock/reveal animations\n * expand past the cell. Safe only for server-placed ids (weight 0): unmasked\n * strip symbols mis-track vertically while the reel spins.\n */\n symbolData(overrides: Record<string, Partial<SymbolData>>): this {\n this._symbolData = { ...(this._symbolData ?? {}), ...overrides };\n return this;\n }\n\n /** Respins granted on enter and restored on every hit. Default 3. */\n respins(count: number): this {\n this._respins = count;\n return this;\n }\n\n /** Base spin feel for every cell. Default: NORMAL with a 320ms floor. */\n speedProfile(profile: SpeedProfile): this {\n this._baseProfile = profile;\n return this;\n }\n\n /**\n * Extra milliseconds of spin per cell on top of the base minimum spin time.\n * Default `(reel + cell) * 70` - the diagonal landing wave. Return 0 for\n * simultaneous landings.\n */\n stagger(fn: (reel: number, cell: number) => number): this {\n this._stagger = fn;\n return this;\n }\n\n /**\n * When the predicate returns true for a wave, **every** spinning cell uses a\n * drawn-out tension profile - the \"one cell left for Grand\" moment. Evaluated\n * once per wave for the whole board (not per cell), against the pre-wave state.\n */\n anticipateWhen(\n fn: (state: { locked: number; capacity: number; respinsLeft: number }) => boolean,\n ): this {\n this._anticipateWhen = fn;\n return this;\n }\n\n /** Per-cell background, drawn behind each mini reel. */\n cellChrome(draw: (g: Graphics, size: number) => void): this {\n this._chrome = draw;\n return this;\n }\n\n /**\n * Which way each cell's strip travels while it spins. Cells are 1x1 reel\n * sets, so this picks the edge a coin scrolls in from; the board's own\n * `cols` x `rows` layout is unaffected. Defaults to vertical / forward.\n */\n axis(orientation: Orientation, direction: Direction = 'forward'): this {\n this._orientation = orientation;\n this._direction = direction;\n return this;\n }\n\n ticker(ticker: Ticker): this {\n this._ticker = ticker;\n return this;\n }\n\n /** Injected RNG for the spin strips (deterministic demos / tests). */\n rng(fn: () => number): this {\n this._rng = fn;\n return this;\n }\n\n build(): HoldAndWinBoard<TData> {\n if (!this._configurator) {\n throw new Error('HoldAndWinBuilder: .symbols(...) is required — register at least one coin id.');\n }\n if (!this._ticker) {\n throw new Error('HoldAndWinBuilder: .ticker(...) is required.');\n }\n return new HoldAndWinBoard<TData>({\n cols: this._cols,\n rows: this._rows,\n cell: this._cell,\n gap: this._gap,\n emptyId: this._emptyId,\n respins: this._respins,\n configurator: this._configurator,\n weights: this._weights,\n symbolData: this._symbolData,\n baseProfile: this._baseProfile,\n stagger: this._stagger,\n anticipateWhen: this._anticipateWhen,\n chrome: this._chrome,\n orientation: this._orientation,\n direction: this._direction,\n ticker: this._ticker,\n rng: this._rng,\n });\n }\n}\n","import type { SymbolPosition, Win } from '../config/types.js';\n\nexport type { Win, SymbolPosition };\n\n/** Return a new array sorted by `value` descending (non-mutating). Missing values sort as 0. */\nexport function sortByValueDesc<T extends { value?: number }>(wins: readonly T[]): T[] {\n return [...wins].sort((a, b) => (b.value ?? 0) - (a.value ?? 0));\n}\n","import type { Win } from '../config/types.js';\nimport type { ReelSet } from '../core/ReelSet.js';\nimport type { SymbolPosition } from '../events/ReelEvents.js';\nimport type { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { sortByValueDesc } from './Win.js';\n\n/**\n * What to play on each cell being highlighted.\n *\n * - `'win'`. default. Calls `symbol.playWin()` (your subclass's hook).\n * - `string`. a named animation. If the symbol exposes a\n * `playAnimation(name)` method (e.g. SpineSymbol), it's invoked; else\n * it falls back to `playWin()`.\n * - `(symbol, cell, win) => Promise<void>`. drive the animation\n * yourself. Good for GSAP bounces, line-specific pulses, etc.\n */\nexport type WinSymbolAnim =\n | 'win'\n | string\n | ((symbol: ReelSymbol, cell: SymbolPosition, win: Win) => Promise<void>);\n\nexport interface WinPresenterOptions {\n /**\n * Fade non-winning symbols to this alpha while a win is active.\n * - `true` (default) → alpha 0.35\n * - number → that alpha\n * - `false` → don't touch non-winners\n *\n * Restored on `win:end`.\n */\n dimLosers?: boolean | { alpha?: number };\n /** See {@link WinSymbolAnim}. Default `'win'`. */\n symbolAnim?: WinSymbolAnim;\n /**\n * Delay between cells *within* a single win (ms).\n * - `0` (default) → all cells animate simultaneously\n * - `> 0` → cells start one after another in array order (e.g. a\n * left-to-right sweep across a payline's cells)\n */\n stagger?: number;\n /** Delay between successive wins in the sequence (ms). Default 400. */\n cycleGap?: number;\n /** Number of full cycles through the wins list. `-1` for infinite. Default 1. */\n cycles?: number;\n /** Sort wins by `value` descending before cycling. Default true. */\n sortByValue?: boolean;\n}\n\ninterface ResolvedOptions {\n dimAlpha: number | null;\n symbolAnim: WinSymbolAnim;\n stagger: number;\n cycleGap: number;\n cycles: number;\n sortByValue: boolean;\n}\n\n/**\n * Highlights winning cells on a reel set. One job: animate the symbols.\n *\n * The presenter doesn't draw lines, outlines, or any per-win visual. it\n * emits `win:start` / `win:group` / `win:symbol` / `win:end` events so\n * your code can hook anything it wants (polylines, Spine line rigs,\n * popup numbers, sound cues) by subscribing and using\n * `reelSet.getCellBounds(reel, cell)` to place graphics.\n *\n * Two knobs cover the common presentation modes:\n *\n * - `stagger: 0` → all cells in a win pulse together\n * - `stagger: 60` → cells start one after another. a left-to-right\n * sweep if you pass cells in reel order\n *\n * ```ts\n * const presenter = new WinPresenter(reelSet, { stagger: 80 });\n *\n * reelSet.events.on('spin:complete', async () => {\n * const wins = await server.wins(result); // your wins, your shape\n * await presenter.show(wins);\n * });\n * reelSet.events.on('spin:start', () => presenter.abort());\n * ```\n *\n * Cascades: drive `presenter.show([{ cells: winners }])` from\n * `runCascade`'s `onWinnersVanish` hook. cluster pops and payline hits\n * are the same shape to the presenter.\n */\nexport class WinPresenter implements Disposable {\n private _reelSet: ReelSet;\n private _options: ResolvedOptions;\n private _abort: AbortController | null = null;\n private _isActive = false;\n private _isDestroyed = false;\n\n constructor(reelSet: ReelSet, options: WinPresenterOptions = {}) {\n this._reelSet = reelSet;\n this._options = WinPresenter._resolve(options);\n }\n\n get isActive(): boolean {\n return this._isActive;\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n /**\n * Present the given wins. Cancels any in-flight sequence first.\n * Resolves when all cycles complete or when `abort()` is called.\n *\n * Empty input resolves immediately without firing any events.\n */\n async show(wins: readonly Win[]): Promise<void> {\n this.abort();\n if (this._isDestroyed) return;\n if (wins.length === 0) return;\n\n const ordered = this._options.sortByValue\n ? sortByValueDesc(wins)\n : [...wins];\n\n const abort = new AbortController();\n this._abort = abort;\n this._isActive = true;\n this._reelSet.events.emit('win:start', ordered);\n\n let loop = 0;\n try {\n while (this._options.cycles === -1 || loop < this._options.cycles) {\n for (const win of ordered) {\n if (abort.signal.aborted) return;\n await this._showOne(win, abort.signal);\n if (abort.signal.aborted) return;\n await this._wait(this._options.cycleGap, abort.signal);\n }\n loop++;\n }\n } finally {\n this._restoreAlpha();\n const wasAborted = abort.signal.aborted;\n if (this._abort === abort) this._abort = null;\n this._isActive = false;\n this._reelSet.events.emit('win:end', wasAborted ? 'aborted' : 'complete');\n }\n }\n\n /** Abort any in-flight `show()`. */\n abort(): void {\n if (this._abort) this._abort.abort();\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n this._isDestroyed = true;\n this.abort();\n }\n\n private async _showOne(win: Win, signal: AbortSignal): Promise<void> {\n const cells = [...win.cells];\n if (cells.length === 0) return;\n\n // Apply dim before firing win:group so listeners observe the live\n // visual state (e.g. a UI snapshot sees losers already faded).\n this._applyDim(cells);\n this._reelSet.events.emit('win:group', win, cells);\n\n const stagger = this._options.stagger;\n const animPromises: Promise<void>[] = [];\n\n for (let i = 0; i < cells.length; i++) {\n if (signal.aborted) break;\n if (i > 0 && stagger > 0) await this._wait(stagger, signal);\n if (signal.aborted) break;\n const cell = cells[i];\n const reel = this._reelSet.getReel(cell.reelIndex);\n if (!reel) continue;\n const symbol = reel.getSymbolAt(cell.cellIndex);\n if (!symbol) continue;\n this._reelSet.events.emit('win:symbol', symbol, cell, win);\n animPromises.push(this._playAnim(symbol, cell, win));\n }\n\n await Promise.all(animPromises);\n }\n\n private async _playAnim(\n symbol: ReelSymbol,\n cell: SymbolPosition,\n win: Win,\n ): Promise<void> {\n const anim = this._options.symbolAnim;\n if (typeof anim === 'function') return anim(symbol, cell, win);\n if (anim === 'win') return symbol.playWin();\n const withPlay = symbol as unknown as { playAnimation?: (name: string) => Promise<void> };\n if (typeof withPlay.playAnimation === 'function') return withPlay.playAnimation(anim);\n return symbol.playWin();\n }\n\n private _applyDim(winCells: readonly SymbolPosition[]): void {\n const alpha = this._options.dimAlpha;\n if (alpha === null) return;\n\n const winKeys = new Set<string>();\n for (const c of winCells) winKeys.add(`${c.reelIndex}:${c.cellIndex}`);\n\n const reels = this._reelSet.reels;\n for (let r = 0; r < reels.length; r++) {\n const reel = reels[r];\n for (let cell = 0; cell < reel.visibleCells; cell++) {\n const view = reel.getSymbolAt(cell).view;\n view.alpha = winKeys.has(`${r}:${cell}`) ? 1 : alpha;\n }\n }\n }\n\n private _restoreAlpha(): void {\n const reels = this._reelSet.reels;\n for (const reel of reels) {\n for (let cell = 0; cell < reel.visibleCells; cell++) {\n reel.getSymbolAt(cell).view.alpha = 1;\n }\n }\n }\n\n private _wait(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal.aborted) return resolve();\n const t = setTimeout(resolve, ms);\n signal.addEventListener(\n 'abort',\n () => {\n clearTimeout(t);\n resolve();\n },\n { once: true },\n );\n });\n }\n\n private static _resolve(opts: WinPresenterOptions): ResolvedOptions {\n let dimAlpha: number | null;\n if (opts.dimLosers === false) {\n dimAlpha = null;\n } else if (typeof opts.dimLosers === 'object' && opts.dimLosers !== null) {\n dimAlpha = opts.dimLosers.alpha ?? 0.35;\n } else {\n dimAlpha = 0.35;\n }\n\n return {\n dimAlpha,\n symbolAnim: opts.symbolAnim ?? 'win',\n stagger: Math.max(0, opts.stagger ?? 0),\n cycleGap: opts.cycleGap ?? 400,\n cycles: opts.cycles ?? 1,\n sortByValue: opts.sortByValue ?? true,\n };\n }\n}\n","import type { Ticker } from 'pixi.js';\nimport { DEFAULT_GSAP, type Gsap } from './gsap.js';\n\n/**\n * Drive GSAP from a PixiJS ticker instead of its own requestAnimationFrame\n * loop, and return a disposer that restores GSAP's default loop.\n *\n * **Why this matters:** GSAP's default ticker runs on `requestAnimationFrame`,\n * which browsers throttle — and often freeze entirely — in hidden tabs and many\n * iframes. Casino lobbies and iframed clients are routinely backgrounded. When\n * that happens GSAP stalls while the PixiJS ticker keeps running, so every\n * engine animation (spin easing, the landing bounce, spotlight pulses) freezes\n * mid-flight. Pinning GSAP to the same ticker keeps animation and rendering in\n * lockstep everywhere, foreground or background.\n *\n * This is the one line every integration has to remember\n * (`gsap.ticker.remove(gsap.updateRoot); ticker.add(...)`); calling this\n * function instead removes that footgun.\n *\n * Pass the SAME instance you gave `ReelSetBuilder.gsap(...)`. gsap is held per\n * reel set in v2, not process-wide, so this function cannot look it up for you\n * -- and driving a different instance than the engine's tweens live on is the\n * dual-instance trap all over again. Omit it only if you never called\n * `.gsap(...)` either.\n *\n * Call it **once per app**, after creating the PixiJS `Application` and before\n * the first spin. Do NOT also detach `gsap.updateRoot` yourself — driving GSAP\n * from two sources advances it twice per frame (animations run at double speed).\n *\n * @param ticker - The PixiJS ticker to drive GSAP from (usually `app.ticker`).\n * @param instance - The gsap instance to drive. Defaults to the one resolved\n * at lib-load time; pass the one you handed `ReelSetBuilder.gsap(...)`.\n * @returns A disposer that detaches the driver and restores GSAP's own ticker.\n *\n * @example\n * import { Application } from 'pixi.js';\n * import { driveGsapWithTicker } from 'pixi-reels';\n *\n * const app = new Application();\n * await app.init({ ... });\n * const stopGsapSync = driveGsapWithTicker(app.ticker);\n * // ...on teardown:\n * stopGsapSync();\n */\nexport function driveGsapWithTicker(ticker: Ticker, instance: Gsap = DEFAULT_GSAP): () => void {\n const gsap = instance;\n gsap.ticker.remove(gsap.updateRoot);\n const driver = (): void => {\n gsap.updateRoot(ticker.lastTime / 1000);\n };\n ticker.add(driver);\n\n let disposed = false;\n return () => {\n if (disposed) return;\n disposed = true;\n ticker.remove(driver);\n gsap.ticker.add(gsap.updateRoot);\n };\n}\n"],"mappings":"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"}