feat: make items in backpack draggable to and from quickslot
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -5,6 +5,9 @@ import { type CombatantActor } from "../../core/types";
|
||||
export class InventoryOverlay extends OverlayComponent {
|
||||
private equipmentSlots: Map<string, Phaser.GameObjects.Container> = 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user