d-zone
Version:
An ambient life simulation driven by the user activity within a Discord server
328 lines (303 loc) • 15 kB
JavaScript
'use strict';
var util = require('./../common/util.js');
var geometry = require('./../common/geometry.js');
var Pathfinder = require('./../actors/pathfinder.js');
var Slab = require('./slab.js');
var Tile = require('./tile.js');
var TileSheet = require('./sheet2.js');
module.exports = World;
var Canvas = require('./../common/bettercanvas.js');
var testCanvas = new Canvas(200,100);
var unoccupiedGrids; // For faster actor placement on init
//document.body.appendChild(testCanvas.canvas);
function World(game,worldSize) {
this.game = game;
this.game.world = this;
this.worldSize = Math.max(24,Math.floor(worldSize/2)*2); // Must be an even number >= 24
//this.worldSize = Math.max(12,Math.floor(worldSize/2)*2); // Must be an even number >= 24
this.worldRadius = Math.floor(this.worldSize/2);
this.objects = {};
this.map = {}; // Grid-based map to hold world tiles
this.walkable = {}; // Grid-based map to hold walkable surfaces
// TODO: Move world generation into a new module
geometry.generateClosestGrids(this.worldSize);
testCanvas.clear();
var noiseBig = geometry.buildNoiseMap(this.worldRadius/3 + 1, this.worldRadius/3 + 1);
var noiseSmall = geometry.buildNoiseMap(this.worldRadius/1.5 + 1,this.worldRadius/1.5 + 1);
var bigBlur = (noiseBig.length - 1) / this.worldSize;
var smallBlur = (noiseSmall.length - 1) / this.worldSize;
this.mapBounds = { xl: 0, yl: 0, xh: 0, yh: 0 }; // TODO: Center canvas using this
for(var tx = 0; tx < this.worldSize; tx++) for(var ty = 0; ty < this.worldSize; ty++) {
var bigNoiseValue = geometry.getNoiseMapPoint(noiseBig, tx * bigBlur, ty * bigBlur);
var smallNoiseValue = geometry.getNoiseMapPoint(noiseSmall, tx * smallBlur, ty * smallBlur);
var noiseValue = (bigNoiseValue + smallNoiseValue*2) / 3;
//var color = 'rgba(255,255,255,'+noiseValue+')'; // Draw debug noise map
//testCanvas.fillRect(color, tx, ty, 1, 1);
var grid;
var x = (tx-this.worldRadius), y = (ty-this.worldRadius);
var farness = (this.worldRadius - (Math.abs(x)+Math.abs(y))/2)/this.worldRadius;
if(noiseValue/1.1 < farness) {
this.mapBounds.xl = Math.min(x,this.mapBounds.xl);
this.mapBounds.yl = Math.min(y,this.mapBounds.yl);
this.mapBounds.xh = Math.max(x,this.mapBounds.xh);
this.mapBounds.yh = Math.max(y,this.mapBounds.yh);
grid = new Slab('grass', x, y, -0.5);
grid.grid = x+':'+y;
this.map[x+':'+y] = grid;
grid.addToGame(game);
}
}
this.staticMap = [];
this.crawlMap(); // Examine map to determine islands, borders, etc
this.createTiles(); // Create map tiles from grid intersections
var lowestScreenX = 0, lowestScreenY = 0, highestScreenX = 0, highestScreenY = 0;
for(var i = 0; i < this.staticMap.length; i++) {
var preTile = this.staticMap[i];
var preScreen = { x: preTile.screen.x, y: preTile.screen.y };
preScreen.x += preTile.sprite.metrics.ox || 0;
preScreen.y += preTile.sprite.metrics.oy || 0;
lowestScreenX = lowestScreenX < preScreen.x ? lowestScreenX : preScreen.x;
lowestScreenY = lowestScreenY < preScreen.y ? lowestScreenY : preScreen.y;
highestScreenX = highestScreenX > preScreen.x ? highestScreenX : preScreen.x;
highestScreenY = highestScreenY > preScreen.y ? highestScreenY : preScreen.y;
}
var bgCanvas = new Canvas(
(highestScreenX - lowestScreenX) + 32 + 1, (highestScreenY - lowestScreenY) + 32 + 9
);
for(var j = 0; j < this.staticMap.length; j++) {
var tile = this.staticMap[j];
var screen = { x: tile.screen.x, y: tile.screen.y };
screen.x += tile.sprite.metrics.ox || 0;
screen.y += tile.sprite.metrics.oy || 0;
screen.x -= lowestScreenX;
screen.y -= lowestScreenY;
bgCanvas.drawImage(
this.game.renderer.images[tile.sprite.image], tile.sprite.metrics.x, tile.sprite.metrics.y,
tile.sprite.metrics.w, tile.sprite.metrics.h,
Math.round(screen.x), Math.round(screen.y), tile.sprite.metrics.w, tile.sprite.metrics.h
);
}
//bgCanvas.context.globalCompositeOperation = 'color';
//bgCanvas.fill('rgba(30,30,50,0.7)');
this.game.renderer.bgCanvas = {
x: lowestScreenX, y: lowestScreenY, image: bgCanvas.canvas
};
Pathfinder.loadMap(this.walkable);
unoccupiedGrids = Object.keys(this.map);
unoccupiedGrids.splice(unoccupiedGrids.indexOf('0:0'), 1); // 0,0 is taken by beacon
console.log('Created world with',Object.keys(this.map).length,'tiles');
// TODO: Retry if tile count is too high/low
}
World.prototype.crawlMap = function() {
this.islands = [];
var crawled = {};
var thisIsland = 0;
for(var x = this.mapBounds.xl; x <= this.mapBounds.xh; x++) {
for(var y = this.mapBounds.yl; y <= this.mapBounds.yh; y++) {
var currentTile = this.map[x+':'+y]; if(!currentTile) continue;
if(crawled[currentTile.grid]) continue; // Skip already-crawled tiles
var neighborsToCrawl = [];
while(true) { // Keep crawling outward until no neighbors are left
crawled[currentTile.grid] = currentTile;
if(this.islands[thisIsland]) this.islands[thisIsland].push(currentTile);
else this.islands.push([currentTile]);
var currentNeighbors = geometry.getNeighbors(currentTile.grid);
currentNeighbors = geometry.getNeighbors(currentTile.grid);
for(var iKey in currentNeighbors) { if (!currentNeighbors.hasOwnProperty(iKey)) continue;
var neighbor = this.map[currentNeighbors[iKey]];
if(!neighbor) { currentTile.border = true; continue; }
if(!crawled[neighbor.grid]) neighborsToCrawl.push(neighbor);
}
if(neighborsToCrawl.length > 0) {
currentTile = neighborsToCrawl.pop();
} else { thisIsland++; break; } // No more neighbors, this island is done
}
}
}
this.mainIsland = 0;
for(var i = 1; i < this.islands.length; i++) {
this.mainIsland = this.islands[i].length > this.islands[this.mainIsland].length ?
i : this.mainIsland;
}
for(var i2 = 0; i2 < this.islands.length; i2++) { if(i2 == this.mainIsland) continue;
for(var it = 0; it < this.islands[i2].length; it++) {
delete this.map[this.islands[i2][it].grid];
this.islands[i2][it].remove();
}
}
// Set border tiles to slab
for(var gKey in this.map) { if(!this.map.hasOwnProperty(gKey)) continue;
var finalTile = this.map[gKey];
if(finalTile.border) {
finalTile.style = 'plain';
} else {
var finalNeighbors = geometry.get8Neighbors(finalTile.grid);
for(var nKey in finalNeighbors) { if (!finalNeighbors.hasOwnProperty(nKey)) continue;
if(!this.map[finalNeighbors[nKey]]) {
finalTile.style = 'plain';
break;
}
}
}
}
this.map['0:0'].style = 'plain'; // Slab around beacon
this.map['1:0'].style = 'plain';
this.map['-1:0'].style = 'plain';
this.map['0:1'].style = 'plain';
this.map['0:-1'].style = 'plain';
// Create flower patches
for(var fp = 0; fp < Math.ceil(Math.pow(this.worldRadius,2) / 80); fp++) {
var safety = 0;
do {
var valid = true;
var grid = this.map[util.pickInObject(this.map)];
var flowerNeighbors = geometry.get8Neighbors(grid.grid);
for(var fKey in flowerNeighbors) { if (!flowerNeighbors.hasOwnProperty(fKey)) continue;
var fNeighbor = this.map[flowerNeighbors[fKey]];
if(!fNeighbor || fNeighbor.style != 'grass') {
valid = false;
break;
}
}
safety++;
} while(safety < 1000 && (grid.style != 'grass' || !valid));
if(safety == 1000) continue;
grid.style = 'flowers';
var spread = util.randomIntRange(1,4);
for(var s = 0; s < spread; s++) {
var canSpread = true;
var spreadX = grid.position.x+util.randomIntRange(-1,1),
spreadY = grid.position.y+util.randomIntRange(-1,1);
var spreadGrid = this.map[spreadX+':'+spreadY];
var spreadNeighbors = geometry.get8Neighbors(spreadGrid.grid);
for(var sKey in spreadNeighbors) { if (!spreadNeighbors.hasOwnProperty(sKey)) continue;
var sNeighbor = this.map[spreadNeighbors[sKey]];
if(!sNeighbor || (sNeighbor.style != 'grass' && sNeighbor.style != 'flowers')) {
canSpread = false;
break;
}
}
if(canSpread) spreadGrid.style = 'flowers';
}
}
};
World.prototype.createTiles = function() {
// Tile types:
// Grass G
// Slab S
// Flowers F
// Empty E
// Tile code constructed as NW-NE-SE-SW (eg. "S-X-X-B")
this.tileMap = {};
var self = this;
function tileType(grid) { return self.map[grid].style[0].replace(/p/,'s').toUpperCase(); }
function getTileCode(oGrid, nGrid) {
if(oGrid == nGrid) return tileType(oGrid);
var neighbor = self.map[nGrid];
if(!neighbor) return 'E';
return tileType(nGrid);
}
function generateTile(oGrid, tile, grid, game) {
var nGrids = tile.grids;
var tileCode = getTileCode(oGrid,nGrids[0])+'-'+getTileCode(oGrid,nGrids[1])
+'-'+getTileCode(oGrid,nGrids[2])+'-'+getTileCode(oGrid,nGrids[3]);
var tileSprite = (new TileSheet('tile')).map[tileCode];
if(!tileSprite) console.error('unknown tile code',tileCode,nGrids);
return {
tileCode: tileCode, position: tile, grid: grid, game: game
};
}
for(var key in this.map) { if(!this.map.hasOwnProperty(key)) continue;
var x = +key.split(':')[0], y = +key.split(':')[1], z = this.map[key].position.z;
var neighbors = geometry.get8Neighbors(key);
var nw = { x: x-0.5, y: y-0.5, z: z, grids: [neighbors.nw, neighbors.n, key, neighbors.w] },
ne = { x: x+0.5, y: y-0.5, z: z, grids: [neighbors.n, neighbors.ne, neighbors.e, key] },
se = { x: x+0.5, y: y+0.5, z: z, grids: [key, neighbors.e, neighbors.se, neighbors.s] },
sw = { x: x-0.5, y: y+0.5, z: z, grids: [neighbors.w, key, neighbors.s, neighbors.sw] };
var tiles = [nw,ne,se,sw];
for(var i = 0; i < tiles.length; i++) {
var tileGrid = z+':'+tiles[i].x+':'+tiles[i].y;
if(this.tileMap[tileGrid]) continue;
this.tileMap[tileGrid] = new Tile(generateTile(key, tiles[i], tileGrid, this.game));
this.staticMap.push(this.tileMap[tileGrid]);
}
}
this.staticMap.sort(function(a,b) { return a.zDepth - b.zDepth; });
};
World.prototype.addToWorld = function(obj) {
//console.log('world: adding object at',obj.position.x,obj.position.y,obj.position.z);
if(this.objects[obj.position.x]) {
if(this.objects[obj.position.x][obj.position.y]) {
if(this.objects[obj.position.x][obj.position.y][obj.position.z]) {
console.error('occupado!',obj.position.x,obj.position.y,obj.position.z,
obj,this.objects[obj.position.x][obj.position.y][obj.position.z]);
return false;
}
} else {
this.objects[obj.position.x][obj.position.y] = {};
}
} else {
this.objects[obj.position.x] = {};
this.objects[obj.position.x][obj.position.y] = {};
}
this.objects[obj.position.x][obj.position.y][obj.position.z] = obj;
this.updateWalkable(obj.position.x, obj.position.y);
};
World.prototype.removeFromWorld = function(obj) {
//console.log('world: removing object at',obj.position.x,obj.position.y,obj.position.z);
delete this.objects[obj.position.x][obj.position.y][obj.position.z];
this.updateWalkable(obj.position.x, obj.position.y);
};
World.prototype.moveObject = function(obj,x,y,z) {
//console.log('world: moving object from',obj.position.x,obj.position.y,obj.position.z,'to',x,y,z);
this.removeFromWorld(obj);
obj.position.x = x; obj.position.y = y; obj.position.z = z;
this.addToWorld(obj)
};
World.prototype.updateWalkable = function(x, y) {
//console.log('world: updating walkable at',x,y);
var objects = this.objects[x][y];
if(!objects || Object.keys(objects).length == 0) {
delete this.walkable[x+':'+y];
//console.log('world: ',x,y,'is now unwalkable');
return;
}
var zKeys = Object.keys(objects).sort(function(a, b) { return a - b; });
var topObject = objects[zKeys[zKeys.length-1]];
if(topObject.unWalkable) {
delete this.walkable[x+':'+y];
//console.log('world: ',x,y,'is now unwalkable');
} else {
this.walkable[x+':'+y] = topObject.position.z + topObject.height;
//console.log('world: ',x,y,'is now walkable',this.walkable[x+':'+y]);
}
};
World.prototype.randomEmptyGrid = function() {
return unoccupiedGrids.splice(util.randomIntRange(0, unoccupiedGrids.length - 1), 1)[0];
};
World.prototype.objectAtXYZ = function(x,y,z) {
if(!this.objects[x]) return false;
if(!this.objects[x][y]) return false;
return this.objects[x][y][z];
};
World.prototype.objectUnderXYZ = function(x,y,z) {
if(!this.objects[x]) return false;
if(!this.objects[x][y]) return false;
var highest = -1000;
for(var zKey in this.objects[x][y]) { if(!this.objects[x][y].hasOwnProperty(zKey)) continue;
if(+zKey > z) continue;
highest = +zKey > highest ? +zKey : highest;
}
return this.objects[x][y][highest];
};
World.prototype.findObject = function(obj) { // For debugging
for(var xKey in this.objects) { if (!this.objects.hasOwnProperty(xKey)) continue;
var xObjects = this.objects[xKey];
for(var yKey in xObjects) { if (!xObjects.hasOwnProperty(yKey)) continue;
var yObjects = xObjects[yKey];
for(var zKey in yObjects) { if (!yObjects.hasOwnProperty(zKey)) continue;
if(obj === yObjects[zKey]) return [xKey,yKey,zKey];
}
}
}
};