class Vector2D { constructor(x, y) { this.x = x; this.y = y; } // Add another vector to this vector add(other) { return new Vector2D(this.x + other.x, this.y + other.y); } // Subtract another vector from this vector subtract(other) { return new Vector2D(this.x - other.x, this.y - other.y); } // Scale the vector by a scalar scale(scalar) { return new Vector2D(this.x * scalar, this.y * scalar); } // Ensure the vector stays within bounds clamp(min, max) { return new Vector2D( Math.max(min.x, Math.min(this.x, max.x)), Math.max(min.y, Math.min(this.y, max.y)) ); } } // GameObject Superclass class GameObject { constructor({ position, size, color = "white" }) { this.position = position instanceof Vector2D ? position : new Vector2D(position.x, position.y); this.size = size; this.color = color; } isCollidingWith(other) { return !( this.position.x + this.size < other.position.x || this.position.x > other.position.x + other.size || this.position.y + this.size < other.position.y || this.position.y > other.position.y + other.size ); } draw(ctx, camera) { ctx.fillStyle = this.color; ctx.fillRect( this.position.x - camera.position.x, this.position.y - camera.position.y, this.size, this.size ); } drawOnMinimap(ctx, minimapScale, minimapX, minimapY) { const scaledPosition = new Vector2D(minimapX, minimapY).add( this.position.scale(minimapScale) ); const scaledSize = this.size * minimapScale; ctx.fillStyle = this.color; ctx.fillRect(scaledPosition.x, scaledPosition.y, scaledSize, scaledSize); } } // Player Class class Player extends GameObject { constructor({ position, size }) { super({ position, size, color: "cyan" }); this.speed = 5; // Movement speed } update(inputHandler, map) { const movement = new Vector2D(0, 0); if (inputHandler.isActionActive("moveUp")) movement.y -= this.speed; if (inputHandler.isActionActive("moveLeft")) movement.x -= this.speed; if (inputHandler.isActionActive("moveDown")) movement.y += this.speed; if (inputHandler.isActionActive("moveRight")) movement.x += this.speed; const newPosition = this.position.add(movement); // Check collisions with obstacles if (!map.isCollidingWithObstacles(newPosition, this.size)) { this.position = newPosition; } // Check bounds after updating position map.checkBounds(this); } } // Obstacle Class class Obstacle extends GameObject { constructor({ position, size }) { super({ position, size, color: "red" }); } draw(ctx, camera) { super.draw(ctx, camera); } } // Camera Class class Camera extends GameObject { constructor({ width, height }) { super({ position: new Vector2D(0, 0), size: 0 }); this.width = width; this.height = height; } follow(target, mapWidth, mapHeight) { this.target = target; this.mapWidth = mapWidth; this.mapHeight = mapHeight; } update() { if (this.target) { // Scale width and height by 0.5 (equivalent to dividing by 2), then subtract const halfSize = new Vector2D(this.width, this.height).scale(0.5); // Set the camera position by subtracting the scaled vector from the target position this.position = this.target.position.subtract(halfSize); // Keep the camera within bounds this.position = this.position.clamp( new Vector2D(0, 0), // Minimum position new Vector2D(this.mapWidth - this.width, this.mapHeight - this.height) // Maximum position ); } } } // InputHandler Class class InputHandler { constructor({ keymap }) { this.keys = new Set(); this.actions = new Set(); // Create and store the reverse keymap as a property of InputHandler this.reverseKeymap = this.buildReverseKeymap(keymap); // Bind keydown and keyup events inline in the constructor window.addEventListener("keydown", (e) => this.handleKeydown(e)); window.addEventListener("keyup", (e) => this.handleKeyup(e)); } // Helper method to create the reverse keymap buildReverseKeymap(keymap) { const reverseKeymap = {}; Object.entries(keymap).forEach(([action, keys]) => { keys.forEach((key) => { reverseKeymap[key] = action; }); }); return reverseKeymap; } handleKeydown(e) { const action = this.reverseKeymap[e.key]; if (action) { this.actions.add(action); } } handleKeyup(e) { const action = this.reverseKeymap[e.key]; if (action) { this.actions.delete(action); } } isActionActive(action) { return this.actions.has(action); } } // Renderer Class class Renderer { constructor({ context, minimap }) { this.ctx = context; this.minimap = minimap; } render(map, player, camera) { this.ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw the map and obstacles map.draw(this.ctx, camera); // Draw the player player.draw(this.ctx, camera); // Draw the minimap this.minimap.draw(this.ctx); } } // Map Class class Map { constructor({ width, height, obstacles }) { this.width = width; this.height = height; this.obstacles = obstacles; } checkBounds(object) { object.position = object.position.clamp( new Vector2D(0, 0), new Vector2D(this.width - object.size, this.height - object.size) ); } isCollidingWithObstacles(position, size) { return this.obstacles.some((obstacle) => { return !( position.x + size < obstacle.position.x || position.x > obstacle.position.x + obstacle.size || position.y + size < obstacle.position.y || position.y > obstacle.position.y + obstacle.size ); }); } draw(ctx, camera) { const gridSize = 50; ctx.fillStyle = "#222"; ctx.fillRect(0, 0, this.width, this.height); ctx.strokeStyle = "#444"; ctx.lineWidth = 1; for (let x = 0; x < this.width; x += gridSize) { ctx.beginPath(); ctx.moveTo(x - camera.position.x, 0 - camera.position.y); ctx.lineTo(x - camera.position.x, this.height - camera.position.y); ctx.stroke(); } for (let y = 0; y < this.height; y += gridSize) { ctx.beginPath(); ctx.moveTo(0 - camera.position.x, y - camera.position.y); ctx.lineTo(this.width - camera.position.x, y - camera.position.y); ctx.stroke(); } this.obstacles.forEach((obstacle) => obstacle.draw(ctx, camera)); } } // Minimap Class class Minimap { constructor({ map, player, size = 64 }) { this.map = map; this.player = player; this.size = size; } draw(ctx) { const minimapX = canvas.width - this.size - 10; const minimapY = 10; ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; ctx.fillRect(minimapX, minimapY, this.size, this.size); ctx.strokeStyle = "yellow"; ctx.lineWidth = 1; ctx.strokeRect(minimapX - 1, minimapY - 1, this.size + 2, this.size + 2); const scale = this.size / this.map.width; this.map.obstacles.forEach((obstacle) => { obstacle.drawOnMinimap(ctx, scale, minimapX, minimapY); }); this.player.drawOnMinimap(ctx, scale, minimapX, minimapY); } } // Scene Class class Scene { constructor({ player, map, camera }) { this.player = player; this.map = map; this.camera = camera; } update(inputHandler) { this.player.update(inputHandler, this.map); this.camera.update(); } draw(renderer) { renderer.render(this.map, this.player, this.camera); } } class Game { constructor(config) { const mapWidth = config.map.width || 2000; const mapHeight = config.map.height || 2000; const playerConfig = config.player || { position: new Vector2D(100, 100), size: 20, }; const obstacleCount = config.map.obstacleCount || 10; this.player = new Player(playerConfig); this.obstacles = this.generateObstacles(obstacleCount, mapWidth, mapHeight); this.map = new Map({ width: mapWidth, height: mapHeight, obstacles: this.obstacles, }); this.camera = new Camera({ width: canvas.width, height: canvas.height }); this.camera.follow(this.player, mapWidth, mapHeight); this.inputHandler = new InputHandler({ keymap: config.keymap }); this.minimap = new Minimap({ map: this.map, player: this.player, size: config.minimap.size || 64, }); this.renderer = new Renderer({ context: ctx, minimap: this.minimap }); this.scene = new Scene({ player: this.player, map: this.map, camera: this.camera, }); } generateObstacles(count, mapWidth, mapHeight) { const obstacles = []; for (let i = 0; i < count; i++) { const x = Math.random() * (mapWidth - 50); const y = Math.random() * (mapHeight - 50); const size = 20 + Math.random() * 40; obstacles.push(new Obstacle({ position: new Vector2D(x, y), size })); } return obstacles; } update() { this.scene.update(this.inputHandler); } draw() { this.scene.draw(this.renderer); } gameLoop() { this.update(); this.draw(); requestAnimationFrame(() => this.gameLoop()); } start() { this.gameLoop(); } } // Initialize and start the game const canvas = document.getElementById("gameCanvas"); const ctx = canvas.getContext("2d"); const config = { player: { position: new Vector2D(150, 150), size: 30, }, map: { width: 3000, height: 3000, obstacleCount: 20, }, minimap: { size: 64, }, keymap: { moveUp: ["w", "ArrowUp"], moveLeft: ["a", "ArrowLeft"], moveDown: ["s", "ArrowDown"], moveRight: ["d", "ArrowRight"], }, }; const game = new Game(config); game.start();