diff --git a/public/break_escape/js/config/combat-config.js b/public/break_escape/js/config/combat-config.js index d453f17..0c25e92 100644 --- a/public/break_escape/js/config/combat-config.js +++ b/public/break_escape/js/config/combat-config.js @@ -36,7 +36,7 @@ export const COMBAT_CONFIG = { player: { maxHP: 100, punchDamage: 20, - punchRange: 60, + punchRange: 32, punchCooldown: 1000, punchAnimationDuration: 500 }, diff --git a/public/break_escape/js/systems/debug.js b/public/break_escape/js/systems/debug.js index cf0b8bf..5f292de 100644 --- a/public/break_escape/js/systems/debug.js +++ b/public/break_escape/js/systems/debug.js @@ -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() { diff --git a/public/break_escape/js/systems/player-combat.js b/public/break_escape/js/systems/player-combat.js index c5a5fc5..4f907fe 100644 --- a/public/break_escape/js/systems/player-combat.js +++ b/public/break_escape/js/systems/player-combat.js @@ -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(); + }); } /** diff --git a/public/break_escape/js/utils/constants.js b/public/break_escape/js/utils/constants.js index 9025bb4..7281ee6 100644 --- a/public/break_escape/js/utils/constants.js +++ b/public/break_escape/js/utils/constants.js @@ -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; \ No newline at end of file diff --git a/scenarios/m01_first_contact/scenario.json.erb b/scenarios/m01_first_contact/scenario.json.erb index 81de3d8..716a0de 100644 --- a/scenarios/m01_first_contact/scenario.json.erb +++ b/scenarios/m01_first_contact/scenario.json.erb @@ -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": {