Source: rmgen-common/gaia_entities.js

/**
 * @file These functions are often used to place gaia entities, like forests, mines, animals or decorative bushes.
 */

/**
 * Returns the number of trees in forests and straggler trees.
 */
function getTreeCounts(minTrees, maxTrees, forestRatio)
{
	return [forestRatio, 1 - forestRatio].map(p => p * scaleByMapSize(minTrees, maxTrees));
}

/**
 * Places uniformly sized forests at random locations.
 * Unless you want a custom number of forest, prefer createDefaultForests.
 * Generates two variants of forests from the given terrain textures and tree templates.
 * The forest border has less trees than the inside.
 * @param terrainsSet - a list of 5 terrains to use. The first 3 are border terrains, the later 2 interior.
 * @param constraint - constraints to respect
 * @param tileClass - the tileclass to print
 * @param treeCount - Either { "nbForests": X, "treesPerForest": X } or (legacy) a number of trees.
 * @param retryFactor - @see createAreas
 */
function createForests(terrainSet, constraint, tileClass, treeCount, retryFactor)
{
	if (!treeCount)
		return;

	// Construct different forest types from the terrain textures and template names.
	const [mainTerrain, terrainForestFloor1, terrainForestFloor2, terrainForestTree1, terrainForestTree2] = terrainSet;

	// The painter will pick a random Terrain for each part of the forest.
	const forestVariants = [
		{
			"borderTerrains": [terrainForestFloor2, mainTerrain, terrainForestTree1],
			"interiorTerrains": [terrainForestFloor2, terrainForestTree1]
		},
		{
			"borderTerrains": [terrainForestFloor1, mainTerrain, terrainForestTree2],
			"interiorTerrains": [terrainForestFloor1, terrainForestTree2]
		}
	];

	let numberOfTrees;
	let numberOfForests;
	if (typeof treeCount === "number")
	{
		numberOfTrees = treeCount;
		numberOfForests = Math.floor(numberOfTrees / (scaleByMapSize(3, 6) * getNumPlayers() * forestVariants.length));
	}
	else
	{
		numberOfForests = treeCount.nbForests;
		numberOfTrees = numberOfForests * treeCount.treesPerForest;
	}

	if (!numberOfForests)
		return;

	g_Map.log("Creating forests");
	for (const forestVariant of forestVariants)
		createAreas(
			new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), numberOfTrees / numberOfForests, 0.5),
			[
				new LayeredPainter([forestVariant.borderTerrains, forestVariant.interiorTerrains], [2]),
				new TileClassPainter(tileClass)
			],
			constraint,
			numberOfForests,
			retryFactor);
}

var g_DefaultNumberOfForests = scaleByMapSize(8, 36);

/**
 * Passes some sane defaults to createForests.
 */
function createDefaultForests(terrainSet, constraints, tileClass, totalNumberOfTrees)
{
	return createForests(
		terrainSet,
		constraints,
		tileClass,
		{
			"nbForests": g_DefaultNumberOfForests,
			"treesPerForest": totalNumberOfTrees / g_DefaultNumberOfForests,
		}
	);
}


/**
 * Places the given amount of Entities at random places meeting the given Constraint, chosing a different template for each.
 */
function createStragglerTrees(templateNames, constraint, tileClass, treeCount, retryFactor)
{
	g_Map.log("Creating straggler trees");
	for (let templateName of templateNames)
		createObjectGroupsDeprecated(
			new SimpleGroup([new SimpleObject(templateName, 1, 1, 0, 3)], true, tileClass),
			0,
			constraint,
			Math.floor(treeCount / templateNames.length),
			retryFactor);
}

/**
 * Places a SimpleGroup consisting of the given number of the given Objects
 * at random locations that meet the given Constraint.
 */
function createMines(objects, constraint, tileClass, count)
{
	for (let object of objects)
		createObjectGroupsDeprecated(
			new SimpleGroup(object, true, tileClass),
			0,
			constraint,
			count || scaleByMapSize(4, 16),
			70);
}

