Add ammo counter for ranged items in quickslot

This commit is contained in:
Peter Stockings
2026-01-20 21:35:34 +11:00
parent bac2c130aa
commit d4f763d1d0
5 changed files with 131 additions and 14 deletions

View File

@@ -315,6 +315,10 @@ export class DungeonRenderer {
this.fxRenderer.showAlert(x, y);
}
showFloatingText(x: number, y: number, message: string, color: string) {
this.fxRenderer.showFloatingText(x, y, message, color);
}
showProjectile(from: Vec2, to: Vec2, itemId: string, onComplete: () => void) {
// World coords
const startX = from.x * TILE_SIZE + TILE_SIZE / 2;

View File

@@ -174,6 +174,56 @@ export class GameScene extends Phaser.Scene {
if (itemIdx === -1) return;
const item = player.inventory.items[itemIdx];
// Ranged Weapon Logic
if (item.type === "Weapon" && item.weaponType === "ranged") {
// Check Ammo
if (item.stats.currentAmmo <= 0) {
// Try Reload
const ammoId = `ammo_${item.stats.ammoType}`;
const ammoItem = player.inventory.items.find(it => it.id === ammoId); // Simple check
if (ammoItem && ammoItem.quantity && ammoItem.quantity > 0) {
const needed = item.stats.magazineSize - item.stats.currentAmmo;
const toTake = Math.min(needed, ammoItem.quantity);
item.stats.currentAmmo += toTake;
ammoItem.quantity -= toTake;
if (ammoItem.quantity <= 0) {
player.inventory.items = player.inventory.items.filter(it => it !== ammoItem);
}
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Reloaded!", "#00ff00");
console.log("Reloaded. Ammo:", item.stats.currentAmmo);
this.commitPlayerAction({ type: "wait" });
this.emitUIUpdate();
} else {
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "No Ammo!", "#ff0000");
console.log("No ammo found for", item.name);
}
return;
}
// Has ammo, start targeting
if (this.targetingSystem.isActive && this.targetingSystem.itemId === item.id) {
// Already targeting - execute shoot
if (this.targetingSystem.cursorPos) {
this.executeThrow(this.targetingSystem.cursorPos.x, this.targetingSystem.cursorPos.y);
}
return;
}
this.targetingSystem.startTargeting(
item.id,
player.pos,
this.world,
this.dungeonRenderer.seenArray,
this.world.width
);
this.emitUIUpdate();
return;
}
const result = this.itemManager.handleUse(data.itemId, player);
if (result.success && result.consumed) {
@@ -565,10 +615,12 @@ export class GameScene extends Phaser.Scene {
this.playerId,
this.entityManager,
(blockedPos, hitActorId, item) => {
// Damage Logic
if (hitActorId !== undefined) {
const victim = this.world.actors.get(hitActorId) as CombatantActor;
if (victim) {
const dmg = item.stats?.attack ?? 1;
const stats = 'stats' in item ? item.stats : undefined;
const dmg = (stats && 'attack' in stats) ? (stats.attack ?? 1) : 1;
victim.stats.hp -= dmg;
this.dungeonRenderer.showDamage(victim.pos.x, victim.pos.y, dmg);
this.dungeonRenderer.shakeCamera();
@@ -576,13 +628,30 @@ export class GameScene extends Phaser.Scene {
}
const player = this.world.actors.get(this.playerId) as CombatantActor;
// Projectile Visuals
let projectileId = item.id;
if (item.type === "Weapon" && item.weaponType === "ranged") {
projectileId = `ammo_${item.stats.ammoType}`; // Show ammo sprite
// Consume Ammo
if (item.stats.currentAmmo > 0) {
item.stats.currentAmmo--;
}
}
this.dungeonRenderer.showProjectile(
player.pos,
blockedPos,
item.id,
projectileId,
() => {
// Drop the actual item at the landing spot
this.itemManager.spawnItem(item, blockedPos);
// Only drop item if it acts as a thrown item (Consumable/Misc), NOT Weapon
const shouldDrop = item.type !== "Weapon";
if (shouldDrop) {
// Drop the actual item at the landing spot
this.itemManager.spawnItem(item, blockedPos);
}
// Trigger destruction/interaction
if (tryDestructTile(this.world, blockedPos.x, blockedPos.y)) {
@@ -590,7 +659,7 @@ export class GameScene extends Phaser.Scene {
}
this.targetingSystem.cancel();
this.commitPlayerAction({ type: "throw" });
this.commitPlayerAction({ type: "throw" }); // Or 'attack' if shooting? 'throw' is fine for now
this.emitUIUpdate();
}
);

View File

@@ -37,12 +37,18 @@ export class ItemManager {
spawnItem(item: Item, pos: Vec2): void {
if (!this.world || !this.entityManager) return;
// Deep clone item (crucial for items with mutable stats like ammo)
const clonedItem = { ...item } as Item;
if ('stats' in clonedItem && clonedItem.stats) {
(clonedItem as any).stats = { ...clonedItem.stats };
}
const id = this.entityManager.getNextId();
const drop: ItemDropActor = {
id,
pos: { x: pos.x, y: pos.y },
category: "item_drop",
item: { ...item } // Clone item
item: clonedItem
};
this.entityManager.addActor(drop);
@@ -61,7 +67,20 @@ export class ItemManager {
if (itemActor) {
const item = itemActor.item;
// Stacking Logic
if (item.stackable) {
const existingItem = player.inventory.items.find(it => it.id === item.id);
if (existingItem) {
existingItem.quantity = (existingItem.quantity || 1) + (item.quantity || 1);
console.log(`Stacked ${item.name}. New quantity: ${existingItem.quantity}`);
this.entityManager.removeActor(itemActor.id);
return existingItem;
}
}
// Add to inventory
item.quantity = item.quantity || 1;
player.inventory.items.push(item);
// Remove from world
@@ -91,7 +110,7 @@ export class ItemManager {
const item = player.inventory.items[itemIdx];
// Check if item is a healing consumable
if (item.stats && item.stats.hp && item.stats.hp > 0) {
if (item.type === "Consumable" && item.stats?.hp) {
const healAmount = item.stats.hp;
if (player.stats.hp >= player.stats.maxHp) {
@@ -100,8 +119,12 @@ export class ItemManager {
player.stats.hp = Math.min(player.stats.hp + healAmount, player.stats.maxHp);
// Remove item after use
player.inventory.items.splice(itemIdx, 1);
// Consume item (check stack)
if (item.quantity && item.quantity > 1) {
item.quantity--;
} else {
player.inventory.items.splice(itemIdx, 1);
}
return {
success: true,
@@ -110,8 +133,8 @@ export class ItemManager {
};
}
// Throwable items are handled by TargetingSystem, not here
if (item.throwable) {
// Throwable items
if (item.type === "Consumable" && item.throwable) {
return {
success: true,
consumed: false,

View File

@@ -79,8 +79,16 @@ export class TargetingSystem {
}
const item = player.inventory.items[itemIdx];
// Remove item from inventory before throw
player.inventory.items.splice(itemIdx, 1);
// Only remove if it's a consumable throwable
if (item.type === "Consumable" && item.throwable) {
// Handle stack decrement if applicable, or remove
if (item.quantity && item.quantity > 1) {
item.quantity--;
} else {
player.inventory.items.splice(itemIdx, 1);
}
}
const start = player.pos;
const end = { x: this.cursor.x, y: this.cursor.y };

View File

@@ -6,7 +6,7 @@ export class QuickSlotComponent {
private container!: Phaser.GameObjects.Container;
private slots: Phaser.GameObjects.Container[] = [];
private itemMap: (Item | null)[] = new Array(10).fill(null);
private assignedIds: string[] = ["health_potion", "throwing_dagger", ...new Array(8).fill("")];
private assignedIds: string[] = ["health_potion", "pistol", "throwing_dagger", ...new Array(7).fill("")];
constructor(scene: Phaser.Scene) {
this.scene = scene;
@@ -125,6 +125,19 @@ export class QuickSlotComponent {
}).setOrigin(1, 1);
slot.add(countText);
}
// Add Ammo Counter for Ranged Weapons (Top-Right)
if (foundItem.type === "Weapon" && foundItem.weaponType === "ranged" && foundItem.stats) {
const ammoText = `${foundItem.stats.currentAmmo}/${foundItem.stats.magazineSize}`;
const ammoDisplay = this.scene.add.text(slotSize - 2, 2, ammoText, {
fontSize: "10px",
color: "#00FF00", // Green text
fontStyle: "bold",
stroke: "#000000",
strokeThickness: 2
}).setOrigin(1, 0); // Top-right anchor
slot.add(ammoDisplay);
}
}
} else {
this.itemMap[i] = null;