feat: make equipment slots equipable and ensure stat bonuses are granted
This commit is contained in:
@@ -56,7 +56,8 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
{ ...ITEMS["health_potion"], quantity: 2 },
|
||||
ITEMS["iron_sword"],
|
||||
{ ...ITEMS["throwing_dagger"], quantity: 3 },
|
||||
ITEMS["pistol"]
|
||||
ITEMS["pistol"],
|
||||
ITEMS["leather_armor"]
|
||||
] : [])
|
||||
]
|
||||
},
|
||||
|
||||
@@ -299,6 +299,46 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
});
|
||||
|
||||
this.events.on("equip-item", (data: { itemId: string, slotKey: string }) => {
|
||||
const player = this.world.actors.get(this.playerId) as CombatantActor;
|
||||
if (!player || !player.inventory) return;
|
||||
|
||||
const itemIdx = player.inventory.items.findIndex(it => it.id === data.itemId);
|
||||
if (itemIdx === -1) return;
|
||||
const item = player.inventory.items[itemIdx];
|
||||
|
||||
// Type check
|
||||
const isValid = this.isItemValidForSlot(item, data.slotKey);
|
||||
if (!isValid) {
|
||||
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Cannot equip there!", "#ff0000");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle swapping
|
||||
if (!player.equipment) player.equipment = {};
|
||||
const oldItem = (player.equipment as any)[data.slotKey];
|
||||
if (oldItem) {
|
||||
this.handleDeEquipItem(data.slotKey, player, false); // De-equip without emitting UI update yet
|
||||
}
|
||||
|
||||
// Move to equipment
|
||||
player.inventory.items.splice(itemIdx, 1);
|
||||
(player.equipment as any)[data.slotKey] = item;
|
||||
|
||||
// Apply stats
|
||||
this.applyItemStats(player, item, true);
|
||||
|
||||
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, `Equipped ${item.name}`, "#d4af37");
|
||||
this.emitUIUpdate();
|
||||
});
|
||||
|
||||
this.events.on("de-equip-item", (data: { slotKey: string }) => {
|
||||
const player = this.world.actors.get(this.playerId) as CombatantActor;
|
||||
if (!player || !player.equipment) return;
|
||||
|
||||
this.handleDeEquipItem(data.slotKey, player, true);
|
||||
});
|
||||
|
||||
// Right Clicks to cancel targeting
|
||||
this.input.on('pointerdown', (p: Phaser.Input.Pointer) => {
|
||||
if (p.rightButtonDown() && this.targetingSystem.isActive) {
|
||||
@@ -725,4 +765,63 @@ export class GameScene extends Phaser.Scene {
|
||||
};
|
||||
}
|
||||
|
||||
private isItemValidForSlot(item: any, slotKey: string): boolean {
|
||||
if (!item || !item.type) return false;
|
||||
if (item.type === "Weapon") return slotKey === "mainHand" || slotKey === "offHand";
|
||||
if (item.type === "BodyArmour") return slotKey === "bodyArmour";
|
||||
if (item.type === "Helmet") return slotKey === "helmet";
|
||||
if (item.type === "Boots") return slotKey === "boots";
|
||||
if (item.type === "Ring") return slotKey === "ringLeft" || slotKey === "ringRight";
|
||||
if (item.type === "Belt") return slotKey === "belt";
|
||||
if (item.type === "Offhand") return slotKey === "offHand";
|
||||
return false;
|
||||
}
|
||||
|
||||
private applyItemStats(player: CombatantActor, item: any, isAdding: boolean) {
|
||||
if (!item.stats) return;
|
||||
|
||||
const modifier = isAdding ? 1 : -1;
|
||||
|
||||
// Apply stats from ArmourItem or MiscItem
|
||||
if (item.stats.defense) player.stats.defense += item.stats.defense * modifier;
|
||||
if (item.stats.attack) player.stats.attack += item.stats.attack * modifier;
|
||||
if (item.stats.maxHp) {
|
||||
const diff = item.stats.maxHp * modifier;
|
||||
player.stats.maxHp += diff;
|
||||
player.stats.hp = Math.min(player.stats.maxHp, player.stats.hp + (isAdding ? diff : 0));
|
||||
}
|
||||
if (item.stats.maxMana) {
|
||||
const diff = item.stats.maxMana * modifier;
|
||||
player.stats.maxMana += diff;
|
||||
player.stats.mana = Math.min(player.stats.maxMana, player.stats.mana + (isAdding ? diff : 0));
|
||||
}
|
||||
|
||||
// Other secondary stats
|
||||
if (item.stats.critChance) player.stats.critChance += item.stats.critChance * modifier;
|
||||
if (item.stats.accuracy) player.stats.accuracy += item.stats.accuracy * modifier;
|
||||
if (item.stats.evasion) player.stats.evasion += item.stats.evasion * modifier;
|
||||
if (item.stats.blockChance) player.stats.blockChance += item.stats.blockChance * modifier;
|
||||
}
|
||||
|
||||
private handleDeEquipItem(slotKey: string, player: CombatantActor, emitUpdate: boolean) {
|
||||
if (!player.equipment) return;
|
||||
const item = (player.equipment as any)[slotKey];
|
||||
if (!item) return;
|
||||
|
||||
// Remove from equipment
|
||||
delete (player.equipment as any)[slotKey];
|
||||
|
||||
// Remove stats
|
||||
this.applyItemStats(player, item, false);
|
||||
|
||||
// Add back to inventory
|
||||
if (!player.inventory) player.inventory = { gold: 0, items: [] };
|
||||
player.inventory.items.push(item);
|
||||
|
||||
if (emitUpdate) {
|
||||
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, `De-equipped ${item.name}`, "#aaaaaa");
|
||||
this.emitUIUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -300,6 +300,62 @@ export class InventoryOverlay extends OverlayComponent {
|
||||
this.setupDragEvents();
|
||||
}
|
||||
|
||||
private highlightCompatibleSlots(item: any) {
|
||||
if (!item || !item.type) return;
|
||||
|
||||
this.equipmentSlots.forEach((container, key) => {
|
||||
let compatible = false;
|
||||
|
||||
// Simple type compatibility check
|
||||
if (item.type === "Weapon" && (key === "mainHand" || key === "offHand")) compatible = true;
|
||||
else if (item.type === "BodyArmour" && key === "bodyArmour") compatible = true;
|
||||
else if (item.type === "Helmet" && key === "helmet") compatible = true;
|
||||
else if (item.type === "Boots" && key === "boots") compatible = true;
|
||||
else if (item.type === "Ring" && (key === "ringLeft" || key === "ringRight")) compatible = true;
|
||||
else if (item.type === "Belt" && key === "belt") compatible = true;
|
||||
else if (item.type === "Offhand" && key === "offHand") compatible = true;
|
||||
|
||||
if (compatible) {
|
||||
const graphics = container.list[0] as Phaser.GameObjects.Graphics;
|
||||
if (graphics) {
|
||||
graphics.clear();
|
||||
const size = (key === "bodyArmour") ? 58 : (key === "belt") ? 32 : (key === "boots") ? 46 : (key.startsWith("ring")) ? 38 : 46;
|
||||
|
||||
// Glowing border
|
||||
graphics.lineStyle(4, 0xffd700, 1);
|
||||
graphics.strokeRect(-size / 2, -size / 2, size, size);
|
||||
|
||||
graphics.lineStyle(1, 0x8b7355, 1);
|
||||
graphics.strokeRect(-size / 2 + 2, -size / 2 + 2, size - 4, size - 4);
|
||||
|
||||
graphics.fillStyle(0x4a3a3a, 1);
|
||||
graphics.fillRect(-size / 2 + 3, -size / 2 + 3, size - 6, size - 6);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private clearHighlights() {
|
||||
this.equipmentSlots.forEach((container, key) => {
|
||||
const graphics = container.list[0] as Phaser.GameObjects.Graphics;
|
||||
if (graphics) {
|
||||
graphics.clear();
|
||||
const slotBorder = 0xd4af37;
|
||||
const slotBg = 0x3a2a2a;
|
||||
const size = (key === "bodyArmour") ? 58 : (key === "belt") ? 32 : (key === "boots") ? 46 : (key.startsWith("ring")) ? 38 : 46;
|
||||
|
||||
graphics.lineStyle(2, slotBorder, 1);
|
||||
graphics.strokeRect(-size / 2, -size / 2, size, size);
|
||||
|
||||
graphics.lineStyle(1, 0x8b7355, 1);
|
||||
graphics.strokeRect(-size / 2 + 2, -size / 2 + 2, size - 4, size - 4);
|
||||
|
||||
graphics.fillStyle(slotBg, 1);
|
||||
graphics.fillRect(-size / 2 + 3, -size / 2 + 3, size - 6, size - 6);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupDragEvents() {
|
||||
this.scene.input.on("dragstart", (pointer: Phaser.Input.Pointer, gameObject: any) => {
|
||||
const gameScene = this.scene.scene.get("GameScene") as any;
|
||||
@@ -334,6 +390,10 @@ export class InventoryOverlay extends OverlayComponent {
|
||||
}
|
||||
this.dragIcon.setPosition(pointer.x, pointer.y);
|
||||
|
||||
if (item.type !== "Consumable" && item.type !== "Currency" && item.type !== "Ammo") {
|
||||
this.highlightCompatibleSlots(item);
|
||||
}
|
||||
|
||||
// Ghost original
|
||||
const sprite = gameObject.list.find((child: any) => child instanceof Phaser.GameObjects.Sprite);
|
||||
if (sprite) sprite.setAlpha(0.3);
|
||||
@@ -359,6 +419,7 @@ export class InventoryOverlay extends OverlayComponent {
|
||||
this.draggedEquipmentKey = null;
|
||||
|
||||
if (this.dragIcon) this.dragIcon.setVisible(false);
|
||||
this.clearHighlights();
|
||||
|
||||
// Reset alpha
|
||||
const sprite = gameObject.list.find((child: any) => child instanceof Phaser.GameObjects.Sprite);
|
||||
@@ -380,9 +441,30 @@ export class InventoryOverlay extends OverlayComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check Backpack (for swapping/reordering) - ONLY if dragged from backpack
|
||||
// Check Equipment Slots
|
||||
if (isFromBackpack && this.isPointerOver(pointer.x, pointer.y)) {
|
||||
const targetEqKey = this.getEquipmentSlotAt(pointer.x, pointer.y);
|
||||
if (targetEqKey) {
|
||||
gameScene.events.emit("equip-item", {
|
||||
itemId: item.id,
|
||||
slotKey: targetEqKey
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check Backpack (for swapping/reordering or de-equipping)
|
||||
if (this.isPointerOver(pointer.x, pointer.y)) {
|
||||
const targetIndex = this.getBackpackSlotAt(pointer.x, pointer.y);
|
||||
|
||||
if (!isFromBackpack) {
|
||||
// De-equip item
|
||||
gameScene.events.emit("de-equip-item", {
|
||||
slotKey: startEqKey
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetIndex !== null && targetIndex !== startIndex) {
|
||||
const items = player.inventory.items;
|
||||
const itemToMove = items[startIndex!];
|
||||
@@ -408,6 +490,24 @@ export class InventoryOverlay extends OverlayComponent {
|
||||
});
|
||||
}
|
||||
|
||||
private getEquipmentSlotAt(x: number, y: number): string | null {
|
||||
// Relative to container
|
||||
const localX = x - this.container.x;
|
||||
const localY = y - this.container.y;
|
||||
|
||||
for (const [key, slot] of this.equipmentSlots.entries()) {
|
||||
const size = (key === "bodyArmour") ? 58 : (key === "belt") ? 32 : (key === "boots") ? 46 : (key.startsWith("ring")) ? 38 : 46;
|
||||
const halfSize = size / 2;
|
||||
const dx = localX - slot.x;
|
||||
const dy = localY - slot.y;
|
||||
|
||||
if (dx >= -halfSize && dx <= halfSize && dy >= -halfSize && dy <= halfSize) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private getBackpackSlotAt(x: number, y: number): number | null {
|
||||
// Relative to container
|
||||
const localX = x - this.container.x;
|
||||
|
||||
Reference in New Issue
Block a user