Add reload logic for ranged weapons

This commit is contained in:
Peter Stockings
2026-01-27 17:16:46 +11:00
parent 7260781f38
commit 165cde6ca3
7 changed files with 129 additions and 26 deletions

View File

@@ -38,7 +38,8 @@ export const GAME_CONFIG = {
passiveNodes: [] as string[]
},
speed: 100,
viewRadius: 8
viewRadius: 8,
reloadDuration: 3,
},
map: {

View File

@@ -157,6 +157,7 @@ export function createRangedWeapon(
textureKey: t.textureKey,
spriteIndex: t.spriteIndex,
currentAmmo: t.magazineSize,
reloadingTurnsLeft: 0,
variant,
stats: {
attack: t.attack + attackBonus,

View File

@@ -101,6 +101,7 @@ export interface RangedWeaponItem extends BaseItem {
type: "Weapon";
weaponType: "ranged";
currentAmmo: number; // Runtime state - moved to top level for easier access
reloadingTurnsLeft: number;
stats: {
attack: number;
range: number;

View File

@@ -7,7 +7,8 @@ import {
createMeleeWeapon,
createRangedWeapon,
createArmour,
createUpgradeScroll
createUpgradeScroll,
createAmmo
} from "../../core/config/Items";
import { seededRandom } from "../../core/math";
import * as ROT from "rot-js";
@@ -60,6 +61,7 @@ export function generateWorld(floor: number, runState: RunState): { world: World
createMeleeWeapon("iron_sword", "sharp"),
createConsumable("throwing_dagger", 3),
createRangedWeapon("pistol"),
createAmmo("ammo_9mm", 10),
createArmour("leather_armor", "heavy"),
createUpgradeScroll(2)
] : [])

View File

@@ -1,4 +1,3 @@
// Reading types.ts to verify actor structure before next step
import Phaser from "phaser";
import {
type EntityId,
@@ -6,7 +5,9 @@ import {
type Action,
type RunState,
type World,
type UIUpdatePayload
type UIUpdatePayload,
type CombatantActor,
type RangedWeaponItem,
} from "../core/types";
import { TILE_SIZE } from "../core/constants";
import { inBounds, isBlocked, isPlayerOnExit, tryDestructTile } from "../engine/world/world-logic";
@@ -126,6 +127,33 @@ export class GameScene extends Phaser.Scene {
this.input.keyboard?.on("keydown-C", () => {
this.events.emit("toggle-character");
});
this.input.keyboard?.on("keydown-R", () => {
const player = this.entityAccessor.getPlayer();
if (!player || !player.inventory) return;
// Check for active targeted item first
const activeId = this.targetingSystem.itemId;
let weaponToReload: RangedWeaponItem | null = null;
if (activeId) {
const item = player.inventory.items.find(it => it.id === activeId);
if (item && item.type === "Weapon" && item.weaponType === "ranged") {
weaponToReload = item;
}
}
// If no active targeted weapon, check main hand
if (!weaponToReload && player.equipment?.mainHand) {
const item = player.equipment.mainHand;
if (item.type === "Weapon" && item.weaponType === "ranged") {
weaponToReload = item;
}
}
if (weaponToReload) {
this.startReload(player, weaponToReload);
}
});
this.input.keyboard?.on("keydown-SPACE", () => {
if (!this.awaitingPlayer) return;
@@ -192,29 +220,19 @@ export class GameScene extends Phaser.Scene {
if (item.type === "Weapon" && item.weaponType === "ranged") {
// Check Ammo
if (item.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.currentAmmo;
const toTake = Math.min(needed, ammoItem.quantity);
item.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.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);
if (item.reloadingTurnsLeft > 0) {
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Reloading...", "#aaaaaa");
return;
}
// Try Reload
this.startReload(player, item);
return;
}
// Is it already reloading?
if (item.reloadingTurnsLeft > 0) {
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Reloading...", "#aaaaaa");
return;
}
@@ -597,6 +615,44 @@ export class GameScene extends Phaser.Scene {
this.awaitingPlayer = false;
this.cameraController.enableFollowMode();
// Process reloading progress
const player = this.entityAccessor.getPlayer();
if (player && player.inventory) {
// Check all items for reloading (usually only equipped or active)
for (const item of player.inventory.items) {
if (item.type === "Weapon" && item.weaponType === "ranged" && item.reloadingTurnsLeft > 0) {
item.reloadingTurnsLeft--;
if (item.reloadingTurnsLeft === 0) {
// Finalize Reload
const ammoId = `ammo_${item.stats.ammoType}`;
const ammoItem = player.inventory.items.find(it => it.id === ammoId);
if (ammoItem && ammoItem.quantity && ammoItem.quantity > 0) {
const needed = item.stats.magazineSize - item.currentAmmo;
const toTake = Math.min(needed, ammoItem.quantity);
item.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 ${item.name}. Ammo:`, item.currentAmmo);
} else {
// Should be checked at startReload, but safe fallback
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "No Ammo!", "#ff0000");
}
} else {
// Show reloading progress
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Reloading...", "#aaaaaa");
}
}
}
}
// Check for pickups right after move (before enemy turn, so you get it efficiently)
if (action.type === "move") {
const player = this.entityAccessor.getPlayer();
@@ -847,6 +903,34 @@ export class GameScene extends Phaser.Scene {
}
}
private startReload(player: CombatantActor, item: RangedWeaponItem) {
if (item.currentAmmo >= item.stats.magazineSize) {
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Full!", "#aaaaaa");
return;
}
if (item.reloadingTurnsLeft > 0) return;
const ammoId = `ammo_${item.stats.ammoType}`;
const ammoItem = player.inventory?.items.find(it => it.id === ammoId);
if (ammoItem && ammoItem.quantity && ammoItem.quantity > 0) {
item.reloadingTurnsLeft = GAME_CONFIG.player.reloadDuration;
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Reloading...", "#aaaaaa");
console.log(`Started reloading ${item.name}. Duration: ${item.reloadingTurnsLeft}`);
if (this.targetingSystem.isActive && this.targetingSystem.itemId === item.id) {
this.targetingSystem.cancel();
}
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);
}
}
private getPointerTilePos(pointer: Phaser.Input.Pointer): { x: number, y: number } {
const worldPoint = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
return {

View File

@@ -266,6 +266,7 @@ describe("InventoryUtils", () => {
textureKey: "items",
spriteIndex: 7,
currentAmmo: 6,
reloadingTurnsLeft: 0,
stats: {
attack: 8,
range: 5,
@@ -315,6 +316,7 @@ describe("InventoryUtils", () => {
textureKey: "items",
spriteIndex: 7,
currentAmmo: 4,
reloadingTurnsLeft: 0,
stats: {
attack: 8,
range: 5,

View File

@@ -250,6 +250,18 @@ export class QuickSlotComponent {
}).setOrigin(1, 1);
slot.add(display);
}
// Reloading overlay
if (foundItem.type === "Weapon" && foundItem.weaponType === "ranged" && foundItem.reloadingTurnsLeft > 0) {
const reloadText = this.scene.add.text(slotSize / 2, slotSize / 2, "RELOADING", {
fontSize: "8px",
color: "#ff0000",
fontStyle: "bold",
backgroundColor: "#000000aa",
padding: { x: 2, y: 1 }
}).setOrigin(0.5, 0.5);
slot.add(reloadText);
}
}
} else {
this.itemMap[i] = null;