diff --git a/packages/sts-like-viewer/src/gameobjects/InventoryItemContainer.ts b/packages/sts-like-viewer/src/gameobjects/InventoryItemContainer.ts index 22fb4f5..1c85c7f 100644 --- a/packages/sts-like-viewer/src/gameobjects/InventoryItemContainer.ts +++ b/packages/sts-like-viewer/src/gameobjects/InventoryItemContainer.ts @@ -11,44 +11,44 @@ import { } 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; -} +import { moveItem } from "@/state/inventory"; +import { + InventorySurface, + InventorySurfaceState, + inventoryToScene, + sceneToInventory, +} from "@/state/inventorySurfaceState"; export class InventoryItemContainer extends Phaser.GameObjects.Container { private itemState: InventoryItemState; + private surfaceState: InventorySurfaceState; + private surfaces: Iterable; private hitArea: Point2D[] = []; constructor( scene: Phaser.Scene, - private gridOffsetX: number, - private gridOffsetY: number, - private callbacks: InventoryItemContainerCallbacks, + item: GameItem, + surface: InventorySurface, + surfaces: Iterable, ) { - super(scene, gridOffsetX, gridOffsetY); + super(scene, surface.gridOffsetX, surface.gridOffsetY); scene.add.existing(this); + const cellSize = surface.cellSize; const graphics = this.scene.add.graphics(); - graphics.setPosition( - GRID_CONFIG.VIEWER_CELL_SIZE / 2, - GRID_CONFIG.VIEWER_CELL_SIZE / 2, - ); + graphics.setPosition(cellSize / 2, cellSize / 2); const label = this.scene.add.text( - GRID_CONFIG.VIEWER_CELL_SIZE / 2, - GRID_CONFIG.VIEWER_CELL_SIZE / 2, + cellSize / 2, + cellSize / 2, "", GRID_CONFIG.ITEM_NAME_STYLE, ); this.add([graphics, label]); this.setupInteractive(); - this.itemState = new InventoryItemState(); + this.itemState = new InventoryItemState(item); + this.surfaceState = new InventorySurfaceState(surface); + this.surfaces = surfaces; const disposables = new DisposableBag(this); @@ -60,9 +60,9 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container { disposables.addEffect(() => { graphics.clear(); - if (!this.itemState.shape.value) return; this.renderGraphics( graphics, + this.surfaceState.cellSize, this.itemState.shape.value, this.itemState.color.value, ); @@ -79,13 +79,13 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container { }); disposables.addEffect(() => { - if (!this.itemState.transform.value) return; this.snapBack(this.itemState.transform.value); this.itemState.setPreviewRotation(0); }); } - setItem(item: GameItem): void { + setSurfaceItem(surface: InventorySurface, item: GameItem): void { + this.surfaceState.setSurface(surface); this.itemState.setItem(item); } @@ -100,7 +100,7 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container { flipX: false, flipY: false, }); - const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE; + const cellSize = this.surfaceState.cellSize; return cells.map((cell) => ({ x: (cell.x - cells[0].x) * cellSize, @@ -110,28 +110,28 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container { private renderGraphics( graphics: Phaser.GameObjects.Graphics, + cellSize: number, shape: ParsedShape, itemColor: number, ) { const cells = transformShape(shape, IDENTITY_TRANSFORM); for (const cell of cells) { - const localX = (cell.x - cells[0].x) * GRID_CONFIG.VIEWER_CELL_SIZE; - const localY = (cell.y - cells[0].y) * GRID_CONFIG.VIEWER_CELL_SIZE; + const localX = (cell.x - cells[0].x) * cellSize; + const localY = (cell.y - cells[0].y) * cellSize; 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, + localX + 2 - cellSize / 2, + localY + 2 - cellSize / 2, + cellSize - 4, + cellSize - 4, ); } } private setupInteractive() { this.setScrollFactor(0); - const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE; this.setInteractive({ hitArea: this, hitAreaCallback: ( @@ -142,9 +142,9 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container { hitArea.hitArea.some( (cell) => x >= cell.x && - x < cell.x + cellSize && + x < cell.x + this.surfaceState.cellSize && y >= cell.y && - y < cell.y + cellSize, + y < cell.y + this.surfaceState.cellSize, ), useHandCursor: true, } as Phaser.Types.Input.InputConfiguration); @@ -164,58 +164,68 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container { } 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)) { + const finalX = this.x; + const finalY = this.y; + if (!this.handleDragEnd(finalX, finalY, finalRotation)) { const t = this.itemState.transform.peek(); t && this.snapBack(t); } + this.setAlpha(1); this.itemState.setPreviewRotation(0); startX = startY = 0; } }); } - private handleDragEnd(finalRotation: number): boolean { + private handleDragEnd( + finalX: number, + finalY: number, + 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)); + const target = this.findDropSurface(finalX, finalY); + if (!target) return false; if ( - clampedX !== item.transform.offset.x || - clampedY !== item.transform.offset.y || - finalRotation !== item.transform.rotation - ) { - return this.callbacks.onMoveItem( - item.id, - clampedX, - clampedY, - finalRotation, - ); + this.surfaceState.surface === target.surface && + item.transform.offset.x === target.x && + item.transform.offset.y === target.x && + item.transform.rotation === finalRotation + ) + return false; + + return moveItem( + this.surfaceState.invSignal, + target.surface.invSignal, + item.id, + target.x, + target.y, + finalRotation, + ); + } + + private findDropSurface(x: number, y: number) { + for (const surface of this.surfaces) { + const target = sceneToInventory(surface, x, y); + if (target) + return { + surface, + ...target, + }; } - return false; + return null; } 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; + const target = inventoryToScene(this.surfaceState, x, y); this.scene.tweens.add({ targets: this, - x: targetX, - y: targetY, + ...target, duration: 150, ease: "Power2", }); diff --git a/packages/sts-like-viewer/src/gameobjects/InventoryItemSpawner.ts b/packages/sts-like-viewer/src/gameobjects/InventoryItemSpawner.ts index e5e14b8..5bad81a 100644 --- a/packages/sts-like-viewer/src/gameobjects/InventoryItemSpawner.ts +++ b/packages/sts-like-viewer/src/gameobjects/InventoryItemSpawner.ts @@ -1,64 +1,53 @@ 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 type { GameItem } from "boardgame-core/samples/slay-the-spire-like"; import { spawnEffect } from "boardgame-phaser"; import { InventoryItemContainer } from "./InventoryItemContainer"; -import type { InventoryItemContainerCallbacks } from "./InventoryItemContainer"; - -export interface InventoryItemSpawnerCallbacks extends InventoryItemContainerCallbacks {} +import { InventorySurface } from "@/state/inventorySurfaceState"; export class InventoryItemSpawner implements Spawner< - [string, InventoryItem], + [InventorySurface, GameItem], InventoryItemContainer > { constructor( private scene: Phaser.Scene, - private inventorySignal: InventorySignal, - private gridOffsetX: number, - private gridOffsetY: number, - private callbacks: InventoryItemSpawnerCallbacks, + private surfaces: Iterable, ) {} - *getData(): Iterable<[string, InventoryItem]> { - const inventory = this.inventorySignal.value; - yield* inventory.items.entries(); + *getData(): Iterable<[InventorySurface, GameItem]> { + for (const surface of this.surfaces) { + const inv = surface.invSignal.value; + for (const item of inv.items.values()) { + yield [surface, item]; + } + } } - getKey(entry: [string, InventoryItem]): string { - return entry[0]; + getKey(entry: [InventorySurface, GameItem]): string { + return entry[1].id; } - onSpawn( - entry: [string, InventoryItem], - ): InventoryItemContainer | null { - const [itemId, item] = entry; + onSpawn(entry: [InventorySurface, GameItem]): InventoryItemContainer | null { + const [surface, 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); - }, - }, + item, + surface, + this.surfaces, ); - container.setItem(item); + container.setSurfaceItem(surface, item); return container; } onUpdate( - entry: [string, InventoryItem], + entry: [InventorySurface, GameItem], container: InventoryItemContainer, ): void { - const [itemId, item] = entry; + const [surface, item] = entry; - container.setItem(item); + container.setSurfaceItem(surface, item); } onDespawn(container: InventoryItemContainer): void { @@ -69,18 +58,7 @@ export class InventoryItemSpawner implements Spawner< export function createInventoryItemSpawner( scene: Phaser.Scene, - inventorySignal: InventorySignal, - gridOffsetX: number, - gridOffsetY: number, - callbacks: InventoryItemSpawnerCallbacks, + surfaces: Iterable, ) { - return spawnEffect( - new InventoryItemSpawner( - scene, - inventorySignal, - gridOffsetX, - gridOffsetY, - callbacks, - ), - ); + return spawnEffect(new InventoryItemSpawner(scene, surfaces)); } diff --git a/packages/sts-like-viewer/src/state/InventoryItemState.ts b/packages/sts-like-viewer/src/state/InventoryItemState.ts index e0c67f0..b0bfec6 100644 --- a/packages/sts-like-viewer/src/state/InventoryItemState.ts +++ b/packages/sts-like-viewer/src/state/InventoryItemState.ts @@ -7,37 +7,37 @@ import { } from "boardgame-core/samples/slay-the-spire-like"; export class InventoryItemState { - private readonly _item: Signal; + private readonly _item: Signal; private readonly _previewRotation: Signal; readonly name: ReadonlySignal; - readonly shape: ReadonlySignal; + readonly shape: ReadonlySignal; readonly color: ReadonlySignal; - readonly transform: ReadonlySignal; + readonly transform: ReadonlySignal; readonly previewRotation: ReadonlySignal; - constructor(initialItem?: GameItem) { + 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 ?? ""; + 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(() => { - const base = this._item.value?.transform?.rotation ?? 0; + const base = this._item.value.transform.rotation ?? 0; return (base + this._previewRotation.value) % 360; }); } - get item(): GameItem | undefined { + get item(): GameItem { return this._item.value; } diff --git a/packages/sts-like-viewer/src/state/inventory.ts b/packages/sts-like-viewer/src/state/inventory.ts index 161bab9..3a95be8 100644 --- a/packages/sts-like-viewer/src/state/inventory.ts +++ b/packages/sts-like-viewer/src/state/inventory.ts @@ -17,12 +17,14 @@ function genId() { export type InventorySignal = ReturnType; -export function createInventorySignal() { +export function createInventorySignal(giveStart = false) { const inventory = createGridInventory(4, 6); - const startingItems = data.desert.getStartingItems(); - for (const d of startingItems) { - createItemIn(inventory, `${d.id}-${genId()}`, d); + if (giveStart) { + const startingItems = data.desert.getStartingItems(); + for (const d of startingItems) { + createItemIn(inventory, `${d.id}-${genId()}`, d); + } } return mutableSignal(inventory); @@ -33,13 +35,14 @@ export function createInventorySignal() { * Returns true if the move was successful, false if the new position is invalid. */ export function moveItem( - inventorySignal: InventorySignal, + from: InventorySignal, + to: InventorySignal, itemId: string, newX: number, newY: number, newRotation?: number, ): boolean { - const inventory = inventorySignal.value; + const inventory = from.value; const item = inventory.items.get(itemId); if (!item) { @@ -53,17 +56,20 @@ export function moveItem( flipY: false, }; - const removed = create(inventory, (inv) => { + const removed = create(to.value, (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)!; + from.produce((inv) => { removeItemFromGrid(inv, itemId); - item.transform = newTransform; - placeItem(inv, item); + }); + to.produce((inv) => { + placeItem(inv, { + ...item, + transform: newTransform, + }); }); return true; diff --git a/packages/sts-like-viewer/src/state/inventorySurfaceState.ts b/packages/sts-like-viewer/src/state/inventorySurfaceState.ts new file mode 100644 index 0000000..47ac3fc --- /dev/null +++ b/packages/sts-like-viewer/src/state/inventorySurfaceState.ts @@ -0,0 +1,68 @@ +import { InventorySignal } from "./inventory"; +import { MutableSignal, mutableSignal } from "boardgame-core"; + +export type InventorySurface = { + invSignal: InventorySignal; + gridOffsetX: number; + gridOffsetY: number; + cellSize: number; +}; + +export class InventorySurfaceState { + private readonly _signal: MutableSignal; + + 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.cellSize); + const invY = Math.round(y / 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, + }; +}