From 1b313fbdaa3d2724ded4030c234df9ba49e4084f Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Wed, 22 Oct 2025 13:20:05 +0100 Subject: [PATCH] Implement keyboard controls for player movement and interaction - Added keyboard input handling for movement using arrow keys and WASD. - Integrated spacebar for jump functionality and 'E' key for interacting with nearest objects. - Enhanced player movement logic to prioritize keyboard input over mouse movement. - Created visual effects for player jumping, including cooldown management and overlay animations. --- js/core/player.js | 285 ++++++++++++++++++++++++++++++++++- js/systems/doors.js | 3 +- js/systems/interactions.js | 130 ++++++++++++++++ js/systems/player-effects.js | 96 ++++++++++++ 4 files changed, 507 insertions(+), 7 deletions(-) diff --git a/js/core/player.js b/js/core/player.js index 0f69021..f70175b 100644 --- a/js/core/player.js +++ b/js/core/player.js @@ -17,6 +17,16 @@ 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 +}; +let isKeyboardMoving = false; + // Create player sprite export function createPlayer(gameInstance) { gameRef = gameInstance; @@ -63,9 +73,135 @@ export function createPlayer(gameInstance) { // 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) => { + const key = event.key.toLowerCase(); + + // 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) => { + const key = event.key.toLowerCase(); + + // 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) { + // 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 createPlayerAnimations() { // Create walking animations with correct frame numbers from original gameRef.anims.create({ @@ -133,6 +269,25 @@ function createPlayerAnimations() { frames: [{ key: 'hacker', 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: 'hacker', frame: 0 }], + frameRate: 1 + }); + + gameRef.anims.create({ + key: 'idle-down-left', + frames: [{ key: 'hacker', frame: 20 }], + frameRate: 1 + }); + + gameRef.anims.create({ + key: 'idle-up-left', + frames: [{ key: 'hacker', frame: 15 }], + frameRate: 1 + }); } export function movePlayerToPoint(x, y) { @@ -194,6 +349,118 @@ export function updatePlayerMovement() { return; } + // Handle keyboard movement (takes priority over mouse movement) + if (isKeyboardMoving) { + updatePlayerKeyboardMovement(); + return; + } + + // Handle mouse-based movement (original behavior) + updatePlayerMouseMovement(); +} + +function updatePlayerKeyboardMovement() { + // 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); + velocityX = (dirX / magnitude) * MOVEMENT_SPEED; + velocityY = (dirY / magnitude) * MOVEMENT_SPEED; + } + + // 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'; // Track both left and right directions + player.setFlipX(velocityX < 0); // Flip sprite horizontally if moving left + + if (!player.isMoving || player.lastDirection !== player.direction) { + // Use 'right' animation for both left and right (flip handled by setFlipX) + player.anims.play(`walk-right`, 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'; + } + player.setFlipX(velocityX < 0); // Flip sprite horizontally if moving left + + if (!player.isMoving || player.lastDirection !== player.direction) { + // Use the base direction for animation (right or left for horizontal component) + const baseDir = velocityY > 0 ? 'down-right' : 'up-right'; + 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); @@ -221,10 +488,12 @@ export function updatePlayerMovement() { if (distanceSq < ARRIVAL_THRESHOLD * ARRIVAL_THRESHOLD) { isMoving = false; player.body.setVelocity(0, 0); - player.isMoving = false; - - // Play idle animation based on last direction - player.anims.play(`idle-${player.direction}`, true); + if (player.isMoving) { + player.isMoving = false; + const animDir = getAnimationKey(player.direction); + player.anims.stop(); // Stop current animation + player.anims.play(`idle-${animDir}`, true); + } return; } @@ -274,8 +543,12 @@ export function updatePlayerMovement() { if (player.body.blocked.none === false) { isMoving = false; player.body.setVelocity(0, 0); - player.isMoving = false; - player.anims.play(`idle-${player.direction}`, true); + if (player.isMoving) { + player.isMoving = false; + const animDir = getAnimationKey(player.direction); + player.anims.stop(); // Stop current animation + player.anims.play(`idle-${animDir}`, true); + } } } diff --git a/js/systems/doors.js b/js/systems/doors.js index f8c355a..c8292c5 100644 --- a/js/systems/doors.js +++ b/js/systems/doors.js @@ -875,6 +875,7 @@ window.checkDoorTransitions = checkDoorTransitions; window.setupDoorOverlapChecks = setupDoorOverlapChecks; window.updateDoorZoneVisibility = updateDoorZoneVisibility; window.processAllDoorCollisions = processAllDoorCollisions; +window.handleDoorInteraction = handleDoorInteraction; // Export functions for use by other modules -export { unlockDoor }; +export { unlockDoor, handleDoorInteraction }; diff --git a/js/systems/interactions.js b/js/systems/interactions.js index 8c589fc..cc2a85e 100644 --- a/js/systems/interactions.js +++ b/js/systems/interactions.js @@ -2,6 +2,7 @@ import { INTERACTION_RANGE, INTERACTION_RANGE_SQ, INTERACTION_CHECK_INTERVAL } from '../utils/constants.js?v=7'; import { rooms } from '../core/rooms.js?v=16'; import { handleUnlock } from './unlock-system.js'; +import { handleDoorInteraction } from './doors.js'; import { collectFingerprint, handleBiometricScan } from './biometrics.js'; import { addToInventory, removeFromInventory, createItemIdentifier } from './inventory.js'; @@ -562,7 +563,136 @@ function handleContainerInteraction(sprite) { } } +// Try to interact with the nearest interactable object within range +export function tryInteractWithNearest() { + const player = window.player; + if (!player) { + return; + } + + const px = player.x; + const py = player.y; + + let nearestObject = null; + let nearestDistance = INTERACTION_RANGE; // Only consider objects within interaction range + + // Get player's facing direction and convert to angle for direction filtering + const playerDirection = player.direction || 'down'; + let facingAngle = 0; + let angleTolerance = 70; // degrees - how wide the cone in front of player is + + // Determine facing angle based on direction + // In canvas/Phaser: right=0°, down=90°, left=180°, up=270° + switch(playerDirection) { + case 'right': + facingAngle = 0; + break; + case 'down-right': + facingAngle = 45; + break; + case 'down': + facingAngle = 90; + break; + case 'down-left': + facingAngle = 135; + break; + case 'left': + facingAngle = 180; + break; + case 'up-left': + facingAngle = 225; + break; + case 'up': + facingAngle = 270; // Use 270 instead of -90 + break; + case 'up-right': + facingAngle = 315; // Use 315 instead of -45 + break; + default: // Fallback for any unknown directions + facingAngle = 90; // Default to down + } + + // Helper function to check if an object is in front of the player + function isInFrontOfPlayer(objX, objY) { + const dx = objX - px; + const dy = objY - py; + + // Calculate angle to object (in canvas coordinates where Y increases downward) + let angleToObject = Math.atan2(dy, dx) * 180 / Math.PI; + + // Normalize to 0-360 + angleToObject = (angleToObject + 360) % 360; + + // Calculate angular difference + let angleDiff = Math.abs(facingAngle - angleToObject); + if (angleDiff > 180) { + angleDiff = 360 - angleDiff; + } + + return angleDiff <= angleTolerance; + } + + // Check all objects in all rooms + Object.entries(rooms).forEach(([roomId, room]) => { + if (!room.objects) return; + + Object.values(room.objects).forEach(obj => { + // Only consider interactable, active, and visible objects + if (!obj.active || !obj.interactable || !obj.visible) { + return; + } + + const dx = px - obj.x; + const dy = py - obj.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + // Check if within range and in front of player + if (distance <= INTERACTION_RANGE && isInFrontOfPlayer(obj.x, obj.y)) { + if (distance < nearestDistance) { + nearestDistance = distance; + nearestObject = obj; + } + } + }); + + // Also check door sprites (including all doors, not just locked ones) + if (room.doorSprites) { + Object.values(room.doorSprites).forEach(door => { + // Only consider active doors (check all doors, not just locked) + if (!door.active || !door.doorProperties) { + return; + } + + const dx = px - door.x; + const dy = py - door.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + // Check if within range and in front of player + if (distance <= INTERACTION_RANGE && isInFrontOfPlayer(door.x, door.y)) { + if (distance < nearestDistance) { + nearestDistance = distance; + nearestObject = door; + } + } + }); + } + }); + + // Interact with the nearest object if one was found + if (nearestObject) { + // Check if this is a door (doors have doorProperties instead of scenarioData) + if (nearestObject.doorProperties) { + // Handle door interaction - triggers unlock/open sequence based on lock state + handleDoorInteraction(nearestObject); + } else { + // Handle regular object interaction + handleObjectInteraction(nearestObject); + } + } +} + // Export for global access window.checkObjectInteractions = checkObjectInteractions; window.handleObjectInteraction = handleObjectInteraction; window.handleContainerInteraction = handleContainerInteraction; +window.tryInteractWithNearest = tryInteractWithNearest; diff --git a/js/systems/player-effects.js b/js/systems/player-effects.js index 4d8d0da..6d4c255 100644 --- a/js/systems/player-effects.js +++ b/js/systems/player-effects.js @@ -18,7 +18,9 @@ let lastPlayerPosition = { x: 0, y: 0 }; let steppedOverItems = new Set(); // Track items we've already stepped over let playerVisualOverlay = null; // Visual overlay for hop effect let lastHopTime = 0; // Track when last hop occurred +let lastJumpTime = 0; // Track when last jump occurred const HOP_COOLDOWN = 300; // 300ms cooldown between hops +const JUMP_COOLDOWN = 600; // 600ms cooldown between jumps // Initialize player effects system export function initializePlayerEffects(gameInstance, roomsRef) { @@ -188,6 +190,99 @@ export function createPlayerBumpEffect() { }); } +// Create player jump effect when spacebar is pressed +export function createPlayerJump() { + if (!window.player || isPlayerBumping) return; + + // Check cooldown to prevent rapid jumping + const currentTime = Date.now(); + if (currentTime - lastJumpTime < JUMP_COOLDOWN) { + return; // Still in cooldown, skip this jump + } + + const player = window.player; + + // Update jump time + lastJumpTime = currentTime; + isPlayerBumping = true; + + // Create hop effect using visual overlay (same as bump effect) + if (playerBumpTween) { + playerBumpTween.destroy(); + } + + // Create a visual overlay sprite that follows the player + if (playerVisualOverlay) { + playerVisualOverlay.destroy(); + } + + playerVisualOverlay = gameRef.add.sprite(player.x, player.y, player.texture.key); + playerVisualOverlay.setFrame(player.frame.name); + playerVisualOverlay.setScale(player.scaleX, player.scaleY); + playerVisualOverlay.setFlipX(player.flipX); // Copy horizontal flip state + playerVisualOverlay.setFlipY(player.flipY); // Copy vertical flip state + playerVisualOverlay.setDepth(player.depth + 1); + playerVisualOverlay.setAlpha(0.8); + + // Hide the original player temporarily + player.setAlpha(0); + + // Jump upward - negative Y values move sprite up on screen + const jumpHeight = -20; // Consistent upward jump + + // Debug: Log the jump details + console.log(`Jump triggered - Player Y: ${player.y}, Overlay Y: ${playerVisualOverlay.y}, Jump Height: ${jumpHeight}, Target Y: ${playerVisualOverlay.y + jumpHeight}`); + + // Start the jump animation with a simple up-down motion + playerBumpTween = gameRef.tweens.add({ + targets: { jumpOffset: 0 }, + jumpOffset: jumpHeight, + duration: 150, + ease: 'Power2', + yoyo: true, + onUpdate: (tween) => { + if (playerVisualOverlay && playerVisualOverlay.active) { + // Apply the jump offset to the current player position + playerVisualOverlay.setY(player.y + tween.getValue()); + } + }, + onComplete: () => { + // Clean up overlay and restore player + if (playerVisualOverlay) { + playerVisualOverlay.destroy(); + playerVisualOverlay = null; + } + player.setAlpha(1); // Restore player visibility + isPlayerBumping = false; + playerBumpTween = null; + } + }); + + // Make overlay follow player movement during jump + const followPlayer = () => { + if (playerVisualOverlay && playerVisualOverlay.active) { + // Update X position and flip states, Y is handled by the tween + playerVisualOverlay.setX(player.x); + playerVisualOverlay.setFlipX(player.flipX); // Update flip state + playerVisualOverlay.setFlipY(player.flipY); // Update flip state + } + }; + + // Update overlay position every frame during jump + const followInterval = setInterval(() => { + if (!playerVisualOverlay || !playerVisualOverlay.active) { + clearInterval(followInterval); + return; + } + followPlayer(); + }, 16); // ~60fps + + // Clean up interval when jump completes + setTimeout(() => { + clearInterval(followInterval); + }, 280); // Slightly longer than animation duration +} + // Create plant animation effect when player bumps into animated plants export function createPlantBumpEffect() { if (!window.player) return; @@ -231,4 +326,5 @@ export function createPlantBumpEffect() { // Export for global access window.createPlayerBumpEffect = createPlayerBumpEffect; +window.createPlayerJump = createPlayerJump; window.createPlantBumpEffect = createPlantBumpEffect;