From 40551a2eae3161bb3a8ab149c18cc4ecacca0a65 Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Fri, 13 Feb 2026 23:34:37 +0000 Subject: [PATCH] feat: Improve NPC and player combat interactions with enhanced state management and safety checks --- public/break_escape/js/systems/npc-combat.js | 64 ++++++++++++++++- .../break_escape/js/systems/player-combat.js | 69 ++++++++++++++++--- 2 files changed, 120 insertions(+), 13 deletions(-) diff --git a/public/break_escape/js/systems/npc-combat.js b/public/break_escape/js/systems/npc-combat.js index f4c7206..c2fff87 100644 --- a/public/break_escape/js/systems/npc-combat.js +++ b/public/break_escape/js/systems/npc-combat.js @@ -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); } }); diff --git a/public/break_escape/js/systems/player-combat.js b/public/break_escape/js/systems/player-combat.js index 6a11952..c5a5fc5 100644 --- a/public/break_escape/js/systems/player-combat.js +++ b/public/break_escape/js/systems/player-combat.js @@ -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(); + } }); } }