feat(sts-viewer): add item rotation support in inventory
Introduces the ability to rotate inventory items using the Alt button during drag-and-drop. - Updates `InventoryItemContainer` to handle rotation via `previewRotation` - Adds `newRotation` parameter to `onMoveItem` callback and `moveItem` command - Updates `InventoryItemState` to manage rotation state and compute transformed shapes - Implements rotation logic in `renderGraphics` using `transformShape`
This commit is contained in:
parent
cc796d1e11
commit
c9f573ef86
|
|
@ -10,9 +10,15 @@ 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 { effect } from "@preact/signals-core";
|
||||||
|
|
||||||
export interface InventoryItemContainerCallbacks {
|
export interface InventoryItemContainerCallbacks {
|
||||||
onMoveItem: (itemId: string, newX: number, newY: number) => boolean;
|
onMoveItem: (
|
||||||
|
itemId: string,
|
||||||
|
newX: number,
|
||||||
|
newY: number,
|
||||||
|
newRotation: number,
|
||||||
|
) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
||||||
|
|
@ -44,21 +50,25 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
||||||
label.setText(this.itemState.name.value);
|
label.setText(this.itemState.name.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
disposables.addEffect(() => {
|
disposables.add(
|
||||||
graphics.clear();
|
effect(() => {
|
||||||
if (!this.itemState.shape.value) return;
|
graphics.clear();
|
||||||
const cellRects = this.renderGraphics(
|
if (!this.itemState.shape.value) return;
|
||||||
graphics,
|
const cellRects = this.renderGraphics(
|
||||||
this.itemState.shape.value,
|
graphics,
|
||||||
this.itemState.color.value,
|
this.itemState.shape.value,
|
||||||
);
|
this.itemState.color.value,
|
||||||
|
this.itemState.previewRotation.value,
|
||||||
|
);
|
||||||
|
|
||||||
this.setupInteractive(cellRects);
|
return this.setupInteractive(cellRects);
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
disposables.addEffect(() => {
|
disposables.addEffect(() => {
|
||||||
if (!this.itemState.transform.value) return;
|
if (!this.itemState.transform.value) return;
|
||||||
this.snapBack(this.itemState.transform.value);
|
this.snapBack(this.itemState.transform.value);
|
||||||
|
this.itemState.setPreviewRotation(0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,8 +80,15 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
||||||
graphics: Phaser.GameObjects.Graphics,
|
graphics: Phaser.GameObjects.Graphics,
|
||||||
shape: ParsedShape,
|
shape: ParsedShape,
|
||||||
itemColor: number,
|
itemColor: number,
|
||||||
|
rotation: number,
|
||||||
): { x: number; y: number }[] {
|
): { x: number; y: number }[] {
|
||||||
const cells = transformShape(shape, IDENTITY_TRANSFORM);
|
const transform: Transform2D = {
|
||||||
|
offset: { x: 0, y: 0 },
|
||||||
|
rotation,
|
||||||
|
flipX: false,
|
||||||
|
flipY: false,
|
||||||
|
};
|
||||||
|
const cells = transformShape(shape, transform);
|
||||||
const cellRects: { x: number; y: number }[] = [];
|
const cellRects: { x: number; y: number }[] = [];
|
||||||
|
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
|
|
@ -92,7 +109,7 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
||||||
return cellRects;
|
return cellRects;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupInteractive(cellRects: { x: number; y: number }[]): void {
|
private setupInteractive(cellRects: { x: number; y: number }[]) {
|
||||||
this.setScrollFactor(0);
|
this.setScrollFactor(0);
|
||||||
const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE;
|
const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||||
this.setInteractive({
|
this.setInteractive({
|
||||||
|
|
@ -114,7 +131,8 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
||||||
|
|
||||||
let startX = 0;
|
let startX = 0;
|
||||||
let startY = 0;
|
let startY = 0;
|
||||||
dragDropEventEffect(this, (event) => {
|
return dragDropEventEffect(this, (event) => {
|
||||||
|
console.log(event.type);
|
||||||
if (event.type === DragDropEventType.DOWN) {
|
if (event.type === DragDropEventType.DOWN) {
|
||||||
startX = this.x;
|
startX = this.x;
|
||||||
startY = this.y;
|
startY = this.y;
|
||||||
|
|
@ -122,18 +140,25 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
||||||
} else if (event.type === DragDropEventType.MOVE) {
|
} else if (event.type === DragDropEventType.MOVE) {
|
||||||
this.x = startX + event.deltaX;
|
this.x = startX + event.deltaX;
|
||||||
this.y = startY + event.deltaY;
|
this.y = startY + event.deltaY;
|
||||||
|
} else if (event.type === DragDropEventType.ALTBUTTON) {
|
||||||
|
const current = this.itemState.previewRotation.peek();
|
||||||
|
this.itemState.setPreviewRotation(current + 90);
|
||||||
} else if (event.type === DragDropEventType.UP) {
|
} else if (event.type === DragDropEventType.UP) {
|
||||||
this.setAlpha(1);
|
this.setAlpha(1);
|
||||||
if (!this.handleDragEnd()) {
|
const finalRotation = this.itemState.previewRotation.peek();
|
||||||
|
if (!this.handleDragEnd(finalRotation)) {
|
||||||
|
this.itemState.setPreviewRotation(0);
|
||||||
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);
|
||||||
}
|
}
|
||||||
startX = startY = 0;
|
startX = startY = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleDragEnd(): boolean {
|
private handleDragEnd(finalRotation: number): boolean {
|
||||||
const item = this.itemState.item;
|
const item = this.itemState.item;
|
||||||
if (!item) return false;
|
if (!item) return false;
|
||||||
|
|
||||||
|
|
@ -151,9 +176,15 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
clampedX !== item.transform.offset.x ||
|
clampedX !== item.transform.offset.x ||
|
||||||
clampedY !== item.transform.offset.y
|
clampedY !== item.transform.offset.y ||
|
||||||
|
finalRotation !== 0
|
||||||
) {
|
) {
|
||||||
return this.callbacks.onMoveItem(item.id, clampedX, clampedY);
|
return this.callbacks.onMoveItem(
|
||||||
|
item.id,
|
||||||
|
clampedX,
|
||||||
|
clampedY,
|
||||||
|
finalRotation,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@ export class InventoryItemSpawner implements Spawner<
|
||||||
this.gridOffsetX,
|
this.gridOffsetX,
|
||||||
this.gridOffsetY,
|
this.gridOffsetY,
|
||||||
{
|
{
|
||||||
onMoveItem: (id, newX, newY) => {
|
onMoveItem: (id, newX, newY, newRotation) => {
|
||||||
return this.callbacks.onMoveItem(id, newX, newY);
|
return this.callbacks.onMoveItem(id, newX, newY, newRotation);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,19 @@ export class InventoryTestScene extends ReactiveScene {
|
||||||
this.gridOffsetX,
|
this.gridOffsetX,
|
||||||
this.gridOffsetY,
|
this.gridOffsetY,
|
||||||
{
|
{
|
||||||
onMoveItem: (itemId: string, newX: number, newY: number) => {
|
onMoveItem: (
|
||||||
return moveItem(this.inventorySignal, itemId, newX, newY);
|
itemId: string,
|
||||||
|
newX: number,
|
||||||
|
newY: number,
|
||||||
|
newRotation: number,
|
||||||
|
) => {
|
||||||
|
return moveItem(
|
||||||
|
this.inventorySignal,
|
||||||
|
itemId,
|
||||||
|
newX,
|
||||||
|
newY,
|
||||||
|
newRotation,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,57 @@
|
||||||
import { computed, signal, Signal } from "@preact/signals-core"
|
import { computed, ReadonlySignal, signal, Signal } from "@preact/signals-core";
|
||||||
import { ITEM_COLORS } from "@/config"
|
import { ITEM_COLORS } from "@/config";
|
||||||
import {
|
import {
|
||||||
type GameItem,
|
type GameItem,
|
||||||
type ParsedShape,
|
type ParsedShape,
|
||||||
type Transform2D,
|
type Transform2D,
|
||||||
} 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 | undefined>
|
private readonly _item: Signal<GameItem | undefined>;
|
||||||
|
private readonly _previewRotation: Signal<number>;
|
||||||
|
|
||||||
readonly name: Signal<string>
|
readonly name: ReadonlySignal<string>;
|
||||||
readonly shape: Signal<ParsedShape | undefined>
|
readonly shape: ReadonlySignal<ParsedShape | undefined>;
|
||||||
readonly color: Signal<number>
|
readonly color: ReadonlySignal<number>;
|
||||||
readonly transform: Signal<Transform2D | undefined>
|
readonly transform: ReadonlySignal<Transform2D | undefined>;
|
||||||
|
readonly previewRotation: ReadonlySignal<number>;
|
||||||
|
|
||||||
constructor(initialItem?: GameItem) {
|
constructor(initialItem?: GameItem) {
|
||||||
this._item = signal(initialItem)
|
this._item = signal(initialItem);
|
||||||
|
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(() => {
|
||||||
|
const base = this._item.value?.transform?.rotation ?? 0;
|
||||||
|
return base + this._previewRotation.value;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get item(): GameItem | undefined {
|
get item(): GameItem | undefined {
|
||||||
return this._item.value
|
return this._item.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
setItem(item: GameItem): void {
|
setItem(item: GameItem): void {
|
||||||
this._item.value = item
|
this._item.value = item;
|
||||||
|
this._previewRotation.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreviewRotation(rotation: number): void {
|
||||||
|
this._previewRotation.value = rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private computeColor(itemId: string): number {
|
private computeColor(itemId: string): number {
|
||||||
const hash = itemId.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0)
|
const hash = itemId.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
||||||
return ITEM_COLORS[hash % ITEM_COLORS.length]
|
return ITEM_COLORS[hash % ITEM_COLORS.length];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
GameItemMeta,
|
GameItemMeta,
|
||||||
placeItem,
|
placeItem,
|
||||||
removeItemFromGrid,
|
removeItemFromGrid,
|
||||||
|
Transform2D,
|
||||||
validatePlacement,
|
validatePlacement,
|
||||||
} from "boardgame-core/samples/slay-the-spire-like";
|
} from "boardgame-core/samples/slay-the-spire-like";
|
||||||
|
|
||||||
|
|
@ -36,6 +37,7 @@ export function moveItem(
|
||||||
itemId: string,
|
itemId: string,
|
||||||
newX: number,
|
newX: number,
|
||||||
newY: number,
|
newY: number,
|
||||||
|
newRotation?: number,
|
||||||
): boolean {
|
): boolean {
|
||||||
const inventory = inventorySignal.value;
|
const inventory = inventorySignal.value;
|
||||||
const item = inventory.items.get(itemId);
|
const item = inventory.items.get(itemId);
|
||||||
|
|
@ -44,9 +46,11 @@ export function moveItem(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTransform = {
|
const newTransform: Transform2D = {
|
||||||
...item.transform,
|
|
||||||
offset: { x: newX, y: newY },
|
offset: { x: newX, y: newY },
|
||||||
|
rotation: newRotation === undefined ? item.transform.rotation : newRotation,
|
||||||
|
flipX: false,
|
||||||
|
flipY: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const removed = create(inventory, (inv) => {
|
const removed = create(inventory, (inv) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue