Index: ps/trunk/binaries/data/mods/public/maps/random/red_sea.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/red_sea.js (revision 21155) +++ ps/trunk/binaries/data/mods/public/maps/random/red_sea.js (revision 21156) @@ -1,365 +1,382 @@ /** * Heightmap image source: * Imagery by Jesse Allen, NASA's Earth Observatory, * using data from the General Bathymetric Chart of the Oceans (GEBCO) * produced by the British Oceanographic Data Centre. * https://visibleearth.nasa.gov/view.php?id=73934 * * Licensing: Public Domain, https://visibleearth.nasa.gov/useterms.php * * The heightmap image is reproduced using: * wget https://eoimages.gsfc.nasa.gov/images/imagerecords/73000/73934/gebco_08_rev_elev_C1_grey_geo.tif - * lat=22; lon=41; width=30; gdal_translate -projwin $((lon-width/2)) $((lat+width/2)) $((lon+width/2)) $((lat-width/2)) gebco_08_rev_elev_C1_grey_geo.tif red_sea.tif + * lat=22; lon=41; width=30; + * gdal_translate -projwin $((lon-width/2)) $((lat+width/2)) $((lon+width/2)) $((lat-width/2)) gebco_08_rev_elev_C1_grey_geo.tif red_sea.tif * convert red_sea.tif -resize 512 -contrast-stretch 0 red_sea.png * No further changes should be applied to the image to keep it easily interchangeable. */ Engine.LoadLibrary("rmgen"); Engine.LoadLibrary("rmgen2"); Engine.LoadLibrary("rmbiome"); Engine.LoadLibrary("heightmap"); setBiome("generic/desert"); g_Terrains.mainTerrain = new Array(4).fill("desert_sand_dunes_50").concat(["desert_sand_dunes_rocks", "desert_dirt_rough_2"]); g_Terrains.forestFloor1 = "desert_grass_a_sand"; g_Terrains.cliff = "desert_cliff_3_dirty"; g_Terrains.forestFloor2 = "desert_grass_a_sand"; g_Terrains.tier1Terrain = "desert_dirt_rocks_2"; g_Terrains.tier2Terrain = "desert_dirt_rough"; g_Terrains.tier3Terrain = "desert_dirt_rough"; g_Terrains.tier4Terrain = "desert_sand_stones"; g_Terrains.roadWild = "road2"; g_Terrains.road = "road2"; g_Terrains.additionalDirt1 = "desert_plants_b"; g_Terrains.additionalDirt2 = "desert_sand_scrub"; g_Gaia.tree1 = "gaia/flora_tree_date_palm"; g_Gaia.tree2 = "gaia/flora_tree_senegal_date_palm"; g_Gaia.tree3 = "gaia/flora_tree_fig"; g_Gaia.tree4 = "gaia/flora_tree_cretan_date_palm_tall"; g_Gaia.tree5 = "gaia/flora_tree_cretan_date_palm_short"; g_Gaia.fruitBush = "gaia/flora_bush_grapes"; g_Decoratives.grass = "actor|props/flora/grass_field_dry_tall_b.xml"; g_Decoratives.grassShort = "actor|props/flora/grass_field_parched_short.xml"; g_Decoratives.rockLarge = "actor|geology/stone_desert_med.xml"; g_Decoratives.rockMedium = "actor|geology/stone_savanna_med.xml"; g_Decoratives.bushMedium = "actor|props/flora/bush_desert_dry_a.xml"; g_Decoratives.bushSmall = "actor|props/flora/bush_medit_sm_dry.xml"; g_Decoratives.dust = "actor|particle/dust_storm_reddish.xml"; const heightScale = num => num * g_MapSettings.Size / 320; const heightSeaGround = heightScale(-4); const heightReedsMin = heightScale(-2); const heightReedsMax = heightScale(-0.5); const heightWaterLevel = heightScale(0); const heightShoreline = heightScale(0.5); const heightHills = heightScale(16); var g_Map = new RandomMap(0, g_Terrains.mainTerrain); var mapCenter = g_Map.getCenter(); initTileClasses(["shoreline"]); g_Map.LoadHeightmapImage("red_sea.png", 0, 25); Engine.SetProgress(15); g_Map.log("Lowering sea ground"); createArea( new MapBoundsPlacer(), new SmoothElevationPainter(ELEVATION_SET, heightSeaGround, 2), new HeightConstraint(-Infinity, heightWaterLevel)); Engine.SetProgress(20); g_Map.log("Smoothing heightmap"); globalSmoothHeightmap(scaleByMapSize(0.1, 0.5)); Engine.SetProgress(25); g_Map.log("Marking water"); createArea( new MapBoundsPlacer(), new TileClassPainter(g_TileClasses.water), new HeightConstraint(-Infinity, heightWaterLevel)); Engine.SetProgress(30); g_Map.log("Marking land"); createArea( new ClumpPlacer(diskArea(fractionToTiles(0.5)), 1, 1, Infinity, mapCenter), new TileClassPainter(g_TileClasses.land), avoidClasses(g_TileClasses.water, 0)); Engine.SetProgress(35); g_Map.log("Painting shoreline"); createArea( new MapBoundsPlacer(), [ new TerrainPainter(g_Terrains.water), new TileClassPainter(g_TileClasses.shoreline) ], new HeightConstraint(-Infinity, heightShoreline)); Engine.SetProgress(40); g_Map.log("Painting cliffs"); createArea( new MapBoundsPlacer(), [ new TerrainPainter(g_Terrains.cliff), new TileClassPainter(g_TileClasses.mountain), ], [ avoidClasses(g_TileClasses.water, 2), new SlopeConstraint(2, Infinity) ]); Engine.SetProgress(45); if (!isNomad()) { g_Map.log("Placing players"); - var playerBases = placeRandom(sortAllPlayers(), new AndConstraint([avoidClasses(g_TileClasses.mountain, 5), stayClasses(g_TileClasses.land, defaultPlayerBaseRadius())])); + let playerBases = placeRandom( + sortAllPlayers(), + [ + avoidClasses(g_TileClasses.mountain, 5), + stayClasses(g_TileClasses.land, defaultPlayerBaseRadius()) + ]); g_Map.log("Flatten the initial CC area..."); for (let player of playerBases) createArea( new ClumpPlacer(diskArea(defaultPlayerBaseRadius() * 0.8), 0.95, 0.6, Infinity, player.position), new SmoothElevationPainter(ELEVATION_SET, g_Map.getHeight(player.position), 6)); } addElements(shuffleArray([ { "func": addMetal, "avoid": [ g_TileClasses.berries, 5, g_TileClasses.forest, 3, g_TileClasses.mountain, 2, g_TileClasses.player, 30, g_TileClasses.rock, 10, g_TileClasses.metal, 20, g_TileClasses.water, 3 ], "sizes": ["normal"], "mixes": ["same"], "amounts": ["normal"] }, { "func": addStone, "avoid": [ g_TileClasses.berries, 5, g_TileClasses.forest, 3, g_TileClasses.mountain, 2, g_TileClasses.player, 30, g_TileClasses.rock, 20, g_TileClasses.metal, 10, g_TileClasses.water, 3 ], "sizes": ["normal"], "mixes": ["same"], "amounts": ["normal"] }, { "func": addForests, "avoid": [ g_TileClasses.berries, 3, g_TileClasses.forest, 20, g_TileClasses.metal, 4, g_TileClasses.mountain, 3, g_TileClasses.player, 20, g_TileClasses.rock, 4, g_TileClasses.water, 2 ], "sizes": ["big"], "mixes": ["similar"], "amounts": ["few"] } ])); Engine.SetProgress(60); // Ensure initial forests addElements([{ "func": addForests, "avoid": [ g_TileClasses.berries, 2, g_TileClasses.forest, 25, g_TileClasses.metal, 4, g_TileClasses.mountain, 5, g_TileClasses.player, 15, g_TileClasses.rock, 4, g_TileClasses.water, 2 ], "sizes": ["small"], "mixes": ["similar"], "amounts": ["tons"] }]); Engine.SetProgress(65); addElements(shuffleArray([ { "func": addBerries, "avoid": [ g_TileClasses.berries, 30, g_TileClasses.forest, 5, g_TileClasses.metal, 10, g_TileClasses.mountain, 2, g_TileClasses.player, 20, g_TileClasses.rock, 10, g_TileClasses.water, 3 ], "sizes": ["normal"], "mixes": ["same"], "amounts": ["normal", "many"] }, { "func": addAnimals, "avoid": [ g_TileClasses.animals, 20, g_TileClasses.forest, 2, g_TileClasses.metal, 2, g_TileClasses.mountain, 1, g_TileClasses.player, 20, g_TileClasses.rock, 4, g_TileClasses.water, 3 ], "sizes": ["normal"], "mixes": ["same"], "amounts": ["many"] }, { "func": addFish, "avoid": [ g_TileClasses.fish, 12, g_TileClasses.player, 8 ], "stay": [g_TileClasses.water, 4], "sizes": ["normal"], "mixes": ["same"], "amounts": ["many"] }, { "func": addStragglerTrees, "avoid": [ g_TileClasses.berries, 5, g_TileClasses.forest, 15, g_TileClasses.metal, 2, g_TileClasses.mountain, 1, g_TileClasses.player, 20, g_TileClasses.rock, 4, g_TileClasses.water, 5 ], "sizes": ["normal"], "mixes": ["same"], "amounts": ["many"] } ])); Engine.SetProgress(70); addElements([ { "func": addLayeredPatches, "avoid": [ g_TileClasses.dirt, 5, g_TileClasses.forest, 2, g_TileClasses.mountain, 2, g_TileClasses.player, 12, g_TileClasses.water, 3, g_TileClasses.shoreline, 2 ], "sizes": ["normal"], "mixes": ["normal"], "amounts": ["tons"] }, { "func": addDecoration, "avoid": [ g_TileClasses.forest, 2, g_TileClasses.mountain, 2, g_TileClasses.player, 12, g_TileClasses.water, 3 ], "sizes": ["normal"], "mixes": ["similar"], "amounts": ["many"] } ]); Engine.SetProgress(80); g_Map.log("Painting dirt patches"); var dirtPatches = [ { "sizes": [2, 4], "count": scaleByMapSize(2, 5), "terrain": g_Terrains.additionalDirt1 }, { "sizes": [4, 6, 8], "count": scaleByMapSize(4, 8), "terrain": g_Terrains.additionalDirt2 } ]; for (let dirtPatch of dirtPatches) createPatches( dirtPatch.sizes, dirtPatch.terrain, [ stayClasses(g_TileClasses.land, 6), avoidClasses( g_TileClasses.mountain, 4, g_TileClasses.forest, 2, g_TileClasses.shoreline, 2, g_TileClasses.player, 12) ], dirtPatch.count, g_TileClasses.dirt, 0.5); Engine.SetProgress(85); g_Map.log("Adding reeds"); createObjectGroups( new SimpleGroup( [ new SimpleObject(g_Decoratives.reeds, 5, 12, 1, 4), new SimpleObject(g_Decoratives.rockMedium, 1, 2, 1, 5) ], false, g_TileClasses.dirt), 0, new HeightConstraint(heightReedsMin, heightReedsMax), scaleByMapSize(10, 25), 5); Engine.SetProgress(90); g_Map.log("Adding dust..."); createObjectGroups( new SimpleGroup([new SimpleObject(g_Decoratives.dust, 1, 1, 1, 4)], false), 0, [ stayClasses(g_TileClasses.land, 5), avoidClasses(g_TileClasses.player, 10) ], scaleByMapSize(10, 50), 20); Engine.SetProgress(95); -placePlayersNomad(g_Map.createTileClass()); +placePlayersNomad( + g_Map.createTileClass(), + [ + stayClasses(g_TileClasses.land, 5), + avoidClasses( + g_TileClasses.forest, 2, + g_TileClasses.rock, 4, + g_TileClasses.metal, 4, + g_TileClasses.berries, 2, + g_TileClasses.animals, 2, + g_TileClasses.mountain, 2) + ]); setWindAngle(-0.43); setWaterTint(0.161, 0.286, 0.353); setWaterColor(0.129, 0.176, 0.259); setWaterWaviness(8); setWaterMurkiness(0.87); setWaterType("lake"); setTerrainAmbientColor(0.58, 0.443, 0.353); setSunColor(0.733, 0.746, 0.574); setSunRotation(Math.PI * 1.1); setSunElevation(Math.PI / 7); setFogFactor(0); setFogThickness(0); setFogColor(0.69, 0.616, 0.541); setPPEffect("hdr"); setPPContrast(0.67); setPPSaturation(0.42); setPPBloom(0.23); g_Map.ExportMap(); 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 21155) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/placer_noncentered.js (revision 21156) @@ -1,283 +1,285 @@ /** * @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 returns all tiles between the two given points that meet the Constraint. */ function RectPlacer(start, end, failFraction = Infinity) { this.bounds = getBoundingBox([start, end]); + this.bounds.min.floor(); + this.bounds.max.floor(); this.failFraction = failFraction; } RectPlacer.prototype.place = function(constraint) { let bboxPoints = getPointsInBoundingBox(this.bounds); let points = bboxPoints.filter(point => g_Map.inMapBounds(point) && constraint.allows(point)); return (bboxPoints.length - points.length) / bboxPoints.length <= this.failFraction ? points : undefined; }; /** * The MapBoundsPlacer returns all points on the tilemap that meet the constraint. */ function MapBoundsPlacer(failFraction = Infinity) { let mapBounds = g_Map.getBounds(); this.rectPlacer = new RectPlacer(new Vector2D(mapBounds.left, mapBounds.top), new Vector2D(mapBounds.right, mapBounds.bottom), failFraction); } 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 ? position => g_Map.getHeight(position) > minElevation && g_Map.getHeight(position) < maxElevation : mode == Elevation_IncludeMin_ExcludeMax ? position => g_Map.getHeight(position) >= minElevation && g_Map.getHeight(position) < maxElevation : mode == Elevation_ExcludeMin_IncludeMax ? position => g_Map.getHeight(position) > minElevation && g_Map.getHeight(position) <= maxElevation : mode == Elevation_IncludeMin_IncludeMax ? position => g_Map.getHeight(position) >= minElevation && g_Map.getHeight(position) <= maxElevation : undefined; if (!this.withinHeightRange) throw new Error("Invalid HeightPlacer mode: " + mode); } HeightPlacer.prototype.place = function(constraint) { let mapSize = g_Map.getSize(); return getPointsInBoundingBox(getBoundingBox([new Vector2D(0, 0), new Vector2D(mapSize - 1, mapSize - 1)])).filter( point => this.withinHeightRange(point) && constraint.allows(point)); }; /** * Creates a winding path between two points. * * @param {Vector2D} start - Starting position of the path. * @param {Vector2D} end - Endposition of the path. * @param {number} width - Number of tiles between two sides of the path. * @param {number} waviness - 0 is a straight line, higher numbers are. * @param {number} smoothness - the higher the number, the smoother the path. * @param {number} offset - Maximum amplitude of waves along the path. 0 is straight line. * @param {number} 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, waviness, smoothness, offset, tapering, failFraction = 0) { this.start = start; this.end = end; this.width = width; this.waviness = waviness; this.smoothness = smoothness; this.offset = offset; this.tapering = tapering; this.failFraction = failFraction; } PathPlacer.prototype.place = function(constraint) { let pathLength = this.start.distanceTo(this.end); let numStepsWaviness = 1 + Math.floor(pathLength / 4 * this.waviness); let numStepsLength = 1 + Math.floor(pathLength / 4 * this.smoothness); let offset = 1 + Math.floor(pathLength / 4 * this.offset); // Generate random offsets let ctrlVals = new Float32Array(numStepsWaviness); for (let j = 1; j < numStepsWaviness - 1; ++j) ctrlVals[j] = randFloat(-offset, offset); // Interpolate for smoothed 1D noise let totalSteps = numStepsWaviness * numStepsLength; let noise = new Float32Array(totalSteps + 1); for (let j = 0; j < numStepsWaviness; ++j) for (let k = 0; k < numStepsLength; ++k) noise[j * numStepsLength + k] = cubicInterpolation( 1, k / numStepsLength, ctrlVals[(j + numStepsWaviness - 1) % numStepsWaviness], ctrlVals[j], ctrlVals[(j + 1) % numStepsWaviness], ctrlVals[(j + 2) % numStepsWaviness]); // Add smoothed noise to straight path let pathPerpendicular = Vector2D.sub(this.end, this.start).normalize().perpendicular(); let segments1 = []; let segments2 = []; for (let 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.tapering) * this.width / 2; segments1.push(Vector2D.sub(noiseStart, Vector2D.mult(noisePerpendicular, taperedWidth)).round()); segments2.push(Vector2D.add(noiseEnd, Vector2D.mult(noisePerpendicular, taperedWidth)).round()); } // Draw path segments let size = g_Map.getSize(); let gotRet = new Array(size).fill(0).map(i => new Uint8Array(size)); let retVec = []; let failed = 0; for (let j = 0; j < segments1.length - 1; ++j) { let points = new ConvexPolygonPlacer([segments1[j], segments1[j + 1], segments2[j], segments2[j + 1]], Infinity).place(new NullConstraint()); if (!points) continue; for (let point of points) { if (!constraint.allows(point)) { ++failed; continue; } if (g_Map.inMapBounds(point) && !gotRet[point.x][point.y]) { retVec.push(point); gotRet[point.x][point.y] = 1; } } } 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, Infinity); 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.setCenterPosition(position); for (let point of this.clumpPlacer.place(constraint) || []) if (points.every(p => p.x != point.x || p.y != point.y)) points.push(point); } return points; }; /** * Returns all points on the tilegrid within the convex hull of the given positions. */ function ConvexPolygonPlacer(points, failFraction = 0) { this.polygonVertices = this.getConvexHull(points.map(point => point.clone().round())); this.failFraction = failFraction; }; ConvexPolygonPlacer.prototype.place = function(constraint) { let points = []; let count = 0; let failed = 0; for (let point of getPointsInBoundingBox(getBoundingBox(this.polygonVertices))) { if (this.polygonVertices.some((vertex, i) => distanceOfPointFromLine(this.polygonVertices[i], this.polygonVertices[(i + 1) % this.polygonVertices.length], point) > 0)) continue; ++count; if (g_Map.inMapBounds(point) && constraint.allows(point)) points.push(point); else ++failed; } return failed <= this.failFraction * count ? points : undefined; }; /** * Applies the gift-wrapping algorithm. * Returns a sorted subset of the given points that are the vertices of the convex polygon containing all given points. */ ConvexPolygonPlacer.prototype.getConvexHull = function(points) { let uniquePoints = []; for (let point of points) if (uniquePoints.every(p => p.x != point.x || p.y != point.y)) uniquePoints.push(point); // Start with the leftmost point let result = [uniquePoints.reduce((leftMost, point) => point.x < leftMost.x ? point : leftMost, uniquePoints[0])]; // Add the vector most left of the most recently added point until a cycle is reached while (result.length < uniquePoints.length) { let nextLeftmostPoint; // Of all points, find the one that is leftmost for (let point of uniquePoints) { if (point == result[result.length - 1]) continue; if (!nextLeftmostPoint || distanceOfPointFromLine(nextLeftmostPoint, result[result.length - 1], point) <= 0) nextLeftmostPoint = point; } // If it was a known one, then the remaining points are inside this hull if (result.indexOf(nextLeftmostPoint) != -1) break; result.push(nextLeftmostPoint); } return result; } Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js (revision 21155) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js (revision 21156) @@ -1,485 +1,496 @@ var g_Amounts = { "scarce": 0.2, "few": 0.5, "normal": 1, "many": 1.75, "tons": 3 }; var g_Mixes = { "same": 0, "similar": 0.1, "normal": 0.25, "varied": 0.5, "unique": 0.75 }; var g_Sizes = { "tiny": 0.5, "small": 0.75, "normal": 1, "big": 1.25, "huge": 1.5, }; var g_AllAmounts = Object.keys(g_Amounts); var g_AllMixes = Object.keys(g_Mixes); var g_AllSizes = Object.keys(g_Sizes); var g_DefaultTileClasses = [ "animals", "baseResource", "berries", "bluff", "bluffSlope", "dirt", "fish", "food", "forest", "hill", "land", "map", "metal", "mountain", "plateau", "player", "prop", "ramp", "rock", "settlement", "spine", "valley", "water" ]; var g_TileClasses; /** * Adds an array of elements to the map. */ function addElements(elements) { for (let element of elements) element.func( [ avoidClasses.apply(null, element.avoid), stayClasses.apply(null, element.stay || null) ], pickSize(element.sizes), pickMix(element.mixes), pickAmount(element.amounts), element.baseHeight || 0); } /** * Converts "amount" terms to numbers. */ function pickAmount(amounts) { let amount = pickRandom(amounts); if (amount in g_Amounts) return g_Amounts[amount]; return g_Amounts.normal; } /** * Converts "mix" terms to numbers. */ function pickMix(mixes) { let mix = pickRandom(mixes); if (mix in g_Mixes) return g_Mixes[mix]; return g_Mixes.normal; } /** * Converts "size" terms to numbers. */ function pickSize(sizes) { let size = pickRandom(sizes); if (size in g_Sizes) return g_Sizes[size]; return g_Sizes.normal; } /** * Choose starting locations for all players. * * @param {string} type - "radial", "line", "stronghold", "random" * @param {number} distance - radial distance from the center of the map * @param {number} groupedDistance - space between players within a team * @param {number} startAngle - determined by the map that might want to place something between players * @returns {Array|undefined} - If successful, each element is an object that contains id, angle, x, z for each player */ function addBases(type, distance, groupedDistance, startAngle) { g_Map.log("Creating bases"); let playerIDs = sortAllPlayers(); let teamsArray = getTeamsArray(); switch(type) { case "line": return placeLine(teamsArray, distance, groupedDistance, startAngle); case "radial": return placeRadial(playerIDs, distance, startAngle); case "random": return placeRandom(playerIDs) || placeRadial(playerIDs, distance, startAngle); case "stronghold": return placeStronghold(teamsArray, distance, groupedDistance, startAngle); default: warn("Unknown base placement type:" + type); return undefined; } } /** * Create the base for a single player. * * @param {Object} player - contains id, angle, x, z * @param {boolean} walls - Whether or not iberian gets starting walls */ function createBase(player, walls = true) { placePlayerBase({ "playerID": player.id, "playerPosition": player.position, "PlayerTileClass": g_TileClasses.player, "BaseResourceClass": g_TileClasses.baseResource, + "baseResourceConstraint": avoidClasses(g_TileClasses.water, 0), "Walls": g_Map.getSize() > 192 && walls, "CityPatch": { "outerTerrain": g_Terrains.roadWild, "innerTerrain": g_Terrains.road, "painters": [ new TileClassPainter(g_TileClasses.player) ] }, "Chicken": { "template": g_Gaia.chicken }, "Berries": { "template": g_Gaia.fruitBush }, "Mines": { "types": [ { "template": g_Gaia.metalLarge }, { "template": g_Gaia.stoneLarge } ] }, "Trees": { "template": g_Gaia.tree1, "count": currentBiome() == "generic/savanna" ? 5 : 15 }, "Decoratives": { "template": g_Decoratives.grassShort } }); } /** * Return an array where each element is an array of playerIndices of a team. */ function getTeamsArray() { var playerIDs = sortAllPlayers(); var numPlayers = getNumPlayers(); // Group players by team var teams = []; for (let i = 0; i < numPlayers; ++i) { let team = getPlayerTeam(playerIDs[i]); if (team == -1) continue; if (!teams[team]) teams[team] = []; teams[team].push(playerIDs[i]); } // Players without a team get a custom index for (let i = 0; i < numPlayers; ++i) if (getPlayerTeam(playerIDs[i]) == -1) teams.push([playerIDs[i]]); // Remove unused indices return teams.filter(team => true); } /** * Choose a random pattern for placing the bases of the players. */ function randomStartingPositionPattern(teamsArray) { var formats = ["radial"]; var mapSize = g_Map.getSize(); var numPlayers = getNumPlayers(); // Enable stronghold if we have a few teams and a big enough map if (teamsArray.length >= 2 && numPlayers >= 4 && mapSize >= 256) formats.push("stronghold"); // Enable random if we have enough teams or enough players on a big enough map if (mapSize >= 256 && (teamsArray.length >= 3 || numPlayers > 4)) formats.push("random"); // Enable line if we have enough teams and players on a big enough map if (teamsArray.length >= 2 && numPlayers >= 4 && mapSize >= 384) formats.push("line"); return { "setup": pickRandom(formats), "distance": fractionToTiles(randFloat(0.2, 0.35)), "separation": fractionToTiles(randFloat(0.05, 0.1)) }; } /** * Place teams in a line-pattern. * * @param {Array} playerIDs - typically randomized indices of players of a single team * @param {number} distance - radial distance from the center of the map * @param {number} groupedDistance - distance between players * @param {number} startAngle - determined by the map that might want to place something between players. * * @returns {Array} - contains id, angle, x, z for every player */ function placeLine(teamsArray, distance, groupedDistance, startAngle) { let players = []; let mapCenter = g_Map.getCenter(); let dist = fractionToTiles(0.45); for (let i = 0; i < teamsArray.length; ++i) { var safeDist = distance; if (distance + teamsArray[i].length * groupedDistance > dist) safeDist = dist - teamsArray[i].length * groupedDistance; var teamAngle = startAngle + (i + 1) * 2 * Math.PI / teamsArray.length; // Create player base for (let p = 0; p < teamsArray[i].length; ++p) { players[teamsArray[i][p]] = { "id": teamsArray[i][p], "position": Vector2D.add(mapCenter, new Vector2D(safeDist + p * groupedDistance, 0).rotate(-teamAngle)).round() }; createBase(players[teamsArray[i][p]], false); } } return players; } /** * Place players in a circle-pattern. * * @param {Array} playerIDs - order of playerIDs to be placed * @param {number} distance - radial distance from the center of the map * @param {number} startAngle - determined by the map that might want to place something between players */ function placeRadial(playerIDs, distance, startAngle) { let mapCenter = g_Map.getCenter(); let players = []; let numPlayers = getNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let angle = startAngle + i * 2 * Math.PI / numPlayers; players[i] = { "id": playerIDs[i], "position": Vector2D.add(mapCenter, new Vector2D(distance, 0).rotate(-angle)).round() }; createBase(players[i]); } return players; } /** - * Choose arbitrary starting locations. + * Place playerbases on random locations on the map meeting the given constraints. */ function placeRandom(playerIDs, constraints = undefined) { + let players = randomPlayerLocations(playerIDs, constraints); + if (!players) + return undefined; + + for (let player of players) + createBase(player); + + return players; +} + +/** + * Choose arbitrary starting locations. + */ +function randomPlayerLocations(playerIDs, constraints = undefined) +{ let locations = []; let attempts = 0; let resets = 0; let mapCenter = g_Map.getCenter(); let playerMinDist = fractionToTiles(0.25); - let borderDistance = tilesToFraction(0.08); + 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) + 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) { error("Could not place playerbases!"); return undefined; } } continue; } locations[i] = position; } - - let players = groupPlayersByLocations(playerIDs, locations); - for (let player of players) - createBase(player); - - return players; + return groupPlayersByLocations(playerIDs, locations); } /** * Pick locations from the given set so that teams end up grouped. * * @param {Array} playerIDs - sorted by teams. * @param {Array} locations - array of Vector2D of possible starting locations. */ function groupPlayersByLocations(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; } }); let players = []; for (let i = 0; i < playerIDs.length; ++i) players[i] = { "id": playerIDs[i], "position": minLocations[i] }; return players; } /** * Place given players in a stronghold-pattern. * * @param teamsArray - each item is an array of playerIDs placed per stronghold * @param distance - radial distance from the center of the map * @param groupedDistance - distance between neighboring players * @param {number} startAngle - determined by the map that might want to place something between players */ function placeStronghold(teamsArray, distance, groupedDistance, startAngle) { var players = []; var mapCenter = g_Map.getCenter(); for (let i = 0; i < teamsArray.length; ++i) { var teamAngle = startAngle + (i + 1) * 2 * Math.PI / teamsArray.length; var teamPosition = Vector2D.add(mapCenter, new Vector2D(distance, 0).rotate(-teamAngle)); var teamGroupDistance = groupedDistance; // If we have a team of above average size, make sure they're spread out if (teamsArray[i].length > 4) teamGroupDistance = Math.max(fractionToTiles(0.08), groupedDistance); // If we have a solo player, place them on the center of the team's location if (teamsArray[i].length == 1) teamGroupDistance = fractionToTiles(0); // TODO: Ensure players are not placed outside of the map area, similar to placeLine // Create player base for (var p = 0; p < teamsArray[i].length; ++p) { var angle = startAngle + (p + 1) * 2 * Math.PI / teamsArray[i].length; players[teamsArray[i][p]] = { "id": teamsArray[i][p], "position": Vector2D.add(teamPosition, new Vector2D(teamGroupDistance, 0).rotate(-angle)).round() }; createBase(players[teamsArray[i][p]], false); } } return players; } /** * Creates tileClass for the default classes and every class given. * * @param {Array} newClasses * @returns {Object} - maps from classname to ID */ function initTileClasses(newClasses) { var classNames = g_DefaultTileClasses; if (newClasses) classNames = classNames.concat(newClasses); g_TileClasses = {}; for (var className of classNames) g_TileClasses[className] = g_Map.createTileClass(); }