Auto reload last reloadble weapon when reload is triggered

This commit is contained in:
2026-01-28 18:32:52 +11:00
parent 90aebc892a
commit f01d8de15c
4 changed files with 436 additions and 277 deletions

View File

@@ -163,6 +163,7 @@ export type Inventory = {
export type RunState = { export type RunState = {
stats: Stats; stats: Stats;
inventory: Inventory; inventory: Inventory;
lastReloadableWeaponId?: string | null;
}; };
export interface BaseActor { export interface BaseActor {

View File

@@ -42,7 +42,8 @@ export class GameScene extends Phaser.Scene {
public runState: RunState = { public runState: RunState = {
stats: { ...GAME_CONFIG.player.initialStats }, stats: { ...GAME_CONFIG.player.initialStats },
inventory: { gold: 0, items: [] } inventory: { gold: 0, items: [] },
lastReloadableWeaponId: null
}; };
public gameInput!: GameInput; public gameInput!: GameInput;
@@ -388,14 +389,16 @@ export class GameScene extends Phaser.Scene {
this.runState = { this.runState = {
stats: { ...p.stats }, stats: { ...p.stats },
inventory: { gold: p.inventory.gold, items: [...p.inventory.items] } inventory: { gold: p.inventory.gold, items: [...p.inventory.items] },
lastReloadableWeaponId: this.runState.lastReloadableWeaponId
}; };
} }
public restartGame() { public restartGame() {
this.runState = { this.runState = {
stats: { ...GAME_CONFIG.player.initialStats }, stats: { ...GAME_CONFIG.player.initialStats },
inventory: { gold: 0, items: [] } inventory: { gold: 0, items: [] },
lastReloadableWeaponId: null
}; };
this.floorIndex = 1; this.floorIndex = 1;
this.loadFloor(this.floorIndex); this.loadFloor(this.floorIndex);
@@ -436,6 +439,8 @@ export class GameScene extends Phaser.Scene {
if (item.currentAmmo > 0) { if (item.currentAmmo > 0) {
item.currentAmmo--; item.currentAmmo--;
} }
// Track as last used reloadable weapon
this.runState.lastReloadableWeaponId = item.id;
} }
this.dungeonRenderer.showProjectile( this.dungeonRenderer.showProjectile(
@@ -483,6 +488,7 @@ export class GameScene extends Phaser.Scene {
if (ammoItem && ammoItem.quantity && ammoItem.quantity > 0) { if (ammoItem && ammoItem.quantity && ammoItem.quantity > 0) {
item.reloadingTurnsLeft = GAME_CONFIG.player.reloadDuration; item.reloadingTurnsLeft = GAME_CONFIG.player.reloadDuration;
this.runState.lastReloadableWeaponId = item.id;
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Reloading...", "#aaaaaa"); this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Reloading...", "#aaaaaa");
console.log(`Started reloading ${item.name}. Duration: ${item.reloadingTurnsLeft}`); console.log(`Started reloading ${item.name}. Duration: ${item.reloadingTurnsLeft}`);

View File

@@ -64,6 +64,13 @@ export class PlayerInputHandler {
} }
} }
if (!weaponToReload && this.scene.runState.lastReloadableWeaponId) {
const item = player.inventory.items.find(it => it.id === this.scene.runState.lastReloadableWeaponId);
if (item && item.type === "Weapon" && item.weaponType === "ranged") {
weaponToReload = item;
}
}
if (weaponToReload) { if (weaponToReload) {
this.scene.startReload(player, weaponToReload); this.scene.startReload(player, weaponToReload);
} }

View File

@@ -0,0 +1,145 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock Phaser
vi.mock('phaser', () => {
class MockEventEmitter {
on = vi.fn().mockReturnThis();
once = vi.fn().mockReturnThis();
emit = vi.fn().mockReturnThis();
off = vi.fn().mockReturnThis();
removeAllListeners = vi.fn().mockReturnThis();
}
return {
default: {
Events: {
EventEmitter: MockEventEmitter
},
Scene: class {
events = new MockEventEmitter();
add = {
graphics: vi.fn(),
sprite: vi.fn(),
};
}
}
};
});
import { PlayerInputHandler } from '../PlayerInputHandler';
import { GameScene } from '../../GameScene';
import type { CombatantActor, RangedWeaponItem } from '../../../core/types';
// Minimal mock for GameScene
const createMockScene = () => {
const scene = {
gameInput: {
on: vi.fn(),
},
targetingSystem: {
itemId: null,
isActive: false,
},
entityAccessor: {
getPlayer: vi.fn(),
},
runState: {
lastReloadableWeaponId: null,
},
startReload: vi.fn(),
} as any;
return scene;
};
describe('Reload Last Used Weapon Logic', () => {
let scene: any;
let inputHandler: PlayerInputHandler;
let reloadCallback: Function;
beforeEach(() => {
scene = createMockScene();
inputHandler = new PlayerInputHandler(scene);
inputHandler.registerListeners();
// Find the reload listener
const reloadCall = scene.gameInput.on.mock.calls.find((call: any[]) => call[0] === 'reload');
reloadCallback = reloadCall[1];
});
it('should reload the last reloadable weapon if nothing else is targeted or equipped', () => {
const pistol: RangedWeaponItem = {
id: 'pistol-1',
name: 'Pistol',
type: 'Weapon',
weaponType: 'ranged',
currentAmmo: 0,
reloadingTurnsLeft: 0,
stats: { attack: 1, range: 5, magazineSize: 6, ammoType: '9mm', projectileSpeed: 10 },
textureKey: 'weapons',
spriteIndex: 0
};
const player: CombatantActor = {
id: 1,
pos: { x: 0, y: 0 },
category: 'combatant',
isPlayer: true,
type: 'player',
inventory: { items: [pistol], gold: 0 },
equipment: { mainHand: { type: 'Weapon', weaponType: 'melee', id: 'sword-1' } as any },
stats: {} as any,
energy: 100,
speed: 100
};
scene.entityAccessor.getPlayer.mockReturnValue(player);
scene.runState.lastReloadableWeaponId = 'pistol-1';
// Trigger reload (simulating 'R' press)
reloadCallback();
expect(scene.startReload).toHaveBeenCalledWith(player, pistol);
});
it('should prioritize targeted item over last used', () => {
const pistol1: RangedWeaponItem = { id: 'p1', name: 'P1', type: 'Weapon', weaponType: 'ranged' } as any;
const pistol2: RangedWeaponItem = { id: 'p2', name: 'P2', type: 'Weapon', weaponType: 'ranged' } as any;
const player: CombatantActor = {
id: 1, inventory: { items: [pistol1, pistol2] }, equipment: {}
} as any;
scene.entityAccessor.getPlayer.mockReturnValue(player);
scene.targetingSystem.itemId = 'p2';
scene.runState.lastReloadableWeaponId = 'p1';
reloadCallback();
expect(scene.startReload).toHaveBeenCalledWith(player, pistol2);
});
it('should prioritize equipped ranged weapon over last used', () => {
const pistol1: RangedWeaponItem = { id: 'p1', name: 'P1', type: 'Weapon', weaponType: 'ranged' } as any;
const pistol2: RangedWeaponItem = { id: 'p2', name: 'P2', type: 'Weapon', weaponType: 'ranged' } as any;
const player: CombatantActor = {
id: 1, inventory: { items: [pistol1, pistol2] }, equipment: { mainHand: pistol2 }
} as any;
scene.entityAccessor.getPlayer.mockReturnValue(player);
scene.runState.lastReloadableWeaponId = 'p1';
reloadCallback();
expect(scene.startReload).toHaveBeenCalledWith(player, pistol2);
});
it('should do nothing if no weapon is found', () => {
const player: CombatantActor = { id: 1, inventory: { items: [] }, equipment: {} } as any;
scene.entityAccessor.getPlayer.mockReturnValue(player);
reloadCallback();
expect(scene.startReload).not.toHaveBeenCalled();
});
});