interface CellFrameStyle { radius?: number; idle?: { color: number; alpha: number; }; winning?: { color: number; alpha: number; }; removed?: { color: number; alpha: number; }; fresh?: { color: number; alpha: number; }; } interface CellData { symbol: string | null; multiplier?: number; bonus?: number; sticky?: { remaining: number; }; } /** Per-strip cell size override: a square scalar, or an explicit width/height. */ type CellSizeSpec = number | { width: number; height: number; }; /** Names of easing functions available in the engine's `Easing` map (see anim/easing-map.ts). */ type EasingName = 'linear' | 'easeInQuad' | 'easeOutQuad' | 'easeInOutQuad' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic' | 'easeInBack' | 'easeOutBack' | 'easeInOutBack' | 'easeOutBounce' | 'easeInBounce' | 'easeOutElastic' | 'easeInSine' | 'easeOutSine' | 'easeInOutSine'; /** * A value that is either flat across the board, or per-reel: an array indexed by REEL INDEX * (holes fall back to the scalar default). Used for anticipation timings so a game can make * each successive reel slower than the last. */ type PerReel = T | (T | undefined)[]; /** Win-evaluation model. Purely presentational here (affects geometry + highlight), the math lives in Lua. */ type EvaluationMode = 'lines' | 'ways' | 'anywhere' | 'cluster' | 'megaways' | 'infinity'; interface GridConfig { cols: number; /** Uniform row count. Ignored when `rowsPerReel` is set. */ rows: number; /** Per-reel row counts (Megaways / variable-height reels). Length should equal `cols`. */ rowsPerReel?: number[]; /** Square cell size (shorthand: same width & height, all reels). */ cellSize: number; /** Rectangular cells, uniform across reels. Override `cellSize` when set. */ cellWidth?: number; cellHeight?: number; /** Per-strip cell size (square scalar or {width,height}). Overrides the above for that reel. */ cellSizePerReel?: CellSizeSpec[]; /** Uniform gap (shorthand for both axes). */ gap: number; /** Horizontal gap between adjacent reels. Scalar, or per-boundary (length cols-1). Overrides `gap`. */ colGap?: number | number[]; /** Vertical gap between rows. Scalar, or per-reel (length cols). Overrides `gap`. */ rowGap?: number | number[]; evaluation: EvaluationMode; /** For Megaways: clamp per-reel rows to [minRows, maxRows]. */ minRows?: number; maxRows?: number; /** Megaways top horizontal reel (renders above the grid, adds to ways). */ topReel?: { size: number; } | null; mask: boolean; frameStyle?: CellFrameStyle; decoration?: { padding?: number; }; } type MotionStyle = 'swap' | 'strip' | 'cascade-drop'; type StopMode = 'sequential' | 'sync' | 'random'; /** `cascade-drop` fill direction within one reel. */ type DropOrder = 'top-down' | 'bottom-up'; /** How `cascade-drop` spaces reels: by formula, or one strictly after the other. */ type DropSequence = 'parallel' | 'chained' | 'chained-when-anticipated'; type StopOrder = 'ltr' | 'rtl'; type Intensity = 'full' | 'reduced' | 'minimal'; interface SettleConfig { /** Overshoot amplitude in px applied to the reel before it springs back. */ amp: number; ms: number; easing: EasingName; } /** Squash & stretch on landing impact (Stone-Rush / magnus style). */ interface SquashConfig { enabled: boolean; scaleX: number; scaleY: number; ms: number; } interface BlurConfig { enabled: boolean; /** Alpha applied to in-motion symbols (hot-ross uses 0.82). */ alpha: number; /** Runtime BlurFilter strength factor (PixiJS slots demo uses ~8), 0 disables the filter. */ strength: number; /** Draw vertical motion streaks behind moving symbols. */ streaks: boolean; } interface MotionConfig { style: MotionStyle; /** Base spin-up duration (ms) before the first reel can stop. */ spinUp: number; /** Extra hold (ms) at full speed before deceleration. */ hold: number; /** Delay (ms) between consecutive reel stops in sequential mode. */ stopStagger: number; stopMode: StopMode; stopOrder: StopOrder; settle: SettleConfig; squash: SquashConfig; blur: BlurConfig; /** Multiply all durations in turbo mode (0.5 = twice as fast). */ turboFactor: number; /** Global animation-intensity scale (accessibility): full=1, reduced=0.7, minimal=0.4. */ intensity: Intensity; /** Allow slam/quick-stop (snaps reels to target without changing outcome). */ slamStop: boolean; /** Symbols visible on a reel tape while spinning (swap/strip). */ symbolsPerReel: number; /** `cascade-drop`: ms between consecutive cells of ONE reel (top→bottom). Default 24. */ cellStagger: number; /** `cascade-drop`: multiplier on `stopStagger` for the per-reel offset. Default 0.4. */ reelStaggerFactor: number; /** `cascade-drop`: fall duration as a fraction of `spinUp`. Default 0.6. */ dropFallFactor: number; /** * `cascade-drop`: which cell of a reel lands first. `'top-down'` (default, the engine's original * behaviour) deals the reel like cards from the top; `'bottom-up'` fills it the way gravity * would — the lowest cell arrives first and the rest stack on top of it. */ dropOrder: DropOrder; /** * `cascade-drop`: how reels are spaced. * - `'parallel'` (default) starts every reel at its own formula offset — * `reel * stopStagger * reelStaggerFactor` — so a reel can open while the previous one is * still dropping. Fast, and what a normal spin wants. * - `'chained'` starts a reel only once the previous one has seated its LAST cell, plus that * same offset as the gap. One reel at a time, always — correct, but it makes every spin as * long as the sum of its reels. * - `'chained-when-anticipated'` runs the un-armed reels in parallel and switches to the chain * from the first ANTICIPATED reel onwards. The base spin keeps its stop window; the hunt for * the last scatter goes strictly reel by reel, each slower than the last. */ dropSequence: DropSequence; } /** What a game-supplied `AnticipationConfig.decide` may return instead of a bare reel list. */ interface AnticipationOverride { /** Reels to anticipate, in the order the progression should ramp. Empty = no anticipation. */ reels: number[]; /** Speed factor (lower = slower). Scalar, or per-reel indexed by reel index. */ slowdown?: PerReel; /** Extra hold before landing. Scalar, or per-reel indexed by reel index. */ holdMs?: PerReel; /** Growth of the gap between successive cells within a reel. Scalar, or per-reel. */ cellStaggerRamp?: PerReel; } interface AnticipationConfig { enabled: boolean; /** Symbols that count toward the anticipation threshold (scatter/bonus). */ triggerSymbols: string[]; /** Number of trigger symbols already landed that arms anticipation (N−1, usually 2). */ threshold: number; /** Which reels get the slow treatment: the still-spinning trailing reels, or explicit indices. */ reels: 'trailing' | number[]; /** Multiply the trailing reel's spin speed (lower = slower / longer). */ slowdownFactor: number; /** Extra hold (ms) before the final anticipation reel lands (300–500 typical). */ holdMs: number; /** * Game-supplied decision, REPLACING the built-in `triggerSymbols`/`threshold` counting. * Return the reels to anticipate (or an `AnticipationOverride`); `null` / `[]` = no anticipation. * Use this when the trigger is not expressible as "N of symbol X landed" — e.g. "the round is * still alive on every reel so far", or "reel 3 missed its symbol, so let 4 and 5 stop normally". */ decide?: ((targetGrid: CellData[][]) => number[] | AnticipationOverride | null) | null; /** * Ramp the slowdown across successive anticipated reels: reel #i of the decision gets * `slowdownFactor * progressiveSlowdown ** i`. 1 = flat (default); < 1 = each reel slower * than the last. */ progressiveSlowdown: number; /** Extra hold (ms) added per successive anticipated reel: reel #i gets `holdMs + i * this`. */ progressiveHoldMs: number; /** * `cascade-drop`: how the gap between SUCCESSIVE CELLS grows inside an anticipated reel. The * gap before cell #i is `cellStagger * * progressiveCellStagger ** i`, so 1 * (the default) keeps the reel's cells evenly spaced and > 1 makes each symbol land later than * the last — the drip that turns a reel into a countdown. * * It compounds with `progressiveSlowdown`: that widens the base gap reel by reel, this widens * it cell by cell, so a later reel drips both slower AND with a steeper ramp. */ progressiveCellStagger: number; /** Optional grid zoom while anticipating (magnum-opus uses 1.3×). */ zoom: { enabled: boolean; scale: number; ms: number; }; } interface CascadeConfig { enabled: boolean; /** Animate surviving symbols sliding down into the gaps (true tumble). */ gravity: boolean; timings: { reveal: number; highlight: number; remove: number; drop: number; refill: number; wait: number; }; easings: { highlight: EasingName; remove: EasingName; drop: EasingName; }; /** Slow each successive cascade step to build tension: factor = 1 + step*perStepDecel (capped). */ perStepDecel: number; perStepDecelCap: number; /** Dim non-winning symbols during the highlight phase. */ dimNonWinners: boolean; dimAlpha: number; /** Running win multiplier that climbs per cascade (Pragmatic Tumble / Avalanche). */ multiplier: { enabled: boolean; start: number; /** 'add' → +step each step; 'mul' → ×step each step. */ mode: 'add' | 'mul'; step: number; cap: number | null; /** Keep climbing across free-spins instead of resetting per spin. */ persistInFreeSpins: boolean; }; } interface WinConfig { highlightScale: number; glow: boolean; /** Frame shake on landing/win. */ frameShake: { enabled: boolean; amp: number; ms: number; onlyOnSymbols: string[] | null; }; } interface ExpandingWildConfig { enabled: boolean; symbol: string; /** Reels that may host an expanding wild (empty = all). */ reels: number[]; /** Expand to the full reel height. */ toFullReel: boolean; ms: number; easing: EasingName; onlyInFreeSpins: boolean; } interface StickyConfig { enabled: boolean; symbols: string[]; /** How many spins a sticky symbol persists (0 = whole feature/bonus). */ durationSpins: number; /** Ring colour drawn around sticky cells. */ ringColor: number; } interface WalkingWildConfig { enabled: boolean; symbol: string; direction: 'left' | 'right'; stepPerSpin: number; awardsRespin: boolean; asStacked: boolean; } interface MultiplierConfig { enabled: boolean; /** Multiplier symbol id (carries a value), or null when multipliers are per-cell data. */ symbol: string | null; scope: 'perSymbol' | 'perReel' | 'global'; combine: 'additive' | 'multiplicative'; attachedToWild: boolean; max: number; accumulateAcrossFreeSpins: boolean; } interface MysteryConfig { enabled: boolean; symbol: string; /** Eligible reveal targets; all mystery tiles reveal the SAME single draw per spin. */ revealPool: string[]; canRevealWild: boolean; ms: number; } interface TransformConfig { enabled: boolean; /** Trigger symbol whose landing fires the transform (null = data-driven). */ trigger: string | null; /** 'randomLow' picks one low symbol at random; or pin a specific source. */ source: 'randomLow' | string; target: string; /** Convert every instance of the source symbol. */ allInstances: boolean; /** Restrict to low→high (upgrade) direction. */ upgradeOnly: boolean; ms: number; } interface GiantConfig { enabled: boolean; /** Footprint in cells. */ width: number; height: number; /** Symbols that may appear giant (empty = any). */ symbols: string[]; /** Pick one giant symbol type per spin. */ chosenPerSpin: boolean; onlyInFreeSpins: boolean; } interface SplitConfig { enabled: boolean; symbol: string; factor: number; /** Reels that can host the split trigger (xSplit lands on the last reel). */ reels: number[]; /** Defer the split execution until all respins finish. */ deferUntilRespinsEnd: boolean; } interface StackedConfig { enabled: boolean; symbols: string[]; /** Max stack height (positions of the same symbol on one reel). */ height: number; } interface NudgeConfig { enabled: boolean; /** Reels eligible to nudge. */ reels: number[]; step: number; /** xNudge: nudge a stacked wild into full view, +1 multiplier per nudge. */ toFullReel: boolean; multiplierStart: number; multiplierPerNudge: number; } interface ReelModifierConfig { enabled: boolean; /** Random pre-spin modifiers, each with a trigger weight. */ pool: { effect: 'addRows' | 'addWilds' | 'setGiant' | 'guaranteedWilds'; magnitude: number; weight: number; }[]; appliesIn: 'base' | 'freeSpins' | 'both'; } interface HoldAndSpinConfig { enabled: boolean; lockSymbols: string[]; triggerThreshold: number; respinsAwarded: number; /** Reset the respin counter to full whenever a new symbol locks. */ resetOnNewSymbol: boolean; jackpotTiers: string[]; fullGridAwardsGrand: boolean; } interface RandomWildConfig { enabled: boolean; /** Fixed count, or [min,max] range. */ count: number | [number, number]; sticky: boolean; multiplier: number; trigger: 'randomBaseSpin' | 'onFreeSpins'; chance: number; } interface FeaturesConfig { expandingWild: ExpandingWildConfig; sticky: StickyConfig; walkingWild: WalkingWildConfig; multiplier: MultiplierConfig; mystery: MysteryConfig; transform: TransformConfig; giant: GiantConfig; split: SplitConfig; stacked: StackedConfig; nudge: NudgeConfig; reelModifier: ReelModifierConfig; holdAndSpin: HoldAndSpinConfig; randomWild: RandomWildConfig; } interface ReelSystemConfig { grid: GridConfig; motion: MotionConfig; anticipation: AnticipationConfig; cascade: CascadeConfig; win: WinConfig; features: FeaturesConfig; } /** Deep-partial helper for overrides. */ type DeepPartial = { [K in keyof T]?: T[K] extends (infer U)[] ? T[K] : T[K] extends object ? DeepPartial : T[K]; }; /** Minimal ReelSystem surface the bridge needs (keeps it decoupled/testable). */ interface ReelDevBridgeTarget { readonly config: ReelSystemConfig; update(partial: DeepPartial): void; } interface ReelDevBridgeOptions { system: ReelDevBridgeTarget; /** postMessage targetOrigin for the parent. Default '*' (dev harness, same-origin). */ targetOrigin?: string; /** * Guard: only mount when true. Defaults to `import.meta.env?.DEV` when available, * else true. Pass `false` to hard-disable. */ enabled?: boolean; } interface ReelDevBridge { /** Re-announce the current config to the parent. */ announce(): void; /** Detach the message listener. */ dispose(): void; } declare function mountReelDevBridge(opts: ReelDevBridgeOptions): ReelDevBridge; type Control = { kind: 'select'; path: string; label: string; options: string[]; } | { kind: 'range'; path: string; label: string; min: number; max: number; step: number; /** When the bound value is unset (undefined / non-scalar), show this path's value instead. */ fallback?: string; } | { kind: 'toggle'; path: string; label: string; } | { kind: 'color'; path: string; label: string; }; /** Stable section identifiers — used to toggle whole sections on/off from the plugin. */ type SectionKey = 'geometry' | 'motion' | 'anticipation' | 'cascade' | 'win' | 'features'; /** All section keys, in panel order. */ declare const REEL_SECTION_KEYS: SectionKey[]; interface Section { /** Stable key for enabling/disabling the whole section (optional for ad-hoc schemas). */ key?: SectionKey; title: string; controls: Control[]; collapsed?: boolean; } /** The editable reel-config fields, grouped into collapsible sections. */ declare const REEL_FIELD_SCHEMA: Section[]; declare function getPath(obj: any, path: string): any; declare function setPath(obj: any, path: string, value: any): void; interface ControlPanelOptions { /** The live config object the controls read from and mutate. */ config: unknown; /** Called with the changed dot-path after each edit. */ onChange: (path: string) => void; /** Schema to render. Defaults to REEL_FIELD_SCHEMA. */ schema?: Section[]; } /** Build the control panel into `root`. Returns a `refresh()` that re-syncs inputs from config. */ declare function buildControlPanel(root: HTMLElement, opts: ControlPanelOptions): { refresh: () => void; }; /** Deep diff `obj` against `base`: arrays compared whole, objects recursed, scalars by !==. */ declare function configDiff(base: any, obj: any): any; /** Diff a working config against DEFAULT_REEL_CONFIG (via resolveReelConfig()). */ declare function diffFromDefaults(config: ReelSystemConfig): Partial; /** Emit a paste-ready TS module exporting the reel config as overrides-only. */ declare function emitReelConfigTs(config: ReelSystemConfig): string; /** Game → panel: announces the current resolved config (on mount + on request). */ interface ReelReadyMessage { type: 'e8:reel:ready'; config: ReelSystemConfig; } /** Panel → game: apply a partial config live (system.update). */ interface ReelApplyMessage { type: 'e8:reel:apply'; patch: DeepPartial; } /** Panel → game: (re)request the current config, e.g. when the panel opens late. */ interface ReelRequestMessage { type: 'e8:reel:request'; } type ReelDevMessage = ReelReadyMessage | ReelApplyMessage | ReelRequestMessage; declare const REEL_READY = "e8:reel:ready"; declare const REEL_APPLY = "e8:reel:apply"; declare const REEL_REQUEST = "e8:reel:request"; export { REEL_APPLY, REEL_FIELD_SCHEMA, REEL_READY, REEL_REQUEST, REEL_SECTION_KEYS, buildControlPanel, configDiff, diffFromDefaults, emitReelConfigTs, getPath, mountReelDevBridge, setPath }; export type { Control, ControlPanelOptions, ReelApplyMessage, ReelDevBridge, ReelDevBridgeOptions, ReelDevBridgeTarget, ReelDevMessage, ReelReadyMessage, ReelRequestMessage, Section, SectionKey };