// Created by Niek ten Brinke (aka niektb)
// Based on FeXoR's Daimond Square Algorithm for heightmap generation and several official random maps

'use strict';

RMS.LoadLibrary('rmgen');

// initialize map

log('Initializing map...');

InitMap();


////////////////
//
//  Initializing
//
////////////////

//sky
setSkySet("fog");
setFogFactor(0.35);
setFogThickness(0.19);

// water
setWaterColour(0.501961, 0.501961, 0.501961);
setWaterTint(0.25098, 0.501961, 0.501961);
setWaterWaviness(0.5);
setWaterType("clap");
setWaterMurkiness(0.75);

// post processing
setPPSaturation(0.37);
setPPContrast(0.4);
setPPBrightness(0.4);
setPPEffect("hdr");
setPPBloom(0.4);

// Setup tile classes
var clPlayer = createTileClass();
var clPath = createTileClass();
var clHill = createTileClass();
var clForest = createTileClass();
var clWater = createTileClass();
var clRock = createTileClass();
var clFood = createTileClass();
var clBaseResource = createTileClass();
var clOpen = createTileClass();

// Setup Templates
var templateStone = 'gaia/geology_stone_alpine_a';
var templateStoneMine = 'gaia/geology_stonemine_alpine_quarry';
var templateMetal = 'actor|geology/stone_granite_med.xml';
var templateMetalMine = 'gaia/geology_metal_alpine_slabs';
var startingResources = ['gaia/flora_tree_pine', 'gaia/flora_tree_pine','gaia/flora_tree_pine', templateStoneMine,
	'gaia/flora_bush_grapes', 'gaia/flora_tree_aleppo_pine','gaia/flora_tree_aleppo_pine','gaia/flora_tree_aleppo_pine', 'gaia/flora_bush_berry', templateMetalMine];
var aGrass = 'actor|props/flora/grass_soft_small_tall.xml';
var aGrassShort = 'actor|props/flora/grass_soft_large.xml';
var aRockLarge = 'actor|geology/stone_granite_med.xml';
var aRockMedium = 'actor|geology/stone_granite_med.xml';
var aBushMedium = 'actor|props/flora/bush_medit_me.xml';
var aBushSmall = 'actor|props/flora/bush_medit_sm.xml';
var aReeds = 'actor|props/flora/reeds_pond_lush_b.xml';
var oFish = "gaia/fauna_fish";


// Setup terrain
var terrainWood = ['alpine_forrestfloor|gaia/flora_tree_oak', 'alpine_forrestfloor|gaia/flora_tree_pine']; 
var terrainWoodBorder = ['new_alpine_grass_mossy|gaia/flora_tree_oak', 'alpine_forrestfloor|gaia/flora_tree_pine',
	'temp_grass_long|gaia/flora_bush_temperate', 'temp_grass_clovers|gaia/flora_bush_berry', 'temp_grass_clovers_2|gaia/flora_bush_grapes',
	'temp_grass_plants|gaia/fauna_deer', 'temp_grass_plants|gaia/fauna_rabbit', 'new_alpine_grass_dirt_a'];
var terrainBase = ['temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_grass_plants|gaia/fauna_sheep'];
var terrainBaseBorder = ['temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
	'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants'];
var terrainBaseCenter = ['temp_dirt_gravel_b'];
var baseTex = ['temp_road', 'temp_road_overgrown'];
var terrainPath = ['temp_road', 'temp_road_overgrown'];
var terrainHill = ['temp_highlands', 'temp_highlands', 'temp_highlands', 'temp_grass_plants_b', 'temp_cliff_a'];
var terrainHillBorder = ['temp_highlands', 'temp_highlands', 'temp_highlands', 'temp_grass_plants_b', 'temp_grass_plants_plants',
	'temp_highlands', 'temp_highlands', 'temp_highlands', 'temp_grass_plants_b', 'temp_grass_plants_plants',
	'temp_highlands', 'temp_highlands', 'temp_highlands', 'temp_cliff_b', 'temp_grass_plants_plants',
	'temp_highlands', 'temp_highlands', 'temp_highlands', 'temp_cliff_b', 'temp_grass_plants_plants',
	'temp_highlands|gaia/fauna_goat'];
