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.
This commit is contained in:
hypercross 2026-04-22 21:29:43 +08:00
parent c29b9a43b3
commit dfbdaa3499
6 changed files with 60 additions and 17 deletions

View File

@ -129,4 +129,24 @@ export class Buff extends Phaser.GameObjects.Container {
this.tooltipContainer.destroy(fromScene); this.tooltipContainer.destroy(fromScene);
super.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(),
});
}
} }

View File

@ -1,10 +1,12 @@
import { GameHostScene } from "boardgame-phaser";
import { BuffData, Buff } from "./Buff"; import { BuffData, Buff } from "./Buff";
import { SpawnerCallback } from "boardgame-phaser"; import { SpawnerCallback } from "boardgame-phaser";
import { CombatState } from "boardgame-core/samples/slay-the-spire-like";
export type BuffDataWithIndex = BuffData & { x: number }; export type BuffDataWithIndex = BuffData & { x: number };
export class BuffSpawner implements SpawnerCallback<BuffDataWithIndex, Buff> { export class BuffSpawner implements SpawnerCallback<BuffDataWithIndex, Buff> {
constructor( constructor(
public scene: Phaser.Scene, public scene: GameHostScene<CombatState>,
public buffContainer: Phaser.GameObjects.Container, public buffContainer: Phaser.GameObjects.Container,
) {} ) {}
getKey(t: BuffDataWithIndex) { getKey(t: BuffDataWithIndex) {
@ -13,10 +15,13 @@ export class BuffSpawner implements SpawnerCallback<BuffDataWithIndex, Buff> {
onSpawn(t: BuffDataWithIndex) { onSpawn(t: BuffDataWithIndex) {
const buff = new Buff(this.scene, t.x, 0, t); const buff = new Buff(this.scene, t.x, 0, t);
this.buffContainer.add(buff); this.buffContainer.add(buff);
buff.playSpawnTween();
this.scene.addInterruption(new Promise((r) => setTimeout(r, 50)));
return buff; return buff;
} }
onDespawn(obj: Buff) { onDespawn(obj: Buff) {
obj.destroy(); this.scene.addInterruption(new Promise((r) => setTimeout(r, 50)));
obj.playDespawnTween();
} }
onUpdate(t: BuffDataWithIndex, obj: Buff) { onUpdate(t: BuffDataWithIndex, obj: Buff) {
obj.setPosition(t.x, 0); obj.setPosition(t.x, 0);

View File

@ -165,10 +165,10 @@ export class CardContainer extends Phaser.GameObjects.Container {
return this._selected; return this._selected;
} }
playSpawnTween(delay = 0): void { playSpawnTween(delay = 0) {
this.setAlpha(0); this.setAlpha(0);
this.setScale(0.5); this.setScale(0.5);
this.scene.tweens.add({ return this.scene.tweens.add({
targets: this, targets: this,
alpha: 1, alpha: 1,
scale: 1, scale: 1,
@ -178,8 +178,8 @@ export class CardContainer extends Phaser.GameObjects.Container {
}); });
} }
playDespawnTween(onComplete?: () => void): void { playDespawnTween(onComplete?: () => void) {
this.scene.tweens.add({ return this.scene.tweens.add({
targets: this, targets: this,
alpha: 0, alpha: 0,
scale: 0.5, scale: 0.5,

View File

@ -4,6 +4,7 @@ import type {
GameCard, GameCard,
} from "boardgame-core/samples/slay-the-spire-like"; } from "boardgame-core/samples/slay-the-spire-like";
import { CardContainer } from "./CardContainer"; import { CardContainer } from "./CardContainer";
import { GameHostScene } from "boardgame-phaser";
export interface CardSpawnData { export interface CardSpawnData {
cardId: string; cardId: string;
@ -17,7 +18,7 @@ const HAND_MARGIN = 100;
export class CardSpawner implements Spawner<CardSpawnData, CardContainer> { export class CardSpawner implements Spawner<CardSpawnData, CardContainer> {
constructor( constructor(
private scene: Phaser.Scene, private scene: GameHostScene<CombatState>,
private getState: () => CombatState, private getState: () => CombatState,
private onCardClick: (cardId: string) => void, private onCardClick: (cardId: string) => void,
) {} ) {}
@ -47,6 +48,7 @@ export class CardSpawner implements Spawner<CardSpawnData, CardContainer> {
}); });
container.playSpawnTween(data.index * 40); container.playSpawnTween(data.index * 40);
this.scene.addInterruption(new Promise((r) => setTimeout(r, 40)));
return container; return container;
} }
@ -63,8 +65,9 @@ export class CardSpawner implements Spawner<CardSpawnData, CardContainer> {
} }
} }
onDespawn(obj: CardContainer): void { onDespawn(obj: CardContainer, data: CardSpawnData): void {
obj.playDespawnTween(() => obj.destroy()); obj.playDespawnTween(() => obj.destroy());
this.scene.addInterruption(new Promise((r) => setTimeout(r, 40)));
} }
private getCardPosition( private getCardPosition(
@ -104,7 +107,7 @@ export class CardSpawner implements Spawner<CardSpawnData, CardContainer> {
} }
export function createCardSpawner( export function createCardSpawner(
scene: Phaser.Scene, scene: GameHostScene<CombatState>,
getState: () => CombatState, getState: () => CombatState,
onCardClick: (cardId: string) => void, onCardClick: (cardId: string) => void,
) { ) {

View File

@ -1,12 +1,14 @@
import Phaser from "phaser"; import Phaser from "phaser";
import type { import type {
CombatEntity, CombatEntity,
CombatState,
EffectTable, EffectTable,
EnemyEntity, EnemyEntity,
} from "boardgame-core/samples/slay-the-spire-like"; } from "boardgame-core/samples/slay-the-spire-like";
import { BuffData } from "./Buff"; import { BuffData } from "./Buff";
import { createSpawnUpdate } from "boardgame-phaser"; import { createSpawnUpdate } from "boardgame-phaser";
import { BuffDataWithIndex, BuffSpawner } from "./BuffSpawner"; import { BuffDataWithIndex, BuffSpawner } from "./BuffSpawner";
import { GameHostScene } from "boardgame-phaser";
export type CombatUnitData = { export type CombatUnitData = {
key: string; key: string;
@ -30,15 +32,22 @@ export class CombatUnitContainer extends Phaser.GameObjects.Container {
private hpText!: Phaser.GameObjects.Text; private hpText!: Phaser.GameObjects.Text;
private buffContainer!: Phaser.GameObjects.Container; private buffContainer!: Phaser.GameObjects.Container;
private intentText!: Phaser.GameObjects.Text | null; private intentText!: Phaser.GameObjects.Text | null;
private hostScene: GameHostScene<CombatState>;
private updateBuffs!: (buffs: Iterable<BuffData & { x: number }>) => void; private updateBuffs!: (buffs: Iterable<BuffData & { x: number }>) => void;
private currentEntity!: CombatEntity; private currentEntity!: CombatEntity;
private currentName: string; private currentName: string;
private currentIsPlayer: boolean; private currentIsPlayer: boolean;
constructor(scene: Phaser.Scene, x: number, y: number, data: CombatUnitData) { constructor(
scene: GameHostScene<CombatState>,
x: number,
y: number,
data: CombatUnitData,
) {
super(scene, x, y); super(scene, x, y);
scene.add.existing(this); scene.add.existing(this);
this.hostScene = scene;
this.currentEntity = data.entity; this.currentEntity = data.entity;
this.currentName = data.name; 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.buffContainer = this.scene.add.container(0, CONTAINER_HEIGHT / 2 - 40);
this.updateBuffs = createSpawnUpdate( this.updateBuffs = createSpawnUpdate(
new BuffSpawner(this.scene, this.buffContainer), new BuffSpawner(this.hostScene, this.buffContainer),
); );
if (!this.currentIsPlayer) { if (!this.currentIsPlayer) {

View File

@ -1,11 +1,17 @@
import Phaser from "phaser";
import type { Spawner } from "boardgame-phaser"; import type { Spawner } from "boardgame-phaser";
import { spawnEffect } from "boardgame-phaser"; import { spawnEffect } from "boardgame-phaser";
import type { CombatState, CombatEntity, EnemyEntity } from "boardgame-core/samples/slay-the-spire-like"; import type { CombatState } from "boardgame-core/samples/slay-the-spire-like";
import { CombatUnitContainer, type CombatUnitData } from "./CombatUnitContainer"; import {
CombatUnitContainer,
type CombatUnitData,
} from "./CombatUnitContainer";
import { GameHostScene } from "boardgame-phaser";
export class CombatUnitSpawner implements Spawner<CombatUnitData, CombatUnitContainer> { export class CombatUnitSpawner implements Spawner<
constructor(private scene: Phaser.Scene) {} CombatUnitData,
CombatUnitContainer
> {
constructor(private scene: GameHostScene<CombatState>) {}
*getData(): Iterable<CombatUnitData> { *getData(): Iterable<CombatUnitData> {
const combat = this.getCombatState(); const combat = this.getCombatState();
@ -70,6 +76,6 @@ export class CombatUnitSpawner implements Spawner<CombatUnitData, CombatUnitCont
} }
} }
export function createCombatUnitSpawner(scene: Phaser.Scene) { export function createCombatUnitSpawner(scene: GameHostScene<CombatState>) {
return spawnEffect(new CombatUnitSpawner(scene)); return spawnEffect(new CombatUnitSpawner(scene));
} }