From 00e15b442a3121b9c771cceac8735389501e4d01 Mon Sep 17 00:00:00 2001 From: Dylan T <106625299+beepboopdylan@users.noreply.github.com> Date: Mon, 15 Sep 2025 18:00:20 -0400 Subject: [PATCH] Added map feature and smoother animation (not really working) --- index.html | 13 ++ script.js | 457 +++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 17 ++ 3 files changed, 487 insertions(+) create mode 100644 index.html create mode 100644 script.js create mode 100644 style.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..7084339 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + Mini MMORPG Client + + + + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..10c5287 --- /dev/null +++ b/script.js @@ -0,0 +1,457 @@ +// Game state +let gameState = { + playerId: null, + playerPosition: { x: 0, y: 0 }, + playerAvatar: null, + viewportOffset: { x: 0, y: 0 }, + worldImage: null, + avatarImages: new Map(), // Cache for loaded avatar images + canvas: null, + ctx: null, + ws: null, + keysPressed: new Set(), // Track currently pressed keys + isMoving: false, + otherPlayers: new Map(), // Store other players data by playerId + allAvatars: new Map(), // Store all avatar data by avatar name + animationFrame: 0 // Current animation frame for our player +}; + +// Constants +const WORLD_SIZE = 2048; +const AVATAR_SIZE = 48; +const ANIMATION_SPEED = 200; // Milliseconds per frame +const MINIMAP_SIZE = 200; // Size of mini-map in pixels +const MINIMAP_SCALE = MINIMAP_SIZE / WORLD_SIZE; // Scale factor for mini-map + +// Get the canvas and context +gameState.canvas = document.getElementById('gameCanvas'); +gameState.ctx = gameState.canvas.getContext('2d'); + +// Set canvas size to fill the browser window +function resizeCanvas() { + gameState.canvas.width = window.innerWidth; + gameState.canvas.height = window.innerHeight; + updateViewport(); + draw(); +} + +// Initialize canvas size +resizeCanvas(); + +// Listen for window resize +window.addEventListener('resize', resizeCanvas); + +// Animation system - only animate when moving +let animationInterval = null; + +function startAnimation() { + if (animationInterval) return; // Already animating + + animationInterval = setInterval(() => { + gameState.animationFrame = (gameState.animationFrame + 1) % 3; + draw(); // Only redraw when animation changes + }, ANIMATION_SPEED); +} + +function stopAnimation() { + if (animationInterval) { + clearInterval(animationInterval); + animationInterval = null; + } +} + +// Load the world map +gameState.worldImage = new Image(); +gameState.worldImage.onload = function() { + draw(); +}; + +// Load the world image +gameState.worldImage.src = 'world.jpg'; + +// WebSocket connection +function connectToServer() { + gameState.ws = new WebSocket('wss://codepath-mmorg.onrender.com'); + + gameState.ws.onopen = function() { + console.log('Connected to game server'); + // Send join game message + const joinMessage = { + action: 'join_game', + username: 'Dylan' + }; + gameState.ws.send(JSON.stringify(joinMessage)); + }; + + gameState.ws.onmessage = function(event) { + const message = JSON.parse(event.data); + handleServerMessage(message); + }; + + gameState.ws.onerror = function(error) { + console.error('WebSocket error:', error); + }; + + gameState.ws.onclose = function() { + console.log('Disconnected from server'); + }; +} + +// Handle messages from server +function handleServerMessage(message) { + console.log('Received message:', message); + + switch (message.action) { + case 'join_game': + if (message.success) { + gameState.playerId = message.playerId; + + // Store all avatars + Object.values(message.avatars).forEach(avatar => { + gameState.allAvatars.set(avatar.name, avatar); + loadAvatarImages(avatar); + }); + + // Store all other players (excluding ourselves) + Object.entries(message.players).forEach(([playerId, playerData]) => { + if (playerId !== message.playerId) { + gameState.otherPlayers.set(playerId, playerData); + } + }); + + // Find our player data + const ourPlayer = message.players[message.playerId]; + if (ourPlayer) { + gameState.playerPosition = { x: ourPlayer.x, y: ourPlayer.y }; + gameState.playerAvatar = message.avatars[ourPlayer.avatar]; + updateViewport(); + draw(); + } + } else { + console.error('Failed to join game:', message.error); + } + break; + + case 'players_moved': + // Update all moved players (including ourselves) + Object.entries(message.players).forEach(([playerId, playerData]) => { + if (playerId === gameState.playerId) { + // Update our position + gameState.playerPosition = { x: playerData.x, y: playerData.y }; + updateViewport(); + + // Start animation if moving, stop if not + if (playerData.isMoving) { + startAnimation(); + } else { + stopAnimation(); + gameState.animationFrame = 0; // Reset to standing frame + } + } else { + // Update other players + gameState.otherPlayers.set(playerId, playerData); + } + }); + draw(); + break; + + case 'player_joined': + // Add new player and their avatar + if (message.player && message.avatar) { + gameState.otherPlayers.set(message.player.id, message.player); + gameState.allAvatars.set(message.avatar.name, message.avatar); + loadAvatarImages(message.avatar); + draw(); + } + break; + + case 'player_left': + // Remove player who left + if (message.playerId) { + gameState.otherPlayers.delete(message.playerId); + draw(); + } + break; + } +} + +// Load avatar images into cache +function loadAvatarImages(avatarData) { + if (!avatarData) return; + + const avatarName = avatarData.name; + + // Load images for each direction + ['north', 'south', 'east'].forEach(direction => { + avatarData.frames[direction].forEach((frameData, frameIndex) => { + const img = new Image(); + img.onload = function() { + draw(); // Redraw when image loads + }; + img.src = frameData; + + const key = `${avatarName}_${direction}_${frameIndex}`; + gameState.avatarImages.set(key, img); + }); + }); +} + +// Update viewport to center player +function updateViewport() { + const canvasWidth = gameState.canvas.width; + const canvasHeight = gameState.canvas.height; + + // Center the player in the viewport + gameState.viewportOffset.x = gameState.playerPosition.x - canvasWidth / 2; + gameState.viewportOffset.y = gameState.playerPosition.y - canvasHeight / 2; + + // Clamp to world bounds + gameState.viewportOffset.x = Math.max(0, Math.min(gameState.viewportOffset.x, WORLD_SIZE - canvasWidth)); + gameState.viewportOffset.y = Math.max(0, Math.min(gameState.viewportOffset.y, WORLD_SIZE - canvasHeight)); +} + +// Draw everything +function draw() { + if (!gameState.worldImage) return; + + const ctx = gameState.ctx; + const canvas = gameState.canvas; + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw world map with viewport offset + ctx.drawImage( + gameState.worldImage, + gameState.viewportOffset.x, gameState.viewportOffset.y, + Math.min(canvas.width, WORLD_SIZE - gameState.viewportOffset.x), + Math.min(canvas.height, WORLD_SIZE - gameState.viewportOffset.y), + 0, 0, + Math.min(canvas.width, WORLD_SIZE - gameState.viewportOffset.x), + Math.min(canvas.height, WORLD_SIZE - gameState.viewportOffset.y) + ); + + // Draw all players + drawAllPlayers(); + + // Draw mini-map + drawMiniMap(); +} + +// Draw all players (our player + other players) +function drawAllPlayers() { + const ctx = gameState.ctx; + const canvas = gameState.canvas; + + // Draw our player at center of screen + if (gameState.playerAvatar && gameState.playerId) { + drawPlayer(ctx, canvas.width / 2, canvas.height / 2, 'Dylan', gameState.playerAvatar, 'south', gameState.animationFrame); + } + + // Draw other players + gameState.otherPlayers.forEach(playerData => { + const avatar = gameState.allAvatars.get(playerData.avatar); + if (avatar) { + // Convert world coordinates to screen coordinates + const screenX = playerData.x - gameState.viewportOffset.x; + const screenY = playerData.y - gameState.viewportOffset.y; + + // Only draw if player is visible on screen + if (screenX >= -AVATAR_SIZE && screenX <= canvas.width + AVATAR_SIZE && + screenY >= -AVATAR_SIZE && screenY <= canvas.height + AVATAR_SIZE) { + + drawPlayer(ctx, screenX, screenY, playerData.username, avatar, playerData.facing, playerData.animationFrame); + } + } + }); +} + +// Draw a single player +function drawPlayer(ctx, screenX, screenY, username, avatar, facing, animationFrame) { + // Get avatar image based on facing direction and animation frame + const avatarKey = `${avatar.name}_${facing}_${animationFrame}`; + const avatarImg = gameState.avatarImages.get(avatarKey); + + if (avatarImg) { + // Calculate avatar size maintaining aspect ratio + const aspectRatio = avatarImg.width / avatarImg.height; + let avatarWidth = AVATAR_SIZE; + let avatarHeight = AVATAR_SIZE / aspectRatio; + + // Draw avatar centered + ctx.drawImage( + avatarImg, + screenX - avatarWidth / 2, + screenY - avatarHeight / 2, + avatarWidth, + avatarHeight + ); + } + + // Draw username label + ctx.fillStyle = 'white'; + ctx.strokeStyle = 'black'; + ctx.lineWidth = 2; + ctx.font = '16px Arial'; + ctx.textAlign = 'center'; + + const textY = screenY - avatarHeight / 2 - 10; + + // Draw text outline + ctx.strokeText(username, screenX, textY); + // Draw text fill + ctx.fillText(username, screenX, textY); +} + +// Draw mini-map +function drawMiniMap() { + const ctx = gameState.ctx; + const canvas = gameState.canvas; + + // Mini-map position (top-right corner) + const minimapX = canvas.width - MINIMAP_SIZE - 20; + const minimapY = 20; + + // Draw mini-map background + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(minimapX, minimapY, MINIMAP_SIZE, MINIMAP_SIZE); + + // Draw mini-map border + ctx.strokeStyle = 'white'; + ctx.lineWidth = 2; + ctx.strokeRect(minimapX, minimapY, MINIMAP_SIZE, MINIMAP_SIZE); + + // Draw world map on mini-map + if (gameState.worldImage) { + ctx.drawImage( + gameState.worldImage, + 0, 0, WORLD_SIZE, WORLD_SIZE, + minimapX, minimapY, MINIMAP_SIZE, MINIMAP_SIZE + ); + } + + // Draw viewport rectangle on mini-map + const viewportMinimapX = minimapX + (gameState.viewportOffset.x * MINIMAP_SCALE); + const viewportMinimapY = minimapY + (gameState.viewportOffset.y * MINIMAP_SCALE); + const viewportMinimapWidth = canvas.width * MINIMAP_SCALE; + const viewportMinimapHeight = canvas.height * MINIMAP_SCALE; + + ctx.strokeStyle = 'yellow'; + ctx.lineWidth = 2; + ctx.strokeRect(viewportMinimapX, viewportMinimapY, viewportMinimapWidth, viewportMinimapHeight); + + // Draw our player on mini-map + const playerMinimapX = minimapX + (gameState.playerPosition.x * MINIMAP_SCALE); + const playerMinimapY = minimapY + (gameState.playerPosition.y * MINIMAP_SCALE); + + ctx.fillStyle = 'blue'; + ctx.beginPath(); + ctx.arc(playerMinimapX, playerMinimapY, 3, 0, 2 * Math.PI); + ctx.fill(); + + // Draw other players on mini-map + gameState.otherPlayers.forEach(playerData => { + const otherPlayerMinimapX = minimapX + (playerData.x * MINIMAP_SCALE); + const otherPlayerMinimapY = minimapY + (playerData.y * MINIMAP_SCALE); + + // Only draw if player is visible on mini-map + if (otherPlayerMinimapX >= minimapX && otherPlayerMinimapX <= minimapX + MINIMAP_SIZE && + otherPlayerMinimapY >= minimapY && otherPlayerMinimapY <= minimapY + MINIMAP_SIZE) { + + ctx.fillStyle = 'red'; + ctx.beginPath(); + ctx.arc(otherPlayerMinimapX, otherPlayerMinimapY, 2, 0, 2 * Math.PI); + ctx.fill(); + } + }); +} + +// Keyboard event handling +function handleKeyDown(event) { + const key = event.code; + + // Only handle arrow keys + if (key.startsWith('Arrow')) { + event.preventDefault(); // Prevent page scrolling + + // Add key to pressed keys + gameState.keysPressed.add(key); + + // Send move command + sendMoveCommand(); + + console.log('Key pressed:', key, 'Keys pressed:', Array.from(gameState.keysPressed)); + } +} + +function handleKeyUp(event) { + const key = event.code; + + if (key.startsWith('Arrow')) { + event.preventDefault(); + + // Remove key from pressed keys + gameState.keysPressed.delete(key); + + console.log('Key released:', key, 'Keys pressed:', Array.from(gameState.keysPressed)); + + // Send stop command if no keys are pressed + if (gameState.keysPressed.size === 0) { + sendStopCommand(); + } + } +} + +// Send move command to server +function sendMoveCommand() { + if (!gameState.ws || gameState.ws.readyState !== WebSocket.OPEN) { + return; + } + + // Determine movement direction based on pressed keys + let direction = null; + + if (gameState.keysPressed.has('ArrowUp')) { + direction = 'up'; + } else if (gameState.keysPressed.has('ArrowDown')) { + direction = 'down'; + } else if (gameState.keysPressed.has('ArrowLeft')) { + direction = 'left'; + } else if (gameState.keysPressed.has('ArrowRight')) { + direction = 'right'; + } + + if (direction && !gameState.isMoving) { + const moveMessage = { + action: 'move', + direction: direction + }; + console.log('Sending move command:', moveMessage); + gameState.ws.send(JSON.stringify(moveMessage)); + gameState.isMoving = true; + } +} + +// Send stop command to server +function sendStopCommand() { + if (!gameState.ws || gameState.ws.readyState !== WebSocket.OPEN) { + return; + } + + if (gameState.isMoving) { + const stopMessage = { + action: 'stop' + }; + gameState.ws.send(JSON.stringify(stopMessage)); + gameState.isMoving = false; + } +} + +// Connect to server when page loads +window.addEventListener('load', function() { + connectToServer(); + + // Add keyboard event listeners + document.addEventListener('keydown', handleKeyDown); + document.addEventListener('keyup', handleKeyUp); +}); diff --git a/style.css b/style.css new file mode 100644 index 0000000..5f0b66e --- /dev/null +++ b/style.css @@ -0,0 +1,17 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + overflow: hidden; + background-color: #000; +} + +#gameCanvas { + display: block; + width: 100vw; + height: 100vh; + cursor: crosshair; +}