import * as Phaser from "phaser"; import type { LevelPath } from "../logic/types"; import { colorToTextureKey, directionToAngle, directionToDelta } from "../logic/types"; import { GameConfig } from "../GameConfig"; import { BoardRenderer } from "./BoardRenderer"; import { TrayRenderer } from "./TrayRenderer"; /** * 动画管理器 * 整条路径滑出棋盘 → head 车飞入篮子 → 消除 → 抖动 */ export class AnimationManager { private scene: Phaser.Scene; constructor(scene: Phaser.Scene) { this.scene = scene; } /** * 整条路径沿方向滑出棋盘,然后 head 飞入篮子 */ slideOutAndFlyToTray( path: LevelPath, stepsToExit: number, boardRenderer: BoardRenderer, trayRenderer: TrayRenderer, trayInsertIndex: number, onSlideComplete: () => void, onFlyComplete: () => void, ): void { const { dx, dy } = directionToDelta(path.direction); const cellSize = boardRenderer.cellSize; const slideDistance = (stepsToExit + path.points.length) * cellSize; // 创建一个容器来装整条路径的所有汽车 const slideContainer = this.scene.add.container(0, 0); slideContainer.setDepth(1000); const textureKey = colorToTextureKey(path.color); const carSize = cellSize * GameConfig.CAR_CELL_RATIO; // 为路径的每个点创建一辆车 const carSprites: Phaser.GameObjects.Image[] = []; for (let ptIdx = 0; ptIdx < path.points.length; ptIdx++) { const pt = path.points[ptIdx]; const worldPos = boardRenderer.getCellWorldPos(pt.y, pt.x); const car = this.scene.add.image(worldPos.x, worldPos.y, textureKey); car.setOrigin(0.5); // 所有车统一朝向路径推出方向 car.setRotation(directionToAngle(path.direction)); const scale = Math.min(carSize / car.width, carSize / car.height); car.setScale(scale); slideContainer.add(car); carSprites.push(car); } // 阶段 1:整条路径沿方向滑出 this.scene.tweens.add({ targets: slideContainer, x: dx * slideDistance, y: dy * slideDistance, duration: 350, ease: "Power2", onComplete: () => { // 销毁 body 车,保留 head 车做飞入动画 const headCar = carSprites[carSprites.length - 1]; for (let i = 0; i < carSprites.length - 1; i++) { carSprites[i].destroy(); } slideContainer.remove(headCar); slideContainer.destroy(); // head 车的当前世界位置 const currentX = headCar.x + dx * slideDistance; const currentY = headCar.y + dy * slideDistance; headCar.setPosition(currentX, currentY); onSlideComplete(); // 阶段 2:head 车飞入篮子 const toPos = trayRenderer.getSlotWorldPos(trayInsertIndex); const midX = (currentX + toPos.x) / 2; const midY = Math.min(currentY, toPos.y) - 60; const traySize = Math.min(cellSize * 0.5, GameConfig.CAR_TRAY_SIZE); const endScale = Math.min(traySize / headCar.width, traySize / headCar.height); const startX = currentX; const startY = currentY; const startScale = headCar.scaleX; this.scene.tweens.add({ targets: { t: 0 }, t: 1, duration: GameConfig.FLY_DURATION, ease: "Power2", onUpdate: (_tween: Phaser.Tweens.Tween, target: { t: number }) => { const t = target.t; const px = (1 - t) * (1 - t) * startX + 2 * (1 - t) * t * midX + t * t * toPos.x; const py = (1 - t) * (1 - t) * startY + 2 * (1 - t) * t * midY + t * t * toPos.y; headCar.setPosition(px, py); headCar.setScale(startScale + (endScale - startScale) * t); headCar.setRotation(headCar.rotation * (1 - t * 0.5)); }, onComplete: () => { headCar.destroy(); onFlyComplete(); }, }); }, }); } /** * 物品入场弹跳动画 */ bounceIn(sprite: Phaser.GameObjects.Image): void { const targetScale = sprite.scaleX; sprite.setScale(0); this.scene.tweens.add({ targets: sprite, scaleX: targetScale, scaleY: targetScale, duration: 200, ease: "Back.easeOut", }); } /** * 胜利粒子动画 */ playWinAnimation(): void { const { width, height } = this.scene.scale; const particles = 25; for (let i = 0; i < particles; i++) { const x = Phaser.Math.Between(Math.round(width * 0.1), Math.round(width * 0.9)); const y = Phaser.Math.Between(Math.round(height * 0.2), Math.round(height * 0.7)); const size = Phaser.Math.Between(8, 20); const color = Phaser.Math.Between(0x000000, 0xffffff); const particle = this.scene.add.graphics(); particle.fillStyle(color, 1); particle.fillCircle(0, 0, size); particle.setPosition(x, y); particle.setAlpha(0); particle.setDepth(2000); this.scene.tweens.add({ targets: particle, alpha: 1, y: y - Phaser.Math.Between(50, 150), scaleX: 0, scaleY: 0, duration: Phaser.Math.Between(600, 1200), delay: i * 50, ease: "Power2", onComplete: () => particle.destroy(), }); } } }