Index: ps/trunk/binaries/data/mods/public/maps/random/deep_forest.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/deep_forest.js (revision 20976) +++ ps/trunk/binaries/data/mods/public/maps/random/deep_forest.js (revision 20977) @@ -1,202 +1,202 @@ Engine.LoadLibrary("rmgen"); var templateStone = "gaia/geology_stone_temperate"; var templateStoneMine = "gaia/geology_stonemine_temperate_quarry"; var templateMetalMine = "gaia/geology_metal_temperate_slabs"; var templateTemple = "other/unfinished_greek_temple"; var terrainPrimary = ["temp_grass", "temp_grass_b", "temp_grass_c", "temp_grass_d", "temp_grass_long_b", "temp_grass_clovers_2", "temp_grass_mossy", "temp_grass_plants"]; var terrainWood = ['temp_grass_mossy|gaia/flora_tree_oak', 'temp_forestfloor_pine|gaia/flora_tree_pine', 'temp_mud_plants|gaia/flora_tree_dead', 'temp_plants_bog|gaia/flora_tree_oak_large', "temp_dirt_gravel_plants|gaia/flora_tree_aleppo_pine", 'temp_forestfloor_autumn|gaia/flora_tree_carob']; //'temp_forestfloor_autumn|gaia/flora_tree_fig' var terrainWoodBorder = ['temp_grass_plants|gaia/flora_tree_euro_beech', 'temp_grass_mossy|gaia/flora_tree_poplar', 'temp_grass_mossy|gaia/flora_tree_poplar_lombardy', 'temp_grass_long|gaia/flora_bush_temperate', 'temp_mud_plants|gaia/flora_bush_temperate', 'temp_mud_plants|gaia/flora_bush_badlands', 'temp_grass_long|gaia/flora_tree_apple', 'temp_grass_clovers|gaia/flora_bush_berry', 'temp_grass_clovers_2|gaia/flora_bush_grapes', 'temp_grass_plants|gaia/fauna_deer', "temp_grass_long_b|gaia/fauna_rabbit", "temp_grass_plants"]; var terrainBase = ["temp_dirt_gravel", "temp_grass_b"]; var terrainBaseBorder = ["temp_grass_b", "temp_grass_b", "temp_grass", "temp_grass_c", "temp_grass_mossy"]; var terrainBaseCenter = ['temp_dirt_gravel', 'temp_dirt_gravel', 'temp_grass_b']; var terrainPath = ['temp_road', "temp_road_overgrown", 'temp_grass_b']; var terrainHill = ["temp_highlands", "temp_highlands", "temp_highlands", "temp_dirt_gravel_b", "temp_cliff_a"]; var terrainHillBorder = ["temp_highlands", "temp_highlands", "temp_highlands", "temp_dirt_gravel_b", "temp_dirt_gravel_plants", "temp_highlands", "temp_highlands", "temp_highlands", "temp_dirt_gravel_b", "temp_dirt_gravel_plants", "temp_highlands", "temp_highlands", "temp_highlands", "temp_cliff_b", "temp_dirt_gravel_plants", "temp_highlands", "temp_highlands", "temp_highlands", "temp_cliff_b", "temp_dirt_gravel_plants", "temp_highlands|gaia/fauna_goat"]; var heightPath = -2; var heightLand = 0; var heightOffsetRandomPath = 1; InitMap(heightLand, terrainPrimary); var mapSize = getMapSize(); var mapRadius = mapSize/2; var mapCenter = getMapCenter(); var clPlayer = createTileClass(); var clPath = createTileClass(); var clHill = createTileClass(); var clForest = createTileClass(); var clBaseResource = createTileClass(); var numPlayers = getNumPlayers(); var baseRadius = 20; var minPlayerRadius = Math.min(mapRadius - 1.5 * baseRadius, 5/8 * mapRadius); var maxPlayerRadius = Math.min(mapRadius - baseRadius, 3/4 * mapRadius); var playerPosition = []; var playerAngle = []; var playerAngleStart = randomAngle(); var playerAngleAddAvrg = 2 * Math.PI / numPlayers; var playerAngleMaxOff = playerAngleAddAvrg/4; var radiusEC = Math.max(mapRadius/8, baseRadius/2); var resourceRadius = fractionToTiles(1/3); var resourcePerPlayer = [templateStone, templateMetalMine]; // For large maps there are memory errors with too many trees. A density of 256*192/mapArea works with 0 players. // Around each player there is an area without trees so with more players the max density can increase a bit. var maxTreeDensity = Math.min(256 * (192 + 8 * numPlayers) / Math.square(mapSize), 1); // Has to be tweeked but works ok var bushChance = 1/3; // 1 means 50% chance in deepest wood, 0.5 means 25% chance in deepest wood var playerIDs = sortAllPlayers(); for (var i=0; i < numPlayers; i++) { playerAngle[i] = (playerAngleStart + i * playerAngleAddAvrg + randFloat(0, playerAngleMaxOff)) % (2 * Math.PI); playerPosition[i] = Vector2D.add(mapCenter, new Vector2D(randFloat(minPlayerRadius, maxPlayerRadius), 0).rotate(-playerAngle[i]).round()); } Engine.SetProgress(10); placePlayerBases({ "PlayerPlacement": [playerIDs, playerPosition], "BaseResourceClass": clBaseResource, // player class painted below "CityPatch": { "radius": 0.8 * baseRadius, "smoothness": 1/8, "painters": [ new LayeredPainter([terrainBaseBorder, terrainBase, terrainBaseCenter], [baseRadius/4, baseRadius/4]), paintClass(clPlayer) ] }, "Chicken": { }, "Berries": { "template": "gaia/flora_bush_grapes", "minCount": 2, "maxCount": 2, "distance": 12, "minDist": 5, "maxDist": 8 }, "Mines": { "types": [ { "template": templateMetalMine }, { "template": templateStoneMine } ], "minAngle": Math.PI / 2, "maxAngle": Math.PI }, "Trees": { "template": "gaia/flora_tree_oak_large", "count": 2 } }); Engine.SetProgress(10); log("Painting paths..."); var pathBlending = numPlayers <= 4; for (let i = 0; i < numPlayers + (pathBlending ? 1 : 0); ++i) for (let j = pathBlending ? 0 : i + 1; j < numPlayers + 1; ++j) { let pathStart = i < numPlayers ? playerPosition[i] : mapCenter; let pathEnd = j < numPlayers ? playerPosition[j] : mapCenter; createArea( new RandomPathPlacer(pathStart, pathEnd, 1.25, baseRadius / 2, pathBlending), [ new TerrainPainter(terrainPath), new SmoothElevationPainter(ELEVATION_SET, heightPath, 2, heightOffsetRandomPath), paintClass(clPath) ], avoidClasses(clHill, 0, clBaseResource, 4)); } Engine.SetProgress(50); log("Placing expansion resources..."); for (let i = 0; i < numPlayers; ++i) for (let rIndex = 0; rIndex < resourcePerPlayer.length; ++rIndex) { let angleDist = numPlayers > 1 ? (playerAngle[(i + 1) % numPlayers] - playerAngle[i] + 2 * Math.PI) % (2 * Math.PI) : 2 * Math.PI; // they are supposed to be in between players on the same radius let angle = playerAngle[i] + angleDist * (rIndex + 1) / (resourcePerPlayer.length + 1); let position = Vector2D.add(mapCenter, new Vector2D(resourceRadius, 0).rotate(-angle)).round(); placeObject(position.x, position.y, resourcePerPlayer[rIndex], 0, randomAngle()); createArea( new ClumpPlacer(40, 1/2, 1/8, 1, position), [ new LayeredPainter([terrainHillBorder, terrainHill], [1]), new ElevationPainter(randFloat(1, 2)), paintClass(clHill) ]); } Engine.SetProgress(60); log("Placing temple..."); placeObject(mapCenter.x, mapCenter.y, templateTemple, 0, randomAngle()); addToClass(mapCenter.x, mapCenter.y, clBaseResource); log("Creating central mountain..."); createArea( new ClumpPlacer(Math.square(radiusEC), 1/2, 1/8, 1, mapCenter), [ new LayeredPainter([terrainHillBorder, terrainHill], [radiusEC/4]), new ElevationPainter(randFloat(1, 2)), paintClass(clHill) ]); // Woods and general hight map for (var x = 0; x < mapSize; x++) for (var z = 0;z < mapSize;z++) { let position = new Vector2D(x, z); // The 0.5 is a correction for the entities placed on the center of tiles var radius = mapCenter.distanceTo(Vector2D.add(position, new Vector2D(0.5, 0.5))); var minDistToSL = mapSize; for (var i=0; i < numPlayers; i++) minDistToSL = Math.min(minDistToSL, position.distanceTo(playerPosition[i])); // Woods tile based var tDensFactSL = Math.max(Math.min((minDistToSL - baseRadius) / baseRadius, 1), 0); var tDensFactRad = Math.abs((resourceRadius - radius) / resourceRadius); var tDensFactEC = Math.max(Math.min((radius - radiusEC) / radiusEC, 1), 0); var tDensActual = maxTreeDensity * tDensFactSL * tDensFactRad * tDensFactEC; if (randBool(tDensActual) && g_Map.validT(x, z)) { let border = tDensActual < randFloat(0, bushChance * maxTreeDensity); createArea( - new RectPlacer(position.x, position.y, position.x + 1, position.y + 1) + new RectPlacer(position.x, position.y, position.x + 1, position.y + 1), [ new TerrainPainter(border ? terrainWoodBorder : terrainWood), new ElevationPainter(randFloat(0, 1)), paintClass(clForest) ], avoidClasses(clPath, 1, clHill, border ? 0 : 1)); } // General hight map var hVarMiddleHill = mapSize / 64 * (1 + Math.cos(3/2 * Math.PI * radius / mapRadius)); var hVarHills = 5 * (1 + Math.sin(x / 10) * Math.sin(z / 10)); g_Map.setHeight(position, getHeight(x, z) + hVarMiddleHill + hVarHills + 1); } Engine.SetProgress(95); placePlayersNomad(clPlayer, avoidClasses(clForest, 1, clBaseResource, 4)); ExportMap(); Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js (revision 20976) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js (revision 20977) @@ -1,424 +1,424 @@ const TERRAIN_SEPARATOR = "|"; const SEA_LEVEL = 20.0; const HEIGHT_UNITS_PER_METRE = 92; 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; /** * Main RandomMap object. */ var g_Map; function InitMap(baseHeight, baseTerrain) { g_Map = new RandomMap(baseHeight, baseTerrain); } function ExportMap() { g_Map.ExportMap(); } 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); } /** * 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; } /** * Sets the x and z property of the given object (typically a Placer or Group) to a random point on the map. * @param passableOnly - Should be true for entity placement and false for terrain or elevation operations. */ function randomizeCoordinates(obj, passableOnly) { let border = passableOnly ? MAP_BORDER_WIDTH : 0; if (g_MapSettings.CircularMap) { // Polar coordinates // Uniformly distributed on the disk let halfMapSize = g_Map.size / 2 - border; let r = halfMapSize * Math.sqrt(randFloat(0, 1)); let theta = randomAngle(); obj.x = Math.floor(r * Math.cos(theta)) + halfMapSize; obj.z = Math.floor(r * Math.sin(theta)) + halfMapSize; } else { // Rectangular coordinates obj.x = randIntExclusive(border, g_Map.size - border); obj.z = randIntExclusive(border, g_Map.size - border); } } /** * Sets the x and z property of the given JS object (typically a Placer or Group) to a random point of the area. */ function randomizeCoordinatesFromAreas(obj, areas) { let pt = pickRandom(pickRandom(areas).points); obj.x = pt.x; - obj.z = pt.z; + obj.z = pt.y; } // 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() { randomizeCoordinates(centeredPlacer, 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() { randomizeCoordinatesFromAreas(centeredPlacer, areas); 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() { randomizeCoordinates(group, 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() { randomizeCoordinatesFromAreas(group, areas); 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))); } function placeObject(x, z, type, player, angle) { if (g_Map.validT(x, z)) g_Map.addObject(new Entity(type, player, x, z, angle)); } function isCircularMap() { return !!g_MapSettings.CircularMap; } function createTileClass() { return g_Map.createTileClass(); } function getTileClass(id) { if (!g_Map.validClass(id)) return undefined; return g_Map.tileClasses[id]; } /** * 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, painter, constraints) { let points = placer.place(new AndConstraint(constraints)); if (!points) return undefined; let area = g_Map.createArea(points); if (painter instanceof Array) painter = new MultiPainter(painter); painter.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(getTileClass(tileClass))); } function unPaintTileClassBasedOnHeight(minHeight, maxHeight, mode, tileClass) { createArea( new HeightPlacer(mode, minHeight, maxHeight), new TileClassUnPainter(getTileClass(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)); } function getMapSize() { return g_Map.size; } function getMapCenter() { return deepfreeze(new Vector2D(g_Map.size / 2, g_Map.size / 2)); } function getMapBounds() { return deepfreeze({ "left": fractionToTiles(0), "right": fractionToTiles(1), "top": fractionToTiles(1), "bottom": fractionToTiles(0) }); } 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; } function getHeight(x, z) { return g_Map.getHeight(x, z); } /** * Utility functions for classes */ /** * Add point to given class by id */ function addToClass(x, z, id) { let tileClass = getTileClass(id); if (tileClass !== null) tileClass.add(x, z); } /** * Remove point from the given class by id */ function removeFromClass(x, z, id) { let tileClass = getTileClass(id); if (tileClass !== null) tileClass.remove(x, z); } /** * Create a painter for the given class */ function paintClass(id) { return new TileClassPainter(getTileClass(id)); } /** * Create a painter for the given class */ function unPaintClass(id) { return new TileClassUnPainter(getTileClass(id)); } /** * 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); } Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js (revision 20976) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js (revision 20977) @@ -1,318 +1,321 @@ /** * @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.x, point.z); + this.tileClass.add(point.x, point.y); }; /** * 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.x, point.z); + this.tileClass.remove(point.x, point.y); }; /** * The MultiPainter applies several painters to the given area. */ function MultiPainter(painters) { 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.x, point.z); + this.terrain.place(point.x, point.y); }; /** * 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": getMapSize(), "withinArea": (areaID, x, z) => g_Map.area[x][z] == 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.x, point.z); + this.terrains[i].place(point.x, point.y); } }); }; /** * 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 [dx, dz] of [[0, 0], [1, 0], [0, 1], [1, 1]]) { let x = point.x + dx; - let z = point.z + dz; + let z = point.y + dz; if (g_Map.inMapBounds(x, z)) g_Map.height[x][z] = 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.z + dz; + let nz = point.y + dz; + let position = new Vector2D(nx, nz); if (g_Map.validH(nx, nz) && !gotHeightPt[nx][nz]) { newHeight[nx][nz] = g_Map.height[nx][nz]; gotHeightPt[nx][nz] = 1; - heightPoints.push({ "x": nx, "z": nz }); + heightPoints.push(position); } } } // Every vertex of a tile is considered within the area let withinArea = (areaID, x, z) => { for (let [dx, dz] of [[0, 0], [1, 0], [0, 1], [1, 1]]) if (g_Map.inMapBounds(x - dx, z - dz) && g_Map.area[x - dx][z - dz] == 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.z] = (1 - a) * g_Map.height[point.x][point.z]; + newHeight[point.x][point.y] = (1 - a) * g_Map.height[point.x][point.y]; - newHeight[point.x][point.z] += a * this.elevation + randFloat(-0.5, 0.5) * this.randomElevation; + 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.x, point.z)) + if (!withinArea(areaID, point.x, point.y)) 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.z + dz; + let nz = point.y + dz; if (g_Map.validH(nx, nz)) { sum += newHeight[nx][nz]; ++count; } } } - g_Map.height[point.x][point.z] = (newHeight[point.x][point.z] + sum / count) / 2; + g_Map.height[point.x][point.y] = (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.z + dz; + let nz = point.y + dz; + let position = new Vector2D(nx, nz); if (!withinGrid(nx, nz) || args.withinArea(areaID, nx, nz) || saw[nx][nz]) continue; saw[nx][nz] = 1; dist[nx][nz] = 0; - pointQueue.push({ "x": nx, "z": nz }); + 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.z]; + let distance = dist[point.x][point.y]; - if (args.withinArea(areaID, point.x, point.z)) + if (args.withinArea(areaID, point.x, point.y)) 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.z + dz; + let nz = point.y + dz; + let position = new Vector2D(nx, nz); if (!withinGrid(nx, nz) || !args.withinArea(areaID, nx, nz) || saw[nx][nz]) continue; saw[nx][nz] = 1; dist[nx][nz] = distance + 1; - pointQueue.push({ "x": nx, "z": nz }); + pointQueue.push(position); } } } } Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/placer_centered.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/placer_centered.js (revision 20976) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/placer_centered.js (revision 20977) @@ -1,261 +1,264 @@ /** * @file A Centered Placer generates a shape (array of points) around a variable center location satisfying a Constraint. * The center is determined by the x and z property which can be modified externally, typically by createAreas. */ ///////////////////////////////////////////////////////////////////////////////////////// // ClumpPlacer // // Class for generating a roughly circular clump of points // // size: The average number of points in the clump // coherence: How much the radius of the clump varies (1.0 = circle, 0.0 = very random) // smoothness: How smooth the border of the clump is (1.0 = few "peaks", 0.0 = very jagged) // failfraction: Percentage of place attempts allowed to fail (optional) // position: Tile coordinates of placer center (optional) // ///////////////////////////////////////////////////////////////////////////////////////// function ClumpPlacer(size, coherence, smoothness, failFraction = 0, position = undefined) { this.size = size; this.coherence = coherence; this.smoothness = smoothness; this.failFraction = failFraction; this.x = position ? Math.round(position.x) : -1; this.z = position ? Math.round(position.y) : -1; } ClumpPlacer.prototype.place = function(constraint) { // Preliminary bounds check if (!g_Map.inMapBounds(this.x, this.z) || !constraint.allows(this.x, this.z)) return undefined; var retVec = []; var size = getMapSize(); var gotRet = new Array(size).fill(0).map(p => new Uint8Array(size)); // booleans var radius = Math.sqrt(this.size / Math.PI); var perim = 4 * radius * 2 * Math.PI; var intPerim = Math.ceil(perim); var ctrlPts = 1 + Math.floor(1.0/Math.max(this.smoothness,1.0/intPerim)); if (ctrlPts > radius * 2 * Math.PI) ctrlPts = Math.floor(radius * 2 * Math.PI) + 1; var noise = new Float32Array(intPerim); //float32 var ctrlCoords = new Float32Array(ctrlPts+1); //float32 var ctrlVals = new Float32Array(ctrlPts+1); //float32 // Generate some interpolated noise for (var i=0; i < ctrlPts; i++) { ctrlCoords[i] = i * perim / ctrlPts; ctrlVals[i] = randFloat(0, 2); } var c = 0; var looped = 0; for (var i=0; i < intPerim; i++) { if (ctrlCoords[(c+1) % ctrlPts] < i && !looped) { c = (c+1) % ctrlPts; if (c == ctrlPts-1) looped = 1; } noise[i] = cubicInterpolation( 1, (i - ctrlCoords[c]) / ((looped ? perim : ctrlCoords[(c + 1) % ctrlPts]) - ctrlCoords[c]), ctrlVals[(c + ctrlPts - 1) % ctrlPts], ctrlVals[c], ctrlVals[(c + 1) % ctrlPts], ctrlVals[(c + 2) % ctrlPts]); } var failed = 0; for (var p=0; p < intPerim; p++) { var th = 2 * Math.PI * p / perim; var r = radius * (1 + (1-this.coherence)*noise[p]); var s = Math.sin(th); var c = Math.cos(th); var xx = this.x; var yy = this.z; for (var k = 0; k < Math.ceil(r); ++k) { var i = Math.floor(xx); var j = Math.floor(yy); + let position = new Vector2D(i, j); + if (g_Map.inMapBounds(i, j) && constraint.allows(i, j)) { if (!gotRet[i][j]) { // Only include each point once gotRet[i][j] = 1; - retVec.push({ "x": i, "z": j }); + retVec.push(position); } } else failed++; xx += s; yy += c; } } return failed > this.size * this.failFraction ? undefined : retVec; }; ///////////////////////////////////////////////////////////////////////////////////////// // Chain Placer // // Class for generating a more random clump of points it randomly creates circles around the edges of the current clump // // minRadius: minimum radius of the circles // maxRadius: maximum radius of the circles // numCircles: the number of the circles // failfraction: Percentage of place attempts allowed to fail (optional) // position: Tile coordinates of placer center (optional) // fcc: Farthest circle center (optional) // q: a list containing numbers. each time if the list still contains values, pops one from the end and uses it as the radius (optional) // ///////////////////////////////////////////////////////////////////////////////////////// function ChainPlacer(minRadius, maxRadius, numCircles, failFraction, position, fcc, q) { this.minRadius = minRadius; this.maxRadius = maxRadius; this.numCircles = numCircles; this.failFraction = failFraction !== undefined ? failFraction : 0; this.x = position ? Math.round(position.x) : -1; this.z = position ? Math.round(position.y) : -1; this.fcc = fcc !== undefined ? fcc : 0; this.q = q !== undefined ? q : []; } ChainPlacer.prototype.place = function(constraint) { // Preliminary bounds check if (!g_Map.inMapBounds(this.x, this.z) || !constraint.allows(this.x, this.z)) return undefined; var retVec = []; var size = getMapSize(); var failed = 0, count = 0; var queueEmpty = !this.q.length; var gotRet = new Array(size).fill(0).map(p => new Array(size).fill(-1)); --size; this.minRadius = Math.min(this.maxRadius, Math.max(this.minRadius, 1)); var edges = [[this.x, this.z]]; for (var i = 0; i < this.numCircles; ++i) { var [cx, cz] = pickRandom(edges); if (queueEmpty) var radius = randIntInclusive(this.minRadius, this.maxRadius); else { var radius = this.q.pop(); queueEmpty = !this.q.length; } var sx = cx - radius, lx = cx + radius; var sz = cz - radius, lz = cz + radius; sx = Math.max(0, sx); sz = Math.max(0, sz); lx = Math.min(lx, size); lz = Math.min(lz, size); var radius2 = radius * radius; var dx, dz; for (var ix = sx; ix <= lx; ++ix) for (var iz = sz; iz <= lz; ++ iz) { + let position = new Vector2D(ix, iz); dx = ix - cx; dz = iz - cz; if (dx * dx + dz * dz <= radius2) { if (g_Map.inMapBounds(ix, iz) && constraint.allows(ix, iz)) { var state = gotRet[ix][iz]; if (state == -1) { - retVec.push({ "x": ix, "z": iz }); + retVec.push(position); gotRet[ix][iz] = -2; } else if (state >= 0) { var s = edges.splice(state, 1); gotRet[ix][iz] = -2; var edgesLength = edges.length; for (var k = state; k < edges.length; ++k) --gotRet[edges[k][0]][edges[k][1]]; } } else ++failed; ++count; } } for (var ix = sx; ix <= lx; ++ix) for (var iz = sz; iz <= lz; ++ iz) { if (this.fcc) if ((this.x - ix) > this.fcc || (ix - this.x) > this.fcc || (this.z - iz) > this.fcc || (iz - this.z) > this.fcc) continue; if (gotRet[ix][iz] == -2) { if (ix > 0) { if (gotRet[ix-1][iz] == -1) { edges.push([ix, iz]); gotRet[ix][iz] = edges.length - 1; continue; } } if (iz > 0) { if (gotRet[ix][iz-1] == -1) { edges.push([ix, iz]); gotRet[ix][iz] = edges.length - 1; continue; } } if (ix < size) { if (gotRet[ix+1][iz] == -1) { edges.push([ix, iz]); gotRet[ix][iz] = edges.length - 1; continue; } } if (iz < size) { if (gotRet[ix][iz+1] == -1) { edges.push([ix, iz]); gotRet[ix][iz] = edges.length - 1; continue; } } } } } return failed > count * this.failFraction ? undefined : retVec; }; Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/placer_noncentered.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/placer_noncentered.js (revision 20976) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/placer_noncentered.js (revision 20977) @@ -1,294 +1,299 @@ /** * @file A Non-Centered Placer generates a shape (array of points) at a fixed location meeting a Constraint and * is typically called by createArea. * Since this type of Placer has no x and z property, its location cannot be randomized using createAreas. */ /** * The RectPlacer provides the points on the tilegrid between (x1, z1) and (x2, z2) that meet the Constraint. */ function RectPlacer(x1, z1, x2, z2) { let mapSize = getMapSize(); this.x1 = Math.round(Math.max(Math.min(x1, x2), 0)); this.x2 = Math.round(Math.min(Math.max(x1, x2), mapSize - 1)); this.z1 = Math.round(Math.max(Math.min(z1, z2), 0)); this.z2 = Math.round(Math.min(Math.max(z1, z2), mapSize - 1)); } RectPlacer.prototype.place = function(constraint) { let points = []; for (let x = this.x1; x <= this.x2; ++x) for (let z = this.z1; z <= this.z2; ++z) + { + let position = new Vector2D(x, z); if (constraint.allows(x, z)) - points.push({ "x": x, "z": z }); + points.push(position); + } return points; }; /** * The MapBoundsPlacer returns all points on the tilemap that meet the constraint. */ function MapBoundsPlacer() { let mapBounds = getMapBounds(); this.rectPlacer = new RectPlacer(mapBounds.left, mapBounds.top, mapBounds.right, mapBounds.bottom); } MapBoundsPlacer.prototype.place = function(constraint) { return this.rectPlacer.place(constraint); }; /** * HeightPlacer constants determining whether the extrema should be included by the placer too. */ const Elevation_ExcludeMin_ExcludeMax = 0; const Elevation_IncludeMin_ExcludeMax = 1; const Elevation_ExcludeMin_IncludeMax = 2; const Elevation_IncludeMin_IncludeMax = 3; /** * The HeightPlacer provides all points between the minimum and maximum elevation that meet the Constraint, * even if they are far from the passable area of the map. */ function HeightPlacer(mode, minElevation, maxElevation) { this.withinHeightRange = mode == Elevation_ExcludeMin_ExcludeMax ? (x, z) => g_Map.height[x][z] > minElevation && g_Map.height[x][z] < maxElevation : mode == Elevation_IncludeMin_ExcludeMax ? (x, z) => g_Map.height[x][z] >= minElevation && g_Map.height[x][z] < maxElevation : mode == Elevation_ExcludeMin_IncludeMax ? (x, z) => g_Map.height[x][z] > minElevation && g_Map.height[x][z] <= maxElevation : mode == Elevation_IncludeMin_IncludeMax ? (x, z) => g_Map.height[x][z] >= minElevation && g_Map.height[x][z] <= maxElevation : undefined; if (!this.withinHeightRange) throw new Error("Invalid HeightPlacer mode: " + mode); } HeightPlacer.prototype.place = function(constraint) { let mapSize = getMapSize(); let points = []; for (let x = 0; x < mapSize; ++x) for (let z = 0; z < mapSize; ++z) + { + let position = new Vector2D(x, z); if (this.withinHeightRange(x, z) && constraint.allows(x, z)) - points.push({ "x": x, "z": z }); + points.push(position); + } return points; }; ///////////////////////////////////////////////////////////////////////////////////////// // PathPlacer // // Class for creating a winding path between two points // // x1,z1: Starting point of path // x2,z2: Ending point of path // width: Width of the path // a: Waviness - how wavy the path will be (higher is wavier, 0.0 is straight line) // b: Smoothness - how smooth the path will be (higher is smoother) // c: Offset - max amplitude of waves along the path (0.0 is straight line) // taper: Tapering - how much the width of the path changes from start to end // if positive, the width will decrease by that factor, if negative the width // will increase by that factor // ///////////////////////////////////////////////////////////////////////////////////////// function PathPlacer(start, end, width, a, b, c, taper, failFraction = 0) { this.start = start; this.end = end; this.width = width; this.a = a; this.b = b; this.c = c; this.taper = taper; this.failFraction = failFraction; } PathPlacer.prototype.place = function(constraint) { var failed = 0; let pathLength = this.start.distanceTo(this.end); var numSteps = 1 + Math.floor(pathLength / 4 * this.a); var numISteps = 1 + Math.floor(pathLength / 4 * this.b); var totalSteps = numSteps*numISteps; var offset = 1 + Math.floor(pathLength / 4 * this.c); var size = getMapSize(); var gotRet = []; for (var i = 0; i < size; ++i) gotRet[i] = new Uint8Array(size); // bool / uint8 // Generate random offsets var ctrlVals = new Float32Array(numSteps); //float32 for (var j = 1; j < (numSteps-1); ++j) { ctrlVals[j] = randFloat(-offset, offset); } // Interpolate for smoothed 1D noise var noise = new Float32Array(totalSteps+1); //float32 for (var j = 0; j < numSteps; ++j) for (let k = 0; k < numISteps; ++k) noise[j * numISteps + k] = cubicInterpolation( 1, k / numISteps, ctrlVals[(j + numSteps - 1) % numSteps], ctrlVals[j], ctrlVals[(j + 1) % numSteps], ctrlVals[(j + 2) % numSteps]); // Add smoothed noise to straight path let pathPerpendicular = Vector2D.sub(this.end, this.start).normalize().perpendicular(); var segments1 = []; var segments2 = []; for (var j = 0; j < totalSteps; ++j) { // Interpolated points along straight path let step1 = j / totalSteps; let step2 = (j + 1) / totalSteps; let stepStart = Vector2D.add(Vector2D.mult(this.start, 1 - step1), Vector2D.mult(this.end, step1)); let stepEnd = Vector2D.add(Vector2D.mult(this.start, 1 - step2), Vector2D.mult(this.end, step2)); // Find noise offset points let noiseStart = Vector2D.add(stepStart, Vector2D.mult(pathPerpendicular, noise[j])); let noiseEnd = Vector2D.add(stepEnd, Vector2D.mult(pathPerpendicular, noise[j + 1])); let noisePerpendicular = Vector2D.sub(noiseEnd, noiseStart).normalize().perpendicular(); let taperedWidth = (1 - step1 * this.taper) * this.width / 2; let point1 = Vector2D.sub(noiseStart, Vector2D.mult(noisePerpendicular, taperedWidth)).round(); let point2 = Vector2D.add(noiseEnd, Vector2D.mult(noisePerpendicular, taperedWidth)).round(); segments1.push({ "x": point1.x, "z": point1.y }); segments2.push({ "x": point2.x, "z": point2.y }); } var retVec = []; // Draw path segments var num = segments1.length - 1; for (var j = 0; j < num; ++j) { // Fill quad formed by these 4 points // Note the code assumes these points have been rounded to integer values var pt11 = segments1[j]; var pt12 = segments1[j+1]; var pt21 = segments2[j]; var pt22 = segments2[j+1]; var tris = [[pt12, pt11, pt21], [pt12, pt21, pt22]]; for (var t = 0; t < 2; ++t) { // Sort vertices by min z var tri = tris[t].sort( function(a, b) { return a.z - b.z; } ); // Fills in a line from (z, x1) to (z,x2) var fillLine = function(z, x1, x2) { var left = Math.round(Math.min(x1, x2)); var right = Math.round(Math.max(x1, x2)); for (var x = left; x <= right; x++) { + let position = new Vector2D(x, z); if (constraint.allows(x, z)) { if (g_Map.inMapBounds(x, z) && !gotRet[x][z]) { - retVec.push({ "x": x, "z": z }); + retVec.push(position); gotRet[x][z] = 1; } } else - { failed++; - } } }; var A = tri[0]; var B = tri[1]; var C = tri[2]; var dx1 = (B.z != A.z) ? ((B.x - A.x) / (B.z - A.z)) : 0; var dx2 = (C.z != A.z) ? ((C.x - A.x) / (C.z - A.z)) : 0; var dx3 = (C.z != B.z) ? ((C.x - B.x) / (C.z - B.z)) : 0; if (A.z == B.z) { fillLine(A.z, A.x, B.x); } else { for (var z = A.z; z <= B.z; z++) { fillLine(z, A.x + dx1*(z - A.z), A.x + dx2*(z - A.z)); } } if (B.z == C.z) { fillLine(B.z, B.x, C.x); } else { for (var z = B.z + 1; z < C.z; z++) { fillLine(z, B.x + dx3*(z - B.z), A.x + dx2*(z - A.z)); } } } } return failed > this.failFraction * this.width * pathLength ? undefined : retVec; }; /** * Creates a winded path between the given two vectors. * Uses a random angle at each step, so it can be more random than the sin form of the PathPlacer. * Omits the given offset after the start and before the end. */ function RandomPathPlacer(pathStart, pathEnd, pathWidth, offset, blended) { this.pathStart = Vector2D.add(pathStart, Vector2D.sub(pathEnd, pathStart).normalize().mult(offset)).round(); this.pathEnd = pathEnd; this.offset = offset; this.blended = blended; this.clumpPlacer = new ClumpPlacer(diskArea(pathWidth), 1, 1, 1); this.maxPathLength = fractionToTiles(2); } RandomPathPlacer.prototype.place = function(constraint) { let pathLength = 0; let points = []; let position = this.pathStart; while (position.distanceTo(this.pathEnd) >= this.offset && pathLength++ < this.maxPathLength) { position.add( new Vector2D(1, 0).rotate( -getAngle(this.pathStart.x, this.pathStart.y, this.pathEnd.x, this.pathEnd.y) + -Math.PI / 2 * (randFloat(-1, 1) + (this.blended ? 0.5 : 0)))).round(); this.clumpPlacer.x = position.x; this.clumpPlacer.z = position.y; for (let point of this.clumpPlacer.place(constraint) || []) - if (points.every(p => p.x != point.x || p.z != point.z)) + if (points.every(p => p.x != point.x || p.y != point.y)) points.push(point); } return points; }; 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 20976) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/random_map.js (revision 20977) @@ -1,385 +1,385 @@ /** * @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) { log("Initializing map..."); // 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.terrainObjects = []; this.area = []; for (let i = 0; i < this.size; ++i) { // Area IDs this.area[i] = new Uint16Array(this.size); // Entities this.terrainObjects[i] = []; for (let j = 0; j < this.size; ++j) this.terrainObjects[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; } // Array of Entities this.objects = []; // Array of integers this.tileClasses = []; this.areaID = 0; // Starting entity ID, arbitrary number to leave some space for player entities this.entityCount = 150; } /** * 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++; }; /** * 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.validT = function(x, z, distance = 0) { distance += MAP_BORDER_WIDTH; if (g_MapSettings.CircularMap) { let halfSize = Math.floor(this.size / 2); return Math.round(Math.euclidDistance2D(x, z, halfSize, halfSize)) < halfSize - distance - 1; } else return x >= distance && z >= distance && x < this.size - distance && z < this.size - distance; }; /** * Determines whether the given coordinates are within the tile grid, passable or not. * Should be used to restrict texture painting. */ RandomMap.prototype.inMapBounds = function(x, z) { return x >= 0 && z >= 0 && x < this.size && z < this.size; }; /** * Determines whether the given coordinates are within the heightmap grid. * Should be used to restrict elevation changes. */ RandomMap.prototype.validH = function(x, z) { if (x < 0 || z < 0) return false; if (TILE_CENTERED_HEIGHT_MAP) return x < this.size && z < this.size; return x <= this.size && z <= this.size; }; /** * Tests if there is a tileclass with the given ID. */ RandomMap.prototype.validClass = function(tileClassID) { return tileClassID >= 0 && tileClassID < this.tileClasses.length; }; /** * Returns the name of the texture of the given tile. */ RandomMap.prototype.getTexture = function(x, z) { if (!this.validT(x, z)) throw new Error("getTexture: invalid tile position (" + x + ", " + z + ")"); return this.IDToName[this.texture[x][z]]; }; /** * Paints the given texture on the given tile. */ RandomMap.prototype.setTexture = function(x, z, texture) { if (!this.validT(x, z)) throw new Error("setTexture: invalid tile position (" + x + ", " + z + ")"); this.texture[x][z] = this.getTextureID(texture); }; RandomMap.prototype.getHeight = function(x, z) { if (!this.validH(x, z)) throw new Error("getHeight: invalid vertex position (" + x + ", " + z + ")"); return this.height[x][z]; }; RandomMap.prototype.setHeight = function(position, height) { if (!this.validH(position.x, position.y)) throw new Error("setHeight: invalid vertex position " + uneval(position)); this.height[position.x][position.y] = height; }; /** * Returns the Entity that was painted by a Terrain class on the given tile or undefined otherwise. */ RandomMap.prototype.getTerrainObject = function(x, z) { if (!this.validT(x, z)) throw new Error("getTerrainObject: invalid tile position (" + x + ", " + z + ")"); return this.terrainObjects[x][z]; }; /** * Places the Entity on the given tile and allows to later replace it if the terrain was painted over. */ RandomMap.prototype.setTerrainObject = function(x, z, object) { if (!this.validT(x, z)) throw new Error("setTerrainObject: invalid tile position (" + x + ", " + z + ")"); this.terrainObjects[x][z] = object; }; /** * Adds the given Entity to the map at the location it defines. */ RandomMap.prototype.addObject = function(obj) { this.objects.push(obj); }; /** * 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.z] = areaID; + this.area[p.x][p.y] = areaID; return new Area(points, areaID); }; /** * Returns an unused tileclass ID. */ RandomMap.prototype.createTileClass = function() { let newID = this.tileClasses.length; this.tileClasses.push(new TileClass(this.size, newID)); return newID; }; /** * Retrieve interpolated height for arbitrary coordinates within the heightmap grid. */ RandomMap.prototype.getExactHeight = function(x, z) { let xi = Math.min(Math.floor(x), this.size); let zi = Math.min(Math.floor(z), this.size); let xf = x - xi; let zf = z - 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(x, z) { let count = 0; let sumHeight = 0; for (let dir of [[-1, -1], [-1, 0], [0, -1], [0, 0]]) if (this.validH(x + dir[0], z + dir[1])) { ++count; sumHeight += this.height[x + dir[0]][z + dir[1]]; } if (count == 0) 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.x, adjacentPos.y)) 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.x, pos.y), 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.x, adjacentPos.y) - this.getHeight(position.x, position.y)), 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 obj of this.objects) obj.rotation.y = Math.PI / 2 - obj.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.terrainObjects[x][z]) this.objects.push(this.terrainObjects[x][z]); log("Number of entities: " + this.objects.length); return this.objects; }; /** * 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 currentHeight = TILE_CENTERED_HEIGHT_MAP ? this.cornerHeight(x, z) : this.height[x][z]; // 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() { log("Saving map..."); if (g_Environment.Water.WaterBody.Height === undefined) g_Environment.Water.WaterBody.Height = SEA_LEVEL - 0.1; 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 }); }