// Player System // Handles player creation, movement, and animation // Player management system import { MOVEMENT_SPEED, RUN_SPEED_MULTIPLIER, RUN_ANIMATION_MULTIPLIER, ARRIVAL_THRESHOLD, PLAYER_FEET_OFFSET_Y, ROOM_CHECK_THRESHOLD, CLICK_INDICATOR_SIZE, CLICK_INDICATOR_DURATION, SPRITE_PADDING_BOTTOM_ATLAS, SPRITE_PADDING_BOTTOM_LEGACY } from '../utils/constants.js?v=8'; export let player = null; export let targetPoint = null; export let isMoving = false; export let lastPlayerPosition = { x: 0, y: 0 }; let gameRef = null; // Keyboard input state const keyboardInput = { up: false, down: false, left: false, right: false, space: false, shift: false }; let isKeyboardMoving = false; // Keyboard pause state (for when minigames need keyboard input) let keyboardPaused = false; // Export functions to pause/resume keyboard interception export function pauseKeyboardInput() { keyboardPaused = true; console.log('🔒 Keyboard input PAUSED for minigame (keyboardPaused = true)'); } export function resumeKeyboardInput() { keyboardPaused = false; // Clear all keyboard state when resuming keyboardInput.up = false; keyboardInput.down = false; keyboardInput.left = false; keyboardInput.right = false; keyboardInput.space = false; keyboardInput.shift = false; isKeyboardMoving = false; console.log('🔓 Keyboard input RESUMED (keyboardPaused = false)'); } /** * Update the player sprite to use a different character * This allows changing the player's appearance mid-game * @param {string} newSpriteKey - The texture key for the new sprite */ export async function updatePlayerSprite(newSpriteKey) { if (!player || !gameRef) { console.error('❌ Cannot update player sprite - player or game not initialized'); return false; } console.log('🔄 Updating player sprite from', player.texture.key, 'to', newSpriteKey); // Check if the new sprite is already loaded const newTexture = gameRef.textures.get(newSpriteKey); if (!newTexture || newTexture.key === '__MISSING') { console.log('📦 Loading new sprite:', newSpriteKey); // Load the new sprite const assetsPath = window.breakEscapeConfig?.assetsPath || '/break_escape/assets'; const atlasPath = `${assetsPath}/characters/${newSpriteKey}.png`; const jsonPath = `${assetsPath}/characters/${newSpriteKey}.json`; try { await new Promise((resolve, reject) => { gameRef.load.atlas(newSpriteKey, atlasPath, jsonPath); gameRef.load.once('complete', resolve); gameRef.load.once('loaderror', reject); gameRef.load.start(); }); console.log('✅ New sprite loaded:', newSpriteKey); } catch (error) { console.error('❌ Failed to load new sprite:', error); return false; } } // Store current state const currentDirection = player.direction || 'down'; const wasMoving = player.isMoving; // Detect if new sprite is atlas or legacy const frames = gameRef.textures.get(newSpriteKey).getFrameNames(); const isAtlas = frames.length > 0 && typeof frames[0] === 'string' && (frames[0].includes('breathing-idle') || frames[0].includes('walk_') || frames[0].includes('_frame_')); // Update collision box for new sprite type if (isAtlas) { player.body.setSize(18, 10); player.body.setOffset(31, 66); console.log('🎮 Updated collision box for atlas sprite (80x80)'); } else { player.body.setSize(15, 10); player.body.setOffset(25, 50); console.log('🎮 Updated collision box for legacy sprite (64x64)'); } // Store the atlas flag on player player.isAtlas = isAtlas; // Update scenario reference BEFORE recreating animations so createPlayerAnimations() uses the new sprite if (window.gameScenario?.player) { window.gameScenario.player.spriteSheet = newSpriteKey; } // Destroy old animations before creating new ones (they reference the old sprite texture) const animKeysToDestroy = [ 'idle-down', 'idle-up', 'idle-left', 'idle-right', 'idle-down-left', 'idle-down-right', 'idle-up-left', 'idle-up-right', 'walk-down', 'walk-up', 'walk-left', 'walk-right', 'walk-down-left', 'walk-down-right', 'walk-up-left', 'walk-up-right', 'punch-down', 'punch-up', 'punch-left', 'punch-right' ]; // Also destroy punch animations with compass directions const punchDirections = ['north', 'south', 'east', 'west', 'north-east', 'north-west', 'south-east', 'south-west']; punchDirections.forEach(dir => { animKeysToDestroy.push(`cross-punch_${dir}`); animKeysToDestroy.push(`lead-jab_${dir}`); }); animKeysToDestroy.forEach(key => { if (gameRef.anims.exists(key)) { gameRef.anims.remove(key); } }); console.log('🗑️ Removed old animations'); // Change the texture of the existing sprite let initialFrame; if (isAtlas) { const breathingIdleFrames = frames.filter(f => f.startsWith('breathing-idle_south_frame_')); initialFrame = breathingIdleFrames.length > 0 ? breathingIdleFrames[0] : frames[0]; } else { initialFrame = 20; } player.setTexture(newSpriteKey, initialFrame); // Recreate animations for the new sprite (now reads updated scenario) createPlayerAnimations(); // Play appropriate animation const animKey = wasMoving ? `walk-${currentDirection}` : `idle-${currentDirection}`; if (player.anims.exists(animKey)) { player.anims.play(animKey, true); } console.log('✅ Player sprite updated successfully to', newSpriteKey); return true; } // Create player sprite export function createPlayer(gameInstance) { gameRef = gameInstance; console.log('Creating player'); // Get starting room position and calculate center const scenario = window.gameScenario; const startRoomId = scenario ? scenario.startRoom : 'reception'; const startRoomPosition = getStartingRoomCenter(startRoomId); // Get player sprite - prioritize saved preference over scenario default const playerSprite = window.breakEscapeConfig?.playerSprite || window.gameScenario?.player?.spriteSheet || 'male_hacker'; const source = window.breakEscapeConfig?.playerSprite ? 'saved preference' : (window.gameScenario?.player ? 'scenario' : 'default'); console.log(`🎮 Loading player sprite: ${playerSprite} (from ${source})`); // Update scenario to match saved preference if (window.gameScenario?.player && window.breakEscapeConfig?.playerSprite) { window.gameScenario.player.spriteSheet = window.breakEscapeConfig.playerSprite; } // Check if this is an atlas sprite (has named frames) or legacy (numbered frames) const texture = gameInstance.textures.get(playerSprite); const frames = texture ? texture.getFrameNames() : []; // More robust atlas detection let isAtlas = false; if (frames.length > 0) { const firstFrame = frames[0]; isAtlas = typeof firstFrame === 'string' && (firstFrame.includes('breathing-idle') || firstFrame.includes('walk_') || firstFrame.includes('_frame_')); } console.log(`🔍 Player sprite ${playerSprite}: ${frames.length} frames, first: "${frames[0]}", isAtlas: ${isAtlas}`); // Create player sprite with appropriate initial frame let initialFrame; if (isAtlas) { // Find first breathing-idle_south frame const breathingIdleFrames = frames.filter(f => f.startsWith('breathing-idle_south_frame_')); initialFrame = breathingIdleFrames.length > 0 ? breathingIdleFrames[0] : frames[0]; } else { initialFrame = 20; // Legacy default } player = gameInstance.add.sprite(startRoomPosition.x, startRoomPosition.y, playerSprite, initialFrame); gameInstance.physics.add.existing(player); // Store atlas detection flag for depth calculations player.isAtlas = isAtlas; // Keep the character at original size player.setScale(1); // Set smaller collision box at the feet // Atlas sprites (80x80) vs Legacy sprites (64x64) have different offsets if (isAtlas) { // 80x80 sprite - collision box at feet player.body.setSize(18, 10); player.body.setOffset(31, 66); // Center horizontally (80-18)/2=31, feet at bottom 80-14=66 console.log('🎮 Player using atlas sprite (80x80) with adjusted collision box'); } else { // 64x64 sprite - legacy collision box player.body.setSize(15, 10); player.body.setOffset(25, 50); // Legacy offset for 64px sprite console.log('🎮 Player using legacy sprite (64x64) with standard collision box'); } player.body.setCollideWorldBounds(true); player.body.setBounce(0); player.body.setDrag(0); player.body.setFriction(0); // Set initial player depth (will be updated dynamically during movement) updatePlayerDepth(startRoomPosition.x, startRoomPosition.y); // Track player direction and movement state player.direction = 'down'; // Initial direction player.isMoving = false; player.lastDirection = 'down'; // Create animations createPlayerAnimations(); // Set initial animation player.anims.play('idle-down', true); // Initialize last position lastPlayerPosition = { x: player.x, y: player.y }; // Store player globally immediately for safety window.player = player; // Setup keyboard input listeners setupKeyboardInput(); return player; } function setupKeyboardInput() { // Handle keydown events document.addEventListener('keydown', (event) => { // Skip if keyboard input is paused (for minigames that need keyboard input) if (keyboardPaused) { console.log('⏸️ Keydown blocked (paused):', event.key); return; } const key = event.key.toLowerCase(); // Shift key for running if (key === 'shift') { keyboardInput.shift = true; event.preventDefault(); return; } // Spacebar for jump if (key === ' ') { keyboardInput.space = true; if (window.createPlayerJump) { window.createPlayerJump(); } event.preventDefault(); return; } // E key for interaction if (key === 'e') { // Check interaction mode - if in punch mode, just punch in current direction if (window.playerCombat) { const currentMode = window.playerCombat.getInteractionMode(); if (currentMode === 'jab' || currentMode === 'cross') { // Punch in current facing direction (don't interact) window.playerCombat.punch(); event.preventDefault(); return; } } // Normal interaction mode - interact with nearest object if (window.tryInteractWithNearest) { window.tryInteractWithNearest(); } event.preventDefault(); return; } // Arrow keys if (key === 'arrowup') { keyboardInput.up = true; isKeyboardMoving = true; event.preventDefault(); } else if (key === 'arrowdown') { keyboardInput.down = true; isKeyboardMoving = true; event.preventDefault(); } else if (key === 'arrowleft') { keyboardInput.left = true; isKeyboardMoving = true; event.preventDefault(); } else if (key === 'arrowright') { keyboardInput.right = true; isKeyboardMoving = true; event.preventDefault(); } // WASD keys if (key === 'w') { keyboardInput.up = true; isKeyboardMoving = true; event.preventDefault(); } else if (key === 's') { keyboardInput.down = true; isKeyboardMoving = true; event.preventDefault(); } else if (key === 'a') { keyboardInput.left = true; isKeyboardMoving = true; event.preventDefault(); } else if (key === 'd') { keyboardInput.right = true; isKeyboardMoving = true; event.preventDefault(); } }); // Handle keyup events document.addEventListener('keyup', (event) => { // Skip if keyboard input is paused (for minigames that need keyboard input) if (keyboardPaused) { return; } const key = event.key.toLowerCase(); // Shift key if (key === 'shift') { keyboardInput.shift = false; event.preventDefault(); return; } // Spacebar if (key === ' ') { keyboardInput.space = false; event.preventDefault(); return; } // Arrow keys if (key === 'arrowup') { keyboardInput.up = false; event.preventDefault(); } else if (key === 'arrowdown') { keyboardInput.down = false; event.preventDefault(); } else if (key === 'arrowleft') { keyboardInput.left = false; event.preventDefault(); } else if (key === 'arrowright') { keyboardInput.right = false; event.preventDefault(); } // WASD keys if (key === 'w') { keyboardInput.up = false; event.preventDefault(); } else if (key === 's') { keyboardInput.down = false; event.preventDefault(); } else if (key === 'a') { keyboardInput.left = false; event.preventDefault(); } else if (key === 'd') { keyboardInput.right = false; event.preventDefault(); } // Check if any keys are still pressed isKeyboardMoving = keyboardInput.up || keyboardInput.down || keyboardInput.left || keyboardInput.right; }); } function getAnimationKey(direction) { // Check if player uses atlas-based animations (has native left directions) // For atlas sprites, all 8 directions exist natively const hasNativeLeft = gameRef?.anims?.exists(`idle-left`) || gameRef?.anims?.exists(`walk-left`); if (hasNativeLeft) { // Atlas sprite - use native directions return direction; } // Legacy sprite - map left directions to their right counterparts (sprite is flipped) switch(direction) { case 'left': return 'right'; case 'down-left': return 'down-right'; case 'up-left': return 'up-right'; default: return direction; } } /** * Map player directions to compass directions for punch animations * @param {string} direction - Player direction (down, up, left, right, down-left, etc.) * @returns {string} - Compass direction (south, north, west, east, south-west, etc.) */ function mapPlayerDirectionToCompass(direction) { const directionMap = { 'right': 'east', 'left': 'west', 'up': 'north', 'down': 'south', 'up-right': 'north-east', 'up-left': 'north-west', 'down-right': 'south-east', 'down-left': 'south-west' }; return directionMap[direction] || 'south'; } function updateAnimationSpeed(isRunning) { // Update animation speed based on whether player is running if (!player || !player.anims) { return; } const frameRate = isRunning ? 8 * RUN_ANIMATION_MULTIPLIER : 8; // If there's a current animation playing, update its frameRate if (player.anims.currentAnim) { player.anims.currentAnim.frameRate = frameRate; } } function createPlayerAnimations() { const playerSprite = window.gameScenario?.player?.spriteSheet || 'hacker'; // Check if this is an atlas sprite (has named frames) or legacy (numbered frames) const texture = gameRef.textures.get(playerSprite); const frames = texture ? texture.getFrameNames() : []; // More robust atlas detection let isAtlas = false; if (frames.length > 0) { const firstFrame = frames[0]; isAtlas = typeof firstFrame === 'string' && (firstFrame.includes('breathing-idle') || firstFrame.includes('walk_') || firstFrame.includes('_frame_')); } console.log(`🔍 Player sprite ${playerSprite}: ${frames.length} frames, first: "${frames[0]}", isAtlas: ${isAtlas}`); if (isAtlas) { console.log(`🎮 Player using atlas sprite: ${playerSprite}`); createAtlasPlayerAnimations(playerSprite); } else { console.log(`🎮 Player using legacy sprite: ${playerSprite}`); createLegacyPlayerAnimations(playerSprite); } } function createAtlasPlayerAnimations(spriteSheet) { // Get texture and build animation data from frame names const texture = gameRef.textures.get(spriteSheet); const frameNames = texture.getFrameNames(); // Build animations object from frame names const animations = {}; frameNames.forEach(frameName => { // Parse frame name: "breathing-idle_south_frame_000" -> animation: "breathing-idle_south" const match = frameName.match(/^(.+)_frame_\d+$/); if (match) { const animKey = match[1]; if (!animations[animKey]) { animations[animKey] = []; } animations[animKey].push(frameName); } }); // Sort frames within each animation Object.keys(animations).forEach(key => { animations[key].sort(); }); if (Object.keys(animations).length === 0) { console.warn(`⚠️ No animation data found in atlas: ${spriteSheet}`); return; } // Get frame rates from player config const playerConfig = window.gameScenario?.player?.spriteConfig || {}; const idleFrameRate = playerConfig.idleFrameRate || 6; // Slower for breathing effect const walkFrameRate = playerConfig.walkFrameRate || 10; const punchFrameRate = playerConfig.punchFrameRate || 12; // Faster for action animations // Direction mapping: atlas directions → player directions const directionMap = { 'east': 'right', 'west': 'left', 'north': 'up', 'south': 'down', 'north-east': 'up-right', 'north-west': 'up-left', 'south-east': 'down-right', 'south-west': 'down-left' }; // Animation type mapping: atlas animations → player animations const animTypeMap = { 'breathing-idle': 'idle', 'walk': 'walk', 'taking-punch': 'hit' }; // Animation type framework (for grouping and frame rate) const animationFramework = { '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' }, 'taking-punch': { frameRate: 12, repeat: 0, name: 'hit' }, 'falling-back-death': { frameRate: 10, repeat: 0, name: 'death' } }; // Create animations from atlas metadata for (const [atlasAnimKey, frames] of Object.entries(animations)) { // Parse animation key: "breathing-idle_east" → type: "breathing-idle", direction: "east" const parts = atlasAnimKey.split('_'); const atlasDirection = parts[parts.length - 1]; const atlasType = parts.slice(0, -1).join('_'); // Map to player direction and type const playerDirection = directionMap[atlasDirection] || atlasDirection; const playerType = animTypeMap[atlasType] || atlasType; // Create animation key: "walk-right", "idle-down", "cross-punch_east", "lead-jab_south", etc. const animKey = playerType === 'idle' || playerType === 'walk' ? `${playerType}-${playerDirection}` : `${playerType}_${atlasDirection}`; // Keep atlas direction for punch animations // Debug for punch animations if (playerType === 'cross-punch' || playerType === 'lead-jab') { console.log(` - Punch anim: ${atlasAnimKey} → type: ${playerType}, direction: ${atlasDirection}, key: ${animKey}, frames: ${frames.length}`); } // For idle animations, create a custom sequence: hold rotation frame for 2s, then loop breathing animation if (playerType === 'idle') { // Use the first frame of the rotation image (e.g., breathing-idle_{direction}_frame_000) const rotationFrame = frames[0]; // Remaining frames are the breathing animation const breathFrames = frames.slice(1); // Build custom animation sequence const idleAnimFrames = [ { key: spriteSheet, frame: rotationFrame, duration: 2000 }, // Hold for 2s ...breathFrames.map(frameName => ({ key: spriteSheet, frame: frameName, duration: 200 })) ]; if (!gameRef.anims.exists(animKey)) { gameRef.anims.create({ key: animKey, frames: idleAnimFrames, frameRate: idleFrameRate, repeat: -1 }); console.log(` ✓ Created custom idle animation: ${animKey} (rotation + breath, ${idleAnimFrames.length} frames)`); } } else { // Standard animation (walk, cross-punch, lead-jab, etc.) const frameConfig = animationFramework[playerType] || { frameRate: walkFrameRate, repeat: -1 }; if (!gameRef.anims.exists(animKey)) { const frameArray = frames.map(frameName => ({ key: spriteSheet, frame: frameName })); console.log(` - Creating ${animKey} with ${frameArray.length} frames, frameRate: ${frameConfig.frameRate}, repeat: ${frameConfig.repeat}`); if (frameArray.length === 0) { console.warn(` ⚠️ Warning: Animation has 0 frames!`); } gameRef.anims.create({ key: animKey, frames: frameArray, frameRate: frameConfig.frameRate, repeat: frameConfig.repeat }); console.log(` ✓ Created ${frameConfig.name} animation: ${animKey} (${frames.length} frames @ ${frameConfig.frameRate} fps, repeat: ${frameConfig.repeat})`); } } } console.log(`✅ Player atlas animations created for ${spriteSheet} (idle: ${idleFrameRate} fps, walk: ${walkFrameRate} fps, punch: ${punchFrameRate} fps)`); // Log all punch animations created const punchAnims = Object.keys(animations).filter(key => key.includes('cross-punch') || key.includes('lead-jab')); if (punchAnims.length > 0) { console.log(`🥊 Punch animations available (${punchAnims.length} total):`); punchAnims.forEach(animName => { const frameCount = animations[animName].length; console.log(` - ${animName}: ${frameCount} frames`); }); } else { console.warn('⚠️ No punch animations found in atlas!'); } } function createLegacyPlayerAnimations(spriteSheet) { // Create walking animations with correct frame numbers from original gameRef.anims.create({ key: 'walk-right', frames: gameRef.anims.generateFrameNumbers(spriteSheet, { start: 1, end: 4 }), frameRate: 8, repeat: -1 }); gameRef.anims.create({ key: 'walk-down', frames: gameRef.anims.generateFrameNumbers(spriteSheet, { start: 6, end: 9 }), frameRate: 8, repeat: -1 }); gameRef.anims.create({ key: 'walk-up', frames: gameRef.anims.generateFrameNumbers(spriteSheet, { start: 11, end: 14 }), frameRate: 8, repeat: -1 }); gameRef.anims.create({ key: 'walk-up-right', frames: gameRef.anims.generateFrameNumbers(spriteSheet, { start: 16, end: 19 }), frameRate: 8, repeat: -1 }); gameRef.anims.create({ key: 'walk-down-right', frames: gameRef.anims.generateFrameNumbers(spriteSheet, { start: 21, end: 24 }), frameRate: 8, repeat: -1 }); // Create idle frames (first frame of each row) with correct frame numbers gameRef.anims.create({ key: 'idle-right', frames: [{ key: spriteSheet, frame: 0 }], frameRate: 1 }); gameRef.anims.create({ key: 'idle-down', frames: [{ key: spriteSheet, frame: 5 }], frameRate: 1 }); gameRef.anims.create({ key: 'idle-up', frames: [{ key: spriteSheet, frame: 10 }], frameRate: 1 }); gameRef.anims.create({ key: 'idle-up-right', frames: [{ key: spriteSheet, frame: 15 }], frameRate: 1 }); gameRef.anims.create({ key: 'idle-down-right', frames: [{ key: spriteSheet, frame: 20 }], frameRate: 1 }); // Create left-facing idle animations (same frames as right, but sprite will be flipped) gameRef.anims.create({ key: 'idle-left', frames: [{ key: spriteSheet, frame: 0 }], frameRate: 1 }); gameRef.anims.create({ key: 'idle-down-left', frames: [{ key: spriteSheet, frame: 20 }], frameRate: 1 }); gameRef.anims.create({ key: 'idle-up-left', frames: [{ key: spriteSheet, frame: 15 }], frameRate: 1 }); console.log(`✅ Player legacy animations created for ${spriteSheet}`); } export function movePlayerToPoint(x, y) { const worldBounds = gameRef.physics.world.bounds; // Ensure coordinates are within bounds x = Phaser.Math.Clamp(x, worldBounds.x, worldBounds.x + worldBounds.width); y = Phaser.Math.Clamp(y, worldBounds.y, worldBounds.y + worldBounds.height); // Create click indicator createClickIndicator(x, y); targetPoint = { x, y }; isMoving = true; // Notify tutorial of movement if (window.getTutorialManager) { const tutorialManager = window.getTutorialManager(); tutorialManager.notifyPlayerMoved(); tutorialManager.notifyPlayerClickedToMove(); } } /** * Turn the player to face a world position, updating direction and idle animation. * Call this before triggering a click-based interaction so the player visually * faces the object/NPC they are acting on. */ export function facePlayerToward(targetX, targetY) { if (!player) return; const dx = targetX - player.x; const dy = targetY - player.y; const absX = Math.abs(dx); const absY = Math.abs(dy); let direction; if (absX > absY * 2) { direction = dx > 0 ? 'right' : 'left'; } else if (absY > absX * 2) { direction = dy > 0 ? 'down' : 'up'; } else { direction = dy > 0 ? (dx > 0 ? 'down-right' : 'down-left') : (dx > 0 ? 'up-right' : 'up-left'); } player.direction = direction; player.lastDirection = direction; // Play idle animation for the new direction (handles atlas vs legacy sprite mapping) const animDir = getAnimationKey(direction); const currentAnim = player.anims.currentAnim?.key || ''; if (!currentAnim.includes('punch') && !currentAnim.includes('jab') && !currentAnim.includes('death') && !currentAnim.includes('taking-punch')) { player.anims.play(`idle-${animDir}`, true); } } function updatePlayerDepth(x, y) { // Get the bottom of the player sprite, accounting for padding // Atlas sprites (80x80) have 16px padding at bottom, legacy sprites (64x64) have minimal padding // Use actual y parameter so depth follows visual position (including during death animations) const spriteCenterToBottom = (player.height * player.scaleY) / 2; const paddingOffset = player.isAtlas ? SPRITE_PADDING_BOTTOM_ATLAS : SPRITE_PADDING_BOTTOM_LEGACY; const playerBottomY = y + spriteCenterToBottom - paddingOffset; // Simple depth calculation: world Y position + layer offset const playerDepth = playerBottomY + 0.5; // World Y + sprite layer offset // Set the player depth (always update, no threshold) if (player) { player.setDepth(playerDepth); // Debug logging - only show when depth actually changes significantly const lastDepth = player.lastDepth || 0; if (Math.abs(playerDepth - lastDepth) > 25) { // Reduced threshold for finer granularity console.log(`Player depth: ${playerDepth} (Feet Y: ${playerBottomY}, Padding: ${paddingOffset}px)`); console.log(` Player layers: feetY(${playerBottomY}) + 0.5`); player.lastDepth = playerDepth; } } } function createClickIndicator(x, y) { // Create a circle at the click position const indicator = gameRef.add.circle(x, y, CLICK_INDICATOR_SIZE, 0xffffff, 0.7); indicator.setDepth(1000); // Above ground but below player // Add a pulsing animation gameRef.tweens.add({ targets: indicator, scale: { from: 0.5, to: 1.5 }, alpha: { from: 0.7, to: 0 }, duration: CLICK_INDICATOR_DURATION, ease: 'Sine.easeOut', onComplete: () => { indicator.destroy(); } }); } export function updatePlayerMovement() { // Safety check: ensure player exists if (!player || !player.body) { return; } // Check if player is KO (knocked out) - disable movement if (window.playerHealth && window.playerHealth.isKO()) { player.body.setVelocity(0, 0); return; } // Check if movement is explicitly disabled if (player.disableMovement) { player.body.setVelocity(0, 0); return; } // Handle keyboard movement (takes priority over mouse movement) if (isKeyboardMoving) { updatePlayerKeyboardMovement(); } else { // Handle mouse-based movement (original behavior) updatePlayerMouseMovement(); } // Final check: if velocity is 0 and player is marked as moving, switch to idle if (player.body.velocity.x === 0 && player.body.velocity.y === 0 && player.isMoving) { player.isMoving = false; const animDir = getAnimationKey(player.direction); // Don't interrupt special animations: punch, death, hit, etc. const currentAnim = player.anims.currentAnim?.key || ''; if (!currentAnim.includes('punch') && !currentAnim.includes('jab') && !currentAnim.includes('death') && !currentAnim.includes('taking-punch')) { player.anims.play(`idle-${animDir}`, true); } } } function updatePlayerKeyboardMovement() { // Cancel click-to-move when keyboard input is detected if (isMoving || targetPoint) { isMoving = false; targetPoint = null; } // Calculate movement direction based on keyboard input let dirX = 0; let dirY = 0; if (keyboardInput.right) dirX += 1; if (keyboardInput.left) dirX -= 1; if (keyboardInput.down) dirY += 1; if (keyboardInput.up) dirY -= 1; // Normalize diagonal movement to maintain consistent speed let velocityX = 0; let velocityY = 0; if (dirX !== 0 || dirY !== 0) { const magnitude = Math.sqrt(dirX * dirX + dirY * dirY); // Apply run speed multiplier if shift is held const speed = keyboardInput.shift ? MOVEMENT_SPEED * RUN_SPEED_MULTIPLIER : MOVEMENT_SPEED; velocityX = (dirX / magnitude) * speed; velocityY = (dirY / magnitude) * speed; // Notify tutorial of movement and running if (window.getTutorialManager) { const tutorialManager = window.getTutorialManager(); tutorialManager.notifyPlayerMoved(); if (keyboardInput.shift) { tutorialManager.notifyPlayerRan(); } } } // Update animation speed every frame while moving if (player.isMoving) { updateAnimationSpeed(keyboardInput.shift); } // Check if movement is being blocked by collisions let isBlocked = false; if (velocityX !== 0 || velocityY !== 0) { // Check if blocked in the direction we want to move if (velocityX > 0 && player.body.blocked.right) isBlocked = true; if (velocityX < 0 && player.body.blocked.left) isBlocked = true; if (velocityY > 0 && player.body.blocked.down) isBlocked = true; if (velocityY < 0 && player.body.blocked.up) isBlocked = true; } // Apply velocity player.body.setVelocity(velocityX, velocityY); // Update player depth based on actual player position updatePlayerDepth(player.x, player.y); // Update last player position for depth calculations lastPlayerPosition.x = player.x; lastPlayerPosition.y = player.y; // Determine direction based on velocity const absVX = Math.abs(velocityX); const absVY = Math.abs(velocityY); // Set player direction and animation if (velocityX === 0 && velocityY === 0) { // No movement - stop if (player.isMoving) { player.isMoving = false; const animDir = getAnimationKey(player.direction); player.anims.stop(); // Stop current animation // Don't interrupt special animations: punch, death, hit, etc. const currentAnim = player.anims.currentAnim?.key || ''; if (!currentAnim.includes('punch') && !currentAnim.includes('jab') && !currentAnim.includes('death') && !currentAnim.includes('taking-punch')) { player.anims.play(`idle-${animDir}`, true); } } } else if (isBlocked) { // Blocked by collision - preserve special animations but switch walk to idle player.isMoving = false; const currentAnim = player.anims.currentAnim?.key || ''; // Only change animation if it's a walk animation // Preserve punch, jab, death, taking-punch, etc. if (currentAnim.includes('walk')) { const animDir = getAnimationKey(player.direction); player.anims.play(`idle-${animDir}`, true); } } else if (absVX > absVY * 2) { // Mostly horizontal movement player.direction = velocityX > 0 ? 'right' : 'left'; // Check if we have native left animations (atlas sprite) const hasNativeLeft = gameRef.anims.exists('walk-left'); const animDir = hasNativeLeft ? player.direction : (velocityX > 0 ? 'right' : 'right'); const shouldFlip = !hasNativeLeft && velocityX < 0; player.setFlipX(shouldFlip); if (!player.isMoving || player.lastDirection !== player.direction) { const currentAnim = player.anims.currentAnim?.key || ''; // If punching, restart punch animation in new direction if (currentAnim.includes('punch') || currentAnim.includes('jab')) { const animType = currentAnim.includes('cross-punch') ? 'cross-punch' : 'lead-jab'; const compassDir = mapPlayerDirectionToCompass(player.direction); const newPunchKey = `${animType}_${compassDir}`; if (gameRef.anims.exists(newPunchKey)) { player.anims.play(newPunchKey, true); console.log(`🥊 Direction changed during punch, restarting: ${newPunchKey}`); } } else { // Normal walk animation player.anims.play(`walk-${animDir}`, true); } player.isMoving = true; player.lastDirection = player.direction; } } else if (absVY > absVX * 2) { // Mostly vertical movement player.direction = velocityY > 0 ? 'down' : 'up'; player.setFlipX(false); if (!player.isMoving || player.lastDirection !== player.direction) { const currentAnim = player.anims.currentAnim?.key || ''; // If punching, restart punch animation in new direction if (currentAnim.includes('punch') || currentAnim.includes('jab')) { const animType = currentAnim.includes('cross-punch') ? 'cross-punch' : 'lead-jab'; const compassDir = mapPlayerDirectionToCompass(player.direction); const newPunchKey = `${animType}_${compassDir}`; if (gameRef.anims.exists(newPunchKey)) { player.anims.play(newPunchKey, true); console.log(`🥊 Direction changed during punch, restarting: ${newPunchKey}`); } } else { // Normal walk animation player.anims.play(`walk-${player.direction}`, true); } player.isMoving = true; player.lastDirection = player.direction; } } else { // Diagonal movement if (velocityY > 0) { player.direction = velocityX > 0 ? 'down-right' : 'down-left'; } else { player.direction = velocityX > 0 ? 'up-right' : 'up-left'; } // Check if we have native left animations (atlas sprite) const hasNativeLeft = gameRef.anims.exists('walk-down-left') || gameRef.anims.exists('walk-up-left'); const baseDir = hasNativeLeft ? player.direction : (velocityY > 0 ? 'down-right' : 'up-right'); const shouldFlip = !hasNativeLeft && velocityX < 0; player.setFlipX(shouldFlip); if (!player.isMoving || player.lastDirection !== player.direction) { const currentAnim = player.anims.currentAnim?.key || ''; // If punching, restart punch animation in new direction if (currentAnim.includes('punch') || currentAnim.includes('jab')) { const animType = currentAnim.includes('cross-punch') ? 'cross-punch' : 'lead-jab'; const compassDir = mapPlayerDirectionToCompass(player.direction); const newPunchKey = `${animType}_${compassDir}`; if (gameRef.anims.exists(newPunchKey)) { player.anims.play(newPunchKey, true); console.log(`🥊 Direction changed during punch, restarting: ${newPunchKey}`); } } else { // Normal walk animation player.anims.play(`walk-${baseDir}`, true); } player.isMoving = true; player.lastDirection = player.direction; } } } function updatePlayerMouseMovement() { if (!isMoving || !targetPoint) { if (player.body.velocity.x !== 0 || player.body.velocity.y !== 0) { player.body.setVelocity(0, 0); player.isMoving = false; // Play idle animation based on last direction // Don't interrupt special animations: punch, death, hit, etc. const currentAnim = player.anims.currentAnim?.key || ''; if (!currentAnim.includes('punch') && !currentAnim.includes('jab') && !currentAnim.includes('death') && !currentAnim.includes('taking-punch')) { player.anims.play(`idle-${player.direction}`, true); } } return; } // Cache player position - adjust for feet position const px = player.x; const py = player.y + PLAYER_FEET_OFFSET_Y; // Add offset to target the feet // Update player depth based on actual player position (not feet-adjusted) updatePlayerDepth(px, player.y); // Use squared distance for performance const dx = targetPoint.x - px; const dy = targetPoint.y - py; // Compare with feet position const distanceSq = dx * dx + dy * dy; // Reached target point if (distanceSq < ARRIVAL_THRESHOLD * ARRIVAL_THRESHOLD) { isMoving = false; player.body.setVelocity(0, 0); if (player.isMoving) { player.isMoving = false; const animDir = getAnimationKey(player.direction); player.anims.stop(); // Stop current animation // Don't interrupt special animations: punch, death, hit, etc. const currentAnim = player.anims.currentAnim?.key || ''; if (!currentAnim.includes('punch') && !currentAnim.includes('jab') && !currentAnim.includes('death') && !currentAnim.includes('taking-punch')) { player.anims.play(`idle-${animDir}`, true); } } return; } // Update last player position for depth calculations lastPlayerPosition.x = px; lastPlayerPosition.y = py - PLAYER_FEET_OFFSET_Y; // Store actual player position // Normalize movement vector for consistent speed const distance = Math.sqrt(distanceSq); const velocityX = (dx / distance) * MOVEMENT_SPEED; const velocityY = (dy / distance) * MOVEMENT_SPEED; // Set velocity directly without checking for changes player.body.setVelocity(velocityX, velocityY); // Determine direction based on velocity const absVX = Math.abs(velocityX); const absVY = Math.abs(velocityY); // Check if we have native left animations (atlas sprite) const hasNativeLeft = gameRef.anims.exists('walk-left') || gameRef.anims.exists('walk-down-left'); // Set player direction and animation if (absVX > absVY * 2) { // Mostly horizontal movement player.direction = velocityX > 0 ? 'right' : (hasNativeLeft ? 'left' : 'right'); player.setFlipX(!hasNativeLeft && velocityX < 0); } else if (absVY > absVX * 2) { // Mostly vertical movement player.direction = velocityY > 0 ? 'down' : 'up'; player.setFlipX(false); } else { // Diagonal movement if (velocityY > 0) { player.direction = velocityX > 0 ? 'down-right' : (hasNativeLeft ? 'down-left' : 'down-right'); } else { player.direction = velocityX > 0 ? 'up-right' : (hasNativeLeft ? 'up-left' : 'up-right'); } player.setFlipX(!hasNativeLeft && velocityX < 0); } // Play appropriate animation if not already playing if (!player.isMoving || player.lastDirection !== player.direction) { const currentAnim = player.anims.currentAnim?.key || ''; // If punching, restart punch animation in new direction if (currentAnim.includes('punch') || currentAnim.includes('jab')) { const animType = currentAnim.includes('cross-punch') ? 'cross-punch' : 'lead-jab'; const compassDir = mapPlayerDirectionToCompass(player.direction); const newPunchKey = `${animType}_${compassDir}`; if (gameRef.anims.exists(newPunchKey)) { player.anims.play(newPunchKey, true); console.log(`🥊 Mouse movement: direction changed during punch, restarting: ${newPunchKey}`); } } else { // Normal walk animation player.anims.play(`walk-${player.direction}`, true); } player.isMoving = true; player.lastDirection = player.direction; } // Stop if collision detected if (player.body.blocked.none === false) { isMoving = false; player.body.setVelocity(0, 0); player.isMoving = false; // Switch walk animations to idle, but preserve special animations const currentAnim = player.anims.currentAnim?.key || ''; if (currentAnim.includes('walk')) { const animDir = getAnimationKey(player.direction); player.anims.play(`idle-${animDir}`, true); } } } function getStartingRoomCenter(startRoomId) { // Default position if rooms not initialized yet const defaultPos = { x: 160, y: 144 }; // If rooms are available, get the actual room position if (window.rooms && window.rooms[startRoomId]) { const roomPos = window.rooms[startRoomId].position; // Center of 320x288 room return { x: roomPos.x + 160, y: roomPos.y + 144 }; } // Fallback to reasonable center position for reception room // Reception is typically at (0,0) so center would be (160, 144) return defaultPos; } // Export for global access window.createPlayer = createPlayer; window.pauseKeyboardInput = pauseKeyboardInput; window.resumeKeyboardInput = resumeKeyboardInput; window.updatePlayerSprite = updatePlayerSprite; console.log('✅ Player module loaded - keyboard control functions exported to window:', { createPlayer: typeof window.createPlayer, pauseKeyboardInput: typeof window.pauseKeyboardInput, resumeKeyboardInput: typeof window.resumeKeyboardInput, updatePlayerSprite: typeof window.updatePlayerSprite });