From ff6b6bfb734d951631d85f6749b1f4797ff1805a Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Wed, 21 Jan 2026 15:18:42 +1100 Subject: [PATCH] feat: make items in backpack draggable to and from quickslot --- src/scenes/GameScene.ts | 17 ++- src/ui/GameUI.ts | 14 +- src/ui/components/InventoryOverlay.ts | 165 +++++++++++++++++++++++- src/ui/components/OverlayComponent.ts | 18 +++ src/ui/components/QuickSlotComponent.ts | 83 ++++++++++-- 5 files changed, 273 insertions(+), 24 deletions(-) diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index 7d7e5a5..8749ee3 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -272,8 +272,21 @@ export class GameScene extends Phaser.Scene { const item = this.itemManager.getItem(player, data.itemId); if (!item) return; - // Drop position is simply on the player's current tile - const dropPos = { x: player.pos.x, y: player.pos.y }; + // Determine drop position based on pointer or player pos + let dropPos = { x: player.pos.x, y: player.pos.y }; + if (data.pointerX !== undefined && data.pointerY !== undefined) { + const tilePos = this.getPointerTilePos({ x: data.pointerX, y: data.pointerY } as Phaser.Input.Pointer); + + // Limit drop distance to 1 tile from player for balance/fairness + const dx = Math.sign(tilePos.x - player.pos.x); + const dy = Math.sign(tilePos.y - player.pos.y); + const targetX = player.pos.x + dx; + const targetY = player.pos.y + dy; + + if (inBounds(this.world, targetX, targetY) && !isBlocked(this.world, targetX, targetY, this.entityManager)) { + dropPos = { x: targetX, y: targetY }; + } + } // Remove from inventory and spawn in world if (this.itemManager.removeFromInventory(player, data.itemId)) { diff --git a/src/ui/GameUI.ts b/src/ui/GameUI.ts index 13babc9..e9db2df 100644 --- a/src/ui/GameUI.ts +++ b/src/ui/GameUI.ts @@ -9,13 +9,13 @@ import { QuickSlotComponent } from "./components/QuickSlotComponent"; import { ActionButtonComponent } from "./components/ActionButtonComponent"; export default class GameUI extends Phaser.Scene { - private hud: HudComponent; - private menu: MenuComponent; - private inventory: InventoryOverlay; - private character: CharacterOverlay; - private death: DeathOverlay; - private quickSlots: QuickSlotComponent; - private actionButtons: ActionButtonComponent; + public hud: HudComponent; + public menu: MenuComponent; + public inventory: InventoryOverlay; + public character: CharacterOverlay; + public death: DeathOverlay; + public quickSlots: QuickSlotComponent; + public actionButtons: ActionButtonComponent; constructor() { super({ key: "GameUI" }); diff --git a/src/ui/components/InventoryOverlay.ts b/src/ui/components/InventoryOverlay.ts index 19e56dd..ab3ddf6 100644 --- a/src/ui/components/InventoryOverlay.ts +++ b/src/ui/components/InventoryOverlay.ts @@ -5,6 +5,9 @@ import { type CombatantActor } from "../../core/types"; export class InventoryOverlay extends OverlayComponent { private equipmentSlots: Map = new Map(); private backpackSlots: Phaser.GameObjects.Container[] = []; + private dragIcon: Phaser.GameObjects.Sprite | null = null; + private draggedItemIndex: number | null = null; + private draggedEquipmentKey: string | null = null; protected setupContent() { // Base overlay is 700x500, so we need to fit within those bounds @@ -216,13 +219,20 @@ export class InventoryOverlay extends OverlayComponent { update(player: CombatantActor) { if (!player.inventory) return; - // Clear existing items from slots + // Clear existing items from backpack slots this.backpackSlots.forEach(slot => { if (slot.list.length > 1) { slot.removeBetween(1, undefined, true); } }); + // Clear existing items from equipment slots + this.equipmentSlots.forEach(slot => { + if (slot.list.length > 1) { + slot.removeBetween(1, undefined, true); + } + }); + // Populate items player.inventory.items.forEach((item, index) => { if (index >= this.backpackSlots.length) return; @@ -257,11 +267,162 @@ export class InventoryOverlay extends OverlayComponent { slot.add(display); } - // Add interactivity + // Add interactivity for backpack items slot.setInteractive(new Phaser.Geom.Rectangle(-22, -22, 44, 44), Phaser.Geom.Rectangle.Contains); + slot.setData("index", index); + slot.setData("equipmentKey", undefined); // Explicitly clear to avoid confusion + this.scene.input.setDraggable(slot); + slot.on("pointerdown", () => { console.log("Clicked item:", item); }); }); + + // Populate equipment slots + if (player.equipment) { + Object.entries(player.equipment).forEach(([key, item]) => { + if (!item) return; + const slot = this.equipmentSlots.get(key); + if (!slot) return; + + const sprite = this.scene.add.sprite(0, 0, item.textureKey, item.spriteIndex); + sprite.setScale(2.2); + slot.add(sprite); + + // Add interactivity + const size = (key === "bodyArmour") ? 58 : (key === "belt") ? 32 : (key === "boots") ? 46 : (key.startsWith("ring")) ? 38 : 46; + slot.setInteractive(new Phaser.Geom.Rectangle(-size/2, -size/2, size, size), Phaser.Geom.Rectangle.Contains); + slot.setData("equipmentKey", key); + this.scene.input.setDraggable(slot); + }); + } + + this.setupDragEvents(); + } + + private setupDragEvents() { + this.scene.input.on("dragstart", (pointer: Phaser.Input.Pointer, gameObject: any) => { + const gameScene = this.scene.scene.get("GameScene") as any; + const player = gameScene.world.actors.get(gameScene.playerId); + if (!player) return; + + let item: any = null; + + // Check if it's a backpack slot or equipment slot + const index = gameObject.getData("index"); + const eqKey = gameObject.getData("equipmentKey"); + + if (index !== undefined && this.backpackSlots.includes(gameObject)) { + item = player.inventory?.items[index]; + this.draggedItemIndex = index; + this.draggedEquipmentKey = null; + } else if (eqKey !== undefined && this.equipmentSlots.get(eqKey) === gameObject) { + item = player.equipment?.[eqKey]; + this.draggedItemIndex = null; + this.draggedEquipmentKey = eqKey; + } + + if (!item) return; + + // Setup drag icon + if (!this.dragIcon) { + this.dragIcon = this.scene.add.sprite(0, 0, item.textureKey ?? "items", item.spriteIndex); + this.dragIcon.setDepth(2500).setScale(2.5).setAlpha(0.7); + } else { + this.dragIcon.setTexture(item.textureKey ?? "items", item.spriteIndex); + this.dragIcon.setVisible(true); + } + this.dragIcon.setPosition(pointer.x, pointer.y); + + // Ghost original + const sprite = gameObject.list.find((child: any) => child instanceof Phaser.GameObjects.Sprite); + if (sprite) sprite.setAlpha(0.3); + }); + + this.scene.input.on("drag", (pointer: Phaser.Input.Pointer) => { + if (this.dragIcon && this.dragIcon.visible) { + this.dragIcon.setPosition(pointer.x, pointer.y); + } + }); + + this.scene.input.on("dragend", (pointer: Phaser.Input.Pointer, gameObject: any) => { + if (this.draggedItemIndex === null && this.draggedEquipmentKey === null) return; + + const isFromBackpack = this.draggedItemIndex !== null; + if (isFromBackpack && !this.backpackSlots.includes(gameObject)) return; + if (!isFromBackpack && this.equipmentSlots.get(this.draggedEquipmentKey!) !== gameObject) return; + + const startIndex = this.draggedItemIndex; + const startEqKey = this.draggedEquipmentKey; + + this.draggedItemIndex = null; + this.draggedEquipmentKey = null; + + if (this.dragIcon) this.dragIcon.setVisible(false); + + // Reset alpha + const sprite = gameObject.list.find((child: any) => child instanceof Phaser.GameObjects.Sprite); + if (sprite) sprite.setAlpha(1.0); + + const gameUI = this.scene as any; + const gameScene = this.scene.scene.get("GameScene") as any; + const player = gameScene.world.actors.get(gameScene.playerId); + + const item = isFromBackpack ? player.inventory.items[startIndex!] : (player.equipment as any)[startEqKey!]; + + // Check Quick Slots + if (gameUI.quickSlots && gameUI.quickSlots.isPointerOver(pointer.x, pointer.y)) { + const targetSlot = gameUI.quickSlots.getSlotIndexAt(pointer.x, pointer.y); + if (targetSlot !== null) { + gameUI.quickSlots.assignItem(targetSlot, item.id); + console.log(`Assigned backpack item ${item.name} to quick slot ${targetSlot}`); + } + return; + } + + // Check Backpack (for swapping/reordering) - ONLY if dragged from backpack + if (isFromBackpack && this.isPointerOver(pointer.x, pointer.y)) { + const targetIndex = this.getBackpackSlotAt(pointer.x, pointer.y); + if (targetIndex !== null && targetIndex !== startIndex) { + const items = player.inventory.items; + const itemToMove = items[startIndex!]; + + // Remove from old position + items.splice(startIndex!, 1); + + // Insert at new position (clamped to end of list) + const finalTargetIndex = Math.min(targetIndex, items.length); + items.splice(finalTargetIndex, 0, itemToMove); + + gameScene.events.emit("request-ui-update"); + } + return; + } + + // Drop on ground + gameScene.events.emit("drop-item", { + itemId: item.id, + pointerX: pointer.x, + pointerY: pointer.y + }); + }); + } + + private getBackpackSlotAt(x: number, y: number): number | null { + // Relative to container + const localX = x - this.container.x; + const localY = y - this.container.y; + + for (let i = 0; i < this.backpackSlots.length; i++) { + const slot = this.backpackSlots[i]; + const halfSize = 22; // slotSize 44 / 2 + const dx = localX - slot.x; + const dy = localY - slot.y; + + if (dx >= -halfSize && dx <= halfSize && dy >= -halfSize && dy <= halfSize) { + return i; + } + } + return null; } } diff --git a/src/ui/components/OverlayComponent.ts b/src/ui/components/OverlayComponent.ts index cd71093..dceaf12 100644 --- a/src/ui/components/OverlayComponent.ts +++ b/src/ui/components/OverlayComponent.ts @@ -37,4 +37,22 @@ export abstract class OverlayComponent { } protected onOpen() {} + + public isPointerOver(x: number, y: number): boolean { + if (!this.isOpen || !this.container.visible) return false; + + // Get world bounds of the first child (the background rectangle) + const bg = this.container.list[0] as Phaser.GameObjects.Rectangle; + if (!bg) return false; + + // Local coordinates in container are centered at 0,0 + const halfW = bg.width / 2; + const halfH = bg.height / 2; + + // Container position is fixed on screen (scrollFactor 0) + const localX = x - this.container.x; + const localY = y - this.container.y; + + return localX >= -halfW && localX <= halfW && localY >= -halfH && localY <= halfH; + } } diff --git a/src/ui/components/QuickSlotComponent.ts b/src/ui/components/QuickSlotComponent.ts index 6531340..97961f2 100644 --- a/src/ui/components/QuickSlotComponent.ts +++ b/src/ui/components/QuickSlotComponent.ts @@ -71,7 +71,10 @@ export class QuickSlotComponent { // Drag and Drop Events this.scene.input.on("dragstart", (pointer: Phaser.Input.Pointer, gameObject: Phaser.GameObjects.Container) => { - const index = gameObject.getData("index") as number; + // Only handle if it's one of our slots + const index = gameObject.getData("index"); + if (index === undefined || !this.slots.includes(gameObject)) return; + const item = this.itemMap[index]; if (!item) return; @@ -99,7 +102,7 @@ export class QuickSlotComponent { }); this.scene.input.on("dragend", (pointer: Phaser.Input.Pointer, gameObject: Phaser.GameObjects.Container) => { - if (this.draggedSlotIndex === null) return; + if (this.draggedSlotIndex === null || !this.slots.includes(gameObject)) return; const startIndex = this.draggedSlotIndex; this.draggedSlotIndex = null; @@ -138,18 +141,26 @@ export class QuickSlotComponent { this.assignedIds[targetIndex] = temp; console.log(`Moved/Swapped slot ${startIndex} to ${targetIndex}`); } else if (targetIndex === null) { - // Dropped outside - drop on ground - const item = this.itemMap[startIndex]; - if (item) { - const gameScene = this.scene.scene.get("GameScene") as any; - gameScene.events.emit("drop-item", { - itemId: item.id, - pointerX: pointer.x, - pointerY: pointer.y - }); - - // Clear the slot + // Check if dropped over inventory backpack + const gameUI = this.scene as any; + if (gameUI.inventory && gameUI.inventory.isPointerOver(pointer.x, pointer.y)) { + // Clear the quick slot (returning to backpack) this.assignedIds[startIndex] = ""; + console.log(`Cleared quick slot ${startIndex} (returned to backpack)`); + } else { + // Dropped outside - drop on ground + const item = this.itemMap[startIndex]; + if (item) { + const gameScene = this.scene.scene.get("GameScene") as any; + gameScene.events.emit("drop-item", { + itemId: item.id, + pointerX: pointer.x, + pointerY: pointer.y + }); + + // Clear the slot + this.assignedIds[startIndex] = ""; + } } } @@ -262,4 +273,50 @@ export class QuickSlotComponent { console.log(`Slot ${index + 1} is empty`); } } + + public isPointerOver(x: number, y: number): boolean { + const slotSize = 48; + const slotSpacing = 4; + const totalWidth = (slotSize + slotSpacing) * 10 - slotSpacing; + + const localX = x - this.container.x; + const localY = y - this.container.y; + + return localX >= 0 && localX <= totalWidth && localY >= 0 && localY <= slotSize; + } + + public getSlotIndexAt(x: number, y: number): number | null { + const slotSize = 48; + const slotSpacing = 4; + + const localX = x - this.container.x; + const localY = y - this.container.y; + + if (localY >= 0 && localY <= slotSize) { + const index = Math.floor(localX / (slotSize + slotSpacing)); + const remainder = localX % (slotSize + slotSpacing); + + if (index >= 0 && index < 10 && remainder <= slotSize) { + return index; + } + } + return null; + } + + public assignItem(index: number, itemId: string) { + if (index >= 0 && index < 10) { + // Prevent duplicate assignments + const existingIndex = this.assignedIds.indexOf(itemId); + if (existingIndex !== -1 && existingIndex !== index) { + this.assignedIds[existingIndex] = ""; + console.log(`Cleared duplicate assignment of ${itemId} from slot ${existingIndex}`); + } + + this.assignedIds[index] = itemId; + + // Refresh UI + const gameScene = this.scene.scene.get("GameScene"); + gameScene.events.emit("request-ui-update"); + } + } }