Compare commits
3
Commits
c29b9a43b3
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad2c808482 | ||
|
|
f893ac9ff7 | ||
|
|
dfbdaa3499 |
@@ -1,17 +1,23 @@
|
||||
import { ReactiveScene } from "./ReactiveScene";
|
||||
|
||||
import type { GameHost } from "boardgame-core";
|
||||
import type { GameHost, GameModule } from "boardgame-core";
|
||||
|
||||
export interface GameHostSceneOptions<TState extends Record<string, unknown>> {
|
||||
gameHost: GameHost<TState>;
|
||||
export interface GameHostSceneOptions<
|
||||
TState extends Record<string, unknown>,
|
||||
TResult,
|
||||
TModule extends GameModule<TState, TResult>,
|
||||
> {
|
||||
gameHost: GameHost<TState, TResult, TModule>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export abstract class GameHostScene<
|
||||
TState extends Record<string, unknown>,
|
||||
> extends ReactiveScene<GameHostSceneOptions<TState>> {
|
||||
public get gameHost(): GameHost<TState> {
|
||||
const gameHost = this.initData.gameHost as GameHost<TState>;
|
||||
TResult,
|
||||
TModule extends GameModule<TState, TResult>,
|
||||
> extends ReactiveScene<GameHostSceneOptions<TState, TResult, TModule>> {
|
||||
public get gameHost(): GameHost<TState, TResult, TModule> {
|
||||
const gameHost = this.initData.gameHost;
|
||||
if (!gameHost) {
|
||||
throw new Error(
|
||||
`GameHostScene (${this.scene.key}): gameHost 未提供。` +
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BuffDataWithIndex, Buff> {
|
||||
constructor(
|
||||
public scene: Phaser.Scene,
|
||||
public scene: GameHostScene<CombatState>,
|
||||
public buffContainer: Phaser.GameObjects.Container,
|
||||
) {}
|
||||
getKey(t: BuffDataWithIndex) {
|
||||
@@ -13,10 +15,13 @@ export class BuffSpawner implements SpawnerCallback<BuffDataWithIndex, Buff> {
|
||||
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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<CardSpawnData, CardContainer> {
|
||||
constructor(
|
||||
private scene: Phaser.Scene,
|
||||
private scene: GameHostScene<CombatState>,
|
||||
private getState: () => CombatState,
|
||||
private onCardClick: (cardId: string) => void,
|
||||
) {}
|
||||
@@ -47,6 +48,7 @@ export class CardSpawner implements Spawner<CardSpawnData, CardContainer> {
|
||||
});
|
||||
|
||||
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<CardSpawnData, CardContainer> {
|
||||
}
|
||||
}
|
||||
|
||||
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<CardSpawnData, CardContainer> {
|
||||
}
|
||||
|
||||
export function createCardSpawner(
|
||||
scene: Phaser.Scene,
|
||||
scene: GameHostScene<CombatState>,
|
||||
getState: () => CombatState,
|
||||
onCardClick: (cardId: string) => void,
|
||||
) {
|
||||
|
||||
@@ -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<CombatState>;
|
||||
private updateBuffs!: (buffs: Iterable<BuffData & { x: number }>) => void;
|
||||
|
||||
private currentEntity!: CombatEntity;
|
||||
private currentName: string;
|
||||
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);
|
||||
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) {
|
||||
|
||||
@@ -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<CombatUnitData, CombatUnitContainer> {
|
||||
constructor(private scene: Phaser.Scene) {}
|
||||
export class CombatUnitSpawner implements Spawner<
|
||||
CombatUnitData,
|
||||
CombatUnitContainer
|
||||
> {
|
||||
constructor(private scene: GameHostScene<CombatState>) {}
|
||||
|
||||
*getData(): Iterable<CombatUnitData> {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Phaser from "phaser";
|
||||
import { GameHostScene } from "boardgame-phaser";
|
||||
import { spawnEffect, type Spawner } from "boardgame-phaser";
|
||||
import {
|
||||
CombatResult,
|
||||
type CombatState,
|
||||
prompts,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
@@ -12,12 +13,17 @@ import {
|
||||
type CombatUnitData,
|
||||
} from "@/gameobjects/CombatUnitContainer";
|
||||
import { CardSpawner } from "@/gameobjects/CardSpawner";
|
||||
import { CombatModule } from "@/state/combatState";
|
||||
|
||||
const CARD_SPACING = 160;
|
||||
const HAND_MARGIN = 100;
|
||||
const HAND_Y = 140;
|
||||
|
||||
export class CombatTestScene extends GameHostScene<CombatState> {
|
||||
export class CombatTestScene extends GameHostScene<
|
||||
CombatState,
|
||||
CombatResult | null,
|
||||
CombatModule
|
||||
> {
|
||||
private selectedCardId: string | null = null;
|
||||
private isTargeting = false;
|
||||
private targetingText!: Phaser.GameObjects.Text;
|
||||
@@ -120,6 +126,13 @@ export class CombatTestScene extends GameHostScene<CombatState> {
|
||||
|
||||
// Start the game loop
|
||||
this.gameHost.start();
|
||||
this.gameHost.gameModule.triggers.onEffectApplied.use(async (ctx, next) => {
|
||||
const { effect, entityId, stacks, targetId, cardId } = ctx;
|
||||
console.log(
|
||||
`${effect.name} x${stacks} ${entityId}/${cardId} :${targetId}`,
|
||||
);
|
||||
await next();
|
||||
});
|
||||
}
|
||||
|
||||
private onCardClick(cardId: string): void {
|
||||
@@ -150,13 +163,14 @@ export class CombatTestScene extends GameHostScene<CombatState> {
|
||||
}
|
||||
|
||||
private tryPlayCard(cardId: string, targetId?: string): void {
|
||||
const error = this.gameHost.tryAnswerPrompt(
|
||||
const error = this.gameHost.prompts.tryCommit(
|
||||
prompts.mainAction,
|
||||
"global",
|
||||
cardId,
|
||||
targetId,
|
||||
);
|
||||
if (error) {
|
||||
console.warn("Play failed:", error);
|
||||
if (!error.ok) {
|
||||
console.warn("Play failed:", error.reason);
|
||||
}
|
||||
this.clearTargeting();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
buildCombatState,
|
||||
CombatState,
|
||||
createRunState,
|
||||
createStartWith,
|
||||
createStart,
|
||||
createTriggers,
|
||||
data,
|
||||
generateDeckFromInventory,
|
||||
getAdjacentItems,
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
IRunContext,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { createInventorySignal } from "./inventory";
|
||||
import { GameModule } from "boardgame-core";
|
||||
|
||||
export function createCombatState() {
|
||||
export type CombatModule = ReturnType<typeof createCombatModule>;
|
||||
export function createCombatModule() {
|
||||
const inventory = createInventorySignal(true);
|
||||
|
||||
const deck = generateDeckFromInventory(inventory.value);
|
||||
@@ -51,12 +51,14 @@ export function createCombatState() {
|
||||
},
|
||||
};
|
||||
|
||||
const start = createStartWith((triggers, ctx) => {
|
||||
data.desert.addTriggers(triggers, runContext);
|
||||
}, runContext);
|
||||
const triggers = createTriggers(runContext);
|
||||
data.desert.addTriggers(triggers, runContext);
|
||||
const start = createStart(triggers, runContext);
|
||||
|
||||
return {
|
||||
start,
|
||||
createInitialState: () => combat,
|
||||
} as GameModule<CombatState>;
|
||||
triggers,
|
||||
runContext,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import { createGameHost, type GameModule } from "boardgame-core";
|
||||
import { GAME_CONFIG } from "@/config";
|
||||
import { CombatTestScene } from "@/scenes/CombatTestScene";
|
||||
import { GridViewerScene } from "@/scenes/GridViewerScene";
|
||||
import { IndexScene } from "@/scenes/IndexScene";
|
||||
import { InventoryTestScene } from "@/scenes/InventoryTestScene";
|
||||
import { MapViewerScene } from "@/scenes/MapViewerScene";
|
||||
import { ShapeViewerScene } from "@/scenes/ShapeViewerScene";
|
||||
import { CombatModule, createCombatModule } from "@/state/combatState";
|
||||
import { createGameHost } from "boardgame-core";
|
||||
import {
|
||||
CombatResult,
|
||||
CombatState,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { PhaserGame, PhaserScene } from "boardgame-phaser";
|
||||
import { useMemo } from "preact/hooks";
|
||||
import { IndexScene } from "@/scenes/IndexScene";
|
||||
import { MapViewerScene } from "@/scenes/MapViewerScene";
|
||||
import { GridViewerScene } from "@/scenes/GridViewerScene";
|
||||
import { ShapeViewerScene } from "@/scenes/ShapeViewerScene";
|
||||
import { GAME_CONFIG } from "@/config";
|
||||
import { InventoryTestScene } from "@/scenes/InventoryTestScene";
|
||||
import { CombatTestScene } from "@/scenes/CombatTestScene";
|
||||
import { createCombatState } from "@/state/combatState";
|
||||
import type { CombatState } from "boardgame-core/samples/slay-the-spire-like";
|
||||
|
||||
export default function App() {
|
||||
const gameHost = useMemo(
|
||||
() => createGameHost(createCombatState() as GameModule<CombatState>),
|
||||
() =>
|
||||
createGameHost<CombatState, CombatResult | null, CombatModule>(
|
||||
createCombatModule(),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user