feat: Improve NPC and player combat interactions with enhanced state management and safety checks

This commit is contained in:
Z. Cliffe Schreuders
2026-02-13 23:34:37 +00:00
parent a28e79bd6d
commit 40551a2eae
2 changed files with 120 additions and 13 deletions

View File

@@ -26,11 +26,16 @@ export class NPCCombat {
const state = window.npcHostileSystem.getState(npcId);
if (!state || !state.isHostile || state.isKO) {
// If NPC is KO, make sure they're not stuck in attacking state
if (state && state.isKO) {
this.npcAttacking.delete(npcId);
}
return false;
}
// Don't attack if already attacking
if (this.npcAttacking.has(npcId)) {
console.log(`🥊 ${npcId} already attacking, skipping`);
return false;
}
@@ -92,6 +97,7 @@ export class NPCCombat {
performAttack(npcId, npcSprite, state) {
// Mark NPC as attacking
this.npcAttacking.add(npcId);
console.log(`🥊 ${npcId} starting attack sequence, added to attacking set (size: ${this.npcAttacking.size})`);
// Update attack timer
this.npcAttackTimers.set(npcId, Date.now());
@@ -119,6 +125,20 @@ export class NPCCombat {
return;
}
// Check if NPC is still valid and not destroyed
if (!npcSprite || npcSprite.destroyed) {
console.log(`${npcId} sprite destroyed during attack`);
this.npcAttacking.delete(npcId);
return;
}
// Check if NPC became KO during windup
if (window.npcHostileSystem && window.npcHostileSystem.isNPCKO(npcId)) {
console.log(`${npcId} became KO during attack windup`);
this.npcAttacking.delete(npcId);
return;
}
// Check if player is still in range
const distance = Phaser.Math.Distance.Between(
npcSprite.x,
@@ -144,10 +164,18 @@ export class NPCCombat {
* @param {Object} state - NPC hostile state
*/
applyAttackDamage(npcId, npcSprite, state) {
// Clear attacking flag
// Clear attacking flag - must be first to ensure it's always cleared
this.npcAttacking.delete(npcId);
console.log(`🥊 ${npcId} applying attack damage, cleared attacking flag`);
if (!window.player || !window.playerHealth) {
console.log(`⚠️ ${npcId} attack aborted - no player or health system`);
return;
}
// Check if sprite is still valid
if (!npcSprite || npcSprite.destroyed) {
console.log(`⚠️ ${npcId} attack aborted - sprite invalid`);
return;
}
@@ -267,8 +295,22 @@ export class NPCCombat {
npcSprite.play(attackAnimKey);
console.log(`🥊 Playing NPC attack animation: ${attackAnimKey}`);
// Safety timeout to ensure flag is cleared even if animation doesn't complete
const maxAttackDuration = 2000; // 2 seconds max
const safetyTimeout = this.scene.time.delayedCall(maxAttackDuration, () => {
if (this.npcAttacking.has(npcId)) {
console.warn(`⚠️ Attack animation timeout for ${npcId}, clearing flag`);
this.npcAttacking.delete(npcId);
}
});
// Apply damage when animation completes, then return to idle
npcSprite.once('animationcomplete', (anim) => {
// Cancel safety timeout if animation completes properly
if (safetyTimeout) {
safetyTimeout.remove();
}
if (anim.key === attackAnimKey && !npcSprite.destroyed) {
// Apply damage on animation complete
this.applyAttackDamage(npcId, npcSprite, state);
@@ -277,21 +319,39 @@ export class NPCCombat {
if (npcSprite.scene.anims.exists(idleAnimKey)) {
npcSprite.play(idleAnimKey);
}
} else if (this.npcAttacking.has(npcId)) {
// Animation was different or sprite destroyed, clear flag
this.npcAttacking.delete(npcId);
}
});
} else {
console.warn(`⚠️ Attack animation not found: ${attackAnimKey}, using fallback`);
// Fallback: red tint + delayed damage
console.log(`🥊 Using fallback attack for ${npcId}`);
if (window.spriteEffects) {
window.spriteEffects.applyAttackTint(npcSprite);
}
// Safety timeout to ensure flag is cleared
const maxAttackDuration = 2000;
const safetyTimeout = this.scene.time.delayedCall(maxAttackDuration, () => {
if (this.npcAttacking.has(npcId)) {
console.warn(`⚠️ Fallback attack timeout for ${npcId}, clearing flag`);
this.npcAttacking.delete(npcId);
}
});
// Apply damage and remove tint after animation duration
this.scene.time.delayedCall(COMBAT_CONFIG.player.punchAnimationDuration, () => {
// Cancel safety timeout
if (safetyTimeout) {
safetyTimeout.remove();
}
this.applyAttackDamage(npcId, npcSprite, state);
if (window.spriteEffects) {
if (window.spriteEffects && npcSprite && !npcSprite.destroyed) {
window.spriteEffects.clearAttackTint(npcSprite);
}
});

View File

@@ -69,8 +69,13 @@ export class PlayerCombat {
* Damage applies to ALL NPCs in punch range and facing direction
*/
punch() {
if (this.isPunching || !this.canPunch()) {
console.log('Punch on cooldown');
if (this.isPunching) {
console.log('🥊 Punch blocked - already punching');
return false;
}
if (!this.canPunch()) {
console.log('🥊 Punch blocked - on cooldown');
return false;
}
@@ -81,6 +86,8 @@ export class PlayerCombat {
this.isPunching = true;
this.lastPunchTime = Date.now();
console.log(`🥊 Player starting punch, isPunching set to true`);
// Play punch animation and wait for completion
this.playPunchAnimation();
@@ -141,16 +148,39 @@ export class PlayerCombat {
if (animPlayed) {
console.log(`🥊 Playing punch animation: ${animKey}`);
// Safety timeout to ensure flag is cleared even if animation is interrupted
const maxAttackDuration = 2000; // 2 seconds max
const safetyTimeout = this.scene.time.delayedCall(maxAttackDuration, () => {
if (this.isPunching) {
console.warn(`⚠️ Player punch animation timeout, clearing isPunching flag`);
this.isPunching = false;
}
});
// Animation will complete naturally
// Apply damage when animation completes, then return to idle
player.once('animationcomplete', () => {
// Apply damage on animation complete
this.checkForHits();
this.isPunching = false;
player.once('animationcomplete', (anim) => {
// Cancel safety timeout if animation completes properly
if (safetyTimeout) {
safetyTimeout.remove();
}
const idleKey = `idle-${direction}`;
if (player.anims && player.anims.exists && this.scene.anims.exists(idleKey)) {
player.anims.play(idleKey, true);
// Check if the completed animation is a punch/jab animation
if (anim.key.includes('punch') || anim.key.includes('jab')) {
console.log(`🥊 Player punch animation completed: ${anim.key}`);
// Apply damage on animation complete
this.checkForHits();
this.isPunching = false;
const idleKey = `idle-${direction}`;
if (player.anims && player.anims.exists && this.scene.anims.exists(idleKey)) {
player.anims.play(idleKey, true);
}
} else if (this.isPunching) {
// Animation was different (interrupted), just clear the flag
console.warn(`⚠️ Player punch interrupted - animation changed to: ${anim.key}`);
this.isPunching = false;
}
});
} else {
@@ -170,16 +200,33 @@ export class PlayerCombat {
}
}
// Safety timeout to ensure flag is cleared
const maxAttackDuration = 2000;
const safetyTimeout = this.scene.time.delayedCall(maxAttackDuration, () => {
if (this.isPunching) {
console.warn(`⚠️ Player fallback punch timeout, clearing isPunching flag`);
this.isPunching = false;
}
});
// Apply damage and remove tint after animation duration (fallback)
this.scene.time.delayedCall(COMBAT_CONFIG.player.punchAnimationDuration, () => {
// Cancel safety timeout
if (safetyTimeout) {
safetyTimeout.remove();
}
console.log(`🥊 Player fallback punch completed`);
this.checkForHits();
this.isPunching = false;
if (window.spriteEffects) {
if (window.spriteEffects && player && !player.destroyed) {
window.spriteEffects.clearAttackTint(player);
}
// Stop animation
player.anims.stop();
if (player && player.anims && !player.destroyed) {
player.anims.stop();
}
});
}
}