Index: ps/trunk/binaries/data/mods/public/maps/random/rivers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rivers.js (revision 20516) +++ ps/trunk/binaries/data/mods/public/maps/random/rivers.js (revision 20517) @@ -1,352 +1,352 @@ RMS.LoadLibrary("rmgen"); RMS.LoadLibrary("rmbiome"); setSelectedBiome(); const tMainTerrain = g_Terrains.mainTerrain; const tForestFloor1 = g_Terrains.forestFloor1; const tForestFloor2 = g_Terrains.forestFloor2; const tCliff = g_Terrains.cliff; const tTier1Terrain = g_Terrains.tier1Terrain; const tTier2Terrain = g_Terrains.tier2Terrain; const tTier3Terrain = g_Terrains.tier3Terrain; const tHill = g_Terrains.hill; const tRoad = g_Terrains.road; const tRoadWild = g_Terrains.roadWild; const tTier4Terrain = g_Terrains.tier4Terrain; var tShore = g_Terrains.shore; var tWater = g_Terrains.water; if (currentBiome() == "tropic") { tShore = "tropic_dirt_b_plants"; tWater = "tropic_dirt_b"; } const oTree1 = g_Gaia.tree1; const oTree2 = g_Gaia.tree2; const oTree3 = g_Gaia.tree3; const oTree4 = g_Gaia.tree4; const oTree5 = g_Gaia.tree5; const oFruitBush = g_Gaia.fruitBush; const oMainHuntableAnimal = g_Gaia.mainHuntableAnimal; const oFish = g_Gaia.fish; const oSecondaryHuntableAnimal = g_Gaia.secondaryHuntableAnimal; const oStoneLarge = g_Gaia.stoneLarge; const oStoneSmall = g_Gaia.stoneSmall; const oMetalLarge = g_Gaia.metalLarge; const aGrass = g_Decoratives.grass; const aGrassShort = g_Decoratives.grassShort; const aReeds = g_Decoratives.reeds; const aLillies = g_Decoratives.lillies; const aRockLarge = g_Decoratives.rockLarge; const aRockMedium = g_Decoratives.rockMedium; const aBushMedium = g_Decoratives.bushMedium; const aBushSmall = g_Decoratives.bushSmall; const pForest1 = [tForestFloor2 + TERRAIN_SEPARATOR + oTree1, tForestFloor2 + TERRAIN_SEPARATOR + oTree2, tForestFloor2]; const pForest2 = [tForestFloor1 + TERRAIN_SEPARATOR + oTree4, tForestFloor1 + TERRAIN_SEPARATOR + oTree5, tForestFloor1]; InitMap(); const numPlayers = getNumPlayers(); const mapSize = getMapSize(); const mapArea = getMapArea(); var clPlayer = createTileClass(); var clHill = createTileClass(); var clForest = createTileClass(); var clWater = createTileClass(); var clDirt = createTileClass(); var clRock = createTileClass(); var clMetal = createTileClass(); var clFood = createTileClass(); var clBaseResource = createTileClass(); var clShallow = createTileClass(); var waterHeight = -3; var shallowHeight = -1; initTerrain(tMainTerrain); log("Creating central lake..."); var centralLake = [0.5, 0.5]; createArea( new ClumpPlacer( mapArea / 100 * Math.pow(scaleByMapSize(1, 6), 1/8), 0.7, 0.1, 10, fractionToTiles(centralLake[0]), fractionToTiles(centralLake[1])), [ new LayeredPainter([tShore, tWater, tWater, tWater], [1, 4, 2]), new SmoothElevationPainter(ELEVATION_SET, waterHeight, 4), paintClass(clWater) ], null); var [playerIDs, playerX, playerZ, playerAngle, startAngle] = radialPlayerPlacement(); for (var i = 0; i < numPlayers; i++) { var id = playerIDs[i]; log("Creating base for player " + id + "..."); var radius = scaleByMapSize(15,25); var cliffRadius = 2; var elevation = 20; // get the x and z in tiles var fx = fractionToTiles(playerX[i]); var fz = fractionToTiles(playerZ[i]); var ix = round(fx); var iz = round(fz); addCivicCenterAreaToClass(ix, iz, clPlayer); // create the city patch var cityRadius = radius/3; var placer = new ClumpPlacer(PI*cityRadius*cityRadius, 0.6, 0.3, 10, ix, iz); var painter = new LayeredPainter([tRoadWild, tRoad], [1]); createArea(placer, painter, null); placeCivDefaultEntities(fx, fz, id); placeDefaultChicken(fx, fz, clBaseResource); // create berry bushes var bbAngle = randFloat(0, TWO_PI); var bbDist = 12; var bbX = round(fx + bbDist * cos(bbAngle)); var bbZ = round(fz + bbDist * sin(bbAngle)); var group = new SimpleGroup( [new SimpleObject(oFruitBush, 5,5, 0,3)], true, clBaseResource, bbX, bbZ ); createObjectGroup(group, 0); // create metal mine var mAngle = bbAngle; while(abs(mAngle - bbAngle) < PI/3) mAngle = randFloat(0, TWO_PI); var mDist = 12; var mX = round(fx + mDist * cos(mAngle)); var mZ = round(fz + mDist * sin(mAngle)); group = new SimpleGroup( [new SimpleObject(oMetalLarge, 1,1, 0,0)], true, clBaseResource, mX, mZ ); createObjectGroup(group, 0); // create stone mines mAngle += randFloat(PI/8, PI/4); mX = round(fx + mDist * cos(mAngle)); mZ = round(fz + mDist * sin(mAngle)); group = new SimpleGroup( [new SimpleObject(oStoneLarge, 1,1, 0,2)], true, clBaseResource, mX, mZ ); createObjectGroup(group, 0); // create starting trees var num = 2; var tAngle = randFloat(-PI/3, 4*PI/3); var tDist = randFloat(11, 13); var tX = round(fx + tDist * cos(tAngle)); var tZ = round(fz + tDist * sin(tAngle)); group = new SimpleGroup( [new SimpleObject(oTree1, num, num, 0,5)], false, clBaseResource, tX, tZ ); createObjectGroup(group, 0, avoidClasses(clBaseResource,2)); placeDefaultDecoratives(fx, fz, aGrassShort, clBaseResource, radius); } RMS.SetProgress(20); log("Creating rivers between opponents..."); var [riverX, riverZ] = distributePointsOnCircle(numPlayers, startAngle + Math.PI / numPlayers, 0.5, ...centralLake); for (let i = 0; i < numPlayers; ++i) { - if (areAllies(playerIDs[i] - 1, playerIDs[(i + 1) % numPlayers] - 1)) + if (areAllies(playerIDs[i], playerIDs[(i + 1) % numPlayers])) continue; let shallowLocation = randFloat(0.2, 0.7); let shallowWidth = randFloat(0.12, 0.21); paintRiver({ "parallel": true, "startX": riverX[i], "startZ": riverZ[i], "endX": centralLake[0], "endZ": centralLake[1], "width": tilesToFraction(scaleByMapSize(10, 30)), "fadeDist": tilesToFraction(5), "deviation": 0, "landHeight": getMapBaseHeight(), "waterHeight": waterHeight, "minHeight": waterHeight, "meanderShort": tilesToFraction(scaleByMapSize(20, 60) * scaleByMapSize(35, 160)), "meanderLong": 0, "waterFunc": (ix, iz, height, riverFraction) => { addToClass(ix, iz, clWater); let isShallow = height < shallowHeight && riverFraction > shallowLocation && riverFraction < shallowLocation + shallowWidth; let newHeight = isShallow ? shallowHeight : Math.max(height, waterHeight); if (getHeight(ix, iz) < newHeight) return; setHeight(ix, iz, newHeight); placeTerrain(ix, iz, height >= 0 ? tShore : tWater); if (isShallow) addToClass(ix, iz, clShallow); } }); } RMS.SetProgress(40); createBumps(avoidClasses(clWater, 2, clPlayer, 20)); if (randBool()) createHills([tMainTerrain, tCliff, tHill], avoidClasses(clPlayer, 20, clHill, 15, clWater, 2), clHill, scaleByMapSize(3, 15)); else createMountains(tCliff, avoidClasses(clPlayer, 20, clHill, 15, clWater, 2), clHill, scaleByMapSize(3, 15)); var [forestTrees, stragglerTrees] = getTreeCounts(...rBiomeTreeCount(1)); createForests( [tMainTerrain, tForestFloor1, tForestFloor2, pForest1, pForest2], avoidClasses(clPlayer, 20, clForest, 17, clHill, 0, clWater, 2), clForest, forestTrees); RMS.SetProgress(50); log("Creating dirt patches..."); createLayeredPatches( [scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)], [[tMainTerrain,tTier1Terrain],[tTier1Terrain,tTier2Terrain], [tTier2Terrain,tTier3Terrain]], [1,1], avoidClasses(clWater, 3, clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12), scaleByMapSize(15, 45), clDirt); log("Creating grass patches..."); createPatches( [scaleByMapSize(2, 4), scaleByMapSize(3, 7), scaleByMapSize(5, 15)], tTier4Terrain, avoidClasses(clWater, 3, clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12), scaleByMapSize(15, 45), clDirt); RMS.SetProgress(55); log("Creating stone mines..."); createMines( [ [new SimpleObject(oStoneSmall, 0,2, 0,4), new SimpleObject(oStoneLarge, 1,1, 0,4)], [new SimpleObject(oStoneSmall, 2,5, 1,3)] ], avoidClasses(clWater, 3, clForest, 1, clPlayer, 20, clRock, 10, clHill, 1), clRock); log("Creating metal mines..."); createMines( [ [new SimpleObject(oMetalLarge, 1,1, 0,4)] ], avoidClasses(clWater, 3, clForest, 1, clPlayer, 20, clMetal, 10, clRock, 5, clHill, 1), clMetal ); RMS.SetProgress(65); var planetm = 1; if (currentBiome() == "tropic") planetm = 8; createDecoration ( [[new SimpleObject(aRockMedium, 1,3, 0,1)], [new SimpleObject(aRockLarge, 1,2, 0,1), new SimpleObject(aRockMedium, 1,3, 0,2)], [new SimpleObject(aGrassShort, 1,2, 0,1, -PI/8,PI/8)], [new SimpleObject(aGrass, 2,4, 0,1.8, -PI/8,PI/8), new SimpleObject(aGrassShort, 3,6, 1.2,2.5, -PI/8,PI/8)], [new SimpleObject(aBushMedium, 1,2, 0,2), new SimpleObject(aBushSmall, 2,4, 0,2)] ], [ scaleByMapSize(16, 262), scaleByMapSize(8, 131), planetm * scaleByMapSize(13, 200), planetm * scaleByMapSize(13, 200), planetm * scaleByMapSize(13, 200) ], avoidClasses(clWater, 0, clForest, 0, clPlayer, 0, clHill, 0) ); // create water decoration in the shallow parts createDecoration ( [[new SimpleObject(aReeds, 1,3, 0,1)], [new SimpleObject(aLillies, 1,2, 0,1)] ], [ scaleByMapSize(800, 12800), scaleByMapSize(800, 12800) ], stayClasses(clShallow, 0) ); RMS.SetProgress(70); createFood ( [ [new SimpleObject(oMainHuntableAnimal, 5,7, 0,4)], [new SimpleObject(oSecondaryHuntableAnimal, 2,3, 0,2)] ], [ 3 * numPlayers, 3 * numPlayers ], avoidClasses(clWater, 3, clForest, 0, clPlayer, 20, clHill, 1, clFood, 20) ); createFood ( [ [new SimpleObject(oFruitBush, 5,7, 0,4)] ], [ 3 * numPlayers ], avoidClasses(clWater, 3, clForest, 0, clPlayer, 20, clHill, 1, clFood, 10) ); createFood ( [ [new SimpleObject(oFish, 2,3, 0,2)] ], [ 25 * numPlayers ], [avoidClasses(clFood, 20), stayClasses(clWater, 6)] ); RMS.SetProgress(85); createStragglerTrees( [oTree1, oTree2, oTree4, oTree3], avoidClasses(clWater, 5, clForest, 7, clHill, 1, clPlayer, 12, clMetal, 6, clRock, 6), clForest, stragglerTrees); setWaterWaviness(3.0); setWaterType("lake"); 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 20516) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js (revision 20517) @@ -1,524 +1,523 @@ const PI = Math.PI; const TWO_PI = 2 * Math.PI; const TERRAIN_SEPARATOR = "|"; const SEA_LEVEL = 20.0; const HEIGHT_UNITS_PER_METRE = 92; const MAP_BORDER_WIDTH = 3; /** * 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 = RMS.GetTerrainTileSize(); const MAX_HEIGHT = MAX_HEIGHT_RANGE - SEA_LEVEL; // Default angle for buildings const BUILDING_ORIENTATION = - PI / 4; function fractionToTiles(f) { return g_Map.size * f; } function tilesToFraction(t) { return t / g_Map.size; } function fractionToSize(f) { return getMapArea() * f; } function sizeToFraction(s) { return s / getMapArea(); } function scaleByMapSize(min, max, minMapSize = 128, maxMapSize = 512) { return min + (max - min) * (g_Map.size - minMapSize) / (maxMapSize - minMapSize); } function cos(x) { return Math.cos(x); } function sin(x) { return Math.sin(x); } function abs(x) { return Math.abs(x); } function round(x) { return Math.round(x); } function lerp(a, b, t) { return a + (b-a) * t; } function sqrt(x) { return Math.sqrt(x); } function ceil(x) { return Math.ceil(x); } function floor(x) { return Math.floor(x); } function max(a, b) { return a > b ? a : b; } function min(a, b) { return a < b ? a : b; } /** * 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 = randFloat(0, 2 * Math.PI); 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; } // 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) { if (!(terrain instanceof Array)) return createSimpleTerrain(terrain); return new RandomTerrain(terrain.map(t => createTerrain(t))); } function createSimpleTerrain(terrain) { if (typeof(terrain) != "string") throw new Error("createSimpleTerrain expects string as input, received " + uneval(terrain)); // Split string by pipe | character, this allows specifying terrain + tree type in single string let params = terrain.split(TERRAIN_SEPARATOR, 2); if (params.length != 2) return new SimpleTerrain(terrain); return new SimpleTerrain(params[0], params[1]); } function placeObject(x, z, type, player, angle) { if (g_Map.validT(x, z)) g_Map.addObject(new Entity(type, player, x, z, angle)); } function placeTerrain(x, z, terrainNames) { createTerrain(terrainNames).place(x, z); } function initTerrain(terrainNames) { let terrain = createTerrain(terrainNames); for (let x = 0; x < getMapSize(); ++x) for (let z = 0; z < getMapSize(); ++z) terrain.place(x, z); } function isCircularMap() { return !!g_MapSettings.CircularMap; } function getMapBaseHeight() { return g_MapSettings.BaseHeight; } 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 Constraint and calls the Painters there. * Supports both Centered and Non-Centered Placers. */ function createArea(placer, painter, constraint) { if (!constraint) constraint = new NullConstraint(); else if (constraint instanceof Array) constraint = new AndConstraint(constraint); let points = placer.place(constraint); 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 Constraint * and sets the given player as the owner. */ function createObjectGroup(group, player, constraint) { if (!constraint) constraint = new NullConstraint(); else if (constraint instanceof Array) constraint = new AndConstraint(constraint); return group.place(player, constraint); } function getMapSize() { return g_Map.size; } function getMapArea() { return Math.square(g_Map.size); } function getMapCenter() { return deepfreeze(new Vector2D(g_Map.size / 2, g_Map.size / 2)); } function getNumPlayers() { return g_MapSettings.PlayerData.length - 1; } function getCivCode(playerID) { return g_MapSettings.PlayerData[playerID].Civ; } -function areAllies(player1, player2) +function areAllies(playerID1, playerID2) { - if (g_MapSettings.PlayerData[player1+1].Team === undefined || - g_MapSettings.PlayerData[player2+1].Team === undefined || - g_MapSettings.PlayerData[player2+1].Team == -1 || - g_MapSettings.PlayerData[player1+1].Team == -1) - return false; - - return g_MapSettings.PlayerData[player1+1].Team === g_MapSettings.PlayerData[player2+1].Team; + 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(player) +function getPlayerTeam(playerID) { - if (g_MapSettings.PlayerData[player+1].Team === undefined) + if (g_MapSettings.PlayerData[playerID].Team === undefined) return -1; - return g_MapSettings.PlayerData[player+1].Team; + return g_MapSettings.PlayerData[playerID].Team; } function getHeight(x, z) { return g_Map.getHeight(x, z); } function setHeight(x, z, height) { g_Map.setHeight(x, z, height); } function initHeight(height) { g_Map.initHeight(height); } /** * 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); } /** * Checks if the given tile is in class "id" */ function checkIfInClass(x, z, id) { let tileClass = getTileClass(id); if (tileClass === null) return 0; let members = tileClass.countMembersInRadius(x, z, 1); if (members === null) return 0; return members; } function getTerrainTexture(x, y) { return g_Map.getTexture(x, 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 20516) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/player.js (revision 20517) @@ -1,261 +1,259 @@ /** * @file These functions locate and place the starting entities of players. */ /** * 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 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(fx, fz, playerID, civEntities, dist = 6, orientation = BUILDING_ORIENTATION) { // Place the central structure let i = 0; let firstTemplate = civEntities[i].Template; if (firstTemplate.startsWith("structures/")) { placeObject(fx, fz, firstTemplate, playerID, 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) placeObject( fx + dist * Math.cos(angle) + space * (-num + 0.75 * Math.floor(count / 2)) * Math.sin(angle), fz + dist * Math.sin(angle) + space * (num - 0.75 * Math.floor(count / 2)) * Math.cos(angle), civEntities[j].Template, playerID, angle); } } /** * Places the default starting entities as defined by the civilization definition and walls for Iberians. */ function placeCivDefaultEntities(fx, fz, playerID, kwargs, dist = 6, orientation = BUILDING_ORIENTATION) { placeStartingEntities(fx, fz, playerID, getStartingEntities(playerID), dist, orientation); let civ = getCivCode(playerID); if (civ == 'iber' && getMapSize() > 128) { if (kwargs && kwargs.iberWall == 'towers') placePolygonalWall(fx, fz, 15, ['entry'], 'tower', civ, playerID, orientation, 7); else if (!kwargs || kwargs.iberWall) placeGenericFortress(fx, fz, 20, playerID); } } /** * 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(ix, iz, tileClass) { addToClass(ix, iz, tileClass); addToClass(ix, iz + 5, tileClass); addToClass(ix, iz - 5, tileClass); addToClass(ix + 5, iz, tileClass); addToClass(ix - 5, iz, tileClass); } /** * 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((p1, p2) => getPlayerTeam(p1 - 1) - getPlayerTeam(p2 - 1)); + 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 radialPlayerPlacement(radius = 0.35, startingAngle = undefined, centerX = 0.5, centerZ = 0.5) { let startAngle = startingAngle !== undefined ? startingAngle : randFloat(0, 2 * Math.PI); return [sortAllPlayers(), ...distributePointsOnCircle(getNumPlayers(), startAngle, radius, centerX, centerZ), startAngle]; } /** * 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) { let positions = []; let numPlayers = getNumPlayers(); let numPlayersEven = numPlayers % 2 == 0; let mapCenter = new Vector2D(0.5, 0.5); 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; positions[i] = new Vector2D( width * (i % 2) + (1 - width) / 2, ((i - 1 + offsetDivident) / 2 + 1) / ((numPlayers + offsetDivisor) / 2 + 1)).rotateAround(angle, mapCenter); } return [primeSortAllPlayers(), positions.map(p => p.x), positions.map(p => p.y)]; } /*** * Returns starting positions located on two parallel lines. * The locations on the first line are shifted in comparison to the other line. * The players are grouped per team and hence they can be found on both lines. */ function playerPlacementLine(horizontal, center, width) { let playerX = []; let playerZ = []; let numPlayers = getNumPlayers(); for (let i = 0; i < numPlayers; ++i) { playerX[i] = (i + 1) / (numPlayers + 1); playerZ[i] = center + width * (i % 2 - 1/2); if (!horizontal) [playerX[i], playerZ[i]] = [playerZ[i], playerX[i]]; } return [sortAllPlayers(), playerX, playerZ]; } /** * Sorts the playerIDs so that team members are as close as possible. */ function sortPlayersByLocation(startLocations) { // Sort start locations to form a "ring" 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 p1 = playerIDs[(pi + s) % playerIDs.length] - 1; - let t1 = getPlayerTeam(p1); + let t1 = getPlayerTeam(playerIDs[(pi + s) % playerIDs.length]); if (teams.indexOf(t1) === -1) continue; for (let pj = pi + 1; pj < playerIDs.length; ++pj) { - let p2 = playerIDs[(pj + s) % playerIDs.length] - 1; - if (t1 != getPlayerTeam(p2)) + 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/rmgen2/setup.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js (revision 20516) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js (revision 20517) @@ -1,620 +1,621 @@ 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; } /** * Paints the entire map with the given terrain texture, tileclass and elevation. */ function resetTerrain(terrain, tileClass, elevation) { let center = Math.round(fractionToTiles(0.5)); createArea( new ClumpPlacer(getMapArea(), 1, 1, 1, center, center), [ new LayeredPainter([terrain], []), new SmoothElevationPainter(ELEVATION_SET, elevation, 1), paintClass(tileClass) ], null); } /** * 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) { 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) { var mapSize = getMapSize(); // Get the x and z in tiles var fx = fractionToTiles(player.x); var fz = fractionToTiles(player.z); var ix = round(fx); var iz = round(fz); addCivicCenterAreaToClass(ix, iz, g_TileClasses.player); if (walls && mapSize > 192) placeCivDefaultEntities(fx, fz, player.id); else placeCivDefaultEntities(fx, fz, player.id, { 'iberWall': false }); // Create the city patch var radius = scaleByMapSize(15, 25); var cityRadius = radius / 3; var placer = new ClumpPlacer(PI * cityRadius * cityRadius, 0.6, 0.3, 10, ix, iz); var painter = new LayeredPainter([g_Terrains.roadWild, g_Terrains.road], [1]); createArea(placer, painter, null); // TODO: retry loops are needed as resources might conflict with neighboring ones // Create initial berry bushes at random angle var bbAngle = randFloat(0, TWO_PI); var bbDist = 10; var bbX = round(fx + bbDist * cos(bbAngle)); var bbZ = round(fz + bbDist * sin(bbAngle)); var group = new SimpleGroup( [new SimpleObject(g_Gaia.fruitBush, 5, 5, 0, 3)], true, g_TileClasses.baseResource, bbX, bbZ ); createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 2)); // Create metal mine at a different angle var mAngle = bbAngle; while(abs(mAngle - bbAngle) < PI / 3) mAngle = randFloat(0, TWO_PI); var mDist = 12; var mX = round(fx + mDist * cos(mAngle)); var mZ = round(fz + mDist * sin(mAngle)); group = new SimpleGroup( [new SimpleObject(g_Gaia.metalLarge, 1, 1, 0, 0)], true, g_TileClasses.baseResource, mX, mZ ); createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 2)); // Create stone mine beside metal mAngle += randFloat(PI / 8, PI / 4); mX = round(fx + mDist * cos(mAngle)); mZ = round(fz + mDist * sin(mAngle)); group = new SimpleGroup( [new SimpleObject(g_Gaia.stoneLarge, 1, 1, 0, 2)], true, g_TileClasses.baseResource, mX, mZ ); createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 2)); placeDefaultChicken( fx, fz, g_TileClasses.baseResource, avoidClasses(g_TileClasses.baseResource, 4), g_Gaia.chicken ); // Create starting trees var num = currentBiome() == "savanna" ? 5 : 15; for (var tries = 0; tries < 10; ++tries) { var tAngle = randFloat(0, TWO_PI); var tDist = randFloat(12, 13); var tX = round(fx + tDist * cos(tAngle)); var tZ = round(fz + tDist * sin(tAngle)); group = new SimpleGroup( [new SimpleObject(g_Gaia.tree1, num, num, 1, 3)], false, g_TileClasses.baseResource, tX, tZ ); if (createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 4))) break; } placeDefaultDecoratives( fx, fz, g_Decoratives.grassShort, g_TileClasses.baseResource, radius, avoidClasses(g_TileClasses.baseResource, 4)); } /** * 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(i); + let team = getPlayerTeam(playerIDs[i]); if (team == -1) continue; if (!teams[team]) teams[team] = []; - teams[team].push(i+1); + teams[team].push(playerIDs[i]); } // Players without a team get a custom index for (let i = 0; i < numPlayers; ++i) - if (getPlayerTeam(i) == -1) - teams.push([i+1]); + 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 = getMapSize(); 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": randFloat(0.2, 0.35), "separation": 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) { var players = []; for (let i = 0; i < teamsArray.length; ++i) { var safeDist = distance; if (distance + teamsArray[i].length * groupedDistance > 0.45) safeDist = 0.45 - teamsArray[i].length * groupedDistance; var teamAngle = startAngle + (i + 1) * 2 * Math.PI / teamsArray.length; // Create player base for (var p = 0; p < teamsArray[i].length; ++p) { players[teamsArray[i][p]] = { "id": teamsArray[i][p], "x": 0.5 + (safeDist + p * groupedDistance) * cos(teamAngle), "z": 0.5 + (safeDist + p * groupedDistance) * sin(teamAngle) }; 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 players = []; let numPlayers = getNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let angle = startAngle + i * 2 * Math.PI / numPlayers; players[i] = { "id": playerIDs[i], "x": 0.5 + distance * cos(angle), "z": 0.5 + distance * sin(angle) }; createBase(players[i]); } return players; } /** * Choose arbitrary starting locations. */ function placeRandom(playerIDs) { var locations = []; var attempts = 0; var resets = 0; for (let i = 0; i < getNumPlayers(); ++i) { var playerAngle = randFloat(0, TWO_PI); // Distance from the center of the map in percent // Mapsize being used as a diameter, so 0.5 is the edge of the map var distance = randFloat(0, 0.42); var x = 0.5 + distance * cos(playerAngle); var z = 0.5 + distance * sin(playerAngle); // Minimum distance between initial bases must be a quarter of the map diameter if (locations.some(loc => Math.euclidDistance2D(x, z, loc.x, loc.z) < 0.25)) { --i; ++attempts; // Reset if we're in what looks like an infinite loop if (attempts > 100) { locations = []; i = -1; attempts = 0; ++resets; // If we only pick bad locations, stop trying to place randomly if (resets == 100) return undefined; } continue; } locations[i] = { "x": x, "z": z }; } let players = groupPlayersByLocations(playerIDs, locations); for (let player of players) createBase(player); return players; } /** * Pick locations from the given set so that teams end up grouped. * * @param {Array} playerIDs - sorted by teams. * @param {Array} locations - array of x/z pairs 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), function(permutation) { let dist = 0; let teamDist = 0; let teamSize = 0; for (let i = 1; i < playerIDs.length; ++i) { let team1 = g_MapSettings.PlayerData[playerIDs[i - 1]].Team; let team2 = g_MapSettings.PlayerData[playerIDs[i]].Team; ++teamSize; if (team1 != -1 && team1 == team2) teamDist += Math.euclidDistance2D(permutation[i - 1].x, permutation[i - 1].z, permutation[i].x, permutation[i].z); 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) { let player = minLocations[i]; player.id = playerIDs[i]; players.push(player); } 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 = []; for (let i = 0; i < teamsArray.length; ++i) { var teamAngle = startAngle + (i + 1) * 2 * Math.PI / teamsArray.length; var fractionX = 0.5 + distance * cos(teamAngle); var fractionZ = 0.5 + distance * sin(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(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 = 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], "x": fractionX + teamGroupDistance * cos(angle), "z": fractionZ + teamGroupDistance * sin(angle) }; createBase(players[teamsArray[i][p]], false); } } return players; } /** * Places players either randomly or in a stronghold-pattern at a set of given heightmap coordinates. * * @param teamsArray - Array where each item is an array of playerIDs, possibly going to be grouped. * @param singleBases - pair of coordinates of the heightmap to place isolated bases. * @param singleBases - pair of coordinates of the heightmap to place team bases. * @param groupedDistance - distance between neighboring players. * @param func - A function called for every player base or stronghold placed. */ function randomPlayerPlacementAt(teamsArray, singleBases, strongholdBases, heightmapScale, groupedDistance, func) { let strongholdBasesRandom = shuffleArray(strongholdBases); let mapSize = getMapSize(); if (randBool(1/3) && mapSize >= 256 && teamsArray.length >= 2 && teamsArray.length < getNumPlayers() && teamsArray.length <= strongholdBasesRandom.length) { let startAngle = randFloat(0, 2 * Math.PI); for (let t = 0; t < teamsArray.length; ++t) { let tileX = Math.floor(strongholdBasesRandom[t][0] / heightmapScale); let tileY = Math.floor(strongholdBasesRandom[t][1] / heightmapScale); let x = tileX / mapSize; let z = tileY / mapSize; let team = teamsArray[t].map(playerID => ({ "id": playerID })); let players = []; if (func) func(tileX, tileY); for (let p = 0; p < team.length; ++p) { let angle = startAngle + (p + 1) * TWO_PI / team.length; players[p] = { "id": team[p].id, "x": x + groupedDistance * cos(angle), "z": z + groupedDistance * sin(angle) }; createBase(players[p], false); } } } else { let players = groupPlayersByLocations(sortAllPlayers(), singleBases.map(l => ({ "x": l[0] / heightmapScale / mapSize, "z": l[1] / heightmapScale / mapSize }))); for (let player of players) { if (func) func(Math.floor(player.x * mapSize), Math.floor(player.z * mapSize)); createBase(player); } } } /** * 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 !== undefined) classNames = classNames.concat(newClasses); g_TileClasses = {}; for (var className of classNames) g_TileClasses[className] = createTileClass(); }