refactor: support multiple inventory surfaces
Introduce `InventorySurface` to allow items to be moved between different inventory grids. - Update `InventoryItemContainer` to use surface-specific cell sizes and coordinate transformations. - Update `InventoryItemSpawner` to handle multiple surfaces. - Refactor `moveItem` to support transferring items from one `InventorySignal` to another. - Add `InventorySurfaceState` to manage surface-specific metadata. - Remove reliance on global `GRID_CONFIG` in favor of surface data.
This commit is contained in:
parent
093eadbefd
commit
422ddde200
|
|
@ -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<InventorySurface>;
|
||||
private hitArea: Point2D[] = [];
|
||||
|
||||
constructor(
|
||||
scene: Phaser.Scene,
|
||||
private gridOffsetX: number,
|
||||
private gridOffsetY: number,
|
||||
private callbacks: InventoryItemContainerCallbacks,
|
||||
item: GameItem,
|
||||
surface: InventorySurface,
|
||||
surfaces: Iterable<InventorySurface>,
|
||||
) {
|
||||
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",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<GameItemMeta>],
|
||||
[InventorySurface, GameItem],
|
||||
InventoryItemContainer
|
||||
> {
|
||||
constructor(
|
||||
private scene: Phaser.Scene,
|
||||
private inventorySignal: InventorySignal,
|
||||
private gridOffsetX: number,
|
||||
private gridOffsetY: number,
|
||||
private callbacks: InventoryItemSpawnerCallbacks,
|
||||
private surfaces: Iterable<InventorySurface>,
|
||||
) {}
|
||||
|
||||
*getData(): Iterable<[string, InventoryItem<GameItemMeta>]> {
|
||||
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<GameItemMeta>]): string {
|
||||
return entry[0];
|
||||
getKey(entry: [InventorySurface, GameItem]): string {
|
||||
return entry[1].id;
|
||||
}
|
||||
|
||||
onSpawn(
|
||||
entry: [string, InventoryItem<GameItemMeta>],
|
||||
): 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<GameItemMeta>],
|
||||
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<InventorySurface>,
|
||||
) {
|
||||
return spawnEffect(
|
||||
new InventoryItemSpawner(
|
||||
scene,
|
||||
inventorySignal,
|
||||
gridOffsetX,
|
||||
gridOffsetY,
|
||||
callbacks,
|
||||
),
|
||||
);
|
||||
return spawnEffect(new InventoryItemSpawner(scene, surfaces));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,37 +7,37 @@ import {
|
|||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
|
||||
export class InventoryItemState {
|
||||
private readonly _item: Signal<GameItem | undefined>;
|
||||
private readonly _item: Signal<GameItem>;
|
||||
private readonly _previewRotation: Signal<number>;
|
||||
|
||||
readonly name: ReadonlySignal<string>;
|
||||
readonly shape: ReadonlySignal<ParsedShape | undefined>;
|
||||
readonly shape: ReadonlySignal<ParsedShape>;
|
||||
readonly color: ReadonlySignal<number>;
|
||||
readonly transform: ReadonlySignal<Transform2D | undefined>;
|
||||
readonly transform: ReadonlySignal<Transform2D>;
|
||||
readonly previewRotation: ReadonlySignal<number>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,12 +17,14 @@ function genId() {
|
|||
|
||||
export type InventorySignal = ReturnType<typeof createInventorySignal>;
|
||||
|
||||
export function createInventorySignal() {
|
||||
export function createInventorySignal(giveStart = false) {
|
||||
const inventory = createGridInventory<GameItemMeta>(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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import { InventorySignal } from "./inventory";
|
||||
import { MutableSignal, mutableSignal } from "boardgame-core";
|
||||
|
||||
export type InventorySurface = {
|
||||
invSignal: InventorySignal;
|
||||
gridOffsetX: number;
|
||||
gridOffsetY: number;
|
||||
cellSize: number;
|
||||
};
|
||||
|
||||
export class InventorySurfaceState {
|
||||
private readonly _signal: MutableSignal<InventorySurface>;
|
||||
|
||||
constructor(init: InventorySurface) {
|
||||
this._signal = mutableSignal(init);
|
||||
}
|
||||
|
||||
public get surface() {
|
||||
return this._signal.value;
|
||||
}
|
||||
|
||||
public get invSignal() {
|
||||
return this._signal.value.invSignal;
|
||||
}
|
||||
|
||||
public get gridOffsetX() {
|
||||
return this._signal.value.gridOffsetX;
|
||||
}
|
||||
|
||||
public get gridOffsetY() {
|
||||
return this._signal.value.gridOffsetY;
|
||||
}
|
||||
|
||||
public get cellSize() {
|
||||
return this._signal.value.cellSize;
|
||||
}
|
||||
|
||||
public setSurface(surface: InventorySurface) {
|
||||
this._signal.value = surface;
|
||||
}
|
||||
}
|
||||
|
||||
export function sceneToInventory(
|
||||
surface: InventorySurface | InventorySurfaceState,
|
||||
x: number,
|
||||
y: number,
|
||||
): { x: number; y: number } | null {
|
||||
const invX = Math.round(x / surface.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,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue