Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 23865)
@@ -1,2173 +1,2173 @@
/**
* This is an attack plan:
* It deals with everything in an attack, from picking a target to picking a path to it
* To making sure units are built, and pushing elements to the queue manager otherwise
* It also handles the actual attack, though much work is needed on that.
*/
PETRA.AttackPlan = function(gameState, Config, uniqueID, type, data)
{
this.Config = Config;
this.name = uniqueID;
this.type = type || "Attack";
this.state = "unexecuted";
this.forced = false; // true when this attacked has been forced to help an ally
if (data && data.target)
{
this.target = data.target;
this.targetPos = this.target.position();
this.targetPlayer = this.target.owner();
}
else
{
this.target = undefined;
this.targetPos = undefined;
this.targetPlayer = undefined;
}
this.uniqueTargetId = data && data.uniqueTargetId || undefined;
// get a starting rallyPoint ... will be improved later
let rallyPoint;
let rallyAccess;
let allAccesses = {};
for (let base of gameState.ai.HQ.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
let access = PETRA.getLandAccess(gameState, base.anchor);
if (!rallyPoint)
{
rallyPoint = base.anchor.position();
rallyAccess = access;
}
if (!allAccesses[access])
allAccesses[access] = base.anchor.position();
}
if (!rallyPoint) // no base ? take the position of any of our entities
{
for (let ent of gameState.getOwnEntities().values())
{
if (!ent.position())
continue;
let access = PETRA.getLandAccess(gameState, ent);
rallyPoint = ent.position();
rallyAccess = access;
allAccesses[access] = rallyPoint;
break;
}
if (!rallyPoint)
{
this.failed = true;
return false;
}
}
this.rallyPoint = rallyPoint;
this.overseas = 0;
if (gameState.ai.HQ.navalMap)
{
for (let structure of gameState.getEnemyStructures().values())
{
if (this.target && structure.id() != this.target.id())
continue;
if (!structure.position())
continue;
let access = PETRA.getLandAccess(gameState, structure);
if (access in allAccesses)
{
this.overseas = 0;
this.rallyPoint = allAccesses[access];
break;
}
else if (!this.overseas)
{
let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access);
if (!sea)
{
if (this.target)
{
API3.warn("Petra: " + this.type + " " + this.name + " has an inaccessible target " +
this.target.templateName() + " indices " + rallyAccess + " " + access);
this.failed = true;
return false;
}
continue;
}
this.overseas = sea;
gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, sea, 1);
}
}
}
this.paused = false;
this.maxCompletingTime = 0;
// priority of the queues we'll create.
let priority = 70;
// unitStat priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize".
// if not, this is a "bonus". The higher the priority, the faster this unit will get built.
// Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm)
// Eg: if all are priority 1, and the siege is 0.5, the siege units will get built
// only once every other category is at least 50% of its target size.
// note: siege build order is currently added by the military manager if a fortress is there.
this.unitStat = {};
// neededShips is the minimal number of ships which should be available for transport
if (type == "Rush")
{
priority = 250;
this.unitStat.Infantry = { "priority": 1, "minSize": 10, "targetSize": 20, "batchSize": 2, "classes": ["Infantry"],
"interests": [["strength", 1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"]] };
this.unitStat.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"],
"interests": [["strength", 1]] };
if (data && data.targetSize)
this.unitStat.Infantry.targetSize = data.targetSize;
this.neededShips = 1;
}
else if (type == "Raid")
{
priority = 150;
this.unitStat.Cavalry = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"],
"interests": [ ["strength", 1] ] };
this.neededShips = 1;
}
else if (type == "HugeAttack")
{
priority = 90;
// basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units.
this.unitStat.RangedInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry", "Ranged", "CitizenSoldier"],
"interests": [["strength", 3]] };
this.unitStat.MeleeInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry", "Melee", "CitizenSoldier"],
"interests": [["strength", 3]] };
this.unitStat.ChampRangedInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Ranged", "Champion"],
"interests": [["strength", 3]] };
this.unitStat.ChampMeleeInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Melee", "Champion"],
"interests": [["strength", 3]] };
this.unitStat.RangedCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Ranged", "CitizenSoldier"],
"interests": [["strength", 2]] };
this.unitStat.MeleeCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Melee", "CitizenSoldier"],
"interests": [["strength", 2]] };
this.unitStat.ChampRangedCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Ranged", "Champion"],
"interests": [["strength", 3]] };
this.unitStat.ChampMeleeCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Melee", "Champion"],
"interests": [["strength", 2]] };
this.unitStat.Hero = { "priority": 1, "minSize": 0, "targetSize": 1, "batchSize": 1, "classes": ["Hero"],
"interests": [["strength", 2]] };
this.neededShips = 5;
}
else
{
priority = 70;
this.unitStat.RangedInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry", "Ranged"],
"interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] };
this.unitStat.MeleeInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry", "Melee"],
"interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] };
this.unitStat.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"],
"interests": [["strength", 1]] };
this.neededShips = 3;
}
// Put some randomness on the attack size
let variation = randFloat(0.8, 1.2);
// and lower priority and smaller sizes for easier difficulty levels
if (this.Config.difficulty < 2)
{
priority *= 0.6;
variation *= 0.5;
}
else if (this.Config.difficulty < 3)
{
priority *= 0.8;
variation *= 0.8;
}
for (let cat in this.unitStat)
{
this.unitStat[cat].targetSize = Math.round(variation * this.unitStat[cat].targetSize);
this.unitStat[cat].minSize = Math.min(this.unitStat[cat].minSize, this.unitStat[cat].targetSize);
}
// change the sizes according to max population
this.neededShips = Math.ceil(this.Config.popScaling * this.neededShips);
for (let cat in this.unitStat)
{
this.unitStat[cat].targetSize = Math.round(this.Config.popScaling * this.unitStat[cat].targetSize);
this.unitStat[cat].minSize = Math.ceil(this.Config.popScaling * this.unitStat[cat].minSize);
}
// TODO: there should probably be one queue per type of training building
gameState.ai.queueManager.addQueue("plan_" + this.name, priority);
gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority+1);
gameState.ai.queueManager.addQueue("plan_" + this.name +"_siege", priority);
// each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ]
this.buildOrders = [];
this.canBuildUnits = gameState.ai.HQ.canBuildUnits;
this.siegeState = 0; // 0 = not yet tested, 1 = not yet any siege trainer, 2 = siege added in build orders
// some variables used during the attack
this.position5TurnsAgo = [0, 0];
this.lastPosition = [0, 0];
this.position = [0, 0];
this.isBlocked = false; // true when this attack faces walls
return true;
};
PETRA.AttackPlan.prototype.init = function(gameState)
{
this.queue = gameState.ai.queues["plan_" + this.name];
this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"];
this.queueSiege = gameState.ai.queues["plan_" + this.name +"_siege"];
this.unitCollection = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", this.name));
this.unitCollection.registerUpdates();
this.unit = {};
// defining the entity collections. Will look for units I own, that are part of this plan.
// Also defining the buildOrders.
for (let cat in this.unitStat)
{
let Unit = this.unitStat[cat];
this.unit[cat] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes));
this.unit[cat].registerUpdates();
if (this.canBuildUnits)
this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]);
}
};
PETRA.AttackPlan.prototype.getName = function()
{
return this.name;
};
PETRA.AttackPlan.prototype.getType = function()
{
return this.type;
};
PETRA.AttackPlan.prototype.isStarted = function()
{
return this.state !== "unexecuted" && this.state !== "completing";
};
PETRA.AttackPlan.prototype.isPaused = function()
{
return this.paused;
};
PETRA.AttackPlan.prototype.setPaused = function(boolValue)
{
this.paused = boolValue;
};
/**
* Returns true if the attack can be executed at the current time
* Basically it checks we have enough units.
*/
PETRA.AttackPlan.prototype.canStart = function()
{
if (!this.canBuildUnits)
return true;
for (let unitCat in this.unitStat)
if (this.unit[unitCat].length < this.unitStat[unitCat].minSize)
return false;
return true;
};
PETRA.AttackPlan.prototype.mustStart = function()
{
if (this.isPaused())
return false;
if (!this.canBuildUnits)
return this.unitCollection.hasEntities();
let MaxReachedEverywhere = true;
let MinReachedEverywhere = true;
for (let unitCat in this.unitStat)
{
let Unit = this.unitStat[unitCat];
if (this.unit[unitCat].length < Unit.targetSize)
MaxReachedEverywhere = false;
if (this.unit[unitCat].length < Unit.minSize)
{
MinReachedEverywhere = false;
break;
}
}
if (MaxReachedEverywhere)
return true;
if (MinReachedEverywhere)
return this.type == "Raid" && this.target && this.target.foundationProgress() &&
this.target.foundationProgress() > 50;
return false;
};
PETRA.AttackPlan.prototype.forceStart = function()
{
for (let unitCat in this.unitStat)
{
let Unit = this.unitStat[unitCat];
Unit.targetSize = 0;
Unit.minSize = 0;
}
this.forced = true;
};
PETRA.AttackPlan.prototype.emptyQueues = function()
{
this.queue.empty();
this.queueChamp.empty();
this.queueSiege.empty();
};
PETRA.AttackPlan.prototype.removeQueues = function(gameState)
{
gameState.ai.queueManager.removeQueue("plan_" + this.name);
gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ");
gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege");
};
/** Adds a build order. If resetQueue is true, this will reset the queue. */
PETRA.AttackPlan.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue)
{
if (!this.isStarted())
{
// no minsize as we don't want the plan to fail at the last minute though.
this.unitStat[name] = unitStats;
let Unit = this.unitStat[name];
this.unit[name] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes));
this.unit[name].registerUpdates();
this.buildOrders.push([0, Unit.classes, this.unit[name], Unit, name]);
if (resetQueue)
this.emptyQueues();
}
};
PETRA.AttackPlan.prototype.addSiegeUnits = function(gameState)
{
if (this.siegeState == 2 || this.state !== "unexecuted")
return false;
let civ = gameState.getPlayerCiv();
let classes = [[ "Siege", "Melee"], ["Siege", "Ranged"], ["Elephant", "Melee", "Champion"]];
let hasTrainer = [false, false, false];
for (let ent of gameState.getOwnTrainingFacilities().values())
{
let trainables = ent.trainableEntities(civ);
if (!trainables)
continue;
for (let trainable of trainables)
{
if (gameState.isTemplateDisabled(trainable))
continue;
let template = gameState.getTemplate(trainable);
if (!template || !template.available(gameState))
continue;
for (let i = 0; i < classes.length; ++i)
if (classes[i].every(c => template.hasClass(c)))
hasTrainer[i] = true;
}
}
if (hasTrainer.every(e => !e))
return false;
let i = this.name % classes.length;
for (let k = 0; k < classes.length; ++k)
{
if (hasTrainer[i])
break;
i = ++i % classes.length;
}
this.siegeState = 2;
let targetSize;
if (this.Config.difficulty < 3)
targetSize = this.type == "HugeAttack" ? Math.max(this.Config.difficulty, 1) : Math.max(this.Config.difficulty - 1, 0);
else
targetSize = this.type == "HugeAttack" ? this.Config.difficulty + 1 : this.Config.difficulty - 1;
targetSize = Math.max(Math.round(this.Config.popScaling * targetSize), this.type == "HugeAttack" ? 1 : 0);
if (!targetSize)
return true;
// no minsize as we don't want the plan to fail at the last minute though.
let stat = { "priority": 1, "minSize": 0, "targetSize": targetSize, "batchSize": Math.min(targetSize, 2),
"classes": classes[i], "interests": [ ["siegeStrength", 3] ] };
this.addBuildOrder(gameState, "Siege", stat, true);
return true;
};
/** Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start". */
PETRA.AttackPlan.prototype.updatePreparation = function(gameState)
{
// the completing step is used to return resources and regroup the units
// so we check that we have no more forced order before starting the attack
if (this.state == "completing")
{
// if our target was destroyed, go back to "unexecuted" state
if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id()))
{
this.state = "unexecuted";
this.target = undefined;
}
else
{
// check that all units have finished with their transport if needed
if (this.waitingForTransport())
return 1;
// bloqued units which cannot finish their order should not stop the attack
if (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder())
return 1;
return 2;
}
}
if (this.Config.debug > 3 && gameState.ai.playedTurn % 50 === 0)
this.debugAttack();
// if we need a transport, wait for some transport ships
if (this.overseas && !gameState.ai.HQ.navalManager.seaTransportShips[this.overseas].length)
return 1;
if (this.type != "Raid" || !this.forced) // Forced Raids have special purposes (as relic capture)
this.assignUnits(gameState);
if (this.type != "Raid" && gameState.ai.HQ.attackManager.getAttackInPreparation("Raid") !== undefined)
this.reassignCavUnit(gameState); // reassign some cav (if any) to fasten raid preparations
// Fasten the end game.
if (gameState.ai.playedTurn % 5 == 0 && this.hasSiegeUnits())
{
let totEnemies = 0;
let hasEnemies = false;
for (let i = 1; i < gameState.sharedScript.playersData.length; ++i)
{
if (!gameState.isPlayerEnemy(i) || gameState.ai.HQ.attackManager.defeated[i])
continue;
hasEnemies = true;
totEnemies += gameState.getEnemyUnits(i).length;
}
if (hasEnemies && this.unitCollection.length > 20 + 2 * totEnemies)
this.forceStart();
}
// special case: if we've reached max pop, and we can start the plan, start it.
if (gameState.getPopulationMax() - gameState.getPopulation() < 5)
{
let lengthMin = 16;
if (gameState.getPopulationMax() < 300)
lengthMin -= Math.floor(8 * (300 - gameState.getPopulationMax()) / 300);
if (this.canStart() || this.unitCollection.length > lengthMin)
{
this.emptyQueues();
}
else // Abort the plan so that its units will be reassigned to other plans.
{
if (this.Config.debug > 1)
{
let am = gameState.ai.HQ.attackManager;
API3.warn(" attacks upcoming: raid " + am.upcomingAttacks.Raid.length +
" rush " + am.upcomingAttacks.Rush.length +
" attack " + am.upcomingAttacks.Attack.length +
" huge " + am.upcomingAttacks.HugeAttack.length);
API3.warn(" attacks started: raid " + am.startedAttacks.Raid.length +
" rush " + am.startedAttacks.Rush.length +
" attack " + am.startedAttacks.Attack.length +
" huge " + am.startedAttacks.HugeAttack.length);
}
return 0;
}
}
else if (this.mustStart())
{
if (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0)
{
// keep on while the units finish being trained, then we'll start
this.emptyQueues();
return 1;
}
}
else
{
if (this.canBuildUnits)
{
// We still have time left to recruit units and do stuffs.
if (this.siegeState == 0 || this.siegeState == 1 && gameState.ai.playedTurn % 5 == 0)
this.addSiegeUnits(gameState);
this.trainMoreUnits(gameState);
// may happen if we have no more training facilities and build orders are canceled
if (!this.buildOrders.length)
return 0; // will abort the plan
}
return 1;
}
// if we're here, it means we must start
this.state = "completing";
// Raids have their predefined target
if (!this.target && !this.chooseTarget(gameState))
return 0;
if (!this.overseas)
this.getPathToTarget(gameState);
if (this.type == "Raid")
this.maxCompletingTime = this.forced ? 0 : gameState.ai.elapsedTime + 20;
else
{
if (this.type == "Rush" || this.forced)
this.maxCompletingTime = gameState.ai.elapsedTime + 40;
else
this.maxCompletingTime = gameState.ai.elapsedTime + 60;
// warn our allies so that they can help if possible
if (!this.requested)
Engine.PostCommand(PlayerID, { "type": "attack-request", "source": PlayerID, "player": this.targetPlayer });
}
// Remove those units which were in a temporary bombing attack
for (let unitIds of gameState.ai.HQ.attackManager.bombingAttacks.values())
{
for (let entId of unitIds.values())
{
let ent = gameState.getEntityById(entId);
if (!ent || ent.getMetadata(PlayerID, "plan") != this.name)
continue;
unitIds.delete(entId);
ent.stopMoving();
}
}
let rallyPoint = this.rallyPoint;
let rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint);
for (let ent of this.unitCollection.values())
{
// For the time being, if occupied in a transport, remove the unit from this plan TODO improve that
if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined)
{
ent.setMetadata(PlayerID, "plan", -1);
continue;
}
ent.setMetadata(PlayerID, "role", "attack");
ent.setMetadata(PlayerID, "subrole", "completing");
let queued = false;
if (ent.resourceCarrying() && ent.resourceCarrying().length)
queued = PETRA.returnResources(gameState, ent);
let index = PETRA.getLandAccess(gameState, ent);
if (index == rallyIndex)
ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15, queued);
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, index, rallyIndex, rallyPoint);
}
// reset all queued units
this.removeQueues(gameState);
return 1;
};
PETRA.AttackPlan.prototype.trainMoreUnits = function(gameState)
{
// let's sort by training advancement, ie 'current size / target size'
// count the number of queued units too.
// substract priority.
for (let order of this.buildOrders)
{
let special = "Plan_" + this.name + "_" + order[4];
let aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special);
aQueued += this.queue.countQueuedUnitsWithMetadata("special", special);
aQueued += this.queueChamp.countQueuedUnitsWithMetadata("special", special);
aQueued += this.queueSiege.countQueuedUnitsWithMetadata("special", special);
order[0] = order[2].length + aQueued;
}
this.buildOrders.sort((a, b) => {
let va = a[0]/a[3].targetSize - a[3].priority;
if (a[0] >= a[3].targetSize)
va += 1000;
let vb = b[0]/b[3].targetSize - b[3].priority;
if (b[0] >= b[3].targetSize)
vb += 1000;
return va - vb;
});
if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0)
{
API3.warn("====================================");
API3.warn("======== build order for plan " + this.name);
for (let order of this.buildOrders)
{
let specialData = "Plan_"+this.name+"_"+order[4];
let inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData);
let queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData);
let queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData);
let queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData);
API3.warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining +
" queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize);
}
API3.warn("====================================");
}
let firstOrder = this.buildOrders[0];
if (firstOrder[0] < firstOrder[3].targetSize)
{
// find the actual queue we want
let queue = this.queue;
if (firstOrder[3].classes.indexOf("Siege") != -1 || firstOrder[3].classes.indexOf("Elephant") != -1 &&
firstOrder[3].classes.indexOf("Melee") != -1 && firstOrder[3].classes.indexOf("Champion") != -1)
queue = this.queueSiege;
else if (firstOrder[3].classes.indexOf("Hero") != -1)
queue = this.queueSiege;
else if (firstOrder[3].classes.indexOf("Champion") != -1)
queue = this.queueChamp;
if (queue.length() <= 5)
{
let template = gameState.ai.HQ.findBestTrainableUnit(gameState, firstOrder[1], firstOrder[3].interests);
// HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder,
// effectively removing the unit from the plan.
if (template === undefined)
{
if (this.Config.debug > 1)
API3.warn("attack no template found " + firstOrder[1]);
delete this.unitStat[firstOrder[4]]; // deleting the associated unitstat.
this.buildOrders.splice(0, 1);
}
else
{
if (this.Config.debug > 2)
API3.warn("attack template " + template + " added for plan " + this.name);
let max = firstOrder[3].batchSize;
let specialData = "Plan_" + this.name + "_" + firstOrder[4];
let data = { "plan": this.name, "special": specialData, "base": 0 };
data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ? "worker" : "attack";
let trainingPlan = new PETRA.TrainingPlan(gameState, template, data, max, max);
if (trainingPlan.template)
queue.addPlan(trainingPlan);
else if (this.Config.debug > 1)
API3.warn("training plan canceled because no template for " + template + " build1 " + uneval(firstOrder[1]) +
" build3 " + uneval(firstOrder[3].interests));
}
}
}
};
PETRA.AttackPlan.prototype.assignUnits = function(gameState)
{
let plan = this.name;
let added = false;
// If we can not build units, assign all available except those affected to allied defense to the current attack
if (!this.canBuildUnits)
{
for (let ent of gameState.getOwnUnits().values())
{
if (ent.getMetadata(PlayerID, "allied") || !this.isAvailableUnit(gameState, ent))
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
return added;
}
if (this.type == "Raid")
{
// Raid are fast cavalry attack: assign all cav except some for hunting
let num = 0;
for (let ent of gameState.getOwnUnits().values())
{
if (!ent.hasClass("Cavalry") || !this.isAvailableUnit(gameState, ent))
continue;
if (num++ < 2)
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
return added;
}
// Assign all units without specific role
for (let ent of gameState.getOwnEntitiesByRole(undefined, true).values())
{
if (!ent.hasClass("Unit") || !this.isAvailableUnit(gameState, ent))
continue;
if (ent.hasClass("Ship") || ent.hasClass("Support") || ent.attackTypes() === undefined)
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
// Add units previously in a plan, but which left it because needed for defense or attack finished
for (let ent of gameState.ai.HQ.attackManager.outOfPlan.values())
{
if (!this.isAvailableUnit(gameState, ent))
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
// Finally add also some workers,
// If Rush, assign all kind of workers, keeping only a minimum number of defenders
// Otherwise, assign only some idle workers if too much of them
let num = 0;
let numbase = {};
let keep = this.type != "Rush" ?
6 + 4 * gameState.getNumPlayerEnemies() + 8 * this.Config.personality.defensive : 8;
keep = Math.round(this.Config.popScaling * keep);
for (let ent of gameState.getOwnEntitiesByRole("worker", true).values())
{
if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent))
continue;
let baseID = ent.getMetadata(PlayerID, "base");
if (baseID)
numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1;
else
{
API3.warn("Petra problem ent without base ");
PETRA.dumpEntity(ent);
continue;
}
if (num++ < keep || numbase[baseID] < 5)
continue;
if (this.type != "Rush" && ent.getMetadata(PlayerID, "subrole") != "idle")
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
return added;
};
PETRA.AttackPlan.prototype.isAvailableUnit = function(gameState, ent)
{
if (!ent.position())
return false;
if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1 ||
ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined)
return false;
if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id()) && (this.overseas || ent.healthLevel() < 0.8))
return false;
return true;
};
/** Reassign one (at each turn) Cav unit to fasten raid preparation. */
PETRA.AttackPlan.prototype.reassignCavUnit = function(gameState)
{
for (let ent of this.unitCollection.values())
{
if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
if (!ent.hasClass("Cavalry") || !ent.hasClass("CitizenSoldier"))
continue;
let raid = gameState.ai.HQ.attackManager.getAttackInPreparation("Raid");
ent.setMetadata(PlayerID, "plan", raid.name);
this.unitCollection.updateEnt(ent);
raid.unitCollection.updateEnt(ent);
return;
}
};
PETRA.AttackPlan.prototype.chooseTarget = function(gameState)
{
if (this.targetPlayer === undefined)
{
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
if (this.targetPlayer === undefined)
return false;
}
this.target = this.getNearestTarget(gameState, this.rallyPoint);
if (!this.target)
{
if (this.uniqueTargetId)
return false;
// may-be all our previous enemey target (if not recomputed here) have been destroyed ?
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
if (this.targetPlayer !== undefined)
this.target = this.getNearestTarget(gameState, this.rallyPoint);
if (!this.target)
return false;
}
this.targetPos = this.target.position();
// redefine a new rally point for this target if we have a base on the same land
// find a new one on the pseudo-nearest base (dist weighted by the size of the island)
let targetIndex = PETRA.getLandAccess(gameState, this.target);
let rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
if (targetIndex != rallyIndex)
{
let distminSame = Math.min();
let rallySame;
let distminDiff = Math.min();
let rallyDiff;
for (let base of gameState.ai.HQ.baseManagers)
{
let anchor = base.anchor;
if (!anchor || !anchor.position())
continue;
let dist = API3.SquareVectorDistance(anchor.position(), this.targetPos);
if (base.accessIndex == targetIndex)
{
if (dist >= distminSame)
continue;
distminSame = dist;
rallySame = anchor.position();
}
else
{
dist /= Math.sqrt(gameState.ai.accessibility.regionSize[base.accessIndex]);
if (dist >= distminDiff)
continue;
distminDiff = dist;
rallyDiff = anchor.position();
}
}
if (rallySame)
{
this.rallyPoint = rallySame;
this.overseas = 0;
}
else if (rallyDiff)
{
rallyIndex = gameState.ai.accessibility.getAccessValue(rallyDiff);
this.rallyPoint = rallyDiff;
let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex);
if (sea)
{
this.overseas = sea;
gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips);
}
else
{
API3.warn("Petra: " + this.type + " " + this.name + " has an inaccessible target" +
" with indices " + rallyIndex + " " + targetIndex + " from " + this.target.templateName());
return false;
}
}
}
else if (this.overseas)
this.overseas = 0;
return true;
};
/**
* sameLand true means that we look for a target for which we do not need to take a transport
*/
PETRA.AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand)
{
this.isBlocked = false;
// Temporary variables needed by isValidTarget
this.gameState = gameState;
this.sameLand = sameLand && sameLand > 1 ? sameLand : false;
let targets;
if (this.uniqueTargetId)
{
targets = new API3.EntityCollection(gameState.sharedScript);
let ent = gameState.getEntityById(this.uniqueTargetId);
if (ent)
targets.addEnt(ent);
}
else
{
if (this.type == "Raid")
targets = this.raidTargetFinder(gameState);
else if (this.type == "Rush" || this.type == "Attack")
{
targets = this.rushTargetFinder(gameState, this.targetPlayer);
if (!targets.hasEntities() && (this.hasSiegeUnits() || this.forced))
targets = this.defaultTargetFinder(gameState, this.targetPlayer);
}
else
targets = this.defaultTargetFinder(gameState, this.targetPlayer);
}
if (!targets.hasEntities())
return undefined;
// picking the nearest target
let target;
let minDist = Math.min();
for (let ent of targets.values())
{
if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") &&
(!ent.hasClass("Relic") || gameState.ai.HQ.victoryManager.targetedGaiaRelics.has(ent.id())))
continue;
// Do not bother with some pointless targets
if (!this.isValidTarget(ent))
continue;
let dist = API3.SquareVectorDistance(ent.position(), position);
// In normal attacks, disfavor fields
if (this.type != "Rush" && this.type != "Raid" && ent.hasClass("Field"))
dist += 100000;
if (dist < minDist)
{
minDist = dist;
target = ent;
}
}
if (!target)
return undefined;
// Check that we can reach this target
target = this.checkTargetObstruction(gameState, target, position);
if (!target)
return undefined;
if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") && target.hasClass("Relic"))
gameState.ai.HQ.victoryManager.targetedGaiaRelics.set(target.id(), [this.name]);
// Rushes can change their enemy target if nothing found with the preferred enemy
// Obstruction also can change the enemy target
this.targetPlayer = target.owner();
return target;
};
/**
* Default target finder aims for conquest critical targets
* We must apply the *same* selection (isValidTarget) as done in getNearestTarget
*/
PETRA.AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy)
{
let targets = new API3.EntityCollection(gameState.sharedScript);
if (gameState.getVictoryConditions().has("wonder"))
for (let ent of gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Wonder")).values())
targets.addEnt(ent);
if (gameState.getVictoryConditions().has("regicide"))
for (let ent of gameState.getEnemyUnits(playerEnemy).filter(API3.Filters.byClass("Hero")).values())
targets.addEnt(ent);
if (gameState.getVictoryConditions().has("capture_the_relic"))
for (let ent of gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).filter(relic => relic.owner() == playerEnemy).values())
targets.addEnt(ent);
targets = targets.filter(this.isValidTarget, this);
if (targets.hasEntities())
return targets;
let validTargets = gameState.getEnemyStructures(playerEnemy).filter(this.isValidTarget, this);
targets = validTargets.filter(API3.Filters.byClass("CivCentre"));
if (!targets.hasEntities())
targets = validTargets.filter(API3.Filters.byClass("ConquestCritical"));
// If there's nothing, attack anything else that's less critical
if (!targets.hasEntities())
targets = validTargets.filter(API3.Filters.byClass("Town"));
if (!targets.hasEntities())
targets = validTargets.filter(API3.Filters.byClass("Village"));
// No buildings, attack anything conquest critical, units included.
// TODO Should add naval attacks against the last remaining ships.
if (!targets.hasEntities())
targets = gameState.getEntities(playerEnemy).filter(API3.Filters.byClass("ConquestCritical")).
filter(API3.Filters.not(API3.Filters.byClass("Ship")));
return targets;
};
PETRA.AttackPlan.prototype.isValidTarget = function(ent)
{
if (!ent.position())
return false;
if (this.sameLand && PETRA.getLandAccess(this.gameState, ent) != this.sameLand)
return false;
return !ent.decaying() || ent.getDefaultArrow() || ent.isGarrisonHolder() && ent.garrisoned().length;
};
/** Rush target finder aims at isolated non-defended buildings */
PETRA.AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy)
{
let targets = new API3.EntityCollection(gameState.sharedScript);
let buildings;
if (playerEnemy !== undefined)
buildings = gameState.getEnemyStructures(playerEnemy).toEntityArray();
else
buildings = gameState.getEnemyStructures().toEntityArray();
if (!buildings.length)
return targets;
this.position = this.unitCollection.getCentrePosition();
if (!this.position)
this.position = this.rallyPoint;
let target;
let minDist = Math.min();
for (let building of buildings)
{
if (building.owner() == 0)
continue;
if (building.hasDefensiveFire())
continue;
if (!this.isValidTarget(building))
continue;
let pos = building.position();
let defended = false;
for (let defense of buildings)
{
if (!defense.hasDefensiveFire())
continue;
let dist = API3.SquareVectorDistance(pos, defense.position());
if (dist < 6400) // TODO check on defense range rather than this fixed 80*80
{
defended = true;
break;
}
}
if (defended)
continue;
let dist = API3.SquareVectorDistance(pos, this.position);
if (dist > minDist)
continue;
minDist = dist;
target = building;
}
if (target)
targets.addEnt(target);
if (!targets.hasEntities() && this.type == "Rush" && playerEnemy)
targets = this.rushTargetFinder(gameState);
return targets;
};
/** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */
PETRA.AttackPlan.prototype.raidTargetFinder = function(gameState)
{
let targets = new API3.EntityCollection(gameState.sharedScript);
for (let targetId of gameState.ai.HQ.defenseManager.targetList)
{
let target = gameState.getEntityById(targetId);
if (target && target.position())
targets.addEnt(target);
}
return targets;
};
/**
* Check that we can have a path to this target
* otherwise we may be blocked by walls and try to react accordingly
* This is done only when attacker and target are on the same land
*/
PETRA.AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position)
{
if (PETRA.getLandAccess(gameState, target) != gameState.ai.accessibility.getAccessValue(position))
return target;
let targetPos = target.position();
let startPos = { "x": position[0], "y": position[1] };
let endPos = { "x": targetPos[0], "y": targetPos[1] };
let blocker;
let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default"));
if (!path.length)
return undefined;
let pathPos = [path[0].x, path[0].y];
let dist = API3.VectorDistance(pathPos, targetPos);
let radius = target.obstructionRadius().max;
for (let struct of gameState.getEnemyStructures().values())
{
if (!struct.position() || !struct.get("Obstruction") || struct.hasClass("Field"))
continue;
// we consider that we can reach the target, but nonetheless check that we did not cross any enemy gate
- if (dist < radius + 10 && !struct.hasClass("Gates"))
+ if (dist < radius + 10 && !struct.hasClass("Gate"))
continue;
// Check that we are really blocked by this structure, i.e. advancing by 1+0.8(clearance)m
// in the target direction would bring us inside its obstruction.
let structPos = struct.position();
let x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist;
let y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist;
if (struct.get("Obstruction/Static"))
{
if (!struct.angle())
continue;
let angle = struct.angle();
let width = +struct.get("Obstruction/Static/@width");
let depth = +struct.get("Obstruction/Static/@depth");
let cosa = Math.cos(angle);
let sina = Math.sin(angle);
let u = x * cosa - y * sina;
let v = x * sina + y * cosa;
if (Math.abs(u) < width/2 && Math.abs(v) < depth/2)
{
blocker = struct;
break;
}
}
else if (struct.get("Obstruction/Obstructions"))
{
if (!struct.angle())
continue;
let angle = struct.angle();
let width = +struct.get("Obstruction/Obstructions/Door/@width");
let depth = +struct.get("Obstruction/Obstructions/Door/@depth");
let doorHalfWidth = width / 2;
width += +struct.get("Obstruction/Obstructions/Left/@width");
depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Left/@depth"));
width += +struct.get("Obstruction/Obstructions/Right/@width");
depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Right/@depth"));
let cosa = Math.cos(angle);
let sina = Math.sin(angle);
let u = x * cosa - y * sina;
let v = x * sina + y * cosa;
if (Math.abs(u) < width/2 && Math.abs(v) < depth/2)
{
blocker = struct;
break;
}
// check that the path does not cross this gate (could happen if not locked)
for (let i = 1; i < path.length; ++i)
{
let u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina;
let v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa;
let u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina;
let v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa;
if (v1 * v2 < 0)
{
let u0 = (u1*v2 - u2*v1) / (v2-v1);
if (Math.abs(u0) > doorHalfWidth)
continue;
blocker = struct;
break;
}
}
if (blocker)
break;
}
else if (struct.get("Obstruction/Unit"))
{
let r = +this.get("Obstruction/Unit/@radius");
if (x*x + y*y < r*r)
{
blocker = struct;
break;
}
}
}
if (blocker)
{
this.isBlocked = true;
return blocker;
}
return target;
};
PETRA.AttackPlan.prototype.getPathToTarget = function(gameState, fixedRallyPoint = false)
{
let startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
let endAccess = PETRA.getLandAccess(gameState, this.target);
if (startAccess != endAccess)
return false;
Engine.ProfileStart("AI Compute path");
let startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] };
let endPos = { "x": this.targetPos[0], "y": this.targetPos[1] };
let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large"));
this.path = [];
this.path.push(this.targetPos);
for (let p in path)
this.path.push([path[p].x, path[p].y]);
this.path.push(this.rallyPoint);
this.path.reverse();
// Change the rally point to something useful
if (!fixedRallyPoint)
this.setRallyPoint(gameState);
Engine.ProfileStop();
return true;
};
/** Set rally point at the border of our territory */
PETRA.AttackPlan.prototype.setRallyPoint = function(gameState)
{
for (let i = 0; i < this.path.length; ++i)
{
if (gameState.ai.HQ.territoryMap.getOwner(this.path[i]) === PlayerID)
continue;
if (i === 0)
this.rallyPoint = this.path[0];
else if (i > 1 && gameState.ai.HQ.isDangerousLocation(gameState, this.path[i-1], 20))
{
this.rallyPoint = this.path[i-2];
this.path.splice(0, i-2);
}
else
{
this.rallyPoint = this.path[i-1];
this.path.splice(0, i-1);
}
break;
}
};
/**
* Executes the attack plan, after this is executed the update function will be run every turn
* If we're here, it's because we have enough units.
*/
PETRA.AttackPlan.prototype.StartAttack = function(gameState)
{
if (this.Config.debug > 1)
API3.warn("start attack " + this.name + " with type " + this.type);
// if our target was destroyed during preparation, choose a new one
if ((this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) &&
!this.chooseTarget(gameState))
return false;
// erase our queue. This will stop any leftover unit from being trained.
this.removeQueues(gameState);
for (let ent of this.unitCollection.values())
{
ent.setMetadata(PlayerID, "subrole", "walking");
let stance = ent.isPackable() ? "standground" : "aggressive";
if (ent.getStance() != stance)
ent.setStance(stance);
}
let rallyAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
let targetAccess = PETRA.getLandAccess(gameState, this.target);
if (rallyAccess == targetAccess)
{
if (!this.path)
this.getPathToTarget(gameState, true);
if (!this.path || !this.path[0][0] || !this.path[0][1])
return false;
this.overseas = 0;
this.state = "walking";
this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15);
}
else
{
this.overseas = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, targetAccess);
if (!this.overseas)
return false;
this.state = "transporting";
// TODO require a global transport for the collection,
// and put back its state to "walking" when the transport is finished
for (let ent of this.unitCollection.values())
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, rallyAccess, targetAccess, this.targetPos);
}
return true;
};
/** Runs every turn after the attack is executed */
PETRA.AttackPlan.prototype.update = function(gameState, events)
{
if (!this.unitCollection.hasEntities())
return 0;
Engine.ProfileStart("Update Attack");
this.position = this.unitCollection.getCentrePosition();
// we are transporting our units, let's wait
// TODO instead of state "arrived", made a state "walking" with a new path
if (this.state == "transporting")
this.UpdateTransporting(gameState, events);
if (this.state == "walking" && !this.UpdateWalking(gameState, events))
{
Engine.ProfileStop();
return 0;
}
if (this.state == "arrived")
{
// let's proceed on with whatever happens now.
this.state = "";
this.startingAttack = true;
this.unitCollection.forEach(ent => {
ent.stopMoving();
ent.setMetadata(PlayerID, "subrole", "attacking");
});
if (this.type == "Rush") // try to find a better target for rush
{
let newtarget = this.getNearestTarget(gameState, this.position);
if (newtarget)
{
this.target = newtarget;
this.targetPos = this.target.position();
}
}
}
// basic state of attacking.
if (this.state == "")
{
// First update the target and/or its position if needed
if (!this.UpdateTarget(gameState))
{
Engine.ProfileStop();
return false;
}
let time = gameState.ai.elapsedTime;
let attackedByStructure = {};
for (let evt of events.Attacked)
{
if (!this.unitCollection.hasEntId(evt.target))
continue;
let attacker = gameState.getEntityById(evt.attacker);
let ourUnit = gameState.getEntityById(evt.target);
if (!ourUnit || !attacker || !attacker.position())
continue;
if (!attacker.hasClass("Unit"))
{
attackedByStructure[evt.target] = true;
continue;
}
if (PETRA.isSiegeUnit(ourUnit))
{ // if our siege units are attacked, we'll send some units to deal with enemies.
let collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5);
for (let ent of collec.values())
{
if (PETRA.isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out
continue;
let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
if (!ent.canAttackTarget(attacker, allowCapture))
continue;
ent.attack(attacker.id(), allowCapture);
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
// And if this attacker is a non-ranged siege unit and our unit also, attack it
if (PETRA.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee") && ourUnit.canAttackTarget(attacker, PETRA.allowCapture(gameState, ourUnit, attacker)))
{
ourUnit.attack(attacker.id(), PETRA.allowCapture(gameState, ourUnit, attacker));
ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
else
{
if (this.isBlocked && !ourUnit.hasClass("Ranged") && attacker.hasClass("Ranged"))
{
// do not react if our melee units are attacked by ranged one and we are blocked by walls
// TODO check that the attacker is from behind the wall
continue;
}
else if (PETRA.isSiegeUnit(attacker))
{ // if our unit is attacked by a siege unit, we'll send some melee units to help it.
let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5);
for (let ent of collec.values())
{
let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
if (!ent.canAttackTarget(attacker, allowCapture))
continue;
ent.attack(attacker.id(), allowCapture);
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
else
{
// Look first for nearby units to help us if possible
let collec = this.unitCollection.filterNearest(ourUnit.position(), 2);
for (let ent of collec.values())
{
let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
if (PETRA.isSiegeUnit(ent) || !ent.canAttackTarget(attacker, allowCapture))
continue;
let orderData = ent.unitAIOrderData();
if (orderData && orderData.length && orderData[0].target)
{
if (orderData[0].target === attacker.id())
continue;
let target = gameState.getEntityById(orderData[0].target);
if (target && !target.hasClass("Structure") && !target.hasClass("Support"))
continue;
}
ent.attack(attacker.id(), allowCapture);
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
// Then the unit under attack: abandon its target (if it was a structure or a support) and retaliate
// also if our unit is attacking a range unit and the attacker is a melee unit, retaliate
let orderData = ourUnit.unitAIOrderData();
if (orderData && orderData.length && orderData[0].target)
{
if (orderData[0].target === attacker.id())
continue;
let target = gameState.getEntityById(orderData[0].target);
if (target && !target.hasClass("Structure") && !target.hasClass("Support"))
{
if (!target.hasClass("Ranged") || !attacker.hasClass("Melee"))
continue;
}
}
let allowCapture = PETRA.allowCapture(gameState, ourUnit, attacker);
if (ourUnit.canAttackTarget(attacker, allowCapture))
{
ourUnit.attack(attacker.id(), allowCapture);
ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
}
}
let enemyUnits = gameState.getEnemyUnits(this.targetPlayer);
let enemyStructures = gameState.getEnemyStructures(this.targetPlayer);
// Count the number of times an enemy is targeted, to prevent all units to follow the same target
let unitTargets = {};
for (let ent of this.unitCollection.values())
{
if (ent.hasClass("Ship")) // TODO What to do with ships
continue;
let orderData = ent.unitAIOrderData();
if (!orderData || !orderData.length || !orderData[0].target)
continue;
let targetId = orderData[0].target;
let target = gameState.getEntityById(targetId);
if (!target || target.hasClass("Structure"))
continue;
if (!(targetId in unitTargets))
{
if (PETRA.isSiegeUnit(target) || target.hasClass("Hero"))
unitTargets[targetId] = -8;
else if (target.hasClass("Champion") || target.hasClass("Ship"))
unitTargets[targetId] = -5;
else
unitTargets[targetId] = -3;
}
++unitTargets[targetId];
}
let veto = {};
for (let target in unitTargets)
if (unitTargets[target] > 0)
veto[target] = true;
let targetClassesUnit;
let targetClassesSiege;
if (this.type == "Rush")
- targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "Tower", "Fortress"], "vetoEntities": veto };
+ targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "Tower", "Fortress"], "vetoEntities": veto };
else
{
if (this.target.hasClass("Fortress"))
- targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall"], "vetoEntities": veto };
- else if (this.target.hasClass("Palisade") || this.target.hasClass("StoneWall"))
+ targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall"], "vetoEntities": veto };
+ else if (this.target.hasClass("Palisade") || this.target.hasClass("Wall"))
targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto };
else
- targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "Fortress"], "vetoEntities": veto };
+ targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "Fortress"], "vetoEntities": veto };
}
if (this.target.hasClass("Structure"))
targetClassesSiege = { "attack": ["Structure"], "avoid": [], "vetoEntities": veto };
else
targetClassesSiege = { "attack": ["Unit", "Structure"], "avoid": [], "vetoEntities": veto };
// do not loose time destroying buildings which do not help enemy's defense and can be easily captured later
if (this.target.hasDefensiveFire())
{
targetClassesUnit.avoid = targetClassesUnit.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith");
targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith");
}
if (this.unitCollUpdateArray === undefined || !this.unitCollUpdateArray.length)
this.unitCollUpdateArray = this.unitCollection.toIdArray();
// Let's check a few units each time we update (currently 10) except when attack starts
let lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10;
for (let check = 0; check < lgth; check++)
{
let ent = gameState.getEntityById(this.unitCollUpdateArray[check]);
if (!ent || !ent.position())
continue;
// Do not reassign units which have reacted to an attack in that same turn
if (ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") == time)
continue;
let targetId;
let orderData = ent.unitAIOrderData();
if (orderData && orderData.length && orderData[0].target)
targetId = orderData[0].target;
// update the order if needed
let needsUpdate = false;
let maybeUpdate = false;
let siegeUnit = PETRA.isSiegeUnit(ent);
if (ent.isIdle())
needsUpdate = true;
else if (siegeUnit && targetId)
{
let target = gameState.getEntityById(targetId);
if (!target || gameState.isPlayerAlly(target.owner()))
needsUpdate = true;
else if (unitTargets[targetId] && unitTargets[targetId] > 0)
{
needsUpdate = true;
--unitTargets[targetId];
}
else if (!target.hasClass("Structure"))
maybeUpdate = true;
}
else if (targetId)
{
let target = gameState.getEntityById(targetId);
if (!target || gameState.isPlayerAlly(target.owner()))
needsUpdate = true;
else if (unitTargets[targetId] && unitTargets[targetId] > 0)
{
needsUpdate = true;
--unitTargets[targetId];
}
else if (target.hasClass("Ship") && !ent.hasClass("Ship"))
maybeUpdate = true;
else if (attackedByStructure[ent.id()] && target.hasClass("Field"))
maybeUpdate = true;
else if (!ent.hasClass("Cavalry") && !ent.hasClass("Ranged") &&
target.hasClass("FemaleCitizen") && target.unitAIState().split(".")[1] == "FLEEING")
maybeUpdate = true;
}
// don't update too soon if not necessary
if (!needsUpdate)
{
if (!maybeUpdate)
continue;
let deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5;
let lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime");
if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat)
continue;
}
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
let range = 60;
let attackTypes = ent.attackTypes();
if (this.isBlocked)
{
if (attackTypes && attackTypes.indexOf("Ranged") !== -1)
range = ent.attackRange("Ranged").max;
else if (attackTypes && attackTypes.indexOf("Melee") !== -1)
range = ent.attackRange("Melee").max;
else
range = 10;
}
else if (attackTypes && attackTypes.indexOf("Ranged") !== -1)
range = 30 + ent.attackRange("Ranged").max;
else if (ent.hasClass("Cavalry"))
range += 30;
range *= range;
let entAccess = PETRA.getLandAccess(gameState, ent);
// Checking for gates if we're a siege unit.
if (siegeUnit)
{
let mStruct = enemyStructures.filter(enemy => {
if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy)))
return false;
if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range)
return false;
if (enemy.foundationProgress() == 0)
return false;
if (PETRA.getLandAccess(gameState, enemy) != entAccess)
return false;
return true;
}).toEntityArray();
if (mStruct.length)
{
mStruct.sort((structa, structb) => {
let vala = structa.costSum();
- if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall"))
+ if (structa.hasClass("Gate") && ent.canAttackClass("Wall"))
vala += 10000;
else if (structa.hasDefensiveFire())
vala += 1000;
else if (structa.hasClass("ConquestCritical"))
vala += 200;
let valb = structb.costSum();
- if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall"))
+ if (structb.hasClass("Gate") && ent.canAttackClass("Wall"))
valb += 10000;
else if (structb.hasDefensiveFire())
valb += 1000;
else if (structb.hasClass("ConquestCritical"))
valb += 200;
return valb - vala;
});
- if (mStruct[0].hasClass("Gates"))
+ if (mStruct[0].hasClass("Gate"))
ent.attack(mStruct[0].id(), PETRA.allowCapture(gameState, ent, mStruct[0]));
else
{
let rand = randIntExclusive(0, mStruct.length * 0.2);
ent.attack(mStruct[rand].id(), PETRA.allowCapture(gameState, ent, mStruct[rand]));
}
}
else
{
if (!ent.hasClass("Ranged"))
{
let targetClasses = { "attack": targetClassesSiege.attack, "avoid": targetClassesSiege.avoid.concat("Ship"), "vetoEntities": veto };
ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses);
}
else
ent.attackMove(this.targetPos[0], this.targetPos[1], targetClassesSiege);
}
}
else
{
let nearby = !ent.hasClass("Cavalry") && !ent.hasClass("Ranged");
let mUnit = enemyUnits.filter(enemy => {
if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy)))
return false;
if (enemy.hasClass("Animal"))
return false;
if (nearby && enemy.hasClass("FemaleCitizen") && enemy.unitAIState().split(".")[1] == "FLEEING")
return false;
let dist = API3.SquareVectorDistance(enemy.position(), ent.position());
if (dist > range)
return false;
if (PETRA.getLandAccess(gameState, enemy) != entAccess)
return false;
// if already too much units targeting this enemy, let's continue towards our main target
if (veto[enemy.id()] && API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500)
return false;
enemy.setMetadata(PlayerID, "distance", Math.sqrt(dist));
return true;
}, this).toEntityArray();
if (mUnit.length)
{
mUnit.sort((unitA, unitB) => {
let vala = unitA.hasClass("Support") ? 50 : 0;
if (ent.countersClasses(unitA.classes()))
vala += 100;
let valb = unitB.hasClass("Support") ? 50 : 0;
if (ent.countersClasses(unitB.classes()))
valb += 100;
let distA = unitA.getMetadata(PlayerID, "distance");
let distB = unitB.getMetadata(PlayerID, "distance");
if (distA && distB)
{
vala -= distA;
valb -= distB;
}
if (veto[unitA.id()])
vala -= 20000;
if (veto[unitB.id()])
valb -= 20000;
return valb - vala;
});
let rand = randIntExclusive(0, mUnit.length * 0.1);
ent.attack(mUnit[rand].id(), PETRA.allowCapture(gameState, ent, mUnit[rand]));
}
// This may prove dangerous as we may be blocked by something we
// cannot attack. See similar behaviour at #5741.
else if (this.isBlocked && ent.canAttackTarget(this.target, false))
ent.attack(this.target.id(), false);
else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500)
{
let targetClasses = targetClassesUnit;
if (maybeUpdate && ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING") // we may be blocked by walls, attack everything
{
if (!ent.hasClass("Ranged") && !ent.hasClass("Ship"))
targetClasses = { "attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto };
else
targetClasses = { "attack": ["Unit", "Structure"], "vetoEntities": veto };
}
else if (!ent.hasClass("Ranged") && !ent.hasClass("Ship"))
targetClasses = { "attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto };
ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses);
}
else
{
let mStruct = enemyStructures.filter(enemy => {
if (this.isBlocked && enemy.id() != this.target.id())
return false;
if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy)))
return false;
if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range)
return false;
if (PETRA.getLandAccess(gameState, enemy) != entAccess)
return false;
return true;
}, this).toEntityArray();
if (mStruct.length)
{
mStruct.sort((structa, structb) => {
let vala = structa.costSum();
- if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall"))
+ if (structa.hasClass("Gate") && ent.canAttackClass("Wall"))
vala += 10000;
else if (structa.hasClass("ConquestCritical"))
vala += 100;
let valb = structb.costSum();
- if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall"))
+ if (structb.hasClass("Gate") && ent.canAttackClass("Wall"))
valb += 10000;
else if (structb.hasClass("ConquestCritical"))
valb += 100;
return valb - vala;
});
- if (mStruct[0].hasClass("Gates"))
+ if (mStruct[0].hasClass("Gate"))
ent.attack(mStruct[0].id(), false);
else
{
let rand = randIntExclusive(0, mStruct.length * 0.2);
ent.attack(mStruct[rand].id(), PETRA.allowCapture(gameState, ent, mStruct[rand]));
}
}
else if (needsUpdate) // really nothing let's try to help our nearest unit
{
let distmin = Math.min();
let attacker;
this.unitCollection.forEach(unit => {
if (!unit.position())
return;
if (unit.unitAIState().split(".")[1] != "COMBAT" || !unit.unitAIOrderData().length ||
!unit.unitAIOrderData()[0].target)
return;
let target = gameState.getEntityById(unit.unitAIOrderData()[0].target);
if (!target)
return;
let dist = API3.SquareVectorDistance(unit.position(), ent.position());
if (dist > distmin)
return;
distmin = dist;
if (!ent.canAttackTarget(target, PETRA.allowCapture(gameState, ent, target)))
return;
attacker = target;
});
if (attacker)
ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker));
}
}
}
}
this.unitCollUpdateArray.splice(0, lgth);
this.startingAttack = false;
// check if this enemy has resigned
if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0)
this.target = undefined;
}
this.lastPosition = this.position;
Engine.ProfileStop();
return this.unitCollection.length;
};
PETRA.AttackPlan.prototype.UpdateTransporting = function(gameState, events)
{
let done = true;
for (let ent of this.unitCollection.values())
{
if (this.Config.debug > 1 && ent.getMetadata(PlayerID, "transport") !== undefined)
Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 2, 0] });
else if (this.Config.debug > 1)
Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [1, 1, 1] });
if (!done)
continue;
if (ent.getMetadata(PlayerID, "transport") !== undefined)
done = false;
}
if (done)
{
this.state = "arrived";
return;
}
// if we are attacked while waiting the rest of the army, retaliate
for (let evt of events.Attacked)
{
if (!this.unitCollection.hasEntId(evt.target))
continue;
let attacker = gameState.getEntityById(evt.attacker);
if (!attacker || !gameState.getEntityById(evt.target))
continue;
for (let ent of this.unitCollection.values())
{
if (ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
if (!ent.isIdle() || !ent.canAttackTarget(attacker, allowCapture))
continue;
ent.attack(attacker.id(), allowCapture);
}
break;
}
};
PETRA.AttackPlan.prototype.UpdateWalking = function(gameState, events)
{
// we're marching towards the target
// Let's check if any of our unit has been attacked.
// In case yes, we'll determine if we're simply off against an enemy army, a lone unit/building
// or if we reached the enemy base. Different plans may react differently.
let attackedNB = 0;
let attackedUnitNB = 0;
for (let evt of events.Attacked)
{
if (!this.unitCollection.hasEntId(evt.target))
continue;
let attacker = gameState.getEntityById(evt.attacker);
if (attacker && (attacker.owner() !== 0 || this.targetPlayer === 0))
{
attackedNB++;
if (attacker.hasClass("Unit"))
attackedUnitNB++;
}
}
// Are we arrived at destination ?
if (attackedNB > 1 && (attackedUnitNB || this.hasSiegeUnits()))
{
if (gameState.ai.HQ.territoryMap.getOwner(this.position) === this.targetPlayer || attackedNB > 3)
{
this.state = "arrived";
return true;
}
}
// basically haven't moved an inch: very likely stuck)
if (API3.SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && this.path.length > 0 && gameState.ai.playedTurn % 5 === 0)
{
// check for stuck siege units
let farthest = 0;
let farthestEnt;
for (let ent of this.unitCollection.filter(API3.Filters.byClass("Siege")).values())
{
let dist = API3.SquareVectorDistance(ent.position(), this.position);
if (dist < farthest)
continue;
farthest = dist;
farthestEnt = ent;
}
if (farthestEnt)
farthestEnt.destroy();
}
if (gameState.ai.playedTurn % 5 === 0)
this.position5TurnsAgo = this.position;
if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 16 && this.path.length > 0)
{
if (!this.path[0][0] || !this.path[0][1])
API3.warn("Start: Problem with path " + uneval(this.path));
// We're stuck, presumably. Check if there are no walls just close to us.
- for (let ent of gameState.getEnemyStructures().filter(API3.Filters.byClass(["Palisade", "StoneWall"])).values())
+ for (let ent of gameState.getEnemyStructures().filter(API3.Filters.byClass(["Palisade", "Wall"])).values())
{
if (API3.SquareVectorDistance(this.position, ent.position()) > 800)
continue;
- let enemyClass = ent.hasClass("StoneWall") ? "StoneWall" : "Palisade";
+ let enemyClass = ent.hasClass("Wall") ? "Wall" : "Palisade";
// there are walls, so check if we can attack
if (this.unitCollection.filter(API3.Filters.byCanAttackClass(enemyClass)).hasEntities())
{
if (this.Config.debug > 1)
API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and is not happy.");
this.state = "arrived";
return true;
}
// abort plan
if (this.Config.debug > 1)
API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and gives up.");
return false;
}
// this.unitCollection.move(this.path[0][0], this.path[0][1]);
this.unitCollection.moveIndiv(this.path[0][0], this.path[0][1]);
}
// check if our units are close enough from the next waypoint.
if (API3.SquareVectorDistance(this.position, this.targetPos) < 10000)
{
if (this.Config.debug > 1)
API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination.");
this.state = "arrived";
return true;
}
else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0]) < 1600)
{
this.path.shift();
if (this.path.length)
this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15);
else
{
if (this.Config.debug > 1)
API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination.");
this.state = "arrived";
return true;
}
}
return true;
};
PETRA.AttackPlan.prototype.UpdateTarget = function(gameState)
{
// First update the target position in case it's a unit (and check if it has garrisoned)
if (this.target && this.target.hasClass("Unit"))
{
this.targetPos = this.target.position();
if (!this.targetPos)
{
let holder = PETRA.getHolder(gameState, this.target);
if (holder && gameState.isPlayerEnemy(holder.owner()))
{
this.target = holder;
this.targetPos = holder.position();
}
else
this.target = undefined;
}
}
// Then update the target if needed:
if (this.targetPlayer === undefined || !gameState.isPlayerEnemy(this.targetPlayer))
{
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
if (this.targetPlayer === undefined)
return false;
if (this.target && this.target.owner() !== this.targetPlayer)
this.target = undefined;
}
if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) // this enemy has resigned
this.target = undefined;
if (!this.target || !gameState.getEntityById(this.target.id()))
{
if (this.Config.debug > 1)
API3.warn("Seems like our target for plan " + this.name + " has been destroyed or captured. Switching.");
let accessIndex = this.getAttackAccess(gameState);
this.target = this.getNearestTarget(gameState, this.position, accessIndex);
if (!this.target)
{
if (this.uniqueTargetId)
return false;
// Check if we could help any current attack
let attackManager = gameState.ai.HQ.attackManager;
for (let attackType in attackManager.startedAttacks)
{
for (let attack of attackManager.startedAttacks[attackType])
{
if (attack.name == this.name)
continue;
if (!attack.target || !gameState.getEntityById(attack.target.id()) ||
!gameState.isPlayerEnemy(attack.target.owner()))
continue;
if (accessIndex != PETRA.getLandAccess(gameState, attack.target))
continue;
if (attack.target.owner() == 0 && attack.targetPlayer != 0) // looks like it has resigned
continue;
if (!gameState.isPlayerEnemy(attack.targetPlayer))
continue;
this.target = attack.target;
this.targetPlayer = attack.targetPlayer;
this.targetPos = this.target.position();
return true;
}
}
// If not, let's look for another enemy
if (!this.target)
{
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
if (this.targetPlayer !== undefined)
this.target = this.getNearestTarget(gameState, this.position, accessIndex);
if (!this.target)
{
if (this.Config.debug > 1)
API3.warn("No new target found. Remaining units " + this.unitCollection.length);
return false;
}
}
if (this.Config.debug > 1)
API3.warn("We will help one of our other attacks");
}
this.targetPos = this.target.position();
}
return true;
};
/** reset any units */
PETRA.AttackPlan.prototype.Abort = function(gameState)
{
this.unitCollection.unregister();
if (this.unitCollection.hasEntities())
{
// If the attack was started, look for a good rallyPoint to withdraw
let rallyPoint;
if (this.isStarted())
{
let access = this.getAttackAccess(gameState);
let dist = Math.min();
if (this.rallyPoint && gameState.ai.accessibility.getAccessValue(this.rallyPoint) == access)
{
rallyPoint = this.rallyPoint;
dist = API3.SquareVectorDistance(this.position, rallyPoint);
}
// Then check if we have a nearer base (in case this attack has captured one)
for (let base of gameState.ai.HQ.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (PETRA.getLandAccess(gameState, base.anchor) != access)
continue;
let newdist = API3.SquareVectorDistance(this.position, base.anchor.position());
if (newdist > dist)
continue;
dist = newdist;
rallyPoint = base.anchor.position();
}
}
for (let ent of this.unitCollection.values())
{
if (ent.getMetadata(PlayerID, "role") == "attack")
ent.stopMoving();
if (rallyPoint)
ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15);
this.removeUnit(ent);
}
}
for (let unitCat in this.unitStat)
this.unit[unitCat].unregister();
this.removeQueues(gameState);
};
PETRA.AttackPlan.prototype.removeUnit = function(ent, update)
{
if (ent.getMetadata(PlayerID, "role") == "attack")
{
if (ent.hasClass("CitizenSoldier"))
ent.setMetadata(PlayerID, "role", "worker");
else
ent.setMetadata(PlayerID, "role", undefined);
ent.setMetadata(PlayerID, "subrole", undefined);
}
ent.setMetadata(PlayerID, "plan", -1);
if (update)
this.unitCollection.updateEnt(ent);
};
PETRA.AttackPlan.prototype.checkEvents = function(gameState, events)
{
for (let evt of events.EntityRenamed)
{
if (!this.target || this.target.id() != evt.entity)
continue;
if (this.type == "Raid" && !this.isStarted())
this.target = undefined;
else
this.target = gameState.getEntityById(evt.newentity);
if (this.target)
this.targetPos = this.target.position();
}
for (let evt of events.OwnershipChanged) // capture event
if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to))
this.target = undefined;
for (let evt of events.PlayerDefeated)
{
if (this.targetPlayer !== evt.playerId)
continue;
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
this.target = undefined;
}
if (!this.overseas || this.state !== "unexecuted")
return;
// let's check if an enemy has built a structure at our access
for (let evt of events.Create)
{
let ent = gameState.getEntityById(evt.entity);
if (!ent || !ent.position() || !ent.hasClass("Structure"))
continue;
if (!gameState.isPlayerEnemy(ent.owner()))
continue;
let access = PETRA.getLandAccess(gameState, ent);
for (let base of gameState.ai.HQ.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (base.accessIndex != access)
continue;
this.overseas = 0;
this.rallyPoint = base.anchor.position();
}
}
};
PETRA.AttackPlan.prototype.waitingForTransport = function()
{
for (let ent of this.unitCollection.values())
if (ent.getMetadata(PlayerID, "transport") !== undefined)
return true;
return false;
};
PETRA.AttackPlan.prototype.hasSiegeUnits = function()
{
for (let ent of this.unitCollection.values())
if (PETRA.isSiegeUnit(ent))
return true;
return false;
};
PETRA.AttackPlan.prototype.hasForceOrder = function(data, value)
{
for (let ent of this.unitCollection.values())
{
if (data && +ent.getMetadata(PlayerID, data) !== value)
continue;
let orders = ent.unitAIOrderData();
for (let order of orders)
if (order.force)
return true;
}
return false;
};
/**
* The center position of this attack may be in an inaccessible area. So we use the access
* of the unit nearest to this center position.
*/
PETRA.AttackPlan.prototype.getAttackAccess = function(gameState)
{
for (let ent of this.unitCollection.filterNearest(this.position, 1).values())
return PETRA.getLandAccess(gameState, ent);
return 0;
};
PETRA.AttackPlan.prototype.debugAttack = function()
{
API3.warn("---------- attack " + this.name);
for (let unitCat in this.unitStat)
{
let Unit = this.unitStat[unitCat];
API3.warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" + Unit.targetSize);
}
API3.warn("------------------------------");
};
PETRA.AttackPlan.prototype.Serialize = function()
{
let properties = {
"name": this.name,
"type": this.type,
"state": this.state,
"forced": this.forced,
"rallyPoint": this.rallyPoint,
"overseas": this.overseas,
"paused": this.paused,
"maxCompletingTime": this.maxCompletingTime,
"neededShips": this.neededShips,
"unitStat": this.unitStat,
"siegeState": this.siegeState,
"position5TurnsAgo": this.position5TurnsAgo,
"lastPosition": this.lastPosition,
"position": this.position,
"isBlocked": this.isBlocked,
"targetPlayer": this.targetPlayer,
"target": this.target !== undefined ? this.target.id() : undefined,
"targetPos": this.targetPos,
"uniqueTargetId": this.uniqueTargetId,
"path": this.path
};
return { "properties": properties };
};
PETRA.AttackPlan.prototype.Deserialize = function(gameState, data)
{
for (let key in data.properties)
this[key] = data.properties[key];
if (this.target)
this.target = gameState.getEntityById(this.target);
this.failed = undefined;
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 23865)
@@ -1,1106 +1,1106 @@
/**
* Base Manager
* Handles lower level economic stuffs.
* Some tasks:
* -tasking workers: gathering/hunting/building/repairing?/scouting/plans.
* -giving feedback/estimates on GR
* -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans.
* -getting good spots for dropsites
* -managing dropsite use in the base
* -updating whatever needs updating, keeping track of stuffs (rebuilding needs…)
*/
PETRA.BaseManager = function(gameState, Config)
{
this.Config = Config;
this.ID = gameState.ai.uniqueIDs.bases++;
// anchor building: seen as the main building of the base. Needs to have territorial influence
this.anchor = undefined;
this.anchorId = undefined;
this.accessIndex = undefined;
// Maximum distance (from any dropsite) to look for resources
// 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max
this.maxDistResourceSquare = 360*360;
this.constructing = false;
// Defenders to train in this cc when its construction is finished
this.neededDefenders = this.Config.difficulty > 2 ? 3 + 2*(this.Config.difficulty - 3) : 0;
// vector for iterating, to check one use the HQ map.
this.territoryIndices = [];
this.timeNextIdleCheck = 0;
};
PETRA.BaseManager.prototype.init = function(gameState, state)
{
if (state == "unconstructed")
this.constructing = true;
else if (state != "captured")
this.neededDefenders = 0;
this.workerObject = new PETRA.Worker(this);
// entitycollections
this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));
this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID, "role", "worker"));
this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));
this.mobileDropsites = this.units.filter(API3.Filters.isDropsite());
this.units.registerUpdates();
this.workers.registerUpdates();
this.buildings.registerUpdates();
this.mobileDropsites.registerUpdates();
// array of entity IDs, with each being
this.dropsites = {};
this.dropsiteSupplies = {};
this.gatherers = {};
for (let res of Resources.GetCodes())
{
this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] };
this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 };
}
};
PETRA.BaseManager.prototype.reset = function(gameState, state)
{
if (state == "unconstructed")
this.constructing = true;
else
this.constructing = false;
if (state != "captured" || this.Config.difficulty < 3)
this.neededDefenders = 0;
else
this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3);
};
PETRA.BaseManager.prototype.assignEntity = function(gameState, ent)
{
ent.setMetadata(PlayerID, "base", this.ID);
this.units.updateEnt(ent);
this.workers.updateEnt(ent);
this.buildings.updateEnt(ent);
if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant"))
this.assignResourceToDropsite(gameState, ent);
};
PETRA.BaseManager.prototype.setAnchor = function(gameState, anchorEntity)
{
if (!anchorEntity.hasClass("CivCentre"))
API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as anchor.");
else
{
this.anchor = anchorEntity;
this.anchorId = anchorEntity.id();
this.anchor.setMetadata(PlayerID, "baseAnchor", true);
gameState.ai.HQ.resetBaseCache();
}
anchorEntity.setMetadata(PlayerID, "base", this.ID);
this.buildings.updateEnt(anchorEntity);
this.accessIndex = PETRA.getLandAccess(gameState, anchorEntity);
return true;
};
/* we lost our anchor. Let's reassign our units and buildings */
PETRA.BaseManager.prototype.anchorLost = function(gameState, ent)
{
this.anchor = undefined;
this.anchorId = undefined;
this.neededDefenders = 0;
gameState.ai.HQ.resetBaseCache();
};
/** Set a building of an anchorless base */
PETRA.BaseManager.prototype.setAnchorlessEntity = function(gameState, ent)
{
if (!this.buildings.hasEntities())
{
if (!PETRA.getBuiltEntity(gameState, ent).resourceDropsiteTypes())
API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as origin.");
this.accessIndex = PETRA.getLandAccess(gameState, ent);
}
else if (this.accessIndex != PETRA.getLandAccess(gameState, ent))
API3.warn(" Error: Petra base " + this.ID + " with access " + this.accessIndex +
" has been assigned " + ent.templateName() + " with access" + PETRA.getLandAccess(gameState, ent));
ent.setMetadata(PlayerID, "base", this.ID);
this.buildings.updateEnt(ent);
return true;
};
/**
* Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area.
* Moving resources (animals) and buildable resources (fields) are treated elsewhere.
*/
PETRA.BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite)
{
if (this.dropsites[dropsite.id()])
{
if (this.Config.debug > 0)
warn("assignResourceToDropsite: dropsite already in the list. Should never happen");
return;
}
let accessIndex = this.accessIndex;
let dropsitePos = dropsite.position();
let dropsiteId = dropsite.id();
this.dropsites[dropsiteId] = true;
if (this.ID == gameState.ai.HQ.baseManagers[0].ID)
accessIndex = PETRA.getLandAccess(gameState, dropsite);
let maxDistResourceSquare = this.maxDistResourceSquare;
for (let type of dropsite.resourceDropsiteTypes())
{
let resources = gameState.getResourceSupplies(type);
if (!resources.length)
continue;
let nearby = this.dropsiteSupplies[type].nearby;
let medium = this.dropsiteSupplies[type].medium;
let faraway = this.dropsiteSupplies[type].faraway;
resources.forEach(function(supply)
{
if (!supply.position())
return;
if (supply.hasClass("Animal")) // moving resources are treated differently
return;
if (supply.hasClass("Field")) // fields are treated separately
return;
if (supply.resourceSupplyType().generic == "treasure") // treasures are treated separately
return;
// quick accessibility check
if (PETRA.getLandAccess(gameState, supply) != accessIndex)
return;
let dist = API3.SquareVectorDistance(supply.position(), dropsitePos);
if (dist < maxDistResourceSquare)
{
if (dist < maxDistResourceSquare/16) // distmax/4
nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
else if (dist < maxDistResourceSquare/4) // distmax/2
medium.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
else
faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
}
});
nearby.sort((r1, r2) => r1.dist - r2.dist);
medium.sort((r1, r2) => r1.dist - r2.dist);
faraway.sort((r1, r2) => r1.dist - r2.dist);
/*
let debug = false;
if (debug)
{
faraway.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]});
});
medium.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]});
});
nearby.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]});
});
}
*/
}
// Allows all allies to use this dropsite except if base anchor to be sure to keep
// a minimum of resources for this base
Engine.PostCommand(PlayerID, {
"type": "set-dropsite-sharing",
"entities": [dropsiteId],
"shared": dropsiteId != this.anchorId
});
};
// completely remove the dropsite resources from our list.
PETRA.BaseManager.prototype.removeDropsite = function(gameState, ent)
{
if (!ent.id())
return;
let removeSupply = function(entId, supply){
for (let i = 0; i < supply.length; ++i)
{
// exhausted resource, remove it from this list
if (!supply[i].ent || !gameState.getEntityById(supply[i].id))
supply.splice(i--, 1);
// resource assigned to the removed dropsite, remove it
else if (supply[i].dropsite == entId)
supply.splice(i--, 1);
}
};
for (let type in this.dropsiteSupplies)
{
removeSupply(ent.id(), this.dropsiteSupplies[type].nearby);
removeSupply(ent.id(), this.dropsiteSupplies[type].medium);
removeSupply(ent.id(), this.dropsiteSupplies[type].faraway);
}
this.dropsites[ent.id()] = undefined;
};
/**
* Returns the position of the best place to build a new dropsite for the specified resource
*/
PETRA.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource)
{
let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_storehouse"));
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
// This builds a map. The procedure is fairly simple. It adds the resource maps
// (which are dynamically updated and are made so that they will facilitate DP placement)
// Then checks for a good spot in the territory. If none, and town/city phase, checks outside
// The AI will currently not build a CC if it wouldn't connect with an existing CC.
let obstructions = PETRA.createObstructionMap(gameState, this.accessIndex, template);
let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray();
let dpEnts = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Storehouse", "Dock"])).toEntityArray();
let bestIdx;
let bestVal = 0;
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let territoryMap = gameState.ai.HQ.territoryMap;
let width = territoryMap.width;
let cellSize = territoryMap.cellSize;
for (let j of this.territoryIndices)
{
let i = territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0) // no room around
continue;
// we add 3 times the needed resource and once the others (except food)
let total = 2*gameState.sharedScript.resourceMaps[resource].map[j];
for (let res in gameState.sharedScript.resourceMaps)
if (res != "food")
total += gameState.sharedScript.resourceMaps[res].map[j];
total *= 0.7; // Just a normalisation factor as the locateMap is limited to 255
if (total <= bestVal)
continue;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
for (let dp of dpEnts)
{
let dpPos = dp.position();
if (!dpPos)
continue;
let dist = API3.SquareVectorDistance(dpPos, pos);
if (dist < 3600)
{
total = 0;
break;
}
else if (dist < 6400)
total *= (Math.sqrt(dist)-60)/20;
}
if (total <= bestVal)
continue;
for (let cc of ccEnts)
{
let ccPos = cc.position();
if (!ccPos)
continue;
let dist = API3.SquareVectorDistance(ccPos, pos);
if (dist < 3600)
{
total = 0;
break;
}
else if (dist < 6400)
total *= (Math.sqrt(dist)-60)/20;
}
if (total <= bestVal)
continue;
if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = total;
bestIdx = i;
}
if (this.Config.debug > 2)
warn(" for dropsite best is " + bestVal);
if (bestVal <= 0)
return { "quality": bestVal, "pos": [0, 0] };
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return { "quality": bestVal, "pos": [x, z] };
};
PETRA.BaseManager.prototype.getResourceLevel = function(gameState, type, nearbyOnly = false)
{
let count = 0;
let check = {};
for (let supply of this.dropsiteSupplies[type].nearby)
{
if (check[supply.id]) // avoid double counting as same resource can appear several time
continue;
check[supply.id] = true;
count += supply.ent.resourceSupplyAmount();
}
if (nearbyOnly)
return count;
for (let supply of this.dropsiteSupplies[type].medium)
{
if (check[supply.id])
continue;
check[supply.id] = true;
count += 0.6*supply.ent.resourceSupplyAmount();
}
return count;
};
/** check our resource levels and react accordingly */
PETRA.BaseManager.prototype.checkResourceLevels = function(gameState, queues)
{
for (let type of Resources.GetCodes())
{
if (type == "food")
{
if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field")) // let's see if we need to add new farms.
{
let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted
let numFarms = gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).length; // including foundations
let numQueue = queues.field.countQueuedUnits();
// TODO if not yet farms, add a check on time used/lost and build farmstead if needed
if (numFarms + numQueue == 0) // starting game, rely on fruits as long as we have enough of them
{
if (count < 600)
{
queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_field", { "favoredBase": this.ID }));
gameState.ai.HQ.needFarm = true;
}
}
else if (!gameState.ai.HQ.maxFields || numFarms + numQueue < gameState.ai.HQ.maxFields)
{
let numFound = gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).length;
let goal = this.Config.Economy.provisionFields;
if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5)
goal = Math.max(goal-1, 1);
if (numFound + numQueue < goal)
queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_field", { "favoredBase": this.ID }));
}
else if (gameState.ai.HQ.needCorral && !gameState.getOwnEntitiesByClass("Corral", true).hasEntities() &&
!queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_corral"))
queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_corral", { "favoredBase": this.ID }));
continue;
}
if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() &&
!queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_corral"))
{
let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted
if (count < 900)
{
queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_corral", { "favoredBase": this.ID }));
gameState.ai.HQ.needCorral = true;
}
}
continue;
}
// Non food stuff
if (!gameState.sharedScript.resourceMaps[type] || queues.dropsites.hasQueuedUnits() ||
gameState.getOwnFoundations().filter(API3.Filters.byClass("Storehouse")).hasEntities())
{
this.gatherers[type].nextCheck = gameState.ai.playedTurn;
this.gatherers[type].used = 0;
this.gatherers[type].lost = 0;
continue;
}
if (gameState.ai.playedTurn < this.gatherers[type].nextCheck)
continue;
for (let ent of this.gatherersByType(gameState, type).values())
{
if (ent.unitAIState() == "INDIVIDUAL.GATHER.GATHERING")
++this.gatherers[type].used;
else if (ent.unitAIState() == "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
++this.gatherers[type].lost;
}
// TODO add also a test on remaining resources.
let total = this.gatherers[type].used + this.gatherers[type].lost;
if (total > 150 || total > 60 && type != "wood")
{
let ratio = this.gatherers[type].lost / total;
if (ratio > 0.15)
{
let newDP = this.findBestDropsiteLocation(gameState, type);
if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse"))
queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos));
else if (!gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() && !queues.civilCentre.hasQueuedUnits())
{
// No good dropsite, try to build a new base if no base already planned,
// and if not possible, be less strict on dropsite quality.
if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) &&
newDP.quality > Math.min(25, 50*0.15/ratio) &&
gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse"))
queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos));
}
}
this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20;
this.gatherers[type].used = 0;
this.gatherers[type].lost = 0;
}
else if (total == 0)
this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10;
}
};
/** Adds the estimated gather rates from this base to the currentRates */
PETRA.BaseManager.prototype.addGatherRates = function(gameState, currentRates)
{
for (let res in currentRates)
{
// I calculate the exact gathering rate for each unit.
// I must then lower that to account for travel time.
// Given that the faster you gather, the more travel time matters,
// I use some logarithms.
// TODO: this should take into account for unit speed and/or distance to target
this.gatherersByType(gameState, res).forEach(ent => {
if (ent.isIdle() || !ent.position())
return;
let gRate = ent.currentGatherRate();
if (gRate)
currentRates[res] += Math.log(1+gRate)/1.1;
});
if (res == "food")
{
this.workersBySubrole(gameState, "hunter").forEach(ent => {
if (ent.isIdle() || !ent.position())
return;
let gRate = ent.currentGatherRate();
if (gRate)
currentRates[res] += Math.log(1+gRate)/1.1;
});
this.workersBySubrole(gameState, "fisher").forEach(ent => {
if (ent.isIdle() || !ent.position())
return;
let gRate = ent.currentGatherRate();
if (gRate)
currentRates[res] += Math.log(1+gRate)/1.1;
});
}
}
};
PETRA.BaseManager.prototype.assignRolelessUnits = function(gameState, roleless)
{
if (!roleless)
roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))).values();
for (let ent of roleless)
{
if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier") || ent.hasClass("FishingBoat"))
ent.setMetadata(PlayerID, "role", "worker");
else if (ent.hasClass("Support") && ent.hasClass("Elephant"))
ent.setMetadata(PlayerID, "role", "worker");
}
};
/**
* If the numbers of workers on the resources is unbalanced then set some of workers to idle so
* they can be reassigned by reassignIdleWorkers.
* TODO: actually this probably should be in the HQ.
*/
PETRA.BaseManager.prototype.setWorkersIdleByPriority = function(gameState)
{
this.timeNextIdleCheck = gameState.ai.elapsedTime + 8;
// change resource only towards one which is more needed, and if changing will not change this order
let nb = 1; // no more than 1 change per turn (otherwise we should update the rates)
let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
let sumWanted = 0;
let sumCurrent = 0;
for (let need of mostNeeded)
{
sumWanted += need.wanted;
sumCurrent += need.current;
}
let scale = 1;
if (sumWanted > 0)
scale = sumCurrent / sumWanted;
for (let i = mostNeeded.length-1; i > 0; --i)
{
let lessNeed = mostNeeded[i];
for (let j = 0; j < i; ++j)
{
let moreNeed = mostNeeded[j];
let lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type];
if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20)
continue;
// Ensure that the most wanted resource is not exhausted
if (moreNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(moreNeed.type))
{
if (lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type))
continue;
// And if so, move the gatherer to the less wanted one.
nb = this.switchGatherer(gameState, moreNeed.type, lessNeed.type, nb);
if (nb == 0)
return;
}
// If we assume a mean rate of 0.5 per gatherer, this diff should be > 1
// but we require a bit more to avoid too frequent changes
if (scale*moreNeed.wanted - moreNeed.current - scale*lessNeed.wanted + lessNeed.current > 1.5 ||
lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type))
{
nb = this.switchGatherer(gameState, lessNeed.type, moreNeed.type, nb);
if (nb == 0)
return;
}
}
}
};
/**
* Switch some gatherers (limited to number) from resource "from" to resource "to"
* and return remaining number of possible switches.
* Prefer FemaleCitizen for food and CitizenSoldier for other resources.
*/
PETRA.BaseManager.prototype.switchGatherer = function(gameState, from, to, number)
{
let num = number;
let only;
let gatherers = this.gatherersByType(gameState, from);
if (from == "food" && gatherers.filter(API3.Filters.byClass("CitizenSoldier")).hasEntities())
only = "CitizenSoldier";
else if (to == "food" && gatherers.filter(API3.Filters.byClass("FemaleCitizen")).hasEntities())
only = "FemaleCitizen";
for (let ent of gatherers.values())
{
if (num == 0)
return num;
if (!ent.canGather(to))
continue;
if (only && !ent.hasClass(only))
continue;
--num;
ent.stopMoving();
ent.setMetadata(PlayerID, "gather-type", to);
gameState.ai.HQ.AddTCResGatherer(to);
}
return num;
};
PETRA.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers)
{
// Search for idle workers, and tell them to gather resources based on demand
if (!idleWorkers)
{
let filter = API3.Filters.byMetadata(PlayerID, "subrole", "idle");
idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values();
}
for (let ent of idleWorkers)
{
// Check that the worker isn't garrisoned
if (!ent.position())
continue;
// Support elephant can only be builders
if (ent.hasClass("Support") && ent.hasClass("Elephant"))
{
ent.setMetadata(PlayerID, "subrole", "idle");
continue;
}
if (ent.hasClass("Worker"))
{
// Just emergency repairing here. It is better managed in assignToFoundations
if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() &&
gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2)
ent.repair(this.anchor);
else if (ent.isGatherer())
{
let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
for (let needed of mostNeeded)
{
if (!ent.canGather(needed.type))
continue;
let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type];
if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20)
continue;
if (needed.type != "food" && gameState.ai.HQ.isResourceExhausted(needed.type))
continue;
ent.setMetadata(PlayerID, "subrole", "gatherer");
ent.setMetadata(PlayerID, "gather-type", needed.type);
gameState.ai.HQ.AddTCResGatherer(needed.type);
break;
}
}
}
else if (ent.hasClass("Cavalry"))
ent.setMetadata(PlayerID, "subrole", "hunter");
else if (ent.hasClass("FishingBoat"))
ent.setMetadata(PlayerID, "subrole", "fisher");
}
};
PETRA.BaseManager.prototype.workersBySubrole = function(gameState, subrole)
{
return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers);
};
PETRA.BaseManager.prototype.gatherersByType = function(gameState, type)
{
return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer"));
};
/**
* returns an entity collection of workers.
* They are idled immediatly and their subrole set to idle.
*/
PETRA.BaseManager.prototype.pickBuilders = function(gameState, workers, number)
{
let availableWorkers = this.workers.filter(ent => {
if (!ent.position() || !ent.isBuilder())
return false;
if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return false;
if (ent.getMetadata(PlayerID, "transport"))
return false;
return true;
}).toEntityArray();
availableWorkers.sort((a, b) => {
let vala = 0;
let valb = 0;
if (a.getMetadata(PlayerID, "subrole") == "builder")
vala = 100;
if (b.getMetadata(PlayerID, "subrole") == "builder")
valb = 100;
if (a.getMetadata(PlayerID, "subrole") == "idle")
vala = -50;
if (b.getMetadata(PlayerID, "subrole") == "idle")
valb = -50;
if (a.getMetadata(PlayerID, "plan") === undefined)
vala = -20;
if (b.getMetadata(PlayerID, "plan") === undefined)
valb = -20;
return vala - valb;
});
let needed = Math.min(number, availableWorkers.length - 3);
for (let i = 0; i < needed; ++i)
{
availableWorkers[i].stopMoving();
availableWorkers[i].setMetadata(PlayerID, "subrole", "idle");
workers.addEnt(availableWorkers[i]);
}
return;
};
/**
* If we have some foundations, and we don't have enough builder-workers,
* try reassigning some other workers who are nearby
* AI tries to use builders sensibly, not completely stopping its econ.
*/
PETRA.BaseManager.prototype.assignToFoundations = function(gameState, noRepair)
{
let foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.not(API3.Filters.byClass("Field"))));
let damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair());
// Check if nothing to build
if (!foundations.length && !damagedBuildings.length)
return;
let workers = this.workers.filter(ent => ent.isBuilder());
let builderWorkers = this.workersBySubrole(gameState, "builder");
let idleBuilderWorkers = builderWorkers.filter(API3.Filters.isIdle());
// if we're constructing and we have the foundations to our base anchor, only try building that.
if (this.constructing && foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).hasEntities())
{
foundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true));
let tID = foundations.toEntityArray()[0].id();
workers.forEach(ent => {
let target = ent.getMetadata(PlayerID, "target-foundation");
if (target && target != tID)
{
ent.stopMoving();
ent.setMetadata(PlayerID, "target-foundation", tID);
}
});
}
if (workers.length < 3)
{
let fromOtherBase = gameState.ai.HQ.bulkPickWorkers(gameState, this, 2);
if (fromOtherBase)
{
let baseID = this.ID;
fromOtherBase.forEach(worker => {
worker.setMetadata(PlayerID, "base", baseID);
worker.setMetadata(PlayerID, "subrole", "builder");
workers.updateEnt(worker);
builderWorkers.updateEnt(worker);
idleBuilderWorkers.updateEnt(worker);
});
}
}
let builderTot = builderWorkers.length - idleBuilderWorkers.length;
// Make the limit on number of builders depends on the available resources
let availableResources = gameState.ai.queueManager.getAvailableResources(gameState);
let builderRatio = 1;
for (let res of Resources.GetCodes())
{
if (availableResources[res] < 200)
{
builderRatio = 0.2;
break;
}
else if (availableResources[res] < 1000)
builderRatio = Math.min(builderRatio, availableResources[res] / 1000);
}
for (let target of foundations.values())
{
if (target.hasClass("Field"))
continue; // we do not build fields
if (gameState.ai.HQ.isNearInvadingArmy(target.position()))
- if (!target.hasClass("CivCentre") && !target.hasClass("StoneWall") &&
+ if (!target.hasClass("CivCentre") && !target.hasClass("Wall") &&
(!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder")))
continue;
// if our territory has shrinked since this foundation was positioned, do not build it
if (PETRA.isNotWorthBuilding(gameState, target))
continue;
let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
let maxTotalBuilders = Math.ceil(workers.length * builderRatio);
if (maxTotalBuilders < 2 && workers.length > 1)
maxTotalBuilders = 2;
if (target.hasClass("House") && gameState.getPopulationLimit() < gameState.getPopulation() + 5 &&
gameState.getPopulationLimit() < gameState.getPopulationMax())
maxTotalBuilders += 2;
let targetNB = 2;
if (target.hasClass("Fortress") || target.hasClass("Wonder") ||
target.getMetadata(PlayerID, "phaseUp") == true)
targetNB = 7;
else if (target.hasClass("Barracks") || target.hasClass("Range") || target.hasClass("Stable") ||
- target.hasClass("Tower") || target.hasClass("Market"))
+ target.hasClass("Tower") || target.hasClass("Market"))
targetNB = 4;
else if (target.hasClass("House") || target.hasClass("DropsiteWood"))
targetNB = 3;
if (target.getMetadata(PlayerID, "baseAnchor") == true ||
target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder"))
{
targetNB = 15;
maxTotalBuilders = Math.max(maxTotalBuilders, 15);
}
// if no base yet, everybody should build
if (gameState.ai.HQ.numActiveBases() == 0)
{
targetNB = workers.length;
maxTotalBuilders = targetNB;
}
if (assigned >= targetNB)
continue;
idleBuilderWorkers.forEach(function(ent) {
if (ent.getMetadata(PlayerID, "target-foundation") !== undefined)
return;
if (assigned >= targetNB || !ent.position() ||
API3.SquareVectorDistance(ent.position(), target.position()) > 40000)
return;
++assigned;
++builderTot;
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
if (assigned >= targetNB || builderTot >= maxTotalBuilders)
continue;
let nonBuilderWorkers = workers.filter(function(ent) {
if (ent.getMetadata(PlayerID, "subrole") == "builder")
return false;
if (!ent.position())
return false;
if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return false;
if (ent.getMetadata(PlayerID, "transport"))
return false;
return true;
}).toEntityArray();
let time = target.buildTime();
nonBuilderWorkers.sort((workerA, workerB) => {
let coeffA = API3.SquareVectorDistance(target.position(), workerA.position());
// elephant moves slowly, so when far away they are only useful if build time is long
if (workerA.hasClass("Elephant"))
coeffA *= 0.5 * (1 + Math.sqrt(coeffA)/5/time);
else if (workerA.getMetadata(PlayerID, "gather-type") == "food")
coeffA *= 3;
let coeffB = API3.SquareVectorDistance(target.position(), workerB.position());
if (workerB.hasClass("Elephant"))
coeffB *= 0.5 * (1 + Math.sqrt(coeffB)/5/time);
else if (workerB.getMetadata(PlayerID, "gather-type") == "food")
coeffB *= 3;
return coeffA - coeffB;
});
let current = 0;
let nonBuilderTot = nonBuilderWorkers.length;
while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot)
{
++assigned;
++builderTot;
let ent = nonBuilderWorkers[current++];
ent.stopMoving();
ent.setMetadata(PlayerID, "subrole", "builder");
ent.setMetadata(PlayerID, "target-foundation", target.id());
}
}
for (let target of damagedBuildings.values())
{
// Don't repair if we're still under attack, unless it's a vital (civcentre or wall) building
// that's being destroyed.
if (gameState.ai.HQ.isNearInvadingArmy(target.position()))
{
if (target.healthLevel() > 0.5 ||
- !target.hasClass("CivCentre") && !target.hasClass("StoneWall") &&
+ !target.hasClass("CivCentre") && !target.hasClass("Wall") &&
(!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder")))
continue;
}
else if (noRepair && !target.hasClass("CivCentre"))
continue;
if (target.decaying())
continue;
let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
let maxTotalBuilders = Math.ceil(workers.length * builderRatio);
let targetNB = 1;
if (target.hasClass("Fortress") || target.hasClass("Wonder"))
targetNB = 3;
if (target.getMetadata(PlayerID, "baseAnchor") == true ||
target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder"))
{
maxTotalBuilders = Math.ceil(workers.length * Math.max(0.3, builderRatio));
targetNB = 5;
if (target.healthLevel() < 0.3)
{
maxTotalBuilders = Math.ceil(workers.length * Math.max(0.6, builderRatio));
targetNB = 7;
}
}
if (assigned >= targetNB)
continue;
idleBuilderWorkers.forEach(function(ent) {
if (ent.getMetadata(PlayerID, "target-foundation") !== undefined)
return;
if (assigned >= targetNB || !ent.position() ||
API3.SquareVectorDistance(ent.position(), target.position()) > 40000)
return;
++assigned;
++builderTot;
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
if (assigned >= targetNB || builderTot >= maxTotalBuilders)
continue;
let nonBuilderWorkers = workers.filter(function(ent) {
if (ent.getMetadata(PlayerID, "subrole") == "builder")
return false;
if (!ent.position())
return false;
if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return false;
if (ent.getMetadata(PlayerID, "transport"))
return false;
return true;
});
let num = Math.min(nonBuilderWorkers.length, targetNB-assigned);
let nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num);
nearestNonBuilders.forEach(function(ent) {
++assigned;
++builderTot;
ent.stopMoving();
ent.setMetadata(PlayerID, "subrole", "builder");
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
}
};
/** Return false when the base is not active (no workers on it) */
PETRA.BaseManager.prototype.update = function(gameState, queues, events)
{
if (this.ID == gameState.ai.HQ.baseManagers[0].ID) // base for unaffected units
{
// if some active base, reassigns the workers/buildings
// otherwise look for anything useful to do, i.e. treasures to gather
if (gameState.ai.HQ.numActiveBases() > 0)
{
for (let ent of this.units.values())
{
let bestBase = PETRA.getBestBase(gameState, ent);
if (bestBase.ID != this.ID)
bestBase.assignEntity(gameState, ent);
}
for (let ent of this.buildings.values())
{
let bestBase = PETRA.getBestBase(gameState, ent);
if (!bestBase)
{
if (ent.hasClass("Dock"))
API3.warn("Petra: dock in baseManager[0]. It may be useful to do an anchorless base for " + ent.templateName());
continue;
}
if (ent.resourceDropsiteTypes())
this.removeDropsite(gameState, ent);
bestBase.assignEntity(gameState, ent);
}
}
else if (gameState.ai.HQ.canBuildUnits)
{
this.assignToFoundations(gameState);
if (gameState.ai.elapsedTime > this.timeNextIdleCheck)
this.setWorkersIdleByPriority(gameState);
this.assignRolelessUnits(gameState);
this.reassignIdleWorkers(gameState);
for (let ent of this.workers.values())
this.workerObject.update(gameState, ent);
for (let ent of this.mobileDropsites.values())
this.workerObject.moveToGatherer(gameState, ent, false);
}
return false;
}
if (!this.anchor) // This anchor has been destroyed, but the base may still be usable
{
if (!this.buildings.hasEntities())
{
// Reassign all remaining entities to its nearest base
for (let ent of this.units.values())
{
let base = PETRA.getBestBase(gameState, ent, false, this.ID);
base.assignEntity(gameState, ent);
}
return false;
}
// If we have a base with anchor on the same land, reassign everything to it
let reassignedBase;
for (let ent of this.buildings.values())
{
if (!ent.position())
continue;
let base = PETRA.getBestBase(gameState, ent);
if (base.anchor)
reassignedBase = base;
break;
}
if (reassignedBase)
{
for (let ent of this.units.values())
reassignedBase.assignEntity(gameState, ent);
for (let ent of this.buildings.values())
{
if (ent.resourceDropsiteTypes())
this.removeDropsite(gameState, ent);
reassignedBase.assignEntity(gameState, ent);
}
return false;
}
this.assignToFoundations(gameState);
if (gameState.ai.elapsedTime > this.timeNextIdleCheck)
this.setWorkersIdleByPriority(gameState);
this.assignRolelessUnits(gameState);
this.reassignIdleWorkers(gameState);
for (let ent of this.workers.values())
this.workerObject.update(gameState, ent);
for (let ent of this.mobileDropsites.values())
this.workerObject.moveToGatherer(gameState, ent, false);
return true;
}
Engine.ProfileStart("Base update - base " + this.ID);
this.checkResourceLevels(gameState, queues);
this.assignToFoundations(gameState);
if (this.constructing)
{
let owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position());
if(owner != 0 && !gameState.isPlayerAlly(owner))
{
// we're in enemy territory. If we're too close from the enemy, destroy us.
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
for (let cc of ccEnts.values())
{
if (cc.owner() != owner)
continue;
if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000)
continue;
this.anchor.destroy();
gameState.ai.HQ.resetBaseCache();
break;
}
}
}
else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()]))
--this.neededDefenders;
if (gameState.ai.elapsedTime > this.timeNextIdleCheck &&
(gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2))
this.setWorkersIdleByPriority(gameState);
this.assignRolelessUnits(gameState);
this.reassignIdleWorkers(gameState);
// check if workers can find something useful to do
for (let ent of this.workers.values())
this.workerObject.update(gameState, ent);
for (let ent of this.mobileDropsites.values())
this.workerObject.moveToGatherer(gameState, ent, false);
Engine.ProfileStop();
return true;
};
PETRA.BaseManager.prototype.Serialize = function()
{
return {
"ID": this.ID,
"anchorId": this.anchorId,
"accessIndex": this.accessIndex,
"maxDistResourceSquare": this.maxDistResourceSquare,
"constructing": this.constructing,
"gatherers": this.gatherers,
"neededDefenders": this.neededDefenders,
"territoryIndices": this.territoryIndices,
"timeNextIdleCheck": this.timeNextIdleCheck
};
};
PETRA.BaseManager.prototype.Deserialize = function(gameState, data)
{
for (let key in data)
this[key] = data[key];
this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined;
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 23865)
@@ -1,2897 +1,2897 @@
/**
* Headquarters
* Deal with high level logic for the AI. Most of the interesting stuff gets done here.
* Some tasks:
* -defining RESS needs
* -BO decisions.
* > training workers
* > building stuff (though we'll send that to bases)
* -picking strategy (specific manager?)
* -diplomacy -> diplomacyManager
* -planning attacks -> attackManager
* -picking new CC locations.
*/
PETRA.HQ = function(Config)
{
this.Config = Config;
this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i
// Cache various quantities.
this.turnCache = {};
this.lastFailedGather = {};
this.firstBaseConfig = false;
this.currentBase = 0; // Only one base (from baseManager) is run every turn.
// Workers configuration.
this.targetNumWorkers = this.Config.Economy.targetNumWorkers;
this.supportRatio = this.Config.Economy.supportRatio;
this.fortStartTime = 180; // Sentry towers, will start at fortStartTime + towerLapseTime.
this.towerStartTime = 0; // Stone towers, will start as soon as available (town phase).
this.towerLapseTime = this.Config.Military.towerLapseTime;
this.fortressStartTime = 0; // Fortresses, will start as soon as available (city phase).
this.fortressLapseTime = this.Config.Military.fortressLapseTime;
this.extraTowers = Math.round(Math.min(this.Config.difficulty, 3) * this.Config.personality.defensive);
this.extraFortresses = Math.round(Math.max(Math.min(this.Config.difficulty - 1, 2), 0) * this.Config.personality.defensive);
this.baseManagers = [];
this.attackManager = new PETRA.AttackManager(this.Config);
this.buildManager = new PETRA.BuildManager();
this.defenseManager = new PETRA.DefenseManager(this.Config);
this.tradeManager = new PETRA.TradeManager(this.Config);
this.navalManager = new PETRA.NavalManager(this.Config);
this.researchManager = new PETRA.ResearchManager(this.Config);
this.diplomacyManager = new PETRA.DiplomacyManager(this.Config);
this.garrisonManager = new PETRA.GarrisonManager(this.Config);
this.victoryManager = new PETRA.VictoryManager(this.Config);
this.capturableTargets = new Map();
this.capturableTargetsTime = 0;
};
/** More initialisation for stuff that needs the gameState */
PETRA.HQ.prototype.init = function(gameState, queues)
{
this.territoryMap = PETRA.createTerritoryMap(gameState);
// initialize base map. Each pixel is a base ID, or 0 if not or not accessible
this.basesMap = new API3.Map(gameState.sharedScript, "territory");
// create borderMap: flag cells on the border of the map
// then this map will be completed with our frontier in updateTerritories
this.borderMap = PETRA.createBorderMap(gameState);
// list of allowed regions
this.landRegions = {};
// try to determine if we have a water map
this.navalMap = false;
this.navalRegions = {};
this.treasures = gameState.getEntities().filter(ent => {
let type = ent.resourceSupplyType();
return type && type.generic == "treasure";
});
this.treasures.registerUpdates();
this.currentPhase = gameState.currentPhase();
this.decayingStructures = new Set();
};
/**
* initialization needed after deserialization (only called when deserialization)
*/
PETRA.HQ.prototype.postinit = function(gameState)
{
// Rebuild the base maps from the territory indices of each base
this.basesMap = new API3.Map(gameState.sharedScript, "territory");
for (let base of this.baseManagers)
for (let j of base.territoryIndices)
this.basesMap.map[j] = base.ID;
for (let ent of gameState.getOwnEntities().values())
{
if (!ent.resourceDropsiteTypes() || !ent.hasClass("Structure"))
continue;
// Entities which have been built or have changed ownership after the last AI turn have no base.
// they will be dealt with in the next checkEvents
let baseID = ent.getMetadata(PlayerID, "base");
if (baseID === undefined)
continue;
let base = this.getBaseByID(baseID);
base.assignResourceToDropsite(gameState, ent);
}
this.updateTerritories(gameState);
};
/**
* Create a new base in the baseManager:
* If an existing one without anchor already exist, use it.
* Otherwise create a new one.
* TODO when buildings, criteria should depend on distance
* allowedType: undefined => new base with an anchor
* "unconstructed" => new base with a foundation anchor
* "captured" => captured base with an anchor
* "anchorless" => anchorless base, currently with dock
*/
PETRA.HQ.prototype.createBase = function(gameState, ent, type)
{
let access = PETRA.getLandAccess(gameState, ent);
let newbase;
for (let base of this.baseManagers)
{
if (base.accessIndex != access)
continue;
if (type != "anchorless" && base.anchor)
continue;
if (type != "anchorless")
{
// TODO we keep the fisrt one, we should rather use the nearest if buildings
// and possibly also cut on distance
newbase = base;
break;
}
else
{
// TODO here also test on distance instead of first
if (newbase && !base.anchor)
continue;
newbase = base;
if (newbase.anchor)
break;
}
}
if (this.Config.debug > 0)
{
API3.warn(" ----------------------------------------------------------");
API3.warn(" HQ createBase entrance avec access " + access + " and type " + type);
API3.warn(" with access " + uneval(this.baseManagers.map(base => base.accessIndex)) +
" and base nbr " + uneval(this.baseManagers.map(base => base.ID)) +
" and anchor " + uneval(this.baseManagers.map(base => !!base.anchor)));
}
if (!newbase)
{
newbase = new PETRA.BaseManager(gameState, this.Config);
newbase.init(gameState, type);
this.baseManagers.push(newbase);
}
else
newbase.reset(type);
if (type != "anchorless")
newbase.setAnchor(gameState, ent);
else
newbase.setAnchorlessEntity(gameState, ent);
return newbase;
};
/**
* returns the sea index linking regions 1 and region 2 (supposed to be different land region)
* otherwise return undefined
* for the moment, only the case land-sea-land is supported
*/
PETRA.HQ.prototype.getSeaBetweenIndices = function(gameState, index1, index2)
{
let path = gameState.ai.accessibility.getTrajectToIndex(index1, index2);
if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] == "water")
return path[1];
if (this.Config.debug > 1)
{
API3.warn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path));
API3.warn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1]));
API3.warn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2]));
}
return undefined;
};
/** TODO check if the new anchorless bases should be added to addBase */
PETRA.HQ.prototype.checkEvents = function(gameState, events)
{
let addBase = false;
this.buildManager.checkEvents(gameState, events);
if (events.TerritoriesChanged.length || events.DiplomacyChanged.length)
this.updateTerritories(gameState);
for (let evt of events.DiplomacyChanged)
{
if (evt.player != PlayerID && evt.otherPlayer != PlayerID)
continue;
// Reset the entities collections which depend on diplomacy
gameState.resetOnDiplomacyChanged();
break;
}
for (let evt of events.Destroy)
{
// Let's check we haven't lost an important building here.
if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] &&
evt.metadata[PlayerID].base)
{
let ent = evt.entityObj;
if (ent.owner() != PlayerID)
continue;
// A new base foundation was created and destroyed on the same (AI) turn
if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2)
continue;
let base = this.getBaseByID(evt.metadata[PlayerID].base);
if (ent.resourceDropsiteTypes() && ent.hasClass("Structure"))
base.removeDropsite(gameState, ent);
if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true)
base.anchorLost(gameState, ent);
}
}
for (let evt of events.EntityRenamed)
{
let ent = gameState.getEntityById(evt.newentity);
if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined)
continue;
let base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
if (!base.anchorId || base.anchorId != evt.entity)
continue;
base.anchorId = evt.newentity;
base.anchor = ent;
}
for (let evt of events.Create)
{
// Let's check if we have a valuable foundation needing builders quickly
// (normal foundations are taken care in baseManager.assignToFoundations)
let ent = gameState.getEntityById(evt.entity);
if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined)
continue;
if (ent.getMetadata(PlayerID, "base") == -1) // Standard base around a cc
{
// Okay so let's try to create a new base around this.
let newbase = this.createBase(gameState, ent, "unconstructed");
// Let's get a few units from other bases there to build this.
let builders = this.bulkPickWorkers(gameState, newbase, 10);
if (builders !== false)
{
builders.forEach(worker => {
worker.setMetadata(PlayerID, "base", newbase.ID);
worker.setMetadata(PlayerID, "subrole", "builder");
worker.setMetadata(PlayerID, "target-foundation", ent.id());
});
}
}
else if (ent.getMetadata(PlayerID, "base") == -2) // anchorless base around a dock
{
let newbase = this.createBase(gameState, ent, "anchorless");
// Let's get a few units from other bases there to build this.
let builders = this.bulkPickWorkers(gameState, newbase, 4);
if (builders != false)
{
builders.forEach(worker => {
worker.setMetadata(PlayerID, "base", newbase.ID);
worker.setMetadata(PlayerID, "subrole", "builder");
worker.setMetadata(PlayerID, "target-foundation", ent.id());
});
}
}
}
for (let evt of events.ConstructionFinished)
{
if (evt.newentity == evt.entity) // repaired building
continue;
let ent = gameState.getEntityById(evt.newentity);
if (!ent || ent.owner() != PlayerID)
continue;
- if (ent.hasClass("BarterMarket") && this.maxFields)
+ if (ent.hasClass("Market") && this.maxFields)
this.maxFields = false;
if (ent.getMetadata(PlayerID, "base") === undefined)
continue;
let base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
base.buildings.updateEnt(ent);
if (ent.resourceDropsiteTypes())
base.assignResourceToDropsite(gameState, ent);
if (ent.getMetadata(PlayerID, "baseAnchor") === true)
{
if (base.constructing)
base.constructing = false;
addBase = true;
}
}
for (let evt of events.OwnershipChanged) // capture events
{
if (evt.from == PlayerID)
{
let ent = gameState.getEntityById(evt.entity);
if (!ent || ent.getMetadata(PlayerID, "base") === undefined)
continue;
let base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
if (ent.resourceDropsiteTypes() && ent.hasClass("Structure"))
base.removeDropsite(gameState, ent);
if (ent.getMetadata(PlayerID, "baseAnchor") === true)
base.anchorLost(gameState, ent);
}
if (evt.to != PlayerID)
continue;
let ent = gameState.getEntityById(evt.entity);
if (!ent)
continue;
if (ent.hasClass("Unit"))
{
PETRA.getBestBase(gameState, ent).assignEntity(gameState, ent);
ent.setMetadata(PlayerID, "role", undefined);
ent.setMetadata(PlayerID, "subrole", undefined);
ent.setMetadata(PlayerID, "plan", undefined);
ent.setMetadata(PlayerID, "PartOfArmy", undefined);
if (ent.hasClass("Trader"))
{
ent.setMetadata(PlayerID, "role", "trader");
ent.setMetadata(PlayerID, "route", undefined);
}
if (ent.hasClass("Worker"))
{
ent.setMetadata(PlayerID, "role", "worker");
ent.setMetadata(PlayerID, "subrole", "idle");
}
if (ent.hasClass("Ship"))
PETRA.setSeaAccess(gameState, ent);
if (!ent.hasClass("Support") && !ent.hasClass("Ship") && ent.attackTypes() !== undefined)
ent.setMetadata(PlayerID, "plan", -1);
continue;
}
if (ent.hasClass("CivCentre")) // build a new base around it
{
let newbase;
if (ent.foundationProgress() !== undefined)
newbase = this.createBase(gameState, ent, "unconstructed");
else
{
newbase = this.createBase(gameState, ent, "captured");
addBase = true;
}
newbase.assignEntity(gameState, ent);
}
else
{
let base;
// If dropsite on new island, create a base around it
if (!ent.decaying() && ent.resourceDropsiteTypes())
base = this.createBase(gameState, ent, "anchorless");
else
base = PETRA.getBestBase(gameState, ent) || this.baseManagers[0];
base.assignEntity(gameState, ent);
if (ent.decaying())
{
if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true))
continue;
if (!this.decayingStructures.has(evt.entity))
this.decayingStructures.add(evt.entity);
}
}
}
// deal with the different rally points of training units: the rally point is set when the training starts
// for the time being, only autogarrison is used
for (let evt of events.TrainingStarted)
{
let ent = gameState.getEntityById(evt.entity);
if (!ent || !ent.isOwn(PlayerID))
continue;
if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length)
continue;
let metadata = ent._entity.trainingQueue[0].metadata;
if (metadata && metadata.garrisonType)
ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison
else
ent.unsetRallyPoint();
}
for (let evt of events.TrainingFinished)
{
for (let entId of evt.entities)
{
let ent = gameState.getEntityById(entId);
if (!ent || !ent.isOwn(PlayerID))
continue;
if (!ent.position())
{
// we are autogarrisoned, check that the holder is registered in the garrisonManager
let holderId = ent.unitAIOrderData()[0].target;
let holder = gameState.getEntityById(holderId);
if (holder)
this.garrisonManager.registerHolder(gameState, holder);
}
else if (ent.getMetadata(PlayerID, "garrisonType"))
{
// we were supposed to be autogarrisoned, but this has failed (may-be full)
ent.setMetadata(PlayerID, "garrisonType", undefined);
}
// Check if this unit is no more needed in its attack plan
// (happen when the training ends after the attack is started or aborted)
let plan = ent.getMetadata(PlayerID, "plan");
if (plan !== undefined && plan >= 0)
{
let attack = this.attackManager.getPlan(plan);
if (!attack || attack.state != "unexecuted")
ent.setMetadata(PlayerID, "plan", -1);
}
// Assign it immediately to something useful to do
if (ent.getMetadata(PlayerID, "role") == "worker")
{
let base;
if (ent.getMetadata(PlayerID, "base") === undefined)
{
base = PETRA.getBestBase(gameState, ent);
base.assignEntity(gameState, ent);
}
else
base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
base.reassignIdleWorkers(gameState, [ent]);
base.workerObject.update(gameState, ent);
}
else if (ent.resourceSupplyType() && ent.position())
{
let type = ent.resourceSupplyType();
if (!type.generic)
continue;
let dropsites = gameState.getOwnDropsites(type.generic);
let pos = ent.position();
let access = PETRA.getLandAccess(gameState, ent);
let distmin = Math.min();
let goal;
for (let dropsite of dropsites.values())
{
if (!dropsite.position() || PETRA.getLandAccess(gameState, dropsite) != access)
continue;
let dist = API3.SquareVectorDistance(pos, dropsite.position());
if (dist > distmin)
continue;
distmin = dist;
goal = dropsite.position();
}
if (goal)
ent.moveToRange(goal[0], goal[1]);
}
}
}
for (let evt of events.TerritoryDecayChanged)
{
let ent = gameState.getEntityById(evt.entity);
if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() !== undefined)
continue;
if (evt.to)
{
if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity))
continue;
if (!this.decayingStructures.has(evt.entity))
this.decayingStructures.add(evt.entity);
}
else if (ent.isGarrisonHolder())
this.garrisonManager.removeDecayingStructure(evt.entity);
}
if (addBase)
{
if (!this.firstBaseConfig)
{
// This is our first base, let us configure our starting resources
this.configFirstBase(gameState);
}
else
{
// Let us hope this new base will fix our possible resource shortage
this.saveResources = undefined;
this.saveSpace = undefined;
this.maxFields = false;
}
}
// Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties)
if (this.Config.difficulty < 2)
return;
for (let entId of this.decayingStructures)
{
let ent = gameState.getEntityById(entId);
if (ent && ent.decaying() && ent.isOwn(PlayerID))
{
let capture = ent.capturePoints();
if (!capture)
continue;
let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b);
if (captureRatio < 0.50)
continue;
let decayToGaia = true;
for (let i = 1; i < capture.length; ++i)
{
if (gameState.isPlayerAlly(i) || !capture[i])
continue;
decayToGaia = false;
break;
}
if (decayToGaia)
continue;
let ratioMax = 0.7 + randFloat(0, 0.1);
for (let evt of events.Attacked)
{
if (ent.id() != evt.target)
continue;
ratioMax = 0.85 + randFloat(0, 0.1);
break;
}
if (captureRatio > ratioMax)
continue;
ent.destroy();
}
this.decayingStructures.delete(entId);
}
};
/** Ensure that all requirements are met when phasing up*/
PETRA.HQ.prototype.checkPhaseRequirements = function(gameState, queues)
{
if (gameState.getNumberOfPhases() == this.currentPhase)
return;
let requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1);
let plan;
let queue;
for (let entityReq of requirements)
{
// Village requirements are met elsewhere by constructing more houses
if (entityReq.class == "Village" || entityReq.class == "NotField")
continue;
if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count)
continue;
switch (entityReq.class)
{
case "Town":
if (!queues.economicBuilding.hasQueuedUnits() &&
!queues.militaryBuilding.hasQueuedUnits() &&
!queues.defenseBuilding.hasQueuedUnits())
{
- if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() &&
+ if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() &&
this.canBuild(gameState, "structures/{civ}_market"))
{
plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}_market", { "phaseUp": true });
queue = "economicBuilding";
break;
}
if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() &&
this.canBuild(gameState, "structures/{civ}_temple"))
{
plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}_temple", { "phaseUp": true });
queue = "economicBuilding";
break;
}
if (!gameState.getOwnEntitiesByClass("Blacksmith", true).hasEntities() &&
this.canBuild(gameState, "structures/{civ}_blacksmith"))
{
plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}_blacksmith", { "phaseUp": true });
queue = "militaryBuilding";
break;
}
if (this.canBuild(gameState, "structures/{civ}_defense_tower"))
{
plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}_defense_tower", { "phaseUp": true });
queue = "defenseBuilding";
break;
}
}
break;
default:
// All classes not dealt with inside vanilla game.
// We put them for the time being on the economic queue, except if wonder
queue = entityReq.class == "Wonder" ? "wonder" : "economicBuilding";
if (!queues[queue].hasQueuedUnits())
{
let structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]);
if (structure && this.canBuild(gameState, structure))
plan = new PETRA.ConstructionPlan(gameState, structure, { "phaseUp": true });
}
}
if (plan)
{
if (queue == "wonder")
{
gameState.ai.queueManager.changePriority("majorTech", 400, { "phaseUp": true });
plan.queueToReset = "majorTech";
}
else
{
gameState.ai.queueManager.changePriority(queue, 1000, { "phaseUp": true });
plan.queueToReset = queue;
}
queues[queue].addPlan(plan);
return;
}
}
};
/** Called by any "phase" research plan once it's started */
PETRA.HQ.prototype.OnPhaseUp = function(gameState, phase)
{
};
/** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */
PETRA.HQ.prototype.trainMoreWorkers = function(gameState, queues)
{
// default template
let requirementsDef = [ ["costsResource", 1, "food"] ];
let classesDef = ["Support", "Worker"];
let templateDef = this.findBestTrainableUnit(gameState, classesDef, requirementsDef);
// counting the workers that aren't part of a plan
let numberOfWorkers = 0; // all workers
let numberOfSupports = 0; // only support workers (i.e. non fighting)
gameState.getOwnUnits().forEach(ent => {
if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") === undefined)
{
++numberOfWorkers;
if (ent.hasClass("Support"))
++numberOfSupports;
}
});
let numberInTraining = 0;
gameState.getOwnTrainingFacilities().forEach(function(ent) {
for (let item of ent.trainingQueue())
{
numberInTraining += item.count;
if (item.metadata && item.metadata.role && item.metadata.role == "worker" &&
item.metadata.plan === undefined)
{
numberOfWorkers += item.count;
if (item.metadata.support)
numberOfSupports += item.count;
}
}
});
// Anticipate the optimal batch size when this queue will start
// and adapt the batch size of the first and second queued workers to the present population
// to ease a possible recovery if our population was drastically reduced by an attack
// (need to go up to second queued as it is accounted in queueManager)
let size = numberOfWorkers < 12 ? 1 : Math.min(5, Math.ceil(numberOfWorkers / 10));
if (queues.villager.plans[0])
{
queues.villager.plans[0].number = Math.min(queues.villager.plans[0].number, size);
if (queues.villager.plans[1])
queues.villager.plans[1].number = Math.min(queues.villager.plans[1].number, size);
}
if (queues.citizenSoldier.plans[0])
{
queues.citizenSoldier.plans[0].number = Math.min(queues.citizenSoldier.plans[0].number, size);
if (queues.citizenSoldier.plans[1])
queues.citizenSoldier.plans[1].number = Math.min(queues.citizenSoldier.plans[1].number, size);
}
let numberOfQueuedSupports = queues.villager.countQueuedUnits();
let numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits();
let numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers;
let numberTotal = numberOfWorkers + numberQueued;
if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10)
return;
if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 &&
this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))))
return;
if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15)
return;
// Choose whether we want soldiers or support units: when full pop, we aim at targetNumWorkers workers
// with supportRatio fraction of support units. But we want to have more support (less cost) at startup.
// So we take: supportRatio*targetNumWorkers*(1 - exp(-alfa*currentWorkers/supportRatio/targetNumWorkers))
// This gives back supportRatio*targetNumWorkers when currentWorkers >> supportRatio*targetNumWorkers
// and gives a ratio alfa at startup.
let supportRatio = this.supportRatio;
let alpha = 0.85;
if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field")))
supportRatio = Math.min(this.supportRatio, 0.1);
if (this.attackManager.rushNumber < this.attackManager.maxRushes || this.attackManager.upcomingAttacks.Rush.length)
alpha = 0.7;
if (gameState.isCeasefireActive())
alpha += (1 - alpha) * Math.min(Math.max(gameState.ceasefireTimeRemaining - 120, 0), 180) / 180;
let supportMax = supportRatio * this.targetNumWorkers;
let supportNum = supportMax * (1 - Math.exp(-alpha*numberTotal/supportMax));
let template;
if (!templateDef || numberOfSupports + numberOfQueuedSupports > supportNum)
{
let requirements;
if (numberTotal < 45)
requirements = [ ["speed", 0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"] ];
else
requirements = [ ["strength", 1] ];
let classes = ["CitizenSoldier", "Infantry"];
// We want at least 33% ranged and 33% melee
classes.push(pickRandom(["Ranged", "Melee", "Infantry"]));
template = this.findBestTrainableUnit(gameState, classes, requirements);
}
// If the template variable is empty, the default unit (Support unit) will be used
// base "0" means automatic choice of base
if (!template && templateDef)
queues.villager.addPlan(new PETRA.TrainingPlan(gameState, templateDef, { "role": "worker", "base": 0, "support": true }, size, size));
else if (template)
queues.citizenSoldier.addPlan(new PETRA.TrainingPlan(gameState, template, { "role": "worker", "base": 0 }, size, size));
};
/** picks the best template based on parameters and classes */
PETRA.HQ.prototype.findBestTrainableUnit = function(gameState, classes, requirements)
{
let units;
if (classes.indexOf("Hero") != -1)
units = gameState.findTrainableUnits(classes, []);
else if (classes.indexOf("Siege") != -1) // We do not want siege tower as AI does not know how to use it
units = gameState.findTrainableUnits(classes, ["SiegeTower"]);
else // We do not want hero when not explicitely specified
units = gameState.findTrainableUnits(classes, ["Hero"]);
if (!units.length)
return undefined;
let parameters = requirements.slice();
let remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory
let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources
for (let type in remainingResources)
{
if (availableResources[type] > 800)
continue;
if (remainingResources[type] > 800)
continue;
let costsResource = remainingResources[type] > 400 ? 0.6 : 0.2;
let toAdd = true;
for (let param of parameters)
{
if (param[0] != "costsResource" || param[2] != type)
continue;
param[1] = Math.min(param[1], costsResource);
toAdd = false;
break;
}
if (toAdd)
parameters.push(["costsResource", costsResource, type]);
}
units.sort((a, b) => {
let aCost = 1 + a[1].costSum();
let bCost = 1 + b[1].costSum();
let aValue = 0.1;
let bValue = 0.1;
for (let param of parameters)
{
if (param[0] == "strength")
{
aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1];
bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1];
}
else if (param[0] == "siegeStrength")
{
aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1];
bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1];
}
else if (param[0] == "speed")
{
aValue += a[1].walkSpeed() * param[1];
bValue += b[1].walkSpeed() * param[1];
}
else if (param[0] == "costsResource")
{
// requires a third parameter which is the resource
if (a[1].cost()[param[2]])
aValue *= param[1];
if (b[1].cost()[param[2]])
bValue *= param[1];
}
else if (param[0] == "canGather")
{
// checking against wood, could be anything else really.
if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"])
aValue *= param[1];
if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"])
bValue *= param[1];
}
else
API3.warn(" trainMoreUnits avec non prevu " + uneval(param));
}
return -aValue/aCost + bValue/bCost;
});
return units[0][0];
};
/**
* returns an entity collection of workers through BaseManager.pickBuilders
* TODO: when same accessIndex, sort by distance
*/
PETRA.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number)
{
let accessIndex = baseRef.accessIndex;
if (!accessIndex)
return false;
// sorting bases by whether they are on the same accessindex or not.
let baseBest = this.baseManagers.slice().sort((a, b) => {
if (a.accessIndex == accessIndex && b.accessIndex != accessIndex)
return -1;
else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex)
return 1;
return 0;
});
let needed = number;
let workers = new API3.EntityCollection(gameState.sharedScript);
for (let base of baseBest)
{
if (base.ID == baseRef.ID)
continue;
base.pickBuilders(gameState, workers, needed);
if (workers.length >= number)
break;
needed = number - workers.length;
}
if (!workers.length)
return false;
return workers;
};
PETRA.HQ.prototype.getTotalResourceLevel = function(gameState)
{
let total = {};
for (let res of Resources.GetCodes())
total[res] = 0;
for (let base of this.baseManagers)
for (let res in total)
total[res] += base.getResourceLevel(gameState, res);
return total;
};
/**
* Returns the current gather rate
* This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that.
*/
PETRA.HQ.prototype.GetCurrentGatherRates = function(gameState)
{
if (!this.turnCache.currentRates)
{
let currentRates = {};
for (let res of Resources.GetCodes())
currentRates[res] = 0.5 * this.GetTCResGatherer(res);
for (let base of this.baseManagers)
base.addGatherRates(gameState, currentRates);
for (let res of Resources.GetCodes())
currentRates[res] = Math.max(currentRates[res], 0);
this.turnCache.currentRates = currentRates;
}
return this.turnCache.currentRates;
};
/**
* Returns the wanted gather rate.
*/
PETRA.HQ.prototype.GetWantedGatherRates = function(gameState)
{
if (!this.turnCache.wantedRates)
this.turnCache.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState);
return this.turnCache.wantedRates;
};
/**
* Pick the resource which most needs another worker
* How this works:
* We get the rates we would want to have to be able to deal with our plans
* We get our current rates
* We compare; we pick the one where the discrepancy is highest.
* Need to balance long-term needs and possible short-term needs.
*/
PETRA.HQ.prototype.pickMostNeededResources = function(gameState, allowedResources = [])
{
let wantedRates = this.GetWantedGatherRates(gameState);
let currentRates = this.GetCurrentGatherRates(gameState);
if (!allowedResources.length)
allowedResources = Resources.GetCodes();
let needed = [];
for (let res of allowedResources)
needed.push({ "type": res, "wanted": wantedRates[res], "current": currentRates[res] });
needed.sort((a, b) => {
if (a.current < a.wanted && b.current < b.wanted)
{
if (a.current && b.current)
return b.wanted / b.current - a.wanted / a.current;
if (a.current)
return 1;
if (b.current)
return -1;
return b.wanted - a.wanted;
}
if (a.current < a.wanted || a.wanted && !b.wanted)
return -1;
if (b.current < b.wanted || b.wanted && !a.wanted)
return 1;
return a.current - a.wanted - b.current + b.wanted;
});
return needed;
};
/**
* Returns the best position to build a new Civil Center
* Whose primary function would be to reach new resources of type "resource".
*/
PETRA.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic)
{
// This builds a map. The procedure is fairly simple. It adds the resource maps
// (which are dynamically updated and are made so that they will facilitate DP placement)
// Then look for a good spot.
Engine.ProfileStart("findEconomicCCLocation");
// obstruction map
let obstructions = PETRA.createObstructionMap(gameState, 0, template);
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
let dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClassesOr(["CivCentre", "Elephant"])));
let ccList = [];
for (let cc of ccEnts.values())
ccList.push({ "ent": cc, "pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner()) });
let dpList = [];
for (let dp of dpEnts.values())
dpList.push({ "ent": dp, "pos": dp.position(), "territory": this.territoryMap.getOwner(dp.position()) });
let bestIdx;
let bestVal;
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let scale = 250 * 250;
let proxyAccess;
let nbShips = this.navalManager.transportShips.length;
if (proximity) // this is our first base
{
// if our first base, ensure room around
radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize);
// scale is the typical scale at which we want to find a location for our first base
// look for bigger scale if we start from a ship (access < 2) or from a small island
let cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize;
proxyAccess = gameState.ai.accessibility.getAccessValue(proximity);
if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000)
scale = 400 * 400;
}
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
// DistanceSquare cuts to other ccs (bigger or no cuts on inaccessible ccs to allow colonizing other islands).
let reduce = (template.hasClass("Colony") ? 30 : 0) + 30 * this.Config.personality.defensive;
let nearbyRejected = Math.square(120); // Reject if too near from any cc
let nearbyAllyRejected = Math.square(200); // Reject if too near from an allied cc
let nearbyAllyDisfavored = Math.square(250); // Disfavor if quite near an allied cc
let maxAccessRejected = Math.square(410); // Reject if too far from an accessible ally cc
let maxAccessDisfavored = Math.square(360 - reduce); // Disfavor if quite far from an accessible ally cc
let maxNoAccessDisfavored = Math.square(500); // Disfavor if quite far from an inaccessible ally cc
let cut = 60;
if (fromStrategic || proximity) // be less restrictive
cut = 30;
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (this.territoryMap.getOwnerIndex(j) != 0)
continue;
// With enough room around to build the cc
let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
// We require that it is accessible
let index = gameState.ai.accessibility.landPassMap[i];
if (!this.landRegions[index])
continue;
if (proxyAccess && nbShips == 0 && proxyAccess != index)
continue;
let norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps
// Checking distance to other cc
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
// We will be more tolerant for cc around our oversea docks
let oversea = false;
if (proximity) // This is our first cc, let's do it near our units
norm /= 1 + API3.SquareVectorDistance(proximity, pos) / scale;
else
{
let minDist = Math.min();
let accessible = false;
for (let cc of ccList)
{
let dist = API3.SquareVectorDistance(cc.pos, pos);
if (dist < nearbyRejected)
{
norm = 0;
break;
}
if (!cc.ally)
continue;
if (dist < nearbyAllyRejected)
{
norm = 0;
break;
}
if (dist < nearbyAllyDisfavored)
norm *= 0.5;
if (dist < minDist)
minDist = dist;
accessible = accessible || index == PETRA.getLandAccess(gameState, cc.ent);
}
if (norm == 0)
continue;
if (accessible && minDist > maxAccessRejected)
continue;
if (minDist > maxAccessDisfavored) // Disfavor if quite far from any allied cc
{
if (!accessible)
{
if (minDist > maxNoAccessDisfavored)
norm *= 0.5;
else
norm *= 0.8;
}
else
norm *= 0.5;
}
// Not near any of our dropsite, except for oversea docks
oversea = !accessible && dpList.some(dp => PETRA.getLandAccess(gameState, dp.ent) == index);
if (!oversea)
{
for (let dp of dpList)
{
let dist = API3.SquareVectorDistance(dp.pos, pos);
if (dist < 3600)
{
norm = 0;
break;
}
else if (dist < 6400)
norm *= 0.5;
}
}
if (norm == 0)
continue;
}
if (this.borderMap.map[j] & PETRA.fullBorder_Mask) // disfavor the borders of the map
norm *= 0.5;
let val = 2 * gameState.sharedScript.ccResourceMaps[resource].map[j];
for (let res in gameState.sharedScript.resourceMaps)
if (res != "food")
val += gameState.sharedScript.ccResourceMaps[res].map[j];
val *= norm;
// If oversea, be just above threshold to be accepted if nothing else
if (oversea)
val = Math.max(val, cut + 0.1);
if (bestVal !== undefined && val < bestVal)
continue;
if (this.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = val;
bestIdx = i;
}
Engine.ProfileStop();
if (bestVal === undefined)
return false;
if (this.Config.debug > 1)
API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal);
// not good enough.
if (bestVal < cut)
return false;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
// Define a minimal number of wanted ships in the seas reaching this new base
let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx];
for (let base of this.baseManagers)
{
if (!base.anchor || base.accessIndex == indexIdx)
continue;
let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx);
if (sea !== undefined)
this.navalManager.setMinimalTransportShips(gameState, sea, 1);
}
return [x, z];
};
/**
* Returns the best position to build a new Civil Center
* Whose primary function would be to assure territorial continuity with our allies
*/
PETRA.HQ.prototype.findStrategicCCLocation = function(gameState, template)
{
// This builds a map. The procedure is fairly simple.
// We minimize the Sum((dist - 300)^2) where the sum is on the three nearest allied CC
// with the constraints that all CC have dist > 200 and at least one have dist < 400
// This needs at least 2 CC. Otherwise, go back to economic CC.
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
let ccList = [];
let numAllyCC = 0;
for (let cc of ccEnts.values())
{
let ally = gameState.isPlayerAlly(cc.owner());
ccList.push({ "pos": cc.position(), "ally": ally });
if (ally)
++numAllyCC;
}
if (numAllyCC < 2)
return this.findEconomicCCLocation(gameState, template, "wood", undefined, true);
Engine.ProfileStart("findStrategicCCLocation");
// obstruction map
let obstructions = PETRA.createObstructionMap(gameState, 0, template);
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
let bestIdx;
let bestVal;
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
let currentVal, delta;
let distcc0, distcc1, distcc2;
let favoredDistance = (template.hasClass("Colony") ? 220 : 280) - 40 * this.Config.personality.defensive;
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (this.territoryMap.getOwnerIndex(j) != 0)
continue;
// with enough room around to build the cc
let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
// we require that it is accessible
let index = gameState.ai.accessibility.landPassMap[i];
if (!this.landRegions[index])
continue;
// checking distances to other cc
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
let minDist = Math.min();
distcc0 = undefined;
for (let cc of ccList)
{
let dist = API3.SquareVectorDistance(cc.pos, pos);
if (dist < 14000) // Reject if too near from any cc
{
minDist = 0;
break;
}
if (!cc.ally)
continue;
if (dist < 62000) // Reject if quite near from ally cc
{
minDist = 0;
break;
}
if (dist < minDist)
minDist = dist;
if (!distcc0 || dist < distcc0)
{
distcc2 = distcc1;
distcc1 = distcc0;
distcc0 = dist;
}
else if (!distcc1 || dist < distcc1)
{
distcc2 = distcc1;
distcc1 = dist;
}
else if (!distcc2 || dist < distcc2)
distcc2 = dist;
}
if (minDist < 1 || minDist > 170000 && !this.navalMap)
continue;
delta = Math.sqrt(distcc0) - favoredDistance;
currentVal = delta*delta;
delta = Math.sqrt(distcc1) - favoredDistance;
currentVal += delta*delta;
if (distcc2)
{
delta = Math.sqrt(distcc2) - favoredDistance;
currentVal += delta*delta;
}
// disfavor border of the map
if (this.borderMap.map[j] & PETRA.fullBorder_Mask)
currentVal += 10000;
if (bestVal !== undefined && currentVal > bestVal)
continue;
if (this.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = currentVal;
bestIdx = i;
}
if (this.Config.debug > 1)
API3.warn("We've found a strategic base with bestVal = " + bestVal);
Engine.ProfileStop();
if (bestVal === undefined)
return undefined;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
// Define a minimal number of wanted ships in the seas reaching this new base
let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx];
for (let base of this.baseManagers)
{
if (!base.anchor || base.accessIndex == indexIdx)
continue;
let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx);
if (sea !== undefined)
this.navalManager.setMinimalTransportShips(gameState, sea, 1);
}
return [x, z];
};
/**
* Returns the best position to build a new market: if the allies already have a market, build it as far as possible
* from it, although not in our border to be able to defend it easily. If no allied market, our second market will
* follow the same logic.
* To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance
* for performance reasons.
*/
PETRA.HQ.prototype.findMarketLocation = function(gameState, template)
{
- let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).toEntityArray();
+ let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Trade"), gameState.getExclusiveAllyEntities()).toEntityArray();
if (!markets.length)
- markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).toEntityArray();
+ markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Trade"), gameState.getOwnStructures()).toEntityArray();
if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan
return [-1, -1, -1, 0];
// No need for more than one market when we cannot trade.
if (!Resources.GetTradableCodes().length)
return false;
// obstruction map
let obstructions = PETRA.createObstructionMap(gameState, 0, template);
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
let bestIdx;
let bestJdx;
let bestVal;
let bestDistSq;
let bestGainMult;
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
- let isNavalMarket = template.hasClass("NavalMarket");
+ let isNavalMarket = template.hasClass("Naval") && template.hasClass("Trade");
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
let traderTemplatesGains = gameState.getTraderTemplatesGains();
for (let j = 0; j < this.territoryMap.length; ++j)
{
// do not try on the narrow border of our territory
if (this.borderMap.map[j] & PETRA.narrowFrontier_Mask)
continue;
if (this.basesMap.map[j] == 0) // only in our territory
continue;
// with enough room around to build the market
let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
let index = gameState.ai.accessibility.landPassMap[i];
if (!this.landRegions[index])
continue;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
// checking distances to other markets
let maxVal = 0;
let maxDistSq;
let maxGainMult;
let gainMultiplier;
for (let market of markets)
{
- if (isNavalMarket && market.hasClass("NavalMarket"))
+ if (isNavalMarket && template.hasClass("Naval") && template.hasClass("Trade"))
{
if (PETRA.getSeaAccess(gameState, market) != gameState.ai.accessibility.getAccessValue(pos, true))
continue;
gainMultiplier = traderTemplatesGains.navalGainMultiplier;
}
else if (PETRA.getLandAccess(gameState, market) == index &&
!PETRA.isLineInsideEnemyTerritory(gameState, market.position(), pos))
gainMultiplier = traderTemplatesGains.landGainMultiplier;
else
continue;
if (!gainMultiplier)
continue;
let distSq = API3.SquareVectorDistance(market.position(), pos);
if (gainMultiplier * distSq > maxVal)
{
maxVal = gainMultiplier * distSq;
maxDistSq = distSq;
maxGainMult = gainMultiplier;
}
}
if (maxVal == 0)
continue;
if (bestVal !== undefined && maxVal < bestVal)
continue;
if (this.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = maxVal;
bestDistSq = maxDistSq;
bestGainMult = maxGainMult;
bestIdx = i;
bestJdx = j;
}
if (this.Config.debug > 1)
API3.warn("We found a market position with bestVal = " + bestVal);
if (bestVal === undefined) // no constraints. For the time being, place it arbitrarily by the ConstructionPlan
return [-1, -1, -1, 0];
let expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize));
if (this.Config.debug > 1)
API3.warn("this would give a trading gain of " + expectedGain);
- // do not keep it if gain is too small, except if this is our first BarterMarket
+ // Do not keep it if gain is too small, except if this is our first Market.
let idx;
if (expectedGain < this.tradeManager.minimalGain)
{
- if (template.hasClass("BarterMarket") &&
- !gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities())
- idx = -1; // needed by queueplanBuilding manager to keep that market
+ if (template.hasClass("Market") &&
+ !gameState.getOwnEntitiesByClass("Market", true).hasEntities())
+ idx = -1; // Needed by queueplanBuilding manager to keep that Market.
else
return false;
}
else
idx = this.basesMap.map[bestJdx];
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return [x, z, idx, expectedGain];
};
/**
* Returns the best position to build defensive buildings (fortress and towers)
* Whose primary function is to defend our borders
*/
PETRA.HQ.prototype.findDefensiveLocation = function(gameState, template)
{
// We take the point in our territory which is the nearest to any enemy cc
// but requiring a minimal distance with our other defensive structures
// and not in range of any enemy defensive structure to avoid building under fire.
let ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Fortress", "Tower"])).toEntityArray();
let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))).
filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"]));
if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals
{
enemyStructures = gameState.getNeutralStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))).
filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"]));
if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory())
enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))).
filter(API3.Filters.byClassesOr(["CivCentre", "Fortress", "Tower"]));
if (!enemyStructures.hasEntities())
return undefined;
}
enemyStructures = enemyStructures.toEntityArray();
let wonderMode = gameState.getVictoryConditions().has("wonder");
let wonderDistmin;
let wonders;
if (wonderMode)
{
wonders = gameState.getOwnStructures().filter(API3.Filters.byClass("Wonder")).toEntityArray();
wonderMode = wonders.length != 0;
if (wonderMode)
wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius());
}
// obstruction map
let obstructions = PETRA.createObstructionMap(gameState, 0, template);
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
let bestIdx;
let bestJdx;
let bestVal;
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
let isTower = template.hasClass("Tower");
let isFortress = template.hasClass("Fortress");
let radius;
if (isFortress)
radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize);
else
radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (!wonderMode)
{
// do not try if well inside or outside territory
if (!(this.borderMap.map[j] & PETRA.fullFrontier_Mask))
continue;
if (this.borderMap.map[j] & PETRA.largeFrontier_Mask && isTower)
continue;
}
if (this.basesMap.map[j] == 0) // inaccessible cell
continue;
// with enough room around to build the cc
let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
// checking distances to other structures
let minDist = Math.min();
let dista = 0;
if (wonderMode)
{
dista = API3.SquareVectorDistance(wonders[0].position(), pos);
if (dista < wonderDistmin)
continue;
dista *= 200; // empirical factor (TODO should depend on map size) to stay near the wonder
}
for (let str of enemyStructures)
{
if (str.foundationProgress() !== undefined)
continue;
let strPos = str.position();
if (!strPos)
continue;
let dist = API3.SquareVectorDistance(strPos, pos);
if (dist < 6400) // TODO check on true attack range instead of this 80×80
{
minDist = -1;
break;
}
if (str.hasClass("CivCentre") && dist + dista < minDist)
minDist = dist + dista;
}
if (minDist < 0)
continue;
let cutDist = 900; // 30×30 TODO maybe increase it
for (let str of ownStructures)
{
let strPos = str.position();
if (!strPos)
continue;
if (API3.SquareVectorDistance(strPos, pos) < cutDist)
{
minDist = -1;
break;
}
}
if (minDist < 0 || minDist == Math.min())
continue;
if (bestVal !== undefined && minDist > bestVal)
continue;
if (this.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = minDist;
bestIdx = i;
bestJdx = j;
}
if (bestVal === undefined)
return undefined;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return [x, z, this.basesMap.map[bestJdx]];
};
PETRA.HQ.prototype.buildTemple = function(gameState, queues)
{
// at least one market (which have the same queue) should be build before any temple
if (queues.economicBuilding.hasQueuedUnits() ||
gameState.getOwnEntitiesByClass("Temple", true).hasEntities() ||
- !gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities())
+ !gameState.getOwnEntitiesByClass("Market", true).hasEntities())
return;
// Try to build a temple earlier if in regicide to recruit healer guards
if (this.currentPhase < 3 && !gameState.getVictoryConditions().has("regicide"))
return;
let templateName = "structures/{civ}_temple";
if (this.canBuild(gameState, "structures/{civ}_temple_vesta"))
templateName = "structures/{civ}_temple_vesta";
else if (!this.canBuild(gameState, templateName))
return;
queues.economicBuilding.addPlan(new PETRA.ConstructionPlan(gameState, templateName));
};
PETRA.HQ.prototype.buildMarket = function(gameState, queues)
{
- if (gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() ||
+ if (gameState.getOwnEntitiesByClass("Market", true).hasEntities() ||
!this.canBuild(gameState, "structures/{civ}_market"))
return;
- if (queues.economicBuilding.hasQueuedUnitsWithClass("BarterMarket"))
+ if (queues.economicBuilding.hasQueuedUnitsWithClass("Market"))
{
if (!queues.economicBuilding.paused)
{
// Put available resources in this market
let queueManager = gameState.ai.queueManager;
let cost = queues.economicBuilding.plans[0].getCost();
queueManager.setAccounts(gameState, cost, "economicBuilding");
if (!queueManager.canAfford("economicBuilding", cost))
{
for (let q in queueManager.queues)
{
if (q == "economicBuilding")
continue;
queueManager.transferAccounts(cost, q, "economicBuilding");
if (queueManager.canAfford("economicBuilding", cost))
break;
}
}
}
return;
}
gameState.ai.queueManager.changePriority("economicBuilding", 3 * this.Config.priorities.economicBuilding);
let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}_market");
plan.queueToReset = "economicBuilding";
queues.economicBuilding.addPlan(plan);
};
/** Build a farmstead */
PETRA.HQ.prototype.buildFarmstead = function(gameState, queues)
{
// Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs)
if (gameState.getOwnEntitiesByClass("Farmstead", true).hasEntities())
return;
// Wait to have at least one dropsite and house before the farmstead
if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities())
return;
if (!gameState.getOwnEntitiesByClass("House", true).hasEntities())
return;
if (queues.economicBuilding.hasQueuedUnitsWithClass("DropsiteFood"))
return;
if (!this.canBuild(gameState, "structures/{civ}_farmstead"))
return;
queues.economicBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_farmstead"));
};
/**
* Try to build a wonder when required
* force = true when called from the victoryManager in case of Wonder victory condition.
*/
PETRA.HQ.prototype.buildWonder = function(gameState, queues, force = false)
{
if (queues.wonder && queues.wonder.hasQueuedUnits() ||
gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() ||
!this.canBuild(gameState, "structures/{civ}_wonder"))
return;
if (!force)
{
let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_wonder"));
// Check that we have enough resources to start thinking to build a wonder
let cost = template.cost();
let resources = gameState.getResources();
let highLevel = 0;
let lowLevel = 0;
for (let res in cost)
{
if (resources[res] && resources[res] > 0.7 * cost[res])
++highLevel;
else if (!resources[res] || resources[res] < 0.3 * cost[res])
++lowLevel;
}
if (highLevel == 0 || lowLevel > 1)
return;
}
queues.wonder.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_wonder"));
};
/** Build a corral, and train animals there */
PETRA.HQ.prototype.manageCorral = function(gameState, queues)
{
if (queues.corral.hasQueuedUnits())
return;
let nCorral = gameState.getOwnEntitiesByClass("Corral", true).length;
if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_field")) &&
nCorral < this.currentPhase && gameState.getPopulation() > 30 * nCorral)
{
if (this.canBuild(gameState, "structures/{civ}_corral"))
{
queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_corral"));
return;
}
if (!nCorral)
return;
}
// And train some animals
let civ = gameState.getPlayerCiv();
for (let corral of gameState.getOwnEntitiesByClass("Corral", true).values())
{
if (corral.foundationProgress() !== undefined)
continue;
let trainables = corral.trainableEntities(civ);
for (let trainable of trainables)
{
if (gameState.isTemplateDisabled(trainable))
continue;
let template = gameState.getTemplate(trainable);
if (!template || !template.isHuntable())
continue;
let count = gameState.countEntitiesByType(trainable, true);
for (let item of corral.trainingQueue())
count += item.count;
if (count > nCorral)
continue;
queues.corral.addPlan(new PETRA.TrainingPlan(gameState, trainable, { "trainer": corral.id() }));
return;
}
}
};
/**
* build more houses if needed.
* kinda ugly, lots of special cases to both build enough houses but not tooo many…
*/
PETRA.HQ.prototype.buildMoreHouses = function(gameState, queues)
{
if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}_house")) ||
gameState.getPopulationMax() <= gameState.getPopulationLimit())
return;
let numPlanned = queues.house.length();
if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80)
{
let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}_house");
// change the starting condition according to the situation.
plan.goRequirement = "houseNeeded";
queues.house.addPlan(plan);
}
if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length)
{
let houseTemplateName = gameState.applyCiv("structures/{civ}_house");
let houseTemplate = gameState.getTemplate(houseTemplateName);
let needed = 0;
for (let entityReq of gameState.getPhaseEntityRequirements(this.phasing))
{
if (!houseTemplate.hasClass(entityReq.class))
continue;
let count = gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length;
if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName))
{
if (this.Config.debug > 1)
API3.warn("no room to place a house ... try to be less restrictive");
this.buildManager.setBuildable(houseTemplateName);
this.requireHouses = true;
}
needed = Math.max(needed, entityReq.count - count);
}
let houseQueue = queues.house.plans;
for (let i = 0; i < numPlanned; ++i)
if (houseQueue[i].isGo(gameState))
--needed;
else if (needed > 0)
{
houseQueue[i].goRequirement = undefined;
--needed;
}
}
if (this.requireHouses)
{
let houseTemplate = gameState.getTemplate(gameState.applyCiv("structures/{civ}_house"));
if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req =>
!houseTemplate.hasClass(req.class) || gameState.getOwnStructures().filter(API3.Filters.byClass(req.class)).length >= req.count))
this.requireHouses = undefined;
}
// When population limit too tight
// - if no room to build, try to improve with technology
// - otherwise increase temporarily the priority of houses
let house = gameState.applyCiv("structures/{civ}_house");
let HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length;
let popBonus = gameState.getTemplate(house).getPopulationBonus();
let freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - this.getAccountedPopulation(gameState);
let priority;
if (freeSlots < 5)
{
if (this.buildManager.isUnbuildable(gameState, house))
{
if (this.Config.debug > 1)
API3.warn("no room to place a house ... try to improve with technology");
this.researchManager.researchPopulationBonus(gameState, queues);
}
else
priority = 2 * this.Config.priorities.house;
}
else
priority = this.Config.priorities.house;
if (priority && priority != gameState.ai.queueManager.getPriority("house"))
gameState.ai.queueManager.changePriority("house", priority);
};
/** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */
PETRA.HQ.prototype.checkBaseExpansion = function(gameState, queues)
{
if (queues.civilCentre.hasQueuedUnits())
return;
// First build one cc if all have been destroyed
if (this.numPotentialBases() == 0)
{
this.buildFirstBase(gameState);
return;
}
// Then expand if we have not enough room available for buildings
if (this.buildManager.numberMissingRoom(gameState) > 1)
{
if (this.Config.debug > 2)
API3.warn("try to build a new base because not enough room to build ");
this.buildNewBase(gameState, queues);
return;
}
// If we've already planned to phase up, wait a bit before trying to expand
if (this.phasing)
return;
// Finally expand if we have lots of units (threshold depending on the aggressivity value)
let activeBases = this.numActiveBases();
let numUnits = gameState.getOwnUnits().length;
let numvar = 10 * (1 - this.Config.personality.aggressive);
if (numUnits > activeBases * (65 + numvar + (10 + numvar)*(activeBases-1)) || this.saveResources && numUnits > 50)
{
if (this.Config.debug > 2)
API3.warn("try to build a new base because of population " + numUnits + " for " + activeBases + " CCs");
this.buildNewBase(gameState, queues);
}
};
PETRA.HQ.prototype.buildNewBase = function(gameState, queues, resource)
{
if (this.numPotentialBases() > 0 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))
return false;
if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits())
return false;
let template;
// We require at least one of this civ civCentre as they may allow specific units or techs
let hasOwnCC = false;
for (let ent of gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).values())
{
if (ent.owner() != PlayerID || ent.templateName() != gameState.applyCiv("structures/{civ}_civil_centre"))
continue;
hasOwnCC = true;
break;
}
if (hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony"))
template = "structures/{civ}_military_colony";
else if (this.canBuild(gameState, "structures/{civ}_civil_centre"))
template = "structures/{civ}_civil_centre";
else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}_military_colony"))
template = "structures/{civ}_military_colony";
else
return false;
// base "-1" means new base.
if (this.Config.debug > 1)
API3.warn("new base " + gameState.applyCiv(template) + " planned with resource " + resource);
queues.civilCentre.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": -1, "resource": resource }));
return true;
};
/** Deals with building fortresses and towers along our border with enemies. */
PETRA.HQ.prototype.buildDefenses = function(gameState, queues)
{
if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits())
return;
if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3))))
{
// Try to build fortresses.
if (this.canBuild(gameState, "structures/{civ}_fortress"))
{
let numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length;
if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10 * numFortresses) * this.fortressLapseTime + this.fortressStartTime) &&
numFortresses < this.numActiveBases() + 1 + this.extraFortresses &&
numFortresses < Math.floor(gameState.getPopulation() / 25) &&
gameState.getOwnFoundationsByClass("Fortress").length < 2)
{
this.fortressStartTime = gameState.ai.elapsedTime;
if (!numFortresses)
gameState.ai.queueManager.changePriority("defenseBuilding", 2 * this.Config.priorities.defenseBuilding);
let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}_fortress");
plan.queueToReset = "defenseBuilding";
queues.defenseBuilding.addPlan(plan);
return;
}
}
}
if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}_sentry_tower"))
{
// Count all towers + wall towers.
let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length + gameState.getOwnEntitiesByClass("WallTower", true).length;
let towerLapseTime = this.saveResource ? (1 + 0.5 * numTowers) * this.towerLapseTime : this.towerLapseTime;
if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime)
{
this.fortStartTime = gameState.ai.elapsedTime;
queues.defenseBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_sentry_tower"));
}
return;
}
if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}_defense_tower"))
return;
let numTowers = gameState.getOwnEntitiesByClass("StoneTower", true).length;
let towerLapseTime = this.saveResource ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime;
if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1 * numTowers) * towerLapseTime + this.towerStartTime) &&
numTowers < 2 * this.numActiveBases() + 3 + this.extraTowers &&
numTowers < Math.floor(gameState.getPopulation() / 8) &&
gameState.getOwnFoundationsByClass("Tower").length < 3)
{
this.towerStartTime = gameState.ai.elapsedTime;
if (numTowers > 2 * this.numActiveBases() + 3)
gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7 * this.Config.priorities.defenseBuilding));
let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}_defense_tower");
plan.queueToReset = "defenseBuilding";
queues.defenseBuilding.addPlan(plan);
}
};
PETRA.HQ.prototype.buildBlacksmith = function(gameState, queues)
{
if (this.getAccountedPopulation(gameState) < this.Config.Military.popForBlacksmith ||
queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Blacksmith", true).length)
return;
// Build a market before the blacksmith.
- if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities())
+ if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities())
return;
if (this.canBuild(gameState, "structures/{civ}_blacksmith"))
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_blacksmith"));
};
/**
* Deals with constructing military buildings (barracks, stables…)
* They are mostly defined by Config.js. This is unreliable since changes could be done easily.
*/
PETRA.HQ.prototype.constructTrainingBuildings = function(gameState, queues)
{
if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits())
return;
let numBarracks = gameState.getOwnEntitiesByClass("Barracks", true).length;
if (this.saveResources && numBarracks != 0)
return;
let barracksTemplate = this.canBuild(gameState, "structures/{civ}_barracks") ? "structures/{civ}_barracks" : undefined;
let rangeTemplate = this.canBuild(gameState, "structures/{civ}_range") ? "structures/{civ}_range" : undefined;
let numRanges = gameState.getOwnEntitiesByClass("Range", true).length;
let stableTemplate = this.canBuild(gameState, "structures/{civ}_stable") ? "structures/{civ}_stable" : undefined;
let numStables = gameState.getOwnEntitiesByClass("Stable", true).length;
if (this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks1 ||
this.phasing == 2 && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5)
{
// first barracks/range and stables.
if (numBarracks + numRanges == 0)
{
let template = barracksTemplate || rangeTemplate;
if (template)
{
gameState.ai.queueManager.changePriority("militaryBuilding", 2 * this.Config.priorities.militaryBuilding);
let plan = new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true });
plan.queueToReset = "militaryBuilding";
queues.militaryBuilding.addPlan(plan);
return;
}
}
if (numStables == 0 && stableTemplate)
{
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true }));
return;
}
// Second range/barracks and stables
if (numBarracks + numRanges == 1 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2)
{
let template = numBarracks == 0 ? (barracksTemplate || rangeTemplate) : (rangeTemplate || barracksTemplate);
if (template)
{
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true }));
return;
}
}
if (numStables == 1 && stableTemplate && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2)
{
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true }));
return;
}
// Then 3rd barracks/range/stables if needed
if (numBarracks + numRanges + numStables == 2 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2 + 30)
{
let template = barracksTemplate || stableTemplate || rangeTemplate;
if (template)
{
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true }));
return;
}
}
}
if (this.saveResources)
return;
if (this.currentPhase < 3)
return;
if (this.canBuild(gameState, "structures/{civ}_elephant_stables") && !gameState.getOwnEntitiesByClass("ElephantStable", true).hasEntities())
{
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_elephant_stables", { "militaryBase": true }));
return;
}
if (this.canBuild(gameState, "structures/{civ}_workshop") && !gameState.getOwnEntitiesByClass("Workshop", true).hasEntities())
{
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_workshop", { "militaryBase": true }));
return;
}
if (this.getAccountedPopulation(gameState) < 80 || !this.bAdvanced.length)
return;
// Build advanced military buildings
let nAdvanced = 0;
for (let advanced of this.bAdvanced)
nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true);
if (!nAdvanced || nAdvanced < this.bAdvanced.length && this.getAccountedPopulation(gameState) > 110)
{
for (let advanced of this.bAdvanced)
{
if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced))
continue;
let template = gameState.getTemplate(advanced);
if (!template)
continue;
let civ = gameState.getPlayerCiv();
if (template.hasDefensiveFire() || template.trainableEntities(civ))
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, advanced, { "militaryBase": true }));
else // not a military building, but still use this queue
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, advanced));
return;
}
}
};
/**
* Find base nearest to ennemies for military buildings.
*/
PETRA.HQ.prototype.findBestBaseForMilitary = function(gameState)
{
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray();
let bestBase;
let enemyFound = false;
let distMin = Math.min();
for (let cce of ccEnts)
{
if (gameState.isPlayerAlly(cce.owner()))
continue;
if (enemyFound && !gameState.isPlayerEnemy(cce.owner()))
continue;
let access = PETRA.getLandAccess(gameState, cce);
let isEnemy = gameState.isPlayerEnemy(cce.owner());
for (let cc of ccEnts)
{
if (cc.owner() != PlayerID)
continue;
if (PETRA.getLandAccess(gameState, cc) != access)
continue;
let dist = API3.SquareVectorDistance(cc.position(), cce.position());
if (!enemyFound && isEnemy)
enemyFound = true;
else if (dist > distMin)
continue;
bestBase = cc.getMetadata(PlayerID, "base");
distMin = dist;
}
}
return bestBase;
};
/**
* train with highest priority ranged infantry in the nearest civil center from a given set of positions
* and garrison them there for defense
*/
PETRA.HQ.prototype.trainEmergencyUnits = function(gameState, positions)
{
if (gameState.ai.queues.emergency.hasQueuedUnits())
return false;
let civ = gameState.getPlayerCiv();
// find nearest base anchor
let distcut = 20000;
let nearestAnchor;
let distmin;
for (let pos of positions)
{
let access = gameState.ai.accessibility.getAccessValue(pos);
// check nearest base anchor
for (let base of this.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (PETRA.getLandAccess(gameState, base.anchor) != access)
continue;
if (!base.anchor.trainableEntities(civ)) // base still in construction
continue;
let queue = base.anchor._entity.trainingQueue;
if (queue)
{
let time = 0;
for (let item of queue)
if (item.progress > 0 || item.metadata && item.metadata.garrisonType)
time += item.timeRemaining;
if (time/1000 > 5)
continue;
}
let dist = API3.SquareVectorDistance(base.anchor.position(), pos);
if (nearestAnchor && dist > distmin)
continue;
distmin = dist;
nearestAnchor = base.anchor;
}
}
if (!nearestAnchor || distmin > distcut)
return false;
// We will choose randomly ranged and melee units, except when garrisonHolder is full
// in which case we prefer melee units
let numGarrisoned = this.garrisonManager.numberOfGarrisonedUnits(nearestAnchor);
if (nearestAnchor._entity.trainingQueue)
{
for (let item of nearestAnchor._entity.trainingQueue)
{
if (item.metadata && item.metadata.garrisonType)
numGarrisoned += item.count;
else if (!item.progress && (!item.metadata || !item.metadata.trainer))
nearestAnchor.stopProduction(item.id);
}
}
let autogarrison = numGarrisoned < nearestAnchor.garrisonMax() &&
nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints();
let rangedWanted = randBool() && autogarrison;
let total = gameState.getResources();
let templateFound;
let trainables = nearestAnchor.trainableEntities(civ);
let garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses();
for (let trainable of trainables)
{
if (gameState.isTemplateDisabled(trainable))
continue;
let template = gameState.getTemplate(trainable);
if (!template || !template.hasClass("Infantry") || !template.hasClass("CitizenSoldier"))
continue;
if (autogarrison && !MatchesClassList(template.classes(), garrisonArrowClasses))
continue;
if (!total.canAfford(new API3.Resources(template.cost())))
continue;
templateFound = [trainable, template];
if (template.hasClass("Ranged") == rangedWanted)
break;
}
if (!templateFound)
return false;
// Check first if we can afford it without touching the other accounts
// and if not, take some of other accounted resources
// TODO sort the queues to be substracted
let queueManager = gameState.ai.queueManager;
let cost = new API3.Resources(templateFound[1].cost());
queueManager.setAccounts(gameState, cost, "emergency");
if (!queueManager.canAfford("emergency", cost))
{
for (let q in queueManager.queues)
{
if (q == "emergency")
continue;
queueManager.transferAccounts(cost, q, "emergency");
if (queueManager.canAfford("emergency", cost))
break;
}
}
let metadata = { "role": "worker", "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() };
if (autogarrison)
metadata.garrisonType = "protection";
gameState.ai.queues.emergency.addPlan(new PETRA.TrainingPlan(gameState, templateFound[0], metadata, 1, 1));
return true;
};
PETRA.HQ.prototype.canBuild = function(gameState, structure)
{
let type = gameState.applyCiv(structure);
if (this.buildManager.isUnbuildable(gameState, type))
return false;
if (gameState.isTemplateDisabled(type))
{
this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled");
return false;
}
let template = gameState.getTemplate(type);
if (!template)
{
this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate");
return false;
}
if (!template.available(gameState))
{
this.buildManager.setUnbuildable(gameState, type, 30, "tech");
return false;
}
if (!this.buildManager.hasBuilder(type))
{
this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder");
return false;
}
if (this.numActiveBases() < 1)
{
// if no base, check that we can build outside our territory
let buildTerritories = template.buildTerritories();
if (buildTerritories && (!buildTerritories.length || buildTerritories.length == 1 && buildTerritories[0] == "own"))
{
this.buildManager.setUnbuildable(gameState, type, 180, "room");
return false;
}
}
// build limits
let limits = gameState.getEntityLimits();
let category = template.buildCategory();
if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category])
{
this.buildManager.setUnbuildable(gameState, type, 90, "limit");
return false;
}
return true;
};
PETRA.HQ.prototype.updateTerritories = function(gameState)
{
const around = [ [-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0] ];
let alliedVictory = gameState.getAlliedVictory();
let passabilityMap = gameState.getPassabilityMap();
let width = this.territoryMap.width;
let cellSize = this.territoryMap.cellSize;
let insideSmall = Math.round(45 / cellSize);
let insideLarge = Math.round(80 / cellSize); // should be about the range of towers
let expansion = 0;
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (this.borderMap.map[j] & PETRA.outside_Mask)
continue;
if (this.borderMap.map[j] & PETRA.fullFrontier_Mask)
this.borderMap.map[j] &= ~PETRA.fullFrontier_Mask; // reset the frontier
if (this.territoryMap.getOwnerIndex(j) != PlayerID)
{
// If this tile was already accounted, remove it
if (this.basesMap.map[j] == 0)
continue;
let base = this.getBaseByID(this.basesMap.map[j]);
if (base)
{
let index = base.territoryIndices.indexOf(j);
if (index != -1)
base.territoryIndices.splice(index, 1);
else
API3.warn(" problem in headquarters::updateTerritories for base " + this.basesMap.map[j]);
}
else
API3.warn(" problem in headquarters::updateTerritories without base " + this.basesMap.map[j]);
this.basesMap.map[j] = 0;
}
else
{
// Update the frontier
let ix = j%width;
let iz = Math.floor(j/width);
let onFrontier = false;
for (let a of around)
{
let jx = ix + Math.round(insideSmall*a[0]);
if (jx < 0 || jx >= width)
continue;
let jz = iz + Math.round(insideSmall*a[1]);
if (jz < 0 || jz >= width)
continue;
if (this.borderMap.map[jx+width*jz] & PETRA.outside_Mask)
continue;
let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz);
if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner)))
{
this.borderMap.map[j] |= PETRA.narrowFrontier_Mask;
break;
}
jx = ix + Math.round(insideLarge*a[0]);
if (jx < 0 || jx >= width)
continue;
jz = iz + Math.round(insideLarge*a[1]);
if (jz < 0 || jz >= width)
continue;
if (this.borderMap.map[jx+width*jz] & PETRA.outside_Mask)
continue;
territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz);
if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner)))
onFrontier = true;
}
if (onFrontier && !(this.borderMap.map[j] & PETRA.narrowFrontier_Mask))
this.borderMap.map[j] |= PETRA.largeFrontier_Mask;
// If this tile was not already accounted, add it.
if (this.basesMap.map[j] != 0)
continue;
let landPassable = false;
let ind = API3.getMapIndices(j, this.territoryMap, passabilityMap);
let access;
for (let k of ind)
{
if (!this.landRegions[gameState.ai.accessibility.landPassMap[k]])
continue;
landPassable = true;
access = gameState.ai.accessibility.landPassMap[k];
break;
}
if (!landPassable)
continue;
let distmin = Math.min();
let baseID;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
for (let base of this.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (base.accessIndex != access)
continue;
let dist = API3.SquareVectorDistance(base.anchor.position(), pos);
if (dist >= distmin)
continue;
distmin = dist;
baseID = base.ID;
}
if (!baseID)
continue;
this.getBaseByID(baseID).territoryIndices.push(j);
this.basesMap.map[j] = baseID;
expansion++;
}
}
if (!expansion)
return;
// We've increased our territory, so we may have some new room to build
this.buildManager.resetMissingRoom(gameState);
// And if sufficient expansion, check if building a new market would improve our present trade routes
let cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize;
if (expansion * cellArea > 960)
this.tradeManager.routeProspection = true;
};
/** Reassign territories when a base is going to be deleted */
PETRA.HQ.prototype.reassignTerritories = function(deletedBase)
{
let cellSize = this.territoryMap.cellSize;
let width = this.territoryMap.width;
for (let j = 0; j < this.territoryMap.length; ++j)
{
if (this.basesMap.map[j] != deletedBase.ID)
continue;
if (this.territoryMap.getOwnerIndex(j) != PlayerID)
{
API3.warn("Petra reassignTerritories: should never happen");
this.basesMap.map[j] = 0;
continue;
}
let distmin = Math.min();
let baseID;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
for (let base of this.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (base.accessIndex != deletedBase.accessIndex)
continue;
let dist = API3.SquareVectorDistance(base.anchor.position(), pos);
if (dist >= distmin)
continue;
distmin = dist;
baseID = base.ID;
}
if (baseID)
{
this.getBaseByID(baseID).territoryIndices.push(j);
this.basesMap.map[j] = baseID;
}
else
this.basesMap.map[j] = 0;
}
};
/**
* returns the base corresponding to baseID
*/
PETRA.HQ.prototype.getBaseByID = function(baseID)
{
for (let base of this.baseManagers)
if (base.ID == baseID)
return base;
return undefined;
};
/**
* returns the number of bases with a cc
* ActiveBases includes only those with a built cc
* PotentialBases includes also those with a cc in construction
*/
PETRA.HQ.prototype.numActiveBases = function()
{
if (!this.turnCache.base)
this.updateBaseCache();
return this.turnCache.base.active;
};
PETRA.HQ.prototype.numPotentialBases = function()
{
if (!this.turnCache.base)
this.updateBaseCache();
return this.turnCache.base.potential;
};
PETRA.HQ.prototype.updateBaseCache = function()
{
this.turnCache.base = { "active": 0, "potential": 0 };
for (let base of this.baseManagers)
{
if (!base.anchor)
continue;
++this.turnCache.base.potential;
if (base.anchor.foundationProgress() === undefined)
++this.turnCache.base.active;
}
};
PETRA.HQ.prototype.resetBaseCache = function()
{
this.turnCache.base = undefined;
};
/**
* Count gatherers returning resources in the number of gatherers of resourceSupplies
* to prevent the AI always reassigning idle workers to these resourceSupplies (specially in naval maps).
*/
PETRA.HQ.prototype.assignGatherers = function()
{
for (let base of this.baseManagers)
{
for (let worker of base.workers.values())
{
if (worker.unitAIState().split(".")[1] != "RETURNRESOURCE")
continue;
let orders = worker.unitAIOrderData();
if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply"))
continue;
this.AddTCGatherer(orders[1].target);
}
}
};
PETRA.HQ.prototype.isDangerousLocation = function(gameState, pos, radius)
{
return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius);
};
/** Check that the chosen position is not too near from an invading army */
PETRA.HQ.prototype.isNearInvadingArmy = function(pos)
{
for (let army of this.defenseManager.armies)
if (army.foePosition && API3.SquareVectorDistance(army.foePosition, pos) < 12000)
return true;
return false;
};
PETRA.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0)
{
if (!this.turnCache.firingStructures)
this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", API3.Filters.hasDefensiveFire(), gameState.getEnemyStructures());
for (let ent of this.turnCache.firingStructures.values())
{
let range = radius + ent.attackRange("Ranged").max;
if (API3.SquareVectorDistance(ent.position(), pos) < range*range)
return true;
}
return false;
};
/** Compute the capture strength of all units attacking a capturable target */
PETRA.HQ.prototype.updateCaptureStrength = function(gameState)
{
this.capturableTargets.clear();
for (let ent of gameState.getOwnUnits().values())
{
if (!ent.canCapture())
continue;
let state = ent.unitAIState();
if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT")
continue;
let orderData = ent.unitAIOrderData();
if (!orderData || !orderData.length || !orderData[0].target)
continue;
let targetId = orderData[0].target;
let target = gameState.getEntityById(targetId);
if (!target || !target.isCapturable() || !ent.canCapture(target))
continue;
if (!this.capturableTargets.has(targetId))
this.capturableTargets.set(targetId, {
"strength": ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"),
"ents": new Set([ent.id()])
});
else
{
let capturableTarget = this.capturableTargets.get(target.id());
capturableTarget.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
capturableTarget.ents.add(ent.id());
}
}
for (let [targetId, capturableTarget] of this.capturableTargets)
{
let target = gameState.getEntityById(targetId);
let allowCapture;
for (let entId of capturableTarget.ents)
{
let ent = gameState.getEntityById(entId);
if (allowCapture === undefined)
allowCapture = PETRA.allowCapture(gameState, ent, target);
let orderData = ent.unitAIOrderData();
if (!orderData || !orderData.length || !orderData[0].attackType)
continue;
if ((orderData[0].attackType == "Capture") !== allowCapture)
ent.attack(targetId, allowCapture);
}
}
this.capturableTargetsTime = gameState.ai.elapsedTime;
};
/** Some functions that register that we assigned a gatherer to a resource this turn */
/** add a gatherer to the turn cache for this supply. */
PETRA.HQ.prototype.AddTCGatherer = function(supplyID)
{
if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined)
++this.turnCache.resourceGatherer[supplyID];
else
{
if (!this.turnCache.resourceGatherer)
this.turnCache.resourceGatherer = {};
this.turnCache.resourceGatherer[supplyID] = 1;
}
};
/** remove a gatherer to the turn cache for this supply. */
PETRA.HQ.prototype.RemoveTCGatherer = function(supplyID)
{
if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID])
--this.turnCache.resourceGatherer[supplyID];
else
{
if (!this.turnCache.resourceGatherer)
this.turnCache.resourceGatherer = {};
this.turnCache.resourceGatherer[supplyID] = -1;
}
};
PETRA.HQ.prototype.GetTCGatherer = function(supplyID)
{
if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID])
return this.turnCache.resourceGatherer[supplyID];
return 0;
};
/** The next two are to register that we assigned a gatherer to a resource this turn. */
PETRA.HQ.prototype.AddTCResGatherer = function(resource)
{
if (this.turnCache["resourceGatherer-" + resource])
++this.turnCache["resourceGatherer-" + resource];
else
this.turnCache["resourceGatherer-" + resource] = 1;
if (this.turnCache.currentRates)
this.turnCache.currentRates[resource] += 0.5;
};
PETRA.HQ.prototype.GetTCResGatherer = function(resource)
{
if (this.turnCache["resourceGatherer-" + resource])
return this.turnCache["resourceGatherer-" + resource];
return 0;
};
/**
* flag a resource as exhausted
*/
PETRA.HQ.prototype.isResourceExhausted = function(resource)
{
if (this.turnCache["exhausted-" + resource] == undefined)
this.turnCache["exhausted-" + resource] = this.baseManagers.every(base =>
!base.dropsiteSupplies[resource].nearby.length &&
!base.dropsiteSupplies[resource].medium.length &&
!base.dropsiteSupplies[resource].faraway.length);
return this.turnCache["exhausted-" + resource];
};
/**
* Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around)
*/
PETRA.HQ.prototype.isDefendable = function(ent)
{
if (!this.turnCache.numAround)
this.turnCache.numAround = {};
if (this.turnCache.numAround[ent.id()] === undefined)
this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130);
return +this.turnCache.numAround[ent.id()] > 8;
};
/**
* Get the number of population already accounted for
*/
PETRA.HQ.prototype.getAccountedPopulation = function(gameState)
{
if (this.turnCache.accountedPopulation == undefined)
{
let pop = gameState.getPopulation();
for (let ent of gameState.getOwnTrainingFacilities().values())
{
for (let item of ent.trainingQueue())
{
if (!item.unitTemplate)
continue;
let unitPop = gameState.getTemplate(item.unitTemplate).get("Cost/Population");
if (unitPop)
pop += item.count * unitPop;
}
}
this.turnCache.accountedPopulation = pop;
}
return this.turnCache.accountedPopulation;
};
/**
* Get the number of workers already accounted for
*/
PETRA.HQ.prototype.getAccountedWorkers = function(gameState)
{
if (this.turnCache.accountedWorkers == undefined)
{
let workers = gameState.getOwnEntitiesByRole("worker", true).length;
for (let ent of gameState.getOwnTrainingFacilities().values())
{
for (let item of ent.trainingQueue())
{
if (!item.metadata || !item.metadata.role || item.metadata.role != "worker")
continue;
workers += item.count;
}
}
this.turnCache.accountedWorkers = workers;
}
return this.turnCache.accountedWorkers;
};
/**
* Some functions are run every turn
* Others once in a while
*/
PETRA.HQ.prototype.update = function(gameState, queues, events)
{
Engine.ProfileStart("Headquarters update");
this.turnCache = {};
this.territoryMap = PETRA.createTerritoryMap(gameState);
- this.canBarter = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).hasEntities();
+ this.canBarter = gameState.getOwnEntitiesByClass("Market", true).filter(API3.Filters.isBuilt()).hasEntities();
// TODO find a better way to update
if (this.currentPhase != gameState.currentPhase())
{
if (this.Config.debug > 0)
API3.warn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase +
" to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime +
" phasing " + this.phasing);
this.currentPhase = gameState.currentPhase();
// In principle, this.phasing should be already reset to 0 when starting the research
// but this does not work in case of an autoResearch tech
if (this.phasing)
this.phasing = 0;
}
/*
if (this.Config.debug > 1)
{
gameState.getOwnUnits().forEach (function (ent) {
if (!ent.position())
return;
PETRA.dumpEntity(ent);
});
}
*/
this.checkEvents(gameState, events);
this.navalManager.checkEvents(gameState, queues, events);
if (this.phasing)
this.checkPhaseRequirements(gameState, queues);
else
this.researchManager.checkPhase(gameState, queues);
if (this.numActiveBases() > 0)
{
if (gameState.ai.playedTurn % 4 == 0)
this.trainMoreWorkers(gameState, queues);
if (gameState.ai.playedTurn % 4 == 1)
this.buildMoreHouses(gameState, queues);
if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2)
this.buildFarmstead(gameState, queues);
if (this.needCorral && gameState.ai.playedTurn % 4 == 3)
this.manageCorral(gameState, queues);
if (!queues.minorTech.hasQueuedUnits() && gameState.ai.playedTurn % 5 == 1)
this.researchManager.update(gameState, queues);
}
if (this.numPotentialBases() < 1 ||
this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1)
this.checkBaseExpansion(gameState, queues);
if (this.currentPhase > 1 && gameState.ai.playedTurn % 3 == 0)
{
if (!this.canBarter)
this.buildMarket(gameState, queues);
if (!this.saveResources)
{
this.buildBlacksmith(gameState, queues);
this.buildTemple(gameState, queues);
}
if (gameState.ai.playedTurn % 30 == 0 &&
gameState.getPopulation() > 0.9 * gameState.getPopulationMax())
this.buildWonder(gameState, queues, false);
}
this.tradeManager.update(gameState, events, queues);
this.garrisonManager.update(gameState, events);
this.defenseManager.update(gameState, events);
if (gameState.ai.playedTurn % 3 == 0)
{
this.constructTrainingBuildings(gameState, queues);
if (this.Config.difficulty > 0)
this.buildDefenses(gameState, queues);
}
this.assignGatherers();
let nbBases = this.baseManagers.length;
let activeBase; // We will loop only on 1 active base per turn
do
{
this.currentBase %= this.baseManagers.length;
activeBase = this.baseManagers[this.currentBase++].update(gameState, queues, events);
--nbBases;
// TODO what to do with this.reassignTerritories(this.baseManagers[this.currentBase]);
}
while (!activeBase && nbBases != 0);
this.navalManager.update(gameState, queues, events);
if (this.Config.difficulty > 0 && (this.numActiveBases() > 0 || !this.canBuildUnits))
this.attackManager.update(gameState, queues, events);
this.diplomacyManager.update(gameState, events);
this.victoryManager.update(gameState, events, queues);
// We update the capture strength at the end as it can change attack orders
if (gameState.ai.elapsedTime - this.capturableTargetsTime > 3)
this.updateCaptureStrength(gameState);
Engine.ProfileStop();
};
PETRA.HQ.prototype.Serialize = function()
{
let properties = {
"phasing": this.phasing,
"currentBase": this.currentBase,
"lastFailedGather": this.lastFailedGather,
"firstBaseConfig": this.firstBaseConfig,
"supportRatio": this.supportRatio,
"targetNumWorkers": this.targetNumWorkers,
"fortStartTime": this.fortStartTime,
"towerStartTime": this.towerStartTime,
"fortressStartTime": this.fortressStartTime,
"bAdvanced": this.bAdvanced,
"saveResources": this.saveResources,
"saveSpace": this.saveSpace,
"needCorral": this.needCorral,
"needFarm": this.needFarm,
"needFish": this.needFish,
"maxFields": this.maxFields,
"canExpand": this.canExpand,
"canBuildUnits": this.canBuildUnits,
"navalMap": this.navalMap,
"landRegions": this.landRegions,
"navalRegions": this.navalRegions,
"decayingStructures": this.decayingStructures,
"capturableTargets": this.capturableTargets,
"capturableTargetsTime": this.capturableTargetsTime
};
let baseManagers = [];
for (let base of this.baseManagers)
baseManagers.push(base.Serialize());
if (this.Config.debug == -100)
{
API3.warn(" HQ serialization ---------------------");
API3.warn(" properties " + uneval(properties));
API3.warn(" baseManagers " + uneval(baseManagers));
API3.warn(" attackManager " + uneval(this.attackManager.Serialize()));
API3.warn(" buildManager " + uneval(this.buildManager.Serialize()));
API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize()));
API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize()));
API3.warn(" navalManager " + uneval(this.navalManager.Serialize()));
API3.warn(" researchManager " + uneval(this.researchManager.Serialize()));
API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize()));
API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize()));
API3.warn(" victoryManager " + uneval(this.victoryManager.Serialize()));
}
return {
"properties": properties,
"baseManagers": baseManagers,
"attackManager": this.attackManager.Serialize(),
"buildManager": this.buildManager.Serialize(),
"defenseManager": this.defenseManager.Serialize(),
"tradeManager": this.tradeManager.Serialize(),
"navalManager": this.navalManager.Serialize(),
"researchManager": this.researchManager.Serialize(),
"diplomacyManager": this.diplomacyManager.Serialize(),
"garrisonManager": this.garrisonManager.Serialize(),
"victoryManager": this.victoryManager.Serialize(),
};
};
PETRA.HQ.prototype.Deserialize = function(gameState, data)
{
for (let key in data.properties)
this[key] = data.properties[key];
this.baseManagers = [];
for (let base of data.baseManagers)
{
// the first call to deserialize set the ID base needed by entitycollections
let newbase = new PETRA.BaseManager(gameState, this.Config);
newbase.Deserialize(gameState, base);
newbase.init(gameState);
newbase.Deserialize(gameState, base);
this.baseManagers.push(newbase);
}
this.navalManager = new PETRA.NavalManager(this.Config);
this.navalManager.init(gameState, true);
this.navalManager.Deserialize(gameState, data.navalManager);
this.attackManager = new PETRA.AttackManager(this.Config);
this.attackManager.Deserialize(gameState, data.attackManager);
this.attackManager.init(gameState);
this.attackManager.Deserialize(gameState, data.attackManager);
this.buildManager = new PETRA.BuildManager();
this.buildManager.Deserialize(data.buildManager);
this.defenseManager = new PETRA.DefenseManager(this.Config);
this.defenseManager.Deserialize(gameState, data.defenseManager);
this.tradeManager = new PETRA.TradeManager(this.Config);
this.tradeManager.init(gameState);
this.tradeManager.Deserialize(gameState, data.tradeManager);
this.researchManager = new PETRA.ResearchManager(this.Config);
this.researchManager.Deserialize(data.researchManager);
this.diplomacyManager = new PETRA.DiplomacyManager(this.Config);
this.diplomacyManager.Deserialize(data.diplomacyManager);
this.garrisonManager = new PETRA.GarrisonManager(this.Config);
this.garrisonManager.Deserialize(data.garrisonManager);
this.victoryManager = new PETRA.VictoryManager(this.Config);
this.victoryManager.Deserialize(data.victoryManager);
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 23865)
@@ -1,888 +1,888 @@
/**
* Naval Manager
* Will deal with anything ships.
* -Basically trade over water (with fleets and goals commissioned by the economy manager)
* -Defense over water (commissioned by the defense manager)
* -Transport of units over water (a few units).
* -Scouting, ultimately.
* Also deals with handling docks, making sure we have access and stuffs like that.
*/
PETRA.NavalManager = function(Config)
{
this.Config = Config;
// ship subCollections. Also exist for land zones, idem, not caring.
this.seaShips = [];
this.seaTransportShips = [];
this.seaWarShips = [];
this.seaFishShips = [];
// wanted NB per zone.
this.wantedTransportShips = [];
this.wantedWarShips = [];
this.wantedFishShips = [];
// needed NB per zone.
this.neededTransportShips = [];
this.neededWarShips = [];
this.transportPlans = [];
// shore-line regions where we can load and unload units
this.landingZones = {};
};
/** More initialisation for stuff that needs the gameState */
PETRA.NavalManager.prototype.init = function(gameState, deserializing)
{
// docks
this.docks = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Dock", "Shipyard"]));
this.docks.registerUpdates();
this.ships = gameState.getOwnUnits().filter(API3.Filters.and(API3.Filters.byClass("Ship"), API3.Filters.not(API3.Filters.byMetadata(PlayerID, "role", "trader"))));
// note: those two can overlap (some transport ships are warships too and vice-versa).
this.transportShips = this.ships.filter(API3.Filters.and(API3.Filters.byCanGarrison(), API3.Filters.not(API3.Filters.byClass("FishingBoat"))));
this.warShips = this.ships.filter(API3.Filters.byClass("Warship"));
this.fishShips = this.ships.filter(API3.Filters.byClass("FishingBoat"));
this.ships.registerUpdates();
this.transportShips.registerUpdates();
this.warShips.registerUpdates();
this.fishShips.registerUpdates();
let availableFishes = {};
for (let fish of gameState.getFishableSupplies().values())
{
let sea = this.getFishSea(gameState, fish);
if (sea && availableFishes[sea])
availableFishes[sea] += fish.resourceSupplyAmount();
else if (sea)
availableFishes[sea] = fish.resourceSupplyAmount();
}
for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i)
{
if (!gameState.ai.HQ.navalRegions[i])
{
// push dummies
this.seaShips.push(undefined);
this.seaTransportShips.push(undefined);
this.seaWarShips.push(undefined);
this.seaFishShips.push(undefined);
this.wantedTransportShips.push(0);
this.wantedWarShips.push(0);
this.wantedFishShips.push(0);
this.neededTransportShips.push(0);
this.neededWarShips.push(0);
}
else
{
let collec = this.ships.filter(API3.Filters.byMetadata(PlayerID, "sea", i));
collec.registerUpdates();
this.seaShips.push(collec);
collec = this.transportShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i));
collec.registerUpdates();
this.seaTransportShips.push(collec);
collec = this.warShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i));
collec.registerUpdates();
this.seaWarShips.push(collec);
collec = this.fishShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i));
collec.registerUpdates();
this.seaFishShips.push(collec);
this.wantedTransportShips.push(0);
this.wantedWarShips.push(0);
if (availableFishes[i] && availableFishes[i] > 1000)
this.wantedFishShips.push(this.Config.Economy.targetNumFishers);
else
this.wantedFishShips.push(0);
this.neededTransportShips.push(0);
this.neededWarShips.push(0);
}
}
if (deserializing)
return;
// determination of the possible landing zones
let width = gameState.getPassabilityMap().width;
let length = width * gameState.getPassabilityMap().height;
for (let i = 0; i < length; ++i)
{
let land = gameState.ai.accessibility.landPassMap[i];
if (land < 2)
continue;
let naval = gameState.ai.accessibility.navalPassMap[i];
if (naval < 2)
continue;
if (!this.landingZones[land])
this.landingZones[land] = {};
if (!this.landingZones[land][naval])
this.landingZones[land][naval] = new Set();
this.landingZones[land][naval].add(i);
}
// and keep only thoses with enough room around when possible
for (let land in this.landingZones)
{
for (let sea in this.landingZones[land])
{
let landing = this.landingZones[land][sea];
let nbaround = {};
let nbcut = 0;
for (let i of landing)
{
let nb = 0;
if (landing.has(i-1))
nb++;
if (landing.has(i+1))
nb++;
if (landing.has(i+width))
nb++;
if (landing.has(i-width))
nb++;
nbaround[i] = nb;
nbcut = Math.max(nb, nbcut);
}
nbcut = Math.min(2, nbcut);
for (let i of landing)
{
if (nbaround[i] < nbcut)
landing.delete(i);
}
}
}
// Assign our initial docks and ships
for (let ship of this.ships.values())
PETRA.setSeaAccess(gameState, ship);
for (let dock of this.docks.values())
PETRA.setSeaAccess(gameState, dock);
};
PETRA.NavalManager.prototype.updateFishingBoats = function(sea, num)
{
if (this.wantedFishShips[sea])
this.wantedFishShips[sea] = num;
};
PETRA.NavalManager.prototype.resetFishingBoats = function(gameState, sea)
{
if (sea !== undefined)
this.wantedFishShips[sea] = 0;
else
this.wantedFishShips.fill(0);
};
/** Get the sea, cache it if not yet done and check if in opensea */
PETRA.NavalManager.prototype.getFishSea = function(gameState, fish)
{
let sea = fish.getMetadata(PlayerID, "sea");
if (sea)
return sea;
const ntry = 4;
const around = [[-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0]];
let pos = gameState.ai.accessibility.gamePosToMapPos(fish.position());
let width = gameState.ai.accessibility.width;
let k = pos[0] + pos[1]*width;
sea = gameState.ai.accessibility.navalPassMap[k];
fish.setMetadata(PlayerID, "sea", sea);
let radius = 120 / gameState.ai.accessibility.cellSize / ntry;
if (around.every(a => {
for (let t = 0; t < ntry; ++t)
{
let i = pos[0] + Math.round(a[0]*radius*(ntry-t));
let j = pos[1] + Math.round(a[1]*radius*(ntry-t));
if (i < 0 || i >= width || j < 0 || j >= width)
continue;
if (gameState.ai.accessibility.landPassMap[i + j*width] === 1)
{
let navalPass = gameState.ai.accessibility.navalPassMap[i + j*width];
if (navalPass == sea)
return true;
else if (navalPass == 1) // we could be outside the map
continue;
}
return false;
}
return true;
}))
fish.setMetadata(PlayerID, "opensea", true);
return sea;
};
/** check if we can safely fish at the fish position */
PETRA.NavalManager.prototype.canFishSafely = function(gameState, fish)
{
if (fish.getMetadata(PlayerID, "opensea"))
return true;
const ntry = 2;
const around = [[-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0]];
let territoryMap = gameState.ai.HQ.territoryMap;
let width = territoryMap.width;
let radius = 120 / territoryMap.cellSize / ntry;
let pos = territoryMap.gamePosToMapPos(fish.position());
return around.every(a => {
for (let t = 0; t < ntry; ++t)
{
let i = pos[0] + Math.round(a[0]*radius*(ntry-t));
let j = pos[1] + Math.round(a[1]*radius*(ntry-t));
if (i < 0 || i >= width || j < 0 || j >= width)
continue;
let owner = territoryMap.getOwnerIndex(i + j*width);
if (owner != 0 && gameState.isPlayerEnemy(owner))
return false;
}
return true;
});
};
/** get the list of seas (or lands) around this region not connected by a dock */
PETRA.NavalManager.prototype.getUnconnectedSeas = function(gameState, region)
{
let seas = gameState.ai.accessibility.regionLinks[region].slice();
this.docks.forEach(dock => {
if (!dock.hasClass("Dock") || PETRA.getLandAccess(gameState, dock) != region)
return;
let i = seas.indexOf(PETRA.getSeaAccess(gameState, dock));
if (i != -1)
seas.splice(i--, 1);
});
return seas;
};
PETRA.NavalManager.prototype.checkEvents = function(gameState, queues, events)
{
for (let evt of events.Create)
{
if (!evt.entity)
continue;
let ent = gameState.getEntityById(evt.entity);
if (ent && ent.isOwn(PlayerID) && ent.foundationProgress() !== undefined && (ent.hasClass("Dock") || ent.hasClass("Shipyard")))
PETRA.setSeaAccess(gameState, ent);
}
for (let evt of events.TrainingFinished)
{
if (!evt.entities)
continue;
for (let entId of evt.entities)
{
let ent = gameState.getEntityById(entId);
if (!ent || !ent.hasClass("Ship") || !ent.isOwn(PlayerID))
continue;
PETRA.setSeaAccess(gameState, ent);
}
}
for (let evt of events.Destroy)
{
if (!evt.entityObj || evt.entityObj.owner() !== PlayerID || !evt.metadata || !evt.metadata[PlayerID])
continue;
if (!evt.entityObj.hasClass("Ship") || !evt.metadata[PlayerID].transporter)
continue;
let plan = this.getPlan(evt.metadata[PlayerID].transporter);
if (!plan)
continue;
let shipId = evt.entityObj.id();
if (this.Config.debug > 1)
API3.warn("one ship " + shipId + " from plan " + plan.ID + " destroyed during " + plan.state);
if (plan.state == "boarding")
{
// just reset the units onBoard metadata and wait for a new ship to be assigned to this plan
plan.units.forEach(ent => {
if (ent.getMetadata(PlayerID, "onBoard") == "onBoard" && ent.position() ||
ent.getMetadata(PlayerID, "onBoard") == shipId)
ent.setMetadata(PlayerID, "onBoard", undefined);
});
plan.needTransportShips = !plan.transportShips.hasEntities();
}
else if (plan.state == "sailing")
{
let endIndex = plan.endIndex;
for (let ent of plan.units.values())
{
if (!ent.position()) // unit from another ship of this plan ... do nothing
continue;
let access = PETRA.getLandAccess(gameState, ent);
let endPos = ent.getMetadata(PlayerID, "endPos");
ent.setMetadata(PlayerID, "transport", undefined);
ent.setMetadata(PlayerID, "onBoard", undefined);
ent.setMetadata(PlayerID, "endPos", undefined);
// nothing else to do if access = endIndex as already at destination
// otherwise, we should require another transport
// TODO if attacking and no more ships available, remove the units from the attack
// to avoid delaying it too much
if (access != endIndex)
this.requireTransport(gameState, ent, access, endIndex, endPos);
}
}
}
for (let evt of events.OwnershipChanged) // capture events
{
if (evt.to !== PlayerID)
continue;
let ent = gameState.getEntityById(evt.entity);
if (ent && (ent.hasClass("Dock") || ent.hasClass("Shipyard")))
PETRA.setSeaAccess(gameState, ent);
}
};
PETRA.NavalManager.prototype.getPlan = function(ID)
{
for (let plan of this.transportPlans)
if (plan.ID === ID)
return plan;
return undefined;
};
PETRA.NavalManager.prototype.addPlan = function(plan)
{
this.transportPlans.push(plan);
};
/**
* complete already existing plan or create a new one for this requirement
* (many units can then call this separately and end up in the same plan)
* TODO check garrison classes
*/
PETRA.NavalManager.prototype.requireTransport = function(gameState, ent, startIndex, endIndex, endPos)
{
if (!ent.canGarrison())
return false;
if (ent.getMetadata(PlayerID, "transport") !== undefined)
{
if (this.Config.debug > 0)
API3.warn("Petra naval manager error: unit " + ent.id() + " has already required a transport");
return false;
}
let plans = [];
for (let plan of this.transportPlans)
{
if (plan.startIndex != startIndex || plan.endIndex != endIndex || plan.state != "boarding")
continue;
// Limit the number of siege units per transport to avoid problems when ungarrisoning
if (PETRA.isSiegeUnit(ent) && plan.units.filter(unit => PETRA.isSiegeUnit(unit)).length > 3)
continue;
plans.push(plan);
}
if (plans.length)
{
plans.sort(plan => plan.units.length);
plans[0].addUnit(ent, endPos);
return true;
}
let plan = new PETRA.TransportPlan(gameState, [ent], startIndex, endIndex, endPos);
if (plan.failed)
{
if (this.Config.debug > 1)
API3.warn(">>>> transport plan aborted <<<<");
return false;
}
plan.init(gameState);
this.transportPlans.push(plan);
return true;
};
/** split a transport plan in two, moving all entities not yet affected to a ship in the new plan */
PETRA.NavalManager.prototype.splitTransport = function(gameState, plan)
{
if (this.Config.debug > 1)
API3.warn(">>>> split of transport plan started <<<<");
let newplan = new PETRA.TransportPlan(gameState, [], plan.startIndex, plan.endIndex, plan.endPos);
if (newplan.failed)
{
if (this.Config.debug > 1)
API3.warn(">>>> split of transport plan aborted <<<<");
return false;
}
newplan.init(gameState);
for (let ent of plan.needSplit)
{
if (ent.getMetadata(PlayerID, "onBoard")) // Should never happen.
continue;
newplan.addUnit(ent, ent.getMetadata(PlayerID, "endPos"));
plan.units.updateEnt(ent);
}
if (newplan.units.length)
this.transportPlans.push(newplan);
return newplan.units.length != 0;
};
/**
* create a transport from a garrisoned ship to a land location
* needed at start game when starting with a garrisoned ship
*/
PETRA.NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos, toAccess)
{
let fromAccess = gameState.ai.accessibility.getAccessValue(fromPos);
if (fromAccess !== 1)
return;
if (toAccess < 2)
return;
for (let ship of this.ships.values())
{
if (!ship.isGarrisonHolder() || !ship.garrisoned().length)
continue;
if (ship.getMetadata(PlayerID, "transporter") !== undefined)
continue;
let units = [];
for (let entId of ship.garrisoned())
units.push(gameState.getEntityById(entId));
// TODO check that the garrisoned units have not another purpose
let plan = new PETRA.TransportPlan(gameState, units, fromAccess, toAccess, toPos, ship);
if (plan.failed)
continue;
plan.init(gameState);
this.transportPlans.push(plan);
}
};
// set minimal number of needed ships when a new event (new base or new attack plan)
PETRA.NavalManager.prototype.setMinimalTransportShips = function(gameState, sea, number)
{
if (!sea)
return;
if (this.wantedTransportShips[sea] < number)
this.wantedTransportShips[sea] = number;
};
// bumps up the number of ships we want if we need more.
PETRA.NavalManager.prototype.checkLevels = function(gameState, queues)
{
if (queues.ships.hasQueuedUnits())
return;
for (let sea = 0; sea < this.neededTransportShips.length; sea++)
this.neededTransportShips[sea] = 0;
for (let plan of this.transportPlans)
{
if (!plan.needTransportShips || plan.units.length < 2)
continue;
let sea = plan.sea;
if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0 ||
this.seaTransportShips[sea].length < this.wantedTransportShips[sea])
continue;
++this.neededTransportShips[sea];
if (this.wantedTransportShips[sea] === 0 || this.seaTransportShips[sea].length < plan.transportShips.length + 2)
{
++this.wantedTransportShips[sea];
return;
}
}
for (let sea = 0; sea < this.neededTransportShips.length; sea++)
if (this.neededTransportShips[sea] > 2)
++this.wantedTransportShips[sea];
};
PETRA.NavalManager.prototype.maintainFleet = function(gameState, queues)
{
if (queues.ships.hasQueuedUnits())
return;
if (!this.docks.filter(API3.Filters.isBuilt()).hasEntities())
return;
// check if we have enough transport ships per region.
for (let sea = 0; sea < this.seaShips.length; ++sea)
{
if (this.seaShips[sea] === undefined)
continue;
if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0)
continue;
if (this.seaTransportShips[sea].length < this.wantedTransportShips[sea])
{
let template = this.getBestShip(gameState, sea, "transport");
if (template)
{
queues.ships.addPlan(new PETRA.TrainingPlan(gameState, template, { "sea": sea }, 1, 1));
continue;
}
}
if (this.seaFishShips[sea].length < this.wantedFishShips[sea])
{
let template = this.getBestShip(gameState, sea, "fishing");
if (template)
{
queues.ships.addPlan(new PETRA.TrainingPlan(gameState, template, { "base": 0, "role": "worker", "sea": sea }, 1, 1));
continue;
}
}
}
};
/** assigns free ships to plans that need some */
PETRA.NavalManager.prototype.assignShipsToPlans = function(gameState)
{
for (let plan of this.transportPlans)
if (plan.needTransportShips)
plan.assignShip(gameState);
};
/** Return true if this ship is likeky (un)garrisoning units */
PETRA.NavalManager.prototype.isShipBoarding = function(ship)
{
if (!ship.position())
return false;
let plan = this.getPlan(ship.getMetadata(PlayerID, "transporter"));
if (!plan || !plan.boardingPos[ship.id()])
return false;
return API3.SquareVectorDistance(plan.boardingPos[ship.id()], ship.position()) < plan.boardingRange;
};
/** let blocking ships move apart from active ships (waiting for a better pathfinder)
* TODO Ships entity collections are currently in two parts as the trader ships are dealt with
* in the tradeManager. That should be modified to avoid dupplicating all the code here.
*/
PETRA.NavalManager.prototype.moveApart = function(gameState)
{
let blockedShips = [];
let blockedIds = [];
for (let ship of this.ships.values())
{
let shipPosition = ship.position();
if (!shipPosition)
continue;
if (ship.getMetadata(PlayerID, "transporter") !== undefined && this.isShipBoarding(ship))
continue;
let unitAIState = ship.unitAIState();
if (ship.getMetadata(PlayerID, "transporter") !== undefined ||
unitAIState == "INDIVIDUAL.GATHER.APPROACHING" ||
unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
{
let previousPosition = ship.getMetadata(PlayerID, "previousPosition");
if (!previousPosition || previousPosition[0] != shipPosition[0] ||
previousPosition[1] != shipPosition[1])
{
ship.setMetadata(PlayerID, "previousPosition", shipPosition);
ship.setMetadata(PlayerID, "turnPreviousPosition", gameState.ai.playedTurn);
continue;
}
// New transport ships receive boarding commands only on the following turn.
if (gameState.ai.playedTurn < ship.getMetadata(PlayerID, "turnPreviousPosition") + 2)
continue;
ship.moveToRange(shipPosition[0] + randFloat(-1, 1), shipPosition[1] + randFloat(-1, 1), 30, 30);
blockedShips.push(ship);
blockedIds.push(ship.id());
}
else if (ship.isIdle())
{
let previousIdlePosition = ship.getMetadata(PlayerID, "previousIdlePosition");
if (!previousIdlePosition || previousIdlePosition[0] != shipPosition[0] ||
previousIdlePosition[1] != shipPosition[1])
{
ship.setMetadata(PlayerID, "previousIdlePosition", shipPosition);
ship.setMetadata(PlayerID, "stationnary", undefined);
continue;
}
if (ship.getMetadata(PlayerID, "stationnary"))
continue;
ship.setMetadata(PlayerID, "stationnary", true);
// Check if there are some treasure around
if (PETRA.gatherTreasure(gameState, ship, true))
continue;
// Do not stay idle near a dock to not disturb other ships
let sea = ship.getMetadata(PlayerID, "sea");
for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values())
{
if (PETRA.getSeaAccess(gameState, dock) != sea)
continue;
if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900)
continue;
ship.moveToRange(dock.position()[0], dock.position()[1], 70, 70);
}
}
}
for (let ship of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values())
{
let shipPosition = ship.position();
if (!shipPosition)
continue;
let role = ship.getMetadata(PlayerID, "role");
if (!role || role != "trader") // already accounted before
continue;
let unitAIState = ship.unitAIState();
if (unitAIState == "INDIVIDUAL.TRADE.APPROACHINGMARKET")
{
let previousPosition = ship.getMetadata(PlayerID, "previousPosition");
if (!previousPosition || previousPosition[0] != shipPosition[0] ||
previousPosition[1] != shipPosition[1])
{
ship.setMetadata(PlayerID, "previousPosition", shipPosition);
ship.setMetadata(PlayerID, "turnPreviousPosition", gameState.ai.playedTurn);
continue;
}
// New transport ships receives boarding commands only on the following turn.
if (gameState.ai.playedTurn < ship.getMetadata(PlayerID, "turnPreviousPosition") + 2)
continue;
ship.moveToRange(shipPosition[0] + randFloat(-1, 1), shipPosition[1] + randFloat(-1, 1), 30, 30);
blockedShips.push(ship);
blockedIds.push(ship.id());
}
else if (ship.isIdle())
{
let previousIdlePosition = ship.getMetadata(PlayerID, "previousIdlePosition");
if (!previousIdlePosition || previousIdlePosition[0] != shipPosition[0] ||
previousIdlePosition[1] != shipPosition[1])
{
ship.setMetadata(PlayerID, "previousIdlePosition", shipPosition);
ship.setMetadata(PlayerID, "stationnary", undefined);
continue;
}
if (ship.getMetadata(PlayerID, "stationnary"))
continue;
ship.setMetadata(PlayerID, "stationnary", true);
// Check if there are some treasure around
if (PETRA.gatherTreasure(gameState, ship, true))
continue;
// Do not stay idle near a dock to not disturb other ships
let sea = ship.getMetadata(PlayerID, "sea");
for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values())
{
if (PETRA.getSeaAccess(gameState, dock) != sea)
continue;
if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900)
continue;
ship.moveToRange(dock.position()[0], dock.position()[1], 70, 70);
}
}
}
for (let ship of blockedShips)
{
let shipPosition = ship.position();
let sea = ship.getMetadata(PlayerID, "sea");
for (let blockingShip of this.seaShips[sea].values())
{
if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position())
continue;
let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position());
let unitAIState = blockingShip.unitAIState();
if (blockingShip.getMetadata(PlayerID, "transporter") === undefined &&
unitAIState != "INDIVIDUAL.GATHER.APPROACHING" &&
unitAIState != "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
{
if (distSquare < 1600)
blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 40);
}
else if (distSquare < 900)
blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 30);
}
for (let blockingShip of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values())
{
if (blockingShip.getMetadata(PlayerID, "sea") != sea)
continue;
if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position())
continue;
let role = blockingShip.getMetadata(PlayerID, "role");
if (!role || role != "trader") // already accounted before
continue;
let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position());
let unitAIState = blockingShip.unitAIState();
if (unitAIState != "INDIVIDUAL.TRADE.APPROACHINGMARKET")
{
if (distSquare < 1600)
blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 40);
}
else if (distSquare < 900)
blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 30);
}
}
};
PETRA.NavalManager.prototype.buildNavalStructures = function(gameState, queues)
{
if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.baseManagers[1])
return;
if (gameState.ai.HQ.getAccountedPopulation(gameState) > this.Config.Economy.popForDock)
{
- if (queues.dock.countQueuedUnitsWithClass("NavalMarket") === 0 &&
- !gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("NavalMarket"), API3.Filters.isFoundation())).hasEntities() &&
+ if (queues.dock.countQueuedUnitsWithClass("Dock") === 0 &&
+ !gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("Dock"), API3.Filters.isFoundation())).hasEntities() &&
gameState.ai.HQ.canBuild(gameState, "structures/{civ}_dock"))
{
let dockStarted = false;
for (let base of gameState.ai.HQ.baseManagers)
{
if (dockStarted)
break;
if (!base.anchor || base.constructing)
continue;
let remaining = this.getUnconnectedSeas(gameState, base.accessIndex);
for (let sea of remaining)
{
if (!gameState.ai.HQ.navalRegions[sea])
continue;
let wantedLand = {};
wantedLand[base.accessIndex] = true;
queues.dock.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_dock", { "land": wantedLand, "sea": sea }));
dockStarted = true;
break;
}
}
}
}
if (gameState.currentPhase() < 2 || gameState.ai.HQ.getAccountedPopulation(gameState) < this.Config.Economy.popPhase2 + 15 ||
queues.militaryBuilding.hasQueuedUnits())
return;
if (!this.docks.filter(API3.Filters.byClass("Dock")).hasEntities() ||
this.docks.filter(API3.Filters.byClass("Shipyard")).hasEntities())
return;
- // Use in priority resources to build a market
- if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() &&
+ // Use in priority resources to build a Market.
+ if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() &&
gameState.ai.HQ.canBuild(gameState, "structures/{civ}_market"))
return;
let template;
if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_super_dock"))
template = "structures/{civ}_super_dock";
else if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_shipyard"))
template = "structures/{civ}_shipyard";
else
return;
let wantedLand = {};
for (let base of gameState.ai.HQ.baseManagers)
if (base.anchor)
wantedLand[base.accessIndex] = true;
let sea = this.docks.toEntityArray()[0].getMetadata(PlayerID, "sea");
queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "land": wantedLand, "sea": sea }));
};
/** goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) */
PETRA.NavalManager.prototype.getBestShip = function(gameState, sea, goal)
{
let civ = gameState.getPlayerCiv();
let trainableShips = [];
gameState.getOwnTrainingFacilities().filter(API3.Filters.byMetadata(PlayerID, "sea", sea)).forEach(function(ent) {
let trainables = ent.trainableEntities(civ);
for (let trainable of trainables)
{
if (gameState.isTemplateDisabled(trainable))
continue;
let template = gameState.getTemplate(trainable);
if (template && template.hasClass("Ship") && trainableShips.indexOf(trainable) === -1)
trainableShips.push(trainable);
}
});
let best = 0;
let bestShip;
let limits = gameState.getEntityLimits();
let current = gameState.getEntityCounts();
for (let trainable of trainableShips)
{
let template = gameState.getTemplate(trainable);
if (!template.available(gameState))
continue;
let category = template.trainingCategory();
if (category && limits[category] && current[category] >= limits[category])
continue;
let arrows = +(template.getDefaultArrow() || 0);
if (goal === "attack") // choose the maximum default arrows
{
if (best > arrows)
continue;
best = arrows;
}
else if (goal === "transport") // choose the maximum capacity, with a bonus if arrows or if siege transport
{
let capacity = +(template.garrisonMax() || 0);
if (capacity < 2)
continue;
capacity += 10*arrows;
if (MatchesClassList(template.garrisonableClasses(), "Siege"))
capacity += 50;
if (best > capacity)
continue;
best = capacity;
}
else if (goal === "fishing")
if (!template.hasClass("FishingBoat"))
continue;
bestShip = trainable;
}
return bestShip;
};
PETRA.NavalManager.prototype.update = function(gameState, queues, events)
{
Engine.ProfileStart("Naval Manager update");
// close previous transport plans if finished
for (let i = 0; i < this.transportPlans.length; ++i)
{
let remaining = this.transportPlans[i].update(gameState);
if (remaining)
continue;
if (this.Config.debug > 1)
API3.warn("no more units on transport plan " + this.transportPlans[i].ID);
this.transportPlans[i].releaseAll();
this.transportPlans.splice(i--, 1);
}
// assign free ships to plans which need them
this.assignShipsToPlans(gameState);
// and require for more ships/structures if needed
if (gameState.ai.playedTurn % 3 === 0)
{
this.checkLevels(gameState, queues);
this.maintainFleet(gameState, queues);
this.buildNavalStructures(gameState, queues);
}
// let inactive ships move apart from active ones (waiting for a better pathfinder)
this.moveApart(gameState);
Engine.ProfileStop();
};
PETRA.NavalManager.prototype.Serialize = function()
{
let properties = {
"wantedTransportShips": this.wantedTransportShips,
"wantedWarShips": this.wantedWarShips,
"wantedFishShips": this.wantedFishShips,
"neededTransportShips": this.neededTransportShips,
"neededWarShips": this.neededWarShips,
"landingZones": this.landingZones
};
let transports = {};
for (let plan in this.transportPlans)
transports[plan] = this.transportPlans[plan].Serialize();
return { "properties": properties, "transports": transports };
};
PETRA.NavalManager.prototype.Deserialize = function(gameState, data)
{
for (let key in data.properties)
this[key] = data.properties[key];
this.transportPlans = [];
for (let i in data.transports)
{
let dataPlan = data.transports[i];
let plan = new PETRA.TransportPlan(gameState, [], dataPlan.startIndex, dataPlan.endIndex, dataPlan.endPos);
plan.Deserialize(dataPlan);
plan.init(gameState);
this.transportPlans.push(plan);
}
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 23865)
@@ -1,945 +1,945 @@
/**
* Defines a construction plan, ie a building.
* We'll try to fing a good position if non has been provided
*/
PETRA.ConstructionPlan = function(gameState, type, metadata, position)
{
if (!PETRA.QueuePlan.call(this, gameState, type, metadata))
return false;
this.position = position ? position : 0;
this.category = "building";
return true;
};
PETRA.ConstructionPlan.prototype = Object.create(PETRA.QueuePlan.prototype);
PETRA.ConstructionPlan.prototype.canStart = function(gameState)
{
if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn
return false;
if (!this.isGo(gameState))
return false;
if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech()))
return false;
return gameState.ai.HQ.buildManager.hasBuilder(this.type);
};
PETRA.ConstructionPlan.prototype.start = function(gameState)
{
Engine.ProfileStart("Building construction start");
// We don't care which builder we assign, since they won't actually do
// the building themselves - all we care about is that there is at least
// one unit that can start the foundation (should always be the case here).
let builder = gameState.findBuilder(this.type);
if (!builder)
{
API3.warn("petra error: builder not found when starting construction.");
Engine.ProfileStop();
return;
}
let pos = this.findGoodPosition(gameState);
if (!pos)
{
gameState.ai.HQ.buildManager.setUnbuildable(gameState, this.type, 90, "room");
Engine.ProfileStop();
return;
}
- if (this.metadata && this.metadata.expectedGain && (!this.template.hasClass("BarterMarket") ||
- gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities()))
+ if (this.metadata && this.metadata.expectedGain && (!this.template.hasClass("Market") ||
+ gameState.getOwnEntitiesByClass("Market", true).hasEntities()))
{
- // Check if this market is still worth building (others may have been built making it useless)
+ // Check if this Market is still worth building (others may have been built making it useless).
let tradeManager = gameState.ai.HQ.tradeManager;
tradeManager.checkRoutes(gameState);
if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain))
{
Engine.ProfileStop();
return;
}
}
gameState.ai.HQ.turnCache.buildingBuilt = true;
if (this.metadata === undefined)
this.metadata = { "base": pos.base };
else if (this.metadata.base === undefined)
this.metadata.base = pos.base;
if (pos.access)
this.metadata.access = pos.access; // needed for Docks whose position is on water
else
this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]);
if (this.template.buildPlacementType() == "shore")
{
// adjust a bit the position if needed
let cosa = Math.cos(pos.angle);
let sina = Math.sin(pos.angle);
let shiftMax = gameState.ai.HQ.territoryMap.cellSize;
for (let shift = 0; shift <= shiftMax; shift += 2)
{
builder.construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata);
if (shift > 0)
builder.construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata);
}
}
else if (pos.xx === undefined || pos.x == pos.xx && pos.z == pos.zz)
builder.construct(this.type, pos.x, pos.z, pos.angle, this.metadata);
else // try with the lowest, move towards us unless we're same
{
for (let step = 0; step <= 1; step += 0.2)
builder.construct(this.type, step*pos.x + (1-step)*pos.xx, step*pos.z + (1-step)*pos.zz,
pos.angle, this.metadata);
}
this.onStart(gameState);
Engine.ProfileStop();
if (this.metadata && this.metadata.proximity)
gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access);
};
PETRA.ConstructionPlan.prototype.findGoodPosition = function(gameState)
{
let template = this.template;
if (template.buildPlacementType() == "shore")
return this.findDockPosition(gameState);
let HQ = gameState.ai.HQ;
if (template.hasClass("Storehouse") && this.metadata && this.metadata.base)
{
// recompute the best dropsite location in case some conditions have changed
let base = HQ.getBaseByID(this.metadata.base);
let type = this.metadata.type ? this.metadata.type : "wood";
let newpos = base.findBestDropsiteLocation(gameState, type);
if (newpos && newpos.quality > 0)
{
let pos = newpos.pos;
return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base };
}
}
if (!this.position)
{
if (template.hasClass("CivCentre"))
{
let pos;
if (this.metadata && this.metadata.resource)
{
let proximity = this.metadata.proximity ? this.metadata.proximity : undefined;
pos = HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity);
}
else
pos = HQ.findStrategicCCLocation(gameState, template);
if (pos)
return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 };
// No possible location, try to build instead a dock in a not-enemy island
let templateName = gameState.applyCiv("structures/{civ}_dock");
if (gameState.ai.HQ.canBuild(gameState, templateName) && !gameState.isTemplateDisabled(templateName))
{
template = gameState.getTemplate(templateName);
if (template && gameState.getResources().canAfford(new API3.Resources(template.cost())))
this.buildOverseaDock(gameState, template);
}
return false;
}
else if (template.hasClass("Tower") || template.hasClass("Fortress") || template.hasClass("ArmyCamp"))
{
let pos = HQ.findDefensiveLocation(gameState, template);
if (pos)
return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] };
// if this fortress is our first one, just try the standard placement
if (!template.hasClass("Fortress") || gameState.getOwnEntitiesByClass("Fortress", true).hasEntities())
return false;
}
- else if (template.hasClass("Market")) // Docks (i.e. NavalMarket) are done before
+ else if (template.hasClass("Market")) // Docks are done before.
{
let pos = HQ.findMarketLocation(gameState, template);
if (pos && pos[2] > 0)
{
if (!this.metadata)
this.metadata = {};
this.metadata.expectedGain = pos[3];
return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] };
}
else if (!pos)
return false;
}
}
// Compute each tile's closeness to friendly structures:
let placement = new API3.Map(gameState.sharedScript, "territory");
let cellSize = placement.cellSize; // size of each tile
let alreadyHasHouses = false;
if (this.position) // If a position was specified then place the building as close to it as possible
{
let x = Math.floor(this.position[0] / cellSize);
let z = Math.floor(this.position[1] / cellSize);
placement.addInfluence(x, z, 255);
}
else // No position was specified so try and find a sensible place to build
{
// give a small > 0 level as the result of addInfluence is constrained to be > 0
// if we really need houses (i.e. Phasing without enough village building), do not apply these constraints
if (this.metadata && this.metadata.base !== undefined)
{
let base = this.metadata.base;
for (let j = 0; j < placement.map.length; ++j)
if (HQ.basesMap.map[j] == base)
placement.set(j, 45);
}
else
{
for (let j = 0; j < placement.map.length; ++j)
if (HQ.basesMap.map[j] != 0)
placement.set(j, 45);
}
if (!HQ.requireHouses || !template.hasClass("House"))
{
gameState.getOwnStructures().forEach(function(ent) {
let pos = ent.position();
let x = Math.round(pos[0] / cellSize);
let z = Math.round(pos[1] / cellSize);
let struct = PETRA.getBuiltEntity(gameState, ent);
if (struct.resourceDropsiteTypes() && struct.resourceDropsiteTypes().indexOf("food") != -1)
{
if (template.hasClass("Field") || template.hasClass("Corral"))
- placement.addInfluence(x, z, 80/cellSize, 50);
+ placement.addInfluence(x, z, 80 / cellSize, 50);
else // If this is not a field add a negative influence because we want to leave this area for fields
- placement.addInfluence(x, z, 80/cellSize, -20);
+ placement.addInfluence(x, z, 80 / cellSize, -20);
}
else if (template.hasClass("House"))
{
if (ent.hasClass("House"))
{
- placement.addInfluence(x, z, 60/cellSize, 40); // houses are close to other houses
+ placement.addInfluence(x, z, 60 / cellSize, 40); // houses are close to other houses
alreadyHasHouses = true;
}
- else if (!ent.hasClass("StoneWall") || ent.hasClass("Gates"))
- placement.addInfluence(x, z, 60/cellSize, -40); // and further away from other stuffs
+ else if (!ent.hasClass("Wall") || ent.hasClass("Gate"))
+ placement.addInfluence(x, z, 60 / cellSize, -40); // and further away from other stuffs
}
else if (template.hasClass("Farmstead") && (!ent.hasClass("Field") && !ent.hasClass("Corral") &&
- (!ent.hasClass("StoneWall") || ent.hasClass("Gates"))))
- placement.addInfluence(x, z, 100/cellSize, -25); // move farmsteads away to make room (StoneWall test needed for iber)
+ (!ent.hasClass("Wall") || ent.hasClass("Gate"))))
+ placement.addInfluence(x, z, 100 / cellSize, -25); // move farmsteads away to make room (Wall test needed for iber)
else if (template.hasClass("GarrisonFortress") && ent.hasClass("House"))
- placement.addInfluence(x, z, 120/cellSize, -50);
+ placement.addInfluence(x, z, 120 / cellSize, -50);
else if (template.hasClass("Military"))
- placement.addInfluence(x, z, 40/cellSize, -40);
+ placement.addInfluence(x, z, 40 / cellSize, -40);
else if (template.genericName() == "Rotary Mill" && ent.hasClass("Field"))
- placement.addInfluence(x, z, 60/cellSize, 40);
+ placement.addInfluence(x, z, 60 / cellSize, 40);
});
}
if (template.hasClass("Farmstead"))
{
for (let j = 0; j < placement.map.length; ++j)
{
let value = placement.map[j] - gameState.sharedScript.resourceMaps.wood.map[j]/3;
if (HQ.borderMap.map[j] & PETRA.fullBorder_Mask)
value /= 2; // we need space around farmstead, so disfavor map border
placement.set(j, value);
}
}
}
// Requires to be inside our territory, and inside our base territory if required
- // and if our first market, put it on border if possible to maximize distance with next market
- let favorBorder = template.hasClass("BarterMarket");
+ // and if our first market, put it on border if possible to maximize distance with next Market.
+ let favorBorder = template.hasClass("Market");
let disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire();
let favoredBase = this.metadata && (this.metadata.favoredBase ||
(this.metadata.militaryBase ? HQ.findBestBaseForMilitary(gameState) : undefined));
if (this.metadata && this.metadata.base !== undefined)
{
let base = this.metadata.base;
for (let j = 0; j < placement.map.length; ++j)
{
if (HQ.basesMap.map[j] != base)
placement.map[j] = 0;
else if (placement.map[j] > 0)
{
if (favorBorder && HQ.borderMap.map[j] & PETRA.border_Mask)
placement.set(j, placement.map[j] + 50);
else if (disfavorBorder && !(HQ.borderMap.map[j] & PETRA.fullBorder_Mask))
placement.set(j, placement.map[j] + 10);
let x = (j % placement.width + 0.5) * cellSize;
let z = (Math.floor(j / placement.width) + 0.5) * cellSize;
if (HQ.isNearInvadingArmy([x, z]))
placement.map[j] = 0;
}
}
}
else
{
for (let j = 0; j < placement.map.length; ++j)
{
if (HQ.basesMap.map[j] == 0)
placement.map[j] = 0;
else if (placement.map[j] > 0)
{
if (favorBorder && HQ.borderMap.map[j] & PETRA.border_Mask)
placement.set(j, placement.map[j] + 50);
else if (disfavorBorder && !(HQ.borderMap.map[j] & PETRA.fullBorder_Mask))
placement.set(j, placement.map[j] + 10);
let x = (j % placement.width + 0.5) * cellSize;
let z = (Math.floor(j / placement.width) + 0.5) * cellSize;
if (HQ.isNearInvadingArmy([x, z]))
placement.map[j] = 0;
else if (favoredBase && HQ.basesMap.map[j] == favoredBase)
placement.set(j, placement.map[j] + 100);
}
}
}
// Find the best non-obstructed:
// Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close,
// this allows room for units to walk between buildings.
// note: not for houses and dropsites who ought to be closer to either each other or a resource.
// also not for fields who can be stacked quite a bit
let obstructions = PETRA.createObstructionMap(gameState, 0, template);
// obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png");
let radius = 0;
if (template.hasClass("Fortress") || template.hasClass("Workshop") ||
this.type == gameState.applyCiv("structures/{civ}_elephant_stables"))
radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize);
else if (template.resourceDropsiteTypes() === undefined && !template.hasClass("House") &&
- !template.hasClass("Field") && !template.hasClass("BarterMarket"))
+ !template.hasClass("Field") && !template.hasClass("Market"))
radius = Math.ceil((template.obstructionRadius().max + 4) / obstructions.cellSize);
else
radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let bestTile;
if (template.hasClass("House") && !alreadyHasHouses)
{
// try to get some space to place several houses first
bestTile = placement.findBestTile(3*radius, obstructions);
if (!bestTile.val)
bestTile = undefined;
}
if (!bestTile)
bestTile = placement.findBestTile(radius, obstructions);
if (!bestTile.val)
return false;
let bestIdx = bestTile.idx;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
let territorypos = placement.gamePosToMapPos([x, z]);
let territoryIndex = territorypos[0] + territorypos[1]*placement.width;
// default angle = 3*Math.PI/4;
return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.basesMap.map[territoryIndex] };
};
/**
* Placement of buildings with Dock build category
* metadata.proximity is defined when first dock without any territory
* => we try to minimize distance from our current point
* metadata.oversea is defined for dock in oversea islands
* => we try to maximize distance to our current docks (for trade)
* otherwise standard dock on an island where we already have a cc
* => we try not to be too far from our territory
* In all cases, we add a bonus for nearby resources, and when a large extend of water in front ot it.
*/
PETRA.ConstructionPlan.prototype.findDockPosition = function(gameState)
{
let template = this.template;
let territoryMap = gameState.ai.HQ.territoryMap;
let obstructions = PETRA.createObstructionMap(gameState, 0, template);
// obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png");
let bestIdx;
let bestJdx;
let bestAngle;
let bestLand;
let bestWater;
let bestVal = -1;
let navalPassMap = gameState.ai.accessibility.navalPassMap;
let width = gameState.ai.HQ.territoryMap.width;
let cellSize = gameState.ai.HQ.territoryMap.cellSize;
let nbShips = gameState.ai.HQ.navalManager.transportShips.length;
let wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null;
let wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null;
let proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null;
let oversea = this.metadata && this.metadata.oversea ? this.metadata.oversea : null;
if (nbShips == 0 && proxyAccess && proxyAccess > 1)
{
wantedLand = {};
wantedLand[proxyAccess] = true;
}
let dropsiteTypes = template.resourceDropsiteTypes();
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let halfSize = 0; // used for dock angle
let halfDepth = 0; // used by checkPlacement
let halfWidth = 0; // used by checkPlacement
if (template.get("Footprint/Square"))
{
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
halfDepth = +template.get("Footprint/Square/@depth") / 2;
halfWidth = +template.get("Footprint/Square/@width") / 2;
}
else if (template.get("Footprint/Circle"))
{
halfSize = +template.get("Footprint/Circle/@radius");
halfDepth = halfSize;
halfWidth = halfSize;
}
// res is a measure of the amount of resources around, and maxRes is the max value taken into account
// water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement
const maxRes = 10;
const maxWater = 16;
let ccEnts = oversea ? gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")) : null;
let docks = oversea ? gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")) : null;
// Normalisation factors (only guessed, no attempt to optimize them)
let factor = proxyAccess ? 1 : oversea ? 0.2 : 40;
for (let j = 0; j < territoryMap.length; ++j)
{
if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea))
continue;
let score = 0;
if (!proxyAccess && !oversea)
{
// if not in our (or allied) territory, we do not want it too far to be able to defend it
score = this.getFrontierProximity(gameState, j);
if (score > 4)
continue;
score *= factor;
}
let i = territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
if (wantedSea && navalPassMap[i] != wantedSea)
continue;
let res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
// If proximity is given, we look for the nearest point
if (proxyAccess)
score = API3.VectorDistance(this.metadata.proximity, pos);
// Bonus for resources
score += 20 * (maxRes - res);
if (oversea)
{
// Not much farther to one of our cc than to enemy ones
let enemyDist;
let ownDist;
for (let cc of ccEnts.values())
{
let owner = cc.owner();
if (owner != PlayerID && !gameState.isPlayerEnemy(owner))
continue;
let dist = API3.SquareVectorDistance(pos, cc.position());
if (owner == PlayerID && (!ownDist || dist < ownDist))
ownDist = dist;
if (gameState.isPlayerEnemy(owner) && (!enemyDist || dist < enemyDist))
enemyDist = dist;
}
if (ownDist && enemyDist && enemyDist < 0.5 * ownDist)
continue;
// And maximize distance for trade.
let dockDist = 0;
for (let dock of docks.values())
{
if (PETRA.getSeaAccess(gameState, dock) != navalPassMap[i])
continue;
let dist = API3.SquareVectorDistance(pos, dock.position());
if (dist > dockDist)
dockDist = dist;
}
if (dockDist > 0)
{
dockDist = Math.sqrt(dockDist);
if (dockDist > width * cellSize) // Could happen only on square maps, but anyway we don't want to be too far away
continue;
score += factor * (width * cellSize - dockDist);
}
}
// Add a penalty if on the map border as ship movement will be difficult
if (gameState.ai.HQ.borderMap.map[j] & PETRA.fullBorder_Mask)
score += 20;
// Do a pre-selection, supposing we will have the best possible water
if (bestIdx !== undefined && score > bestVal + 5 * maxWater)
continue;
let x = (i % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize;
let angle = this.getDockAngle(gameState, x, z, halfSize);
if (angle == false)
continue;
let ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle);
if (!ret || !gameState.ai.HQ.landRegions[ret.land] || wantedLand && !wantedLand[ret.land])
continue;
// Final selection now that the checkDockPlacement water is known
if (bestIdx !== undefined && score + 5 * (maxWater - ret.water) > bestVal)
continue;
if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000)
continue;
if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = score + maxWater - ret.water;
bestIdx = i;
bestJdx = j;
bestAngle = angle;
bestLand = ret.land;
bestWater = ret.water;
}
if (bestVal < 0)
return false;
// if no good place with enough water around and still in first phase, wait for expansion at the next phase
if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1)
return false;
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
// Assign this dock to a base
let baseIndex = gameState.ai.HQ.basesMap.map[bestJdx];
if (!baseIndex)
baseIndex = -2; // We'll do an anchorless base around it
return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand };
};
/**
* Find a good island to build a dock.
*/
PETRA.ConstructionPlan.prototype.buildOverseaDock = function(gameState, template)
{
let docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock"));
if (!docks.hasEntities())
return;
let passabilityMap = gameState.getPassabilityMap();
let cellArea = passabilityMap.cellSize * passabilityMap.cellSize;
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
let land = {};
let found;
for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i)
{
if (gameState.ai.accessibility.regionType[i] != "land" ||
cellArea * gameState.ai.accessibility.regionSize[i] < 3600)
continue;
let keep = true;
for (let dock of docks.values())
{
if (PETRA.getLandAccess(gameState, dock) != i)
continue;
keep = false;
break;
}
if (!keep)
continue;
let sea;
for (let cc of ccEnts.values())
{
let ccAccess = PETRA.getLandAccess(gameState, cc);
if (ccAccess != i)
{
if (cc.owner() == PlayerID && !sea)
sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, ccAccess, i);
continue;
}
// Docks on island where we have a cc are already done elsewhere
if (cc.owner() == PlayerID || gameState.isPlayerEnemy(cc.owner()))
{
keep = false;
break;
}
}
if (!keep || !sea)
continue;
land[i] = true;
found = true;
}
if (!found)
return;
if (!gameState.ai.HQ.navalMap)
API3.warn("petra.findOverseaLand on a non-naval map??? we should never go there ");
let oldTemplate = this.template;
let oldMetadata = this.metadata;
this.template = template;
let pos;
this.metadata = { "land": land, "oversea": true };
pos = this.findDockPosition(gameState);
if (pos)
{
let type = template.templateName();
let builder = gameState.findBuilder(type);
this.metadata.base = pos.base;
// Adjust a bit the position if needed
let cosa = Math.cos(pos.angle);
let sina = Math.sin(pos.angle);
let shiftMax = gameState.ai.HQ.territoryMap.cellSize;
for (let shift = 0; shift <= shiftMax; shift += 2)
{
builder.construct(type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata);
if (shift > 0)
builder.construct(type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata);
}
}
this.template = oldTemplate;
this.metadata = oldMetadata;
};
/** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */
PETRA.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size)
{
let pos = gameState.ai.accessibility.gamePosToMapPos([x, z]);
let k = pos[0] + pos[1]*gameState.ai.accessibility.width;
let seaRef = gameState.ai.accessibility.navalPassMap[k];
if (seaRef < 2)
return false;
const numPoints = 16;
for (let dist = 0; dist < 4; ++dist)
{
let waterPoints = [];
for (let i = 0; i < numPoints; ++i)
{
let angle = 2 * Math.PI * i / numPoints;
pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)];
pos = gameState.ai.accessibility.gamePosToMapPos(pos);
if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width ||
pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height)
continue;
let j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.navalPassMap[j] == seaRef)
waterPoints.push(i);
}
let length = waterPoints.length;
if (!length)
continue;
let consec = [];
for (let i = 0; i < length; ++i)
{
let count = 0;
for (let j = 0; j < length-1; ++j)
{
if ((waterPoints[(i + j) % length]+1) % numPoints == waterPoints[(i + j + 1) % length])
++count;
else
break;
}
consec[i] = count;
}
let start = 0;
let count = 0;
for (let c in consec)
{
if (consec[c] > count)
{
start = c;
count = consec[c];
}
}
// If we've found a shoreline, stop searching
if (count != numPoints-1)
return -((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI;
}
return false;
};
/**
* Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js
* to determine the special dock requirements
* returns {"land": land index for this dock, "water": amount of water around this spot}
*/
PETRA.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle)
{
let sz = halfDepth * Math.sin(angle);
let cz = halfDepth * Math.cos(angle);
// center back position
let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]);
let j = pos[0] + pos[1]*gameState.ai.accessibility.width;
let land = gameState.ai.accessibility.landPassMap[j];
if (land < 2)
return null;
// center front position
pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]);
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2)
return null;
// additional constraints compared to BuildRestriction.js to assure we have enough place to build
let sw = halfWidth * Math.cos(angle) * 3 / 4;
let cw = halfWidth * Math.sin(angle) * 3 / 4;
pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]);
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] != land)
return null;
pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]);
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] != land)
return null;
let water = 0;
let sp = 15 * Math.sin(angle);
let cp = 15 * Math.cos(angle);
for (let i = 1; i < 5; ++i)
{
pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]);
if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width ||
pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height)
break;
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2)
break;
pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]);
if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width ||
pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height)
break;
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2)
break;
pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]);
if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width ||
pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height)
break;
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2)
break;
water += 4;
}
return { "land": land, "water": water };
};
/**
* fast check if we can build a dock: returns false if nearest land is farther than the dock dimension
* if the (object) wantedLand is given, this nearest land should have one of these accessibility
* if wantedSea is given, this tile should be inside this sea
*/
const around = [[ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50],
[-1.0, 0.0], [-0.87, -0.50], [-0.50, -0.87], [ 0.0, -1.0], [ 0.50, -0.87], [ 0.87, -0.50]];
PETRA.ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea)
{
let width = gameState.ai.HQ.territoryMap.width;
let cellSize = gameState.ai.HQ.territoryMap.cellSize;
let dimLand = dimension + 1.5 * cellSize;
let dimSea = dimension + 2 * cellSize;
let accessibility = gameState.ai.accessibility;
let x = (j%width + 0.5) * cellSize;
let z = (Math.floor(j/width) + 0.5) * cellSize;
let pos = accessibility.gamePosToMapPos([x, z]);
let k = pos[0] + pos[1]*accessibility.width;
let landPass = accessibility.landPassMap[k];
if (landPass > 1 && wantedLand && !wantedLand[landPass] ||
landPass < 2 && accessibility.navalPassMap[k] < 2)
return false;
for (let a of around)
{
pos = accessibility.gamePosToMapPos([x + dimLand*a[0], z + dimLand*a[1]]);
if (pos[0] < 0 || pos[0] >= accessibility.width)
continue;
if (pos[1] < 0 || pos[1] >= accessibility.height)
continue;
k = pos[0] + pos[1]*accessibility.width;
landPass = accessibility.landPassMap[k];
if (landPass < 2 || wantedLand && !wantedLand[landPass])
continue;
pos = accessibility.gamePosToMapPos([x - dimSea*a[0], z - dimSea*a[1]]);
if (pos[0] < 0 || pos[0] >= accessibility.width)
continue;
if (pos[1] < 0 || pos[1] >= accessibility.height)
continue;
k = pos[0] + pos[1]*accessibility.width;
if (wantedSea && accessibility.navalPassMap[k] != wantedSea ||
!wantedSea && accessibility.navalPassMap[k] < 2)
continue;
return true;
}
return false;
};
/**
* return a measure of the proximity to our frontier (including our allies)
* 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m
*/
PETRA.ConstructionPlan.prototype.getFrontierProximity = function(gameState, j)
{
let alliedVictory = gameState.getAlliedVictory();
let territoryMap = gameState.ai.HQ.territoryMap;
let territoryOwner = territoryMap.getOwnerIndex(j);
if (territoryOwner == PlayerID || alliedVictory && gameState.isPlayerAlly(territoryOwner))
return 0;
let borderMap = gameState.ai.HQ.borderMap;
let width = territoryMap.width;
let step = Math.round(24 / territoryMap.cellSize);
let ix = j % width;
let iz = Math.floor(j / width);
let best = 5;
for (let a of around)
{
for (let i = 1; i < 5; ++i)
{
let jx = ix + Math.round(i*step*a[0]);
if (jx < 0 || jx >= width)
continue;
let jz = iz + Math.round(i*step*a[1]);
if (jz < 0 || jz >= width)
continue;
if (borderMap.map[jx+width*jz] & PETRA.outside_Mask)
continue;
territoryOwner = territoryMap.getOwnerIndex(jx+width*jz);
if (alliedVictory && gameState.isPlayerAlly(territoryOwner) || territoryOwner == PlayerID)
{
best = Math.min(best, i);
break;
}
}
if (best == 1)
break;
}
return best;
};
/**
* get the sum of the resources (except food) around, inside a given radius
* resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood
*/
PETRA.ConstructionPlan.prototype.getResourcesAround = function(gameState, types, i, radius)
{
let resourceMaps = gameState.sharedScript.resourceMaps;
let w = resourceMaps.wood.width;
let cellSize = resourceMaps.wood.cellSize;
let size = Math.floor(radius / cellSize);
let ix = i % w;
let iy = Math.floor(i / w);
let total = 0;
let nbcell = 0;
for (let k of types)
{
if (k == "food" || !resourceMaps[k])
continue;
let weigh0 = k == "wood" ? 2 : 1;
for (let dy = 0; dy <= size; ++dy)
{
let dxmax = size - dy;
let ky = iy + dy;
if (ky >= 0 && ky < w)
{
for (let dx = -dxmax; dx <= dxmax; ++dx)
{
let kx = ix + dx;
if (kx < 0 || kx >= w)
continue;
let ddx = dx > 0 ? dx : -dx;
let weight = weigh0 * (dxmax - ddx) / size;
total += weight * resourceMaps[k].map[kx + w * ky];
nbcell += weight;
}
}
if (dy == 0)
continue;
ky = iy - dy;
if (ky >= 0 && ky < w)
{
for (let dx = -dxmax; dx <= dxmax; ++dx)
{
let kx = ix + dx;
if (kx < 0 || kx >= w)
continue;
let ddx = dx > 0 ? dx : -dx;
let weight = weigh0 * (dxmax - ddx) / size;
total += weight * resourceMaps[k].map[kx + w * ky];
nbcell += weight;
}
}
}
}
return nbcell ? total / nbcell : 0;
};
PETRA.ConstructionPlan.prototype.isGo = function(gameState)
{
if (this.goRequirement && this.goRequirement == "houseNeeded")
{
if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_house"))
return false;
if (gameState.getPopulationMax() <= gameState.getPopulationLimit())
return false;
let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation();
for (let ent of gameState.getOwnFoundations().values())
{
let template = gameState.getBuiltTemplate(ent.templateName());
if (template)
freeSlots += template.getPopulationBonus();
}
if (gameState.ai.HQ.saveResources)
return freeSlots <= 10;
if (gameState.getPopulation() > 55)
return freeSlots <= 21;
if (gameState.getPopulation() > 30)
return freeSlots <= 15;
return freeSlots <= 10;
}
return true;
};
PETRA.ConstructionPlan.prototype.onStart = function(gameState)
{
if (this.queueToReset)
gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]);
};
PETRA.ConstructionPlan.prototype.Serialize = function()
{
return {
"category": this.category,
"type": this.type,
"ID": this.ID,
"metadata": this.metadata,
"cost": this.cost.Serialize(),
"number": this.number,
"position": this.position,
"goRequirement": this.goRequirement || undefined,
"queueToReset": this.queueToReset || undefined
};
};
PETRA.ConstructionPlan.prototype.Deserialize = function(gameState, data)
{
for (let key in data)
this[key] = data[key];
this.cost = new API3.Resources();
this.cost.Deserialize(data.cost);
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/researchManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/researchManager.js (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/researchManager.js (revision 23865)
@@ -1,237 +1,237 @@
/**
* Manage the research
*/
PETRA.ResearchManager = function(Config)
{
this.Config = Config;
};
/**
* Check if we can go to the next phase
*/
PETRA.ResearchManager.prototype.checkPhase = function(gameState, queues)
{
if (queues.majorTech.hasQueuedUnits())
return;
// Don't try to phase up if already trying to gather resources for a civil-centre or wonder
if (queues.civilCentre.hasQueuedUnits() || queues.wonder.hasQueuedUnits())
return;
let currentPhaseIndex = gameState.currentPhase();
let nextPhaseName = gameState.getPhaseName(currentPhaseIndex+1);
if (!nextPhaseName)
return;
let petraRequirements =
currentPhaseIndex == 1 && gameState.ai.HQ.getAccountedPopulation(gameState) >= this.Config.Economy.popPhase2 ||
currentPhaseIndex == 2 && gameState.ai.HQ.getAccountedWorkers(gameState) > this.Config.Economy.workPhase3 ||
currentPhaseIndex >= 3 && gameState.ai.HQ.getAccountedWorkers(gameState) > this.Config.Economy.workPhase4;
if (petraRequirements && gameState.hasResearchers(nextPhaseName, true))
{
gameState.ai.HQ.phasing = currentPhaseIndex + 1;
// Reset the queue priority in case it was changed during a previous phase update
gameState.ai.queueManager.changePriority("majorTech", gameState.ai.Config.priorities.majorTech);
queues.majorTech.addPlan(new PETRA.ResearchPlan(gameState, nextPhaseName, true));
}
};
PETRA.ResearchManager.prototype.researchPopulationBonus = function(gameState, queues)
{
if (queues.minorTech.hasQueuedUnits())
return;
let techs = gameState.findAvailableTech();
for (let tech of techs)
{
if (!tech[1]._template.modifications)
continue;
// TODO may-be loop on all modifs and check if the effect if positive ?
if (tech[1]._template.modifications[0].value !== "Cost/PopulationBonus")
continue;
queues.minorTech.addPlan(new PETRA.ResearchPlan(gameState, tech[0]));
break;
}
};
PETRA.ResearchManager.prototype.researchTradeBonus = function(gameState, queues)
{
if (queues.minorTech.hasQueuedUnits())
return;
let techs = gameState.findAvailableTech();
for (let tech of techs)
{
if (!tech[1]._template.modifications || !tech[1]._template.affects)
continue;
if (tech[1]._template.affects.indexOf("Trader") === -1)
continue;
// TODO may-be loop on all modifs and check if the effect if positive ?
if (tech[1]._template.modifications[0].value !== "UnitMotion/WalkSpeed" &&
tech[1]._template.modifications[0].value !== "Trader/GainMultiplier")
continue;
queues.minorTech.addPlan(new PETRA.ResearchPlan(gameState, tech[0]));
break;
}
};
/** Techs to be searched for as soon as they are available */
PETRA.ResearchManager.prototype.researchWantedTechs = function(gameState, techs)
{
let phase1 = gameState.currentPhase() === 1;
let available = phase1 ? gameState.ai.queueManager.getAvailableResources(gameState) : null;
let numWorkers = phase1 ? gameState.getOwnEntitiesByRole("worker", true).length : 0;
for (let tech of techs)
{
if (!tech[1]._template.modifications)
continue;
let template = tech[1]._template;
if (phase1)
{
let cost = template.cost;
let costMax = 0;
for (let res in cost)
costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0));
if (10*numWorkers < costMax)
continue;
}
for (let i in template.modifications)
{
if (gameState.ai.HQ.navalMap && template.modifications[i].value === "ResourceGatherer/Rates/food.fish")
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
else if (template.modifications[i].value === "ResourceGatherer/Rates/food.fruit")
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
else if (template.modifications[i].value === "ResourceGatherer/Rates/food.grain")
return { "name": tech[0], "increasePriority": false };
else if (template.modifications[i].value === "ResourceGatherer/Rates/wood.tree")
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
else if (template.modifications[i].value.startsWith("ResourceGatherer/Capacities"))
return { "name": tech[0], "increasePriority": false };
else if (template.modifications[i].value === "Attack/Ranged/MaxRange")
return { "name": tech[0], "increasePriority": false };
}
}
return null;
};
/** Techs to be searched for as soon as they are available, but only after phase 2 */
PETRA.ResearchManager.prototype.researchPreferredTechs = function(gameState, techs)
{
let phase2 = gameState.currentPhase() === 2;
let available = phase2 ? gameState.ai.queueManager.getAvailableResources(gameState) : null;
let numWorkers = phase2 ? gameState.getOwnEntitiesByRole("worker", true).length : 0;
for (let tech of techs)
{
if (!tech[1]._template.modifications)
continue;
let template = tech[1]._template;
if (phase2)
{
let cost = template.cost;
let costMax = 0;
for (let res in cost)
costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0));
if (10*numWorkers < costMax)
continue;
}
for (let i in template.modifications)
{
if (template.modifications[i].value === "ResourceGatherer/Rates/stone.rock")
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
else if (template.modifications[i].value === "ResourceGatherer/Rates/metal.ore")
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
else if (template.modifications[i].value === "BuildingAI/DefaultArrowCount")
return { "name": tech[0], "increasePriority": this.CostSum(template.cost) < 400 };
else if (template.modifications[i].value === "Health/RegenRate")
return { "name": tech[0], "increasePriority": false };
else if (template.modifications[i].value === "Health/IdleRegenRate")
return { "name": tech[0], "increasePriority": false };
}
}
return null;
};
PETRA.ResearchManager.prototype.update = function(gameState, queues)
{
if (queues.minorTech.hasQueuedUnits() || queues.majorTech.hasQueuedUnits())
return;
let techs = gameState.findAvailableTech();
let techName = this.researchWantedTechs(gameState, techs);
if (techName)
{
if (techName.increasePriority)
{
gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech);
let plan = new PETRA.ResearchPlan(gameState, techName.name);
plan.queueToReset = "minorTech";
queues.minorTech.addPlan(plan);
}
else
queues.minorTech.addPlan(new PETRA.ResearchPlan(gameState, techName.name));
return;
}
if (gameState.currentPhase() < 2)
return;
techName = this.researchPreferredTechs(gameState, techs);
if (techName)
{
if (techName.increasePriority)
{
gameState.ai.queueManager.changePriority("minorTech", 2*this.Config.priorities.minorTech);
let plan = new PETRA.ResearchPlan(gameState, techName.name);
plan.queueToReset = "minorTech";
queues.minorTech.addPlan(plan);
}
else
queues.minorTech.addPlan(new PETRA.ResearchPlan(gameState, techName.name));
return;
}
if (gameState.currentPhase() < 3)
return;
// remove some techs not yet used by this AI
// remove also sharedLos if we have no ally
for (let i = 0; i < techs.length; ++i)
{
let template = techs[i][1]._template;
if (template.affects && template.affects.length === 1 &&
- (template.affects[0] === "Healer" || template.affects[0] === "Outpost" || template.affects[0] === "StoneWall"))
+ (template.affects[0] === "Healer" || template.affects[0] === "Outpost" || template.affects[0] === "Wall"))
{
techs.splice(i--, 1);
continue;
}
if (template.modifications && template.modifications.length === 1 &&
template.modifications[0].value === "Player/sharedLos" &&
!gameState.hasAllies())
{
techs.splice(i--, 1);
continue;
}
}
if (!techs.length)
return;
// randomly pick one. No worries about pairs in that case.
queues.minorTech.addPlan(new PETRA.ResearchPlan(gameState, pickRandom(techs)[0]));
};
PETRA.ResearchManager.prototype.CostSum = function(cost)
{
let costSum = 0;
for (let res in cost)
costSum += cost[res];
return costSum;
};
PETRA.ResearchManager.prototype.Serialize = function()
{
return {};
};
PETRA.ResearchManager.prototype.Deserialize = function(data)
{
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/tradeManager.js (revision 23865)
@@ -1,732 +1,732 @@
/**
* Manage the trade
*/
PETRA.TradeManager = function(Config)
{
this.Config = Config;
this.tradeRoute = undefined;
this.potentialTradeRoute = undefined;
this.routeProspection = false;
this.targetNumTraders = this.Config.Economy.targetNumTraders;
this.warnedAllies = {};
};
PETRA.TradeManager.prototype.init = function(gameState)
{
this.traders = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "role", "trader"));
this.traders.registerUpdates();
this.minimalGain = gameState.ai.HQ.navalMap ? 3 : 5;
};
PETRA.TradeManager.prototype.hasTradeRoute = function()
{
return this.tradeRoute !== undefined;
};
PETRA.TradeManager.prototype.assignTrader = function(ent)
{
ent.setMetadata(PlayerID, "role", "trader");
this.traders.updateEnt(ent);
};
PETRA.TradeManager.prototype.trainMoreTraders = function(gameState, queues)
{
if (!this.hasTradeRoute() || queues.trader.hasQueuedUnits())
return;
let numTraders = this.traders.length;
let numSeaTraders = this.traders.filter(API3.Filters.byClass("Ship")).length;
let numLandTraders = numTraders - numSeaTraders;
// add traders already in training
gameState.getOwnTrainingFacilities().forEach(function(ent) {
for (let item of ent.trainingQueue())
{
if (!item.metadata || !item.metadata.role || item.metadata.role != "trader")
continue;
numTraders += item.count;
if (item.metadata.sea !== undefined)
numSeaTraders += item.count;
else
numLandTraders += item.count;
}
});
if (numTraders >= this.targetNumTraders &&
(!this.tradeRoute.sea && numLandTraders >= Math.floor(this.targetNumTraders/2) ||
this.tradeRoute.sea && numSeaTraders >= Math.floor(this.targetNumTraders/2)))
return;
let template;
let metadata = { "role": "trader" };
if (this.tradeRoute.sea)
{
// if we have some merchand ships assigned to transport, try first to reassign them
// May-be, there were produced at an early stage when no other ship were available
// and the naval manager will train now more appropriate ships.
let already = false;
let shipToSwitch;
gameState.ai.HQ.navalManager.seaTransportShips[this.tradeRoute.sea].forEach(function(ship) {
if (already || !ship.hasClass("Trader"))
return;
if (ship.getMetadata(PlayerID, "role") == "switchToTrader")
{
already = true;
return;
}
shipToSwitch = ship;
});
if (already)
return;
if (shipToSwitch)
{
if (shipToSwitch.getMetadata(PlayerID, "transporter") === undefined)
shipToSwitch.setMetadata(PlayerID, "role", "trader");
else
shipToSwitch.setMetadata(PlayerID, "role", "switchToTrader");
return;
}
template = gameState.applyCiv("units/{civ}_ship_merchant");
metadata.sea = this.tradeRoute.sea;
}
else
{
template = gameState.applyCiv("units/{civ}_support_trader");
- if (!this.tradeRoute.source.hasClass("NavalMarket"))
+ if (!this.tradeRoute.source.hasClass("Naval"))
metadata.base = this.tradeRoute.source.getMetadata(PlayerID, "base");
else
metadata.base = this.tradeRoute.target.getMetadata(PlayerID, "base");
}
if (!gameState.getTemplate(template))
{
if (this.Config.debug > 0)
API3.warn("Petra error: trying to train " + template + " for civ " +
gameState.getPlayerCiv() + " but no template found.");
return;
}
queues.trader.addPlan(new PETRA.TrainingPlan(gameState, template, metadata, 1, 1));
};
PETRA.TradeManager.prototype.updateTrader = function(gameState, ent)
{
if (ent.hasClass("Ship") && gameState.ai.playedTurn % 5 == 0 &&
!ent.unitAIState().startsWith("INDIVIDUAL.GATHER") &&
PETRA.gatherTreasure(gameState, ent, true))
return;
if (!this.hasTradeRoute() || !ent.isIdle() || !ent.position())
return;
if (ent.getMetadata(PlayerID, "transport") !== undefined)
return;
// TODO if the trader is idle and has workOrders, restore them to avoid losing the current gain
Engine.ProfileStart("Trade Manager");
let access = ent.hasClass("Ship") ? PETRA.getSeaAccess(gameState, ent) : PETRA.getLandAccess(gameState, ent);
let route = this.checkRoutes(gameState, access);
if (!route)
{
// TODO try to garrison land trader inside merchant ship when only sea routes available
if (this.Config.debug > 0)
API3.warn(" no available route for " + ent.genericName() + " " + ent.id());
Engine.ProfileStop();
return;
}
let nearerSource = true;
if (API3.SquareVectorDistance(route.target.position(), ent.position()) < API3.SquareVectorDistance(route.source.position(), ent.position()))
nearerSource = false;
if (!ent.hasClass("Ship") && route.land != access)
{
if (nearerSource)
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.source.position());
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, route.land, route.target.position());
Engine.ProfileStop();
return;
}
if (nearerSource)
ent.tradeRoute(route.target, route.source);
else
ent.tradeRoute(route.source, route.target);
ent.setMetadata(PlayerID, "route", this.routeEntToId(route));
Engine.ProfileStop();
};
PETRA.TradeManager.prototype.setTradingGoods = function(gameState)
{
let resTradeCodes = Resources.GetTradableCodes();
if (!resTradeCodes.length)
return;
let tradingGoods = {};
for (let res of resTradeCodes)
tradingGoods[res] = 0;
// first, try to anticipate future needs
let stocks = gameState.ai.HQ.getTotalResourceLevel(gameState);
let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState, resTradeCodes);
let wantedRates = gameState.ai.HQ.GetWantedGatherRates(gameState);
let remaining = 100;
let targetNum = this.Config.Economy.targetNumTraders;
for (let res of resTradeCodes)
{
if (res == "food")
continue;
let wantedRate = wantedRates[res];
if (stocks[res] < 200)
{
tradingGoods[res] = wantedRate > 0 ? 20 : 10;
targetNum += Math.min(5, 3 + Math.ceil(wantedRate/30));
}
else if (stocks[res] < 500)
{
tradingGoods[res] = wantedRate > 0 ? 15 : 10;
targetNum += 2;
}
else if (stocks[res] < 1000)
{
tradingGoods[res] = 10;
targetNum += 1;
}
remaining -= tradingGoods[res];
}
this.targetNumTraders = Math.round(this.Config.popScaling * targetNum);
// then add what is needed now
let mainNeed = Math.floor(remaining * 70 / 100);
let nextNeed = remaining - mainNeed;
tradingGoods[mostNeeded[0].type] += mainNeed;
if (mostNeeded[1] && mostNeeded[1].wanted > 0)
tradingGoods[mostNeeded[1].type] += nextNeed;
else
tradingGoods[mostNeeded[0].type] += nextNeed;
Engine.PostCommand(PlayerID, { "type": "set-trading-goods", "tradingGoods": tradingGoods });
if (this.Config.debug > 2)
API3.warn(" trading goods set to " + uneval(tradingGoods));
};
/**
* Try to barter unneeded resources for needed resources.
* only once per turn because the info is not updated within a turn
*/
PETRA.TradeManager.prototype.performBarter = function(gameState)
{
- let barterers = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).toEntityArray();
+ let barterers = gameState.getOwnEntitiesByClass("Barter", true).filter(API3.Filters.isBuilt()).toEntityArray();
if (barterers.length == 0)
return false;
let resBarterCodes = Resources.GetBarterableCodes();
if (!resBarterCodes.length)
return false;
// Available resources after account substraction
let available = gameState.ai.queueManager.getAvailableResources(gameState);
let needs = gameState.ai.queueManager.currentNeeds(gameState);
let rates = gameState.ai.HQ.GetCurrentGatherRates(gameState);
let barterPrices = gameState.getBarterPrices();
// calculates conversion rates
let getBarterRate = (prices, buy, sell) => Math.round(100 * prices.sell[sell] / prices.buy[buy]);
// loop through each missing resource checking if we could barter and help finishing a queue quickly.
for (let buy of resBarterCodes)
{
// Check if our rate allows to gather it fast enough
if (needs[buy] == 0 || needs[buy] < rates[buy] * 30)
continue;
// Pick the best resource to barter.
let bestToSell;
let bestRate = 0;
for (let sell of resBarterCodes)
{
if (sell == buy)
continue;
// Do not sell if we need it or do not have enough buffer
if (needs[sell] > 0 || available[sell] < 500)
continue;
let barterRateMin;
if (sell == "food")
{
barterRateMin = 30;
if (available[sell] > 40000)
barterRateMin = 0;
else if (available[sell] > 15000)
barterRateMin = 5;
else if (available[sell] > 1000)
barterRateMin = 10;
}
else
{
barterRateMin = 70;
if (available[sell] > 5000)
barterRateMin = 30;
else if (available[sell] > 1000)
barterRateMin = 50;
if (buy == "food")
barterRateMin += 20;
}
let barterRate = getBarterRate(barterPrices, buy, sell);
if (barterRate > bestRate && barterRate > barterRateMin)
{
bestRate = barterRate;
bestToSell = sell;
}
}
if (bestToSell !== undefined)
{
let amount = available[bestToSell] > 5000 ? 500 : 100;
barterers[0].barter(buy, bestToSell, amount);
if (this.Config.debug > 2)
API3.warn("Necessity bartering: sold " + bestToSell +" for " + buy +
" >> need sell " + needs[bestToSell] + " need buy " + needs[buy] +
" rate buy " + rates[buy] + " available sell " + available[bestToSell] +
" available buy " + available[buy] + " barterRate " + bestRate +
" amount " + amount);
return true;
}
}
// now do contingency bartering, selling food to buy finite resources (and annoy our ennemies by increasing prices)
if (available.food < 1000 || needs.food > 0 || resBarterCodes.indexOf("food") == -1)
return false;
let bestToBuy;
let bestChoice = 0;
for (let buy of resBarterCodes)
{
if (buy == "food")
continue;
let barterRateMin = 80;
if (available[buy] < 5000 && available.food > 5000)
barterRateMin -= 20 - Math.floor(available[buy]/250);
let barterRate = getBarterRate(barterPrices, buy, "food");
if (barterRate < barterRateMin)
continue;
let choice = barterRate / (100 + available[buy]);
if (choice > bestChoice)
{
bestChoice = choice;
bestToBuy = buy;
}
}
if (bestToBuy !== undefined)
{
let amount = available.food > 5000 ? 500 : 100;
barterers[0].barter(bestToBuy, "food", amount);
if (this.Config.debug > 2)
API3.warn("Contingency bartering: sold food for " + bestToBuy +
" available sell " + available.food + " available buy " + available[bestToBuy] +
" barterRate " + getBarterRate(barterPrices, bestToBuy, "food") +
" amount " + amount);
return true;
}
return false;
};
PETRA.TradeManager.prototype.checkEvents = function(gameState, events)
{
// check if one market from a traderoute is renamed, change the route accordingly
for (let evt of events.EntityRenamed)
{
let ent = gameState.getEntityById(evt.newentity);
- if (!ent || !ent.hasClass("Market"))
+ if (!ent || !ent.hasClass("Trade"))
continue;
for (let trader of this.traders.values())
{
let route = trader.getMetadata(PlayerID, "route");
if (!route)
continue;
if (route.source == evt.entity)
route.source = evt.newentity;
else if (route.target == evt.entity)
route.target = evt.newentity;
else
continue;
trader.setMetadata(PlayerID, "route", route);
}
}
// if one market (or market-foundation) is destroyed, we should look for a better route
for (let evt of events.Destroy)
{
if (!evt.entityObj)
continue;
let ent = evt.entityObj;
- if (!ent || !ent.hasClass("Market") || !gameState.isPlayerAlly(ent.owner()))
+ if (!ent || !ent.hasClass("Trade") || !gameState.isPlayerAlly(ent.owner()))
continue;
this.activateProspection(gameState);
return true;
}
// same thing if one market is built
for (let evt of events.Create)
{
let ent = gameState.getEntityById(evt.entity);
- if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market") ||
+ if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Trade") ||
!gameState.isPlayerAlly(ent.owner()))
continue;
this.activateProspection(gameState);
return true;
}
// and same thing for captured markets
for (let evt of events.OwnershipChanged)
{
if (!gameState.isPlayerAlly(evt.from) && !gameState.isPlayerAlly(evt.to))
continue;
let ent = gameState.getEntityById(evt.entity);
- if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Market"))
+ if (!ent || ent.foundationProgress() !== undefined || !ent.hasClass("Trade"))
continue;
this.activateProspection(gameState);
return true;
}
// or if diplomacy changed
if (events.DiplomacyChanged.length)
{
this.activateProspection(gameState);
return true;
}
return false;
};
PETRA.TradeManager.prototype.activateProspection = function(gameState)
{
this.routeProspection = true;
gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_market"));
gameState.ai.HQ.buildManager.setBuildable(gameState.applyCiv("structures/{civ}_dock"));
};
/**
* fills the best trade route in this.tradeRoute and the best potential route in this.potentialTradeRoute
* If an index is given, it returns the best route with this index or the best land route if index is a land index
*/
PETRA.TradeManager.prototype.checkRoutes = function(gameState, accessIndex)
{
// If we cannot trade, do not bother checking routes.
if (!Resources.GetTradableCodes().length)
{
this.tradeRoute = undefined;
this.potentialTradeRoute = undefined;
return false;
}
- let market1 = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures());
- let market2 = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities());
+ let market1 = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Trade"), gameState.getOwnStructures());
+ let market2 = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Trade"), gameState.getExclusiveAllyEntities());
if (market1.length + market2.length < 2) // We have to wait ... markets will be built soon
{
this.tradeRoute = undefined;
this.potentialTradeRoute = undefined;
return false;
}
let onlyOurs = !market2.hasEntities();
if (onlyOurs)
market2 = market1;
let candidate = { "gain": 0 };
let potential = { "gain": 0 };
let bestIndex = { "gain": 0 };
let bestLand = { "gain": 0 };
let mapSize = gameState.sharedScript.mapSize;
let traderTemplatesGains = gameState.getTraderTemplatesGains();
for (let m1 of market1.values())
{
if (!m1.position())
continue;
let access1 = PETRA.getLandAccess(gameState, m1);
- let sea1 = m1.hasClass("NavalMarket") ? PETRA.getSeaAccess(gameState, m1) : undefined;
+ let sea1 = m1.hasClass("Naval") ? PETRA.getSeaAccess(gameState, m1) : undefined;
for (let m2 of market2.values())
{
if (onlyOurs && m1.id() >= m2.id())
continue;
if (!m2.position())
continue;
let access2 = PETRA.getLandAccess(gameState, m2);
- let sea2 = m2.hasClass("NavalMarket") ? PETRA.getSeaAccess(gameState, m2) : undefined;
+ let sea2 = m2.hasClass("Naval") ? PETRA.getSeaAccess(gameState, m2) : undefined;
let land = access1 == access2 ? access1 : undefined;
let sea = sea1 && sea1 == sea2 ? sea1 : undefined;
if (!land && !sea)
continue;
if (land && PETRA.isLineInsideEnemyTerritory(gameState, m1.position(), m2.position()))
continue;
let gainMultiplier;
if (land && traderTemplatesGains.landGainMultiplier)
gainMultiplier = traderTemplatesGains.landGainMultiplier;
else if (sea && traderTemplatesGains.navalGainMultiplier)
gainMultiplier = traderTemplatesGains.navalGainMultiplier;
else
continue;
let gain = Math.round(gainMultiplier * TradeGain(API3.SquareVectorDistance(m1.position(), m2.position()), mapSize));
if (gain < this.minimalGain)
continue;
if (m1.foundationProgress() === undefined && m2.foundationProgress() === undefined)
{
if (accessIndex)
{
if (gameState.ai.accessibility.regionType[accessIndex] == "water" && sea == accessIndex)
{
if (gain < bestIndex.gain)
continue;
bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea };
}
else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && land == accessIndex)
{
if (gain < bestIndex.gain)
continue;
bestIndex = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea };
}
else if (gameState.ai.accessibility.regionType[accessIndex] == "land")
{
if (gain < bestLand.gain)
continue;
bestLand = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea };
}
}
if (gain < candidate.gain)
continue;
candidate = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea };
}
if (gain < potential.gain)
continue;
potential = { "source": m1, "target": m2, "gain": gain, "land": land, "sea": sea };
}
}
if (potential.gain < 1)
this.potentialTradeRoute = undefined;
else
this.potentialTradeRoute = potential;
if (candidate.gain < 1)
{
if (this.Config.debug > 2)
API3.warn("no better trade route possible");
this.tradeRoute = undefined;
return false;
}
if (this.Config.debug > 1 && this.tradeRoute)
{
if (candidate.gain > this.tradeRoute.gain)
API3.warn("one better trade route set with gain " + candidate.gain + " instead of " + this.tradeRoute.gain);
}
else if (this.Config.debug > 1)
API3.warn("one trade route set with gain " + candidate.gain);
this.tradeRoute = candidate;
if (this.Config.chat)
{
let owner = this.tradeRoute.source.owner();
if (owner == PlayerID)
owner = this.tradeRoute.target.owner();
if (owner != PlayerID && !this.warnedAllies[owner])
{ // Warn an ally that we have a trade route with him
PETRA.chatNewTradeRoute(gameState, owner);
this.warnedAllies[owner] = true;
}
}
if (accessIndex)
{
if (bestIndex.gain > 0)
return bestIndex;
else if (gameState.ai.accessibility.regionType[accessIndex] == "land" && bestLand.gain > 0)
return bestLand;
return false;
}
return true;
};
/** Called when a market was built or destroyed, and checks if trader orders should be changed */
PETRA.TradeManager.prototype.checkTrader = function(gameState, ent)
{
let presentRoute = ent.getMetadata(PlayerID, "route");
if (!presentRoute)
return;
if (!ent.position())
{
// This trader is garrisoned, we will decide later (when ungarrisoning) what to do
ent.setMetadata(PlayerID, "route", undefined);
return;
}
let access = ent.hasClass("Ship") ? PETRA.getSeaAccess(gameState, ent) : PETRA.getLandAccess(gameState, ent);
let possibleRoute = this.checkRoutes(gameState, access);
// Warning: presentRoute is from metadata, so contains entity ids
if (!possibleRoute ||
possibleRoute.source.id() != presentRoute.source && possibleRoute.source.id() != presentRoute.target ||
possibleRoute.target.id() != presentRoute.source && possibleRoute.target.id() != presentRoute.target)
{
// Trader will be assigned in updateTrader
ent.setMetadata(PlayerID, "route", undefined);
if (!possibleRoute && !ent.hasClass("Ship"))
{
let closestBase = PETRA.getBestBase(gameState, ent, true);
if (closestBase.accessIndex == access)
{
let closestBasePos = closestBase.anchor.position();
ent.moveToRange(closestBasePos[0], closestBasePos[1], 0, 15);
return;
}
}
ent.stopMoving();
}
};
PETRA.TradeManager.prototype.prospectForNewMarket = function(gameState, queues)
{
- if (queues.economicBuilding.hasQueuedUnitsWithClass("Market") || queues.dock.hasQueuedUnitsWithClass("Market"))
+ if (queues.economicBuilding.hasQueuedUnitsWithClass("Trade") || queues.dock.hasQueuedUnitsWithClass("Trade"))
return;
if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_market"))
return;
- if (!gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()).hasEntities() &&
- !gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()).hasEntities())
+ if (!gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Trade"), gameState.getOwnStructures()).hasEntities() &&
+ !gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Trade"), gameState.getExclusiveAllyEntities()).hasEntities())
return;
let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_market"));
if (!template)
return;
this.checkRoutes(gameState);
let marketPos = gameState.ai.HQ.findMarketLocation(gameState, template);
if (!marketPos || marketPos[3] == 0) // marketPos[3] is the expected gain
{ // no position found
- if (gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities())
+ if (gameState.getOwnEntitiesByClass("Market", true).hasEntities())
gameState.ai.HQ.buildManager.setUnbuildable(gameState, gameState.applyCiv("structures/{civ}_market"));
else
this.routeProspection = false;
return;
}
this.routeProspection = false;
if (!this.isNewMarketWorth(marketPos[3]))
return; // position found, but not enough gain compared to our present route
if (this.Config.debug > 1)
{
if (this.potentialTradeRoute)
API3.warn("turn " + gameState.ai.playedTurn + "we could have a new route with gain " +
marketPos[3] + " instead of the present " + this.potentialTradeRoute.gain);
else
API3.warn("turn " + gameState.ai.playedTurn + "we could have a first route with gain " +
marketPos[3]);
}
if (!this.tradeRoute)
- gameState.ai.queueManager.changePriority("economicBuilding", 2*this.Config.priorities.economicBuilding);
+ gameState.ai.queueManager.changePriority("economicBuilding", 2 * this.Config.priorities.economicBuilding);
let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}_market");
if (!this.tradeRoute)
plan.queueToReset = "economicBuilding";
queues.economicBuilding.addPlan(plan);
};
PETRA.TradeManager.prototype.isNewMarketWorth = function(expectedGain)
{
if (!Resources.GetTradableCodes().length)
return false;
if (expectedGain < this.minimalGain)
return false;
if (this.potentialTradeRoute && expectedGain < 2*this.potentialTradeRoute.gain &&
expectedGain < this.potentialTradeRoute.gain + 20)
return false;
return true;
};
PETRA.TradeManager.prototype.update = function(gameState, events, queues)
{
if (gameState.ai.HQ.canBarter && Resources.GetBarterableCodes().length)
this.performBarter(gameState);
if (this.Config.difficulty <= 1)
return;
if (this.checkEvents(gameState, events)) // true if one market was built or destroyed
{
this.traders.forEach(ent => { this.checkTrader(gameState, ent); });
this.checkRoutes(gameState);
}
if (this.tradeRoute)
{
this.traders.forEach(ent => { this.updateTrader(gameState, ent); });
if (gameState.ai.playedTurn % 5 == 0)
this.trainMoreTraders(gameState, queues);
if (gameState.ai.playedTurn % 20 == 0 && this.traders.length >= 2)
gameState.ai.HQ.researchManager.researchTradeBonus(gameState, queues);
if (gameState.ai.playedTurn % 60 == 0)
this.setTradingGoods(gameState);
}
if (this.routeProspection)
this.prospectForNewMarket(gameState, queues);
};
PETRA.TradeManager.prototype.routeEntToId = function(route)
{
if (!route)
return undefined;
let ret = {};
for (let key in route)
{
if (key == "source" || key == "target")
{
if (!route[key])
return undefined;
ret[key] = route[key].id();
}
else
ret[key] = route[key];
}
return ret;
};
PETRA.TradeManager.prototype.routeIdToEnt = function(gameState, route)
{
if (!route)
return undefined;
let ret = {};
for (let key in route)
{
if (key == "source" || key == "target")
{
ret[key] = gameState.getEntityById(route[key]);
if (!ret[key])
return undefined;
}
else
ret[key] = route[key];
}
return ret;
};
PETRA.TradeManager.prototype.Serialize = function()
{
return {
"tradeRoute": this.routeEntToId(this.tradeRoute),
"potentialTradeRoute": this.routeEntToId(this.potentialTradeRoute),
"routeProspection": this.routeProspection,
"targetNumTraders": this.targetNumTraders,
"warnedAllies": this.warnedAllies
};
};
PETRA.TradeManager.prototype.Deserialize = function(gameState, data)
{
for (let key in data)
{
if (key == "tradeRoute" || key == "potentialTradeRoute")
this[key] = this.routeIdToEnt(gameState, data[key]);
else
this[key] = data[key];
}
};
Index: ps/trunk/binaries/data/mods/public/simulation/components/Barter.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Barter.js (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Barter.js (revision 23865)
@@ -1,158 +1,158 @@
function Barter() {}
Barter.prototype.Schema =
"";
/**
* The "true price" is a base price of 100 units of resource (for the case of some resources being of more worth than others).
* With current bartering system only relative values makes sense so if for example stone is two times more expensive than wood,
* there will 2:1 exchange rate.
*
* Constant part of price percentage difference between true price and buy/sell price.
* Buy price equal to true price plus constant difference.
* Sell price equal to true price minus constant difference.
*/
Barter.prototype.CONSTANT_DIFFERENCE = 10;
/**
* Additional difference of prices in percents, added after each deal to specified resource price.
*/
Barter.prototype.DIFFERENCE_PER_DEAL = 2;
/**
* Price difference percentage which restored each restore timer tick
*/
Barter.prototype.DIFFERENCE_RESTORE = 0.5;
/**
* Interval of timer which slowly restore prices after deals
*/
Barter.prototype.RESTORE_TIMER_INTERVAL = 5000;
Barter.prototype.Init = function()
{
this.priceDifferences = {};
for (let resource of Resources.GetBarterableCodes())
this.priceDifferences[resource] = 0;
this.restoreTimer = undefined;
};
Barter.prototype.GetPrices = function(playerID)
{
let prices = { "buy": {}, "sell": {} };
let multiplier = QueryPlayerIDInterface(playerID).GetBarterMultiplier();
for (let resource of Resources.GetBarterableCodes())
{
let truePrice = Resources.GetResource(resource).truePrice;
prices.buy[resource] = truePrice * (100 + this.CONSTANT_DIFFERENCE + this.priceDifferences[resource]) * multiplier.buy[resource] / 100;
prices.sell[resource] = truePrice * (100 - this.CONSTANT_DIFFERENCE + this.priceDifferences[resource]) * multiplier.sell[resource] / 100;
}
return prices;
};
Barter.prototype.PlayerHasMarket = function(playerID)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var entities = cmpRangeManager.GetEntitiesByPlayer(playerID);
for (var entity of entities)
{
var cmpFoundation = Engine.QueryInterface(entity, IID_Foundation);
var cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
- if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
+ if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter"))
return true;
}
return false;
};
Barter.prototype.ExchangeResources = function(playerID, resourceToSell, resourceToBuy, amount)
{
if (amount <= 0)
{
warn("ExchangeResources: incorrect amount: " + uneval(amount));
return;
}
let availResources = Resources.GetBarterableCodes();
if (availResources.indexOf(resourceToSell) == -1)
{
warn("ExchangeResources: incorrect resource to sell: " + uneval(resourceToSell));
return;
}
if (availResources.indexOf(resourceToBuy) == -1)
{
warn("ExchangeResources: incorrect resource to buy: " + uneval(resourceToBuy));
return;
}
// This can occur when the player issues the order just before the market is destroyed or captured
if (!this.PlayerHasMarket(playerID))
return;
if (amount != 100 && amount != 500)
return;
var cmpPlayer = QueryPlayerIDInterface(playerID);
var prices = this.GetPrices(playerID);
var amountsToSubtract = {};
amountsToSubtract[resourceToSell] = amount;
if (cmpPlayer.TrySubtractResources(amountsToSubtract))
{
var amountToAdd = Math.round(prices["sell"][resourceToSell] / prices["buy"][resourceToBuy] * amount);
cmpPlayer.AddResource(resourceToBuy, amountToAdd);
// Display chat message to observers
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
if (cmpGUIInterface)
cmpGUIInterface.PushNotification({
"type": "barter",
"players": [playerID],
"amountsSold": amount,
"amountsBought": amountToAdd,
"resourceSold": resourceToSell,
"resourceBought": resourceToBuy
});
var cmpStatisticsTracker = QueryPlayerIDInterface(playerID, IID_StatisticsTracker);
if (cmpStatisticsTracker)
{
cmpStatisticsTracker.IncreaseResourcesSoldCounter(resourceToSell, amount);
cmpStatisticsTracker.IncreaseResourcesBoughtCounter(resourceToBuy, amountToAdd);
}
let difference = this.DIFFERENCE_PER_DEAL * amount / 100;
// Increase price difference for both exchange resources.
// Overall price difference (dynamic +/- constant) can't exceed +-99%.
this.priceDifferences[resourceToSell] -= difference;
this.priceDifferences[resourceToSell] = Math.min(99 - this.CONSTANT_DIFFERENCE, Math.max(this.CONSTANT_DIFFERENCE - 99, this.priceDifferences[resourceToSell]));
this.priceDifferences[resourceToBuy] += difference;
this.priceDifferences[resourceToBuy] = Math.min(99 - this.CONSTANT_DIFFERENCE, Math.max(this.CONSTANT_DIFFERENCE - 99, this.priceDifferences[resourceToBuy]));
}
if (this.restoreTimer === undefined)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.restoreTimer = cmpTimer.SetInterval(this.entity, IID_Barter, "ProgressTimeout", this.RESTORE_TIMER_INTERVAL, this.RESTORE_TIMER_INTERVAL, {});
}
};
Barter.prototype.ProgressTimeout = function(data)
{
let needRestore = false;
for (let resource of Resources.GetBarterableCodes())
{
// Calculate value to restore, it should be limited to [-DIFFERENCE_RESTORE; DIFFERENCE_RESTORE] interval
let differenceRestore = Math.min(this.DIFFERENCE_RESTORE, Math.max(-this.DIFFERENCE_RESTORE, this.priceDifferences[resource]));
differenceRestore = -differenceRestore;
this.priceDifferences[resource] += differenceRestore;
// If price difference still exists then set flag to run timer again
if (this.priceDifferences[resource] != 0)
needRestore = true;
}
if (!needRestore)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.restoreTimer);
this.restoreTimer = undefined;
}
};
Engine.RegisterSystemComponentType(IID_Barter, "Barter", Barter);
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 23865)
@@ -1,2044 +1,2044 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronized for the biggest part,
// so most of the attributes shouldn't be serialized.
// Return an object with a small selection of deterministic data.
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
this.templateModified = {};
this.selectionDirty = {};
this.obstructionSnap = new ObstructionSnap();
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
// Work out which phase we are in.
let phase = "";
let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).PlayerHasMarket(i),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(i)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = cmpTerrain.GetMapSize();
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function()
{
let ret = this.GetSimulationState();
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
}
return ret;
};
/**
* Returns the gamesettings that were chosen at the time the match started.
*/
GuiInterface.prototype.GetInitAttributes = function()
{
return InitAttributes;
};
/**
* This data will be stored in the replay metadata file after a match has been finished recording.
*/
GuiInterface.prototype.GetReplayMetadata = function()
{
let extendedSimState = this.GetExtendedSimulationState();
return {
"timeElapsed": extendedSimState.timeElapsed,
"playerStates": extendedSimState.players,
"mapSettings": InitAttributes.settings
};
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui.
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id.
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"player": INVALID_PLAYER,
"template": template
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"visibleClasses": cmpIdentity.GetVisibleClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable(),
"hasSomeFormation": cmpIdentity.HasSomeFormation(),
"formations": cmpIdentity.GetFormationsList()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
ret.position = cmpPosition.GetPosition();
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured();
ret.needsHeal = !cmpHealth.IsUnhealable();
}
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
let cmpMarket = QueryMiragedInterface(ent, IID_Market);
if (cmpMarket)
ret.market = {
"land": cmpMarket.HasType("land"),
"naval": cmpMarket.HasType("naval")
};
let cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress()
};
let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
ret.upgrade = {
"upgrades": cmpUpgrade.GetUpgrades(),
"progress": cmpUpgrade.GetProgress(),
"template": cmpUpgrade.GetUpgradingTo()
};
let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
if (cmpStatusEffects)
ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"queue": cmpProductionQueue.GetQueue()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods()
};
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"numBuilders": cmpFoundation.GetNumBuilders(),
"buildTime": cmpFoundation.GetBuildTime()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = {
"numBuilders": cmpRepairable.GetNumBuilders(),
"buildTime": cmpRepairable.GetBuildTime()
};
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
};
let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
if (cmpTurretHolder)
ret.turretHolder = {
"turretPoints": cmpTurretHolder.GetTurretPoints()
};
ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"canPatrol": cmpUnitAI.CanPatrol(),
"selectableStances": cmpUnitAI.GetSelectableStances(),
"isIdle": cmpUnitAI.IsIdle()
};
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities()
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
}
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked()
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = true;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = {};
Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type));
ret.attack[type].splash = cmpAttack.GetSplashData(type);
if (ret.attack[type].splash)
Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true));
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// Not a ranged attack, set some defaults.
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
if (cmpPosition && cmpPosition.IsInWorld())
// For units, take the range in front of it, no spread, so angle = 0,
// else, take the average elevation around it: angle = 2 * pi.
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, cmpUnitAI ? 0 : 2 * Math.PI);
else
// Not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
let cmpArmour = Engine.QueryInterface(ent, IID_Resistance);
if (cmpArmour)
ret.armour = cmpArmour.GetArmourStrengths("Damage");
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
- if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
+ if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter"))
ret.isBarterMarket = true;
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"health": cmpHeal.GetHealth(),
"range": cmpHeal.GetRange().max,
"interval": cmpHeal.GetInterval(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses()
};
let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
if (cmpLoot)
{
ret.loot = cmpLoot.GetResources();
ret.loot.xp = cmpLoot.GetXp();
}
let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
if (cmpResourceTrickle)
ret.resourceTrickle = {
"interval": cmpResourceTrickle.GetTimer(),
"rates": cmpResourceTrickle.GetRates()
};
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
"run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier()
};
return ret;
};
GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
{
return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2 * Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, data)
{
let templateName = data.templateName;
let owner = data.player !== undefined ? data.player : player;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, owner, aurasTemplate);
let auraNames = template.Auras._string.split(/\s+/);
for (let name of auraNames)
aurasTemplate[name] = AuraTemplates.Get(name);
return GetTemplateDataHelper(template, owner, aurasTemplate);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
/**
* Checks whether the requirements for this technology have been met.
*/
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
/**
* Returns technologies that are being actively researched, along with
* which entity is researching them and how far along the research is.
*/
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech of cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
{
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
ret[tech].timeRemaining = cmpProductionQueue.GetQueue()[0].timeRemaining;
}
else
{
ret[tech].progress = 0;
ret[tech].timeRemaining = 0;
}
}
return ret;
};
/**
* Returns the battle state of the player.
*/
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
/**
* Returns a list of ongoing attacks against the player.
*/
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks();
};
/**
* Used to show a red square over GUI elements you can't yet afford.
*/
GuiInterface.prototype.GetNeededResources = function(player, data)
{
return QueryPlayerIDInterface(data.player !== undefined ? data.player : player).GetNeededResources(data.cost);
};
/**
* State of the templateData (player dependent): true when some template values have been modified
* and need to be reloaded by the gui.
*/
GuiInterface.prototype.OnTemplateModification = function(msg)
{
this.templateModified[msg.player] = true;
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.IsTemplateModified = function(player)
{
return this.templateModified[player] || false;
};
GuiInterface.prototype.ResetTemplateModified = function()
{
this.templateModified = {};
};
/**
* Some changes may require an update to the selection panel,
* which is cached for efficiency. Inform the GUI it needs reloading.
*/
GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.SetSelectionDirty = function(player)
{
this.selectionDirty[player] = true;
};
GuiInterface.prototype.IsSelectionDirty = function(player)
{
return this.selectionDirty[player] || false;
};
GuiInterface.prototype.ResetSelectionDirty = function()
{
this.selectionDirty = {};
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default.
if (!notification.players)
{
notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
notification.players[0] = -1;
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// Filter on players and time, since the delete timer might be executed with a delay.
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
return QueryPlayerIDInterface(wantedPlayer).GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
return data.ents.some(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate;
});
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data)
{
let updateEntityColor = (iids, entities) => {
for (let ent of entities)
for (let iid of iids)
{
let cmp = Engine.QueryInterface(ent, iid);
if (cmp)
cmp.UpdateColor();
}
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 1; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i, IID_Player);
if (!cmpPlayer)
continue;
cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors);
if (data.displayDiplomacyColors)
cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]);
updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ?
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] :
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer],
cmpRangeManager.GetEntitiesByPlayer(i));
}
updateEntityColor([IID_Selectable, IID_StatusBars], data.selected);
Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors();
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
// Cache of owner -> color map
let playerColors = {};
for (let ent of cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color.
let owner = INVALID_PLAYER;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r": 1, "g": 1, "b": 1 };
let cmpPlayer = QueryPlayerIDInterface(owner);
if (cmpPlayer)
color = cmpPlayer.GetDisplayedColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER)
continue;
cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
}
};
GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
{
this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return Array.from(this.entsWithAuraAndStatusBars);
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
{
for (let ent of cmd.entities)
{
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them.
for (let ent of this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities.
for (let ent of cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// Entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location).
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner.
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position.
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
// May return undefined if no rally point is set.
pos = cmpRallyPoint.GetPositions()[0];
if (pos)
{
// Only update the position if we changed it (cmd.queued is set).
// Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z.
if ("queued" in cmd)
{
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z));
else
cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z));
}
else if (!cmpRallyPointRenderer.IsSet())
// Rebuild the renderer when not set (when reading saved game or in case of building update).
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z));
cmpRallyPointRenderer.SetDisplayed(true);
// Remember which entities have their rally points displayed so we can hide them again.
this.entsRallyPointsDisplayed.push(ent);
}
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": []
};
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
// Set it to a red shade if this is an invalid location.
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* 'populationBonus': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
// Did the start position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// Did the end position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// --------------------------------------------------------------------------------
// Do some entity cache management and check for snapping.
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// We're clearing the preview, clear the entity cache and bail.
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// Keep template data around.
}
return false;
}
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
{
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
pos.MoveOutOfWorld();
}
this.placementWallEntities[tpl].numUsed = 0;
}
// Create cache entries for templates we haven't seen before.
for (let type in wallSet.templates)
{
if (type == "curves")
continue;
let tpl = wallSet.templates[type];
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, { "templateName": tpl }),
};
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
// Prevent division by zero errors further on if the start and end positions are the same.
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
// Value of 0.5 was determined through trial and error.
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5;
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// Clear the single-building preview entity (we'll be rolling our own).
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// Calculate wall placement and position preview entities.
let result = {
"pieces": [],
"cost": { "population": 0, "populationBonus": 0, "time": 0 }
};
for (let res of Resources.GetCodes())
result.cost[res] = 0;
let previewEntities = [];
if (end.pos)
// See helpers/Walls.js.
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end);
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group.
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true // Preview only, must not appear in the result.
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle
});
}
if (end.pos)
{
// Analogous to the starting side case above.
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || [];
previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// If we're snapping to a foundation, add an extra preview tower and also set it to the same control group.
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
// Number of entities that are required to build the entire wall, regardless of validity.
let numRequiredPieces = 0;
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// Move piece to right location.
// TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities.
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces.
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region.
// TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta.
let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement.
validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: We should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
for (let res of Resources.GetCodes().concat(["population", "populationBonus", "time"]))
result.cost[res] += tplData.cost[res];
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// See if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest.
// (TODO: Break unlikely ties by choosing the lowest entity ID.)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for (let ent of data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (data.snapToEdges)
{
let position = this.obstructionSnap.getPosition(data, template);
if (position)
return position;
}
if (template.BuildRestrictions.PlacementType == "shore")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
/**
* Find any idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
* @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
* @param data.excludeUnits Array of units to exclude.
*
* Returns an array of idle units.
* If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
*/
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let idleUnits = [];
// The general case is that only the 'first' idle unit is required; filtering would examine every unit.
// This loop imitates a grouping/aggregation on the first matching idle class.
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
{
let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
if (!filtered.idle)
continue;
// If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
// By adding to the 'end', there is no pause if the series of units loops.
let bucket = filtered.bucket;
if (bucket == 0 && data.prevUnit && entity <= data.prevUnit)
bucket = data.idleClasses.length;
if (!idleUnits[bucket])
idleUnits[bucket] = [];
idleUnits[bucket].push(entity);
// If enough units have been collected in the first bucket, go ahead and return them.
if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
return idleUnits[0];
}
let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
if (data.limit && reduced.length > data.limit)
return reduced.slice(0, data.limit);
return reduced;
};
/**
* Discover if the player has idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.excludeUnits Array of units to exclude.
*
* Returns a boolean of whether the player has any idle units
*/
GuiInterface.prototype.HasIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
};
/**
* Whether to filter an idle unit
*
* @param unit The unit to filter.
* @param idleclasses Array of class names to include.
* @param excludeUnits Array of units to exclude.
*
* Returns an object with the following fields:
* - idle - true if the unit is considered idle by the filter, false otherwise.
* - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
*/
GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
return { "idle": false };
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
if (!cmpIdentity)
return { "idle": false };
let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
return { "idle": false };
return { "idle": true, "bucket": bucket };
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
else if (!firstMarket)
result = { "type": "set first" };
else if (!secondMarket)
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
else
result = { "type": "set first" };
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for (let ent of data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for (let ent of traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player)
{
return QueryPlayerIDInterface(player).GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
/**
* List the GuiInterface functions that can be safely called by GUI scripts.
* (GUI scripts are non-deterministic and untrusted, so these functions must be
* appropriately careful. They are called with a first argument "player", which is
* trusted and indicates the player associated with the current client; no data should
* be returned unless this player is meant to be able to see it.)
*/
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetInitAttributes": 1,
"GetReplayMetadata": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetMultipleEntityStates": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"UpdateDisplayedPlayerColors": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"AddTargetMarker": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"EnableVisualRangeOverlayType": 1,
"SetRangeOverlays": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
"IsTemplateModified": 1,
"ResetTemplateModified": 1,
"IsSelectionDirty": 1,
"ResetSelectionDirty": 1
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
throw new Error("Invalid GuiInterface Call name \"" + name + "\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Identity.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 23865)
@@ -1,187 +1,187 @@
function Identity() {}
Identity.prototype.Schema =
"Specifies various names and values associated with the entity, typically for GUI display to users." +
"" +
"athen" +
"Athenian Hoplite" +
"Hoplī́tēs Athēnaïkós" +
"units/athen_infantry_spearman.png" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"Basic" +
"Advanced" +
"Elite" +
"" +
"" +
"" +
"" +
- "" +
+ "" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
- "" +
+ "" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
Identity.prototype.Init = function()
{
this.classesList = GetIdentityClasses(this.template);
this.visibleClassesList = GetVisibleIdentityClasses(this.template);
if (this.template.Phenotype)
this.phenotype = pickRandom(this.GetPossiblePhenotypes());
else
this.phenotype = "default";
};
Identity.prototype.HasSomeFormation = function()
{
return this.GetFormationsList().length > 0;
};
Identity.prototype.GetCiv = function()
{
return this.template.Civ;
};
Identity.prototype.GetLang = function()
{
return this.template.Lang || "greek"; // ugly default
};
/**
* Get a list of possible Phenotypes.
* @return {string[]} A list of possible phenotypes.
*/
Identity.prototype.GetPossiblePhenotypes = function()
{
return this.template.Phenotype._string.split(/\s+/);
};
/**
* Get the current Phenotype.
* @return {string} The current phenotype.
*/
Identity.prototype.GetPhenotype = function()
{
return this.phenotype;
};
Identity.prototype.GetRank = function()
{
return this.template.Rank || "";
};
Identity.prototype.GetClassesList = function()
{
return this.classesList;
};
Identity.prototype.GetVisibleClassesList = function()
{
return this.visibleClassesList;
};
Identity.prototype.HasClass = function(name)
{
return this.GetClassesList().indexOf(name) != -1;
};
Identity.prototype.GetFormationsList = function()
{
if (this.template.Formations && this.template.Formations._string)
return this.template.Formations._string.split(/\s+/);
return [];
};
Identity.prototype.CanUseFormation = function(template)
{
return this.GetFormationsList().indexOf(template) != -1;
};
Identity.prototype.GetSelectionGroupName = function()
{
return this.template.SelectionGroupName || "";
};
Identity.prototype.GetGenericName = function()
{
return this.template.GenericName;
};
Identity.prototype.IsUndeletable = function()
{
return this.template.Undeletable == "true";
};
Engine.RegisterComponentType(IID_Identity, "Identity", Identity);
Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/cart_player_teambonus.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/cart_player_teambonus.json (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/cart_player_teambonus.json (revision 23865)
@@ -1,10 +1,10 @@
{
"type": "global",
- "affects": ["Market"],
+ "affects": ["Trade"],
"affectedPlayers": ["ExclusiveMutualAlly"],
"modifications": [
{ "value": "Market/InternationalBonus", "add": 0.1 }
],
"auraName": "Trademasters",
- "auraDescription": "Allied Markets +10% international trade bonus."
+ "auraDescription": "Allies +10% international trade bonus."
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/cart.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/civs/cart.json (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/data/civs/cart.json (revision 23865)
@@ -1,182 +1,182 @@
{
"Code": "cart",
"Culture": "cart",
"Name": "Carthaginians",
"Emblem": "session/portraits/emblems/emblem_carthaginians.png",
"History": "Carthage, a city-state in modern-day Tunisia, was a formidable force in the western Mediterranean, eventually taking over much of North Africa and modern-day Spain in the third century B.C. The sailors of Carthage were among the fiercest contenders on the high seas, and masters of naval trade. They deployed towered War Elephants on the battlefield to fearsome effect, and had defensive walls so strong, they were never breached.",
"Music":[
{"File":"Mediterranean_Waves.ogg", "Type":"peace"},
{"File":"Harsh_Lands_Rugged_People.ogg", "Type":"peace"},
{"File":"Peaks_of_Atlas.ogg", "Type":"peace"}
],
"Factions":
[
{
"Name": "",
"Description": "",
"Technologies":
[
{
"Name": "Exploration",
"History": "Nobody knew better than the Carthaginians where in the ancient world they were going and going to go; their merchant traders had missions to everywhere.",
"Description": "All Traders and Ships +25% vision range."
},
{
"Name": "Colonization",
"History": "Carthaginians established many trading centers as colonies and ultimately held dominion over 300 cities and towns in North Africa alone.",
"Description": "Civic Centers, Temples, and Houses -25% build time."
}
],
"Heroes":
[
{
"Name": "Hannibal Barca",
"Class": "",
"Armament": "",
"Emblem": "",
"History": "Carthage's most famous son. Hannibal Barca was the eldest son of Hamilcar Barca and proved an even greater commander than his father. Lived 247-182 B.C. While he ultimately lost the Second Punic War his victories at Trebia, Lake Trasimene, and Cannae, and the feat of crossing the Alps have secured his position as among the best tacticians and strategists in history."
},
{
"Name": "Hamilcar Barca",
"Class": "",
"Armament": "",
"Emblem": "",
"History": "Father of Hannibal and virtual military dictator. Hamilcar Barca was a soldier and politician who excelled along his entire career. Lived 275-228 B.C. While overshadowed by his sons, Hamilcar was great general in his own right, earning the nickname Baraq or Barca for the lightning speed of his advance."
},
{
"Name": "Maharbal",
"Class": "",
"Armament": "",
"Emblem": "",
"History": "Maharbal was Hannibal Barca's 'brash young cavalry commander' during the 2nd Punic War. He is credited with turning the wing of the legions at Cannae resulting in defeat in which 30,000 of 50,000 Romans were lost, as well as significant contributions to the winning of many other battles during the 2nd Punic War. He is known for having said, after the battle of Cannae, 'Hannibal, you know how to win the victory; just not what to do with it.'"
}
]
}
],
"CivBonuses":
[
{
"Name": "Triple Walls",
"History": "Carthaginians built triple city walls.",
"Description": "Carthaginian walls, gates, and towers have 3x health of a standard wall, but also 2x build time."
},
{
"Name": "Roundup",
"History": "Not unlike the Iberian Peninsula, North Africa was known as horse country, capable of producing up to 100,000 new mounts each year. It was also the home of the North African Forest Elephant.",
"Description": "The resource cost of training elephant-mounted (war elephant) or horse-mounted units (cavalry) is reduced by 5% per animal corralled (as appropriate)."
}
],
"TeamBonuses":
[
{
"Name": "Trademasters",
"History": "The Phoenicians and Carthaginians were broadly known as the greatest trading civilization of the ancient and classical world.",
- "Description": "Allied Markets +10% international trade bonus."
+ "Description": "Allies +10% international trade bonus."
}
],
"Structures":
[
{
"Name":"Naval Shipyard",
"Class":"",
"Emblem":"",
"History":"The structure is based upon the center island of the inner harbour constructed to house the war fleet of the Carthaginian navy at Carthage.",
"Requirements":".",
"Phase":"",
"Special":"Construct the powerful warships of the Carthaginian navy."
},
{
"Name":"Celtic Embassy",
"Class":"",
"Emblem":"",
"History":"The Celts supplied fierce warrior mercenaries for Carthaginian armies.",
"Requirements":".",
"Phase":"",
"Special":"Hire Celtic mercenaries."
},
{
"Name":"Italiote Embassy",
"Class":"",
"Emblem":"",
"History":"When Hannibal invaded Italy and defeated the Romans in a series of battles, many of the Italian peoples subject to Rome, including the Italian Greeks and powerful Samnites, revolted and joined the Carthaginian cause.",
"Requirements":".",
"Phase":"",
"Special":"Hire Italian mercenaries."
},
{
"Name":"Iberian Embassy",
"Class":"",
"Emblem":"",
"History":"The Iberians were known as fierce mercenaries, loyal to their paymasters.",
"Requirements":".",
"Phase":"",
"Special":"Hire Iberian mercenaries."
}
],
"WallSets":
[
"structures/wallset_palisade",
"structures/cart_wallset_short",
"structures/cart_wallset_stone"
],
"StartEntities":
[
{
"Template": "structures/cart_civil_centre"
},
{
"Template": "units/cart_support_female_citizen",
"Count": 4
},
{
"Template": "units/cart_infantry_spearman_b",
"Count": 2
},
{
"Template": "units/cart_infantry_archer_b",
"Count": 2
},
{
"Template": "units/cart_cavalry_javelinist_b"
}
],
"Formations":
[
"special/formations/null",
"special/formations/box",
"special/formations/column_closed",
"special/formations/line_closed",
"special/formations/column_open",
"special/formations/line_open",
"special/formations/flank",
"special/formations/battle_line",
"special/formations/skirmish",
"special/formations/wedge",
"special/formations/phalanx"
],
"AINames":
[
"Hannibal Barca",
"Hamilcar Barca",
"Hasdrubal Barca",
"Hasdrubal Gisco",
"Hanno the Elder",
"Maharbal",
"Mago Barca",
"Hasdrubal the Fair",
"Hanno the Great",
"Himilco",
"Hampsicora",
"Hannibal Gisco",
"Dido",
"Xanthippus",
"Himilco Phameas",
"Hasdrubal the Boetharch"
],
"SkirmishReplacements":
{
"skirmish/units/default_infantry_ranged_b": "units/cart_infantry_archer_b",
"skirmish/structures/default_house_10": "structures/{civ}_house"
},
"SelectableInGameSetup": true
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/buildtime_walls_rubble.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/buildtime_walls_rubble.json (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/buildtime_walls_rubble.json (revision 23865)
@@ -1,17 +1,17 @@
{
"pair": "pair_walls_01",
"genericName": "Rubble Materials",
"description": "Using rubble materials reduces the costs and build times of walls.",
"cost": { "food": 0, "wood": 200, "stone": 0, "metal": 0 },
"requirements": { "tech": "phase_town" },
"requirementsTooltip": "Unlocked in Town Phase.",
"icon": "stone_blocks_brown.png",
"researchTime": 40,
- "tooltip": "City walls -20% build time, but -1 crush armor level.",
+ "tooltip": "Walls −20% build time, but −1 crush armor.",
"modifications": [
- { "value": "Cost/BuildTime", "multiply": 0.8 },
- { "value": "Armour/Crush", "add": -1 }
+ { "value": "Armour/Crush", "add": -1 },
+ { "value": "Cost/BuildTime", "multiply": 0.8 }
],
- "affects": ["StoneWall"],
+ "affects": ["Wall"],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/civbonuses/cart_walls.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/civbonuses/cart_walls.json (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/civbonuses/cart_walls.json (revision 23865)
@@ -1,14 +1,14 @@
{
"genericName": "Triple Walls",
"autoResearch": true,
"description": "Carthaginians built their city walls in three concentric circuits. These walls were never breached. Even when the city was taken by the Romans, it was via the city's harbor, not by storming its walls.",
"icon": "wall.png",
"tooltip": "Carthaginian Walls +100% build time, +100% stone cost, and +200% health.",
"requirements": { "civ": "cart" },
"modifications": [
- { "value": "Health/Max", "multiply": 3 },
{ "value": "Cost/BuildTime", "multiply": 2 },
- { "value": "Cost/Resources/stone", "multiply": 2 }
+ { "value": "Cost/Resources/stone", "multiply": 2 },
+ { "value": "Health/Max", "multiply": 3 }
],
- "affects": ["StoneWall"]
+ "affects": ["Wall"]
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/civbonuses/maur_walls.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/civbonuses/maur_walls.json (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/civbonuses/maur_walls.json (revision 23865)
@@ -1,13 +1,13 @@
{
"genericName": "Wooden Walls",
"autoResearch": true,
"description": "The Mauryas built their city walls out of wood, an abundant natural resource in India.",
"requirements": { "civ": "maur" },
"icon": "wooden_walls.png",
"tooltip": "Mauryan City Walls −20% build time and −20% health.",
"modifications": [
- { "value": "Health/Max", "multiply": 0.8 },
- { "value": "Cost/BuildTime", "multiply": 0.8 }
+ { "value": "Cost/BuildTime", "multiply": 0.8 },
+ { "value": "Health/Max", "multiply": 0.8 }
],
- "affects": ["StoneWall"]
+ "affects": ["Wall"]
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/health_walls_geometric_masonry.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/health_walls_geometric_masonry.json (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/health_walls_geometric_masonry.json (revision 23865)
@@ -1,17 +1,17 @@
{
"pair": "pair_walls_01",
"genericName": "Geometric Masonry",
"description": "Using geometric masonry increases the sturdiness of defensive walls.",
"cost": { "food": 0, "wood": 0, "stone": 200, "metal": 0 },
"requirements": { "tech": "phase_town" },
"requirementsTooltip": "Unlocked in Town Phase.",
"icon": "stone_blocks.png",
"researchTime": 40,
- "tooltip": "City walls +2 crush armor levels, but +10% build time.",
+ "tooltip": "Walls +2 crush armor, but +10% build time.",
"modifications": [
- { "value": "Cost/BuildTime", "multiply": 1.1 },
- { "value": "Armour/Crush", "add": 2 }
+ { "value": "Armour/Crush", "add": 2 },
+ { "value": "Cost/BuildTime", "multiply": 1.1 }
],
- "affects": ["StoneWall"],
+ "affects": ["Wall"],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/hellenes/special_long_walls.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/hellenes/special_long_walls.json (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/hellenes/special_long_walls.json (revision 23865)
@@ -1,15 +1,20 @@
{
"genericName": "Athenian Long Walls",
"description": "The Long Walls of Athens were constructed under the auspices of the wily Themistocles and extended 6 km from the city to the port of Piraeus. This secured the city's sea supply routes and prevented an enemy from starving out the city during a siege.",
"cost": { "food": 0, "wood": 0, "stone": 250, "metal": 250 },
- "requirements": { "all": [{ "tech": "phase_city" }, { "civ": "athen" }] },
+ "requirements": {
+ "all": [
+ { "tech": "phase_city" },
+ { "civ": "athen" }
+ ]
+ },
"requirementsTooltip": "Unlocked in City Phase.",
"icon": "crenelations.png",
"researchTime": 60,
- "tooltip": "Build stone walls in neutral territory.",
+ "tooltip": "Build Walls in own or neutral territory.",
"modifications": [
{ "value": "BuildRestrictions/Territory", "replace": ["own", "neutral"] }
],
- "affects": ["StoneWall"],
+ "affects": ["Wall"],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/pair_walls_01.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/pair_walls_01.json (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/pair_walls_01.json (revision 23865)
@@ -1,6 +1,6 @@
{
- "genericName": "Walls build time vs. Health",
+ "genericName": "Wall armor vs build time.",
"top": "buildtime_walls_rubble",
"bottom": "health_walls_geometric_masonry",
"requirements": { "notciv": "iber" }
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/phase_city_athen.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/phase_city_athen.json (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/phase_city_athen.json (revision 23865)
@@ -1,22 +1,27 @@
{
"genericName": "City Phase",
"specificName": {
"athen": "Megalópolis"
},
"description": "Advances from a bustling town to a veritable metropolis, full of the wonders of modern technology. This is the Athenian city phase, where metal gathering rates are boosted because of the 'Silver Owls' bonus.",
"cost": { "food": 0, "wood": 0, "stone": 750, "metal": 750 },
- "requirements": { "entity": { "class": "Town", "number": 4 } },
- "requirementsTooltip": "Requires 4 new Town Phase structures (except Walls and Civic Centers).",
+ "requirements": {
+ "entity": {
+ "class": "Town",
+ "number": 4
+ }
+ },
+ "requirementsTooltip": "Requires 4 Town Structures.",
"supersedes": "phase_town_athen",
"replaces": ["phase_city"],
"icon": "city_phase.png",
"researchTime": 60,
"tooltip": "Advance to City Phase, which unlocks more structures and units. Territory radius for Civic Centers increased by another +50%. Silver Owls civ bonus grants an extra +10% metal gather rate to all workers. Citizen Soldiers +10% health. All structures +9 garrisoned regeneration rate.",
"modifications": [
{ "value": "ResourceGatherer/Rates/metal.ore", "multiply": 1.1, "affects": "Worker" },
{ "value": "TerritoryInfluence/Radius", "multiply": 1.5, "affects": "CivCentre" },
{ "value": "Health/Max", "multiply": 1.1, "affects": "Citizen Soldier" },
{ "value": "Capturable/GarrisonRegenRate", "add": 9, "affects": "Structure" }
],
"soundComplete": "interface/alarm/alarm_phase.xml"
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/phase_city_generic.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/phase_city_generic.json (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/phase_city_generic.json (revision 23865)
@@ -1,22 +1,27 @@
{
"genericName": "City Phase",
"specificName": {
"mace": "Megalópolis",
"spart": "Megalópolis"
},
"description": "Advances from a bustling town to a veritable metropolis, full of the wonders of modern technology.",
"cost": { "food": 0, "wood": 0, "stone": 750, "metal": 750 },
- "requirements": { "entity": { "class": "Town", "number": 4 } },
- "requirementsTooltip": "Requires 4 new Town Phase structures (except Walls and Civic Centers).",
+ "requirements": {
+ "entity": {
+ "class": "Town",
+ "number": 4
+ }
+ },
+ "requirementsTooltip": "Requires 4 Town Structures.",
"supersedes": "phase_town_generic",
"replaces": ["phase_city"],
"icon": "city_phase.png",
"researchTime": 60,
"tooltip": "Advance to City Phase, which unlocks more structures and units. Territory radius for Civic Centers increased by another +50%. Citizen Soldiers +10% health. All structures +9 garrisoned regeneration rate.",
"modifications": [
{ "value": "TerritoryInfluence/Radius", "multiply": 1.5, "affects": "CivCentre" },
{ "value": "Health/Max", "multiply": 1.1, "affects": "Citizen Soldier" },
{ "value": "Capturable/GarrisonRegenRate", "add": 9, "affects": "Structure" }
],
"soundComplete": "interface/alarm/alarm_phase.xml"
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/phase_town_athen.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/phase_town_athen.json (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/phase_town_athen.json (revision 23865)
@@ -1,22 +1,27 @@
{
"genericName": "Town Phase",
"specificName": {
"athen": "Kōmópolis"
},
"description": "Advances from a small village to a bustling town, ready to expand rapidly. This is the Athenian town phase, where metal gathering rates are boosted because of the 'Silver Owls' bonus.",
"cost": { "food": 500, "wood": 500, "stone": 0, "metal": 0 },
- "requirements": { "entity": { "class": "Village", "number": 5 } },
- "requirementsTooltip": "Requires 5 Village Phase structures (except Palisades and Farm Fields).",
+ "requirements": {
+ "entity": {
+ "class": "Village",
+ "number": 5
+ }
+ },
+ "requirementsTooltip": "Requires 5 Village Structures.",
"supersedes": "phase_village",
"replaces": ["phase_town"],
"icon": "town_phase.png",
"researchTime": 30,
"tooltip": "Advance to Town Phase, which unlocks more structures and units. Territory radius for Civic Centers increased by +30%. 'Silver Owls' civ bonus grants an extra +10% metal gather rate to all workers. Citizen Soldiers +20% health. All structures +7 garrisoned regeneration rate.",
"modifications": [
{ "value": "ResourceGatherer/Rates/metal.ore", "multiply": 1.1, "affects": "Worker" },
{ "value": "TerritoryInfluence/Radius", "multiply": 1.3, "affects": "CivCentre" },
{ "value": "Health/Max", "multiply": 1.2, "affects": "Citizen Soldier" },
{ "value": "Capturable/GarrisonRegenRate", "add": 7, "affects": "Structure" }
],
"soundComplete": "interface/alarm/alarm_phase.xml"
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/phase_town_generic.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/phase_town_generic.json (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/phase_town_generic.json (revision 23865)
@@ -1,22 +1,27 @@
{
"genericName": "Town Phase",
"specificName": {
"mace": "Kōmópolis",
"spart": "Kōmópolis"
},
"description": "Advances from a small village to a bustling town, ready to expand rapidly.",
"cost": { "food": 500, "wood": 500, "stone": 0, "metal": 0 },
- "requirements": { "entity": { "class": "Village", "number": 5 } },
- "requirementsTooltip": "Requires 5 Village Phase structures (except Palisades and Farm Fields).",
+ "requirements": {
+ "entity": {
+ "class": "Village",
+ "number": 5
+ }
+ },
+ "requirementsTooltip": "Requires 5 Village Structures.",
"supersedes": "phase_village",
"replaces": ["phase_town"],
"icon": "town_phase.png",
"researchTime": 30,
"tooltip": "Advance to Town Phase, which unlocks more structures and units. Territory radius for Civic Centers increased by +30%. Citizen Soldiers +20% health. All structures +7 garrisoned regeneration rate.",
"modifications": [
{ "value": "TerritoryInfluence/Radius", "multiply": 1.3, "affects": "CivCentre" },
{ "value": "Health/Max", "multiply": 1.2, "affects": "Citizen Soldier" },
{ "value": "Capturable/GarrisonRegenRate", "add": 7, "affects": "Structure" }
],
"soundComplete": "interface/alarm/alarm_phase.xml"
}
Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/trade_commercial_treaty.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/technologies/trade_commercial_treaty.json (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/trade_commercial_treaty.json (revision 23865)
@@ -1,15 +1,15 @@
{
"genericName": "Commercial Treaty",
"description": "Improve the international trading profit.",
"cost": { "food": 0, "wood": 0, "stone": 0, "metal": 100 },
"requirements": { "tech": "phase_town" },
"requirementsTooltip": "Unlocked in Town Phase.",
"icon": "sibylline_books.png",
"researchTime": 40,
- "tooltip": "Market +10% International Bonus.",
+ "tooltip": "+10% international trade bonus.",
"modifications": [
{ "value": "Market/InternationalBonus", "add": 0.1 }
],
- "affects": ["Market"],
+ "affects": ["Trade"],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}
Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player/player.xml (revision 23865)
@@ -1,117 +1,118 @@
8016060000200120.044081301011
- 1
+ 135011111
- 1
+ 122421
+ 10205415phase_townPlayerPlayertrue1.01.01.01.01.01.01.01.0unlock_shared_losunlock_shared_dropsites1.00.00.00.00.01000
Infantry
Worker
FemaleCitizen
Cavalry
Champion
Hero
Siege
Ship
Domestic
Trader
House
Economic
Outpost
Military
Fortress
CivCentre
Wonder
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen_gymnasium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen_gymnasium.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen_gymnasium.xml (revision 23865)
@@ -1,52 +1,53 @@
2002002008.0102000athen
- ConquestCriticalGymnasiumGymnasionTrain Champions.
+ ConquestCritical
+ Gymnasiumstructures/gymnasium.png40400.7
units/{civ}_champion_infantry
units/{civ}_champion_ranged
interface/complete/building/complete_gymnasium.xmlstructures/athenians/gymnasium.xmlstructures/fndn_8x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit_crannog.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit_crannog.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit_crannog.xml (revision 23865)
@@ -1,48 +1,48 @@
own ally neutralshore8.0
- Island Settlementbrit
- Naval
- structures/crannog.png
- Increase population limit and defend waterways.
+ Island SettlementCranogion
+ Build upon a shoreline in own, neutral, or allied territory. Acquire large tracts of territory. Territory root. Dropsite for food, wood, stone, and metal. Train Citizens, construct Ships, and research technologies. Garrison Soldiers for additional arrows.
+ Naval
+ structures/crannog.pngphase_towntrue0.0
units/{civ}_infantry_spearman_b
units/{civ}_infantry_slinger_b
units/{civ}_cavalry_javelinist_b
units/{civ}_ship_fishing
units/{civ}_ship_merchant
units/{civ}_ship_bireme
units/{civ}_ship_trireme
-phase_town_{civ}
shipstructures/britons/crannog.xmlstructures/fndn_8x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart_super_dock.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart_super_dock.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart_super_dock.xml (revision 23865)
@@ -1,96 +1,96 @@
35
structures/cart_super_dock_repair
own ally neutralshoreDock200010105003002008.050.1UnitSupport Infantry Cavalry Ship025000cart
- ConquestCriticalNaval ShipyardCothon
- -City Shipyard Town
- Construct and repair mighty warships.
+ Build upon a shoreline in own, neutral, or allied territory. Acquire large tracts of territory. Territory root. Construct Warships and research technologies.
+ ConquestCritical
+ Naval -City Town Shipyardstructures/uber_dock.pngphase_town6040true0.00.7
units/{civ}_ship_bireme
units/{civ}_ship_trireme
units/{civ}_ship_quinquereme
carthaginians/training_phoenician_naval_architects
carthaginians/special_exploration
shipinterface/complete/building/complete_dock.xmltrue20025000100structures/carthaginians/super_dock.xmlstructures/fndn_dock_super.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul_wall_tower.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul_wall_tower.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul_wall_tower.xml (revision 23865)
@@ -1,27 +1,27 @@
21.5gaulUxelon
- Does not shoot or garrison.
+ 22.5structures/gauls/wall_tower.xml8
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush_temple.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush_temple.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush_temple.xml (revision 23865)
@@ -1,25 +1,25 @@
kushTemple of ApedemakPr-ʿIprmk
- Train priests to heal your troops. Train Meroitic Temple Guards. Garrison units to heal them at a quick rate.
- TempleOfApedemak
+ Train Healers and Apedamak Champions and research healing technologies.
+ TempleOfApedemak
units/{civ}_champion_infantry_apedemak
structures/kushites/temple.xmlstructures/fndn_6x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_pillar_ashoka.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_pillar_ashoka.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_pillar_ashoka.xml (revision 23865)
@@ -1,60 +1,60 @@
structures/maur_pillar
PillarPillar75801001005.01000decay|rubble/rubble_stone_2x2maurEdict Pillar of AshokaŚāsana Stambha Aśokā
- Pillar
+ Territory root.
+ Pillarstructures/ashoka_pillar.png
- The famous pillar of Ashoka. Increases the walk speed of traders. Buildings in the territory of the monument do not decay.2020interface/complete/building/complete_iber_monument.xmltrueprops/structures/mauryas/ashoka_pillar.xmlstructures/fndn_2x2.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_fort.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_fort.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_fort.xml (revision 23865)
@@ -1,25 +1,24 @@
9.0
- Palisade FortWooden Towerstructures/palisade_fort.pngPalisadeprops/special/palisade_rocks_fort.xml8.0
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_medium.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_medium.xml (revision 23865)
@@ -1,35 +1,32 @@
8860.5
-
- Palisade
- 27props/special/palisade_rocks_medium.xmlstructures/fndn_3x1.xml9
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_spikes_small.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_spikes_small.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_spikes_small.xml (revision 23865)
@@ -1,33 +1,33 @@
1232.00.2
- Small Spikes
+ Small Spikes1props/special/palisade_small_spikes.xml110.5
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_palace.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_palace.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_palace.xml (revision 23865)
@@ -1,47 +1,48 @@
200102002005.0maur
- ConquestCriticalHarmya
- gaia/special_stoa.pngTrain Female Citizens, Champions, and Heroes.
+ ConquestCritical
+ Palace
+ gaia/special_stoa.png40400.7
units/{civ}_support_female_citizen
units/{civ}_champion_maiden_archer
units/{civ}_hero_chanakya
units/{civ}_hero_chandragupta
units/{civ}_hero_ashoka
false3865535structures/mauryas/misc_structure_01.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_end.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_end.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_end.xml (revision 23865)
@@ -1,32 +1,29 @@
855
-
- Palisade End
- 16props/special/palisade_rocks_end.xml0.81.5
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_long.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_long.xml (revision 23865)
@@ -1,46 +1,46 @@
121260.75
- Palisade
+ Can be converted into a Palisade Gate.37structures/palisades_gate
+ Allow units access through Palisades. Can be locked to prevent access.20
- This will allow you to let units circulate through your fortifications.upgradingprops/special/palisade_rocks_long.xmlstructures/fndn_3x1.xml14
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_spike_angle.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_spike_angle.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_spike_angle.xml (revision 23865)
@@ -1,34 +1,34 @@
859.00.5
- Angle Spike
+ Angle Spike1props/special/palisade_angle_spike.xml-0.731.5
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen_prytaneion.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen_prytaneion.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen_prytaneion.xml (revision 23865)
@@ -1,56 +1,57 @@
Council2001002008.02000athen
- ConquestCriticalCouncil ChamberPrytaneionTrain Heroes and research technologies.
+ ConquestCritical
+ Councilstructures/tholos.png20400.7
units/{civ}_hero_themistocles
units/{civ}_hero_pericles
units/{civ}_hero_iphicrates
hellenes/special_long_walls
hellenes/special_iphicratean_reforms
interface/complete/building/complete_tholos.xmlstructures/athenians/prytaneion.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit_wall_tower.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit_wall_tower.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit_wall_tower.xml (revision 23865)
@@ -1,27 +1,27 @@
20britUxelon
- Does not shoot or garrison.
+ 21structures/britons/wall_tower.xml10
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart_temple.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart_temple.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart_temple.xml (revision 23865)
@@ -1,32 +1,32 @@
40012.0cartMaqdaš
- Train Healers and Champions and research healing technologies. Garrison units to heal them at a quick rate.
+ Train Healers and Champions and research healing technologies.80
units/{civ}_champion_infantry
units/{civ}_champion_cavalry
structures/carthaginians/temple_big.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber_monument.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber_monument.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber_monument.xml (revision 23865)
@@ -1,63 +1,63 @@
structures/iber_monument
MonumentMonument1501201001008.01200decay|rubble/rubble_stone_2x2iberRevered MonumentGur Oroigarri
- Monument
+ Territory root.
+ Monumentstructures/iberian_bull.png
- All units within vision of this monument will fight harder. Buildings in the territory of the monument do not decay.2020interface/complete/building/complete_iber_monument.xmltrue60structures/iberians/sb_1.xmlstructures/fndn_2x2.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush_temple_amun.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush_temple_amun.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush_temple_amun.xml (revision 23865)
@@ -1,54 +1,53 @@
structures/kush_temple_amun
TempleOfAmun10300500303000decay|rubble/rubble_stone_6x6kushGrand Temple of AmunPr-ʿImn
- Train priests to heal your troops. Train Napatan Temple Guards and research unique technologies. Garrison units to heal them at a quick rate.
+ Train Healers and Amun Champions and research healing technologies.
+ -Town City TempleOfAmunstructures/temple_epic.png
- TempleOfAmun
- City -Townphase_city100
units/{civ}_champion_infantry_amun
kushites/upgrade_rank_elite_healer
structures/kushites/temple_amun.xmlstructures/fndn_9x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_curve.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_curve.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_curve.xml (revision 23865)
@@ -1,34 +1,31 @@
12106
-
- Palisade Curve
- 27props/special/palisade_rocks_curve.xml80.752.80.5
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_gate.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_gate.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_gate.xml (revision 23865)
@@ -1,56 +1,56 @@
11207200.64
- Gates
- Palisade Gate
+ Palisade Gatestructures/palisades_gate
+ Allow units access through Palisades. Can be locked to prevent access.
+ Gatestructures/wooden_gate.png
- Allow units access through a palisade wall. Can be locked to prevent access.4interface/complete/building/complete_gate.xmlactor/gate/stonegate_close.xmlactor/gate/stonegate_open.xmlinterface/select/building/sel_gate.xmlinterface/select/building/sel_gate.xml8structures/fndn_3x1.xmlprops/special/palisade_rocks_gate.xml14
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_short.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_short.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_short.xml (revision 23865)
@@ -1,35 +1,32 @@
4460.25
-
- Palisade
- 17props/special/palisade_rocks_short.xmlstructures/fndn_1x1pal.xml4
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_straight.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_straight.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_straight.xml (revision 23865)
@@ -1,28 +1,25 @@
12106
-
- Palisade
- 27props/special/palisade_rocks_straight.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_hall.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_hall.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_hall.xml (revision 23865)
@@ -1,62 +1,63 @@
Hall3002502508.0103000decay|rubble/rubble_stone_6x6pers
- ConquestCriticalGate of All NationsDuvarθi Visadahyu
+ ConquestCritical
+ Hallstructures/pers_hall.pngTrain Champions.50500.7
units/{civ}_kardakes_hoplite
units/{civ}_kardakes_skirmisher
units/{civ}_champion_elephant
interface/complete/building/complete_broch.xmlfalse38structures/persians/hall.xmlstructures/fndn_7x7.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_military_colony.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_military_colony.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_military_colony.xml (revision 23865)
@@ -1,28 +1,27 @@
12ptolKlērouchia
- This is the Ptolemaic expansion building, similar to Civic Centers for other factions. It is weaker and carries a smaller territory influence, but is cheaper and built faster. Train settler-soldiers of various nationalities.
units/{civ}_support_female_citizen
units/{civ}_infantry_spearman_merc_b
units/{civ}_infantry_swordsman_merc_b
units/{civ}_cavalry_spearman_merc_b
units/{civ}_cavalry_javelinist_merc_b
structures/ptolemies/military_colony.xmlstructures/fndn_9x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml (revision 23865)
@@ -1,91 +1,90 @@
15355473own neutral enemy6006.70.75romestructures/rome_wallset_siegeSiege WallMūrus Circummūnītiōnis
- SiegeWall
+ SiegeWallstructures/siege_wall.png
- A wooden and turf palisade buildable in enemy and neutral territories.1207.705.7085.70-85.7045.70-45.70structures/rome_siege_wall_gate800upgradingstructures/romans/siege_wall_long.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_temple_mars.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_temple_mars.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_temple_mars.xml (revision 23865)
@@ -1,25 +1,27 @@
12.030romeTemple of MarsAedēs Mārtiālis
+ -Town City TempleOfMars
+ phase_cityfalse52structures/romans/temple_mars.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart_gerousia.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart_gerousia.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart_gerousia.xml (revision 23865)
@@ -1,47 +1,51 @@
+
+ Council
+ 2001002008.02000spart
- ConquestCriticalSpartan SenateGerousiaTrain Heroes.
+ ConquestCritical
+ Councilstructures/tholos.png20400.7
units/{civ}_hero_leonidas
interface/complete/building/complete_tholos.xmlstructures/spartans/gerousia.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml (revision 23865)
@@ -1,57 +1,57 @@
1own neutralColonyCivilCentre1203002002002002000decay|rubble/rubble_stone_5x5Military Colony
- Colonytemplate_structure_civic_civil_centre_military_colony
+ Colonystructures/military_settlement.pngphase_town404040
-phase_town_{civ}
-phase_city_{civ}
upgrade_rank_advanced_mercenary
interface/complete/building/complete_gymnasium.xml180
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml (revision 23865)
@@ -1,44 +1,43 @@
4252land-shoreWall12001000decay|rubble/rubble_stone_2x2
- Palisade
- Wooden Wall
+ Palisadetemplate_structure_defensive_palisade
- Wall off your town for a stout defense.
+ Wall off an area.
+ Palisadegaia/special_palisade.png
- phase_village4.5interface/complete/building/complete_wall.xml5.020
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_spikes_tall.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_spikes_tall.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_spikes_tall.xml (revision 23865)
@@ -1,33 +1,33 @@
1292.00.5
- Tall Spikes
+ Tall Spikes2props/special/palisade_tall_spikes.xml110.5
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_apadana.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_apadana.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_apadana.xml (revision 23865)
@@ -1,69 +1,70 @@
- Apadana
+ Palace300103002008.0103000decay|rubble/rubble_stone_6x6persThrone HallApadāna
- Apadana ConquestCritical Palace
+ Train Champions and Heroes.
+ ConquestCritical
+ Palacestructures/palace.png
- "Satrapy Tribute": Gain a trickle of food, wood, stone, and metal resources. Train Champions and Heroes.60400.8
units/{civ}_champion_infantry
units/{civ}_hero_cyrus
units/{civ}_hero_darius
units/{civ}_hero_xerxes
persians/immortals
1.01.00.750.751000true48structures/persians/apadana.xmlstructures/fndn_8x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_lighthouse.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_lighthouse.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_lighthouse.xml (revision 23865)
@@ -1,62 +1,61 @@
own ally neutralshoreLighthouse2002002008.0decay|rubble/rubble_stone_4x6ptolLighthousePharos
- Lighthouse
- -City Town
- Build along the shore to reveal the shorelines over the entire map. Very large vision range: 180 meters.
+ Build upon a shoreline in own, neutral, or allied territory. Reveal the shorelines over the entire map. Very large vision range.
+ -City Town Lighthousestructures/lighthouse.pngphase_town4040true0.0interface/complete/building/complete_temple.xml0180truestructures/ptolemies/lighthouse.xmlstructures/fndn_4x4_dock.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_gate.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_gate.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_gate.xml (revision 23865)
@@ -1,54 +1,54 @@
15355473own neutral enemy80012.50.75romeSiege Wall GatePorta Circummūnītiōnis
- SiegeWall
+ SiegeWallstructures/wooden_gate.png16013.5structures/romans/siege_wall_gate.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_tower.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_tower.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_tower.xml (revision 23865)
@@ -1,53 +1,53 @@
15355473own neutral enemy100012.50.75romeSiege Wall TowerTurris Circummūnītiōnis
- SiegeWall
+ SiegeWallstructures/siege_wall.png20013.5structures/romans/siege_wall_tower.xml6
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_military_colony.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_military_colony.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_military_colony.xml (revision 23865)
@@ -1,26 +1,25 @@
8seleKlērouchia
- This is the Seleucid expansion building, similar to Civic Centers for other factions. It is weaker and carries a smaller territory influence, but is cheaper and built faster. Train settler-soldiers of various nationalities.
units/{civ}_infantry_swordsman_merc_b
units/{civ}_infantry_archer_merc_b
units/{civ}_cavalry_spearman_merc_b
structures/seleucids/military_colony.xmlstructures/fndn_9x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 23865)
@@ -1,138 +1,138 @@
FemaleCitizen1401901005551530.012.00.072.00.012002000075.01.59.81falseHumanoutline_border.pngoutline_border_mask.png0.17531own neutralCivilCentreCivilCentre20025005.0205005005005008.0200.1UnitSupport Infantry Cavalry113000decay|rubble/rubble_stone_6x6Civic Centertemplate_structure_civic_civil_centre
- Build to acquire large tracts of territory. Train citizens.
+ Build in own or neutral territory. Acquire large tracts of territory. Territory root. Dropsite for food, wood, stone, and metal. Train Citizens and research technologies. Garrison Soldiers for additional arrows.CivCentreDefensive CivilCentrestructures/civic_centre.png2001001001000.8
units/{civ}_support_female_citizen
phase_town_{civ}
phase_city_{civ}
unlock_spies
spy_counter
food wood stone metaltrueinterface/complete/building/complete_civ_center.xmlinterface/alarm/alarm_alert_0.xmlinterface/alarm/alarm_alert_1.xmltrue1401000090structures/fndn_8x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml (revision 23865)
@@ -1,77 +1,77 @@
structures/temple_heal
Temple520030012.0200.1UnitSupport Infantry Cavalry322000decay|rubble/rubble_stone_4x6Templetemplate_structure_civic_temple
- Train Healers and research healing technologies. Garrison units to heal them at a quick rate.
+ Train Healers and research healing technologies.Town Templestructures/temple.pngphase_town600.8
units/{civ}_support_healer_b
heal_range
heal_range_2
heal_rate
heal_rate_2
heal_temple
health_regen_units
interface/complete/building/complete_temple.xmlfalse403000040structures/fndn_4x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_watchtower.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_watchtower.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_watchtower.xml (revision 23865)
@@ -1,27 +1,27 @@
181512.0
- Watchtower
+ Watchtower3props/special/palisade_rocks_watchtower.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_tacara.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_tacara.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_tacara.xml (revision 23865)
@@ -1,45 +1,46 @@
300102004008.0103000decay|rubble/rubble_stone_6x6persPalaceTaçara
- ConquestCritical Palace
+ ConquestCritical
+ Palacestructures/palace.png4080true48structures/persians/palace.xmlstructures/fndn_6x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml (revision 23865)
@@ -1,129 +1,130 @@
152521510.025.00.060.00.012002000075.01.59.81falseoutline_border.pngoutline_border_mask.png0.1751151neutral enemyArmyCampArmyCamp80150010.03.0525050012.040Support Infantry Cavalry Siege162500decay|rubble/rubble_rome_sbromeArmy CampCastra
- ArmyCamp ConquestCritical
- structures/roman_camp.pngBuild in neutral or enemy territory. Train Citizen-Soldiers and construct Siege Engines. Garrison Soldiers for additional arrows.
+ ConquestCritical
+ ArmyCamp
+ structures/roman_camp.png1001000.7
units/{civ}_infantry_swordsman_b
units/{civ}_infantry_spearman_a
units/{civ}_infantry_javelinist_b
units/{civ}_cavalry_spearman_b
units/{civ}_siege_ballista_packed
units/{civ}_siege_scorpio_packed
units/{civ}_siege_oxybeles_packed
units/{civ}_siege_lithobolos_packed
units/{civ}_siege_ram
units/{civ}_siege_tower
interface/complete/building/complete_broch.xml37.560structures/romans/camp.xmlstructures/fndn_8x8.xml29.58
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_short.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_short.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_short.xml (revision 23865)
@@ -1,53 +1,52 @@
15355473own neutral enemy2006.70.75romestructures/rome_wallset_siegeSiege WallMūrus Circummūnītiōnis
- SiegeWall
+ SiegeWallstructures/siege_wall.png
- A wooden and turf palisade buildable in enemy and neutral territories.407.7structures/romans/siege_wall_short.xmlstructures/fndn_1x1.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_wallset_siege.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_wallset_siege.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_wallset_siege.xml (revision 23865)
@@ -1,23 +1,24 @@
romeSiege WallMūrus Circummūnītiōnis
+ Wall off an area. Build in own, neutral, or enemy territory.
+ SiegeWallstructures/siege_wall.png
- A wooden and turf palisade buildable in enemy and neutral territories.phase_citystructures/rome_siege_wall_towerstructures/rome_siege_wall_gatestructures/rome_army_campstructures/rome_siege_wall_longstructures/rome_siege_wall_mediumstructures/rome_siege_wall_short1.000.05
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/wallset_palisade.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/wallset_palisade.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/wallset_palisade.xml (revision 23865)
@@ -1,22 +1,21 @@
- Wooden Wall
- Palisade
- -StoneWall Palisade
+ Palisade
+ -Wall Palisadestructures/palisade_wall.pngphase_villagestructures/palisades_towerstructures/palisades_gatestructures/palisades_fortstructures/palisades_longstructures/palisades_mediumstructures/palisades_shortstructures/palisades_curvestructures/palisades_end
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_stoa.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_stoa.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_stoa.xml (revision 23865)
@@ -1,64 +1,64 @@
Stoa1015010015010.0100.1UnitSupport Infantry Cavalry122500decay|rubble/rubble_stone_6x4Stoatemplate_structure_civic_stoa
- Recruit special units.
+ Train Mercenary Champions.
+ Town Stoagaia/special_stoa.pngphase_town
- Town20300.7
units/{civ}_black_cloak
units/{civ}_thureophoros
units/{civ}_thorakites
false406553540structures/fndn_8x4.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_tower.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_tower.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_tower.xml (revision 23865)
@@ -1,37 +1,33 @@
7580.75
-
- Palisade
- Tower
- 19props/special/palisade_rocks_tower.xmlstructures/fndn_1x1pal.xml3
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_ishtar_gate.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_ishtar_gate.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_ishtar_gate.xml (revision 23865)
@@ -1,55 +1,55 @@
structures/loyalty_regen
- UniqueBuilding
+ IshtarGate2002505008.0102000pers
- Persian Special Building
- Ishtar Gate of Babylon
- Increases the loyalty regeneration of nearby structures, making them harder to capture.
+ Ishtar Gate of Babylon
+ Territory root.ConquestCritical
+ IshtarGatestructures/pers_gate.png50100truespecial/pers_ishtar_gate.xmlstructures/fndn_9x3_wall.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_arch.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_arch.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_arch.xml (revision 23865)
@@ -1,48 +1,49 @@
2002004008.02000decay|rubble/rubble_stone_4x4romeTriumphal ArchArcus Triumphālis
- ConquestCritical
+ Territory root.
+ TriumphalArchstructures/arch.png4080interface/complete/building/complete_theater.xmltruestructures/romans/triumphal_arch.xmlstructures/fndn_5x3.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_medium.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_medium.xml (revision 23865)
@@ -1,71 +1,70 @@
15355473own neutral enemy4006.70.75romestructures/rome_wallset_siegeSiege WallMūrus Circummūnītiōnis
- SiegeWall
+ SiegeWallstructures/siege_wall.png
- A wooden and turf palisade buildable in enemy and neutral territories.807.705.7045.70-45.70structures/romans/siege_wall_medium.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_temple_vesta.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_temple_vesta.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_temple_vesta.xml (revision 23865)
@@ -1,28 +1,29 @@
structures/loyalty_regen
- UniqueBuilding
+ TempleOfVesta12.0decay|rubble/rubble_stone_3x3Temple of Vesta
- structures/temple_vesta.pngAedēs Vestālis
+ TempleOfVesta
+ structures/temple_vesta.pngstructures/romans/temple_vesta.xmlstructures/fndn_5x5.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart_syssiton.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart_syssiton.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart_syssiton.xml (revision 23865)
@@ -1,58 +1,59 @@
2002002008.0102000decay|rubble/rubble_stone_4x6spart
- ConquestCritical SyssitonMilitary Mess HallSyssitionTrain Champions and Heroes.
+ ConquestCritical
+ Syssitonstructures/syssition.png40400.7
units/{civ}_champion_infantry_spear
units/{civ}_hero_leonidas
units/{civ}_hero_brasidas
units/{civ}_hero_agis
hellenes/spartans_agoge
interface/complete/building/complete_gymnasium.xmlstructures/spartans/syssiton.xmlstructures/fndn_5x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml (revision 23865)
@@ -1,82 +1,82 @@
House3004.0530755.0300.1UnitSupport+!Elephant1800decay|rubble/rubble_stone_2x2House
- Increase the population limit.
- Village Housetemplate_structure_civic_house
+ Village Housestructures/house.png
+ phase_village15
units/{civ}_support_female_citizen_house
health_females_01
pop_house_01
pop_house_02
unlock_females_house
interface/complete/building/complete_house.xml6.00.68.010false166553520structures/fndn_3x3.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml (revision 23865)
@@ -1,71 +1,72 @@
00012002000075.01.539.81falseHumanoutline_border.pngoutline_border_mask.png0.17511InfantryTowerTower600.1UnitSupport Infantry021000decay|rubble/rubble_stone_2x2
+ TowerTowerinterface/complete/building/complete_tower.xml6.00.618.080structures/fndn_3x3.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_stone.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_stone.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_stone.xml (revision 23865)
@@ -1,59 +1,59 @@
1276101515010010015.051000
- Defense Tower
+ Stone Towertemplate_structure_defensive_tower_stoneGarrison Infantry for additional arrows. Needs the “Murder Holes” technology to protect its foot.Town StoneTowerstructures/defense_tower.pngphase_town2020
tower_watch
tower_crenellations
tower_range
tower_murderholes
tower_armour
false3230000
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_medium.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_medium.xml (revision 23865)
@@ -1,34 +1,37 @@
structures/wall_garrisoned
24243Ranged+Infantry0.1Unit022000decay|rubble/rubble_stone_wall_medium
+
+ WallMedium
+ 5structures/fndn_6x3_wall.xml24
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml (revision 23865)
@@ -1,79 +1,79 @@
Trader+!Ship-1-1100Market1503008.01500decay|rubble/rubble_stone_5x5
- BarterMarketMarkettemplate_structure_economic_market
+ Barter resources. Establish trade routes. Train Traders and research trade and barter technologies.
+ Barter
+ Trade Town Marketstructures/market.pngphase_town
- Create trade units to trade between other markets. Barter resources. Research trading and bartering improvements.
- Town Market60land0.20.7
unlock_shared_los
unlock_shared_dropsites
trade_convoys_speed
trade_convoys_armor
trade_gain_01
trade_gain_02
trade_commercial_treaty
units/{civ}_support_trader
interface/complete/building/complete_market.xmlinterface/alarm/alarm_alert_0.xmlinterface/alarm/alarm_alert_1.xmlfalse403000032structures/fndn_8x8.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_sentry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_sentry.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_sentry.xml (revision 23865)
@@ -1,70 +1,71 @@
-5-5-2970109401009.03500Sentry Towertemplate_structure_defensive_tower_sentryGarrison Infantry for additional arrows. Needs the “Murder Holes” technology to protect its foot.Village SentryTowerstructures/sentry_tower.png
+ phase_village20
tower_watch
false1630000structures/{civ}_defense_towerReinforce with stone and upgrade to a defense tower.phase_town50100upgrading
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_long.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_long.xml (revision 23865)
@@ -1,49 +1,49 @@
structures/wall_garrisoned
36365Ranged+Infantry0.1Unit023000decay|rubble/rubble_stone_wall_long
- LongWall
- Long wall segments can be converted to gates.
+ Can be converted into a Gate.
+ WallLong7structures/{civ}_wall_gate
- This will allow you to let units circulate through your fortifications.
+ Allow units access through Walls. Can be locked to prevent access.60upgradingstructures/fndn_9x3_wall.xml36
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml (revision 23865)
@@ -1,72 +1,73 @@
FemaleCitizen60100100Farmstead300451008.0900decay|rubble/rubble_stone_4x4
- DropsiteFoodFarmsteadtemplate_structure_economic_farmstead
- structures/farmstead.png
- Dropsite for the food resource. Research food gathering improvements.
+ Dropsite for food. Research food gathering technologies.
+ DropsiteFoodVillage Farmstead
+ structures/farmstead.png
+ phase_village20
gather_wicker_baskets
gather_farming_plows
gather_farming_training
gather_farming_fertilizer
gather_farming_harvester
foodtrueinterface/complete/building/complete_farmstead.xmlinterface/alarm/alarm_alert_0.xmlinterface/alarm/alarm_alert_1.xmlfalse203000020structures/fndn_5x5.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_outpost.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_outpost.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_outpost.xml (revision 23865)
@@ -1,72 +1,72 @@
520115116551300OutpostOutpost50own neutral408015.01800Outposttemplate_structure_defensive_tower_outpostBuild in own or neutral territory. Slowly converts to Gaia while in neutral territory.
- Village -Tower Outpost
+ -Tower Outpoststructures/outpost.png16
tower_vision
tower_decay
2
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_gate.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_gate.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_gate.xml (revision 23865)
@@ -1,50 +1,50 @@
3860202500decay|rubble/rubble_stone_wall_long
- Gates
- City Gate
+ Gatetemplate_structure_defensive_wall_gate
+ Allow units access through Walls. Can be locked to prevent access.
+ Gatestructures/gate.png
- Allow units access through a city wall. Can be locked to prevent access.12interface/complete/building/complete_gate.xmlinterface/complete/building/complete_gate.xmlactor/gate/stonegate_close.xmlactor/gate/stonegate_open.xmlinterface/select/building/sel_gate.xmlinterface/select/building/sel_gate.xmlstructures/fndn_9x3_wall.xml36
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_tower.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_tower.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_tower.xml (revision 23865)
@@ -1,83 +1,83 @@
0.08.00.072.012.012002000075.01.539.81falseHumanoutline_border.pngoutline_border_mask.png0.17501Infantry809020.1UnitSupport Infantry024000decay|rubble/rubble_stone_wall_towerWall Turrettemplate_structure_defensive_wall_tower
- Garrison Infantry for additional arrows. Needs the “Murder Holes” technology to protect its foot.
+ Garrison Infantry for additional arrows.WallTowerstructures/tower.png18
pair_walls_01
interface/complete/building/complete_tower.xml60structures/fndn_3x3_tower.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml (revision 23865)
@@ -1,45 +1,45 @@
land-shoreWall12008.0
- StoneWall
- Stone Wall
+ Walltemplate_structure_defensive_wallWall off your town for a stout defense.
+ Wallstructures/wall.pngphase_town4.5interface/complete/building/complete_wall.xml5.0false206553520
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_short.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_short.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_short.xml (revision 23865)
@@ -1,24 +1,27 @@
12121000decay|rubble/rubble_stone_wall_short
+
+ WallShort
+ 3structures/fndn_3x3_wall.xml12
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml (revision 23865)
@@ -1,83 +1,84 @@
FemaleCitizen60100100Storehouse300401008.0800decay|rubble/rubble_stone_3x3
- DropsiteWood DropsiteMetal DropsiteStoneStorehousetemplate_structure_economic_storehouse
- structures/storehouse.png
- Dropsite for wood, stone, and metal resources. Research gathering improvements for these resources.
+ Dropsite for wood, stone, and metal. Research gathering technologies.
+ DropsiteWood DropsiteMetal DropsiteStoneVillage Storehouse
+ structures/storehouse.png
+ phase_village20
gather_lumbering_ironaxes
gather_lumbering_strongeraxes
gather_lumbering_sharpaxes
gather_mining_servants
gather_mining_serfs
gather_mining_slaves
gather_mining_wedgemallet
gather_mining_shaftmining
gather_mining_silvermining
gather_capacity_basket
gather_capacity_wheelbarrow
gather_capacity_carts
wood stone metaltrueinterface/complete/building/complete_storehouse.xmlinterface/alarm/alarm_alert_0.xmlinterface/alarm/alarm_alert_1.xmlfalse203000020structures/fndn_3x3.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml (revision 23865)
@@ -1,58 +1,59 @@
501005.0500decay|rubble/rubble_stone_3x3Corraltemplate_structure_resource_corral
- Raise herd animals for food. Task domestic animals here to gain a trickle of food or other bonus (Not yet implemented).
+ Raise Domestic Animals for food.Village Corralstructures/corral.png
+ phase_village200.7
gaia/fauna_sheep_trainable
gather_animals_stockbreeding
speed_cavalry_01
speed_cavalry_02
interface/complete/building/complete_corral.xmlfalse203000020structures/fndn_3x3.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_rotarymill.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_rotarymill.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_rotarymill.xml (revision 23865)
@@ -1,48 +1,49 @@
structures/rotary_mill
10022001006.0Rotary Milltemplate_structure_special_rotarymill
+ RotaryMillstructures/rotarymill.png4020interface/complete/building/complete_ffactri.xml8.032structures/fndn_6x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml (revision 23865)
@@ -1,38 +1,38 @@
541000
- Field Palisade SiegeWall StoneWall
+ Field Palisade WallHumanSoldier ChampionChampion Unitphase_city256x256/arrow.png256x256/arrow_mask.png1501010020attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_wallset.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_wallset.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_wallset.xml (revision 23865)
@@ -1,26 +1,26 @@
gaia
- StoneWall
- City Wall
+ Wall
+ Wall off an area.
+ Wallstructures/wall.pngphase_town
- Wall off your town for a stout defense.truefalsefalsefalsefalse0.850.05
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml (revision 23865)
@@ -1,84 +1,83 @@
own ally neutralshoreDock51502008.02500decay|rubble/rubble_stone_4x4Docktemplate_structure_military_dock
- Build upon a shoreline to construct naval vessels and to open sea trade. Research naval improvements.
- Naval NavalMarket
- Economic Village Market Dock
+ Build upon a shoreline in own, neutral, or allied territory. Dropsite for food, wood, stone, and metal. Establish trade routes. Construct Ships and research Ship technologies.
+ Economic Naval Trade Village Dockstructures/dock.png40land naval0.2true0.00.8
units/{civ}_ship_fishing
units/{civ}_ship_merchant
units/{civ}_ship_bireme
units/{civ}_ship_trireme
gather_capacity_fishing
gather_fishing_net
training_naval_architects
armor_ship_reinforcedhull
armor_ship_hypozomata
armor_ship_hullsheathing
shipfood wood stone metaltrueinterface/complete/building/complete_dock.xml40structures/fndn_4x4_dock.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_library.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_library.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_library.xml (revision 23865)
@@ -1,53 +1,53 @@
structures/library
Library2002002009.0decay|rubble/rubble_stone_4x6Librarytemplate_structure_special_library
- Research special technologies.
+ Librarystructures/library_scroll.png4040
successors/special_hellenistic_metropolis
interface/complete/building/complete_temple.xml50structures/fndn_7x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml (revision 23865)
@@ -1,103 +1,103 @@
31152.541000
- Field Palisade SiegeWall StoneWall
+ Field Palisade Wall100.00.00.021151007.5160Human CitizenSoldierCitizen Soldier CavalryCavalryBasic
special/formations/wedge
130105001502.01.0520202020128x256/ellipse.png128x256/ellipse_mask.pngvoice/{lang}/civ/civ_{phenotype}_walk.xmlvoice/{lang}/civ/civ_{phenotype}_attack.xmlvoice/{lang}/civ/civ_{phenotype}_gather.xmlvoice/{lang}/civ/civ_{phenotype}_garrison.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlattack/impact/arrow_impact.xmlattack/weapon/sword_attack.xmlattack/weapon/bow_attack.xmlactor/fauna/death/death_horse.xmlinterface/alarm/alarm_create_cav.xml6.5292
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_ram.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_ram.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_ram.xml (revision 23865)
@@ -1,56 +1,56 @@
0.00.0150.06.57501500
- Gates Defensive Structure
+ Gate Defensive StructureField Organic30350200100.1UnitSupport Infantry02400Melee RamBattering RamCannot attack Fields or Organic Units.attack/siege/ram_move.xmlattack/siege/ram_attack_order.xmlattack/siege/ram_trained.xmlattack/siege/ram_attack.xml0.880
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml (revision 23865)
@@ -1,81 +1,81 @@
structures/barracks_xp_trickle
15020010012.010Infantry Cavalry2000decay|rubble/rubble_stone_4x4Barrackstemplate_structure_military_barracks
- Train citizen-soldiers. Research training improvements.
+ Train Citizen Soldiers and research technologies.Village Barracksstructures/barracks.pngphase_village40200.8
units/{civ}_infantry_spearman_b
units/{civ}_infantry_pikeman_b
units/{civ}_infantry_axeman_b
units/{civ}_infantry_swordsman_b
units/{civ}_infantry_javelinist_b
units/{civ}_infantry_slinger_b
units/{civ}_infantry_archer_b
units/{civ}_cavalry_axeman_b
units/{civ}_cavalry_swordsman_b
units/{civ}_cavalry_spearman_b
units/{civ}_cavalry_javelinist_b
units/{civ}_cavalry_archer_b
units/{civ}_champion_infantry_barracks
units/{civ}_champion_infantry_swordsman_barracks
units/{civ}_champion_maiden_barracks
units/{civ}_champion_cavalry_barracks
units/{civ}_champion_chariot_barracks
units/{civ}_war_dog_b
heal_barracks
training_conscription
unlock_champion_units
archery_tradition
interface/complete/building/complete_barracks.xml32structures/fndn_6x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_amphitheater.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_amphitheater.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_amphitheater.xml (revision 23865)
@@ -1,52 +1,52 @@
Amphitheater50050050020.02000
- AmphitheaterAmphitheatertemplate_structure_special_amphitheater
+ Amphitheaterstructures/theater.png1001000.8
units/{civ}_champion_infantry_spear_gladiator
units/{civ}_champion_infantry_sword_gladiator
interface/complete/building/complete_tholos.xml100structures/fndn_6x6.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml (revision 23865)
@@ -1,103 +1,103 @@
152532102
structures/wonder_pop_1
structures/wonder_pop_2
Wonder20005.0100010001500100010.0500.1UnitSupport Soldier525000decay|rubble/rubble_stone_6x6
- ConquestCriticalWondertemplate_structure_wonderBring glory to your civilization and add large tracts of land to your empire.
+ ConquestCriticalCity Wonderstructures/wonder.pngphase_city300200300200
pop_wonder
1.01.01.01.02000interface/complete/building/complete_wonder.xml6.00.612.0true1006553572structures/fndn_9x9.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml (revision 23865)
@@ -1,127 +1,127 @@
24152.541000
- Field Palisade SiegeWall StoneWall
+ Field Palisade Wall50.00.00.021.0
structures/{civ}_civil_centre
structures/{civ}_crannog
structures/{civ}_military_colony
structures/{civ}_house
structures/{civ}_storehouse
structures/{civ}_farmstead
structures/{civ}_field
structures/{civ}_corral
structures/{civ}_outpost
structures/wallset_palisade
structures/{civ}_sentry_tower
structures/{civ}_dock
structures/{civ}_barracks
structures/{civ}_blacksmith
structures/{civ}_temple
structures/{civ}_market
structures/{civ}_defense_tower
structures/{civ}_wallset_stone
structures/{civ}_workshop
structures/{civ}_fortress
structures/{civ}_wonder
10500002.580Human CitizenSoldierCitizen Worker Soldier InfantryInfantryBasic1005000upright1002.01.00.50.2510.7550.520.5voice/{lang}/civ/civ_{phenotype}_walk.xmlvoice/{lang}/civ/civ_{phenotype}_attack.xmlvoice/{lang}/civ/civ_{phenotype}_gather.xmlvoice/{lang}/civ/civ_{phenotype}_repair.xmlvoice/{lang}/civ/civ_{phenotype}_build.xmlvoice/{lang}/civ/civ_{phenotype}_garrison.xmlactor/human/movement/walk.xmlactor/human/movement/run.xmlattack/impact/arrow_impact.xmlattack/weapon/sword_attack.xmlattack/weapon/bow_attack.xmlactor/human/death/{phenotype}_death.xmlresource/construction/con_wood.xmlresource/foraging/forage_leaves.xmlresource/farming/farm.xmlresource/lumbering/lumbering.xmlresource/mining/pickaxe.xmlresource/mining/mining.xmlresource/mining/mining.xmlinterface/alarm/alarm_create_infantry.xml80
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special.xml (revision 23865)
@@ -1,56 +1,54 @@
203033103Special8.050.1UnitSupport Infantry Cavalry122000decay|rubble/rubble_stone_6x6
- Special Building
- This is a special building unique to a particular civilization.
- SpecialBuilding
+ Special StructureCityphase_cityinterface/complete/building/complete_broch.xmlfalse3840000structures/fndn_5x5.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_theater.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_theater.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_theater.xml (revision 23865)
@@ -1,46 +1,46 @@
structures/theater
Theater50050050010.02000
- TheaterTheatertemplate_structure_special_theater
+ Theaterstructures/theater.png100100interface/complete/building/complete_greek_theater.xml100
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml (revision 23864)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml (revision 23865)
@@ -1,80 +1,80 @@
1020151041000
- Field Palisade SiegeWall StoneWall
+ Field Palisade Wall
units/heroes/hero_garrison
240100250600HumanSoldier HeroHerotechnologies/laurel_wreath.pngphase_city400100025hero256x256/star.png256x256/star_mask.pnginterface/alarm/alarm_create_infantry.xmlvoice/{lang}/civ/civ_{phenotype}_heal.xmlvoice/{lang}/civ/civ_{phenotype}_walk.xmlvoice/{lang}/civ/civ_{phenotype}_attack.xmlvoice/{lang}/civ/civ_{phenotype}_gather.xmlvoice/{lang}/civ/civ_{phenotype}_repair.xmlvoice/{lang}/civ/civ_{phenotype}_garrison.xmlattack/weapon/sword_attack.xmlattack/impact/arrow_impact.xmlattack/weapon/bow_attack.xmlactor/human/movement/walk.xmlactor/human/movement/walk.xmlactor/human/death/{phenotype}_death.xmlHero88