Add gun to inventory that fires bullets

This commit is contained in:
Peter Stockings
2026-01-20 21:31:21 +11:00
parent 1713ba76de
commit bac2c130aa
5 changed files with 263 additions and 7 deletions

View File

@@ -0,0 +1,152 @@
import { describe, it, expect, beforeEach } from "vitest";
import { ItemManager } from "../../../scenes/systems/ItemManager";
import { EntityManager } from "../../EntityManager";
import type { World, CombatantActor, RangedWeaponItem, AmmoItem, Item } from "../../../core/types";
import { ITEMS } from "../../../core/config/Items";
// Mock World and EntityManager
const mockWorld: World = {
width: 10,
height: 10,
tiles: new Array(100).fill(0),
actors: new Map(),
exit: { x: 9, y: 9 }
};
describe("Fireable Weapons & Ammo System", () => {
let entityManager: EntityManager;
let itemManager: ItemManager;
let player: CombatantActor;
beforeEach(() => {
entityManager = new EntityManager(mockWorld);
itemManager = new ItemManager(mockWorld, entityManager);
player = {
id: 1,
pos: { x: 0, y: 0 },
category: "combatant",
type: "player",
isPlayer: true,
speed: 1,
energy: 0,
stats: {
maxHp: 100, hp: 100,
maxMana: 50, mana: 50,
attack: 1, defense: 0,
level: 1, exp: 0, expToNextLevel: 100,
critChance: 0, critMultiplier: 0, accuracy: 0, lifesteal: 0,
evasion: 0, blockChance: 0, luck: 0,
statPoints: 0, skillPoints: 0, strength: 0, dexterity: 0, intelligence: 0,
passiveNodes: []
},
inventory: { gold: 0, items: [] },
equipment: {}
};
mockWorld.actors.clear();
mockWorld.actors.set(player.id, player);
});
it("should stack ammo correctly", () => {
// Spawn Ammo pack 1
const ammo1 = { ...ITEMS["ammo_9mm"] } as AmmoItem;
ammo1.quantity = 10;
itemManager.spawnItem(ammo1, { x: 0, y: 0 });
// Pickup
itemManager.tryPickup(player);
expect(player.inventory!.items.length).toBe(1);
expect(player.inventory!.items[0].quantity).toBe(10);
// Spawn Ammo pack 2
const ammo2 = { ...ITEMS["ammo_9mm"] } as AmmoItem;
ammo2.quantity = 5;
itemManager.spawnItem(ammo2, { x: 0, y: 0 });
// Pickup (should merge)
itemManager.tryPickup(player);
expect(player.inventory!.items.length).toBe(1); // Still 1 stack
expect(player.inventory!.items[0].quantity).toBe(15);
});
it("should consume ammo from weapon when fired", () => {
// Manually Equip Pistol
const pistol = { ...ITEMS["pistol"] } as RangedWeaponItem;
// Deep clone stats for test isolation
pistol.stats = { ...pistol.stats };
player.inventory!.items.push(pistol);
// Sanity Check
expect(pistol.stats.currentAmmo).toBe(6);
expect(pistol.stats.magazineSize).toBe(6);
// Simulate Firing (logic mimic from GameScene)
if (pistol.stats.currentAmmo > 0) {
pistol.stats.currentAmmo--;
}
expect(pistol.stats.currentAmmo).toBe(5);
});
it("should reload weapon using inventory ammo", () => {
const pistol = { ...ITEMS["pistol"] } as RangedWeaponItem;
pistol.stats = { ...pistol.stats };
pistol.stats.currentAmmo = 0; // Empty
player.inventory!.items.push(pistol);
const ammo = { ...ITEMS["ammo_9mm"] } as AmmoItem;
ammo.quantity = 10;
player.inventory!.items.push(ammo);
// Logic mimic from GameScene
const needed = pistol.stats.magazineSize - pistol.stats.currentAmmo; // 6
const toTake = Math.min(needed, ammo.quantity); // 6
pistol.stats.currentAmmo += toTake;
ammo.quantity -= toTake;
expect(pistol.stats.currentAmmo).toBe(6);
expect(ammo.quantity).toBe(4);
});
it("should handle partial reload if not enough ammo", () => {
const pistol = { ...ITEMS["pistol"] } as RangedWeaponItem;
pistol.stats = { ...pistol.stats };
pistol.stats.currentAmmo = 0;
player.inventory!.items.push(pistol);
const ammo = { ...ITEMS["ammo_9mm"] } as AmmoItem;
ammo.quantity = 3; // Only 3 bullets
player.inventory!.items.push(ammo);
// Logic mimic
const needed = pistol.stats.magazineSize - pistol.stats.currentAmmo; // 6
const toTake = Math.min(needed, ammo.quantity); // 3
pistol.stats.currentAmmo += toTake;
ammo.quantity -= toTake;
expect(pistol.stats.currentAmmo).toBe(3);
expect(ammo.quantity).toBe(0);
});
it("should deep clone stats on spawn so pistols remain independent", () => {
const pistolDef = ITEMS["pistol"] as RangedWeaponItem;
// Spawn 1
itemManager.spawnItem(pistolDef, {x:0, y:0});
const picked1 = itemManager.tryPickup(player)! as RangedWeaponItem;
// Spawn 2
itemManager.spawnItem(pistolDef, {x:0, y:0});
const picked2 = itemManager.tryPickup(player)! as RangedWeaponItem;
expect(picked1).not.toBe(picked2);
expect(picked1.stats).not.toBe(picked2.stats); // Critical!
// Modifying one should not affect other
picked1.stats.currentAmmo = 0;
expect(picked2.stats.currentAmmo).toBe(6);
});
});

View File

@@ -52,7 +52,7 @@ export function generateWorld(floor: number, runState: RunState): { world: World
items: [
...runState.inventory.items,
// Add starting items for testing if empty
...(runState.inventory.items.length === 0 ? [ITEMS["health_potion"], ITEMS["health_potion"], ITEMS["iron_sword"], ITEMS["throwing_dagger"], ITEMS["throwing_dagger"], ITEMS["throwing_dagger"]] : [])
...(runState.inventory.items.length === 0 ? [ITEMS["health_potion"], ITEMS["health_potion"], ITEMS["iron_sword"], ITEMS["throwing_dagger"], ITEMS["throwing_dagger"], ITEMS["throwing_dagger"], ITEMS["pistol"]] : [])
]
},
energy: 0