Compare commits

..

No commits in common. "89343c19549145f8737bdd53aa6688600f6ba9b7" and "fe0621cedf3fa37eb5f26b3eadf75788187d63a8" have entirely different histories.

6 changed files with 200 additions and 278 deletions

View File

@ -11,44 +11,44 @@ import {
} from "boardgame-core/samples/slay-the-spire-like"; } from "boardgame-core/samples/slay-the-spire-like";
import { DisposableBag } from "../../../framework/dist"; import { DisposableBag } from "../../../framework/dist";
import { InventoryItemState } from "@/state/InventoryItemState"; import { InventoryItemState } from "@/state/InventoryItemState";
import { moveItem } from "@/state/inventory";
import { export interface InventoryItemContainerCallbacks {
InventorySurface, onMoveItem: (
InventorySurfaceState, itemId: string,
inventoryToScene, newX: number,
sceneToInventory, newY: number,
} from "@/state/inventorySurfaceState"; newRotation: number,
) => boolean;
}
export class InventoryItemContainer extends Phaser.GameObjects.Container { export class InventoryItemContainer extends Phaser.GameObjects.Container {
private itemState: InventoryItemState; private itemState: InventoryItemState;
private surfaceState: InventorySurfaceState;
private surfaces: Iterable<InventorySurface>;
private hitArea: Point2D[] = []; private hitArea: Point2D[] = [];
constructor( constructor(
scene: Phaser.Scene, scene: Phaser.Scene,
item: GameItem, private gridOffsetX: number,
surface: InventorySurface, private gridOffsetY: number,
surfaces: Iterable<InventorySurface>, private callbacks: InventoryItemContainerCallbacks,
) { ) {
super(scene, surface.gridOffsetX, surface.gridOffsetY); super(scene, gridOffsetX, gridOffsetY);
scene.add.existing(this); scene.add.existing(this);
const cellSize = surface.cellSize;
const graphics = this.scene.add.graphics(); const graphics = this.scene.add.graphics();
graphics.setPosition(cellSize / 2, cellSize / 2); graphics.setPosition(
GRID_CONFIG.VIEWER_CELL_SIZE / 2,
GRID_CONFIG.VIEWER_CELL_SIZE / 2,
);
const label = this.scene.add.text( const label = this.scene.add.text(
cellSize / 2, GRID_CONFIG.VIEWER_CELL_SIZE / 2,
cellSize / 2, GRID_CONFIG.VIEWER_CELL_SIZE / 2,
"", "",
GRID_CONFIG.ITEM_NAME_STYLE, GRID_CONFIG.ITEM_NAME_STYLE,
); );
this.add([graphics, label]); this.add([graphics, label]);
this.setupInteractive(); this.setupInteractive();
this.itemState = new InventoryItemState(item); this.itemState = new InventoryItemState();
this.surfaceState = new InventorySurfaceState(surface);
this.surfaces = surfaces;
const disposables = new DisposableBag(this); const disposables = new DisposableBag(this);
@ -60,9 +60,9 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
disposables.addEffect(() => { disposables.addEffect(() => {
graphics.clear(); graphics.clear();
if (!this.itemState.shape.value) return;
this.renderGraphics( this.renderGraphics(
graphics, graphics,
this.surfaceState.cellSize,
this.itemState.shape.value, this.itemState.shape.value,
this.itemState.color.value, this.itemState.color.value,
); );
@ -79,17 +79,13 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
}); });
disposables.addEffect(() => { disposables.addEffect(() => {
if (!this.itemState.transform.value) return;
this.snapBack(this.itemState.transform.value); this.snapBack(this.itemState.transform.value);
this.itemState.setPreviewRotation(0); this.itemState.setPreviewRotation(0);
}); });
} }
setSurfaceItem(surface: InventorySurface, item: GameItem): void { updateHitArea(value: number): Point2D[] {
this.surfaceState.setSurface(surface);
this.itemState.setItem(item);
}
private updateHitArea(value: number): Point2D[] {
const shape = this.itemState.shape.value; const shape = this.itemState.shape.value;
if (!shape) return []; if (!shape) return [];
@ -100,7 +96,7 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
flipX: false, flipX: false,
flipY: false, flipY: false,
}); });
const cellSize = this.surfaceState.cellSize; const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE;
return cells.map((cell) => ({ return cells.map((cell) => ({
x: (cell.x - cells[0].x) * cellSize, x: (cell.x - cells[0].x) * cellSize,
@ -108,30 +104,34 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
})); }));
} }
private renderGraphics( setItem(item: GameItem): void {
this.itemState.setItem(item);
}
renderGraphics(
graphics: Phaser.GameObjects.Graphics, graphics: Phaser.GameObjects.Graphics,
cellSize: number,
shape: ParsedShape, shape: ParsedShape,
itemColor: number, itemColor: number,
) { ) {
const cells = transformShape(shape, IDENTITY_TRANSFORM); const cells = transformShape(shape, IDENTITY_TRANSFORM);
for (const cell of cells) { for (const cell of cells) {
const localX = (cell.x - cells[0].x) * cellSize; const localX = (cell.x - cells[0].x) * GRID_CONFIG.VIEWER_CELL_SIZE;
const localY = (cell.y - cells[0].y) * cellSize; const localY = (cell.y - cells[0].y) * GRID_CONFIG.VIEWER_CELL_SIZE;
graphics.fillStyle(itemColor); graphics.fillStyle(itemColor);
graphics.fillRect( graphics.fillRect(
localX + 2 - cellSize / 2, localX + 2 - GRID_CONFIG.VIEWER_CELL_SIZE / 2,
localY + 2 - cellSize / 2, localY + 2 - GRID_CONFIG.VIEWER_CELL_SIZE / 2,
cellSize - 4, GRID_CONFIG.VIEWER_CELL_SIZE - 4,
cellSize - 4, GRID_CONFIG.VIEWER_CELL_SIZE - 4,
); );
} }
} }
private setupInteractive() { private setupInteractive() {
this.setScrollFactor(0); this.setScrollFactor(0);
const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE;
this.setInteractive({ this.setInteractive({
hitArea: this, hitArea: this,
hitAreaCallback: ( hitAreaCallback: (
@ -142,9 +142,9 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
hitArea.hitArea.some( hitArea.hitArea.some(
(cell) => (cell) =>
x >= cell.x && x >= cell.x &&
x < cell.x + this.surfaceState.cellSize && x < cell.x + cellSize &&
y >= cell.y && y >= cell.y &&
y < cell.y + this.surfaceState.cellSize, y < cell.y + cellSize,
), ),
useHandCursor: true, useHandCursor: true,
} as Phaser.Types.Input.InputConfiguration); } as Phaser.Types.Input.InputConfiguration);
@ -164,68 +164,60 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
} else if (event.type === DragDropEventType.ALTBUTTON) { } else if (event.type === DragDropEventType.ALTBUTTON) {
this.itemState.addPreviewRotation(90); this.itemState.addPreviewRotation(90);
} else if (event.type === DragDropEventType.UP) { } else if (event.type === DragDropEventType.UP) {
this.setAlpha(1);
const finalRotation = this.itemState.previewRotation.peek(); const finalRotation = this.itemState.previewRotation.peek();
const finalX = this.x; if (!this.handleDragEnd(finalRotation)) {
const finalY = this.y; this.itemState.setPreviewRotation(0);
if (!this.handleDragEnd(finalX, finalY, finalRotation)) {
const t = this.itemState.transform.peek(); const t = this.itemState.transform.peek();
t && this.snapBack(t); t && this.snapBack(t);
} else {
this.itemState.setPreviewRotation(0);
} }
this.setAlpha(1);
this.itemState.setPreviewRotation(0);
startX = startY = 0; startX = startY = 0;
} }
}); });
} }
private handleDragEnd( private handleDragEnd(finalRotation: number): boolean {
finalX: number,
finalY: number,
finalRotation: number,
): boolean {
const item = this.itemState.item; const item = this.itemState.item;
if (!item) return false; if (!item) return false;
const target = this.findDropSurface(finalX, finalY); const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE;
if (!target) return false; 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 ( if (
this.surfaceState.surface === target.surface && clampedX !== item.transform.offset.x ||
item.transform.offset.x === target.x && clampedY !== item.transform.offset.y ||
item.transform.offset.y === target.y && finalRotation !== 0
item.transform.rotation === finalRotation ) {
) return this.callbacks.onMoveItem(
return false; item.id,
clampedX,
return moveItem( clampedY,
this.surfaceState.invSignal, finalRotation,
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; return false;
} }
private snapBack(transform: Transform2D): void { private snapBack(transform: Transform2D): void {
const { x, y } = transform.offset; const { x, y } = transform.offset;
const target = inventoryToScene(this.surfaceState, x, y); const targetX = this.gridOffsetX + x * GRID_CONFIG.VIEWER_CELL_SIZE;
const targetY = this.gridOffsetY + y * GRID_CONFIG.VIEWER_CELL_SIZE;
this.scene.tweens.add({ this.scene.tweens.add({
targets: this, targets: this,
...target, x: targetX,
y: targetY,
duration: 150, duration: 150,
ease: "Power2", ease: "Power2",
}); });

View File

@ -1,53 +1,64 @@
import Phaser from "phaser"; import Phaser from "phaser";
import type { Spawner } from "boardgame-phaser"; import type { Spawner } from "boardgame-phaser";
import type { GameItem } from "boardgame-core/samples/slay-the-spire-like"; import type {
GameItemMeta,
InventoryItem,
} from "boardgame-core/samples/slay-the-spire-like";
import type { InventorySignal } from "@/state/inventory";
import { spawnEffect } from "boardgame-phaser"; import { spawnEffect } from "boardgame-phaser";
import { InventoryItemContainer } from "./InventoryItemContainer"; import { InventoryItemContainer } from "./InventoryItemContainer";
import { InventorySurface } from "@/state/inventorySurfaceState"; import type { InventoryItemContainerCallbacks } from "./InventoryItemContainer";
export interface InventoryItemSpawnerCallbacks extends InventoryItemContainerCallbacks {}
export class InventoryItemSpawner implements Spawner< export class InventoryItemSpawner implements Spawner<
[InventorySurface, GameItem], [string, InventoryItem<GameItemMeta>],
InventoryItemContainer InventoryItemContainer
> { > {
constructor( constructor(
private scene: Phaser.Scene, private scene: Phaser.Scene,
private surfaces: Iterable<InventorySurface>, private inventorySignal: InventorySignal,
private gridOffsetX: number,
private gridOffsetY: number,
private callbacks: InventoryItemSpawnerCallbacks,
) {} ) {}
*getData(): Iterable<[InventorySurface, GameItem]> { *getData(): Iterable<[string, InventoryItem<GameItemMeta>]> {
for (const surface of this.surfaces) { const inventory = this.inventorySignal.value;
const inv = surface.invSignal.value; yield* inventory.items.entries();
for (const item of inv.items.values()) {
yield [surface, item];
}
}
} }
getKey(entry: [InventorySurface, GameItem]): string { getKey(entry: [string, InventoryItem<GameItemMeta>]): string {
return entry[1].id; return entry[0];
} }
onSpawn(entry: [InventorySurface, GameItem]): InventoryItemContainer | null { onSpawn(
const [surface, item] = entry; entry: [string, InventoryItem<GameItemMeta>],
): InventoryItemContainer | null {
const [itemId, item] = entry;
const container = new InventoryItemContainer( const container = new InventoryItemContainer(
this.scene, this.scene,
item, this.gridOffsetX,
surface, this.gridOffsetY,
this.surfaces, {
onMoveItem: (id, newX, newY, newRotation) => {
return this.callbacks.onMoveItem(id, newX, newY, newRotation);
},
},
); );
container.setSurfaceItem(surface, item); container.setItem(item);
return container; return container;
} }
onUpdate( onUpdate(
entry: [InventorySurface, GameItem], entry: [string, InventoryItem<GameItemMeta>],
container: InventoryItemContainer, container: InventoryItemContainer,
): void { ): void {
const [surface, item] = entry; const [itemId, item] = entry;
container.setSurfaceItem(surface, item); container.setItem(item);
} }
onDespawn(container: InventoryItemContainer): void { onDespawn(container: InventoryItemContainer): void {
@ -58,7 +69,18 @@ export class InventoryItemSpawner implements Spawner<
export function createInventoryItemSpawner( export function createInventoryItemSpawner(
scene: Phaser.Scene, scene: Phaser.Scene,
surfaces: Iterable<InventorySurface>, inventorySignal: InventorySignal,
gridOffsetX: number,
gridOffsetY: number,
callbacks: InventoryItemSpawnerCallbacks,
) { ) {
return spawnEffect(new InventoryItemSpawner(scene, surfaces)); return spawnEffect(
new InventoryItemSpawner(
scene,
inventorySignal,
gridOffsetX,
gridOffsetY,
callbacks,
),
);
} }

View File

@ -1,20 +1,15 @@
import { ReactiveScene } from "boardgame-phaser"; import { ReactiveScene } from "boardgame-phaser";
import { createButton } from "@/utils/createButton"; import { createButton } from "@/utils/createButton";
import { GRID_CONFIG } from "@/config"; import { GRID_CONFIG } from "@/config";
import { createInventorySignal, type InventorySignal } from "@/state/inventory"; import { createInventorySignal, moveItem } from "@/state/inventory";
import { createItemIn, data } from "boardgame-core/samples/slay-the-spire-like"; import { createItemIn, data } from "boardgame-core/samples/slay-the-spire-like";
import { createInventoryItemSpawner } from "@/gameobjects/InventoryItemSpawner"; import { createInventoryItemSpawner } from "@/gameobjects/InventoryItemSpawner";
import { SceneKey } from "./types"; import { SceneKey } from "./types";
import { InventorySurface } from "@/state/inventorySurfaceState";
export class InventoryTestScene extends ReactiveScene { export class InventoryTestScene extends ReactiveScene {
private readonly _surfaces: InventorySurface[] = []; private inventorySignal = createInventorySignal();
private _leftInv!: InventorySignal; private gridOffsetX = 0;
private _rightInv!: InventorySignal; private gridOffsetY = 0;
private _leftGridOffsetX = 0;
private _leftGridOffsetY = 0;
private _rightGridOffsetX = 0;
private _rightGridOffsetY = 0;
constructor() { constructor() {
super("InventoryTestScene"); super("InventoryTestScene");
@ -24,62 +19,22 @@ export class InventoryTestScene extends ReactiveScene {
super.create(); super.create();
const { width, height } = this.scale; const { width, height } = this.scale;
const inventory = this.inventorySignal.value;
const invWidth = inventory.width;
const invHeight = inventory.height;
this._leftInv = createInventorySignal(); this.gridOffsetX = (width - invWidth * GRID_CONFIG.VIEWER_CELL_SIZE) / 2;
this._rightInv = createInventorySignal(); this.gridOffsetY =
(height - invHeight * GRID_CONFIG.VIEWER_CELL_SIZE) / 2 + 40;
const invWidth = 4; this.drawGrid(invWidth, invHeight);
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.setupItemSpawner();
this.add this.add
.text( .text(
width / 2, width / 2,
30, 30,
"Inventory Signal Test (4x6 x2)", "Inventory Signal Test (4x6)",
GRID_CONFIG.TITLE_STYLE, GRID_CONFIG.TITLE_STYLE,
) )
.setOrigin(0.5); .setOrigin(0.5);
@ -90,28 +45,24 @@ export class InventoryTestScene extends ReactiveScene {
.text( .text(
width / 2, width / 2,
height - 40, height - 40,
"Drag items between inventories", "Drag items to move them",
GRID_CONFIG.SUBTITLE_STYLE, GRID_CONFIG.SUBTITLE_STYLE,
) )
.setOrigin(0.5); .setOrigin(0.5);
} }
private drawGrid( private drawGrid(invWidth: number, invHeight: number): void {
inv: InventorySignal,
invWidth: number,
invHeight: number,
offsetX: number,
offsetY: number,
): void {
const graphics = this.add.graphics(); const graphics = this.add.graphics();
this.addEffect(() => { this.addEffect(() => {
for (let y = 0; y < invHeight; y++) { for (let y = 0; y < invHeight; y++) {
for (let x = 0; x < invWidth; x++) { for (let x = 0; x < invWidth; x++) {
const px = offsetX + x * GRID_CONFIG.VIEWER_CELL_SIZE; const px = this.gridOffsetX + x * GRID_CONFIG.VIEWER_CELL_SIZE;
const py = offsetY + y * GRID_CONFIG.VIEWER_CELL_SIZE; const py = this.gridOffsetY + y * GRID_CONFIG.VIEWER_CELL_SIZE;
const isOccupied = inv.value.occupiedCells.has(`${x},${y}`); const isOccupied = this.inventorySignal.value.occupiedCells.has(
`${x},${y}`,
);
graphics.fillStyle( graphics.fillStyle(
isOccupied isOccupied
? GRID_CONFIG.CELL_OCCUPIED_COLOR ? GRID_CONFIG.CELL_OCCUPIED_COLOR
@ -136,7 +87,28 @@ export class InventoryTestScene extends ReactiveScene {
} }
private setupItemSpawner(): void { private setupItemSpawner(): void {
const spawner = createInventoryItemSpawner(this, this._surfaces); 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); this.disposables.add(spawner);
} }
@ -155,29 +127,29 @@ export class InventoryTestScene extends ReactiveScene {
createButton({ createButton({
scene: this, scene: this,
label: "添加道具(左)", label: "添加道具",
x: width - 350, x: width - 300,
y: 40, y: 40,
onClick: () => { onClick: () => {
this.addRandomItem(this._leftInv); this.addRandomItem();
}, },
}); });
createButton({ createButton({
scene: this, scene: this,
label: "添加道具(右)", label: "移除最后一个",
x: width - 180, x: width - 150,
y: 40, y: 40,
onClick: () => { onClick: () => {
this.addRandomItem(this._rightInv); this.removeLastItem();
}, },
}); });
} }
private addRandomItem(inv: InventorySignal): void { private addRandomItem(): void {
const items = data.desert.getItems(); const items = data.desert.getItems();
inv.produce((inventory) => { this.inventorySignal.produce((inventory) => {
const usedIndices = new Set<number>(); const usedIndices = new Set<number>();
for (const item of inventory.items.values()) { for (const item of inventory.items.values()) {
@ -201,4 +173,17 @@ export class InventoryTestScene extends ReactiveScene {
createItemIn(inventory, id, itemData); 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);
});
}
} }

View File

@ -7,37 +7,37 @@ import {
} from "boardgame-core/samples/slay-the-spire-like"; } from "boardgame-core/samples/slay-the-spire-like";
export class InventoryItemState { export class InventoryItemState {
private readonly _item: Signal<GameItem>; private readonly _item: Signal<GameItem | undefined>;
private readonly _previewRotation: Signal<number>; private readonly _previewRotation: Signal<number>;
readonly name: ReadonlySignal<string>; readonly name: ReadonlySignal<string>;
readonly shape: ReadonlySignal<ParsedShape>; readonly shape: ReadonlySignal<ParsedShape | undefined>;
readonly color: ReadonlySignal<number>; readonly color: ReadonlySignal<number>;
readonly transform: ReadonlySignal<Transform2D>; readonly transform: ReadonlySignal<Transform2D | undefined>;
readonly previewRotation: ReadonlySignal<number>; readonly previewRotation: ReadonlySignal<number>;
constructor(initialItem: GameItem) { constructor(initialItem?: GameItem) {
this._item = signal(initialItem); this._item = signal(initialItem);
this._previewRotation = signal(0); this._previewRotation = signal(0);
this.name = computed(() => { this.name = computed(() => {
const item = this._item.value; const item = this._item.value;
return item.meta?.itemData.name ?? item?.id ?? ""; return item?.meta?.itemData.name ?? item?.id ?? "";
}); });
this.shape = computed(() => this._item.value.shape); this.shape = computed(() => this._item.value?.shape);
this.color = computed(() => this.computeColor(this._item.value.id ?? "")); this.color = computed(() => this.computeColor(this._item.value?.id ?? ""));
this.transform = computed(() => this._item.value.transform); this.transform = computed(() => this._item.value?.transform);
this.previewRotation = computed(() => { this.previewRotation = computed(() => {
const base = this._item.value.transform.rotation ?? 0; const base = this._item.value?.transform?.rotation ?? 0;
return (base + this._previewRotation.value) % 360; return (base + this._previewRotation.value) % 360;
}); });
} }
get item(): GameItem { get item(): GameItem | undefined {
return this._item.value; return this._item.value;
} }

View File

@ -10,7 +10,6 @@ import {
Transform2D, Transform2D,
validatePlacement, validatePlacement,
} from "boardgame-core/samples/slay-the-spire-like"; } from "boardgame-core/samples/slay-the-spire-like";
import { batch } from "@preact/signals-core";
function genId() { function genId() {
return Math.random().toString(16).slice(-8); return Math.random().toString(16).slice(-8);
@ -18,14 +17,12 @@ function genId() {
export type InventorySignal = ReturnType<typeof createInventorySignal>; export type InventorySignal = ReturnType<typeof createInventorySignal>;
export function createInventorySignal(giveStart = false) { export function createInventorySignal() {
const inventory = createGridInventory<GameItemMeta>(4, 6); const inventory = createGridInventory<GameItemMeta>(4, 6);
if (giveStart) { const startingItems = data.desert.getStartingItems();
const startingItems = data.desert.getStartingItems(); for (const d of startingItems) {
for (const d of startingItems) { createItemIn(inventory, `${d.id}-${genId()}`, d);
createItemIn(inventory, `${d.id}-${genId()}`, d);
}
} }
return mutableSignal(inventory); return mutableSignal(inventory);
@ -36,14 +33,13 @@ export function createInventorySignal(giveStart = false) {
* Returns true if the move was successful, false if the new position is invalid. * Returns true if the move was successful, false if the new position is invalid.
*/ */
export function moveItem( export function moveItem(
from: InventorySignal, inventorySignal: InventorySignal,
to: InventorySignal,
itemId: string, itemId: string,
newX: number, newX: number,
newY: number, newY: number,
newRotation?: number, newRotation?: number,
): boolean { ): boolean {
const inventory = from.value; const inventory = inventorySignal.value;
const item = inventory.items.get(itemId); const item = inventory.items.get(itemId);
if (!item) { if (!item) {
@ -57,22 +53,17 @@ export function moveItem(
flipY: false, flipY: false,
}; };
const removed = create(to.value, (inv) => { const removed = create(inventory, (inv) => {
removeItemFromGrid(inv, itemId); removeItemFromGrid(inv, itemId);
}); });
const validation = validatePlacement(removed, item.shape, newTransform); const validation = validatePlacement(removed, item.shape, newTransform);
if (!validation.valid) return false; if (!validation.valid) return false;
batch(() => { inventorySignal.produce((inv) => {
from.produce((inv) => { const item = inv.items.get(itemId)!;
removeItemFromGrid(inv, itemId); removeItemFromGrid(inv, itemId);
}); item.transform = newTransform;
to.produce((inv) => { placeItem(inv, item);
placeItem(inv, {
...item,
transform: newTransform,
});
});
}); });
return true; return true;

View File

@ -1,68 +0,0 @@
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,
};
}