Compare commits

...
8 Commits
Author SHA1 Message Date
hyper ad2c808482 feat(sts-viewer): add effect application logging and fix command error
handling

- Log effect application details in `CombatTestScene`
- Update `tryPlayCard` to use "global" scope and handle error reasons
- Register desert triggers in `createCombatModule`
2026-04-23 16:28:10 +08:00
hyper f893ac9ff7 feat(framework): add generic types to GameHostScene
Update GameHostScene to support generic types for TResult and TModule,
allowing better type safety when interacting with the game host and
its modules.
2026-04-23 16:11:15 +08:00
hypercross dfbdaa3499 feat(sts-viewer): add spawn/despawn tweens and async interruptions
Add visual tweens for Buff and Card game objects. Update spawners to
use `GameHostScene` and `addInterruption` to ensure animations
complete before the next state change occurs.
2026-04-22 21:29:43 +08:00
hypercross c29b9a43b3 feat(framework): decouple spawner update logic
Extract the core update logic from `spawnEffect` into
`createSpawnUpdate`.
This allows manual updates of spawned objects without relying on
Preact effects, which is useful for non-reactive or imperative
update cycles.

Refactor `Buff` and `BuffSpawner` in `sts-like-viewer` to use the new
`createSpawnUpdate` API for more efficient buff rendering.
2026-04-22 20:59:35 +08:00
hypercross 447a5cfbd0 feat(sts-viewer): improve card and effect visual indicators
- Update `CardContainer` to use descriptive emojis for different target
  types
- Use emoji icons for combat unit status effects instead of keys
- Update `CardSpawner` default target type to 'player'
- Fix target type check in `CombatTestScene` to use 'enemy' instead of
  'single'
