Auto reload last reloadble weapon when reload is triggered
This commit is contained in:
@@ -93,7 +93,7 @@ export interface MeleeWeaponItem extends BaseItem {
|
|||||||
type: "Weapon";
|
type: "Weapon";
|
||||||
weaponType: "melee";
|
weaponType: "melee";
|
||||||
stats: {
|
stats: {
|
||||||
attack: number;
|
attack: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,12 +103,12 @@ export interface RangedWeaponItem extends BaseItem {
|
|||||||
currentAmmo: number; // Runtime state - moved to top level for easier access
|
currentAmmo: number; // Runtime state - moved to top level for easier access
|
||||||
reloadingTurnsLeft: number;
|
reloadingTurnsLeft: number;
|
||||||
stats: {
|
stats: {
|
||||||
attack: number;
|
attack: number;
|
||||||
range: number;
|
range: number;
|
||||||
magazineSize: number;
|
magazineSize: number;
|
||||||
ammoType: string;
|
ammoType: string;
|
||||||
projectileSpeed: number;
|
projectileSpeed: number;
|
||||||
fireSound?: string;
|
fireSound?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,15 +117,15 @@ export type WeaponItem = MeleeWeaponItem | RangedWeaponItem;
|
|||||||
export interface ArmourItem extends BaseItem {
|
export interface ArmourItem extends BaseItem {
|
||||||
type: "BodyArmour" | "Helmet" | "Gloves" | "Boots";
|
type: "BodyArmour" | "Helmet" | "Gloves" | "Boots";
|
||||||
stats: {
|
stats: {
|
||||||
defense: number;
|
defense: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConsumableItem extends BaseItem {
|
export interface ConsumableItem extends BaseItem {
|
||||||
type: "Consumable";
|
type: "Consumable";
|
||||||
stats?: {
|
stats?: {
|
||||||
hp?: number;
|
hp?: number;
|
||||||
attack?: number;
|
attack?: number;
|
||||||
};
|
};
|
||||||
throwable?: boolean;
|
throwable?: boolean;
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
@@ -196,9 +197,9 @@ export interface CollectibleActor extends BaseActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ItemDropActor extends BaseActor {
|
export interface ItemDropActor extends BaseActor {
|
||||||
category: "item_drop";
|
category: "item_drop";
|
||||||
// type: string; // "health_potion", etc. or reuse Item
|
// type: string; // "health_potion", etc. or reuse Item
|
||||||
item: Item;
|
item: Item;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Actor = CombatantActor | CollectibleActor | ItemDropActor;
|
export type Actor = CombatantActor | CollectibleActor | ItemDropActor;
|
||||||
@@ -216,6 +217,6 @@ export interface UIUpdatePayload {
|
|||||||
player: CombatantActor | null; // Added for ECS Access
|
player: CombatantActor | null; // Added for ECS Access
|
||||||
floorIndex: number;
|
floorIndex: number;
|
||||||
uiState: {
|
uiState: {
|
||||||
targetingItemId: string | null;
|
targetingItemId: string | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -146,12 +147,12 @@ export class GameScene extends Phaser.Scene {
|
|||||||
const enemy = this.entityAccessor.findEnemyAt(next.x, next.y);
|
const enemy = this.entityAccessor.findEnemyAt(next.x, next.y);
|
||||||
|
|
||||||
if (enemy) {
|
if (enemy) {
|
||||||
this.commitPlayerAction({ type: "attack", targetId: enemy.id });
|
this.commitPlayerAction({ type: "attack", targetId: enemy.id });
|
||||||
this.playerPath = [];
|
this.playerPath = [];
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this.playerPath = [];
|
this.playerPath = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,23 +170,23 @@ export class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public emitUIUpdate() {
|
public emitUIUpdate() {
|
||||||
const payload: UIUpdatePayload = {
|
const payload: UIUpdatePayload = {
|
||||||
world: this.world,
|
world: this.world,
|
||||||
playerId: this.playerId,
|
playerId: this.playerId,
|
||||||
player: this.entityAccessor.getPlayer(),
|
player: this.entityAccessor.getPlayer(),
|
||||||
floorIndex: this.floorIndex,
|
floorIndex: this.floorIndex,
|
||||||
uiState: {
|
uiState: {
|
||||||
targetingItemId: this.targetingSystem.itemId
|
targetingItemId: this.targetingSystem.itemId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.events.emit("update-ui", payload);
|
this.events.emit("update-ui", payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public commitPlayerAction(action: Action) {
|
public commitPlayerAction(action: Action) {
|
||||||
const playerEvents = applyAction(this.world, this.playerId, action, this.entityAccessor);
|
const playerEvents = applyAction(this.world, this.playerId, action, this.entityAccessor);
|
||||||
|
|
||||||
if (playerEvents.some(ev => ev.type === "move-blocked")) {
|
if (playerEvents.some(ev => ev.type === "move-blocked")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.awaitingPlayer = false;
|
this.awaitingPlayer = false;
|
||||||
@@ -194,81 +195,81 @@ export class GameScene extends Phaser.Scene {
|
|||||||
// Process reloading progress
|
// Process reloading progress
|
||||||
const player = this.entityAccessor.getPlayer();
|
const player = this.entityAccessor.getPlayer();
|
||||||
if (player && player.inventory) {
|
if (player && player.inventory) {
|
||||||
// Check all items for reloading (usually only equipped or active)
|
// Check all items for reloading (usually only equipped or active)
|
||||||
for (const item of player.inventory.items) {
|
for (const item of player.inventory.items) {
|
||||||
if (item.type === "Weapon" && item.weaponType === "ranged" && item.reloadingTurnsLeft > 0) {
|
if (item.type === "Weapon" && item.weaponType === "ranged" && item.reloadingTurnsLeft > 0) {
|
||||||
item.reloadingTurnsLeft--;
|
item.reloadingTurnsLeft--;
|
||||||
|
|
||||||
if (item.reloadingTurnsLeft === 0) {
|
if (item.reloadingTurnsLeft === 0) {
|
||||||
// Finalize Reload
|
// Finalize Reload
|
||||||
const ammoId = `ammo_${item.stats.ammoType}`;
|
const ammoId = `ammo_${item.stats.ammoType}`;
|
||||||
const ammoItem = player.inventory.items.find(it => it.id === ammoId);
|
const ammoItem = player.inventory.items.find(it => it.id === ammoId);
|
||||||
|
|
||||||
if (ammoItem && ammoItem.quantity && ammoItem.quantity > 0) {
|
if (ammoItem && ammoItem.quantity && ammoItem.quantity > 0) {
|
||||||
const needed = item.stats.magazineSize - item.currentAmmo;
|
const needed = item.stats.magazineSize - item.currentAmmo;
|
||||||
const toTake = Math.min(needed, ammoItem.quantity);
|
const toTake = Math.min(needed, ammoItem.quantity);
|
||||||
|
|
||||||
item.currentAmmo += toTake;
|
item.currentAmmo += toTake;
|
||||||
ammoItem.quantity -= toTake;
|
ammoItem.quantity -= toTake;
|
||||||
|
|
||||||
if (ammoItem.quantity <= 0) {
|
if (ammoItem.quantity <= 0) {
|
||||||
player.inventory.items = player.inventory.items.filter(it => it !== ammoItem);
|
player.inventory.items = player.inventory.items.filter(it => it !== ammoItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Reloaded!", "#00ff00");
|
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Reloaded!", "#00ff00");
|
||||||
console.log(`Reloaded ${item.name}. Ammo:`, item.currentAmmo);
|
console.log(`Reloaded ${item.name}. Ammo:`, item.currentAmmo);
|
||||||
} else {
|
} else {
|
||||||
// Should be checked at startReload, but safe fallback
|
// Should be checked at startReload, but safe fallback
|
||||||
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "No Ammo!", "#ff0000");
|
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "No Ammo!", "#ff0000");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Show reloading progress
|
// Show reloading progress
|
||||||
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Reloading...", "#aaaaaa");
|
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)
|
// Check for pickups right after move (before enemy turn, so you get it efficiently)
|
||||||
if (action.type === "move") {
|
if (action.type === "move") {
|
||||||
const player = this.entityAccessor.getPlayer();
|
const player = this.entityAccessor.getPlayer();
|
||||||
if (!player) return;
|
if (!player) return;
|
||||||
|
|
||||||
const pickedItem = this.itemManager.tryPickup(player);
|
const pickedItem = this.itemManager.tryPickup(player);
|
||||||
if (pickedItem) {
|
if (pickedItem) {
|
||||||
this.emitUIUpdate();
|
this.emitUIUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process traps and status effects
|
// Process traps and status effects
|
||||||
this.ecsRegistry.updateAll();
|
this.ecsRegistry.updateAll();
|
||||||
|
|
||||||
// Handle trap events from ECS
|
// Handle trap events from ECS
|
||||||
const trapEvents = this.ecsEventBus.drain();
|
const trapEvents = this.ecsEventBus.drain();
|
||||||
if (trapEvents.length > 0) {
|
if (trapEvents.length > 0) {
|
||||||
console.log(`[GameScene] Traps triggered: ${trapEvents.length} events`, trapEvents);
|
console.log(`[GameScene] Traps triggered: ${trapEvents.length} events`, trapEvents);
|
||||||
}
|
}
|
||||||
for (const ev of trapEvents) {
|
for (const ev of trapEvents) {
|
||||||
if (ev.type === "trigger_activated") {
|
if (ev.type === "trigger_activated") {
|
||||||
const activator = this.entityAccessor.getActor(ev.activatorId);
|
const activator = this.entityAccessor.getActor(ev.activatorId);
|
||||||
const trapTrigger = this.ecsWorld.getComponent(ev.triggerId, "trigger");
|
const trapTrigger = this.ecsWorld.getComponent(ev.triggerId, "trigger");
|
||||||
|
|
||||||
if (trapTrigger?.effect && activator) {
|
if (trapTrigger?.effect && activator) {
|
||||||
const color = getEffectColor(trapTrigger.effect);
|
const color = getEffectColor(trapTrigger.effect);
|
||||||
const text = getEffectName(trapTrigger.effect);
|
const text = getEffectName(trapTrigger.effect);
|
||||||
this.dungeonRenderer.showFloatingText(activator.pos.x, activator.pos.y, text, color);
|
this.dungeonRenderer.showFloatingText(activator.pos.x, activator.pos.y, text, color);
|
||||||
}
|
|
||||||
} else if (ev.type === "damage") {
|
|
||||||
const victim = this.entityAccessor.getActor(ev.entityId);
|
|
||||||
if (victim) {
|
|
||||||
this.dungeonRenderer.showDamage(victim.pos.x, victim.pos.y, ev.amount);
|
|
||||||
}
|
|
||||||
} else if (ev.type === "status_applied") {
|
|
||||||
// Already handled above via trigger_activated
|
|
||||||
} else if (ev.type === "status_tick" && ev.entityId) {
|
|
||||||
// Show DOT damage tick
|
|
||||||
// Optional: could show small floating text here
|
|
||||||
}
|
}
|
||||||
|
} else if (ev.type === "damage") {
|
||||||
|
const victim = this.entityAccessor.getActor(ev.entityId);
|
||||||
|
if (victim) {
|
||||||
|
this.dungeonRenderer.showDamage(victim.pos.x, victim.pos.y, ev.amount);
|
||||||
|
}
|
||||||
|
} else if (ev.type === "status_applied") {
|
||||||
|
// Already handled above via trigger_activated
|
||||||
|
} else if (ev.type === "status_tick" && ev.entityId) {
|
||||||
|
// Show DOT damage tick
|
||||||
|
// Optional: could show small floating text here
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const enemyStep = stepUntilPlayerTurn(this.world, this.playerId, this.entityAccessor);
|
const enemyStep = stepUntilPlayerTurn(this.world, this.playerId, this.entityAccessor);
|
||||||
@@ -388,122 +389,127 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
public executeThrow() {
|
public executeThrow() {
|
||||||
const success = this.targetingSystem.executeThrow(
|
const success = this.targetingSystem.executeThrow(
|
||||||
this.world,
|
this.world,
|
||||||
this.playerId,
|
this.playerId,
|
||||||
this.entityAccessor,
|
this.entityAccessor,
|
||||||
(blockedPos, hitActorId, item) => {
|
(blockedPos, hitActorId, item) => {
|
||||||
// Damage Logic
|
// Damage Logic
|
||||||
if (hitActorId !== undefined) {
|
if (hitActorId !== undefined) {
|
||||||
const victim = this.entityAccessor.getCombatant(hitActorId);
|
const victim = this.entityAccessor.getCombatant(hitActorId);
|
||||||
const player = this.entityAccessor.getPlayer();
|
const player = this.entityAccessor.getPlayer();
|
||||||
if (victim && player) {
|
if (victim && player) {
|
||||||
const damageResult = calculateDamage(player.stats, victim.stats, item);
|
const damageResult = calculateDamage(player.stats, victim.stats, item);
|
||||||
|
|
||||||
if (damageResult.hit) {
|
if (damageResult.hit) {
|
||||||
victim.stats.hp -= damageResult.dmg;
|
victim.stats.hp -= damageResult.dmg;
|
||||||
this.dungeonRenderer.showDamage(victim.pos.x, victim.pos.y, damageResult.dmg, damageResult.isCrit, damageResult.isBlock);
|
this.dungeonRenderer.showDamage(victim.pos.x, victim.pos.y, damageResult.dmg, damageResult.isCrit, damageResult.isBlock);
|
||||||
this.dungeonRenderer.shakeCamera();
|
this.dungeonRenderer.shakeCamera();
|
||||||
} else {
|
} else {
|
||||||
this.dungeonRenderer.showDodge(victim.pos.x, victim.pos.y);
|
this.dungeonRenderer.showDodge(victim.pos.x, victim.pos.y);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const player = this.entityAccessor.getPlayer();
|
|
||||||
if (!player) return;
|
|
||||||
|
|
||||||
// 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.currentAmmo > 0) {
|
|
||||||
item.currentAmmo--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dungeonRenderer.showProjectile(
|
|
||||||
player.pos,
|
|
||||||
blockedPos,
|
|
||||||
projectileId,
|
|
||||||
() => {
|
|
||||||
// Only drop item if it acts as a thrown item (Consumable/Misc), NOT Weapon
|
|
||||||
const shouldDrop = item.type !== "Weapon";
|
|
||||||
|
|
||||||
if (shouldDrop) {
|
|
||||||
// Drop a SINGLE item at the landing spot (not the whole stack)
|
|
||||||
const singleItem = { ...item, quantity: 1 };
|
|
||||||
this.itemManager.spawnItem(singleItem, blockedPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger destruction/interaction
|
|
||||||
if (tryDestructTile(this.world, blockedPos.x, blockedPos.y)) {
|
|
||||||
this.dungeonRenderer.updateTile(blockedPos.x, blockedPos.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.targetingSystem.cancel();
|
|
||||||
this.commitPlayerAction({ type: "throw" }); // Or 'attack' if shooting? 'throw' is fine for now
|
|
||||||
this.emitUIUpdate();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
if (!success) {
|
const player = this.entityAccessor.getPlayer();
|
||||||
this.emitUIUpdate();
|
if (!player) return;
|
||||||
|
|
||||||
|
// 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.currentAmmo > 0) {
|
||||||
|
item.currentAmmo--;
|
||||||
|
}
|
||||||
|
// Track as last used reloadable weapon
|
||||||
|
this.runState.lastReloadableWeaponId = item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dungeonRenderer.showProjectile(
|
||||||
|
player.pos,
|
||||||
|
blockedPos,
|
||||||
|
projectileId,
|
||||||
|
() => {
|
||||||
|
// Only drop item if it acts as a thrown item (Consumable/Misc), NOT Weapon
|
||||||
|
const shouldDrop = item.type !== "Weapon";
|
||||||
|
|
||||||
|
if (shouldDrop) {
|
||||||
|
// Drop a SINGLE item at the landing spot (not the whole stack)
|
||||||
|
const singleItem = { ...item, quantity: 1 };
|
||||||
|
this.itemManager.spawnItem(singleItem, blockedPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger destruction/interaction
|
||||||
|
if (tryDestructTile(this.world, blockedPos.x, blockedPos.y)) {
|
||||||
|
this.dungeonRenderer.updateTile(blockedPos.x, blockedPos.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.targetingSystem.cancel();
|
||||||
|
this.commitPlayerAction({ type: "throw" }); // Or 'attack' if shooting? 'throw' is fine for now
|
||||||
|
this.emitUIUpdate();
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
this.emitUIUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public startReload(player: CombatantActor, item: RangedWeaponItem) {
|
public startReload(player: CombatantActor, item: RangedWeaponItem) {
|
||||||
if (item.currentAmmo >= item.stats.magazineSize) {
|
if (item.currentAmmo >= item.stats.magazineSize) {
|
||||||
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Full!", "#aaaaaa");
|
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Full!", "#aaaaaa");
|
||||||
return;
|
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.runState.lastReloadableWeaponId = item.id;
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.reloadingTurnsLeft > 0) return;
|
this.commitPlayerAction({ type: "wait" });
|
||||||
|
this.emitUIUpdate();
|
||||||
const ammoId = `ammo_${item.stats.ammoType}`;
|
} else {
|
||||||
const ammoItem = player.inventory?.items.find(it => it.id === ammoId);
|
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "No Ammo!", "#ff0000");
|
||||||
|
console.log("No ammo found for", item.name);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPointerTilePos(pointer: Phaser.Input.Pointer): { x: number, y: number } {
|
public getPointerTilePos(pointer: Phaser.Input.Pointer): { x: number, y: number } {
|
||||||
const worldPoint = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
|
const worldPoint = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
|
||||||
return {
|
return {
|
||||||
x: Math.floor(worldPoint.x / TILE_SIZE),
|
x: Math.floor(worldPoint.x / TILE_SIZE),
|
||||||
y: Math.floor(worldPoint.y / TILE_SIZE)
|
y: Math.floor(worldPoint.y / TILE_SIZE)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -95,12 +102,12 @@ export class PlayerInputHandler {
|
|||||||
|
|
||||||
input.on("cursor-move", (worldX: number, worldY: number) => {
|
input.on("cursor-move", (worldX: number, worldY: number) => {
|
||||||
if (this.scene.targetingSystem.isActive) {
|
if (this.scene.targetingSystem.isActive) {
|
||||||
const tx = Math.floor(worldX / TILE_SIZE);
|
const tx = Math.floor(worldX / TILE_SIZE);
|
||||||
const ty = Math.floor(worldY / TILE_SIZE);
|
const ty = Math.floor(worldY / TILE_SIZE);
|
||||||
const player = this.scene.entityAccessor.getPlayer();
|
const player = this.scene.entityAccessor.getPlayer();
|
||||||
if (player) {
|
if (player) {
|
||||||
this.scene.targetingSystem.updateCursor({ x: tx, y: ty }, player.pos);
|
this.scene.targetingSystem.updateCursor({ x: tx, y: ty }, player.pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,56 +117,56 @@ export class PlayerInputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleTileClick(tx: number, ty: number, button: number) {
|
private handleTileClick(tx: number, ty: number, button: number) {
|
||||||
// Targeting Click
|
// Targeting Click
|
||||||
if (this.scene.targetingSystem.isActive) {
|
if (this.scene.targetingSystem.isActive) {
|
||||||
// Only Left Click throws
|
// Only Left Click throws
|
||||||
if (button === 0) {
|
if (button === 0) {
|
||||||
if (this.scene.targetingSystem.cursorPos) {
|
if (this.scene.targetingSystem.cursorPos) {
|
||||||
this.scene.executeThrow();
|
this.scene.executeThrow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Movement Click
|
// Movement Click
|
||||||
if (button !== 0) return;
|
if (button !== 0) return;
|
||||||
|
|
||||||
this.scene.cameraController.enableFollowMode();
|
this.scene.cameraController.enableFollowMode();
|
||||||
|
|
||||||
if (!this.scene.awaitingPlayer) return;
|
if (!this.scene.awaitingPlayer) return;
|
||||||
if (this.scene.isMenuOpen || this.scene.isInventoryOpen || this.scene.dungeonRenderer.isMinimapVisible()) return;
|
if (this.scene.isMenuOpen || this.scene.isInventoryOpen || this.scene.dungeonRenderer.isMinimapVisible()) return;
|
||||||
|
|
||||||
if (!inBounds(this.scene.world, tx, ty)) return;
|
if (!inBounds(this.scene.world, tx, ty)) return;
|
||||||
|
|
||||||
if (!this.scene.dungeonRenderer.isSeen(tx, ty)) return;
|
if (!this.scene.dungeonRenderer.isSeen(tx, ty)) return;
|
||||||
|
|
||||||
const isEnemy = this.scene.entityAccessor.hasEnemyAt(tx, ty);
|
const isEnemy = this.scene.entityAccessor.hasEnemyAt(tx, ty);
|
||||||
|
|
||||||
const player = this.scene.entityAccessor.getPlayer();
|
const player = this.scene.entityAccessor.getPlayer();
|
||||||
if (!player) return;
|
if (!player) return;
|
||||||
|
|
||||||
const dx = tx - player.pos.x;
|
const dx = tx - player.pos.x;
|
||||||
const dy = ty - player.pos.y;
|
const dy = ty - player.pos.y;
|
||||||
const isDiagonalNeighbor = Math.abs(dx) === 1 && Math.abs(dy) === 1;
|
const isDiagonalNeighbor = Math.abs(dx) === 1 && Math.abs(dy) === 1;
|
||||||
|
|
||||||
if (isEnemy && isDiagonalNeighbor) {
|
if (isEnemy && isDiagonalNeighbor) {
|
||||||
const enemy = this.scene.entityAccessor.findEnemyAt(tx, ty);
|
const enemy = this.scene.entityAccessor.findEnemyAt(tx, ty);
|
||||||
if (enemy) {
|
if (enemy) {
|
||||||
this.scene.commitPlayerAction({ type: "attack", targetId: enemy.id });
|
this.scene.commitPlayerAction({ type: "attack", targetId: enemy.id });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = findPathAStar(
|
const path = findPathAStar(
|
||||||
this.scene.world,
|
this.scene.world,
|
||||||
this.scene.dungeonRenderer.seenArray,
|
this.scene.dungeonRenderer.seenArray,
|
||||||
{ ...player.pos },
|
{ ...player.pos },
|
||||||
{ x: tx, y: ty },
|
{ x: tx, y: ty },
|
||||||
{ ignoreBlockedTarget: isEnemy, accessor: this.scene.entityAccessor }
|
{ ignoreBlockedTarget: isEnemy, accessor: this.scene.entityAccessor }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (path.length >= 2) this.scene.playerPath = path;
|
if (path.length >= 2) this.scene.playerPath = path;
|
||||||
this.scene.dungeonRenderer.render(this.scene.playerPath);
|
this.scene.dungeonRenderer.render(this.scene.playerPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleCursorMovement(): Action | null {
|
public handleCursorMovement(): Action | null {
|
||||||
@@ -180,11 +187,11 @@ export class PlayerInputHandler {
|
|||||||
const enemy = this.scene.entityAccessor.findEnemyAt(targetX, targetY);
|
const enemy = this.scene.entityAccessor.findEnemyAt(targetX, targetY);
|
||||||
|
|
||||||
if (enemy) {
|
if (enemy) {
|
||||||
return { type: "attack", targetId: enemy.id };
|
return { type: "attack", targetId: enemy.id };
|
||||||
} else {
|
} else {
|
||||||
if (Math.abs(dx) + Math.abs(dy) === 1) {
|
if (Math.abs(dx) + Math.abs(dy) === 1) {
|
||||||
return { type: "move", dx, dy };
|
return { type: "move", dx, dy };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
145
src/scenes/systems/__tests__/ReloadLastWeapon.test.ts
Normal file
145
src/scenes/systems/__tests__/ReloadLastWeapon.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user