Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/upgrade.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/upgrade.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/upgrade.png (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/upgrade.png (revision 18467)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/upgrade.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 18466)
+++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 18467)
@@ -1,371 +1,370 @@
/**
* Gets an array of all classes for this identity template
*/
function GetIdentityClasses(template)
{
var classList = [];
if (template.Classes && template.Classes._string)
classList = classList.concat(template.Classes._string.split(/\s+/));
if (template.VisibleClasses && template.VisibleClasses._string)
classList = classList.concat(template.VisibleClasses._string.split(/\s+/));
if (template.Rank)
classList = classList.concat(template.Rank);
return classList;
}
/**
* Gets an array with all classes for this identity template
* that should be shown in the GUI
*/
function GetVisibleIdentityClasses(template)
{
if (template.VisibleClasses && template.VisibleClasses._string)
return template.VisibleClasses._string.split(/\s+/);
return [];
}
/**
* Check if the classes given in the identity template
* match a list of classes
* @param classes List of the classes to check against
* @param match Either a string in the form
* "Class1 Class2+Class3"
* where spaces are handled as OR and '+'-signs as AND,
* and ! is handled as NOT, thus Class1+!Class2 = Class1 AND NOT Class2
* Or a list in the form
* [["Class1"], ["Class2", "Class3"]]
* where the outer list is combined as OR, and the inner lists are AND-ed
* Or a hybrid format containing a list of strings, where the list is
* combined as OR, and the strings are split by space and '+' and AND-ed
*
* @return undefined if there are no classes or no match object
* true if the the logical combination in the match object matches the classes
* false otherwise
*/
function MatchesClassList(classes, match)
{
if (!match || !classes)
return undefined;
// transform the string to an array
if (typeof match == "string")
match = match.split(/\s+/);
for (var sublist of match)
{
// if the elements are still strings, split them by space or by '+'
if (typeof sublist == "string")
sublist = sublist.split(/[+\s]+/);
if (sublist.every(c => (c[0] == "!" && classes.indexOf(c.substr(1)) == -1)
|| (c[0] != "!" && classes.indexOf(c) != -1)))
return true;
}
return false;
}
/**
* Get information about a template with or without technology modifications.
* @param template A valid template as returned by the template loader.
* @param player An optional player id to get the technology modifications
* of properties.
* @param auraTemplates An object in the form of {key: {auraName: "", auraDescription: ""}}
*/
function GetTemplateDataHelper(template, player, auraTemplates)
{
let getEntityValue = function(tech_type) {
let current_value = template;
for (let property of tech_type.split("/"))
current_value = current_value[property] || 0;
current_value = +current_value;
if (!player)
return current_value;
return ApplyValueModificationsToTemplate(tech_type, current_value, player, template);
};
let ret = {};
if (template.Armour)
ret.armour = {
"hack": getEntityValue("Armour/Hack"),
"pierce": getEntityValue("Armour/Pierce"),
"crush": getEntityValue("Armour/Crush")
};
if (template.Attack)
{
let getAttackStat = function(type, stat)
{
return getEntityValue("Attack/" + type + "/"+stat);
};
ret.attack = {};
for (let type in template.Attack)
{
if (type == "Capture")
ret.attack.Capture = {
"value": getAttackStat(type,"Value"),
};
else
ret.attack[type] = {
"hack": getAttackStat(type, "Hack"),
"pierce": getAttackStat(type, "Pierce"),
"crush": getAttackStat(type, "Crush"),
"minRange": getAttackStat(type, "MinRange"),
"maxRange": getAttackStat(type, "MaxRange"),
"elevationBonus": getAttackStat(type, "ElevationBonus"),
};
ret.attack[type].repeatTime = +(template.Attack[type].RepeatTime || 0);
}
}
if (template.Auras)
{
ret.auras = {};
for (let auraID of template.Auras._string.split(/\s+/))
{
let aura = auraTemplates[auraID];
if (aura.auraName)
ret.auras[auraID] = {
"name": aura.auraName || null,
"description": aura.auraDescription || null
};
}
}
if (template.BuildingAI)
{
ret.buildingAI = {};
if (template.BuildingAI.DefaultArrowCount)
ret.buildingAI.defaultArrowCount = getEntityValue("BuildingAI/DefaultArrowCount");
if (template.BuildingAI.GarrisonArrowMultiplier)
ret.buildingAI.garrisonArrowMultiplier = getEntityValue("BuildingAI/GarrisonArrowMultiplier");
if (template.BuildingAI.MaxArrowCount)
ret.buildingAI.maxArrowCount = getEntityValue("BuildingAI/MaxArrowCount");
}
if (template.BuildRestrictions)
{
// required properties
ret.buildRestrictions = {
"placementType": template.BuildRestrictions.PlacementType,
"territory": template.BuildRestrictions.Territory,
"category": template.BuildRestrictions.Category,
};
// optional properties
if (template.BuildRestrictions.Distance)
{
ret.buildRestrictions.distance = {
"fromClass": template.BuildRestrictions.Distance.FromClass,
};
if (template.BuildRestrictions.Distance.MinDistance)
ret.buildRestrictions.distance.min = +template.BuildRestrictions.Distance.MinDistance;
if (template.BuildRestrictions.Distance.MaxDistance)
ret.buildRestrictions.distance.max = +template.BuildRestrictions.Distance.MaxDistance;
}
}
if (template.TrainingRestrictions)
ret.trainingRestrictions = {
"category": template.TrainingRestrictions.Category,
};
if (template.Cost)
{
ret.cost = {};
if (template.Cost.Resources.food)
ret.cost.food = getEntityValue("Cost/Resources/food");
if (template.Cost.Resources.wood)
ret.cost.wood = getEntityValue("Cost/Resources/wood");
if (template.Cost.Resources.stone)
ret.cost.stone = getEntityValue("Cost/Resources/stone");
if (template.Cost.Resources.metal)
ret.cost.metal = getEntityValue("Cost/Resources/metal");
if (template.Cost.Population)
ret.cost.population = getEntityValue("Cost/Population");
if (template.Cost.PopulationBonus)
ret.cost.populationBonus = getEntityValue("Cost/PopulationBonus");
if (template.Cost.BuildTime)
ret.cost.time = getEntityValue("Cost/BuildTime");
}
if (template.Footprint)
{
ret.footprint = { "height": template.Footprint.Height };
if (template.Footprint.Square)
ret.footprint.square = {
"width": +template.Footprint.Square["@width"],
"depth": +template.Footprint.Square["@depth"]
};
else if (template.Footprint.Circle)
ret.footprint.circle = { "radius": +template.Footprint.Circle["@radius"] };
else
warn("GetTemplateDataHelper(): Unrecognized Footprint type");
}
if (template.GarrisonHolder)
{
ret.garrisonHolder = {};
if (template.GarrisonHolder.Max)
ret.garrisonHolder.max = getEntityValue("GarrisonHolder/Max");
}
if (template.Heal)
ret.heal = {
"hp": getEntityValue("Heal/HP"),
"range": getEntityValue("Heal/Range"),
"rate": getEntityValue("Heal/Rate")
};
if (template.Obstruction)
{
ret.obstruction = {
"active": ("" + template.Obstruction.Active == "true"),
"blockMovement": ("" + template.Obstruction.BlockMovement == "true"),
"blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"),
"blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"),
"blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"),
"disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"),
"disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"),
"shape": {}
};
if (template.Obstruction.Static)
{
ret.obstruction.shape.type = "static";
ret.obstruction.shape.width = +template.Obstruction.Static["@width"];
ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"];
}
else if (template.Obstruction.Unit)
{
ret.obstruction.shape.type = "unit";
ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"];
}
else
ret.obstruction.shape.type = "cluster";
}
if (template.Pack)
ret.pack = {
"state": template.Pack.State,
"time": getEntityValue("Pack/Time"),
};
if (template.Health)
ret.health = Math.round(getEntityValue("Health/Max"));
if (template.Identity)
{
ret.selectionGroupName = template.Identity.SelectionGroupName;
ret.name = {
"specific": (template.Identity.SpecificName || template.Identity.GenericName),
"generic": template.Identity.GenericName
};
ret.icon = template.Identity.Icon;
ret.tooltip = template.Identity.Tooltip;
- ret.gateConversionTooltip = template.Identity.GateConversionTooltip;
ret.requiredTechnology = template.Identity.RequiredTechnology;
ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity);
}
if (template.UnitMotion)
{
ret.speed = {
"walk": getEntityValue("UnitMotion/WalkSpeed"),
};
if (template.UnitMotion.Run)
ret.speed.run = getEntityValue("UnitMotion/Run/Speed");
}
if (template.ProductionQueue)
{
ret.techCostMultiplier = {};
for (let res in template.ProductionQueue.TechCostMultiplier)
ret.techCostMultiplier[res] = getEntityValue("ProductionQueue/TechCostMultiplier/" + res);
}
if (template.Trader)
ret.trader = {
"GainMultiplier": getEntityValue("Trader/GainMultiplier")
};
if (template.WallSet)
ret.wallSet = {
"templates": {
"tower": template.WallSet.Templates.Tower,
"gate": template.WallSet.Templates.Gate,
"long": template.WallSet.Templates.WallLong,
"medium": template.WallSet.Templates.WallMedium,
"short": template.WallSet.Templates.WallShort,
},
"maxTowerOverlap": +template.WallSet.MaxTowerOverlap,
"minTowerOverlap": +template.WallSet.MinTowerOverlap,
};
if (template.WallPiece)
ret.wallPiece = { "length": +template.WallPiece.Length };
return ret;
}
/**
* Get information about a technology template.
* @param template A valid template as obtained by loading the tech JSON file.
* @param civ Civilization for which the specific name should be returned.
*/
function GetTechnologyDataHelper(template, civ)
{
var ret = {};
// Get specific name for this civ or else the generic specific name
var specific;
if (template.specificName)
{
if (template.specificName[civ])
specific = template.specificName[civ];
else
specific = template.specificName['generic'];
}
ret.name = {
"specific": specific,
"generic": template.genericName,
};
ret.icon = template.icon ? "technologies/" + template.icon : null;
ret.cost = {
"food": template.cost ? +template.cost.food : 0,
"wood": template.cost ? +template.cost.wood : 0,
"metal": template.cost ? +template.cost.metal : 0,
"stone": template.cost ? +template.cost.stone : 0,
"time": template.researchTime ? +template.researchTime : 0,
}
ret.tooltip = template.tooltip;
ret.requirementsTooltip = template.requirementsTooltip || "";
if (template.requirements && template.requirements.class)
ret.classRequirements = {
"class": template.requirements.class,
"number": template.requirements.number
};
ret.description = template.description;
return ret;
}
Index: ps/trunk/binaries/data/mods/public/gui/common/tooltips.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 18466)
+++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 18467)
@@ -1,541 +1,542 @@
const g_CostDisplayIcons = {
"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]'],
"comma": ['[font="sans-12"]', '[/font]']
};
const g_AttackTypes = {
"Charge": translate("Charge Attack:"),
"Melee": translate("Melee Attack:"),
"Ranged": translate("Ranged Attack:"),
"Capture": translate("Capture Attack:")
};
const g_DamageTypes = {
"hack": translate("Hack"),
"pierce": translate("Pierce"),
"crush": translate("Crush"),
};
function bodyFont(text)
{
return g_TooltipTextFormats.body[0] + text + g_TooltipTextFormats.body[1];
}
function headerFont(text)
{
return g_TooltipTextFormats.header[0] + text + g_TooltipTextFormats.header[1];
}
function unitFont(text)
{
return g_TooltipTextFormats.unit[0] + text + g_TooltipTextFormats.unit[1];
}
function commaFont(text)
{
return g_TooltipTextFormats.comma[0] + text + g_TooltipTextFormats.comma[1];
}
function getEntityTooltip(template)
{
if (!template.tooltip)
return "";
return bodyFont(template.tooltip);
}
function getHealthTooltip(template)
{
if (!template.health)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Health:")),
"details": template.health
});
}
function attackRateDetails(template, type)
{
// Either one arrow shot by UnitAI,
let time = template.attack[type].repeatTime / 1000;
let timeString = sprintf(translatePlural("%(time)s %(second)s", "%(time)s %(second)s", time), {
"time": time,
"second": unitFont(translatePlural("second", "seconds", time))
});
// or multiple arrows shot by BuildingAI
if (!template.buildingAI || type != "Ranged")
return timeString;
// Show either current rate from simulation or default count if the sim is not running
let arrows = template.buildingAI.arrowCount || template.buildingAI.defaultArrowCount;
let arrowString = sprintf(translatePlural("%(arrowcount)s %(arrows)s", "%(arrowcount)s %(arrows)s", arrows), {
"arrowcount": arrows,
"arrows": unitFont(translatePlural("arrow", "arrows", arrows))
});
return sprintf(translate("%(arrowString)s / %(timeString)s"), {
"arrowString": arrowString,
"timeString": timeString
});
}
/**
* Converts an armor level into the actual reduction percentage
*/
function armorLevelToPercentageString(level)
{
return sprintf(translate("%(percentage)s%%"), {
"percentage": (100 - Math.round(Math.pow(0.9, level) * 100))
});
}
function getArmorTooltip(template)
{
if (!template.armour)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Armor:")),
"details":
Object.keys(template.armour).map(
dmgType => sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), {
"damage": template.armour[dmgType].toFixed(1),
"damageType": unitFont(g_DamageTypes[dmgType]),
"armorPercentage":
'[font="sans-10"]' +
sprintf(translate("(%(armorPercentage)s)"), {
"armorPercentage": armorLevelToPercentageString(template.armour[dmgType])
}) + '[/font]'
})
).join(commaFont(translate(", ")))
});
}
function damageTypesToText(dmg)
{
if (!dmg)
return '[font="sans-12"]' + translate("(None)") + '[/font]';
return Object.keys(g_DamageTypes).filter(
dmgType => dmg[dmgType]).map(
dmgType => sprintf(translate("%(damage)s %(damageType)s"), {
"damage": dmg[dmgType].toFixed(1),
"damageType": unitFont(g_DamageTypes[dmgType])
})).join(commaFont(translate(", ")));
}
// TODO: should also show minRange and splash damage
function getAttackTooltip(template)
{
if (!template.attack)
return "";
let attacks = [];
for (let type in template.attack)
{
if (type == "Slaughter")
continue; // Slaughter is used to kill animals, 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":
headerFont(
template.buildingAI && type == "Ranged" ?
translate("Interval:") :
translate("Rate:")),
"details": attackRateDetails(template, type)
});
let attackLabel = headerFont(g_AttackTypes[type]);
if (type == "Capture" || type != "Ranged")
{
attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), {
"attackLabel": attackLabel,
"details":
type == "Capture" ?
template.attack.Capture.value :
damageTypesToText(template.attack[type]),
"rate": rate
}));
continue;
}
let realRange = template.attack[type].elevationAdaptedRange;
let range = Math.round(template.attack[type].maxRange);
let relativeRange = realRange ? Math.round(realRange - range) : 0;
let rangeString = relativeRange ?
translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(rangeString)s (%(relative)s), %(rate)s") :
translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(rangeString)s, %(rate)s");
attacks.push(sprintf(rangeString, {
"attackLabel": attackLabel,
"damageTypes": damageTypesToText(template.attack[type]),
"rangeLabel": translate("Range:"),
"rangeString": sprintf(
translatePlural("%(range)s %(meters)s", "%(range)s %(meters)s", range), {
"range": range,
"meters": unitFont(translatePlural("meter", "meters", range))
}),
"rate": rate,
"relative": relativeRange > 0 ? "+" + relativeRange : relativeRange,
}));
}
return attacks.join("\n");
}
function getGarrisonTooltip(template)
{
if (!template.garrisonHolder)
return "";
return sprintf(translate("%(label)s: %(garrisonLimit)s"), {
"label": headerFont(translate("Garrison Limit")),
"garrisonLimit": template.garrisonHolder.capacity || template.garrisonHolder.max
});
}
function getProjectilesTooltip(template)
{
if (!template.garrisonHolder || !template.buildingAI)
return "";
let limit = Math.min(
template.buildingAI.maxArrowCount || Infinity,
template.buildingAI.defaultArrowCount +
template.buildingAI.garrisonArrowMultiplier *
(template.garrisonHolder.capacity || template.garrisonHolder.max)
);
if (!limit)
return "";
return [
sprintf(translate("%(label)s: %(value)s"), {
"label": headerFont(translate("Projectile Limit")),
"value": limit
}),
sprintf(translate("%(label)s: %(value)s"), {
"label": headerFont(translateWithContext("projectiles", "Default")),
"value": template.buildingAI.defaultArrowCount
}),
sprintf(translate("%(label)s: %(value)s"), {
"label": headerFont(translateWithContext("projectiles", "Per Unit")),
"value": template.buildingAI.garrisonArrowMultiplier
})
].join(commaFont(translate(", ")));
}
function getRepairRateTooltip(template)
{
if (!template.repairRate)
return "";
return sprintf(translate("%(repairRateLabel)s %(value)s %(health)s / %(second)s / %(worker)s"), {
"repairRateLabel": headerFont(translate("Repair Rate:")),
"value": template.repairRate.toFixed(1),
"health": unitFont(translate("health")),
"second": unitFont(translate("second")),
"worker": unitFont(translate("worker"))
});
}
function getBuildRateTooltip(template)
{
if (!template.buildRate)
return "";
return sprintf(translate("%(buildRateLabel)s %(value)s %(health)s / %(second)s / %(worker)s"), {
"buildRateLabel": headerFont(translate("Build Rate:")),
"value": template.buildRate.toFixed(1),
"health": unitFont(translate("health")),
"second": unitFont(translate("second")),
"worker": unitFont(translate("worker"))
});
}
/**
* 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));
+ if (template.cost.time)
+ totalCosts.time = Math.ceil(template.cost.time * (entity ? Engine.GuiInterfaceCall("GetBatchTime", { "entity": entity, "batchSize": trainNum }) : 1));
let costs = [];
for (let type in g_CostDisplayIcons)
if (totalCosts[type])
costs.push(sprintf(translate("%(component)s %(cost)s"), {
"component": g_CostDisplayIcons[type],
"cost": totalCosts[type]
}));
return costs;
}
function getGatherTooltip(template)
{
if (!template.gather)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Gather Rates:")),
"details":
Object.keys(template.gather).map(
type => sprintf(translate("%(resourceIcon)s %(rate)s"), {
"resourceIcon": g_CostDisplayIcons[type],
"rate": template.gather[type]
})
).join(" ")
});
}
/**
* 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)
// 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": g_CostDisplayIcons[resource],
"minimum": Math.min.apply(Math, resourceCount[resource]),
"maximum": Math.max.apply(Math, resourceCount[resource])
}));
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 = sprintf(translate("%(label)s %(populationBonus)s"), {
"label": headerFont(translate("Population Bonus:")),
"populationBonus": template.cost.populationBonus
});
return popBonus;
}
/**
* Returns a message with the amount of each resource needed to create an entity.
*/
function getNeededResourcesTooltip(resources)
{
if (!resources)
return "";
let formatted = [];
for (let resource in resources)
formatted.push(sprintf(translate("%(component)s %(cost)s"), {
"component": '[font="sans-12"]' + g_CostDisplayIcons[resource] + '[/font]',
"cost": resources[resource]
}));
return '\n[font="sans-bold-13"][color="red"]' + translate("Insufficient resources:") + '[/color][/font]\n' + formatted.join(" ");
}
function getSpeedTooltip(template)
{
if (!template.speed)
return "";
let speeds = [];
if (template.speed.walk)
speeds.push(sprintf(translate("%(speed)s %(movementType)s"), {
"speed": template.speed.walk.toFixed(1),
"movementType": unitFont(translate("Walk"))
}));
if (template.speed.run)
speeds.push(sprintf(translate("%(speed)s %(movementType)s"), {
"speed": template.speed.run.toFixed(1),
"movementType": unitFont(translate("Run"))
}));
return sprintf(translate("%(label)s %(speeds)s"), {
"label": headerFont(translate("Speed:")),
"speeds": speeds.join(translate(", "))
});
}
function getHealerTooltip(template)
{
if (!template.heal)
return "";
let hp = +(template.heal.hp.toFixed(1));
let range = +(template.heal.range.toFixed(0));
let rate = +((template.heal.rate / 1000).toFixed(1));
return [
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", hp), {
"label": headerFont(translate("Heal:")),
"val": hp,
// Translation: Short for hit points (or health points) that are healed in one healing action
"unit": unitFont(translatePlural("HP", "HP", hp))
}),
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", range), {
"label": headerFont(translate("Range:")),
"val": range,
"unit": unitFont(translatePlural("meter", "meters", range))
}),
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", rate), {
"label": headerFont(translate("Rate:")),
"val": rate,
"unit": unitFont(translatePlural("second", "seconds", rate))
})
].join(translate(", "));
}
function getAurasTooltip(template)
{
if (!template.auras)
return "";
let tooltips = Object.keys(template.auras).map(
aura => sprintf(translate("%(auralabel)s %(aurainfo)s"), {
"auralabel": headerFont(sprintf(translate("%(auraname)s:"), {
"auraname": translate(template.auras[aura].name)
})),
"aurainfo": bodyFont(translate(template.auras[aura].description))
}));
return tooltips.join("\n");
}
function getEntityNames(template)
{
if (!template.name.specific)
return template.name.generic;
if (template.name.specific == template.name.generic)
return template.name.specific;
return sprintf(translate("%(specificName)s (%(genericName)s)"), {
"specificName": template.name.specific,
"genericName": template.name.generic
});
}
function getEntityNamesFormatted(template)
{
if (!template.name.specific)
return '[font="sans-bold-16"]' + template.name.generic + "[/font]";
return sprintf(translate("%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s"), {
"specificName":
'[font="sans-bold-16"]' + template.name.specific[0] + '[/font]' +
'[font="sans-bold-12"]' + template.name.specific.slice(1).toUpperCase() + '[/font]',
"genericName": template.name.generic,
"fontStart": '[font="sans-bold-16"]',
"fontEnd": '[/font]'
});
}
function getVisibleEntityClassesFormatted(template)
{
if (!template.visibleIdentityClasses || !template.visibleIdentityClasses.length)
return "";
return headerFont(translate("Classes:")) + ' ' +
bodyFont(template.visibleIdentityClasses.map(c => translate(c)).join(translate(", ")));
}
Index: ps/trunk/binaries/data/mods/public/gui/session/input.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 18466)
+++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 18467)
@@ -1,1892 +1,1890 @@
const SDL_BUTTON_LEFT = 1;
const SDL_BUTTON_MIDDLE = 2;
const SDL_BUTTON_RIGHT = 3;
const SDLK_LEFTBRACKET = 91;
const SDLK_RIGHTBRACKET = 93;
const SDLK_RSHIFT = 303;
const SDLK_LSHIFT = 304;
const SDLK_RCTRL = 305;
const SDLK_LCTRL = 306;
const SDLK_RALT = 307;
const SDLK_LALT = 308;
// TODO: these constants should be defined somewhere else instead, in
// case any other code wants to use them too
const ACTION_NONE = 0;
const ACTION_GARRISON = 1;
const ACTION_REPAIR = 2;
const ACTION_GUARD = 3;
var preSelectedAction = ACTION_NONE;
const INPUT_NORMAL = 0;
const INPUT_SELECTING = 1;
const INPUT_BANDBOXING = 2;
const INPUT_BUILDING_PLACEMENT = 3;
const INPUT_BUILDING_CLICK = 4;
const INPUT_BUILDING_DRAG = 5;
const INPUT_BATCHTRAINING = 6;
const INPUT_PRESELECTEDACTION = 7;
const INPUT_BUILDING_WALL_CLICK = 8;
const INPUT_BUILDING_WALL_PATHING = 9;
const INPUT_MASSTRIBUTING = 10;
var inputState = INPUT_NORMAL;
const INVALID_ENTITY = 0;
var mouseX = 0;
var mouseY = 0;
var mouseIsOverObject = false;
// Number of pixels the mouse can move before the action is considered a drag
var maxDragDelta = 4;
// Time in milliseconds in which a double click is recognized
const doubleClickTime = 500;
var doubleClickTimer = 0;
var doubleClicked = false;
// Store the previously clicked entity - ensure a double/triple click happens on the same entity
var prevClickedEntity = 0;
// Same double-click behaviour for hotkey presses
const doublePressTime = 500;
var doublePressTimer = 0;
var prevHotkey = 0;
function updateCursorAndTooltip()
{
var cursorSet = false;
var tooltipSet = false;
var informationTooltip = Engine.GetGUIObjectByName("informationTooltip");
if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION))
{
let action = determineAction(mouseX, mouseY);
if (action)
{
if (action.cursor)
{
Engine.SetCursor(action.cursor);
cursorSet = true;
}
if (action.tooltip)
{
tooltipSet = true;
informationTooltip.caption = action.tooltip;
informationTooltip.hidden = false;
}
}
}
if (!cursorSet)
Engine.SetCursor("arrow-default");
if (!tooltipSet)
informationTooltip.hidden = true;
var placementTooltip = Engine.GetGUIObjectByName("placementTooltip");
if (placementSupport.tooltipMessage)
placementTooltip.sprite = placementSupport.tooltipError ? "BackgroundErrorTooltip" : "BackgroundInformationTooltip";
placementTooltip.caption = placementSupport.tooltipMessage || "";
placementTooltip.hidden = !placementSupport.tooltipMessage;
}
function updateBuildingPlacementPreview()
{
// The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or
// in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to.
// See onSimulationUpdate in session.js.
if (placementSupport.mode === "building")
{
if (placementSupport.template && placementSupport.position)
{
var result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"actorSeed": placementSupport.actorSeed
});
// Show placement info tooltip if invalid position
placementSupport.tooltipError = !result.success;
placementSupport.tooltipMessage = "";
if (!result.success)
{
if (result.message && result.parameters)
{
var message = result.message;
if (result.translateMessage)
if (result.pluralMessage)
message = translatePlural(result.message, result.pluralMessage, result.pluralCount);
else
message = translate(message);
var parameters = result.parameters;
if (result.translateParameters)
translateObjectKeys(parameters, result.translateParameters);
placementSupport.tooltipMessage = sprintf(message, parameters);
}
return false;
}
if (placementSupport.attack && placementSupport.attack.Ranged)
{
// building can be placed here, and has an attack
// show the range advantage in the tooltip
var cmd = {
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"range": placementSupport.attack.Ranged.maxRange,
"elevationBonus": placementSupport.attack.Ranged.elevationBonus,
};
var averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range);
var range = Math.round(cmd.range);
placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" +
sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange });
}
return true;
}
}
else if (placementSupport.mode === "wall")
{
if (placementSupport.wallSet && placementSupport.position)
{
// Fetch an updated list of snapping candidate entities
placementSupport.wallSnapEntities = Engine.PickSimilarPlayerEntities(
placementSupport.wallSet.templates.tower,
placementSupport.wallSnapEntitiesIncludeOffscreen,
true, // require exact template match
true // include foundations
);
return Engine.GuiInterfaceCall("SetWallPlacementPreview", {
"wallSet": placementSupport.wallSet,
"start": placementSupport.position,
"end": placementSupport.wallEndPosition,
"snapEntities": placementSupport.wallSnapEntities, // snapping entities (towers) for starting a wall segment
});
}
}
return false;
}
function findGatherType(gatherer, supply)
{
if (!("resourceGatherRates" in gatherer) || !gatherer.resourceGatherRates || !supply)
return undefined;
if (gatherer.resourceGatherRates[supply.type.generic+"."+supply.type.specific])
return supply.type.specific;
if (gatherer.resourceGatherRates[supply.type.generic])
return supply.type.generic;
return undefined;
}
function getActionInfo(action, target)
{
var simState = GetSimState();
var selection = g_Selection.toList();
// If the selection doesn't exist, no action
var entState = GetEntityState(selection[0]);
if (!entState)
return { "possible": false };
if (!target) // TODO move these non-target actions to an object like unit_actions.js
{
if (action == "set-rallypoint")
{
var cursor = "";
var data = { "command": "walk" };
if (Engine.HotkeyIsPressed("session.attackmove"))
{
data.command = "attack-walk";
data.targetClasses = Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] };
cursor = "action-attack-move";
}
return { "possible": true, "data": data, "cursor": cursor };
}
return { "possible": (action == "move" || action == "attack-move" || action == "remove-guard") };
}
// Look at the first targeted entity
// (TODO: maybe we eventually want to look at more, and be more context-sensitive?
// e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
var targetState = GetExtendedEntityState(target);
// Check if the target entity is a resource, dropsite, foundation, or enemy unit.
// Check if any entities in the selection can gather the requested resource,
// can return to the dropsite, can build the foundation, or can attack the enemy
for each (var entityID in selection)
{
var entState = GetExtendedEntityState(entityID);
if (!entState)
continue;
if (unitActions[action] && unitActions[action].getActionInfo)
{
var r = unitActions[action].getActionInfo(entState, targetState, simState);
if (r) // return true if it's possible for one of the entities
return r;
}
}
return { "possible": false };
}
/**
* Determine the context-sensitive action that should be performed when the mouse is at (x,y)
*/
function determineAction(x, y, fromMinimap)
{
var selection = g_Selection.toList();
// No action if there's no selection
if (!selection.length)
{
preSelectedAction = ACTION_NONE;
return undefined;
}
// If the selection doesn't exist, no action
var entState = GetEntityState(selection[0]);
if (!entState)
return undefined;
// If the selection isn't friendly units, no action
var allOwnedByPlayer = selection.every(ent => {
var entState = GetEntityState(ent);
return entState && entState.player == g_ViewedPlayer;
});
if (!g_DevSettings.controlAll && !allOwnedByPlayer)
return undefined;
var target = undefined;
if (!fromMinimap)
{
var ent = Engine.PickEntityAtPoint(x, y);
if (ent != INVALID_ENTITY)
target = ent;
}
// decide between the following ordered actions
// if two actions are possible, the first one is taken
// so the most specific should appear first
var actions = Object.keys(unitActions).slice();
actions.sort((a, b) => unitActions[a].specificness - unitActions[b].specificness);
var actionInfo = undefined;
if (preSelectedAction != ACTION_NONE)
{
for (var action of actions)
{
if (unitActions[action].preSelectedActionCheck)
{
var r = unitActions[action].preSelectedActionCheck(target, selection);
if (r)
return r;
}
}
return { "type": "none", "cursor": "", "target": target };
}
for (var action of actions)
{
if (unitActions[action].hotkeyActionCheck)
{
var r = unitActions[action].hotkeyActionCheck(target, selection);
if (r)
return r;
}
}
for (var action of actions)
{
if (unitActions[action].actionCheck)
{
var r = unitActions[action].actionCheck(target, selection);
if (r)
return r;
}
}
return { "type": "none", "cursor": "", "target": target };
}
var dragStart; // used for remembering mouse coordinates at start of drag operations
function tryPlaceBuilding(queued)
{
if (placementSupport.mode !== "building")
{
error(sprintf("[%(functionName)s] Called while in '%(mode)s' placement mode instead of 'building'", {
"functionName": "tryPlaceBuilding",
"mode": placementSupport.mode
}));
return false;
}
// Use the preview to check it's a valid build location
if (!updateBuildingPlacementPreview())
{
// invalid location - don't build it
// TODO: play a sound?
return false;
}
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "construct",
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"actorSeed": placementSupport.actorSeed,
"entities": selection,
"autorepair": true,
"autocontinue": true,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
if (!queued)
placementSupport.Reset();
else
placementSupport.RandomizeActorSeed();
return true;
}
function tryPlaceWall(queued)
{
if (placementSupport.mode !== "wall")
{
error(sprintf("[%(functionName)s] Called while in '%(mode)s' placement mode; expected 'wall' mode", {
functionName: "tryPlaceWall",
mode: placementSupport.mode
}));
return false;
}
var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...)
if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object"))
{
error(sprintf("[%(functionName)s] Unexpected return value from %(function2Name)s: '%(value)s'; expected either 'false' or 'object'", {
functionName: "tryPlaceWall",
function2Name: "updateBuildingPlacementPreview",
value: uneval(placementInfo)
}));
return false;
}
if (!wallPlacementInfo)
return false;
var selection = g_Selection.toList();
var cmd = {
"type": "construct-wall",
"autorepair": true,
"autocontinue": true,
"queued": queued,
"entities": selection,
"wallSet": placementSupport.wallSet,
"pieces": wallPlacementInfo.pieces,
"startSnappedEntity": wallPlacementInfo.startSnappedEnt,
"endSnappedEntity": wallPlacementInfo.endSnappedEnt,
};
// make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end
// point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed
// (this is somewhat non-ideal and hardcode-ish)
var hasWallSegment = false;
for (let piece of cmd.pieces)
{
if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :(
{
hasWallSegment = true;
break;
}
}
if (hasWallSegment)
{
Engine.PostNetworkCommand(cmd);
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
}
return true;
}
// Updates the bandbox object with new positions and visibility.
// The coordinates [x0, y0, x1, y1] are returned for further use.
function updateBandbox(bandbox, ev, hidden)
{
var x0 = dragStart[0];
var y0 = dragStart[1];
var x1 = ev.x;
var y1 = ev.y;
// normalize the orientation of the rectangle
if (x0 > x1) { let t = x0; x0 = x1; x1 = t; }
if (y0 > y1) { let t = y0; y0 = y1; y1 = t; }
bandbox.size = [x0, y0, x1, y1].join(" ");
bandbox.hidden = hidden;
return [x0, y0, x1, y1];
}
// Define some useful unit filters for getPreferredEntities
var unitFilters = {
"isUnit": entity => {
var entState = GetEntityState(entity);
return entState && hasClass(entState, "Unit");
},
"isDefensive": entity => {
var entState = GetEntityState(entity);
return entState && hasClass(entState, "Defensive");
},
"isNotSupport": entity => {
var entState = GetEntityState(entity);
return entState &&
hasClass(entState, "Unit") &&
!hasClass(entState, "Support") &&
!hasClass(entState, "Domestic");
},
"isIdle": entity => {
var entState = GetEntityState(entity);
return entState &&
hasClass(entState, "Unit") &&
entState.unitAI &&
entState.unitAI.isIdle &&
!hasClass(entState, "Domestic");
},
"isAnything": entity => {
return true;
}
};
// Choose, inside a list of entities, which ones will be selected.
// We may use several entity filters, until one returns at least one element.
function getPreferredEntities(ents)
{
// Default filters
var filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything];
// Handle hotkeys
if (Engine.HotkeyIsPressed("selection.milonly"))
filters = [unitFilters.isNotSupport];
if (Engine.HotkeyIsPressed("selection.idleonly"))
filters = [unitFilters.isIdle];
var preferredEnts = [];
for (var i = 0; i < filters.length; ++i)
{
preferredEnts = ents.filter(filters[i]);
if (preferredEnts.length)
break;
}
return preferredEnts;
}
function handleInputBeforeGui(ev, hoveredObject)
{
// Capture mouse position so we can use it for displaying cursors,
// and key states
switch (ev.type)
{
case "mousebuttonup":
case "mousebuttondown":
case "mousemotion":
mouseX = ev.x;
mouseY = ev.y;
break;
}
// Remember whether the mouse is over a GUI object or not
mouseIsOverObject = (hoveredObject != null);
// Close the menu when interacting with the game world
if (!mouseIsOverObject && (ev.type =="mousebuttonup" || ev.type == "mousebuttondown")
&& (ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT))
closeMenu();
// State-machine processing:
//
// (This is for states which should override the normal GUI processing - events will
// be processed here before being passed on, and propagation will stop if this function
// returns true)
//
// TODO: it'd probably be nice to have a better state-machine system, with guaranteed
// entry/exit functions, since this is a bit broken now
switch (inputState)
{
case INPUT_BANDBOXING:
var bandbox = Engine.GetGUIObjectByName("bandbox");
switch (ev.type)
{
case "mousemotion":
var rect = updateBandbox(bandbox, ev, false);
var ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer);
var preferredEntities = getPreferredEntities(ents);
g_Selection.setHighlightList(preferredEntities);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
var rect = updateBandbox(bandbox, ev, true);
// Get list of entities limited to preferred entities
var ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer));
// Remove the bandbox hover highlighting
g_Selection.setHighlightList([]);
// Update the list of selected units
if (Engine.HotkeyIsPressed("selection.add"))
{
g_Selection.addList(ents);
}
else if (Engine.HotkeyIsPressed("selection.remove"))
{
g_Selection.removeList(ents);
}
else
{
g_Selection.reset();
g_Selection.addList(ents);
}
inputState = INPUT_NORMAL;
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel selection
bandbox.hidden = true;
g_Selection.setHighlightList([]);
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_CLICK:
switch (ev.type)
{
case "mousemotion":
// If the mouse moved far enough from the original click location,
// then switch to drag-orientation mode
var dragDeltaX = ev.x - dragStart[0];
var dragDeltaY = ev.y - dragStart[1];
var maxDragDelta = 16;
if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
{
inputState = INPUT_BUILDING_DRAG;
return false;
}
break;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
// If shift is down, let the player continue placing another of the same building
var queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceBuilding(queued))
{
if (queued)
inputState = INPUT_BUILDING_PLACEMENT;
else
inputState = INPUT_NORMAL;
}
else
{
inputState = INPUT_BUILDING_PLACEMENT;
}
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_WALL_CLICK:
// User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point
// by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode.
switch (ev.type)
{
case "mousebuttonup":
if (ev.button === SDL_BUTTON_LEFT)
{
inputState = INPUT_BUILDING_WALL_PATHING;
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building
placementSupport.Reset();
updateBuildingPlacementPreview();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_WALL_PATHING:
// User has chosen a starting point for constructing the wall, and is now looking to set the endpoint.
// Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to
// normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the
// user to continue building walls.
switch (ev.type)
{
case "mousemotion":
placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
// Update the building placement preview, and by extension, the list of snapping candidate entities for both (!)
// the ending point and the starting point to snap to.
//
// TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case
// where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a
// foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on
// the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers
// in them. Might be useful to query only for entities within a certain range around the starting point and ending
// points.
placementSupport.wallSnapEntitiesIncludeOffscreen = true;
var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
if (result && result.cost)
{
placementSupport.tooltipMessage = getEntityCostTooltip(result);
var neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost });
if (neededResources)
placementSupport.tooltipMessage += getNeededResourcesTooltip(neededResources);
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
var queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceWall(queued))
{
if (queued)
{
// continue building, just set a new starting position where we left off
placementSupport.position = placementSupport.wallEndPosition;
placementSupport.wallEndPosition = undefined;
inputState = INPUT_BUILDING_WALL_CLICK;
}
else
{
placementSupport.Reset();
inputState = INPUT_NORMAL;
}
}
else
{
placementSupport.tooltipMessage = translate("Cannot build wall here!");
}
updateBuildingPlacementPreview();
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// reset to normal input mode
placementSupport.Reset();
updateBuildingPlacementPreview();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_DRAG:
switch (ev.type)
{
case "mousemotion":
var dragDeltaX = ev.x - dragStart[0];
var dragDeltaY = ev.y - dragStart[1];
var maxDragDelta = 16;
if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
{
// Rotate in the direction of the mouse
var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
placementSupport.angle = Math.atan2(target.x - placementSupport.position.x, target.z - placementSupport.position.z);
}
else
{
// If the mouse is near the center, snap back to the default orientation
placementSupport.SetDefaultAngle();
}
var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z
});
if (snapData)
{
placementSupport.angle = snapData.angle;
placementSupport.position.x = snapData.x;
placementSupport.position.z = snapData.z;
}
updateBuildingPlacementPreview();
break;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
// If shift is down, let the player continue placing another of the same building
var queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceBuilding(queued))
{
if (queued)
inputState = INPUT_BUILDING_PLACEMENT;
else
inputState = INPUT_NORMAL;
}
else
{
inputState = INPUT_BUILDING_PLACEMENT;
}
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_MASSTRIBUTING:
if (ev.type == "hotkeyup" && ev.hotkey == "session.masstribute")
{
g_FlushTributing();
inputState = INPUT_NORMAL;
}
break;
case INPUT_BATCHTRAINING:
if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain")
{
flushTrainingBatch();
inputState = INPUT_NORMAL;
}
break;
}
return false;
}
function handleInputAfterGui(ev)
{
if (ev.hotkey === undefined)
ev.hotkey = null;
// Handle the time-warp testing features, restricted to single-player
if (!g_IsNetworked && Engine.GetGUIObjectByName("devTimeWarp").checked)
{
if (ev.type == "hotkeydown" && ev.hotkey == "session.timewarp.fastforward")
Engine.SetSimRate(20.0);
else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.fastforward")
Engine.SetSimRate(1.0);
else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.rewind")
Engine.RewindTimeWarp();
}
if (ev.hotkey == "session.showstatusbars")
{
g_ShowAllStatusBars = (ev.type == "hotkeydown");
recalculateStatusBarDisplay();
}
else if (ev.hotkey == "session.highlightguarding")
{
g_ShowGuarding = (ev.type == "hotkeydown");
updateAdditionalHighlight();
}
else if (ev.hotkey == "session.highlightguarded")
{
g_ShowGuarded = (ev.type == "hotkeydown");
updateAdditionalHighlight();
}
// State-machine processing:
switch (inputState)
{
case INPUT_NORMAL:
switch (ev.type)
{
case "mousemotion":
// Highlight the first hovered entity (if any)
var ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
dragStart = [ ev.x, ev.y ];
inputState = INPUT_SELECTING;
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
var action = determineAction(ev.x, ev.y);
if (!action)
break;
return doAction(action, ev);
}
break;
case "hotkeydown":
if (ev.hotkey.indexOf("selection.group.") == 0)
{
var now = new Date();
if ((now.getTime() - doublePressTimer < doublePressTime) && (ev.hotkey == prevHotkey))
{
if (ev.hotkey.indexOf("selection.group.select.") == 0)
{
var sptr = ev.hotkey.split(".");
performGroup("snap", sptr[3]);
}
}
else
{
var sptr = ev.hotkey.split(".");
performGroup(sptr[2], sptr[3]);
doublePressTimer = now.getTime();
prevHotkey = ev.hotkey;
}
}
break;
}
break;
case INPUT_PRESELECTEDACTION:
switch (ev.type)
{
case "mousemotion":
// Highlight the first hovered entity (if any)
var ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE)
{
var action = determineAction(ev.x, ev.y);
if (!action)
break;
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
return doAction(action, ev);
}
else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE)
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
// else
default:
// Slight hack: If selection is empty, reset the input state
if (g_Selection.toList().length == 0)
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
}
break;
case INPUT_SELECTING:
switch (ev.type)
{
case "mousemotion":
// If the mouse moved further than a limit, switch to bandbox mode
var dragDeltaX = ev.x - dragStart[0];
var dragDeltaY = ev.y - dragStart[1];
if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
{
inputState = INPUT_BANDBOXING;
return false;
}
var ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
var ents = [];
var selectedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
if (selectedEntity == INVALID_ENTITY)
{
if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove"))
{
g_Selection.reset();
resetIdleUnit();
}
inputState = INPUT_NORMAL;
return true;
}
var now = new Date();
// If camera following and we select different unit, stop
if (Engine.GetFollowedEntity() != selectedEntity)
{
Engine.CameraFollow(0);
}
if ((now.getTime() - doubleClickTimer < doubleClickTime) && (selectedEntity == prevClickedEntity))
{
// Double click or triple click has occurred
var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen");
var matchRank = true;
var templateToMatch;
// Check for double click or triple click
if (!doubleClicked)
{
// If double click hasn't already occurred, this is a double click.
// Select similar units regardless of rank
templateToMatch = GetEntityState(selectedEntity).identity.selectionGroupName;
if (templateToMatch)
{
matchRank = false;
}
else
{ // No selection group name defined, so fall back to exact match
templateToMatch = GetEntityState(selectedEntity).template;
}
doubleClicked = true;
// Reset the timer so the user has an extra period 'doubleClickTimer' to do a triple-click
doubleClickTimer = now.getTime();
}
else
{
// Double click has already occurred, so this is a triple click.
// Select units matching exact template name (same rank)
templateToMatch = GetEntityState(selectedEntity).template;
}
// TODO: Should we handle "control all units" here as well?
ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false);
}
else
{
// It's single click right now but it may become double or triple click
doubleClicked = false;
doubleClickTimer = now.getTime();
prevClickedEntity = selectedEntity;
// We only want to include the first picked unit in the selection
ents = [selectedEntity];
}
// Update the list of selected units
if (Engine.HotkeyIsPressed("selection.add"))
{
g_Selection.addList(ents);
}
else if (Engine.HotkeyIsPressed("selection.remove"))
{
g_Selection.removeList(ents);
}
else
{
g_Selection.reset();
g_Selection.addList(ents);
}
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_PLACEMENT:
switch (ev.type)
{
case "mousemotion":
placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
if (placementSupport.mode === "wall")
{
// Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is
// still selecting a starting point (which must necessarily be on-screen). (The update of the snap entities
// itself happens in the call to updateBuildingPlacementPreview below).
placementSupport.wallSnapEntitiesIncludeOffscreen = false;
}
else
{
// cancel if not enough resources
if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost }))
{
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
});
if (snapData)
{
placementSupport.angle = snapData.angle;
placementSupport.position.x = snapData.x;
placementSupport.position.z = snapData.z;
}
}
updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
return false; // continue processing mouse motion
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
if (placementSupport.mode === "wall")
{
var validPlacement = updateBuildingPlacementPreview();
if (validPlacement !== false)
inputState = INPUT_BUILDING_WALL_CLICK;
}
else
{
placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
dragStart = [ ev.x, ev.y ];
inputState = INPUT_BUILDING_CLICK;
}
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
break;
case "hotkeydown":
var rotation_step = Math.PI / 12; // 24 clicks make a full rotation
switch (ev.hotkey)
{
case "session.rotate.cw":
placementSupport.angle += rotation_step;
updateBuildingPlacementPreview();
break;
case "session.rotate.ccw":
placementSupport.angle -= rotation_step;
updateBuildingPlacementPreview();
break;
}
break;
}
break;
}
return false;
}
function doAction(action, ev)
{
var selection = g_Selection.toList();
// If shift is down, add the order to the unit's order queue instead
// of running it immediately
var queued = Engine.HotkeyIsPressed("session.queue");
var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
if (unitActions[action.type] && unitActions[action.type].execute)
return unitActions[action.type].execute(target, action, selection, queued);
error("Invalid action.type "+action.type);
return false;
}
function handleMinimapEvent(target)
{
// Partly duplicated from handleInputAfterGui(), but with the input being
// world coordinates instead of screen coordinates.
if (inputState != INPUT_NORMAL)
return false;
var fromMinimap = true;
var action = determineAction(undefined, undefined, fromMinimap);
if (!action)
return false;
var selection = g_Selection.toList();
var queued = Engine.HotkeyIsPressed("session.queue");
if (unitActions[action.type] && unitActions[action.type].execute)
return unitActions[action.type].execute(target, action, selection, queued);
error("Invalid action.type "+action.type);
return false;
}
// Called by GUI when user clicks construction button
// @param buildTemplate Template name of the entity the user wants to build
function startBuildingPlacement(buildTemplate, playerState)
{
if(getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0)
return;
// TODO: we should clear any highlight selection rings here. If the mouse was over an entity before going onto the GUI
// to start building a structure, then the highlight selection rings are kept during the construction of the building.
// Gives the impression that somehow the hovered-over entity has something to do with the building you're constructing.
placementSupport.Reset();
// find out if we're building a wall, and change the entity appropriately if so
var templateData = GetTemplateData(buildTemplate);
if (templateData.wallSet)
{
placementSupport.mode = "wall";
placementSupport.wallSet = templateData.wallSet;
inputState = INPUT_BUILDING_PLACEMENT;
}
else
{
placementSupport.mode = "building";
placementSupport.template = buildTemplate;
inputState = INPUT_BUILDING_PLACEMENT;
}
if (templateData.attack &&
templateData.attack.Ranged &&
templateData.attack.Ranged.maxRange)
{
// add attack information to display a good tooltip
placementSupport.attack = templateData.attack;
}
}
// Camera jumping: when the user presses a hotkey the current camera location is marked.
// When they press another hotkey the camera jumps back to that position. If the camera is already roughly at that location,
// jump back to where it was previously.
var jumpCameraPositions = [];
var jumpCameraLast;
var jumpCameraDistanceThreshold = Engine.ConfigDB_GetValue("user", "gui.session.camerajump.threshold");
function jumpCamera(index)
{
var position = jumpCameraPositions[index];
if (position)
{
if (jumpCameraLast &&
Math.abs(Engine.CameraGetX() - position.x) < jumpCameraDistanceThreshold &&
Math.abs(Engine.CameraGetZ() - position.z) < jumpCameraDistanceThreshold)
Engine.CameraMoveTo(jumpCameraLast.x, jumpCameraLast.z);
else
{
jumpCameraLast = {x: Engine.CameraGetX(), z: Engine.CameraGetZ()};
Engine.CameraMoveTo(position.x, position.z);
}
}
}
function setJumpCamera(index)
{
jumpCameraPositions[index] = {x: Engine.CameraGetX(), z: Engine.CameraGetZ()};
}
// Batch training:
// When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING
// When the user releases shift, or clicks on a different training button, we create the batched units
var batchTrainingEntities;
var batchTrainingType;
var batchTrainingCount;
var batchTrainingEntityAllowedCount;
function flushTrainingBatch()
{
var appropriateBuildings = getBuildingsWhichCanTrainEntity(batchTrainingEntities, batchTrainingType);
// If training limits don't allow us to train batchTrainingCount in each appropriate building
if (batchTrainingEntityAllowedCount !== undefined &&
batchTrainingEntityAllowedCount < batchTrainingCount * appropriateBuildings.length)
{
// Train as many full batches as we can
var buildingsCountToTrainFullBatch = Math.floor(batchTrainingEntityAllowedCount / batchTrainingCount);
var buildingsToTrainFullBatch = appropriateBuildings.slice(0, buildingsCountToTrainFullBatch);
Engine.PostNetworkCommand({
"type": "train",
"entities": buildingsToTrainFullBatch,
"template": batchTrainingType,
"count": batchTrainingCount
});
// Train remainer in one more building
Engine.PostNetworkCommand({
"type": "train",
"entities": [ appropriateBuildings[buildingsCountToTrainFullBatch] ],
"template": batchTrainingType,
"count": batchTrainingEntityAllowedCount % batchTrainingCount
});
}
else
Engine.PostNetworkCommand({
"type": "train",
"entities": appropriateBuildings,
"template": batchTrainingType,
"count": batchTrainingCount
});
}
function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType)
{
return entitiesToCheck.filter(entity => {
var state = GetEntityState(entity);
var canTrain = state && state.production && state.production.entities.length &&
state.production.entities.indexOf(trainEntType) != -1;
return canTrain;
});
}
function getEntityLimitAndCount(playerState, entType)
{
var r = {
"entLimit": undefined,
"entCount": undefined,
"entLimitChangers": undefined,
"canBeAddedCount": undefined
};
if (!playerState.entityLimits)
return r;
var template = GetTemplateData(entType);
var entCategory = null;
if (template.trainingRestrictions)
entCategory = template.trainingRestrictions.category;
else if (template.buildRestrictions)
entCategory = template.buildRestrictions.category;
if (entCategory && playerState.entityLimits[entCategory] !== undefined)
{
r.entLimit = playerState.entityLimits[entCategory] || 0;
r.entCount = playerState.entityCounts[entCategory] || 0;
r.entLimitChangers = playerState.entityLimitChangers[entCategory];
r.canBeAddedCount = Math.max(r.entLimit - r.entCount, 0);
}
return r;
}
// Add the unit shown at position to the training queue for all entities in the selection
function addTrainingByPosition(position)
{
var simState = GetSimState();
var playerState = simState.players[Engine.GetPlayerID()];
var selection = g_Selection.toList();
if (!playerState || !selection.length)
return;
var trainableEnts = getAllTrainableEntitiesFromSelection();
// Check if the position is valid
if (!trainableEnts.length || trainableEnts.length <= position)
return;
var entToTrain = trainableEnts[position];
addTrainingToQueue(selection, entToTrain, playerState);
return;
}
// Called by GUI when user clicks training button
function addTrainingToQueue(selection, trainEntType, playerState)
{
// Create list of buildings which can train trainEntType
var appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
// Check trainEntType entity limit and count
var limits = getEntityLimitAndCount(playerState, trainEntType);
// Batch training possible if we can train at least 2 units
var batchTrainingPossible = limits.canBeAddedCount == undefined || limits.canBeAddedCount > 1;
var decrement = Engine.HotkeyIsPressed("selection.remove");
if (!decrement)
var template = GetTemplateData(trainEntType);
let batchIncrementSize = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize");
if (Engine.HotkeyIsPressed("session.batchtrain") && batchTrainingPossible)
{
if (inputState == INPUT_BATCHTRAINING)
{
// Check if we are training in the same building(s) as the last batch
var sameEnts = false;
if (batchTrainingEntities.length == selection.length)
{
// NOTE: We just check if the arrays are the same and if the order is the same
// If the order changed, we have a new selection and we should create a new batch.
for (var i = 0; i < batchTrainingEntities.length; ++i)
{
if (!(sameEnts = batchTrainingEntities[i] == selection[i]))
break;
}
}
// If we're already creating a batch of this unit (in the same building(s)), then just extend it
// (if training limits allow)
if (sameEnts && batchTrainingType == trainEntType)
{
if (decrement)
{
batchTrainingCount -= batchIncrementSize;
if (batchTrainingCount <= 0)
inputState = INPUT_NORMAL;
}
else if (limits.canBeAddedCount == undefined ||
limits.canBeAddedCount > batchTrainingCount * appropriateBuildings.length)
{
if (Engine.GuiInterfaceCall("GetNeededResources", { "cost":
multiplyEntityCosts(template, batchTrainingCount + batchIncrementSize) }))
return;
batchTrainingCount += batchIncrementSize;
}
batchTrainingEntityAllowedCount = limits.canBeAddedCount;
return;
}
// Otherwise start a new one
else if (!decrement)
{
flushTrainingBatch();
// fall through to create the new batch
}
}
// Don't start a new batch if decrementing or unable to afford it.
if (decrement || Engine.GuiInterfaceCall("GetNeededResources", { "cost":
multiplyEntityCosts(template, batchIncrementSize) }))
return;
inputState = INPUT_BATCHTRAINING;
batchTrainingEntities = selection;
batchTrainingType = trainEntType;
batchTrainingEntityAllowedCount = limits.canBeAddedCount;
batchTrainingCount = batchIncrementSize;
}
else
{
// Non-batched - just create a single entity in each building
// (but no more than entity limit allows)
var buildingsForTraining = appropriateBuildings;
if (limits.entLimit)
buildingsForTraining = buildingsForTraining.slice(0, limits.canBeAddedCount);
Engine.PostNetworkCommand({
"type": "train",
"template": trainEntType,
"count": 1,
"entities": buildingsForTraining
});
}
}
// Called by GUI when user clicks research button
function addResearchToQueue(entity, researchType)
{
Engine.PostNetworkCommand({ "type": "research", "entity": entity, "template": researchType });
}
// Returns the number of units that will be present in a batch if the user clicks
// the training button with shift down
function getTrainingBatchStatus(playerState, entity, trainEntType, selection)
{
let batchIncrementSize = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize");
var appropriateBuildings = [entity];
if (selection && selection.indexOf(entity) != -1)
appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
var nextBatchTrainingCount = 0;
var currentBatchTrainingCount = 0;
if (inputState == INPUT_BATCHTRAINING && batchTrainingEntities.indexOf(entity) != -1 &&
batchTrainingType == trainEntType)
{
nextBatchTrainingCount = batchTrainingCount;
currentBatchTrainingCount = batchTrainingCount;
var limits = {
"canBeAddedCount": batchTrainingEntityAllowedCount
};
}
else
{
var limits = getEntityLimitAndCount(playerState, trainEntType);
}
// We need to calculate count after the next increment if it's possible
if (limits.canBeAddedCount == undefined ||
limits.canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length)
nextBatchTrainingCount += batchIncrementSize;
// If training limits don't allow us to train batchTrainingCount in each appropriate building
// train as many full batches as we can and remainer in one more building.
var buildingsCountToTrainFullBatch = appropriateBuildings.length;
var remainderToTrain = 0;
if (limits.canBeAddedCount !== undefined &&
limits.canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length)
{
buildingsCountToTrainFullBatch = Math.floor(limits.canBeAddedCount / nextBatchTrainingCount);
remainderToTrain = limits.canBeAddedCount % nextBatchTrainingCount;
}
return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain, currentBatchTrainingCount];
}
// Called by GUI when user clicks production queue item
function removeFromProductionQueue(entity, id)
{
Engine.PostNetworkCommand({ "type": "stop-production", "entity": entity, "id": id });
}
// Called by unit selection buttons
function changePrimarySelectionGroup(templateName, deselectGroup)
{
g_Selection.makePrimarySelection(templateName, Engine.HotkeyIsPressed("session.deselectgroup") || deselectGroup);
}
// Performs the specified command (delete, town bell, repair, etc.)
function performCommand(entity, commandName)
{
if (!entity)
return;
var entState = GetExtendedEntityState(entity);
if (!controlsPlayer(entState.player) &&
!(g_IsObserver && commandName == "focus-rally"))
return;
if (g_EntityCommands[commandName])
g_EntityCommands[commandName].execute(entState);
}
// Performs the specified command for ally unit
function performAllyCommand(entity, commandName)
{
if (!entity)
return;
var entState = GetExtendedEntityState(entity);
var playerState = GetSimState().players[Engine.GetPlayerID()];
if (!playerState.isMutualAlly[entState.player] || g_IsObserver)
return;
if (g_AllyEntityCommands[commandName])
g_AllyEntityCommands[commandName].execute(entState);
}
// Performs the specified formation
function performFormation(entity, formationTemplate)
{
if (entity)
Engine.PostNetworkCommand({
"type": "formation",
"entities": g_Selection.toList(),
"name": formationTemplate
});
}
// Performs the specified group
function performGroup(action, groupId)
{
switch (action)
{
case "snap":
case "select":
case "add":
var toSelect = [];
g_Groups.update();
for (var ent in g_Groups.groups[groupId].ents)
toSelect.push(+ent);
if (action != "add")
g_Selection.reset();
g_Selection.addList(toSelect);
if (action == "snap" && toSelect.length)
{
let entState = GetEntityState(toSelect[0]);
let position = entState.position;
if (position && entState.visibility != "hidden")
Engine.CameraMoveTo(position.x, position.z);
}
break;
case "save":
case "breakUp":
g_Groups.groups[groupId].reset();
if (action == "save")
g_Groups.addEntities(groupId, g_Selection.toList());
updateGroups();
break;
}
}
// Performs the specified stance
function performStance(entity, stanceName)
{
if (entity)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "stance",
"entities": selection,
"name": stanceName
});
}
}
// Lock / Unlock the gate
function lockGate(lock)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "lock-gate",
"entities": selection,
"lock": lock,
});
}
// Pack / unpack unit(s)
function packUnit(pack)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "pack",
"entities": selection,
"pack": pack,
"queued": false
});
}
// Cancel un/packing unit(s)
function cancelPackUnit(pack)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "cancel-pack",
"entities": selection,
"pack": pack,
"queued": false
});
}
-// Transform a wall to a gate
-function transformWallToGate(template)
+// Upgrade an entity to another
+function upgradeEntity(Template)
{
- var selection = g_Selection.toList();
Engine.PostNetworkCommand({
- "type": "wall-to-gate",
- "entities": selection.filter(e => getWallGateTemplate(e) == template),
- "template": template,
+ "type": "upgrade",
+ "entities": g_Selection.toList(),
+ "template": Template,
+ "queued": false
});
}
-// Gets the gate form (if any) of a given long wall piece
-function getWallGateTemplate(entity)
+// Cancel upgrading entities
+function cancelUpgradeEntity()
{
- // TODO: find the gate template name in a better way
- var entState = GetEntityState(entity);
- var index;
-
- if (entState && !entState.foundation && hasClass(entState, "LongWall") && (index = entState.template.indexOf("long")) >= 0)
- return entState.template.substr(0, index) + "gate";
- return undefined;
+ Engine.PostNetworkCommand({
+ "type": "cancel-upgrade",
+ "entities": g_Selection.toList(),
+ "queued": false
+ });
}
// Set the camera to follow the given unit
function setCameraFollow(entity)
{
// Follow the given entity if it's a unit
if (entity)
{
var entState = GetEntityState(entity);
if (entState && hasClass(entState, "Unit"))
{
Engine.CameraFollow(entity);
return;
}
}
// Otherwise stop following
Engine.CameraFollow(0);
}
var lastIdleUnit = 0;
var currIdleClassIndex = 0;
var lastIdleClasses = [];
function resetIdleUnit()
{
lastIdleUnit = 0;
currIdleClassIndex = 0;
lastIdleClasses = [];
}
function findIdleUnit(classes)
{
var append = Engine.HotkeyIsPressed("selection.add");
var selectall = Engine.HotkeyIsPressed("selection.offscreen");
// Reset the last idle unit, etc., if the selection type has changed.
if (selectall || classes.length != lastIdleClasses.length || !classes.every((v,i) => v === lastIdleClasses[i]))
resetIdleUnit();
lastIdleClasses = classes;
var data = {
"viewedPlayer": g_ViewedPlayer,
"excludeUnits": append ? g_Selection.toList() : [],
// If the current idle class index is not 0, put the class at that index first.
"idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex))
};
if (!selectall)
{
data.limit = 1;
data.prevUnit = lastIdleUnit;
}
var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data);
if (!idleUnits.length)
{
// TODO: display a message or play a sound to indicate no more idle units, or something
// Reset for next cycle
resetIdleUnit();
return;
}
if (!append)
g_Selection.reset();
g_Selection.addList(idleUnits);
if (selectall)
return;
lastIdleUnit = idleUnits[0];
var entityState = GetEntityState(lastIdleUnit);
var position = entityState.position;
if (position)
Engine.CameraMoveTo(position.x, position.z);
// Move the idle class index to the first class an idle unit was found for.
var indexChange = data.idleClasses.findIndex(elem => hasClass(entityState, elem));
currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length;
}
function stopUnits(entities)
{
Engine.PostNetworkCommand({ "type": "stop", "entities": entities, "queued": false });
}
function unload(garrisonHolder, entities)
{
if (Engine.HotkeyIsPressed("session.unloadtype"))
Engine.PostNetworkCommand({ "type": "unload", "entities": entities, "garrisonHolder": garrisonHolder });
else
Engine.PostNetworkCommand({ "type": "unload", "entities": [entities[0]], "garrisonHolder": garrisonHolder });
}
function unloadTemplate(template)
{
// Filter out all entities that aren't garrisonable.
var garrisonHolders = g_Selection.toList().filter(e => {
var state = GetEntityState(e);
if (state && state.garrisonHolder)
return true;
return false;
});
Engine.PostNetworkCommand({
"type": "unload-template",
"all": Engine.HotkeyIsPressed("session.unloadtype"),
"template": template,
"garrisonHolders": garrisonHolders
});
}
function unloadSelection()
{
var parent = 0;
var ents = [];
for each (var ent in g_Selection.selected)
{
var state = GetExtendedEntityState(ent);
if (!state || !state.turretParent)
continue;
if (!parent)
{
parent = state.turretParent;
ents.push(ent);
}
else if (state.turretParent == parent)
ents.push(ent);
}
if (parent)
Engine.PostNetworkCommand({ "type": "unload", "entities":ents, "garrisonHolder": parent });
}
function unloadAllByOwner()
{
var garrisonHolders = g_Selection.toList().filter(e => {
var state = GetEntityState(e);
return state && state.garrisonHolder;
});
Engine.PostNetworkCommand({ "type": "unload-all-by-owner", "garrisonHolders": garrisonHolders });
}
function unloadAll()
{
// Filter out all entities that aren't garrisonable.
var garrisonHolders = g_Selection.toList().filter(e => {
var state = GetEntityState(e);
return state && state.garrisonHolder;
});
Engine.PostNetworkCommand({ "type": "unload-all", "garrisonHolders": garrisonHolders });
}
function backToWork()
{
// Filter out all entities that can't go back to work.
var workers = g_Selection.toList().filter(e => {
var state = GetEntityState(e);
return state && state.unitAI && state.unitAI.hasWorkOrders;
});
Engine.PostNetworkCommand({ "type": "back-to-work", "entities": workers });
}
function removeGuard()
{
// Filter out all entities that are currently guarding/escorting.
var entities = g_Selection.toList().filter(e => {
var state = GetEntityState(e);
return state && state.unitAI && state.unitAI.isGuarding;
});
Engine.PostNetworkCommand({ "type": "remove-guard", "entities": entities });
}
function increaseAlertLevel()
{
var entities = g_Selection.toList().filter(e => {
var state = GetEntityState(e);
return state && state.alertRaiser && state.alertRaiser.canIncreaseLevel;
});
Engine.PostNetworkCommand({ "type": "increase-alert-level", "entities": entities });
}
function endOfAlert()
{
var entities = g_Selection.toList().filter(e => {
var state = GetEntityState(e);
return state && state.alertRaiser && state.alertRaiser.hasRaisedAlert;
});
Engine.PostNetworkCommand({ "type": "alert-end", "entities": entities });
}
function clearSelection()
{
if(inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING)
{
inputState = INPUT_NORMAL;
placementSupport.Reset();
}
else
g_Selection.reset();
preSelectedAction = ACTION_NONE;
}
/**
* Returns a list of all items in the productionqueue of the selection
* @param selection List with entity ids
*/
function getTrainingQueueItems(selection)
{
var entStates = [];
for (var ent of selection)
{
var entState = GetEntityState(ent);
if (entState.production)
entStates.push(entState);
}
var queue = [];
var i = 0;
do
{
var foundNewItems = false;
for (entState of entStates)
{
if (!entState.production.queue[i])
continue;
var item = entState.production.queue[i];
item.producingEnt = entState.id;
queue.push(item);
foundNewItems = true;
}
++i;
}
while (foundNewItems);
return queue;
}
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 18466)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 18467)
@@ -1,1081 +1,1171 @@
/**
* Contains the layout and button settings per selection panel
*
* getItems returns a list of basic items used to fill the panel.
* This method is obligated. If the items list is empty, the panel
* won't be rendered.
*
* Then there's a loop over all items provided. In the loop,
* the item and some other standard data is added to a data object.
*
* The standard data is
* {
* "i": index
* "item": item coming from the getItems function
* "selection": list of currently selected items
* "playerState": playerState
* "unitEntState": first selected entity state
* "rowLength": rowLength
* "numberOfItems": number of items that will be processed
* "button": gui Button object
* "icon": gui Icon object
* "guiSelection": gui button Selection overlay
* "countDisplay": gui caption space
* }
*
* Then for every data object, the setupButton function is called which
* sets the view and handlers of the button.
*/
// Cache some formation info
// Available formations per player
let g_AvailableFormations = new Map();
let g_FormationsInfo = new Map();
let g_SelectionPanels = {};
g_SelectionPanels.Alert = {
"getMaxNumberOfItems": function()
{
return 2;
},
"getItems": function(unitEntState)
{
if (!unitEntState.alertRaiser)
return [];
return ["increase", "end"];
},
"setupButton": function(data)
{
data.button.onPress = function() {
if (data.item == "increase")
increaseAlertLevel();
else if (data.item == "end")
endOfAlert();
};
if (data.item == "increase")
{
if (data.unitEntState.alertRaiser.hasRaisedAlert)
data.button.tooltip = translate("Increase the alert level to protect more units");
else
data.button.tooltip = translate("Raise an alert!");
}
else if (data.item == "end")
data.button.tooltip = translate("End of alert.");
if (data.item == "increase")
{
data.button.hidden = !data.unitEntState.alertRaiser.canIncreaseLevel;
if (data.unitEntState.alertRaiser.hasRaisedAlert)
data.icon.sprite = "stretched:session/icons/bell_level2.png";
else
data.icon.sprite = "stretched:session/icons/bell_level1.png";
}
else if (data.item == "end")
{
data.button.hidden = !data.unitEntState.alertRaiser.hasRaisedAlert;
data.icon.sprite = "stretched:session/icons/bell_level0.png";
}
data.button.enabled = !data.button.hidden && controlsPlayer(data.unitEntState.player);
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Barter = {
"getMaxNumberOfItems": function()
{
return 4;
},
"rowLength": 4,
"getItems": function(unitEntState, selection)
{
if (!unitEntState.barterMarket)
return [];
// ["food", "wood", "stone", "metal"]
return BARTER_RESOURCES;
},
"setupButton": function(data)
{
// data.item is the resource name in this case
let button = {};
let icon = {};
let amount = {};
for (let a of BARTER_ACTIONS)
{
button[a] = Engine.GetGUIObjectByName("unitBarter" + a + "Button[" + data.i + "]");
icon[a] = Engine.GetGUIObjectByName("unitBarter" + a + "Icon[" + data.i + "]");
amount[a] = Engine.GetGUIObjectByName("unitBarter" + a + "Amount[" + data.i + "]");
}
let selectionIcon = Engine.GetGUIObjectByName("unitBarterSellSelection[" + data.i + "]");
let amountToSell = BARTER_RESOURCE_AMOUNT_TO_SELL;
if (Engine.HotkeyIsPressed("session.massbarter"))
amountToSell *= BARTER_BUNCH_MULTIPLIER;
amount.Sell.caption = "-" + amountToSell;
let prices = data.unitEntState.barterMarket.prices;
amount.Buy.caption = "+" + Math.round(prices.sell[g_BarterSell] / prices.buy[data.item] * amountToSell);
let resource = getLocalizedResourceName(data.item, "withinSentence");
button.Buy.tooltip = sprintf(translate("Buy %(resource)s"), { "resource": resource });
button.Sell.tooltip = sprintf(translate("Sell %(resource)s"), { "resource": resource });
button.Sell.onPress = function() {
g_BarterSell = data.item;
updateSelectionDetails();
};
button.Buy.onPress = function() {
Engine.PostNetworkCommand({
"type": "barter",
"sell": g_BarterSell,
"buy": data.item,
"amount": amountToSell
});
};
let isSelected = data.item == g_BarterSell;
let grayscale = isSelected ? "color: 0 0 0 100:grayscale:" : "";
// do we have enough of this resource to sell?
let neededRes = {};
neededRes[data.item] = amountToSell;
let canSellCurrent = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": neededRes,
"player": data.unitEntState.player
}) ? "color:255 0 0 80:" : "";
// Let's see if we have enough resources to barter.
neededRes = {};
neededRes[g_BarterSell] = amountToSell;
let canBuyAny = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": neededRes,
"player": data.unitEntState.player
}) ? "color:255 0 0 80:" : "";
icon.Sell.sprite = canSellCurrent + "stretched:" + grayscale + "session/icons/resources/" + data.item + ".png";
icon.Buy.sprite = canBuyAny + "stretched:" + grayscale + "session/icons/resources/" + data.item + ".png";
button.Buy.hidden = isSelected;
button.Buy.enabled = controlsPlayer(data.unitEntState.player);
button.Sell.hidden = false;
selectionIcon.hidden = !isSelected;
setPanelObjectPosition(button.Sell, data.i, data.rowLength);
setPanelObjectPosition(button.Buy, data.i + data.rowLength, data.rowLength);
return true;
}
};
g_SelectionPanels.Command = {
"getMaxNumberOfItems": function()
{
return 6;
},
"getItems": function(unitEntState)
{
let commands = [];
for (let c in g_EntityCommands)
{
let info = g_EntityCommands[c].getInfo(unitEntState);
if (!info)
continue;
info.name = c;
commands.push(info);
}
return commands;
},
"setupButton": function(data)
{
data.button.tooltip = data.item.tooltip;
data.button.onPress = function() {
if (data.item.callback)
data.item.callback(data.item);
else
performCommand(data.unitEntState.id, data.item.name);
};
data.countDisplay.caption = data.item.count || "";
data.button.enabled =
controlsPlayer(data.unitEntState.player) ||
(g_IsObserver && data.item.name == "focus-rally");
data.icon.sprite = "stretched:session/icons/" + data.item.icon;
let size = data.button.size;
// count on square buttons, so size.bottom is the width too
let spacer = size.bottom + 1;
// relative to the center ( = 50%)
size.rleft = size.rright = 50;
// offset from the center calculation
size.left = (data.i - data.numberOfItems/2) * spacer;
size.right = size.left + size.bottom;
data.button.size = size;
return true;
}
};
g_SelectionPanels.AllyCommand = {
"getMaxNumberOfItems": function()
{
return 2;
},
"conflictsWith": ["Command"],
"getItems": function(unitEntState)
{
let commands = [];
for (let c in g_AllyEntityCommands)
{
let info = g_AllyEntityCommands[c].getInfo(unitEntState);
if (!info)
continue;
info.name = c;
commands.push(info);
}
return commands;
},
"setupButton": function(data)
{
data.button.tooltip = data.item.tooltip;
data.button.onPress = function() {
if (data.item.callback)
data.item.callback(data.item);
else
performAllyCommand(data.unitEntState.id, data.item.name);
};
data.countDisplay.caption = data.item.count || "";
data.button.enabled = !!data.item.count;
let grayscale = data.button.enabled ? "" : "grayscale:";
data.icon.sprite = "stretched:" + grayscale + "session/icons/" + data.item.icon;
let size = data.button.size;
// count on square buttons, so size.bottom is the width too
let spacer = size.bottom + 1;
// relative to the center ( = 50%)
size.rleft = size.rright = 50;
// offset from the center calculation
size.left = (data.i - data.numberOfItems/2) * spacer;
size.right = size.left + size.bottom;
data.button.size = size;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Construction = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function()
{
return getAllBuildableEntitiesFromSelection();
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item);
if (!template)
return false;
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": data.unitEntState.player
});
let neededResources;
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, 1),
"player": data.unitEntState.player
});
if (template.wallSet)
template.auras = GetTemplateData(template.wallSet.templates.long).auras;
data.button.onPress = function () { startBuildingPlacement(data.item, data.playerState); };
let tooltips = [
getEntityNamesFormatted,
getVisibleEntityClassesFormatted,
getAurasTooltip,
getEntityTooltip,
getEntityCostTooltip,
getGarrisonTooltip,
getProjectilesTooltip,
getPopulationBonusTooltip
].map(func => func(template));
let limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers));
if (!technologyEnabled)
tooltips.push(sprintf(translate("Requires %(technology)s"), {
"technology": getEntityNames(GetTechnologyData(template.requiredTechnology))
}));
tooltips.push(getNeededResourcesTooltip(neededResources));
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
if (!technologyEnabled || limits.canBeAddedCount == 0)
{
data.button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
data.button.enabled = false;
modifier += resourcesToAlphaMask(neededResources) +":";
}
else
data.button.enabled = controlsPlayer(data.unitEntState.player);
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
let index = data.i + getNumberOfRightPanelButtons();
setPanelObjectPosition(data.button, index, data.rowLength);
return true;
}
};
g_SelectionPanels.Formation = {
"getMaxNumberOfItems": function()
{
return 16;
},
"rowLength": 4,
"conflictsWith": ["Garrison"],
"getItems": function(unitEntState)
{
if (!hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal"))
return [];
if (!g_AvailableFormations.has(unitEntState.player))
g_AvailableFormations.set(unitEntState.player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntState.player));
return g_AvailableFormations.get(unitEntState.player);
},
"setupButton": function(data)
{
if (!g_FormationsInfo.has(data.item))
g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item }));
let formationInfo = g_FormationsInfo.get(data.item);
let formationOk = canMoveSelectionIntoFormation(data.item);
let formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", {
"ents": data.selection,
"formationTemplate": data.item
});
data.button.onPress = function() { performFormation(data.unitEntState.id, data.item); };
let tooltip = translate(formationInfo.name);
if (!formationOk && formationInfo.tooltip)
tooltip += "\n" + "[color=\"red\"]" + translate(formationInfo.tooltip) + "[/color]";
data.button.tooltip = tooltip;
data.button.enabled = formationOk && controlsPlayer(data.unitEntState.player);
let grayscale = formationOk ? "" : "grayscale:";
data.guiSelection.hidden = !formationSelected;
data.icon.sprite = "stretched:" + grayscale + "session/icons/" + formationInfo.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Garrison = {
"getMaxNumberOfItems": function()
{
return 12;
},
"rowLength": 4,
"getItems": function(unitEntState, selection)
{
if (!unitEntState.garrisonHolder)
return [];
let groups = new EntityGroups();
for (let ent of selection)
{
let state = GetEntityState(ent);
if (state.garrisonHolder)
groups.add(state.garrisonHolder.entities);
}
return groups.getEntsGrouped();
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item.template);
if (!template)
return false;
data.button.onPress = function() {
unloadTemplate(data.item.template);
};
data.countDisplay.caption = data.item.ents.length || "";
let garrisonedUnitOwner = GetEntityState(data.item.ents[0]).player;
let canUngarrison =
g_ViewedPlayer == data.unitEntState.player ||
g_ViewedPlayer == garrisonedUnitOwner;
data.button.enabled = canUngarrison && controlsPlayer(g_ViewedPlayer);
let tooltip = canUngarrison || g_IsObserver ?
sprintf(translate("Unload %(name)s"),
{ "name": getEntityNames(template) }) + "\n" +
translate("Single-click to unload 1. Shift-click to unload all of this type.") :
getEntityNames(template);
tooltip += "\n" + sprintf(translate("Player: %(playername)s"), {
"playername": g_Players[garrisonedUnitOwner].name
});
data.button.tooltip = tooltip;
data.button.sprite = "color:" + rgbToGuiColor(g_Players[garrisonedUnitOwner].color) + ":";
data.button.sprite_disabled = data.button.sprite;
// Selection panel buttons only appear disabled if they
// also appear disabled to the owner of the building.
data.icon.sprite =
(canUngarrison || g_IsObserver ? "" : "grayscale:") +
"stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Gate = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function(unitEntState, selection)
{
- // Allow long wall pieces to be converted to gates
- let longWallTypes = {};
- let walls = [];
let gates = [];
for (let ent of selection)
{
let state = GetEntityState(ent);
- if (hasClass(state, "LongWall") && !state.gate && !longWallTypes[state.template])
- {
- let gateTemplate = getWallGateTemplate(state.id);
- if (gateTemplate)
- {
- let tooltipString = GetTemplateDataWithoutLocalization(state.template).gateConversionTooltip;
- if (!tooltipString)
- {
- warn(state.template + " is supposed to be convertable to a gate, but it's missing the GateConversionTooltip in the Identity template");
- tooltipString = "";
- }
- walls.push({
- "tooltip": translate(tooltipString),
- "template": gateTemplate,
- "callback": function (item) { transformWallToGate(item.template); }
- });
- }
-
- // We only need one entity per type.
- longWallTypes[state.template] = true;
- }
- else if (state.gate && !gates.length)
+ if (state.gate && !gates.length)
{
gates.push({
"gate": state.gate,
"tooltip": translate("Lock Gate"),
"locked": true,
"callback": function (item) { lockGate(item.locked); }
});
gates.push({
"gate": state.gate,
"tooltip": translate("Unlock Gate"),
"locked": false,
"callback": function (item) { lockGate(item.locked); }
});
}
// Show both 'locked' and 'unlocked' as active if the selected gates have both lock states.
else if (state.gate && state.gate.locked != gates[0].gate.locked)
for (let j = 0; j < gates.length; ++j)
delete gates[j].gate.locked;
}
- // Place wall conversion options after gate lock/unlock icons.
- return gates.concat(walls);
+ return gates;
},
"setupButton": function(data)
{
data.button.onPress = function() {data.item.callback(data.item); };
- let tooltips = [data.item.tooltip];
- if (data.item.template)
- {
- data.template = GetTemplateData(data.item.template);
- data.wallCount = data.selection.reduce(count, ent => {
- let state = GetEntityState(ent);
- if (hasClass(state, "LongWall") && !state.gate)
- ++count;
- return count;
- }, 0);
-
- tooltips.push(getEntityCostTooltip(data.template, data.wallCount));
-
- data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
- "cost": multiplyEntityCosts(data.template, data.wallCount)
- });
-
- tooltips.push(getNeededResourcesTooltip(data.neededResources));
- }
- data.button.tooltip = tooltips.filter(tip => tip).join("\n");
+ data.button.tooltip = data.item.tooltip;
data.button.enabled = controlsPlayer(data.unitEntState.player);
let gateIcon;
if (data.item.gate)
{
- // If already a gate, show locking actions
+ // show locking actions
gateIcon = "icons/lock_" + GATE_ACTIONS[data.item.locked ? 0 : 1] + "ed.png";
if (data.item.gate.locked === undefined)
data.guiSelection.hidden = false;
else
data.guiSelection.hidden = data.item.gate.locked != data.item.locked;
}
- else
- {
- // Otherwise show gate upgrade icon
- let template = GetTemplateData(data.item.template);
- if (!template)
- return false;
- gateIcon = data.template.icon ? "portraits/" + data.template.icon : "icons/gate_closed.png";
- data.guiSelection.hidden = true;
- }
data.icon.sprite = (data.neededResources ? resourcesToAlphaMask(data.neededResources) + ":" : "") + "stretched:session/" + gateIcon;
let index = data.i + getNumberOfRightPanelButtons();
setPanelObjectPosition(data.button, index, data.rowLength);
return true;
}
};
g_SelectionPanels.Pack = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function(unitEntState, selection)
{
let checks = {};
for (let ent of selection)
{
let state = GetEntityState(ent);
if (!state.pack)
continue;
if (state.pack.progress == 0)
{
if (!state.pack.packed)
checks.packButton = true;
else if (state.pack.packed)
checks.unpackButton = true;
}
else
{
// Already un/packing - show cancel button
if (!state.pack.packed)
checks.packCancelButton = true;
else if (state.pack.packed)
checks.unpackCancelButton = true;
}
}
let items = [];
if (checks.packButton)
- items.push({ "packing": false, "packed": false, "tooltip": translate("Pack"), "callback": function() { packUnit(true); } });
+ items.push({
+ "packing": false,
+ "packed": false,
+ "tooltip": translate("Pack"),
+ "callback": function() { packUnit(true); }
+ });
if (checks.unpackButton)
- items.push({ "packing": false, "packed": true, "tooltip": translate("Unpack"), "callback": function() { packUnit(false); } });
+ items.push({
+ "packing": false,
+ "packed": true,
+ "tooltip": translate("Unpack"),
+ "callback": function() { packUnit(false); }
+ });
if (checks.packCancelButton)
- items.push({ "packing": true, "packed": false, "tooltip": translate("Cancel Packing"), "callback": function() { cancelPackUnit(true); } });
+ items.push({
+ "packing": true,
+ "packed": false,
+ "tooltip": translate("Cancel Packing"),
+ "callback": function() { cancelPackUnit(true); }
+ });
if (checks.unpackCancelButton)
- items.push({ "packing": true, "packed": true, "tooltip": translate("Cancel Unpacking"), "callback": function() { cancelPackUnit(false); } });
+ items.push({
+ "packing": true,
+ "packed": true,
+ "tooltip": translate("Cancel Unpacking"),
+ "callback": function() { cancelPackUnit(false); }
+ });
return items;
},
"setupButton": function(data)
{
data.button.onPress = function() {data.item.callback(data.item); };
data.button.tooltip = data.item.tooltip;
if (data.item.packing)
data.icon.sprite = "stretched:session/icons/cancel.png";
else if (data.item.packed)
data.icon.sprite = "stretched:session/icons/unpack.png";
else
data.icon.sprite = "stretched:session/icons/pack.png";
data.button.enabled = controlsPlayer(data.unitEntState.player);
let index = data.i + getNumberOfRightPanelButtons();
setPanelObjectPosition(data.button, index, data.rowLength);
return true;
}
};
g_SelectionPanels.Queue = {
"getMaxNumberOfItems": function()
{
return 16;
},
"getItems": function(unitEntState, selection)
{
return getTrainingQueueItems(selection);
},
"resizePanel": function(numberOfItems, rowLength)
{
let numRows = Math.ceil(numberOfItems / rowLength);
let panel = Engine.GetGUIObjectByName("unitQueuePanel");
let size = panel.size;
let buttonSize = Engine.GetGUIObjectByName("unitQueueButton[0]").size.bottom;
let margin = 4;
size.top = size.bottom - numRows*buttonSize - (numRows+2)*margin;
panel.size = size;
},
"setupButton": function(data)
{
// Differentiate between units and techs
let template;
if (data.item.unitTemplate)
template = GetTemplateData(data.item.unitTemplate);
else if (data.item.technologyTemplate)
template = GetTechnologyData(data.item.technologyTemplate);
if (!template)
return false;
data.button.onPress = function() { removeFromProductionQueue(data.item.producingEnt, data.item.id); };
let tooltip = getEntityNames(template);
if (data.item.neededSlots)
{
tooltip += "\n[color=\"red\"]" + translate("Insufficient population capacity:") + "\n[/color]";
tooltip += sprintf(translate("%(population)s %(neededSlots)s"), {
"population": g_CostDisplayIcons.population,
"neededSlots": data.item.neededSlots
});
}
data.button.tooltip = tooltip;
data.countDisplay.caption = data.item.count > 1 ? data.item.count : "";
// Show the progress number for the first item
if (data.i == 0)
Engine.GetGUIObjectByName("queueProgress").caption = Math.round(data.item.progress*100) + "%";
let guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider["+data.i+"]");
let size = guiObject.size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(data.item.progress * (size.right - size.left));
guiObject.size = size;
if (template.icon)
data.icon.sprite = "stretched:session/portraits/" + template.icon;
data.button.enabled = controlsPlayer(data.unitEntState.player);
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Research = {
"getMaxNumberOfItems": function()
{
return 8;
},
"getItems": function(unitEntState, selection)
{
// Tech-pairs require two rows. Thus only show techs when there is only one row in use.
// TODO Use a reference instead of a magic number
if (getNumberOfRightPanelButtons() > 8 && selection.length > 1)
return [];
for (let ent of selection)
{
let entState = GetEntityState(ent);
if (entState.production && entState.production.technologies.length)
return entState.production.technologies.map(tech => ({
"tech": tech,
"techCostMultiplier": entState.production.techCostMultiplier
}));
}
return [];
},
"hideItem": function(i, rowLength) // Called when no item is found
{
Engine.GetGUIObjectByName("unitResearchButton[" + i + "]").hidden = true;
// We also remove the paired tech and the pair symbol
Engine.GetGUIObjectByName("unitResearchButton[" + (i + rowLength) + "]").hidden = true;
Engine.GetGUIObjectByName("unitResearchPair[" + i + "]").hidden = true;
},
"setupButton": function(data)
{
if (!data.item.tech)
{
g_SelectionPanels.Research.hideItem(data.i, data.rowLength);
return false;
}
let techs = data.item.tech.pair ? [data.item.tech.bottom, data.item.tech.top] : [data.item.tech];
// Start position (start at the bottom)
let position = data.i + data.rowLength;
// Only show the top button for pairs
if (!data.item.tech.pair)
Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true;
// Set up the tech connector
let pair = Engine.GetGUIObjectByName("unitResearchPair[" + data.i + "]");
pair.hidden = data.item.tech.pair == null;
setPanelObjectPosition(pair, data.i, data.rowLength);
// Handle one or two techs
for (let i in techs)
{
let tech = techs[i];
let template = GetTechnologyData(tech);
if (!template)
return false;
for (let res in template.cost)
template.cost[res] *= data.item.techCostMultiplier[res];
let neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": template.cost,
"player": data.unitEntState.player
});
let requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", {
"tech": tech,
"player": data.unitEntState.player
});
let button = Engine.GetGUIObjectByName("unitResearchButton[" + position + "]");
let icon = Engine.GetGUIObjectByName("unitResearchIcon[" + position + "]");
let tooltips = [
getEntityNamesFormatted,
getEntityTooltip,
getEntityCostTooltip
].map(func => func(template));
if (!requirementsPassed)
{
let tip = template.requirementsTooltip;
if (template.classRequirements)
{
let player = data.unitEntState.player;
let current = GetSimState().players[player].classCounts[template.classRequirements.class] || 0;
let remaining = template.classRequirements.number - current;
tip += " " + sprintf(translatePlural("Remaining: %(number)s to build.", "Remaining: %(number)s to build.", remaining), {
"number": remaining
});
}
tooltips.push(tip);
}
tooltips.push(getNeededResourcesTooltip(neededResources));
button.tooltip = tooltips.filter(tip => tip).join("\n");
button.onPress = function () {
addResearchToQueue(data.unitEntState.id, tech);
};
if (data.item.tech.pair)
{
// On mouse enter, show a cross over the other icon
let otherPosition = (position + data.rowLength) % (2 * data.rowLength);
let unchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + otherPosition + "]");
button.onMouseEnter = function() {
unchosenIcon.hidden = false;
};
button.onMouseLeave = function() {
unchosenIcon.hidden = true;
};
}
button.hidden = false;
let modifier = "";
if (!requirementsPassed)
{
button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
button.enabled = false;
modifier += resourcesToAlphaMask(neededResources) + ":";
}
else
button.enabled = controlsPlayer(data.unitEntState.player);
if (template.icon)
icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(button, position, data.rowLength);
// Prepare to handle the top button (if any)
position -= data.rowLength;
}
return true;
}
};
g_SelectionPanels.Selection = {
"getMaxNumberOfItems": function()
{
return 16;
},
"rowLength": 4,
"getItems": function(unitEntState, selection)
{
if (selection.length < 2)
return [];
return g_Selection.groups.getTemplateNames();
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item);
if (!template)
return false;
let ents = g_Selection.groups.getEntsByName(data.item);
for (let ent of ents)
{
let state = GetEntityState(ent);
if (state.resourceCarrying && state.resourceCarrying.length !== 0)
{
if (!data.carried)
data.carried = {};
let carrying = state.resourceCarrying[0];
if (data.carried[carrying.type])
data.carried[carrying.type] += carrying.amount;
else
data.carried[carrying.type] = carrying.amount;
}
if (state.trader && state.trader.goods && state.trader.goods.amount)
{
if (!data.carried)
data.carried = {};
let amount = state.trader.goods.amount;
let type = state.trader.goods.type;
let totalGain = amount.traderGain;
if (amount.market1Gain)
totalGain += amount.market1Gain;
if (amount.market2Gain)
totalGain += amount.market2Gain;
if (data.carried[type])
data.carried[type] += totalGain;
else
data.carried[type] = totalGain;
}
}
let tooltip = getEntityNames(template);
if (data.carried)
tooltip += "\n" + Object.keys(data.carried).map(res =>
g_CostDisplayIcons[res] + data.carried[res]
).join(" ");
data.button.tooltip = tooltip;
data.countDisplay.caption = ents.length || "";
data.button.onPress = function() { changePrimarySelectionGroup(data.item, false); };
data.button.onPressRight = function() { changePrimarySelectionGroup(data.item, true); };
if (template.icon)
data.icon.sprite = "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Stance = {
"getMaxNumberOfItems": function()
{
return 5;
},
"getItems": function(unitEntState)
{
if (!unitEntState.unitAI || !hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal"))
return [];
return unitEntState.unitAI.possibleStances;
},
"setupButton": function(data)
{
data.button.onPress = function() { performStance(data.unitEntState, data.item); };
data.button.tooltip = getStanceDisplayName(data.item) + "\n" +
"[font=\"sans-13\"]" + getStanceTooltip(data.item) + "[/font]";
data.guiSelection.hidden = !Engine.GuiInterfaceCall("IsStanceSelected", {
"ents": data.selection,
"stance": data.item
});
data.icon.sprite = "stretched:session/icons/stances/" + data.item + ".png";
data.button.enabled = controlsPlayer(data.unitEntState.player);
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Training = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function()
{
return getAllTrainableEntitiesFromSelection();
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item);
if (!template)
return false;
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": data.unitEntState.player
});
let [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] =
getTrainingBatchStatus(data.playerState, data.unitEntState.id, data.item, data.selection);
let trainNum = buildingsCountToTrainFullBatch || 1;
if (Engine.HotkeyIsPressed("session.batchtrain"))
trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
let neededResources;
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, trainNum),
"player": data.unitEntState.player
});
data.button.onPress = function() { addTrainingToQueue(data.selection, data.item, data.playerState); };
data.countDisplay.caption = trainNum > 1 ? trainNum : "";
let tooltips = [
"[font=\"sans-bold-16\"]" +
colorizeHotkey("%(hotkey)s", "session.queueunit." + (data.i + 1)) +
"[/font]" + " " + getEntityNamesFormatted(template),
getVisibleEntityClassesFormatted(template),
getAurasTooltip(template),
getEntityTooltip(template),
getEntityCostTooltip(template, trainNum, data.unitEntState.id)
];
let limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers));
if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true")
tooltips.push(
getHealthTooltip(template),
getAttackTooltip(template),
getHealerTooltip(template),
getArmorTooltip(template),
getGarrisonTooltip(template),
getProjectilesTooltip(template),
getSpeedTooltip(template)
);
tooltips.push(
"[color=\"" + g_HotkeyColor + "\"]" +
formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch) +
"[/color]");
if (!technologyEnabled)
tooltips.push(sprintf(translate("Requires %(technology)s"), {
"technology": getEntityNames(GetTechnologyData(template.requiredTechnology))
}));
if (neededResources)
tooltips.push(getNeededResourcesTooltip(neededResources));
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
if (!technologyEnabled || limits.canBeAddedCount == 0)
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
data.button.enabled = false;
modifier = resourcesToAlphaMask(neededResources) +":";
}
else
data.button.enabled = controlsPlayer(data.unitEntState.player);
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
let index = data.i + getNumberOfRightPanelButtons();
setPanelObjectPosition(data.button, index, data.rowLength);
return true;
}
};
+g_SelectionPanels.Upgrade = {
+ "getMaxNumberOfItems": function()
+ {
+ return 24 - getNumberOfRightPanelButtons();
+ },
+ "getItems": function(unitEntState, selection)
+ {
+ // Interface becomes complicated with multiple units and this is meant per-entity, so prevent it if the selection has multiple units.
+ // TODO: if the units are all the same, this should probably still be possible.
+ if (selection.length > 1)
+ return false;
+
+ if (!unitEntState.upgrade)
+ return false;
+
+ var items = [];
+
+ for (let upgrade of unitEntState.upgrade.upgrades)
+ {
+ items.push({
+ "entity": upgrade.entity,
+ "cost": upgrade.cost,
+ "time": upgrade.time,
+ "icon": upgrade.icon,
+ "tooltip": upgrade.tooltip,
+ "requiredTechnology": upgrade.requiredTechnology,
+ });
+ }
+ return items;
+ },
+ "setupButton" : function(data)
+ {
+ let template = GetTemplateData(data.item.entity);
+ if (!template)
+ return false;
+
+ let technologyEnabled = true;
+
+ if (data.item.requiredTechnology)
+ technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
+ "tech": requiredTechnology,
+ "player": data.unitEntState.player
+ });
+
+ let neededResources;
+ if (data.item.cost)
+ neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
+ "cost": data.item.cost,
+ "player": data.unitEntState.player
+ });
+
+ let limits = getEntityLimitAndCount(data.playerState, data.item.entity);
+
+ let progress = data.unitEntState.upgrade.progress || 0;
+ let isUpgrading = data.unitEntState.upgrade.template == data.item.entity;
+
+ let tooltip;
+ if (!progress)
+ {
+ if (data.item.tooltip)
+ tooltip = sprintf(translate("Upgrade into a %(name)s. %(tooltip)s"), {
+ "name": template.name.generic,
+ "tooltip": data.item.tooltip
+ });
+ else
+ tooltip = sprintf(translate("Upgrade into a %(name)s."), {"name": template.name.generic});
+
+ if (data.item.cost)
+ tooltip += "\n" + getEntityCostTooltip(data.item);
+
+ tooltip += formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers);
+ if (!technologyEnabled)
+ tooltip += "\n" + sprintf(translate("Requires %(technology)s"), {
+ "technology": getEntityNames(GetTechnologyData(data.item.requiredTechnology))
+ });
+ if (neededResources)
+ tooltip += getNeededResourcesTooltip(neededResources);
+
+ data.button.onPress = function() { upgradeEntity(data.item.entity); };
+ }
+ else if (isUpgrading)
+ {
+ tooltip = translate("Cancel Upgrading");
+ data.button.onPress = function() { cancelUpgradeEntity(); };
+ }
+ else
+ {
+ tooltip = translate("Cannot upgrade when the entity is already upgrading.");
+ data.button.onPress = function() {};
+ }
+ data.button.tooltip = tooltip;
+
+ let modifier = "";
+ if (!isUpgrading)
+ {
+ if (progress || !technologyEnabled || limits.canBeAddedCount == 0)
+ {
+ data.button.enabled = false;
+ modifier = "color:0 0 0 127:grayscale:";
+ }
+ else if (neededResources)
+ {
+ data.button.enabled = false;
+ modifier = resourcesToAlphaMask(neededResources) + ":";
+ }
+ }
+
+ data.icon.sprite = modifier + "stretched:session/" +
+ (data.item.icon || "portraits/" + template.icon);
+
+ let progressOverlay = Engine.GetGUIObjectByName("unitUpgradeProgressSlider[" + data.i + "]");
+ if (isUpgrading)
+ progressOverlay.size.top = progressOverlay.size.left + Math.round(progress * (progressOverlay.size.right - progressOverlay.size.left));
+
+ progressOverlay.hidden = !isUpgrading;
+
+ let index = data.i + getNumberOfRightPanelButtons();
+ setPanelObjectPosition(data.button, index, data.rowLength);
+ return true;
+ }
+};
+
/**
* If two panels need the same space, so they collide,
* the one appearing first in the order is rendered.
*
* Note that the panel needs to appear in the list to get rendered.
*/
let g_PanelsOrder = [
// LEFT PANE
"Barter", // Must always be visible on markets
"Garrison", // More important than Formation, as you want to see the garrisoned units in ships
"Alert",
"Formation",
"Stance", // Normal together with formation
// RIGHT PANE
"Gate", // Must always be shown on gates
"Pack", // Must always be shown on packable entities
+ "Upgrade", // Must always be shown on upgradable entities
"Training",
"Construction",
"Research", // Normal together with training
// UNIQUE PANES (importance doesn't matter)
"Command",
"AllyCommand",
"Queue",
"Selection",
];
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 18466)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 18467)
@@ -1,147 +1,154 @@
const BARTER_RESOURCE_AMOUNT_TO_SELL = 100;
const BARTER_BUNCH_MULTIPLIER = 5;
const BARTER_RESOURCES = ["food", "wood", "stone", "metal"];
const BARTER_ACTIONS = ["Sell", "Buy"];
const GATE_ACTIONS = ["lock", "unlock"];
+// upgrade constants
+const UPGRADING_NOT_STARTED = -2;
+const UPGRADING_CHOSEN_OTHER = -1;
+
+// ==============================================
+// BARTER HELPERS
+// Resources to sell on barter panel
var g_BarterSell = "food";
function canMoveSelectionIntoFormation(formationTemplate)
{
if (!(formationTemplate in g_canMoveIntoFormation))
{
g_canMoveIntoFormation[formationTemplate] = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", {
"ents": g_Selection.toList(),
"formationTemplate": formationTemplate
});
}
return g_canMoveIntoFormation[formationTemplate];
}
function getStanceDisplayName(name)
{
switch (name)
{
case "violent":
return translateWithContext("stance", "Violent");
case "aggressive":
return translateWithContext("stance", "Aggressive");
case "defensive":
return translateWithContext("stance", "Defensive");
case "passive":
return translateWithContext("stance", "Passive");
case "standground":
return translateWithContext("stance", "Standground");
default:
warn("Internationalization: Unexpected stance found: " + name);
return name;
}
}
function getStanceTooltip(name)
{
switch (name)
{
case "violent":
return translateWithContext("stance", "Attack nearby opponents, focus on attackers and chase while visible");
case "aggressive":
return translateWithContext("stance", "Attack nearby opponents");
case "defensive":
return translateWithContext("stance", "Attack nearby opponents, chase a short distance and return to the original location");
case "passive":
return translateWithContext("stance", "Flee if attacked");
case "standground":
return translateWithContext("stance", "Attack opponents in range, but don't move");
default:
return "";
}
}
/**
* Format entity count/limit message for the tooltip
*/
function formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers)
{
if (trainEntLimit == undefined)
return "";
var text = "\n" + sprintf(translate("Current Count: %(count)s, Limit: %(limit)s."), {
"count": trainEntCount,
"limit": trainEntLimit
});
if (trainEntCount >= trainEntLimit)
text = "[color=\"red\"]" + text + "[/color]";
for (var c in trainEntLimitChangers)
{
if (!trainEntLimitChangers[c])
continue;
let string = trainEntLimitChangers[c] > 0 ?
translate("%(changer)s enlarges the limit with %(change)s.") :
translate("%(changer)s lessens the limit with %(change)s.");
text += "\n" + sprintf(string, {
"changer": translate(c),
"change": trainEntLimitChangers[c]
});
}
return text;
}
/**
* Format batch training string for the tooltip
* Examples:
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 5"
* buildingsCountToTrainFullBatch = 2, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 10 (2*5)"
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 15, remainderBatch = 12:
* "Shift-click to train 27 (15 + 12)"
*/
function formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
{
var totalBatchTrainingCount = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
// Don't show the batch training tooltip if either units of this type can't be trained at all
// or only one unit can be trained
if (totalBatchTrainingCount < 2)
return "";
var fullBatchesString = "";
if (buildingsCountToTrainFullBatch > 0)
{
if (buildingsCountToTrainFullBatch > 1)
fullBatchesString = sprintf(translate("%(buildings)s*%(batchSize)s"), {
"buildings": buildingsCountToTrainFullBatch,
"batchSize": fullBatchSize
});
else
fullBatchesString = fullBatchSize;
}
// We need to display the batch details part if there is either more than
// one building with full batch or one building with the full batch and
// another with a partial batch
let batchString;
if (buildingsCountToTrainFullBatch > 1 ||
buildingsCountToTrainFullBatch == 1 && remainderBatch > 0)
{
if (remainderBatch > 0)
batchString = translate("%(action)s to train %(number)s (%(fullBatch)s + %(remainderBatch)s).");
else
batchString = translate("%(action)s to train %(number)s (%(fullBatch)s).");
}
else
batchString = translate("%(action)s to train %(number)s.");
return "[font=\"sans-13\"]" + sprintf(batchString, {
"action": "[font=\"sans-bold-13\"]" + translate("Shift-click") + "[/font]",
"number": totalBatchTrainingCount,
"fullBatch": fullBatchesString,
"remainderBatch": remainderBatch
}) + "[/font]";
}
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_right/upgrade_panel.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_right/upgrade_panel.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_right/upgrade_panel.xml (revision 18467)
@@ -0,0 +1,15 @@
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_right/upgrade_panel.xml
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+text/xml
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 18466)
+++ ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 18467)
@@ -1,233 +1,249 @@
// The number of currently visible buttons (used to optimise showing/hiding)
-var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Training": 0, "Research": 0, "Alert": 0, "Barter": 0, "Construction": 0, "Command": 0, "AllyCommand": 0, "Stance": 0, "Gate": 0, "Pack": 0};
+var g_unitPanelButtons = {
+ "Selection": 0,
+ "Queue": 0,
+ "Formation": 0,
+ "Garrison": 0,
+ "Training": 0,
+ "Research": 0,
+ "Alert": 0,
+ "Barter": 0,
+ "Construction": 0,
+ "Command": 0,
+ "AllyCommand": 0,
+ "Stance": 0,
+ "Gate":0,
+ "Pack": 0,
+ "Upgrade": 0
+};
/**
* Set the position of a panel object according to the index,
* from left to right, from top to bottom.
* Will wrap around to subsequent rows if the index
* is larger than rowLength.
*/
function setPanelObjectPosition(object, index, rowLength, vMargin = 1, hMargin = 1)
{
var size = object.size;
// horizontal position
var oWidth = size.right - size.left;
var hIndex = index % rowLength;
size.left = hIndex * (oWidth + vMargin);
size.right = size.left + oWidth;
// vertical position
var oHeight = size.bottom - size.top;
var vIndex = Math.floor(index / rowLength);
size.top = vIndex * (oHeight + hMargin);
size.bottom = size.top + oHeight;
object.size = size;
}
/**
* Helper function for updateUnitCommands; sets up "unit panels"
* (i.e. panels with rows of icons) for the currently selected unit.
*
* @param guiName Short identifier string of this panel. See g_SelectionPanels.
* @param unitEntState Entity state of the selected unit with the lowest id.
* @param payerState Player state
*/
function setupUnitPanel(guiName, unitEntState, playerState)
{
if (!g_SelectionPanels[guiName])
{
error("unknown guiName used '" + guiName + "'");
return;
}
let selection = g_Selection.toList();
let items = g_SelectionPanels[guiName].getItems(unitEntState, selection);
if (!items || !items.length)
return;
let numberOfItems = items.length;
// Determine how many buttons there should be
let maxNumberOfItems = g_SelectionPanels[guiName].getMaxNumberOfItems();
if (maxNumberOfItems < numberOfItems)
numberOfItems = maxNumberOfItems;
let rowLength = g_SelectionPanels[guiName].rowLength || 8;
if (g_SelectionPanels[guiName].resizePanel)
g_SelectionPanels[guiName].resizePanel(numberOfItems, rowLength);
// Make buttons
for (let i = 0; i < numberOfItems; ++i)
{
// STANDARD DATA
// add standard data
let data = {
"i": i,
"item": items[i],
"selection": selection,
"playerState": playerState,
"unitEntState": unitEntState,
"rowLength": rowLength,
"numberOfItems": numberOfItems,
};
// add standard gui objects to the data
// depending on the actual XML, some of this may be undefined
data.button = Engine.GetGUIObjectByName("unit"+guiName+"Button["+i+"]");
data.icon = Engine.GetGUIObjectByName("unit"+guiName+"Icon["+i+"]");
data.guiSelection = Engine.GetGUIObjectByName("unit"+guiName+"Selection["+i+"]");
data.countDisplay = Engine.GetGUIObjectByName("unit"+guiName+"Count["+i+"]");
// DEFAULTS
if (data.button)
{
data.button.hidden = false;
data.button.enabled = true;
data.button.tooltip = "";
data.button.caption = "";
}
// SET CONTENT
if (g_SelectionPanels[guiName].setupButton)
if (!g_SelectionPanels[guiName].setupButton(data))
continue;
// TODO: we should require all entities to have icons, so this case never occurs
if (data.icon && !data.icon.sprite)
data.icon.sprite = "bkFillBlack";
}
// Hide any buttons we're no longer using
for (let i = numberOfItems; i < g_unitPanelButtons[guiName]; ++i)
if (g_SelectionPanels[guiName].hideItem)
g_SelectionPanels[guiName].hideItem(i, rowLength);
else
Engine.GetGUIObjectByName("unit"+guiName+"Button["+i+"]").hidden = true;
// remember the number of items
g_unitPanelButtons[guiName] = numberOfItems;
g_SelectionPanels[guiName].used = true;
}
/**
* Updates the selection panels where buttons are supposed to
* depend on the context.
* Runs in the main session loop via updateSelectionDetails().
* Delegates to setupUnitPanel to set up individual subpanels,
* appropriately activated depending on the selected unit's state.
*
* @param entState Entity state of the selected unit with the lowest id.
* @param supplementalDetailsPanel Reference to the
* "supplementalSelectionDetails" GUI Object
* @param commandsPanel Reference to the "commandsPanel" GUI Object
* @param selection Array of currently selected entity IDs.
*/
function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection)
{
for (let panel in g_SelectionPanels)
g_SelectionPanels[panel].used = false;
// If the selection is friendly units, add the command panels
// Get player state to check some constraints
// e.g. presence of a hero or build limits
let playerStates = GetSimState().players;
let playerState = playerStates[Engine.GetPlayerID()];
if (controlsPlayer(entState.player) || g_IsObserver)
{
for (var guiName of g_PanelsOrder)
{
if (
g_SelectionPanels[guiName].conflictsWith &&
g_SelectionPanels[guiName].conflictsWith.some(p => g_SelectionPanels[p].used)
)
continue;
setupUnitPanel(guiName, entState, playerStates[entState.player]);
}
supplementalDetailsPanel.hidden = false;
commandsPanel.hidden = false;
}
else if (playerState.isMutualAlly[entState.player]) // owned by allied player
{
// TODO if there's a second panel needed for a different player
// we should consider adding the players list to g_SelectionPanels
setupUnitPanel("Garrison", entState, playerState);
setupUnitPanel("AllyCommand", entState, playerState);
supplementalDetailsPanel.hidden = !g_SelectionPanels.Garrison.used;
commandsPanel.hidden = true;
}
else // owned by another player
{
supplementalDetailsPanel.hidden = true;
commandsPanel.hidden = true;
}
// Hides / unhides Unit Panels (panels should be grouped by type, not by order, but we will leave that for another time)
for (var panelName in g_SelectionPanels)
Engine.GetGUIObjectByName("unit" + panelName + "Panel").hidden = !g_SelectionPanels[panelName].used;
}
// Force hide commands panels
function hideUnitCommands()
{
for (var panelName in g_SelectionPanels)
Engine.GetGUIObjectByName("unit" + panelName + "Panel").hidden = true;
}
// Get all of the available entities which can be trained by the selected entities
function getAllTrainableEntities(selection)
{
var trainableEnts = [];
var state;
// Get all buildable and trainable entities
for (let ent of selection)
{
if ((state = GetEntityState(ent)) && state.production && state.production.entities.length)
trainableEnts = trainableEnts.concat(state.production.entities);
}
// Remove duplicates
removeDupes(trainableEnts);
return trainableEnts;
}
function getAllTrainableEntitiesFromSelection()
{
if (!g_allTrainableEntities)
g_allTrainableEntities = getAllTrainableEntities(g_Selection.toList());
return g_allTrainableEntities;
}
// Get all of the available entities which can be built by the selected entities
function getAllBuildableEntities(selection)
{
return Engine.GuiInterfaceCall("GetAllBuildableEntities", { "entities": selection });
}
function getAllBuildableEntitiesFromSelection()
{
if (!g_allBuildableEntities)
g_allBuildableEntities = getAllBuildableEntities(g_Selection.toList());
return g_allBuildableEntities;
}
function getNumberOfRightPanelButtons()
{
var sum = 0;
- for (let prop of ["Construction", "Training", "Pack", "Gate"])
+ for (let prop of ["Construction", "Training", "Pack", "Gate", "Upgrade"])
if (g_SelectionPanels[prop].used)
sum += g_unitPanelButtons[prop];
return sum;
}
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 18466)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 18467)
@@ -1,1987 +1,1996 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronised 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();
};
/*
* 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 cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let playerEnt = cmpPlayerManager.GetPlayerByID(i);
let cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits);
let cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
// Work out what phase we are in
let phase = "";
let cmpTechnologyManager = Engine.QueryInterface(playerEnt, 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";
}
// store player ally/neutral/enemy data as arrays
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(),
"heroes": cmpPlayer.GetHeroes(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"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.GetStartedResearch() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null
});
}
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 = 4 * cmpTerrain.GetTilesPerSide();
// Add timeElapsed
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
// Add ceasefire info
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;
}
// Add the game type
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.gameType = cmpEndGameManager.GetGameType();
// Add bartering prices
let cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
ret.barterPrices = cmpBarter.GetPrices();
// Add basic statistics to each player
for (let i = 0; i < numPlayers; ++i)
{
let playerEnt = cmpPlayerManager.GetPlayerByID(i);
let cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, 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()
{
// Get basic simulation info
let ret = this.GetSimulationState();
// Add statistics to each player
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let n = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < n; ++i)
{
let playerEnt = cmpPlayerManager.GetPlayerByID(i);
let cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics();
}
return ret;
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
else
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,
"template": template,
"alertRaiser": null,
"builder": null,
"identity": null,
"fogging": null,
"foundation": null,
"garrisonHolder": null,
"gate": null,
"guard": null,
"market": null,
"mirage": null,
"pack": null,
+ "upgrade" : null,
"player": -1,
"position": null,
"production": null,
"rallyPoint": null,
"resourceCarrying": null,
"rotation": null,
"trader": null,
"unitAI": null,
"visibility": null,
};
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()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
ret.position = cmpPosition.GetPosition();
ret.rotation = cmpPosition.GetRotation();
}
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints());
ret.needsHeal = !cmpHealth.IsUnhealable();
ret.canDelete = !cmpHealth.IsUndeletable();
}
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(),
};
+ var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
+ if (cmpUpgrade)
+ ret.upgrade = {
+ "upgrades" : cmpUpgrade.GetUpgrades(),
+ "progress": cmpUpgrade.GetProgress(),
+ "template": cmpUpgrade.GetUpgradingTo()
+ };
+
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 cmpFogging = Engine.QueryInterface(ent, IID_Fogging);
if (cmpFogging)
{
if (cmpFogging.IsMiraged(player))
ret.fogging = { "mirage": cmpFogging.GetMirage(player) };
else
ret.fogging = { "mirage": null };
}
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"progress": cmpFoundation.GetBuildPercentage(),
"numBuilders": cmpFoundation.GetNumBuilders()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders() };
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(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
};
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(),
"possibleStances": cmpUnitAI.GetPossibleStances(),
"isIdle":cmpUnitAI.IsIdle(),
};
// Add some information needed for ungarrisoning
if (cmpUnitAI.IsGarrisoned() && ret.player !== undefined)
ret.template = "p" + ret.player + "&" + ret.template;
}
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();
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 = {
"level": cmpAlertRaiser.GetLevel(),
"canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(),
"hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(),
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
return ret;
};
/**
* Get additionnal entity info, rarely used in the gui
*/
GuiInterface.prototype.GetExtendedEntityState = function(player, ent)
{
let ret = {
"armour": null,
"attack": null,
"barterMarket": null,
"buildingAI": null,
"healer": null,
"obstruction": null,
"turretParent":null,
"promotion": null,
"repairRate": null,
"buildRate": null,
"resourceDropsite": null,
"resourceGatherRates": null,
"resourceSupply": null,
};
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
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] = cmpAttack.GetAttackStrengths(type);
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;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
{
// For units, take the rage in front of it, no spread. So angle = 0
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
}
else if(cmpPosition && cmpPosition.IsInWorld())
{
// For buildings, take the average elevation around it. So angle = 2*pi
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI);
}
else
{
// not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
}
let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
if (cmpArmour)
ret.armour = cmpArmour.GetArmourStrengths();
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (cmpAuras)
ret.auras = cmpAuras.GetDescriptions();
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (cmpObstruction)
ret.obstruction = {
"controlGroup": cmpObstruction.GetControlGroup(),
"controlGroup2": cmpObstruction.GetControlGroup2(),
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpRepairable = Engine.QueryInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairRate = cmpRepairable.GetRepairRate();
let cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.buildRate = cmpFoundation.GetBuildRate();
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 cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
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"))
{
let cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
ret.barterMarket = { "prices": cmpBarter.GetPrices() };
}
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"hp": cmpHeal.GetHP(),
"range": cmpHeal.GetRange().max,
"rate": cmpHeal.GetRate(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses(),
};
return ret;
};
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, extendedName)
{
let name = extendedName;
// Special case for garrisoned units which have a extended template
if (extendedName.indexOf("&") != -1)
name = extendedName.slice(extendedName.indexOf("&")+1);
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(name);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, player, aurasTemplate);
// Add aura name and description loaded from JSON file
let auraNames = template.Auras._string.split(/\s+/);
let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
for (let name of auraNames)
{
let auraTemplate = cmpDataTemplateManager.GetAuraTemplate(name);
if (!auraTemplate)
{
// The following warning is perhaps useless since it's yet done in DataTemplateManager
warn("Tried to get data for invalid aura: " + name);
continue;
}
aurasTemplate[name] = {};
aurasTemplate[name].auraName = auraTemplate.auraName || null;
aurasTemplate[name].auraDescription = auraTemplate.auraDescription || null;
}
return GetTemplateDataHelper(template, player, aurasTemplate);
};
GuiInterface.prototype.GetTechnologyData = function(player, name)
{
let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
let template = cmpDataTemplateManager.GetTechnologyTemplate(name);
if (!template)
{
warn("Tried to get data for invalid technology: " + name);
return null;
}
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
return GetTechnologyDataHelper(template, cmpPlayer.GetCiv());
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(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 || 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 in cmpTechnologyManager.GetTechsStarted())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
else
ret[tech].progress = 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 || player).GetNeededResources(data.cost);
};
/**
* 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 == undefined)
{
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
notification.players = [-1];
for (let i = 1; i < numPlayers; ++i)
notification.players.push(i);
}
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)
{
for each (let ent in data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
// GetLastFormationName is named in a strange way as it (also) is
// the value of the current formation (see Formation.js LoadFormation)
if (cmpUnitAI && cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate)
return true;
}
return false;
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for each (let ent in 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 each (let ent in 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.SetSelectionHighlight = function(player, cmd)
{
let playerColors = {}; // cache of owner -> color map
for each (let ent in cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color:
let owner = -1;
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.GetColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
}
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return [...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);
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.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 each (let ent in 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 each (let ent in 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
pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set
if (pos)
{
// Only update the position if we changed it (cmd.queued is set)
if ("queued" in cmd)
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z
else
cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
// rebuild the renderer when not set (when reading saved game or in case of building update)
else if (!cmpRallyPointRenderer.IsSet())
for each (let posi in cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z });
cmpRallyPointRenderer.SetDisplayed(true);
// remember which entities have their rally points displayed so we can hide them again
this.entsRallyPointsDisplayed.push(ent);
}
}
};
/**
* 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": [],
};
// See if we're changing template
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
// Destroy the old preview if there was one
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
// Load the new template
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
// Move the preview into the right location
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);
// Check whether building placement is valid
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
// 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;
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
// --------------------------------------------------------------------------------
// 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 each (let ent in this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// keep template data around
}
return false;
}
else
{
// Move all existing cached entities outside of the world and reset their use count
for (let tpl in this.placementWallEntities)
{
for each (let ent in 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 each (let tpl in wallSet.templates)
{
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, tpl),
};
// ensure that the loaded template data contains a wallPiece component
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)
{
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
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": { "food": 0, "wood": 0, "stone": 0, "metal": 0, "population": 0, "populationBonus": 0, "time": 0 },
};
let previewEntities = [];
if (end.pos)
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
// 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 > 0 && 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 > 0 ? 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 > 0 ? 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;
let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
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)
{
// allocate new entity
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
// reuse an existing one
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);
// check whether this wall piece can be validly positioned here
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.
result.cost.food += tplData.cost.food;
result.cost.wood += tplData.cost.wood;
result.cost.stone += tplData.cost.stone;
result.cost.metal += tplData.cost.metal;
result.cost.population += tplData.cost.population;
result.cost.populationBonus += tplData.cost.populationBonus;
result.cost.time += tplData.cost.time;
}
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 each (let ent in 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 (template.BuildRestrictions.Category == "Dock")
{
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.
var 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 => cmpIdentity.HasClass(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
{
// Else both markets are not null and target is different from them
result = { "type": "set first" };
}
return result;
};
GuiInterface.prototype.CanCapture = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
if (!cmpAttack)
return false;
let owner = QueryOwnerInterface(data.entity).GetPlayerID();
let cmpCapturable = QueryMiragedInterface(data.target, IID_Capturable);
if (cmpCapturable && cmpCapturable.CanCapture(owner) && cmpAttack.GetAttackTypes().indexOf("Capture") != -1)
return cmpAttack.CanAttack(data.target);
return false;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
if (!cmpAttack)
return false;
let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player);
let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player);
if (!cmpEntityPlayer || !cmpTargetPlayer)
return false;
// if the owner is an enemy, it's up to the attack component to decide
if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()))
return cmpAttack.CanAttack(data.target);
return false;
};
/*
* 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 each (let ent in 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 each (let ent in 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,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetExtendedEntityState": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"GetTechnologyData": 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,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanCapture": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
else
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 18466)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 18467)
@@ -1,176 +1,171 @@
function Identity() {}
Identity.prototype.Schema =
"Specifies various names and values associated with the unit type, typically for GUI display to users." +
"" +
"athen" +
"Athenian Hoplite" +
"Hoplī́tēs Athēnaïkós" +
"units/athen_infantry_spearman.png" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
- "" +
- "" +
- "" +
- "" +
- "" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"Basic" +
"Advanced" +
"Elite" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
Identity.prototype.Init = function()
{
// caching
this.classesList = GetIdentityClasses(this.template);
this.visibleClassesList = GetVisibleIdentityClasses(this.template);
};
Identity.prototype.Deserialize = function ()
{
this.Init();
};
Identity.prototype.Serialize = null; // we have no dynamic state to save
Identity.prototype.GetCiv = function()
{
return this.template.Civ;
};
Identity.prototype.GetLang = function()
{
return this.template.Lang || "greek"; // ugly default
};
Identity.prototype.GetGender = function()
{
return this.template.Gender || "male"; // ugly default
};
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 && "_string" in this.template.Formations)
{
var string = this.template.Formations._string;
return 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;
};
Engine.RegisterComponentType(IID_Identity, "Identity", Identity);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Pack.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Pack.js (revision 18466)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Pack.js (revision 18467)
@@ -1,202 +1,137 @@
function Pack() {}
const PACKING_INTERVAL = 250;
Pack.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"packed" +
"unpacked" +
"" +
"";
Pack.prototype.Init = function()
{
this.packed = (this.template.State == "packed");
this.packing = false;
this.elapsedTime = 0;
this.timer = undefined;
};
Pack.prototype.OnDestroy = function()
{
this.CancelTimer();
};
Pack.prototype.CancelTimer = 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;
}
};
Pack.prototype.IsPacked = function()
{
return this.packed;
};
Pack.prototype.IsPacking = function()
{
return this.packing;
};
Pack.prototype.Pack = function()
{
// Ignore pointless pack command
if (this.IsPacked() || this.IsPacking())
return;
this.packing = true;
- var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetInterval(this.entity, IID_Pack, "PackProgress", 0, PACKING_INTERVAL, {"packing": true});
- var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+ let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation("packing", true, 1.0, "packing");
};
Pack.prototype.Unpack = function()
{
// Ignore pointless unpack command
if (!this.IsPacked() || this.IsPacking())
return;
this.packing = true;
- var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetInterval(this.entity, IID_Pack, "PackProgress", 0, PACKING_INTERVAL, {"packing": false});
- var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+ let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation("unpacking", true, 1.0, "unpacking");
};
Pack.prototype.CancelPack = function()
{
// Ignore pointless cancel command
if (!this.IsPacking())
return;
this.CancelTimer();
this.packing = false;
this.SetElapsedTime(0);
// Clear animation
- var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+ let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation("idle", false, 1.0, "");
};
Pack.prototype.GetPackTime = function()
{
return ApplyValueModificationsToEntity("Pack/Time", +this.template.Time, this.entity);
};
Pack.prototype.GetElapsedTime = function()
{
return this.elapsedTime;
};
Pack.prototype.GetProgress = function()
{
return this.elapsedTime / this.GetPackTime();
};
Pack.prototype.SetElapsedTime = function(time)
{
this.elapsedTime = time;
Engine.PostMessage(this.entity, MT_PackProgressUpdate, { progress: this.elapsedTime });
};
Pack.prototype.PackProgress = function(data, lateness)
{
- if (this.elapsedTime >= this.GetPackTime())
+ if (this.elapsedTime < this.GetPackTime())
{
+ this.SetElapsedTime(this.GetElapsedTime() + PACKING_INTERVAL + lateness);
+ return;
+ }
+
this.CancelTimer();
this.packed = !this.packed;
- this.packing = false;
Engine.PostMessage(this.entity, MT_PackFinished, { packed: this.packed });
- // Done un/packing, copy our parameters to the final entity
- var newEntity = Engine.AddEntity(this.template.Entity);
- if (newEntity == INVALID_ENTITY)
- {
- // Error (e.g. invalid template names)
- error("PackProgress: Error creating entity for '" + this.template.Entity + "'");
- return;
- }
-
- var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- var cmpNewPosition = Engine.QueryInterface(newEntity, IID_Position);
- if (cmpPosition.IsInWorld())
- {
- var pos = cmpPosition.GetPosition2D();
- cmpNewPosition.JumpTo(pos.x, pos.y);
- }
- var rot = cmpPosition.GetRotation();
- cmpNewPosition.SetYRotation(rot.y);
- cmpNewPosition.SetXZRotation(rot.x, rot.z);
- cmpNewPosition.SetHeightOffset(cmpPosition.GetHeightOffset());
-
- var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
- var cmpNewOwnership = Engine.QueryInterface(newEntity, IID_Ownership);
- cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
-
- // rescale capture points
- var cmpCapturable = Engine.QueryInterface(this.entity, IID_Capturable);
- var cmpNewCapturable = Engine.QueryInterface(newEntity, IID_Capturable);
- if (cmpCapturable && cmpNewCapturable)
- {
- let scale = cmpCapturable.GetMaxCapturePoints() / cmpNewCapturable.GetMaxCapturePoints();
- let newCp = cmpCapturable.GetCapturePoints().map(function (v) { return v / scale; });
- cmpNewCapturable.SetCapturePoints(newCp);
- }
-
- // Maintain current health level
- var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
- var cmpNewHealth = Engine.QueryInterface(newEntity, IID_Health);
- var healthLevel = Math.max(0, Math.min(1, cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints()));
- cmpNewHealth.SetHitpoints(Math.round(cmpNewHealth.GetMaxHitpoints() * healthLevel));
-
- var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
- var cmpNewUnitAI = Engine.QueryInterface(newEntity, IID_UnitAI);
- if (cmpUnitAI && cmpNewUnitAI)
- {
- var pos = cmpUnitAI.GetHeldPosition();
- if (pos)
- cmpNewUnitAI.SetHeldPosition(pos.x, pos.z);
- if (cmpUnitAI.GetStanceName())
- cmpNewUnitAI.SwitchToStance(cmpUnitAI.GetStanceName());
- cmpNewUnitAI.AddOrders(cmpUnitAI.GetOrders());
- cmpNewUnitAI.SetGuardOf(cmpUnitAI.IsGuardOf());
- }
-
- // Maintain the list of guards
- var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard);
- var cmpNewGuard = Engine.QueryInterface(newEntity, IID_Guard);
- if (cmpGuard && cmpNewGuard)
- cmpNewGuard.SetEntities(cmpGuard.GetEntities());
-
- Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: newEntity });
-
- // Play notification sound
- var sound = this.packed ? "packed" : "unpacked";
- PlaySound(sound, newEntity);
+ let newEntity = ChangeEntityTemplate(this.entity, this.template.Entity);
+
+ if (newEntity)
+ PlaySound(this.packed ? "packed" : "unpacked", newEntity);
- // Destroy current entity
- Engine.DestroyEntity(this.entity);
- }
- else
- {
- this.SetElapsedTime(this.GetElapsedTime() + PACKING_INTERVAL + lateness);
- }
};
Engine.RegisterComponentType(IID_Pack, "Pack", Pack);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js (revision 18467)
@@ -0,0 +1,306 @@
+function Upgrade() {}
+
+const UPGRADING_PROGRESS_INTERVAL = 250;
+
+Upgrade.prototype.Schema =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
+Upgrade.prototype.Init = function()
+{
+ this.upgrading = false;
+ this.elapsedTime = 0;
+ this.timer = undefined;
+
+ this.upgradeTemplates = {};
+
+ for (let choice in this.template)
+ {
+ let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
+ let name = this.template[choice].Entity;
+ if (cmpIdentity)
+ name = name.replace(/\{civ\}/g, cmpIdentity.GetCiv());
+ if (this.upgradeTemplates.name)
+ warn("Upgrade Component: entity " + this.entity + " has two upgrades to the same entity, only the last will be used.");
+ this.upgradeTemplates[name] = choice;
+ }
+};
+
+// On owner change, abort the upgrade
+// This will also deal with the "OnDestroy" case.
+Upgrade.prototype.OnOwnershipChanged = function(msg)
+{
+ this.CancelUpgrade();
+
+ if (msg.to !== -1)
+ this.owner = msg.to;
+};
+
+Upgrade.prototype.ChangeUpgradedEntityCount = function(amount)
+{
+ if (!this.IsUpgrading())
+ return;
+
+ let cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ let template = cmpTempMan.GetTemplate(this.upgrading);
+
+ let category;
+ if (template.TrainingRestrictions)
+ category = template.TrainingRestrictions.Category;
+ else if (template.BuildRestrictions)
+ category = template.BuildRestrictions.Category;
+
+ if (!category)
+ return;
+
+ let cmpEntityLimits = QueryPlayerIDInterface(this.owner, IID_EntityLimits);
+ cmpEntityLimits.ChangeCount(category, amount);
+};
+
+Upgrade.prototype.CanUpgradeTo = function(template)
+{
+ return this.upgradeTemplates[template] !== undefined;
+};
+
+Upgrade.prototype.GetUpgrades = function()
+{
+ let ret = [];
+
+ let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
+
+ for (let option in this.template)
+ {
+ let choice = this.template[option];
+ let entType = choice.Entity;
+ if (cmpIdentity)
+ entType = entType.replace(/\{civ\}/g, cmpIdentity.GetCiv());
+
+ let hasCosts;
+ let cost = {};
+ if (choice.Cost)
+ {
+ hasCosts = true;
+ for (let type in choice.Cost)
+ cost[type] = ApplyValueModificationsToTemplate("Upgrade/Cost/"+type, +choice.Cost[type], this.owner, entType);
+ }
+ if (choice.Time)
+ {
+ hasCosts = true;
+ cost.time = ApplyValueModificationsToTemplate("Upgrade/Time", +choice.Time/1000.0, this.owner, entType);
+ }
+ ret.push({
+ "entity": entType,
+ "icon": choice.Icon || undefined,
+ "cost": hasCosts,
+ "tooltip": choice.Tooltip || undefined,
+ "requiredTechnology": this.GetRequiredTechnology(option),
+ });
+ }
+
+ return ret;
+};
+
+Upgrade.prototype.CancelTimer = function()
+{
+ if (!this.timer)
+ return;
+
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.timer);
+ this.timer = undefined;
+};
+
+Upgrade.prototype.IsUpgrading = function()
+{
+ return !!this.upgrading;
+};
+
+Upgrade.prototype.GetUpgradingTo = function()
+{
+ return this.upgrading;
+};
+
+Upgrade.prototype.WillCheckPlacementRestrictions = function(template)
+{
+ if (!this.upgradeTemplates[template])
+ return undefined;
+
+ // is undefined by default so use X in Y
+ return "CheckPlacementRestrictions" in this.template[this.upgradeTemplates[template]];
+};
+
+Upgrade.prototype.GetRequiredTechnology = function(templateArg)
+{
+ let choice = this.upgradeTemplates[templateArg] || templateArg
+
+ if (this.template[choice].RequiredTechnology)
+ return this.template[choice].RequiredTechnology;
+
+ if (!("RequiredTechnology" in this.template[choice]))
+ return undefined;
+
+ let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
+
+ let entType = this.template[choice].Entity;
+ if (cmpIdentity)
+ entType = entType.replace(/\{civ\}/g, cmpIdentity.GetCiv());
+
+ let template = cmpTemplateManager.GetTemplate(entType);
+ if (template.Identity.RequiredTechnology)
+ return template.Identity.RequiredTechnology;
+
+ return undefined;
+};
+
+Upgrade.prototype.GetResourceCosts = function(template)
+{
+ if (!this.upgradeTemplates[template])
+ return undefined;
+
+ let choice = this.upgradeTemplates[template];
+ if (!this.template[choice].Cost)
+ return {};
+
+ let costs = {};
+ for (let r in this.template[choice].Cost)
+ {
+ costs[r] = ApplyValueModificationsToEntity("Upgrade/Cost/"+r, +this.template[choice].Cost[r], this.entity);
+ }
+ return costs;
+};
+
+Upgrade.prototype.Upgrade = function(template)
+{
+ if (this.IsUpgrading() || !this.upgradeTemplates[template])
+ return false;
+
+ let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
+
+ if (!cmpPlayer.TrySubtractResources(this.GetResourceCosts(template)))
+ return false;
+
+ this.upgrading = template;
+
+ // Prevent cheating
+ this.ChangeUpgradedEntityCount(1);
+
+ if (this.GetUpgradeTime(template) !== 0)
+ {
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.timer = cmpTimer.SetInterval(this.entity, IID_Upgrade, "UpgradeProgress", 0, UPGRADING_PROGRESS_INTERVAL, { "upgrading": template });
+ }
+ else
+ this.UpgradeProgress();
+
+ return true;
+};
+
+Upgrade.prototype.CancelUpgrade = function()
+{
+ if (!this.IsUpgrading())
+ return;
+
+ let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
+ if (cmpPlayer)
+ {
+ let costs = this.GetResourceCosts(this.upgrading);
+ cmpPlayer.AddResources(costs);
+ }
+
+ this.ChangeUpgradedEntityCount(-1);
+
+ this.upgrading = false;
+ this.CancelTimer();
+ this.SetElapsedTime(0);
+};
+
+Upgrade.prototype.GetUpgradeTime = function(templateArg)
+{
+ let template = this.upgrading || templateArg;
+ let choice = this.upgradeTemplates[template];
+ if (!choice)
+ return undefined;
+ return this.template[choice].Time ? ApplyValueModificationsToEntity("Upgrade/Time", +this.template[choice].Time, this.entity) : 0;
+};
+
+Upgrade.prototype.GetElapsedTime = function()
+{
+ return this.elapsedTime;
+};
+
+Upgrade.prototype.GetProgress = function()
+{
+ if (!this.IsUpgrading())
+ return undefined;
+ return this.GetUpgradeTime() == 0 ? 1 : this.elapsedTime / this.GetUpgradeTime();
+};
+
+Upgrade.prototype.SetElapsedTime = function(time)
+{
+ this.elapsedTime = time;
+};
+
+Upgrade.prototype.UpgradeProgress = function(data, lateness)
+{
+ if (this.elapsedTime < this.GetUpgradeTime())
+ {
+ this.SetElapsedTime(this.GetElapsedTime() + UPGRADING_PROGRESS_INTERVAL + lateness);
+ return;
+ }
+
+ this.CancelTimer();
+
+ let newEntity = ChangeEntityTemplate(this.entity, this.upgrading);
+
+ if (newEntity)
+ PlaySound("upgraded", newEntity);
+};
+
+Engine.RegisterComponentType(IID_Upgrade, "Upgrade", Upgrade);
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Upgrade.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Upgrade.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Upgrade.js (revision 18467)
@@ -0,0 +1 @@
+Engine.RegisterInterface("Upgrade");
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 18466)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 18467)
@@ -1,1637 +1,1622 @@
// Setting this to true will display some warnings when commands
// are likely to fail, which may be useful for debugging AIs
var g_DebugCommands = false;
function ProcessCommand(player, cmd)
{
let data = {
"cmpPlayerManager": Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager)
};
if (!data.cmpPlayerManager || player < 0)
return;
data.playerEnt = data.cmpPlayerManager.GetPlayerByID(player);
if (data.playerEnt == INVALID_ENTITY)
return;
data.cmpPlayer = Engine.QueryInterface(data.playerEnt, IID_Player);
if (!data.cmpPlayer)
return;
data.controlAllUnits = data.cmpPlayer.CanControlAllUnits();
if (cmd.entities)
data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits);
// Allow focusing the camera on recent commands
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "playercommand",
"players": [player],
"cmd": cmd
});
// Note: checks of UnitAI targets are not robust enough here, as ownership
// can change after the order is issued, they should be checked by UnitAI
// when the specific behavior (e.g. attack, garrison) is performed.
// (Also it's not ideal if a command silently fails, it's nicer if UnitAI
// moves the entities closer to the target before giving up.)
// Now handle various commands
if (g_Commands[cmd.type])
{
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("PlayerCommand", { "player": player, "cmd": cmd });
g_Commands[cmd.type](player, cmd, data);
}
else
error("Invalid command: unknown command type: "+uneval(cmd));
}
var g_Commands = {
"debug-print": function(player, cmd, data)
{
print(cmd.message);
},
"chat": function(player, cmd, data)
{
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({ "type": cmd.type, "players": [player], "message": cmd.message });
},
"aichat": function(player, cmd, data)
{
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
var notification = { "players": [player] };
for (var key in cmd)
notification[key] = cmd[key];
cmpGuiInterface.PushNotification(notification);
},
"cheat": function(player, cmd, data)
{
Cheat(cmd);
},
"quit": function(player, cmd, data)
{
// Let the AI exit the game for testing purposes
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({ "type": "quit", "players": [player] });
},
"diplomacy": function(player, cmd, data)
{
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (data.cmpPlayer.GetLockTeams() ||
cmpCeasefireManager && cmpCeasefireManager.IsCeasefireActive())
return;
switch(cmd.to)
{
case "ally":
data.cmpPlayer.SetAlly(cmd.player);
break;
case "neutral":
data.cmpPlayer.SetNeutral(cmd.player);
break;
case "enemy":
data.cmpPlayer.SetEnemy(cmd.player);
break;
default:
warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to);
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "diplomacy",
"players": [player],
"targetPlayer": cmd.player,
"status": cmd.to
});
},
"tribute": function(player, cmd, data)
{
data.cmpPlayer.TributeResource(cmd.player, cmd.amounts);
},
"control-all": function(player, cmd, data)
{
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": markForTranslation("(Cheat - control all units)")
});
data.cmpPlayer.SetControlAllUnits(cmd.flag);
},
"reveal-map": function(player, cmd, data)
{
// Reveal the map for all players, not just the current player,
// primarily to make it obvious to everyone that the player is cheating
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetLosRevealAll(-1, cmd.enable);
},
"walk": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued);
});
},
"walk-to-range": function(player, cmd, data)
{
// Only used by the AI
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued);
}
},
"attack-walk": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued);
});
},
"attack": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target)))
warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
// See UnitAI.CanAttack for target checks
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.Attack(cmd.target, cmd.queued, allowCapture);
});
},
"heal": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target)))
warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd));
// See UnitAI.CanHeal for target checks
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.Heal(cmd.target, cmd.queued);
});
},
"repair": function(player, cmd, data)
{
// This covers both repairing damaged buildings, and constructing unfinished foundations
if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd));
// See UnitAI.CanRepair for target checks
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued);
});
},
"gather": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target)))
warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd));
// See UnitAI.CanGather for target checks
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.Gather(cmd.target, cmd.queued);
});
},
"gather-near-position": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued);
});
},
"returnresource": function(player, cmd, data)
{
if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target))
warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd));
// See UnitAI.CanReturnResource for target checks
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.ReturnResource(cmd.target, cmd.queued);
});
},
"back-to-work": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if(!cmpUnitAI || !cmpUnitAI.BackToWork())
notifyBackToWorkFailure(player);
}
},
"remove-guard": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.RemoveGuard();
}
},
"train": function(player, cmd, data)
{
// Check entity limits
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempMan.GetTemplate(cmd.template);
var unitCategory = null;
if (template.TrainingRestrictions)
unitCategory = template.TrainingRestrictions.Category;
// Verify that the building(s) can be controlled by the player
if (data.entities.length <= 0)
{
if (g_DebugCommands)
warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
for (let ent of data.entities)
{
if (unitCategory)
{
var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits);
if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count))
{
if (g_DebugCommands)
warn(unitCategory + " train limit is reached: " + uneval(cmd));
continue;
}
}
var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
if (!cmpTechnologyManager.CanProduce(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: training requires unresearched technology: " + uneval(cmd));
continue;
}
var queue = Engine.QueryInterface(ent, IID_ProductionQueue);
// Check if the building can train the unit
// TODO: the AI API does not take promotion technologies into account for the list
// of trainable units (taken directly from the unit template). Here is a temporary fix.
if (queue && data.cmpPlayer.IsAI())
{
var list = queue.GetEntitiesList();
if (list.indexOf(cmd.template) === -1 && cmd.promoted)
{
for (var promoted of cmd.promoted)
{
if (list.indexOf(promoted) === -1)
continue;
cmd.template = promoted;
break;
}
}
}
if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1)
if ("metadata" in cmd)
queue.AddBatch(cmd.template, "unit", +cmd.count, cmd.metadata);
else
queue.AddBatch(cmd.template, "unit", +cmd.count);
}
},
"research": function(player, cmd, data)
{
if (!CanControlUnit(cmd.entity, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: research building cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager);
if (!cmpTechnologyManager.CanResearch(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd));
return;
}
var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (queue)
queue.AddBatch(cmd.template, "technology");
},
"stop-production": function(player, cmd, data)
{
if (!CanControlUnit(cmd.entity, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: production building cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (queue)
queue.RemoveBatch(cmd.id);
},
"construct": function(player, cmd, data)
{
TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd);
},
"construct-wall": function(player, cmd, data)
{
TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd);
},
"delete-entities": function(player, cmd, data)
{
for (let ent of data.entities)
{
// don't allow to delete entities who are half-captured
var cmpCapturable = Engine.QueryInterface(ent, IID_Capturable);
if (cmpCapturable)
{
var capturePoints = cmpCapturable.GetCapturePoints();
var maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
if (capturePoints[player] < maxCapturePoints / 2)
return;
}
// either kill or delete the entity
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
{
if (cmpHealth.IsUndeletable())
continue;
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (!cmpResourceSupply || !cmpResourceSupply.GetKillBeforeGather() || data.controlAllUnits)
cmpHealth.Kill();
}
else
Engine.DestroyEntity(ent);
}
},
"set-rallypoint": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
{
if (!cmd.queued)
cmpRallyPoint.Unset();
cmpRallyPoint.AddPosition(cmd.x, cmd.z);
cmpRallyPoint.AddData(clone(cmd.data));
}
}
},
"unset-rallypoint": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
cmpRallyPoint.Reset();
}
},
"defeat-player": function(player, cmd, data)
{
let cmpPlayer = QueryPlayerIDInterface(player);
if (cmpPlayer)
cmpPlayer.SetState("defeated", !!cmd.resign);
},
"garrison": function(player, cmd, data)
{
// Verify that the building can be controlled by the player or is mutualAlly
if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
return;
}
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.Garrison(cmd.target, cmd.queued);
});
},
"guard": function(player, cmd, data)
{
// Verify that the target can be controlled by the player or is mutualAlly
if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: guard/escort target cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.Guard(cmd.target, cmd.queued);
});
},
"stop": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.Stop(cmd.queued);
});
},
"unload": function(player, cmd, data)
{
// Verify that the building can be controlled by the player or is mutualAlly
if (!CanControlUnitOrIsAlly(cmd.garrisonHolder, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
return;
}
var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
var notUngarrisoned = 0;
// The owner can ungarrison every garrisoned unit
if (IsOwnedByPlayer(player, cmd.garrisonHolder))
data.entities = cmd.entities;
for (let ent of data.entities)
if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent))
++notUngarrisoned;
if (notUngarrisoned != 0)
notifyUnloadFailure(player, cmd.garrisonHolder);
},
"unload-template": function(player, cmd, data)
{
var index = cmd.template.indexOf("&"); // Templates for garrisoned units are extended
if (index == -1)
return;
var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
for (let garrisonHolder of entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
// Only the owner of the garrisonHolder may unload entities from any owners
if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits
&& player != +cmd.template.slice(1,index))
continue;
if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.all))
notifyUnloadFailure(player, garrisonHolder);
}
}
},
"unload-all-by-owner": function(player, cmd, data)
{
var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
for (let garrisonHolder of entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllByOwner(player))
notifyUnloadFailure(player, garrisonHolder);
}
},
"unload-all": function(player, cmd, data)
{
var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits);
for (let garrisonHolder of entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll())
notifyUnloadFailure(player, garrisonHolder);
}
},
"increase-alert-level": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (!cmpAlertRaiser || !cmpAlertRaiser.IncreaseAlertLevel())
notifyAlertFailure(player);
}
},
"alert-end": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
cmpAlertRaiser.EndOfAlert();
}
},
"formation": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd.name).forEach(cmpUnitAI => {
cmpUnitAI.MoveIntoFormation(cmd);
});
},
"promote": function(player, cmd, data)
{
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": markForTranslation("(Cheat - promoted units)"),
"translateMessage": true
});
for (let ent of cmd.entities)
{
var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp());
}
},
"stance": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && !cmpUnitAI.IsTurret())
cmpUnitAI.SwitchToStance(cmd.name);
}
},
- "wall-to-gate": function(player, cmd, data)
- {
- for (let ent of data.entities)
- TryTransformWallToGate(ent, data.cmpPlayer, cmd);
- },
-
"lock-gate": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (!cmpGate)
continue;
if (cmd.lock)
cmpGate.LockGate();
else
cmpGate.UnlockGate();
}
},
"setup-trade-route": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued);
});
},
"set-trading-goods": function(player, cmd, data)
{
data.cmpPlayer.SetTradingGoods(cmd.tradingGoods);
},
"barter": function(player, cmd, data)
{
var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
cmpBarter.ExchangeResources(data.playerEnt, cmd.sell, cmd.buy, cmd.amount);
},
"set-shading-color": function(player, cmd, data)
{
// Debug command to make an entity brightly colored
for (let ent of cmd.entities)
{
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
cmpVisual.SetShadingColor(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0); // alpha isn't used so just send 0
}
},
"pack": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
if (cmd.pack)
cmpUnitAI.Pack(cmd.queued);
else
cmpUnitAI.Unpack(cmd.queued);
}
},
"cancel-pack": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
if (cmd.pack)
cmpUnitAI.CancelPack(cmd.queued);
else
cmpUnitAI.CancelUnpack(cmd.queued);
}
},
+ "upgrade": function(player, cmd, data)
+ {
+ for (let ent of data.entities)
+ {
+ var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
+
+ if (!cmpUpgrade || !cmpUpgrade.CanUpgradeTo(cmd.template))
+ continue;
+
+ if (cmpUpgrade.WillCheckPlacementRestrictions(cmd.template) && ObstructionsBlockingTemplateChange(ent, cmd.template))
+ {
+ var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
+ cmpGUIInterface.PushNotification({
+ "players": [data.cmpPlayer.GetPlayerID()],
+ "message": markForTranslation("Cannot upgrade as distance requirements are not verified or terrain is obstructed.")
+ });
+ continue;
+ }
+
+ if (!CanGarrisonedChangeTemplate(ent, cmd.template))
+ {
+ var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
+ cmpGUIInterface.PushNotification({
+ "players": [data.cmpPlayer.GetPlayerID()],
+ "message": markForTranslation("Cannot upgrade a garrisoned entity.")
+ });
+ continue;
+ }
+
+ // Check entity limits
+ var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ var template = cmpTemplateManager.GetTemplate(cmd.template);
+ var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
+ if (template.TrainingRestrictions && !cmpEntityLimits.AllowedToTrain(template.TrainingRestrictions.Category, 1) ||
+ template.BuildRestrictions && !cmpEntityLimits.AllowedToBuild(template.BuildRestrictions.Category))
+ {
+ if (g_DebugCommands)
+ warn("Invalid command: build limits check failed for player " + player + ": " + uneval(cmd));
+ continue;
+ }
+
+ var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
+ if (cmpUpgrade.GetRequiredTechnology(cmd.template) && !cmpTechnologyManager.IsTechnologyResearched(cmpUpgrade.GetRequiredTechnology(cmd.template)))
+ {
+ if (g_DebugCommands)
+ warn("Invalid command: upgrading requires unresearched technology: " + uneval(cmd));
+ continue;
+ }
+
+ cmpUpgrade.Upgrade(cmd.template, data.cmpPlayer);
+ }
+ },
+
+ "cancel-upgrade": function(player, cmd, data)
+ {
+ for (let ent of data.entities)
+ {
+ let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
+ if (cmpUpgrade)
+ cmpUpgrade.CancelUpgrade(data.cmpPlayer);
+ }
+ },
+
"attack-request": function(player, cmd, data)
{
// Send a chat message to human players
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": "/allies " + markForTranslation("Attack against %(_player_)s requested."),
"translateParameters": ["_player_"],
"parameters": { "_player_": cmd.target }
});
// And send an attackRequest event to the AIs
let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
if (cmpAIInterface)
cmpAIInterface.PushEvent("AttackRequest", cmd);
},
"dialog-answer": function(player, cmd, data)
{
// Currently nothing. Triggers can read it anyway, and send this
// message to any component you like.
},
"set-dropsite-sharing": function(player, cmd, data)
{
for (let ent of data.entities)
{
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite && cmpResourceDropsite.IsSharable())
cmpResourceDropsite.SetSharing(cmd.shared);
}
},
};
/**
* Sends a GUI notification about unit(s) that failed to ungarrison.
*/
function notifyUnloadFailure(player, garrisonHolder)
{
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("Unable to ungarrison unit(s)"),
"translateMessage": true
});
}
/**
* Sends a GUI notification about worker(s) that failed to go back to work.
*/
function notifyBackToWorkFailure(player)
{
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("Some unit(s) can't go back to work"),
"translateMessage": true
});
}
/**
* Sends a GUI notification about Alerts that failed to be raised
*/
function notifyAlertFailure(player)
{
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": "You can't raise the alert to a higher level!",
"translateMessage": true
});
}
/**
* Get some information about the formations used by entities.
* The entities must have a UnitAI component.
*/
function ExtractFormations(ents)
{
var entities = []; // subset of ents that have UnitAI
var members = {}; // { formationentity: [ent, ent, ...], ... }
for (let ent of ents)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
var fid = cmpUnitAI.GetFormationController();
if (fid != INVALID_ENTITY)
{
if (!members[fid])
members[fid] = [];
members[fid].push(ent);
}
entities.push(ent);
}
var ids = [ id for (id in members) ];
return { "entities": entities, "members": members, "ids": ids };
}
/**
* Tries to find the best angle to put a dock at a given position
* Taken from GuiInterface.js
*/
function GetDockAngle(template, x, z)
{
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
if (!cmpTerrain || !cmpWaterManager)
return undefined;
// Get footprint size
var halfSize = 0;
if (template.Footprint.Square)
halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2;
else if (template.Footprint.Circle)
halfSize = template.Footprint.Circle["@radius"];
/* Find direction of most open water, algorithm:
* 1. Pick points in a circle around dock
* 2. If point is in water, add to array
* 3. Scan array looking for consecutive points
* 4. Find longest sequence of consecutive points
* 5. If sequence equals all points, no direction can be determined,
* expand search outward and try (1) again
* 6. Calculate angle using average of sequence
*/
const numPoints = 16;
for (var dist = 0; dist < 4; ++dist)
{
var waterPoints = [];
for (var i = 0; i < numPoints; ++i)
{
var angle = (i/numPoints)*2*Math.PI;
var d = halfSize*(dist+1);
var nx = x - d*Math.sin(angle);
var nz = z + d*Math.cos(angle);
if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz))
waterPoints.push(i);
}
var consec = [];
var length = waterPoints.length;
if (!length)
continue;
for (var i = 0; i < length; ++i)
{
var 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;
}
var start = 0;
var count = 0;
for (var 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 undefined;
}
/**
* Attempts to construct a building using the specified parameters.
* Returns true on success, false on failure.
*/
function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
{
// Message structure:
// {
// "type": "construct",
// "entities": [...], // entities that will be ordered to construct the building (if applicable)
// "template": "...", // template name of the entity being constructed
// "x": ...,
// "z": ...,
// "angle": ...,
// "metadata": "...", // AI metadata of the building
// "actorSeed": ...,
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this foundation to entities' queue (if applicable)
// "obstructionControlGroup": ..., // Optional; the obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. If specified, must be a valid control group ID (> 0).
// "obstructionControlGroup2": ..., // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. May be INVALID_ENTITY.
// }
/*
* Construction process:
* . Take resources away immediately.
* . Create a foundation entity with 1hp, 0% build progress.
* . Increase hp and build progress up to 100% when people work on it.
* . If it's destroyed, an appropriate fraction of the resource cost is refunded.
* . If it's completed, it gets replaced with the real building.
*/
// Check whether we can control these units
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
if (!entities.length)
return false;
var foundationTemplate = "foundation|" + cmd.template;
// Tentatively create the foundation (we might find later that it's a invalid build command)
var ent = Engine.AddEntity(foundationTemplate);
if (ent == INVALID_ENTITY)
{
// Error (e.g. invalid template names)
error("Error creating foundation entity for '" + cmd.template + "'");
return false;
}
// If it's a dock, get the right angle.
var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateMgr.GetTemplate(cmd.template);
var angle = cmd.angle;
if (template.BuildRestrictions.Category === "Dock")
{
let angleDock = GetDockAngle(template, cmd.x, cmd.z);
if (angleDock !== undefined)
angle = angleDock;
}
// Move the foundation to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
cmpPosition.SetYRotation(angle);
// Set the obstruction control group if needed
if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2)
{
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
// primary control group must always be valid
if (cmd.obstructionControlGroup)
{
if (cmd.obstructionControlGroup <= 0)
warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0");
cmpObstruction.SetControlGroup(cmd.obstructionControlGroup);
}
if (cmd.obstructionControlGroup2)
cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2);
}
// Make it owned by the current player
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (cmpBuildRestrictions)
{
var ret = cmpBuildRestrictions.CheckPlacement();
if (!ret.success)
{
if (g_DebugCommands)
warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd));
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
ret.players = [player];
cmpGuiInterface.PushNotification(ret);
// Remove the foundation because the construction was aborted
// move it out of world because it's not destroyed immediately.
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
return false;
}
}
else
error("cmpBuildRestrictions not defined");
// Check entity limits
var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
if (!cmpEntityLimits || !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
{
if (g_DebugCommands)
warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
// Remove the foundation because the construction was aborted
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
return false;
}
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager.CanProduce(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd));
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("The building's technology requirements are not met."),
"translateMessage": true
});
// Remove the foundation because the construction was aborted
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
}
// We need the cost after tech and aura modifications
// To calculate this with an entity requires ownership, so use the template instead
let cmpCost = Engine.QueryInterface(ent, IID_Cost);
let costs = cmpCost.GetResourceCosts(player);
if (!cmpPlayer.TrySubtractResources(costs))
{
if (g_DebugCommands)
warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
Engine.DestroyEntity(ent);
cmpPosition.MoveOutOfWorld();
return false;
}
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual && cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
// Initialise the foundation
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
cmpFoundation.InitialiseConstruction(player, cmd.template);
// send Metadata info if any
if (cmd.metadata)
Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } );
// Tell the units to start building this new entity
if (cmd.autorepair)
{
ProcessCommand(player, {
"type": "repair",
"entities": entities,
"target": ent,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued
});
}
return ent;
}
function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd)
{
// 'cmd' message structure:
// {
// "type": "construct-wall",
// "entities": [...], // entities that will be ordered to construct the wall (if applicable)
// "pieces": [ // ordered list of information about the pieces making up the wall (towers, wall segments, ...)
// {
// "template": "...", // one of the templates from the wallset
// "x": ...,
// "z": ...,
// "angle": ...,
// },
// ...
// ],
// "wallSet": {
// "templates": {
// "tower": // tower template name
// "long": // long wall segment template name
// ... // etc.
// },
// "maxTowerOverlap": ...,
// "minTowerOverlap": ...,
// },
// "startSnappedEntity": // optional; entity ID of tower being snapped to at the starting side of the wall
// "endSnappedEntity": // optional; entity ID of tower being snapped to at the ending side of the wall
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable)
// }
if (cmd.pieces.length <= 0)
return;
if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side");
return;
}
if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side");
return;
}
// Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement
// and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control
// groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing
// towers in the case of snapping). The towers themselves all keep their default unique control groups.
// To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour
// it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the
// first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of
// the first tower encountered towards the ending side of the wall (if any).
// We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the
// wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes:
//
// FIRST PASS:
// - Go from start to end and construct wall piece foundations as far as we can without running into a piece that
// cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID
// as the primary control group, thus allowing it to be built overlapping the previous piece.
// - If we encounter a new tower along the way (which will gain its own control group), do the following:
// o First build it using temporarily the same control group of the previous (non-tower) piece
// o Set the previous piece's secondary control group to the tower's entity ID
// o Restore the primary control group of the constructed tower back its original (unique) value.
// The temporary control group is necessary to allow the newer tower with its unique control group ID to be able
// to be placed while overlapping the previous piece.
//
// SECOND PASS:
// - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this
// time registering the right neighbouring tower in each non-tower piece.
// first pass; L -> R
var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces
var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that
// the first wall piece can be built while overlapping it.
if (cmd.startSnappedEntity)
{
var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction);
if (!cmpSnappedStartObstruction)
{
error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup();
//warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup);
}
var i = 0;
var queued = cmd.queued;
var pieces = clone(cmd.pieces);
for (; i < pieces.length; ++i)
{
var piece = pieces[i];
// All wall pieces after the first must be queued.
if (i > 0 && !queued)
queued = true;
// 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do
// start position snapping (implying that the first entity we build must be a tower)
if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY)
{
if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity))
{
error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")");
break;
}
}
var constructPieceCmd = {
"type": "construct",
"entities": cmd.entities,
"template": piece.template,
"x": piece.x,
"z": piece.z,
"angle": piece.angle,
"autorepair": cmd.autorepair,
"autocontinue": cmd.autocontinue,
"queued": queued,
// Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed
// using the control group of the last tower (see comments above).
"obstructionControlGroup": lastTowerControlGroup,
};
// If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's
// control group directly at construction time (instead of setting it in the second pass) to allow it to be built
// while overlapping the snapped entity.
if (i == pieces.length - 1 && cmd.endSnappedEntity)
{
var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (cmpEndSnappedObstruction)
constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup();
}
var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd);
if (pieceEntityId)
{
// wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later
piece.ent = pieceEntityId;
// if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex
if (piece.template == cmd.wallSet.templates.tower)
{
var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction);
var newTowerControlGroup = pieceEntityId;
if (i > 0)
{
//warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup);
var cmpPreviousObstruction = Engine.QueryInterface(pieces[i-1].ent, IID_Obstruction);
// TODO: ensure that cmpPreviousObstruction exists
// TODO: ensure that the previous obstruction does not yet have a secondary control group set
cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup);
}
// TODO: ensure that cmpTowerObstruction exists
cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group
lastTowerIndex = i;
lastTowerControlGroup = newTowerControlGroup;
}
}
else // failed to build wall piece, abort
break;
}
var lastBuiltPieceIndex = i - 1;
var wallComplete = (lastBuiltPieceIndex == pieces.length - 1);
// At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower).
// Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any)
// as their secondary control groups.
lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// only start off with the ending side's snapped tower's control group if we were able to build the entire wall
if (cmd.endSnappedEntity && wallComplete)
{
var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (!cmpSnappedEndObstruction)
{
error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup();
}
for (var j = lastBuiltPieceIndex; j >= 0; --j)
{
var piece = pieces[j];
if (!piece.ent)
{
error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'");
continue;
}
var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction);
if (!cmpPieceObstruction)
{
error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component");
continue;
}
if (piece.template == cmd.wallSet.templates.tower)
{
// encountered a tower entity, update the last tower control group
lastTowerControlGroup = cmpPieceObstruction.GetControlGroup();
}
else
{
// Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'.
// Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group
// dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'.
var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2();
if (existingSecondaryControlGroup == INVALID_ENTITY)
{
if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY)
{
cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup);
}
}
else if (existingSecondaryControlGroup != lastTowerControlGroup)
{
error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")");
break;
}
}
}
}
/**
* Remove the given list of entities from their current formations.
*/
function RemoveFromFormation(ents)
{
var formation = ExtractFormations(ents);
for (var fid in formation.members)
{
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
}
/**
* Returns a list of UnitAI components, each belonging either to a
* selected unit or to a formation entity for groups of the selected units.
*/
function GetFormationUnitAIs(ents, player, formationTemplate)
{
// If an individual was selected, remove it from any formation
// and command it individually
if (ents.length == 1)
{
// Skip unit if it has no UnitAI
var cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI);
if (!cmpUnitAI)
return [];
RemoveFromFormation(ents);
return [ cmpUnitAI ];
}
// Separate out the units that don't support the chosen formation
var formedEnts = [];
var nonformedUnitAIs = [];
for (let ent of ents)
{
// Skip units with no UnitAI or no position
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld())
continue;
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
// TODO: We only check if the formation is usable by some units
// if we move them to it. We should check if we can use formations
// for the other cases.
var nullFormation = (formationTemplate || cmpUnitAI.GetLastFormationTemplate()) == "formations/null";
if (!nullFormation && cmpIdentity && cmpIdentity.CanUseFormation(formationTemplate || "formations/null"))
formedEnts.push(ent);
else
{
if (nullFormation)
cmpUnitAI.SetLastFormationTemplate("formations/null");
nonformedUnitAIs.push(cmpUnitAI);
}
}
if (formedEnts.length == 0)
{
// No units support the foundation - return all the others
return nonformedUnitAIs;
}
// Find what formations the formationable selected entities are currently in
var formation = ExtractFormations(formedEnts);
var formationUnitAIs = [];
if (formation.ids.length == 1)
{
// Selected units either belong to this formation or have no formation
// Check that all its members are selected
var fid = formation.ids[0];
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length
&& cmpFormation.GetMemberCount() == formation.entities.length)
{
cmpFormation.DeleteTwinFormations();
// The whole formation was selected, so reuse its controller for this command
formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)];
if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate))
cmpFormation.LoadFormation(formationTemplate);
}
}
if (!formationUnitAIs.length)
{
// We need to give the selected units a new formation controller
// Remove selected units from their current formation
for (var fid in formation.members)
{
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
// TODO replace the fixed 60 with something sensible, based on vision range f.e.
var formationSeparation = 60;
var clusters = ClusterEntities(formation.entities, formationSeparation);
var formationEnts = [];
for (let cluster of clusters)
{
if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate))
{
// get the most recently used formation, or default to line closed
var lastFormationTemplate = undefined;
for (let ent of cluster)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
var template = cmpUnitAI.GetLastFormationTemplate();
if (lastFormationTemplate === undefined)
{
lastFormationTemplate = template;
}
else if (lastFormationTemplate != template)
{
lastFormationTemplate = undefined;
break;
}
}
}
if (lastFormationTemplate && CanMoveEntsIntoFormation(cluster, lastFormationTemplate))
formationTemplate = lastFormationTemplate;
else
formationTemplate = "formations/null";
}
// Create the new controller
var formationEnt = Engine.AddEntity(formationTemplate);
var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI));
cmpFormation.SetFormationSeparation(formationSeparation);
cmpFormation.SetMembers(cluster);
for (let ent of formationEnts)
cmpFormation.RegisterTwinFormation(ent);
formationEnts.push(formationEnt);
var cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
cmpOwnership.SetOwner(player);
}
}
return nonformedUnitAIs.concat(formationUnitAIs);
}
/**
* Group a list of entities in clusters via single-links
*/
function ClusterEntities(ents, separationDistance)
{
var clusters = [];
if (!ents.length)
return clusters;
var distSq = separationDistance * separationDistance;
var positions = [];
// triangular matrix with the (squared) distances between the different clusters
// the other half is not initialised
var matrix = [];
for (let i = 0; i < ents.length; ++i)
{
matrix[i] = [];
clusters.push([ents[i]]);
var cmpPosition = Engine.QueryInterface(ents[i], IID_Position);
positions.push(cmpPosition.GetPosition2D());
for (let j = 0; j < i; ++j)
matrix[i][j] = positions[i].distanceToSquared(positions[j]);
}
while (clusters.length > 1)
{
// search two clusters that are closer than the required distance
var closeClusters = undefined;
for (var i = matrix.length - 1; i >= 0 && !closeClusters; --i)
for (var j = i - 1; j >= 0 && !closeClusters; --j)
if (matrix[i][j] < distSq)
closeClusters = [i,j];
// if no more close clusters found, just return all found clusters so far
if (!closeClusters)
return clusters;
// make a new cluster with the entities from the two found clusters
var newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]);
// calculate the minimum distance between the new cluster and all other remaining
// clusters by taking the minimum of the two distances.
var distances = [];
for (let i = 0; i < clusters.length; ++i)
{
if (i == closeClusters[1] || i == closeClusters[0])
continue;
var dist1 = matrix[closeClusters[1]][i] || matrix[i][closeClusters[1]];
var dist2 = matrix[closeClusters[0]][i] || matrix[i][closeClusters[0]];
distances.push(Math.min(dist1, dist2));
}
// remove the rows and columns in the matrix for the merged clusters,
// and the clusters themselves from the cluster list
clusters.splice(closeClusters[0],1);
clusters.splice(closeClusters[1],1);
matrix.splice(closeClusters[0],1);
matrix.splice(closeClusters[1],1);
for (let i = 0; i < matrix.length; ++i)
{
if (matrix[i].length > closeClusters[0])
matrix[i].splice(closeClusters[0],1);
if (matrix[i].length > closeClusters[1])
matrix[i].splice(closeClusters[1],1);
}
// add a new row of distances to the matrix and the new cluster
clusters.push(newCluster);
matrix.push(distances);
}
return clusters;
}
function GetFormationRequirements(formationTemplate)
{
var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempManager.GetTemplate(formationTemplate);
if (!template.Formation)
return false;
return { "minCount": +template.Formation.RequiredMemberCount };
}
function CanMoveEntsIntoFormation(ents, formationTemplate)
{
// TODO: should check the player's civ is allowed to use this formation
// See simulation/components/Player.js GetFormations() for a list of all allowed formations
var requirements = GetFormationRequirements(formationTemplate);
if (!requirements)
return false;
var count = 0;
for (let ent of ents)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate))
continue;
++count;
}
return count >= requirements.minCount;
}
/**
* Check if player can control this entity
* returns: true if the entity is valid and owned by the player
* or control all units is activated, else false
*/
function CanControlUnit(entity, player, controlAll)
{
return IsOwnedByPlayer(player, entity) || controlAll;
}
/**
* Check if player can control this entity
* returns: true if the entity is valid and owned by the player
* or the entity is owned by an mutualAlly
* or control all units is activated, else false
*/
function CanControlUnitOrIsAlly(entity, player, controlAll)
{
return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity) || controlAll;
}
/**
* Filter entities which the player can control
*/
function FilterEntityList(entities, player, controlAll)
{
return entities.filter(ent => CanControlUnit(ent, player, controlAll));
}
/**
* Filter entities which the player can control or are mutualAlly
*/
function FilterEntityListWithAllies(entities, player, controlAll)
{
return entities.filter(ent => CanControlUnitOrIsAlly(ent, player, controlAll));
}
-/**
- * Try to transform a wall to a gate
- */
-function TryTransformWallToGate(ent, cmpPlayer, cmd)
-{
- var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
- if (!cmpIdentity)
- return;
-
- if (!cmpIdentity.HasClass("LongWall"))
- {
- if (g_DebugCommands)
- warn("Invalid command: invalid wall conversion to gate for player: " + uneval(cmd));
- return;
- }
-
- var gate = Engine.AddEntity(cmd.template);
-
- var cmpCost = Engine.QueryInterface(gate, IID_Cost);
- if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
- {
- if (g_DebugCommands)
- warn("Invalid command: convert gate cost check failed: " + uneval(cmd));
-
- Engine.DestroyEntity(gate);
- return;
- }
-
- ReplaceBuildingWith(ent, gate);
-}
-
-/**
- * Unconditionally replace a building with another one
- */
-function ReplaceBuildingWith(ent, building)
-{
- // Move the building to the right place
- var cmpPosition = Engine.QueryInterface(ent, IID_Position);
- var cmpBuildingPosition = Engine.QueryInterface(building, IID_Position);
- var pos = cmpPosition.GetPosition2D();
- cmpBuildingPosition.JumpTo(pos.x, pos.y);
- var rot = cmpPosition.GetRotation();
- cmpBuildingPosition.SetYRotation(rot.y);
- cmpBuildingPosition.SetXZRotation(rot.x, rot.z);
-
- // Copy ownership
- var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
- var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership);
- cmpBuildingOwnership.SetOwner(cmpOwnership.GetOwner());
-
- // Copy control groups
- var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
- var cmpBuildingObstruction = Engine.QueryInterface(building, IID_Obstruction);
- cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup());
- cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2());
-
- // Copy health level from the old entity to the new
- var cmpHealth = Engine.QueryInterface(ent, IID_Health);
- var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health);
- var healthFraction = Math.max(0, Math.min(1, cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints()));
- var buildingHitpoints = Math.round(cmpBuildingHealth.GetMaxHitpoints() * healthFraction);
- cmpBuildingHealth.SetHitpoints(buildingHitpoints);
-
- PlaySound("constructed", building);
-
- Engine.PostMessage(ent, MT_ConstructionFinished,
- { "entity": ent, "newentity": building });
- Engine.BroadcastMessage(MT_EntityRenamed, { entity: ent, newentity: building });
-
- Engine.DestroyEntity(ent);
-}
-
Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements);
Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation);
Engine.RegisterGlobal("GetDockAngle", GetDockAngle);
Engine.RegisterGlobal("ProcessCommand", ProcessCommand);
Engine.RegisterGlobal("g_Commands", g_Commands);
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Transform.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Transform.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Transform.js (revision 18467)
@@ -0,0 +1,206 @@
+// Helper functions to change an entity's template and check if the transformation is possible
+
+// returns the ID of the new entity or INVALID_ENTITY.
+function ChangeEntityTemplate(oldEnt, newTemplate)
+{
+ // Done un/packing, copy our parameters to the final entity
+ var newEnt = Engine.AddEntity(newTemplate);
+ if (newEnt == INVALID_ENTITY)
+ {
+ error("Transform.js: Error replacing entity " + oldEnt + " for a '" + newTemplate + "'");
+ return INVALID_ENTITY;
+ }
+
+ var cmpPosition = Engine.QueryInterface(oldEnt, IID_Position);
+ var cmpNewPosition = Engine.QueryInterface(newEnt, IID_Position);
+ if (cmpPosition && cmpNewPosition)
+ {
+ if (cmpPosition.IsInWorld())
+ {
+ var pos = cmpPosition.GetPosition2D();
+ cmpNewPosition.JumpTo(pos.x, pos.y);
+ }
+ var rot = cmpPosition.GetRotation();
+ cmpNewPosition.SetYRotation(rot.y);
+ cmpNewPosition.SetXZRotation(rot.x, rot.z);
+ cmpNewPosition.SetHeightOffset(cmpPosition.GetHeightOffset());
+ }
+
+ var cmpOwnership = Engine.QueryInterface(oldEnt, IID_Ownership);
+ var cmpNewOwnership = Engine.QueryInterface(newEnt, IID_Ownership);
+ if (cmpOwnership && cmpNewOwnership)
+ cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
+
+ // Copy control groups
+ var cmpObstruction = Engine.QueryInterface(oldEnt, IID_Obstruction);
+ var cmpNewObstruction = Engine.QueryInterface(newEnt, IID_Obstruction);
+ if (cmpObstruction && cmpNewObstruction)
+ {
+ cmpNewObstruction.SetControlGroup(cmpObstruction.GetControlGroup());
+ cmpNewObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2());
+ }
+
+ // Rescale capture points
+ var cmpCapturable = Engine.QueryInterface(oldEnt, IID_Capturable);
+ var cmpNewCapturable = Engine.QueryInterface(newEnt, IID_Capturable);
+ if (cmpCapturable && cmpNewCapturable)
+ {
+ let scale = cmpCapturable.GetMaxCapturePoints() / cmpNewCapturable.GetMaxCapturePoints();
+ let newCp = cmpCapturable.GetCapturePoints().map(v => v / scale);
+ cmpNewCapturable.SetCapturePoints(newCp);
+ }
+
+ // Maintain current health level
+ var cmpHealth = Engine.QueryInterface(oldEnt, IID_Health);
+ var cmpNewHealth = Engine.QueryInterface(newEnt, IID_Health);
+ if (cmpHealth && cmpNewHealth)
+ {
+ var healthLevel = Math.max(0, Math.min(1, cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints()));
+ cmpNewHealth.SetHitpoints(Math.round(cmpNewHealth.GetMaxHitpoints() * healthLevel));
+ }
+
+ var cmpUnitAI = Engine.QueryInterface(oldEnt, IID_UnitAI);
+ var cmpNewUnitAI = Engine.QueryInterface(newEnt, IID_UnitAI);
+ if (cmpUnitAI && cmpNewUnitAI)
+ {
+ var pos = cmpUnitAI.GetHeldPosition();
+ if (pos)
+ cmpNewUnitAI.SetHeldPosition(pos.x, pos.z);
+ if (cmpUnitAI.GetStanceName())
+ cmpNewUnitAI.SwitchToStance(cmpUnitAI.GetStanceName());
+ cmpNewUnitAI.AddOrders(cmpUnitAI.GetOrders());
+ cmpNewUnitAI.SetGuardOf(cmpUnitAI.IsGuardOf());
+ }
+
+ // Maintain the list of guards
+ var cmpGuard = Engine.QueryInterface(oldEnt, IID_Guard);
+ var cmpNewGuard = Engine.QueryInterface(newEnt, IID_Guard);
+ if (cmpGuard && cmpNewGuard)
+ cmpNewGuard.SetEntities(cmpGuard.GetEntities());
+
+ TransferGarrisonedUnits(oldEnt, newEnt);
+
+ Engine.BroadcastMessage(MT_EntityRenamed, { "entity": oldEnt, "newentity": newEnt });
+
+ Engine.DestroyEntity(oldEnt);
+
+ return newEnt;
+};
+
+function CanGarrisonedChangeTemplate(ent, template)
+{
+ var cmpPosition = Engine.QueryInterface(ent, IID_Position);
+ var unitAI = Engine.QueryInterface(ent, IID_UnitAI);
+ if (cmpPosition && !cmpPosition.IsInWorld() && unitAI && unitAI.IsGarrisoned())
+ {
+ // We're a garrisoned unit, assume impossibility as I've been unable to find a way to get the holder ID.
+ // TODO: change this if that ever becomes possibles
+ return false;
+ }
+ return true;
+}
+
+function ObstructionsBlockingTemplateChange(ent, templateArg)
+{
+ var previewEntity = Engine.AddEntity("preview|"+templateArg);
+
+ if (previewEntity == INVALID_ENTITY)
+ return true;
+
+ var cmpBuildRestrictions = Engine.QueryInterface(previewEntity, IID_BuildRestrictions);
+ var cmpPosition = Engine.QueryInterface(ent, IID_Position);
+ var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
+
+ var cmpNewPosition = Engine.QueryInterface(previewEntity, IID_Position);
+
+ // Return false if no ownership as BuildRestrictions.CheckPlacement needs an owner and I have no idea if false or true is better
+ // Plus there are no real entities without owners currently.
+ if (!cmpBuildRestrictions || !cmpPosition || !cmpOwnership)
+ return DeleteEntityAndReturn(previewEntity, cmpPosition, pos, angle, cmpNewPosition, false);
+
+ var pos = cmpPosition.GetPosition2D();
+ var angle = cmpPosition.GetRotation();
+ // move us away to prevent our own obstruction from blocking the upgrade.
+ cmpPosition.MoveOutOfWorld();
+
+ cmpNewPosition.JumpTo(pos.x, pos.y);
+ cmpNewPosition.SetYRotation(angle.y);
+
+ var cmpNewOwnership = Engine.QueryInterface(previewEntity, IID_Ownership);
+ cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
+
+ var checkPlacement = cmpBuildRestrictions.CheckPlacement();
+
+ if (checkPlacement && !checkPlacement.success)
+ return DeleteEntityAndReturn(previewEntity, cmpPosition, pos, angle, cmpNewPosition, true);
+
+ var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ var template = cmpTemplateManager.GetTemplate(cmpTemplateManager.GetCurrentTemplateName(ent));
+ var newTemplate = cmpTemplateManager.GetTemplate(templateArg);
+
+ // Check if units are blocking our template change
+ if (template.Obstruction && newTemplate.Obstruction)
+ {
+ // This only needs to be done if the new template is strictly bigger than the old one
+ // "Obstructions" are annoying to test so just check.
+ if (newTemplate.Obstruction.Obstructions ||
+
+ newTemplate.Obstruction.Static && template.Obstruction.Static &&
+ (newTemplate.Obstruction.Static["@width"] > template.Obstruction.Static["@width"] ||
+ newTemplate.Obstruction.Static["@depth"] > template.Obstruction.Static["@depth"]) ||
+ newTemplate.Obstruction.Static && template.Obstruction.Unit &&
+ (newTemplate.Obstruction.Static["@width"] > template.Obstruction.Unit["@radius"] ||
+ newTemplate.Obstruction.Static["@depth"] > template.Obstruction.Unit["@radius"]) ||
+
+ newTemplate.Obstruction.Unit && template.Obstruction.Unit &&
+ newTemplate.Obstruction.Unit["@radius"] > template.Obstruction.Unit["@radius"] ||
+ newTemplate.Obstruction.Unit && template.Obstruction.Static &&
+ (newTemplate.Obstruction.Unit["@radius"] > template.Obstruction.Static["@width"] ||
+ newTemplate.Obstruction.Unit["@radius"] > template.Obstruction.Static["@depth"]))
+ {
+ var cmpNewObstruction = Engine.QueryInterface(previewEntity, IID_Obstruction);
+ if (cmpNewObstruction && cmpNewObstruction.GetBlockMovementFlag())
+ {
+ // Check for units
+ var collisions = cmpNewObstruction.GetEntityCollisions(false, true);
+ if (collisions.length)
+ return DeleteEntityAndReturn(previewEntity, cmpPosition, pos, angle, cmpNewPosition, true);
+ }
+ }
+ }
+
+ return DeleteEntityAndReturn(previewEntity, cmpPosition, pos, angle, cmpNewPosition, false);
+};
+
+function DeleteEntityAndReturn(ent, cmpPosition, position, angle, cmpNewPosition, ret)
+{
+ // prevent preview from interfering in the world
+ cmpNewPosition.MoveOutOfWorld();
+ cmpPosition.JumpTo(position.x, position.y);
+ cmpPosition.SetYRotation(angle.y);
+
+ Engine.DestroyEntity(ent);
+ return ret;
+};
+
+function TransferGarrisonedUnits(oldEnt, newEnt)
+{
+ // Transfer garrisoned units if possible, or unload them
+ var cmpGarrison = Engine.QueryInterface(oldEnt, IID_GarrisonHolder);
+ var cmpNewGarrison = Engine.QueryInterface(newEnt, IID_GarrisonHolder);
+ if (!cmpNewGarrison || !cmpGarrison || !cmpGarrison.GetEntities().length)
+ return; // nothing to do as the code will by default unload all.
+
+ var garrisonedEntities = cmpGarrison.GetEntities().slice();
+ for (let j in garrisonedEntities)
+ {
+ var cmpUnitAI = Engine.QueryInterface(garrisonedEntities[j], IID_UnitAI);
+ cmpGarrison.Eject(garrisonedEntities[j]);
+ cmpUnitAI.Autogarrison(newEnt);
+ cmpNewGarrison.Garrison(garrisonedEntities[j]);
+ }
+};
+
+Engine.RegisterGlobal("ChangeEntityTemplate", ChangeEntityTemplate);
+Engine.RegisterGlobal("CanGarrisonedChangeTemplate", CanGarrisonedChangeTemplate);
+Engine.RegisterGlobal("ObstructionsBlockingTemplateChange", ObstructionsBlockingTemplateChange);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/other/palisades_rocks_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/other/palisades_rocks_long.xml (revision 18466)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/other/palisades_rocks_long.xml (revision 18467)
@@ -1,48 +1,57 @@
4.025.02.0111309.0600rubble/rubble_1x3palgaiaother/wallset_palisadePalisadeWooden Wall
- Convert Wooden Wall into Wooden Gate-StoneWall Palisadegaia/special_palisade.pngphase_village9.0props/special/palisade_rocks_long.xmlstructures/fndn_1x3pal.xml11.0
+
+
+ other/palisades_rocks_gate
+
+ 0
+ 20
+
+
+
+
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 18466)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml (revision 18467)
@@ -1,75 +1,74 @@
15.035.05.04.07.03.0own neutral enemy6007.005.7085.70-85.7045.70-45.702000romestructures/rome_wallset_siegeSiege WallMurus Circummunitionis
SiegeWall
structures/palisade_wall.pngA wooden and turf palisade buildable in enemy and neutral territories.
- Convert Siege Wall into Siege Wall GateQuick building, but expensive wooden and earthen walls used to surround and siege an enemy town or fortified position. The most famous examples are the Roman sieges of the Iberian stronghold of Numantia and the Gallic stronghold of Alesia.1structures/romans/siege_wall_long.xmlstructures/fndn_wall.xml36.0
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_long.xml (revision 18466)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_long.xml (revision 18467)
@@ -1,43 +1,52 @@
wall_garrisoned4528rubble/rubble_stone_wall_long5Ranged+Infantry0.1Unit02011.50811.50-811.50411.50-411.50LongWallLong wall segments can be converted to gates.
- Convert Stone Wall into City Gate
+
+
+ structures/{civ}_wall_gate
+ This will allow you to let units circulate through your fortifications.
+
+ 60
+
+
+
+