/**
 * Place large/small mines on the map in such a way that it should be relatively fair.
 * @param oSmall - the small mine object
 * @param oLarge - the large mine object
 * @param clMine - the 'mine' class to paint.
 * @param constraints - Custom constraints. Note that the function automatically avoids clMine as well.
 * @param counts - a dict of numbers { "largeCount": 10, "smallCount": 100, "randomSmallCount": 2 }
 * @param randomness - randomize counts by a random multiplier between [1 - randomness, 1 + randomness]
 */
function createBalancedMines(oSmall, oLarge, clMine, constraints, counts, randomness)
{
	let largeCount = counts.largeCount;
	let smallCount = counts.smallCount;
	let randomSmallCount = counts.randomSmallCount;

	if (randomness > 0 && randomness < 1)
	{
		largeCount = Math.round(largeCount * randFloat(1 - randomness, 1 + randomness));
		smallCount = Math.round(smallCount * randFloat(1 - randomness, 1 + randomness));
		randomSmallCount = Math.round(randomSmallCount * randFloat(1 - randomness, 1 + randomness));
	}

	const arrayConstraints = Array.isArray(constraints) ? constraints : [constraints];

	// Plop large mines far away from each other.
	createObjectGroups(
		new SimpleGroup([new SimpleObject(oLarge, 1, 1, 0, 1)], true, clMine),
		0,
		new AndConstraint([avoidClasses(clMine, scaleByMapSize(25, 50)), ...arrayConstraints]),
		largeCount,
		100);

	// Plop smaller clusters of small mines, also somewhat farther away.
	createObjectGroups(
		new SimpleGroup([new SimpleObject(oSmall, 2, 3, 0, 2)], true, clMine),
		0,
		new AndConstraint([avoidClasses(clMine, scaleByMapSize(18, 35)), ...arrayConstraints]),
		smallCount,
		50);

	// Plop a few smaller clusters in a random fashion, occasionally making very good dropsites spots.
	createObjectGroups(
		new SimpleGroup([new SimpleObject(oSmall, 1, 2, 0, 2)], true, clMine),
		0,
		new AndConstraint([avoidClasses(clMine, 5), ...arrayConstraints]),
		randomSmallCount,
		50);
}

/**
 * Helper for createBalancedMines with default metal counts.
 * The current settings are so that a Small 1v1 has about 40K metal,
 * and a Normal 4v4 has about 140K.
 * The setup is biaised so that with fewer players, there are more small mines,
 * and with more players there are proportionally more big mines, to maintain
 * some randomness to the distribution but keep it somewhat fair in 1v1.
 */
function createBalancedMetalMines(oSmall, oLarge, clMine, constraints, counts = 1.0, randomness = 0.05)
{
	return createBalancedMines(
		oSmall,
		oLarge,
		clMine,
		constraints,
		{
			"largeCount": (Math.max(scaleByMapSize(1, 9), getNumPlayers() * 1.8 - 0.8)) * counts,
			"smallCount": (scaleByMapSize(4, 12)) * counts,
			"randomSmallCount": (scaleByMapSize(1, 8)) * counts,
		},
		randomness
	);
}

/**
 * Helper for createBalancedMines with default stone counts.
 * There is a little less stone than metal overall.
 */
function createBalancedStoneMines(oSmall, oLarge, clMine, constraints, counts = 1.0, randomness = 0.05)
{
	return createBalancedMines(
		oSmall,
		oLarge,
		clMine,
		constraints,
		{
			"largeCount": (Math.max(scaleByMapSize(1, 9), getNumPlayers() * 1.25)) * counts,
			"smallCount": (scaleByMapSize(1, 8)) * counts,
			"randomSmallCount": (scaleByMapSize(1, 8)) * counts,
		},
		randomness
	);
}


/**
 * Places Entities of the given templateName in a circular pattern (leaving out a quarter of the circle).
 */
function createStoneMineFormation(position, templateName, terrain, radius = 2.5, count = 8, startAngle = undefined, maxOffset = 1)
{
	createArea(
		new ChainPlacer(radius / 2, radius, 2, Infinity, position, undefined, [5]),
		new TerrainPainter(terrain));

	let angle = startAngle !== undefined ? startAngle : randomAngle();

	for (let i = 0; i < count; ++i)
	{
		let pos = Vector2D.add(position, new Vector2D(radius + randFloat(0, maxOffset), 0).rotate(-angle)).round();
		g_Map.placeEntityPassable(templateName, 0, pos, randomAngle());
		angle += 3/2 * Math.PI / count;
	}
}

