mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
feat: Enhance combat mechanics with knockback and animation improvements
- Added knockback functionality for both player and NPCs, allowing for dynamic interactions during combat. - Implemented new animations for player and NPC hit reactions, including 'taking-punch' and 'death' animations. - Updated player and NPC behavior to prevent animation interruptions during attacks and knockouts. - Improved player health system to trigger death animations and disable movement upon KO. - Refactored NPC combat to manage attack states and ensure animations play correctly. - Introduced utility functions for applying knockback effects and checking knockback states.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 356 KiB |
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 289 KiB |
@@ -403,7 +403,8 @@ function createAtlasPlayerAnimations(spriteSheet) {
|
||||
// Animation type mapping: atlas animations → player animations
|
||||
const animTypeMap = {
|
||||
'breathing-idle': 'idle',
|
||||
'walk': 'walk'
|
||||
'walk': 'walk',
|
||||
'taking-punch': 'hit'
|
||||
};
|
||||
|
||||
// Animation type framework (for grouping and frame rate)
|
||||
@@ -411,7 +412,9 @@ function createAtlasPlayerAnimations(spriteSheet) {
|
||||
'idle': { frameRate: idleFrameRate, repeat: -1, name: 'idle' },
|
||||
'walk': { frameRate: walkFrameRate, repeat: -1, name: 'walk' },
|
||||
'cross-punch': { frameRate: punchFrameRate, repeat: 0, name: 'attack' },
|
||||
'lead-jab': { frameRate: punchFrameRate, repeat: 0, name: 'attack' }
|
||||
'lead-jab': { frameRate: punchFrameRate, repeat: 0, name: 'attack' },
|
||||
'taking-punch': { frameRate: 12, repeat: 0, name: 'hit' },
|
||||
'falling-back-death': { frameRate: 10, repeat: 0, name: 'death' }
|
||||
};
|
||||
|
||||
// Create animations from atlas metadata
|
||||
@@ -670,7 +673,11 @@ export function updatePlayerMovement() {
|
||||
if (player.body.velocity.x === 0 && player.body.velocity.y === 0 && player.isMoving) {
|
||||
player.isMoving = false;
|
||||
const animDir = getAnimationKey(player.direction);
|
||||
player.anims.play(`idle-${animDir}`, true);
|
||||
// Don't interrupt punch animations
|
||||
const currentAnim = player.anims.currentAnim?.key || '';
|
||||
if (!currentAnim.includes('punch') && !currentAnim.includes('jab')) {
|
||||
player.anims.play(`idle-${animDir}`, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -747,7 +754,11 @@ function updatePlayerKeyboardMovement() {
|
||||
player.isMoving = false;
|
||||
const animDir = getAnimationKey(player.direction);
|
||||
player.anims.stop(); // Stop current animation
|
||||
player.anims.play(`idle-${animDir}`, true);
|
||||
// Don't interrupt punch animations
|
||||
const currentAnim = player.anims.currentAnim?.key || '';
|
||||
if (!currentAnim.includes('punch') && !currentAnim.includes('jab')) {
|
||||
player.anims.play(`idle-${animDir}`, true);
|
||||
}
|
||||
}
|
||||
} else if (isBlocked) {
|
||||
// Blocked by collision - play idle animation in the direction we're facing
|
||||
@@ -755,7 +766,11 @@ function updatePlayerKeyboardMovement() {
|
||||
player.isMoving = false;
|
||||
const animDir = getAnimationKey(player.direction);
|
||||
player.anims.stop(); // Stop current animation
|
||||
player.anims.play(`idle-${animDir}`, true);
|
||||
// Don't interrupt punch animations
|
||||
const currentAnim = player.anims.currentAnim?.key || '';
|
||||
if (!currentAnim.includes('punch') && !currentAnim.includes('jab')) {
|
||||
player.anims.play(`idle-${animDir}`, true);
|
||||
}
|
||||
}
|
||||
} else if (absVX > absVY * 2) {
|
||||
// Mostly horizontal movement
|
||||
@@ -769,7 +784,11 @@ function updatePlayerKeyboardMovement() {
|
||||
player.setFlipX(shouldFlip);
|
||||
|
||||
if (!player.isMoving || player.lastDirection !== player.direction) {
|
||||
player.anims.play(`walk-${animDir}`, true);
|
||||
// Don't interrupt punch animations
|
||||
const currentAnim = player.anims.currentAnim?.key || '';
|
||||
if (!currentAnim.includes('punch') && !currentAnim.includes('jab')) {
|
||||
player.anims.play(`walk-${animDir}`, true);
|
||||
}
|
||||
player.isMoving = true;
|
||||
player.lastDirection = player.direction;
|
||||
}
|
||||
@@ -779,7 +798,11 @@ function updatePlayerKeyboardMovement() {
|
||||
player.setFlipX(false);
|
||||
|
||||
if (!player.isMoving || player.lastDirection !== player.direction) {
|
||||
player.anims.play(`walk-${player.direction}`, true);
|
||||
// Don't interrupt punch animations
|
||||
const currentAnim = player.anims.currentAnim?.key || '';
|
||||
if (!currentAnim.includes('punch') && !currentAnim.includes('jab')) {
|
||||
player.anims.play(`walk-${player.direction}`, true);
|
||||
}
|
||||
player.isMoving = true;
|
||||
player.lastDirection = player.direction;
|
||||
}
|
||||
@@ -799,7 +822,11 @@ function updatePlayerKeyboardMovement() {
|
||||
player.setFlipX(shouldFlip);
|
||||
|
||||
if (!player.isMoving || player.lastDirection !== player.direction) {
|
||||
player.anims.play(`walk-${baseDir}`, true);
|
||||
// Don't interrupt punch animations
|
||||
const currentAnim = player.anims.currentAnim?.key || '';
|
||||
if (!currentAnim.includes('punch') && !currentAnim.includes('jab')) {
|
||||
player.anims.play(`walk-${baseDir}`, true);
|
||||
}
|
||||
player.isMoving = true;
|
||||
player.lastDirection = player.direction;
|
||||
}
|
||||
@@ -813,7 +840,11 @@ function updatePlayerMouseMovement() {
|
||||
player.isMoving = false;
|
||||
|
||||
// Play idle animation based on last direction
|
||||
player.anims.play(`idle-${player.direction}`, true);
|
||||
// Don't interrupt punch animations
|
||||
const currentAnim = player.anims.currentAnim?.key || '';
|
||||
if (!currentAnim.includes('punch') && !currentAnim.includes('jab')) {
|
||||
player.anims.play(`idle-${player.direction}`, true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -838,7 +869,11 @@ function updatePlayerMouseMovement() {
|
||||
player.isMoving = false;
|
||||
const animDir = getAnimationKey(player.direction);
|
||||
player.anims.stop(); // Stop current animation
|
||||
player.anims.play(`idle-${animDir}`, true);
|
||||
// Don't interrupt punch animations
|
||||
const currentAnim = player.anims.currentAnim?.key || '';
|
||||
if (!currentAnim.includes('punch') && !currentAnim.includes('jab')) {
|
||||
player.anims.play(`idle-${animDir}`, true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -883,7 +918,11 @@ function updatePlayerMouseMovement() {
|
||||
|
||||
// Play appropriate animation if not already playing
|
||||
if (!player.isMoving || player.lastDirection !== player.direction) {
|
||||
player.anims.play(`walk-${player.direction}`, true);
|
||||
// Don't interrupt punch animations
|
||||
const currentAnim = player.anims.currentAnim?.key || '';
|
||||
if (!currentAnim.includes('punch') && !currentAnim.includes('jab')) {
|
||||
player.anims.play(`walk-${player.direction}`, true);
|
||||
}
|
||||
player.isMoving = true;
|
||||
player.lastDirection = player.direction;
|
||||
}
|
||||
@@ -896,7 +935,11 @@ function updatePlayerMouseMovement() {
|
||||
player.isMoving = false;
|
||||
const animDir = getAnimationKey(player.direction);
|
||||
player.anims.stop(); // Stop current animation
|
||||
player.anims.play(`idle-${animDir}`, true);
|
||||
// Don't interrupt punch animations
|
||||
const currentAnim = player.anims.currentAnim?.key || '';
|
||||
if (!currentAnim.includes('punch') && !currentAnim.includes('jab')) {
|
||||
player.anims.play(`idle-${animDir}`, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,6 +534,15 @@ class NPCBehavior {
|
||||
executeState(state, time, delta, playerPos) {
|
||||
this.currentState = state;
|
||||
|
||||
// Check if NPC is KO - if so, don't override death animation
|
||||
const isKO = window.npcHostileSystem && window.npcHostileSystem.isNPCKO(this.npcId);
|
||||
if (isKO) {
|
||||
// NPC is knocked out - stop movement but don't change animation (death anim is playing)
|
||||
this.sprite.body.setVelocity(0, 0);
|
||||
this.isMoving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case 'idle':
|
||||
this.sprite.body.setVelocity(0, 0);
|
||||
@@ -1069,6 +1078,12 @@ class NPCBehavior {
|
||||
}
|
||||
|
||||
playAnimation(state, direction) {
|
||||
// Don't interrupt attack animations (red tint placeholder)
|
||||
const currentAnim = this.sprite.anims?.currentAnim?.key || '';
|
||||
if (currentAnim.includes('attack') || currentAnim.includes('punch')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this NPC uses atlas-based animations (8 native directions)
|
||||
// by checking if the direct left-facing animation exists
|
||||
const directAnimKey = `npc-${this.npcId}-${state}-${direction}`;
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
*/
|
||||
|
||||
import { COMBAT_CONFIG } from '../config/combat-config.js';
|
||||
import { applyKnockback } from '../utils/knockback.js';
|
||||
import { getNPCDirection } from './npc-sprites.js';
|
||||
|
||||
export class NPCCombat {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.npcAttackTimers = new Map(); // npcId -> last attack time
|
||||
this.npcAttacking = new Set(); // Track NPCs currently in attack animation
|
||||
|
||||
console.log('✅ NPC combat system initialized');
|
||||
}
|
||||
@@ -26,6 +29,11 @@ export class NPCCombat {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't attack if already attacking
|
||||
if (this.npcAttacking.has(npcId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't attack while a minigame is active (conversation, combat, etc.)
|
||||
if (window.MinigameFramework && window.MinigameFramework.currentMinigame) {
|
||||
return false;
|
||||
@@ -82,6 +90,9 @@ export class NPCCombat {
|
||||
* @param {Object} state - NPC hostile state
|
||||
*/
|
||||
performAttack(npcId, npcSprite, state) {
|
||||
// Mark NPC as attacking
|
||||
this.npcAttacking.add(npcId);
|
||||
|
||||
// Update attack timer
|
||||
this.npcAttackTimers.set(npcId, Date.now());
|
||||
|
||||
@@ -92,18 +103,19 @@ export class NPCCombat {
|
||||
|
||||
// Play attack animation after windup
|
||||
this.scene.time.delayedCall(COMBAT_CONFIG.npc.attackWindupDuration, () => {
|
||||
this.executeAttack(npcId, npcSprite, state);
|
||||
this.startAttackAnimation(npcId, npcSprite, state);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the actual attack damage
|
||||
* Start attack animation and setup damage application
|
||||
* @param {string} npcId
|
||||
* @param {Phaser.GameObjects.Sprite} npcSprite
|
||||
* @param {Object} state - NPC hostile state
|
||||
*/
|
||||
executeAttack(npcId, npcSprite, state) {
|
||||
startAttackAnimation(npcId, npcSprite, state) {
|
||||
if (!window.player || !window.playerHealth) {
|
||||
this.npcAttacking.delete(npcId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,16 +129,48 @@ export class NPCCombat {
|
||||
|
||||
if (distance > state.attackRange) {
|
||||
console.log(`${npcId} attack missed - player moved out of range`);
|
||||
this.npcAttacking.delete(npcId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Play attack animation (placeholder: walk animation with red tint)
|
||||
this.playAttackAnimation(npcSprite);
|
||||
// Play attack animation
|
||||
this.playAttackAnimation(npcSprite, npcId, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply damage to player (called when attack animation completes)
|
||||
* @param {string} npcId
|
||||
* @param {Phaser.GameObjects.Sprite} npcSprite
|
||||
* @param {Object} state - NPC hostile state
|
||||
*/
|
||||
applyAttackDamage(npcId, npcSprite, state) {
|
||||
// Clear attacking flag
|
||||
this.npcAttacking.delete(npcId);
|
||||
|
||||
if (!window.player || !window.playerHealth) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player is still in range (they might have moved during animation)
|
||||
const distance = Phaser.Math.Distance.Between(
|
||||
npcSprite.x,
|
||||
npcSprite.y,
|
||||
window.player.x,
|
||||
window.player.y
|
||||
);
|
||||
|
||||
if (distance > state.attackRange * 1.5) { // 1.5x range for leniency during animation
|
||||
console.log(`${npcId} attack missed - player moved away during animation`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply damage to player
|
||||
const damage = state.attackDamage;
|
||||
window.playerHealth.damage(damage);
|
||||
|
||||
// Play hit animation if available
|
||||
this.playHitAnimation(window.player);
|
||||
|
||||
// Visual feedback
|
||||
if (window.spriteEffects) {
|
||||
window.spriteEffects.flashDamage(window.player);
|
||||
@@ -137,6 +181,11 @@ export class NPCCombat {
|
||||
window.damageNumbers.show(window.player.x, window.player.y - 30, damage, 'damage');
|
||||
}
|
||||
|
||||
// Knockback
|
||||
if (window.player) {
|
||||
applyKnockback(window.player, npcSprite.x, npcSprite.y);
|
||||
}
|
||||
|
||||
// Screen effects
|
||||
if (window.screenEffects) {
|
||||
window.screenEffects.flashDamage();
|
||||
@@ -147,36 +196,106 @@ export class NPCCombat {
|
||||
}
|
||||
|
||||
/**
|
||||
* Play attack animation (placeholder)
|
||||
* @param {Phaser.GameObjects.Sprite} npcSprite
|
||||
* Play hit/taking-punch animation on a sprite
|
||||
* @param {Phaser.GameObjects.Sprite} sprite - The sprite taking damage
|
||||
*/
|
||||
playAttackAnimation(npcSprite) {
|
||||
if (!npcSprite) return;
|
||||
playHitAnimation(sprite) {
|
||||
if (!sprite || !sprite.scene) return;
|
||||
|
||||
// Apply red tint
|
||||
if (window.spriteEffects) {
|
||||
window.spriteEffects.applyAttackTint(npcSprite);
|
||||
}
|
||||
|
||||
// Play walk animation if available
|
||||
if (npcSprite.anims && !npcSprite.anims.isPlaying) {
|
||||
const direction = npcSprite.lastDirection || 'down';
|
||||
const animKey = `${npcSprite.npcId}_walk_${direction}`;
|
||||
if (npcSprite.anims.exists(animKey)) {
|
||||
npcSprite.play(animKey, true);
|
||||
// Get sprite's current direction
|
||||
const direction = sprite.lastDirection || 'down';
|
||||
|
||||
// Check if this is player or NPC
|
||||
const isPlayer = sprite === window.player;
|
||||
|
||||
if (isPlayer) {
|
||||
// Player hit animations use atlas compass directions
|
||||
const compassMap = {
|
||||
'down': 'south',
|
||||
'up': 'north',
|
||||
'left': 'west',
|
||||
'right': 'east',
|
||||
'down-left': 'south-west',
|
||||
'down-right': 'south-east',
|
||||
'up-left': 'north-west',
|
||||
'up-right': 'north-east'
|
||||
};
|
||||
|
||||
const compassDir = compassMap[direction] || 'south';
|
||||
const hitAnimKey = `taking-punch_${compassDir}`;
|
||||
|
||||
if (sprite.scene.anims.exists(hitAnimKey)) {
|
||||
sprite.play(hitAnimKey);
|
||||
}
|
||||
} else {
|
||||
// NPC hit animations
|
||||
const npcId = sprite.npcId;
|
||||
if (npcId) {
|
||||
const hitAnimKey = `npc-${npcId}-hit-${direction}`;
|
||||
|
||||
if (sprite.scene.anims.exists(hitAnimKey)) {
|
||||
sprite.play(hitAnimKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove tint after animation
|
||||
this.scene.time.delayedCall(COMBAT_CONFIG.player.punchAnimationDuration, () => {
|
||||
if (window.spriteEffects) {
|
||||
window.spriteEffects.clearAttackTint(npcSprite);
|
||||
}
|
||||
// Stop animation
|
||||
if (npcSprite.anims) {
|
||||
/**
|
||||
* Play attack animation
|
||||
* @param {Phaser.GameObjects.Sprite} npcSprite
|
||||
* @param {string} npcId
|
||||
* @param {Object} state - NPC hostile state
|
||||
*/
|
||||
playAttackAnimation(npcSprite, npcId, state) {
|
||||
if (!npcSprite || !npcId) {
|
||||
this.npcAttacking.delete(npcId);
|
||||
return;
|
||||
}
|
||||
|
||||
const direction = getNPCDirection(npcId, npcSprite);
|
||||
const attackAnimKey = `npc-${npcId}-attack-${direction}`;
|
||||
|
||||
console.log(`🥊 NPC ${npcId} attempting attack animation: ${attackAnimKey}`);
|
||||
|
||||
// Try to play cross-punch animation
|
||||
if (npcSprite.scene.anims.exists(attackAnimKey)) {
|
||||
// Stop any current animation
|
||||
if (npcSprite.anims.isPlaying) {
|
||||
npcSprite.anims.stop();
|
||||
}
|
||||
});
|
||||
|
||||
npcSprite.play(attackAnimKey);
|
||||
console.log(`🥊 Playing NPC attack animation: ${attackAnimKey}`);
|
||||
|
||||
// Apply damage when animation completes, then return to idle
|
||||
npcSprite.once('animationcomplete', (anim) => {
|
||||
if (anim.key === attackAnimKey && !npcSprite.destroyed) {
|
||||
// Apply damage on animation complete
|
||||
this.applyAttackDamage(npcId, npcSprite, state);
|
||||
|
||||
const idleAnimKey = `npc-${npcId}-idle-${direction}`;
|
||||
if (npcSprite.scene.anims.exists(idleAnimKey)) {
|
||||
npcSprite.play(idleAnimKey);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn(`⚠️ Attack animation not found: ${attackAnimKey}, using fallback`);
|
||||
|
||||
// Fallback: red tint + delayed damage
|
||||
if (window.spriteEffects) {
|
||||
window.spriteEffects.applyAttackTint(npcSprite);
|
||||
}
|
||||
|
||||
// Apply damage and remove tint after animation duration
|
||||
this.scene.time.delayedCall(COMBAT_CONFIG.player.punchAnimationDuration, () => {
|
||||
this.applyAttackDamage(npcId, npcSprite, state);
|
||||
|
||||
if (window.spriteEffects) {
|
||||
window.spriteEffects.clearAttackTint(npcSprite);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { COMBAT_CONFIG } from '../config/combat-config.js';
|
||||
import { CombatEvents } from '../events/combat-events.js';
|
||||
import { getNPCDirection } from './npc-sprites.js';
|
||||
|
||||
const npcHostileStates = new Map();
|
||||
|
||||
@@ -94,10 +95,29 @@ function damageNPC(npcId, amount) {
|
||||
if (state.currentHP <= 0) {
|
||||
state.isKO = true;
|
||||
|
||||
// Apply KO visual effect to sprite
|
||||
// Play death animation and disable physics after it completes
|
||||
const npc = window.npcManager?.getNPC(npcId);
|
||||
if (npc && npc.sprite && window.spriteEffects) {
|
||||
window.spriteEffects.setKOAlpha(npc.sprite, 0.5);
|
||||
const sprite = npc?._sprite || npc?.sprite;
|
||||
if (sprite) {
|
||||
// Disable collisions immediately so player can walk through
|
||||
if (sprite.body) {
|
||||
sprite.body.setVelocity(0, 0);
|
||||
// Disable all collision checks but keep body enabled for animation
|
||||
sprite.body.checkCollision.none = true;
|
||||
console.log(`💀 Disabled collisions for ${npcId}, starting death animation`);
|
||||
}
|
||||
|
||||
playNPCDeathAnimation(npcId, sprite);
|
||||
|
||||
// Disable physics body completely after death animation completes
|
||||
// Use animationcomplete event to ensure all frames play
|
||||
sprite.once('animationcomplete', (anim) => {
|
||||
console.log(`💀 Animation complete event fired for ${npcId}, anim key: ${anim.key}`);
|
||||
if (anim.key.includes('death') && sprite && sprite.body && !sprite.destroyed) {
|
||||
sprite.body.enable = false; // Disable physics body entirely
|
||||
console.log(`💀 Disabled physics body for ${npcId} after animation complete`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Drop any items the NPC was holding
|
||||
@@ -111,6 +131,35 @@ function damageNPC(npcId, amount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play death animation for NPC
|
||||
* @param {string} npcId - The NPC ID
|
||||
* @param {Phaser.GameObjects.Sprite} sprite - The NPC sprite
|
||||
*/
|
||||
function playNPCDeathAnimation(npcId, sprite) {
|
||||
if (!sprite || !sprite.scene) {
|
||||
console.warn(`⚠️ Cannot play death animation - invalid sprite for ${npcId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get NPC's current facing direction
|
||||
const direction = getNPCDirection(npcId, sprite);
|
||||
|
||||
// Build death animation key: npc-{npcId}-death-{direction}
|
||||
const deathAnimKey = `npc-${npcId}-death-${direction}`;
|
||||
|
||||
if (sprite.scene.anims.exists(deathAnimKey)) {
|
||||
// Stop any current animation first
|
||||
if (sprite.anims.isPlaying) {
|
||||
sprite.anims.stop();
|
||||
}
|
||||
sprite.play(deathAnimKey);
|
||||
console.log(`💀 Playing NPC death animation: ${deathAnimKey}`);
|
||||
} else {
|
||||
console.warn(`⚠️ Death animation not found: ${deathAnimKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop items around a defeated NPC
|
||||
* Items spawn at NPC location and are launched outward with physics
|
||||
@@ -119,30 +168,22 @@ function damageNPC(npcId, amount) {
|
||||
*/
|
||||
function dropNPCItems(npcId) {
|
||||
const npc = window.npcManager?.getNPC(npcId);
|
||||
const sprite = npc?._sprite || npc?.sprite;
|
||||
if (!npc || !npc.itemsHeld || npc.itemsHeld.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the NPC sprite and room to get its position
|
||||
let npcSprite = null;
|
||||
let npcRoomId = null;
|
||||
if (window.rooms) {
|
||||
for (const roomId in window.rooms) {
|
||||
const room = window.rooms[roomId];
|
||||
if (!room.npcSprites) continue;
|
||||
for (const sprite of room.npcSprites) {
|
||||
if (sprite.npcId === npcId) {
|
||||
npcSprite = sprite;
|
||||
npcRoomId = roomId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (npcSprite) break;
|
||||
}
|
||||
// Use the sprite we already have and find its room
|
||||
if (!sprite) {
|
||||
console.warn(`⚠️ Cannot drop items - no sprite found for NPC: ${npcId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!npcSprite || !npcRoomId) {
|
||||
console.warn(`Could not find NPC sprite to drop items for ${npcId}`);
|
||||
// Find the NPC's room
|
||||
let npcRoomId = npc.roomId;
|
||||
|
||||
if (!npcRoomId) {
|
||||
console.warn(`Could not find room for NPC ${npcId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -156,8 +197,8 @@ function dropNPCItems(npcId) {
|
||||
const angle = (index / itemCount) * Math.PI * 2;
|
||||
|
||||
// All items spawn at NPC center location
|
||||
const spawnX = Math.round(npcSprite.x);
|
||||
const spawnY = Math.round(npcSprite.y);
|
||||
const spawnX = Math.round(sprite.x);
|
||||
const spawnY = Math.round(sprite.y);
|
||||
|
||||
// Create actual Phaser sprite for the dropped item
|
||||
const texture = item.texture || item.type || 'key';
|
||||
|
||||
@@ -144,6 +144,40 @@ export function createNPCSprite(scene, npc, roomData) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NPC's current facing direction
|
||||
* @param {string} npcId - The NPC ID
|
||||
* @param {Phaser.GameObjects.Sprite} sprite - The NPC sprite (optional, will look up if not provided)
|
||||
* @returns {string} Direction string (e.g., 'down', 'up', 'left', 'right', etc.)
|
||||
*/
|
||||
export function getNPCDirection(npcId, sprite = null) {
|
||||
// Try to get direction from behavior system first
|
||||
if (window.npcBehaviorManager) {
|
||||
const behavior = window.npcBehaviorManager.getBehavior(npcId);
|
||||
if (behavior && behavior.direction) {
|
||||
return behavior.direction;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to checking sprite's current animation
|
||||
if (!sprite) {
|
||||
const npc = window.npcManager?.getNPC(npcId);
|
||||
sprite = npc?._sprite || npc?.sprite;
|
||||
}
|
||||
|
||||
if (sprite && sprite.anims && sprite.anims.currentAnim) {
|
||||
const animKey = sprite.anims.currentAnim.key;
|
||||
// Extract direction from animation key (e.g., "npc-sarah-idle-down" → "down")
|
||||
const parts = animKey.split('-');
|
||||
if (parts.length >= 3) {
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
// Default to 'down' if we can't determine direction
|
||||
return 'down';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate NPC's world position from scenario data
|
||||
*
|
||||
@@ -1224,6 +1258,11 @@ function handleNPCPlayerCollision(npcSprite, player) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't handle collision if NPC is KO'd
|
||||
if (window.npcHostileSystem && window.npcHostileSystem.isNPCKO(npcSprite.npcId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get behavior instance for NPC
|
||||
const npcBehavior = window.npcBehaviorManager?.getBehavior(npcSprite.npcId);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { COMBAT_CONFIG } from '../config/combat-config.js';
|
||||
import { applyKnockback } from '../utils/knockback.js';
|
||||
|
||||
export class PlayerCombat {
|
||||
constructor(scene) {
|
||||
@@ -81,15 +82,9 @@ export class PlayerCombat {
|
||||
this.isPunching = true;
|
||||
this.lastPunchTime = Date.now();
|
||||
|
||||
// Play punch animation (placeholder: walk animation with red tint)
|
||||
// Play punch animation and wait for completion
|
||||
this.playPunchAnimation();
|
||||
|
||||
// After animation duration, check for hits
|
||||
this.scene.time.delayedCall(COMBAT_CONFIG.player.punchAnimationDuration, () => {
|
||||
this.checkForHits();
|
||||
this.isPunching = false;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -147,8 +142,12 @@ export class PlayerCombat {
|
||||
if (animPlayed) {
|
||||
console.log(`🥊 Playing punch animation: ${animKey}`);
|
||||
// Animation will complete naturally
|
||||
// Listen for animation complete event to return to idle
|
||||
// Apply damage when animation completes, then return to idle
|
||||
player.once('animationcomplete', () => {
|
||||
// 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);
|
||||
@@ -171,8 +170,11 @@ export class PlayerCombat {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove tint after animation
|
||||
// Apply damage and remove tint after animation duration (fallback)
|
||||
this.scene.time.delayedCall(COMBAT_CONFIG.player.punchAnimationDuration, () => {
|
||||
this.checkForHits();
|
||||
this.isPunching = false;
|
||||
|
||||
if (window.spriteEffects) {
|
||||
window.spriteEffects.clearAttackTint(player);
|
||||
}
|
||||
@@ -351,6 +353,14 @@ export class PlayerCombat {
|
||||
// Apply damage
|
||||
window.npcHostileSystem.damageNPC(npcId, damage);
|
||||
|
||||
// Check if NPC is now KO (after damage)
|
||||
const isKO = window.npcHostileSystem && window.npcHostileSystem.isNPCKO(npcId);
|
||||
|
||||
// Play hit animation if available (only if not KO - death anim takes priority)
|
||||
if (npcSprite && !isKO) {
|
||||
this.playHitAnimation(npcSprite, npcId);
|
||||
}
|
||||
|
||||
// Visual feedback
|
||||
if (npcSprite && window.spriteEffects) {
|
||||
window.spriteEffects.flashDamage(npcSprite);
|
||||
@@ -361,6 +371,11 @@ export class PlayerCombat {
|
||||
window.damageNumbers.show(npcSprite.x, npcSprite.y - 30, damage, 'damage');
|
||||
}
|
||||
|
||||
// Knockback (only if NPC is not KO)
|
||||
if (npcSprite && window.player && !isKO) {
|
||||
applyKnockback(npcSprite, window.player.x, window.player.y);
|
||||
}
|
||||
|
||||
// Screen shake (light)
|
||||
if (window.screenEffects) {
|
||||
window.screenEffects.shakeNPCHit();
|
||||
@@ -369,6 +384,40 @@ export class PlayerCombat {
|
||||
console.log(`Dealt ${damage} damage to ${npcId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play hit/taking-punch animation on NPC sprite
|
||||
* @param {Phaser.GameObjects.Sprite} sprite - The NPC sprite
|
||||
* @param {string} npcId - The NPC ID
|
||||
*/
|
||||
playHitAnimation(sprite, npcId) {
|
||||
if (!sprite || !sprite.scene || !npcId) return;
|
||||
|
||||
// Get NPC's current direction from behavior or animation
|
||||
let direction = 'down';
|
||||
|
||||
if (window.npcBehaviorManager) {
|
||||
const behavior = window.npcBehaviorManager.getBehavior(npcId);
|
||||
if (behavior && behavior.direction) {
|
||||
direction = behavior.direction;
|
||||
}
|
||||
}
|
||||
|
||||
// If no behavior direction, try to extract from current animation
|
||||
if (!direction && sprite.anims && sprite.anims.currentAnim) {
|
||||
const animKey = sprite.anims.currentAnim.key;
|
||||
const parts = animKey.split('-');
|
||||
if (parts.length >= 3) {
|
||||
direction = parts[parts.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
const hitAnimKey = `npc-${npcId}-hit-${direction}`;
|
||||
|
||||
if (sprite.scene.anims.exists(hitAnimKey)) {
|
||||
sprite.play(hitAnimKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply kick velocity to chair
|
||||
* @param {Phaser.GameObjects.Sprite} chair - Chair sprite
|
||||
|
||||
@@ -51,6 +51,12 @@ function damagePlayer(amount) {
|
||||
// Check for KO
|
||||
if (state.currentHP <= 0 && !state.isKO) {
|
||||
state.isKO = true;
|
||||
|
||||
// Play death animation
|
||||
if (window.player) {
|
||||
playPlayerDeathAnimation();
|
||||
}
|
||||
|
||||
if (window.eventDispatcher) {
|
||||
window.eventDispatcher.emit(CombatEvents.PLAYER_KO, {});
|
||||
}
|
||||
@@ -60,6 +66,59 @@ function damagePlayer(amount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play death animation for player
|
||||
*/
|
||||
function playPlayerDeathAnimation() {
|
||||
const player = window.player;
|
||||
if (!player || !player.scene) return;
|
||||
|
||||
// Get player's last facing direction
|
||||
const direction = player.lastDirection || 'down';
|
||||
|
||||
// Check if player uses atlas-based animations
|
||||
const texture = player.scene.textures.get(player.texture.key);
|
||||
const frames = texture ? texture.getFrameNames() : [];
|
||||
const isAtlas = frames.length > 0 && typeof frames[0] === 'string' && frames[0].includes('_frame_');
|
||||
|
||||
if (isAtlas) {
|
||||
// Try atlas-based death animations
|
||||
// Convert player direction to atlas compass direction
|
||||
const compassMap = {
|
||||
'down': 'south',
|
||||
'up': 'north',
|
||||
'left': 'west',
|
||||
'right': 'east',
|
||||
'down-left': 'south-west',
|
||||
'down-right': 'south-east',
|
||||
'up-left': 'north-west',
|
||||
'up-right': 'north-east'
|
||||
};
|
||||
|
||||
const compassDir = compassMap[direction] || 'south';
|
||||
const deathAnimKey = `falling-back-death_${compassDir}`;
|
||||
|
||||
if (player.scene.anims.exists(deathAnimKey)) {
|
||||
player.play(deathAnimKey);
|
||||
console.log(`💀 Playing player death animation: ${deathAnimKey}`);
|
||||
} else {
|
||||
console.warn(`⚠️ Death animation not found: ${deathAnimKey}`);
|
||||
// Log available death animations for debugging
|
||||
const deathAnims = Object.keys(player.scene.anims.anims.entries)
|
||||
.filter(key => key.includes('falling-back-death'));
|
||||
if (deathAnims.length > 0) {
|
||||
console.log(` Available death animations: ${deathAnims.join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disable player physics body to prevent further movement
|
||||
if (player.body) {
|
||||
player.body.setVelocity(0, 0);
|
||||
// Don't disable body entirely to keep collision detection for NPCs
|
||||
}
|
||||
}
|
||||
|
||||
function healPlayer(amount) {
|
||||
if (!state) return false;
|
||||
|
||||
|
||||
77
public/break_escape/js/utils/knockback.js
Normal file
77
public/break_escape/js/utils/knockback.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Knockback Utility
|
||||
* Handles pushing entities away when hit
|
||||
*/
|
||||
|
||||
const KNOCKBACK_CONFIG = {
|
||||
player: {
|
||||
strength: 200, // Knockback velocity when player is hit
|
||||
duration: 200 // How long knockback lasts (ms)
|
||||
},
|
||||
npc: {
|
||||
strength: 250, // Knockback velocity when NPC is hit
|
||||
duration: 250
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply knockback to a sprite
|
||||
* @param {Phaser.GameObjects.Sprite} target - The sprite to knockback
|
||||
* @param {number} sourceX - X position of the attacker
|
||||
* @param {number} sourceY - Y position of the attacker
|
||||
* @param {number} strength - Knockback force (default based on target type)
|
||||
* @param {number} duration - How long knockback lasts in ms
|
||||
*/
|
||||
export function applyKnockback(target, sourceX, sourceY, strength = null, duration = null) {
|
||||
if (!target || !target.body) {
|
||||
console.warn('⚠️ Cannot apply knockback - invalid target or no physics body');
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine default strength and duration based on target type
|
||||
const isPlayer = target === window.player;
|
||||
const config = isPlayer ? KNOCKBACK_CONFIG.player : KNOCKBACK_CONFIG.npc;
|
||||
|
||||
if (strength === null) strength = config.strength;
|
||||
if (duration === null) duration = config.duration;
|
||||
|
||||
// Calculate knockback direction (from source to target)
|
||||
const angle = Phaser.Math.Angle.Between(sourceX, sourceY, target.x, target.y);
|
||||
const velocityX = Math.cos(angle) * strength;
|
||||
const velocityY = Math.sin(angle) * strength;
|
||||
|
||||
// Apply knockback velocity
|
||||
target.body.setVelocity(velocityX, velocityY);
|
||||
|
||||
// Store original velocities to restore after knockback
|
||||
const originalVelX = target.body.velocity.x;
|
||||
const originalVelY = target.body.velocity.y;
|
||||
|
||||
// Clear knockback after duration
|
||||
if (target.knockbackTimer) {
|
||||
target.knockbackTimer.remove();
|
||||
}
|
||||
|
||||
const scene = target.scene;
|
||||
target.knockbackTimer = scene.time.delayedCall(duration, () => {
|
||||
// Reduce velocity gradually rather than stopping abruptly
|
||||
if (target.body) {
|
||||
target.body.setVelocity(
|
||||
target.body.velocity.x * 0.3,
|
||||
target.body.velocity.y * 0.3
|
||||
);
|
||||
}
|
||||
target.knockbackTimer = null;
|
||||
});
|
||||
|
||||
console.log(`💥 Knockback applied: ${target.name || 'sprite'} pushed away (strength: ${strength})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sprite is currently in knockback state
|
||||
* @param {Phaser.GameObjects.Sprite} sprite
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isInKnockback(sprite) {
|
||||
return sprite && sprite.knockbackTimer !== null && sprite.knockbackTimer !== undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user