Compare commits
No commits in common. "89343c19549145f8737bdd53aa6688600f6ba9b7" and "fe0621cedf3fa37eb5f26b3eadf75788187d63a8" have entirely different histories.
89343c1954
...
fe0621cedf
|
|
@ -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",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue