1 Engine.LoadLibrary("rmgen");
2 Engine.LoadLibrary("rmgen-common");
3 Engine.LoadLibrary("rmbiome");
4 Engine.LoadLibrary("heightmap");
6 function* GenerateMap()
8 const tGrove = "temp_grass_plants";
9 const tPath = "road_rome_a";
11 const oGroveEntities = ["structures/gaul/outpost", "gaia/tree/oak_new"];
13 globalThis.g_Map = new RandomMap(0, "whiteness");
16 * Design resource spots
20 "actor|geology/gray1.xml",
21 "actor|geology/gray_rock1.xml",
22 "actor|geology/highland1.xml",
23 "actor|geology/highland2.xml",
24 "actor|geology/highland3.xml",
25 "actor|geology/highland_c.xml",
26 "actor|geology/highland_d.xml",
27 "actor|geology/highland_e.xml",
28 "actor|props/flora/bush.xml",
29 "actor|props/flora/bush_dry_a.xml",
30 "actor|props/flora/bush_highlands.xml",
31 "actor|props/flora/bush_tempe_a.xml",
32 "actor|props/flora/bush_tempe_b.xml",
33 "actor|props/flora/ferns.xml"
36 function placeMine(point, centerEntity)
38 g_Map.placeEntityPassable(centerEntity, 0, point, randomAngle());
39 const quantity = randIntInclusive(11, 23);
40 const dAngle = 2 * Math.PI / quantity;
42 for (let i = 0; i < quantity; ++i)
43 g_Map.placeEntityPassable(
44 pickRandom(decorations),
47 new Vector2D(randFloat(2, 5), 0).rotate(-dAngle * randFloat(i, i + 1))),
51 // Food, fences with domestic animals
52 g_WallStyles.other = {
54 "fence": readyWallElement("structures/fence_long", "gaia"),
55 "fence_short": readyWallElement("structures/fence_short", "gaia"),
61 "templateName": "structures/bench"
68 "templateName": "gaia/fauna_sheep"
75 "templateName": "gaia/treasure/food_bin"
82 "templateName": "structures/brit/farmstead"
87 new Fortress("fence", [
88 "foodBin", "farmstead", "bench",
89 "turn_0.25", "sheep", "turn_0.25", "fence",
90 "turn_0.25", "sheep", "turn_0.25", "fence",
91 "turn_0.25", "sheep", "turn_0.25", "fence"
93 new Fortress("fence", [
94 "foodBin", "farmstead", "fence",
95 "turn_0.25", "sheep", "turn_0.25", "fence",
96 "turn_0.25", "sheep", "turn_0.25", "bench", "sheep", "fence",
97 "turn_0.25", "sheep", "turn_0.25", "fence"
99 new Fortress("fence", [
100 "foodBin", "farmstead", "turn_0.5", "bench", "turn_-0.5", "fence_short",
101 "turn_0.25", "sheep", "turn_0.25", "fence",
102 "turn_0.25", "sheep", "turn_0.25", "fence",
103 "turn_0.25", "sheep", "turn_0.25", "fence_short", "sheep", "fence"
105 new Fortress("fence", [
106 "foodBin", "farmstead", "turn_0.5", "fence_short", "turn_-0.5", "bench",
107 "turn_0.25", "sheep", "turn_0.25", "fence",
108 "turn_0.25", "sheep", "turn_0.25", "fence",
109 "turn_0.25", "sheep", "turn_0.25", "fence_short", "sheep", "fence"
111 new Fortress("fence", [
112 "foodBin", "farmstead", "fence",
113 "turn_0.25", "sheep", "turn_0.25", "bench", "sheep", "fence",
114 "turn_0.25", "sheep", "turn_0.25", "fence_short", "sheep", "fence",
115 "turn_0.25", "sheep", "turn_0.25", "fence_short", "sheep", "fence"
117 ].flatMap(fence => [fence, new Fortress("fence", clone(fence.wall).reverse())]);
120 const groveEntities = ["gaia/tree/bush_temperate", "gaia/tree/euro_beech"];
121 const groveActors = [
122 "actor|geology/highland1_moss.xml",
123 "actor|geology/highland2_moss.xml",
124 "actor|props/flora/bush.xml",
125 "actor|props/flora/bush_dry_a.xml",
126 "actor|props/flora/bush_highlands.xml",
127 "actor|props/flora/bush_tempe_a.xml",
128 "actor|props/flora/bush_tempe_b.xml",
129 "actor|props/flora/ferns.xml"
132 function placeGrove(point)
134 g_Map.placeEntityPassable(pickRandom(oGroveEntities), 0, point, randomAngle());
135 const quantity = randIntInclusive(20, 30);
136 const dAngle = 2 * Math.PI / quantity;
137 for (let i = 0; i < quantity; ++i)
139 const angle = dAngle * randFloat(i, i + 1);
140 const dist = randFloat(2, 5);
141 let objectList = groveEntities;
143 objectList = groveActors;
144 const position = Vector2D.add(point, new Vector2D(dist, 0).rotate(-angle));
145 g_Map.placeEntityPassable(pickRandom(objectList), 0, position, randomAngle());
147 new ClumpPlacer(5, 1, 1, Infinity, position),
148 new TerrainPainter(tGrove));
152 // Camps with fire and gold treasure
153 function placeCamp(point)
155 const centerEntity = "actor|props/special/eyecandy/campfire.xml";
156 const otherEntities = [
157 "gaia/treasure/metal",
158 "gaia/treasure/standing_stone",
159 "units/brit/infantry_slinger_b",
160 "units/brit/infantry_javelineer_b",
161 "units/gaul/infantry_slinger_b",
162 "units/gaul/infantry_javelineer_b",
163 "units/gaul/champion_fanatic",
164 "actor|props/special/common/waypoint_flag.xml",
165 "actor|props/special/eyecandy/barrel_a.xml",
166 "actor|props/special/eyecandy/basket_celt_a.xml",
167 "actor|props/special/eyecandy/crate_a.xml",
168 "actor|props/special/eyecandy/dummy_a.xml",
169 "actor|props/special/eyecandy/handcart_1.xml",
170 "actor|props/special/eyecandy/handcart_1_broken.xml",
171 "actor|props/special/eyecandy/sack_1.xml",
172 "actor|props/special/eyecandy/sack_1_rough.xml"
174 g_Map.placeEntityPassable(centerEntity, 0, point, randomAngle());
175 const quantity = randIntInclusive(5, 11);
176 const dAngle = 2 * Math.PI / quantity;
177 for (let i = 0; i < quantity; ++i)
179 const angle = dAngle * randFloat(i, i + 1);
180 const dist = randFloat(1, 3);
181 g_Map.placeEntityPassable(pickRandom(otherEntities), 0,
182 Vector2D.add(point, new Vector2D(dist, 0).rotate(-angle)), randomAngle());
186 function placeStartLocationResources(point)
188 const foodEntities = ["gaia/fruit/berry_01", "gaia/fauna_chicken", "gaia/fauna_chicken"];
189 let currentAngle = randomAngle();
191 let dAngle = 4/9 * Math.PI;
192 let angle = currentAngle + randFloat(1, 3) * dAngle / 4;
193 const stonePosition = Vector2D.add(point, new Vector2D(12, 0).rotate(-angle));
194 placeMine(stonePosition, "gaia/rock/temperate_large");
195 currentAngle += dAngle;
199 dAngle = 2 * Math.PI / quantity / 3;
200 for (let i = 0; i < quantity; ++i)
202 angle = currentAngle + randFloat(0, dAngle);
203 let objectList = groveEntities;
205 objectList = groveActors;
207 Vector2D.add(point, new Vector2D(randFloat(10, 15), 0).rotate(-angle));
208 g_Map.placeEntityPassable(pickRandom(objectList), 0, woodPosition, randomAngle());
210 new ClumpPlacer(5, 1, 1, Infinity, woodPosition),
211 new TerrainPainter("temp_grass_plants"));
212 currentAngle += dAngle;
216 dAngle = 2 * Math.PI * 2 / 9;
217 angle = currentAngle + dAngle * randFloat(1, 3) / 4;
218 const metalPosition = Vector2D.add(point, new Vector2D(13, 0).rotate(-angle));
219 placeMine(metalPosition, "gaia/ore/temperate_large");
220 currentAngle += dAngle;
224 dAngle = 2 * Math.PI / quantity * 2 / 9;
225 for (let i = 0; i < quantity; ++i)
227 angle = currentAngle + randFloat(0, dAngle);
228 const berriesPosition =
229 Vector2D.add(point, new Vector2D(randFloat(10, 15), 0).rotate(-angle));
230 g_Map.placeEntityPassable(pickRandom(foodEntities), 0, berriesPosition, randomAngle());
231 currentAngle += dAngle;
236 * Environment settings
238 setBiome("generic/alpine");
239 g_Environment.Fog.FogColor = { "r": 0.8, "g": 0.8, "b": 0.8, "a": 0.01 };
240 g_Environment.Water.WaterBody.Colour = { "r": 0.3, "g": 0.05, "b": 0.1, "a": 0.1 };
241 g_Environment.Water.WaterBody.Murkiness = 0.4;
244 * Base terrain shape generation and settings
246 const heightScale = (g_Map.size + 256) / 768 / 4;
247 const heightRange = { "min": MIN_HEIGHT * heightScale, "max": MAX_HEIGHT * heightScale };
250 // NOTE: Since terrain generation is quite unpredictable actual water
251 // coverage might vary much with the same value
252 const averageWaterCoverage = 1 / 5;
253 // Water height in environment and the engine
254 const heightSeaGround = -MIN_HEIGHT + heightRange.min + averageWaterCoverage *
255 (heightRange.max - heightRange.min);
256 // Water height in RMGEN
257 const heightSeaGroundAdjusted = heightSeaGround + MIN_HEIGHT;
258 setWaterHeight(heightSeaGround);
260 g_Map.log("Generating terrain using diamon-square");
261 const medH = (heightRange.min + heightRange.max) / 2;
262 const initialHeightmap = [[medH, medH], [medH, medH]];
263 setBaseTerrainDiamondSquare(heightRange.min, heightRange.max, initialHeightmap, 0.8);
265 g_Map.log("Apply erosion");
266 for (let i = 0; i < 5; ++i)
269 rescaleHeightmap(heightRange.min, heightRange.max);
273 const heighLimits = [
284 // 5 Player and path height
288 // 7 Lower forest border
292 // 9 Upper forest border
296 ].map(([underWater, ratio]) => {
297 const base = underWater ? heightRange.min : heightSeaGround;
298 const factor = underWater ? heightSeaGroundAdjusted - heightRange.min :
299 heightRange.max - heightSeaGroundAdjusted;
300 return base + ratio * factor;
303 const playerHeight = (heighLimits[4] + heighLimits[5]) / 2; // Average player height
305 g_Map.log("Determining height-dependent biome");
306 // Texture and actor presets
311 "texture": ["shoreline_stoney_a"],
312 "entity": ["gaia/fish/generic", "actor|geology/stone_granite_boulder.xml"],
313 "entityPropability": 0.02
316 "texture": ["alpine_mountainside"],
317 "entity": ["gaia/fish/generic"],
318 "entityPropability": 0.1
325 "texture": ["shoreline_stoney_a", "alpine_shore_rocks"],
328 "actor|geology/stone_granite_boulder.xml",
329 "actor|geology/stone_granite_med.xml"
331 "entityPropability": 0.03
334 "texture": ["alpine_mountainside"],
337 "actor|geology/stone_granite_boulder.xml",
338 "actor|geology/stone_granite_med.xml"
340 "entityPropability": 0.0
347 "texture": ["alpine_shore_rocks"],
349 "actor|props/flora/reeds_pond_dry.xml",
350 "actor|geology/stone_granite_large.xml",
351 "actor|geology/stone_granite_med.xml",
352 "actor|props/flora/reeds_pond_lush_b.xml"
354 "entityPropability": 0.2
357 "texture": ["alpine_mountainside"],
360 "actor|props/flora/reeds_pond_dry.xml",
361 "actor|geology/stone_granite_med.xml"
363 "entityPropability": 0.1
370 "texture": ["alpine_shore_rocks_grass_50", "alpine_grass_rocky"],
373 "gaia/tree/bush_badlands",
374 "actor|geology/highland1_moss.xml",
375 "actor|props/flora/grass_soft_tuft_a.xml",
376 "actor|props/flora/bush.xml"
378 "entityPropability": 0.3
381 "texture": ["alpine_mountainside"],
382 "entity": ["actor|props/flora/grass_soft_tuft_a.xml"],
383 "entityPropability": 0.1
390 "texture": ["alpine_dirt_grass_50", "alpine_grass_rocky"],
392 "actor|geology/stone_granite_med.xml",
393 "actor|props/flora/grass_soft_tuft_a.xml",
394 "actor|props/flora/bush.xml",
395 "actor|props/flora/grass_medit_flowering_tall.xml"
397 "entityPropability": 0.2
400 "texture": ["alpine_grass_rocky"],
403 "actor|geology/stone_granite_med.xml",
404 "actor|props/flora/grass_soft_tuft_a.xml"
406 "entityPropability": 0.1
410 // 5 Player and path height
413 "texture": ["new_alpine_grass_c", "new_alpine_grass_b", "new_alpine_grass_d"],
415 "actor|geology/stone_granite_small.xml",
416 "actor|props/flora/grass_soft_small.xml",
417 "actor|props/flora/grass_medit_flowering_tall.xml"
419 "entityPropability": 0.2
422 "texture": ["alpine_grass_rocky"],
425 "actor|geology/stone_granite_small.xml",
426 "actor|props/flora/grass_soft_small.xml"
428 "entityPropability": 0.1
435 "texture": ["new_alpine_grass_a", "alpine_grass_rocky"],
437 "actor|geology/stone_granite_med.xml",
438 "actor|props/flora/grass_tufts_a.xml",
439 "actor|props/flora/bush_highlands.xml",
440 "actor|props/flora/grass_medit_flowering_tall.xml"
442 "entityPropability": 0.2
445 "texture": ["alpine_grass_rocky"],
448 "actor|geology/stone_granite_med.xml",
449 "actor|props/flora/grass_tufts_a.xml"
451 "entityPropability": 0.1
455 // 7 Lower forest border
458 "texture": ["new_alpine_grass_mossy", "alpine_grass_rocky"],
462 "actor|props/flora/grass_tufts_a.xml",
463 "gaia/fruit/berry_01",
464 "actor|geology/highland2_moss.xml",
466 "actor|props/flora/bush_tempe_underbrush.xml"
468 "entityPropability": 0.3
471 "texture": ["alpine_cliff_c"],
474 "actor|props/flora/grass_tufts_a.xml",
475 "actor|geology/highland2_moss.xml"
477 "entityPropability": 0.1
484 "texture": ["alpine_forrestfloor"],
490 "actor|geology/highland2_moss.xml",
491 "actor|props/flora/bush_highlands.xml"
493 "entityPropability": 0.5
496 "texture": ["alpine_cliff_c"],
498 "actor|geology/highland2_moss.xml", "actor|geology/stone_granite_med.xml"
500 "entityPropability": 0.1
504 // 9 Upper forest border
507 "texture": ["alpine_forrestfloor_snow", "new_alpine_grass_dirt_a"],
508 "entity": ["gaia/tree/pine", "actor|geology/snow1.xml"],
509 "entityPropability": 0.3
512 "texture": ["alpine_cliff_b"],
513 "entity": ["actor|geology/stone_granite_med.xml", "actor|geology/snow1.xml"],
514 "entityPropability": 0.1
521 "texture": ["alpine_cliff_a", "alpine_cliff_snow"],
522 "entity": ["actor|geology/highland1.xml"],
523 "entityPropability": 0.05
526 "texture": ["alpine_cliff_c"],
527 "entity": ["actor|geology/highland1.xml"],
528 "entityPropability": 0.0
533 const [playerIDs, playerPosition] = groupPlayersCycle(
534 getStartLocationsByHeightmap({ "min": heighLimits[4], "max": heighLimits[5] }, 1000, 30));
537 g_Map.log("Smoothing player locations");
538 for (const position of playerPosition)
540 new DiskPlacer(35, position),
541 new SmoothElevationPainter(ELEVATION_SET, g_Map.getHeight(position), 35));
543 g_Map.log("Creating paths between players");
544 const clPath = g_Map.createTileClass();
545 for (let i = 0; i < playerPosition.length; ++i)
547 new RandomPathPlacer(playerPosition[i],
548 playerPosition[(i + 1) % playerPosition.length], 4, 2, false),
550 new TerrainPainter(tPath),
551 new ElevationBlendingPainter(playerHeight, 0.4),
552 new TileClassPainter(clPath)
555 g_Map.log("Smoothing paths");
557 new MapBoundsPlacer(),
558 new SmoothingPainter(5, 1, 1),
559 new NearTileClassConstraint(clPath, 5));
563 g_Map.log("Determining resource locations");
564 const avoidPoints = playerPosition.map(pos => pos.clone());
565 avoidPoints.forEach(point => { point.dist = 30; });
566 const resourceSpots = getPointsByHeight(
568 "min": (heighLimits[3] + heighLimits[4]) / 2,
569 "max": (heighLimits[5] + heighLimits[6]) / 2
576 * Divide tiles in areas by height and avoid paths
578 const tchm = getTileCenteredHeightmap();
579 const areas = heighLimits.map(heightLimit => []);
580 for (let x = 0; x < tchm.length; ++x)
581 for (let y = 0; y < tchm[0].length; ++y)
583 const position = new Vector2D(x, y);
584 if (!avoidClasses(clPath, 0).allows(position) || tchm[x][y] < heightRange.min)
587 const index = heighLimits.findIndex(limit => tchm[x][y] <= limit);
589 areas[index].push(position);
593 * Get midpoint slope of each area
595 const slopeMap = getSlopeMap();
596 const slopeMidpoints = areas.map(area => {
597 const slopesInThisArea = area.map(({ x, y }) => slopeMap[x][y]);
598 return Math.min(...slopesInThisArea) + Math.max(...slopesInThisArea);
601 g_Map.log("Painting areas by height and slope");
602 for (let h = 0; h < heighLimits.length; ++h)
603 for (const point of areas[h])
605 const isFlat = slopeMap[point.x][point.y] < 0.4 * slopeMidpoints[h];
606 const selectedBiome = myBiome[h][isFlat? "flat" : "steep"];
608 g_Map.setTexture(point, pickRandom(selectedBiome.texture));
610 if (randBool(selectedBiome.entityPropability))
611 g_Map.placeEntityPassable(pickRandom(selectedBiome.entity), 0,
612 randomPositionOnTile(point), randomAngle());
616 g_Map.log("Placing players");
618 placePlayersNomad(g_Map.createTileClass(),
619 new HeightConstraint(heighLimits[4], heighLimits[5]));
621 for (let p = 0; p < playerIDs.length; ++p)
623 placeCivDefaultStartingEntities(playerPosition[p], playerIDs[p], true);
624 placeStartLocationResources(playerPosition[p]);
627 g_Map.log("Placing resources, farmsteads, groves and camps");
628 for (let i = 0; i < resourceSpots.length; ++i)
630 const pos = new Vector2D(resourceSpots[i].x, resourceSpots[i].y);
631 const choice = i % (isNomad() ? 4 : 5);
633 placeMine(pos, "gaia/rock/temperate_large_02");
635 placeMine(pos, "gaia/ore/temperate_large");
637 placeCustomFortress(pos, pickRandom(fences), "other", 0, randomAngle());