From dfbdaa34996c4283775236f4ce6a306f37fad333 Mon Sep 17 00:00:00 2001 From: hypercross Date: Wed, 22 Apr 2026 21:29:43 +0800 Subject: [PATCH] feat(sts-viewer): add spawn/despawn tweens and async interruptions Add visual tweens for Buff and Card game objects. Update spawners to use `GameHostScene` and `addInterruption` to ensure animations complete before the next state change occurs. --- .../sts-like-viewer/src/gameobjects/Buff.ts | 20 +++++++++++++++++++ .../src/gameobjects/BuffSpawner.ts | 9 +++++++-- .../src/gameobjects/CardContainer.ts | 8 ++++---- .../src/gameobjects/CardSpawner.ts | 9 ++++++--- .../src/gameobjects/CombatUnitContainer.ts | 13 ++++++++++-- .../src/gameobjects/CombatUnitSpawner.ts | 18 +++++++++++------ 6 files changed, 60 insertions(+), 17 deletions(-) diff --git a/packages/sts-like-viewer/src/gameobjects/Buff.ts b/packages/sts-like-viewer/src/gameobjects/Buff.ts index 4c6fbb2..901246f 100644 --- a/packages/sts-like-viewer/src/gameobjects/Buff.ts +++ b/packages/sts-like-viewer/src/gameobjects/Buff.ts @@ -129,4 +129,24 @@ export class Buff extends Phaser.GameObjects.Container { this.tooltipContainer.destroy(fromScene); super.destroy(fromScene); } + + playSpawnTween() { + this.setScale(0); + this.scene.tweens.add({ + targets: this, + scale: 1, + duration: 200, + ease: "Back.Out", + }); + } + + playDespawnTween() { + this.scene.tweens.add({ + targets: this, + scale: 0, + duration: 200, + ease: "Back.In", + onComplete: () => this.destroy(), + }); + } } diff --git a/packages/sts-like-viewer/src/gameobjects/BuffSpawner.ts b/packages/sts-like-viewer/src/gameobjects/BuffSpawner.ts index 6567daa..d6e9bcf 100644 --- a/packages/sts-like-viewer/src/gameobjects/BuffSpawner.ts +++ b/packages/sts-like-viewer/src/gameobjects/BuffSpawner.ts @@ -1,10 +1,12 @@ +import { GameHostScene } from "boardgame-phaser"; import { BuffData, Buff } from "./Buff"; import { SpawnerCallback } from "boardgame-phaser"; +import { CombatState } from "boardgame-core/samples/slay-the-spire-like"; export type BuffDataWithIndex = BuffData & { x: number }; export class BuffSpawner implements SpawnerCallback { constructor( - public scene: Phaser.Scene, + public scene: GameHostScene, public buffContainer: Phaser.GameObjects.Container, ) {} getKey(t: BuffDataWithIndex) { @@ -13,10 +15,13 @@ export class BuffSpawner implements SpawnerCallback { onSpawn(t: BuffDataWithIndex) { const buff = new Buff(this.scene, t.x, 0, t); this.buffContainer.add(buff); + buff.playSpawnTween(); + this.scene.addInterruption(new Promise((r) => setTimeout(r, 50))); return buff; } onDespawn(obj: Buff) { - obj.destroy(); + this.scene.addInterruption(new Promise((r) => setTimeout(r, 50))); + obj.playDespawnTween(); } onUpdate(t: BuffDataWithIndex, obj: Buff) { obj.setPosition(t.x, 0); diff --git a/packages/sts-like-viewer/src/gameobjects/CardContainer.ts b/packages/sts-like-viewer/src/gameobjects/CardContainer.ts index 0f06da3..c2fcdc5 100644 --- a/packages/sts-like-viewer/src/gameobjects/CardContainer.ts +++ b/packages/sts-like-viewer/src/gameobjects/CardContainer.ts @@ -165,10 +165,10 @@ export class CardContainer extends Phaser.GameObjects.Container { return this._selected; } - playSpawnTween(delay = 0): void { + playSpawnTween(delay = 0) { this.setAlpha(0); this.setScale(0.5); - this.scene.tweens.add({ + return this.scene.tweens.add({ targets: this, alpha: 1, scale: 1, @@ -178,8 +178,8 @@ export class CardContainer extends Phaser.GameObjects.Container { }); } - playDespawnTween(onComplete?: () => void): void { - this.scene.tweens.add({ + playDespawnTween(onComplete?: () => void) { + return this.scene.tweens.add({ targets: this, alpha: 0, scale: 0.5, diff --git a/packages/sts-like-viewer/src/gameobjects/CardSpawner.ts b/packages/sts-like-viewer/src/gameobjects/CardSpawner.ts index d06c198..b0f1760 100644 --- a/packages/sts-like-viewer/src/gameobjects/CardSpawner.ts +++ b/packages/sts-like-viewer/src/gameobjects/CardSpawner.ts @@ -4,6 +4,7 @@ import type { GameCard, } from "boardgame-core/samples/slay-the-spire-like"; import { CardContainer } from "./CardContainer"; +import { GameHostScene } from "boardgame-phaser"; export interface CardSpawnData { cardId: string; @@ -17,7 +18,7 @@ const HAND_MARGIN = 100; export class CardSpawner implements Spawner { constructor( - private scene: Phaser.Scene, + private scene: GameHostScene, private getState: () => CombatState, private onCardClick: (cardId: string) => void, ) {} @@ -47,6 +48,7 @@ export class CardSpawner implements Spawner { }); container.playSpawnTween(data.index * 40); + this.scene.addInterruption(new Promise((r) => setTimeout(r, 40))); return container; } @@ -63,8 +65,9 @@ export class CardSpawner implements Spawner { } } - onDespawn(obj: CardContainer): void { + onDespawn(obj: CardContainer, data: CardSpawnData): void { obj.playDespawnTween(() => obj.destroy()); + this.scene.addInterruption(new Promise((r) => setTimeout(r, 40))); } private getCardPosition( @@ -104,7 +107,7 @@ export class CardSpawner implements Spawner { } export function createCardSpawner( - scene: Phaser.Scene, + scene: GameHostScene, getState: () => CombatState, onCardClick: (cardId: string) => void, ) { diff --git a/packages/sts-like-viewer/src/gameobjects/CombatUnitContainer.ts b/packages/sts-like-viewer/src/gameobjects/CombatUnitContainer.ts index 4d6b6c8..606537c 100644 --- a/packages/sts-like-viewer/src/gameobjects/CombatUnitContainer.ts +++ b/packages/sts-like-viewer/src/gameobjects/CombatUnitContainer.ts @@ -1,12 +1,14 @@ import Phaser from "phaser"; import type { CombatEntity, + CombatState, EffectTable, EnemyEntity, } from "boardgame-core/samples/slay-the-spire-like"; import { BuffData } from "./Buff"; import { createSpawnUpdate } from "boardgame-phaser"; import { BuffDataWithIndex, BuffSpawner } from "./BuffSpawner"; +import { GameHostScene } from "boardgame-phaser"; export type CombatUnitData = { key: string; @@ -30,15 +32,22 @@ export class CombatUnitContainer extends Phaser.GameObjects.Container { private hpText!: Phaser.GameObjects.Text; private buffContainer!: Phaser.GameObjects.Container; private intentText!: Phaser.GameObjects.Text | null; + private hostScene: GameHostScene; private updateBuffs!: (buffs: Iterable) => void; private currentEntity!: CombatEntity; private currentName: string; private currentIsPlayer: boolean; - constructor(scene: Phaser.Scene, x: number, y: number, data: CombatUnitData) { + constructor( + scene: GameHostScene, + x: number, + y: number, + data: CombatUnitData, + ) { super(scene, x, y); scene.add.existing(this); + this.hostScene = scene; this.currentEntity = data.entity; this.currentName = data.name; @@ -92,7 +101,7 @@ export class CombatUnitContainer extends Phaser.GameObjects.Container { this.buffContainer = this.scene.add.container(0, CONTAINER_HEIGHT / 2 - 40); this.updateBuffs = createSpawnUpdate( - new BuffSpawner(this.scene, this.buffContainer), + new BuffSpawner(this.hostScene, this.buffContainer), ); if (!this.currentIsPlayer) { diff --git a/packages/sts-like-viewer/src/gameobjects/CombatUnitSpawner.ts b/packages/sts-like-viewer/src/gameobjects/CombatUnitSpawner.ts index 8e40b7a..053d27f 100644 --- a/packages/sts-like-viewer/src/gameobjects/CombatUnitSpawner.ts +++ b/packages/sts-like-viewer/src/gameobjects/CombatUnitSpawner.ts @@ -1,11 +1,17 @@ -import Phaser from "phaser"; import type { Spawner } from "boardgame-phaser"; import { spawnEffect } from "boardgame-phaser"; -import type { CombatState, CombatEntity, EnemyEntity } from "boardgame-core/samples/slay-the-spire-like"; -import { CombatUnitContainer, type CombatUnitData } from "./CombatUnitContainer"; +import type { CombatState } from "boardgame-core/samples/slay-the-spire-like"; +import { + CombatUnitContainer, + type CombatUnitData, +} from "./CombatUnitContainer"; +import { GameHostScene } from "boardgame-phaser"; -export class CombatUnitSpawner implements Spawner { - constructor(private scene: Phaser.Scene) {} +export class CombatUnitSpawner implements Spawner< + CombatUnitData, + CombatUnitContainer +> { + constructor(private scene: GameHostScene) {} *getData(): Iterable { const combat = this.getCombatState(); @@ -70,6 +76,6 @@ export class CombatUnitSpawner implements Spawner) { return spawnEffect(new CombatUnitSpawner(scene)); }