var tWater = ['dirt_brown_d'];
var tWaterBorder = ['dirt_brown_d'];

const BUILDING_ANGlE = -PI/4;

// Setup map 
var mapSize = getMapSize();
var mapRadius = mapSize/2;
var playableMapRadius = mapRadius - 5;
var mapCenterX = mapRadius;
var mapCenterZ = mapRadius;

// Setup players and bases
var numPlayers = getNumPlayers();
var baseRadius = 15;
var minPlayerRadius = min(mapRadius-1.5*baseRadius, 5*mapRadius/8);
var maxPlayerRadius = min(mapRadius-baseRadius, 3*mapRadius/4);

var playerStartLocX = new Array(numPlayers);
var playerStartLocZ = new Array(numPlayers);
var playerAngle = new Array(numPlayers);
var playerAngleStart = randFloat(0, 2*PI);
var playerAngleAddAvrg = 2*PI / numPlayers;
var playerAngleMaxOff = playerAngleAddAvrg/4;

// Setup paths
var pathSucsessRadius = baseRadius/2;
var pathAngleOff = PI/2;
var pathWidth = 10; // This is not really the path's thickness in tiles but the number of tiles in the clumbs of the path

// Setup additional resources
var resourceRadius = 2*mapRadius/3; // 3*mapRadius/8;
//var resourcePerPlayer = [templateStone, templateMetalMine];

// Setup woods
// For large maps there are memory errors with too many trees.  A density of 256*192/mapArea works with 0 players.
// Around each player there is an area without trees so with more players the max density can increase a bit.
var maxTreeDensity = min(256 * (192 + 8 * numPlayers) / (mapSize * mapSize), 1); // Has to be tweeked but works ok 
var bushChance = 1/3; // 1 means 50% chance in deepest wood, 0.5 means 25% chance in deepest wood


////////////////
//
//  Some general functions
//
////////////////

function HeightPlacer(lowerBound, upperBound) {
    this.lowerBound = lowerBound;
    this.upperBound = upperBound;
}

HeightPlacer.prototype.place = function (constraint) {
	constraint = (constraint || new NullConstraint());
	
    var ret = [];
    for (var x = 0; x < g_Map.size; x++) {
        for (var y = 0; y < g_Map.size; y++) {
            if (g_Map.height[x][y] >= this.lowerBound && g_Map.height[x][y] <= this.upperBound && constraint.allows(x, y)) {
                ret.push(new PointXZ(x, y));
            }
        }
    }
    return ret;
};

/*
Takes an array of 2D points (arrays of length 2)
Returns the order to go through the points for the shortest closed path (array of indices)
*/
function getOrderOfPointsForShortestClosePath(points)
{
	var order = [];
	var distances = [];
	if (points.length <= 3)
	{
		for (var i = 0; i < points.length; i++)
			order.push(i);
		
		return order;
	}
	
	// Just add the first 3 points
	var pointsToAdd = deepcopy(points);
	for (var i = 0; i < min(points.length, 3); i++)
	{
		order.push(i);
		pointsToAdd.shift(i);
		if (i)
			distances.push(getDistance(points[order[i]][0], points[order[i]][1], points[order[i - 1]][0], points[order[i - 1]][1]));
	}
	distances.push(getDistance(points[order[0]][0], points[order[0]][1], points[order[order.length - 1]][0], points[order[order.length - 1]][1]));
	
	// Add remaining points so the path lengthens the least
	var numPointsToAdd = pointsToAdd.length;
	for (var i = 0; i < numPointsToAdd; i++)
	{
		var indexToAddTo = undefined;
		var minEnlengthen = Infinity;
		var minDist1 = 0;
		var minDist2 = 0;
		for (var k = 0; k < order.length; k++)
		{
			var dist1 = getDistance(pointsToAdd[0][0], pointsToAdd[0][1], points[order[k]][0], points[order[k]][1]);
			var dist2 = getDistance(pointsToAdd[0][0], pointsToAdd[0][1], points[order[(k + 1) % order.length]][0], points[order[(k + 1) % order.length]][1]);
			var enlengthen = dist1 + dist2 - distances[k];
			if (enlengthen < minEnlengthen)
			{
				indexToAddTo = k;
				minEnlengthen = enlengthen;
				minDist1 = dist1;
				minDist2 = dist2;
			}
		}
		order.splice(indexToAddTo + 1, 0, i + 3);
		distances.splice(indexToAddTo, 1, minDist1, minDist2);
		pointsToAdd.shift();
	}
	
	return order;
}


