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";
|
||||
import { DisposableBag } from "../../../framework/dist";
|
||||
import { InventoryItemState } from "@/state/InventoryItemState";
|
||||
import { effect } from "@preact/signals-core";
|
||||
|
||||
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 {
|
||||
|
|
@ -44,21 +50,25 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
|||
label.setText(this.itemState.name.value);
|
||||
});
|
||||
|
||||
disposables.addEffect(() => {
|
||||
disposables.add(
|
||||
effect(() => {
|
||||
graphics.clear();
|
||||
if (!this.itemState.shape.value) return;
|
||||
const cellRects = this.renderGraphics(
|
||||
graphics,
|
||||
this.itemState.shape.value,
|
||||
this.itemState.color.value,
|
||||
this.itemState.previewRotation.value,
|
||||
);
|
||||
|
||||
this.setupInteractive(cellRects);
|
||||
});
|
||||
return this.setupInteractive(cellRects);
|
||||
}),
|
||||
);
|
||||
|
||||
disposables.addEffect(() => {
|
||||
if (!this.itemState.transform.value) return;
|
||||
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,
|
||||
shape: ParsedShape,
|
||||
itemColor: number,
|
||||
rotation: 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 }[] = [];
|
||||
|
||||
for (const cell of cells) {
|
||||
|
|
@ -92,7 +109,7 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
|||
return cellRects;
|
||||
}
|
||||
|
||||
private setupInteractive(cellRects: { x: number; y: number }[]): void {
|
||||
private setupInteractive(cellRects: { x: number; y: number }[]) {
|
||||
this.setScrollFactor(0);
|
||||
const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
this.setInteractive({
|
||||
|
|
@ -114,7 +131,8 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
|||
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
dragDropEventEffect(this, (event) => {
|
||||
return dragDropEventEffect(this, (event) => {
|
||||
console.log(event.type);
|
||||
if (event.type === DragDropEventType.DOWN) {
|
||||
startX = this.x;
|
||||
startY = this.y;
|
||||
|
|
@ -122,18 +140,25 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
|||
} else if (event.type === DragDropEventType.MOVE) {
|
||||
this.x = startX + event.deltaX;
|
||||
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) {
|
||||
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();
|
||||
t && this.snapBack(t);
|
||||
} else {
|
||||
this.itemState.setPreviewRotation(0);
|
||||
}
|
||||
startX = startY = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleDragEnd(): boolean {
|
||||
private handleDragEnd(finalRotation: number): boolean {
|
||||
const item = this.itemState.item;
|
||||
if (!item) return false;
|
||||
|
||||
|
|
@ -151,9 +176,15 @@ export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
|||
|
||||
if (
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ export class InventoryItemSpawner implements Spawner<
|
|||
this.gridOffsetX,
|
||||
this.gridOffsetY,
|
||||
{
|
||||
onMoveItem: (id, newX, newY) => {
|
||||
return this.callbacks.onMoveItem(id, newX, newY);
|
||||
onMoveItem: (id, newX, newY, newRotation) => {
|
||||
return this.callbacks.onMoveItem(id, newX, newY, newRotation);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -93,8 +93,19 @@ export class InventoryTestScene extends ReactiveScene {
|
|||
this.gridOffsetX,
|
||||
this.gridOffsetY,
|
||||
{
|
||||
onMoveItem: (itemId: string, newX: number, newY: number) => {
|
||||
return moveItem(this.inventorySignal, itemId, newX, newY);
|
||||
onMoveItem: (
|
||||
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 { ITEM_COLORS } from "@/config"
|
||||
import { computed, ReadonlySignal, signal, Signal } from "@preact/signals-core";
|
||||
import { ITEM_COLORS } from "@/config";
|
||||
import {
|
||||
type GameItem,
|
||||
type ParsedShape,
|
||||
type Transform2D,
|
||||
} from "boardgame-core/samples/slay-the-spire-like"
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
|
||||
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 shape: Signal<ParsedShape | undefined>
|
||||
readonly color: Signal<number>
|
||||
readonly transform: Signal<Transform2D | undefined>
|
||||
readonly name: ReadonlySignal<string>;
|
||||
readonly shape: ReadonlySignal<ParsedShape | undefined>;
|
||||
readonly color: ReadonlySignal<number>;
|
||||
readonly transform: ReadonlySignal<Transform2D | undefined>;
|
||||
readonly previewRotation: ReadonlySignal<number>;
|
||||
|
||||
constructor(initialItem?: GameItem) {
|
||||
this._item = signal(initialItem)
|
||||
this._item = signal(initialItem);
|
||||
this._previewRotation = signal(0);
|
||||
|
||||
this.name = computed(() => {
|
||||
const item = this._item.value
|
||||
return item?.meta?.itemData.name ?? item?.id ?? ""
|
||||
})
|
||||
const item = this._item.value;
|
||||
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 {
|
||||
return this._item.value
|
||||
return this._item.value;
|
||||
}
|
||||
|
||||
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 {
|
||||
const hash = itemId.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0)
|
||||
return ITEM_COLORS[hash % ITEM_COLORS.length]
|
||||
const hash = itemId.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
||||
return ITEM_COLORS[hash % ITEM_COLORS.length];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
GameItemMeta,
|
||||
placeItem,
|
||||
removeItemFromGrid,
|
||||
Transform2D,
|
||||
validatePlacement,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ export function moveItem(
|
|||
itemId: string,
|
||||
newX: number,
|
||||
newY: number,
|
||||
newRotation?: number,
|
||||
): boolean {
|
||||
const inventory = inventorySignal.value;
|
||||
const item = inventory.items.get(itemId);
|
||||
|
|
@ -44,9 +46,11 @@ export function moveItem(
|
|||
return false;
|
||||
}
|
||||
|
||||
const newTransform = {
|
||||
...item.transform,
|
||||
const newTransform: Transform2D = {
|
||||
offset: { x: newX, y: newY },
|
||||
rotation: newRotation === undefined ? item.transform.rotation : newRotation,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
};
|
||||
|
||||
const removed = create(inventory, (inv) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue