Index: ps/trunk/binaries/data/mods/public/globalscripts/interpolation.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/interpolation.js (revision 21132) +++ ps/trunk/binaries/data/mods/public/globalscripts/interpolation.js (revision 21133) @@ -1,41 +1,41 @@ /** * Interpolates the value of a point that is located between four known equidistant points with given values by * constructing a polynomial of degree three that goes through all points. * Computes a cardinal or Catmull-Rom spline. * * @param {Number} tension - determines how sharply the curve bends at the given points. * @param {Number} x - Location of the point to interpolate, relative to p1 */ function cubicInterpolation(tension, x, p0, p1, p2, p3) { let P = -tension * p0 + (2 - tension) * p1 + (tension - 2) * p2 + tension * p3; let Q = 2 * tension * p0 + (tension - 3) * p1 + (3 - 2 * tension) * p2 - tension * p3; let R = -tension * p0 + tension * p2; let S = p1; return ((P * x + Q) * x + R) * x + S; } /** * Two dimensional interpolation within a square grid using a polynomial of degree three. * - * @param {Number} x, y - Location of the point to interpolate, relative to p11 + * @param {Vector2D} position - Location of the point to interpolate, relative to p11 */ function bicubicInterpolation ( - x, y, + position, p00, p01, p02, p03, p10, p11, p12, p13, p20, p21, p22, p23, p30, p31, p32, p33 ) { let tension = 0.5; return cubicInterpolation( tension, - x, - cubicInterpolation(tension, y, p00, p01, p02, p03), - cubicInterpolation(tension, y, p10, p11, p12, p13), - cubicInterpolation(tension, y, p20, p21, p22, p23), - cubicInterpolation(tension, y, p30, p31, p32, p33)); + position.x, + cubicInterpolation(tension, position.y, p00, p01, p02, p03), + cubicInterpolation(tension, position.y, p10, p11, p12, p13), + cubicInterpolation(tension, position.y, p20, p21, p22, p23), + cubicInterpolation(tension, position.y, p30, p31, p32, p33)); } Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js (revision 21132) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js (revision 21133) @@ -1,327 +1,440 @@ /** * @file A Painter modifies an arbitrary feature in a given Area, for instance terrain textures, elevation or calling other painters on that Area. * Typically the area is determined by a Placer called from createArea or createAreas. */ /** * Marks the affected area with the given tileclass. */ function TileClassPainter(tileClass) { this.tileClass = tileClass; } TileClassPainter.prototype.paint = function(area) { for (let point of area.points) this.tileClass.add(point); }; /** * Removes the given tileclass from a given area. */ function TileClassUnPainter(tileClass) { this.tileClass = tileClass; } TileClassUnPainter.prototype.paint = function(area) { for (let point of area.points) this.tileClass.remove(point); }; /** * The MultiPainter applies several painters to the given area. */ function MultiPainter(painters) { if (painters instanceof Array) this.painters = painters; else if (!painters) this.painters = []; else this.painters = [painters]; } MultiPainter.prototype.paint = function(area) { for (let painter of this.painters) painter.paint(area); }; /** * The TerrainPainter draws a given terrain texture over the given area. * When used with TERRAIN_SEPARATOR, an entity is placed on each tile. */ function TerrainPainter(terrain) { this.terrain = createTerrain(terrain); } TerrainPainter.prototype.paint = function(area) { for (let point of area.points) this.terrain.place(point); }; /** * The LayeredPainter sets different Terrains within the Area. * It choses the Terrain depending on the distance to the border of the Area. * * The Terrains given in the first array are painted from the border of the area towards the center (outermost first). * The widths array has one item less than the Terrains array. * Each width specifies how many tiles the corresponding Terrain should be wide (distance to the prior Terrain border). * The remaining area is filled with the last terrain. */ function LayeredPainter(terrainArray, widths) { if (!(terrainArray instanceof Array)) throw new Error("LayeredPainter: terrains must be an array!"); this.terrains = terrainArray.map(terrain => createTerrain(terrain)); this.widths = widths; } LayeredPainter.prototype.paint = function(area) { breadthFirstSearchPaint({ "area": area, "brushSize": 1, "gridSize": g_Map.getSize(), "withinArea": (areaID, position) => g_Map.area[position.x][position.y] == areaID, "paintTile": (point, distance) => { let width = 0; let i = 0; for (; i < this.widths.length; ++i) { width += this.widths[i]; if (width >= distance) break; } this.terrains[i].place(point); } }); }; /** * Sets the given height in the given Area. */ function ElevationPainter(elevation) { this.elevation = elevation; } ElevationPainter.prototype.paint = function(area) { for (let point of area.points) for (let vertex of g_TileVertices) { let position = Vector2D.add(point, vertex); if (g_Map.inMapBounds(position)) g_Map.setHeight(position, this.elevation); } }; /** * Absolute height change. */ const ELEVATION_SET = 0; /** * Relative height change. */ const ELEVATION_MODIFY = 1; /** * Sets the elevation of the Area in dependence to the given blendRadius and * interpolates it with the existing elevation. * * @param type - ELEVATION_MODIFY or ELEVATION_SET. * @param elevation - target height. * @param blendRadius - How steep the elevation change is. * @param randomElevation - maximum random elevation difference added to each vertex. */ function SmoothElevationPainter(type, elevation, blendRadius, randomElevation = 0) { this.type = type; this.elevation = elevation; this.blendRadius = blendRadius; this.randomElevation = randomElevation; if (type != ELEVATION_SET && type != ELEVATION_MODIFY) throw new Error("SmoothElevationPainter: invalid type '" + type + "'"); } SmoothElevationPainter.prototype.paint = function(area) { // The heightmap grid has one more vertex per side than the tile grid let heightmapSize = g_Map.height.length; // Remember height inside the area before changing it let gotHeightPt = []; let newHeight = []; for (let i = 0; i < heightmapSize; ++i) { gotHeightPt[i] = new Uint8Array(heightmapSize); newHeight[i] = new Float32Array(heightmapSize); } // Get heightmap grid vertices within or adjacent to the area let brushSize = 2; let heightPoints = []; for (let point of area.points) for (let dx = -1; dx < 1 + brushSize; ++dx) { let nx = point.x + dx; for (let dz = -1; dz < 1 + brushSize; ++dz) { let nz = point.y + dz; let position = new Vector2D(nx, nz); if (g_Map.validHeight(position) && !gotHeightPt[nx][nz]) { newHeight[nx][nz] = g_Map.getHeight(position); gotHeightPt[nx][nz] = 1; heightPoints.push(position); } } } // Every vertex of a tile is considered within the area let withinArea = (areaID, position) => { for (let vertex of g_TileVertices) { let vertexPos = Vector2D.sub(position, vertex); if (g_Map.inMapBounds(vertexPos) && g_Map.area[vertexPos.x][vertexPos.y] == areaID) return true; } return false; }; // Change height inside the area depending on the distance to the border breadthFirstSearchPaint({ "area": area, "brushSize": brushSize, "gridSize": heightmapSize, "withinArea": withinArea, "paintTile": (point, distance) => { let a = 1; if (distance <= this.blendRadius) a = (distance - 1) / this.blendRadius; if (this.type == ELEVATION_SET) newHeight[point.x][point.y] = (1 - a) * g_Map.getHeight(point); newHeight[point.x][point.y] += a * this.elevation + randFloat(-0.5, 0.5) * this.randomElevation; } }); // Smooth everything out let areaID = area.getID(); for (let point of heightPoints) { if (!withinArea(areaID, point)) continue; let count = 0; let sum = 0; for (let dx = -1; dx <= 1; ++dx) { let nx = point.x + dx; for (let dz = -1; dz <= 1; ++dz) { let nz = point.y + dz; if (g_Map.validHeight(new Vector2D(nx, nz))) { sum += newHeight[nx][nz]; ++count; } } } g_Map.setHeight(point, (newHeight[point.x][point.y] + sum / count) / 2); } }; /** * Calls the given paintTile function on all points within the given Area, * providing the distance to the border of the area (1 for points on the border). * This function can traverse any grid, for instance the tile grid or the larger heightmap grid. * * @property area - An Area storing the set of points on the tile grid. * @property gridSize - The size of the grid to be traversed. * @property brushSize - Number of points per axis on the grid that are considered a point on the tilemap. * @property withinArea - Whether a point of the grid is considered part of the Area. * @property paintTile - Called for each point of the Area of the tile grid. */ function breadthFirstSearchPaint(args) { // These variables save which points were visited already and the shortest distance to the area let saw = []; let dist = []; for (let i = 0; i < args.gridSize; ++i) { saw[i] = new Uint8Array(args.gridSize); dist[i] = new Uint16Array(args.gridSize); } let withinGrid = (x, z) => Math.min(x, z) >= 0 && Math.max(x, z) < args.gridSize; // Find all points outside of the area, mark them as seen and set zero distance let pointQueue = []; let areaID = args.area.getID(); for (let point of args.area.points) // The brushSize is added because the entire brushSize is by definition part of the area for (let dx = -1; dx < 1 + args.brushSize; ++dx) { let nx = point.x + dx; for (let dz = -1; dz < 1 + args.brushSize; ++dz) { let nz = point.y + dz; let position = new Vector2D(nx, nz); if (!withinGrid(nx, nz) || args.withinArea(areaID, position) || saw[nx][nz]) continue; saw[nx][nz] = 1; dist[nx][nz] = 0; pointQueue.push(position); } } // Visit these points, then direct neighbors of them, then their neighbors recursively. // Call the paintTile method for each point within the area, with distance == 1 for the border. while (pointQueue.length) { let point = pointQueue.shift(); let distance = dist[point.x][point.y]; if (args.withinArea(areaID, point)) args.paintTile(point, distance); // Enqueue neighboring points for (let dx = -1; dx <= 1; ++dx) { let nx = point.x + dx; for (let dz = -1; dz <= 1; ++dz) { let nz = point.y + dz; let position = new Vector2D(nx, nz); if (!withinGrid(nx, nz) || !args.withinArea(areaID, position) || saw[nx][nz]) continue; saw[nx][nz] = 1; dist[nx][nz] = distance + 1; pointQueue.push(position); } } } } + +/** + * Paints the given texture-mapping to the given tiles. + * + * @param {String[]} textureIDs - Names of the terrain textures + * @param {Number[]} textureNames - One-dimensional array of indices of texturenames, one for each tile of the entire map. + * @returns + */ +function TerrainTextureArrayPainter(textureIDs, textureNames) +{ + this.textureIDs = textureIDs; + this.textureNames = textureNames; +}; + +TerrainTextureArrayPainter.prototype.paint = function(area) +{ + let sourceSize = Math.sqrt(this.textureIDs.length); + let scale = sourceSize / g_Map.getSize(); + + for (let point of area.points) + { + let sourcePos = Vector2D.mult(point, scale).floor(); + g_Map.setTexture(point, this.textureNames[this.textureIDs[sourcePos.x * sourceSize + sourcePos.y]]); + } +}; + +/** + * Copies the given heightmap to the given area. + * Scales the horizontal plane proportionally and optionally uses bicubic interpolation. + * The heightrange is either scaled proportionally or mapped to the given heightrange. + * + * @param {Uint16Array} heightmap - One dimensional array of vertex heights. + * @param {Number} [normalMinHeight] - The minimum height the elevation grid of 320 tiles would have. + * @param {Number} [normalMaxHeight] - The maximum height the elevation grid of 320 tiles would have. + */ +function HeightmapPainter(heightmap, bicubicInterpolation, normalMinHeight = undefined, normalMaxHeight = undefined) +{ + this.heightmap = heightmap; + this.bicubicInterpolation = bicubicInterpolation; + this.verticesPerSide = Math.sqrt(heightmap.length); + this.normalMinHeight = normalMinHeight; + this.normalMaxHeight = normalMaxHeight; +}; + +HeightmapPainter.prototype.paint = function(area) +{ + if (this.bicubicInterpolation) + this.paintBicubic(area); + else + this.paintNearest(area); +}; + +HeightmapPainter.prototype.getScale = function() +{ + return this.verticesPerSide / (g_Map.getSize() + 1); +}; + +HeightmapPainter.prototype.scaleHeight = function(height) +{ + if (this.normalMinHeight === undefined || this.normalMaxHeight === undefined) + return height / this.getScale() / HEIGHT_UNITS_PER_METRE; + + let minHeight = this.normalMinHeight * (g_Map.getSize() + 1) / 321; + let maxHeight = this.normalMaxHeight * (g_Map.getSize() + 1) / 321; + + return minHeight + (maxHeight - minHeight) * height / 0xFFFF; +}; + +HeightmapPainter.prototype.paintNearest = function(area) +{ + let scale = this.getScale(); + for (let point of area.points) + { + let sourcePos = Vector2D.mult(point, scale).floor(); + g_Map.setHeight(point, scaleHeight(this.heightmap[sourcePos.y * this.verticesPerSide + sourcePos.x])); + } +}; + +HeightmapPainter.prototype.paintBicubic = function(area) +{ + let scale = this.getScale(); + let leftBottom = new Vector2D(0, 0); + let rightTop = new Vector2D(this.verticesPerSide, this.verticesPerSide); + let brushSize = new Vector2D(3, 3); + let brushCenter = new Vector2D(1, 1); + + // Additional complexity to process all 4 vertices of each tile, i.e the last row too + let seen = new Array(g_Map.getSize() + 1).fill(0).map(zero => new Uint8Array(g_Map.getSize() + 1).fill(false)); + + for (let point of area.points) + for (let vertex of g_TileVertices) + { + let vertexPos = Vector2D.add(point, vertex); + if (seen[vertexPos.x][vertexPos.y]) + continue; + + seen[vertexPos.x][vertexPos.y] = true; + + let sourcePos = Vector2D.mult(vertexPos, scale); + let sourceTilePos = sourcePos.clone().floor(); + + let brushPosition = Vector2D.max( + leftBottom, + Vector2D.min( + Vector2D.sub(sourceTilePos, brushCenter), + Vector2D.sub(rightTop, brushSize).sub(brushCenter))); + + g_Map.setHeight(vertexPos, bicubicInterpolation( + Vector2D.sub(sourcePos, brushPosition).sub(brushCenter), + ...getPointsInBoundingBox(getBoundingBox([brushPosition, Vector2D.add(brushPosition, brushSize)])).map(pos => + this.scaleHeight(this.heightmap[pos.y * this.verticesPerSide + pos.x])))); + } +}; Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/random_map.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/random_map.js (revision 21132) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/random_map.js (revision 21133) @@ -1,451 +1,493 @@ /** * @file The RandomMap stores the elevation grid, terrain textures and entities that are exported to the engine. * * @param {Number} baseHeight - Initial elevation of the map * @param {String|Array} baseTerrain - One or more texture names */ function RandomMap(baseHeight, baseTerrain) { this.logger = new RandomMapLogger(); // Size must be 0 to 1024, divisible by patches this.size = g_MapSettings.Size; // Create name <-> id maps for textures this.nameToID = {}; this.IDToName = []; // Texture 2D array this.texture = []; for (let x = 0; x < this.size; ++x) { this.texture[x] = new Uint16Array(this.size); for (let z = 0; z < this.size; ++z) this.texture[x][z] = this.getTextureID( typeof baseTerrain == "string" ? baseTerrain : pickRandom(baseTerrain)); } // Create 2D arrays for terrain objects and areas this.terrainEntities = []; this.area = []; for (let i = 0; i < this.size; ++i) { this.area[i] = new Uint16Array(this.size); this.terrainEntities[i] = []; for (let j = 0; j < this.size; ++j) this.terrainEntities[i][j] = undefined; } // Create 2D array for heightmap let mapSize = this.size; if (!TILE_CENTERED_HEIGHT_MAP) ++mapSize; this.height = []; for (let i = 0; i < mapSize; ++i) { this.height[i] = new Float32Array(mapSize); for (let j = 0; j < mapSize; ++j) this.height[i][j] = baseHeight; } this.entities = []; this.areaID = 0; // Starting entity ID, arbitrary number to leave some space for player entities this.entityCount = 150; } +/** + * Prints a timed log entry to stdout and the logfile. + */ RandomMap.prototype.log = function(text) { this.logger.print(text); }; /** + * Loads an imagefile and uses it as the heightmap for the current map. + * Scales the map (including height) proportionally with the mapsize. + */ +RandomMap.prototype.LoadMapTerrain = function(filename) +{ + g_Map.log("Loading terrain file " + filename); + let mapTerrain = Engine.LoadMapTerrain("maps/random/" + filename + ".pmp"); + + let heightmapPainter = new HeightmapPainter(mapTerrain.height, true); + + createArea( + new MapBoundsPlacer(), + [ + heightmapPainter, + new TerrainTextureArrayPainter(mapTerrain.textureIDs, mapTerrain.textureNames) + ]); + + return heightmapPainter.getScale(); +}; + +/** + * Loads PMP terrain file that contains elevation grid and terrain textures created in atlas. + * Scales the map (including height) proportionally with the mapsize. + * Notice that the image heights can only be between 0 and 255, but the resulting sizes can exceed that range due to the cubic interpolation. + */ +RandomMap.prototype.LoadHeightmapImage = function(filename, normalMinHeight, normalMaxHeight) +{ + g_Map.log("Loading heightmap " + filename); + + let heightmapPainter = new HeightmapPainter(Engine.LoadHeightmapImage("maps/random/" + filename), true, normalMinHeight, normalMaxHeight); + + createArea( + new MapBoundsPlacer(), + heightmapPainter); + + return heightmapPainter.getScale(); +}; + +/** * Returns the ID of a texture name. * Creates a new ID if there isn't one assigned yet. */ RandomMap.prototype.getTextureID = function(texture) { if (texture in this.nameToID) return this.nameToID[texture]; let id = this.IDToName.length; this.nameToID[texture] = id; this.IDToName[id] = texture; return id; }; /** * Returns the next unused entityID. */ RandomMap.prototype.getEntityID = function() { return this.entityCount++; }; RandomMap.prototype.isCircularMap = function() { return !!g_MapSettings.CircularMap; }; RandomMap.prototype.getSize = function() { return this.size; }; /** * Returns the center tile coordinates of the map. */ RandomMap.prototype.getCenter = function() { return deepfreeze(new Vector2D(this.size / 2, this.size / 2)); }; /** * Returns a human-readable reference to the smallest and greatest coordinates of the map. */ RandomMap.prototype.getBounds = function() { return deepfreeze({ "left": 0, "right": this.size, "top": this.size, "bottom": 0 }); }; /** * Determines whether the given coordinates are within the given distance of the map area. * Should be used to restrict actor placement. * Entity placement should be checked against validTilePassable to exclude the map border. * Terrain texture changes should be tested against inMapBounds. */ RandomMap.prototype.validTile = function(position, distance = 0) { if (this.isCircularMap()) return Math.round(position.distanceTo(this.getCenter())) < this.size / 2 - distance - 1; return position.x >= distance && position.y >= distance && position.x < this.size - distance && position.y < this.size - distance; }; /** * Determines whether the given coordinates are within the given distance of the passable map area. * Should be used to restrict entity placement and path creation. */ RandomMap.prototype.validTilePassable = function(position, distance = 0) { return this.validTile(position, distance + MAP_BORDER_WIDTH); }; /** * Determines whether the given coordinates are within the tile grid, passable or not. * Should be used to restrict texture painting. */ RandomMap.prototype.inMapBounds = function(position) { return position.x >= 0 && position.y >= 0 && position.x < this.size && position.y < this.size; }; /** * Determines whether the given coordinates are within the heightmap grid. * Should be used to restrict elevation changes. */ RandomMap.prototype.validHeight = function(position) { if (position.x < 0 || position.y < 0) return false; if (TILE_CENTERED_HEIGHT_MAP) return position.x < this.size && position.y < this.size; return position.x <= this.size && position.y <= this.size; }; /** * Returns a random point on the map. * @param passableOnly - Should be true for entity placement and false for terrain or elevation operations. */ RandomMap.prototype.randomCoordinate = function(passableOnly) { let border = passableOnly ? MAP_BORDER_WIDTH : 0; if (this.isCircularMap()) // Polar coordinates // Uniformly distributed on the disk return Vector2D.add( this.getCenter(), new Vector2D((this.size / 2 - border) * Math.sqrt(randFloat(0, 1)), 0).rotate(randomAngle()).floor()); // Rectangular coordinates return new Vector2D( randIntExclusive(border, this.size - border), randIntExclusive(border, this.size - border)); }; /** * Returns the name of the texture of the given tile. */ RandomMap.prototype.getTexture = function(position) { if (!this.inMapBounds(position)) throw new Error("getTexture: invalid tile position " + uneval(position)); return this.IDToName[this.texture[position.x][position.y]]; }; /** * Paints the given texture on the given tile. */ RandomMap.prototype.setTexture = function(position, texture) { if (position.x < 0 || position.y < 0 || position.x >= this.texture.length || position.y >= this.texture[position.x].length) throw new Error("setTexture: invalid tile position " + uneval(position)); this.texture[position.x][position.y] = this.getTextureID(texture); }; RandomMap.prototype.getHeight = function(position) { if (!this.validHeight(position)) throw new Error("getHeight: invalid vertex position " + uneval(position)); return this.height[position.x][position.y]; }; RandomMap.prototype.setHeight = function(position, height) { if (!this.validHeight(position)) throw new Error("setHeight: invalid vertex position " + uneval(position)); this.height[position.x][position.y] = height; }; /** * Adds the given Entity to the map at the location it defines, even if at the impassable map border. */ RandomMap.prototype.placeEntityAnywhere = function(templateName, playerID, position, orientation) { let entity = new Entity(this.getEntityID(), templateName, playerID, position, orientation); this.entities.push(entity); }; /** * Adds the given Entity to the map at the location it defines, if that area is not at the impassable map border. */ RandomMap.prototype.placeEntityPassable = function(templateName, playerID, position, orientation) { if (this.validTilePassable(position)) this.placeEntityAnywhere(templateName, playerID, position, orientation); }; /** * Returns the Entity that was painted by a Terrain class on the given tile or undefined otherwise. */ RandomMap.prototype.getTerrainEntity = function(position) { if (!this.validTilePassable(position)) throw new Error("getTerrainEntity: invalid tile position " + uneval(position)); return this.terrainEntities[position.x][position.y]; }; /** * Places the Entity on the given tile and allows to later replace it if the terrain was painted over. */ RandomMap.prototype.setTerrainEntity = function(templateName, playerID, position, orientation) { let tilePosition = position.clone().floor(); if (!this.validTilePassable(tilePosition)) throw new Error("setTerrainEntity: invalid tile position " + uneval(position)); this.terrainEntities[tilePosition.x][tilePosition.y] = new Entity(this.getEntityID(), templateName, playerID, position, orientation); }; /** * Constructs a new Area object and informs the Map which points correspond to this area. */ RandomMap.prototype.createArea = function(points) { let areaID = ++this.areaID; for (let p of points) this.area[p.x][p.y] = areaID; return new Area(points, areaID); }; RandomMap.prototype.createTileClass = function() { return new TileClass(this.size); }; /** * Retrieve interpolated height for arbitrary coordinates within the heightmap grid. */ RandomMap.prototype.getExactHeight = function(position) { let xi = Math.min(Math.floor(position.x), this.size); let zi = Math.min(Math.floor(position.y), this.size); let xf = position.x - xi; let zf = position.y - zi; let h00 = this.height[xi][zi]; let h01 = this.height[xi][zi + 1]; let h10 = this.height[xi + 1][zi]; let h11 = this.height[xi + 1][zi + 1]; return (1 - zf) * ((1 - xf) * h00 + xf * h10) + zf * ((1 - xf) * h01 + xf * h11); }; // Converts from the tile centered height map to the corner based height map, used when TILE_CENTERED_HEIGHT_MAP = true RandomMap.prototype.cornerHeight = function(position) { let count = 0; let sumHeight = 0; for (let vertex of g_TileVertices) { let pos = Vector2D.sub(position, vertex); if (this.validHeight(pos)) { ++count; sumHeight += this.getHeight(pos); } } if (!count) return 0; return sumHeight / count; }; RandomMap.prototype.getAdjacentPoints = function(position) { let adjacentPositions = []; for (let x = -1; x <= 1; ++x) for (let z = -1; z <= 1; ++z) if (x || z ) { let adjacentPos = Vector2D.add(position, new Vector2D(x, z)).round(); if (this.inMapBounds(adjacentPos)) adjacentPositions.push(adjacentPos); } return adjacentPositions; }; /** * Returns the average height of adjacent tiles, helpful for smoothing. */ RandomMap.prototype.getAverageHeight = function(position) { let adjacentPositions = this.getAdjacentPoints(position); if (!adjacentPositions.length) return 0; return adjacentPositions.reduce((totalHeight, pos) => totalHeight + this.getHeight(pos), 0) / adjacentPositions.length; }; /** * Returns the steepness of the given location, defined as the average height difference of the adjacent tiles. */ RandomMap.prototype.getSlope = function(position) { let adjacentPositions = this.getAdjacentPoints(position); if (!adjacentPositions.length) return 0; return adjacentPositions.reduce((totalSlope, adjacentPos) => totalSlope + Math.abs(this.getHeight(adjacentPos) - this.getHeight(position)), 0) / adjacentPositions.length; }; /** * Retrieve an array of all Entities placed on the map. */ RandomMap.prototype.exportEntityList = function() { // Change rotation from simple 2d to 3d befor giving to engine for (let entity of this.entities) entity.rotation.y = Math.PI / 2 - entity.rotation.y; // Terrain objects e.g. trees for (let x = 0; x < this.size; ++x) for (let z = 0; z < this.size; ++z) if (this.terrainEntities[x][z]) this.entities.push(this.terrainEntities[x][z]); this.logger.printDirectly("Total entities: " + this.entities.length + ".\n") return this.entities; }; /** * Convert the elevation grid to a one-dimensional array. */ RandomMap.prototype.exportHeightData = function() { let heightmapSize = this.size + 1; let heightmap = new Uint16Array(Math.square(heightmapSize)); for (let x = 0; x < heightmapSize; ++x) for (let z = 0; z < heightmapSize; ++z) { let position = new Vector2D(x, z); let currentHeight = TILE_CENTERED_HEIGHT_MAP ? this.cornerHeight(position) : this.getHeight(position); // Correct height by SEA_LEVEL and prevent under/overflow in terrain data heightmap[z * heightmapSize + x] = Math.max(0, Math.min(0xFFFF, Math.floor((currentHeight + SEA_LEVEL) * HEIGHT_UNITS_PER_METRE))); } return heightmap; }; /** * Assemble terrain textures in a one-dimensional array. */ RandomMap.prototype.exportTerrainTextures = function() { let tileIndex = new Uint16Array(Math.square(this.size)); let tilePriority = new Uint16Array(Math.square(this.size)); for (let x = 0; x < this.size; ++x) for (let z = 0; z < this.size; ++z) { // TODO: For now just use the texture's index as priority, might want to do this another way tileIndex[z * this.size + x] = this.texture[x][z]; tilePriority[z * this.size + x] = this.texture[x][z]; } return { "index": tileIndex, "priority": tilePriority }; }; RandomMap.prototype.ExportMap = function() { if (g_Environment.Water.WaterBody.Height === undefined) g_Environment.Water.WaterBody.Height = SEA_LEVEL - 0.1; this.logger.close(); Engine.ExportMap({ "entities": this.exportEntityList(), "height": this.exportHeightData(), "seaLevel": SEA_LEVEL, "size": this.size, "textureNames": this.IDToName, "tileData": this.exportTerrainTextures(), "Camera": g_Camera, "Environment": g_Environment }); }; Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen2/gaia.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen2/gaia.js (revision 21132) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen2/gaia.js (revision 21133) @@ -1,1271 +1,1209 @@ var g_Props = { "barrels": "actor|props/special/eyecandy/barrels_buried.xml", "crate": "actor|props/special/eyecandy/crate_a.xml", "cart": "actor|props/special/eyecandy/handcart_1_broken.xml", "well": "actor|props/special/eyecandy/well_1_c.xml", "skeleton": "actor|props/special/eyecandy/skeleton.xml", }; /** * Create bluffs, i.e. a slope hill reachable from ground level. * Fill it with wood, mines, animals and decoratives. * * @param {Array} constraint - where to place them * @param {number} size - size of the bluffs (1.2 would be 120% of normal) * @param {number} deviation - degree of deviation from the defined size (0.2 would be 20% plus/minus) * @param {number} fill - size of map to fill (1.5 would be 150% of normal) * @param {number} baseHeight - elevation of the floor, making the bluff reachable */ function addBluffs(constraint, size, deviation, fill, baseHeight) { g_Map.log("Creating bluffs"); var constrastTerrain = g_Terrains.tier2Terrain; if (currentBiome() == "generic/tropic") constrastTerrain = g_Terrains.dirt; if (currentBiome() == "generic/autumn") constrastTerrain = g_Terrains.tier3Terrain; var count = fill * 15; var minSize = 5; var maxSize = 7; var elevation = 30; var spread = 100; for (var i = 0; i < count; ++i) { var offset = getRandomDeviation(size, deviation); var rendered = createAreas( new ChainPlacer(Math.floor(minSize * offset), Math.floor(maxSize * offset), Math.floor(spread * offset), 0.5), [ new LayeredPainter([g_Terrains.cliff, g_Terrains.mainTerrain, constrastTerrain], [2, 3]), new SmoothElevationPainter(ELEVATION_MODIFY, Math.floor(elevation * offset), 2), new TileClassPainter(g_TileClasses.bluff) ], constraint, 1); // Find the bounding box of the bluff if (rendered[0] === undefined) continue; var points = rendered[0].points; var corners = findCorners(points); // Seed an array the size of the bounding box var bb = createBoundingBox(points, corners); // Get a random starting position for the baseline and the endline var angle = randIntInclusive(0, 3); var opAngle = angle - 2; if (angle < 2) opAngle = angle + 2; // Find the edges of the bluff var baseLine; var endLine; // If we can't access the bluff, try different angles var retries = 0; var bluffCat = 2; while (bluffCat != 0 && retries < 5) { baseLine = findClearLine(bb, corners, angle, baseHeight); endLine = findClearLine(bb, corners, opAngle, baseHeight); bluffCat = unreachableBluff(bb, corners, baseLine, endLine); ++angle; if (angle > 3) angle = 0; opAngle = angle - 2; if (angle < 2) opAngle = angle + 2; ++retries; } // Inaccessible, turn it into a plateau if (bluffCat > 0) { removeBluff(points); continue; } // Create an entrance area by using a small margin var margin = 0.08; var ground = createTerrain(g_Terrains.mainTerrain); var slopeLength = (1 - margin) * Math.euclidDistance2D(baseLine.midX, baseLine.midZ, endLine.midX, endLine.midZ); // Adjust the height of each point in the bluff for (let point of points) { var dist = Math.abs(distanceOfPointFromLine( new Vector2D(baseLine.x1, baseLine.z1), new Vector2D(baseLine.x2, baseLine.z2), point)); var curHeight = g_Map.getHeight(point); var newHeight = curHeight - curHeight * (dist / slopeLength) - 2; newHeight = Math.max(newHeight, endLine.height); if (newHeight <= endLine.height + 2 && g_Map.validTile(point) && g_Map.getTexture(point).indexOf('cliff') != -1) ground.place(point); g_Map.setHeight(point, newHeight); } // Smooth out the ground around the bluff fadeToGround(bb, corners.minX, corners.minZ, endLine.height); } addElements([ { "func": addHills, "avoid": [ g_TileClasses.hill, 3, g_TileClasses.player, 20, g_TileClasses.valley, 2, g_TileClasses.water, 2 ], "stay": [g_TileClasses.bluff, 3], "sizes": g_AllSizes, "mixes": g_AllMixes, "amounts": g_AllAmounts } ]); addElements([ { "func": addLayeredPatches, "avoid": [ g_TileClasses.dirt, 5, g_TileClasses.forest, 2, g_TileClasses.mountain, 2, g_TileClasses.player, 12, g_TileClasses.water, 3 ], "stay": [g_TileClasses.bluff, 5], "sizes": ["normal"], "mixes": ["normal"], "amounts": ["normal"] } ]); addElements([ { "func": addDecoration, "avoid": [ g_TileClasses.forest, 2, g_TileClasses.player, 12, g_TileClasses.water, 3 ], "stay": [g_TileClasses.bluff, 5], "sizes": ["normal"], "mixes": ["normal"], "amounts": ["normal"] } ]); addElements([ { "func": addProps, "avoid": [ g_TileClasses.forest, 2, g_TileClasses.player, 12, g_TileClasses.prop, 40, g_TileClasses.water, 3 ], "stay": [ g_TileClasses.bluff, 7, g_TileClasses.mountain, 7 ], "sizes": ["normal"], "mixes": ["normal"], "amounts": ["scarce"] } ]); addElements(shuffleArray([ { "func": addForests, "avoid": [ g_TileClasses.berries, 5, g_TileClasses.forest, 18, g_TileClasses.metal, 5, g_TileClasses.mountain, 5, g_TileClasses.player, 20, g_TileClasses.rock, 5, g_TileClasses.water, 2 ], "stay": [g_TileClasses.bluff, 6], "sizes": g_AllSizes, "mixes": g_AllMixes, "amounts": ["normal", "many", "tons"] }, { "func": addMetal, "avoid": [ g_TileClasses.berries, 5, g_TileClasses.forest, 5, g_TileClasses.mountain, 2, g_TileClasses.player, 50, g_TileClasses.rock, 15, g_TileClasses.metal, 40, g_TileClasses.water, 3 ], "stay": [g_TileClasses.bluff, 6], "sizes": ["normal"], "mixes": ["same"], "amounts": ["normal"] }, { "func": addStone, "avoid": [ g_TileClasses.berries, 5, g_TileClasses.forest, 5, g_TileClasses.mountain, 2, g_TileClasses.player, 50, g_TileClasses.rock, 40, g_TileClasses.metal, 15, g_TileClasses.water, 3 ], "stay": [g_TileClasses.bluff, 6], "sizes": ["normal"], "mixes": ["same"], "amounts": ["normal"] } ])); let savanna = currentBiome() == "generic/savanna"; addElements(shuffleArray([ { "func": addStragglerTrees, "avoid": [ g_TileClasses.berries, 5, g_TileClasses.forest, 10, g_TileClasses.metal, 5, g_TileClasses.mountain, 1, g_TileClasses.player, 12, g_TileClasses.rock, 5, g_TileClasses.water, 5 ], "stay": [g_TileClasses.bluff, 6], "sizes": savanna ? ["big"] : g_AllSizes, "mixes": savanna ? ["varied"] : g_AllMixes, "amounts": savanna ? ["tons"] : ["normal", "many", "tons"] }, { "func": addAnimals, "avoid": [ g_TileClasses.animals, 20, g_TileClasses.forest, 5, g_TileClasses.mountain, 1, g_TileClasses.player, 20, g_TileClasses.rock, 5, g_TileClasses.metal, 5, g_TileClasses.water, 3 ], "stay": [g_TileClasses.bluff, 6], "sizes": g_AllSizes, "mixes": g_AllMixes, "amounts": ["normal", "many", "tons"] }, { "func": addBerries, "avoid": [ g_TileClasses.berries, 50, g_TileClasses.forest, 5, g_TileClasses.metal, 10, g_TileClasses.mountain, 2, g_TileClasses.player, 20, g_TileClasses.rock, 10, g_TileClasses.water, 3 ], "stay": [g_TileClasses.bluff, 6], "sizes": g_AllSizes, "mixes": g_AllMixes, "amounts": ["normal", "many", "tons"] } ])); } /** * Add grass, rocks and bushes. */ function addDecoration(constraint, size, deviation, fill) { g_Map.log("Creating decoration"); var offset = getRandomDeviation(size, deviation); var decorations = [ [ new SimpleObject(g_Decoratives.rockMedium, offset, 3 * offset, 0, offset) ], [ new SimpleObject(g_Decoratives.rockLarge, offset, 2 * offset, 0, offset), new SimpleObject(g_Decoratives.rockMedium, offset, 3 * offset, 0, 2 * offset) ], [ new SimpleObject(g_Decoratives.grassShort, offset, 2 * offset, 0, offset) ], [ new SimpleObject(g_Decoratives.grass, 2 * offset, 4 * offset, 0, 1.8 * offset), new SimpleObject(g_Decoratives.grassShort, 3 * offset, 6 * offset, 1.2 * offset, 2.5 * offset) ], [ new SimpleObject(g_Decoratives.bushMedium, offset, 2 * offset, 0, 2 * offset), new SimpleObject(g_Decoratives.bushSmall, 2 * offset, 4 * offset, 0, 2 * offset) ] ]; var baseCount = 1; if (currentBiome() == "generic/tropic") baseCount = 8; var counts = [ scaleByMapSize(16, 262), scaleByMapSize(8, 131), baseCount * scaleByMapSize(13, 200), baseCount * scaleByMapSize(13, 200), baseCount * scaleByMapSize(13, 200) ]; for (var i = 0; i < decorations.length; ++i) { var decorCount = Math.floor(counts[i] * fill); var group = new SimpleGroup(decorations[i], true); createObjectGroupsDeprecated(group, 0, constraint, decorCount, 5); } } /** * Create varying elevations. * * @param {Array} constraint - avoid/stay-classes * * @param {Object} el - the element to be rendered, for example: * "class": g_TileClasses.hill, * "painter": [g_Terrains.mainTerrain, g_Terrains.mainTerrain], * "size": 1, * "deviation": 0.2, * "fill": 1, * "count": scaleByMapSize(4, 8), * "minSize": Math.floor(scaleByMapSize(3, 8)), * "maxSize": Math.floor(scaleByMapSize(5, 10)), * "spread": Math.floor(scaleByMapSize(10, 20)), * "minElevation": 6, * "maxElevation": 12, * "steepness": 1.5 */ function addElevation(constraint, el) { var count = el.fill * el.count; var minSize = el.minSize; var maxSize = el.maxSize; var spread = el.spread; var elType = ELEVATION_MODIFY; if (el.class == g_TileClasses.water) elType = ELEVATION_SET; var widths = []; // Allow for shore and cliff rendering for (var s = el.painter.length; s > 2; --s) widths.push(1); for (var i = 0; i < count; ++i) { var elevation = randIntExclusive(el.minElevation, el.maxElevation); var smooth = Math.floor(elevation / el.steepness); var offset = getRandomDeviation(el.size, el.deviation); var pMinSize = Math.floor(minSize * offset); var pMaxSize = Math.floor(maxSize * offset); var pSpread = Math.floor(spread * offset); var pSmooth = Math.abs(Math.floor(smooth * offset)); var pElevation = Math.floor(elevation * offset); pElevation = Math.max(el.minElevation, Math.min(pElevation, el.maxElevation)); pMinSize = Math.min(pMinSize, pMaxSize); pMaxSize = Math.min(pMaxSize, el.maxSize); pMinSize = Math.max(pMaxSize, el.minSize); pSmooth = Math.max(pSmooth, 1); createAreas( new ChainPlacer(pMinSize, pMaxSize, pSpread, 0.5), [ new LayeredPainter(el.painter, [widths.concat(pSmooth)]), new SmoothElevationPainter(elType, pElevation, pSmooth), new TileClassPainter(el.class) ], constraint, 1); } } /** * Create rolling hills. */ function addHills(constraint, size, deviation, fill) { g_Map.log("Creating hills"); addElevation(constraint, { "class": g_TileClasses.hill, "painter": [g_Terrains.mainTerrain, g_Terrains.mainTerrain], "size": size, "deviation": deviation, "fill": fill, "count": 8, "minSize": 5, "maxSize": 8, "spread": 20, "minElevation": 6, "maxElevation": 12, "steepness": 1.5 }); } /** * Create random lakes with fish in it. */ function addLakes(constraint, size, deviation, fill) { g_Map.log("Creating lakes"); var lakeTile = g_Terrains.water; if (currentBiome() == "generic/temperate" || currentBiome() == "generic/tropic") lakeTile = g_Terrains.dirt; if (currentBiome() == "generic/mediterranean") lakeTile = g_Terrains.tier2Terrain; if (currentBiome() == "generic/autumn") lakeTile = g_Terrains.shore; addElevation(constraint, { "class": g_TileClasses.water, "painter": [lakeTile, lakeTile], "size": size, "deviation": deviation, "fill": fill, "count": 6, "minSize": 7, "maxSize": 9, "spread": 70, "minElevation": -15, "maxElevation": -2, "steepness": 1.5 }); addElements([ { "func": addFish, "avoid": [ g_TileClasses.fish, 12, g_TileClasses.hill, 8, g_TileClasses.mountain, 8, g_TileClasses.player, 8 ], "stay": [g_TileClasses.water, 7], "sizes": g_AllSizes, "mixes": g_AllMixes, "amounts": ["normal", "many", "tons"] } ]); var group = new SimpleGroup([new SimpleObject(g_Decoratives.rockMedium, 1, 3, 1, 3)], true, g_TileClasses.dirt); createObjectGroupsDeprecated(group, 0, [stayClasses(g_TileClasses.water, 1), borderClasses(g_TileClasses.water, 4, 3)], 1000, 100); group = new SimpleGroup([new SimpleObject(g_Decoratives.reeds, 10, 15, 1, 3), new SimpleObject(g_Decoratives.rockMedium, 1, 3, 1, 3)], true, g_TileClasses.dirt); createObjectGroupsDeprecated(group, 0, [stayClasses(g_TileClasses.water, 2), borderClasses(g_TileClasses.water, 4, 3)], 1000, 100); } /** * Universal function to create layered patches. */ function addLayeredPatches(constraint, size, deviation, fill) { g_Map.log("Creating layered patches"); var minRadius = 1; var maxRadius = Math.floor(scaleByMapSize(3, 5)); var count = fill * scaleByMapSize(15, 45); var patchSizes = [ scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21) ]; for (let patchSize of patchSizes) { var offset = getRandomDeviation(size, deviation); var patchMinRadius = Math.floor(minRadius * offset); var patchMaxRadius = Math.floor(maxRadius * offset); createAreas( new ChainPlacer(Math.min(patchMinRadius, patchMaxRadius), patchMaxRadius, Math.floor(patchSize * offset), 0.5), [ new LayeredPainter( [ [g_Terrains.mainTerrain, g_Terrains.tier1Terrain], [g_Terrains.tier1Terrain, g_Terrains.tier2Terrain], [g_Terrains.tier2Terrain, g_Terrains.tier3Terrain], [g_Terrains.tier4Terrain] ], [1, 1]), new TileClassPainter(g_TileClasses.dirt) ], constraint, count * offset); } } /** * Create steep mountains. */ function addMountains(constraint, size, deviation, fill) { g_Map.log("Creating mountains"); addElevation(constraint, { "class": g_TileClasses.mountain, "painter": [g_Terrains.cliff, g_Terrains.hill], "size": size, "deviation": deviation, "fill": fill, "count": 8, "minSize": 2, "maxSize": 4, "spread": 100, "minElevation": 100, "maxElevation": 120, "steepness": 4 }); } /** * Create plateaus. */ function addPlateaus(constraint, size, deviation, fill) { g_Map.log("Creating plateaus"); var plateauTile = g_Terrains.dirt; if (currentBiome() == "generic/snowy") plateauTile = g_Terrains.tier1Terrain; if (currentBiome() == "generic/alpine" || currentBiome() == "generic/savanna") plateauTile = g_Terrains.tier2Terrain; if (currentBiome() == "generic/autumn") plateauTile = g_Terrains.tier4Terrain; addElevation(constraint, { "class": g_TileClasses.plateau, "painter": [g_Terrains.cliff, plateauTile], "size": size, "deviation": deviation, "fill": fill, "count": 15, "minSize": 2, "maxSize": 4, "spread": 200, "minElevation": 20, "maxElevation": 30, "steepness": 8 }); for (var i = 0; i < 40; ++i) { var hillElevation = randIntInclusive(4, 18); createAreas( new ChainPlacer(3, 15, 1, 0.5), [ new LayeredPainter([plateauTile, plateauTile], [3]), new SmoothElevationPainter(ELEVATION_MODIFY, hillElevation, hillElevation - 2), new TileClassPainter(g_TileClasses.hill) ], [ avoidClasses(g_TileClasses.hill, 7), stayClasses(g_TileClasses.plateau, 7) ], 1); } addElements([ { "func": addDecoration, "avoid": [ g_TileClasses.dirt, 15, g_TileClasses.forest, 2, g_TileClasses.player, 12, g_TileClasses.water, 3 ], "stay": [g_TileClasses.plateau, 8], "sizes": ["normal"], "mixes": ["normal"], "amounts": ["tons"] }, { "func": addProps, "avoid": [ g_TileClasses.forest, 2, g_TileClasses.player, 12, g_TileClasses.prop, 40, g_TileClasses.water, 3 ], "stay": [g_TileClasses.plateau, 8], "sizes": ["normal"], "mixes": ["normal"], "amounts": ["scarce"] } ]); } /** * Place less usual decoratives like barrels or crates. */ function addProps(constraint, size, deviation, fill) { g_Map.log("Creating rare actors"); var offset = getRandomDeviation(size, deviation); var props = [ [ new SimpleObject(g_Props.skeleton, offset, 5 * offset, 0, 3 * offset + 2), ], [ new SimpleObject(g_Props.barrels, offset, 2 * offset, 2, 3 * offset + 2), new SimpleObject(g_Props.cart, 0, offset, 5, 2.5 * offset + 5), new SimpleObject(g_Props.crate, offset, 2 * offset, 2, 2 * offset + 2), new SimpleObject(g_Props.well, 0, 1, 2, 2 * offset + 2) ] ]; var baseCount = 1; var counts = [ scaleByMapSize(16, 262), scaleByMapSize(8, 131), baseCount * scaleByMapSize(13, 200), baseCount * scaleByMapSize(13, 200), baseCount * scaleByMapSize(13, 200) ]; // Add small props for (var i = 0; i < props.length; ++i) { var propCount = Math.floor(counts[i] * fill); var group = new SimpleGroup(props[i], true); createObjectGroupsDeprecated(group, 0, constraint, propCount, 5); } // Add decorative trees var trees = new SimpleObject(g_Decoratives.tree, 5 * offset, 30 * offset, 2, 3 * offset + 10); createObjectGroupsDeprecated(new SimpleGroup([trees], true), 0, constraint, counts[0] * 5 * fill, 5); } function addValleys(constraint, size, deviation, fill, baseHeight) { if (baseHeight < 6) return; g_Map.log("Creating valleys"); let minElevation = Math.max(-baseHeight, 1 - baseHeight / (size * (deviation + 1))); var valleySlope = g_Terrains.tier1Terrain; var valleyFloor = g_Terrains.tier4Terrain; if (currentBiome() == "generic/desert") { valleySlope = g_Terrains.tier3Terrain; valleyFloor = g_Terrains.dirt; } if (currentBiome() == "generic/mediterranean") { valleySlope = g_Terrains.tier2Terrain; valleyFloor = g_Terrains.dirt; } if (currentBiome() == "generic/alpine" || currentBiome() == "generic/savanna") valleyFloor = g_Terrains.tier2Terrain; if (currentBiome() == "generic/tropic") valleySlope = g_Terrains.dirt; if (currentBiome() == "generic/autumn") valleyFloor = g_Terrains.tier3Terrain; addElevation(constraint, { "class": g_TileClasses.valley, "painter": [valleySlope, valleyFloor], "size": size, "deviation": deviation, "fill": fill, "count": 8, "minSize": 5, "maxSize": 8, "spread": 30, "minElevation": minElevation, "maxElevation": -2, "steepness": 4 }); } /** * Create huntable animals. */ function addAnimals(constraint, size, deviation, fill) { g_Map.log("Creating animals"); var groupOffset = getRandomDeviation(size, deviation); var animals = [ [new SimpleObject(g_Gaia.mainHuntableAnimal, 5 * groupOffset, 7 * groupOffset, 0, 4 * groupOffset)], [new SimpleObject(g_Gaia.secondaryHuntableAnimal, 2 * groupOffset, 3 * groupOffset, 0, 2 * groupOffset)] ]; for (let animal of animals) createObjectGroupsDeprecated( new SimpleGroup(animal, true, g_TileClasses.animals), 0, constraint, Math.floor(30 * fill), 50); } function addBerries(constraint, size, deviation, fill) { g_Map.log("Creating berries"); let groupOffset = getRandomDeviation(size, deviation); createObjectGroupsDeprecated( new SimpleGroup([new SimpleObject(g_Gaia.fruitBush, 5 * groupOffset, 5 * groupOffset, 0, 3 * groupOffset)], true, g_TileClasses.berries), 0, constraint, Math.floor(50 * fill), 40); } function addFish(constraint, size, deviation, fill) { g_Map.log("Creating fish"); var groupOffset = getRandomDeviation(size, deviation); var fishes = [ [new SimpleObject(g_Gaia.fish, groupOffset, 2 * groupOffset, 0, 2 * groupOffset)], [new SimpleObject(g_Gaia.fish, 2 * groupOffset, 4 * groupOffset, 10 * groupOffset, 20 * groupOffset)] ]; for (let fish of fishes) createObjectGroupsDeprecated( new SimpleGroup(fish, true, g_TileClasses.fish), 0, constraint, Math.floor(40 * fill), 50); } function addForests(constraint, size, deviation, fill) { if (currentBiome() == "generic/savanna") return; g_Map.log("Creating forests"); let treeTypes = [ [ g_Terrains.forestFloor2 + TERRAIN_SEPARATOR + g_Gaia.tree1, g_Terrains.forestFloor2 + TERRAIN_SEPARATOR + g_Gaia.tree2, g_Terrains.forestFloor2 ], [ g_Terrains.forestFloor1 + TERRAIN_SEPARATOR + g_Gaia.tree4, g_Terrains.forestFloor1 + TERRAIN_SEPARATOR + g_Gaia.tree5, g_Terrains.forestFloor1 ] ]; let forestTypes = [ [ [g_Terrains.forestFloor2, g_Terrains.mainTerrain, treeTypes[0]], [g_Terrains.forestFloor2, treeTypes[0]] ], [ [g_Terrains.forestFloor2, g_Terrains.mainTerrain, treeTypes[1]], [g_Terrains.forestFloor1, treeTypes[1]]], [ [g_Terrains.forestFloor1, g_Terrains.mainTerrain, treeTypes[0]], [g_Terrains.forestFloor2, treeTypes[0]]], [ [g_Terrains.forestFloor1, g_Terrains.mainTerrain, treeTypes[1]], [g_Terrains.forestFloor1, treeTypes[1]] ] ]; for (let forestType of forestTypes) { let offset = getRandomDeviation(size, deviation); createAreas( new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5) * offset), Math.floor(50 * offset), 0.5), [ new LayeredPainter(forestType, [2]), new TileClassPainter(g_TileClasses.forest) ], constraint, 10 * fill); } } function addMetal(constraint, size, deviation, fill) { g_Map.log("Creating metal mines"); var offset = getRandomDeviation(size, deviation); createObjectGroupsDeprecated( new SimpleGroup([new SimpleObject(g_Gaia.metalLarge, offset, offset, 0, 4 * offset)], true, g_TileClasses.metal), 0, constraint, 1 + 20 * fill, 100); } function addSmallMetal(constraint, size, mixes, amounts) { g_Map.log("Creating small metal mines"); let deviation = getRandomDeviation(size, mixes); createObjectGroupsDeprecated( new SimpleGroup([new SimpleObject(g_Gaia.metalSmall, 2 * deviation, 5 * deviation, deviation, 3 * deviation)], true, g_TileClasses.metal), 0, constraint, 1 + 20 * amounts, 100); } /** * Create stone mines. */ function addStone(constraint, size, deviation, fill) { g_Map.log("Creating stone mines"); var offset = getRandomDeviation(size, deviation); var mines = [ [ new SimpleObject(g_Gaia.stoneSmall, 0, 2 * offset, 0, 4 * offset), new SimpleObject(g_Gaia.stoneLarge, offset, offset, 0, 4 * offset) ], [ new SimpleObject(g_Gaia.stoneSmall, 2 * offset, 5 * offset, offset, 3 * offset) ] ]; for (let mine of mines) createObjectGroupsDeprecated( new SimpleGroup(mine, true, g_TileClasses.rock), 0, constraint, 1 + 20 * fill, 100); } /** * Create straggler trees. */ function addStragglerTrees(constraint, size, deviation, fill) { g_Map.log("Creating straggler trees"); // Ensure minimum distribution on african biome if (currentBiome() == "generic/savanna") { fill = Math.max(fill, 2); size = Math.max(size, 1); } var trees = [g_Gaia.tree1, g_Gaia.tree2, g_Gaia.tree3, g_Gaia.tree4]; var treesPerPlayer = 40; var playerBonus = Math.max(1, (getNumPlayers() - 3) / 2); var offset = getRandomDeviation(size, deviation); var treeCount = treesPerPlayer * playerBonus * fill; var totalTrees = scaleByMapSize(treeCount, treeCount); var count = Math.floor(totalTrees / trees.length) * fill; var min = offset; var max = 4 * offset; var minDist = offset; var maxDist = 5 * offset; // More trees for the african biome if (currentBiome() == "generic/savanna") { min = 3 * offset; max = 5 * offset; minDist = 2 * offset + 1; maxDist = 3 * offset + 2; } for (var i = 0; i < trees.length; ++i) { var treesMax = max; // Don't clump fruit trees if (i == 2 && (currentBiome() == "generic/desert" || currentBiome() == "generic/mediterranean")) treesMax = 1; min = Math.min(min, treesMax); var group = new SimpleGroup([new SimpleObject(trees[i], min, treesMax, minDist, maxDist)], true, g_TileClasses.forest); createObjectGroupsDeprecated(group, 0, constraint, count); } } /////////// // Terrain Helpers /////////// /** * Determine if the endline of the bluff is within the tilemap. * * @returns {Number} 0 if the bluff is reachable, otherwise a positive number */ function unreachableBluff(bb, corners, baseLine, endLine) { // If we couldn't find a slope line if (typeof baseLine.midX === "undefined" || typeof endLine.midX === "undefined") return 1; // If the end points aren't on the tilemap if (!g_Map.validTile(new Vector2D(endLine.x1, endLine.z1)) && !g_Map.validTile(new Vector2D(endLine.x2, endLine.z2))) return 2; var minTilesInGroup = 1; var insideBluff = false; var outsideBluff = false; // If there aren't enough points in each row for (var x = 0; x < bb.length; ++x) { var count = 0; for (var z = 0; z < bb[x].length; ++z) { if (!bb[x][z].isFeature) continue; var valid = g_Map.validTile(new Vector2D(x + corners.minX, z + corners.minZ)); if (valid) ++count; if (!insideBluff && valid) insideBluff = true; if (outsideBluff && valid) return 3; } // We're expecting the end of the bluff if (insideBluff && count < minTilesInGroup) outsideBluff = true; } var insideBluff = false; var outsideBluff = false; // If there aren't enough points in each column for (var z = 0; z < bb[0].length; ++z) { var count = 0; for (var x = 0; x < bb.length; ++x) { if (!bb[x][z].isFeature) continue; var valid = g_Map.validTile(new Vector2D(x + corners.minX, z + corners.minZ)); if (valid) ++count; if (!insideBluff && valid) insideBluff = true; if (outsideBluff && valid) return 3; } // We're expecting the end of the bluff if (insideBluff && count < minTilesInGroup) outsideBluff = true; } // Bluff is reachable return 0; } /** * Remove the bluff class and turn it into a plateau. */ function removeBluff(points) { g_Map.log("Replacing bluff with plateau"); for (let point of points) g_TileClasses.mountain.add(point); } /** * Create an array of points the fill a bounding box around a terrain feature. */ function createBoundingBox(points, corners) { var bb = []; var width = corners.maxX - corners.minX + 1; var length = corners.maxZ - corners.minZ + 1; for (var w = 0; w < width; ++w) { bb[w] = []; for (let l = 0; l < length; ++l) bb[w][l] = { "height": g_Map.getHeight(new Vector2D(w + corners.minX, l + corners.minZ)), "isFeature": false }; } // Define the coordinates that represent the bluff for (let point of points) bb[point.x - corners.minX][point.y - corners.minZ].isFeature = true; return bb; } /** * Flattens the ground touching a terrain feature. */ function fadeToGround(bb, minX, minZ, elevation) { var ground = createTerrain(g_Terrains.mainTerrain); for (var x = 0; x < bb.length; ++x) for (var z = 0; z < bb[x].length; ++z) { if (!bb[x][z].isFeature && nextToFeature(bb, x, z)) { let position = new Vector2D(x + minX, z + minZ); g_Map.setHeight(position, g_Map.getAverageHeight(position)); ground.place(position); } } } /** * Find a 45 degree line in a bounding box that does not intersect any terrain feature. */ function findClearLine(bb, corners, angle, baseHeight) { // Angle - 0: northwest; 1: northeast; 2: southeast; 3: southwest var z = corners.maxZ; var xOffset = -1; var zOffset = -1; switch(angle) { case 1: xOffset = 1; break; case 2: xOffset = 1; zOffset = 1; z = corners.minZ; break; case 3: zOffset = 1; z = corners.minZ; break; } var clearLine = {}; for (var x = corners.minX; x <= corners.maxX; ++x) { let position2 = new Vector2D(x, z); var clear = true; while (position2.x >= corners.minX && position2.x <= corners.maxX && position2.y >= corners.minZ && position2.y <= corners.maxZ) { var bp = bb[position2.x - corners.minX][position2.y - corners.minZ]; if (bp.isFeature && g_Map.validTile(position2)) { clear = false; break; } position2.add(new Vector2D(xOffset, zOffset)); } if (clear) { var lastX = position2.x - xOffset; var lastZ = position2.y - zOffset; var midX = Math.floor((x + lastX) / 2); var midZ = Math.floor((z + lastZ) / 2); clearLine = { "x1": x, "z1": z, "x2": lastX, "z2": lastZ, "midX": midX, "midZ": midZ, "height": baseHeight }; } if (clear && (angle == 1 || angle == 2)) break; if (!clear && (angle == 0 || angle == 3)) break; } return clearLine; } /** * Returns the corners of a bounding box. */ function findCorners(points) { // Find the bounding box of the terrain feature var mapSize = g_Map.getSize(); var minX = mapSize + 1; var minZ = mapSize + 1; var maxX = -1; var maxZ = -1; for (let point of points) { minX = Math.min(point.x, minX); minZ = Math.min(point.y, minZ); maxX = Math.max(point.x, maxX); maxZ = Math.max(point.y, maxZ); } return { "minX": minX, "minZ": minZ, "maxX": maxX, "maxZ": maxZ }; } /** * Determines if a point in a bounding box array is next to a terrain feature. */ function nextToFeature(bb, x, z) { for (var xOffset = -1; xOffset <= 1; ++xOffset) for (var zOffset = -1; zOffset <= 1; ++zOffset) { var thisX = x + xOffset; var thisZ = z + zOffset; if (thisX < 0 || thisX >= bb.length || thisZ < 0 || thisZ >= bb[x].length || thisX == 0 && thisZ == 0) continue; if (bb[thisX][thisZ].isFeature) return true; } return false; } /** * Returns a number within a random deviation of a base number. */ function getRandomDeviation(base, deviation) { return base + randFloat(-1, 1) * Math.min(base, deviation); } - -/** - * Import a given digital elevation model. - * Scale it to the mapsize and paint the textures specified by coordinate on it. - * - * @return the ratio of heightmap tiles per map size tiles - */ -function paintHeightmap(mapName, func = undefined) -{ - /** - * @property heightmap - An array with a square number of heights. - * @property tilemap - The IDs of the palletmap to be painted for each heightmap tile. - * @property pallet - The tile texture names used by the tilemap. - */ - let mapData = Engine.ReadJSONFile("maps/random/" + mapName + ".hmap"); - - let mapSize = g_Map.getSize(); // Width of the map in terrain tiles - let hmSize = Math.sqrt(mapData.heightmap.length); - let scale = hmSize / (mapSize + 1); // There are mapSize + 1 vertices (each 1 tile is surrounded by 2x2 vertices) - - for (let x = 0; x <= mapSize; ++x) - for (let y = 0; y <= mapSize; ++y) - { - let position = new Vector2D(x, y); - let hmPoint = Vector2D.mult(position, scale); - let hmTile = new Vector2D(Math.floor(hmPoint.x), Math.floor(hmPoint.y)); - let shift = new Vector2D(0, 0); - - if (hmTile.x == 0) - shift.x = 1; - else if (hmTile.x == hmSize - 1) - shift.x = - 2; - else if (hmTile.x == hmSize - 2) - shift.x = - 1; - - if (hmTile.y == 0) - shift.y = 1; - else if (hmTile.y == hmSize - 1) - shift.y = - 2; - else if (hmTile.y == hmSize - 2) - shift.y = - 1; - - let neighbors = []; - for (let localXi = 0; localXi < 4; ++localXi) - for (let localYi = 0; localYi < 4; ++localYi) - neighbors.push(mapData.heightmap[(hmTile.x + localXi + shift.x - 1) * hmSize + (hmTile.y + localYi + shift.y - 1)]); - - g_Map.setHeight(position, bicubicInterpolation(hmPoint.x - hmTile.x - shift.x, hmPoint.y - hmTile.y - shift.y, ...neighbors) / scale); - - if (x < mapSize && y < mapSize) - { - let i = hmTile.x * hmSize + hmTile.y; - let tile = mapData.pallet[mapData.tilemap[i]]; - createTerrain(tile).place(position); - - if (func) - func(tile, x, y); - } - } - - return scale; -}