////////////////
//
//  Heightmap functionality
//
////////////////

// Some heightmap constants
const MIN_HEIGHT = - SEA_LEVEL; // -20
const MAX_HEIGHT = 0xFFFF/HEIGHT_UNITS_PER_METRE - SEA_LEVEL; // A bit smaller than 90

// Get the diferrence between minimum and maxumum height
function getMinAndMaxHeight(reliefmap)
{
	var height = {};
	height.min = Infinity;
	height.max = - Infinity;
	for (var x = 0; x < reliefmap.length; x++)
	{
		for (var y = 0; y < reliefmap[x].length; y++)
		{
			if (reliefmap[x][y] < height.min)
				height.min = reliefmap[x][y];
			else if (reliefmap[x][y] > height.max)
				height.max = reliefmap[x][y];
		}
	}
	return height;
}

function rescaleHeightmap(minHeight, maxHeight, heightmap)
{
	minHeight = (minHeight || - SEA_LEVEL);
	maxHeight = (maxHeight || 0xFFFF / HEIGHT_UNITS_PER_METRE - SEA_LEVEL);
	heightmap = (heightmap || g_Map.height);
	
	var oldHeightRange = getMinAndMaxHeight(heightmap);
	var max_x = heightmap.length;
	var max_y = heightmap[0].length;
	for (var x = 0; x < max_x; x++)
		for (var y = 0; y < max_y; y++)
			heightmap[x][y] = minHeight + (heightmap[x][y] - oldHeightRange.min) / (oldHeightRange.max - oldHeightRange.min) * (maxHeight - minHeight);
}

/*
getStartLocationsByHeightmap
Takes
	hightRange		An associative array with keys 'min' and 'max' each containing a float (the height range start locations are allowed)
	heightmap		Optional, default is g_Map.height, an array of (map width) arrays of (map depth) floats
	maxTries		Optional, default is 1000, an integer, how often random player distributions are rolled to be compared
	minDistToBorder	Optional, default is 20, an integer, how far start locations have to be 
	numberOfPlayers	Optional, default is getNumPlayers, an integer, how many start locations should be placed
Returns
	An array of 2D points (arrays of length 2)
*/
function getStartLocationsByHeightmap(hightRange, maxTries, minDistToBorder, numberOfPlayers, heightmap)
{
	maxTries = (maxTries || 1000);
	minDistToBorder = (minDistToBorder || 20);
	numberOfPlayers = (numberOfPlayers || getNumPlayers());
	heightmap = (heightmap || g_Map.height);
	
	var validStartLocTiles = [];
	for (var x = minDistToBorder; x < heightmap.length - minDistToBorder; x++)
		for (var y = minDistToBorder; y < heightmap[0].length - minDistToBorder; y++)
			if (heightmap[x][y] > hightRange.min && heightmap[x][y] < hightRange.max) // Has the right hight
				validStartLocTiles.push([x, y]);
	
	var maxMinDist = 0;
	for (var tries = 0; tries < maxTries; tries++)
	{
		var startLoc = [];
		var minDist = heightmap.length;
		for (var p = 0; p < numberOfPlayers; p++)
			startLoc.push(validStartLocTiles[randInt(validStartLocTiles.length)]);
		for (var p1 = 0; p1 < numberOfPlayers - 1; p1++)
		{
			for (var p2 = p1 + 1; p2 < numberOfPlayers; p2++)
			{
				var dist = getDistance(startLoc[p1][0], startLoc[p1][1], startLoc[p2][0], startLoc[p2][1]);
				if (dist < minDist)
					minDist = dist;
			}
		}
		if (minDist > maxMinDist)
		{
			maxMinDist = minDist;
			var finalStartLoc = startLoc;
		}
	}
	
	return finalStartLoc;
}

