import { Container, Graphics, Ticker } from 'pixi.js' import { CELL_SIZE } from 'shared/board.ts' import { theme } from '../../theme.ts' import { ParticleSystem } from './ParticleSystem.ts' import type { CameraController } from '../CameraController.ts' const ticker = Ticker.shared export class WinEffect { private effectLayer: Container private particleSys: ParticleSystem private camera: CameraController private vw: number private vh: number private screenLayer: Container private flashGraphics: Graphics private highlightGraphics: Graphics | null = null constructor(effectLayer: Container, particleSys: ParticleSystem, camera: CameraController, vw: number, vh: number, screenLayer: Container) { this.effectLayer = effectLayer this.particleSys = particleSys this.camera = camera this.vw = vw this.vh = vh this.screenLayer = screenLayer this.flashGraphics = new Graphics() } clear(): void { if (this.highlightGraphics) { this.effectLayer.removeChild(this.highlightGraphics) this.highlightGraphics.destroy() this.highlightGraphics = null } } play(cells: { x: number; y: number }[]): void { if (cells.length === 0) return this.clear() this.cameraFocus(cells) this.highlightLine(cells) this.emitGold(cells) this.screenFlash() } private cameraFocus(cells: { x: number; y: number }[]): void { let sumX = 0, sumY = 0 for (const c of cells) { sumX += c.x; sumY += c.y } const targetX = (sumX / cells.length) * CELL_SIZE + CELL_SIZE / 2 const targetY = (sumY / cells.length) * CELL_SIZE + CELL_SIZE / 2 const startX = this.camera.state.x const startY = this.camera.state.y let elapsed = 0 const dur = 0.3 let cb: () => void const tick = (dt: number) => { elapsed += dt const t = Math.min(1, elapsed / dur) this.camera.state.x = startX + (targetX - startX) * t this.camera.state.y = startY + (targetY - startY) * t this.camera.apply() this.camera.triggerChange() if (t >= 1) ticker.remove(cb) } if (ticker) { cb = () => tick(ticker.deltaMS / 1000) ticker.add(cb) } } private highlightLine(cells: { x: number; y: number }[]): void { const g = new Graphics() this.highlightGraphics = g this.effectLayer.addChild(g) for (const c of cells) { const lx = c.x * CELL_SIZE + CELL_SIZE / 2 const ly = c.y * CELL_SIZE + CELL_SIZE / 2 g.circle(lx, ly, CELL_SIZE * 0.5) } g.stroke({ width: 3, color: theme.winLineColor }) g.fill({ color: theme.winLineColor, alpha: 0.15 }) let elapsed = 0 const dur = 0.8 let cb: () => void const tick = (dt: number) => { elapsed += dt const t = Math.min(1, elapsed / dur) const pulse = 0.5 + 0.5 * Math.sin(t * Math.PI * 4) g.alpha = 0.4 + 0.6 * pulse if (t >= 1) { g.alpha = 1 ticker.remove(cb) } } if (ticker) { cb = () => tick(ticker.deltaMS / 1000) ticker.add(cb) } } private emitGold(cells: { x: number; y: number }[]): void { for (const c of cells) { const lx = c.x * CELL_SIZE + CELL_SIZE / 2 const ly = c.y * CELL_SIZE + CELL_SIZE / 2 this.particleSys.emit({ count: 12, x: lx, y: ly, color: theme.particleGold, spread: 120, speed: 150, size: 3, life: 0.8, }) this.particleSys.emit({ count: 4, x: lx, y: ly, color: theme.particleSpark, spread: 80, speed: 200, size: 1.5, life: 0.5, }) } } private screenFlash(): void { this.flashGraphics.clear() this.flashGraphics.rect(0, 0, this.vw, this.vh) this.flashGraphics.fill({ color: theme.screenFlash, alpha: 0.4 }) this.screenLayer.addChild(this.flashGraphics) let elapsed = 0 const dur = 0.5 let cb: () => void const tick = (dt: number) => { elapsed += dt const t = Math.min(1, elapsed / dur) this.flashGraphics.alpha = 0.4 * (1 - t) if (t >= 1) { this.screenLayer.removeChild(this.flashGraphics) this.flashGraphics.clear() ticker.remove(cb) } } if (ticker) { cb = () => tick(ticker.deltaMS / 1000) ticker.add(cb) } } }