Compare commits
3
Commits
c29b9a43b3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad2c808482 | ||
|
|
f893ac9ff7 | ||
|
|
dfbdaa3499 |
@@ -1,17 +1,23 @@
|
|||||||
import { ReactiveScene } from "./ReactiveScene";
|
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>> {
|
export interface GameHostSceneOptions<
|
||||||
gameHost: GameHost<TState>;
|
TState extends Record<string, unknown>,
|
||||||
|
TResult,
|
||||||
|
TModule extends GameModule<TState, TResult>,
|
||||||
|
> {
|
||||||
|
gameHost: GameHost<TState, TResult, TModule>;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class GameHostScene<
|
export abstract class GameHostScene<
|
||||||
TState extends Record<string, unknown>,
|
TState extends Record<string, unknown>,
|
||||||
> extends ReactiveScene<GameHostSceneOptions<TState>> {
|
TResult,
|
||||||
public get gameHost(): GameHost<TState> {
|
TModule extends GameModule<TState, TResult>,
|
||||||
const gameHost = this.initData.gameHost as GameHost<TState>;
|
> extends ReactiveScene<GameHostSceneOptions<TState, TResult, TModule>> {
|
||||||
|
public get gameHost(): GameHost<TState, TResult, TModule> {
|
||||||
|
const gameHost = this.initData.gameHost;
|
||||||
if (!gameHost) {
|
if (!gameHost) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`GameHostScene (${this.scene.key}): gameHost 未提供。` +
|
`GameHostScene (${this.scene.key}): gameHost 未提供。` +
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Phaser from "phaser";
|
|||||||
import { GameHostScene } from "boardgame-phaser";
|
import { GameHostScene } from "boardgame-phaser";
|
||||||
import { spawnEffect, type Spawner } from "boardgame-phaser";
|
import { spawnEffect, type Spawner } from "boardgame-phaser";
|
||||||
import {
|
import {
|
||||||
|
CombatResult,
|
||||||
type CombatState,
|
type CombatState,
|
||||||
prompts,
|
prompts,
|
||||||
} from "boardgame-core/samples/slay-the-spire-like";
|
} from "boardgame-core/samples/slay-the-spire-like";
|
||||||
@@ -12,12 +13,17 @@ import {
|
|||||||
type CombatUnitData,
|
type CombatUnitData,
|
||||||
} from "@/gameobjects/CombatUnitContainer";
|
} from "@/gameobjects/CombatUnitContainer";
|
||||||
import { CardSpawner } from "@/gameobjects/CardSpawner";
|
import { CardSpawner } from "@/gameobjects/CardSpawner";
|
||||||
|
import { CombatModule } from "@/state/combatState";
|
||||||
|
|
||||||
const CARD_SPACING = 160;
|
const CARD_SPACING = 160;
|
||||||
const HAND_MARGIN = 100;
|
const HAND_MARGIN = 100;
|
||||||
const HAND_Y = 140;
|
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 selectedCardId: string | null = null;
|
||||||
private isTargeting = false;
|
private isTargeting = false;
|
||||||
private targetingText!: Phaser.GameObjects.Text;
|
private targetingText!: Phaser.GameObjects.Text;
|
||||||
@@ -120,6 +126,13 @@ export class CombatTestScene extends GameHostScene<CombatState> {
|
|||||||
|
|
||||||
// Start the game loop
|
// Start the game loop
|
||||||
this.gameHost.start();
|
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 {
|
private onCardClick(cardId: string): void {
|
||||||
@@ -150,13 +163,14 @@ export class CombatTestScene extends GameHostScene<CombatState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private tryPlayCard(cardId: string, targetId?: string): void {
|
private tryPlayCard(cardId: string, targetId?: string): void {
|
||||||
const error = this.gameHost.tryAnswerPrompt(
|
const error = this.gameHost.prompts.tryCommit(
|
||||||
prompts.mainAction,
|
prompts.mainAction,
|
||||||
|
"global",
|
||||||
cardId,
|
cardId,
|
||||||
targetId,
|
targetId,
|
||||||
);
|
);
|
||||||
if (error) {
|
if (!error.ok) {
|
||||||
console.warn("Play failed:", error);
|
console.warn("Play failed:", error.reason);
|
||||||
}
|
}
|
||||||
this.clearTargeting();
|
this.clearTargeting();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
buildCombatState,
|
buildCombatState,
|
||||||
CombatState,
|
|
||||||
createRunState,
|
createRunState,
|
||||||
createStartWith,
|
createStart,
|
||||||
|
createTriggers,
|
||||||
data,
|
data,
|
||||||
generateDeckFromInventory,
|
generateDeckFromInventory,
|
||||||
getAdjacentItems,
|
getAdjacentItems,
|
||||||
@@ -10,9 +10,9 @@ import {
|
|||||||
IRunContext,
|
IRunContext,
|
||||||
} from "boardgame-core/samples/slay-the-spire-like";
|
} from "boardgame-core/samples/slay-the-spire-like";
|
||||||
import { createInventorySignal } from "./inventory";
|
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 inventory = createInventorySignal(true);
|
||||||
|
|
||||||
const deck = generateDeckFromInventory(inventory.value);
|
const deck = generateDeckFromInventory(inventory.value);
|
||||||
@@ -51,12 +51,14 @@ export function createCombatState() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const start = createStartWith((triggers, ctx) => {
|
const triggers = createTriggers(runContext);
|
||||||
data.desert.addTriggers(triggers, runContext);
|
data.desert.addTriggers(triggers, runContext);
|
||||||
}, runContext);
|
const start = createStart(triggers, runContext);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
createInitialState: () => combat,
|
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 { PhaserGame, PhaserScene } from "boardgame-phaser";
|
||||||
import { useMemo } from "preact/hooks";
|
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() {
|
export default function App() {
|
||||||
const gameHost = useMemo(
|
const gameHost = useMemo(
|
||||||
() => createGameHost(createCombatState() as GameModule<CombatState>),
|
() =>
|
||||||
|
createGameHost<CombatState, CombatResult | null, CombatModule>(
|
||||||
|
createCombatModule(),
|
||||||
|
),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user