/*
derivateEntitiesByHeight
Takes
	hightRange		An associative array with keys 'min' and 'max' each containing a float (the height range start locations are allowed)
	startLoc		An array of 2D points (arrays of length 2)
	heightmap		Optional, default is g_Map.height, an array of (map width) arrays of (map depth) floats
	entityList		Array of entities/actors (strings to be placed with placeObject())
	maxTries		Optional, default is 1000, an integer, how often random player distributions are rolled to be compared
	minDistance		Optional, default is 30, an integer, how far start locations have to be away from start locations and the map border
Returns
	An array of 2D points (arrays of length 2)
*/
function derivateEntitiesByHeight(hightRange, startLoc, entityList, maxTries, minDistance, heightmap)
{
	entityList = (entityList || [templateMetalMine, templateStoneMine]);
	maxTries = (maxTries || 1000);
	minDistance = (minDistance || 40);
	heightmap = (heightmap || g_Map.height);
	
	var placements = deepcopy(startLoc);
	var validTiles = [];
	for (var x = minDistance; x < heightmap.length - minDistance; x++)
		for (var y = minDistance; y < heightmap[0].length - minDistance; y++)
			if (heightmap[x][y] > hightRange.min && heightmap[x][y] < hightRange.max) // Has the right hight
				validTiles.push([x, y]);
	
	if (!validTiles.length)
		return;
	
	for (var tries = 0; tries < maxTries; tries++)
	{
		var tile = validTiles[randInt(validTiles.length)];
		var isValid = true;
		for (var p = 0; p < placements.length; p++)
		{
			if (getDistance(placements[p][0], placements[p][1], tile[0], tile[1]) < minDistance)
			{
				isValid = false;
				break;
			}
		}
		if (isValid)
		{
			placeObject(tile[0], tile[1], entityList[randInt(entityList.length)], 0, randFloat(0, 2*PI));
			// placeObject(tile[0], tile[1], 'actor|geology/decal_stone_medit_b.xml', 0, randFloat(0, 2*PI));
			placements.push(tile);
		}
	}
}


////////////////
//
//  Base terrain generation functionality
//
////////////////

