// Setup the canvas // This'll be where the game is drawn // Everything below is configured in relation to the canvas' size // The canvas is 512px wide and 512px high, which makes for easier math const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); canvas.width = 512; canvas.height = 512; const startingX = 0; const startingY = 0; // Misc. helpers const camera = { x: startingX, y: startingY, width: canvas.width, height: canvas.height }; const cons = ["b", "c", "d", "f", "g", "h", "j", "k", "l", "m", "n", "p", "r", "s", "t", "v", "w", "z", "ch", "sh", "zh", "th"]; const vow = ["a", "e", "i", "o", "u", "y", "ee", "ai", "ae", "au"]; const rpick = t => t[Math.floor(Math.random() * t.length)]; const syl = () => rpick(cons) + rpick(vow); const word = () => { const syllables = Array(Math.floor(Math.random() * 3) + 1).fill(null).map(() => syl()).join(''); return Math.random() > 0.2 ? syllables + rpick(cons) : syllables; }; const speak = numberOfWords => Array(numberOfWords).fill(null).map(() => word()).join(' '); // Create the player // This object tracks all information about the player chracter const player = { x: startingX, // X coordinate on the canvas y: startingY, // Y coordinate on the canvas width: 8, // Width of the player height: 8, // Height of the player step: 8, // How many pixels the player moves per step color: 'blue', // Color of the player spriteUrl: "chickadee.svg" // Sprite of the player, if any }; // Player inventory and inventory mangement player.inventory = []; player.collectItem = function(item) { this.inventory.push(item); } player.dropItem = function(item) { const itemIndex = this.inventory.indexOf(item); if (itemIndex > -1) { this.inventory.splice(itemIndex, 1); } } // If you wanna have a sprite, you need to create an image object const playerSprite = new Image(); playerSprite.src = player.spriteUrl; // Map const TILE_SIZE = 64; const map = [ [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }], [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }], [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }], [{ walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }], [{ walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }], [{ walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }], [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }], [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }], [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }], [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }], [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }], [{ walkable: true, color: 'pink' }, { walkable: false, color: 'grey' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }, { walkable: true, color: 'pink' }] ]; // Items function Item(name, x, y, type, color) { this.name = name; this.x = x; this.y = y; this.type = type; this.color = color; } // Create some bespoke items let items = [ new Item('helmet', 16, 16, 'clothes', 'red'), new Item('banana', 24, 128, 'food', 'yellow'), new Item('bucket', 128, 256, 'object', 'green'), new Item('big key', 216, 216, 'key', 'cyan'), new Item('small key', 456, 256, 'key', 'cyan'), new Item('bike', 0, 48, 'bike', 'orange'), ]; // Populate the world with really very weird objects // FIXME: update this so that items aren't always placed within the 0x0 space of a tile const appendRandomItems = () => { const getRandomColor = () => '#' + Math.floor(Math.random() * 16777215).toString(16); const getRandomName = () => speak(Math.floor(Math.random() * 3) + 1); const getRandomCoordinates = () => { let x, y; do { x = Math.floor(Math.random() * (map[0].length - 2)) + 1; y = Math.floor(Math.random() * (map.length - 2)) + 1; } while (!map[y][x].walkable); return { x, y }; }; const createRandomItem = () => { const { x, y } = getRandomCoordinates(); const name = getRandomName(); const color = getRandomColor(); return new Item(name, x * TILE_SIZE, y * TILE_SIZE, 'mystery', color); }; const numberOfItems = Math.floor(Math.random() * 39); const randomItems = Array.from({ length: numberOfItems }, createRandomItem); items.push(...randomItems); }; function checkForSpecialItems() { if (player.inventory.find(item => item.type === 'bike')) { player.step = 12; } else { player.step = 8; } } // Define the Sign class function Sign(x, y, width, height, message) { this.x = x; this.y = y; this.width = width; this.height = height; this.message = message; } // Create some signs let signs = [ new Sign(34, 38, player.width, player.height, speak(3).toUpperCase()), new Sign(28, 64, player.width, player.height, 'Welcome to the game!'), new Sign(128, 128, player.width, player.height, 'You cannot pass through walls!'), new Sign(24, 212, player.width, player.height, 'You can collect items!'), new Sign(428, 712, player.width, player.height, 'This sign has very long text that will wrap around to the next line!'), new Sign(28, 712, player.width, player.height, 'This sign is very far away.'), ]; // Display the player's stats, useful for debugging function displayStats() { document.getElementById("stats").innerHTML = ""; for (let stat in player) { if (typeof player[stat] === "string" || typeof player[stat] === "number") { if (player[stat] !== player.spriteUrl) { document.getElementById("stats").innerHTML += stat + ": " + player[stat] + "
"; } } } document.getElementById("stats").innerHTML += "Inventory: "; for (let i = 0; i < player.inventory.length; i++) { document.getElementById("stats").innerHTML += player.inventory[i].name + ", "; } } // Game loop function gameLoop() { // Update the camera position to follow the player camera.x = player.x - camera.width / 2; camera.y = player.y - camera.height / 2; // Clamp the camera position to the map boundaries camera.x = Math.max(0, Math.min(map[0].length * TILE_SIZE - camera.width, camera.x)); camera.y = Math.max(0, Math.min(map.length * TILE_SIZE - camera.height, camera.y)); // Draw the map map.forEach((row, y) => { row.forEach((tile, x) => { ctx.fillStyle = tile.color; // Draw the tile ctx.fillRect(x * TILE_SIZE - camera.x, y * TILE_SIZE - camera.y, TILE_SIZE, TILE_SIZE); // Draw the grid ctx.strokeStyle = 'white'; ctx.lineWidth = 1; ctx.strokeRect(x * TILE_SIZE - camera.x, y * TILE_SIZE - camera.y, TILE_SIZE, TILE_SIZE); }); }); // Draw the player ctx.fillStyle = player.color; ctx.fillRect(player.x - camera.x, player.y - camera.y, player.width, player.height); // Draw items and check for collisions items.forEach(item => { ctx.fillStyle = item.color; ctx.fillRect(item.x - camera.x, item.y - camera.y, player.width, player.height); }); items = items.filter(item => { if (player.x === item.x && player.y === item.y) { player.collectItem(item); return false; // Remove the item from the map } return true; }); checkForSpecialItems(); // Draw signs and check for collisions signs.forEach(sign => { const dx = player.x - sign.x; const dy = player.y - sign.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < player.width / 2 + sign.width / 2) { document.getElementById("dialog").innerHTML = sign.message; ctx.fillStyle = 'black'; ctx.font = '22px Serif'; // Calculate the starting position of the text const lineHeight = 24; // Calculate the starting position of the text within the sign const textWidth = ctx.measureText(sign.message).width; const textHeight = lineHeight; const startX = sign.x - camera.x + (sign.width - textWidth) / 2; const startYWithinSign = sign.y - camera.y + (sign.height - textHeight) / 2; // Calculate the final position of the text within the canvas area const padding = 12; // Set the desired padding value const finalX = Math.max(padding, Math.min(canvas.width - textWidth - padding, startX)); const finalY = Math.max(padding, Math.min(canvas.height - textHeight - padding, startYWithinSign)); // Wrap the text to multiple lines if it exceeds the canvas width const words = sign.message.split(' '); let line = ''; let lines = []; for (let i = 0; i < words.length; i++) { const testLine = line + words[i] + ' '; const testWidth = ctx.measureText(testLine).width; if (testWidth > canvas.width - padding * 2) { lines.push(line); line = words[i] + ' '; } else { line = testLine; } } lines.push(line); // Draw each line of the text for (let i = 0; i < lines.length; i++) { ctx.fillText(lines[i], finalX, finalY + i * lineHeight); } } ctx.fillStyle = 'brown'; ctx.beginPath(); ctx.arc(sign.x - camera.x + sign.width / 2, sign.y - camera.y + sign.height / 2, sign.width / 2, 0, Math.PI * 2); ctx.fill(); }); // Call the game loop again next frame requestAnimationFrame(gameLoop); displayStats(); } // Start the game loop gameLoop(); // Handle user input window.addEventListener('keydown', (e) => { let newX = player.x; let newY = player.y; switch (e.key) { case 'ArrowUp': case 'w': case 'k': newY -= player.step; break; case 'ArrowDown': case 's': case 'j': newY += player.step; break; case 'ArrowLeft': case 'a': case 'h': newX -= player.step; break; case 'ArrowRight': case 'd': case 'l': newX += player.step; break; case 'z': case 'n': player.inventory.forEach(item => { player.dropItem(item); items.push(item); }); break; case 'x': case 'm': player.color = '#' + Math.floor(Math.random()*16777215).toString(16); break; case 'q': case 'p': player.color = 'blue'; break; // Add this code where you handle the 'i' key press case 'i': // Create a menu element const menu = document.createElement('div'); menu.style.position = 'fixed'; menu.style.left = '10px'; menu.style.top = '10px'; menu.style.backgroundColor = 'white'; menu.style.padding = '10px'; document.body.appendChild(menu); // Add each inventory item to the menu player.inventory.forEach((item) => { const itemElement = document.createElement('div'); itemElement.textContent = item.type; itemElement.style.cursor = 'pointer'; itemElement.addEventListener('click', () => { player.dropItem(item); items.push(item); menu.remove(); // Remove the entire menu }); menu.appendChild(itemElement); }); break; } // Calculate the tile coordinates const tileX = Math.floor(newX / TILE_SIZE); const tileY = Math.floor(newY / TILE_SIZE); // Only update the player's position if the tile is walkable if (map[tileY] && map[tileY][tileX] && map[tileY][tileX].walkable) { player.x = newX; player.y = newY; } });