Files
BreakEscape/public/break_escape/js/core/player.js
Z. Cliffe Schreuders fb6e9b603c Enhance character sprite loading and animation handling
- Updated the game to support new character sprite atlases for both male and female characters, allowing for a wider variety of NPC designs.
- Improved player sprite initialization to dynamically select between atlas-based and legacy sprites, enhancing flexibility in character representation.
- Refined collision box settings based on sprite type, ensuring accurate physics interactions for both atlas (80x80) and legacy (64x64) sprites.
- Enhanced NPC behavior to utilize atlas animations, allowing for more fluid and diverse animations based on available frames.

Files modified:
- game.js: Added new character atlases and updated sprite loading logic.
- player.js: Improved player sprite handling and collision box adjustments.
- npc-behavior.js: Updated animation handling for NPCs to support atlas-based animations.
- npc-sprites.js: Enhanced NPC sprite creation to accommodate atlas detection and initial frame selection.
- scenario.json.erb: Updated player and NPC configurations to utilize new sprite sheets and animation settings.
- m01_npc_sarah.ink: Revised dialogue options to include new interactions related to NPCs.
2026-02-11 00:18:21 +00:00

863 lines
29 KiB
JavaScript

// 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
} 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)');
}
// 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 from scenario
const playerSprite = window.gameScenario?.player?.spriteSheet || 'hacker';
console.log(`🎮 Loading player sprite: ${playerSprite} (from ${window.gameScenario?.player ? 'scenario' : 'default'})`);
// 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);
// 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') {
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;
}
}
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;
// 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'
};
// 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", etc.
const animKey = `${playerType}-${playerDirection}`;
if (!gameRef.anims.exists(animKey)) {
gameRef.anims.create({
key: animKey,
frames: frames.map(frameName => ({ key: spriteSheet, frame: frameName })),
frameRate: playerType === 'idle' ? idleFrameRate : walkFrameRate,
repeat: -1
});
console.log(` ✓ Created player animation: ${animKey} (${frames.length} frames @ ${playerType === 'idle' ? idleFrameRate : walkFrameRate} fps)`);
}
}
console.log(`✅ Player atlas animations created for ${spriteSheet} (idle: ${idleFrameRate} fps, walk: ${walkFrameRate} fps)`);
}
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();
}
}
function updatePlayerDepth(x, y) {
// Get the bottom of the player sprite (feet position)
const playerBottomY = y + (player.height * player.scaleY) / 2;
// 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} (World Y: ${playerBottomY})`);
console.log(` Player layers: worldY(${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);
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
player.anims.play(`idle-${animDir}`, true);
}
} else if (isBlocked) {
// Blocked by collision - play idle animation in the direction we're facing
if (player.isMoving) {
player.isMoving = false;
const animDir = getAnimationKey(player.direction);
player.anims.stop(); // Stop current animation
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) {
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) {
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) {
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
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
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) {
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);
if (player.isMoving) {
player.isMoving = false;
const animDir = getAnimationKey(player.direction);
player.anims.stop(); // Stop current animation
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;
console.log('✅ Player module loaded - keyboard control functions exported to window:', {
createPlayer: typeof window.createPlayer,
pauseKeyboardInput: typeof window.pauseKeyboardInput,
resumeKeyboardInput: typeof window.resumeKeyboardInput
});