xxxxxxxxxx
391
// Global storage for world cells, frontier (to be collapsed), items, and enemies.
let world = {}; // Keyed by "x,y" holding {type, x, y}
let frontier = new Set(); // Holds neighbor coordinates (as strings) to be processed.
let items = {}; // Keyed by "x,y", e.g. coins.
let enemies = []; // Array of enemy objects: { x, y }.
// Player and score.
let playerX = 0, playerY = 0;
let score = 0;
// Rendering & generation parameters.
const cellSize = 20; // Pixel size of each cell.
const viewRadius = 30; // How many cells (in each direction) to show.
const unloadThreshold = 60; // Cells farther than this (in grid units) are removed.
// Colors for each tile type.
let typeColors;
function setup() {
createCanvas(800, 800);
// Define colors for each terrain type.
typeColors = {
floor: color(220),
wall: color(120),
grass: color(50, 205, 50),
water: color(30, 144, 255),
mountain: color(169, 169, 169)
};
// Start at the center with a preset cell.
world["0,0"] = { type: "floor", x: 0, y: 0 };
addFrontierNeighbors(0, 0);
}
function draw() {
background(51);
// Process frontier cells (spiral–ordered) to gradually collapse them.
processFrontier();
// Unload cells that are too far away.
unloadCells();
// Ensure all visible cells are in the frontier.
let coords = getSpiralCoords();
for (let pos of coords) {
let key = pos.x + "," + pos.y;
if (!world[key]) {
frontier.add(key);
}
}
// --- Update Fun Mechanics ---
updateEnemies();
spawnEnemy();
checkItemCollection();
// --- Draw the Grid in Spiral Order ---
for (let pos of coords) {
let key = pos.x + "," + pos.y;
// Compute screen coordinates so that the player is always centered.
let drawX = width / 2 + (pos.x - playerX) * cellSize;
let drawY = height / 2 + (pos.y - playerY) * cellSize;
stroke(0);
if (world[key]) {
fill(typeColors[world[key].type]);
} else {
fill(150);
}
rect(drawX, drawY, cellSize, cellSize);
// Draw a coin if one exists on this cell.
if (items[key] && items[key].type === "coin") {
noStroke();
fill(255, map(sin(frameCount/16),-1,1,0,1)*30 + 210, 0);
ellipse(1+(drawX + cellSize/2), 1+(drawY + cellSize/2), cellSize * 0.5);
}
}
// --- Draw Enemies ---
for (let enemy of enemies) {
let key = enemy.x + "," + enemy.y;
let drawX = width / 2 + (enemy.x - playerX) * cellSize;
let drawY = height / 2 + (enemy.y - playerY) * cellSize;
fill(138, 43, 226); // A purple tone.
noStroke();
rect(drawX, drawY, cellSize, cellSize);
fill(255,0,0)
stroke(255,0,0)
line(drawX, drawY+cellSize, drawX+cellSize, drawY+(cellSize*.7))
strokeWeight(2)
point(drawX + (cellSize/3), drawY+(cellSize/4))
point(drawX + 2*(cellSize/3), drawY+(cellSize/4))
}
// --- Draw the Player ---
fill(255, 0, 0);
noStroke();
rect(width / 2, height / 2, cellSize, cellSize);
// --- Display Score ---
fill(255);
textSize(16);
text("Score: " + score, 10, 20);
}
// --- Frontier Processing ---
// In spiral order, collapse a limited number of frontier cells.
function processFrontier() {
let processed = 0;
const maxProcess = 20; // Process at most 20 cells per frame.
let coords = getSpiralCoords();
let toProcess = [];
for (let pos of coords) {
let key = pos.x + "," + pos.y;
if (frontier.has(key)) {
toProcess.push(key);
}
}
for (let key of toProcess) {
if (processed >= maxProcess) break;
if (!world[key]) {
let [x, y] = key.split(",").map(Number);
generateCell(x, y);
processed++;
}
frontier.delete(key);
}
}
// --- Generate (Collapse) a Cell ---
// Collapse the cell at (x, y) using a weighted decision and then add neighbors.
function generateCell(x, y) {
let tile = collapseCell(x, y);
let key = x + "," + y;
world[key] = { type: tile, x: x, y: y };
addFrontierNeighbors(x, y);
// If the cell is "floor" or "grass", sometimes spawn a coin.
if ((tile === "floor" || tile === "grass") && random() < 0.05) {
items[key] = { type: "coin" };
}
}
// --- "Collapse" Function ---
// Choose among several tile states (floor, wall, grass, water, mountain)
// based on base weights and the influence of adjacent cells.
function collapseCell(x, y) {
// Base weights for each state.
let baseWeights = {
floor: 2,
wall: 1,
grass: 2,
water: 1,
mountain: 0.5
};
// Copy the base weights.
let weights = Object.assign({}, baseWeights);
// Look at the four cardinal neighbors.
let neighbors = [
{ dx: 1, dy: 0 },
{ dx: -1, dy: 0 },
{ dx: 0, dy: 1 },
{ dx: 0, dy: -1 }
];
let newDist = abs(x) + abs(y); // Manhattan distance from (0,0)
for (let n of neighbors) {
let nx = x + n.dx, ny = y + n.dy;
let key = nx + "," + ny;
if (world[key]) {
let neighborDist = abs(nx) + abs(ny);
// Boost the chance for the neighbor’s state.
if (newDist > neighborDist) {
weights[world[key].type] *= 3;
} else {
weights[world[key].type] *= 1.5;
}
}
}
// Weighted random selection.
let totalWeight = 0;
for (let state in weights) {
totalWeight += weights[state];
}
let r = random(totalWeight);
let cumulative = 0;
for (let state in weights) {
cumulative += weights[state];
if (r < cumulative) {
return state;
}
}
return "floor"; // Fallback.
}
// --- Add Neighbors to Frontier ---
// For a collapsed cell at (x, y), add its four cardinal neighbors.
function addFrontierNeighbors(x, y) {
let neighbors = [
{ dx: 1, dy: 0 },
{ dx: -1, dy: 0 },
{ dx: 0, dy: 1 },
{ dx: 0, dy: -1 }
];
for (let n of neighbors) {
let nx = x + n.dx, ny = y + n.dy;
let key = nx + "," + ny;
if (!world[key]) {
frontier.add(key);
}
}
}
// --- Unload Distant Cells ---
// Remove cells that are far from the player to conserve memory.
function unloadCells() {
for (let key in world) {
let [x, y] = key.split(",").map(Number);
if (abs(x - playerX) > unloadThreshold || abs(y - playerY) > unloadThreshold) {
delete world[key];
}
}
// Also unload items that are far away.
for (let key in items) {
let [x, y] = key.split(",").map(Number);
if (abs(x - playerX) > unloadThreshold || abs(y - playerY) > unloadThreshold) {
delete items[key];
}
}
}
// --- Spiral Ordering ---
// Returns an array of grid positions (with properties x and y)
// covering [playerX - viewRadius, playerX + viewRadius] etc., in spiral order.
function getSpiralCoords() {
let offsets = spiralOffsets(viewRadius);
return offsets.map(off => ({ x: playerX + off.x, y: playerY + off.y }));
}
// spiralOffsets(n) generates offsets (from 0,0) for a square of side (2*n+1)
// in spiral order (center outward).
function spiralOffsets(n) {
let result = [];
let x = 0, y = 0;
result.push({ x: x, y: y });
let step = 1;
while (result.length < (2 * n + 1) * (2 * n + 1)) {
// Move right.
for (let i = 0; i < step; i++) {
x++;
if (abs(x) <= n && abs(y) <= n) result.push({ x, y });
}
// Move down.
for (let i = 0; i < step; i++) {
y++;
if (abs(x) <= n && abs(y) <= n) result.push({ x, y });
}
step++;
// Move left.
for (let i = 0; i < step; i++) {
x--;
if (abs(x) <= n && abs(y) <= n) result.push({ x, y });
}
// Move up.
for (let i = 0; i < step; i++) {
y--;
if (abs(x) <= n && abs(y) <= n) result.push({ x, y });
}
step++;
}
return result;
}
// --- Fun Mechanic: Digging Walls ---
// When the Spacebar is pressed, check the four adjacent cells;
// if any is a wall, convert one to a floor.
function digWall() {
let directions = [
{ dx: 0, dy: -1 },
{ dx: 1, dy: 0 },
{ dx: 0, dy: 1 },
{ dx: -1, dy: 0 }
];
let candidateWalls = [];
for (let d of directions) {
let nx = playerX + d.dx;
let ny = playerY + d.dy;
let key = nx + "," + ny;
if (world[key] && world[key].type === "wall") {
candidateWalls.push({ x: nx, y: ny });
}
}
if (candidateWalls.length > 0) {
let choice = random(candidateWalls);
let key = choice.x + "," + choice.y;
world[key].type = "floor";
// Add its neighbors in case new terrain needs to be generated.
addFrontierNeighbors(choice.x, choice.y);
}
}
// --- Fun Mechanic: Enemy Behavior ---
// Enemies wander randomly and if one reaches the player, you lose a point.
function updateEnemies() {
for (let enemy of enemies) {
// With a small chance, move the enemy one cell.
if (random() < 0.03) {
let directions = [
{ dx: 0, dy: -1 },
{ dx: 1, dy: 0 },
{ dx: 0, dy: 1 },
{ dx: -1, dy: 0 }
];
let move = random(directions);
let newX = enemy.x + move.dx;
let newY = enemy.y + move.dy;
let key = newX + "," + newY;
// Only allow movement if the target cell is generated and isn’t a wall.
if (world[key] && world[key].type !== "wall") {
enemy.x = newX;
enemy.y = newY;
}
}
}
// Check for collisions: if an enemy is on the player, reduce score and remove that enemy.
for (let i = enemies.length - 1; i >= 0; i--) {
if (enemies[i].x === playerX && enemies[i].y === playerY) {
score = max(0, score - 1);
enemies.splice(i, 1);
}
}
}
// Occasionally spawn an enemy in a visible, non-wall cell.
function spawnEnemy() {
if (random() < 0.005) {
let spiral = getSpiralCoords();
let candidates = spiral.filter(pos => {
let key = pos.x + "," + pos.y;
return world[key] &&
(world[key].type === "floor" || world[key].type === "grass") &&
!(pos.x === playerX && pos.y === playerY);
});
if (candidates.length > 0) {
let spawn = random(candidates);
enemies.push({ x: spawn.x, y: spawn.y });
}
}
}
// --- Fun Mechanic: Collecting Items ---
// If the player steps on a cell with a coin, collect it.
function checkItemCollection() {
let key = playerX + "," + playerY;
if (items[key] && items[key].type === "coin") {
score += 1;
delete items[key];
}
}
// --- WSAD & Spacebar Controls ---
// Move the player with WSAD; use Spacebar to dig through an adjacent wall.
function keyPressed() {
// Use keyCode for space (32)
if (keyCode === 32) {
digWall();
} else if (key === 'w' || key === 'W') {
playerY -= 1;
} else if (key === 's' || key === 'S') {
playerY += 1;
} else if (key === 'a' || key === 'A') {
playerX -= 1;
} else if (key === 'd' || key === 'D') {
playerX += 1;
}
}