/**
 * Places the given amounts of the given Objects at random locations meeting the given Constraint.
 */
function createFood(objects, counts, constraint, tileClass)
{
	g_Map.log("Creating food");
	for (let i = 0; i < objects.length; ++i)
		createObjectGroupsDeprecated(
			new SimpleGroup(objects[i], true, tileClass),
			0,
			constraint,
			counts[i],
			50);
}

/**
 * Same as createFood, but doesn't mark the terrain with a TileClass.
 */
function createDecoration(objects, counts, constraint)
{
	g_Map.log("Creating decoration");
	for (let i = 0; i < objects.length; ++i)
		createObjectGroupsDeprecated(
			new SimpleGroup(objects[i], true),
			0,
			constraint,
			counts[i],
			5);
}

/**
 * Places docks in situations where the location of land and water is not known in advance.
 * Do determine the position, it picks a random point on the land, find the closest, significantly large body of water,
 * then places the dock at the first point close to that body of water within the given heightrange.
 *
 * @param {string} template - The template name of the dock to be placed.
 * @param {number} playerID - The owner of the dock.
 * @param {number} count - The number of docks to be placed.
 * @param {Object} tileClassWater - The tileclass the water area is marked with.
 * @param {Object} tileClassDock - The dock position is marked with this class.
 * @param {number} heightMin - The lowest height a dock could be placed.
 * @param {number} heightMax - The greatest height a dock could be placed.
 * @param {Array|Constraint} constraints - Only consider dock positions valid that meet this Constraint.
 * @param {number} offset - How many tiles to move the dock towards the direction of the water after having found a location.
 * @param {number} retryFactor- How many different locations should be tested.
 */
function placeDocks(template, playerID, count, tileClassWater, tileClassDock, heightMin, heightMax, constraints, offset, retryFactor)
{
	let mapCenter = g_Map.getCenter();

	g_Map.log("Marking dock search start area");
	let areaSearchStart = createArea(
		new DiskPlacer(fractionToTiles(0.5) - 10, mapCenter),
		undefined,
		avoidClasses(tileClassWater, 6));

	g_Map.log("Marking dock search end area");
	let areaSearchEnd = createArea(
		new DiskPlacer(fractionToTiles(0.5) - 10, mapCenter),
		undefined,
		stayClasses(tileClassWater, 20));

	g_Map.log("Marking land area");
	let areaLand = createArea(
		new MapBoundsPlacer(),
		undefined,
		avoidClasses(tileClassWater, 0));

	g_Map.log("Marking water area");
	let areaWater = createArea(
		new MapBoundsPlacer(),
		undefined,
		stayClasses(tileClassWater, 0));

	if (!areaSearchEnd || !areaSearchEnd.getPoints().length)
		return;

	// TODO: computing the exact intersection with the waterplane would both not require us to pass reasonable heights and be more precise
	let constraint = new AndConstraint(constraints);
	g_Map.log("Placing docks");
	for (let i = 0; i < count; ++i)
		for (let tries = 0; tries < retryFactor; ++tries)
		{
			let positionLand = pickRandom(areaSearchStart.getPoints());
			let positionWaterLarge = areaSearchEnd.getClosestPointTo(positionLand);
			let positionDock = findLocationInDirectionBasedOnHeight(positionWaterLarge, positionLand, heightMin, heightMax, offset);
			if (!positionDock)
				continue;

			positionDock.round();

			if (!g_Map.inMapBounds(positionDock) || !constraint.allows(positionDock))
				continue;

			let angle = positionDock.angleTo(Vector2D.average(new DiskPlacer(8, positionDock).place(stayClasses(tileClassWater, 0))));

			g_Map.placeEntityPassable(template, playerID, positionDock, -angle + Math.PI / 2);
			tileClassDock.add(positionDock);
			break;
		}
}