function setBaseTerrainDiamondSquare(minHeight, maxHeight, smoothness, initialHeightmap, heightmap)
{
	// Make some arguments optional
	minHeight = (minHeight || 0);
	maxHeight = (maxHeight || 1);
	var heightRange = maxHeight - minHeight;
	if (heightRange <= 0)
		warn('setBaseTerrainDiamondSquare: heightRange < 0');
	smoothness = (smoothness || 1);
	
	var offset = heightRange / 2;
	initialHeightmap = (initialHeightmap || [[randFloat(minHeight / 2, maxHeight / 2), randFloat(minHeight / 2, maxHeight / 2)], [randFloat(minHeight / 2, maxHeight / 2), randFloat(minHeight / 2, maxHeight / 2)]]);
	
	// Double initialHeightmap width untill target width is reached (diamond square method)
	while (initialHeightmap.length < heightmap.length)
	{
		var newHeightmap = [];
		var oldWidth = initialHeightmap.length;
		// Square
		for (var x = 0; x < 2 * oldWidth - 1; x++)
		{
			newHeightmap.push([]);
			for (var y = 0; y < 2 * oldWidth - 1; y++)
			{
				if (x % 2 === 0 && y % 2 === 0) // Old tile
					newHeightmap[x].push(initialHeightmap[x/2][y/2]);
				else if (x % 2 == 1 && y % 2 == 1) // New tile with diagonal old tile neighbors
				{
					newHeightmap[x].push((initialHeightmap[(x-1)/2][(y-1)/2] + initialHeightmap[(x+1)/2][(y-1)/2] + initialHeightmap[(x-1)/2][(y+1)/2] + initialHeightmap[(x+1)/2][(y+1)/2]) / 4);
					newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
				}
				else // New tile with straight old tile neighbors
					newHeightmap[x].push(undefined); // Define later
			}
		}
		// Diamond
		for (var x = 0; x < 2 * oldWidth - 1; x++)
		{
			for (var y = 0; y < 2 * oldWidth - 1; y++)
			{
				if (newHeightmap[x][y] === undefined)
				{
					if (x > 0 && x + 1 < newHeightmap.length - 1 && y > 0 && y + 1 < newHeightmap.length - 1) // Not a border tile
					{
						newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 4;
						newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
					}
					else if (x < newHeightmap.length - 1 && y > 0 && y < newHeightmap.length - 1) // Left border
					{
						newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x][y-1]) / 3;
						newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
					}
					else if (x > 0 && y > 0 && y < newHeightmap.length - 1) // Right border
					{
						newHeightmap[x][y] = (newHeightmap[x][y+1] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 3;
						newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
					}
					else if (x > 0 && x < newHeightmap.length - 1 && y < newHeightmap.length - 1) // Bottom border
					{
						newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x-1][y]) / 3;
						newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
					}
					else if (x > 0 && x < newHeightmap.length - 1 && y > 0) // Top border
					{
						newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 3;
						newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
					}
				}
			}
		}
		initialHeightmap = deepcopy(newHeightmap);
		offset /= Math.pow(2, smoothness);
	}
	
	// Cut initialHeightmap to fit target width
	var shift = [floor((newHeightmap.length - heightmap.length) / 2), floor((newHeightmap[0].length - heightmap[0].length) / 2)];
	for (var x = 0; x < heightmap.length; x++)
		for (var y = 0; y < heightmap[0].length; y++)
			heightmap[x][y] = newHeightmap[x][y];
}


////////////////
//
//  Terrain erosion functionality
//
////////////////

function decayErrodeHeightmap(strength, heightmap)
{
	strength = (strength || 0.9); // 0 to 1
	heightmap = (heightmap || g_Map.height);
	
	var referenceHeightmap = deepcopy(heightmap);
	// var map = [[1, 0], [0, 1], [-1, 0], [0, -1]]; // faster
	var map = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]]; // smoother
	var max_x = heightmap.length;
	var max_y = heightmap[0].length;
	for (var x = 0; x < max_x; x++)
		for (var y = 0; y < max_y; y++)
			for (var i = 0; i < map.length; i++)
				heightmap[x][y] += strength / map.length * (referenceHeightmap[(x + map[i][0] + max_x) % max_x][(y + map[i][1] + max_y) % max_y] - referenceHeightmap[x][y]); // Not entirely sure if scaling with map.length is perfect but tested values seam to indicate it is
}

function rectangularSmoothToHeight(center, dx, dy, targetHeight, strength, heightmap)
{
	var x = round(center[0]);
	var y = round(center[1]);
	dx = round(dx);
	dy = round(dy);
	strength = (strength || 1);
	heightmap = (heightmap || g_Map.height);
	
	var heightmapWin = [];
	for (var wx = 0; wx < 2 * dx + 1; wx++)
	{
		heightmapWin.push([]);
		for (var wy = 0; wy < 2 * dy + 1; wy++)
		{
			var actualX = x - dx + wx;
			var actualY = y - dy + wy;
			if (actualX >= 0 && actualX < heightmap.length - 1 && actualY >= 0 && actualY < heightmap[0].length - 1) // Is in map
				heightmapWin[wx].push(heightmap[actualX][actualY]);
			else
				heightmapWin[wx].push(targetHeight);
		}
	}
	for (var wx = 0; wx < 2 * dx + 1; wx++)
	{
		for (var wy = 0; wy < 2 * dy + 1; wy++)
		{
			var actualX = x - dx + wx;
			var actualY = y - dy + wy;
			if (actualX >= 0 && actualX < heightmap.length - 1 && actualY >= 0 && actualY < heightmap[0].length - 1) // Is in map
			{
				// Window function polynomial 2nd degree
				var scaleX = 1 - (wx / dx - 1) * (wx / dx - 1);
				var scaleY = 1 - (wy / dy - 1) * (wy / dy - 1);

				heightmap[actualX][actualY] = heightmapWin[wx][wy] + strength * scaleX * scaleY * (targetHeight - heightmapWin[wx][wy]);
			}
		}
	}
}


