Index: ps/trunk/binaries/data/mods/public/gui/common/tooltips.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 17782)
+++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 17783)
@@ -1,540 +1,543 @@
const g_CostDisplayNames = {
"food": '[icon="iconFood"]',
"wood": '[icon="iconWood"]',
"stone": '[icon="iconStone"]',
"metal": '[icon="iconMetal"]',
"population": '[icon="iconPopulation"]',
"time": '[icon="iconTime"]'
};
const g_TooltipTextFormats = {
"unit": ['[font="sans-10"][color="orange"]', '[/color][/font]'],
"header": ['[font="sans-bold-13"]', '[/font]'],
"body": ['[font="sans-13"]', '[/font]']
};
function damageValues(dmg)
{
if (!dmg)
return [0, 0, 0];
return [dmg.hack || 0, dmg.pierce || 0, dmg.crush || 0];
}
function damageTypeDetails(dmg)
{
if (!dmg)
return '[font="sans-12"]' + translate("(None)") + '[/font]';
let dmgArray = [];
if (dmg.hack)
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
"damage": dmg.hack.toFixed(1),
"damageType": g_TooltipTextFormats.unit[0] + translate("Hack") + g_TooltipTextFormats.unit[1]
}));
if (dmg.pierce)
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
"damage": dmg.pierce.toFixed(1),
"damageType": g_TooltipTextFormats.unit[0] + translate("Pierce") + g_TooltipTextFormats.unit[1]
}));
if (dmg.crush)
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
"damage": dmg.crush.toFixed(1),
"damageType": g_TooltipTextFormats.unit[0] + translate("Crush") + g_TooltipTextFormats.unit[1]
}));
return dmgArray.join(translate(", "));
}
function attackRateDetails(entState, type)
{
+ // Either one arrow shot by UnitAI,
let time = entState.attack[type].repeatTime / 1000;
let timeString = sprintf(translatePlural("%(time)s %(second)s", "%(time)s %(second)s", time), {
"time": time,
"second": g_TooltipTextFormats.unit[0] + translatePlural("second", "seconds", time) + g_TooltipTextFormats.unit[1]
});
+ // or multiple arrows shot by BuildingAI
if (!entState.buildingAI)
return timeString;
- let arrows = Math.max(entState.buildingAI.arrowCount, entState.buildingAI.defaultArrowCount);
+ let arrows = entState.buildingAI.arrowCount;
let arrowString = sprintf(translatePlural("%(arrowcount)s %(arrow)s", "%(arrowcount)s %(arrow)s", arrows), {
"arrowcount": arrows,
"arrow": g_TooltipTextFormats.unit[0] + translatePlural("arrow", "arrows", arrows) + g_TooltipTextFormats.unit[1]
});
+
return sprintf(translate("%(arrowString)s / %(timeString)s"), {
"arrowString": arrowString,
"timeString": timeString
});
}
// Converts an armor level into the actual reduction percentage
function armorLevelToPercentageString(level)
{
return (100 - Math.round(Math.pow(0.9, level) * 100)) + "%";
// return sprintf(translate("%(armorPercentage)s%"), { armorPercentage: (100 - Math.round(Math.pow(0.9, level) * 100)) }); // Not supported by our sprintf implementation.
}
function getArmorTooltip(dmg)
{
let label = g_TooltipTextFormats.header[0] + translate("Armor:") + g_TooltipTextFormats.header[1];
if (!dmg)
return sprintf(translate("%(label)s %(details)s"), {
"label": label,
"details": '[font="sans-12"]' + translate("(None)") + '[/font]'
});
let dmgArray = [];
if (dmg.hack)
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), {
"damage": dmg.hack,
"damageType": g_TooltipTextFormats.unit[0] + translate("Hack") + g_TooltipTextFormats.unit[1],
"armorPercentage": '[font="sans-10"]' + sprintf(translate("(%(armorPercentage)s)"), { "armorPercentage": armorLevelToPercentageString(dmg.hack) }) + '[/font]'
}));
if (dmg.pierce)
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), {
"damage": dmg.pierce,
"damageType": g_TooltipTextFormats.unit[0] + translate("Pierce") + g_TooltipTextFormats.unit[1],
"armorPercentage": '[font="sans-10"]' + sprintf(translate("(%(armorPercentage)s)"), { "armorPercentage": armorLevelToPercentageString(dmg.pierce) }) + '[/font]'
}));
if (dmg.crush)
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), {
"damage": dmg.crush,
"damageType": g_TooltipTextFormats.unit[0] + translate("Crush") + g_TooltipTextFormats.unit[1],
"armorPercentage": '[font="sans-10"]' + sprintf(translate("(%(armorPercentage)s)"), { "armorPercentage": armorLevelToPercentageString(dmg.crush) }) + '[/font]'
}));
return sprintf(translate("%(label)s %(details)s"), {
"label": label,
"details": dmgArray.join('[font="sans-12"]' + translate(", ") + '[/font]')
});
}
function damageTypesToText(dmg)
{
if (!dmg)
return '[font="sans-12"]' + translate("(None)") + '[/font]';
let dmgArray = [];
if (dmg.hack)
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
"damage": dmg.hack.toFixed(1),
"damageType": g_TooltipTextFormats.unit[0] + translate("Hack") + g_TooltipTextFormats.unit[1]
}));
if (dmg.pierce)
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
"damage": dmg.pierce.toFixed(1),
"damageType": g_TooltipTextFormats.unit[0] + translate("Pierce") + g_TooltipTextFormats.unit[1]
}));
if (dmg.crush)
dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), {
"damage": dmg.crush.toFixed(1),
"damageType": g_TooltipTextFormats.unit[0] + translate("Crush") + g_TooltipTextFormats.unit[1]
}));
return dmgArray.join('[font="sans-12"]' + translate(", ") + '[/font]');
}
function getAttackTypeLabel(type)
{
if (type === "Charge") return translate("Charge Attack:");
if (type === "Melee") return translate("Melee Attack:");
if (type === "Ranged") return translate("Ranged Attack:");
if (type === "Capture") return translate("Capture Attack:");
warn(sprintf("Internationalization: Unexpected attack type found with code ā%(attackType)sā. This attack type must be internationalized.", { "attackType": type }));
return translate("Attack:");
}
function getAttackTooltip(template)
{
let attacks = [];
if (!template.attack)
return "";
let rateLabel = g_TooltipTextFormats.header[0] + (template.buildingAI ? translate("Interval:") : translate("Rate:")) + g_TooltipTextFormats.header[1];
for (let type in template.attack)
{
if (type == "Slaughter")
continue; // Slaughter is not a real attack, so do not show it.
if (type == "Charge")
continue; // Charging isn't implemented yet and shouldn't be displayed.
let rate = sprintf(translate("%(label)s %(details)s"), {
"label": rateLabel,
"details": attackRateDetails(template, type)
});
let attackLabel = g_TooltipTextFormats.header[0] + getAttackTypeLabel(type) + g_TooltipTextFormats.header[1];
if (type == "Capture")
{
attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), {
"attackLabel": attackLabel,
"details": template.attack[type].value,
"rate": rate
}));
continue;
}
if (type != "Ranged")
{
attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), {
"attackLabel": attackLabel,
"details": damageTypesToText(template.attack[type]),
"rate": rate
}));
continue;
}
let realRange = template.attack[type].elevationAdaptedRange;
let range = Math.round(template.attack[type].maxRange);
let rangeLabel = g_TooltipTextFormats.header[0] + translate("Range:") + g_TooltipTextFormats.header[1];
let relativeRange = Math.round((realRange - range));
let meters = g_TooltipTextFormats.unit[0] + translatePlural("meter", "meters", range) + g_TooltipTextFormats.unit[1];
if (relativeRange) // show if it is non-zero
attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rangeLabel)s %(rangeString)s (%(relative)s), %(rate)s"), {
"attackLabel": attackLabel,
"details": damageTypesToText(template.attack[type]),
"rangeLabel": rangeLabel,
"rangeString": sprintf(
translatePlural("%(range)s %(meters)s", "%(range)s %(meters)s", range), {
"range": range,
"meters": meters
}),
"relative": relativeRange > 0 ? "+" + relativeRange : relativeRange,
"rate": rate
}));
else
attacks.push(sprintf(translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(rangeString)s, %(rate)s"), {
"attackLabel": attackLabel,
"damageTypes": damageTypesToText(template.attack[type]),
"rangeLabel": rangeLabel,
"rangeString": sprintf(
translatePlural("%(range)s %(meters)s", "%(range)s %(meters)s", range), {
"range": range,
"meters": meters
}),
rate: rate
}));
}
return attacks.join("\n");
}
function getRepairRateTooltip(rate)
{
return "\n" + sprintf(translate("%(repairRateLabel)s %(value)s %(health)s / %(second)s / %(worker)s"), {
"repairRateLabel": g_TooltipTextFormats.header[0] + translate("Repair Rate:") + g_TooltipTextFormats.header[1],
"value": Math.round(rate * 10) / 10,
"health": g_TooltipTextFormats.unit[0] + translate("health") + g_TooltipTextFormats.unit[1],
"second": g_TooltipTextFormats.unit[0] + translate("second") + g_TooltipTextFormats.unit[1],
"worker": g_TooltipTextFormats.unit[0] + translate("worker") + g_TooltipTextFormats.unit[1]
});
}
function getBuildRateTooltip(rate)
{
return "\n" + sprintf(translate("%(buildRateLabel)s %(value)s %(health)s / %(second)s / %(worker)s"), {
"buildRateLabel": g_TooltipTextFormats.header[0] + translate("Build Rate:") + g_TooltipTextFormats.header[1],
"value": Math.round(rate * 10) / 10,
"health": g_TooltipTextFormats.unit[0] + translate("health") + g_TooltipTextFormats.unit[1],
"second": g_TooltipTextFormats.unit[0] + translate("second") + g_TooltipTextFormats.unit[1],
"worker": g_TooltipTextFormats.unit[0] + translate("worker") + g_TooltipTextFormats.unit[1]
});
}
/**
* Translates a cost component identifier as they are used internally
* (e.g. "population", "food", etc.) to proper display names.
*/
function getCostComponentDisplayName(costComponentName)
{
if (costComponentName in g_CostDisplayNames)
return g_CostDisplayNames[costComponentName];
warn(sprintf("The specified cost component, ā%(component)sā, is not currently supported.", { "component": costComponentName }));
return "";
}
/**
* Multiplies the costs for a template by a given batch size.
*/
function multiplyEntityCosts(template, trainNum)
{
let totalCosts = {};
for (let r in template.cost)
totalCosts[r] = Math.floor(template.cost[r] * trainNum);
return totalCosts;
}
/**
* Helper function for getEntityCostTooltip.
*/
function getEntityCostComponentsTooltipString(template, trainNum, entity)
{
if (!trainNum)
trainNum = 1;
let totalCosts = multiplyEntityCosts(template, trainNum);
totalCosts.time = Math.ceil(template.cost.time * (entity ? Engine.GuiInterfaceCall("GetBatchTime", { "entity": entity, "batchSize": trainNum }) : 1));
let costs = [];
if (totalCosts.food) costs.push(sprintf(translate("%(component)s %(cost)s"), { "component": getCostComponentDisplayName("food"), "cost": totalCosts.food }));
if (totalCosts.wood) costs.push(sprintf(translate("%(component)s %(cost)s"), { "component": getCostComponentDisplayName("wood"), "cost": totalCosts.wood }));
if (totalCosts.metal) costs.push(sprintf(translate("%(component)s %(cost)s"), { "component": getCostComponentDisplayName("metal"), "cost": totalCosts.metal }));
if (totalCosts.stone) costs.push(sprintf(translate("%(component)s %(cost)s"), { "component": getCostComponentDisplayName("stone"), "cost": totalCosts.stone }));
if (totalCosts.population) costs.push(sprintf(translate("%(component)s %(cost)s"), { "component": getCostComponentDisplayName("population"), "cost": totalCosts.population }));
if (totalCosts.time) costs.push(sprintf(translate("%(component)s %(cost)s"), { "component": getCostComponentDisplayName("time"), "cost": totalCosts.time }));
return costs;
}
/**
* Returns an array of strings for a set of wall pieces. If the pieces share
* resource type requirements, output will be of the form '10 to 30 Stone',
* otherwise output will be, e.g. '10 Stone, 20 Stone, 30 Stone'.
*/
function getWallPieceTooltip(wallTypes)
{
let out = [];
let resourceCount = {};
// Initialize the acceptable types for '$x to $y $resource' mode.
for (let resource in wallTypes[0].cost)
if (wallTypes[0].cost[resource])
resourceCount[resource] = [wallTypes[0].cost[resource]];
let sameTypes = true;
for (let i = 1; i < wallTypes.length; ++i)
{
for (let resource in wallTypes[i].cost)
{
// Break out of the same-type mode if this wall requires
// resource types that the first didn't.
if (wallTypes[i].cost[resource] && !resourceCount[resource])
{
sameTypes = false;
break;
}
}
for (let resource in resourceCount)
{
if (wallTypes[i].cost[resource])
resourceCount[resource].push(wallTypes[i].cost[resource]);
else
{
sameTypes = false;
break;
}
}
}
if (sameTypes)
{
for (let resource in resourceCount)
{
let resourceMin = Math.min.apply(Math, resourceCount[resource]);
let resourceMax = Math.max.apply(Math, resourceCount[resource]);
// Translation: This string is part of the resources cost string on
// the tooltip for wall structures.
out.push(sprintf(translate("%(resourceIcon)s %(minimum)s to %(resourceIcon)s %(maximum)s"), {
"resourceIcon": getCostComponentDisplayName(resource),
"minimum": resourceMin,
"maximum": resourceMax
}));
}
}
else
for (let i = 0; i < wallTypes.length; ++i)
out.push(getEntityCostComponentsTooltipString(wallTypes[i]).join(", "));
return out;
}
/**
* Returns the cost information to display in the specified entity's construction button tooltip.
*/
function getEntityCostTooltip(template, trainNum, entity)
{
// Entities with a wallset component are proxies for initiating wall placement and as such do not have a cost of
// their own; the individual wall pieces within it do.
if (template.wallSet)
{
let templateLong = GetTemplateData(template.wallSet.templates.long);
let templateMedium = GetTemplateData(template.wallSet.templates.medium);
let templateShort = GetTemplateData(template.wallSet.templates.short);
let templateTower = GetTemplateData(template.wallSet.templates.tower);
let wallCosts = getWallPieceTooltip([templateShort, templateMedium, templateLong]);
let towerCosts = getEntityCostComponentsTooltipString(templateTower);
return sprintf(translate("Walls: %(costs)s"), { "costs": wallCosts.join(" ") }) + "\n" +
sprintf(translate("Towers: %(costs)s"), { "costs": towerCosts.join(" ") });
}
if (template.cost)
return getEntityCostComponentsTooltipString(template, trainNum, entity).join(" ");
return "";
}
/**
* Returns the population bonus information to display in the specified entity's construction button tooltip.
*/
function getPopulationBonusTooltip(template)
{
let popBonus = "";
if (template.cost && template.cost.populationBonus)
popBonus = "\n" + sprintf(translate("%(label)s %(populationBonus)s"), {
"label": g_TooltipTextFormats.header[0] + translate("Population Bonus:") + g_TooltipTextFormats.header[1],
"populationBonus": template.cost.populationBonus
});
return popBonus;
}
/**
* Returns a message with the amount of each resource needed to create an entity.
*/
function getNeededResourcesTooltip(resources)
{
let formatted = [];
for (let resource in resources)
formatted.push(sprintf(translate("%(component)s %(cost)s"), {
"component": '[font="sans-12"]' + getCostComponentDisplayName(resource) + '[/font]',
"cost": resources[resource]
}));
return '\n\n[font="sans-bold-13"][color="red"]' + translate("Insufficient resources:") + '[/color][/font]\n' + formatted.join(" ");
}
function getSpeedTooltip(template)
{
if (!template.speed)
return "";
let label = g_TooltipTextFormats.header[0] + translate("Speed:") + g_TooltipTextFormats.header[1];
let speeds = [];
if (template.speed.walk)
speeds.push(sprintf(translate("%(speed)s %(movementType)s"), {
"speed": Math.round(template.speed.walk),
"movementType": g_TooltipTextFormats.unit[0] + translate("Walk") + g_TooltipTextFormats.unit[1]
}));
if (template.speed.run)
speeds.push(sprintf(translate("%(speed)s %(movementType)s"), {
"speed": Math.round(template.speed.run),
"movementType": g_TooltipTextFormats.unit[0] + translate("Run") + g_TooltipTextFormats.unit[1]
}));
return sprintf(translate("%(label)s %(speeds)s"), {
"label": label,
"speeds": speeds.join(translate(", "))
});
}
function getHealerTooltip(template)
{
if (!template.healer)
return "";
let healer = [
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", template.healer.HP), {
"label": g_TooltipTextFormats.header[0] + translate("Heal:") + g_TooltipTextFormats.header[1],
"val": template.healer.HP,
// Translation: Short for Health Points (that are healed in one healing action)
"unit": g_TooltipTextFormats.unit[0] + translatePlural("HP", "HP", template.healer.HP) + g_TooltipTextFormats.unit[1]
}),
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", template.healer.Range), {
"label": g_TooltipTextFormats.header[0] + translate("Range:") + g_TooltipTextFormats.header[1],
"val": template.healer.Range,
"unit": g_TooltipTextFormats.unit[0] + translatePlural("meter", "meters", template.healer.Range) + g_TooltipTextFormats.unit[1]
}),
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", template.healer.Rate/1000), {
"label": g_TooltipTextFormats.header[0] + translate("Rate:") + g_TooltipTextFormats.header[1],
"val": template.healer.Rate/1000,
"unit": g_TooltipTextFormats.unit[0] + translatePlural("second", "seconds", template.healer.Rate/1000) + g_TooltipTextFormats.unit[1]
})
];
return healer.join(translate(", "));
}
function getAurasTooltip(template)
{
if (!template.auras)
return "";
let txt = "";
for (let aura in template.auras)
txt += '\n' + sprintf(translate("%(auralabel)s %(aurainfo)s"), {
"auralabel": g_TooltipTextFormats.header[0] + sprintf(translate("%(auraname)s:"), {
"auraname": translate(template.auras[aura].name)
}) + g_TooltipTextFormats.header[1],
"aurainfo": g_TooltipTextFormats.body[0] + translate(template.auras[aura].description) + g_TooltipTextFormats.body[1]
});
return txt;
}
function getEntityNames(template)
{
if (template.name.specific)
{
if (template.name.generic && template.name.specific != template.name.generic)
return sprintf(translate("%(specificName)s (%(genericName)s)"), {
"specificName": template.name.specific,
"genericName": template.name.generic
});
return template.name.specific;
}
if (template.name.generic)
return template.name.generic;
warn("Entity name requested on an entity without a name, specific or generic.");
return translate("???");
}
function getEntityNamesFormatted(template)
{
let names = "";
let generic = template.name.generic;
let specific = template.name.specific;
if (specific)
{
// drop caps for specific name
names += '[font="sans-bold-16"]' + specific[0] + '[/font]' +
'[font="sans-bold-12"]' + specific.slice(1).toUpperCase() + '[/font]';
if (generic)
names += '[font="sans-bold-16"] (' + generic + ')[/font]';
}
else if (generic)
names = '[font="sans-bold-16"]' + generic + "[/font]";
else
names = "???";
return names;
}
function getVisibleEntityClassesFormatted(template)
{
let r = "";
if (template.visibleIdentityClasses && template.visibleIdentityClasses.length)
{
r += '\n' + g_TooltipTextFormats.header[0] + translate("Classes:") + g_TooltipTextFormats.header[1];
let classes = [];
for (let c of template.visibleIdentityClasses)
classes.push(translate(c));
r += ' ' + g_TooltipTextFormats.body[0] + classes.join(translate(", ")) + g_TooltipTextFormats.body[1];
}
return r;
}
Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 17782)
+++ ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 17783)
@@ -1,375 +1,345 @@
//Number of rounds of firing per 2 seconds
const roundCount = 10;
const attackType = "Ranged";
function BuildingAI() {}
BuildingAI.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
BuildingAI.prototype.MAX_PREFERENCE_BONUS = 2;
-/**
- * Initialize BuildingAI Component
- */
BuildingAI.prototype.Init = function()
{
this.currentRound = 0;
- //Arrows left to fire
this.arrowsLeft = 0;
this.targetUnits = [];
};
BuildingAI.prototype.OnOwnershipChanged = function(msg)
{
- // Remove current targets, to prevent them from being added twice
this.targetUnits = [];
-
this.SetupRangeQuery();
this.SetupGaiaRangeQuery();
};
BuildingAI.prototype.OnDiplomacyChanged = function(msg)
{
if (!IsOwnedByPlayer(msg.player, this.entity))
return;
// Remove maybe now allied/neutral units
this.targetUnits = [];
this.SetupRangeQuery();
this.SetupGaiaRangeQuery();
};
-/**
- * Cleanup on destroy
- */
BuildingAI.prototype.OnDestroy = function()
{
if (this.timer)
{
- var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
}
// Clean up range queries
- var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.enemyUnitsQuery)
cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
if (this.gaiaUnitsQuery)
cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
};
/**
* React on Attack value modifications, as it might influence the range
*/
BuildingAI.prototype.OnValueModification = function(msg)
{
if (msg.component != "Attack")
return;
this.targetUnits = [];
this.SetupRangeQuery();
this.SetupGaiaRangeQuery();
};
/**
* Setup the Range Query to detect units coming in & out of range
*/
BuildingAI.prototype.SetupRangeQuery = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.enemyUnitsQuery)
{
cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
this.enemyUnitsQuery = undefined;
}
var cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return;
- var players = [];
+ var enemies = [];
var numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
- for (var i = 1; i < numPlayers; ++i)
- { // Exclude gaia, allies, and self
- // TODO: How to handle neutral players - Special query to attack military only?
+ for (let i = 1; i < numPlayers; ++i)
if (cmpPlayer.IsEnemy(i))
- players.push(i);
- }
+ enemies.push(i);
- if (!players.length)
+ if (!enemies.length)
return;
var range = cmpAttack.GetRange(attackType);
- this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(this.entity, range.min, range.max, range.elevationBonus, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal"));
+ this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
+ this.entity, range.min, range.max, range.elevationBonus,
+ enemies, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal"));
+
cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
};
// Set up a range query for Gaia units within LOS range which can be attacked.
// This should be called whenever our ownership changes.
BuildingAI.prototype.SetupGaiaRangeQuery = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.gaiaUnitsQuery)
{
cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
this.gaiaUnitsQuery = undefined;
}
var cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer || !cmpPlayer.IsEnemy(0))
return;
var range = cmpAttack.GetRange(attackType);
// This query is only interested in Gaia entities that can attack.
- this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(this.entity, range.min, range.max, range.elevationBonus, [0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal"));
+ this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
+ this.entity, range.min, range.max, range.elevationBonus,
+ [0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal"));
+
cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery);
};
/**
* Called when units enter or leave range
*/
BuildingAI.prototype.OnRangeUpdate = function(msg)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
+ // Target enemy units except non-dangerous animals
if (msg.tag == this.gaiaUnitsQuery)
{
- const filter = function(e) {
- var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
- return (cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()));
- };
-
- if (msg.added.length)
- msg.added = msg.added.filter(filter);
-
- // Removed entities may not have cmpUnitAI.
- for (var i = 0; i < msg.removed.length; ++i)
- if (this.targetUnits.indexOf(msg.removed[i]) == -1)
- msg.removed.splice(i--, 1);
+ msg.added = msg.added.filter(e => {
+ let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
+ return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
+ });
}
else if (msg.tag != this.enemyUnitsQuery)
return;
- if (msg.added.length > 0)
- {
- for each (var entity in msg.added)
- {
- if (cmpAttack.CanAttack(entity))
- this.targetUnits.push(entity);
- }
- }
- if (msg.removed.length > 0)
- {
- for each (var entity in msg.removed)
- {
- var index = this.targetUnits.indexOf(entity);
- if (index > -1)
- this.targetUnits.splice(index, 1);
- }
+ // Add new targets
+ for (let entity of msg.added)
+ if (cmpAttack.CanAttack(entity))
+ this.targetUnits.push(entity);
+
+ // Remove targets outside of vision-range
+ for (let entity of msg.removed)
+ {
+ let index = this.targetUnits.indexOf(entity);
+ if (index > -1)
+ this.targetUnits.splice(index, 1);
}
if (this.targetUnits.length)
this.StartTimer();
};
BuildingAI.prototype.StartTimer = function()
{
if (this.timer)
return;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var attackTimers = cmpAttack.GetTimers(attackType);
- this.timer = cmpTimer.SetInterval(this.entity, IID_BuildingAI, "FireArrows", attackTimers.prepare, attackTimers.repeat / roundCount, null);
+
+ this.timer = cmpTimer.SetInterval(this.entity, IID_BuildingAI, "FireArrows",
+ attackTimers.prepare, attackTimers.repeat / roundCount, null);
};
BuildingAI.prototype.GetDefaultArrowCount = function()
{
var arrowCount = +this.template.DefaultArrowCount;
return ApplyValueModificationsToEntity("BuildingAI/DefaultArrowCount", arrowCount, this.entity);
};
BuildingAI.prototype.GetGarrisonArrowMultiplier = function()
{
var arrowMult = +this.template.GarrisonArrowMultiplier;
return ApplyValueModificationsToEntity("BuildingAI/GarrisonArrowMultiplier", arrowMult, this.entity);
};
BuildingAI.prototype.GetGarrisonArrowClasses = function()
{
var string = this.template.GarrisonArrowClasses;
if (string)
return string.split(/\s+/);
return [];
};
/**
* Returns the number of arrows which needs to be fired.
* DefaultArrowCount + Garrisoned Archers(ie., any unit capable
* of shooting arrows from inside buildings)
*/
BuildingAI.prototype.GetArrowCount = function()
{
- var count = this.GetDefaultArrowCount();
- var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
+ let count = this.GetDefaultArrowCount();
+
+ let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
- {
count += Math.round(cmpGarrisonHolder.GetGarrisonedArcherCount(this.GetGarrisonArrowClasses()) * this.GetGarrisonArrowMultiplier());
- }
+
return count;
};
BuildingAI.prototype.SetUnitAITarget = function(ent)
{
this.unitAITarget = ent;
if (ent)
this.StartTimer();
};
/**
- * Fires arrows. Called 'roundCount' times every 'RepeatTime' seconds when there are units in the range
+ * Fire arrows with random temporal distribution on prefered targets.
+ * Called 'roundCount' times every 'RepeatTime' seconds when there are units in the range.
*/
BuildingAI.prototype.FireArrows = function()
{
if (!this.targetUnits.length && !this.unitAITarget)
{
if (!this.timer)
return;
- // stop the timer
- var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
return;
}
- var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
+ let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
- var arrowsToFire = 0;
- if (this.currentRound > (roundCount - 1))
- {
- //Reached end of rounds. Reset count
+ if (this.currentRound > roundCount - 1)
this.currentRound = 0;
- }
if (this.currentRound == 0)
- {
- //First round. Calculate arrows to fire
this.arrowsLeft = this.GetArrowCount();
- }
- if (this.currentRound == (roundCount - 1))
- {
- //Last round. Need to fire all left-over arrows
+ let arrowsToFire = 0;
+ if (this.currentRound == roundCount - 1)
arrowsToFire = this.arrowsLeft;
- }
else
- {
- //Fire N arrows, 0 <= N <= Number of arrows left
arrowsToFire = Math.min(
- Math.round(2*Math.random() * this.GetArrowCount()/roundCount),
+ Math.round(2 * Math.random() * this.GetArrowCount() / roundCount),
this.arrowsLeft
);
- }
+
if (arrowsToFire <= 0)
{
- this.currentRound++;
+ ++this.currentRound;
return;
}
- // add targets to a weighted list, to allow preferences
- var targets = new WeightedList();
- var maxPreferenceBonus = this.MAX_PREFERENCE_BONUS;
- var addTarget = function(target)
+ // Add targets to a weighted list, to allow preferences
+ let targets = new WeightedList();
+ let addTarget = function(target)
{
- var preference = cmpAttack.GetPreference(target);
- var weight = 1;
+ let preference = cmpAttack.GetPreference(target);
+ let weight = 1;
+
if (preference !== null && preference !== undefined)
- {
- // Lower preference scores indicate a higher preference so they
- // should result in a higher weight.
- weight = 1 + maxPreferenceBonus / (1 + preference);
- }
- targets.push(target, weight);
+ weight += this.MAX_PREFERENCE_BONUS / (1 + preference);
+ targets.push(target, weight);
};
- // add the unit ai target separately, as the UnitMotion and RangeManager
- // implementations differ
+
+ // Add the UnitAI target separately, as the UnitMotion and RangeManager implementations differ
if (this.unitAITarget && this.targetUnits.indexOf(this.unitAITarget) == -1)
addTarget(this.unitAITarget);
for (let target of this.targetUnits)
addTarget(target);
- for (var i = 0;i < arrowsToFire;i++)
+ for (let i = 0; i < arrowsToFire; ++i)
{
- var selectedIndex = targets.randomIndex();
- var selectedTarget = targets.itemAt(selectedIndex);
+ let selectedIndex = targets.randomIndex();
+ let selectedTarget = targets.itemAt(selectedIndex);
+
if (selectedTarget && this.CheckTargetVisible(selectedTarget))
{
cmpAttack.PerformAttack(attackType, selectedTarget);
PlaySound("attack", this.entity);
+ continue;
}
- else
+
+ // Could not attack target, retry
+ targets.removeAt(selectedIndex);
+ --i;
+
+ if (!targets.length())
{
- targets.removeAt(selectedIndex);
- i--; // one extra arrow left to fire
- if(targets.length() < 1)
- {
- this.arrowsLeft += arrowsToFire;
- // no targets found in this round, save arrows and go to next round
- break;
- }
+ this.arrowsLeft += arrowsToFire;
+ break;
}
}
this.arrowsLeft -= arrowsToFire;
this.currentRound++;
};
/**
* Returns true if the target entity is visible through the FoW/SoD.
*/
BuildingAI.prototype.CheckTargetVisible = function(target)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
// Entities that are hidden and miraged are considered visible
var cmpFogging = Engine.QueryInterface(target, IID_Fogging);
if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner()))
return true;
- var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
// Either visible directly, or visible in fog
+ let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) != "hidden";
};
Engine.RegisterComponentType(IID_BuildingAI, "BuildingAI", BuildingAI);