// 2D Sandbox Game: Factions, Terrain, and Simple AI // HTML5 Canvas + Vanilla JS const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); document.body.style.margin = "0"; canvas.width = 800; canvas.height = 600; document.body.appendChild(canvas); // --- CONFIGURATION --- const TILE_SIZE = 8; const MAP_W = Math.floor(canvas.width / TILE_SIZE); const MAP_H = Math.floor(canvas.height / TILE_SIZE); const COLORS = { grass: "#59c13b", treeTrunk: "#66462d", treeLeaf: "#24751b", human1Skin: "#ffe5b4", human1Hat: "#4caa50", human2Skin: "#e7c49a", human2Hat: "#223f94", rock: "#bbbbbb", water: "#2d71c2", friendly: "#ffeb3b", war: "#e53935", }; const FACTIONS = [ { name: "GreenHats", skin: COLORS.human1Skin, hat: COLORS.human1Hat, id: 1, }, { name: "BlueHats", skin: COLORS.human2Skin, hat: COLORS.human2Hat, id: 2, } ]; // --- WORLD GENERATION --- // 0: grass, 1: tree, 2: rock, 3: water function generateWorld() { let map = []; for (let y = 0; y < MAP_H; y++) { map[y] = []; for (let x = 0; x < MAP_W; x++) { let tile = 0; if (Math.random() < 0.07) tile = 1; // tree else if (Math.random() < 0.03) tile = 2; // rock else if (Math.random() < 0.02) tile = 3; // water map[y][x] = tile; } } return map; } const world = generateWorld(); // --- HUMAN AGENTS --- function randomEmptyTile() { while (true) { let x = Math.floor(Math.random() * MAP_W); let y = Math.floor(Math.random() * MAP_H); if (world[y][x] === 0) return {x, y}; } } function createHuman(faction) { let pos = randomEmptyTile(); return { x: pos.x, y: pos.y, faction: faction.id, hp: 10, state: "idle", // idle, moving, fighting, social target: null, cooldown: 0, friend: null, }; } let humans = []; for (let i = 0; i < 20; i++) humans.push(createHuman(FACTIONS[0])); for (let i = 0; i < 20; i++) humans.push(createHuman(FACTIONS[1])); // --- DIPLOMACY SYSTEM --- // Simple: factions can be at war or peace. Toggle every 30 seconds. let factionRelations = { "1-2": "war", // "peace" or "war" }; setInterval(() => { factionRelations["1-2"] = (factionRelations["1-2"] === "war") ? "peace" : "war"; }, 30000); // --- GAME LOOP & LOGIC --- function isBlocked(x, y) { if (x < 0 || y < 0 || x >= MAP_W || y >= MAP_H) return true; if (world[y][x] === 1 || world[y][x] === 2 || world[y][x] === 3) return true; return false; } function findNearby(targetX, targetY, range, filterFn) { let found = []; for (let dy = -range; dy <= range; dy++) { for (let dx = -range; dx <= range; dx++) { let x = targetX + dx; let y = targetY + dy; if (x < 0 || y < 0 || x >= MAP_W || y >= MAP_H) continue; let entity = humans.find(h => h.x === x && h.y === y && filterFn(h)); if (entity) found.push(entity); } } return found; } function stepHuman(h) { if (h.hp <= 0) return; // Cooldown if (h.cooldown > 0) { h.cooldown--; return; } // Interactions: war or peace let enemyFaction = (h.faction === 1) ? 2 : 1; let relation = factionRelations["1-2"]; let nearbyEnemies = findNearby(h.x, h.y, 1, hh => hh.faction === enemyFaction && hh.hp > 0); let nearbyFriends = findNearby(h.x, h.y, 1, hh => hh.faction === h.faction && hh !== h && hh.hp > 0); if (relation === "war" && nearbyEnemies.length > 0) { // Attack let enemy = nearbyEnemies[0]; enemy.hp -= 2; h.state = "fighting"; h.cooldown = 10; if (enemy.hp <= 0) { // Remove dead enemy.x = -1; enemy.y = -1; } return; } else if (relation === "peace" && nearbyEnemies.length > 0) { // Socialize (make friend) h.state = "social"; h.friend = nearbyEnemies[0]; h.cooldown = 30; return; } // Random move let dirs = [ {dx: -1, dy: 0}, {dx: 1, dy: 0}, {dx: 0, dy: -1}, {dx: 0, dy: 1} ]; let shuffled = dirs.sort(() => Math.random() - 0.5); for (let dir of shuffled) { let nx = h.x + dir.dx; let ny = h.y + dir.dy; if (!isBlocked(nx, ny) && !humans.some(hu => hu.x === nx && hu.y === ny && hu.hp > 0)) { h.x = nx; h.y = ny; break; } } } function update() { humans.forEach(stepHuman); } function drawTile(x, y, type) { switch (type) { case 0: ctx.fillStyle = COLORS.grass; break; case 1: ctx.fillStyle = COLORS.treeTrunk; ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE / 2); ctx.fillStyle = COLORS.treeLeaf; ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE / 2); return; case 2: ctx.fillStyle = COLORS.rock; break; case 3: ctx.fillStyle = COLORS.water; break; default: ctx.fillStyle = COLORS.grass; } ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE); } function drawHuman(h) { if (h.hp <= 0) return; let px = h.x * TILE_SIZE, py = h.y * TILE_SIZE; // Head (skin) ctx.fillStyle = (h.faction === 1) ? COLORS.human1Skin : COLORS.human2Skin; ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE); // Hat ctx.fillStyle = (h.faction === 1) ? COLORS.human1Hat : COLORS.human2Hat; ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE / 3); // Border (show state) if (h.state === "fighting") { ctx.strokeStyle = COLORS.war; ctx.lineWidth = 2; ctx.strokeRect(px, py, TILE_SIZE, TILE_SIZE); } else if (h.state === "social") { ctx.strokeStyle = COLORS.friendly; ctx.lineWidth = 2; ctx.strokeRect(px, py, TILE_SIZE, TILE_SIZE); } } function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw world for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { drawTile(x * TILE_SIZE, y * TILE_SIZE, world[y][x]); } } // Draw humans humans.forEach(drawHuman); // UI: Relation ctx.fillStyle = "#fff"; ctx.font = "16px monospace"; ctx.fillText( "Relation: " + factionRelations["1-2"], 10, 20 ); } function gameLoop() { update(); render(); requestAnimationFrame(gameLoop); } gameLoop();