Compare commits
No commits in common. "fe0621cedf3fa37eb5f26b3eadf75788187d63a8" and "368d9942d22b0c8ac2c5ca41bd26a091b072101c" have entirely different histories.
fe0621cedf
...
368d9942d2
|
|
@ -1,4 +1,4 @@
|
|||
import { type ReadonlySignal } from "@preact/signals-core";
|
||||
import { effect, 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.addEffect(fn);
|
||||
this.disposables.add(effect(fn));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ export const defaultPhaserConfig: Phaser.Types.Core.GameConfig = {
|
|||
parent: "phaser-container",
|
||||
backgroundColor: "#f9fafb",
|
||||
scene: [],
|
||||
disableContextMenu: true,
|
||||
};
|
||||
|
||||
export interface PhaserGameProps {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { effect } from "@preact/signals-core";
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
|
@ -10,10 +8,6 @@ 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;
|
||||
}
|
||||
|
|
@ -26,10 +20,6 @@ export class DisposableBag implements IDisposable {
|
|||
this._disposables.add(item);
|
||||
}
|
||||
|
||||
addEffect(fn: () => void) {
|
||||
this.add(effect(fn));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._isDisposed) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
type PointerRecord = {
|
||||
id: number;
|
||||
button: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
|
@ -11,7 +10,6 @@ export enum DragDropEventType {
|
|||
DOWN,
|
||||
UP,
|
||||
MOVE,
|
||||
ALTBUTTON,
|
||||
}
|
||||
|
||||
export type DragDropEvent = {
|
||||
|
|
@ -31,22 +29,9 @@ export function dragDropEventEffect(
|
|||
let down: PointerRecord | null = null;
|
||||
|
||||
function onPointerDown(pointer: Phaser.Input.Pointer) {
|
||||
if (down !== null) {
|
||||
if (pointer.button === down.button) return;
|
||||
callback({
|
||||
type: DragDropEventType.ALTBUTTON,
|
||||
deltaX: pointer.x - down.x,
|
||||
deltaY: pointer.y - down.y,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isDragging) return;
|
||||
isDragging = true;
|
||||
down = {
|
||||
id: pointer.id,
|
||||
button: pointer.button,
|
||||
x: pointer.x,
|
||||
y: pointer.y,
|
||||
};
|
||||
down = { id: pointer.id, x: pointer.x, y: pointer.y };
|
||||
|
||||
const event: DragDropEvent = {
|
||||
type: DragDropEventType.DOWN,
|
||||
|
|
@ -57,23 +42,12 @@ export function dragDropEventEffect(
|
|||
}
|
||||
|
||||
function onPointerUp(pointer: Phaser.Input.Pointer) {
|
||||
if (
|
||||
!isDragging ||
|
||||
!down ||
|
||||
pointer.id !== down.id ||
|
||||
pointer.button !== down.button
|
||||
)
|
||||
return;
|
||||
|
||||
const deltaX = pointer.x - down.x;
|
||||
const deltaY = pointer.y - down.y;
|
||||
if (!isDragging || !down || pointer.id !== down.id) return;
|
||||
|
||||
isDragging = false;
|
||||
const event: DragDropEvent = {
|
||||
type: DragDropEventType.UP,
|
||||
deltaX,
|
||||
deltaY,
|
||||
};
|
||||
const deltaX = pointer.x - down.x;
|
||||
const deltaY = pointer.y - down.y;
|
||||
const event: DragDropEvent = { type: DragDropEventType.UP, deltaX, deltaY };
|
||||
callback(event);
|
||||
down = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,29 +33,6 @@ 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 ────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -1,225 +0,0 @@
|
|||
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";
|
||||
|
||||
export interface InventoryItemContainerCallbacks {
|
||||
onMoveItem: (
|
||||
itemId: string,
|
||||
newX: number,
|
||||
newY: number,
|
||||
newRotation: number,
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
||||
private itemState: InventoryItemState;
|
||||
private hitArea: Point2D[] = [];
|
||||
|
||||
constructor(
|
||||
scene: Phaser.Scene,
|
||||
private gridOffsetX: number,
|
||||
private gridOffsetY: number,
|
||||
private callbacks: InventoryItemContainerCallbacks,
|
||||
) {
|
||||
super(scene, gridOffsetX, gridOffsetY);
|
||||
scene.add.existing(this);
|
||||
|
||||
const graphics = this.scene.add.graphics();
|
||||
graphics.setPosition(
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
||||
);
|
||||
const label = this.scene.add.text(
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
||||
"",
|
||||
GRID_CONFIG.ITEM_NAME_STYLE,
|
||||
);
|
||||
this.add([graphics, label]);
|
||||
this.setupInteractive();
|
||||
|
||||
this.itemState = new InventoryItemState();
|
||||
|
||||
const disposables = new DisposableBag(this);
|
||||
|
||||
disposables.addEffect(() => {
|
||||
label.setText(this.itemState.name.value);
|
||||
});
|
||||
|
||||
disposables.add(this.setupDnDEffect());
|
||||
|
||||
disposables.addEffect(() => {
|
||||
graphics.clear();
|
||||
if (!this.itemState.shape.value) return;
|
||||
this.renderGraphics(
|
||||
graphics,
|
||||
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(() => {
|
||||
if (!this.itemState.transform.value) return;
|
||||
this.snapBack(this.itemState.transform.value);
|
||||
this.itemState.setPreviewRotation(0);
|
||||
});
|
||||
}
|
||||
|
||||
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 = GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
|
||||
return cells.map((cell) => ({
|
||||
x: (cell.x - cells[0].x) * cellSize,
|
||||
y: (cell.y - cells[0].y) * cellSize,
|
||||
}));
|
||||
}
|
||||
|
||||
setItem(item: GameItem): void {
|
||||
this.itemState.setItem(item);
|
||||
}
|
||||
|
||||
renderGraphics(
|
||||
graphics: Phaser.GameObjects.Graphics,
|
||||
shape: ParsedShape,
|
||||
itemColor: number,
|
||||
) {
|
||||
const cells = transformShape(shape, IDENTITY_TRANSFORM);
|
||||
|
||||
for (const cell of cells) {
|
||||
const localX = (cell.x - cells[0].x) * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const localY = (cell.y - cells[0].y) * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
|
||||
graphics.fillStyle(itemColor);
|
||||
graphics.fillRect(
|
||||
localX + 2 - GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
||||
localY + 2 - GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE - 4,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE - 4,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private setupInteractive() {
|
||||
this.setScrollFactor(0);
|
||||
const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
this.setInteractive({
|
||||
hitArea: this,
|
||||
hitAreaCallback: (
|
||||
hitArea: InventoryItemContainer,
|
||||
x: number,
|
||||
y: number,
|
||||
) =>
|
||||
hitArea.hitArea.some(
|
||||
(cell) =>
|
||||
x >= cell.x &&
|
||||
x < cell.x + cellSize &&
|
||||
y >= cell.y &&
|
||||
y < cell.y + 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) {
|
||||
this.setAlpha(1);
|
||||
const finalRotation = this.itemState.previewRotation.peek();
|
||||
if (!this.handleDragEnd(finalRotation)) {
|
||||
this.itemState.setPreviewRotation(0);
|
||||
const t = this.itemState.transform.peek();
|
||||
t && this.snapBack(t);
|
||||
} else {
|
||||
this.itemState.setPreviewRotation(0);
|
||||
}
|
||||
startX = startY = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleDragEnd(finalRotation: number): boolean {
|
||||
const item = this.itemState.item;
|
||||
if (!item) return false;
|
||||
|
||||
const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const shapeWidth = item.shape?.width ?? 1;
|
||||
const shapeHeight = item.shape?.height ?? 1;
|
||||
|
||||
const x = this.x - this.gridOffsetX;
|
||||
const y = this.y - this.gridOffsetY;
|
||||
const targetX = Math.round(x / cellSize);
|
||||
const targetY = Math.round(y / cellSize);
|
||||
|
||||
const clampedX = Math.max(0, Math.min(targetX, 10 - shapeWidth));
|
||||
const clampedY = Math.max(0, Math.min(targetY, 10 - shapeHeight));
|
||||
|
||||
if (
|
||||
clampedX !== item.transform.offset.x ||
|
||||
clampedY !== item.transform.offset.y ||
|
||||
finalRotation !== 0
|
||||
) {
|
||||
return this.callbacks.onMoveItem(
|
||||
item.id,
|
||||
clampedX,
|
||||
clampedY,
|
||||
finalRotation,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private snapBack(transform: Transform2D): void {
|
||||
const { x, y } = transform.offset;
|
||||
const targetX = this.gridOffsetX + x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const targetY = this.gridOffsetY + y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
duration: 150,
|
||||
ease: "Power2",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import Phaser from "phaser";
|
||||
import type { Spawner } from "boardgame-phaser";
|
||||
import type {
|
||||
GameItemMeta,
|
||||
InventoryItem,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
import type { InventorySignal } from "@/state/inventory";
|
||||
import { spawnEffect } from "boardgame-phaser";
|
||||
import { InventoryItemContainer } from "./InventoryItemContainer";
|
||||
import type { InventoryItemContainerCallbacks } from "./InventoryItemContainer";
|
||||
|
||||
export interface InventoryItemSpawnerCallbacks extends InventoryItemContainerCallbacks {}
|
||||
|
||||
export class InventoryItemSpawner implements Spawner<
|
||||
[string, InventoryItem<GameItemMeta>],
|
||||
InventoryItemContainer
|
||||
> {
|
||||
constructor(
|
||||
private scene: Phaser.Scene,
|
||||
private inventorySignal: InventorySignal,
|
||||
private gridOffsetX: number,
|
||||
private gridOffsetY: number,
|
||||
private callbacks: InventoryItemSpawnerCallbacks,
|
||||
) {}
|
||||
|
||||
*getData(): Iterable<[string, InventoryItem<GameItemMeta>]> {
|
||||
const inventory = this.inventorySignal.value;
|
||||
yield* inventory.items.entries();
|
||||
}
|
||||
|
||||
getKey(entry: [string, InventoryItem<GameItemMeta>]): string {
|
||||
return entry[0];
|
||||
}
|
||||
|
||||
onSpawn(
|
||||
entry: [string, InventoryItem<GameItemMeta>],
|
||||
): InventoryItemContainer | null {
|
||||
const [itemId, item] = entry;
|
||||
|
||||
const container = new InventoryItemContainer(
|
||||
this.scene,
|
||||
this.gridOffsetX,
|
||||
this.gridOffsetY,
|
||||
{
|
||||
onMoveItem: (id, newX, newY, newRotation) => {
|
||||
return this.callbacks.onMoveItem(id, newX, newY, newRotation);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
container.setItem(item);
|
||||
return container;
|
||||
}
|
||||
|
||||
onUpdate(
|
||||
entry: [string, InventoryItem<GameItemMeta>],
|
||||
container: InventoryItemContainer,
|
||||
): void {
|
||||
const [itemId, item] = entry;
|
||||
|
||||
container.setItem(item);
|
||||
}
|
||||
|
||||
onDespawn(container: InventoryItemContainer): void {
|
||||
// TODO: add tween
|
||||
container.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export function createInventoryItemSpawner(
|
||||
scene: Phaser.Scene,
|
||||
inventorySignal: InventorySignal,
|
||||
gridOffsetX: number,
|
||||
gridOffsetY: number,
|
||||
callbacks: InventoryItemSpawnerCallbacks,
|
||||
) {
|
||||
return spawnEffect(
|
||||
new InventoryItemSpawner(
|
||||
scene,
|
||||
inventorySignal,
|
||||
gridOffsetX,
|
||||
gridOffsetY,
|
||||
callbacks,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -49,11 +49,6 @@ export class IndexScene extends ReactiveScene {
|
|||
scene: SceneKey.ShapeViewerScene,
|
||||
y: centerY + 140,
|
||||
},
|
||||
{
|
||||
label: "Inventory Test",
|
||||
scene: SceneKey.InventoryTestScene,
|
||||
y: centerY + 210,
|
||||
},
|
||||
];
|
||||
|
||||
for (const btn of buttons) {
|
||||
|
|
|
|||
|
|
@ -1,189 +0,0 @@
|
|||
import { ReactiveScene } from "boardgame-phaser";
|
||||
import { createButton } from "@/utils/createButton";
|
||||
import { GRID_CONFIG } from "@/config";
|
||||
import { createInventorySignal, moveItem } from "@/state/inventory";
|
||||
import { createItemIn, data } from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { createInventoryItemSpawner } from "@/gameobjects/InventoryItemSpawner";
|
||||
import { SceneKey } from "./types";
|
||||
|
||||
export class InventoryTestScene extends ReactiveScene {
|
||||
private inventorySignal = createInventorySignal();
|
||||
private gridOffsetX = 0;
|
||||
private gridOffsetY = 0;
|
||||
|
||||
constructor() {
|
||||
super("InventoryTestScene");
|
||||
}
|
||||
|
||||
create(): void {
|
||||
super.create();
|
||||
|
||||
const { width, height } = this.scale;
|
||||
const inventory = this.inventorySignal.value;
|
||||
const invWidth = inventory.width;
|
||||
const invHeight = inventory.height;
|
||||
|
||||
this.gridOffsetX = (width - invWidth * GRID_CONFIG.VIEWER_CELL_SIZE) / 2;
|
||||
this.gridOffsetY =
|
||||
(height - invHeight * GRID_CONFIG.VIEWER_CELL_SIZE) / 2 + 40;
|
||||
|
||||
this.drawGrid(invWidth, invHeight);
|
||||
this.setupItemSpawner();
|
||||
|
||||
this.add
|
||||
.text(
|
||||
width / 2,
|
||||
30,
|
||||
"Inventory Signal Test (4x6)",
|
||||
GRID_CONFIG.TITLE_STYLE,
|
||||
)
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.createControls();
|
||||
|
||||
this.add
|
||||
.text(
|
||||
width / 2,
|
||||
height - 40,
|
||||
"Drag items to move them",
|
||||
GRID_CONFIG.SUBTITLE_STYLE,
|
||||
)
|
||||
.setOrigin(0.5);
|
||||
}
|
||||
|
||||
private drawGrid(invWidth: number, invHeight: 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 = this.gridOffsetX + x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const py = this.gridOffsetY + y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
|
||||
const isOccupied = this.inventorySignal.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.inventorySignal,
|
||||
this.gridOffsetX,
|
||||
this.gridOffsetY,
|
||||
{
|
||||
onMoveItem: (
|
||||
itemId: string,
|
||||
newX: number,
|
||||
newY: number,
|
||||
newRotation: number,
|
||||
) => {
|
||||
return moveItem(
|
||||
this.inventorySignal,
|
||||
itemId,
|
||||
newX,
|
||||
newY,
|
||||
newRotation,
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
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 - 300,
|
||||
y: 40,
|
||||
onClick: () => {
|
||||
this.addRandomItem();
|
||||
},
|
||||
});
|
||||
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "移除最后一个",
|
||||
x: width - 150,
|
||||
y: 40,
|
||||
onClick: () => {
|
||||
this.removeLastItem();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private addRandomItem(): void {
|
||||
const items = data.desert.getItems();
|
||||
|
||||
this.inventorySignal.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);
|
||||
});
|
||||
}
|
||||
|
||||
private removeLastItem(): void {
|
||||
this.inventorySignal.produce((inventory) => {
|
||||
const items = Array.from(inventory.items.entries());
|
||||
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [lastId] = items[items.length - 1];
|
||||
inventory.items.delete(lastId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
export enum SceneKey {
|
||||
GridViewerScene = "GridViewerScene",
|
||||
IndexScene = "IndexScene",
|
||||
InventoryTestScene = "InventoryTestScene",
|
||||
MapViewerScene = "MapViewerScene",
|
||||
ShapeViewerScene = "ShapeViewerScene",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
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 | undefined>;
|
||||
private readonly _previewRotation: Signal<number>;
|
||||
|
||||
readonly name: ReadonlySignal<string>;
|
||||
readonly shape: ReadonlySignal<ParsedShape | undefined>;
|
||||
readonly color: ReadonlySignal<number>;
|
||||
readonly transform: ReadonlySignal<Transform2D | undefined>;
|
||||
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 | undefined {
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import { create } from "mutative";
|
||||
import { mutableSignal } from "boardgame-core";
|
||||
import {
|
||||
createGridInventory,
|
||||
createItemIn,
|
||||
data,
|
||||
GameItemMeta,
|
||||
placeItem,
|
||||
removeItemFromGrid,
|
||||
Transform2D,
|
||||
validatePlacement,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
|
||||
function genId() {
|
||||
return Math.random().toString(16).slice(-8);
|
||||
}
|
||||
|
||||
export type InventorySignal = ReturnType<typeof createInventorySignal>;
|
||||
|
||||
export function createInventorySignal() {
|
||||
const inventory = createGridInventory<GameItemMeta>(4, 6);
|
||||
|
||||
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(
|
||||
inventorySignal: InventorySignal,
|
||||
itemId: string,
|
||||
newX: number,
|
||||
newY: number,
|
||||
newRotation?: number,
|
||||
): boolean {
|
||||
const inventory = inventorySignal.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(inventory, (inv) => {
|
||||
removeItemFromGrid(inv, itemId);
|
||||
});
|
||||
const validation = validatePlacement(removed, item.shape, newTransform);
|
||||
if (!validation.valid) return false;
|
||||
|
||||
inventorySignal.produce((inv) => {
|
||||
const item = inv.items.get(itemId)!;
|
||||
removeItemFromGrid(inv, itemId);
|
||||
item.transform = newTransform;
|
||||
placeItem(inv, item);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ 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";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
|
|
@ -12,7 +11,6 @@ export default function App() {
|
|||
<div className="flex-1 flex relative justify-center items-center">
|
||||
<PhaserGame initialScene="IndexScene" config={GAME_CONFIG}>
|
||||
<PhaserScene scene={IndexScene} />
|
||||
<PhaserScene scene={InventoryTestScene} />
|
||||
<PhaserScene scene={MapViewerScene} />
|
||||
<PhaserScene scene={GridViewerScene} />
|
||||
<PhaserScene scene={ShapeViewerScene} />
|
||||
|
|
|
|||
Loading…
Reference in New Issue