Survival Of The Fittest Overhaul

Exponential attacker increase to prevent boring endless games.
Add a gaia hero per player at later stages of the game (if the previous
one isn't alive anymore).
Remove hardcoded template array and include gimmick templates (gaia),
most notably the fireraiser which we never saw before and the siege
tower.
Remove treasure picker female after defeat to prevent confusion with
treasure on the minimap.
Add flag to the spawnpoints, so that new players know where the enemies
actually come from, as proposed by bbleft and Hannibal Barca.
Move all balancing constants to the top of the file.
Actively chose attacker composition instead of having it relate to the
number of templates that exist.
Add debug output option, so that we can replay games and see which enemy
wave composition attacked at which time.
Add dry run, so that we can test the balancing effects from a non-visual
replay instead of having to play some game.
Remove many unused variables and some pointless tile classes from the
mapgen.
Whitespace and various code style cleanup, moving code to shorter, more
readable functions.

Reviewed By: bb
Differential Revision: https://code.wildfiregames.com/D145
This was SVN commit r19359.
This commit is contained in:
elexis 2017-03-29 16:06:11 +00:00
parent f7a2c3d02c
commit 18e7d8a518
2 changed files with 403 additions and 303 deletions

View file

@ -1,23 +1,16 @@
RMS.LoadLibrary("rmgen");
//random terrain textures
var random_terrain = randomizeBiome();
const tMainTerrain = rBiomeT1();
const tForestFloor1 = rBiomeT2();
const tForestFloor2 = rBiomeT3();
const tCliff = rBiomeT4();
const tHill = rBiomeT8();
const tTier1Terrain = rBiomeT5();
const tTier2Terrain = rBiomeT6();
const tTier3Terrain = rBiomeT7();
const tHill = rBiomeT8();
const tDirt = rBiomeT9();
const tRoad = rBiomeT10();
const tRoadWild = rBiomeT11();
const tTier4Terrain = rBiomeT12();
const tShoreBlend = rBiomeT13();
const tShore = rBiomeT14();
const tWater = rBiomeT15();
// gaia entities
const oTree1 = rBiomeE1();
@ -25,131 +18,105 @@ const oTree2 = rBiomeE2();
const oTree3 = rBiomeE3();
const oTree4 = rBiomeE4();
const oTree5 = rBiomeE5();
const oFruitBush = rBiomeE6();
const oMainHuntableAnimal = rBiomeE8();
const oFish = rBiomeE9();
const oSecondaryHuntableAnimal = rBiomeE10();
const oStoneLarge = rBiomeE11();
const oStoneSmall = rBiomeE12();
const oMetalLarge = rBiomeE13();
const oWood = "gaia/special_treasure_wood";
const oFood = "gaia/special_treasure_food_bin";
// decorative props
const aGrass = rBiomeA1();
const aGrassShort = rBiomeA2();
const aReeds = rBiomeA3();
const aLillies = rBiomeA4();
const aRockLarge = rBiomeA5();
const aRockMedium = rBiomeA6();
const aBushMedium = rBiomeA7();
const aBushSmall = rBiomeA8();
const aTree = rBiomeA9();
const aWaypointFlag = "actor|props/special/common/waypoint_flag.xml";
const pForest1 = [tForestFloor2 + TERRAIN_SEPARATOR + oTree1, tForestFloor2 + TERRAIN_SEPARATOR + oTree2, tForestFloor2];
const pForest2 = [tForestFloor1 + TERRAIN_SEPARATOR + oTree4, tForestFloor1 + TERRAIN_SEPARATOR + oTree5, tForestFloor1];
log("Initializing map...");
const oTreasureSeeker = "skirmish/units/default_support_female_citizen";
const oCivicCenter = "skirmish/structures/default_civil_centre";
const oCitizenInfantry = "skirmish/units/default_infantry_melee_b";
const triggerPointAttacker = "special/trigger_point_A";
const triggerPointTreasures = [
"special/trigger_point_B",
"special/trigger_point_C",
"special/trigger_point_D"
];
log("Initializing map...");
InitMap();
var numPlayers = getNumPlayers();
var mapSize = getMapSize();
var mapArea = mapSize*mapSize;
// create tile classes
var clPlayer = createTileClass();
var clHill = createTileClass();
var clHill2 = createTileClass();
var clForest = createTileClass();
var clWater = createTileClass();
var clDirt = createTileClass();
var clRock = createTileClass();
var clMetal = createTileClass();
var clFood = createTileClass();
var clBaseResource = createTileClass();
var clSettlement = createTileClass();
var clLand = createTileClass();
var clWomen = createTileClass();
for (var ix = 0; ix < mapSize; ix++)
{
for (var iz = 0; iz < mapSize; iz++)
{
var x = ix / (mapSize + 1.0);
var z = iz / (mapSize + 1.0);
placeTerrain(ix, iz, tMainTerrain);
}
}
placeTerrain(ix, iz, tMainTerrain);
var fx = fractionToTiles(0.5);
var fz = fractionToTiles(0.5);
ix = round(fx);
iz = round(fz);
var ix = Math.round(fractionToTiles(0.5));
var iz = Math.round(fractionToTiles(0.5));
var lSize = sqrt(sqrt(sqrt(scaleByMapSize(1, 6))));
var placer = new ClumpPlacer(mapArea * 0.065 * lSize, 0.7, 0.1, 10, ix, iz);
var terrainPainter = new LayeredPainter(
[tMainTerrain, tMainTerrain], // terrains
[3] // widths
);
var elevationPainter = new SmoothElevationPainter(
ELEVATION_SET, // type
3, // elevation
3 // blend radius
);
createArea(placer, [terrainPainter, elevationPainter, paintClass(clLand)], null);
// Create the main treasure area in the middle of the map
createArea(
new ClumpPlacer(mapSize * mapSize * scaleByMapSize(0.065, 0.09), 0.7, 0.1, 10, ix, iz),
[
new LayeredPainter([tMainTerrain, tMainTerrain], [3]),
new SmoothElevationPainter(ELEVATION_SET, 3, 3),
paintClass(clLand)
],
null);
// randomize player order
var playerIDs = [];
for (var i = 0; i < numPlayers; i++)
{
playerIDs.push(i+1);
}
playerIDs = sortPlayers(playerIDs);
// place players
var playerX = new Array(numPlayers);
var playerZ = new Array(numPlayers);
var attackerX = new Array(numPlayers);
var attackerZ = new Array(numPlayers);
var playerAngle = new Array(numPlayers);
var startAngle = randFloat(0, TWO_PI);
for (var i = 0; i < numPlayers; i++)
var startAngle = randFloat(0, 2 * PI);
for (let i = 0; i < numPlayers; ++i)
{
playerAngle[i] = startAngle + i*TWO_PI/numPlayers;
playerAngle[i] = startAngle + i * 2 * PI / numPlayers;
playerX[i] = 0.5 + 0.3*cos(playerAngle[i]);
playerZ[i] = 0.5 + 0.3*sin(playerAngle[i]);
attackerX[i] = 0.5 + 0.45*cos(playerAngle[i]);
attackerZ[i] = 0.5 + 0.45*sin(playerAngle[i]);
}
for (var i = 0; i < numPlayers; i++)
for (let i = 0; i < numPlayers; ++i)
{
var id = playerIDs[i];
log("Creating base for player " + id + "...");
// some constants
var radius = scaleByMapSize(15,25);
var cliffRadius = 2;
var elevation = 20;
var radius = scaleByMapSize(15, 25);
// place the attacker spawning trigger point
var ax = round(fractionToTiles(attackerX[i]));
var az = round(fractionToTiles(attackerZ[i]));
placeObject(ax, az, "special/trigger_point_A", id, PI);
placeObject(ax, az, triggerPointAttacker, id, PI);
placeObject(ax, az, aWaypointFlag, 0, PI/2);
addToClass(ax, az, clPlayer);
addToClass(round(fractionToTiles((attackerX[i] + playerX[i]) / 2)), round(fractionToTiles((attackerZ[i] + playerZ[i]) / 2)), clPlayer);
// get the x and z in tiles
fx = fractionToTiles(playerX[i]);
fz = fractionToTiles(playerZ[i]);
ix = round(fx);
iz = round(fz);
let fx = fractionToTiles(playerX[i]);
let fz = fractionToTiles(playerZ[i]);
let ix = round(fx);
let iz = round(fz);
addToClass(ix, iz, clPlayer);
addToClass(ix+5, iz, clPlayer);
addToClass(ix, iz+5, clPlayer);
@ -159,36 +126,43 @@ for (var i = 0; i < numPlayers; i++)
// Place default civ starting entities
var uDist = 6;
var uSpace = 2;
placeObject(fx, fz, "skirmish/structures/default_civil_centre", id, BUILDING_ORIENTATION);
placeObject(fx, fz, oCivicCenter, id, BUILDING_ORIENTATION);
var uAngle = BUILDING_ORIENTATION - PI / 2;
var count = 4;
for (var numberofentities = 0; numberofentities < count; numberofentities++)
for (let numberofentities = 0; numberofentities < count; ++numberofentities)
{
var ux = fx + uDist * cos(uAngle) + numberofentities * uSpace * cos(uAngle + PI/2) - (0.75 * uSpace * floor(count / 2) * cos(uAngle + PI/2));
var uz = fz + uDist * sin(uAngle) + numberofentities * uSpace * sin(uAngle + PI/2) - (0.75 * uSpace * floor(count / 2) * sin(uAngle + PI/2));
placeObject(ux, uz, "skirmish/units/default_infantry_melee_b", id, uAngle);
placeObject(ux, uz, oCitizenInfantry, id, uAngle);
}
placeDefaultDecoratives(fx, fz, aGrassShort, clBaseResource, radius);
var tang = startAngle + (i+0.5)*TWO_PI/numPlayers;
var placer = new PathPlacer(fractionToTiles(0.5), fractionToTiles(0.5), fractionToTiles(0.5 + 0.5*cos(tang)), fractionToTiles(0.5 + 0.5*sin(tang)), scaleByMapSize(14,24), 0.4, 3*(scaleByMapSize(1,3)), 0.2, 0.05);
var terrainPainter = new LayeredPainter(
[tMainTerrain, tMainTerrain], // terrains
[1] // widths
);
var elevationPainter = new SmoothElevationPainter(
ELEVATION_SET, // type
3, // elevation
4 // blend radius
);
createArea(placer, [terrainPainter, elevationPainter, paintClass(clWater)], null);
var tang = startAngle + (i + 0.5) * 2 * PI / numPlayers;
var placer = new PathPlacer(
fractionToTiles(0.5),
fractionToTiles(0.5),
fractionToTiles(0.5 + 0.5 * Math.cos(tang)),
fractionToTiles(0.5 + 0.5 * Math.sin(tang)),
scaleByMapSize(14, 24),
0.4,
3 * scaleByMapSize(1, 3),
0.2,
0.05);
createArea(
placer,
[
new LayeredPainter([tMainTerrain, tMainTerrain], [1]),
new SmoothElevationPainter(ELEVATION_SET, 3, 4)
],
null);
//creating female citizens
var femaleLocation = getTIPIADBON([ix, iz], [mapSize / 2, mapSize / 2], [-3 , 3.5], 1, 3);
if (femaleLocation !== undefined)
{
placeObject(femaleLocation[0], femaleLocation[1], "skirmish/units/default_support_female_citizen", id, playerAngle[i] + PI);
placeObject(femaleLocation[0], femaleLocation[1], oTreasureSeeker, id, playerAngle[i] + PI);
addToClass(floor(femaleLocation[0]), floor(femaleLocation[1]), clWomen);
}
}
@ -196,92 +170,90 @@ for (var i = 0; i < numPlayers; i++)
paintTerrainBasedOnHeight(3.12, 29, 1, tCliff);
paintTileClassBasedOnHeight(3.12, 29, 1, clHill);
// create trigger points for treasures
var group = new SimpleGroup( [new SimpleObject("special/trigger_point_B", 1,1, 0,0)], true, clWomen);
createObjectGroups(group, 0,
[avoidClasses(clForest, 5, clPlayer, 5, clHill, 5), stayClasses(clLand, 5)],
scaleByMapSize(40, 140), 100
);
for (let triggerPointTreasure of triggerPointTreasures)
createObjectGroups(
new SimpleGroup([new SimpleObject(triggerPointTreasure, 1, 1, 0, 0)], true, clWomen),
0,
[avoidClasses(clForest, 5, clPlayer, 5, clHill, 5), stayClasses(clLand, 5)],
scaleByMapSize(40, 140), 100
);
group = new SimpleGroup( [new SimpleObject("special/trigger_point_C", 1,1, 0,0)], true, clWomen);
createObjectGroups(group, 0,
[avoidClasses(clForest, 5, clPlayer, 5, clHill, 5), stayClasses(clLand, 5)],
scaleByMapSize(40, 140), 100
);
createBumps(stayClasses(clLand, 5));
group = new SimpleGroup( [new SimpleObject("special/trigger_point_D", 1,1, 0,0)], true, clWomen);
createObjectGroups(group, 0,
[avoidClasses(clForest, 5, clPlayer, 5, clHill, 5), stayClasses(clLand, 5)],
scaleByMapSize(40, 140), 100
);
// create bumps
createBumps([avoidClasses(clWater, 2, clPlayer, 10), stayClasses(clLand, 5)]);
// create hills
if (randBool())
createHills([tMainTerrain, tCliff, tHill], [avoidClasses(clPlayer, 20, clHill, 5, clBaseResource, 3, clWomen, 5), stayClasses(clLand, 5)], clHill, scaleByMapSize(10, 60) * numPlayers);
else
createMountains(tCliff, [avoidClasses(clPlayer, 20, clHill, 5, clBaseResource, 3, clWomen, 5), stayClasses(clLand, 5)], clHill, scaleByMapSize(10, 60) * numPlayers);
createHills([tCliff, tCliff, tHill], avoidClasses(clPlayer, 20, clHill, 5, clBaseResource, 3, clWomen, 5, clLand, 5), clHill, scaleByMapSize(15, 90) * numPlayers, undefined, undefined, undefined, undefined, 55);
// create forests
createForests(
[tMainTerrain, tForestFloor1, tForestFloor2, pForest1, pForest2],
[avoidClasses(clPlayer, 20, clForest, 5, clHill, 0, clBaseResource,2, clWomen, 5), stayClasses(clLand, 4)],
clForest,
1.0,
random_terrain
[tMainTerrain, tForestFloor1, tForestFloor2, pForest1, pForest2],
[avoidClasses(clPlayer, 20, clForest, 5, clHill, 0, clBaseResource,2, clWomen, 5), stayClasses(clLand, 4)],
clForest,
1,
random_terrain
);
if (randBool())
createHills(
[tMainTerrain, tCliff, tHill],
[avoidClasses(clPlayer, 20, clHill, 5, clBaseResource, 3, clWomen, 5), stayClasses(clLand, 5)],
clHill,
scaleByMapSize(10, 60) * numPlayers);
else
createMountains(
tCliff,
[avoidClasses(clPlayer, 20, clHill, 5, clBaseResource, 3, clWomen, 5), stayClasses(clLand, 5)],
clHill,
scaleByMapSize(10, 60) * numPlayers);
createHills(
[tCliff, tCliff, tHill],
avoidClasses(clPlayer, 20, clHill, 5, clBaseResource, 3, clWomen, 5, clLand, 5),
clHill,
scaleByMapSize(15, 90) * numPlayers,
undefined,
undefined,
undefined,
undefined,
55);
RMS.SetProgress(50);
// create dirt patches
log("Creating dirt patches...");
createLayeredPatches(
[scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)],
[[tMainTerrain,tTier1Terrain],[tTier1Terrain,tTier2Terrain], [tTier2Terrain,tTier3Terrain]],
[1,1],
[avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12, clWomen, 5), stayClasses(clLand, 5)]
[scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)],
[[tMainTerrain, tTier1Terrain], [tTier1Terrain, tTier2Terrain], [tTier2Terrain, tTier3Terrain]],
[1, 1],
[avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12, clWomen, 5), stayClasses(clLand, 5)]
);
// create grass patches
log("Creating grass patches...");
createPatches(
[scaleByMapSize(2, 4), scaleByMapSize(3, 7), scaleByMapSize(5, 15)],
tTier4Terrain,
[avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12, clWomen, 5), stayClasses(clLand, 5)]
[scaleByMapSize(2, 4), scaleByMapSize(3, 7), scaleByMapSize(5, 15)],
tTier4Terrain,
[avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12, clWomen, 5), stayClasses(clLand, 5)]
);
// create decoration
var planetm = 1;
if (random_terrain == g_BiomeTropic)
planetm = 8;
createDecoration
(
[[new SimpleObject(aRockMedium, 1,3, 0,1)],
[new SimpleObject(aRockLarge, 1,2, 0,1), new SimpleObject(aRockMedium, 1,3, 0,2)],
[new SimpleObject(aGrassShort, 1,2, 0,1, -PI/8,PI/8)],
[new SimpleObject(aGrass, 2,4, 0,1.8, -PI/8,PI/8), new SimpleObject(aGrassShort, 3,6, 1.2,2.5, -PI/8,PI/8)],
[new SimpleObject(aBushMedium, 1,2, 0,2), new SimpleObject(aBushSmall, 2,4, 0,2)]
],
[
scaleByMapSize(16, 262),
scaleByMapSize(8, 131),
planetm * scaleByMapSize(13, 200),
planetm * scaleByMapSize(13, 200),
planetm * scaleByMapSize(13, 200)
],
[avoidClasses(clForest, 0, clPlayer, 0, clHill, 0), stayClasses(clLand, 5)]
createDecoration(
[
[new SimpleObject(aRockMedium, 1, 3, 0, 1)],
[new SimpleObject(aRockLarge, 1, 2, 0, 1), new SimpleObject(aRockMedium, 1, 3, 0, 2)],
[new SimpleObject(aGrassShort, 1, 2, 0, 1, -PI/8, PI/8)],
[new SimpleObject(aGrass, 2,4, 0, 1.8, -PI/8, PI/8), new SimpleObject(aGrassShort, 3,6, 1.2, 2.5, -PI/8, PI/8)],
[new SimpleObject(aBushMedium, 1, 2, 0, 2), new SimpleObject(aBushSmall, 2, 4, 0, 2)]
],
[
scaleByMapSize(16, 262),
scaleByMapSize(8, 131),
planetm * scaleByMapSize(13, 200),
planetm * scaleByMapSize(13, 200),
planetm * scaleByMapSize(13, 200)
],
[avoidClasses(clForest, 0, clPlayer, 0, clHill, 0), stayClasses(clLand, 5)]
);
// create straggler trees
log("Creating straggler trees...");
var types = [oTree1, oTree2, oTree4, oTree3]; // some variation
createStragglerTrees(types, [avoidClasses(clForest, 7, clHill, 1, clPlayer, 9, clMetal, 6, clRock, 6), stayClasses(clLand, 7)]);
createStragglerTrees(
[oTree1, oTree2, oTree4, oTree3],
[avoidClasses(clForest, 7, clHill, 1, clPlayer, 9), stayClasses(clLand, 7)]);
// Export map data
ExportMap();

View file

@ -1,3 +1,56 @@
/**
* If set to true, it will print how many templates would be spawned if the players were not defeated.
*/
const dryRun = false;
/**
* If enabled, prints the number of units to the command line output.
*/
const debugLog = false;
/**
* Least and greatest number of minutes to pass between spawning new treasures.
*/
var treasureTime = [3, 5];
/**
* Earliest and latest time when the first wave of attackers will be spawned.
*/
var firstWaveTime = [4, 6];
/**
* Smallest and largest number of minutes between two consecutive waves.
*/
var waveTime = [2, 4];
/**
* Roughly the number of attackers on the first wave.
*/
var initialAttackers = 5;
/**
* Increase the number of attackers exponentially, by this percent value per minute.
*/
var percentPerMinute = 1.05;
/**
* Greatest amount of attackers that can be spawned.
*/
var totalAttackerLimit = 150;
/**
* Least and greatest amount of siege engines per wave.
*/
var siegeFraction = [0.2, 0.5];
/**
* Potentially / definitely spawn a gaia hero after this number of minutes.
*/
var heroTime = [20, 60];
/**
* The following templates can't be built by any player.
*/
var disabledTemplates = (civ) => [
// Economic structures
"structures/" + civ + "_corral",
@ -23,8 +76,10 @@ var disabledTemplates = (civ) => [
"structures/ptol_lighthouse"
];
var treasures =
[
/**
* Spawn these treasures in regular intervals.
*/
var treasures = [
"gaia/special_treasure_food_barrel",
"gaia/special_treasure_food_bin",
"gaia/special_treasure_food_crate",
@ -36,113 +91,225 @@ var treasures =
"gaia/special_treasure_wood"
];
var attackerEntityTemplates =
[
[
"units/athen_champion_infantry",
"units/athen_champion_marine",
"units/athen_champion_ranged",
"units/athen_mechanical_siege_lithobolos_packed",
"units/athen_mechanical_siege_oxybeles_packed",
],
[
"units/brit_champion_cavalry",
"units/brit_champion_infantry",
"units/brit_mechanical_siege_ram",
],
[
"units/cart_champion_cavalry",
"units/cart_champion_elephant",
"units/cart_champion_infantry",
"units/cart_champion_pikeman",
],
[
"units/gaul_champion_cavalry",
"units/gaul_champion_fanatic",
"units/gaul_champion_infantry",
"units/gaul_mechanical_siege_ram",
],
[
"units/iber_champion_cavalry",
"units/iber_champion_infantry",
"units/iber_mechanical_siege_ram",
],
[
"units/mace_champion_cavalry",
"units/mace_champion_infantry_a",
"units/mace_champion_infantry_e",
"units/mace_mechanical_siege_lithobolos_packed",
"units/mace_mechanical_siege_oxybeles_packed",
],
[
"units/maur_champion_chariot",
"units/maur_champion_elephant",
"units/maur_champion_infantry",
"units/maur_champion_maiden",
"units/maur_champion_maiden_archer",
],
[
"units/pers_champion_cavalry",
"units/pers_champion_infantry",
"units/pers_champion_elephant",
],
[
"units/ptol_champion_cavalry",
"units/ptol_champion_elephant",
],
[
"units/rome_champion_cavalry",
"units/rome_champion_infantry",
"units/rome_mechanical_siege_ballista_packed",
"units/rome_mechanical_siege_scorpio_packed",
],
[
"units/sele_champion_cavalry",
"units/sele_champion_chariot",
"units/sele_champion_elephant",
"units/sele_champion_infantry_pikeman",
"units/sele_champion_infantry_swordsman",
],
[
"units/spart_champion_infantry_pike",
"units/spart_champion_infantry_spear",
"units/spart_champion_infantry_sword",
"units/spart_mechanical_siege_ram",
],
];
/**
* An object that maps from civ [f.e. "spart"] to an object
* that has the keys "champions", "siege" and "heroes",
* which is an array containing all these templates,
* trainable from a building or not.
*/
var attackerUnitTemplates = {};
Trigger.prototype.InitSurvival = function()
{
this.InitStartingUnits();
this.LoadAttackerTemplates();
this.SetDisableTemplates();
this.PlaceTreasures();
this.InitializeEnemyWaves();
};
Trigger.prototype.debugLog = function(txt)
{
if (!debugLog)
return;
print("DEBUG [" + Math.round(Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000) + "] " + txt + "\n");
};
Trigger.prototype.LoadAttackerTemplates = function()
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
for (let templateName of cmpTemplateManager.FindAllTemplates(false))
{
if (!templateName.startsWith("units/") || templateName.endsWith("_unpacked") || templateName.endsWith("_barracks"))
continue;
let identity = cmpTemplateManager.GetTemplate(templateName).Identity;
if (!attackerUnitTemplates[identity.Civ])
attackerUnitTemplates[identity.Civ] = {
"heroes": [],
"champions": [],
"siege": []
};
let classes = GetIdentityClasses(identity);
// Notice some heroes are elephants and war elephants are champions
if (classes.indexOf("Hero") != -1)
attackerUnitTemplates[identity.Civ].heroes.push(templateName);
else if (classes.indexOf("Siege") != -1 || classes.indexOf("Elephant") != -1 && classes.indexOf("Melee") != -1)
attackerUnitTemplates[identity.Civ].siege.push(templateName);
else if (classes.indexOf("Champion") != -1)
attackerUnitTemplates[identity.Civ].champions.push(templateName);
}
this.debugLog("Attacker templates:");
this.debugLog(uneval(attackerUnitTemplates));
};
Trigger.prototype.SetDisableTemplates = function()
{
for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
cmpPlayer.SetDisabledTemplates(disabledTemplates(cmpPlayer.GetCiv()));
}
};
/**
* Remember civic centers and make women invincible.
*/
Trigger.prototype.InitStartingUnits = function()
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i)
{
let playerEntities = cmpRangeManager.GetEntitiesByPlayer(i);
for (let entity of playerEntities)
{
if (TriggerHelper.EntityHasClass(entity, "CivilCentre"))
this.playerCivicCenter[i] = entity;
else if (TriggerHelper.EntityHasClass(entity, "FemaleCitizen"))
{
this.treasureFemale[i] = entity;
let cmpDamageReceiver = Engine.QueryInterface(entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(true);
let cmpHealth = Engine.QueryInterface(entity, IID_Health);
cmpHealth.SetUndeletable(true);
}
}
}
};
Trigger.prototype.InitializeEnemyWaves = function()
{
let time = randFloat(...firstWaveTime) * 60 * 1000;
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).AddTimeNotification({
"message": markForTranslation("The first wave will start in %(time)s!"),
"translateMessage": true
}, time);
this.DoAfterDelay(time, "StartAnEnemyWave", {});
};
Trigger.prototype.StartAnEnemyWave = function()
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let attackerTemplates = attackerEntityTemplates[Math.floor(Math.random() * attackerEntityTemplates.length)];
// A soldier for each 2-3 minutes of the game. Should be waves of 20 soldiers after an hour
let nextTime = Math.round(120000 + Math.random() * 60000);
let attackersPerTemplate = Math.ceil(cmpTimer.GetTime() / nextTime / attackerTemplates.length);
let spawned = false;
let currentMin = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
let nextWaveTime = randFloat(...waveTime);
let civ = pickRandom(Object.keys(attackerUnitTemplates));
// Determine total attacker count of the current wave.
// Exponential increase with time, capped to the limit and fluctuating proportionally with the current wavetime.
let totalAttackers = Math.ceil(Math.min(totalAttackerLimit,
initialAttackers * Math.pow(percentPerMinute, currentMin) * nextWaveTime / waveTime[1]));
this.debugLog("Spawning " + totalAttackers + " attackers");
let attackerTemplates = [];
// Add hero
if (currentMin > randFloat(...heroTime) && attackerUnitTemplates[civ].heroes.length)
{
this.debugLog("Spawning hero");
attackerTemplates.push({
"template": pickRandom(attackerUnitTemplates[civ].heroes),
"count": 1,
"hero": true
});
--totalAttackers;
}
// Random siege to champion ratio
let siegeRatio = randFloat(...siegeFraction);
let siegeCount = Math.round(siegeRatio * totalAttackers);
this.debugLog("Siege Ratio: " + Math.round(siegeRatio * 100) + "%");
let attackerTypeCounts = {
"siege": siegeCount,
"champions": totalAttackers - siegeCount
};
this.debugLog("Spawning:" + uneval(attackerTypeCounts));
// Random ratio of the given templates
for (let attackerType in attackerTypeCounts)
{
let attackerTypeTemplates = attackerUnitTemplates[civ][attackerType];
let attackerEntityRatios = new Array(attackerTypeTemplates.length).fill(1).map(i => randFloat(0, 1));
let attackerEntityRatioSum = attackerEntityRatios.reduce((current, sum) => current + sum, 0);
let remainder = attackerTypeCounts[attackerType];
for (let i in attackerTypeTemplates)
{
let count =
+i == attackerTypeTemplates.length - 1 ?
remainder :
Math.round(attackerEntityRatios[i] / attackerEntityRatioSum * attackerTypeCounts[attackerType]);
attackerTemplates.push({
"template": attackerTypeTemplates[i],
"count": count
});
remainder -= count;
}
if (remainder != 0)
warn("Didn't spawn as many attackers as intended: " + remainder);
}
this.debugLog("Templates: " + uneval(attackerTemplates));
// Spawn the templates
let spawned = false;
for (let point of this.GetTriggerPoints("A"))
{
let cmpPlayer = QueryOwnerInterface(point, IID_Player);
if (cmpPlayer.GetPlayerID() == 0 || cmpPlayer.GetState() != "active")
continue;
let cmpPosition = Engine.QueryInterface(this.playerCivicCenter[cmpPlayer.GetPlayerID()], IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld)
continue;
let targetPos = cmpPosition.GetPosition();
for (let template of attackerTemplates)
if (dryRun)
{
let entities = TriggerHelper.SpawnUnits(point, template, attackersPerTemplate, 0);
spawned = true;
break;
}
let cmpPlayer = QueryOwnerInterface(point, IID_Player);
// Trigger point owned by Gaia if the player is defeated
if (cmpPlayer.GetPlayerID() == 0)
continue;
let targetPos = Engine.QueryInterface(this.playerCivicCenter[cmpPlayer.GetPlayerID()], IID_Position).GetPosition2D();
for (let attackerTemplate of attackerTemplates)
{
// Don't spawn gaia hero if the previous one is still alive
if (attackerTemplate.hero && this.gaiaHeroes[cmpPlayer.GetPlayerID()])
{
let cmpHealth = Engine.QueryInterface(this.gaiaHeroes[cmpPlayer.GetPlayerID()], IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() != 0)
{
this.debugLog("Not spawning hero for player " + cmpPlayer.GetPlayerID() + " as the previous one is still alive");
continue;
}
}
if (dryRun)
continue;
let entities = TriggerHelper.SpawnUnits(point, attackerTemplate.template, attackerTemplate.count, 0);
ProcessCommand(0, {
"type": "attack-walk",
"entities": entities,
"x": targetPos.x,
"z": targetPos.z,
"z": targetPos.y,
"queued": true,
"targetClasses": undefined
});
if (attackerTemplate.hero)
this.gaiaHeroes[cmpPlayer.GetPlayerID()] = entities[0];
}
spawned = true;
}
@ -155,77 +322,38 @@ Trigger.prototype.StartAnEnemyWave = function()
"message": markForTranslation("An enemy wave is attacking!"),
"translateMessage": true
});
this.DoAfterDelay(nextTime, "StartAnEnemyWave", {}); // The next wave will come in 3 minutes
};
Trigger.prototype.InitGame = function()
{
let numberOfPlayers = TriggerHelper.GetNumberOfPlayers();
// Find all of the civic centers, disable some structures
for (let i = 1; i < numberOfPlayers; ++i)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let playerEntities = cmpRangeManager.GetEntitiesByPlayer(i); // Get all of each player's entities
for (let entity of playerEntities)
{
if (TriggerHelper.EntityHasClass(entity, "CivilCentre"))
this.playerCivicCenter[i] = entity;
else if (TriggerHelper.EntityHasClass(entity, "FemaleCitizen"))
{
let cmpDamageReceiver = Engine.QueryInterface(entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(true);
let cmpHealth = Engine.QueryInterface(entity, IID_Health);
cmpHealth.SetUndeletable(true);
}
}
}
this.PlaceTreasures();
for (let i = 1; i < numberOfPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
let civ = cmpPlayer.GetCiv();
cmpPlayer.SetDisabledTemplates(disabledTemplates(civ));
}
this.DoAfterDelay(nextWaveTime * 60 * 1000, "StartAnEnemyWave", {});
};
Trigger.prototype.PlaceTreasures = function()
{
let point = ["B", "C", "D"][Math.floor(Math.random() * 3)];
let point = pickRandom(["B", "C", "D"]);
let triggerPoints = this.GetTriggerPoints(point);
for (let point of triggerPoints)
{
let template = treasures[Math.floor(Math.random() * treasures.length)];
TriggerHelper.SpawnUnits(point, template, 1, 0);
}
this.DoAfterDelay(4*60*1000, "PlaceTreasures", {}); // Place more treasures after 4 minutes
TriggerHelper.SpawnUnits(point, pickRandom(treasures), 1, 0);
this.DoAfterDelay(randFloat(...treasureTime) * 60 * 1000, "PlaceTreasures", {});
};
Trigger.prototype.InitializeEnemyWaves = function()
{
let time = (5 + Math.round(Math.random() * 10)) * 60 * 1000;
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.AddTimeNotification({
"message": markForTranslation("The first wave will start in %(time)s!"),
"translateMessage": true
}, time);
this.DoAfterDelay(time, "StartAnEnemyWave", {});
};
Trigger.prototype.DefeatPlayerOnceCCIsDestroyed = function(data)
Trigger.prototype.OnOwnershipChanged = function(data)
{
if (data.entity == this.playerCivicCenter[data.from])
TriggerHelper.DefeatPlayer(data.from);
else if (data.entity == this.treasureFemale[data.from])
{
this.treasureFemale[data.from] = undefined;
Engine.DestroyEntity(data.entity);
}
};
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.playerCivicCenter = {};
cmpTrigger.DoAfterDelay(1000, "InitializeEnemyWaves", {});
cmpTrigger.RegisterTrigger("OnInitGame", "InitGame", { "enabled": true });
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "DefeatPlayerOnceCCIsDestroyed", { "enabled": true });
cmpTrigger.treasureFemale = [];
cmpTrigger.playerCivicCenter = [];
cmpTrigger.gaiaHeroes = [];
cmpTrigger.RegisterTrigger("OnInitGame", "InitSurvival", { "enabled": true });
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "OnOwnershipChanged", { "enabled": true });
}