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:
Z. Cliffe Schreuders
2026-02-13 23:26:09 +00:00
parent e749d705c6
commit a28e79bd6d
12 changed files with 4713 additions and 1367 deletions

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

View File

@@ -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);
}
}
}
}

View File

@@ -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}`;

View File

@@ -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);
}
});
}
}
/**

View File

@@ -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';

View File

@@ -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);

View File

@@ -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

View File

@@ -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;

View 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;
}