////////////////
//
//  Actually do stuff
//
////////////////

////////////////
// Set height limits and water level by map size
////////////////

// Set target min and max height depending on map size to make average steepness about the same on all map sizes
var heightRange = {'min': MIN_HEIGHT * (g_Map.size + 512) / 1024, 'max': MAX_HEIGHT * (g_Map.size + 512) / 1024, 'avg': ((MIN_HEIGHT * (g_Map.size + 512) / 1024)+(MAX_HEIGHT * (g_Map.size + 512) / 1024))/2};

// Set average water coverage
var averageWaterCoverage = 1/5; // NOTE: Since erosion is not predictable actual water coverage might vary much with the same values
var waterHeight = -MIN_HEIGHT + heightRange.min + averageWaterCoverage * (heightRange.max - heightRange.min);
var waterHeightAdjusted = waterHeight + MIN_HEIGHT;
setWaterHeight(waterHeight);

////////////////
// Generate base terrain
////////////////

// Setting a 3x3 Grid as initial heightmap
var initialReliefmap = [[heightRange.max, heightRange.max, heightRange.max], [heightRange.max, heightRange.min, heightRange.max], [heightRange.max, heightRange.max, heightRange.max]];

setBaseTerrainDiamondSquare(heightRange.min, heightRange.max, 0.5, initialReliefmap, g_Map.height);
// Apply simple erosion
for (var i = 0; i < 5; i++)
	decayErrodeHeightmap(0.5);
rescaleHeightmap(heightRange.min, heightRange.max);

RMS.SetProgress(50);

//////////
// Setup height limit
//////////

// Height presets
var heighLimits = [
	heightRange.min + 1/3 * (waterHeightAdjusted - heightRange.min), // 0 Deep water
	heightRange.min + 2/3 * (waterHeightAdjusted - heightRange.min), // 1 Medium Water
	heightRange.min + (waterHeightAdjusted - heightRange.min), // 2 Shallow water
	waterHeightAdjusted + 1/8 * (heightRange.max - waterHeightAdjusted), // 3 Shore
	waterHeightAdjusted + 2/8 * (heightRange.max - waterHeightAdjusted), // 4 Low ground
	waterHeightAdjusted + 3/8 * (heightRange.max - waterHeightAdjusted), // 5 Player and path height
	waterHeightAdjusted + 4/8 * (heightRange.max - waterHeightAdjusted), // 6 High ground
	waterHeightAdjusted + 5/8 * (heightRange.max - waterHeightAdjusted), // 7 Lower forest border
	waterHeightAdjusted + 6/8 * (heightRange.max - waterHeightAdjusted), // 8 Forest
	waterHeightAdjusted + 7/8 * (heightRange.max - waterHeightAdjusted), // 9 Upper forest border
	waterHeightAdjusted + (heightRange.max - waterHeightAdjusted)]; // 10 Hilltop

//////////
// Place start locations and apply terrain texture and decorative props
//////////

// Get start locations
var startLocations = getStartLocationsByHeightmap({'min': heighLimits[4], 'max': heighLimits[5]});
var playerHeight = (heighLimits[4] + heighLimits[5]) / 2;

