Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js (revision 21226) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js (revision 21227) @@ -1,256 +1,269 @@ const TERRAIN_SEPARATOR = "|"; const SEA_LEVEL = 20.0; const HEIGHT_UNITS_PER_METRE = 92; /** * Number of impassable, unexplorable tiles at the map border. */ const MAP_BORDER_WIDTH = 3; const g_DamageTypes = new DamageTypes(); /** * Constants needed for heightmap_manipulation.js */ const MAX_HEIGHT_RANGE = 0xFFFF / HEIGHT_UNITS_PER_METRE; // Engine limit, Roughly 700 meters const MIN_HEIGHT = - SEA_LEVEL; /** * Length of one tile of the terrain grid in metres. * Useful to transform footprint sizes of templates to the coordinate system used by getMapSize. */ const TERRAIN_TILE_SIZE = Engine.GetTerrainTileSize(); const MAX_HEIGHT = MAX_HEIGHT_RANGE - SEA_LEVEL; /** * Default angle for buildings. */ const BUILDING_ORIENTATION = -1/4 * Math.PI; const g_CivData = deepfreeze(loadCivFiles(false)); /** * Sets whether setHeight operates on the center of a tile or on the vertices. */ var TILE_CENTERED_HEIGHT_MAP = false; function fractionToTiles(f) { return g_MapSettings.Size * f; } function tilesToFraction(t) { return t / g_MapSettings.Size; } function scaleByMapSize(min, max, minMapSize = 128, maxMapSize = 512) { return min + (max - min) * (g_MapSettings.Size - minMapSize) / (maxMapSize - minMapSize); } function randomPositionOnTile(tilePosition) { return Vector2D.add(tilePosition, new Vector2D(randFloat(0, 1), randFloat(0, 1))); } /** * Retries the given function with those arguments as often as specified. */ function retryPlacing(placeFunc, retryFactor, amount, getResult, behaveDeprecated = false) { let maxFail = amount * retryFactor; let results = []; let good = 0; let bad = 0; while (good < amount && bad <= maxFail) { let result = placeFunc(); if (result !== undefined || behaveDeprecated) { ++good; if (getResult) results.push(result); } else ++bad; } return getResult ? results : good; } // TODO this is a hack to simulate the old behaviour of those functions // until all old maps are changed to use the correct version of these functions function createObjectGroupsDeprecated(group, player, constraint, amount, retryFactor = 10) { return createObjectGroups(group, player, constraint, amount, retryFactor, true); } function createObjectGroupsByAreasDeprecated(group, player, constraint, amount, retryFactor, areas) { return createObjectGroupsByAreas(group, player, constraint, amount, retryFactor, areas, true); } /** * Attempts to place the given number of areas in random places of the map. * Returns actually placed areas. */ function createAreas(centeredPlacer, painter, constraint, amount, retryFactor = 10) { let placeFunc = function() { centeredPlacer.setCenterPosition(g_Map.randomCoordinate(false)); return createArea(centeredPlacer, painter, constraint); }; return retryPlacing(placeFunc, retryFactor, amount, true, false); } /** * Attempts to place the given number of areas in random places of the given areas. * Returns actually placed areas. */ function createAreasInAreas(centeredPlacer, painter, constraint, amount, retryFactor, areas) { let placeFunc = function() { centeredPlacer.setCenterPosition(pickRandom(pickRandom(areas).points)); return createArea(centeredPlacer, painter, constraint); }; return retryPlacing(placeFunc, retryFactor, amount, true, false); } /** * Attempts to place the given number of groups in random places of the map. * Returns the number of actually placed groups. */ function createObjectGroups(group, player, constraint, amount, retryFactor = 10, behaveDeprecated = false) { let placeFunc = function() { group.setCenterPosition(g_Map.randomCoordinate(true)); return createObjectGroup(group, player, constraint); }; return retryPlacing(placeFunc, retryFactor, amount, false, behaveDeprecated); } /** * Attempts to place the given number of groups in random places of the given areas. * Returns the number of actually placed groups. */ function createObjectGroupsByAreas(group, player, constraint, amount, retryFactor, areas, behaveDeprecated = false) { let placeFunc = function() { group.setCenterPosition(pickRandom(pickRandom(areas).points)); return createObjectGroup(group, player, constraint); }; return retryPlacing(placeFunc, retryFactor, amount, false, behaveDeprecated); } function createTerrain(terrain) { return typeof terrain == "string" ? new SimpleTerrain(...terrain.split(TERRAIN_SEPARATOR)) : new RandomTerrain(terrain.map(t => createTerrain(t))); } /** * Constructs a new Area shaped by the Placer meeting the Constraints and calls the Painters there. * Supports both Centered and Non-Centered Placers. */ function createArea(placer, painters, constraints) { let points = placer.place(new AndConstraint(constraints)); if (!points) return undefined; let area = g_Map.createArea(points); new MultiPainter(painters).paint(area); return area; } /** * @param mode is one of the HeightPlacer constants determining whether to exclude the min/max elevation. */ function paintTerrainBasedOnHeight(minHeight, maxHeight, mode, terrain) { createArea( new HeightPlacer(mode, minHeight, maxHeight), new TerrainPainter(terrain)); } function paintTileClassBasedOnHeight(minHeight, maxHeight, mode, tileClass) { createArea( new HeightPlacer(mode, minHeight, maxHeight), new TileClassPainter(tileClass)); } function unPaintTileClassBasedOnHeight(minHeight, maxHeight, mode, tileClass) { createArea( new HeightPlacer(mode, minHeight, maxHeight), new TileClassUnPainter(tileClass)); } /** * Places the Entities of the given Group if they meet the Constraints * and sets the given player as the owner. */ function createObjectGroup(group, player, constraints) { return group.place(player, new AndConstraint(constraints)); } /** * Create an avoid constraint for the given classes by the given distances */ function avoidClasses(/*class1, dist1, class2, dist2, etc*/) { let ar = []; for (let i = 0; i < arguments.length/2; ++i) ar.push(new AvoidTileClassConstraint(arguments[2*i], arguments[2*i+1])); // Return single constraint if (ar.length == 1) return ar[0]; return new AndConstraint(ar); } /** * Create a stay constraint for the given classes by the given distances */ function stayClasses(/*class1, dist1, class2, dist2, etc*/) { let ar = []; for (let i = 0; i < arguments.length/2; ++i) ar.push(new StayInTileClassConstraint(arguments[2*i], arguments[2*i+1])); // Return single constraint if (ar.length == 1) return ar[0]; return new AndConstraint(ar); } /** * Create a border constraint for the given classes by the given distances */ function borderClasses(/*class1, idist1, odist1, class2, idist2, odist2, etc*/) { let ar = []; for (let i = 0; i < arguments.length/3; ++i) ar.push(new BorderTileClassConstraint(arguments[3*i], arguments[3*i+1], arguments[3*i+2])); // Return single constraint if (ar.length == 1) return ar[0]; return new AndConstraint(ar); } + +function convertHeightmap1Dto2D(heightmap) +{ + let result = []; + let hmSize = Math.sqrt(heightmap.length); + for (let x = 0; x < hmSize; ++x) + { + result[x] = new Float32Array(hmSize); + for (let y = 0; y < hmSize; ++y) + result[x][y] = heightmap[y * hmSize + x]; + } + return result; +} Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js (revision 21226) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js (revision 21227) @@ -1,524 +1,524 @@ /** * @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); } }); }; /** * Applies smoothing to the given area using Inverse-Distance-Weighting / Shepard's method. * * @param {Number} size - Determines the number of neighboring heights to interpolate. The area is a square with the length twice this size. * @param {Number} strength - Between 0 (no effect) and 1 (only neighbor heights count). This parameter has the lowest performance impact. * @param {Number} iterations - How often the process should be repeated. Typically 1. Can be used to gain even more smoothing. */ function SmoothingPainter(size, strength, iterations) { if (size < 1) throw new Error("Invalid size: " + size); if (strength <= 0 || strength > 1) throw new Error("Invalid strength: " + strength); if (iterations <= 0) throw new Error("Invalid iterations: " + iterations); this.size = Math.floor(size); this.strength = strength; this.iterations = iterations; } SmoothingPainter.prototype.paint = function(area) { let brushPoints = getPointsInBoundingBox(getBoundingBox( new Array(2).fill(0).map((zero, i) => new Vector2D(1, 1).mult(this.size).mult(i ? 1 : -1)))); for (let i = 0; i < this.iterations; ++i) { let heightmap = clone(g_Map.height); // Additional complexity to process all 4 vertices of each tile, i.e the last row too let seen = new Array(heightmap.length).fill(0).map(zero => new Uint8Array(heightmap.length).fill(0)); for (let point of area.points) for (let tileVertex of g_TileVertices) { let vertex = Vector2D.add(point, tileVertex); if (!g_Map.validHeight(vertex) || seen[vertex.x][vertex.y]) continue; seen[vertex.x][vertex.y] = 1; let sumWeightedHeights = 0; let sumWeights = 0; for (let brushPoint of brushPoints) { let position = Vector2D.add(vertex, brushPoint); let distance = Math.abs(brushPoint.x) + Math.abs(brushPoint.y); if (!distance || !g_Map.validHeight(position)) continue; sumWeightedHeights += g_Map.getHeight(position) / distance; sumWeights += 1 / distance; } g_Map.setHeight( vertex, this.strength * sumWeightedHeights / sumWeights + (1 - this.strength) * g_Map.getHeight(vertex)); } } }; /** * 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.validHeight(position)) g_Map.setHeight(position, this.elevation); } }; /** * Sets a random elevation of the given heightrange in the given Area. */ function RandomElevationPainter(minHeight, maxHeight) { this.minHeight = minHeight; this.maxHeight = maxHeight; } RandomElevationPainter.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.validHeight(position)) g_Map.setHeight(position, randFloat(this.minHeight, this.maxHeight)); } }; /** * The ElevationBlendingPainter sets the elevation of each point of the given area to the weighted targetHeight. */ function ElevationBlendingPainter(targetHeight, strength) { this.targetHeight = targetHeight; this.strength = strength; } ElevationBlendingPainter.prototype.paint = function(area) { for (let point of area.points) g_Map.setHeight(point, this.strength * this.targetHeight + (1 - this.strength) * g_Map.getHeight(point)); }; /** * 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 applies 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, normalMinHeight = undefined, normalMaxHeight = undefined) { this.heightmap = heightmap; this.bicubicInterpolation = bicubicInterpolation; - this.verticesPerSide = Math.sqrt(heightmap.length); + this.verticesPerSide = heightmap.length; this.normalMinHeight = normalMinHeight; this.normalMaxHeight = normalMaxHeight; } 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.paint = 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.height.length).fill(0).map(zero => new Uint8Array(g_Map.height.length).fill(0)); for (let point of area.points) for (let vertex of g_TileVertices) { let vertexPos = Vector2D.add(point, vertex); if (!g_Map.validHeight(vertexPos) || seen[vertexPos.x][vertexPos.y]) continue; seen[vertexPos.x][vertexPos.y] = 1; 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])))); + this.scaleHeight(this.heightmap[pos.x][pos.y])))); } }; Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/player.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/player.js (revision 21226) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/player.js (revision 21227) @@ -1,788 +1,788 @@ /** * @file These functions locate and place the starting entities of players. */ var g_NomadTreasureTemplates = { "food": "gaia/treasure/food_jars", "wood": "gaia/treasure/wood", "stone": "gaia/treasure/stone", "metal": "gaia/treasure/metal" }; /** * These are identifiers of functions that can generate parts of a player base. * There must be a function starting with placePlayerBase and ending with this name. * This is a global so mods can extend this from external files. */ var g_PlayerBaseFunctions = [ // Possibly mark player class first here and use it afterwards "CityPatch", // Create the largest and most important entities first "Trees", "Mines", "Treasures", "Berries", "Chicken", "Decoratives" ]; function isNomad() { return !!g_MapSettings.Nomad; } function getNumPlayers() { return g_MapSettings.PlayerData.length - 1; } function getCivCode(playerID) { return g_MapSettings.PlayerData[playerID].Civ; } function areAllies(playerID1, playerID2) { return g_MapSettings.PlayerData[playerID1].Team !== undefined && g_MapSettings.PlayerData[playerID2].Team !== undefined && g_MapSettings.PlayerData[playerID1].Team != -1 && g_MapSettings.PlayerData[playerID2].Team != -1 && g_MapSettings.PlayerData[playerID1].Team === g_MapSettings.PlayerData[playerID2].Team; } function getPlayerTeam(playerID) { if (g_MapSettings.PlayerData[playerID].Team === undefined) return -1; return g_MapSettings.PlayerData[playerID].Team; } /** * Gets the default starting entities for the civ of the given player, as defined by the civ file. */ function getStartingEntities(playerID) { return g_CivData[getCivCode(playerID)].StartEntities; } /** * Places the given entities at the given location (typically a civic center and starting units). * @param location - A Vector2D specifying tile coordinates. * @param civEntities - An array of objects with the Template property and optionally a Count property. * The first entity is placed in the center, the other ones surround it. */ function placeStartingEntities(location, playerID, civEntities, dist = 6, orientation = BUILDING_ORIENTATION) { // Place the central structure let i = 0; let firstTemplate = civEntities[i].Template; if (firstTemplate.startsWith("structures/")) { g_Map.placeEntityPassable(firstTemplate, playerID, location, orientation); ++i; } // Place entities surrounding it let space = 2; for (let j = i; j < civEntities.length; ++j) { let angle = orientation - Math.PI * (1 - j / 2); let count = civEntities[j].Count || 1; for (let num = 0; num < count; ++num) { let position = Vector2D.sum([ location, new Vector2D(dist, 0).rotate(-angle), new Vector2D(space * (-num + 0.75 * Math.floor(count / 2)), 0).rotate(angle) ]); g_Map.placeEntityPassable(civEntities[j].Template, playerID, position, angle); } } } /** * Places the default starting entities as defined by the civilization definition, optionally including city walls. */ function placeCivDefaultStartingEntities(position, playerID, wallType, dist = 6, orientation = BUILDING_ORIENTATION) { placeStartingEntities(position, playerID, getStartingEntities(playerID), dist, orientation); placeStartingWalls(position, playerID, wallType, orientation); } /** * If the map is large enough and the civilization defines them, places the initial city walls or towers. * @param {string|boolean} wallType - Either "towers" to only place the wall turrets or a boolean indicating enclosing city walls. */ function placeStartingWalls(position, playerID, wallType, orientation = BUILDING_ORIENTATION) { let civ = getCivCode(playerID); if (civ != "iber" || g_Map.getSize() <= 128) return; if (wallType == "towers") placePolygonalWall(position, 15, ["entry"], "tower", civ, playerID, orientation, 7); else if (wallType) placeGenericFortress(position, 20, playerID); } /** * Places the civic center and starting resources for all given players. */ function placePlayerBases(playerBaseArgs) { g_Map.log("Creating playerbases"); let [playerIDs, playerPosition] = playerBaseArgs.PlayerPlacement; for (let i = 0; i < getNumPlayers(); ++i) { playerBaseArgs.playerID = playerIDs[i]; playerBaseArgs.playerPosition = playerPosition[i]; placePlayerBase(playerBaseArgs); } } /** * Places the civic center and starting resources. */ function placePlayerBase(playerBaseArgs) { if (isNomad()) return; placeCivDefaultStartingEntities(playerBaseArgs.playerPosition, playerBaseArgs.playerID, playerBaseArgs.Walls !== undefined ? playerBaseArgs.Walls : true); if (playerBaseArgs.PlayerTileClass) addCivicCenterAreaToClass(playerBaseArgs.playerPosition, playerBaseArgs.PlayerTileClass); for (let functionID of g_PlayerBaseFunctions) { let funcName = "placePlayerBase" + functionID; let func = global[funcName]; if (!func) throw new Error("Could not find " + funcName); if (!playerBaseArgs[functionID]) continue; let args = playerBaseArgs[functionID]; // Copy some global arguments to the arguments for each function for (let prop of ["playerID", "playerPosition", "BaseResourceClass", "baseResourceConstraint"]) args[prop] = playerBaseArgs[prop]; func(args); } } function defaultPlayerBaseRadius() { return scaleByMapSize(15, 25); } /** * Marks the corner and center tiles of an area that is about the size of a Civic Center with the given TileClass. * Used to prevent resource collisions with the Civic Center. */ function addCivicCenterAreaToClass(position, tileClass) { createArea( - new ClumpPlacer(diskArea(5), 1, 1, Infinity, position), + new DiskPlacer(5, position), new TileClassPainter(tileClass)); } /** * Helper function. */ function getPlayerBaseArgs(playerBaseArgs) { let baseResourceConstraint = playerBaseArgs.BaseResourceClass && avoidClasses(playerBaseArgs.BaseResourceClass, 4); if (playerBaseArgs.baseResourceConstraint) baseResourceConstraint = new AndConstraint([baseResourceConstraint, playerBaseArgs.baseResourceConstraint]); return [ (property, defaultVal) => playerBaseArgs[property] === undefined ? defaultVal : playerBaseArgs[property], playerBaseArgs.playerPosition, baseResourceConstraint ]; } function placePlayerBaseCityPatch(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); let painters = []; if (args.outerTerrain && args.innerTerrain) painters.push(new LayeredPainter([args.outerTerrain, args.innerTerrain], [get("width", 1)])); if (args.painters) painters = painters.concat(args.painters); createArea( new ClumpPlacer( Math.floor(diskArea(get("radius", defaultPlayerBaseRadius() / 3))), get("coherence", 0.6), get("smoothness", 0.3), get("failFraction", Infinity), basePosition), painters); } function placePlayerBaseChicken(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); for (let i = 0; i < get("groupCount", 2); ++i) { let success = false; for (let tries = 0; tries < get("maxTries", 30); ++tries) { let position = new Vector2D(0, get("distance", 9)).rotate(randomAngle()).add(basePosition); if (createObjectGroup( new SimpleGroup( [ new SimpleObject( get("template", "gaia/fauna_chicken"), get("minGroupCount", 5), get("maxGroupCount", 5), get("minGroupDistance", 0), get("maxGroupDistance", 2)) ], true, args.BaseResourceClass, position), 0, baseResourceConstraint)) { success = true; break; } } if (!success) { error("Could not place chicken for player " + args.playerID); return; } } } function placePlayerBaseBerries(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); for (let tries = 0; tries < get("maxTries", 30); ++tries) { let position = new Vector2D(0, get("distance", 12)).rotate(randomAngle()).add(basePosition); if (createObjectGroup( new SimpleGroup( [new SimpleObject(args.template, get("minCount", 5), get("maxCount", 5), get("maxDist", 1), get("maxDist", 3))], true, args.BaseResourceClass, position), 0, baseResourceConstraint)) return; } error("Could not place berries for player " + args.playerID); } function placePlayerBaseMines(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); let angleBetweenMines = randFloat(get("minAngle", Math.PI / 6), get("maxAngle", Math.PI / 3)); let mineCount = args.types.length; let groupElements = []; if (args.groupElements) groupElements = groupElements.concat(args.groupElements); for (let tries = 0; tries < get("maxTries", 75); ++tries) { // First find a place where all mines can be placed let pos = []; let startAngle = randomAngle(); for (let i = 0; i < mineCount; ++i) { let angle = startAngle + angleBetweenMines * (i + (mineCount - 1) / 2); pos[i] = new Vector2D(0, get("distance", 12)).rotate(angle).add(basePosition).round(); if (!g_Map.validTilePassable(pos[i]) || !baseResourceConstraint.allows(pos[i])) { pos = undefined; break; } } if (!pos) continue; // Place the mines for (let i = 0; i < mineCount; ++i) { if (args.types[i].type && args.types[i].type == "stone_formation") { createStoneMineFormation(pos[i], args.types[i].template, args.types[i].terrain); args.BaseResourceClass.add(pos[i]); continue; } createObjectGroup( new SimpleGroup( [new SimpleObject(args.types[i].template, 1, 1, 0, 0)].concat(groupElements), true, args.BaseResourceClass, pos[i]), 0); } return; } error("Could not place mines for player " + args.playerID); } function placePlayerBaseTrees(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); let num = Math.floor(get("count", scaleByMapSize(7, 20))); for (let x = 0; x < get("maxTries", 30); ++x) { let position = new Vector2D(0, randFloat(get("minDist", 11), get("maxDist", 13))).rotate(randomAngle()).add(basePosition).round(); if (createObjectGroup( new SimpleGroup( [new SimpleObject(args.template, num, num, get("minDistGroup", 0), get("maxDistGroup", 5))], false, args.BaseResourceClass, position), 0, baseResourceConstraint)) return; } error("Could not place starting trees for player " + args.playerID); } function placePlayerBaseTreasures(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); for (let resourceTypeArgs of args.types) { get = (property, defaultVal) => resourceTypeArgs[property] === undefined ? defaultVal : resourceTypeArgs[property]; let success = false; for (let tries = 0; tries < get("maxTries", 30); ++tries) { let position = new Vector2D(0, randFloat(get("minDist", 11), get("maxDist", 13))).rotate(randomAngle()).add(basePosition).round(); if (createObjectGroup( new SimpleGroup( [new SimpleObject(resourceTypeArgs.template, get("count", 14), get("count", 14), get("minDistGroup", 1), get("maxDistGroup", 3))], false, args.BaseResourceClass, position), 0, baseResourceConstraint)) { success = true; break; } } if (!success) { error("Could not place treasure " + resourceTypeArgs.template + " for player " + args.playerID); return; } } } /** * Typically used for placing grass tufts around the civic centers. */ function placePlayerBaseDecoratives(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); for (let i = 0; i < get("count", scaleByMapSize(2, 5)); ++i) { let success = false; for (let x = 0; x < get("maxTries", 30); ++x) { let position = new Vector2D(0, randIntInclusive(get("minDist", 8), get("maxDist", 11))).rotate(randomAngle()).add(basePosition).round(); if (createObjectGroup( new SimpleGroup( [new SimpleObject(args.template, get("minCount", 2), get("maxCount", 5), 0, 1)], false, args.BaseResourceClass, position), 0, baseResourceConstraint)) { success = true; break; } } if (!success) // Don't warn since the decoratives are not important return; } } function placePlayersNomad(playerClass, constraints) { if (!isNomad()) return undefined; g_Map.log("Placing nomad starting units"); let distance = scaleByMapSize(60, 240); let constraint = new AndConstraint(constraints); let numPlayers = getNumPlayers(); let playerIDs = shuffleArray(sortAllPlayers()); let playerPosition = []; for (let i = 0; i < numPlayers; ++i) { let objects = getStartingEntities(playerIDs[i]).filter(ents => ents.Template.startsWith("units/")).map( ents => new SimpleObject(ents.Template, ents.Count || 1, ents.Count || 1, 1, 3)); // Add treasure if too few resources for a civic center let ccCost = Engine.GetTemplate("structures/" + getCivCode(playerIDs[i]) + "_civil_centre").Cost.Resources; for (let resourceType in ccCost) { let treasureTemplate = g_NomadTreasureTemplates[resourceType]; let count = Math.max(0, Math.ceil( (ccCost[resourceType] - (g_MapSettings.StartingResources || 0)) / Engine.GetTemplate(treasureTemplate).ResourceSupply.Amount)); objects.push(new SimpleObject(treasureTemplate, count, count, 3, 5)); } // Try place these entities at a random location let group = new SimpleGroup(objects, true, playerClass); let success = false; for (let distanceFactor of [1, 1/2, 1/4, 0]) { if (createObjectGroups(group, playerIDs[i], new AndConstraint([constraint, avoidClasses(playerClass, distance * distanceFactor)]), 1, 200, false)) { success = true; playerPosition[i] = group.centerPosition; break; } } if (!success) throw new Error("Could not place starting units for player " + playerIDs[i] + "!"); } return [playerIDs, playerPosition]; } /** * Sorts an array of player IDs by team index. Players without teams come first. * Randomize order for players of the same team. */ function sortPlayers(playerIDs) { return shuffleArray(playerIDs).sort((playerID1, playerID2) => getPlayerTeam(playerID1) - getPlayerTeam(playerID2)); } /** * Randomize playerIDs but sort by team. * * @returns {Array} - every item is an array of player indices */ function sortAllPlayers() { let playerIDs = []; for (let i = 0; i < getNumPlayers(); ++i) playerIDs.push(i+1); return sortPlayers(playerIDs); } /** * Rearrange order so that teams of neighboring players alternate (if the given IDs are sorted by team). */ function primeSortPlayers(playerIDs) { if (!playerIDs.length) return []; let prime = []; for (let i = 0; i < Math.ceil(playerIDs.length / 2); ++i) { prime.push(playerIDs[i]); prime.push(playerIDs[playerIDs.length - 1 - i]); } return prime; } function primeSortAllPlayers() { return primeSortPlayers(sortAllPlayers()); } /** * Determine player starting positions on a circular pattern. */ function playerPlacementCircle(radius, startingAngle = undefined, center = undefined) { let startAngle = startingAngle !== undefined ? startingAngle : randomAngle(); let [playerPosition, playerAngle] = distributePointsOnCircle(getNumPlayers(), startAngle, radius, center || g_Map.getCenter()); return [sortAllPlayers(), playerPosition.map(p => p.round()), playerAngle, startAngle]; } /** * Determine player starting positions on a circular pattern, with a custom angle for each player. * Commonly used for gulf terrains. */ function playerPlacementCustomAngle(radius, center, playerAngleFunc) { let playerPosition = []; let playerAngle = []; let numPlayers = getNumPlayers(); for (let i = 0; i < numPlayers; ++i) { playerAngle[i] = playerAngleFunc(i); playerPosition[i] = Vector2D.add(center, new Vector2D(radius, 0).rotate(-playerAngle[i])).round(); } return [playerPosition, playerAngle]; } /** * Returns player starting positions located on two parallel lines, typically used by central river maps. * If there are two teams with an equal number of players, each team will occupy exactly one line. * Angle 0 means the players are placed in north to south direction, i.e. along the Z axis. */ function playerPlacementRiver(angle, width, center = undefined) { let numPlayers = getNumPlayers(); let numPlayersEven = numPlayers % 2 == 0; let mapSize = g_Map.getSize(); let centerPosition = center || g_Map.getCenter(); let playerPosition = []; for (let i = 0; i < numPlayers; ++i) { let currentPlayerEven = i % 2 == 0; let offsetDivident = numPlayersEven || currentPlayerEven ? (i + 1) % 2 : 0; let offsetDivisor = numPlayersEven ? 0 : currentPlayerEven ? +1 : -1; playerPosition[i] = new Vector2D( width * (i % 2) + (mapSize - width) / 2, fractionToTiles(((i - 1 + offsetDivident) / 2 + 1) / ((numPlayers + offsetDivisor) / 2 + 1)) ).rotateAround(angle, centerPosition).round(); } return [primeSortAllPlayers(), playerPosition]; } /*** * Returns starting positions located on two parallel lines. * The locations on the first line are shifted in comparison to the other line. */ function playerPlacementLine(angle, center, width) { let playerPosition = []; let numPlayers = getNumPlayers(); for (let i = 0; i < numPlayers; ++i) playerPosition[i] = Vector2D.add( center, new Vector2D( fractionToTiles((i + 1) / (numPlayers + 1) - 0.5), width * (i % 2 - 1/2) ).rotate(angle) ).round(); return playerPosition; } /** * Returns a random location for each player that meets the given constraints and * orders the playerIDs so that players become grouped by team. */ function playerPlacementRandom(playerIDs, constraints = undefined) { let locations = []; let attempts = 0; let resets = 0; let mapCenter = g_Map.getCenter(); let playerMinDist = fractionToTiles(0.25); let borderDistance = fractionToTiles(0.08); let area = createArea(new MapBoundsPlacer(), undefined, new AndConstraint(constraints)); for (let i = 0; i < getNumPlayers(); ++i) { let position = pickRandom(area.points); // Minimum distance between initial bases must be a quarter of the map diameter if (locations.some(loc => loc.distanceTo(position) < playerMinDist) || position.distanceTo(mapCenter) > mapCenter.x - borderDistance) { --i; ++attempts; // Reset if we're in what looks like an infinite loop if (attempts > 500) { locations = []; i = -1; attempts = 0; ++resets; // Reduce minimum player distance progressively if (resets % 25 == 0) playerMinDist *= 0.975; // If we only pick bad locations, stop trying to place randomly if (resets == 500) return undefined; } continue; } locations[i] = position; } return groupPlayersByArea(playerIDs, locations); } /** * Pick locations from the given set so that teams end up grouped. */ function groupPlayersByArea(playerIDs, locations) { playerIDs = sortPlayers(playerIDs); let minDist = Infinity; let minLocations; // Of all permutations of starting locations, find the one where // the sum of the distances between allies is minimal, weighted by teamsize. heapsPermute(shuffleArray(locations).slice(0, playerIDs.length), v => v.clone(), permutation => { let dist = 0; let teamDist = 0; let teamSize = 0; for (let i = 1; i < playerIDs.length; ++i) { let team1 = getPlayerTeam(playerIDs[i - 1]); let team2 = getPlayerTeam(playerIDs[i]); ++teamSize; if (team1 != -1 && team1 == team2) teamDist += permutation[i - 1].distanceTo(permutation[i]); else { dist += teamDist / teamSize; teamDist = 0; teamSize = 0; } } if (teamSize) dist += teamDist / teamSize; if (dist < minDist) { minDist = dist; minLocations = permutation; } }); return [playerIDs, minLocations]; } /** * Sorts the playerIDs so that team members are as close as possible on a ring. */ function groupPlayersCycle(startLocations) { let startLocationOrder = sortPointsShortestCycle(startLocations); let newStartLocations = []; for (let i = 0; i < startLocations.length; ++i) newStartLocations.push(startLocations[startLocationOrder[i]]); startLocations = newStartLocations; // Sort players by team let playerIDs = []; let teams = []; for (let i = 0; i < g_MapSettings.PlayerData.length - 1; ++i) { playerIDs.push(i+1); let t = g_MapSettings.PlayerData[i + 1].Team; if (teams.indexOf(t) == -1 && t !== undefined) teams.push(t); } playerIDs = sortPlayers(playerIDs); if (!teams.length) return [playerIDs, startLocations]; // Minimize maximum distance between players within a team let minDistance = Infinity; let bestShift; for (let s = 0; s < playerIDs.length; ++s) { let maxTeamDist = 0; for (let pi = 0; pi < playerIDs.length - 1; ++pi) { let t1 = getPlayerTeam(playerIDs[(pi + s) % playerIDs.length]); if (teams.indexOf(t1) === -1) continue; for (let pj = pi + 1; pj < playerIDs.length; ++pj) { if (t1 != getPlayerTeam(playerIDs[(pj + s) % playerIDs.length])) continue; maxTeamDist = Math.max( maxTeamDist, Math.euclidDistance2D( startLocations[pi].x, startLocations[pi].y, startLocations[pj].x, startLocations[pj].y)); } } if (maxTeamDist < minDistance) { minDistance = maxTeamDist; bestShift = s; } } if (bestShift) { let newPlayerIDs = []; for (let i = 0; i < playerIDs.length; ++i) newPlayerIDs.push(playerIDs[(i + bestShift) % playerIDs.length]); playerIDs = newPlayerIDs; } return [playerIDs, startLocations]; } 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 21226) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/random_map.js (revision 21227) @@ -1,491 +1,492 @@ /** * @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); + let heightmapPainter = new HeightmapPainter(convertHeightmap1Dto2D(mapTerrain.height)); 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), normalMinHeight, normalMaxHeight); + let heightmapPainter = new HeightmapPainter( + convertHeightmap1Dto2D(Engine.LoadHeightmapImage("maps/random/" + filename)), 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 adjacentCoordinate of g_AdjacentCoordinates) { let adjacentPos = Vector2D.add(position, adjacentCoordinate).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 }); };