fix: adjust punch range and enhance hit detection for player combat system

This commit is contained in:
Z. Cliffe Schreuders
2026-02-19 15:54:38 +00:00
parent cb9be6ce1e
commit 416c0b9d11
5 changed files with 169 additions and 49 deletions

View File

@@ -36,7 +36,7 @@ export const COMBAT_CONFIG = {
player: {
maxHP: 100,
punchDamage: 20,
punchRange: 60,
punchRange: 32,
punchCooldown: 1000,
punchAnimationDuration: 500
},

View File

@@ -4,7 +4,7 @@
// Debug system variables
let debugMode = false;
let debugLevel = 1; // 1 = basic, 2 = detailed, 3 = verbose
let visualDebugMode = false; // Visual debug (collision boxes, movement vectors) - off by default
let visualDebugMode = true; // TEMPORARY: Visual debug (collision boxes, movement vectors) - on for testing
// Initialize the debug system
export function initializeDebugSystem() {

View File

@@ -233,15 +233,23 @@ export class PlayerCombat {
/**
* Check for hits on NPCs in range and direction
* Applies AOE damage to all NPCs in punch range AND facing direction
* Applies AOE damage to all NPCs in punch range AND facing direction.
*
* Origin is the player's foot / collision-box centre (not the sprite centre):
* Atlas 80x80 → body.setOffset(31, 66), size 18x10 → foot centre Y = sprite.y + 31
* Legacy 64x64 → body.setOffset(25, 50), size 15x10 → foot centre Y = sprite.y + 23
*/
checkForHits() {
if (!window.player) {
return;
}
const playerX = window.player.x;
const playerY = window.player.y;
const isAtlas = window.player.isAtlas;
// Foot-centre offset from sprite pivot (sprite uses 0.5 anchor = centre)
const footOffsetY = isAtlas ? 31 : 23;
const playerX = window.player.x; // Horizontally centred
const playerY = window.player.y + footOffsetY; // Feet position
const punchRange = COMBAT_CONFIG.player.punchRange;
// Get damage from current mode
@@ -251,6 +259,9 @@ export class PlayerCombat {
// Get player facing direction
const direction = window.player.lastDirection || 'down';
// Draw debug hit area
this.drawPunchHitbox(playerX, playerY, punchRange, direction);
// Get all NPCs from rooms
let hitCount = 0;
@@ -270,17 +281,8 @@ export class PlayerCombat {
return;
}
const npcX = npcSprite.x;
const npcY = npcSprite.y;
const distance = Phaser.Math.Distance.Between(playerX, playerY, npcX, npcY);
if (distance > punchRange) {
return; // Too far
}
// Check if NPC is in the facing direction
if (!this.isInDirection(playerX, playerY, npcX, npcY, direction)) {
return; // Not in facing direction
if (!this.bodyOverlapsCone(npcSprite.body, playerX, playerY, direction, punchRange)) {
return; // Outside cone
}
// Hit detected!
@@ -307,17 +309,8 @@ export class PlayerCombat {
return;
}
const chairX = chair.x;
const chairY = chair.y;
const distance = Phaser.Math.Distance.Between(playerX, playerY, chairX, chairY);
if (distance > punchRange) {
return; // Too far
}
// Check if chair is in the facing direction
if (!this.isInDirection(playerX, playerY, chairX, chairY, direction)) {
return; // Not in facing direction
if (!this.bodyOverlapsCone(chair.body, playerX, playerY, direction, punchRange)) {
return; // Outside cone
}
// Hit landed! Kick the chair
@@ -338,30 +331,157 @@ export class PlayerCombat {
}
/**
* Check if target is in the player's facing direction
* @param {number} playerX
* @param {number} playerY
* @param {number} targetX
* @param {number} targetY
* @param {string} direction - 'up', 'down', 'left', 'right'
* Check if a target falls inside the player's punch cone.
* The cone extends `range` px from the punch origin and spans ±45° around the
* facing direction. All 8 movement directions are supported.
*
* @param {number} originX - Punch origin X (foot centre)
* @param {number} originY - Punch origin Y (foot centre)
* @param {number} targetX - Target X
* @param {number} targetY - Target Y
* @param {string} direction - Player last direction (e.g. 'down', 'up-right')
* @param {number} range - Max reach in pixels
* @returns {boolean}
*/
isInDirection(playerX, playerY, targetX, targetY, direction) {
const dx = targetX - playerX;
const dy = targetY - playerY;
isInCone(originX, originY, targetX, targetY, direction, range) {
const dx = targetX - originX;
const dy = targetY - originY;
const distSq = dx * dx + dy * dy;
if (distSq > range * range) return false;
switch (direction) {
case 'up':
return dy < 0 && Math.abs(dy) > Math.abs(dx);
case 'down':
return dy > 0 && Math.abs(dy) > Math.abs(dx);
case 'left':
return dx < 0 && Math.abs(dx) > Math.abs(dy);
case 'right':
return dx > 0 && Math.abs(dx) > Math.abs(dy);
default:
return false;
// Facing angle in degrees (Phaser / canvas: right=0°, clockwise positive)
const facingAngles = {
'right': 0,
'down-right': 45,
'down': 90,
'down-left': 135,
'left': 180,
'up-left': 225,
'up': 270,
'up-right': 315
};
const facingDeg = facingAngles[direction] ?? 90; // default: down
const targetDeg = (Math.atan2(dy, dx) * (180 / Math.PI) + 360) % 360;
// Angular difference (shortest path around the circle)
let diff = Math.abs(targetDeg - facingDeg);
if (diff > 180) diff = 360 - diff;
return diff <= 30; // ±30° half-angle = 60° total cone
}
/**
* Check whether a Phaser Arcade physics body's bounding box overlaps the punch cone.
* Samples the 4 corners and centre of the body AABB — if any sample falls inside
* the cone (correct distance AND angle) the body counts as hit.
*
* Also returns true when the punch origin is inside the body itself (zero-distance
* edge case where atan2 is unreliable).
*
* @param {Phaser.Physics.Arcade.Body} body
* @param {number} originX - Punch origin X (player foot centre)
* @param {number} originY - Punch origin Y (player foot centre)
* @param {string} direction
* @param {number} range
* @returns {boolean}
*/
bodyOverlapsCone(body, originX, originY, direction, range) {
if (!body) return false;
// If the punch origin is inside the body, always count as a hit
if (originX >= body.left && originX <= body.right &&
originY >= body.top && originY <= body.bottom) {
return true;
}
// Sample the 4 AABB corners + centre
const cx = body.left + body.width * 0.5;
const cy = body.top + body.height * 0.5;
const samples = [
{ x: body.left, y: body.top },
{ x: body.right, y: body.top },
{ x: body.left, y: body.bottom },
{ x: body.right, y: body.bottom },
{ x: cx, y: cy }
];
return samples.some(p => this.isInCone(originX, originY, p.x, p.y, direction, range));
}
/**
* Draw the punch hit area for debugging.
* Shows a filled cone at the foot-centre origin for ~500 ms.
*
* @param {number} originX
* @param {number} originY
* @param {number} range
* @param {string} direction
*/
drawPunchHitbox(originX, originY, range, direction) {
if (!this.scene) return;
// Reuse or create the graphics layer
if (!this.hitboxGraphics) {
this.hitboxGraphics = this.scene.add.graphics();
this.hitboxGraphics.setDepth(9999);
this.hitboxGraphics.setScrollFactor(1);
}
this.hitboxGraphics.clear();
const facingAngles = {
'right': 0,
'down-right': 45,
'down': 90,
'down-left': 135,
'left': 180,
'up-left': 225,
'up': 270,
'up-right': 315
};
const facingDeg = facingAngles[direction] ?? 90;
const facingRad = facingDeg * (Math.PI / 180);
const halfAngle = 30 * (Math.PI / 180); // ±30° cone
const steps = 24;
// --- Filled cone ---
this.hitboxGraphics.fillStyle(0xff4400, 0.25);
this.hitboxGraphics.beginPath();
this.hitboxGraphics.moveTo(originX, originY);
for (let i = 0; i <= steps; i++) {
const a = (facingRad - halfAngle) + (i / steps) * halfAngle * 2;
this.hitboxGraphics.lineTo(
originX + Math.cos(a) * range,
originY + Math.sin(a) * range
);
}
this.hitboxGraphics.closePath();
this.hitboxGraphics.fillPath();
// --- Cone outline ---
this.hitboxGraphics.lineStyle(2, 0xff2200, 0.9);
this.hitboxGraphics.beginPath();
this.hitboxGraphics.moveTo(originX, originY);
for (let i = 0; i <= steps; i++) {
const a = (facingRad - halfAngle) + (i / steps) * halfAngle * 2;
this.hitboxGraphics.lineTo(
originX + Math.cos(a) * range,
originY + Math.sin(a) * range
);
}
this.hitboxGraphics.closePath();
this.hitboxGraphics.strokePath();
// --- Origin dot ---
this.hitboxGraphics.fillStyle(0xffff00, 1);
this.hitboxGraphics.fillCircle(originX, originY, 4);
// Auto-clear after 500 ms
if (this._hitboxClearTimer) this._hitboxClearTimer.remove();
this._hitboxClearTimer = this.scene.time.delayedCall(500, () => {
if (this.hitboxGraphics) this.hitboxGraphics.clear();
});
}
/**

View File

@@ -82,7 +82,7 @@ export const GAME_CONFIG = typeof Phaser !== 'undefined' ? {
default: 'arcade',
arcade: {
gravity: { y: 0 },
debug: false
debug: true // TEMPORARY: enable physics collision box visualisation
}
}
} : null;

View File

@@ -284,7 +284,7 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A
"id": "sarah_martinez",
"displayName": "Sarah Martinez",
"npcType": "person",
"position": { "x": 4, "y": 1.5 },
"position": { "x": 4, "y": 1.25 },
"spriteSheet": "female_office_worker",
"spriteTalk": "assets/characters/hacker-red-talk.png",
"spriteConfig": {