2026-04-22 19:47:37 +08:00
hypercross dda290bf9c feat(framework): add hoverEffect utility
Add a `hoverEffect` utility to `boardgame-phaser` to simplify
handling pointerover and pointerout events. Refactor
`CardContainer` in `sts-like-viewer` to use this new utility and
centralize card dimensions in `CARD_CONFIG`.
2026-04-22 15:13:33 +08:00
hypercross 5d84c42b78 style: reformat CombatUnitContainer for readability 2026-04-22 11:46:39 +08:00
hypercross a6ddafb802 build: add @types/node to sts-like-viewer 2026-04-22 11:46:24 +08:00
17 changed files with 485 additions and 184 deletions
+3 -2
View File
@@ -5,10 +5,11 @@ export type { IDisposable, DisposableItem } from "./utils";
// Drag & drop utilities // Drag & drop utilities
export { dragDropEventEffect, DragDropEventType } from "./utils"; export { dragDropEventEffect, DragDropEventType } from "./utils";
export type { DragDropEvent, DragDropCallback } from "./utils"; export type { DragDropEvent, DragDropCallback } from "./utils";
export { hoverEffect } from "./utils";
// Data-driven object spawning // Data-driven object spawning
export { spawnEffect } from "./spawner"; export { spawnEffect, createSpawnUpdate } from "./spawner";
export type { Spawner } from "./spawner"; export type { Spawner, SpawnerCallback } from "./spawner";
// Scene base classes // Scene base classes
export { export {
+12 -6
View File
@@ -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 未提供。` +
+25 -7
View File
@@ -4,9 +4,7 @@ import type Phaser from "phaser";
type GO = Phaser.GameObjects.GameObject; type GO = Phaser.GameObjects.GameObject;
export interface Spawner<TData, TObject extends GO = GO> { export interface SpawnerCallback<TData, TObject extends GO = GO> {
/** 数据源迭代器 */
getData(): Iterable<TData>;
/** 获取数据的唯一键 */ /** 获取数据的唯一键 */
getKey(t: TData): string; getKey(t: TData): string;
/** 创建新对象 */ /** 创建新对象 */
@@ -17,15 +15,33 @@ export interface Spawner<TData, TObject extends GO = GO> {
onUpdate(t: TData, obj: TObject): void; 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>( export function spawnEffect<TData, TObject extends GO = GO>(
spawner: Spawner<TData, TObject>, spawner: Spawner<TData, TObject>,
): () => void { ) {
const entries = new Map<string, { object: TObject; data: TData }>(); const update = createSpawnUpdate(spawner);
return effect(() => { 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>(); const current = new Set<string>();
for (const t of spawner.getData()) { for (const t of data) {
const key = spawner.getKey(t); const key = spawner.getKey(t);
current.add(key); current.add(key);
@@ -48,5 +64,7 @@ export function spawnEffect<TData, TObject extends GO = GO>(
entries.delete(key); entries.delete(key);
} }
} }
}); }
return update;
} }
+28
View File
@@ -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;
}
+1
View File
@@ -6,3 +6,4 @@ export {
type DragDropEvent, type DragDropEvent,
type DragDropCallback, type DragDropCallback,
} from "./dnd"; } from "./dnd";
export * from "./hover";
+1
View File
@@ -19,6 +19,7 @@
"@preact/preset-vite": "^2.8.1", "@preact/preset-vite": "^2.8.1",
"@preact/signals": "^2.9.0", "@preact/signals": "^2.9.0",
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"@types/node": "^25.6.0",
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.1.0" "vite": "^5.1.0"
@@ -156,6 +156,12 @@ export const NEGATIVE_EFFECTS = new Set([
"poison", "poison",
]); ]);
export const CARD_CONFIG = {
WIDTH: 140,
HEIGHT: 200,
CORNER_RADIUS: 8,
} as const;
export const GAME_CONFIG: Phaser.Types.Core.GameConfig = { export const GAME_CONFIG: Phaser.Types.Core.GameConfig = {
width: 1920, width: 1920,
height: 1080, 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);
}
}
@@ -1,8 +1,7 @@
import Phaser from "phaser"; import Phaser, { Tweens } from "phaser";
import type { import type { GameCard } from "boardgame-core/samples/slay-the-spire-like";
CardData, import { CARD_CONFIG } from "@/config";
GameCard, import { hoverEffect } from "boardgame-phaser";
} from "boardgame-core/samples/slay-the-spire-like";
export interface CardContainerOptions { export interface CardContainerOptions {
card: GameCard; card: GameCard;
@@ -10,9 +9,9 @@ export interface CardContainerOptions {
playable?: boolean; playable?: boolean;
} }
const CARD_WIDTH = 140; const CARD_WIDTH = CARD_CONFIG.WIDTH;
const CARD_HEIGHT = 200; const CARD_HEIGHT = CARD_CONFIG.HEIGHT;
const CORNER_RADIUS = 8; const CORNER_RADIUS = CARD_CONFIG.CORNER_RADIUS;
export class CardContainer extends Phaser.GameObjects.Container { export class CardContainer extends Phaser.GameObjects.Container {
private bg!: Phaser.GameObjects.Rectangle; private bg!: Phaser.GameObjects.Rectangle;
@@ -87,8 +86,18 @@ export class CardContainer extends Phaser.GameObjects.Container {
this.add(this.descText); this.add(this.descText);
// Target indicator // Target indicator
const targetLabel = const targetLabel = (() => {
cardData.targetType === "single" ? "🎯 Single" : "✨ Self"; switch (cardData.targetType) {
case "enemy":
return "⚔️ Enemy";
case "enemies":
return "⚔️ Enemies";
case "player":
return "🛡️ Player";
default:
return "❓ Unknown";
}
})();
const targetText = this.scene.add const targetText = this.scene.add
.text(0, CARD_HEIGHT / 2 - 20, targetLabel, { .text(0, CARD_HEIGHT / 2 - 20, targetLabel, {
fontSize: "10px", fontSize: "10px",
@@ -110,16 +119,9 @@ export class CardContainer extends Phaser.GameObjects.Container {
this.input.cursor = options.playable ? "pointer" : "not-allowed"; this.input.cursor = options.playable ? "pointer" : "not-allowed";
} }
this.on("pointerover", () => { hoverEffect(this, (hovering) => {
if (options.playable && !this._selected) { if (!options.playable) return;
this.hoverIn(); this.playHoverTween(hovering);
}
});
this.on("pointerout", () => {
if (!this._selected) {
this.hoverOut();
}
}); });
this.on("pointerdown", () => { this.on("pointerdown", () => {
@@ -129,28 +131,6 @@ export class CardContainer extends Phaser.GameObjects.Container {
}); });
} }
private hoverIn(): void {
this.scene.tweens.add({
targets: this,
y: this.y - 10,
scale: 1.08,
duration: 150,
ease: "Power2",
});
this.bg.setStrokeStyle(3, 0x88aaff);
}
private hoverOut(): void {
this.scene.tweens.add({
targets: this,
y: this.y + 10,
scale: 1,
duration: 150,
ease: "Power2",
});
this.bg.setStrokeStyle(2, 0x555577);
}
setSelected(selected: boolean): void { setSelected(selected: boolean): void {
this._selected = selected; this._selected = selected;
if (selected) { if (selected) {
@@ -185,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,
@@ -198,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,
@@ -212,6 +192,20 @@ export class CardContainer extends Phaser.GameObjects.Container {
}); });
} }
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 { destroy(fromScene?: boolean): void {
this.highlight = null; this.highlight = null;
super.destroy(fromScene); super.destroy(fromScene);
@@ -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(
@@ -95,7 +98,7 @@ export class CardSpawner implements Spawner<CardSpawnData, CardContainer> {
type: "item", type: "item",
costType: "none", costType: "none",
costCount: 0, costCount: 0,
targetType: "none", targetType: "player",
effects: [], effects: [],
}, },
itemId: "", itemId: "",
@@ -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,5 +1,14 @@
import Phaser from "phaser"; import Phaser from "phaser";
import type { CombatEntity, EffectTable, EnemyEntity } from "boardgame-core/samples/slay-the-spire-like"; 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 = { export type CombatUnitData = {
key: string; key: string;
@@ -23,14 +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 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;
@@ -56,11 +73,22 @@ export class CombatUnitContainer extends Phaser.GameObjects.Container {
}) })
.setOrigin(0.5); .setOrigin(0.5);
this.hpBarBg = this.scene.add this.hpBarBg = this.scene.add.rectangle(
.rectangle(0, -CONTAINER_HEIGHT / 2 + 60, HP_BAR_WIDTH, HP_BAR_HEIGHT, 0x333333); 0,
-CONTAINER_HEIGHT / 2 + 60,
HP_BAR_WIDTH,
HP_BAR_HEIGHT,
0x333333,
);
this.hpBarFill = this.scene.add this.hpBarFill = this.scene.add
.rectangle(-HP_BAR_WIDTH / 2, -CONTAINER_HEIGHT / 2 + 60, HP_BAR_WIDTH, HP_BAR_HEIGHT, 0x22c55e) .rectangle(
-HP_BAR_WIDTH / 2,
-CONTAINER_HEIGHT / 2 + 60,
HP_BAR_WIDTH,
HP_BAR_HEIGHT,
0x22c55e,
)
.setOrigin(0, 0.5); .setOrigin(0, 0.5);
this.hpText = this.scene.add this.hpText = this.scene.add
@@ -72,6 +100,9 @@ export class CombatUnitContainer extends Phaser.GameObjects.Container {
.setOrigin(0.5); .setOrigin(0.5);
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(
new BuffSpawner(this.hostScene, this.buffContainer),
);
if (!this.currentIsPlayer) { if (!this.currentIsPlayer) {
this.intentText = this.scene.add this.intentText = this.scene.add
@@ -101,19 +132,23 @@ export class CombatUnitContainer extends Phaser.GameObjects.Container {
this.nameText.setText(this.currentName); this.nameText.setText(this.currentName);
const hpPercent = this.currentEntity.maxHp > 0 const hpPercent =
? this.currentEntity.hp / this.currentEntity.maxHp this.currentEntity.maxHp > 0
: 0; ? this.currentEntity.hp / this.currentEntity.maxHp
: 0;
const fillWidth = Math.max(0, HP_BAR_WIDTH * hpPercent); const fillWidth = Math.max(0, HP_BAR_WIDTH * hpPercent);
this.hpBarFill.setDisplaySize(fillWidth, HP_BAR_HEIGHT); this.hpBarFill.setDisplaySize(fillWidth, HP_BAR_HEIGHT);
const hpColor = hpPercent > 0.5 ? 0x22c55e : hpPercent > 0.25 ? 0xf59e0b : 0xef4444; const hpColor =
hpPercent > 0.5 ? 0x22c55e : hpPercent > 0.25 ? 0xf59e0b : 0xef4444;
this.hpBarFill.setFillStyle(hpColor); this.hpBarFill.setFillStyle(hpColor);
this.hpText.setText(`${this.currentEntity.hp} / ${this.currentEntity.maxHp} HP`); this.hpText.setText(
`${this.currentEntity.hp} / ${this.currentEntity.maxHp} HP`,
);
this.renderBuffs(this.currentEntity.effects); this.updateBuffs(this.getBuffEntries());
if (!this.currentIsPlayer && this.intentText) { if (!this.currentIsPlayer && this.intentText) {
const enemyEntity = data.entity as EnemyEntity; const enemyEntity = data.entity as EnemyEntity;
@@ -123,47 +158,20 @@ export class CombatUnitContainer extends Phaser.GameObjects.Container {
} }
} }
private renderBuffs(effects: EffectTable): void { private *getBuffEntries() {
this.buffContainer.removeAll(true); const entries = Object.values(this.currentEntity.effects);
const totalWidth =
const entries = Object.entries(effects); entries.length * BUFF_ICON_SIZE + (entries.length - 1) * BUFF_ICON_GAP;
const totalWidth = entries.length * BUFF_ICON_SIZE + (entries.length - 1) * BUFF_ICON_GAP;
const startX = -totalWidth / 2 + BUFF_ICON_SIZE / 2; const startX = -totalWidth / 2 + BUFF_ICON_SIZE / 2;
for (let i = 0; i < entries.length; i++) {
entries.forEach(([key, entry], index) => { const buff = entries[i];
const x = startX + index * (BUFF_ICON_SIZE + BUFF_ICON_GAP); yield {
const stacks = entry.stacks; ...buff.data,
stacks: buff.stacks,
const isPositive = this.isPositiveEffect(key); isPositive: true,
const iconColor = isPositive ? 0x44aa44 : 0xaa4444; x: startX + i * BUFF_ICON_SIZE,
} as BuffDataWithIndex;
const bg = this.scene.add }
.rectangle(x, 0, BUFF_ICON_SIZE, BUFF_ICON_SIZE, iconColor)
.setStrokeStyle(1, 0xffffff);
const text = this.scene.add
.text(x, 0, `${stacks}`, {
fontSize: "12px",
color: "#ffffff",
fontStyle: "bold",
})
.setOrigin(0.5);
this.buffContainer.add([bg, text]);
});
}
private isPositiveEffect(effectId: string): boolean {
const positive = new Set([
"block",
"strength",
"dexterity",
"regen",
"armor",
"barrier",
"momentum",
]);
return positive.has(effectId.toLowerCase());
} }
playSpawnEffect(): void { playSpawnEffect(): void {
@@ -181,7 +189,14 @@ export class CombatUnitContainer extends Phaser.GameObjects.Container {
playDamageEffect(): void { playDamageEffect(): void {
const flash = this.scene.add const flash = this.scene.add
.rectangle(this.x, this.y, CONTAINER_WIDTH, CONTAINER_HEIGHT, 0xff0000, 0.4) .rectangle(
this.x,
this.y,
CONTAINER_WIDTH,
CONTAINER_HEIGHT,
0xff0000,
0.4,
)
.setDepth(200); .setDepth(200);
this.scene.tweens.add({ this.scene.tweens.add({
@@ -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 {
@@ -129,7 +142,7 @@ export class CombatTestScene extends GameHostScene<CombatState> {
const targetType = card.cardData.targetType; const targetType = card.cardData.targetType;
if (targetType === "single") { if (targetType === "enemy") {
this.selectedCardId = cardId; this.selectedCardId = cardId;
this.isTargeting = true; this.isTargeting = true;
this.targetingText.setText("Select a target!"); this.targetingText.setText("Select a target!");
@@ -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,
};
} }
+17 -11
View File
@@ -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(),
),
[], [],
); );
+55 -37
View File
@@ -25,7 +25,7 @@ importers:
version: 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) version: 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
vitest: vitest:
specifier: ^3.2.4 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: packages/boop-game:
dependencies: dependencies:
@@ -53,13 +53,13 @@ importers:
devDependencies: devDependencies:
'@preact/preset-vite': '@preact/preset-vite':
specifier: ^2.8.1 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': '@preact/signals':
specifier: ^2.9.0 specifier: ^2.9.0
version: 2.9.0(preact@10.29.0) version: 2.9.0(preact@10.29.0)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.0.0 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: tailwindcss:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.2.2 version: 4.2.2
@@ -68,7 +68,7 @@ importers:
version: 5.9.3 version: 5.9.3
vite: vite:
specifier: ^5.1.0 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: packages/framework:
devDependencies: devDependencies:
@@ -98,7 +98,7 @@ importers:
version: 5.9.3 version: 5.9.3
vitest: vitest:
specifier: ^3.2.4 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: packages/onitama-game:
dependencies: dependencies:
@@ -123,13 +123,13 @@ importers:
devDependencies: devDependencies:
'@preact/preset-vite': '@preact/preset-vite':
specifier: ^2.8.1 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': '@preact/signals':
specifier: ^2.9.0 specifier: ^2.9.0
version: 2.9.0(preact@10.29.0) version: 2.9.0(preact@10.29.0)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.0.0 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: tailwindcss:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.2.2 version: 4.2.2
@@ -138,7 +138,7 @@ importers:
version: 5.9.3 version: 5.9.3
vite: vite:
specifier: ^5.1.0 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: packages/regicide-game:
dependencies: dependencies:
@@ -163,13 +163,13 @@ importers:
devDependencies: devDependencies:
'@preact/preset-vite': '@preact/preset-vite':
specifier: ^2.8.1 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': '@preact/signals':
specifier: ^2.9.0 specifier: ^2.9.0
version: 2.9.0(preact@10.29.0) version: 2.9.0(preact@10.29.0)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.0.0 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: tailwindcss:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.2.2 version: 4.2.2
@@ -178,10 +178,10 @@ importers:
version: 5.9.3 version: 5.9.3
vite: vite:
specifier: ^5.1.0 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: vitest:
specifier: ^3.2.4 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: packages/sample-game:
dependencies: dependencies:
@@ -206,13 +206,13 @@ importers:
devDependencies: devDependencies:
'@preact/preset-vite': '@preact/preset-vite':
specifier: ^2.8.1 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': '@preact/signals':
specifier: ^2.9.0 specifier: ^2.9.0
version: 2.9.0(preact@10.29.0) version: 2.9.0(preact@10.29.0)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.0.0 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: tailwindcss:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.2.2 version: 4.2.2
@@ -221,7 +221,7 @@ importers:
version: 5.9.3 version: 5.9.3
vite: vite:
specifier: ^5.1.0 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: packages/sts-like-viewer:
dependencies: dependencies:
@@ -246,13 +246,16 @@ importers:
devDependencies: devDependencies:
'@preact/preset-vite': '@preact/preset-vite':
specifier: ^2.8.1 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': '@preact/signals':
specifier: ^2.9.0 specifier: ^2.9.0
version: 2.9.0(preact@10.29.0) version: 2.9.0(preact@10.29.0)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.0.0 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: tailwindcss:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.2.2 version: 4.2.2
@@ -261,7 +264,7 @@ importers:
version: 5.9.3 version: 5.9.3
vite: vite:
specifier: ^5.1.0 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: packages:
@@ -1020,6 +1023,9 @@ packages:
'@types/json5@0.0.29': '@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/node@25.6.0':
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
'@typescript-eslint/eslint-plugin@8.58.2': '@typescript-eslint/eslint-plugin@8.58.2':
resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==} resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2337,6 +2343,9 @@ packages:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
undici-types@7.19.2:
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
update-browserslist-db@1.2.3: update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
hasBin: true hasBin: true
@@ -2814,19 +2823,19 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@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: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@babel/plugin-transform-react-jsx': 7.28.6(@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) '@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) '@rollup/pluginutils': 5.3.0(rollup@4.60.1)
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0) babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0)
debug: 4.4.3 debug: 4.4.3
magic-string: 0.30.21 magic-string: 0.30.21
picocolors: 1.1.1 picocolors: 1.1.1
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(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 zimmerframe: 1.1.4
transitivePeerDependencies: transitivePeerDependencies:
- preact - preact
@@ -2848,7 +2857,7 @@ snapshots:
'@prefresh/utils@1.2.1': {} '@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: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@prefresh/babel-plugin': 0.5.3 '@prefresh/babel-plugin': 0.5.3
@@ -2856,7 +2865,7 @@ snapshots:
'@prefresh/utils': 1.2.1 '@prefresh/utils': 1.2.1
'@rollup/pluginutils': 4.2.1 '@rollup/pluginutils': 4.2.1
preact: 10.29.0 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: transitivePeerDependencies:
- supports-color - supports-color
@@ -3011,12 +3020,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2
'@tailwindcss/oxide-win32-x64-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: dependencies:
'@tailwindcss/node': 4.2.2 '@tailwindcss/node': 4.2.2
'@tailwindcss/oxide': 4.2.2 '@tailwindcss/oxide': 4.2.2
tailwindcss: 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': '@types/chai@5.2.3':
dependencies: dependencies:
@@ -3031,6 +3040,10 @@ snapshots:
'@types/json5@0.0.29': {} '@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)': '@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: dependencies:
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
@@ -3130,13 +3143,13 @@ snapshots:
chai: 5.3.3 chai: 5.3.3
tinyrainbow: 2.0.0 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: dependencies:
'@vitest/spy': 3.2.4 '@vitest/spy': 3.2.4
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.21 magic-string: 0.30.21
optionalDependencies: 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': '@vitest/pretty-format@3.2.4':
dependencies: dependencies:
@@ -4567,6 +4580,8 @@ snapshots:
has-symbols: 1.1.0 has-symbols: 1.1.0
which-boxed-primitive: 1.1.1 which-boxed-primitive: 1.1.1
undici-types@7.19.2: {}
update-browserslist-db@1.2.3(browserslist@4.28.2): update-browserslist-db@1.2.3(browserslist@4.28.2):
dependencies: dependencies:
browserslist: 4.28.2 browserslist: 4.28.2
@@ -4577,13 +4592,13 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 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: dependencies:
cac: 6.7.14 cac: 6.7.14
debug: 4.4.3 debug: 4.4.3
es-module-lexer: 1.7.0 es-module-lexer: 1.7.0
pathe: 2.0.3 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: transitivePeerDependencies:
- '@types/node' - '@types/node'
- less - less
@@ -4595,7 +4610,7 @@ snapshots:
- supports-color - supports-color
- terser - 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: dependencies:
kolorist: 1.8.0 kolorist: 1.8.0
magic-string: 0.30.21 magic-string: 0.30.21
@@ -4603,22 +4618,23 @@ snapshots:
simple-code-frame: 1.3.0 simple-code-frame: 1.3.0
source-map: 0.7.6 source-map: 0.7.6
stack-trace: 1.0.0-pre2 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: dependencies:
esbuild: 0.21.5 esbuild: 0.21.5
postcss: 8.5.8 postcss: 8.5.8
rollup: 4.60.1 rollup: 4.60.1
optionalDependencies: optionalDependencies:
'@types/node': 25.6.0
fsevents: 2.3.3 fsevents: 2.3.3
lightningcss: 1.32.0 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: dependencies:
'@types/chai': 5.2.3 '@types/chai': 5.2.3
'@vitest/expect': 3.2.4 '@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/pretty-format': 3.2.4
'@vitest/runner': 3.2.4 '@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4 '@vitest/snapshot': 3.2.4
@@ -4636,9 +4652,11 @@ snapshots:
tinyglobby: 0.2.15 tinyglobby: 0.2.15
tinypool: 1.1.1 tinypool: 1.1.1
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
vite: 5.4.21(lightningcss@1.32.0) vite: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)
vite-node: 3.2.4(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 why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 25.6.0
transitivePeerDependencies: transitivePeerDependencies:
- less - less
- lightningcss - lightningcss