Fix drag race condition and stale pointer events

Mark items as dragging before updating inventory state to prevent
the spawner from destroying containers mid-drag. Move inventory
removal to drop time for lost items, and add guards to ignore
stale pointer events on inactive containers.
This commit is contained in:
hypercross 2026-04-19 00:43:43 +08:00
parent 5af7140958
commit e14e41461f
3 changed files with 18 additions and 10 deletions

View File

@ -37,9 +37,10 @@ export interface InventoryItemSpawnerOptions {
* Items currently being dragged are excluded from getData() to prevent * Items currently being dragged are excluded from getData() to prevent
* the spawner from respawning them while they're in flight. * the spawner from respawning them while they're in flight.
*/ */
export class InventoryItemSpawner export class InventoryItemSpawner implements Spawner<
implements Spawner<InventoryItem<GameItemMeta>, Phaser.GameObjects.Container> InventoryItem<GameItemMeta>,
{ Phaser.GameObjects.Container
> {
private scene: Phaser.Scene; private scene: Phaser.Scene;
private gameState: MutableSignal<RunState>; private gameState: MutableSignal<RunState>;
private parentContainer: Phaser.GameObjects.Container; private parentContainer: Phaser.GameObjects.Container;
@ -102,6 +103,7 @@ export class InventoryItemSpawner
obj: Phaser.GameObjects.Container, obj: Phaser.GameObjects.Container,
_item: InventoryItem<GameItemMeta>, _item: InventoryItem<GameItemMeta>,
): void { ): void {
obj.removeAllListeners();
obj.destroy(); obj.destroy();
} }
@ -248,6 +250,8 @@ export class InventoryItemSpawner
container: Phaser.GameObjects.Container, container: Phaser.GameObjects.Container,
): void { ): void {
container.on("pointerdown", (pointer: Phaser.Input.Pointer) => { container.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
// Guard against stale events firing on destroyed containers
if (!container.scene || !container.active) return;
if (this.isLocked()) return; if (this.isLocked()) return;
if (this.isDragging()) return; if (this.isDragging()) return;
if (pointer.button === 0) { if (pointer.button === 0) {

View File

@ -140,12 +140,9 @@ export class InventoryWidget {
item: InventoryItem<GameItemMeta>, item: InventoryItem<GameItemMeta>,
itemContainer: Phaser.GameObjects.Container, itemContainer: Phaser.GameObjects.Container,
): void { ): void {
// Remove from inventory state // Mark as dragging FIRST so spawner excludes it from getData().
this.gameState.produce((state) => { // This prevents the spawner effect from destroying the container
removeItemFromGrid(state.inventory, itemId); // when we later update the inventory state.
});
// Mark as dragging so spawner excludes it from getData()
this.itemSpawner.markDragging(itemId); this.itemSpawner.markDragging(itemId);
// Start drag session // Start drag session
@ -169,9 +166,14 @@ export class InventoryWidget {
x: number, x: number,
y: number, y: number,
): void { ): void {
// Remove from inventory since it's dropped outside valid placement
this.gameState.produce((state) => {
removeItemFromGrid(state.inventory, itemId);
});
this.lostItemManager.createLostItem(itemId, shape, transform, meta, x, y); this.lostItemManager.createLostItem(itemId, shape, transform, meta, x, y);
// Unmark dragging — item is now "lost" and no longer in inventory // Unmark dragging — item is now "lost" and managed by LostItemManager
this.itemSpawner.unmarkDragging(itemId); this.itemSpawner.unmarkDragging(itemId);
} }

View File

@ -107,6 +107,8 @@ export class LostItemManager {
container.setInteractive(hitRect, Phaser.Geom.Rectangle.Contains); container.setInteractive(hitRect, Phaser.Geom.Rectangle.Contains);
container.on("pointerdown", (pointer: Phaser.Input.Pointer) => { container.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
// Guard against stale events firing on destroyed containers
if (!container.scene || !container.active) return;
if (this.isDragging()) return; if (this.isDragging()) return;
if (pointer.button === 0) { if (pointer.button === 0) {
this.onLostItemDragStart(itemId, container); this.onLostItemDragStart(itemId, container);