for (var i=0; i < numPlayers; i++)
{
	playerAngle[i] = (playerAngleStart + i*playerAngleAddAvrg + randFloat(0, playerAngleMaxOff))%(2*PI);
	var x = round(mapCenterX + randFloat(minPlayerRadius, maxPlayerRadius)*cos(playerAngle[i]));
	var z = round(mapCenterZ + randFloat(minPlayerRadius, maxPlayerRadius)*sin(playerAngle[i]));
	playerStartLocX[i] = x;
	playerStartLocZ[i] = z;
	// Place starting entities

	rectangularSmoothToHeight([x,z] , 20, 20, playerHeight, 0.8);

	placeCivDefaultEntities(x, z, i+1, BUILDING_ANGlE, {'iberWall': false});
	
	// Place base texture
	var placer = new ClumpPlacer(2*baseRadius*baseRadius, 2/3, 1/8, 10, x, z);
	var painter = [new TerrainPainter([baseTex], [baseRadius/4, baseRadius/4]), paintClass(clPlayer)];
	createArea(placer, painter);
	
	// Place starting resources
	var distToSL = 15;
	var resStartAngle = playerAngle[i] + PI;
	var resAddAngle = 2*PI / startingResources.length;
	for (var rIndex = 0; rIndex < startingResources.length; rIndex++)
	{
		var angleOff = randFloat(-resAddAngle/2, resAddAngle/2);
		var placeX = x + distToSL*cos(resStartAngle + rIndex*resAddAngle + angleOff);
		var placeZ = z + distToSL*sin(resStartAngle + rIndex*resAddAngle + angleOff);
		placeObject(placeX, placeZ, startingResources[rIndex], 0, randFloat(0, 2*PI));
		addToClass(round(placeX), round(placeZ), clBaseResource);
	}
}

// Add further stone and metal mines
derivateEntitiesByHeight({'min': heighLimits[3], 'max': ((heighLimits[4]+heighLimits[3])/2)}, startLocations);
derivateEntitiesByHeight({'min': ((heighLimits[5]+heighLimits[6])/2), 'max': heighLimits[7]}, startLocations);

RMS.SetProgress(50);

//place water & open terrain textures and assign TileClasses
log("Painting textures...");
var placer = new HeightPlacer(heighLimits[2], (heighLimits[3]+heighLimits[2])/2);
var painter = new LayeredPainter([terrainBase, terrainBaseBorder], [5]);
createArea(placer, painter);
paintTileClassBasedOnHeight(heighLimits[2], (heighLimits[3]+heighLimits[2])/2, 1, clOpen);

var placer = new HeightPlacer(heightRange.min, heighLimits[2]);
var painter = new LayeredPainter([tWaterBorder, tWater], [2]);
createArea(placer, painter);
paintTileClassBasedOnHeight(heightRange.min,  heighLimits[2], 1, clWater);

RMS.SetProgress(60);

// Place paths
log("Placing paths...");
var doublePaths = true;
if (numPlayers > 4)
	doublePaths = false;
var doublePathMayPlayers = 4;
if (doublePaths === true)
	var maxI = numPlayers+1;
else
	var maxI = numPlayers;
