From c25759d1472662948c95e8f024995221a20556d1 Mon Sep 17 00:00:00 2001 From: hypercross Date: Sun, 19 Apr 2026 12:45:05 +0800 Subject: [PATCH] refactor(onitama): extract tweens into factory functions Centralize Onitama animation logic by moving inline Phaser tweens into a dedicated `tweens.ts` configuration file. This improves reusability and cleans up spawners and renderers. Also updated documentation to warn against adding interruptions to looped tweens to prevent game logic hangs. --- docs/GamePatterns.md | 4 +- packages/onitama-game/src/config/index.ts | 30 ++ packages/onitama-game/src/config/tweens.ts | 322 ++++++++++++++++++ .../src/renderers/HighlightRenderer.ts | 28 +- .../src/renderers/SelectionRenderer.ts | 40 +-- .../onitama-game/src/scenes/OnitamaScene.ts | 10 +- .../onitama-game/src/spawners/CardSpawner.ts | 109 +++--- .../src/spawners/HighlightSpawner.ts | 36 +- .../onitama-game/src/spawners/PawnSpawner.ts | 40 +-- 9 files changed, 440 insertions(+), 179 deletions(-) create mode 100644 packages/onitama-game/src/config/tweens.ts diff --git a/docs/GamePatterns.md b/docs/GamePatterns.md index eb91db8..c9d4734 100644 --- a/docs/GamePatterns.md +++ b/docs/GamePatterns.md @@ -59,6 +59,8 @@ Extend `Phaser.GameObjects.Container` to encapsulate visuals and state. ### 5. Tween Interruption Always register state-related tweens: `this.scene.addTweenInterruption(tween)`. Prevents visual glitches when game state changes mid-animation. +DO NOT add interruptions for looped tweens. +Otherwise the game logic will hang. ### 6. Scene Navigation Use `await this.sceneController.launch('SceneKey')`. @@ -103,4 +105,4 @@ See `packages/framework/` for test setup examples. - `AGENTS.md` — Project overview, commands, and code style - `docs/GameModule.md` — GameModule implementation guide - `packages/framework/src/` — `boardgame-phaser` source code -- `packages/onitama-game/src/` — Complete game implementation reference \ No newline at end of file +- `packages/onitama-game/src/` — Complete game implementation reference diff --git a/packages/onitama-game/src/config/index.ts b/packages/onitama-game/src/config/index.ts index 55382fa..7e6a961 100644 --- a/packages/onitama-game/src/config/index.ts +++ b/packages/onitama-game/src/config/index.ts @@ -219,3 +219,33 @@ export function getCardPosition( export function colorToStr(hex: number) { return Display.Color.ValueToColor(hex).toString(); } + +// Tween factory functions +export { + // Card tweens + createCardMoveTween, + createCardRotateTween, + createCardSpawnTween, + createCardDespawnTween, + // Pawn tweens + createPawnMoveTween, + createPawnSpawnTween, + createPawnDespawnTween, + // Highlight tweens + createHighlightSpawnTween, + createHighlightDespawnTween, + createHighlightClickFeedbackTween, + createHighlightInnerPulseTween, + createHighlightOuterPulseTween, + // Selection tweens + createSelectionFadeInTween, + createSelectionPulseTween, + createSelectionFadeOutTween, + createSelectionShowTween, + createSelectionRingPulseTween, + createSelectionHideTween, + // Overlay tweens + createWinnerPulseTween, + // Types + type TweenScene, +} from "@/config/tweens"; diff --git a/packages/onitama-game/src/config/tweens.ts b/packages/onitama-game/src/config/tweens.ts new file mode 100644 index 0000000..a032d0d --- /dev/null +++ b/packages/onitama-game/src/config/tweens.ts @@ -0,0 +1,322 @@ +import type Phaser from "phaser"; + +import { ANIMATIONS } from "@/config"; + +export type TweenScene = Phaser.Scene & { + addTweenInterruption(tween: Phaser.Tweens.Tween): void; +}; + +// ───────────────────────────────────────────────────────────── +// Card Tweens +// ───────────────────────────────────────────────────────────── + +export function createCardMoveTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, + x: number, + y: number, +): Phaser.Tweens.Tween { + const tween = scene.tweens.add({ + targets: target, + x, + y, + duration: ANIMATIONS.cardMove, + ease: "Back.easeOut", + }); + scene.addTweenInterruption(tween); + return tween; +} + +export function createCardRotateTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, + fromAngle: number, + toAngle: number, +): Phaser.Tweens.Tween { + const tween = scene.tweens.add({ + targets: target, + angle: { from: fromAngle, to: toAngle }, + duration: ANIMATIONS.cardRotate, + ease: "Back.easeOut", + }); + scene.addTweenInterruption(tween); + return tween; +} + +export function createCardSpawnTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, +): Phaser.Tweens.Tween { + const tween = scene.tweens.add({ + targets: target, + alpha: 1, + duration: ANIMATIONS.cardSpawn, + ease: "Power2", + }); + scene.addTweenInterruption(tween); + return tween; +} + +export function createCardDespawnTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, + onComplete: () => void, +): Phaser.Tweens.Tween { + const tween = scene.tweens.add({ + targets: target, + alpha: 0, + scale: 0.8, + duration: ANIMATIONS.cardDespawn, + ease: "Power2", + onComplete, + }); + scene.addTweenInterruption(tween); + return tween; +} + +// ───────────────────────────────────────────────────────────── +// Pawn Tweens +// ───────────────────────────────────────────────────────────── + +export function createPawnMoveTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, + x: number, + y: number, +): Phaser.Tweens.Tween { + const tween = scene.tweens.add({ + targets: target, + x, + y, + duration: ANIMATIONS.pawnMove, + ease: "Back.easeOut", + }); + scene.addTweenInterruption(tween); + return tween; +} + +export function createPawnSpawnTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, +): Phaser.Tweens.Tween { + const tween = scene.tweens.add({ + targets: target, + scale: 1, + duration: ANIMATIONS.pawnSpawn, + ease: "Back.easeOut", + }); + scene.addTweenInterruption(tween); + return tween; +} + +export function createPawnDespawnTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, + onComplete: () => void, +): Phaser.Tweens.Tween { + const tween = scene.tweens.add({ + targets: target, + scale: 0, + alpha: 0, + duration: ANIMATIONS.pawnDespawn, + ease: "Back.easeIn", + onComplete, + }); + scene.addTweenInterruption(tween); + return tween; +} + +// ───────────────────────────────────────────────────────────── +// Highlight Tweens +// ───────────────────────────────────────────────────────────── + +export function createHighlightSpawnTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, +): Phaser.Tweens.Tween { + const tween = scene.tweens.add({ + targets: target, + alpha: 1, + scale: 1, + duration: ANIMATIONS.highlightSpawn, + ease: "Back.easeOut", + }); + scene.addTweenInterruption(tween); + return tween; +} + +export function createHighlightDespawnTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, + onComplete: () => void, +): Phaser.Tweens.Tween { + const tween = scene.tweens.add({ + targets: target, + scale: 0, + alpha: 0, + duration: ANIMATIONS.highlightDespawn, + ease: "Back.easeIn", + onComplete, + }); + scene.addTweenInterruption(tween); + return tween; +} + +export function createHighlightClickFeedbackTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, +): Phaser.Tweens.Tween { + return scene.tweens.add({ + targets: target, + scale: 1.5, + alpha: 0.8, + duration: ANIMATIONS.clickFeedback, + ease: "Power2", + yoyo: true, + }); +} + +export function createHighlightInnerPulseTween( + scene: TweenScene, + targets: Phaser.GameObjects.GameObject[], +): Phaser.Tweens.Tween { + return scene.tweens.add({ + targets, + scale: 1.2, + alpha: 0.6, + duration: 600, + ease: "Sine.easeInOut", + yoyo: true, + repeat: -1, + }); +} + +export function createHighlightOuterPulseTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, +): Phaser.Tweens.Tween { + return scene.tweens.add({ + targets: target, + scale: 1.3, + alpha: 0.3, + duration: 800, + ease: "Sine.easeInOut", + yoyo: true, + repeat: -1, + delay: 200, + }); +} + +// ───────────────────────────────────────────────────────────── +// Selection Tweens +// ───────────────────────────────────────────────────────────── + +export function createSelectionFadeInTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, + onComplete: () => void, +): Phaser.Tweens.Tween { + return scene.tweens.add({ + targets: target, + alpha: 1, + scale: 1.05, + duration: ANIMATIONS.selectionFadeIn, + ease: "Power2", + onComplete, + }); +} + +export function createSelectionPulseTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, + lineWidth: number, +): Phaser.Tweens.Tween { + return scene.tweens.add({ + targets: target, + alpha: 0.7, + lineWidth: lineWidth + 1, + duration: ANIMATIONS.selectionPulse, + ease: "Sine.easeInOut", + yoyo: true, + repeat: -1, + }); +} + +export function createSelectionFadeOutTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, + onComplete: () => void, +): Phaser.Tweens.Tween { + return scene.tweens.add({ + targets: target, + alpha: 0, + scale: 0.95, + duration: ANIMATIONS.selectionFadeOut, + ease: "Power2", + onComplete, + }); +} + +export function createSelectionShowTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, + onCompleteFadeIn: () => void, +): Phaser.Tweens.Tween { + return scene.tweens.add({ + targets: target, + alpha: 0.8, + duration: ANIMATIONS.selectionFadeIn, + ease: "Power2", + onComplete: onCompleteFadeIn, + }); +} + +export function createSelectionRingPulseTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, +): Phaser.Tweens.Tween { + return scene.tweens.add({ + targets: target, + scale: 1.15, + alpha: 0.6, + duration: ANIMATIONS.selectionPulse, + ease: "Sine.easeInOut", + yoyo: true, + repeat: -1, + }); +} + +export function createSelectionHideTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, + onComplete: () => void, +): Phaser.Tweens.Tween { + return scene.tweens.add({ + targets: target, + alpha: 0, + scale: 0.9, + duration: ANIMATIONS.selectionFadeOut, + ease: "Power2", + onComplete: () => { + target.destroy(); + onComplete(); + }, + }); +} + +// ───────────────────────────────────────────────────────────── +// Overlay Tweens +// ───────────────────────────────────────────────────────────── + +export function createWinnerPulseTween( + scene: TweenScene, + target: Phaser.GameObjects.GameObject, +): Phaser.Tweens.Tween { + return scene.tweens.add({ + targets: target, + scale: 1.2, + duration: ANIMATIONS.winnerPulse, + yoyo: true, + repeat: 1, + }); +} diff --git a/packages/onitama-game/src/renderers/HighlightRenderer.ts b/packages/onitama-game/src/renderers/HighlightRenderer.ts index 56dbf2d..6f2e1db 100644 --- a/packages/onitama-game/src/renderers/HighlightRenderer.ts +++ b/packages/onitama-game/src/renderers/HighlightRenderer.ts @@ -1,6 +1,11 @@ import type { OnitamaScene } from "@/scenes/OnitamaScene"; -import { CELL_SIZE, COLORS } from "@/config"; +import { + CELL_SIZE, + COLORS, + createHighlightInnerPulseTween, + createHighlightOuterPulseTween, +} from "@/config"; export interface HighlightRenderOptions { x: number; @@ -78,26 +83,9 @@ export class HighlightRenderer { if (!outerCircle || !innerCircle) return; // Inner circle pulse - this.scene.tweens.add({ - targets: [outerCircle, innerCircle], - scale: 1.2, - alpha: 0.6, - duration: 600, - ease: "Sine.easeInOut", - yoyo: true, - repeat: -1, - }); + createHighlightInnerPulseTween(this.scene, [outerCircle, innerCircle]); // Outer circle staggered pulse - this.scene.tweens.add({ - targets: outerCircle, - scale: 1.3, - alpha: 0.3, - duration: 800, - ease: "Sine.easeInOut", - yoyo: true, - repeat: -1, - delay: 200, - }); + createHighlightOuterPulseTween(this.scene, outerCircle); } } diff --git a/packages/onitama-game/src/renderers/SelectionRenderer.ts b/packages/onitama-game/src/renderers/SelectionRenderer.ts index f170dc1..dd1e8d9 100644 --- a/packages/onitama-game/src/renderers/SelectionRenderer.ts +++ b/packages/onitama-game/src/renderers/SelectionRenderer.ts @@ -2,7 +2,13 @@ import { GameObjects } from "phaser"; import type { OnitamaScene } from "@/scenes/OnitamaScene"; -import { CELL_SIZE, COLORS, ANIMATIONS } from "@/config"; +import { + CELL_SIZE, + COLORS, + createSelectionShowTween, + createSelectionRingPulseTween, + createSelectionHideTween, +} from "@/config"; export interface SelectionRenderOptions { x: number; @@ -51,23 +57,9 @@ export class SelectionRenderer { // Fade in animation // DO NOT add interruption here // otherwise the game will stop indefinitely - tweens.add({ - targets: ring, - alpha: 0.8, - duration: ANIMATIONS.selectionFadeIn, - ease: "Power2", - onComplete: () => { - // Start pulse animation after fade-in completes - pulseTween = tweens.add({ - targets: ring, - scale: 1.15, - alpha: 0.6, - duration: ANIMATIONS.selectionPulse, - ease: "Sine.easeInOut", - yoyo: true, - repeat: -1, - }); - }, + createSelectionShowTween(this.scene, ring, () => { + // Start pulse animation after fade-in completes + pulseTween = createSelectionRingPulseTween(this.scene, ring); }); // Return cleanup function @@ -97,17 +89,7 @@ export class SelectionRenderer { tweens.killTweensOf(ring); // Fade out animation - tweens.add({ - targets: ring, - alpha: 0, - scale: 0.9, - duration: ANIMATIONS.selectionFadeOut, - ease: "Power2", - onComplete: () => { - ring.destroy(); - onComplete?.(); - }, - }); + createSelectionHideTween(this.scene, ring, () => onComplete?.()); } /** diff --git a/packages/onitama-game/src/scenes/OnitamaScene.ts b/packages/onitama-game/src/scenes/OnitamaScene.ts index af990c2..413a265 100644 --- a/packages/onitama-game/src/scenes/OnitamaScene.ts +++ b/packages/onitama-game/src/scenes/OnitamaScene.ts @@ -9,11 +9,11 @@ import type Phaser from "phaser"; import { COLORS, FONTS, - ANIMATIONS, MENU_BUTTON, getBoardCenter, getCardLabelPosition, colorToStr, + createWinnerPulseTween, } from "@/config"; import { prompts } from "@/game/onitama"; import { @@ -304,12 +304,6 @@ export class OnitamaScene extends GameHostScene { this.winnerOverlay.add(winText); - this.tweens.add({ - targets: winText, - scale: 1.2, - duration: ANIMATIONS.winnerPulse, - yoyo: true, - repeat: 1, - }); + createWinnerPulseTween(this, winText); } } diff --git a/packages/onitama-game/src/spawners/CardSpawner.ts b/packages/onitama-game/src/spawners/CardSpawner.ts index 9ab3c68..f8a1684 100644 --- a/packages/onitama-game/src/spawners/CardSpawner.ts +++ b/packages/onitama-game/src/spawners/CardSpawner.ts @@ -4,7 +4,18 @@ import type { Card } from "@/game/onitama"; import type { OnitamaScene } from "@/scenes/OnitamaScene"; import type { Spawner } from "boardgame-phaser"; -import { CARD_WIDTH, CARD_HEIGHT, ANIMATIONS, getCardPosition } from "@/config"; +import { + CARD_WIDTH, + CARD_HEIGHT, + getCardPosition, + createCardMoveTween, + createCardRotateTween, + createCardSpawnTween, + createCardDespawnTween, + createSelectionFadeInTween, + createSelectionPulseTween, + createSelectionFadeOutTween, +} from "@/config"; import { CardRenderer } from "@/renderers"; // Re-export for backward compatibility @@ -66,13 +77,12 @@ export class CardContainer extends Phaser.GameObjects.Container { setRotation(rotation: number, animated: boolean = false): this { if (animated) { const currentAngle = this.angle; - const tween = this.scene.tweens.add({ - targets: this, - angle: { from: currentAngle, to: rotation }, - duration: ANIMATIONS.cardRotate, - ease: "Back.easeOut", - }); - (this.scene as OnitamaScene).addTweenInterruption(tween); + createCardRotateTween( + this.scene as OnitamaScene, + this, + currentAngle, + rotation, + ); } else { this.angle = rotation; } @@ -88,32 +98,22 @@ export class CardContainer extends Phaser.GameObjects.Container { if (!this.highlightRect) { // 创建高亮边框(初始透明) - this.highlightRect = (this.scene as OnitamaScene).add + const rect = (this.scene as OnitamaScene).add .rectangle(0, 0, CARD_WIDTH + 8, CARD_HEIGHT + 8, color, 0) .setStrokeStyle(lineWidth, color) .setAlpha(0) .setDepth(-1); + this.highlightRect = rect; this.addAt(this.highlightRect, 0); // 淡入动画 - this.scene.tweens.add({ - targets: this.highlightRect, - alpha: 1, - scale: 1.05, - duration: ANIMATIONS.selectionFadeIn, - ease: "Power2", - onComplete: () => { - // 淡入完成后开始脉冲动画 - this.highlightTween = this.scene.tweens.add({ - targets: this.highlightRect, - alpha: 0.7, - lineWidth: lineWidth + 1, - duration: ANIMATIONS.selectionPulse, - ease: "Sine.easeInOut", - yoyo: true, - repeat: -1, - }); - }, + createSelectionFadeInTween(this.scene as OnitamaScene, rect, () => { + // 淡入完成后开始脉冲动画 + this.highlightTween = createSelectionPulseTween( + this.scene as OnitamaScene, + rect, + lineWidth, + ); }); } else { // 如果已经存在,停止当前动画并重新开始脉冲 @@ -121,15 +121,11 @@ export class CardContainer extends Phaser.GameObjects.Container { this.highlightTween.stop(); } this.highlightRect.setStrokeStyle(lineWidth, color); - this.highlightTween = this.scene.tweens.add({ - targets: this.highlightRect, - alpha: 0.7, - lineWidth: lineWidth + 1, - duration: ANIMATIONS.selectionPulse, - ease: "Sine.easeInOut", - yoyo: true, - repeat: -1, - }); + this.highlightTween = createSelectionPulseTween( + this.scene as OnitamaScene, + this.highlightRect, + lineWidth, + ); } } @@ -146,18 +142,15 @@ export class CardContainer extends Phaser.GameObjects.Container { this.scene.tweens.killTweensOf(this.highlightRect); // 淡出动画 - this.scene.tweens.add({ - targets: this.highlightRect, - alpha: 0, - scale: 0.95, - duration: ANIMATIONS.selectionFadeOut, - ease: "Power2", - onComplete: () => { + createSelectionFadeOutTween( + this.scene as OnitamaScene, + this.highlightRect, + () => { // 淡出完成后销毁矩形 this.highlightRect?.destroy(); this.highlightRect = null; }, - }); + ); } } @@ -242,15 +235,7 @@ export class CardSpawner implements Spawner { const pos = getCardPosition(data.position, data.index); // 播放移动动画并添加中断 - const tween = this.scene.tweens.add({ - targets: obj, - x: pos.x, - y: pos.y, - duration: ANIMATIONS.cardMove, - ease: "Back.easeOut", - }); - - this.scene.addTweenInterruption(tween); + createCardMoveTween(this.scene, obj, pos.x, pos.y); this.previousData.set(data.cardId, { ...data }); } @@ -305,27 +290,13 @@ export class CardSpawner implements Spawner { // 初始状态为透明,然后淡入 container.setAlpha(0); - const tween = this.scene.tweens.add({ - targets: container, - alpha: 1, - duration: ANIMATIONS.cardSpawn, - ease: "Power2", - }); - this.scene.addTweenInterruption(tween); + createCardSpawnTween(this.scene, container); this.previousData.set(data.cardId, { ...data }); return container; } onDespawn(obj: CardContainer): void { - const tween = this.scene.tweens.add({ - targets: obj, - alpha: 0, - scale: 0.8, - duration: ANIMATIONS.cardDespawn, - ease: "Power2", - onComplete: () => obj.destroy(), - }); - this.scene.addTweenInterruption(tween); + createCardDespawnTween(this.scene, obj, () => obj.destroy()); } } diff --git a/packages/onitama-game/src/spawners/HighlightSpawner.ts b/packages/onitama-game/src/spawners/HighlightSpawner.ts index 4dd3bec..90ab4dd 100644 --- a/packages/onitama-game/src/spawners/HighlightSpawner.ts +++ b/packages/onitama-game/src/spawners/HighlightSpawner.ts @@ -4,7 +4,13 @@ import { Geom } from "phaser"; import type { OnitamaScene } from "@/scenes/OnitamaScene"; import type { Spawner } from "boardgame-phaser"; -import { boardToScreen, CELL_SIZE, ANIMATIONS } from "@/config"; +import { + boardToScreen, + CELL_SIZE, + createHighlightSpawnTween, + createHighlightDespawnTween, + createHighlightClickFeedbackTween, +} from "@/config"; import { HighlightRenderer } from "@/renderers"; export interface HighlightData { @@ -86,28 +92,14 @@ export class HighlightSpawner implements Spawner< // 出现动画:从0缩放和透明淡入 container.setAlpha(0); container.setScale(0); - const spawnTween = this.scene.tweens.add({ - targets: container, - alpha: 1, - scale: 1, - duration: ANIMATIONS.highlightSpawn, - ease: "Back.easeOut", - }); - this.scene.addTweenInterruption(spawnTween); + createHighlightSpawnTween(this.scene, container); // 使用 HighlightRenderer 设置脉冲动画 this.renderer.setupPulseAnimations(container); container.on("pointerdown", () => { // 点击时的反馈动画 - this.scene.tweens.add({ - targets: container, - scale: 1.5, - alpha: 0.8, - duration: ANIMATIONS.clickFeedback, - ease: "Power2", - yoyo: true, - }); + createHighlightClickFeedbackTween(this.scene, container); this.scene.onHighlightClick(data); }); @@ -117,14 +109,6 @@ export class HighlightSpawner implements Spawner< onDespawn(obj: Phaser.GameObjects.Container): void { // 消失动画:缩小并淡出 - const despawnTween = this.scene.tweens.add({ - targets: obj, - scale: 0, - alpha: 0, - duration: ANIMATIONS.highlightDespawn, - ease: "Back.easeIn", - onComplete: () => obj.destroy(), - }); - this.scene.addTweenInterruption(despawnTween); + createHighlightDespawnTween(this.scene, obj, () => obj.destroy()); } } diff --git a/packages/onitama-game/src/spawners/PawnSpawner.ts b/packages/onitama-game/src/spawners/PawnSpawner.ts index dbe9964..b5a77d0 100644 --- a/packages/onitama-game/src/spawners/PawnSpawner.ts +++ b/packages/onitama-game/src/spawners/PawnSpawner.ts @@ -5,7 +5,12 @@ import type { Pawn } from "@/game/onitama"; import type { OnitamaScene } from "@/scenes/OnitamaScene"; import type { Spawner } from "boardgame-phaser"; -import { boardToScreen, ANIMATIONS } from "@/config"; +import { + boardToScreen, + createPawnMoveTween, + createPawnSpawnTween, + createPawnDespawnTween, +} from "@/config"; import { PawnRenderer, SelectionRenderer } from "@/renderers"; // Re-export for backward compatibility @@ -97,14 +102,12 @@ export class PawnContainer extends GameObjects.Container { const targetPos = boardToScreen(newPosition[0], newPosition[1]); if (animated) { - const tween = this.scene.tweens.add({ - targets: this, - x: targetPos.x, - y: targetPos.y, - duration: ANIMATIONS.pawnMove, - ease: "Back.easeOut", - }); - (this.scene as OnitamaScene).addTweenInterruption(tween); + createPawnMoveTween( + this.scene as OnitamaScene, + this, + targetPos.x, + targetPos.y, + ); } else { this.x = targetPos.x; this.y = targetPos.y; @@ -153,28 +156,13 @@ export class PawnSpawner implements Spawner { // 淡入动画 container.setScale(0); - const tween = this.scene.tweens.add({ - targets: container, - scale: 1, - duration: ANIMATIONS.pawnSpawn, - ease: "Back.easeOut", - }); - this.scene.addTweenInterruption(tween); + createPawnSpawnTween(this.scene, container); return container; } onDespawn(obj: PawnContainer): void { // 播放消失动画并添加中断 - const tween = this.scene.tweens.add({ - targets: obj, - scale: 0, - alpha: 0, - duration: ANIMATIONS.pawnDespawn, - ease: "Back.easeIn", - onComplete: () => obj.destroy(), - }); - - this.scene.addTweenInterruption(tween); + createPawnDespawnTween(this.scene, obj, () => obj.destroy()); } }