// 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] + "<br>";
}
}
}
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;
}
});