Compare commits
33
Commits
368d9942d2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad2c808482 | ||
|
|
f893ac9ff7 | ||
|
|
dfbdaa3499 | ||
|
|
c29b9a43b3 | ||
|
|
447a5cfbd0 | ||
|
|
dda290bf9c | ||
|
|
5d84c42b78 | ||
|
|
a6ddafb802 | ||
|
|
a181932fb3 | ||
|
|
887da22baa | ||
|
|
5355afb23e | ||
|
|
3f2ba84a06 | ||
|
|
9637312b7c | ||
|
|
82c026628a | ||
|
|
a92b889dfa | ||
|
|
c97c2c4150 | ||
|
|
422ddde200 | ||
|
|
093eadbefd | ||
|
|
eff62bc0ed | ||
|
|
fe0621cedf | ||
|
|
30e2d9ac36 | ||
|
|
c9f573ef86 | ||
|
|
cc796d1e11 | ||
|
|
fd347e7a62 | ||
|
|
19dfafeb30 | ||
|
|
b2862e34d9 | ||
|
|
fd41db2a89 | ||
|
|
f03e6c5a7b | ||
|
|
7039938a72 | ||
|
|
45844ea800 | ||
|
|
29b516c371 | ||
|
|
5884e74d8d | ||
|
|
b06876ccc0 |
@@ -5,10 +5,11 @@ export type { IDisposable, DisposableItem } from "./utils";
|
||||
// Drag & drop utilities
|
||||
export { dragDropEventEffect, DragDropEventType } from "./utils";
|
||||
export type { DragDropEvent, DragDropCallback } from "./utils";
|
||||
export { hoverEffect } from "./utils";
|
||||
|
||||
// Data-driven object spawning
|
||||
export { spawnEffect } from "./spawner";
|
||||
export type { Spawner } from "./spawner";
|
||||
export { spawnEffect, createSpawnUpdate } from "./spawner";
|
||||
export type { Spawner, SpawnerCallback } from "./spawner";
|
||||
|
||||
// Scene base classes
|
||||
export {
|
||||
|
||||
@@ -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 未提供。` +
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { effect, type ReadonlySignal } from "@preact/signals-core";
|
||||
import { type ReadonlySignal } from "@preact/signals-core";
|
||||
import { Scene } from "phaser";
|
||||
|
||||
import { DisposableBag, type IDisposable } from "../utils";
|
||||
@@ -85,6 +85,6 @@ export abstract class ReactiveScene<TData = object>
|
||||
|
||||
/** 注册响应式监听(场景关闭时自动清理) */
|
||||
public addEffect(fn: () => CleanupFn): void {
|
||||
this.disposables.add(effect(fn));
|
||||
this.disposables.addEffect(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import type Phaser from "phaser";
|
||||
|
||||
type GO = Phaser.GameObjects.GameObject;
|
||||
|
||||
export interface Spawner<TData, TObject extends GO = GO> {
|
||||
/** 数据源迭代器 */
|
||||
getData(): Iterable<TData>;
|
||||
export interface SpawnerCallback<TData, TObject extends GO = GO> {
|
||||
/** 获取数据的唯一键 */
|
||||
getKey(t: TData): string;
|
||||
/** 创建新对象 */
|
||||
@@ -17,15 +15,33 @@ export interface Spawner<TData, TObject extends GO = GO> {
|
||||
onUpdate(t: TData, obj: TObject): void;
|
||||
}
|
||||
|
||||
export interface Spawner<
|
||||
TData,
|
||||
TObject extends GO = GO,
|
||||
> extends SpawnerCallback<TData, TObject> {
|
||||
/** 数据源迭代器 */
|
||||
getData(): Iterable<TData>;
|
||||
}
|
||||
|
||||
export function spawnEffect<TData, TObject extends GO = GO>(
|
||||
spawner: Spawner<TData, TObject>,
|
||||
): () => void {
|
||||
const entries = new Map<string, { object: TObject; data: TData }>();
|
||||
) {
|
||||
const update = createSpawnUpdate(spawner);
|
||||
|
||||
return effect(() => {
|
||||
update(spawner.getData());
|
||||
});
|
||||
}
|
||||
|
||||
export function createSpawnUpdate<TData, TObject extends GO = GO>(
|
||||
spawner: SpawnerCallback<TData, TObject>,
|
||||
) {
|
||||
const entries = new Map<string, { object: TObject; data: TData }>();
|
||||
|
||||
function update(data: Iterable<TData>) {
|
||||
const current = new Set<string>();
|
||||
|
||||
for (const t of spawner.getData()) {
|
||||
for (const t of data) {
|
||||
const key = spawner.getKey(t);
|
||||
current.add(key);
|
||||
|
||||
@@ -48,5 +64,7 @@ export function spawnEffect<TData, TObject extends GO = GO>(
|
||||
entries.delete(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return update;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { signal, useSignal, useSignalEffect } from "@preact/signals";
|
||||
import Phaser, { AUTO } from "phaser";
|
||||
import Phaser, { AUTO, Scene } from "phaser";
|
||||
import { createContext } from "preact";
|
||||
import { useContext, useEffect, useRef } from "preact/hooks";
|
||||
|
||||
@@ -37,6 +37,7 @@ export const defaultPhaserConfig: Phaser.Types.Core.GameConfig = {
|
||||
parent: "phaser-container",
|
||||
backgroundColor: "#f9fafb",
|
||||
scene: [],
|
||||
disableContextMenu: true,
|
||||
};
|
||||
|
||||
export interface PhaserGameProps {
|
||||
@@ -214,7 +215,8 @@ export function PhaserScene<TData = {}>(props: PhaserSceneProps<TData>) {
|
||||
const game = ctx.game;
|
||||
|
||||
// 注册场景到 Phaser(但不启动)
|
||||
const scene = "scene" in props.scene ? props.scene : new props.scene();
|
||||
const scene =
|
||||
props.scene instanceof Scene ? props.scene : new props.scene();
|
||||
const sceneKey = props.sceneKey ?? scene.sys.settings.key;
|
||||
if (!game.scene.getScene(sceneKey)) {
|
||||
const initData = {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { effect } from "@preact/signals-core";
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
@@ -8,6 +10,10 @@ export class DisposableBag implements IDisposable {
|
||||
private _disposables = new Set<DisposableItem>();
|
||||
private _isDisposed = false;
|
||||
|
||||
constructor(go?: Phaser.GameObjects.GameObject) {
|
||||
if (go) go.on("shutdown", () => this.dispose());
|
||||
}
|
||||
|
||||
get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
@@ -20,6 +26,10 @@ export class DisposableBag implements IDisposable {
|
||||
this._disposables.add(item);
|
||||
}
|
||||
|
||||
addEffect(fn: () => void) {
|
||||
this.add(effect(fn));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._isDisposed) return;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
type PointerRecord = {
|
||||
id: number;
|
||||
button: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
@@ -10,6 +11,7 @@ export enum DragDropEventType {
|
||||
DOWN,
|
||||
UP,
|
||||
MOVE,
|
||||
ALTBUTTON,
|
||||
}
|
||||
|
||||
export type DragDropEvent = {
|
||||
@@ -29,9 +31,22 @@ export function dragDropEventEffect(
|
||||
let down: PointerRecord | null = null;
|
||||
|
||||
function onPointerDown(pointer: Phaser.Input.Pointer) {
|
||||
if (isDragging) return;
|
||||
if (down !== null) {
|
||||
if (pointer.button === down.button) return;
|
||||
callback({
|
||||
type: DragDropEventType.ALTBUTTON,
|
||||
deltaX: pointer.x - down.x,
|
||||
deltaY: pointer.y - down.y,
|
||||
});
|
||||
return;
|
||||
}
|
||||
isDragging = true;
|
||||
down = { id: pointer.id, x: pointer.x, y: pointer.y };
|
||||
down = {
|
||||
id: pointer.id,
|
||||
button: pointer.button,
|
||||
x: pointer.x,
|
||||
y: pointer.y,
|
||||
};
|
||||
|
||||
const event: DragDropEvent = {
|
||||
type: DragDropEventType.DOWN,
|
||||
@@ -42,12 +57,23 @@ export function dragDropEventEffect(
|
||||
}
|
||||
|
||||
function onPointerUp(pointer: Phaser.Input.Pointer) {
|
||||
if (!isDragging || !down || pointer.id !== down.id) return;
|
||||
if (
|
||||
!isDragging ||
|
||||
!down ||
|
||||
pointer.id !== down.id ||
|
||||
pointer.button !== down.button
|
||||
)
|
||||
return;
|
||||
|
||||
isDragging = false;
|
||||
const deltaX = pointer.x - down.x;
|
||||
const deltaY = pointer.y - down.y;
|
||||
const event: DragDropEvent = { type: DragDropEventType.UP, deltaX, deltaY };
|
||||
|
||||
isDragging = false;
|
||||
const event: DragDropEvent = {
|
||||
type: DragDropEventType.UP,
|
||||
deltaX,
|
||||
deltaY,
|
||||
};
|
||||
callback(event);
|
||||
down = null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
type HoverCallback = (hovering: boolean) => void;
|
||||
export function hoverEffect(
|
||||
gameObject: Phaser.GameObjects.GameObject,
|
||||
callback: HoverCallback,
|
||||
) {
|
||||
let isHovering = false;
|
||||
const onPointerOver = () => {
|
||||
if (isHovering) return;
|
||||
isHovering = true;
|
||||
callback(true);
|
||||
};
|
||||
const onPointerOut = () => {
|
||||
if (!isHovering) return;
|
||||
isHovering = false;
|
||||
callback(false);
|
||||
};
|
||||
|
||||
gameObject.on("pointerover", onPointerOver);
|
||||
gameObject.on("pointerout", onPointerOut);
|
||||
const cleanup = () => {
|
||||
gameObject.off("pointerover", onPointerOver);
|
||||
gameObject.off("pointerout", onPointerOut);
|
||||
gameObject.off("destroy", cleanup);
|
||||
};
|
||||
|
||||
gameObject.once("destroy", cleanup);
|
||||
return cleanup;
|
||||
}
|
||||
@@ -6,3 +6,4 @@ export {
|
||||
type DragDropEvent,
|
||||
type DragDropCallback,
|
||||
} from "./dnd";
|
||||
export * from "./hover";
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@preact/preset-vite": "^2.8.1",
|
||||
"@preact/signals": "^2.9.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.0"
|
||||
|
||||
@@ -33,6 +33,29 @@ export const GRID_CONFIG = {
|
||||
WIDGET_CELL_SIZE: 80,
|
||||
/** Gap between grid cells (pixels) */
|
||||
GRID_GAP: 2,
|
||||
/** Empty cell background color */
|
||||
CELL_EMPTY_COLOR: 0x222233,
|
||||
/** Occupied cell background color */
|
||||
CELL_OCCUPIED_COLOR: 0x334455,
|
||||
/** Grid line color */
|
||||
GRID_LINE_COLOR: 0x555577,
|
||||
/** Title text style */
|
||||
TITLE_STYLE: {
|
||||
fontSize: "24px",
|
||||
color: "#888",
|
||||
fontStyle: "bold",
|
||||
} as const,
|
||||
/** Subtitle/hint text style */
|
||||
SUBTITLE_STYLE: {
|
||||
fontSize: "14px",
|
||||
color: "#aaaaaa",
|
||||
} as const,
|
||||
/** Item name text style */
|
||||
ITEM_NAME_STYLE: {
|
||||
fontSize: "11px",
|
||||
color: "#888",
|
||||
fontStyle: "bold",
|
||||
} as const,
|
||||
} as const;
|
||||
|
||||
// ── Shape Viewer ────────────────────────────────────────────────────────────
|
||||
@@ -133,6 +156,12 @@ export const NEGATIVE_EFFECTS = new Set([
|
||||
"poison",
|
||||
]);
|
||||
|
||||
export const CARD_CONFIG = {
|
||||
WIDTH: 140,
|
||||
HEIGHT: 200,
|
||||
CORNER_RADIUS: 8,
|
||||
} as const;
|
||||
|
||||
export const GAME_CONFIG: Phaser.Types.Core.GameConfig = {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import Phaser from "phaser";
|
||||
|
||||
export type BuffData = {
|
||||
id: string;
|
||||
stacks: number;
|
||||
emoji: string;
|
||||
isPositive: boolean;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
const ICON_SIZE = 28;
|
||||
const ICON_COLOR_POSITIVE = 0x44aa44;
|
||||
const ICON_COLOR_NEGATIVE = 0xaa4444;
|
||||
const TOOLTIP_WIDTH = 200;
|
||||
const TOOLTIP_PADDING = 8;
|
||||
|
||||
export class Buff extends Phaser.GameObjects.Container {
|
||||
private bg!: Phaser.GameObjects.Rectangle;
|
||||
private label!: Phaser.GameObjects.Text;
|
||||
private tooltipContainer!: Phaser.GameObjects.Container;
|
||||
private tooltipBg!: Phaser.GameObjects.Rectangle;
|
||||
private tooltipText!: Phaser.GameObjects.Text;
|
||||
|
||||
private currentData!: BuffData;
|
||||
|
||||
constructor(scene: Phaser.Scene, x: number, y: number, data: BuffData) {
|
||||
super(scene, x, y);
|
||||
scene.add.existing(this);
|
||||
|
||||
this.currentData = data;
|
||||
this.createVisuals();
|
||||
this.createTooltip();
|
||||
this.setupInteraction();
|
||||
this.updateFromData(data);
|
||||
}
|
||||
|
||||
private createVisuals(): void {
|
||||
const color = this.currentData.isPositive
|
||||
? ICON_COLOR_POSITIVE
|
||||
: ICON_COLOR_NEGATIVE;
|
||||
|
||||
this.bg = this.scene.add
|
||||
.rectangle(0, 0, ICON_SIZE, ICON_SIZE, color)
|
||||
.setStrokeStyle(1, 0xffffff);
|
||||
|
||||
this.label = this.scene.add
|
||||
.text(0, 0, "", {
|
||||
fontSize: "12px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.add([this.bg, this.label]);
|
||||
}
|
||||
|
||||
private createTooltip(): void {
|
||||
this.tooltipContainer = this.scene.add.container(0, 0).setVisible(false);
|
||||
|
||||
this.tooltipBg = this.scene.add
|
||||
.rectangle(0, 0, 0, 0, 0x000000, 0.9)
|
||||
.setStrokeStyle(1, 0xffffff);
|
||||
|
||||
this.tooltipText = this.scene.add
|
||||
.text(0, 0, "", {
|
||||
fontSize: "12px",
|
||||
color: "#ffffff",
|
||||
align: "center",
|
||||
wordWrap: { width: TOOLTIP_WIDTH - TOOLTIP_PADDING * 2 },
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.tooltipContainer.add([this.tooltipBg, this.tooltipText]);
|
||||
this.tooltipContainer.setDepth(1000);
|
||||
}
|
||||
|
||||
private setupInteraction(): void {
|
||||
this.setInteractive(
|
||||
new Phaser.Geom.Rectangle(
|
||||
-ICON_SIZE / 2,
|
||||
-ICON_SIZE / 2,
|
||||
ICON_SIZE,
|
||||
ICON_SIZE,
|
||||
),
|
||||
Phaser.Geom.Rectangle.Contains,
|
||||
);
|
||||
|
||||
this.on("pointerover", this.onPointerOver, this);
|
||||
this.on("pointerout", this.onPointerOut, this);
|
||||
}
|
||||
|
||||
private onPointerOver(): void {
|
||||
const desc = this.currentData.description ?? "";
|
||||
const content = desc;
|
||||
|
||||
this.tooltipText.setText(content);
|
||||
|
||||
const bounds = this.tooltipText.getBounds();
|
||||
const width = Math.min(bounds.width + TOOLTIP_PADDING * 2, TOOLTIP_WIDTH);
|
||||
const height = bounds.height + TOOLTIP_PADDING * 2;
|
||||
|
||||
this.tooltipBg.setDisplaySize(width, height);
|
||||
|
||||
const worldPos = this.getBounds();
|
||||
this.tooltipContainer.setPosition(
|
||||
worldPos.x + worldPos.width / 2,
|
||||
worldPos.y - height / 2 - 4,
|
||||
);
|
||||
this.tooltipContainer.setDepth(1000);
|
||||
this.scene.children.bringToTop(this.tooltipContainer);
|
||||
this.tooltipContainer.setVisible(true);
|
||||
}
|
||||
|
||||
private onPointerOut(): void {
|
||||
this.tooltipContainer.setVisible(false);
|
||||
}
|
||||
|
||||
updateFromData(data: BuffData): void {
|
||||
this.currentData = data;
|
||||
this.label.setText(`${data.emoji}${data.stacks}`);
|
||||
|
||||
const color = data.isPositive ? ICON_COLOR_POSITIVE : ICON_COLOR_NEGATIVE;
|
||||
this.bg.setFillStyle(color);
|
||||
}
|
||||
|
||||
destroy(fromScene?: boolean): void {
|
||||
this.off("pointerover", this.onPointerOver, this);
|
||||
this.off("pointerout", this.onPointerOut, this);
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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: GameHostScene<CombatState>,
|
||||
public buffContainer: Phaser.GameObjects.Container,
|
||||
) {}
|
||||
getKey(t: BuffDataWithIndex) {
|
||||
return t.id;
|
||||
}
|
||||
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) {
|
||||
this.scene.addInterruption(new Promise((r) => setTimeout(r, 50)));
|
||||
obj.playDespawnTween();
|
||||
}
|
||||
onUpdate(t: BuffDataWithIndex, obj: Buff) {
|
||||
obj.setPosition(t.x, 0);
|
||||
obj.updateFromData(t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import Phaser, { Tweens } from "phaser";
|
||||
import type { GameCard } from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { CARD_CONFIG } from "@/config";
|
||||
import { hoverEffect } from "boardgame-phaser";
|
||||
|
||||
export interface CardContainerOptions {
|
||||
card: GameCard;
|
||||
onClick?: (cardId: string) => void;
|
||||
playable?: boolean;
|
||||
}
|
||||
|
||||
const CARD_WIDTH = CARD_CONFIG.WIDTH;
|
||||
const CARD_HEIGHT = CARD_CONFIG.HEIGHT;
|
||||
const CORNER_RADIUS = CARD_CONFIG.CORNER_RADIUS;
|
||||
|
||||
export class CardContainer extends Phaser.GameObjects.Container {
|
||||
private bg!: Phaser.GameObjects.Rectangle;
|
||||
private nameText!: Phaser.GameObjects.Text;
|
||||
private costBg!: Phaser.GameObjects.Container;
|
||||
private costText!: Phaser.GameObjects.Text;
|
||||
private descText!: Phaser.GameObjects.Text;
|
||||
private highlight!: Phaser.GameObjects.Rectangle | null;
|
||||
private cardId: string;
|
||||
private _selected = false;
|
||||
|
||||
constructor(
|
||||
scene: Phaser.Scene,
|
||||
x: number,
|
||||
y: number,
|
||||
options: CardContainerOptions,
|
||||
) {
|
||||
super(scene, x, y);
|
||||
scene.add.existing(this);
|
||||
|
||||
this.cardId = options.card.id;
|
||||
this.createVisuals(options);
|
||||
this.setupInteractive(options);
|
||||
}
|
||||
|
||||
private createVisuals(options: CardContainerOptions): void {
|
||||
const cardData = options.card.cardData;
|
||||
const isPlayable = options.playable ?? true;
|
||||
|
||||
// Background
|
||||
const bgColor = isPlayable ? 0x2a2a3a : 0x1a1a24;
|
||||
this.bg = this.scene.add
|
||||
.rectangle(0, 0, CARD_WIDTH, CARD_HEIGHT, bgColor)
|
||||
.setStrokeStyle(2, isPlayable ? 0x555577 : 0x333344);
|
||||
this.add(this.bg);
|
||||
|
||||
// Name
|
||||
this.nameText = this.scene.add
|
||||
.text(0, -CARD_HEIGHT / 2 + 24, cardData.name, {
|
||||
fontSize: "13px",
|
||||
color: isPlayable ? "#ffffff" : "#666666",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
this.add(this.nameText);
|
||||
|
||||
// Cost badge (top-right)
|
||||
this.costBg = this.scene.add.container(
|
||||
CARD_WIDTH / 2 - 18,
|
||||
-CARD_HEIGHT / 2 + 18,
|
||||
);
|
||||
const costCircle = this.scene.add.circle(0, 0, 14, 0x3b82f6);
|
||||
this.costText = this.scene.add
|
||||
.text(0, 0, `${cardData.costCount}`, {
|
||||
fontSize: "14px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
this.costBg.add([costCircle, this.costText]);
|
||||
this.add(this.costBg);
|
||||
|
||||
// Description
|
||||
this.descText = this.scene.add
|
||||
.text(0, 16, cardData.desc, {
|
||||
fontSize: "10px",
|
||||
color: isPlayable ? "#aaaaaa" : "#444444",
|
||||
align: "center",
|
||||
wordWrap: { width: CARD_WIDTH - 16 },
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
this.add(this.descText);
|
||||
|
||||
// Target indicator
|
||||
const targetLabel = (() => {
|
||||
switch (cardData.targetType) {
|
||||
case "enemy":
|
||||
return "⚔️ Enemy";
|
||||
case "enemies":
|
||||
return "⚔️ Enemies";
|
||||
case "player":
|
||||
return "🛡️ Player";
|
||||
default:
|
||||
return "❓ Unknown";
|
||||
}
|
||||
})();
|
||||
const targetText = this.scene.add
|
||||
.text(0, CARD_HEIGHT / 2 - 20, targetLabel, {
|
||||
fontSize: "10px",
|
||||
color: isPlayable ? "#8888cc" : "#444466",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
this.add(targetText);
|
||||
}
|
||||
|
||||
private setupInteractive(options: CardContainerOptions): void {
|
||||
const hitArea = new Phaser.Geom.Rectangle(
|
||||
-CARD_WIDTH / 2,
|
||||
-CARD_HEIGHT / 2,
|
||||
CARD_WIDTH,
|
||||
CARD_HEIGHT,
|
||||
);
|
||||
this.setInteractive(hitArea, Phaser.Geom.Rectangle.Contains);
|
||||
if (this.input) {
|
||||
this.input.cursor = options.playable ? "pointer" : "not-allowed";
|
||||
}
|
||||
|
||||
hoverEffect(this, (hovering) => {
|
||||
if (!options.playable) return;
|
||||
this.playHoverTween(hovering);
|
||||
});
|
||||
|
||||
this.on("pointerdown", () => {
|
||||
if (options.playable && options.onClick) {
|
||||
options.onClick(this.cardId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setSelected(selected: boolean): void {
|
||||
this._selected = selected;
|
||||
if (selected) {
|
||||
if (!this.highlight) {
|
||||
this.highlight = this.scene.add
|
||||
.rectangle(0, 0, CARD_WIDTH + 10, CARD_HEIGHT + 10, 0x000000, 0)
|
||||
.setStrokeStyle(4, 0xfbbf24)
|
||||
.setDepth(-1);
|
||||
this.addAt(this.highlight, 0);
|
||||
}
|
||||
this.highlight.setAlpha(1);
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
scale: 1.05,
|
||||
duration: 100,
|
||||
ease: "Power2",
|
||||
});
|
||||
} else {
|
||||
if (this.highlight) {
|
||||
this.highlight.setAlpha(0);
|
||||
}
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
scale: 1,
|
||||
duration: 100,
|
||||
ease: "Power2",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get selected(): boolean {
|
||||
return this._selected;
|
||||
}
|
||||
|
||||
playSpawnTween(delay = 0) {
|
||||
this.setAlpha(0);
|
||||
this.setScale(0.5);
|
||||
return this.scene.tweens.add({
|
||||
targets: this,
|
||||
alpha: 1,
|
||||
scale: 1,
|
||||
duration: 300,
|
||||
delay,
|
||||
ease: "Back.easeOut",
|
||||
});
|
||||
}
|
||||
|
||||
playDespawnTween(onComplete?: () => void) {
|
||||
return this.scene.tweens.add({
|
||||
targets: this,
|
||||
alpha: 0,
|
||||
scale: 0.5,
|
||||
y: this.y - 50,
|
||||
duration: 200,
|
||||
ease: "Back.easeIn",
|
||||
onComplete: () => {
|
||||
if (onComplete) onComplete();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private playHoverTween(isHovering: boolean) {
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
y: isHovering ? this.y - 10 : this.y + 10,
|
||||
scale: isHovering ? 1.08 : 1,
|
||||
duration: 150,
|
||||
ease: "Power2",
|
||||
});
|
||||
this.bg.setStrokeStyle(
|
||||
isHovering ? 3 : 2,
|
||||
isHovering ? 0x88aaff : 0x555577,
|
||||
);
|
||||
}
|
||||
|
||||
destroy(fromScene?: boolean): void {
|
||||
this.highlight = null;
|
||||
super.destroy(fromScene);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { spawnEffect, type Spawner } from "boardgame-phaser";
|
||||
import type {
|
||||
CombatState,
|
||||
GameCard,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { CardContainer } from "./CardContainer";
|
||||
import { GameHostScene } from "boardgame-phaser";
|
||||
|
||||
export interface CardSpawnData {
|
||||
cardId: string;
|
||||
index: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
const HAND_Y = 140;
|
||||
const CARD_SPACING = 160;
|
||||
const HAND_MARGIN = 100;
|
||||
|
||||
export class CardSpawner implements Spawner<CardSpawnData, CardContainer> {
|
||||
constructor(
|
||||
private scene: GameHostScene<CombatState>,
|
||||
private getState: () => CombatState,
|
||||
private onCardClick: (cardId: string) => void,
|
||||
) {}
|
||||
|
||||
*getData(): Iterable<CardSpawnData> {
|
||||
const state = this.getState();
|
||||
const handIds = state.player.deck.regions.hand.childIds;
|
||||
const total = handIds.length;
|
||||
for (let i = 0; i < total; i++) {
|
||||
yield { cardId: handIds[i], index: i, total };
|
||||
}
|
||||
}
|
||||
|
||||
getKey(data: CardSpawnData): string {
|
||||
return data.cardId;
|
||||
}
|
||||
|
||||
onSpawn(data: CardSpawnData): CardContainer {
|
||||
const state = this.getState();
|
||||
const card = state.player.deck.cards[data.cardId];
|
||||
const { x, y } = this.getCardPosition(data.index, data.total);
|
||||
|
||||
const container = new CardContainer(this.scene, x, y, {
|
||||
card: card ?? this.createFallbackCard(data.cardId),
|
||||
onClick: this.onCardClick,
|
||||
playable: true,
|
||||
});
|
||||
|
||||
container.playSpawnTween(data.index * 40);
|
||||
this.scene.addInterruption(new Promise((r) => setTimeout(r, 40)));
|
||||
return container;
|
||||
}
|
||||
|
||||
onUpdate(data: CardSpawnData, obj: CardContainer): void {
|
||||
const { x, y } = this.getCardPosition(data.index, data.total);
|
||||
if (obj.x !== x || obj.y !== y) {
|
||||
this.scene.tweens.add({
|
||||
targets: obj,
|
||||
x,
|
||||
y,
|
||||
duration: 250,
|
||||
ease: "Power2",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onDespawn(obj: CardContainer, data: CardSpawnData): void {
|
||||
obj.playDespawnTween(() => obj.destroy());
|
||||
this.scene.addInterruption(new Promise((r) => setTimeout(r, 40)));
|
||||
}
|
||||
|
||||
private getCardPosition(
|
||||
index: number,
|
||||
total: number,
|
||||
): { x: number; y: number } {
|
||||
const { width, height } = this.scene.scale;
|
||||
const spacing = Math.min(
|
||||
CARD_SPACING,
|
||||
(width - HAND_MARGIN * 2) / Math.max(1, total),
|
||||
);
|
||||
const startX = width / 2 - ((total - 1) * spacing) / 2;
|
||||
return {
|
||||
x: startX + index * spacing,
|
||||
y: height - HAND_Y,
|
||||
};
|
||||
}
|
||||
|
||||
private createFallbackCard(id: string): GameCard {
|
||||
return {
|
||||
id,
|
||||
regionId: "",
|
||||
position: [],
|
||||
cardData: {
|
||||
id,
|
||||
name: "Unknown",
|
||||
desc: "Card data missing",
|
||||
type: "item",
|
||||
costType: "none",
|
||||
costCount: 0,
|
||||
targetType: "player",
|
||||
effects: [],
|
||||
},
|
||||
itemId: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function createCardSpawner(
|
||||
scene: GameHostScene<CombatState>,
|
||||
getState: () => CombatState,
|
||||
onCardClick: (cardId: string) => void,
|
||||
) {
|
||||
return spawnEffect(new CardSpawner(scene, getState, onCardClick));
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
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;
|
||||
entity: CombatEntity;
|
||||
name: string;
|
||||
isPlayer: boolean;
|
||||
};
|
||||
|
||||
const CONTAINER_WIDTH = 200;
|
||||
const CONTAINER_HEIGHT = 260;
|
||||
const HP_BAR_WIDTH = 160;
|
||||
const HP_BAR_HEIGHT = 16;
|
||||
const BUFF_ICON_SIZE = 28;
|
||||
const BUFF_ICON_GAP = 6;
|
||||
|
||||
export class CombatUnitContainer extends Phaser.GameObjects.Container {
|
||||
private bg!: Phaser.GameObjects.Rectangle;
|
||||
private nameText!: Phaser.GameObjects.Text;
|
||||
private hpBarBg!: Phaser.GameObjects.Rectangle;
|
||||
private hpBarFill!: Phaser.GameObjects.Rectangle;
|
||||
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: 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;
|
||||
this.currentIsPlayer = data.isPlayer;
|
||||
|
||||
this.createVisuals();
|
||||
this.updateFromData(data);
|
||||
}
|
||||
|
||||
private createVisuals(): void {
|
||||
const bgColor = this.currentIsPlayer ? 0x224466 : 0x662222;
|
||||
const borderColor = this.currentIsPlayer ? 0x44aaff : 0xff4444;
|
||||
|
||||
this.bg = this.scene.add
|
||||
.rectangle(0, 0, CONTAINER_WIDTH, CONTAINER_HEIGHT, bgColor)
|
||||
.setStrokeStyle(3, borderColor);
|
||||
|
||||
this.nameText = this.scene.add
|
||||
.text(0, -CONTAINER_HEIGHT / 2 + 24, this.currentName, {
|
||||
fontSize: "18px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.hpBarBg = this.scene.add.rectangle(
|
||||
0,
|
||||
-CONTAINER_HEIGHT / 2 + 60,
|
||||
HP_BAR_WIDTH,
|
||||
HP_BAR_HEIGHT,
|
||||
0x333333,
|
||||
);
|
||||
|
||||
this.hpBarFill = this.scene.add
|
||||
.rectangle(
|
||||
-HP_BAR_WIDTH / 2,
|
||||
-CONTAINER_HEIGHT / 2 + 60,
|
||||
HP_BAR_WIDTH,
|
||||
HP_BAR_HEIGHT,
|
||||
0x22c55e,
|
||||
)
|
||||
.setOrigin(0, 0.5);
|
||||
|
||||
this.hpText = this.scene.add
|
||||
.text(0, -CONTAINER_HEIGHT / 2 + 60, "", {
|
||||
fontSize: "12px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.buffContainer = this.scene.add.container(0, CONTAINER_HEIGHT / 2 - 40);
|
||||
this.updateBuffs = createSpawnUpdate(
|
||||
new BuffSpawner(this.hostScene, this.buffContainer),
|
||||
);
|
||||
|
||||
if (!this.currentIsPlayer) {
|
||||
this.intentText = this.scene.add
|
||||
.text(0, -CONTAINER_HEIGHT / 2 + 86, "", {
|
||||
fontSize: "12px",
|
||||
color: "#ffcc88",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
} else {
|
||||
this.intentText = null;
|
||||
}
|
||||
|
||||
this.add([
|
||||
this.bg,
|
||||
this.nameText,
|
||||
this.hpBarBg,
|
||||
this.hpBarFill,
|
||||
this.hpText,
|
||||
this.buffContainer,
|
||||
...(this.intentText ? [this.intentText] : []),
|
||||
]);
|
||||
}
|
||||
|
||||
updateFromData(data: CombatUnitData): void {
|
||||
this.currentEntity = data.entity;
|
||||
this.currentName = data.name;
|
||||
|
||||
this.nameText.setText(this.currentName);
|
||||
|
||||
const hpPercent =
|
||||
this.currentEntity.maxHp > 0
|
||||
? this.currentEntity.hp / this.currentEntity.maxHp
|
||||
: 0;
|
||||
|
||||
const fillWidth = Math.max(0, HP_BAR_WIDTH * hpPercent);
|
||||
this.hpBarFill.setDisplaySize(fillWidth, HP_BAR_HEIGHT);
|
||||
|
||||
const hpColor =
|
||||
hpPercent > 0.5 ? 0x22c55e : hpPercent > 0.25 ? 0xf59e0b : 0xef4444;
|
||||
this.hpBarFill.setFillStyle(hpColor);
|
||||
|
||||
this.hpText.setText(
|
||||
`${this.currentEntity.hp} / ${this.currentEntity.maxHp} HP`,
|
||||
);
|
||||
|
||||
this.updateBuffs(this.getBuffEntries());
|
||||
|
||||
if (!this.currentIsPlayer && this.intentText) {
|
||||
const enemyEntity = data.entity as EnemyEntity;
|
||||
const intent = enemyEntity.currentIntent;
|
||||
const intentName = intent?.id ?? "unknown";
|
||||
this.intentText.setText(`Intent: ${intentName}`);
|
||||
}
|
||||
}
|
||||
|
||||
private *getBuffEntries() {
|
||||
const entries = Object.values(this.currentEntity.effects);
|
||||
const totalWidth =
|
||||
entries.length * BUFF_ICON_SIZE + (entries.length - 1) * BUFF_ICON_GAP;
|
||||
const startX = -totalWidth / 2 + BUFF_ICON_SIZE / 2;
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const buff = entries[i];
|
||||
yield {
|
||||
...buff.data,
|
||||
stacks: buff.stacks,
|
||||
isPositive: true,
|
||||
x: startX + i * BUFF_ICON_SIZE,
|
||||
} as BuffDataWithIndex;
|
||||
}
|
||||
}
|
||||
|
||||
playSpawnEffect(): void {
|
||||
this.setScale(0);
|
||||
this.setAlpha(0);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
scale: 1,
|
||||
alpha: 1,
|
||||
duration: 400,
|
||||
ease: "Back.easeOut",
|
||||
});
|
||||
}
|
||||
|
||||
playDamageEffect(): void {
|
||||
const flash = this.scene.add
|
||||
.rectangle(
|
||||
this.x,
|
||||
this.y,
|
||||
CONTAINER_WIDTH,
|
||||
CONTAINER_HEIGHT,
|
||||
0xff0000,
|
||||
0.4,
|
||||
)
|
||||
.setDepth(200);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
x: this.x + 8,
|
||||
duration: 60,
|
||||
yoyo: true,
|
||||
repeat: 3,
|
||||
onComplete: () => {
|
||||
this.scene.tweens.add({
|
||||
targets: flash,
|
||||
alpha: 0,
|
||||
duration: 200,
|
||||
onComplete: () => flash.destroy(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
playHealEffect(): void {
|
||||
const healText = this.scene.add
|
||||
.text(this.x, this.y - 40, "+HP", {
|
||||
fontSize: "20px",
|
||||
color: "#22c55e",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setDepth(200);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: healText,
|
||||
y: this.y - 80,
|
||||
alpha: 0,
|
||||
duration: 800,
|
||||
ease: "Power2",
|
||||
onComplete: () => healText.destroy(),
|
||||
});
|
||||
}
|
||||
|
||||
destroy(fromScene?: boolean): void {
|
||||
this.buffContainer.removeAll(true);
|
||||
super.destroy(fromScene);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { Spawner } from "boardgame-phaser";
|
||||
import { spawnEffect } from "boardgame-phaser";
|
||||
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: GameHostScene<CombatState>) {}
|
||||
|
||||
*getData(): Iterable<CombatUnitData> {
|
||||
const combat = this.getCombatState();
|
||||
if (!combat) return;
|
||||
|
||||
yield {
|
||||
key: "player",
|
||||
entity: combat.player,
|
||||
name: "Player",
|
||||
isPlayer: true,
|
||||
};
|
||||
|
||||
for (let i = 0; i < combat.enemies.length; i++) {
|
||||
const enemy = combat.enemies[i];
|
||||
yield {
|
||||
key: `enemy-${i}`,
|
||||
entity: enemy,
|
||||
name: enemy.enemy.name,
|
||||
isPlayer: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getKey(t: CombatUnitData): string {
|
||||
return t.key;
|
||||
}
|
||||
|
||||
onSpawn(t: CombatUnitData): CombatUnitContainer | null {
|
||||
const { width, height } = this.scene.scale;
|
||||
const combat = this.getCombatState();
|
||||
if (!combat) return null;
|
||||
|
||||
const totalUnits = 1 + combat.enemies.length;
|
||||
const spacing = 220;
|
||||
const totalWidth = (totalUnits - 1) * spacing;
|
||||
const startX = width / 2 - totalWidth / 2;
|
||||
|
||||
let x = startX;
|
||||
if (t.key.startsWith("enemy-")) {
|
||||
const index = parseInt(t.key.replace("enemy-", ""), 10);
|
||||
x = startX + (index + 1) * spacing;
|
||||
}
|
||||
|
||||
const y = height / 2;
|
||||
|
||||
const container = new CombatUnitContainer(this.scene, x, y, t);
|
||||
container.playSpawnEffect();
|
||||
return container;
|
||||
}
|
||||
|
||||
onUpdate(t: CombatUnitData, obj: CombatUnitContainer): void {
|
||||
obj.updateFromData(t);
|
||||
}
|
||||
|
||||
onDespawn(obj: CombatUnitContainer): void {
|
||||
obj.destroy();
|
||||
}
|
||||
|
||||
private getCombatState(): CombatState | null {
|
||||
const registry = this.scene.registry;
|
||||
return (registry.get("combatState") as CombatState | undefined) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
export function createCombatUnitSpawner(scene: GameHostScene<CombatState>) {
|
||||
return spawnEffect(new CombatUnitSpawner(scene));
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import Phaser from "phaser";
|
||||
import { dragDropEventEffect, DragDropEventType } from "boardgame-phaser";
|
||||
import { GRID_CONFIG } from "@/config";
|
||||
import {
|
||||
IDENTITY_TRANSFORM,
|
||||
ParsedShape,
|
||||
Point2D,
|
||||
Transform2D,
|
||||
transformShape,
|
||||
type GameItem,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { DisposableBag } from "../../../framework/dist";
|
||||
import { InventoryItemState } from "@/state/InventoryItemState";
|
||||
import { moveItem } from "@/state/inventory";
|
||||
import {
|
||||
InventorySurface,
|
||||
InventorySurfaceState,
|
||||
inventoryToScene,
|
||||
sceneToInventory,
|
||||
} from "@/state/inventorySurfaceState";
|
||||
|
||||
export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
||||
private itemState: InventoryItemState;
|
||||
private surfaceState: InventorySurfaceState;
|
||||
private surfaces: Iterable<InventorySurface>;
|
||||
private hitArea: Point2D[] = [];
|
||||
|
||||
constructor(
|
||||
scene: Phaser.Scene,
|
||||
item: GameItem,
|
||||
surface: InventorySurface,
|
||||
surfaces: Iterable<InventorySurface>,
|
||||
) {
|
||||
super(scene, surface.gridOffsetX, surface.gridOffsetY);
|
||||
scene.add.existing(this);
|
||||
|
||||
const cellSize = surface.cellSize;
|
||||
const graphics = this.scene.add.graphics();
|
||||
graphics.setPosition(cellSize / 2, cellSize / 2);
|
||||
const label = this.scene.add.text(
|
||||
cellSize / 2,
|
||||
cellSize / 2,
|
||||
"",
|
||||
GRID_CONFIG.ITEM_NAME_STYLE,
|
||||
);
|
||||
this.add([graphics, label]);
|
||||
this.setupInteractive();
|
||||
|
||||
this.itemState = new InventoryItemState(item);
|
||||
this.surfaceState = new InventorySurfaceState(surface);
|
||||
this.surfaces = surfaces;
|
||||
|
||||
const disposables = new DisposableBag(this);
|
||||
|
||||
disposables.addEffect(() => {
|
||||
label.setText(this.itemState.name.value);
|
||||
});
|
||||
|
||||
disposables.add(this.setupDnDEffect());
|
||||
|
||||
disposables.addEffect(() => {
|
||||
graphics.clear();
|
||||
this.renderGraphics(
|
||||
graphics,
|
||||
this.surfaceState.cellSize,
|
||||
this.itemState.shape.value,
|
||||
this.itemState.color.value,
|
||||
);
|
||||
});
|
||||
|
||||
disposables.addEffect(() => {
|
||||
this.scene.tweens.add({
|
||||
targets: graphics,
|
||||
rotation: -(this.itemState.previewRotation.value * Math.PI) / 180,
|
||||
duration: 150,
|
||||
ease: "Power2",
|
||||
});
|
||||
this.hitArea = this.updateHitArea(this.itemState.previewRotation.value);
|
||||
});
|
||||
|
||||
disposables.addEffect(() => {
|
||||
this.snapBack(this.itemState.transform.value);
|
||||
this.itemState.setPreviewRotation(0);
|
||||
});
|
||||
}
|
||||
|
||||
setSurfaceItem(surface: InventorySurface, item: GameItem): void {
|
||||
this.surfaceState.setSurface(surface);
|
||||
this.itemState.setItem(item);
|
||||
}
|
||||
|
||||
private updateHitArea(value: number): Point2D[] {
|
||||
const shape = this.itemState.shape.value;
|
||||
if (!shape) return [];
|
||||
|
||||
const rotation = value;
|
||||
const cells = transformShape(shape, {
|
||||
rotation,
|
||||
offset: { x: 0, y: 0 },
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
});
|
||||
const cellSize = this.surfaceState.cellSize;
|
||||
|
||||
return cells.map((cell) => ({
|
||||
x: (cell.x - cells[0].x) * cellSize,
|
||||
y: (cell.y - cells[0].y) * cellSize,
|
||||
}));
|
||||
}
|
||||
|
||||
private renderGraphics(
|
||||
graphics: Phaser.GameObjects.Graphics,
|
||||
cellSize: number,
|
||||
shape: ParsedShape,
|
||||
itemColor: number,
|
||||
) {
|
||||
const cells = transformShape(shape, IDENTITY_TRANSFORM);
|
||||
|
||||
for (const cell of cells) {
|
||||
const localX = (cell.x - cells[0].x) * cellSize;
|
||||
const localY = (cell.y - cells[0].y) * cellSize;
|
||||
|
||||
graphics.fillStyle(itemColor);
|
||||
graphics.fillRect(
|
||||
localX + 2 - cellSize / 2,
|
||||
localY + 2 - cellSize / 2,
|
||||
cellSize - 4,
|
||||
cellSize - 4,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private setupInteractive() {
|
||||
this.setScrollFactor(0);
|
||||
this.setInteractive({
|
||||
hitArea: this,
|
||||
hitAreaCallback: (
|
||||
hitArea: InventoryItemContainer,
|
||||
x: number,
|
||||
y: number,
|
||||
) =>
|
||||
hitArea.hitArea.some(
|
||||
(cell) =>
|
||||
x >= cell.x &&
|
||||
x < cell.x + this.surfaceState.cellSize &&
|
||||
y >= cell.y &&
|
||||
y < cell.y + this.surfaceState.cellSize,
|
||||
),
|
||||
useHandCursor: true,
|
||||
} as Phaser.Types.Input.InputConfiguration);
|
||||
}
|
||||
|
||||
private setupDnDEffect() {
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
return dragDropEventEffect(this, (event) => {
|
||||
if (event.type === DragDropEventType.DOWN) {
|
||||
startX = this.x;
|
||||
startY = this.y;
|
||||
this.setAlpha(0.7);
|
||||
} else if (event.type === DragDropEventType.MOVE) {
|
||||
this.x = startX + event.deltaX;
|
||||
this.y = startY + event.deltaY;
|
||||
} else if (event.type === DragDropEventType.ALTBUTTON) {
|
||||
this.itemState.addPreviewRotation(90);
|
||||
} else if (event.type === DragDropEventType.UP) {
|
||||
const finalRotation = this.itemState.previewRotation.peek();
|
||||
const finalX = this.x;
|
||||
const finalY = this.y;
|
||||
if (!this.handleDragEnd(finalX, finalY, finalRotation)) {
|
||||
const t = this.itemState.transform.peek();
|
||||
t && this.snapBack(t);
|
||||
}
|
||||
this.setAlpha(1);
|
||||
this.itemState.setPreviewRotation(0);
|
||||
startX = startY = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleDragEnd(
|
||||
finalX: number,
|
||||
finalY: number,
|
||||
finalRotation: number,
|
||||
): boolean {
|
||||
const item = this.itemState.item;
|
||||
if (!item) return false;
|
||||
|
||||
const target = this.findDropSurface(finalX, finalY);
|
||||
if (!target) return false;
|
||||
|
||||
if (
|
||||
this.surfaceState.surface === target.surface &&
|
||||
item.transform.offset.x === target.x &&
|
||||
item.transform.offset.y === target.y &&
|
||||
item.transform.rotation === finalRotation
|
||||
)
|
||||
return false;
|
||||
|
||||
return moveItem(
|
||||
this.surfaceState.invSignal,
|
||||
target.surface.invSignal,
|
||||
item.id,
|
||||
target.x,
|
||||
target.y,
|
||||
finalRotation,
|
||||
);
|
||||
}
|
||||
|
||||
private findDropSurface(x: number, y: number) {
|
||||
for (const surface of this.surfaces) {
|
||||
const target = sceneToInventory(surface, x, y);
|
||||
if (target)
|
||||
return {
|
||||
surface,
|
||||
...target,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private snapBack(transform: Transform2D): void {
|
||||
const { x, y } = transform.offset;
|
||||
const target = inventoryToScene(this.surfaceState, x, y);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
...target,
|
||||
duration: 150,
|
||||
ease: "Power2",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import Phaser from "phaser";
|
||||
import type { Spawner } from "boardgame-phaser";
|
||||
import type { GameItem } from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { spawnEffect } from "boardgame-phaser";
|
||||
import { InventoryItemContainer } from "./InventoryItemContainer";
|
||||
import { InventorySurface } from "@/state/inventorySurfaceState";
|
||||
|
||||
export class InventoryItemSpawner implements Spawner<
|
||||
[InventorySurface, GameItem],
|
||||
InventoryItemContainer
|
||||
> {
|
||||
constructor(
|
||||
private scene: Phaser.Scene,
|
||||
private surfaces: Iterable<InventorySurface>,
|
||||
) {}
|
||||
|
||||
*getData(): Iterable<[InventorySurface, GameItem]> {
|
||||
for (const surface of this.surfaces) {
|
||||
const inv = surface.invSignal.value;
|
||||
for (const item of inv.items.values()) {
|
||||
yield [surface, item];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getKey(entry: [InventorySurface, GameItem]): string {
|
||||
return entry[1].id;
|
||||
}
|
||||
|
||||
onSpawn(entry: [InventorySurface, GameItem]): InventoryItemContainer | null {
|
||||
const [surface, item] = entry;
|
||||
|
||||
const container = new InventoryItemContainer(
|
||||
this.scene,
|
||||
item,
|
||||
surface,
|
||||
this.surfaces,
|
||||
);
|
||||
|
||||
container.setSurfaceItem(surface, item);
|
||||
return container;
|
||||
}
|
||||
|
||||
onUpdate(
|
||||
entry: [InventorySurface, GameItem],
|
||||
container: InventoryItemContainer,
|
||||
): void {
|
||||
const [surface, item] = entry;
|
||||
|
||||
container.setSurfaceItem(surface, item);
|
||||
}
|
||||
|
||||
onDespawn(container: InventoryItemContainer): void {
|
||||
// TODO: add tween
|
||||
container.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export function createInventoryItemSpawner(
|
||||
scene: Phaser.Scene,
|
||||
surfaces: Iterable<InventorySurface>,
|
||||
) {
|
||||
return spawnEffect(new InventoryItemSpawner(scene, surfaces));
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
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";
|
||||
import { createButton } from "@/utils/createButton";
|
||||
import { SceneKey } from "./types";
|
||||
import {
|
||||
CombatUnitContainer,
|
||||
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,
|
||||
CombatResult | null,
|
||||
CombatModule
|
||||
> {
|
||||
private selectedCardId: string | null = null;
|
||||
private isTargeting = false;
|
||||
private targetingText!: Phaser.GameObjects.Text;
|
||||
private selectionRect!: Phaser.GameObjects.Rectangle;
|
||||
|
||||
constructor() {
|
||||
super("CombatTestScene");
|
||||
}
|
||||
|
||||
create(): void {
|
||||
super.create();
|
||||
|
||||
const { width, height } = this.scale;
|
||||
|
||||
// Title
|
||||
this.add
|
||||
.text(width / 2, 30, "Combat Test — Card Play", {
|
||||
fontSize: "24px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
// Info text (reactive)
|
||||
const infoText = this.add
|
||||
.text(width / 2, 60, "", {
|
||||
fontSize: "14px",
|
||||
color: "#aaaaaa",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.addEffect(() => {
|
||||
const s = this.state;
|
||||
infoText.setText(
|
||||
`Turn ${s.turnNumber} | Phase: ${s.phase} | Energy: ${s.player.energy}/${s.player.maxEnergy}`,
|
||||
);
|
||||
});
|
||||
|
||||
// Targeting indicator
|
||||
this.targetingText = this.add
|
||||
.text(width / 2, height - 220, "", {
|
||||
fontSize: "18px",
|
||||
color: "#fbbf24",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setAlpha(0);
|
||||
|
||||
// Selection rectangle overlay for selected card
|
||||
this.selectionRect = this.add
|
||||
.rectangle(0, 0, 150, 210, 0x000000, 0)
|
||||
.setStrokeStyle(4, 0xfbbf24)
|
||||
.setAlpha(0)
|
||||
.setDepth(50);
|
||||
|
||||
// Unit spawner (player + enemies)
|
||||
this.disposables.add(spawnEffect(new UnitSpawner(this)));
|
||||
|
||||
// Card spawner (hand)
|
||||
this.disposables.add(
|
||||
spawnEffect(
|
||||
new CardSpawner(
|
||||
this,
|
||||
() => this.state,
|
||||
(cardId) => this.onCardClick(cardId),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Watch hand changes to clear stale selection
|
||||
this.addEffect(() => {
|
||||
const handIds = this.state.player.deck.regions.hand.childIds;
|
||||
if (this.selectedCardId && !handIds.includes(this.selectedCardId)) {
|
||||
this.clearTargeting();
|
||||
} else {
|
||||
this.updateSelectionRect();
|
||||
}
|
||||
});
|
||||
|
||||
// Controls
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "返回菜单",
|
||||
x: 100,
|
||||
y: 40,
|
||||
onClick: async () => {
|
||||
await this.sceneController.launch(SceneKey.IndexScene);
|
||||
},
|
||||
});
|
||||
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "End Turn",
|
||||
x: width - 100,
|
||||
y: 40,
|
||||
onClick: () => {
|
||||
this.tryPlayCard("end-turn");
|
||||
},
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const state = this.state;
|
||||
const card = state.player.deck.cards[cardId];
|
||||
if (!card) return;
|
||||
|
||||
const targetType = card.cardData.targetType;
|
||||
|
||||
if (targetType === "enemy") {
|
||||
this.selectedCardId = cardId;
|
||||
this.isTargeting = true;
|
||||
this.targetingText.setText("Select a target!");
|
||||
this.targetingText.setAlpha(1);
|
||||
this.updateSelectionRect();
|
||||
} else {
|
||||
this.tryPlayCard(cardId);
|
||||
}
|
||||
}
|
||||
|
||||
public onEnemyClick(enemyId: string): void {
|
||||
if (!this.isTargeting || !this.selectedCardId) return;
|
||||
this.tryPlayCard(this.selectedCardId, enemyId);
|
||||
}
|
||||
|
||||
public getIsTargeting(): boolean {
|
||||
return this.isTargeting;
|
||||
}
|
||||
|
||||
private tryPlayCard(cardId: string, targetId?: string): void {
|
||||
const error = this.gameHost.prompts.tryCommit(
|
||||
prompts.mainAction,
|
||||
"global",
|
||||
cardId,
|
||||
targetId,
|
||||
);
|
||||
if (!error.ok) {
|
||||
console.warn("Play failed:", error.reason);
|
||||
}
|
||||
this.clearTargeting();
|
||||
}
|
||||
|
||||
private clearTargeting(): void {
|
||||
this.selectedCardId = null;
|
||||
this.isTargeting = false;
|
||||
this.targetingText.setAlpha(0);
|
||||
this.selectionRect.setAlpha(0);
|
||||
}
|
||||
|
||||
private updateSelectionRect(): void {
|
||||
if (!this.selectedCardId) {
|
||||
this.selectionRect.setAlpha(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const state = this.state;
|
||||
const handIds = state.player.deck.regions.hand.childIds;
|
||||
const index = handIds.indexOf(this.selectedCardId);
|
||||
if (index === -1) {
|
||||
this.selectionRect.setAlpha(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const { width, height } = this.scale;
|
||||
const total = handIds.length;
|
||||
const spacing = Math.min(
|
||||
CARD_SPACING,
|
||||
(width - HAND_MARGIN * 2) / Math.max(1, total),
|
||||
);
|
||||
const startX = width / 2 - ((total - 1) * spacing) / 2;
|
||||
const x = startX + index * spacing;
|
||||
const y = height - HAND_Y;
|
||||
|
||||
this.selectionRect.setPosition(x, y);
|
||||
this.selectionRect.setAlpha(1);
|
||||
}
|
||||
}
|
||||
|
||||
class UnitSpawner implements Spawner<CombatUnitData, CombatUnitContainer> {
|
||||
constructor(private scene: CombatTestScene) {}
|
||||
|
||||
*getData(): Iterable<CombatUnitData> {
|
||||
const state = this.scene.state;
|
||||
yield {
|
||||
key: "player",
|
||||
entity: state.player,
|
||||
name: "Player",
|
||||
isPlayer: true,
|
||||
};
|
||||
for (let i = 0; i < state.enemies.length; i++) {
|
||||
const enemy = state.enemies[i];
|
||||
yield {
|
||||
key: `enemy-${i}`,
|
||||
entity: enemy,
|
||||
name: enemy.enemy.name,
|
||||
isPlayer: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getKey(t: CombatUnitData): string {
|
||||
return t.key;
|
||||
}
|
||||
|
||||
onSpawn(t: CombatUnitData): CombatUnitContainer {
|
||||
const { width, height } = this.scene.scale;
|
||||
const state = this.scene.state;
|
||||
const totalUnits = 1 + state.enemies.length;
|
||||
const spacing = 220;
|
||||
const totalWidth = (totalUnits - 1) * spacing;
|
||||
const startX = width / 2 - totalWidth / 2;
|
||||
|
||||
let x = startX;
|
||||
if (t.key.startsWith("enemy-")) {
|
||||
const index = parseInt(t.key.replace("enemy-", ""), 10);
|
||||
x = startX + (index + 1) * spacing;
|
||||
}
|
||||
|
||||
const container = new CombatUnitContainer(
|
||||
this.scene,
|
||||
x,
|
||||
height / 2 - 80,
|
||||
t,
|
||||
);
|
||||
container.playSpawnEffect();
|
||||
|
||||
// Make enemies clickable when targeting
|
||||
if (!t.isPlayer) {
|
||||
const hitArea = new Phaser.Geom.Rectangle(-100, -130, 200, 260);
|
||||
container.setInteractive(hitArea, Phaser.Geom.Rectangle.Contains);
|
||||
if (container.input) {
|
||||
container.input.cursor = "pointer";
|
||||
}
|
||||
container.on("pointerdown", () => {
|
||||
if (this.scene.getIsTargeting()) {
|
||||
this.scene.onEnemyClick(t.entity.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
onUpdate(t: CombatUnitData, obj: CombatUnitContainer): void {
|
||||
obj.updateFromData(t);
|
||||
}
|
||||
|
||||
onDespawn(obj: CombatUnitContainer): void {
|
||||
obj.destroy();
|
||||
}
|
||||
}
|
||||
@@ -38,16 +38,22 @@ export class IndexScene extends ReactiveScene {
|
||||
scene: SceneKey;
|
||||
y: number;
|
||||
}[] = [
|
||||
{ label: "Map Viewer", scene: SceneKey.MapViewerScene, y: centerY },
|
||||
{ label: "Combat Test", scene: SceneKey.CombatTestScene, y: centerY },
|
||||
{ label: "Map Viewer", scene: SceneKey.MapViewerScene, y: centerY + 70 },
|
||||
{
|
||||
label: "Grid Inventory Viewer",
|
||||
scene: SceneKey.GridViewerScene,
|
||||
y: centerY + 70,
|
||||
y: centerY + 140,
|
||||
},
|
||||
{
|
||||
label: "Shape Viewer",
|
||||
scene: SceneKey.ShapeViewerScene,
|
||||
y: centerY + 140,
|
||||
y: centerY + 210,
|
||||
},
|
||||
{
|
||||
label: "Inventory Test",
|
||||
scene: SceneKey.InventoryTestScene,
|
||||
y: centerY + 280,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import { ReactiveScene } from "boardgame-phaser";
|
||||
import { createButton } from "@/utils/createButton";
|
||||
import { GRID_CONFIG } from "@/config";
|
||||
import { createInventorySignal, type InventorySignal } from "@/state/inventory";
|
||||
import { createItemIn, data } from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { createInventoryItemSpawner } from "@/gameobjects/InventoryItemSpawner";
|
||||
import { SceneKey } from "./types";
|
||||
import { InventorySurface } from "@/state/inventorySurfaceState";
|
||||
|
||||
export class InventoryTestScene extends ReactiveScene {
|
||||
private readonly _surfaces: InventorySurface[] = [];
|
||||
private _leftInv!: InventorySignal;
|
||||
private _rightInv!: InventorySignal;
|
||||
private _leftGridOffsetX = 0;
|
||||
private _leftGridOffsetY = 0;
|
||||
private _rightGridOffsetX = 0;
|
||||
private _rightGridOffsetY = 0;
|
||||
|
||||
constructor() {
|
||||
super("InventoryTestScene");
|
||||
}
|
||||
|
||||
create(): void {
|
||||
super.create();
|
||||
|
||||
const { width, height } = this.scale;
|
||||
|
||||
this._leftInv = createInventorySignal();
|
||||
this._rightInv = createInventorySignal();
|
||||
|
||||
const invWidth = 4;
|
||||
const invHeight = 6;
|
||||
const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const gap = 80;
|
||||
|
||||
this._leftGridOffsetX =
|
||||
(width / 2 - gap / 2) / 2 - (invWidth * cellSize) / 2;
|
||||
this._leftGridOffsetY = (height - invHeight * cellSize) / 2 + 40;
|
||||
|
||||
this._rightGridOffsetX =
|
||||
width / 2 +
|
||||
gap / 2 +
|
||||
(width / 2 - gap / 2) / 2 -
|
||||
(invWidth * cellSize) / 2;
|
||||
this._rightGridOffsetY = (height - invHeight * cellSize) / 2 + 40;
|
||||
|
||||
this._surfaces.push(
|
||||
{
|
||||
invSignal: this._leftInv,
|
||||
gridOffsetX: this._leftGridOffsetX,
|
||||
gridOffsetY: this._leftGridOffsetY,
|
||||
cellSize,
|
||||
},
|
||||
{
|
||||
invSignal: this._rightInv,
|
||||
gridOffsetX: this._rightGridOffsetX,
|
||||
gridOffsetY: this._rightGridOffsetY,
|
||||
cellSize,
|
||||
},
|
||||
);
|
||||
|
||||
this.drawGrid(
|
||||
this._leftInv,
|
||||
invWidth,
|
||||
invHeight,
|
||||
this._leftGridOffsetX,
|
||||
this._leftGridOffsetY,
|
||||
);
|
||||
this.drawGrid(
|
||||
this._rightInv,
|
||||
invWidth,
|
||||
invHeight,
|
||||
this._rightGridOffsetX,
|
||||
this._rightGridOffsetY,
|
||||
);
|
||||
this.setupItemSpawner();
|
||||
|
||||
this.add
|
||||
.text(
|
||||
width / 2,
|
||||
30,
|
||||
"Inventory Signal Test (4x6 x2)",
|
||||
GRID_CONFIG.TITLE_STYLE,
|
||||
)
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.createControls();
|
||||
|
||||
this.add
|
||||
.text(
|
||||
width / 2,
|
||||
height - 40,
|
||||
"Drag items between inventories",
|
||||
GRID_CONFIG.SUBTITLE_STYLE,
|
||||
)
|
||||
.setOrigin(0.5);
|
||||
}
|
||||
|
||||
private drawGrid(
|
||||
inv: InventorySignal,
|
||||
invWidth: number,
|
||||
invHeight: number,
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
): void {
|
||||
const graphics = this.add.graphics();
|
||||
|
||||
this.addEffect(() => {
|
||||
for (let y = 0; y < invHeight; y++) {
|
||||
for (let x = 0; x < invWidth; x++) {
|
||||
const px = offsetX + x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const py = offsetY + y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
|
||||
const isOccupied = inv.value.occupiedCells.has(`${x},${y}`);
|
||||
graphics.fillStyle(
|
||||
isOccupied
|
||||
? GRID_CONFIG.CELL_OCCUPIED_COLOR
|
||||
: GRID_CONFIG.CELL_EMPTY_COLOR,
|
||||
);
|
||||
graphics.fillRect(
|
||||
px + 1,
|
||||
py + 1,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE - 2,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE - 2,
|
||||
);
|
||||
graphics.lineStyle(1, GRID_CONFIG.GRID_LINE_COLOR);
|
||||
graphics.strokeRect(
|
||||
px,
|
||||
py,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupItemSpawner(): void {
|
||||
const spawner = createInventoryItemSpawner(this, this._surfaces);
|
||||
this.disposables.add(spawner);
|
||||
}
|
||||
|
||||
private createControls(): void {
|
||||
const { width } = this.scale;
|
||||
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "返回菜单",
|
||||
x: 100,
|
||||
y: 40,
|
||||
onClick: async () => {
|
||||
await this.sceneController.launch(SceneKey.IndexScene);
|
||||
},
|
||||
});
|
||||
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "添加道具(左)",
|
||||
x: width - 350,
|
||||
y: 40,
|
||||
onClick: () => {
|
||||
this.addRandomItem(this._leftInv);
|
||||
},
|
||||
});
|
||||
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "添加道具(右)",
|
||||
x: width - 180,
|
||||
y: 40,
|
||||
onClick: () => {
|
||||
this.addRandomItem(this._rightInv);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private addRandomItem(inv: InventorySignal): void {
|
||||
const items = data.desert.getItems();
|
||||
|
||||
inv.produce((inventory) => {
|
||||
const usedIndices = new Set<number>();
|
||||
|
||||
for (const item of inventory.items.values()) {
|
||||
const match = item.id.match(/^item-(\d+)-/);
|
||||
if (match) {
|
||||
usedIndices.add(parseInt(match[1], 10));
|
||||
}
|
||||
}
|
||||
|
||||
let availableIndex = 0;
|
||||
while (usedIndices.has(availableIndex) && availableIndex < items.length) {
|
||||
availableIndex++;
|
||||
}
|
||||
|
||||
if (availableIndex >= items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemData = items[availableIndex];
|
||||
const id = `item-${availableIndex}-${Date.now().toString(16).slice(-4)}`;
|
||||
createItemIn(inventory, id, itemData);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
export enum SceneKey {
|
||||
CombatTestScene = "CombatTestScene",
|
||||
GridViewerScene = "GridViewerScene",
|
||||
IndexScene = "IndexScene",
|
||||
InventoryTestScene = "InventoryTestScene",
|
||||
MapViewerScene = "MapViewerScene",
|
||||
ShapeViewerScene = "ShapeViewerScene",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { computed, ReadonlySignal, signal, Signal } from "@preact/signals-core";
|
||||
import { ITEM_COLORS } from "@/config";
|
||||
import {
|
||||
type GameItem,
|
||||
type ParsedShape,
|
||||
type Transform2D,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
|
||||
export class InventoryItemState {
|
||||
private readonly _item: Signal<GameItem>;
|
||||
private readonly _previewRotation: Signal<number>;
|
||||
|
||||
readonly name: ReadonlySignal<string>;
|
||||
readonly shape: ReadonlySignal<ParsedShape>;
|
||||
readonly color: ReadonlySignal<number>;
|
||||
readonly transform: ReadonlySignal<Transform2D>;
|
||||
readonly previewRotation: ReadonlySignal<number>;
|
||||
|
||||
constructor(initialItem: GameItem) {
|
||||
this._item = signal(initialItem);
|
||||
this._previewRotation = signal(0);
|
||||
|
||||
this.name = computed(() => {
|
||||
const item = this._item.value;
|
||||
return item.meta?.itemData.name ?? item?.id ?? "";
|
||||
});
|
||||
|
||||
this.shape = computed(() => this._item.value.shape);
|
||||
|
||||
this.color = computed(() => this.computeColor(this._item.value.id ?? ""));
|
||||
|
||||
this.transform = computed(() => this._item.value.transform);
|
||||
|
||||
this.previewRotation = computed(() => {
|
||||
const base = this._item.value.transform.rotation ?? 0;
|
||||
return (base + this._previewRotation.value) % 360;
|
||||
});
|
||||
}
|
||||
|
||||
get item(): GameItem {
|
||||
return this._item.value;
|
||||
}
|
||||
|
||||
setItem(item: GameItem): void {
|
||||
this._item.value = item;
|
||||
this._previewRotation.value = 0;
|
||||
}
|
||||
|
||||
setPreviewRotation(rotation: number): void {
|
||||
this._previewRotation.value = rotation;
|
||||
}
|
||||
|
||||
addPreviewRotation(rotation: number): void {
|
||||
this._previewRotation.value += rotation;
|
||||
}
|
||||
|
||||
private computeColor(itemId: string): number {
|
||||
const hash = itemId.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
||||
return ITEM_COLORS[hash % ITEM_COLORS.length];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
buildCombatState,
|
||||
createRunState,
|
||||
createStart,
|
||||
createTriggers,
|
||||
data,
|
||||
generateDeckFromInventory,
|
||||
getAdjacentItems,
|
||||
getItemEffects,
|
||||
IRunContext,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { createInventorySignal } from "./inventory";
|
||||
|
||||
export type CombatModule = ReturnType<typeof createCombatModule>;
|
||||
export function createCombatModule() {
|
||||
const inventory = createInventorySignal(true);
|
||||
|
||||
const deck = generateDeckFromInventory(inventory.value);
|
||||
|
||||
const run = createRunState();
|
||||
|
||||
const encounter = data.desert.getEncounters()[0];
|
||||
|
||||
const effects = getItemEffects(inventory.value);
|
||||
const combat = buildCombatState(
|
||||
{
|
||||
hp: run.currentHp,
|
||||
maxHp: run.maxHp,
|
||||
itemEffects: effects,
|
||||
},
|
||||
deck,
|
||||
encounter,
|
||||
);
|
||||
|
||||
const runContext: IRunContext = {
|
||||
getItemData(id: string) {
|
||||
return inventory.value.items.get(id)?.meta?.itemData || null;
|
||||
},
|
||||
getAdjacentItems(id: string) {
|
||||
return getAdjacentItems(inventory.value, id).keys();
|
||||
},
|
||||
getConsumedUses(id: string) {
|
||||
return inventory.value.items.get(id)?.meta?.consumedUses || 0;
|
||||
},
|
||||
async setConsumedUsesAsync(id: string, uses: number) {
|
||||
await inventory.produceAsync((draft) => {
|
||||
const item = draft.items.get(id);
|
||||
if (!item?.meta) return;
|
||||
item.meta.consumedUses = uses;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const triggers = createTriggers(runContext);
|
||||
data.desert.addTriggers(triggers, runContext);
|
||||
const start = createStart(triggers, runContext);
|
||||
|
||||
return {
|
||||
start,
|
||||
createInitialState: () => combat,
|
||||
triggers,
|
||||
runContext,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { create } from "mutative";
|
||||
import { mutableSignal } from "boardgame-core";
|
||||
import {
|
||||
createGridInventory,
|
||||
createItemIn,
|
||||
data,
|
||||
GameItemMeta,
|
||||
placeItem,
|
||||
removeItem,
|
||||
Transform2D,
|
||||
validatePlacement,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { batch } from "@preact/signals-core";
|
||||
|
||||
function genId() {
|
||||
return Math.random().toString(16).slice(-8);
|
||||
}
|
||||
|
||||
export type InventorySignal = ReturnType<typeof createInventorySignal>;
|
||||
|
||||
export function createInventorySignal(giveStart = false) {
|
||||
const inventory = createGridInventory<GameItemMeta>(4, 6);
|
||||
|
||||
if (giveStart) {
|
||||
const startingItems = data.desert.getStartingItems();
|
||||
for (const d of startingItems) {
|
||||
createItemIn(inventory, `${d.id}-${genId()}`, d);
|
||||
}
|
||||
}
|
||||
|
||||
return mutableSignal(inventory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an item to a new position in the inventory.
|
||||
* Returns true if the move was successful, false if the new position is invalid.
|
||||
*/
|
||||
export function moveItem(
|
||||
from: InventorySignal,
|
||||
to: InventorySignal,
|
||||
itemId: string,
|
||||
newX: number,
|
||||
newY: number,
|
||||
newRotation?: number,
|
||||
): boolean {
|
||||
const inventory = from.value;
|
||||
const item = inventory.items.get(itemId);
|
||||
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newTransform: Transform2D = {
|
||||
offset: { x: newX, y: newY },
|
||||
rotation: newRotation === undefined ? item.transform.rotation : newRotation,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
};
|
||||
|
||||
const removed = create(to.value, (inv) => {
|
||||
removeItem(inv, itemId);
|
||||
});
|
||||
const validation = validatePlacement(removed, item.shape, newTransform);
|
||||
if (!validation.valid) return false;
|
||||
|
||||
batch(() => {
|
||||
from.produce((inv) => {
|
||||
removeItem(inv, itemId);
|
||||
});
|
||||
to.produce((inv) => {
|
||||
placeItem(inv, {
|
||||
...item,
|
||||
transform: newTransform,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { InventorySignal } from "./inventory";
|
||||
import { MutableSignal, mutableSignal } from "boardgame-core";
|
||||
|
||||
export type InventorySurface = {
|
||||
invSignal: InventorySignal;
|
||||
gridOffsetX: number;
|
||||
gridOffsetY: number;
|
||||
cellSize: number;
|
||||
};
|
||||
|
||||
export class InventorySurfaceState {
|
||||
private readonly _signal: MutableSignal<InventorySurface>;
|
||||
|
||||
constructor(init: InventorySurface) {
|
||||
this._signal = mutableSignal(init);
|
||||
}
|
||||
|
||||
public get surface() {
|
||||
return this._signal.value;
|
||||
}
|
||||
|
||||
public get invSignal() {
|
||||
return this._signal.value.invSignal;
|
||||
}
|
||||
|
||||
public get gridOffsetX() {
|
||||
return this._signal.value.gridOffsetX;
|
||||
}
|
||||
|
||||
public get gridOffsetY() {
|
||||
return this._signal.value.gridOffsetY;
|
||||
}
|
||||
|
||||
public get cellSize() {
|
||||
return this._signal.value.cellSize;
|
||||
}
|
||||
|
||||
public setSurface(surface: InventorySurface) {
|
||||
this._signal.value = surface;
|
||||
}
|
||||
}
|
||||
|
||||
export function sceneToInventory(
|
||||
surface: InventorySurface | InventorySurfaceState,
|
||||
x: number,
|
||||
y: number,
|
||||
): { x: number; y: number } | null {
|
||||
const invX = Math.round((x - surface.gridOffsetX) / surface.cellSize);
|
||||
const invY = Math.round((y - surface.gridOffsetY) / surface.cellSize);
|
||||
|
||||
const { width, height } = surface.invSignal.peek();
|
||||
if (invX < 0 || invY < 0 || invX >= width || invY >= height) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { x: invX, y: invY };
|
||||
}
|
||||
|
||||
export function inventoryToScene(
|
||||
surface: InventorySurface | InventorySurfaceState,
|
||||
x: number,
|
||||
y: number,
|
||||
): { x: number; y: number } {
|
||||
return {
|
||||
x: x * surface.cellSize + surface.gridOffsetX,
|
||||
y: y * surface.cellSize + surface.gridOffsetY,
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,35 @@
|
||||
import { PhaserGame, PhaserScene } from "boardgame-phaser";
|
||||
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 { 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";
|
||||
|
||||
export default function App() {
|
||||
const gameHost = useMemo(
|
||||
() =>
|
||||
createGameHost<CombatState, CombatResult | null, CombatModule>(
|
||||
createCombatModule(),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
<div className="flex-1 flex relative justify-center items-center">
|
||||
<PhaserGame initialScene="IndexScene" config={GAME_CONFIG}>
|
||||
<PhaserScene scene={IndexScene} />
|
||||
<PhaserScene scene={CombatTestScene} data={{ gameHost }} />
|
||||
<PhaserScene scene={InventoryTestScene} />
|
||||
<PhaserScene scene={MapViewerScene} />
|
||||
<PhaserScene scene={GridViewerScene} />
|
||||
<PhaserScene scene={ShapeViewerScene} />
|
||||
|
||||
Generated
+55
-37
@@ -25,7 +25,7 @@ importers:
|
||||
version: 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(lightningcss@1.32.0)
|
||||
version: 3.2.4(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
|
||||
packages/boop-game:
|
||||
dependencies:
|
||||
@@ -53,13 +53,13 @@ importers:
|
||||
devDependencies:
|
||||
'@preact/preset-vite':
|
||||
specifier: ^2.8.1
|
||||
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(lightningcss@1.32.0))
|
||||
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))
|
||||
'@preact/signals':
|
||||
specifier: ^2.9.0
|
||||
version: 2.9.0(preact@10.29.0)
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.2(vite@5.4.21(lightningcss@1.32.0))
|
||||
version: 4.2.2(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))
|
||||
tailwindcss:
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.2
|
||||
@@ -68,7 +68,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^5.1.0
|
||||
version: 5.4.21(lightningcss@1.32.0)
|
||||
version: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
|
||||
packages/framework:
|
||||
devDependencies:
|
||||
@@ -98,7 +98,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(lightningcss@1.32.0)
|
||||
version: 3.2.4(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
|
||||
packages/onitama-game:
|
||||
dependencies:
|
||||
@@ -123,13 +123,13 @@ importers:
|
||||
devDependencies:
|
||||
'@preact/preset-vite':
|
||||
specifier: ^2.8.1
|
||||
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(lightningcss@1.32.0))
|
||||
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))
|
||||
'@preact/signals':
|
||||
specifier: ^2.9.0
|
||||
version: 2.9.0(preact@10.29.0)
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.2(vite@5.4.21(lightningcss@1.32.0))
|
||||
version: 4.2.2(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))
|
||||
tailwindcss:
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.2
|
||||
@@ -138,7 +138,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^5.1.0
|
||||
version: 5.4.21(lightningcss@1.32.0)
|
||||
version: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
|
||||
packages/regicide-game:
|
||||
dependencies:
|
||||
@@ -163,13 +163,13 @@ importers:
|
||||
devDependencies:
|
||||
'@preact/preset-vite':
|
||||
specifier: ^2.8.1
|
||||
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(lightningcss@1.32.0))
|
||||
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))
|
||||
'@preact/signals':
|
||||
specifier: ^2.9.0
|
||||
version: 2.9.0(preact@10.29.0)
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.2(vite@5.4.21(lightningcss@1.32.0))
|
||||
version: 4.2.2(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))
|
||||
tailwindcss:
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.2
|
||||
@@ -178,10 +178,10 @@ importers:
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^5.1.0
|
||||
version: 5.4.21(lightningcss@1.32.0)
|
||||
version: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(lightningcss@1.32.0)
|
||||
version: 3.2.4(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
|
||||
packages/sample-game:
|
||||
dependencies:
|
||||
@@ -206,13 +206,13 @@ importers:
|
||||
devDependencies:
|
||||
'@preact/preset-vite':
|
||||
specifier: ^2.8.1
|
||||
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(lightningcss@1.32.0))
|
||||
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))
|
||||
'@preact/signals':
|
||||
specifier: ^2.9.0
|
||||
version: 2.9.0(preact@10.29.0)
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.2(vite@5.4.21(lightningcss@1.32.0))
|
||||
version: 4.2.2(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))
|
||||
tailwindcss:
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.2
|
||||
@@ -221,7 +221,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^5.1.0
|
||||
version: 5.4.21(lightningcss@1.32.0)
|
||||
version: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
|
||||
packages/sts-like-viewer:
|
||||
dependencies:
|
||||
@@ -246,13 +246,16 @@ importers:
|
||||
devDependencies:
|
||||
'@preact/preset-vite':
|
||||
specifier: ^2.8.1
|
||||
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(lightningcss@1.32.0))
|
||||
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))
|
||||
'@preact/signals':
|
||||
specifier: ^2.9.0
|
||||
version: 2.9.0(preact@10.29.0)
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.2(vite@5.4.21(lightningcss@1.32.0))
|
||||
version: 4.2.2(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))
|
||||
'@types/node':
|
||||
specifier: ^25.6.0
|
||||
version: 25.6.0
|
||||
tailwindcss:
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.2
|
||||
@@ -261,7 +264,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^5.1.0
|
||||
version: 5.4.21(lightningcss@1.32.0)
|
||||
version: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -1020,6 +1023,9 @@ packages:
|
||||
'@types/json5@0.0.29':
|
||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||
|
||||
'@types/node@25.6.0':
|
||||
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.58.2':
|
||||
resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -2337,6 +2343,9 @@ packages:
|
||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
undici-types@7.19.2:
|
||||
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
|
||||
|
||||
update-browserslist-db@1.2.3:
|
||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||
hasBin: true
|
||||
@@ -2814,19 +2823,19 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(lightningcss@1.32.0))':
|
||||
'@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0)
|
||||
'@prefresh/vite': 2.4.12(preact@10.29.0)(vite@5.4.21(lightningcss@1.32.0))
|
||||
'@prefresh/vite': 2.4.12(preact@10.29.0)(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0)
|
||||
debug: 4.4.3
|
||||
magic-string: 0.30.21
|
||||
picocolors: 1.1.1
|
||||
vite: 5.4.21(lightningcss@1.32.0)
|
||||
vite-prerender-plugin: 0.5.13(vite@5.4.21(lightningcss@1.32.0))
|
||||
vite: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
vite-prerender-plugin: 0.5.13(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))
|
||||
zimmerframe: 1.1.4
|
||||
transitivePeerDependencies:
|
||||
- preact
|
||||
@@ -2848,7 +2857,7 @@ snapshots:
|
||||
|
||||
'@prefresh/utils@1.2.1': {}
|
||||
|
||||
'@prefresh/vite@2.4.12(preact@10.29.0)(vite@5.4.21(lightningcss@1.32.0))':
|
||||
'@prefresh/vite@2.4.12(preact@10.29.0)(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@prefresh/babel-plugin': 0.5.3
|
||||
@@ -2856,7 +2865,7 @@ snapshots:
|
||||
'@prefresh/utils': 1.2.1
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
preact: 10.29.0
|
||||
vite: 5.4.21(lightningcss@1.32.0)
|
||||
vite: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -3011,12 +3020,12 @@ snapshots:
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.2
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.2.2
|
||||
|
||||
'@tailwindcss/vite@4.2.2(vite@5.4.21(lightningcss@1.32.0))':
|
||||
'@tailwindcss/vite@4.2.2(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.2.2
|
||||
'@tailwindcss/oxide': 4.2.2
|
||||
tailwindcss: 4.2.2
|
||||
vite: 5.4.21(lightningcss@1.32.0)
|
||||
vite: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
dependencies:
|
||||
@@ -3031,6 +3040,10 @@ snapshots:
|
||||
|
||||
'@types/json5@0.0.29': {}
|
||||
|
||||
'@types/node@25.6.0':
|
||||
dependencies:
|
||||
undici-types: 7.19.2
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
@@ -3130,13 +3143,13 @@ snapshots:
|
||||
chai: 5.3.3
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@5.4.21(lightningcss@1.32.0))':
|
||||
'@vitest/mocker@3.2.4(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 5.4.21(lightningcss@1.32.0)
|
||||
vite: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
@@ -4567,6 +4580,8 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
which-boxed-primitive: 1.1.1
|
||||
|
||||
undici-types@7.19.2: {}
|
||||
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.2):
|
||||
dependencies:
|
||||
browserslist: 4.28.2
|
||||
@@ -4577,13 +4592,13 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
vite-node@3.2.4(lightningcss@1.32.0):
|
||||
vite-node@3.2.4(@types/node@25.6.0)(lightningcss@1.32.0):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 5.4.21(lightningcss@1.32.0)
|
||||
vite: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
@@ -4595,7 +4610,7 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite-prerender-plugin@0.5.13(vite@5.4.21(lightningcss@1.32.0)):
|
||||
vite-prerender-plugin@0.5.13(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)):
|
||||
dependencies:
|
||||
kolorist: 1.8.0
|
||||
magic-string: 0.30.21
|
||||
@@ -4603,22 +4618,23 @@ snapshots:
|
||||
simple-code-frame: 1.3.0
|
||||
source-map: 0.7.6
|
||||
stack-trace: 1.0.0-pre2
|
||||
vite: 5.4.21(lightningcss@1.32.0)
|
||||
vite: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
|
||||
vite@5.4.21(lightningcss@1.32.0):
|
||||
vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.8
|
||||
rollup: 4.60.1
|
||||
optionalDependencies:
|
||||
'@types/node': 25.6.0
|
||||
fsevents: 2.3.3
|
||||
lightningcss: 1.32.0
|
||||
|
||||
vitest@3.2.4(lightningcss@1.32.0):
|
||||
vitest@3.2.4(@types/node@25.6.0)(lightningcss@1.32.0):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@5.4.21(lightningcss@1.32.0))
|
||||
'@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
@@ -4636,9 +4652,11 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 5.4.21(lightningcss@1.32.0)
|
||||
vite-node: 3.2.4(lightningcss@1.32.0)
|
||||
vite: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
vite-node: 3.2.4(@types/node@25.6.0)(lightningcss@1.32.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 25.6.0
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
|
||||
Reference in New Issue
Block a user