for (var i = 0; i < maxI; i++)
{
	if (doublePaths === true)
		var minJ = 0;
	else
		var minJ = i+1;
	for (var j = minJ; j < numPlayers+1; j++)
	{
		// Setup start and target coordinates
		if (i < numPlayers)
		{
			var x = playerStartLocX[i];
			var z = playerStartLocZ[i];
		}
		else
		{
			var x = mapCenterX;
			var z = mapCenterZ;
		}
		if (j < numPlayers)
		{
			var targetX = playerStartLocX[j];
			var targetZ = playerStartLocZ[j];
		}
		else
		{
			var targetX = mapCenterX;
			var targetZ = mapCenterZ;
		}
		// Prepare path placement
		var angle = getAngle(x, z, targetX, targetZ);
		x += round(pathSucsessRadius*cos(angle));
		z += round(pathSucsessRadius*sin(angle));
		var targetReached = false;
		var tries = 0;
		// Placing paths
		while (targetReached === false && tries < 2*mapSize)
		{
			var placer = new ClumpPlacer(pathWidth, 1, 1, 1, x, z);
			var painter = [new TerrainPainter(terrainPath), new SmoothElevationPainter(ELEVATION_MODIFY, -0.1, 1.0), paintClass(clPath)];
			createArea(placer, painter, avoidClasses(clPath, 0, clOpen, 0 ,clWater, 4, clBaseResource, 4));
			// addToClass(x, z, clPath); // Not needed...
			// Set vars for next loop
			angle = getAngle(x, z, targetX, targetZ);
			if (doublePaths === true) // Bended paths
			{
				x += round(cos(angle + randFloat(-pathAngleOff/2, 3*pathAngleOff/2)));
				z += round(sin(angle + randFloat(-pathAngleOff/2, 3*pathAngleOff/2)));
			}
			else // Straight paths
			{
				x += round(cos(angle + randFloat(-pathAngleOff, pathAngleOff)));
				z += round(sin(angle + randFloat(-pathAngleOff, pathAngleOff)));
			}
			if (getDistance(x, z, targetX, targetZ) < pathSucsessRadius)
				targetReached = true;
			tries++;

		}
	}
}

RMS.SetProgress(75);

//create general decoration
log("Creating decoration...");
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),
  scaleByMapSize(13, 200),
  scaleByMapSize(13, 200),
  scaleByMapSize(13, 200)
 ],
 avoidClasses(clForest, 1, clPlayer, 0, clPath, 3, clWater, 3)
);

RMS.SetProgress(80);

//create fish
log("Growing fish...");
createFood
(
 [
  [new SimpleObject(oFish, 2,3, 0,2)]
 ], 
 [
  100 * numPlayers
 ],
 [avoidClasses(clFood, 5), stayClasses(clWater, 4)]
);

RMS.SetProgress(85);

// create reeds
log("Planting reeds...");
var types = [aReeds];	// some variation
for (var i = 0; i < types.length; ++i)
{
	var group = new SimpleGroup([new SimpleObject(types[i], 1,1, 0,0)], true);
	createObjectGroups(group, 0,
		borderClasses(clWater, 0, 6),
		scaleByMapSize(960, 2000), 1000
	);
}

RMS.SetProgress(90);

// place trees
log("Planting trees...");
for (var x = 0; x < mapSize; x++)
{
	for (var z = 0;z < mapSize;z++)
	{
		// Some variables
		var radius = Math.pow(Math.pow(mapCenterX - x - 0.5, 2) + Math.pow(mapCenterZ - z - 0.5, 2), 1/2); // The 0.5 is a correction for the entities placed on the center of tiles
		var minDistToSL = mapSize;
		for (var i=0; i < numPlayers; i++)
			minDistToSL = min(minDistToSL, getDistance(playerStartLocX[i], playerStartLocZ[i], x, z));
		// Woods tile based
		var tDensFactSL = max(min((minDistToSL - baseRadius) / baseRadius, 1), 0);
		var tDensFactRad = abs((resourceRadius - radius) / resourceRadius);
		var tDensActual = (maxTreeDensity * tDensFactSL * tDensFactRad)*0.75;
		if (randFloat() < tDensActual && radius < playableMapRadius)
		{
			if (tDensActual < bushChance*randFloat()*maxTreeDensity)
			{
				var placer = new ClumpPlacer(1, 1.0, 1.0, 1, x, z);
				var painter = [new TerrainPainter(terrainWoodBorder), paintClass(clForest)];
				createArea(placer, painter, avoidClasses(clPath, 1, clOpen, 2, clWater,3));
			}
			else
			{
				var placer = new ClumpPlacer(1, 1.0, 1.0, 1, x, z);
				var painter = [new TerrainPainter(terrainWood), paintClass(clForest)]; 
				createArea(placer, painter, avoidClasses(clPath, 2, clOpen, 3, clWater, 4));}
		}
	}
}

RMS.SetProgress(100);

// Export map data
ExportMap();
