460 lines
14 KiB
JavaScript
Executable File
460 lines
14 KiB
JavaScript
Executable File
/*
|
|
* Military strategy:
|
|
* * Try training an attack squad of a specified size
|
|
* * When it's the appropriate size, send it to attack the enemy
|
|
* * Repeat forever
|
|
*
|
|
*/
|
|
|
|
var MilitaryAttackManager = function() {
|
|
// these use the structure soldiers[unitId] = true|false to register the units
|
|
this.attackManagers = [AttackMoveToLocation];
|
|
this.availableAttacks = [];
|
|
this.currentAttacks = [];
|
|
|
|
// Counts how many attacks we have sent at the enemy.
|
|
this.attackCount = 0;
|
|
this.lastAttackTime = 0;
|
|
|
|
this.defenceManager = new Defence();
|
|
};
|
|
|
|
MilitaryAttackManager.prototype.init = function(gameState) {
|
|
var civ = gameState.playerData.civ;
|
|
|
|
// load units and buildings from the config files
|
|
|
|
if (civ in Config.buildings.moderate){
|
|
this.bModerate = Config.buildings.moderate[civ];
|
|
}else{
|
|
this.bModerate = Config.buildings.moderate['default'];
|
|
}
|
|
|
|
if (civ in Config.buildings.advanced){
|
|
this.bAdvanced = Config.buildings.advanced[civ];
|
|
}else{
|
|
this.bAdvanced = Config.buildings.advanced['default'];
|
|
}
|
|
|
|
if (civ in Config.buildings.fort){
|
|
this.bFort = Config.buildings.fort[civ];
|
|
}else{
|
|
this.bFort = Config.buildings.fort['default'];
|
|
}
|
|
|
|
for (var i in this.bAdvanced){
|
|
this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]);
|
|
}
|
|
for (var i in this.bFort){
|
|
this.bFort[i] = gameState.applyCiv(this.bFort[i]);
|
|
}
|
|
|
|
this.getEconomicTargets = function(gameState, militaryManager){
|
|
return militaryManager.getEnemyBuildings(gameState, "Economic");
|
|
};
|
|
// TODO: figure out how to make this generic
|
|
for (var i in this.attackManagers){
|
|
this.availableAttacks[i] = new this.attackManagers[i](gameState, this);
|
|
}
|
|
|
|
var enemies = gameState.getEnemyEntities();
|
|
var filter = Filters.byClassesOr(["CitizenSoldier", "Champion", "Hero", "Siege"]);
|
|
this.enemySoldiers = enemies.filter(filter); // TODO: cope with diplomacy changes
|
|
this.enemySoldiers.registerUpdates();
|
|
};
|
|
|
|
/**
|
|
* @param (GameState) gameState
|
|
* @param (string) soldierTypes
|
|
* @returns array of soldiers for which training buildings exist
|
|
*/
|
|
MilitaryAttackManager.prototype.findTrainableUnits = function(gameState, soldierType){
|
|
var allTrainable = [];
|
|
gameState.getOwnEntities().forEach(function(ent) {
|
|
var trainable = ent.trainableEntities();
|
|
for (var i in trainable){
|
|
if (allTrainable.indexOf(trainable[i]) === -1){
|
|
allTrainable.push(trainable[i]);
|
|
}
|
|
}
|
|
});
|
|
|
|
var ret = [];
|
|
for (var i in allTrainable){
|
|
var template = gameState.getTemplate(allTrainable[i]);
|
|
if (soldierType == this.getSoldierType(template)){
|
|
ret.push(allTrainable[i]);
|
|
}
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
// Returns the type of a soldier, either citizenSoldier, advanced or siege
|
|
MilitaryAttackManager.prototype.getSoldierType = function(ent){
|
|
if (ent.hasClass("Hero")){
|
|
return undefined;
|
|
}
|
|
if (ent.hasClass("CitizenSoldier") && !ent.hasClass("Cavalry")){
|
|
return "citizenSoldier";
|
|
}else if (ent.hasClass("Champion") || ent.hasClass("CitizenSoldier")){
|
|
return "advanced";
|
|
}else if (ent.hasClass("Siege")){
|
|
return "siege";
|
|
}else{
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the unit type we should begin training. (Currently this is whatever
|
|
* we have least of.)
|
|
*/
|
|
MilitaryAttackManager.prototype.findBestNewUnit = function(gameState, queue, soldierType) {
|
|
var units = this.findTrainableUnits(gameState, soldierType);
|
|
// Count each type
|
|
var types = [];
|
|
for ( var tKey in units) {
|
|
var t = units[tKey];
|
|
types.push([t, gameState.countEntitiesAndQueuedByType(gameState.applyCiv(t))
|
|
+ queue.countAllByType(gameState.applyCiv(t)) ]);
|
|
}
|
|
|
|
// Sort by increasing count
|
|
types.sort(function(a, b) {
|
|
return a[1] - b[1];
|
|
});
|
|
|
|
if (types.length === 0){
|
|
return false;
|
|
}
|
|
return types[0][0];
|
|
};
|
|
|
|
MilitaryAttackManager.prototype.registerSoldiers = function(gameState) {
|
|
var soldiers = gameState.getOwnEntitiesByRole("soldier");
|
|
var self = this;
|
|
|
|
soldiers.forEach(function(ent) {
|
|
ent.setMetadata("role", "military");
|
|
ent.setMetadata("military", "unassigned");
|
|
});
|
|
};
|
|
|
|
// return count of enemy buildings for a given building class
|
|
MilitaryAttackManager.prototype.getEnemyBuildings = function(gameState,cls) {
|
|
var targets = gameState.entities.filter(function(ent) {
|
|
return (gameState.isEntityEnemy(ent) && ent.hasClass("Structure") && ent.hasClass(cls) && ent.owner() !== 0 && ent.position());
|
|
});
|
|
return targets;
|
|
};
|
|
|
|
// return n available units and makes these units unavailable
|
|
MilitaryAttackManager.prototype.getAvailableUnits = function(n, filter) {
|
|
var ret = [];
|
|
var count = 0;
|
|
|
|
var units = undefined;
|
|
|
|
if (filter){
|
|
units = this.getUnassignedUnits().filter(filter);
|
|
}else{
|
|
units = this.getUnassignedUnits();
|
|
}
|
|
|
|
units.forEach(function(ent){
|
|
ret.push(ent.id());
|
|
ent.setMetadata("military", "assigned");
|
|
ent.setMetadata("role", "military");
|
|
count++;
|
|
if (count >= n) {
|
|
return;
|
|
}
|
|
});
|
|
return ret;
|
|
};
|
|
|
|
// Takes a single unit id, and marks it unassigned
|
|
MilitaryAttackManager.prototype.unassignUnit = function(unit){
|
|
this.entity(unit).setMetadata("military", "unassigned");
|
|
};
|
|
|
|
// Takes an array of unit id's and marks all of them unassigned
|
|
MilitaryAttackManager.prototype.unassignUnits = function(units){
|
|
for (var i in units){
|
|
this.unassignUnit(units[i]);
|
|
}
|
|
};
|
|
|
|
MilitaryAttackManager.prototype.getUnassignedUnits = function(){
|
|
return this.gameState.getOwnEntitiesByMetadata("military", "unassigned");
|
|
};
|
|
|
|
MilitaryAttackManager.prototype.countAvailableUnits = function(filter){
|
|
var count = 0;
|
|
if (filter){
|
|
return this.getUnassignedUnits().filter(filter).length;
|
|
}else{
|
|
return this.getUnassignedUnits().length;
|
|
}
|
|
};
|
|
|
|
// Takes an entity id and returns an entity object or undefined if there is no entity with that id
|
|
// Also sends a debug message warning if the id has no entity
|
|
MilitaryAttackManager.prototype.entity = function(id) {
|
|
return this.gameState.getEntityById(id);
|
|
};
|
|
|
|
// Returns the military strength of unit
|
|
MilitaryAttackManager.prototype.getUnitStrength = function(ent){
|
|
var strength = 0.0;
|
|
var attackTypes = ent.attackTypes();
|
|
var armourStrength = ent.armourStrengths();
|
|
var hp = 2 * ent.hitpoints() / (160 + 1*ent.maxHitpoints()); //100 = typical number of hitpoints
|
|
for (var typeKey in attackTypes) {
|
|
var type = attackTypes[typeKey];
|
|
var attackStrength = ent.attackStrengths(type);
|
|
var attackRange = ent.attackRange(type);
|
|
var attackTimes = ent.attackTimes(type);
|
|
for (var str in attackStrength) {
|
|
var val = parseFloat(attackStrength[str]);
|
|
switch (str) {
|
|
case "crush":
|
|
strength += (val * 0.085) / 3;
|
|
break;
|
|
case "hack":
|
|
strength += (val * 0.075) / 3;
|
|
break;
|
|
case "pierce":
|
|
strength += (val * 0.065) / 3;
|
|
break;
|
|
}
|
|
}
|
|
if (attackRange){
|
|
strength += (attackRange.max * 0.0125) ;
|
|
}
|
|
for (var str in attackTimes) {
|
|
var val = parseFloat(attackTimes[str]);
|
|
switch (str){
|
|
case "repeat":
|
|
strength += (val / 100000);
|
|
break;
|
|
case "prepare":
|
|
strength -= (val / 100000);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
for (var str in armourStrength) {
|
|
var val = parseFloat(armourStrength[str]);
|
|
switch (str) {
|
|
case "crush":
|
|
strength += (val * 0.085) / 3;
|
|
break;
|
|
case "hack":
|
|
strength += (val * 0.075) / 3;
|
|
break;
|
|
case "pierce":
|
|
strength += (val * 0.065) / 3;
|
|
break;
|
|
}
|
|
}
|
|
return strength * hp;
|
|
};
|
|
|
|
// Returns the strength of the available units of ai army
|
|
MilitaryAttackManager.prototype.measureAvailableStrength = function(){
|
|
var strength = 0.0;
|
|
var self = this;
|
|
this.getUnassignedUnits(this.gameState).forEach(function(ent){
|
|
strength += self.getUnitStrength(ent);
|
|
});
|
|
return strength;
|
|
};
|
|
|
|
MilitaryAttackManager.prototype.getEnemySoldiers = function(){
|
|
return this.enemySoldiers;
|
|
};
|
|
|
|
// Returns the number of units in the largest enemy army
|
|
MilitaryAttackManager.prototype.measureEnemyCount = function(gameState){
|
|
// Measure enemy units
|
|
var isEnemy = gameState.playerData.isEnemy;
|
|
var enemyCount = [];
|
|
var maxCount = 0;
|
|
for ( var i = 1; i < isEnemy.length; i++) {
|
|
enemyCount[i] = 0;
|
|
}
|
|
|
|
// Loop through the enemy soldiers and add one to the count for that soldiers player's count
|
|
this.enemySoldiers.forEach(function(ent) {
|
|
enemyCount[ent.owner()]++;
|
|
|
|
if (enemyCount[ent.owner()] > maxCount) {
|
|
maxCount = enemyCount[ent.owner()];
|
|
}
|
|
});
|
|
|
|
return maxCount;
|
|
};
|
|
|
|
// Returns the strength of the largest enemy army
|
|
MilitaryAttackManager.prototype.measureEnemyStrength = function(gameState){
|
|
// Measure enemy strength
|
|
var isEnemy = gameState.playerData.isEnemy;
|
|
var enemyStrength = [];
|
|
var maxStrength = 0;
|
|
var self = this;
|
|
|
|
for ( var i = 1; i < isEnemy.length; i++) {
|
|
enemyStrength[i] = 0;
|
|
}
|
|
|
|
// Loop through the enemy soldiers and add the strength to that soldiers player's total strength
|
|
this.enemySoldiers.forEach(function(ent) {
|
|
enemyStrength[ent.owner()] += self.getUnitStrength(ent);
|
|
|
|
if (enemyStrength[ent.owner()] > maxStrength) {
|
|
maxStrength = enemyStrength[ent.owner()];
|
|
}
|
|
});
|
|
|
|
return maxStrength;
|
|
};
|
|
|
|
// Adds towers to the defenceBuilding queue
|
|
MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
|
|
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower'))
|
|
+ queues.defenceBuilding.totalLength() < gameState.getBuildLimits()["DefenseTower"]) {
|
|
|
|
|
|
gameState.getOwnEntities().forEach(function(dropsiteEnt) {
|
|
if (dropsiteEnt.resourceDropsiteTypes() && dropsiteEnt.getMetadata("defenseTower") !== true){
|
|
var position = dropsiteEnt.position();
|
|
if (position){
|
|
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, 'structures/{civ}_defense_tower', position));
|
|
}
|
|
dropsiteEnt.setMetadata("defenseTower", true);
|
|
}
|
|
});
|
|
}
|
|
|
|
var numFortresses = 0;
|
|
for (var i in this.bFort){
|
|
numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i]));
|
|
}
|
|
|
|
if (numFortresses + queues.defenceBuilding.totalLength() < gameState.getBuildLimits()["Fortress"]) {
|
|
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > gameState.ai.modules["economy"].targetNumWorkers * 0.5){
|
|
if (gameState.getTimeElapsed() > 350 * 1000 * numFortresses){
|
|
if (gameState.ai.pathsToMe && gameState.ai.pathsToMe.length > 0){
|
|
var position = gameState.ai.pathsToMe.shift();
|
|
// TODO: pick a fort randomly from the list.
|
|
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, this.bFort[0], position));
|
|
}else{
|
|
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, this.bFort[0]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
MilitaryAttackManager.prototype.constructTrainingBuildings = function(gameState, queues) {
|
|
// Build more military buildings
|
|
// TODO: make military building better
|
|
Engine.ProfileStart("Build buildings");
|
|
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > 30) {
|
|
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0]))
|
|
+ queues.militaryBuilding.totalLength() < 1) {
|
|
queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0]));
|
|
}
|
|
}
|
|
//build advanced military buildings
|
|
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) >
|
|
gameState.ai.modules["economy"].targetNumWorkers * 0.7){
|
|
if (queues.militaryBuilding.totalLength() === 0){
|
|
for (var i in this.bAdvanced){
|
|
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i])) < 1){
|
|
queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bAdvanced[i]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Engine.ProfileStop();
|
|
};
|
|
|
|
MilitaryAttackManager.prototype.trainMilitaryUnits = function(gameState, queues){
|
|
Engine.ProfileStart("Train Units");
|
|
// Continually try training new units, in batches of 5
|
|
if (queues.citizenSoldier.length() < 6) {
|
|
var newUnit = this.findBestNewUnit(gameState, queues.citizenSoldier, "citizenSoldier");
|
|
if (newUnit){
|
|
queues.citizenSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, {
|
|
"role" : "soldier"
|
|
}, 5));
|
|
}
|
|
}
|
|
if (queues.advancedSoldier.length() < 2) {
|
|
var newUnit = this.findBestNewUnit(gameState, queues.advancedSoldier, "advanced");
|
|
if (newUnit){
|
|
queues.advancedSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, {
|
|
"role" : "soldier"
|
|
}, 5));
|
|
}
|
|
}
|
|
if (queues.siege.length() < 4) {
|
|
var newUnit = this.findBestNewUnit(gameState, queues.siege, "siege");
|
|
if (newUnit){
|
|
queues.siege.addItem(new UnitTrainingPlan(gameState, newUnit, {
|
|
"role" : "soldier"
|
|
}, 2));
|
|
}
|
|
}
|
|
Engine.ProfileStop();
|
|
};
|
|
|
|
MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
|
|
var self = this;
|
|
Engine.ProfileStart("military update");
|
|
this.gameState = gameState;
|
|
|
|
this.registerSoldiers(gameState);
|
|
|
|
this.trainMilitaryUnits(gameState, queues);
|
|
|
|
this.constructTrainingBuildings(gameState, queues);
|
|
|
|
this.buildDefences(gameState, queues);
|
|
|
|
this.defenceManager.update(gameState, events, this);
|
|
|
|
Engine.ProfileStart("Plan new attacks");
|
|
// Look for attack plans which can be executed, only do this once every minute
|
|
for (var i = 0; i < this.availableAttacks.length; i++){
|
|
if (this.availableAttacks[i].canExecute(gameState, this)){
|
|
this.availableAttacks[i].execute(gameState, this);
|
|
this.currentAttacks.push(this.availableAttacks[i]);
|
|
//debug("Attacking!");
|
|
}
|
|
this.availableAttacks.splice(i, 1, new this.attackManagers[i](gameState, this));
|
|
}
|
|
Engine.ProfileStop();
|
|
|
|
Engine.ProfileStart("Update attacks");
|
|
// Keep current attacks updated
|
|
for (var i in this.currentAttacks){
|
|
this.currentAttacks[i].update(gameState, this, events);
|
|
}
|
|
Engine.ProfileStop();
|
|
|
|
Engine.ProfileStart("Use idle military as workers");
|
|
// Set unassigned to be workers TODO: fix this so it doesn't scan all units every time
|
|
this.getUnassignedUnits(gameState).forEach(function(ent){
|
|
if (self.getSoldierType(ent) === "citizenSoldier"){
|
|
ent.setMetadata("role", "worker");
|
|
}
|
|
});
|
|
Engine.ProfileStop();
|
|
|
|
Engine.ProfileStop();
|
|
};
|