Index: binaries/data/mods/public/maps/random/danube.js =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/random/danube.js @@ -0,0 +1,772 @@ +log("Creating treasures..."); + +RMS.LoadLibrary("rmgen"); + +// trigger_point_A used as spawn points for ships +// trigger_point_B are placed on the left side of the river and ships unload units there +// trigger_point_C same on the right side of the river + +// Terrain textures +const tRoad = "steppe_river_rocks"; +const tIsland = ["temp_grass_long_b_aut", "temp_grass_plants_aut", "temp_forestfloor_aut"]; // "temp_highlands_aut" +const tCliff = "temp_cliff_a"; +const tForestFloor = "temp_forestfloor_aut"; +const tGrass = "medit_shrubs_golden"; //"temp_grass_long_b_aut"; +const tGrass2 ="grass_mediterranean_dry_1024test";//"temp_grass_c_aut"; +const tGrass3 = "medit_grass_field_b";//"temp_grass_d_aut"; +const tShore = "temp_dirt_gravel_b"; +const tWater = "steppe_river_rocks_wet"; +const tSeaDepths = "medit_sea_depths"; + +// Gaia entities +const oBerryBush = "gaia/flora_bush_berry"; +const oDeer = "gaia/fauna_deer"; +const oFish = "gaia/fauna_fish"; +const oSheep = "gaia/fauna_sheep"; +const oGoat = "gaia/fauna_goat"; +const oWolf = "gaia/fauna_wolf"; +const oHawk = "gaia/fauna_hawk"; +const oRabbit = "gaia/fauna_rabbit"; +const oBoar = "gaia/fauna_boar"; +const oBear = "gaia/fauna_bear"; +const oStoneLarge = "gaia/geology_stonemine_temperate_quarry"; +const oStoneSmall = "gaia/geology_stone_temperate"; +const oStoneRuins = "gaia/special_ruins_standing_stone"; +const oMetalLarge = "gaia/geology_metal_mediterranean_slabs"; +const oApple = "gaia/flora_tree_apple"; +const oAcacia = "gaia/flora_tree_acacia"; +const oOak = "gaia/flora_tree_oak_aut"; +const oOak2 = "gaia/flora_tree_oak_aut_new"; +const oOak3 = "gaia/flora_tree_oak_dead"; +const oOak4 = "gaia/flora_tree_oak"; +const oPopolar = "gaia/flora_tree_poplar_lombardy"; +const oBeech = "gaia/flora_tree_euro_beech_aut"; +const oBeech2 = "gaia/flora_tree_euro_beech"; +const oTreasures = [ + "gaia/special_treasure_food_barrel", + "gaia/special_treasure_food_bin", + "gaia/special_treasure_food_crate", + "gaia/special_treasure_stone" +]; + +const oCivicCenter = "structures/gaul_civil_centre"; +const oHouse = "structures/gaul_house"; +const oTemple = "structures/gaul_temple"; +const oTavern = "structures/gaul_tavern"; +const oTower= "structures/gaul_defense_tower"; +const oOutpost = "structures/gaul_outpost"; + +const oHut = "other/celt_hut"; +const oLongHouse = "other/celt_longhouse"; +const oPalisadeTower = "other/palisades_rocks_watchtower"; +const oTallSpikes = "other/palisades_tall_spikes"; +const oAngleSpikes = "other/palisades_angle_spike"; + +const oFemale = "units/gaul_support_female_citizen"; +const oHealer = "units/gaul_support_healer_e"; +const oSkirmisher = "units/gaul_infantry_javelinist_e"; +const oNakedFanatic = "units/gaul_champion_fanatic"; + +// Decorative props +const aBush1 = "actor|props/flora/bush_tempe_sm.xml"; +const aBush2 = "actor|props/flora/bush_tempe_me.xml"; +const aBush3 = "actor|props/flora/bush_tempe_la.xml"; +const aBush4 = "actor|props/flora/bush_tempe_me.xml"; +const aRock1 = "actor|geology/stone_granite_med.xml"; +const aRock2 = "actor|geology/stone_granite_boulder.xml"; +const aRock3 = "actor|geology/stone_granite_greek_boulder.xml"; +const aRock4 = "actor|geology/stonemine_alpine_a.xml"; +const aFerns = "actor|props/flora/ferns.xml"; +const aBucket = "actor|props/structures/celts/blacksmith_bucket"; +const aBarrel = "actor|props/structures/gauls/storehouse_barrel_b"; +const aTartan = "actor|props/structures/celts/tartan_a"; +const aWheel = "actor|props/special/eyecandy/wheel_laying"; +const aWell = "actor|props/special/eyecandy/well_1_b"; +const aWoodcord = "actor|props/special/eyecandy/woodcord"; +const aWaterLog = "actor|props/flora/water_log.xml"; +const aBlacksmithDecor = "actor|props/structures/hellenes/blacksmith_spears"; +const aCampfire = "actor|props/special/eyecandy/campfire"; +const aBench = "actor|props/special/eyecandy/bench_1"; +const aRug = "actor|props/special/eyecandy/rug_stand_iber"; + +const pForest1 = [ + tForestFloor, + tForestFloor + TERRAIN_SEPARATOR + oOak, + tForestFloor + TERRAIN_SEPARATOR + oOak2, + tForestFloor + TERRAIN_SEPARATOR + oOak3, + tForestFloor + TERRAIN_SEPARATOR + oOak4, + tForestFloor +]; + +const pForest2 = [ + tForestFloor, + tForestFloor + TERRAIN_SEPARATOR + oPopolar, + tForestFloor + TERRAIN_SEPARATOR + oBeech, + tForestFloor + TERRAIN_SEPARATOR + oBeech2, + tForestFloor + TERRAIN_SEPARATOR + oAcacia, + tForestFloor +]; + +// Minimum distance from the map border to ungarrison points +const ShorelineDistance = 20; + +log("Initializing map..."); +InitMap(); + +const numPlayers = getNumPlayers(); +const mapSize = getMapSize(); + +var clPlayer = createTileClass(); +var clForest = createTileClass(); +var clWater = createTileClass(); +var clShore = [createTileClass(), createTileClass()]; +var clShoreUngarrisonPoint = [createTileClass(), createTileClass()]; +var clShip = createTileClass(); +var clDirt = createTileClass(); +var clRock = createTileClass(); +var clMetal = createTileClass(); +var clFood = createTileClass(); +var clBaseResource = createTileClass(); +var clGrass = createTileClass(); +var clHill = createTileClass(); +var clIsland = createTileClass(); +var clTreasure = createTileClass(); +var clWaterLog = createTileClass(); +var clGauls = createTileClass(); +var clTower = createTileClass(); +var clOutpost = createTileClass(); +var clPath = createTileClass(); +var clRitualPlace = createTileClass(); + +// Percentage of the mapsize that the river takes up +const waterWidth = 0.3; + +// Place a gaia village on small maps and larger +if (mapSize >= 192) +{ + log("Creating gallic villages..."); + let gaulCityRadius = 12; + + // Whether to add a celtic ritual and apath from the gallic city leading to it + let addCelticRitual = true || randIntInclusive(1, 3) > 1; + + // One village left and right of the river + for (let i = 0; i < 2; ++i) + { + let gX = i == 0 ? gaulCityRadius : mapSize - gaulCityRadius; + let gZ = mapSize / 2; + + if (addCelticRitual) + { + // Don't position the meeting place at the center of the map + let mLocation = randFloat(0.15, 0.25) * (randBool() ? 1 : -1); + + // Center of the meeting place + let mX = i == 0 ? + mapSize * waterWidth : + mapSize * (1 - waterWidth); + let mZ = gZ + mapSize * mLocation; + + // Radius of the meeting place + let mRadius = scaleByMapSize(4, 6); + + // Create a path connecting the gaia city with a meeting place at the shoreline. + // To avoid the path going through the palisade wall, start it at the gate, not at the city center. + let placer = new PathPlacer( + gX + gaulCityRadius * (i == 0 ? 1 : -1), + gZ, + mX, + mZ, + 4, // width + 0.4, // waviness + 4, // smoothness + 0.2, // offset + 0.05); // tapering + + let terrainPainter = new LayeredPainter([tShore, tRoad, tRoad], [1, 3]); + let elevationPainter = new SmoothElevationPainter(ELEVATION_SET, 5, 4); + createArea(placer, [terrainPainter, elevationPainter, paintClass(clPath)]); + + // Create the meeting place near the shoreline at the end of the path + createArea( + new ClumpPlacer(mRadius * mRadius * PI, 0.6, 0.3, 10, mX, mZ), + [new LayeredPainter([tShore, tShore], [1]), paintClass(clPath), paintClass(clRitualPlace)], + null); + + placeObject(mX, mZ, aCampfire, 0, randFloat(0, 2 * PI)); + + let femaleCount = Math.round(mRadius * 2); + let maleCount = Math.round(mRadius * 3); + let benchCount = Math.round(mRadius * 2); + let rugCount = Math.round(mRadius * 2.5); + let goatCount = Math.round(mRadius * 1.5); + + let femaleRadus = mRadius * 0.3; + let maleRadus = mRadius * 0.4; + let benchRadius = mRadius * 0.5; + let rugRadus = mRadius * 0.6; + let goatRadus = mRadius * 0.8; + + wallStyles.celt_ritual = { + "female": new WallElement("female", oFemale, PI, femaleRadus, 0, 2 * PI / femaleCount), + "skirmisher": new WallElement("skirmisher", oSkirmisher, PI, maleRadus, 0, 2 * PI / maleCount), + "healer": new WallElement("healer", oHealer, PI, maleRadus, 0, 2 * PI / maleCount), + "fanatic": new WallElement("fanatic", oNakedFanatic, PI, maleRadus, 0, 2 * PI / maleCount), + "bench": new WallElement("bench", aBench, PI/2, benchRadius, 0, 2 * PI / benchCount), + "rug": new WallElement("rug", aRug, 0, rugRadus, 0, 2 * PI / rugCount), + "goat": new WallElement("goat", oGoat, PI, goatRadus, 0, 2 * PI / goatCount), + }; + + placeCustomFortress(mX, mZ, new Fortress("celt ritual females", new Array(femaleCount).fill("female")), "celt_ritual", 0, 0); + + placeCustomFortress(mX, mZ, new Fortress("celt ritual males", new Array(maleCount).fill(0).map(i => + pickRandom(["skirmisher", "healer", "fanatic"]))), "celt_ritual", 0, 0); + + placeCustomFortress(mX, mZ, new Fortress("celt ritual bench", new Array(benchCount).fill("bench")), "celt_ritual", 0, 0); + placeCustomFortress(mX, mZ, new Fortress("celt ritual rug", new Array(rugCount).fill("rug")), "celt_ritual", 0, 0); + placeCustomFortress(mX, mZ, new Fortress("celt ritual goat", new Array(goatCount).fill("goat")), "celt_ritual", 0, 0); + } + + placeObject(gX, gZ, oCivicCenter, 0, BUILDING_ORIENTATION + PI * 3/2 * i); + + // Create the city patch + createArea( + new ClumpPlacer(gaulCityRadius * gaulCityRadius * PI, 0.6, 0.3, 10, gX, gZ), + [new LayeredPainter([tShore, tShore], [1]), paintClass(clGauls)], + null); + + // Place palisade fortress and some city buildings + // Use actors to avoid players capturing the buildings + wallStyles.gaul.house = new WallElement("house", oHouse, PI, 0, 4); + wallStyles.gaul.hut = new WallElement("hut", oHut, PI, 0, 4); + wallStyles.gaul.longhouse = new WallElement("longhouse", oLongHouse, PI, 0, 4); + wallStyles.gaul.tavern = new WallElement("tavern", oTavern, PI*3/2, 0, 4); + wallStyles.gaul.temple = new WallElement("temple", oTemple, PI*3/2, 0, 4); + wallStyles.gaul.defense_tower = new WallElement("defense_tower", + mapSize >= 256 ? oTower : oPalisadeTower, PI/2, 0, 4); + wallStyles.gaul.palisade_tower = wallStyles.palisades.tower; + + // Replace stone walls with palisade walls + for (let template of ["gate", "wallLong", "cornerIn", "cornerOut"]) + wallStyles.gaul[template] = wallStyles.palisades[template]; + + let wall = [ + "gate", "hut", "palisade_tower", "wallLong", "wallLong", + "cornerIn", "defense_tower", "wallLong", "wallLong", "temple", + "palisade_tower", "wallLong", "house", "wallLong", "palisade_tower", "longhouse", "wallLong", "wallLong", + "cornerIn", "defense_tower", "wallLong", "tavern", "wallLong", "palisade_tower"]; + wall = wall.concat(wall); + placeCustomFortress(gX, gZ, new Fortress("Geto-Dacian Tribal Confederation", wall), "gaul", 0, PI); + + // Place spikes + wallStyles.palisades.tall_spikes = new WallElement("tall_spikes", oTallSpikes, PI/2, 2); + wallStyles.palisades.spikeIn = new WallElement("spikeIn", oAngleSpikes, -PI/4, 2.1, 0.7, PI/2); + wallStyles.palisades.spikeMid = new WallElement("spikeIn", oAngleSpikes, -PI/2, 0.7); + wallStyles.palisades.gateGap = new WallElement("gateGap", undefined, PI, 3.6); + + let manySpikes = new Array(4).fill("tall_spikes"); + let spikes= [ + "gateGap", + "spikeMid", ...manySpikes, + "spikeIn", ...manySpikes, + "spikeMid", ...manySpikes, + "spikeMid", ...manySpikes, + "spikeIn", ...manySpikes, + "spikeMid" + ]; + spikes = spikes.concat(spikes); + placeCustomFortress(gX, gZ, new Fortress("spikes", spikes), "palisades", 0, PI); + } +} +RMS.SetProgress(10); + +// Randomize player order +var playerIDs = []; +for (var i = 0; i < numPlayers; i++) + playerIDs.push(i+1); +playerIDs = primeSortPlayers(sortPlayers(playerIDs)); + +// Place players +var playerX = new Array(numPlayers); +var playerZ = new Array(numPlayers); +var iop = 0; +for (var i = 0; i < numPlayers; i++) +{ + iop = i - 1; + + if (numPlayers % 2 == 0) + playerZ[i] = ((iop + Math.abs(iop % 2))/2 + 1) / (numPlayers / 2 + 1); + else if (iop % 2) + playerZ[i] = ((iop + Math.abs(iop % 2))/2 + 1) / (((numPlayers + 1) / 2) + 1); + else + playerZ[i] = (iop/2 + 1) / (((numPlayers - 1) / 2) + 1); + + playerX[i] = 0.2 + 0.6 * (i % 2); +} + +for (var i = 0; i < numPlayers; ++i) +{ + var id = playerIDs[i]; + log("Creating base for player " + id + "..."); + + var radius = scaleByMapSize(15, 25); + + var fx = fractionToTiles(playerX[i]); + var fz = fractionToTiles(playerZ[i]); + var ix = floor(fx); + var iz = floor(fz); + addToClass(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([tShore, tRoad], [1]); + createArea(placer, painter, null); + + placeCivDefaultEntities(fx, fz, id, { 'iberWall': false }); + + placeDefaultChicken(fx, fz, clBaseResource); + + // Create berry bushes + 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(oBerryBush, 5,5, 0,3)], + true, clBaseResource, bbX, bbZ + ); + createObjectGroup(group, 0); + + // Create metal mine + var mDist = scaleByMapSize(9, 14); + bbAngle += randFloat(PI/4, PI/3); + var mX = round(fx + mDist * cos(bbAngle)); + var mZ = round(fz + mDist * sin(bbAngle)); + group = new SimpleGroup( + [new SimpleObject(oMetalLarge, 1,1, 0,0)], + true, clBaseResource, mX, mZ + ); + createObjectGroup(group, 0); + + // Create stone mines + bbAngle += randFloat(PI/3, PI/2); + mX = round(fx + mDist * cos(bbAngle)); + mZ = round(fz + mDist * sin(bbAngle)); + group = new SimpleGroup( + [new SimpleObject(oStoneLarge, 1,1, 0,2)], + true, clBaseResource, mX, mZ + ); + createObjectGroup(group, 0); + + // Create starting trees + var num = 20; + bbAngle += randFloat(-PI/3, 4*PI/3); + var tDist = randFloat(10, 14); + var tX = round(fx + tDist * cos(bbAngle)); + var tZ = round(fz + tDist * sin(bbAngle)); + group = new SimpleGroup( + [new SimpleObject(oOak, num, num, 0,5)], + false, clBaseResource, tX, tZ + ); + createObjectGroup(group, 0, avoidClasses(clBaseResource, 4)); + + placeDefaultDecoratives(fx, fz, aBush1, clBaseResource, radius); +} +RMS.SetProgress(20); + +log("Creating the river"); +var theta = randFloat(0, 0.8); +var theta2 = randFloat(0, 1.2); +var seed = randFloat(3, 5); +var seed2 = randFloat(2, 6); +var fadeDist = 0.05; +for (let ix = 0; ix < mapSize; ++ix) + for (let iz = 0; iz < mapSize; ++iz) + { + let x = ix / (mapSize + 1.0); + let z = iz / (mapSize + 1.0); + + // add the rough shape of the water + let km = 20 / scaleByMapSize(35, 160); + let cu = km * rndRiver(theta + z * mapSize / 128, seed); + let cu2 = km * rndRiver(theta2 + z * mapSize / 128, seed2); + + if (x < cu + 0.5 - waterWidth / 2 || x > cu2 + 0.5 + waterWidth / 2) + continue; + + let height; + if (x < cu + 0.5 + fadeDist - waterWidth / 2) + height = 2 - 5 * (1 - ((cu + 0.5 + fadeDist - waterWidth / 2) - x) / fadeDist); + else if (x > (cu2 + 0.5 - fadeDist + waterWidth / 2)) + height = 2 - 5 * (1 - (x - (cu2 + 0.5 - fadeDist + waterWidth / 2)) / fadeDist); + else + height = -3.0; + + setHeight(ix, iz, height); + if (height < 0.7) + addToClass(ix, iz, clWater); + + // Distinguish left and right shoreline + if (0.5 < height && height < 1 && iz > ShorelineDistance && iz < mapSize - ShorelineDistance) + addToClass(ix, iz, clShore[ix < mapSize / 2 ? 0 : 1]); + } +RMS.SetProgress(30); + +log("Creating shores..."); +paintTerrainBasedOnHeight(-20, 1, 0, tWater); +paintTerrainBasedOnHeight(1, 2, 0, tShore); +RMS.SetProgress(35); + +log("Creating bumps..."); +createBumps(avoidClasses(clPlayer, 6, clWater, 2, clPath, 1), scaleByMapSize(30, 300), 1, 8, 4, 0, 3); +RMS.SetProgress(40); + +log("Creating hills..."); +if (randInt(1,2) == 1) + createHills([tCliff, tCliff, tCliff], avoidClasses(clPlayer, 18, clHill, 20, clWater, 2, clGauls, 5, clPath, 1), clHill, scaleByMapSize(3, 15)); +else + createMountains(tCliff, avoidClasses(clPlayer, 18, clHill, 20, clWater, 3, clGauls, 5, clPath, 1), clHill, scaleByMapSize(3, 15)); +RMS.SetProgress(45); + +log("Creating forests..."); +createForests( + [tForestFloor, tForestFloor, tForestFloor, pForest1, pForest2], + avoidClasses(clPlayer, 16, clForest, 17, clWater, 5, clHill, 2, clGauls, 5, clPath, 1), + clForest +); +RMS.SetProgress(50); + +log("Creating grass patches..."); +createLayeredPatches( + [scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)], + [[tGrass, tGrass2],[tGrass2, tGrass3], [tGrass3, tGrass]], + [1,1], + avoidClasses(clForest, 0, clGrass, 2, clPlayer, 10, clWater, 2, clDirt, 2, clHill, 1, clGauls, 5, clPath, 1) +); +RMS.SetProgress(55); + +log("Creating islands..."); +placer = new ChainPlacer(floor(scaleByMapSize(3, 4)), floor(scaleByMapSize(4, 8)), floor(scaleByMapSize(50, 80)), 0.5); +var terrainPainter = new LayeredPainter([tWater, tShore, tIsland], [2, 1]); +var elevationPainter = new SmoothElevationPainter(ELEVATION_SET, 6, 4); +createAreas( + placer, + [terrainPainter, elevationPainter, paintClass(clIsland)], + [avoidClasses(clIsland, 30), stayClasses (clWater, 6)], + scaleByMapSize(1, 4) * numPlayers +); +RMS.SetProgress(60); + +log("Creating island bumps..."); +createBumps(stayClasses(clIsland, 2), scaleByMapSize(50, 400), 1, 8, 4, 0, 3); + +log("Paint seabed..."); +paintTerrainBasedOnHeight(-20, -3, 3, tSeaDepths); + +log("Creating island stone mines..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(oStoneLarge, 1,1, 0,4)], true, clRock), + 0, + [avoidClasses(clMetal, 30, clRock, 30), stayClasses(clIsland, 5)], + 500, 1 +); + +log("Creating island metal mines..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(oMetalLarge, 1,1, 0,4)], true, clMetal), + 0, + [avoidClasses(clMetal, 30, clRock, 30), stayClasses(clIsland, 5)], + 500, 1 +); +RMS.SetProgress(70); + +log("Creating island towers..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(oTower, 1,1, 0,4)], true, clTower), + 0, + [avoidClasses(clMetal, 4, clRock, 4, clTower, 20), stayClasses(clIsland, 7)], + 500, 1 +); +log("Creating island outposts..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(oOutpost, 1,1, 0,4)], true, clOutpost), + 0, + [avoidClasses(clMetal, 4, clRock, 4, clTower, 5, clOutpost, 20), stayClasses(clIsland, 7)], + 500, 1 +); + +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(clForest, 4, clPlayer, 16, clRock, 10, clWater, 4, clHill, 4, clGauls, 5, clPath, 5) +); + +log("Creating metal mines..."); +createMines( + [ + [new SimpleObject(oMetalLarge, 1,1, 0,4)] + ], + avoidClasses(clForest, 4, clPlayer, 20, clMetal, 10, clRock, 5, clWater, 4, clHill, 4, clGauls, 5, clPath, 5), + clMetal +); + +log("Creating stone ruins..."); +createMines( + [ + [new SimpleObject(oStoneRuins, 1,1, 0,4)] + ], + avoidClasses(clForest, 2, clPlayer, 20, clMetal, 6, clRock, 6, clWater, 4, clHill, 4, clGauls, 5, clPath, 1), + clRock +); +RMS.SetProgress(65); + +log("Creating decoratives..."); +for (let i = 0; i < 1; ++i) + createDecoration( + [ + [new SimpleObject(aRock1, 1,1, 0,1)], + [new SimpleObject(aRock2, 1,1, 0,1)], + [new SimpleObject(aRock3, 1,1, 0,1)], + [new SimpleObject(aRock4, 1,1, 0,1)], + + [new SimpleObject(aBush1, 1,3, 0,2)], + [new SimpleObject(aBush2, 1,2, 0,1)], + [new SimpleObject(aBush3, 1,3, 0,2)], + [new SimpleObject(aBush4, 1,2, 0,1)], + + [new SimpleObject(aFerns, 2,5, 2,4)], + ], + [ + scaleByMapSize(5, 80), + scaleByMapSize(5, 80), + scaleByMapSize(5, 80), + scaleByMapSize(5, 80), + + scaleByMapSize(5, 80), + scaleByMapSize(5, 80), + scaleByMapSize(5, 80), + scaleByMapSize(5, 80), + + scaleByMapSize(20, 80), + ], + i == 0 ? + avoidClasses(clWater, 4, clForest, 1, clPlayer, 16, clRock, 4, clMetal, 4, clHill, 4, clGauls, 5, clPath, 1) : + [stayClasses(clIsland, 2), avoidClasses(clForest, 1, clRock, 4, clMetal, 4)] + ); +RMS.SetProgress(70); + +log("Creating fish..."); +createFood( + [ + [new SimpleObject(oFish, 2,3, 0,2)] + ], + [ + 20 * scaleByMapSize(5, 20) + ], + [avoidClasses(clIsland, 2, clFood, 10, clPath, 1), stayClasses(clWater, 5)], + clFood +); +RMS.SetProgress(75); + +log("Creating huntable animals..."); +createFood( + [ + [new SimpleObject(oSheep, 5,5, 0,4)], + [new SimpleObject(oGoat, 5,5, 0,4)], + [new SimpleObject(oRabbit, 5,8, 0,4)], + [new SimpleObject(oDeer, 4,6, 0,2)], + [new SimpleObject(oHawk, 1,1, 0,4)] + ], + [ + scaleByMapSize(5, 20), + scaleByMapSize(5, 20), + scaleByMapSize(5, 20), + scaleByMapSize(5, 20), + scaleByMapSize(5, 10) + ], + avoidClasses(clIsland, 2, clFood, 10, clWater, 5, clPlayer, 16, clHill, 2, clGauls, 5, clPath, 1), + clFood +); + +log("Creating violent animals..."); +createFood( + [ + [new SimpleObject(oWolf, 1,3, 0,4)], + [new SimpleObject(oBoar, 1,1, 0,4)], + [new SimpleObject(oBear, 1,1, 0,4)] + ], + [ + scaleByMapSize(5, 20), + scaleByMapSize(5, 20), + scaleByMapSize(5, 20) + ], + avoidClasses(clIsland, 2, clFood, 10, clWater, 5, clPlayer, 24, clHill, 2, clGauls, 5, clPath, 1), + clFood +); +RMS.SetProgress(80); + +log("Creating fruits..."); +createFood( + [ + [new SimpleObject(oApple, 3,5, 4,7)], + [new SimpleObject(oBerryBush, 4,6, 0,4)] + ], + [ + scaleByMapSize(5, 20), + scaleByMapSize(5, 20) + ], + avoidClasses(clWater, 5, clForest, 2, clPlayer, 16, clHill, 4, clFood, 10, clMetal, 4, clRock, 4, clGauls, 5, clPath, 1), + clFood +); +RMS.SetProgress(90); + +log("Creating straggler trees..."); +var treeTypes = [oOak, oOak2, oOak3, oOak4, oBeech, oBeech2, oAcacia]; +createStragglerTrees( + treeTypes, + avoidClasses(clForest, 2, clWater, 8, clPlayer, 16, clMetal, 4, clRock, 4, clHill, 2, clGauls, 5, clPath, 5), clForest); + +log("Creating island straggler trees..."); +g_numStragglerTrees *= 7; +createStragglerTrees(treeTypes, [stayClasses(clIsland, 4), avoidClasses(clMetal, 4, clRock, 4, clTower, 4, clOutpost, 4)], clForest); +RMS.SetProgress(95); + +log("Creating animals on islands..."); +createFood( + [ + [new SimpleObject(oSheep, 4,6, 0,4)], + [new SimpleObject(oGoat, 4,6, 0,4)], + [new SimpleObject(oRabbit, 5,8, 0,4)], + ], + [ + 10 * scaleByMapSize(5, 20), + 10 * scaleByMapSize(5, 20), + 10 * scaleByMapSize(5, 20), + ], + [avoidClasses(clRock, 4, clMetal, 4, clFood, 3, clForest, 1, clOutpost, 2, clTower, 2), stayClasses(clIsland, 4)], + clFood +); +RMS.SetProgress(85); + +log("Creating treasures..."); +for (let i = 0; i < randFloat(0, 3 * numPlayers); ++i) + createObjectGroups( + new SimpleGroup( + [new SimpleObject(pickRandom(oTreasures), 1,1, 0,2)], + true, clTreasure + ), + 0, + avoidClasses(clForest, 1, clPlayer, 15, clHill, 1, clWater, 5, clRock, 4, clMetal, 4, clTreasure, 10, clGauls, 5), + 1, + 50 + ); + +log("Creating gallic decoratives..."); +createDecoration( + [ + [new SimpleObject(aBucket, 1,1, 0,1)], + [new SimpleObject(aBarrel, 1,1, 0,1)], + [new SimpleObject(aTartan, 3,3, 4, 4, PI/4, PI/2)], + [new SimpleObject(aWheel, 2,4, 1, 2)], + [new SimpleObject(aWell, 1,1, 0,2)], + [new SimpleObject(aWoodcord, 1,2, 2,2, PI/2, PI/2)] + ], + [ + scaleByMapSize(2, 10), + scaleByMapSize(2, 10), + scaleByMapSize(2, 10), + scaleByMapSize(2, 10), + scaleByMapSize(3, 4), + scaleByMapSize(2, 10) + ], + avoidClasses(clForest, 1, clPlayer, 10, clBaseResource, 5, clHill, 1, clWater, 5, clRock, 4, clMetal, 4, clGauls, 5, clPath, 1) +); + +log("Creating spawn points for ships..."); +createObjectGroups( + new SimpleGroup([new SimpleObject("special/trigger_point_A", 1, 1, 0, 0)], true, clShip), + 0, + [avoidClasses(clShip, 10, clIsland, 5), stayClasses(clWater, 15)], + 1000, + 1000 +); + +// Try to avoid spawning units into forests where units will only be stuck +log("Creating ungarrison points for ships..."); +for (let i=0; i<2; ++i) + createObjectGroups( + new SimpleGroup( + [new SimpleObject( + i == 0 ? "special/trigger_point_B" : "special/trigger_point_C", + 1, 1, + 0, 0)], + true, + clShoreUngarrisonPoint[i]), + 0, + [avoidClasses(clIsland, 3, clForest, 3, clHill, 3, clShoreUngarrisonPoint[i], 10), stayClasses(clShore[i], 0)], + 5000, + 100 + ); + +log("Creating water logs..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(aWaterLog, 1, 1, 0, 0)], true, clWaterLog), + 0, + [avoidClasses(clShip, 3, clIsland, 4), stayClasses(clWater, 4)], + scaleByMapSize(15, 60), + 100 +); + +if (randIntInclusive(1, 3) > 1) +{ + // Day + setSkySet("cumulus"); + + setSunColor(0.9, 0.8, 0.5); + + setFogFactor(0.05); + setFogThickness(0.25); + + setWaterColor(0.2, 0.3, 0.3); + setWaterTint(0.5, 1, 1); + + setPPContrast(0.62); + setPPSaturation(0.51); + setPPBloom(0.12); +} +else +{ + // Night + setSkySet("dark"); + + setSunColor(0.4, 0.9, 1.2); + setSunElevation(0.13499); + setSunRotation(-2.5); + + setTerrainAmbientColor(0.25, 0.3, 0.45); + setUnitsAmbientColor(0.3, 0.35, 0.5); + + setFogFactor(0.004); + setFogThickness(0.25); + setFogColor(0.35, 0.45, 0.5); + + setWaterColor(0.2, 0.25, 0.5); + setWaterTint(0, 0, 0); + + //setPPBrightness(0); + //setPPContrast(1.09961); + //setPPSaturation(0.99); + //setPPBloom(0.1999); +} + +setPPEffect("hdr"); +setWaterWaviness(2.0); +setWaterType("lake"); +setWaterMurkiness(1); +setWaterHeight(21); + +ExportMap(); Index: binaries/data/mods/public/maps/random/danube.json =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/random/danube.json @@ -0,0 +1,16 @@ +{ + "settings" : { + "Name" : "Danube", + "Script" : "danube.js", + "Description" : "Following the annexation of their lands by foreign impostors, the Geto-Dacian Tribal Confederation has decided to summon their longstanding allies of Gaul. Players must not only vie for power amongst themselves, but also to defend themselves against the waves of Gallic liberators.", + "BaseTerrain" : ["temp_grass_aut", "temp_grass_plants_aut", "temp_grass_c_aut", "temp_grass_d_aut"], + "BaseHeight" : 4, + "Keywords": ["new", "trigger"], + "CircularMap" : true, + "Preview" : "danube.png", + "TriggerScripts" : [ + "scripts/TriggerHelper.js", + "random/danube_triggers.js" + ] + } +} Index: binaries/data/mods/public/maps/random/danube_triggers.js =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/random/danube_triggers.js @@ -0,0 +1,444 @@ +const debugLog = true; + +// Spawn behavior: +// Ships spawn every N-M minutes. +// Ensure that no more than P(t) ships exist at time t. +// Fill ships with R(t) units at time t. + +// Increase champion to citizen champion ratio from 0 to 100% in the first 60min +// Randomize whether infantry cavalry +// Cavalry should focus females, skirmishers + +// Ship behavior: +// if there are no enemy ships around, target the docks +// if there are no enemy ships nor docks, patrol the shoreline between trigger points +// if there are enemy ships, target them + +var shipTemplate = "gaul_ship_trireme"; +var siegeTemplate = "gaul_mechanical_siege_ram"; + +var heroTemplates = [ + "gaul_hero_britomartus", + "gaul_hero_vercingetorix", + "gaul_hero_brennus" +]; + +var femaleTemplate = "gaul_support_female_citizen"; +var healerTemplate = "gaul_support_healer_e"; + +var citizenTemplates = [ + "gaul_infantry_javelinist_e", + "gaul_infantry_spearman_e", + "gaul_infantry_slinger_e", + "gaul_cavalry_javelinist_e", + "gaul_cavalry_swordsman_e", +]; + +var championInfantryTemplates = [ + "gaul_champion_fanatic", + "gaul_champion_infantry" +]; + +var championCavalryTemplates = [ + "gaul_champion_cavalry" +]; + +var championTemplates = [...championInfantryTemplates, ...championCavalryTemplates]; + +/** + * Time between two consecutive waves after t minutes game duration. + */ +var shipRespawnTime = t => 1; //randFloat(3, 5); + +/** + * Limit of ships on the map when spawning them. + */ +var shipCount = (t, numPlayers) => 2; //Math.min(10, t * 1.25) * numPlayers; + +/** + * Order all ships to ungarrison at the shoreline + */ +var shipUngarrisonInterval = () => 1; //randFloat(5, 7); + +/** + * Ungarrison ships when being in this range of the target. + */ +var shipUngarrisonDistance = 50; + +/** + * Total count of gaia attackers per shipload. + */ +var attackersPerShip = t => 20; //Math.min(40, t * 2 / 3); + +/** + * Likelihood of adding a non-existing hero at that time. + */ +var heroProbability = t => Math.min(1, (t - 25) / 60); + +/** + * Percent of healers to add per shipload after potentially adding a hero. + */ +var healerRatio = t => randFloat(0, 0.1); + +/** + * Percent of siege engines to add per shipload after adding heroes and healers. + */ +var siegeRatio = t => Math.min(0.4, randFloat(0, Math.max(0, (t - 15) / 60))); + +/** + * Percent of champions to be added after spawning heroes, healers and siege engines. + * Rest will be citizen soldiers. + */ +var championRatio = t => Math.min(1, Math.max(0, (t - 30) / 60)); + +/** + * Ships will queue attack orders for this amount of closest ships. + */ +var shipTargetCount = 3; + +/** + * Number of trigger points to patrol when not having enemies to attack. + */ +var shipPatrolCount = 5; + +/** + * Which units ships should attack when patroling. + * Units will be stuck at the shoreline when encountering unreachable land units. + */ +var shipPatrolTargets = "Unit"; + +/** + * Chance for the units at the meeting place to participate in the ritual. + */ +var ritualProbability = 0.75; + +/** + * Units celebrating at the meeting place will perform one of these animations + * if idle and switch back when becoming idle again. + */ +var ritualAnimations = { + "female": ["attack_slaughter"], + "male": ["attack_capture", "promotion", "attack_slaughter"], + "healer": ["attack_capture", "promotion", "heal"] +}; + +Trigger.prototype.GarrisonAllGallicBuildings = function(gaiaEnts) +{ + if (debugLog) + print("Garrisoning all gallic buildings\n"); + + this.SpawnAndGarrisonBuilding(gaiaEnts, "House", pickRandom([femaleTemplate, healerTemplate])); + this.SpawnAndGarrisonBuilding(gaiaEnts, "SpecialBuilding", pickRandom([femaleTemplate, healerTemplate])); + + for (let targetClass of ["CivCentre", "Temple"]) + this.SpawnAndGarrisonBuilding(gaiaEnts, targetClass, pickRandom(championTemplates)); + + for (let targetClass of ["DefenseTower", "Outpost"]) + this.SpawnAndGarrisonBuilding(gaiaEnts, targetClass, pickRandom(championInfantryTemplates)); +}; + +/** + * Garrisons all targetEnts that match the targetClass with newly spawned entities of the given template. + */ +Trigger.prototype.SpawnAndGarrisonBuilding = function(targetEntities, targetClass, template) +{ + for (let gaiaEnt of targetEntities) + { + let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity); + if (!cmpIdentity || !cmpIdentity.HasClass(targetClass)) + continue; + + let cmpGarrisonHolder = Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder); + + let newEnts = TriggerHelper.SpawnUnits( + gaiaEnt, + "units/" + template, + cmpGarrisonHolder.GetCapacity() - cmpGarrisonHolder.GetEntities(), + 0); + + if (debugLog) + print("Garrisoning " + newEnts.length + " " + template + " at " + targetClass + "\n"); + + for (let newEnt of newEnts) + Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder).Garrison(newEnt); + } +}; + +/** + * Spawn units of the template at each gaia Civic Center and set them to defensive. + */ +Trigger.prototype.SpawnCCDefenders = function(gaiaEnts, template, count) +{ + if (debugLog) + print("Spawning " + count + " " + template + " to defend Civic Centers\n"); + + for (let gaiaEnt of gaiaEnts) + { + let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity); + if (!cmpIdentity || !cmpIdentity.HasClass("CivCentre")) + continue; + + for (let ent of TriggerHelper.SpawnUnits(gaiaEnt, "units/" + template, count, 0)) + { + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + cmpUnitAI.SwitchToStance("defensive"); + } + } +}; + +/** + * Remember most Humans present at the beginning of the match (before spawning any unit) and + * make them defensive. + */ +Trigger.prototype.StartCelticRitual = function(gaiaEnts) +{ + this.ritualEnts = []; + + for (let ent of gaiaEnts) + { + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); + if (!cmpIdentity || !cmpIdentity.HasClass("Human")) + continue; + + if (randFloat(0, 1) < ritualProbability) + this.ritualEnts.push(ent); + + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (cmpUnitAI) + cmpUnitAI.SwitchToStance("defensive"); + } + + this.UpdateCelticRitual(); +}; + +/** + * Play one of the given animations for most participants if and only if they are idle. + */ +Trigger.prototype.UpdateCelticRitual = function() +{ + for (let ent of this.ritualEnts) + { + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (!cmpUnitAI || cmpUnitAI.GetCurrentState() != "INDIVIDUAL.IDLE") + continue; + + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); + + let animations = ritualAnimations[ + cmpIdentity.HasClass("Healer") ? "healer" : + cmpIdentity.HasClass("Female") ? "female" : "male"]; + + let cmpVisual = Engine.QueryInterface(ent, IID_Visual); + if (!cmpVisual || animations.indexOf(cmpVisual.GetAnimationName()) != -1) + continue; + + cmpUnitAI.SelectAnimation(pickRandom(animations)); + } + + this.DoAfterDelay(5 * 1000, "UpdateCelticRitual", {}); +}; + +Trigger.prototype.CheckShipSunk = function(data) +{ + if (this.ships.indexOf(data.entity) != -1 && data.to == -1) + { + print("Ship " + data.entity + " sunk\n"); + this.ships.splice(data.entity, 1); + } +}; + +/** + * Spawn ships with a unique attacker composition each until + * the number of ships is reached that is supposed to exist at the given time. + */ +Trigger.prototype.SpawnShips = function() +{ + + if (debugLog) + print("Spawning ships\n"); + + let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); + let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); + + let shipLimit = shipCount(time, numPlayers) - this.ships.length; + let attackerCount = attackersPerShip(time); + + let ratio = championRatio(time); + let unitTypes = [ + { "templates": citizenTemplates, "ratio": 1 - ratio }, + { "templates": championTemplates, "ratio": ratio } + ]; + + while (this.ships.length < shipLimit) + { + let ship = TriggerHelper.SpawnUnits(pickRandom(this.GetTriggerPoints("A")), "units/" + shipTemplate, 1, 0)[0]; + this.ships.push(ship); + + if (debugLog) + print("Spawned ship " + ship + "\n"); + + let units = []; + let remainder = attackerCount; + + let heroTemplate = pickRandom(heroTemplates.filter(hTemp => this.heroes.every(hero => hTemp != hero.template))); + if (heroTemplate && randFloat(0, 1) > heroProbability(time)) + { + let hero = TriggerHelper.SpawnUnits(ship, "units/" + heroTemplate, 1, 0)[0]; + this.heroes.push({ "template": heroTemplate, "ent": hero }); + units.push(hero); + --remainder; + } + + let healerCount = Math.round(healerRatio(time) * remainder); + units = units.concat(TriggerHelper.SpawnUnits(ship, "units/" + healerTemplate, healerCount, 0)); + remainder -= healerCount; + + let siegeCount = Math.round(siegeRatio(time) * remainder); + units = units.concat(TriggerHelper.SpawnUnits(ship, "units/" + siegeTemplate, siegeCount, 0)); + remainder -= siegeCount; + + for (let unitType of unitTypes) + { + let thisRemainder = Math.round(unitType.ratio * remainder); + for (let template of shuffleArray(unitType.templates)) + { + let count = randIntInclusive(0, thisRemainder); + units = units.concat(TriggerHelper.SpawnUnits(ship, "units/" + template, count, 0)); + thisRemainder -= count; + remainder -= count; + } + } + + for (let unit of units) + Engine.QueryInterface(ship, IID_GarrisonHolder).Garrison(unit); + } + + this.DoAfterDelay(1000, "NavalAttack", {}); + + this.DoAfterDelay(shipRespawnTime() * 60 * 1000, "SpawnShips", {}); +}; + +/** + * Attack the closest enemy ships around, then patrol the sea. + */ +Trigger.prototype.NavalAttack = function() +{ + if (debugLog) + print("Naval attack for ships " + uneval(this.ships) + "\n"); + + let targetShips = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities().filter(ent => { + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); + return cmpIdentity && cmpIdentity.HasClass("Warship"); + }); + + for (let ship of this.ships) + { + let targets = targetShips.sort((ent1, ent2) => + DistanceBetweenEntities(ship, ent1) - DistanceBetweenEntities(ship, ent2)).slice(0, shipTargetCount); + + let cmpUnitAI = Engine.QueryInterface(ship, IID_UnitAI); + + for (let target of targets) + { + print("Ship " + ship + " attacks " + target + "\n"); + cmpUnitAI.Attack(target, true, false); + } + + for (let patrolTarget of shuffleArray(this.GetTriggerPoints("A")).slice(0, shipPatrolCount)) + { + print("Ship " + ship + " patrol to " + patrolTarget + "\n"); + let pos = Engine.QueryInterface(patrolTarget, IID_Position).GetPosition2D(); + cmpUnitAI.Patrol(pos.x, pos.y, shipPatrolTargets, true); + } + } +}; + +/** + * Order all ships to abort naval warfare and move to the shoreline all few minutes. + */ +Trigger.prototype.UngarrisonShipsOrder = function() +{ + // TODO: if one side of the river is entirely empty, ignore it + // That is relevant if the game is started with only 1 player or if one side has been wiped out + + let shipsLeft = shuffleArray(this.ships).slice(0, Math.round(this.ships.length / 2)); + let shipsRight = shuffleArray(this.ships.filter(ship => shipsLeft.indexOf(ship) == -1)); + + let sides = [ + { "ships": shipsLeft, "point": "B" }, + { "ships": shipsRight, "point": "C" }, + ]; + + print("UngarrisonShipsOrder " + uneval(sides) + "\n"); + + for (let side of sides) + for (let ship of side.ships) + { + let target = pickRandom(this.GetTriggerPoints(side.point)); + let pos = Engine.QueryInterface(target, IID_Position).GetPosition2D(); + + if (debugLog) + print("Ship " + ship + " will ungarrison at " + side.point + "(" + pos.x + "," + pos.y + ")\n"); + + Engine.QueryInterface(ship, IID_UnitAI).Walk(pos.x, pos.y, false); + this.shipTarget[ship] = target; + } + + let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); + this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {}); +}; + +/** + * Check frequently whether the ships are close enough to unload at the shoreline or + * whether they are stuck at the shoreline trying to attack unreachable enemies. + */ +Trigger.prototype.CheckShipRange = function() +{ + for (let ship of this.ships) + { + if (this.shipTarget[ship]) + print("CheckShipRange " + ship + " distance to target " + DistanceBetweenEntities(ship, this.shipTarget[ship]) + "\n"); + + if (!this.shipTarget[ship] || DistanceBetweenEntities(ship, this.shipTarget[ship]) > shipUngarrisonDistance) + continue; + + if (debugLog) + warn("Ungarrisoning ship " + ship + " at " + this.shipTarget[ship] + "\n"); + + delete this.shipTarget[ship]; + Engine.QueryInterface(ship, IID_GarrisonHolder).UnloadAll(); + + // TODO: order those units to attack something + } + + this.DoAfterDelay(5 * 1000, "CheckShipRange", {}); +}; + + +{ + let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); + + let gaiaEnts = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(0); + cmpTrigger.StartCelticRitual(gaiaEnts); + cmpTrigger.GarrisonAllGallicBuildings(gaiaEnts); + + for (let i = 0; i < 3; ++i) + cmpTrigger.SpawnCCDefenders(gaiaEnts, pickRandom(championInfantryTemplates), 5); + + cmpTrigger.SpawnCCDefenders(gaiaEnts, pickRandom(championCavalryTemplates), 5); + cmpTrigger.SpawnCCDefenders(gaiaEnts, healerTemplate, 4); + cmpTrigger.SpawnCCDefenders(gaiaEnts, femaleTemplate, 5); + + // Entity IDs of all ships and heroes that currently exist on the map + cmpTrigger.ships = []; + cmpTrigger.heroes = []; + + // Maps from ship entity ID to trigger point entity ID + cmpTrigger.shipTarget = {}; + + cmpTrigger.UngarrisonShipsOrder(); + cmpTrigger.CheckShipRange(); + cmpTrigger.SpawnShips(); + + cmpTrigger.RegisterTrigger("OnOwnershipChanged", "CheckShipSunk", { "enabled": true }); +} Index: binaries/data/mods/public/maps/scripts/TriggerHelper.js =================================================================== --- binaries/data/mods/public/maps/scripts/TriggerHelper.js +++ binaries/data/mods/public/maps/scripts/TriggerHelper.js @@ -32,6 +32,10 @@ */ TriggerHelper.SpawnUnits = function(source, template, count, owner) { + if (count < 1) + return []; + + warn("SpawnUnits " + count + " " + template); let entities = []; let cmpFootprint = Engine.QueryInterface(source, IID_Footprint); let cmpPosition = Engine.QueryInterface(source, IID_Position);