Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 25952)
+++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 25953)
@@ -1,624 +1,626 @@
/**
* Loads history and gameplay data of all civs.
*
* @param selectableOnly {boolean} - Only load civs that can be selected
* in the gamesetup. Scenario maps might set non-selectable civs.
*/
function loadCivFiles(selectableOnly)
{
let propertyNames = [
"Code", "Culture", "Name", "Emblem", "History", "Music", "CivBonuses", "StartEntities",
"Formations", "AINames", "SkirmishReplacements", "SelectableInGameSetup"];
let civData = {};
for (let filename of Engine.ListDirectoryFiles("simulation/data/civs/", "*.json", false))
{
let data = Engine.ReadJSONFile(filename);
for (let prop of propertyNames)
if (data[prop] === undefined)
throw new Error(filename + " doesn't contain " + prop);
if (!selectableOnly || data.SelectableInGameSetup)
civData[data.Code] = data;
}
return civData;
}
/**
* @return {string[]} - All the classes for this identity template.
*/
function GetIdentityClasses(template)
{
let classString = "";
if (template.Classes && template.Classes._string)
classString += " " + template.Classes._string;
if (template.VisibleClasses && template.VisibleClasses._string)
classString += " " + template.VisibleClasses._string;
if (template.Rank)
classString += " " + template.Rank;
return classString.length > 1 ? classString.substring(1).split(" ") : [];
}
/**
* Gets an array with all classes for this identity template
* that should be shown in the GUI
*/
function GetVisibleIdentityClasses(template)
{
return template.VisibleClasses && template.VisibleClasses._string ? template.VisibleClasses._string.split(" ") : [];
}
/**
* Check if a given list of classes matches another list of classes.
* Useful f.e. for checking identity 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 (let 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;
}
/**
* Gets the value originating at the value_path as-is, with no modifiers applied.
*
* @param {Object} template - A valid template as returned from a template loader.
* @param {string} value_path - Route to value within the xml template structure.
* @return {number}
*/
function GetBaseTemplateDataValue(template, value_path)
{
let current_value = template;
for (let property of value_path.split("/"))
current_value = current_value[property] || 0;
return +current_value;
}
/**
* Gets the value originating at the value_path with the modifiers dictated by the mod_key applied.
*
* @param {Object} template - A valid template as returned from a template loader.
* @param {string} value_path - Route to value within the xml template structure.
* @param {string} mod_key - Tech modification key, if different from value_path.
* @param {number} player - Optional player id.
* @param {Object} modifiers - Value modifiers from auto-researched techs, unit upgrades,
* etc. Optional as only used if no player id provided.
* @return {number} Modifier altered value.
*/
function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={})
{
let current_value = GetBaseTemplateDataValue(template, value_path);
mod_key = mod_key || value_path;
if (player)
current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template);
else if (modifiers && modifiers[mod_key])
current_value = GetTechModifiedProperty(modifiers[mod_key], GetIdentityClasses(template.Identity), current_value);
// Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance).
return +current_value.toFixed(8);
}
/**
* Get information about a template with or without technology modifications.
*
* NOTICE: The data returned here should have the same structure as
* the object returned by GetEntityState and GetExtendedEntityState!
*
* @param {Object} template - A valid template as returned by the template loader.
* @param {number} player - An optional player id to get the technology modifications
* of properties.
* @param {Object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }.
* @param {Object} modifiers - Modifications from auto-researched techs, unit upgrades
* etc. Optional as only used if there's no player
* id provided.
*/
function GetTemplateDataHelper(template, player, auraTemplates, modifiers = {})
{
// Return data either from template (in tech tree) or sim state (ingame).
// @param {string} value_path - Route to the value within the template.
// @param {string} mod_key - Modification key, if not the same as the value_path.
let getEntityValue = function(value_path, mod_key) {
return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers);
};
let ret = {};
if (template.Resistance)
{
// Don't show Foundation resistance.
ret.resistance = {};
if (template.Resistance.Entity)
{
if (template.Resistance.Entity.Damage)
{
ret.resistance.Damage = {};
for (let damageType in template.Resistance.Entity.Damage)
ret.resistance.Damage[damageType] = getEntityValue("Resistance/Entity/Damage/" + damageType);
}
if (template.Resistance.Entity.Capture)
ret.resistance.Capture = getEntityValue("Resistance/Entity/Capture");
if (template.Resistance.Entity.ApplyStatus)
{
ret.resistance.ApplyStatus = {};
for (let statusEffect in template.Resistance.Entity.ApplyStatus)
ret.resistance.ApplyStatus[statusEffect] = {
"blockChance": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/BlockChance"),
"duration": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/Duration")
};
}
}
}
let getAttackEffects = (temp, path) => {
let effects = {};
if (temp.Capture)
effects.Capture = getEntityValue(path + "/Capture");
if (temp.Damage)
{
effects.Damage = {};
for (let damageType in temp.Damage)
effects.Damage[damageType] = getEntityValue(path + "/Damage/" + damageType);
}
if (temp.ApplyStatus)
effects.ApplyStatus = temp.ApplyStatus;
return effects;
};
if (template.Attack)
{
ret.attack = {};
for (let type in template.Attack)
{
let getAttackStat = function(stat) {
return getEntityValue("Attack/" + type + "/" + stat);
};
ret.attack[type] = {
"attackName": {
"name": template.Attack[type].AttackName._string || template.Attack[type].AttackName,
"context": template.Attack[type].AttackName["@context"]
},
"minRange": getAttackStat("MinRange"),
"maxRange": getAttackStat("MaxRange"),
"elevationBonus": getAttackStat("ElevationBonus")
};
ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange *
(2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange));
ret.attack[type].repeatTime = getAttackStat("RepeatTime");
if (template.Attack[type].Projectile)
ret.attack[type].friendlyFire = template.Attack[type].Projectile.FriendlyFire == "true";
Object.assign(ret.attack[type], getAttackEffects(template.Attack[type], "Attack/" + type));
if (template.Attack[type].Splash)
{
ret.attack[type].splash = {
"friendlyFire": template.Attack[type].Splash.FriendlyFire != "false",
"shape": template.Attack[type].Splash.Shape,
};
Object.assign(ret.attack[type].splash, getAttackEffects(template.Attack[type].Splash, "Attack/" + type + "/Splash"));
}
}
}
if (template.DeathDamage)
{
ret.deathDamage = {
"friendlyFire": template.DeathDamage.FriendlyFire != "false",
};
Object.assign(ret.deathDamage, getAttackEffects(template.DeathDamage, "DeathDamage"));
}
if (template.Auras && auraTemplates)
{
ret.auras = {};
for (let auraID of template.Auras._string.split(/\s+/))
ret.auras[auraID] = GetAuraDataHelper(auraTemplates[auraID]);
}
if (template.BuildingAI)
ret.buildingAI = {
"defaultArrowCount": Math.round(getEntityValue("BuildingAI/DefaultArrowCount")),
"garrisonArrowMultiplier": getEntityValue("BuildingAI/GarrisonArrowMultiplier"),
"maxArrowCount": Math.round(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 = getEntityValue("BuildRestrictions/Distance/MinDistance");
if (template.BuildRestrictions.Distance.MaxDistance)
ret.buildRestrictions.distance.max = getEntityValue("BuildRestrictions/Distance/MaxDistance");
}
}
if (template.TrainingRestrictions)
{
ret.trainingRestrictions = {
"category": template.TrainingRestrictions.Category
};
if (template.TrainingRestrictions.MatchLimit)
ret.trainingRestrictions.matchLimit = +template.TrainingRestrictions.MatchLimit;
}
if (template.Cost)
{
ret.cost = {};
for (let resCode in template.Cost.Resources)
ret.cost[resCode] = getEntityValue("Cost/Resources/" + resCode);
if (template.Cost.Population)
ret.cost.population = getEntityValue("Cost/Population");
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.Garrisonable)
ret.garrisonable = {
"size": getEntityValue("Garrisonable/Size")
};
if (template.GarrisonHolder)
{
ret.garrisonHolder = {
"buffHeal": getEntityValue("GarrisonHolder/BuffHeal")
};
if (template.GarrisonHolder.Max)
ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max");
}
if (template.Heal)
ret.heal = {
"health": getEntityValue("Heal/Health"),
"range": getEntityValue("Heal/Range"),
"interval": getEntityValue("Heal/Interval")
};
if (template.ResourceGatherer)
{
ret.resourceGatherRates = {};
let baseSpeed = getEntityValue("ResourceGatherer/BaseSpeed");
for (let type in template.ResourceGatherer.Rates)
ret.resourceGatherRates[type] = getEntityValue("ResourceGatherer/Rates/"+ type) * baseSpeed;
}
if (template.ResourceDropsite)
ret.resourceDropsite = {
"types": template.ResourceDropsite.Types.split(" ")
};
if (template.ResourceTrickle)
{
ret.resourceTrickle = {
"interval": +template.ResourceTrickle.Interval,
"rates": {}
};
for (let type in template.ResourceTrickle.Rates)
ret.resourceTrickle.rates[type] = getEntityValue("ResourceTrickle/Rates/" + type);
}
if (template.Loot)
{
ret.loot = {};
for (let type in template.Loot)
ret.loot[type] = getEntityValue("Loot/"+ type);
}
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.Population && template.Population.Bonus)
ret.population = {
"bonus": getEntityValue("Population/Bonus")
};
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.requiredTechnology = template.Identity.RequiredTechnology;
ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity);
ret.nativeCiv = template.Identity.Civ;
}
if (template.UnitMotion)
{
+ const walkSpeed = getEntityValue("UnitMotion/WalkSpeed");
ret.speed = {
- "walk": getEntityValue("UnitMotion/WalkSpeed"),
+ "walk": walkSpeed,
+ "run": walkSpeed,
+ "acceleration": getEntityValue("UnitMotion/Acceleration")
};
- ret.speed.run = getEntityValue("UnitMotion/WalkSpeed");
if (template.UnitMotion.RunMultiplier)
ret.speed.run *= getEntityValue("UnitMotion/RunMultiplier");
}
if (template.Upgrade)
{
ret.upgrades = [];
for (let upgradeName in template.Upgrade)
{
let upgrade = template.Upgrade[upgradeName];
let cost = {};
if (upgrade.Cost)
for (let res in upgrade.Cost)
cost[res] = getEntityValue("Upgrade/" + upgradeName + "/Cost/" + res, "Upgrade/Cost/" + res);
if (upgrade.Time)
cost.time = getEntityValue("Upgrade/" + upgradeName + "/Time", "Upgrade/Time");
ret.upgrades.push({
"entity": upgrade.Entity,
"tooltip": upgrade.Tooltip,
"cost": cost,
"icon": upgrade.Icon || undefined,
"requiredTechnology": upgrade.RequiredTechnology || undefined
});
}
}
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.Treasure)
{
ret.treasure = {
"collectTime": getEntityValue("Treasure/CollectTime"),
"resources": {}
};
for (let resource in template.Treasure.Resources)
ret.treasure.resources[resource] = getEntityValue("Treasure/Resources/" + resource);
}
if (template.TurretHolder)
ret.turretHolder = {
"turretPoints": template.TurretHolder.TurretPoints
};
if (template.Upkeep)
{
ret.upkeep = {
"interval": +template.Upkeep.Interval,
"rates": {}
};
for (let type in template.Upkeep.Rates)
ret.upkeep.rates[type] = getEntityValue("Upkeep/Rates/" + type);
}
if (template.WallSet)
{
ret.wallSet = {
"templates": {
"tower": template.WallSet.Templates.Tower,
"gate": template.WallSet.Templates.Gate,
"fort": template.WallSet.Templates.Fort || "structures/" + template.Identity.Civ + "/fortress",
"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.WallSet.Templates.WallEnd)
ret.wallSet.templates.end = template.WallSet.Templates.WallEnd;
if (template.WallSet.Templates.WallCurves)
ret.wallSet.templates.curves = template.WallSet.Templates.WallCurves.split(/\s+/);
}
if (template.WallPiece)
ret.wallPiece = {
"length": +template.WallPiece.Length,
"angle": +(template.WallPiece.Orientation || 1) * Math.PI,
"indent": +(template.WallPiece.Indent || 0),
"bend": +(template.WallPiece.Bend || 0) * Math.PI
};
return ret;
}
/**
* Get basic information about a technology template.
* @param {Object} template - A valid template as obtained by loading the tech JSON file.
* @param {string} civ - Civilization for which the tech requirements should be calculated.
*/
function GetTechnologyBasicDataHelper(template, civ)
{
return {
"name": {
"generic": template.genericName
},
"icon": template.icon ? "technologies/" + template.icon : undefined,
"description": template.description,
"reqs": DeriveTechnologyRequirements(template, civ),
"modifications": template.modifications,
"affects": template.affects,
"replaces": template.replaces
};
}
/**
* Get information about a technology template.
* @param {Object} template - A valid template as obtained by loading the tech JSON file.
* @param {string} civ - Civilization for which the specific name and tech requirements should be returned.
*/
function GetTechnologyDataHelper(template, civ, resources)
{
let ret = GetTechnologyBasicDataHelper(template, civ);
if (template.specificName)
ret.name.specific = template.specificName[civ] || template.specificName.generic;
ret.cost = { "time": template.researchTime ? +template.researchTime : 0 };
for (let type of resources.GetCodes())
ret.cost[type] = +(template.cost && template.cost[type] || 0);
ret.tooltip = template.tooltip;
ret.requirementsTooltip = template.requirementsTooltip || "";
return ret;
}
/**
* Get information about an aura template.
* @param {object} template - A valid template as obtained by loading the aura JSON file.
*/
function GetAuraDataHelper(template)
{
return {
"name": {
"generic": template.auraName,
},
"description": template.auraDescription || null,
"modifications": template.modifications,
"radius": template.radius || null,
};
}
function calculateCarriedResources(carriedResources, tradingGoods)
{
var resources = {};
if (carriedResources)
for (let resource of carriedResources)
resources[resource.type] = (resources[resource.type] || 0) + resource.amount;
if (tradingGoods && tradingGoods.amount)
resources[tradingGoods.type] =
(resources[tradingGoods.type] || 0) +
(tradingGoods.amount.traderGain || 0) +
(tradingGoods.amount.market1Gain || 0) +
(tradingGoods.amount.market2Gain || 0);
return resources;
}
/**
* Remove filter prefix (mirage, corpse, etc) from template name.
*
* ie. filter|dir/to/template -> dir/to/template
*/
function removeFiltersFromTemplateName(templateName)
{
return templateName.split("|").pop();
}
Index: ps/trunk/binaries/data/mods/public/gui/common/tooltips.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 25952)
+++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 25953)
@@ -1,1229 +1,1235 @@
var g_TooltipTextFormats = {
"unit": { "font": "sans-10", "color": "orange" },
"header": { "font": "sans-bold-13" },
"body": { "font": "sans-13" },
"comma": { "font": "sans-12" },
"namePrimaryBig": { "font": "sans-bold-16" },
"namePrimarySmall": { "font": "sans-bold-12" },
"nameSecondary": { "font": "sans-bold-16" }
};
var g_SpecificNamesPrimary = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 2;
var g_ShowSecondaryNames = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 1;
function initDisplayedNames()
{
registerConfigChangeHandler(changes => {
if (changes.has("gui.session.howtoshownames"))
updateDisplayedNames();
});
}
/**
* String of four spaces to be used as indentation in gui strings.
*/
var g_Indent = " ";
var g_DamageTypesMetadata = new DamageTypesMetadata();
var g_StatusEffectsMetadata = new StatusEffectsMetadata();
/**
* If true, always shows whether the splash damage deals friendly fire.
* Otherwise display the friendly fire tooltip only if it does.
*/
var g_AlwaysDisplayFriendlyFire = false;
function getCostTypes()
{
return g_ResourceData.GetCodes().concat(["population", "time"]);
}
function resourceIcon(resource)
{
return '[icon="icon_' + resource + '"]';
}
function resourceNameFirstWord(type)
{
return translateWithContext("firstWord", g_ResourceData.GetNames()[type]);
}
function resourceNameWithinSentence(type)
{
return translateWithContext("withinSentence", g_ResourceData.GetNames()[type]);
}
/**
* Format resource amounts to proper english and translate (for example: "200 food, 100 wood and 300 metal").
*/
function getLocalizedResourceAmounts(resources)
{
let amounts = g_ResourceData.GetCodes()
.filter(type => !!resources[type])
.map(type => sprintf(translate("%(amount)s %(resourceType)s"), {
"amount": resources[type],
"resourceType": resourceNameWithinSentence(type)
}));
if (amounts.length < 2)
return amounts.join();
let lastAmount = amounts.pop();
return sprintf(translate("%(previousAmounts)s and %(lastAmount)s"), {
// Translation: This comma is used for separating first to penultimate elements in an enumeration.
"previousAmounts": amounts.join(translate(", ")),
"lastAmount": lastAmount
});
}
function bodyFont(text)
{
return setStringTags(text, g_TooltipTextFormats.body);
}
function headerFont(text)
{
return setStringTags(text, g_TooltipTextFormats.header);
}
function unitFont(text)
{
return setStringTags(text, g_TooltipTextFormats.unit);
}
function commaFont(text)
{
return setStringTags(text, g_TooltipTextFormats.comma);
}
function getSecondsString(seconds)
{
return sprintf(translatePlural("%(time)s %(second)s", "%(time)s %(second)s", seconds), {
"time": seconds,
"second": unitFont(translatePlural("second", "seconds", seconds))
});
}
/**
* Entity templates have a `Tooltip` tag in the Identity component.
* (The contents of which are copied to a `tooltip` attribute in globalscripts.)
*
* Technologies have a `tooltip` attribute.
*/
function getEntityTooltip(template)
{
if (!template.tooltip)
return "";
return bodyFont(template.tooltip);
}
/**
* Technologies have a `description` attribute, and Auras have an `auraDescription`
* attribute, which becomes `description`.
*
* (For technologies, this happens in globalscripts.)
*
* (For auras, this happens either in the Auras component (for session gui) or
* reference/common/load.js (for Reference Suite gui))
*/
function getDescriptionTooltip(template)
{
if (!template.description)
return "";
return bodyFont(template.description);
}
/**
* Entity templates have a `History` tag in the Identity component.
* (The contents of which are copied to a `history` attribute in globalscripts.)
*/
function getHistoryTooltip(template)
{
if (!template.history)
return "";
return bodyFont(template.history);
}
function getHealthTooltip(template)
{
if (!template.health)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Health:")),
"details": template.health
});
}
function getCurrentHealthTooltip(entState, label)
{
if (!entState.maxHitpoints)
return "";
return sprintf(translate("%(healthLabel)s %(current)s / %(max)s"), {
"healthLabel": headerFont(label || translate("Health:")),
"current": Math.round(entState.hitpoints),
"max": Math.round(entState.maxHitpoints)
});
}
function getCurrentCaptureTooltip(entState, label)
{
if (!entState.maxCapturePoints)
return "";
return sprintf(translate("%(captureLabel)s %(current)s / %(max)s"), {
"captureLabel": headerFont(label || translate("Capture points:")),
"current": Math.round(entState.capturePoints[entState.player]),
"max": Math.round(entState.maxCapturePoints)
});
}
/**
* Converts an resistance level into the actual reduction percentage.
*/
function resistanceLevelToPercentageString(level)
{
return sprintf(translate("%(percentage)s%%"), {
"percentage": (100 - Math.round(Math.pow(0.9, level) * 100))
});
}
function getResistanceTooltip(template)
{
if (!template.resistance)
return "";
let details = [];
if (template.resistance.Damage)
details.push(getDamageResistanceTooltip(template.resistance.Damage));
if (template.resistance.Capture)
details.push(getCaptureResistanceTooltip(template.resistance.Capture));
if (template.resistance.ApplyStatus)
details.push(getStatusEffectsResistanceTooltip(template.resistance.ApplyStatus));
return details.length ? sprintf(translate("%(label)s\n%(details)s"), {
"label": headerFont(translate("Resistance:")),
"details": g_Indent + details.join("\n" + g_Indent)
}) : "";
}
function getDamageResistanceTooltip(resistanceTypeTemplate)
{
if (!resistanceTypeTemplate)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Damage:")),
"details":
g_DamageTypesMetadata.sort(Object.keys(resistanceTypeTemplate)).map(
dmgType => sprintf(translate("%(damage)s %(damageType)s %(resistancePercentage)s"), {
"damage": resistanceTypeTemplate[dmgType].toFixed(1),
"damageType": unitFont(translateWithContext("damage type", g_DamageTypesMetadata.getName(dmgType))),
"resistancePercentage":
'[font="sans-10"]' +
sprintf(translate("(%(resistancePercentage)s)"), {
"resistancePercentage": resistanceLevelToPercentageString(resistanceTypeTemplate[dmgType])
}) + '[/font]'
})
).join(commaFont(translate(", ")))
});
}
function getCaptureResistanceTooltip(resistanceTypeTemplate)
{
if (!resistanceTypeTemplate)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Capture:")),
"details":
sprintf(translate("%(damage)s %(damageType)s %(resistancePercentage)s"), {
"damage": resistanceTypeTemplate.toFixed(1),
"damageType": unitFont(translateWithContext("damage type", "Capture")),
"resistancePercentage":
'[font="sans-10"]' +
sprintf(translate("(%(resistancePercentage)s)"), {
"resistancePercentage": resistanceLevelToPercentageString(resistanceTypeTemplate)
}) + '[/font]'
})
});
}
function getStatusEffectsResistanceTooltip(resistanceTypeTemplate)
{
if (!resistanceTypeTemplate)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Status Effects:")),
"details":
Object.keys(resistanceTypeTemplate).map(
statusEffect => {
if (resistanceTypeTemplate[statusEffect].blockChance == 1)
return sprintf(translate("Blocks %(name)s"), {
"name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect)))
});
if (resistanceTypeTemplate[statusEffect].blockChance == 0)
return sprintf(translate("%(name)s %(details)s"), {
"name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))),
"details": sprintf(translate("Duration reduction: %(durationReduction)s%%"), {
"durationReduction": (100 - resistanceTypeTemplate[statusEffect].duration * 100)
})
});
if (resistanceTypeTemplate[statusEffect].duration == 1)
return sprintf(translate("%(name)s %(details)s"), {
"name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))),
"details": sprintf(translate("Blocks: %(blockPercentage)s%%"), {
"blockPercentage": resistanceTypeTemplate[statusEffect].blockChance * 100
})
});
return sprintf(translate("%(name)s %(details)s"), {
"name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))),
"details": sprintf(translate("Blocks: %(blockPercentage)s%%, Duration reduction: %(durationReduction)s%%"), {
"blockPercentage": resistanceTypeTemplate[statusEffect].blockChance * 100,
"durationReduction": (100 - resistanceTypeTemplate[statusEffect].duration * 100)
})
});
}
).join(commaFont(translate(", ")))
});
}
function attackRateDetails(interval, projectiles)
{
if (!interval)
return "";
if (projectiles === 0)
return translate("Garrison to fire arrows");
let attackRateString = getSecondsString(interval / 1000);
let header = headerFont(translate("Interval:"));
if (projectiles && +projectiles > 1)
{
header = headerFont(translate("Rate:"));
let projectileString = sprintf(translatePlural("%(projectileCount)s %(projectileName)s", "%(projectileCount)s %(projectileName)s", projectiles), {
"projectileCount": projectiles,
"projectileName": unitFont(translatePlural("arrow", "arrows", projectiles))
});
attackRateString = sprintf(translate("%(projectileString)s / %(attackRateString)s"), {
"projectileString": projectileString,
"attackRateString": attackRateString
});
}
return sprintf(translate("%(label)s %(details)s"), {
"label": header,
"details": attackRateString
});
}
function rangeDetails(attackTypeTemplate)
{
if (!attackTypeTemplate.maxRange)
return "";
let rangeTooltipString = {
"relative": {
// Translation: For example: Range: 2 to 10 (+2) meters
"minRange": translate("%(rangeLabel)s %(minRange)s to %(maxRange)s (%(relativeRange)s) %(rangeUnit)s"),
// Translation: For example: Range: 10 (+2) meters
"no-minRange": translate("%(rangeLabel)s %(maxRange)s (%(relativeRange)s) %(rangeUnit)s"),
},
"non-relative": {
// Translation: For example: Range: 2 to 10 meters
"minRange": translate("%(rangeLabel)s %(minRange)s to %(maxRange)s %(rangeUnit)s"),
// Translation: For example: Range: 10 meters
"no-minRange": translate("%(rangeLabel)s %(maxRange)s %(rangeUnit)s"),
}
};
let minRange = Math.round(attackTypeTemplate.minRange);
let maxRange = Math.round(attackTypeTemplate.maxRange);
let realRange = attackTypeTemplate.elevationAdaptedRange;
let relativeRange = realRange ? Math.round(realRange - maxRange) : 0;
return sprintf(rangeTooltipString[relativeRange ? "relative" : "non-relative"][minRange ? "minRange" : "no-minRange"], {
"rangeLabel": headerFont(translate("Range:")),
"minRange": minRange,
"maxRange": maxRange,
"relativeRange": relativeRange > 0 ? sprintf(translate("+%(number)s"), { "number": relativeRange }) : relativeRange,
"rangeUnit":
unitFont(minRange || relativeRange ?
// Translation: For example "0.5 to 1 meters", "1 (+1) meters" or "1 to 2 (+3) meters"
translate("meters") :
translatePlural("meter", "meters", maxRange))
});
}
function damageDetails(damageTemplate)
{
if (!damageTemplate)
return "";
return g_DamageTypesMetadata.sort(Object.keys(damageTemplate).filter(dmgType => damageTemplate[dmgType])).map(
dmgType => sprintf(translate("%(damage)s %(damageType)s"), {
"damage": (+damageTemplate[dmgType]).toFixed(1),
"damageType": unitFont(translateWithContext("damage type", g_DamageTypesMetadata.getName(dmgType)))
})).join(commaFont(translate(", ")));
}
function captureDetails(captureTemplate)
{
if (!captureTemplate)
return "";
return sprintf(translate("%(amount)s %(name)s"), {
"amount": (+captureTemplate).toFixed(1),
"name": unitFont(translateWithContext("damage type", "Capture"))
});
}
function splashDetails(splashTemplate)
{
let splashLabel = sprintf(headerFont(translate("%(splashShape)s Splash")), {
"splashShape": translate(splashTemplate.shape)
});
let splashDamageTooltip = sprintf(translate("%(label)s: %(effects)s"), {
"label": splashLabel,
"effects": attackEffectsDetails(splashTemplate)
});
if (g_AlwaysDisplayFriendlyFire || splashTemplate.friendlyFire)
splashDamageTooltip += commaFont(translate(", ")) + sprintf(translate("Friendly Fire: %(enabled)s"), {
"enabled": splashTemplate.friendlyFire ? translate("Yes") : translate("No")
});
return splashDamageTooltip;
}
function applyStatusDetails(applyStatusTemplate)
{
if (!applyStatusTemplate)
return "";
return sprintf(translate("gives %(name)s"), {
"name": Object.keys(applyStatusTemplate).map(x =>
unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(x)))
).join(commaFont(translate(", "))),
});
}
function attackEffectsDetails(attackTypeTemplate)
{
if (!attackTypeTemplate)
return "";
let effects = [
captureDetails(attackTypeTemplate.Capture || undefined),
damageDetails(attackTypeTemplate.Damage || undefined),
applyStatusDetails(attackTypeTemplate.ApplyStatus || undefined)
];
return effects.filter(effect => effect).join(commaFont(translate(", ")));
}
function getAttackTooltip(template)
{
if (!template.attack)
return "";
let tooltips = [];
for (let attackType in template.attack)
{
// Slaughter is used to kill animals, so do not show it.
if (attackType == "Slaughter")
continue;
let attackTypeTemplate = template.attack[attackType];
let attackLabel = sprintf(headerFont(translate("%(attackType)s")), {
"attackType": translateWithContext(attackTypeTemplate.attackName.context || "Name of an attack, usually the weapon.", attackTypeTemplate.attackName.name)
});
let projectiles;
// Use either current rate from simulation or default count if the sim is not running.
// TODO: This ought to be extended to include units which fire multiple projectiles.
if (template.buildingAI)
projectiles = template.buildingAI.arrowCount || template.buildingAI.defaultArrowCount;
let splashTemplate = attackTypeTemplate.splash;
// Show the effects of status effects below.
let statusEffectsDetails = [];
if (attackTypeTemplate.ApplyStatus)
for (let status in attackTypeTemplate.ApplyStatus)
statusEffectsDetails.push("\n" + g_Indent + g_Indent + getStatusEffectsTooltip(status, attackTypeTemplate.ApplyStatus[status], true));
statusEffectsDetails = statusEffectsDetails.join("");
tooltips.push(sprintf(translate("%(attackLabel)s: %(effects)s, %(range)s, %(rate)s%(statusEffects)s%(splash)s"), {
"attackLabel": attackLabel,
"effects": attackEffectsDetails(attackTypeTemplate),
"range": rangeDetails(attackTypeTemplate),
"rate": attackRateDetails(attackTypeTemplate.repeatTime, projectiles),
"splash": splashTemplate ? "\n" + g_Indent + g_Indent + splashDetails(splashTemplate) : "",
"statusEffects": statusEffectsDetails
}));
}
return sprintf(translate("%(label)s\n%(details)s"), {
"label": headerFont(translate("Attack:")),
"details": g_Indent + tooltips.join("\n" + g_Indent)
});
}
/**
* @param applier - if true, return the tooltip for the Applier. If false, Receiver is returned.
*/
function getStatusEffectsTooltip(statusCode, template, applier)
{
let tooltipAttributes = [];
let statusData = g_StatusEffectsMetadata.getData(statusCode);
if (template.Damage || template.Capture)
tooltipAttributes.push(attackEffectsDetails(template));
if (template.Interval)
tooltipAttributes.push(attackRateDetails(+template.Interval));
if (template.Duration)
tooltipAttributes.push(getStatusEffectDurationTooltip(template));
if (applier && statusData.applierTooltip)
tooltipAttributes.push(translateWithContext("status effect", statusData.applierTooltip));
else if (!applier && statusData.receiverTooltip)
tooltipAttributes.push(translateWithContext("status effect", statusData.receiverTooltip));
if (applier)
return sprintf(translate("%(statusName)s: %(statusInfo)s %(stackability)s"), {
"statusName": headerFont(translateWithContext("status effect", statusData.statusName)),
"statusInfo": tooltipAttributes.join(commaFont(translate(", "))),
"stackability": getStatusEffectStackabilityTooltip(template)
});
return sprintf(translate("%(statusName)s: %(statusInfo)s"), {
"statusName": headerFont(translateWithContext("status effect", statusData.statusName)),
"statusInfo": tooltipAttributes.join(commaFont(translate(", ")))
});
}
function getStatusEffectDurationTooltip(template)
{
if (!template.Duration)
return "";
return sprintf(translate("%(durName)s: %(duration)s"), {
"durName": headerFont(translate("Duration")),
"duration": getSecondsString((template._timeElapsed ?
+template.Duration - template._timeElapsed :
+template.Duration) / 1000)
});
}
function getStatusEffectStackabilityTooltip(template)
{
if (!template.Stackability || template.Stackability == "Ignore")
return "";
let stackabilityString = "";
if (template.Stackability === "Extend")
stackabilityString = translateWithContext("status effect stackability", "(extends)");
else if (template.Stackability === "Replace")
stackabilityString = translateWithContext("status effect stackability", "(replaces)");
else if (template.Stackability === "Stack")
stackabilityString = translateWithContext("status effect stackability", "(stacks)");
return sprintf(translate("%(stackability)s"), {
"stackability": stackabilityString
});
}
function getGarrisonTooltip(template)
{
let tooltips = [];
if (template.garrisonHolder)
{
tooltips.push (
sprintf(translate("%(label)s: %(garrisonLimit)s"), {
"label": headerFont(translate("Garrison Limit")),
"garrisonLimit": template.garrisonHolder.capacity
})
);
if (template.garrisonHolder.buffHeal)
tooltips.push(
sprintf(translate("%(healRateLabel)s %(value)s %(health)s / %(second)s"), {
"healRateLabel": headerFont(translate("Heal:")),
"value": Math.round(template.garrisonHolder.buffHeal),
"health": unitFont(translateWithContext("garrison tooltip", "Health")),
"second": unitFont(translate("second")),
})
);
tooltips.join(commaFont(translate(", ")));
}
if (template.garrisonable)
{
let extraSize;
if (template.garrisonHolder)
extraSize = template.garrisonHolder.occupiedSlots;
if (template.garrisonable.size > 1 || extraSize)
tooltips.push (
sprintf(translate("%(label)s: %(garrisonSize)s %(extraSize)s"), {
"label": headerFont(translate("Garrison Size")),
"garrisonSize": template.garrisonable.size,
"extraSize": extraSize ?
translateWithContext("nested garrison", "+ ") + extraSize : ""
})
);
}
return tooltips.join("\n");
}
function getTurretsTooltip(template)
{
if (!template.turretHolder)
return "";
return sprintf(translate("%(label)s: %(turretsLimit)s"), {
"label": headerFont(translate("Turret Positions")),
"turretsLimit": Object.keys(template.turretHolder.turretPoints).length
});
}
function getProjectilesTooltip(template)
{
if (!template.garrisonHolder || !template.buildingAI)
return "";
let limit = Math.min(
template.buildingAI.maxArrowCount || Infinity,
template.buildingAI.defaultArrowCount +
Math.round(template.buildingAI.garrisonArrowMultiplier *
template.garrisonHolder.capacity)
);
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.toFixed(2)
})
].join(commaFont(translate(", ")));
}
function getRepairTimeTooltip(entState)
{
let result = [];
result.push(sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Number of repairers:")),
"details": entState.repairable.numBuilders
}));
if (entState.repairable.numBuilders)
{
result.push(sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Remaining repair time:")),
"details": getSecondsString(Math.floor(entState.repairable.buildTime.timeRemaining))
}));
let timeReduction = Math.round(entState.repairable.buildTime.timeRemaining - entState.repairable.buildTime.timeRemainingNew);
result.push(sprintf(translatePlural(
"Add another worker to speed up the repairs by %(second)s second.",
"Add another worker to speed up the repairs by %(second)s seconds.",
timeReduction),
{
"second": timeReduction
}));
}
else
result.push(sprintf(translatePlural(
"Add a worker to finish the repairs in %(second)s second.",
"Add a worker to finish the repairs in %(second)s seconds.",
Math.round(entState.repairable.buildTime.timeRemainingNew)),
{
"second": Math.round(entState.repairable.buildTime.timeRemainingNew)
}));
return result.join("\n");
}
function getBuildTimeTooltip(entState)
{
let result = [];
result.push(sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Number of builders:")),
"details": entState.foundation.numBuilders
}));
if (entState.foundation.numBuilders)
{
result.push(sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Remaining build time:")),
"details": getSecondsString(Math.floor(entState.foundation.buildTime.timeRemaining))
}));
let timeReduction = Math.round(entState.foundation.buildTime.timeRemaining - entState.foundation.buildTime.timeRemainingNew);
result.push(sprintf(translatePlural(
"Add another worker to speed up the construction by %(second)s second.",
"Add another worker to speed up the construction by %(second)s seconds.",
timeReduction),
{
"second": timeReduction
}));
}
else
result.push(sprintf(translatePlural(
"Add a worker to finish the construction in %(second)s second.",
"Add a worker to finish the construction in %(second)s seconds.",
Math.round(entState.foundation.buildTime.timeRemainingNew)),
{
"second": Math.round(entState.foundation.buildTime.timeRemainingNew)
}));
return result.join("\n");
}
/**
* Multiplies the costs for a template by a given batch size.
*/
function multiplyEntityCosts(template, trainNum)
{
let totalCosts = {};
for (let r of getCostTypes())
if (template.cost[r])
totalCosts[r] = Math.floor(template.cost[r] * trainNum);
return totalCosts;
}
/**
* Helper function for getEntityCostTooltip.
*/
function getEntityCostComponentsTooltipString(template, entity, buildingsCountToTrainFullBatch = 1, fullBatchSize = 1, remainderBatch = 0)
{
if (!template.cost)
return [];
let totalCosts = multiplyEntityCosts(template, buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch);
if (template.cost.time)
totalCosts.time = Math.ceil(template.cost.time * (entity ? Engine.GuiInterfaceCall("GetBatchTime", {
"entity": entity,
"batchSize": buildingsCountToTrainFullBatch > 0 ? fullBatchSize : remainderBatch
}) : 1));
let costs = [];
for (let type of getCostTypes())
if (totalCosts[type])
costs.push(sprintf(translate("%(component)s %(cost)s"), {
"component": resourceIcon(type),
"cost": totalCosts[type]
}));
return costs;
}
function getGatherTooltip(template)
{
if (!template.resourceGatherRates)
return "";
let rates = {};
for (let resource of g_ResourceData.GetResources())
{
let types = [resource.code];
for (let subtype in resource.subtypes)
{
// We ignore ruins as those are not that common
if (subtype == "ruins")
continue;
let rate = template.resourceGatherRates[resource.code + "." + subtype];
if (rate > 0)
rates[resource.code + "_" + subtype] = rate;
}
}
if (!Object.keys(rates).length)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Gather Rates:")),
"details":
Object.keys(rates).map(
type => sprintf(translate("%(resourceIcon)s %(rate)s"), {
"resourceIcon": resourceIcon(type),
"rate": rates[type].toFixed(2)
})
).join(" ")
});
}
/**
* Returns the resources this entity supplies in the specified entity's tooltip
*/
function getResourceSupplyTooltip(template)
{
if (!template.supply)
return "";
let supply = template.supply;
// Translation: Label in tooltip showing the resource type and quantity of a given resource supply.
return sprintf(translate("%(label)s %(component)s %(amount)s"), {
"label": headerFont(translate("Resource Supply:")),
"component": resourceIcon(supply.type[0]),
// Translation: Marks that a resource supply entity has an unending, infinite, supply of its resource.
"amount": Number.isFinite(+supply.amount) ? supply.amount : translate("∞")
});
}
/**
* @param {Object} template - The entity's template.
* @return {string} - The resources this entity rewards to a collecter.
*/
function getTreasureTooltip(template)
{
if (!template.treasure)
return "";
let resources = {};
for (let resource of g_ResourceData.GetResources())
{
let type = resource.code;
if (template.treasure.resources[type])
resources[type] = template.treasure.resources[type];
}
let resourceNames = Object.keys(resources);
if (!resourceNames.length)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Reward:")),
"details":
resourceNames.map(
type => sprintf(translate("%(resourceIcon)s %(reward)s"), {
"resourceIcon": resourceIcon(type),
"reward": resources[type]
})
).join(" ")
});
}
function getResourceTrickleTooltip(template)
{
if (!template.resourceTrickle)
return "";
let resCodes = g_ResourceData.GetCodes().filter(res => !!template.resourceTrickle.rates[res]);
if (!resCodes.length)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Resource Trickle:")),
"details": sprintf(translate("%(resources)s / %(time)s"), {
"resources":
resCodes.map(
res => sprintf(translate("%(resourceIcon)s %(rate)s"), {
"resourceIcon": resourceIcon(res),
"rate": template.resourceTrickle.rates[res]
})
).join(" "),
"time": getSecondsString(template.resourceTrickle.interval / 1000)
})
});
}
function getUpkeepTooltip(template)
{
if (!template.upkeep)
return "";
let resCodes = g_ResourceData.GetCodes().filter(res => !!template.upkeep.rates[res]);
if (!resCodes.length)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Upkeep:")),
"details": sprintf(translate("%(resources)s / %(time)s"), {
"resources":
resCodes.map(
res => sprintf(translate("%(resourceIcon)s %(rate)s"), {
"resourceIcon": resourceIcon(res),
"rate": template.upkeep.rates[res]
})
).join(" "),
"time": getSecondsString(template.upkeep.interval / 1000)
})
});
}
/**
* 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 = {};
for (let resource of getCostTypes())
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": resourceIcon(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, player, entity, buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
{
// 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, player);
let templateMedium = GetTemplateData(template.wallSet.templates.medium, player);
let templateShort = GetTemplateData(template.wallSet.templates.short, player);
let templateTower = GetTemplateData(template.wallSet.templates.tower, player);
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)
{
let costs = getEntityCostComponentsTooltipString(template, entity, buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch).join(" ");
if (costs)
// Translation: Label in tooltip showing cost of a unit, structure or technology.
return sprintf(translate("%(label)s %(costs)s"), {
"label": headerFont(translate("Cost:")),
"costs": costs
});
}
return "";
}
function getRequiredTechnologyTooltip(technologyEnabled, requiredTechnology, civ)
{
if (technologyEnabled)
return "";
return sprintf(translate("Requires %(technology)s"), {
"technology": getEntityNames(GetTechnologyData(requiredTechnology, civ))
});
}
/**
* Returns the population bonus information to display in the specified entity's construction button tooltip.
*/
function getPopulationBonusTooltip(template)
{
if (!template.population || !template.population.bonus)
return "";
return sprintf(translate("%(label)s %(bonus)s"), {
"label": headerFont(translate("Population Bonus:")),
"bonus": template.population.bonus
});
}
/**
* 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"]' + resourceIcon(resource) + '[/font]',
"cost": resources[resource]
}));
return coloredText(
'[font="sans-bold-13"]' + translate("Insufficient resources:") + '[/font]',
"red") + " " +
formatted.join(" ");
}
function getSpeedTooltip(template)
{
if (!template.speed)
return "";
- let walk = template.speed.walk.toFixed(1);
- let run = template.speed.run.toFixed(1);
+ const walk = template.speed.walk.toFixed(1);
+ const run = template.speed.run.toFixed(1);
if (walk == 0 && run == 0)
return "";
+ const acceleration = template.speed.acceleration.toFixed(1);
return sprintf(translate("%(label)s %(speeds)s"), {
"label": headerFont(translate("Speed:")),
"speeds":
sprintf(translate("%(speed)s %(movementType)s"), {
"speed": walk,
"movementType": unitFont(translate("Walk"))
}) +
commaFont(translate(", ")) +
sprintf(translate("%(speed)s %(movementType)s"), {
"speed": run,
"movementType": unitFont(translate("Run"))
+ }) +
+ commaFont(translate(", ")) +
+ sprintf(translate("%(speed)s %(movementType)s"), {
+ "speed": acceleration,
+ "movementType": unitFont(translate("Acceleration"))
})
});
}
function getHealerTooltip(template)
{
if (!template.heal)
return "";
let health = +(template.heal.health.toFixed(1));
let range = +(template.heal.range.toFixed(0));
let interval = +((template.heal.interval / 1000).toFixed(1));
return [
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", health), {
"label": headerFont(translate("Heal:")),
"val": health,
"unit": unitFont(translatePlural("Health", "Health", health))
}),
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", interval), {
"label": headerFont(translate("Interval:")),
"val": interval,
"unit": unitFont(translatePlural("second", "seconds", interval))
})
].join(translate(", "));
}
function getAurasTooltip(template)
{
let auras = template.auras || template.wallSet && GetTemplateData(template.wallSet.templates.long).auras;
if (!auras)
return "";
let tooltips = [];
for (let auraID in auras)
{
let tooltip = sprintf(translate("%(auralabel)s %(aurainfo)s"), {
"auralabel": headerFont(sprintf(translate("%(auraname)s:"), {
"auraname": getEntityNames(auras[auraID])
})),
"aurainfo": bodyFont(translate(auras[auraID].description))
});
let radius = +auras[auraID].radius;
if (radius)
tooltip += " " + sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", radius), {
"label": translateWithContext("aura", "Range:"),
"val": radius,
"unit": unitFont(translatePlural("meter", "meters", radius))
});
tooltips.push(tooltip);
}
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;
let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic;
let secondaryName;
if (g_ShowSecondaryNames)
secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific;
if (secondaryName)
return sprintf(translate("%(primaryName)s (%(secondaryName)s)"), {
"primaryName": primaryName,
"secondaryName": secondaryName
});
return sprintf(translate("%(primaryName)s"), {
"primaryName": primaryName
});
}
function getEntityNamesFormatted(template)
{
if (!template.name.specific)
return setStringTags(template.name.generic, g_TooltipTextFormats.namePrimaryBig);
let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic;
let secondaryName;
if (g_ShowSecondaryNames)
secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific;
if (!secondaryName || primaryName == secondaryName)
return sprintf(translate("%(primaryName)s"), {
"primaryName":
setStringTags(primaryName[0], g_TooltipTextFormats.namePrimaryBig) +
setStringTags(primaryName.slice(1).toUpperCase(), g_TooltipTextFormats.namePrimarySmall)
});
// Translation: Example: "Epibátēs Athēnaîos [font="sans-bold-16"](Athenian Marine)[/font]"
return sprintf(translate("%(primaryName)s (%(secondaryName)s)"), {
"primaryName":
setStringTags(primaryName[0], g_TooltipTextFormats.namePrimaryBig) +
setStringTags(primaryName.slice(1).toUpperCase(), g_TooltipTextFormats.namePrimarySmall),
"secondaryName": setStringTags(secondaryName, g_TooltipTextFormats.nameSecondary)
});
}
function getEntityPrimaryNameFormatted(template)
{
let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic;
if (!primaryName)
return setStringTags(g_SpecificNamesPrimary ? template.name.generic : template.name.specific, g_TooltipTextFormats.namePrimaryBig);
return setStringTags(primaryName[0], g_TooltipTextFormats.namePrimaryBig) +
setStringTags(primaryName.slice(1).toUpperCase(), g_TooltipTextFormats.namePrimarySmall);
}
function getVisibleEntityClassesFormatted(template)
{
if (!template.visibleIdentityClasses || !template.visibleIdentityClasses.length)
return "";
return headerFont(translate("Classes:")) + ' ' +
bodyFont(template.visibleIdentityClasses.map(c => translate(c)).join(translate(", ")));
}
function getLootTooltip(template)
{
if (!template.loot && !template.resourceCarrying)
return "";
let resourcesCarried = [];
if (template.resourceCarrying)
resourcesCarried = calculateCarriedResources(
template.resourceCarrying,
template.trader && template.trader.goods
);
let lootLabels = [];
for (let type of g_ResourceData.GetCodes().concat(["xp"]))
{
let loot =
(template.loot && template.loot[type] || 0) +
(resourcesCarried[type] || 0);
if (!loot)
continue;
// Translation: %(component) will be the icon for the loot type and %(loot) will be the value.
lootLabels.push(sprintf(translate("%(component)s %(loot)s"), {
"component": resourceIcon(type),
"loot": loot
}));
}
if (!lootLabels.length)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Loot:")),
"details": lootLabels.join(" ")
});
}
function getResourceDropsiteTooltip(template)
{
if (!template || !template.resourceDropsite || !template.resourceDropsite.types)
return "";
return sprintf(translate("%(label)s %(icons)s"), {
"label": headerFont(translate("Dropsite for:")),
"icons": template.resourceDropsite.types.map(type => resourceIcon(type)).join(" ")
});
}
function showTemplateViewerOnRightClickTooltip()
{
// Translation: Appears in a tooltip to indicate that right-clicking the corresponding GUI element will open the Template Details GUI page.
return translate("Right-click to view more information.");
}
function showTemplateViewerOnClickTooltip()
{
// Translation: Appears in a tooltip to indicate that clicking the corresponding GUI element will open the Template Details GUI page.
return translate("Click to view more information.");
}
/**
* @param {number} number - A number to shorten using SI prefix.
*/
function abbreviateLargeNumbers(number)
{
if (number >= 1e6)
return Math.floor(number / 1e6) + translateWithContext("One letter abbreviation for million", 'M');
if (number >= 1e5)
return Math.floor(number / 1e3) + translateWithContext("One letter abbreviation for thousand", 'k');
if (number >= 1e4)
return (number / 1e3).toFixed(1).replace(/\.0$/, '') + translateWithContext("One letter abbreviation for thousand", 'k');
return number;
}
Index: ps/trunk/binaries/data/mods/public/simulation/components/Formation.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Formation.js (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Formation.js (revision 25953)
@@ -1,1035 +1,1040 @@
function Formation() {}
Formation.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
""+
"2"+
""+
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
// Distance at which we'll switch between column/box formations.
var g_ColumnDistanceThreshold = 128;
Formation.prototype.variablesToSerialize = [
"lastOrderVariant",
"members",
"memberPositions",
"maxRowsUsed",
"maxColumnsUsed",
"finishedEntities",
"idleEntities",
"columnar",
"rearrange",
"formationMembersWithAura",
"width",
"depth",
"twinFormations",
"formationSeparation",
"offsets"
];
Formation.prototype.Init = function(deserialized = false)
{
this.sortingClasses = this.template.SortingClasses.split(/\s+/g);
this.shiftRows = this.template.ShiftRows == "true";
this.separationMultiplier = {
"width": +this.template.UnitSeparationWidthMultiplier,
"depth": +this.template.UnitSeparationDepthMultiplier
};
this.sloppiness = +this.template.Sloppiness;
this.widthDepthRatio = +this.template.WidthDepthRatio;
this.minColumns = +(this.template.MinColumns || 0);
this.maxColumns = +(this.template.MaxColumns || 0);
this.maxRows = +(this.template.MaxRows || 0);
this.centerGap = +(this.template.CenterGap || 0);
if (this.template.AnimationVariants)
{
this.animationvariants = [];
let differentAnimationVariants = this.template.AnimationVariants.split(/\s*;\s*/);
// Loop over the different rectangulars that will map to different animation variants.
for (let rectAnimationVariant of differentAnimationVariants)
{
let rect, replacementAnimationVariant;
[rect, replacementAnimationVariant] = rectAnimationVariant.split(/\s*:\s*/);
let rows, columns;
[rows, columns] = rect.split(/\s*,\s*/);
let minRow, maxRow, minColumn, maxColumn;
[minRow, maxRow] = rows.split(/\s*\.\.\s*/);
[minColumn, maxColumn] = columns.split(/\s*\.\.\s*/);
this.animationvariants.push({
"minRow": +minRow,
"maxRow": +maxRow,
"minColumn": +minColumn,
"maxColumn": +maxColumn,
"name": replacementAnimationVariant
});
}
}
this.lastOrderVariant = undefined;
// Entity IDs currently belonging to this formation.
this.members = [];
this.memberPositions = {};
this.maxRowsUsed = 0;
this.maxColumnsUsed = [];
// Entities that have finished the original task.
this.finishedEntities = new Set();
this.idleEntities = new Set();
// Whether we're travelling in column (vs box) formation.
this.columnar = false;
// Whether we should rearrange all formation members.
this.rearrange = true;
// Members with a formation aura.
this.formationMembersWithAura = [];
this.width = 0;
this.depth = 0;
this.twinFormations = [];
// Distance from which two twin formations will merge into one.
this.formationSeparation = 0;
if (deserialized)
return;
Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer)
.SetInterval(this.entity, IID_Formation, "ShapeUpdate", 1000, 1000, null);
};
Formation.prototype.Serialize = function()
{
let result = {};
for (let key of this.variablesToSerialize)
result[key] = this[key];
return result;
};
Formation.prototype.Deserialize = function(data)
{
this.Init(true);
for (let key in data)
this[key] = data[key];
};
/**
* Set the value from which two twin formations will become one.
*/
Formation.prototype.SetFormationSeparation = function(value)
{
this.formationSeparation = value;
};
Formation.prototype.GetSize = function()
{
return { "width": this.width, "depth": this.depth };
};
Formation.prototype.GetSpeedMultiplier = function()
{
return +this.template.SpeedMultiplier;
};
Formation.prototype.GetMemberCount = function()
{
return this.members.length;
};
Formation.prototype.GetMembers = function()
{
return this.members;
};
Formation.prototype.GetClosestMember = function(ent, filter)
{
let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpEntPosition || !cmpEntPosition.IsInWorld())
return INVALID_ENTITY;
let entPosition = cmpEntPosition.GetPosition2D();
let closestMember = INVALID_ENTITY;
let closestDistance = Infinity;
for (let member of this.members)
{
if (filter && !filter(ent))
continue;
let cmpPosition = Engine.QueryInterface(member, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition2D();
let dist = entPosition.distanceToSquared(pos);
if (dist < closestDistance)
{
closestMember = member;
closestDistance = dist;
}
}
return closestMember;
};
/**
* Returns the 'primary' member of this formation (typically the most
* important unit type), for e.g. playing a representative sound.
* Returns undefined if no members.
* TODO: Actually implement something like that. Currently this just returns
* the arbitrary first one.
*/
Formation.prototype.GetPrimaryMember = function()
{
return this.members[0];
};
/**
* Get the formation animation variant for a certain member of this formation.
* @param entity The entity ID to get the animation for.
* @return The name of the animation variant as defined in the template,
* e.g. "testudo_front" or undefined if does not exist.
*/
Formation.prototype.GetFormationAnimationVariant = function(entity)
{
if (!this.animationvariants || !this.animationvariants.length || this.columnar || !this.memberPositions[entity])
return undefined;
let row = this.memberPositions[entity].row;
let column = this.memberPositions[entity].column;
for (let i = 0; i < this.animationvariants.length; ++i)
{
let minRow = this.animationvariants[i].minRow;
if (minRow < 0)
minRow += this.maxRowsUsed + 1;
if (row < minRow)
continue;
let maxRow = this.animationvariants[i].maxRow;
if (maxRow < 0)
maxRow += this.maxRowsUsed + 1;
if (row > maxRow)
continue;
let minColumn = this.animationvariants[i].minColumn;
if (minColumn < 0)
minColumn += this.maxColumnsUsed[row] + 1;
if (column < minColumn)
continue;
let maxColumn = this.animationvariants[i].maxColumn;
if (maxColumn < 0)
maxColumn += this.maxColumnsUsed[row] + 1;
if (column > maxColumn)
continue;
return this.animationvariants[i].name;
}
return undefined;
};
Formation.prototype.SetFinishedEntity = function(ent)
{
// Rotate the entity to the correct angle.
const cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
const cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpEntPosition && cmpEntPosition.IsInWorld() && cmpPosition && cmpPosition.IsInWorld())
cmpEntPosition.TurnTo(cmpPosition.GetRotation().y);
this.finishedEntities.add(ent);
};
Formation.prototype.UnsetFinishedEntity = function(ent)
{
this.finishedEntities.delete(ent);
};
Formation.prototype.ResetFinishedEntities = function()
{
this.finishedEntities.clear();
};
Formation.prototype.AreAllMembersFinished = function()
{
return this.finishedEntities.size === this.members.length;
};
Formation.prototype.SetIdleEntity = function(ent)
{
this.idleEntities.add(ent);
};
Formation.prototype.UnsetIdleEntity = function(ent)
{
this.idleEntities.delete(ent);
};
Formation.prototype.ResetIdleEntities = function()
{
this.idleEntities.clear();
};
Formation.prototype.AreAllMembersIdle = function()
{
return this.idleEntities.size === this.members.length;
};
/**
* Set whether we are allowed to rearrange formation members.
*/
Formation.prototype.SetRearrange = function(rearrange)
{
this.rearrange = rearrange;
};
/**
* Initialize the members of this formation.
* Must only be called once.
* All members must implement UnitAI.
*/
Formation.prototype.SetMembers = function(ents)
{
this.members = ents;
for (let ent of this.members)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.SetFormationController(this.entity);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (cmpAuras && cmpAuras.HasFormationAura())
{
this.formationMembersWithAura.push(ent);
cmpAuras.ApplyFormationAura(ents);
}
}
this.offsets = undefined;
// Locate this formation controller in the middle of its members.
this.MoveToMembersCenter();
// Compute the speed etc. of the formation.
this.ComputeMotionParameters();
};
/**
* Remove the given list of entities.
* The entities must already be members of this formation.
* @param {boolean} rename - Whether the removal was part of an entity rename
(prevents disbanding of the formation when under the member limit).
*/
Formation.prototype.RemoveMembers = function(ents, renamed = false)
{
this.offsets = undefined;
this.members = this.members.filter(ent => ents.indexOf(ent) === -1);
for (let ent of ents)
{
this.finishedEntities.delete(ent);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.UpdateWorkOrders();
cmpUnitAI.SetFormationController(INVALID_ENTITY);
}
for (let ent of this.formationMembersWithAura)
{
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
cmpAuras.RemoveFormationAura(ents);
// The unit with the aura is also removed from the formation.
if (ents.indexOf(ent) !== -1)
cmpAuras.RemoveFormationAura(this.members);
}
this.formationMembersWithAura = this.formationMembersWithAura.filter(function(e) { return ents.indexOf(e) == -1; });
// If there's nobody left, destroy the formation
// unless this is a rename where we can have 0 members temporarily.
if (this.members.length < +this.template.RequiredMemberCount && !renamed)
{
this.Disband();
return;
}
this.ComputeMotionParameters();
if (!this.rearrange)
return;
// Rearrange the remaining members.
this.MoveMembersIntoFormation(true, true, this.lastOrderVariant);
};
Formation.prototype.AddMembers = function(ents)
{
this.offsets = undefined;
for (let ent of this.formationMembersWithAura)
{
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
cmpAuras.ApplyFormationAura(ents);
}
this.members = this.members.concat(ents);
for (let ent of ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.SetFormationController(this.entity);
if (!cmpUnitAI.GetOrders().length)
cmpUnitAI.SetNextState("FORMATIONMEMBER.IDLE");
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (cmpAuras && cmpAuras.HasFormationAura())
{
this.formationMembersWithAura.push(ent);
cmpAuras.ApplyFormationAura(this.members);
}
}
this.ComputeMotionParameters();
if (!this.rearrange)
return;
this.MoveMembersIntoFormation(true, true, this.lastOrderVariant);
};
/**
* Remove all members and destroy the formation.
*/
Formation.prototype.Disband = function()
{
for (let ent of this.members)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.SetFormationController(INVALID_ENTITY);
}
for (let ent of this.formationMembersWithAura)
{
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
cmpAuras.RemoveFormationAura(this.members);
}
this.members = [];
this.finishedEntities.clear();
this.formationMembersWithAura = [];
this.offsets = undefined;
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
// Hack: switch to a clean state to stop timers.
cmpUnitAI.UnitFsm.SwitchToNextState(cmpUnitAI, "");
Engine.DestroyEntity(this.entity);
};
/**
* Set all members to form up into the formation shape.
* @param {boolean} moveCenter - The formation center will be reinitialized
* to the center of the units.
* @param {boolean} force - All individual orders of the formation units are replaced,
* otherwise the order to walk into formation is just pushed to the front.
* @param {string | undefined} variant - Variant to be passed as order parameter.
*/
Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, variant)
{
if (!this.members.length)
return;
let active = [];
let positions = [];
let rotations = 0;
for (let ent of this.members)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
active.push(ent);
// Query the 2D position as the exact height calculation isn't needed,
// but bring the position to the correct coordinates.
positions.push(cmpPosition.GetPosition2D());
rotations += cmpPosition.GetRotation().y;
}
let avgpos = Vector2D.average(positions);
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
// Reposition the formation if we're told to or if we don't already have a position.
if (moveCenter || (cmpPosition && !cmpPosition.IsInWorld()))
this.SetupPositionAndHandleRotation(avgpos.x, avgpos.y, rotations / active.length);
this.lastOrderVariant = variant;
// Switch between column and box if necessary.
let cmpFormationUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
let walkingDistance = cmpFormationUnitAI.ComputeWalkingDistance();
let columnar = walkingDistance > g_ColumnDistanceThreshold;
if (columnar != this.columnar)
{
this.columnar = columnar;
this.offsets = undefined;
}
let offsetsChanged = false;
let newOrientation = this.GetEstimatedOrientation(avgpos);
if (!this.offsets)
{
this.offsets = this.ComputeFormationOffsets(active, positions);
offsetsChanged = true;
}
let xMax = 0;
let yMax = 0;
let xMin = 0;
let yMin = 0;
if (force)
// Reset finishedEntities as FormationWalk is called.
this.ResetFinishedEntities();
for (let i = 0; i < this.offsets.length; ++i)
{
let offset = this.offsets[i];
let cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI);
if (!cmpUnitAI)
{
warn("Entities without UnitAI in formation are not supported.");
continue;
}
let data =
{
"target": this.entity,
"x": offset.x,
"z": offset.y,
"offsetsChanged": offsetsChanged,
"variant": variant
};
cmpUnitAI.AddOrder("FormationWalk", data, !force);
xMax = Math.max(xMax, offset.x);
yMax = Math.max(yMax, offset.y);
xMin = Math.min(xMin, offset.x);
yMin = Math.min(yMin, offset.y);
}
this.width = xMax - xMin;
this.depth = yMax - yMin;
};
Formation.prototype.MoveToMembersCenter = function()
{
let positions = [];
let rotations = 0;
for (let ent of this.members)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
positions.push(cmpPosition.GetPosition2D());
rotations += cmpPosition.GetRotation().y;
}
let avgpos = Vector2D.average(positions);
this.SetupPositionAndHandleRotation(avgpos.x, avgpos.y, rotations / positions.length);
};
/**
* Set formation position.
* If formation is not in world at time this is called, set new rotation and flag for range manager.
*/
Formation.prototype.SetupPositionAndHandleRotation = function(x, y, rot)
{
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition)
return;
let wasInWorld = cmpPosition.IsInWorld();
cmpPosition.JumpTo(x, y);
if (wasInWorld)
return;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetEntityFlag(this.entity, "normal", false);
cmpPosition.TurnTo(rot);
};
Formation.prototype.GetAvgFootprint = function(active)
{
let footprints = [];
for (let ent of active)
{
let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
if (cmpFootprint)
footprints.push(cmpFootprint.GetShape());
}
if (!footprints.length)
return { "width": 1, "depth": 1 };
let r = { "width": 0, "depth": 0 };
for (let shape of footprints)
{
if (shape.type == "circle")
{
r.width += shape.radius * 2;
r.depth += shape.radius * 2;
}
else if (shape.type == "square")
{
r.width += shape.width;
r.depth += shape.depth;
}
}
r.width /= footprints.length;
r.depth /= footprints.length;
return r;
};
Formation.prototype.ComputeFormationOffsets = function(active, positions)
{
let separation = this.GetAvgFootprint(active);
separation.width *= this.separationMultiplier.width;
separation.depth *= this.separationMultiplier.depth;
let sortingClasses;
if (this.columnar)
sortingClasses = ["Cavalry", "Infantry"];
else
sortingClasses = this.sortingClasses.slice();
sortingClasses.push("Unknown");
// The entities will be assigned to positions in the formation in
// the same order as the types list is ordered.
let types = {};
for (let i = 0; i < sortingClasses.length; ++i)
types[sortingClasses[i]] = [];
for (let i in active)
{
let cmpIdentity = Engine.QueryInterface(active[i], IID_Identity);
let classes = cmpIdentity.GetClassesList();
let done = false;
for (let c = 0; c < sortingClasses.length; ++c)
{
if (classes.indexOf(sortingClasses[c]) > -1)
{
types[sortingClasses[c]].push({ "ent": active[i], "pos": positions[i] });
done = true;
break;
}
}
if (!done)
types.Unknown.push({ "ent": active[i], "pos": positions[i] });
}
let count = active.length;
let shape = this.template.FormationShape;
let shiftRows = this.shiftRows;
let centerGap = this.centerGap;
let sortingOrder = this.template.SortingOrder;
let offsets = [];
// Choose a sensible size/shape for the various formations, depending on number of units.
let cols;
if (this.columnar)
{
shape = "square";
cols = Math.min(count, 3);
shiftRows = false;
centerGap = 0;
sortingOrder = null;
}
else
{
let depth = Math.sqrt(count / this.widthDepthRatio);
if (this.maxRows && depth > this.maxRows)
depth = this.maxRows;
cols = Math.ceil(count / Math.ceil(depth) + (this.shiftRows ? 0.5 : 0));
if (cols < this.minColumns)
cols = Math.min(count, this.minColumns);
if (this.maxColumns && cols > this.maxColumns && this.maxRows != depth)
cols = this.maxColumns;
}
// Define special formations here.
if (this.template.FormationName == "Scatter")
{
let width = Math.sqrt(count) * (separation.width + separation.depth) * 2.5;
for (let i = 0; i < count; ++i)
{
let obj = new Vector2D(randFloat(0, width), randFloat(0, width));
obj.row = 1;
obj.column = i + 1;
offsets.push(obj);
}
}
// For non-special formations, calculate the positions based on the number of entities.
this.maxColumnsUsed = [];
this.maxRowsUsed = 0;
if (shape != "special")
{
offsets = [];
let r = 0;
let left = count;
// While there are units left, start a new row in the formation.
while (left > 0)
{
// Save the position of the row.
let z = -r * separation.depth;
// Alternate between the left and right side of the center to have a symmetrical distribution.
let side = 1;
let n;
// Determine the number of entities in this row of the formation.
if (shape == "square")
{
n = cols;
if (shiftRows)
n -= r % 2;
}
else if (shape == "triangle")
{
if (shiftRows)
n = r + 1;
else
n = r * 2 + 1;
}
if (!shiftRows && n > left)
n = left;
for (let c = 0; c < n && left > 0; ++c)
{
// Switch sides for the next entity.
side *= -1;
let x;
if (n % 2 == 0)
x = side * (Math.floor(c / 2) + 0.5) * separation.width;
else
x = side * Math.ceil(c / 2) * separation.width;
if (centerGap)
{
// Don't use the center position with a center gap.
if (x == 0)
continue;
x += side * centerGap / 2;
}
let column = Math.ceil(n / 2) + Math.ceil(c / 2) * side;
let r1 = randFloat(-1, 1) * this.sloppiness;
let r2 = randFloat(-1, 1) * this.sloppiness;
offsets.push(new Vector2D(x + r1, z + r2));
offsets[offsets.length - 1].row = r + 1;
offsets[offsets.length - 1].column = column;
left--;
}
++r;
this.maxColumnsUsed[r] = n;
}
this.maxRowsUsed = r;
}
// Make sure the average offset is zero, as the formation is centered around that
// calculating offset distances without a zero average makes no sense, as the formation
// will jump to a different position any time.
let avgoffset = Vector2D.average(offsets);
offsets.forEach(function(o) {o.sub(avgoffset);});
// Sort the available places in certain ways.
// The places first in the list will contain the heaviest units as defined by the order
// of the types list.
if (sortingOrder == "fillFromTheSides")
offsets.sort(function(o1, o2) { return Math.abs(o1.x) < Math.abs(o2.x);});
else if (sortingOrder == "fillToTheCenter")
offsets.sort(function(o1, o2) {
return Math.max(Math.abs(o1.x), Math.abs(o1.y)) < Math.max(Math.abs(o2.x), Math.abs(o2.y));
});
// Query the 2D position of the formation.
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let formationPos = cmpPosition.GetPosition2D();
// Use realistic place assignment,
// every soldier searches the closest available place in the formation.
let newOffsets = [];
let realPositions = this.GetRealOffsetPositions(offsets, formationPos);
for (let i = sortingClasses.length; i; --i)
{
let t = types[sortingClasses[i - 1]];
if (!t.length)
continue;
let usedOffsets = offsets.splice(-t.length);
let usedRealPositions = realPositions.splice(-t.length);
for (let entPos of t)
{
let closestOffsetId = this.TakeClosestOffset(entPos, usedRealPositions, usedOffsets);
usedRealPositions.splice(closestOffsetId, 1);
newOffsets.push(usedOffsets.splice(closestOffsetId, 1)[0]);
newOffsets[newOffsets.length - 1].ent = entPos.ent;
}
}
return newOffsets;
};
/**
* Search the closest position in the realPositions list to the given entity.
* @param entPos - Object with entity position and entity ID.
* @param realPositions - The world coordinates of the available offsets.
* @param offsets
* @return The index of the closest offset position.
*/
Formation.prototype.TakeClosestOffset = function(entPos, realPositions, offsets)
{
let pos = entPos.pos;
let closestOffsetId = -1;
let offsetDistanceSq = Infinity;
for (let i = 0; i < realPositions.length; ++i)
{
let distSq = pos.distanceToSquared(realPositions[i]);
if (distSq < offsetDistanceSq)
{
offsetDistanceSq = distSq;
closestOffsetId = i;
}
}
this.memberPositions[entPos.ent] = { "row": offsets[closestOffsetId].row, "column": offsets[closestOffsetId].column };
return closestOffsetId;
};
/**
* Get the world positions for a list of offsets in this formation.
*/
Formation.prototype.GetRealOffsetPositions = function(offsets, pos)
{
let offsetPositions = [];
let { sin, cos } = this.GetEstimatedOrientation(pos);
// Calculate the world positions.
for (let o of offsets)
offsetPositions.push(new Vector2D(pos.x + o.y * sin + o.x * cos, pos.y + o.y * cos - o.x * sin));
return offsetPositions;
};
/**
* Calculate the estimated rotation of the formation based on the current rotation.
* Return the sine and cosine of the angle.
*/
Formation.prototype.GetEstimatedOrientation = function(pos)
{
let r = {};
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition)
return r;
let rot = cmpPosition.GetRotation().y;
r.sin = Math.sin(rot);
r.cos = Math.cos(rot);
return r;
};
/**
* Set formation controller's speed based on its current members.
*/
Formation.prototype.ComputeMotionParameters = function()
{
let maxRadius = 0;
let minSpeed = Infinity;
+ let minAcceleration = Infinity;
for (let ent of this.members)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
+ {
minSpeed = Math.min(minSpeed, cmpUnitMotion.GetWalkSpeed());
+ minAcceleration = Math.min(minAcceleration, cmpUnitMotion.GetAcceleration());
+ }
}
minSpeed *= this.GetSpeedMultiplier();
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.SetSpeedMultiplier(minSpeed / cmpUnitMotion.GetWalkSpeed());
+ cmpUnitMotion.SetAcceleration(minAcceleration);
};
Formation.prototype.ShapeUpdate = function()
{
if (!this.rearrange)
return;
// Check the distance to twin formations, and merge if
// the formations could collide.
for (let i = this.twinFormations.length - 1; i >= 0; --i)
{
// Only do the check on one side.
if (this.twinFormations[i] <= this.entity)
continue;
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let cmpOtherPosition = Engine.QueryInterface(this.twinFormations[i], IID_Position);
let cmpOtherFormation = Engine.QueryInterface(this.twinFormations[i], IID_Formation);
if (!cmpPosition || !cmpOtherPosition || !cmpOtherFormation ||
!cmpPosition.IsInWorld() || !cmpOtherPosition.IsInWorld())
continue;
let thisPosition = cmpPosition.GetPosition2D();
let otherPosition = cmpOtherPosition.GetPosition2D();
let dx = thisPosition.x - otherPosition.x;
let dy = thisPosition.y - otherPosition.y;
let dist = Math.sqrt(dx * dx + dy * dy);
let thisSize = this.GetSize();
let otherSize = cmpOtherFormation.GetSize();
let minDist = Math.max(thisSize.width / 2, thisSize.depth / 2) +
Math.max(otherSize.width / 2, otherSize.depth / 2) +
this.formationSeparation;
if (minDist < dist)
continue;
// Merge the members from the twin formation into this one
// twin formations should always have exactly the same orders.
let otherMembers = cmpOtherFormation.members;
cmpOtherFormation.RemoveMembers(otherMembers);
this.AddMembers(otherMembers);
Engine.DestroyEntity(this.twinFormations[i]);
this.twinFormations.splice(i, 1);
}
// Switch between column and box if necessary.
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
let walkingDistance = cmpUnitAI.ComputeWalkingDistance();
let columnar = walkingDistance > g_ColumnDistanceThreshold;
if (columnar != this.columnar)
{
this.offsets = undefined;
this.columnar = columnar;
// Disable moveCenter so we can't get stuck in a loop of switching
// shape causing center to change causing shape to switch back.
this.MoveMembersIntoFormation(false, true, this.lastOrderVariant);
}
};
Formation.prototype.ResetOrderVariant = function()
{
this.lastOrderVariant = undefined;
};
Formation.prototype.OnGlobalOwnershipChanged = function(msg)
{
// When an entity is captured or destroyed, it should no longer be
// controlled by this formation.
if (this.members.indexOf(msg.entity) != -1)
this.RemoveMembers([msg.entity]);
};
Formation.prototype.OnGlobalEntityRenamed = function(msg)
{
if (this.members.indexOf(msg.entity) === -1)
return;
if (this.finishedEntities.delete(msg.entity))
this.finishedEntities.add(msg.newentity);
// Save rearranging to temporarily set it to false.
let temp = this.rearrange;
this.rearrange = false;
// First remove the old member to be able to reuse its position.
this.RemoveMembers([msg.entity], true);
this.AddMembers([msg.newentity]);
this.memberPositions[msg.newentity] = this.memberPositions[msg.entity];
this.rearrange = temp;
};
Formation.prototype.RegisterTwinFormation = function(entity)
{
let cmpFormation = Engine.QueryInterface(entity, IID_Formation);
if (!cmpFormation)
return;
this.twinFormations.push(entity);
cmpFormation.twinFormations.push(this.entity);
};
Formation.prototype.DeleteTwinFormations = function()
{
for (let ent of this.twinFormations)
{
let cmpFormation = Engine.QueryInterface(ent, IID_Formation);
if (cmpFormation)
cmpFormation.twinFormations.splice(cmpFormation.twinFormations.indexOf(this.entity), 1);
}
this.twinFormations = [];
};
Formation.prototype.LoadFormation = function(newTemplate)
{
const newFormation = ChangeEntityTemplate(this.entity, newTemplate);
return Engine.QueryInterface(newFormation, IID_UnitAI);
};
Formation.prototype.OnEntityRenamed = function(msg)
{
const members = clone(this.members);
this.Disband();
Engine.QueryInterface(msg.newentity, IID_Formation).SetMembers(members);
};
Engine.RegisterComponentType(IID_Formation, "Formation", Formation);
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 25953)
@@ -1,2153 +1,2154 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronized for the biggest part,
// so most of the attributes shouldn't be serialized.
// Return an object with a small selection of deterministic data.
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
this.templateModified = {};
this.selectionDirty = {};
this.obstructionSnap = new ObstructionSnap();
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
// Work out which phase we are in.
let phase = "";
let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"resourceGatherers": cmpPlayer.GetResourceGatherers(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": cmpPlayer.CanBarter(),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = cmpTerrain.GetMapSize();
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function()
{
let ret = this.GetSimulationState();
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
}
return ret;
};
/**
* Returns the gamesettings that were chosen at the time the match started.
*/
GuiInterface.prototype.GetInitAttributes = function()
{
return InitAttributes;
};
/**
* This data will be stored in the replay metadata file after a match has been finished recording.
*/
GuiInterface.prototype.GetReplayMetadata = function()
{
let extendedSimState = this.GetExtendedSimulationState();
return {
"timeElapsed": extendedSimState.timeElapsed,
"playerStates": extendedSimState.players,
"mapSettings": InitAttributes.settings
};
};
/**
* Called when the game ends if the current game is part of a campaign run.
*/
GuiInterface.prototype.GetCampaignGameEndData = function(player)
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
if (Trigger.prototype.OnCampaignGameEnd)
return Trigger.prototype.OnCampaignGameEnd();
return {};
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui.
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
if (!ent)
return null;
// All units must have a template; if not then it's a nonexistent entity id.
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"player": INVALID_PLAYER,
"template": template
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable(),
"hasSomeFormation": cmpIdentity.HasSomeFormation(),
"formations": cmpIdentity.GetFormationsList(),
"controllable": cmpIdentity.IsControllable()
};
const cmpFormation = Engine.QueryInterface(ent, IID_Formation);
if (cmpFormation)
ret.formation = {
"members": cmpFormation.GetMembers()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
ret.position = cmpPosition.GetPosition();
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured();
ret.needsHeal = !cmpHealth.IsUnhealable();
}
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
let cmpMarket = QueryMiragedInterface(ent, IID_Market);
if (cmpMarket)
ret.market = {
"land": cmpMarket.HasType("land"),
"naval": cmpMarket.HasType("naval")
};
let cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress()
};
let cmpPopulation = Engine.QueryInterface(ent, IID_Population);
if (cmpPopulation)
ret.population = {
"bonus": cmpPopulation.GetPopBonus()
};
let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
ret.upgrade = {
"upgrades": cmpUpgrade.GetUpgrades(),
"progress": cmpUpgrade.GetProgress(),
"template": cmpUpgrade.GetUpgradingTo(),
"isUpgrading": cmpUpgrade.IsUpgrading()
};
let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
if (cmpStatusEffects)
ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"queue": cmpProductionQueue.GetQueue(),
"autoqueue": cmpProductionQueue.IsAutoQueueing()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods()
};
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"numBuilders": cmpFoundation.GetNumBuilders(),
"buildTime": cmpFoundation.GetBuildTime()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = {
"numBuilders": cmpRepairable.GetNumBuilders(),
"buildTime": cmpRepairable.GetBuildTime()
};
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"occupiedSlots": cmpGarrisonHolder.OccupiedSlots()
};
let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
if (cmpTurretHolder)
ret.turretHolder = {
"turretPoints": cmpTurretHolder.GetTurretPoints()
};
let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
if (cmpTurretable)
ret.turretable = {
"ejectable": cmpTurretable.IsEjectable(),
"holder": cmpTurretable.HolderID()
};
let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (cmpGarrisonable)
ret.garrisonable = {
"holder": cmpGarrisonable.HolderID(),
"size": cmpGarrisonable.UnitSize()
};
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"canPatrol": cmpUnitAI.CanPatrol(),
"selectableStances": cmpUnitAI.GetSelectableStances(),
"isIdle": cmpUnitAI.IsIdle(),
"formation": cmpUnitAI.GetFormationController()
};
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities()
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
}
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked()
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = true;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = {};
Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type));
ret.attack[type].attackName = cmpAttack.GetAttackName(type);
ret.attack[type].splash = cmpAttack.GetSplashData(type);
if (ret.attack[type].splash)
Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true));
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// Not a ranged attack, set some defaults.
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
if (cmpPosition && cmpPosition.IsInWorld())
// For units, take the range in front of it, no spread, so angle = 0,
// else, take the average elevation around it: angle = 2 * pi.
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, cmpUnitAI ? 0 : 2 * Math.PI);
else
// Not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
let cmpResistance = QueryMiragedInterface(ent, IID_Resistance);
if (cmpResistance)
ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity");
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter"))
ret.isBarterMarket = true;
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"health": cmpHeal.GetHealth(),
"range": cmpHeal.GetRange().max,
"interval": cmpHeal.GetInterval(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses()
};
let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
if (cmpLoot)
{
ret.loot = cmpLoot.GetResources();
ret.loot.xp = cmpLoot.GetXp();
}
let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
if (cmpResourceTrickle)
ret.resourceTrickle = {
"interval": cmpResourceTrickle.GetInterval(),
"rates": cmpResourceTrickle.GetRates()
};
let cmpTreasure = Engine.QueryInterface(ent, IID_Treasure);
if (cmpTreasure)
ret.treasure = {
"collectTime": cmpTreasure.CollectionTime(),
"resources": cmpTreasure.Resources()
};
let cmpTreasureCollector = Engine.QueryInterface(ent, IID_TreasureCollector);
if (cmpTreasureCollector)
ret.treasureCollector = true;
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
- "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier()
+ "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier(),
+ "acceleration": cmpUnitMotion.GetAcceleration()
};
let cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep);
if (cmpUpkeep)
ret.upkeep = {
"interval": cmpUpkeep.GetInterval(),
"rates": cmpUpkeep.GetRates()
};
return ret;
};
GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
{
return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2 * Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, data)
{
let templateName = data.templateName;
let owner = data.player !== undefined ? data.player : player;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, owner, aurasTemplate);
let auraNames = template.Auras._string.split(/\s+/);
for (let name of auraNames)
{
let auraTemplate = AuraTemplates.Get(name);
if (!auraTemplate)
error("Template " + templateName + " has undefined aura " + name);
else
aurasTemplate[name] = auraTemplate;
}
return GetTemplateDataHelper(template, owner, aurasTemplate);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
/**
* Checks whether the requirements for this technology have been met.
*/
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
/**
* Returns technologies that are being actively researched, along with
* which entity is researching them and how far along the research is.
*/
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech of cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
{
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
ret[tech].timeRemaining = cmpProductionQueue.GetQueue()[0].timeRemaining;
}
else
{
ret[tech].progress = 0;
ret[tech].timeRemaining = 0;
}
}
return ret;
};
/**
* Returns the battle state of the player.
*/
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
/**
* Returns a list of ongoing attacks against the player.
*/
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection);
if (!cmpAttackDetection)
return [];
return cmpAttackDetection.GetIncomingAttacks();
};
/**
* Used to show a red square over GUI elements you can't yet afford.
*/
GuiInterface.prototype.GetNeededResources = function(player, data)
{
let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player);
return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {};
};
/**
* State of the templateData (player dependent): true when some template values have been modified
* and need to be reloaded by the gui.
*/
GuiInterface.prototype.OnTemplateModification = function(msg)
{
this.templateModified[msg.player] = true;
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.IsTemplateModified = function(player)
{
return this.templateModified[player] || false;
};
GuiInterface.prototype.ResetTemplateModified = function()
{
this.templateModified = {};
};
/**
* Some changes may require an update to the selection panel,
* which is cached for efficiency. Inform the GUI it needs reloading.
*/
GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.SetSelectionDirty = function(player)
{
this.selectionDirty[player] = true;
};
GuiInterface.prototype.IsSelectionDirty = function(player)
{
return this.selectionDirty[player] || false;
};
GuiInterface.prototype.ResetSelectionDirty = function()
{
this.selectionDirty = {};
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default.
if (!notification.players)
{
notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
notification.players[0] = -1;
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// Filter on players and time, since the delete timer might be executed with a delay.
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
let cmpPlayer = QueryPlayerIDInterface(wantedPlayer);
if (!cmpPlayer)
return [];
return cmpPlayer.GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
return data.ents.some(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate;
});
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data)
{
let updateEntityColor = (iids, entities) => {
for (let ent of entities)
for (let iid of iids)
{
let cmp = Engine.QueryInterface(ent, iid);
if (cmp)
cmp.UpdateColor();
}
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 1; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i, IID_Player);
if (!cmpPlayer)
continue;
cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors);
if (data.displayDiplomacyColors)
cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]);
updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ?
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] :
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer],
cmpRangeManager.GetEntitiesByPlayer(i));
}
updateEntityColor([IID_Selectable, IID_StatusBars], data.selected);
Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors();
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
// Cache of owner -> color map
let playerColors = {};
for (let ent of cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color.
let owner = INVALID_PLAYER;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r": 1, "g": 1, "b": 1 };
let cmpPlayer = QueryPlayerIDInterface(owner);
if (cmpPlayer)
color = cmpPlayer.GetDisplayedColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER)
continue;
cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
}
};
GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
{
this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return Array.from(this.entsWithAuraAndStatusBars);
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
{
for (let ent of cmd.entities)
{
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them.
for (let ent of this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities.
for (let ent of cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// Entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location).
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner.
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position.
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
// May return undefined if no rally point is set.
pos = cmpRallyPoint.GetPositions()[0];
if (pos)
{
// Only update the position if we changed it (cmd.queued is set).
// Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z.
if ("queued" in cmd)
{
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z));
else
cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z));
}
else if (!cmpRallyPointRenderer.IsSet())
// Rebuild the renderer when not set (when reading saved game or in case of building update).
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z));
cmpRallyPointRenderer.SetDisplayed(true);
// Remember which entities have their rally points displayed so we can hide them again.
this.entsRallyPointsDisplayed.push(ent);
}
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
cmpOwnership.SetOwner(cmd.owner);
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": []
};
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
// Set it to a red shade if this is an invalid location.
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
// Did the start position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// Did the end position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// --------------------------------------------------------------------------------
// Do some entity cache management and check for snapping.
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// We're clearing the preview, clear the entity cache and bail.
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// Keep template data around.
}
return false;
}
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
{
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
pos.MoveOutOfWorld();
}
this.placementWallEntities[tpl].numUsed = 0;
}
// Create cache entries for templates we haven't seen before.
for (let type in wallSet.templates)
{
if (type == "curves")
continue;
let tpl = wallSet.templates[type];
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, { "templateName": tpl }),
};
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
// Prevent division by zero errors further on if the start and end positions are the same.
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
// Value of 0.5 was determined through trial and error.
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5;
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// Clear the single-building preview entity (we'll be rolling our own).
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// Calculate wall placement and position preview entities.
let result = {
"pieces": [],
"cost": { "population": 0, "time": 0 }
};
for (let res of Resources.GetCodes())
result.cost[res] = 0;
let previewEntities = [];
if (end.pos)
// See helpers/Walls.js.
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end);
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group.
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true // Preview only, must not appear in the result.
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle
});
}
if (end.pos)
{
// Analogous to the starting side case above.
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || [];
previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// If we're snapping to a foundation, add an extra preview tower and also set it to the same control group.
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
// Number of entities that are required to build the entire wall, regardless of validity.
let numRequiredPieces = 0;
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// Move piece to right location.
// TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities.
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces.
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region.
// TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta.
let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement.
validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: We should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
for (let res of Resources.GetCodes().concat(["population", "time"]))
result.cost[res] += tplData.cost[res];
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// See if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest.
// (TODO: Break unlikely ties by choosing the lowest entity ID.)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for (let ent of data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (data.snapToEdges)
{
let position = this.obstructionSnap.getPosition(data, template);
if (position)
return position;
}
if (template.BuildRestrictions.PlacementType == "shore")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySoundForPlayer = function(player, data)
{
let playerEntityID = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(player);
let cmpSound = Engine.QueryInterface(playerEntityID, IID_Sound);
if (!cmpSound)
return;
let soundGroup = cmpSound.GetSoundGroup(data.name);
if (soundGroup)
Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, player);
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
/**
* Find any idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
* @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
* @param data.excludeUnits Array of units to exclude.
*
* Returns an array of idle units.
* If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
*/
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let idleUnits = [];
// The general case is that only the 'first' idle unit is required; filtering would examine every unit.
// This loop imitates a grouping/aggregation on the first matching idle class.
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
{
let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
if (!filtered.idle)
continue;
// If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
// By adding to the 'end', there is no pause if the series of units loops.
let bucket = filtered.bucket;
if (bucket == 0 && data.prevUnit && entity <= data.prevUnit)
bucket = data.idleClasses.length;
if (!idleUnits[bucket])
idleUnits[bucket] = [];
idleUnits[bucket].push(entity);
// If enough units have been collected in the first bucket, go ahead and return them.
if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
return idleUnits[0];
}
let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
if (data.limit && reduced.length > data.limit)
return reduced.slice(0, data.limit);
return reduced;
};
/**
* Discover if the player has idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.excludeUnits Array of units to exclude.
*
* Returns a boolean of whether the player has any idle units
*/
GuiInterface.prototype.HasIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
};
/**
* Whether to filter an idle unit
*
* @param unit The unit to filter.
* @param idleclasses Array of class names to include.
* @param excludeUnits Array of units to exclude.
*
* Returns an object with the following fields:
* - idle - true if the unit is considered idle by the filter, false otherwise.
* - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
*/
GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle())
return { "idle": false };
let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable);
if (cmpGarrisonable && cmpGarrisonable.IsGarrisoned())
return { "idle": false };
const cmpTurretable = Engine.QueryInterface(unit, IID_Turretable);
if (cmpTurretable && cmpTurretable.IsTurreted())
return { "idle": false };
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
if (!cmpIdentity)
return { "idle": false };
let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
return { "idle": false };
return { "idle": true, "bucket": bucket };
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
let cmpMarket = QueryMiragedInterface(data.firstMarket, IID_Market);
return cmpMarket && cmpMarket.CalculateTraderGain(data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
else if (!firstMarket)
result = { "type": "set first" };
else if (!secondMarket)
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
else
result = { "type": "set first" };
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for (let ent of data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for (let ent of traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player)
{
let cmpPlayer = QueryPlayerIDInterface(player);
if (!cmpPlayer)
return [];
return cmpPlayer.GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
/**
* List the GuiInterface functions that can be safely called by GUI scripts.
* (GUI scripts are non-deterministic and untrusted, so these functions must be
* appropriately careful. They are called with a first argument "player", which is
* trusted and indicates the player associated with the current client; no data should
* be returned unless this player is meant to be able to see it.)
*/
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetInitAttributes": 1,
"GetReplayMetadata": 1,
"GetCampaignGameEndData": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetMultipleEntityStates": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"UpdateDisplayedPlayerColors": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"AddTargetMarker": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"PlaySoundForPlayer": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"EnableVisualRangeOverlayType": 1,
"SetRangeOverlays": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
"IsTemplateModified": 1,
"ResetTemplateModified": 1,
"IsSelectionDirty": 1,
"ResetSelectionDirty": 1
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
throw new Error("Invalid GuiInterface Call name \"" + name + "\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitMotionFlying.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/UnitMotionFlying.js (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/components/UnitMotionFlying.js (revision 25953)
@@ -1,380 +1,390 @@
// (A serious implementation of this might want to use C++ instead of JS
// for performance; this is just for fun.)
const SHORT_FINAL = 2.5;
function UnitMotionFlying() {}
UnitMotionFlying.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
UnitMotionFlying.prototype.Init = function()
{
this.hasTarget = false;
this.reachedTarget = false;
this.targetX = 0;
this.targetZ = 0;
this.targetMinRange = 0;
this.targetMaxRange = 0;
this.speed = 0;
this.landing = false;
this.onGround = true;
this.pitch = 0;
this.roll = 0;
this.waterDeath = false;
this.passabilityClass = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).GetPassabilityClass(this.template.PassabilityClass);
};
UnitMotionFlying.prototype.OnUpdate = function(msg)
{
let turnLength = msg.turnLength;
if (!this.hasTarget)
return;
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let pos = cmpPosition.GetPosition();
let angle = cmpPosition.GetRotation().y;
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
let ground = Math.max(cmpTerrain.GetGroundLevel(pos.x, pos.z), cmpWaterManager.GetWaterLevel(pos.x, pos.z));
let newangle = angle;
let canTurn = true;
let distanceToTargetSquared = Math.euclidDistance2DSquared(pos.x, pos.z, this.targetX, this.targetZ);
if (this.landing)
{
if (this.speed > 0 && this.onGround)
{
if (pos.y <= cmpWaterManager.GetWaterLevel(pos.x, pos.z) && this.template.DiesInWater == "true")
this.waterDeath = true;
this.pitch = 0;
// Deaccelerate forwards...at a very reduced pace.
if (this.waterDeath)
this.speed = Math.max(0, this.speed - turnLength * this.template.BrakingRate * 10);
else
this.speed = Math.max(0, this.speed - turnLength * this.template.BrakingRate);
canTurn = false;
// Clamp to ground if below it, or descend if above.
if (pos.y < ground)
pos.y = ground;
else if (pos.y > ground)
pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate);
}
else if (this.speed == 0 && this.onGround)
{
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
if (this.waterDeath && cmpHealth)
cmpHealth.Kill();
else
{
this.pitch = 0;
// We've stopped.
if (cmpGarrisonHolder)
cmpGarrisonHolder.AllowGarrisoning(true, "UnitMotionFlying");
canTurn = false;
this.hasTarget = false;
this.landing = false;
// Summon planes back from the edge of the map.
let terrainSize = cmpTerrain.GetMapSize();
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager.GetLosCircular())
{
let mapRadius = terrainSize/2;
let x = pos.x - mapRadius;
let z = pos.z - mapRadius;
let div = (mapRadius - 12) / Math.sqrt(x*x + z*z);
if (div < 1)
{
pos.x = mapRadius + x*div;
pos.z = mapRadius + z*div;
newangle += Math.PI;
distanceToTargetSquared = Math.euclidDistance2DSquared(pos.x, pos.z, this.targetX, this.targetZ);
}
}
else
{
pos.x = Math.max(Math.min(pos.x, terrainSize - 12), 12);
pos.z = Math.max(Math.min(pos.z, terrainSize - 12), 12);
newangle += Math.PI;
distanceToTargetSquared = Math.euclidDistance2DSquared(pos.x, pos.z, this.targetX, this.targetZ);
}
}
}
else
{
// Final Approach.
// We need to slow down to land!
this.speed = Math.max(this.template.LandingSpeed, this.speed - turnLength * this.template.SlowingRate);
canTurn = false;
let targetHeight = ground;
// Steep, then gradual descent.
if ((pos.y - targetHeight) / this.template.FlyingHeight > 1 / SHORT_FINAL)
this.pitch = -Math.PI / 18;
else
this.pitch = Math.PI / 18;
let descentRate = ((pos.y - targetHeight) / this.template.FlyingHeight * this.template.ClimbRate + SHORT_FINAL) * SHORT_FINAL;
if (pos.y < targetHeight)
pos.y = Math.max(targetHeight, pos.y + turnLength * descentRate);
else if (pos.y > targetHeight)
pos.y = Math.max(targetHeight, pos.y - turnLength * descentRate);
if (targetHeight == pos.y)
{
this.onGround = true;
if (targetHeight == cmpWaterManager.GetWaterLevel(pos.x, pos.z) && this.template.DiesInWater)
this.waterDeath = true;
}
}
}
else
{
if (this.template.StationaryDistance && distanceToTargetSquared <= +this.template.StationaryDistance * +this.template.StationaryDistance)
{
cmpPosition.SetXZRotation(0, 0);
this.pitch = 0;
this.roll = 0;
this.reachedTarget = true;
cmpPosition.TurnTo(Math.atan2(this.targetX - pos.x, this.targetZ - pos.z));
Engine.PostMessage(this.entity, MT_MotionUpdate, { "updateString": "likelySuccess" });
return;
}
// If we haven't reached max speed yet then we're still on the ground;
// otherwise we're taking off or flying.
// this.onGround in case of a go-around after landing (but not fully stopped).
if (this.speed < this.template.TakeoffSpeed && this.onGround)
{
if (cmpGarrisonHolder)
cmpGarrisonHolder.AllowGarrisoning(false, "UnitMotionFlying");
this.pitch = 0;
// Accelerate forwards.
this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate);
canTurn = false;
// Clamp to ground if below it, or descend if above.
if (pos.y < ground)
pos.y = ground;
else if (pos.y > ground)
pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate);
}
else
{
this.onGround = false;
// Climb/sink to max height above ground.
this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate);
let targetHeight = ground + (+this.template.FlyingHeight);
if (Math.abs(pos.y-targetHeight) > this.template.FlyingHeight/5)
{
this.pitch = Math.PI / 9;
canTurn = false;
}
else
this.pitch = 0;
if (pos.y < targetHeight)
pos.y = Math.min(targetHeight, pos.y + turnLength * this.template.ClimbRate);
else if (pos.y > targetHeight)
{
pos.y = Math.max(targetHeight, pos.y - turnLength * this.template.ClimbRate);
this.pitch = -1 * this.pitch;
}
}
}
// If we're in range of the target then tell people that we've reached it.
// (TODO: quantisation breaks this)
if (!this.reachedTarget &&
this.targetMinRange * this.targetMinRange <= distanceToTargetSquared &&
distanceToTargetSquared <= this.targetMaxRange * this.targetMaxRange)
{
this.reachedTarget = true;
Engine.PostMessage(this.entity, MT_MotionUpdate, { "updateString": "likelySuccess" });
}
// If we're facing away from the target, and are still fairly close to it,
// then carry on going straight so we overshoot in a straight line.
let isBehindTarget = ((this.targetX - pos.x) * Math.sin(angle) + (this.targetZ - pos.z) * Math.cos(angle) < 0);
// Overshoot the target: carry on straight.
if (isBehindTarget && distanceToTargetSquared < this.template.MaxSpeed * this.template.MaxSpeed * this.template.OvershootTime * this.template.OvershootTime)
canTurn = false;
if (canTurn)
{
// Turn towards the target.
let targetAngle = Math.atan2(this.targetX - pos.x, this.targetZ - pos.z);
let delta = targetAngle - angle;
// Wrap delta to -pi..pi.
delta = (delta + Math.PI) % (2*Math.PI);
if (delta < 0)
delta += 2 * Math.PI;
delta -= Math.PI;
// Clamp to max rate.
let deltaClamped = Math.min(Math.max(delta, -this.template.TurnRate * turnLength), this.template.TurnRate * turnLength);
// Calculate new orientation, in a peculiar way in order to make sure the
// result gets close to targetAngle (rather than being n*2*pi out).
newangle = targetAngle + deltaClamped - delta;
if (newangle - angle > Math.PI / 18)
this.roll = Math.PI / 9;
else if (newangle - angle < -Math.PI / 18)
this.roll = -Math.PI / 9;
else
this.roll = newangle - angle;
}
else
this.roll = 0;
pos.x += this.speed * turnLength * Math.sin(angle);
pos.z += this.speed * turnLength * Math.cos(angle);
cmpPosition.SetHeightFixed(pos.y);
cmpPosition.TurnTo(newangle);
cmpPosition.SetXZRotation(this.pitch, this.roll);
cmpPosition.MoveTo(pos.x, pos.z);
};
UnitMotionFlying.prototype.MoveToPointRange = function(x, z, minRange, maxRange)
{
this.hasTarget = true;
this.landing = false;
this.reachedTarget = false;
this.targetX = x;
this.targetZ = z;
this.targetMinRange = minRange;
this.targetMaxRange = maxRange;
return true;
};
UnitMotionFlying.prototype.MoveToTargetRange = function(target, minRange, maxRange)
{
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return false;
let targetPos = cmpTargetPosition.GetPosition2D();
this.hasTarget = true;
this.reachedTarget = false;
this.targetX = targetPos.x;
this.targetZ = targetPos.y;
this.targetMinRange = minRange;
this.targetMaxRange = maxRange;
return true;
};
UnitMotionFlying.prototype.SetMemberOfFormation = function()
{
// Ignored.
};
UnitMotionFlying.prototype.GetWalkSpeed = function()
{
return +this.template.MaxSpeed;
};
UnitMotionFlying.prototype.SetSpeedMultiplier = function(multiplier)
{
// Ignore this, the speed is always the walk speed.
};
UnitMotionFlying.prototype.GetRunMultiplier = function()
{
return 1;
};
/**
* Estimate the next position of the unit. Just linearly extrapolate.
* TODO: Reuse the movement code for a better estimate.
*/
UnitMotionFlying.prototype.EstimateFuturePosition = function(dt)
{
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return Vector2D();
let position = cmpPosition.GetPosition2D();
return Vector2D.add(position, Vector2D.sub(position, cmpPosition.GetPreviousPosition2D()).mult(dt/Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetLatestTurnLength()));
};
UnitMotionFlying.prototype.IsMoveRequested = function()
{
return this.hasTarget;
};
UnitMotionFlying.prototype.GetCurrentSpeed = function()
{
return this.speed;
};
UnitMotionFlying.prototype.GetSpeedMultiplier = function()
{
return this.speed / +this.template.MaxSpeed;
};
+UnitMotionFlying.prototype.GetAcceleration = function()
+{
+ return +this.template.AccelRate;
+};
+
+UnitMotionFlying.prototype.SetAcceleration = function()
+{
+ // Acceleration is set by the template. Ignore.
+};
+
UnitMotionFlying.prototype.GetPassabilityClassName = function()
{
return this.template.PassabilityClass;
};
UnitMotionFlying.prototype.GetPassabilityClass = function()
{
return this.passabilityClass;
};
UnitMotionFlying.prototype.FaceTowardsPoint = function(x, z)
{
// Ignore this - angle is controlled by the target-seeking code instead.
};
UnitMotionFlying.prototype.SetFacePointAfterMove = function()
{
// Ignore this - angle is controlled by the target-seeking code instead.
};
UnitMotionFlying.prototype.StopMoving = function()
{
// Invert.
if (!this.waterDeath)
this.landing = !this.landing;
};
UnitMotionFlying.prototype.SetDebugOverlay = function(enabled)
{
};
Engine.RegisterComponentType(IID_UnitMotion, "UnitMotionFlying", UnitMotionFlying);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js (revision 25953)
@@ -1,528 +1,533 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Position.js");
Engine.LoadHelperScript("Sound.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/Formation.js");
Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Pack.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Formation.js");
Engine.LoadComponentScript("UnitAI.js");
/**
* Fairly straightforward test that entity renaming is handled
* by unitAI states. These ought to be augmented with integration tests, ideally.
*/
function TestTargetEntityRenaming(init_state, post_state, setup)
{
ResetState();
const player_ent = 5;
const target_ent = 6;
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetInterval": () => {},
"SetTimeout": () => {}
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": () => false
});
let unitAI = ConstructComponent(player_ent, "UnitAI", {
"FormationController": "false",
"DefaultStance": "aggressive",
"FleeDistance": 10
});
unitAI.OnCreate();
setup(unitAI, player_ent, target_ent);
TS_ASSERT_EQUALS(unitAI.GetCurrentState(), init_state);
unitAI.OnGlobalEntityRenamed({
"entity": target_ent,
"newentity": target_ent + 1
});
TS_ASSERT_EQUALS(unitAI.GetCurrentState(), post_state);
}
TestTargetEntityRenaming(
"INDIVIDUAL.GARRISON.APPROACHING", "INDIVIDUAL.IDLE",
(unitAI, player_ent, target_ent) => {
unitAI.CanGarrison = (target) => target == target_ent;
unitAI.MoveToTargetRange = (target) => target == target_ent;
unitAI.AbleToMove = () => true;
unitAI.Garrison(target_ent, false);
}
);
TestTargetEntityRenaming(
"INDIVIDUAL.REPAIR.REPAIRING", "INDIVIDUAL.REPAIR.REPAIRING",
(unitAI, player_ent, target_ent) => {
AddMock(player_ent, IID_Builder, {
"StartRepairing": () => true,
"StopRepairing": () => {}
});
QueryBuilderListInterface = () => {};
unitAI.CheckTargetRange = () => true;
unitAI.CanRepair = (target) => target == target_ent;
unitAI.Repair(target_ent, false, false);
}
);
TestTargetEntityRenaming(
"INDIVIDUAL.FLEEING", "INDIVIDUAL.FLEEING",
(unitAI, player_ent, target_ent) => {
PositionHelper.DistanceBetweenEntities = () => 10;
unitAI.CheckTargetRangeExplicit = () => false;
AddMock(player_ent, IID_UnitMotion, {
"MoveToTargetRange": () => true,
"GetRunMultiplier": () => 1,
"SetSpeedMultiplier": () => {},
+ "GetAcceleration": () => 1,
"StopMoving": () => {}
});
unitAI.Flee(target_ent, false);
}
);
/* Regression test.
* Tests the FSM behaviour of a unit when walking as part of a formation,
* then exiting the formation.
* mode == 0: There is no enemy unit nearby.
* mode == 1: There is a live enemy unit nearby.
* mode == 2: There is a dead enemy unit nearby.
*/
function TestFormationExiting(mode)
{
ResetState();
var playerEntity = 5;
var unit = 10;
var enemy = 20;
var controller = 30;
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetInterval": function() { },
"SetTimeout": function() { },
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"CreateActiveQuery": function(ent, minRange, maxRange, players, iid, flags, accountForSize) {
return 1;
},
"EnableActiveQuery": function(id) { },
"ResetActiveQuery": function(id) { if (mode == 0) return []; return [enemy]; },
"DisableActiveQuery": function(id) { },
"GetEntityFlagMask": function(identifier) { },
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; },
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": function(id) { return playerEntity; },
"GetNumPlayers": function() { return 2; },
});
AddMock(playerEntity, IID_Player, {
"IsAlly": function() { return false; },
"IsEnemy": function() { return true; },
"GetEnemies": function() { return [2]; },
});
var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" });
AddMock(unit, IID_Identity, {
"GetClassesList": function() { return []; },
});
AddMock(unit, IID_Ownership, {
"GetOwner": function() { return 1; },
});
AddMock(unit, IID_Position, {
"GetTurretParent": function() { return INVALID_ENTITY; },
"GetPosition": function() { return new Vector3D(); },
"GetPosition2D": function() { return new Vector2D(); },
"GetRotation": function() { return { "y": 0 }; },
"IsInWorld": function() { return true; },
});
AddMock(unit, IID_UnitMotion, {
"GetWalkSpeed": () => 1,
+ "GetAcceleration": () => 1,
"MoveToFormationOffset": (target, x, z) => {},
"MoveToTargetRange": (target, min, max) => true,
"SetMemberOfFormation": () => {},
"StopMoving": () => {},
"SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true,
"GetPassabilityClassName": () => "default"
});
AddMock(unit, IID_Vision, {
"GetRange": function() { return 10; },
});
AddMock(unit, IID_Attack, {
"GetRange": function() { return { "max": 10, "min": 0 }; },
"GetFullAttackRange": function() { return { "max": 40, "min": 0 }; },
"GetBestAttackAgainst": function(t) { return "melee"; },
"GetPreference": function(t) { return 0; },
"GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; },
"CanAttack": function(v) { return true; },
"CompareEntitiesByPreference": function(a, b) { return 0; },
"IsTargetInRange": () => true,
"StartAttacking": () => true
});
unitAI.OnCreate();
unitAI.SetupAttackRangeQuery(1);
if (mode == 1)
{
AddMock(enemy, IID_Health, {
"GetHitpoints": function() { return 10; },
});
AddMock(enemy, IID_UnitAI, {
"IsAnimal": () => "false",
"IsDangerousAnimal": () => "false"
});
}
else if (mode == 2)
AddMock(enemy, IID_Health, {
"GetHitpoints": function() { return 0; },
});
let controllerFormation = ConstructComponent(controller, "Formation", {
"FormationName": "Line Closed",
"FormationShape": "square",
"ShiftRows": "false",
"SortingClasses": "",
"WidthDepthRatio": 1,
"UnitSeparationWidthMultiplier": 1,
"UnitSeparationDepthMultiplier": 1,
"SpeedMultiplier": 1,
"Sloppiness": 0
});
let controllerAI = ConstructComponent(controller, "UnitAI", {
"FormationController": "true",
"DefaultStance": "aggressive"
});
AddMock(controller, IID_Position, {
"JumpTo": function(x, z) { this.x = x; this.z = z; },
"GetTurretParent": function() { return INVALID_ENTITY; },
"GetPosition": function() { return new Vector3D(this.x, 0, this.z); },
"GetPosition2D": function() { return new Vector2D(this.x, this.z); },
"GetRotation": function() { return { "y": 0 }; },
"IsInWorld": function() { return true; },
"MoveOutOfWorld": () => {}
});
AddMock(controller, IID_UnitMotion, {
"GetWalkSpeed": () => 1,
"StopMoving": () => {},
"SetSpeedMultiplier": () => {},
+ "SetAcceleration": (accel) => {},
"MoveToPointRange": () => true,
"SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true,
"GetPassabilityClassName": () => "default"
});
controllerAI.OnCreate();
TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.IDLE");
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
controllerFormation.SetMembers([unit]);
controllerAI.Walk(100, 100, false);
TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.WALKING");
TS_ASSERT_EQUALS(unitAI.fsmStateName, "FORMATIONMEMBER.WALKING");
controllerFormation.Disband();
unitAI.UnitFsm.ProcessMessage(unitAI, { "type": "Timer" });
if (mode == 0)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
else if (mode == 1)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
else if (mode == 2)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
else
TS_FAIL("invalid mode");
}
function TestMoveIntoFormationWhileAttacking()
{
ResetState();
var playerEntity = 5;
var controller = 10;
var enemy = 20;
var unit = 30;
var units = [];
var unitCount = 8;
var unitAIs = [];
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetInterval": function() { },
"SetTimeout": function() { },
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"CreateActiveQuery": function(ent, minRange, maxRange, players, iid, flags, accountForSize) {
return 1;
},
"EnableActiveQuery": function(id) { },
"ResetActiveQuery": function(id) { return [enemy]; },
"DisableActiveQuery": function(id) { },
"GetEntityFlagMask": function(identifier) { },
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; },
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": function(id) { return playerEntity; },
"GetNumPlayers": function() { return 2; },
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": (ent, target, min, max) => true
});
AddMock(playerEntity, IID_Player, {
"IsAlly": function() { return false; },
"IsEnemy": function() { return true; },
"GetEnemies": function() { return [2]; },
});
// create units
for (var i = 0; i < unitCount; i++)
{
units.push(unit + i);
var unitAI = ConstructComponent(unit + i, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" });
AddMock(unit + i, IID_Identity, {
"GetClassesList": function() { return []; },
});
AddMock(unit + i, IID_Ownership, {
"GetOwner": function() { return 1; },
});
AddMock(unit + i, IID_Position, {
"GetTurretParent": function() { return INVALID_ENTITY; },
"GetPosition": function() { return new Vector3D(); },
"GetPosition2D": function() { return new Vector2D(); },
"GetRotation": function() { return { "y": 0 }; },
"IsInWorld": function() { return true; },
});
AddMock(unit + i, IID_UnitMotion, {
"GetWalkSpeed": () => 1,
+ "GetAcceleration": () => 1,
"MoveToFormationOffset": (target, x, z) => {},
"MoveToTargetRange": (target, min, max) => true,
"SetMemberOfFormation": () => {},
"StopMoving": () => {},
"SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true,
"GetPassabilityClassName": () => "default"
});
AddMock(unit + i, IID_Vision, {
"GetRange": function() { return 10; },
});
AddMock(unit + i, IID_Attack, {
"GetRange": function() { return { "max": 10, "min": 0 }; },
"GetFullAttackRange": function() { return { "max": 40, "min": 0 }; },
"GetBestAttackAgainst": function(t) { return "melee"; },
"GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; },
"CanAttack": function(v) { return true; },
"CompareEntitiesByPreference": function(a, b) { return 0; },
"IsTargetInRange": () => true,
"StartAttacking": () => true,
"StopAttacking": () => {}
});
unitAI.OnCreate();
unitAI.SetupAttackRangeQuery(1);
unitAIs.push(unitAI);
}
// create enemy
AddMock(enemy, IID_Health, {
"GetHitpoints": function() { return 40; },
});
let controllerFormation = ConstructComponent(controller, "Formation", {
"FormationName": "Line Closed",
"FormationShape": "square",
"ShiftRows": "false",
"SortingClasses": "",
"WidthDepthRatio": 1,
"UnitSeparationWidthMultiplier": 1,
"UnitSeparationDepthMultiplier": 1,
"SpeedMultiplier": 1,
"Sloppiness": 0
});
let controllerAI = ConstructComponent(controller, "UnitAI", {
"FormationController": "true",
"DefaultStance": "aggressive"
});
AddMock(controller, IID_Position, {
"GetTurretParent": () => INVALID_ENTITY,
"JumpTo": function(x, z) { this.x = x; this.z = z; },
"GetPosition": function(){ return new Vector3D(this.x, 0, this.z); },
"GetPosition2D": function(){ return new Vector2D(this.x, this.z); },
"GetRotation": () => ({ "y": 0 }),
"IsInWorld": () => true,
"MoveOutOfWorld": () => {},
});
AddMock(controller, IID_UnitMotion, {
"GetWalkSpeed": () => 1,
"SetSpeedMultiplier": (speed) => {},
+ "SetAcceleration": (accel) => {},
"MoveToPointRange": (x, z, minRange, maxRange) => {},
"StopMoving": () => {},
"SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true,
"GetPassabilityClassName": () => "default"
});
AddMock(controller, IID_Attack, {
"GetRange": function() { return { "max": 10, "min": 0 }; },
"CanAttackAsFormation": function() { return false; },
});
controllerAI.OnCreate();
controllerFormation.SetMembers(units);
controllerAI.Attack(enemy, []);
for (let ent of unitAIs)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
controllerAI.MoveIntoFormation({ "name": "Circle" });
// let all units be in position
for (let ent of unitAIs)
controllerFormation.SetFinishedEntity(ent);
for (let ent of unitAIs)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
controllerFormation.Disband();
}
TestFormationExiting(0);
TestFormationExiting(1);
TestFormationExiting(2);
TestMoveIntoFormationWhileAttacking();
function TestWalkAndFightTargets()
{
const ent = 10;
let unitAI = ConstructComponent(ent, "UnitAI", {
"FormationController": "false",
"DefaultStance": "aggressive",
"FleeDistance": 10
});
unitAI.OnCreate();
unitAI.losAttackRangeQuery = true;
// The result is stored here
let result;
unitAI.PushOrderFront = function(type, order)
{
if (type === "Attack" && order?.target)
result = order.target;
};
// Create some targets.
AddMock(ent+1, IID_UnitAI, { "IsAnimal": () => true, "IsDangerousAnimal": () => false });
AddMock(ent+2, IID_Ownership, { "GetOwner": () => 2 });
AddMock(ent+3, IID_Ownership, { "GetOwner": () => 2 });
AddMock(ent+4, IID_Ownership, { "GetOwner": () => 2 });
AddMock(ent+5, IID_Ownership, { "GetOwner": () => 2 });
AddMock(ent+6, IID_Ownership, { "GetOwner": () => 2 });
AddMock(ent+7, IID_Ownership, { "GetOwner": () => 2 });
unitAI.CanAttack = function(target)
{
return target !== ent+2 && target !== ent+7;
};
AddMock(ent, IID_Attack, {
"GetPreference": (target) => ({
[ent+4]: 0,
[ent+5]: 1,
[ent+6]: 2,
[ent+7]: 0
}?.[target])
});
let runTest = function(ents, res)
{
result = undefined;
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ResetActiveQuery": () => ents
});
TS_ASSERT_EQUALS(unitAI.FindWalkAndFightTargets(), !!res);
TS_ASSERT_EQUALS(result, res);
};
// No entities.
runTest([]);
// Entities that cannot be attacked.
runTest([ent+1, ent+2, ent+7]);
// No preference, one attackable entity.
runTest([ent+1, ent+2, ent+3], ent+3);
// Check preferences.
runTest([ent+1, ent+2, ent+3, ent+4], ent+4);
runTest([ent+1, ent+2, ent+3, ent+4, ent+5], ent+4);
runTest([ent+1, ent+2, ent+6, ent+3, ent+4, ent+5], ent+4);
runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+4, ent+5], ent+4);
runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+5], ent+5);
runTest([ent+1, ent+2, ent+7, ent+6, ent+3], ent+6);
runTest([ent+1, ent+2, ent+7, ent+3], ent+3);
}
TestWalkAndFightTargets();
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml (revision 25953)
@@ -1,34 +1,35 @@
5.550DromedaryCamelus dromedariusgaia/fauna_camel.png200actor/fauna/movement/camel_order.xmlactor/fauna/death/death_camel.xml6.50.45
+ 0.45fauna/camel.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml (revision 25953)
@@ -1,49 +1,50 @@
1.55ChickenGallus gallus domesticusgaia/fauna_chicken.pngfalseupright405actor/fauna/animal/chickens.xmlactor/fauna/animal/chickens.xml2.54.012.02000800010000400000.15
+ 0.15fauna/chicken.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_crocodile_nile.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_crocodile_nile.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_crocodile_nile.xml (revision 25953)
@@ -1,54 +1,55 @@
Teeth30710002000Structure Ship Siege2.0100Nile CrocodileCrocodylus niloticusgaia/fauna_crocodile.pngpitch-roll128x512/ellipse.png128x512/ellipse_mask.pngactor/fauna/animal/lion_attack.xmlactor/fauna/animal/lion_death.xml3.00.3
+ 0.3fauna/crocodile.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_donkey.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_donkey.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_donkey.xml (revision 25953)
@@ -1,35 +1,36 @@
2.550DonkeyEquus africanus asinusgaia/fauna_donkey.png200actor/fauna/animal/horse_order.xmlactor/fauna/animal/horse_death.xmlactor/fauna/animal/horse_trained.xml3.50.8
+ 0.8fauna/donkey.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml (revision 25953)
@@ -1,27 +1,28 @@
13.080GiraffeGiraffa camelopardalisgaia/fauna_giraffe.png35014.00.6
+ 0.6fauna/giraffe_adult.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml (revision 25953)
@@ -1,27 +1,28 @@
7.540Juvenile GiraffeGiraffa camelopardalisgaia/fauna_giraffe.png1508.50.6
+ 0.6fauna/giraffe_baby.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_goat.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_goat.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_goat.xml (revision 25953)
@@ -1,42 +1,43 @@
30352.035GoatCapra aegagrus hircusgaia/fauna_goat.png702actor/fauna/animal/goat_order.xmlactor/fauna/death/goat.xmlactor/fauna/animal/goat_trained.xml3.00.45
+ 0.45fauna/goat.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml (revision 25953)
@@ -1,35 +1,36 @@
4.050HorseEquus ferus caballusgaia/fauna_horse.png200actor/fauna/animal/horse_order.xmlactor/fauna/animal/horse_death.xmlactor/fauna/animal/horse_trained.xml5.00.8
+ 0.8fauna/horse.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_lion.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_lion.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_lion.xml (revision 25953)
@@ -1,43 +1,44 @@
Paws20310002000Structure Ship Siege3.050LionPanthera leogaia/fauna_lion.pngactor/fauna/animal/lion_attack.xmlactor/fauna/animal/lion_death.xml4.00.45
+ 0.45fauna/lion.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml (revision 25953)
@@ -1,47 +1,48 @@
1.510PeacockPavo cristatusgaia/fauna_peacock.pngupright505actor/fauna/animal/peacock_order.xmlactor/fauna/animal/peacock_call.xmlactor/fauna/animal/peacock_trained.xml2.54.012.02000800010000400000.3
+ 0.3fauna/peacock.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig.xml (revision 25953)
@@ -1,42 +1,43 @@
50751.575PigSus scrofa domesticusgaia/fauna_pig.png1504actor/fauna/animal/pig_order.xmlactor/fauna/animal/pig.xmlactor/fauna/animal/pig_trained.xml2.50.45
+ 0.45fauna/pig1.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig_flaming.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig_flaming.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig_flaming.xml (revision 25953)
@@ -1,37 +1,38 @@
10.00.050.1corpse-20.800.500.20Flaming Pigunits/flaming_pig.pngactor/fauna/animal/pig_flaming_order.xmlactor/fauna/animal/pig_death.xmlactor/fauna/animal/pig_flaming_trained.xml3.5
+ 3.5fauna/pig_flaming.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_piglet.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_piglet.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_piglet.xml (revision 25953)
@@ -1,26 +1,27 @@
0.515Piglet1011.50.25
+ 0.25fauna/piglet.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_shark.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_shark.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_shark.xml (revision 25953)
@@ -1,55 +1,56 @@
2.5150trueGreat White SharkCarcharodon carchariasSeaCreaturegaia/fauna_shark.pngfalsefalse-1uprighttrue05128x512/ellipse.png128x512/ellipse_mask.png100.060.010000030000012ship-small0.6
+ 0.6falsefauna/shark.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_sheep.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_sheep.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_sheep.xml (revision 25953)
@@ -1,42 +1,43 @@
40502.050SheepOvis ariesgaia/fauna_sheep.png1003actor/fauna/animal/sheep_order.xmlactor/fauna/animal/sheep.xmlactor/fauna/animal/sheep_trained.xml3.00.45
+ 0.45fauna/sheep3.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml (revision 25953)
@@ -1,27 +1,28 @@
3.050Blue WildebeestConnochaetes taurinusgaia/fauna_wildebeest.png1504.00.9
+ 0.9fauna/wildebeest.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml (revision 25953)
@@ -1,27 +1,28 @@
2.550Common ZebraEquus quaggagaia/fauna_zebra.png1503.50.9
+ 0.9fauna/zebra.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_formation.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_formation.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_formation.xml (revision 25953)
@@ -1,76 +1,78 @@
2Requires at least 2 Soldiers or Siege Engines.1squareHero Champion Cavalry Melee Rangedfalse1110falsetruefalsefalsefalsefalsefalsefalsefalse0uprightfalse0100.75aggressivetrue12.0truetrue2true
- 1.0
- 100.0
+ 1
+ 100
+ 0.1
+ 100large
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml (revision 25953)
@@ -1,146 +1,148 @@
110000falsefalse30.00.010.04.01corpse10000falsegaiaUnit Organic ConquestCriticalUnit
special/formations/null
special/formations/box
special/formations/column_closed
special/formations/line_closed
special/formations/column_open
special/formations/line_open
special/formations/flank
special/formations/battle_line
malefalseunittruetruefalsefalsetruefalsefalsefalse0pitchfalse0.014111128x128/ellipse.png128x128/ellipse_mask.pnginterface/alarm/alarm_attackplayer.xmlinterface/alarm/alarm_attacked_gaia.xmlinterface/alarm/alarm_attackplayer.xmlinterface/alarm/alarm_attacked_gaia.xmlvoice/{lang}/civ/civ_{phenotype}_collect_treasure.xml2.00.3335.02aggressive12.0falsetruetrue12800falsedefault
- 9.0
+ 91.67
+ 1.5
+ 18falsefalsefalsefalse12falsetruefalsefalse
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_archer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_archer.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_archer.xml (revision 25953)
@@ -1,40 +1,41 @@
Bow76080010001002.550falseHuman50ArcherCavalry Archerattack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml0.85
+ 0.85
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_javelineer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_javelineer.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_javelineer.xml (revision 25953)
@@ -1,40 +1,41 @@
Javelin1830400125070435falseHuman50Cavalry JavelineerJavelineerattack/weapon/javelin_attack.xmlattack/impact/javelin_impact.xml0.9
+ 0.9
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_crossbowman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_crossbowman.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_crossbowman.xml (revision 25953)
@@ -1,46 +1,47 @@
Crossbow406020030001200.830falseHumanChampion Cavalry CrossbowmanRanged Crossbowman
special/formations/skirmish
-2-2attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml0.8
+ 0.8
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_elephant.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_elephant.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_elephant.xml (revision 25953)
@@ -1,62 +1,63 @@
3363002009.01000War ElephantElephant300302047520actor/fauna/animal/elephant_order.xmlactor/fauna/animal/elephant_attack_order.xmlactor/fauna/animal/elephant_order.xmlactor/fauna/animal/elephant_order.xmlactor/fauna/animal/elephant_attack.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlactor/fauna/animal/elephant_death.xmlactor/fauna/animal/elephant_trained.xml10.0large
+ 0.5100
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelineer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelineer.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelineer.xml (revision 25953)
@@ -1,50 +1,51 @@
Javelin32304001250701.635falseHuman120Champion Infantry JavelineerRanged Javelineer
special/formations/skirmish
-2-2attack/weapon/javelin_attack.xmlattack/impact/javelin_impact.xml1.2
+ 1.2
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_elephant.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_elephant.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_elephant.xml (revision 25953)
@@ -1,61 +1,62 @@
9.0War ElephantBasicHuman CitizenSoldierCitizen Soldier Elephant26041504315128x256/ellipse.png128x256/ellipse_mask.pngactor/fauna/animal/elephant_order.xmlactor/fauna/animal/elephant_attack_order.xmlactor/fauna/animal/elephant_order.xmlactor/fauna/animal/elephant_order.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlactor/fauna/animal/elephant_death.xmlactor/fauna/animal/elephant_trained.xml10.0large
+ 0.5100
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_bull.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_bull.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_bull.xml (revision 25953)
@@ -1,20 +1,21 @@
50actor/fauna/animal/cattle_attack.xmlactor/fauna/animal/cattle_death.xmlactor/fauna/animal/cattle_order.xmlactor/fauna/animal/cattle_trained.xmllarge0.42.0
+ 0.4
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_aggressive_dog.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_aggressive_dog.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_wild_aggressive_dog.xml (revision 25953)
@@ -1,18 +1,19 @@
voice/global/civ_dog_move.xmlvoice/global/civ_dog_move.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlattack/weapon/sword.xmlactor/fauna/death/death_animal_gen.xmlinterface/complete/building/complete_kennel.xml1.6
+ 1.6
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelineer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelineer.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelineer.xml (revision 25953)
@@ -1,46 +1,47 @@
Javelin60304001250700.835falseHumanHero Cavalry JavelineerRanged Javelineer
special/formations/skirmish
-2-2attack/weapon/javelin_attack.xmlattack/impact/javelin_impact.xml0.9
+ 0.9
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_crossbowman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_crossbowman.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_crossbowman.xml (revision 25953)
@@ -1,50 +1,51 @@
Crossbow406020030001200.830falseHuman120Champion Infantry CrossbowmanRanged Crossbowman
special/formations/skirmish
-2-2attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml0.6
+ 1.2
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_dog.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_dog.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_dog.xml (revision 25953)
@@ -1,74 +1,75 @@
Fangs7235001000Structure Ship Siege1501001.5110War DogCannot attack Structures, Ships, or Siege Engines.Human FastMovingDog Melee10010128x256/ellipse.png128x256/ellipse_mask.pngvoice/global/civ_dog_move.xmlvoice/global/civ_dog_move.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlattack/weapon/sword.xmlactor/fauna/death/death_animal_gen.xmlinterface/complete/building/complete_kennel.xml2.5WarDog1.52
+ 1.530
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd_domestic_cattle.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd_domestic_cattle.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd_domestic_cattle.xml (revision 25953)
@@ -1,15 +1,16 @@
actor/fauna/animal/cattle_order.xmlactor/fauna/animal/cattle_death.xmlactor/fauna/animal/cattle_trained.xml0.41.4
+ 0.4
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml (revision 25953)
@@ -1,59 +1,60 @@
trueSeaCreatureKill to butcher for food.gaia/fauna_whale.png10falsefalseuprighttrue0.0true2000food.fish5128x512/ellipse.png128x512/ellipse_mask.png4.00.6665.0skittish60.060.0100000300000120ship-small1.81
+ 1.8
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_crossbowman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_crossbowman.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_crossbowman.xml (revision 25953)
@@ -1,46 +1,47 @@
Crossbow806020030001200.430falseHumanHero Cavalry CrossbowmanRanged Crossbowman
special/formations/skirmish
-2-2attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml0.8
+ 0.8
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_pikeman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_pikeman.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_pikeman.xml (revision 25953)
@@ -1,54 +1,55 @@
Pike23810002000Cavalry3.0Human50PikemanCounters: 3× vs Cavalry.Pikeman
special/formations/syntagma
51010attack/weapon/pike_attack.xml0.9
+ 0.9
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_catafalque.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_catafalque.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_catafalque.xml (revision 25953)
@@ -1,59 +1,60 @@
2500102.0-Organic -ConquestCriticalRelicCatafalqueunits/catafalque.pngtemplate_unit_catafalqueA catafalque that holds the remains of a great leader.truepitch-roll128x256/cartouche.png128x256/cartouche_mask.pngactor/singlesteps/steps_grass_order.xmlactor/singlesteps/steps_grass.xmlactor/singlesteps/steps_grass.xmlstandgroundfalselarge0.55
+ 0.275units/global/catafalque.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_crossbowman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_crossbowman.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_crossbowman.xml (revision 25953)
@@ -1,45 +1,46 @@
Crossbow20602003000120230falseHuman3020Cavalry CrossbowmanCrossbowman32attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml0.8
+ 0.8
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml (revision 25953)
@@ -1,46 +1,47 @@
Bow14608001000100150falseHumanRanged ArcherChampion Cavalry Archer
special/formations/skirmish
-2-2attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml0.85
+ 0.85
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelineer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelineer.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelineer.xml (revision 25953)
@@ -1,46 +1,47 @@
Javelin36304001250701.635falseHumanChampion Cavalry JavelineerRanged Javelineer
special/formations/skirmish
-2-2attack/weapon/javelin_attack.xmlattack/impact/javelin_impact.xml0.9
+ 0.9
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml (revision 25953)
@@ -1,50 +1,51 @@
Bow13.5608001000100150falseHuman120Ranged ArcherChampion Archer
special/formations/skirmish
-2-2attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml1.2
+ 2.4
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_pikeman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_pikeman.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_pikeman.xml (revision 25953)
@@ -1,50 +1,51 @@
Pike46810002000Cavalry3.0Human200Champion PikemanCounters: 3× vs Cavalry.Melee Pikeman
special/formations/syntagma
8820attack/weapon/pike_attack.xml0.9
+ 0.9
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna.xml (revision 25953)
@@ -1,56 +1,57 @@
04.0-ConquestCritical Animalgaia/fauna_generic.pngfood4128x256/ellipse.png128x256/ellipse_mask.pngAnimalpassivefalsefalse8.024.02000800015000600000.7
+ 0.7true60
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_elephant.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_elephant.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_elephant.xml (revision 25953)
@@ -1,25 +1,26 @@
Elephant504actor/fauna/animal/elephant_order.xmlactor/fauna/animal/elephant_attack.xmlactor/fauna/animal/elephant_death.xmlactor/fauna/animal/elephant_trained.xmllarge0.5
+ 0.25
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml (revision 25953)
@@ -1,46 +1,47 @@
Bow286080010001000.550falseHumanRanged ArcherHero Cavalry Archer
special/formations/skirmish
-2-2attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml0.85
+ 0.85
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml (revision 25953)
@@ -1,77 +1,78 @@
Trunk6024057501500!Ship6004009.01500Hero ElephantElephant Melee60404101025actor/fauna/animal/elephant_order.xmlactor/fauna/animal/elephant_attack_order.xmlactor/fauna/animal/elephant_order.xmlactor/fauna/animal/elephant_order.xmlactor/fauna/animal/elephant_attack.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlactor/fauna/animal/elephant_death.xmlactor/fauna/animal/elephant_trained.xml10.0large
+ 0.5
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_javelineer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_javelineer.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_javelineer.xml (revision 25953)
@@ -1,51 +1,52 @@
Javelin1630400125070435falseHuman50Infantry JavelineerJavelineer511attack/weapon/javelin_attack.xmlattack/impact/javelin_impact.xml2.4
+ 4.8
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fire.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fire.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fire.xml (revision 25953)
@@ -1,54 +1,55 @@
Fire201250100!Ship30300Circular30true600500-60.850.650.35Fire ShipUnrepairable. Gradually loses health. Can only attack Ships.Melee Warship Fireshipphase_township-small1.6
+ 1.660
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml (revision 25953)
@@ -1,69 +1,70 @@
Bow355510002000100250falseShip Human3131Infantry Cavalry32520010030Support Soldier Siege1400Medium WarshipGarrison units for transport and to increase firepower.Ranged Warship Triremephase_town14040204attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml1.8
+ 1.890
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_stonethrower.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_stonethrower.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_stonethrower.xml (revision 25953)
@@ -1,71 +1,72 @@
Stone210100406000700040620falseprops/units/weapons/rock_explosion.xml0.1Structureoutline_border.pngoutline_border_mask.png0.175254002504.5375Ranged StoneThrowerSiege Catapult2508050attack/impact/siegeprojectilehit.xmlattack/siege/ballist_attack.xmlstandground0.8
+ 0.8100
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_fanatic.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_fanatic.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_fanatic.xml (revision 25953)
@@ -1,24 +1,25 @@
gaulNaked FanaticBariogaisatosunits/gaul_champion_fanatic.pngphase_town-4-41.4
+ 1.4units/gauls/infantry_spearman_c.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_spear_gladiator.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_spear_gladiator.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_spear_gladiator.xml (revision 25953)
@@ -1,46 +1,47 @@
structures/rome/army_camp
structures/rome/temple_vesta
-10205GladiatorromelatinGladiator SpearmanHoplomachusEliteunits/rome_champion_infantry_gladiator_spear.pngphase_town-2Gladiator1.5
+ 1.50.5units/romans/infantry_gladiator_spearman.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_crossbowman.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_crossbowman.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_crossbowman.xml (revision 25953)
@@ -1,53 +1,54 @@
Crossbow20602003000120230falseHuman3020Infantry CrossbowmanCrossbowman3211attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml0.6
+ 1.2
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml (revision 25953)
@@ -1,66 +1,67 @@
Bow354510002000100250falseShip Human2101Infantry Cavalry2201206020Support Cavalry800Light WarshipGarrison units for transport and to increase firepower.Ranged Warship Biremephase_town802412attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml1.55
+ 1.5590
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml (revision 25953)
@@ -1,74 +1,75 @@
Stone150100402000500040620falseShip Structureoutline_border.pngoutline_border_mask.png0.1751101StoneThrower53060030050Support Soldier Siege2000Heavy WarshipGarrison units for transport and to increase firepower.Ranged Warship Quinqueremephase_city200120604attack/siege/ballist_attack.xml1.8
+ 1.8110
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_ram.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_ram.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_ram.xml (revision 25953)
@@ -1,66 +1,67 @@
Ram1506.57501500StructureField Organic30300150100.1UnitSupport Infantry02400Battering RamCannot attack Fields or Organic Units.Melee Ram200603050attack/siege/ram_move.xmlattack/siege/ram_attack_order.xmlattack/siege/ram_trained.xmlattack/siege/ram_attack.xml0.8
+ 0.880
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/ship_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/ship_trireme.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/ship_trireme.xml (revision 25953)
@@ -1,30 +1,31 @@
408.040200britPontosunits/celt_ship_trireme.png200.9
+ 0.9structures/celts/warship.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/ship_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/ship_trireme.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/ship_trireme.xml (revision 25953)
@@ -1,30 +1,31 @@
408.040200maurYudhpotunits/maur_ship_trireme.png200.9
+ 0.9structures/mauryas/trireme.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_archer.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_archer.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_archer.xml (revision 25953)
@@ -1,52 +1,53 @@
Bow6.76080010001002.550falseHuman5050ArcherArcher511attack/impact/arrow_impact.xmlattack/weapon/bow_attack.xml1.2
+ 2.4
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship.xml (revision 25953)
@@ -1,77 +1,79 @@
true0.00.57.010.01010FemaleCitizen Infantry Healer Dog0FemaleCitizen Infantry Healer Dog010truetrueShip-OrganicShipuprighttrue044.05105128x512/ellipse.png128x512/ellipse_mask.pnginterface/alarm/alarm_create_warship.xmlactor/ship/warship_move_01.xmlactor/ship/warship_move_01.xmlactor/ship/warship_move_01.xmlactor/ship/warship_death.xml6.00.56.0ship
+ 0.5
+ 0.25
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_merchant.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_merchant.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_merchant.xml (revision 25953)
@@ -1,49 +1,50 @@
2010015Support Cavalry240Merchantmantemplate_unit_ship_merchantTrade between docks. Garrison a Trader aboard for additional profit (+20% for each garrisoned). Gather profitable aquatic treasures.-ConquestCriticalTrader Bribablephase_town200.750.212passivefalsefalseship-small1.35
+ 1.650true
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml (revision 25953)
@@ -1,78 +1,79 @@
Bolt24090155000600015019.81falseHumanoutline_border.pngoutline_border_mask.png0.175Linear9false802025025022.0200Ranged BoltShooterBolt Shooter2005050attack/impact/arrow_metal.xmlattack/weapon/arrowfly.xmlstandground0.9
+ 0.9100
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_elephant.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_elephant.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_elephant.xml (revision 25953)
@@ -1,77 +1,78 @@
units/elephant_worker
151008.0300Worker ElephantElephant10pitch42210food wood stone metalfalse128x256/ellipse.png128x256/ellipse_mask.pngactor/fauna/animal/elephant_order.xmlactor/fauna/animal/elephant_order.xmlactor/fauna/animal/elephant_order.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlactor/fauna/animal/elephant_death.xmlactor/fauna/animal/elephant_trained.xml9.0falselarge0.6
+ 0.350
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/ship_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/ship_trireme.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/ship_trireme.xml (revision 25953)
@@ -1,31 +1,32 @@
408.040200iberPontiTransport many soldiers across the sea.units/celt_ship_trireme.png200.9
+ 0.9structures/iberians/warship.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_slinger.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_slinger.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_slinger.xml (revision 25953)
@@ -1,53 +1,54 @@
Sling11.51.145400125090345falseHuman3020SlingerSlinger3211attack/weapon/sling_attack.xml1.2
+ 1.2
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fishing.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fishing.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fishing.xml (revision 25953)
@@ -1,78 +1,79 @@
Harpoon1055001000!SeaCreature15501120Fishing Boattemplate_unit_ship_fishingFish the waters for food.-ConquestCriticalFishingBoat106.01.01.840128x256/ellipse.png128x256/ellipse_mask.pngactor/ship/boat_move.xmlactor/ship/boat_move.xml2.00.3335.0passivefalsefalseship-small1.1
+ 1.130
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege.xml (revision 25953)
@@ -1,64 +1,66 @@
30.00.02.0true-OrganicSiegeSiegephase_citypitch-roll44.01255128x256/rounded_rectangle.png128x256/rounded_rectangle_mask.pngattack/siege/ram_move.xmlattack/siege/ram_move.xmlattack/siege/ram_trained.xml4.00.5falselarge1
+ 0.75
+ 0.25
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_tower.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_tower.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_tower.xml (revision 25953)
@@ -1,102 +1,103 @@
Bow122.555101012002000100250falseHumanoutline_border.pngoutline_border_mask.png0.1750110Infantry4050030030.0200.1UnitSupport Infantry02500Ranged SiegeTowerSiege TowerGarrison units for transport and to increase firepower.25010060256x256/rounded_rectangle.png256x256/rounded_rectangle_mask.pngattack/siege/ram_move.xmlattack/siege/ram_move.xmlattack/impact/arrow_metal.xmlattack/weapon/arrowfly.xmlattack/siege/ram_trained.xml12.0500.7
+ 0.780
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/ship_trireme.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/ship_trireme.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/ship_trireme.xml (revision 25953)
@@ -1,30 +1,31 @@
408.040200gaulPontosunits/celt_ship_trireme.png200.9
+ 0.9structures/celts/warship.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_sword_gladiator.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_sword_gladiator.xml (revision 25952)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_sword_gladiator.xml (revision 25953)
@@ -1,47 +1,48 @@
structures/rome/army_camp
structures/rome/temple_vesta
-10205GladiatorromelatinGladiator SwordsmanMurmilloEliteunits/rome_champion_infantry_gladiator_sword.pngphase_town-1-1Gladiator1.4
+ 1.40.5units/romans/infantry_gladiator_swordsman.xml
Index: ps/trunk/source/simulation2/components/CCmpUnitMotion_System.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotion_System.cpp (revision 25952)
+++ ps/trunk/source/simulation2/components/CCmpUnitMotion_System.cpp (revision 25953)
@@ -1,343 +1,344 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "CCmpUnitMotion.h"
#include "CCmpUnitMotionManager.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/Profile.h"
#include
// NB: this TU contains the CCmpUnitMotion/CCmpUnitMotionManager couple.
// In practice, UnitMotionManager functions need access to the full implementation of UnitMotion,
// but UnitMotion needs access to MotionState (defined in UnitMotionManager).
// To avoid inclusion issues, implementation of UnitMotionManager that uses UnitMotion is here.
namespace {
/**
* Units push only within their own grid square. This is the size of each square (in arbitrary units).
* TODO: check other values.
*/
static const int PUSHING_GRID_SIZE = 20;
/**
* For pushing, treat the clearances as a circle - they're defined as squares,
* so we'll take the circumscribing square (approximately).
* Clerances are also full-width instead of half, so we want to divide by two. sqrt(2)/2 is about 0.71 < 5/7.
*/
static const entity_pos_t PUSHING_CORRECTION = entity_pos_t::FromInt(5) / 7;
/**
* Arbitrary constant used to reduce pushing to levels that won't break physics for our turn length.
*/
static const int PUSHING_REDUCTION_FACTOR = 2;
/**
* Maximum distance multiplier.
* NB: this value interacts with the "minimal pushing" force,
* as two perfectly overlapping units exert MAX_DISTANCE_FACTOR * Turn length in ms / REDUCTION_FACTOR
* of force on each other each turn. If this is below the minimal pushing force, any 2 units can entirely overlap.
*/
static const entity_pos_t MAX_DISTANCE_FACTOR = entity_pos_t::FromInt(5) / 2;
}
CCmpUnitMotionManager::MotionState::MotionState(CmpPtr cmpPos, CCmpUnitMotion* cmpMotion)
: cmpPosition(cmpPos), cmpUnitMotion(cmpMotion)
{
}
void CCmpUnitMotionManager::Init(const CParamNode&)
{
// Load some data - see CCmpPathfinder.xml.
// This assumes the pathfinder component is initialised first and registers the validator.
// TODO: there seems to be no real reason why we could not register a 'system' entity somewhere instead.
CParamNode externalParamNode;
CParamNode::LoadXML(externalParamNode, L"simulation/data/pathfinder.xml", "pathfinder");
CParamNode pushingNode = externalParamNode.GetChild("Pathfinder").GetChild("Pushing");
// NB: all values are given sane default, but they are not treated as optional in the schema,
// so the XML file is the reference.
const CParamNode radius = pushingNode.GetChild("Radius");
if (radius.IsOk())
{
m_PushingRadius = radius.ToFixed();
if (m_PushingRadius < entity_pos_t::Zero())
{
LOGWARNING("Pushing radius cannot be below 0. De-activating pushing but 'pathfinder.xml' should be updated.");
m_PushingRadius = entity_pos_t::Zero();
}
// No upper value, but things won't behave sanely if values are too high.
}
else
m_PushingRadius = entity_pos_t::FromInt(8) / 5;
const CParamNode minForce = pushingNode.GetChild("MinimalForce");
if (minForce.IsOk())
m_MinimalPushing = minForce.ToFixed();
else
m_MinimalPushing = entity_pos_t::FromInt(2) / 10;
const CParamNode movingExt = pushingNode.GetChild("MovingExtension");
const CParamNode staticExt = pushingNode.GetChild("StaticExtension");
if (movingExt.IsOk() && staticExt.IsOk())
{
m_MovingPushExtension = movingExt.ToFixed();
m_StaticPushExtension = staticExt.ToFixed();
}
else
{
m_MovingPushExtension = entity_pos_t::FromInt(5) / 2;
m_StaticPushExtension = entity_pos_t::FromInt(2);
}
}
void CCmpUnitMotionManager::Register(CCmpUnitMotion* component, entity_id_t ent, bool formationController)
{
MotionState state(CmpPtr(GetSimContext(), ent), component);
if (!formationController)
m_Units.insert(ent, state);
else
m_FormationControllers.insert(ent, state);
}
void CCmpUnitMotionManager::Unregister(entity_id_t ent)
{
EntityMap::iterator it = m_Units.find(ent);
if (it != m_Units.end())
{
m_Units.erase(it);
return;
}
it = m_FormationControllers.find(ent);
if (it != m_FormationControllers.end())
m_FormationControllers.erase(it);
}
void CCmpUnitMotionManager::OnTurnStart()
{
for (EntityMap::value_type& data : m_FormationControllers)
data.second.cmpUnitMotion->OnTurnStart();
for (EntityMap::value_type& data : m_Units)
data.second.cmpUnitMotion->OnTurnStart();
}
void CCmpUnitMotionManager::MoveUnits(fixed dt)
{
Move(m_Units, dt);
}
void CCmpUnitMotionManager::MoveFormations(fixed dt)
{
Move(m_FormationControllers, dt);
}
void CCmpUnitMotionManager::Move(EntityMap& ents, fixed dt)
{
PROFILE2("MotionMgr_Move");
std::unordered_set::iterator>*> assigned;
for (EntityMap::iterator it = ents.begin(); it != ents.end(); ++it)
{
if (!it->second.cmpPosition->IsInWorld())
{
it->second.needUpdate = false;
continue;
}
else
it->second.cmpUnitMotion->PreMove(it->second);
it->second.initialPos = it->second.cmpPosition->GetPosition2D();
it->second.initialAngle = it->second.cmpPosition->GetRotation().Y;
it->second.pos = it->second.initialPos;
+ it->second.speed = it->second.cmpUnitMotion->GetCurrentSpeed();
it->second.angle = it->second.initialAngle;
ENSURE(it->second.pos.X.ToInt_RoundToZero() / PUSHING_GRID_SIZE < m_MovingUnits.width() &&
it->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE < m_MovingUnits.height());
std::vector::iterator>& subdiv = m_MovingUnits.get(
it->second.pos.X.ToInt_RoundToZero() / PUSHING_GRID_SIZE,
it->second.pos.Y.ToInt_RoundToZero() / PUSHING_GRID_SIZE
);
subdiv.emplace_back(it);
assigned.emplace(&subdiv);
}
for (std::vector::iterator>* vec : assigned)
for (EntityMap::iterator& it : *vec)
if (it->second.needUpdate)
it->second.cmpUnitMotion->Move(it->second, dt);
// Skip pushing entirely if the radius is 0
if (&ents == &m_Units && m_PushingRadius != entity_pos_t::Zero())
{
PROFILE2("MotionMgr_Pushing");
for (std::vector::iterator>* vec : assigned)
{
ENSURE(!vec->empty());
std::vector::iterator>::iterator cit1 = vec->begin();
do
{
if ((*cit1)->second.ignore)
continue;
std::vector::iterator>::iterator cit2 = cit1;
while(++cit2 != vec->end())
if (!(*cit2)->second.ignore)
Push(**cit1, **cit2, dt);
}
while(++cit1 != vec->end());
}
}
if (m_PushingRadius != entity_pos_t::Zero())
{
PROFILE2("MotionMgr_PushAdjust");
CmpPtr cmpPathfinder(GetSystemEntity());
for (std::vector::iterator>* vec : assigned)
{
for (EntityMap::iterator& it : *vec)
{
if (!it->second.needUpdate || it->second.ignore)
continue;
// Prevent pushed units from crossing uncrossable boundaries
// (we can assume that normal movement didn't push units into impassable terrain).
if ((it->second.push.X != entity_pos_t::Zero() || it->second.push.Y != entity_pos_t::Zero()) &&
!cmpPathfinder->CheckMovement(it->second.cmpUnitMotion->GetObstructionFilter(),
it->second.pos.X, it->second.pos.Y,
it->second.pos.X + it->second.push.X, it->second.pos.Y + it->second.push.Y,
it->second.cmpUnitMotion->m_Clearance,
it->second.cmpUnitMotion->m_PassClass))
{
// Mark them as obstructed - this could possibly be optimised
// perhaps it'd make more sense to mark the pushers as blocked.
it->second.wasObstructed = true;
it->second.wentStraight = false;
it->second.push = CFixedVector2D();
}
// Only apply pushing if the effect is significant enough.
if (it->second.push.CompareLength(m_MinimalPushing) > 0)
{
// If there was an attempt at movement, and the pushed movement is in a sufficiently different direction
// (measured by an extremely arbitrary dot product)
// then mark the unit as obstructed still.
if (it->second.pos != it->second.initialPos &&
(it->second.pos - it->second.initialPos).Dot(it->second.pos + it->second.push - it->second.initialPos) < entity_pos_t::FromInt(1)/2)
{
it->second.wasObstructed = true;
it->second.wentStraight = false;
// Push anyways.
}
it->second.pos += it->second.push;
}
it->second.push = CFixedVector2D();
}
}
}
{
PROFILE2("MotionMgr_PostMove");
for (EntityMap::value_type& data : ents)
{
if (!data.second.needUpdate)
continue;
data.second.cmpUnitMotion->PostMove(data.second, dt);
}
}
for (std::vector::iterator>* vec : assigned)
vec->clear();
}
// TODO: ought to better simulate in-flight pushing, e.g. if units would cross in-between turns.
void CCmpUnitMotionManager::Push(EntityMap::value_type& a, EntityMap::value_type& b, fixed dt)
{
// The hard problem for pushing is knowing when to actually use the pathfinder to go around unpushable obstacles.
// For simplicitly, the current logic separates moving & stopped entities:
// moving entities will push moving entities, but not stopped ones, and vice-versa.
// this still delivers most of the value of pushing, without a lot of the complexity.
int movingPush = a.second.isMoving + b.second.isMoving;
// Exception: units in the same control group (i.e. the same formation) never push farther than themselves
// and are also allowed to push idle units (obstructions are ignored within formations,
// so pushing idle units makes one member crossing the formation look better).
bool sameControlGroup = a.second.controlGroup != INVALID_ENTITY && a.second.controlGroup == b.second.controlGroup;
if (sameControlGroup)
movingPush = 0;
if (movingPush == 1)
return;
entity_pos_t combinedClearance = (a.second.cmpUnitMotion->m_Clearance + b.second.cmpUnitMotion->m_Clearance).Multiply(PUSHING_CORRECTION);
entity_pos_t maxDist = combinedClearance;
if (!sameControlGroup)
maxDist = combinedClearance.Multiply(m_PushingRadius) + (movingPush ? m_MovingPushExtension : m_StaticPushExtension);
CFixedVector2D offset = a.second.pos - b.second.pos;
if (offset.CompareLength(maxDist) > 0)
return;
entity_pos_t offsetLength = offset.Length();
// If the offset is small enough that precision would be problematic, pick an arbitrary vector instead.
if (offsetLength <= entity_pos_t::Epsilon() * 10)
{
// Throw in some 'randomness' so that clumped units unclump more naturally.
bool dir = a.first % 2;
offset.X = entity_pos_t::FromInt(dir ? 1 : 0);
offset.Y = entity_pos_t::FromInt(dir ? 0 : 1);
offsetLength = entity_pos_t::Epsilon() * 10;
}
else
{
offset.X = offset.X / offsetLength;
offset.Y = offset.Y / offsetLength;
}
// If the units are moving in opposite direction, check if they might have phased through each other.
// If it looks like yes, move them perpendicularily so it looks like they avoid each other.
// NB: this isn't very precise, nor will it catch 100% of intersections - it's meant as a cheap improvement.
if (movingPush && (a.second.pos - a.second.initialPos).Dot(b.second.pos - b.second.initialPos) < entity_pos_t::Zero())
// Perform some finer checking.
if (Geometry::TestRayAASquare(a.second.initialPos - b.second.initialPos, a.second.pos - b.second.initialPos,
CFixedVector2D(combinedClearance, combinedClearance))
||
Geometry::TestRayAASquare(a.second.initialPos - b.second.pos, a.second.pos - b.second.pos,
CFixedVector2D(combinedClearance, combinedClearance)))
{
offset = offset.Perpendicular();
offsetLength = fixed::Zero();
}
// The pushing distance factor is 1 if the edges are touching, >1 up to MAX if the units overlap, < 1 otherwise.
entity_pos_t distanceFactor = maxDist - combinedClearance;
// Force units that overlap a lot to have the maximum factor.
if (distanceFactor <= entity_pos_t::Zero() || offsetLength < combinedClearance / 2)
distanceFactor = MAX_DISTANCE_FACTOR;
else
distanceFactor = Clamp((maxDist - offsetLength) / distanceFactor, entity_pos_t::Zero(), MAX_DISTANCE_FACTOR);
// Mark both as needing an update so they actually get moved.
a.second.needUpdate = true;
b.second.needUpdate = true;
CFixedVector2D pushingDir = offset.Multiply(distanceFactor);
// Divide by an arbitrary constant to avoid pushing too much.
a.second.push += pushingDir.Multiply(dt / PUSHING_REDUCTION_FACTOR);
b.second.push -= pushingDir.Multiply(dt / PUSHING_REDUCTION_FACTOR);
}
Index: ps/trunk/source/simulation2/components/CCmpUnitMotionManager.h
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotionManager.h (revision 25952)
+++ ps/trunk/source/simulation2/components/CCmpUnitMotionManager.h (revision 25953)
@@ -1,197 +1,199 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_CCMPUNITMOTIONMANAGER
#define INCLUDED_CCMPUNITMOTIONMANAGER
#include "simulation2/system/Component.h"
#include "ICmpUnitMotionManager.h"
#include "simulation2/MessageTypes.h"
#include "simulation2/components/ICmpTerrain.h"
#include "simulation2/helpers/Grid.h"
#include "simulation2/system/EntityMap.h"
class CCmpUnitMotion;
class CCmpUnitMotionManager : public ICmpUnitMotionManager
{
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_TerrainChanged);
componentManager.SubscribeToMessageType(MT_TurnStart);
componentManager.SubscribeToMessageType(MT_Update_Final);
componentManager.SubscribeToMessageType(MT_Update_MotionUnit);
componentManager.SubscribeToMessageType(MT_Update_MotionFormation);
}
DEFAULT_COMPONENT_ALLOCATOR(UnitMotionManager)
// Persisted state for each unit.
struct MotionState
{
MotionState(CmpPtr cmpPos, CCmpUnitMotion* cmpMotion);
// Component references - these must be kept alive for the duration of motion.
// NB: this is generally not something one should do, but because of the tight coupling here it's doable.
CmpPtr cmpPosition;
CCmpUnitMotion* cmpUnitMotion;
// Position before units start moving
CFixedVector2D initialPos;
// Transient position during the movement.
CFixedVector2D pos;
// Accumulated "pushing" from nearby units.
CFixedVector2D push;
+ fixed speed;
+
fixed initialAngle;
fixed angle;
// Used for formations - units with the same control group won't push at a distance.
// (this is required because formations may be tight and large units may end up never settling.
entity_id_t controlGroup = INVALID_ENTITY;
// Meta-flag -> this entity won't push nor be pushed.
// (used for entities that have their obstruction disabled).
bool ignore = false;
// If true, the entity needs to be handled during movement.
bool needUpdate = false;
bool wentStraight = false;
bool wasObstructed = false;
// Clone of the obstruction manager flag for efficiency
bool isMoving = false;
};
// "Template" state, not serialized (cannot be changed mid-game).
// Multiplier for the pushing radius. Pre-multiplied by the circle-square correction factor.
entity_pos_t m_PushingRadius;
// Additive modifiers to the pushing radius for moving units and idle units respectively.
entity_pos_t m_MovingPushExtension;
entity_pos_t m_StaticPushExtension;
// Pushing forces below this value are ignored - this prevents units moving forever by very small increments.
entity_pos_t m_MinimalPushing;
// These vectors are reconstructed on deserialization.
EntityMap m_Units;
EntityMap m_FormationControllers;
// Turn-local state below, not serialised.
Grid::iterator>> m_MovingUnits;
bool m_ComputingMotion;
static std::string GetSchema()
{
return "";
}
virtual void Init(const CParamNode& UNUSED(paramNode));
virtual void Deinit()
{
}
virtual void Serialize(ISerializer& UNUSED(serialize))
{
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize))
{
Init(paramNode);
ResetSubdivisions();
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_TerrainChanged:
{
CmpPtr cmpTerrain(GetSystemEntity());
if (cmpTerrain->GetVerticesPerSide() != m_MovingUnits.width())
ResetSubdivisions();
break;
}
case MT_TurnStart:
{
OnTurnStart();
break;
}
case MT_Update_MotionFormation:
{
fixed dt = static_cast(msg).turnLength;
m_ComputingMotion = true;
MoveFormations(dt);
m_ComputingMotion = false;
break;
}
case MT_Update_MotionUnit:
{
fixed dt = static_cast(msg).turnLength;
m_ComputingMotion = true;
MoveUnits(dt);
m_ComputingMotion = false;
break;
}
}
}
virtual void Register(CCmpUnitMotion* component, entity_id_t ent, bool formationController);
virtual void Unregister(entity_id_t ent);
virtual bool ComputingMotion() const
{
return m_ComputingMotion;
}
virtual bool IsPushingActivated() const
{
return m_PushingRadius != entity_pos_t::Zero();
}
private:
void ResetSubdivisions();
void OnTurnStart();
void MoveUnits(fixed dt);
void MoveFormations(fixed dt);
void Move(EntityMap& ents, fixed dt);
void Push(EntityMap::value_type& a, EntityMap::value_type& b, fixed dt);
};
void CCmpUnitMotionManager::ResetSubdivisions()
{
CmpPtr cmpTerrain(GetSystemEntity());
if (!cmpTerrain)
return;
size_t size = cmpTerrain->GetMapSize();
u16 gridSquareSize = static_cast(size / 20 + 1);
m_MovingUnits.resize(gridSquareSize, gridSquareSize);
}
REGISTER_COMPONENT_TYPE(UnitMotionManager)
#endif // INCLUDED_CCMPUNITMOTIONMANAGER
Index: ps/trunk/source/simulation2/components/CCmpUnitMotion.h
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotion.h (revision 25952)
+++ ps/trunk/source/simulation2/components/CCmpUnitMotion.h (revision 25953)
@@ -1,1821 +1,1877 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_CCMPUNITMOTION
#define INCLUDED_CCMPUNITMOTION
#include "simulation2/system/Component.h"
#include "ICmpUnitMotion.h"
#include "simulation2/components/CCmpUnitMotionManager.h"
#include "simulation2/components/ICmpObstruction.h"
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpOwnership.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpPathfinder.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpValueModificationManager.h"
#include "simulation2/components/ICmpVisual.h"
#include "simulation2/helpers/Geometry.h"
#include "simulation2/helpers/Render.h"
#include "simulation2/MessageTypes.h"
#include "simulation2/serialization/SerializedPathfinder.h"
#include "simulation2/serialization/SerializedTypes.h"
#include "graphics/Overlay.h"
#include "maths/FixedVector2D.h"
#include "ps/CLogger.h"
#include "ps/Profile.h"
#include "renderer/Scene.h"
// NB: this implementation of ICmpUnitMotion is very tightly coupled with UnitMotionManager.
// As such, both are compiled in the same TU.
// For debugging; units will start going straight to the target
// instead of calling the pathfinder
#define DISABLE_PATHFINDER 0
namespace
{
/**
* Min/Max range to restrict short path queries to. (Larger ranges are (much) slower,
* smaller ranges might miss some legitimate routes around large obstacles.)
* NB: keep the max-range in sync with the vertex pathfinder "move the search space" heuristic.
*/
constexpr entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(12 * Pathfinding::NAVCELL_SIZE_INT);
constexpr entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(56 * Pathfinding::NAVCELL_SIZE_INT);
constexpr entity_pos_t SHORT_PATH_SEARCH_RANGE_INCREMENT = entity_pos_t::FromInt(4 * Pathfinding::NAVCELL_SIZE_INT);
constexpr u8 SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY = 1;
/**
* When using the short-pathfinder to rejoin a long-path waypoint, aim for a circle of this radius around the waypoint.
*/
constexpr entity_pos_t SHORT_PATH_LONG_WAYPOINT_RANGE = entity_pos_t::FromInt(4 * Pathfinding::NAVCELL_SIZE_INT);
/**
* Minimum distance to goal for a long path request
*/
constexpr entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(16 * Pathfinding::NAVCELL_SIZE_INT);
/**
* If we are this close to our target entity/point, then think about heading
* for it in a straight line instead of pathfinding.
*/
constexpr entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(24 * Pathfinding::NAVCELL_SIZE_INT);
/**
* To avoid recomputing paths too often, have some leeway for target range checks
* based on our distance to the target. Increase that incertainty by one navcell
* for every this many tiles of distance.
*/
constexpr entity_pos_t TARGET_UNCERTAINTY_MULTIPLIER = entity_pos_t::FromInt(8 * Pathfinding::NAVCELL_SIZE_INT);
/**
* When following a known imperfect path (i.e. a path that won't take us in range of our goal
* we still recompute a new path every N turn to adapt to moving targets (for example, ships that must pickup
* units may easily end up in this state, they still need to adjust to moving units).
* This is rather arbitrary and mostly for simplicity & optimisation (a better recomputing algorithm
* would not need this).
*/
constexpr u8 KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN = 12;
/**
* When we fail to move this many turns in a row, inform other components that the move will fail.
* Experimentally, this number needs to be somewhat high or moving groups of units will lead to stuck units.
* However, too high means units will look idle for a long time when they are failing to move.
* TODO: if UnitMotion could send differentiated "unreachable" and "currently stuck" failing messages,
* this could probably be lowered.
* TODO: when unit pushing is implemented, this number can probably be lowered.
*/
constexpr u8 MAX_FAILED_MOVEMENTS = 35;
/**
* When computing paths but failing to move, we want to occasionally alternate pathfinder systems
* to avoid getting stuck (the short pathfinder can unstuck the long-range one and vice-versa, depending).
*/
constexpr u8 ALTERNATE_PATH_TYPE_DELAY = 3;
constexpr u8 ALTERNATE_PATH_TYPE_EVERY = 6;
/**
* Units can occasionally get stuck near corners. The cause is a mismatch between CheckMovement and the short pathfinder.
* The problem is the short pathfinder finds an impassable path when units are right on an obstruction edge.
* Fixing this math mismatch is perhaps possible, but fixing it in UM is rather easy: just try backing up a bit
* and that will probably un-stuck the unit. This is the 'failed movement' turn on which to try that.
*/
constexpr u8 BACKUP_HACK_DELAY = 10;
/**
* After this many failed computations, start sending "VERY_OBSTRUCTED" messages instead.
* Should probably be larger than ALTERNATE_PATH_TYPE_DELAY.
*/
constexpr u8 VERY_OBSTRUCTED_THRESHOLD = 10;
const CColor OVERLAY_COLOR_LONG_PATH(1, 1, 1, 1);
const CColor OVERLAY_COLOR_SHORT_PATH(1, 0, 0, 1);
} // anonymous namespace
class CCmpUnitMotion final : public ICmpUnitMotion
{
friend class CCmpUnitMotionManager;
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_Create);
componentManager.SubscribeToMessageType(MT_Destroy);
componentManager.SubscribeToMessageType(MT_PathResult);
componentManager.SubscribeToMessageType(MT_OwnershipChanged);
componentManager.SubscribeToMessageType(MT_ValueModification);
componentManager.SubscribeToMessageType(MT_MovementObstructionChanged);
componentManager.SubscribeToMessageType(MT_Deserialized);
}
DEFAULT_COMPONENT_ALLOCATOR(UnitMotion)
bool m_DebugOverlayEnabled;
std::vector m_DebugOverlayLongPathLines;
std::vector m_DebugOverlayShortPathLines;
// Template state:
bool m_IsFormationController;
- fixed m_TemplateWalkSpeed, m_TemplateRunMultiplier;
+ fixed m_TemplateWalkSpeed, m_TemplateRunMultiplier, m_TemplateAcceleration;
pass_class_t m_PassClass;
std::string m_PassClassName;
// Dynamic state:
entity_pos_t m_Clearance;
// cached for efficiency
fixed m_WalkSpeed, m_RunMultiplier;
bool m_FacePointAfterMove;
// Whether the unit participates in pushing.
bool m_Pushing = false;
// Whether the unit blocks movement (& is blocked by movement blockers)
// Cached from ICmpObstruction.
bool m_BlockMovement = false;
// Internal counter used when recovering from obstructed movement.
// Most notably, increases the search range of the vertex pathfinder.
// See HandleObstructedMove() for more details.
u8 m_FailedMovements = 0;
// If > 0, PathingUpdateNeeded returns false always.
// This exists because the goal may be unreachable to the short/long pathfinder.
// In such cases, we would compute inacceptable paths and PathingUpdateNeeded would trigger every turn,
// which would be quite bad for performance.
// To avoid that, when we know the new path is imperfect, treat it as OK and follow it anyways.
// When reaching the end, we'll go through HandleObstructedMove and reset regardless.
// To still recompute now and then (the target may be moving), this is a countdown decremented on each frame.
u8 m_FollowKnownImperfectPathCountdown = 0;
struct Ticket {
u32 m_Ticket = 0; // asynchronous request ID we're waiting for, or 0 if none
enum Type {
SHORT_PATH,
LONG_PATH
} m_Type = SHORT_PATH; // Pick some default value to avoid UB.
void clear() { m_Ticket = 0; }
} m_ExpectedPathTicket;
struct MoveRequest {
enum Type {
NONE,
POINT,
ENTITY,
OFFSET
} m_Type = NONE;
entity_id_t m_Entity = INVALID_ENTITY;
CFixedVector2D m_Position;
entity_pos_t m_MinRange, m_MaxRange;
// For readability
CFixedVector2D GetOffset() const { return m_Position; };
MoveRequest() = default;
MoveRequest(CFixedVector2D pos, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(POINT), m_Position(pos), m_MinRange(minRange), m_MaxRange(maxRange) {};
MoveRequest(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(ENTITY), m_Entity(target), m_MinRange(minRange), m_MaxRange(maxRange) {};
MoveRequest(entity_id_t target, CFixedVector2D offset) : m_Type(OFFSET), m_Entity(target), m_Position(offset) {};
} m_MoveRequest;
// If this is not INVALID_ENTITY, the unit is a formation member.
entity_id_t m_FormationController = INVALID_ENTITY;
// If the entity moves, it will do so at m_WalkSpeed * m_SpeedMultiplier.
fixed m_SpeedMultiplier;
// This caches the resulting speed from m_WalkSpeed * m_SpeedMultiplier for convenience.
fixed m_Speed;
- // Current mean speed (over the last turn).
- fixed m_CurSpeed;
+ // The speed achieved at the end of the current turn.
+ fixed m_CurrentSpeed;
+
+ fixed m_InstantTurnAngle;
+
+ fixed m_Acceleration;
// Currently active paths (storing waypoints in reverse order).
// The last item in each path is the point we're currently heading towards.
WaypointPath m_LongPath;
WaypointPath m_ShortPath;
static std::string GetSchema()
{
return
"Provides the unit with the ability to move around the world by itself."
""
"7.0"
"default"
""
""
""
""
- ""
+ ""
""
""
""
- ""
+ ""
""
""
""
- ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
""
""
""
""
""
""
"";
}
virtual void Init(const CParamNode& paramNode)
{
m_IsFormationController = paramNode.GetChild("FormationController").ToBool();
m_FacePointAfterMove = true;
m_WalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed();
m_SpeedMultiplier = fixed::FromInt(1);
- m_CurSpeed = fixed::Zero();
+ m_CurrentSpeed = fixed::Zero();
m_RunMultiplier = m_TemplateRunMultiplier = fixed::FromInt(1);
if (paramNode.GetChild("RunMultiplier").IsOk())
m_RunMultiplier = m_TemplateRunMultiplier = paramNode.GetChild("RunMultiplier").ToFixed();
+ m_InstantTurnAngle = paramNode.GetChild("InstantTurnAngle").ToFixed();
+
+ m_Acceleration = m_TemplateAcceleration = paramNode.GetChild("Acceleration").ToFixed();
+
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
{
m_PassClassName = paramNode.GetChild("PassabilityClass").ToString();
m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName);
m_Clearance = cmpPathfinder->GetClearance(m_PassClass);
CmpPtr cmpObstruction(GetEntityHandle());
if (cmpObstruction)
{
cmpObstruction->SetUnitClearance(m_Clearance);
m_BlockMovement = cmpObstruction->GetBlockMovementFlag(true);
}
}
SetParticipateInPushing(!paramNode.GetChild("DisablePushing").IsOk() || !paramNode.GetChild("DisablePushing").ToBool());
m_DebugOverlayEnabled = false;
}
virtual void Deinit()
{
}
template
void SerializeCommon(S& serialize)
{
serialize.StringASCII("pass class", m_PassClassName, 0, 64);
serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket.m_Ticket);
Serializer(serialize, "ticket type", m_ExpectedPathTicket.m_Type, Ticket::Type::LONG_PATH);
serialize.NumberU8_Unbounded("failed movements", m_FailedMovements);
serialize.NumberU8_Unbounded("followknownimperfectpath", m_FollowKnownImperfectPathCountdown);
Serializer(serialize, "target type", m_MoveRequest.m_Type, MoveRequest::Type::OFFSET);
serialize.NumberU32_Unbounded("target entity", m_MoveRequest.m_Entity);
serialize.NumberFixed_Unbounded("target pos x", m_MoveRequest.m_Position.X);
serialize.NumberFixed_Unbounded("target pos y", m_MoveRequest.m_Position.Y);
serialize.NumberFixed_Unbounded("target min range", m_MoveRequest.m_MinRange);
serialize.NumberFixed_Unbounded("target max range", m_MoveRequest.m_MaxRange);
serialize.NumberU32_Unbounded("formation controller", m_FormationController);
serialize.NumberFixed_Unbounded("speed multiplier", m_SpeedMultiplier);
- serialize.NumberFixed_Unbounded("current speed", m_CurSpeed);
+ serialize.NumberFixed_Unbounded("current speed", m_CurrentSpeed);
+
+ serialize.NumberFixed_Unbounded("instant turn angle", m_InstantTurnAngle);
+
+ serialize.NumberFixed_Unbounded("acceleration", m_Acceleration);
serialize.Bool("facePointAfterMove", m_FacePointAfterMove);
serialize.Bool("pushing", m_Pushing);
Serializer(serialize, "long path", m_LongPath.m_Waypoints);
Serializer(serialize, "short path", m_ShortPath.m_Waypoints);
}
virtual void Serialize(ISerializer& serialize)
{
SerializeCommon(serialize);
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(paramNode);
SerializeCommon(deserialize);
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName);
CmpPtr cmpObstruction(GetEntityHandle());
if (cmpObstruction)
m_BlockMovement = cmpObstruction->GetBlockMovementFlag(false);
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_RenderSubmit:
{
PROFILE("UnitMotion::RenderSubmit");
const CMessageRenderSubmit& msgData = static_cast (msg);
RenderSubmit(msgData.collector);
break;
}
case MT_PathResult:
{
const CMessagePathResult& msgData = static_cast (msg);
PathResult(msgData.ticket, msgData.path);
break;
}
case MT_Create:
{
if (!ENTITY_IS_LOCAL(GetEntityId()))
CmpPtr(GetSystemEntity())->Register(this, GetEntityId(), m_IsFormationController);
break;
}
case MT_Destroy:
{
if (!ENTITY_IS_LOCAL(GetEntityId()))
CmpPtr(GetSystemEntity())->Unregister(GetEntityId());
break;
}
case MT_MovementObstructionChanged:
{
CmpPtr cmpObstruction(GetEntityHandle());
if (cmpObstruction)
m_BlockMovement = cmpObstruction->GetBlockMovementFlag(false);
break;
}
case MT_ValueModification:
{
const CMessageValueModification& msgData = static_cast (msg);
if (msgData.component != L"UnitMotion")
break;
FALLTHROUGH;
}
case MT_OwnershipChanged:
{
OnValueModification();
break;
}
case MT_Deserialized:
{
OnValueModification();
if (!ENTITY_IS_LOCAL(GetEntityId()))
CmpPtr(GetSystemEntity())->Register(this, GetEntityId(), m_IsFormationController);
break;
}
}
}
void UpdateMessageSubscriptions()
{
bool needRender = m_DebugOverlayEnabled;
GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender);
}
virtual bool IsMoveRequested() const
{
return m_MoveRequest.m_Type != MoveRequest::NONE;
}
virtual fixed GetSpeedMultiplier() const
{
return m_SpeedMultiplier;
}
virtual void SetSpeedMultiplier(fixed multiplier)
{
m_SpeedMultiplier = std::min(multiplier, m_RunMultiplier);
m_Speed = m_SpeedMultiplier.Multiply(GetWalkSpeed());
}
virtual fixed GetSpeed() const
{
return m_Speed;
}
virtual fixed GetWalkSpeed() const
{
return m_WalkSpeed;
}
virtual fixed GetRunMultiplier() const
{
return m_RunMultiplier;
}
virtual CFixedVector2D EstimateFuturePosition(const fixed dt) const
{
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return CFixedVector2D();
// TODO: formation members should perhaps try to use the controller's position.
CFixedVector2D pos = cmpPosition->GetPosition2D();
entity_angle_t angle = cmpPosition->GetRotation().Y;
-
+ fixed speed = m_CurrentSpeed;
// Copy the path so we don't change it.
WaypointPath shortPath = m_ShortPath;
WaypointPath longPath = m_LongPath;
- PerformMove(dt, cmpPosition->GetTurnRate(), shortPath, longPath, pos, angle);
+ PerformMove(dt, cmpPosition->GetTurnRate(), shortPath, longPath, pos, speed, angle);
return pos;
}
+ virtual fixed GetAcceleration() const
+ {
+ return m_Acceleration;
+ }
+
+ virtual void SetAcceleration(fixed acceleration)
+ {
+ m_Acceleration = acceleration;
+ }
+
virtual pass_class_t GetPassabilityClass() const
{
return m_PassClass;
}
virtual std::string GetPassabilityClassName() const
{
return m_PassClassName;
}
virtual void SetPassabilityClassName(const std::string& passClassName)
{
m_PassClassName = passClassName;
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName);
}
virtual fixed GetCurrentSpeed() const
{
- return m_CurSpeed;
+ return m_CurrentSpeed;
}
virtual void SetFacePointAfterMove(bool facePointAfterMove)
{
m_FacePointAfterMove = facePointAfterMove;
}
virtual bool GetFacePointAfterMove() const
{
return m_FacePointAfterMove;
}
virtual void SetDebugOverlay(bool enabled)
{
m_DebugOverlayEnabled = enabled;
UpdateMessageSubscriptions();
}
virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
{
return MoveTo(MoveRequest(CFixedVector2D(x, z), minRange, maxRange));
}
virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{
return MoveTo(MoveRequest(target, minRange, maxRange));
}
virtual void MoveToFormationOffset(entity_id_t controller, entity_pos_t x, entity_pos_t z)
{
// Pass the controller to the move request anyways.
MoveTo(MoveRequest(controller, CFixedVector2D(x, z)));
}
virtual void SetMemberOfFormation(entity_id_t controller)
{
m_FormationController = controller;
}
virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z);
/**
* Clears the current MoveRequest - the unit will stop and no longer try and move.
* This should never be called from UnitMotion, since MoveToX orders are given
* by other components - these components should also decide when to stop.
*/
virtual void StopMoving()
{
if (m_FacePointAfterMove)
{
CmpPtr cmpPosition(GetEntityHandle());
if (cmpPosition && cmpPosition->IsInWorld())
{
CFixedVector2D targetPos;
if (ComputeTargetPosition(targetPos))
FaceTowardsPointFromPos(cmpPosition->GetPosition2D(), targetPos.X, targetPos.Y);
}
}
m_MoveRequest = MoveRequest();
m_ExpectedPathTicket.clear();
m_LongPath.m_Waypoints.clear();
m_ShortPath.m_Waypoints.clear();
}
virtual entity_pos_t GetUnitClearance() const
{
return m_Clearance;
}
private:
bool IsFormationMember() const
{
return m_FormationController != INVALID_ENTITY;
}
bool IsMovingAsFormation() const
{
return IsFormationMember() && m_MoveRequest.m_Type == MoveRequest::OFFSET;
}
bool IsFormationControllerMoving() const
{
CmpPtr cmpControllerMotion(GetSimContext(), m_FormationController);
return cmpControllerMotion && cmpControllerMotion->IsMoveRequested();
}
entity_id_t GetGroup() const
{
return IsFormationMember() ? m_FormationController : GetEntityId();
}
void SetParticipateInPushing(bool pushing)
{
CmpPtr cmpUnitMotionManager(GetSystemEntity());
m_Pushing = pushing && cmpUnitMotionManager->IsPushingActivated();
}
/**
* Warns other components that our current movement will likely fail (e.g. we won't be able to reach our target)
* This should only be called before the actual movement in a given turn, or units might both move and try to do things
* on the same turn, leading to gliding units.
*/
void MoveFailed()
{
// Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time
// if our current offset is unreachable, but we don't want to end up stuck.
// (If the formation controller has stopped moving however, we can safely message).
if (IsFormationMember() && IsFormationControllerMoving())
return;
CMessageMotionUpdate msg(CMessageMotionUpdate::LIKELY_FAILURE);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
/**
* Warns other components that our current movement is likely over (i.e. we probably reached our destination)
* This should only be called before the actual movement in a given turn, or units might both move and try to do things
* on the same turn, leading to gliding units.
*/
void MoveSucceeded()
{
// Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time
// if our current offset is unreachable, but we don't want to end up stuck.
// (If the formation controller has stopped moving however, we can safely message).
if (IsFormationMember() && IsFormationControllerMoving())
return;
CMessageMotionUpdate msg(CMessageMotionUpdate::LIKELY_SUCCESS);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
/**
* Warns other components that our current movement was obstructed (i.e. we failed to move this turn).
* This should only be called before the actual movement in a given turn, or units might both move and try to do things
* on the same turn, leading to gliding units.
*/
void MoveObstructed()
{
// Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time
// if our current offset is unreachable, but we don't want to end up stuck.
// (If the formation controller has stopped moving however, we can safely message).
if (IsFormationMember() && IsFormationControllerMoving())
return;
CMessageMotionUpdate msg(m_FailedMovements >= VERY_OBSTRUCTED_THRESHOLD ?
CMessageMotionUpdate::VERY_OBSTRUCTED : CMessageMotionUpdate::OBSTRUCTED);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
/**
* Increment the number of failed movements and notify other components if required.
* @returns true if the failure was notified, false otherwise.
*/
bool IncrementFailedMovementsAndMaybeNotify()
{
m_FailedMovements++;
if (m_FailedMovements >= MAX_FAILED_MOVEMENTS)
{
MoveFailed();
m_FailedMovements = 0;
return true;
}
return false;
}
/**
* If path would take us farther away from the goal than pos currently is, return false, else return true.
*/
bool RejectFartherPaths(const PathGoal& goal, const WaypointPath& path, const CFixedVector2D& pos) const;
bool ShouldAlternatePathfinder() const
{
return (m_FailedMovements == ALTERNATE_PATH_TYPE_DELAY) || ((MAX_FAILED_MOVEMENTS - ALTERNATE_PATH_TYPE_DELAY) % ALTERNATE_PATH_TYPE_EVERY == 0);
}
bool InShortPathRange(const PathGoal& goal, const CFixedVector2D& pos) const
{
return goal.DistanceToPoint(pos) < LONG_PATH_MIN_DIST;
}
entity_pos_t ShortPathSearchRange() const
{
u8 multiple = m_FailedMovements < SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY ? 0 : m_FailedMovements - SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY;
fixed searchRange = SHORT_PATH_MIN_SEARCH_RANGE + SHORT_PATH_SEARCH_RANGE_INCREMENT * multiple;
if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE)
searchRange = SHORT_PATH_MAX_SEARCH_RANGE;
return searchRange;
}
/**
* Handle the result of an asynchronous path query.
*/
void PathResult(u32 ticket, const WaypointPath& path);
void OnValueModification()
{
CmpPtr cmpValueModificationManager(GetSystemEntity());
if (!cmpValueModificationManager)
return;
m_WalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateWalkSpeed, GetEntityId());
m_RunMultiplier = cmpValueModificationManager->ApplyModifications(L"UnitMotion/RunMultiplier", m_TemplateRunMultiplier, GetEntityId());
// For MT_Deserialize compute m_Speed from the serialized m_SpeedMultiplier.
// For MT_ValueModification and MT_OwnershipChanged, adjust m_SpeedMultiplier if needed
// (in case then new m_RunMultiplier value is lower than the old).
SetSpeedMultiplier(m_SpeedMultiplier);
}
/**
* Check if we are at destination early in the turn, this both lets units react faster
* and ensure that distance comparisons are done while units are not being moved
* (otherwise they won't be commutative).
*/
void OnTurnStart();
void PreMove(CCmpUnitMotionManager::MotionState& state);
void Move(CCmpUnitMotionManager::MotionState& state, fixed dt);
void PostMove(CCmpUnitMotionManager::MotionState& state, fixed dt);
/**
* Returns true if we are possibly at our destination.
* Since the concept of being at destination is dependent on why the move was requested,
* UnitMotion can only ever hint about this, hence the conditional tone.
*/
bool PossiblyAtDestination() const;
/**
* Process the move the unit will do this turn.
* This does not send actually change the position.
* @returns true if the move was obstructed.
*/
- bool PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, entity_angle_t& angle) const;
+ bool PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle) const;
/**
* Update other components on our speed.
* (For performance, this should try to avoid sending messages).
*/
- void UpdateMovementState(entity_pos_t speed);
+ void UpdateMovementState(entity_pos_t speed, entity_pos_t meanSpeed);
/**
* React if our move was obstructed.
* @param moved - true if the unit still managed to move.
* @returns true if the obstruction required handling, false otherwise.
*/
bool HandleObstructedMove(bool moved);
/**
* Returns true if the target position is valid. False otherwise.
* (this may indicate that the target is e.g. out of the world/dead).
* NB: for code-writing convenience, if we have no target, this returns true.
*/
bool TargetHasValidPosition(const MoveRequest& moveRequest) const;
bool TargetHasValidPosition() const
{
return TargetHasValidPosition(m_MoveRequest);
}
/**
* Computes the current location of our target entity (plus offset).
* Returns false if no target entity or no valid position.
*/
bool ComputeTargetPosition(CFixedVector2D& out, const MoveRequest& moveRequest) const;
bool ComputeTargetPosition(CFixedVector2D& out) const
{
return ComputeTargetPosition(out, m_MoveRequest);
}
/**
* Attempts to replace the current path with a straight line to the target,
* if it's close enough and the route is not obstructed.
*/
bool TryGoingStraightToTarget(const CFixedVector2D& from, bool updatePaths);
/**
* Returns whether our we need to recompute a path to reach our target.
*/
bool PathingUpdateNeeded(const CFixedVector2D& from) const;
/**
* Rotate to face towards the target point, given the current pos
*/
void FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z);
/**
* Units in 'pushing' mode are marked as 'moving' in the obstruction manager.
* Units in 'pushing' mode should skip them in checkMovement (to enable pushing).
* However, units for which pushing is deactivated should collide against everyone.
* Units that don't block movement never participate in pushing, but they also
* shouldn't collide with pushing units.
*/
bool ShouldCollideWithMovingUnits() const
{
return !m_Pushing && m_BlockMovement;
}
/**
* Returns an appropriate obstruction filter for use with path requests.
*/
ControlGroupMovementObstructionFilter GetObstructionFilter() const
{
return ControlGroupMovementObstructionFilter(ShouldCollideWithMovingUnits(), GetGroup());
}
/**
* Filter a specific tag on top of the existing control groups.
*/
SkipTagAndControlGroupObstructionFilter GetObstructionFilter(const ICmpObstructionManager::tag_t& tag) const
{
return SkipTagAndControlGroupObstructionFilter(tag, ShouldCollideWithMovingUnits(), GetGroup());
}
/**
* Decide whether to approximate the given range from a square target as a circle,
* rather than as a square.
*/
bool ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const;
/**
* Create a PathGoal from a move request.
* @returns true if the goal was successfully created.
*/
bool ComputeGoal(PathGoal& out, const MoveRequest& moveRequest) const;
/**
* Compute a path to the given goal from the given position.
* Might go in a straight line immediately, or might start an asynchronous path request.
*/
void ComputePathToGoal(const CFixedVector2D& from, const PathGoal& goal);
/**
* Start an asynchronous long path query.
*/
void RequestLongPath(const CFixedVector2D& from, const PathGoal& goal);
/**
* Start an asynchronous short path query.
* @param extendRange - if true, extend the search range to at least the distance to the goal.
*/
void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool extendRange);
/**
* General handler for MoveTo interface functions.
*/
bool MoveTo(MoveRequest request);
/**
* Convert a path into a renderable list of lines
*/
void RenderPath(const WaypointPath& path, std::vector& lines, CColor color);
void RenderSubmit(SceneCollector& collector);
};
REGISTER_COMPONENT_TYPE(UnitMotion)
bool CCmpUnitMotion::RejectFartherPaths(const PathGoal& goal, const WaypointPath& path, const CFixedVector2D& pos) const
{
if (path.m_Waypoints.empty())
return false;
// Reject the new path if it does not lead us closer to the target's position.
if (goal.DistanceToPoint(pos) <= goal.DistanceToPoint(CFixedVector2D(path.m_Waypoints.front().x, path.m_Waypoints.front().z)))
return true;
return false;
}
void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path)
{
// Ignore obsolete path requests
if (ticket != m_ExpectedPathTicket.m_Ticket || m_MoveRequest.m_Type == MoveRequest::NONE)
return;
Ticket::Type ticketType = m_ExpectedPathTicket.m_Type;
m_ExpectedPathTicket.clear();
// If we not longer have a position, we won't be able to do much.
// Fail in the next Move() call.
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return;
CFixedVector2D pos = cmpPosition->GetPosition2D();
// Assume all long paths were towards the goal, and assume short paths were if there are no long waypoints.
bool pathedTowardsGoal = ticketType == Ticket::LONG_PATH || m_LongPath.m_Waypoints.empty();
// Check if we need to run the short-path hack (warning: tricky control flow).
bool shortPathHack = false;
if (path.m_Waypoints.empty())
{
// No waypoints means pathing failed. If this was a long-path, try the short-path hack.
if (!pathedTowardsGoal)
return;
shortPathHack = ticketType == Ticket::LONG_PATH;
}
else if (PathGoal goal; pathedTowardsGoal && ComputeGoal(goal, m_MoveRequest) && RejectFartherPaths(goal, path, pos))
{
// Reject paths that would take the unit further away from the goal.
// This assumes that we prefer being closer 'as the crow flies' to unreachable goals.
// This is a hack of sorts around units 'dancing' between two positions (see e.g. #3144),
// but never actually failing to move, ergo never actually informing unitAI that it succeeds/fails.
// (for short paths, only do so if aiming directly for the goal
// as sub-goals may be farther than we are).
// If this was a long-path and we no longer have waypoints, try the short-path hack.
if (!m_LongPath.m_Waypoints.empty())
return;
shortPathHack = ticketType == Ticket::LONG_PATH;
}
// Short-path hack: if the long-range pathfinder doesn't find an acceptable path, push a fake waypoint at the goal.
// This means HandleObstructedMove will use the short-pathfinder to try and reach it,
// and that may find a path as the vertex pathfinder is more precise.
if (shortPathHack)
{
// If we're resorting to the short-path hack, the situation is dire. Most likely, the goal is unreachable.
// We want to find a path or fail fast. Bump failed movements so the short pathfinder will run at max-range
// right away. This is safe from a performance PoV because it can only happen if the target is unreachable to
// the long-range pathfinder, which is rare, and since the entity will fail to move if the goal is actually unreachable,
// the failed movements will be increased to MAX anyways, so just shortcut.
m_FailedMovements = MAX_FAILED_MOVEMENTS - 2;
CFixedVector2D targetPos;
if (ComputeTargetPosition(targetPos))
m_LongPath.m_Waypoints.emplace_back(Waypoint{ targetPos.X, targetPos.Y });
return;
}
if (ticketType == Ticket::LONG_PATH)
{
m_LongPath = path;
// Long paths don't properly follow diagonals because of JPS/the grid. Since units now take time turning,
// they can actually slow down substantially if they have to do a one navcell diagonal movement,
// which is somewhat common at the beginning of a new path.
// For that reason, if the first waypoint is really close, check if we can't go directly to the second.
if (m_LongPath.m_Waypoints.size() >= 2)
{
const Waypoint& firstWpt = m_LongPath.m_Waypoints.back();
if (CFixedVector2D(firstWpt.x - pos.X, firstWpt.z - pos.Y).CompareLength(Pathfinding::NAVCELL_SIZE * 4) <= 0)
{
CmpPtr cmpPathfinder(GetSystemEntity());
ENSURE(cmpPathfinder);
const Waypoint& secondWpt = m_LongPath.m_Waypoints[m_LongPath.m_Waypoints.size() - 2];
if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, secondWpt.x, secondWpt.z, m_Clearance, m_PassClass))
m_LongPath.m_Waypoints.pop_back();
}
}
}
else
m_ShortPath = path;
m_FollowKnownImperfectPathCountdown = 0;
if (!pathedTowardsGoal)
return;
// Performance hack: If we were pathing towards the goal and this new path won't put us in range,
// it's highly likely that we are going somewhere unreachable.
// However, Move() will try to recompute the path every turn, which can be quite slow.
// To avoid this, act as if our current path leads us to the correct destination.
// NB: for short-paths, the problem might be that the search space is too small
// but we'll still follow this path until the en and try again then.
// Because we reject farther paths, it works out.
if (PathingUpdateNeeded(pos))
{
// Inform other components early, as they might have better behaviour than waiting for the path to carry out.
// Send OBSTRUCTED at first - moveFailed is likely to trigger path recomputation and we might end up
// recomputing too often for nothing.
if (!IncrementFailedMovementsAndMaybeNotify())
MoveObstructed();
// We'll automatically recompute a path when this reaches 0, as a way to improve behaviour.
// (See D665 - this is needed because the target may be moving, and we should adjust to that).
m_FollowKnownImperfectPathCountdown = KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN;
}
}
void CCmpUnitMotion::OnTurnStart()
{
if (PossiblyAtDestination())
MoveSucceeded();
else if (!TargetHasValidPosition())
{
// Scrap waypoints - we don't know where to go.
// If the move request remains unchanged and the target again has a valid position later on,
// moving will be resumed.
// Units may want to move to move to the target's last known position,
// but that should be decided by UnitAI (handling MoveFailed), not UnitMotion.
m_LongPath.m_Waypoints.clear();
m_ShortPath.m_Waypoints.clear();
MoveFailed();
}
}
void CCmpUnitMotion::PreMove(CCmpUnitMotionManager::MotionState& state)
{
state.ignore = !m_Pushing || !m_BlockMovement;
state.wasObstructed = false;
state.wentStraight = false;
// If we were idle and will still be, no need for an update.
state.needUpdate = state.cmpPosition->IsInWorld() &&
- (m_CurSpeed != fixed::Zero() || m_MoveRequest.m_Type != MoveRequest::NONE);
+ (m_CurrentSpeed != fixed::Zero() || m_MoveRequest.m_Type != MoveRequest::NONE);
if (!m_BlockMovement)
return;
state.controlGroup = IsFormationMember() ? m_FormationController : INVALID_ENTITY;
// Update moving flag, this is an internal construct used for pushing,
// so it does not really reflect whether the unit is actually moving or not.
state.isMoving = m_Pushing && m_MoveRequest.m_Type != MoveRequest::NONE;
CmpPtr cmpObstruction(GetEntityHandle());
if (cmpObstruction)
cmpObstruction->SetMovingFlag(state.isMoving);
}
void CCmpUnitMotion::Move(CCmpUnitMotionManager::MotionState& state, fixed dt)
{
PROFILE("Move");
// If we're chasing a potentially-moving unit and are currently close
// enough to its current position, and we can head in a straight line
// to it, then throw away our current path and go straight to it.
state.wentStraight = TryGoingStraightToTarget(state.initialPos, true);
- state.wasObstructed = PerformMove(dt, state.cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, state.pos, state.angle);
+ state.wasObstructed = PerformMove(dt, state.cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, state.pos, state.speed, state.angle);
}
void CCmpUnitMotion::PostMove(CCmpUnitMotionManager::MotionState& state, fixed dt)
{
// Update our speed over this turn so that the visual actor shows the correct animation.
if (state.pos == state.initialPos)
{
if (state.angle != state.initialAngle)
state.cmpPosition->TurnTo(state.angle);
- UpdateMovementState(fixed::Zero());
+ UpdateMovementState(fixed::Zero(), fixed::Zero());
}
else
{
// Update the Position component after our movement (if we actually moved anywhere)
CFixedVector2D offset = state.pos - state.initialPos;
// When moving always set the angle in the direction of the movement,
// if we are not trying to move, assume this is pushing-related movement,
// and maintain the current angle instead.
if (IsMoveRequested())
state.angle = atan2_approx(offset.X, offset.Y);
state.cmpPosition->MoveAndTurnTo(state.pos.X, state.pos.Y, state.angle);
// Calculate the mean speed over this past turn.
- UpdateMovementState(offset.Length() / dt);
+ UpdateMovementState(state.speed, offset.Length() / dt);
}
if (state.wasObstructed && HandleObstructedMove(state.pos != state.initialPos))
return;
else if (!state.wasObstructed && state.pos != state.initialPos)
m_FailedMovements = 0;
// If we moved straight, and didn't quite finish the path, reset - we'll update it next turn if still OK.
if (state.wentStraight && !state.wasObstructed)
m_ShortPath.m_Waypoints.clear();
// We may need to recompute our path sometimes (e.g. if our target moves).
// Since we request paths asynchronously anyways, this does not need to be done before moving.
if (!state.wentStraight && PathingUpdateNeeded(state.pos))
{
PathGoal goal;
if (ComputeGoal(goal, m_MoveRequest))
ComputePathToGoal(state.pos, goal);
}
else if (m_FollowKnownImperfectPathCountdown > 0)
--m_FollowKnownImperfectPathCountdown;
}
bool CCmpUnitMotion::PossiblyAtDestination() const
{
if (m_MoveRequest.m_Type == MoveRequest::NONE)
return false;
CmpPtr cmpObstructionManager(GetSystemEntity());
ENSURE(cmpObstructionManager);
if (m_MoveRequest.m_Type == MoveRequest::POINT)
return cmpObstructionManager->IsInPointRange(GetEntityId(), m_MoveRequest.m_Position.X, m_MoveRequest.m_Position.Y, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false);
if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
return cmpObstructionManager->IsInTargetRange(GetEntityId(), m_MoveRequest.m_Entity, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false);
if (m_MoveRequest.m_Type == MoveRequest::OFFSET)
{
CmpPtr cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity);
if (cmpControllerMotion && cmpControllerMotion->IsMoveRequested())
return false;
// In formation, return a match only if we are exactly at the target position.
// Otherwise, units can go in an infinite "walzting" loop when the Idle formation timer
// reforms them.
CFixedVector2D targetPos;
ComputeTargetPosition(targetPos);
CmpPtr cmpPosition(GetEntityHandle());
return (targetPos-cmpPosition->GetPosition2D()).CompareLength(fixed::Zero()) <= 0;
}
return false;
}
-bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, entity_angle_t& angle) const
+bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle) const
{
// If there are no waypoint, behave as though we were obstructed and let HandleObstructedMove handle it.
if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty())
return true;
// Wrap the angle to (-Pi, Pi].
while (angle > entity_angle_t::Pi())
angle -= entity_angle_t::Pi() * 2;
while (angle < -entity_angle_t::Pi())
angle += entity_angle_t::Pi() * 2;
// TODO: there's some asymmetry here when units look at other
// units' positions - the result will depend on the order of execution.
// Maybe we should split the updates into multiple phases to minimise
// that problem.
CmpPtr cmpPathfinder(GetSystemEntity());
ENSURE(cmpPathfinder);
fixed basicSpeed = m_Speed;
// If in formation, run to keep up; otherwise just walk.
if (IsMovingAsFormation())
basicSpeed = m_Speed.Multiply(m_RunMultiplier);
// Find the speed factor of the underlying terrain.
// (We only care about the tile we start on - it doesn't matter if we're moving
// partially onto a much slower/faster tile).
// TODO: Terrain-dependent speeds are not currently supported.
fixed terrainSpeed = fixed::FromInt(1);
fixed maxSpeed = basicSpeed.Multiply(terrainSpeed);
fixed timeLeft = dt;
fixed zero = fixed::Zero();
ICmpObstructionManager::tag_t specificIgnore;
if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
{
CmpPtr cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
if (cmpTargetObstruction)
specificIgnore = cmpTargetObstruction->GetObstruction();
}
while (timeLeft > zero)
{
// If we ran out of path, we have to stop.
if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty())
break;
CFixedVector2D target;
if (shortPath.m_Waypoints.empty())
target = CFixedVector2D(longPath.m_Waypoints.back().x, longPath.m_Waypoints.back().z);
else
target = CFixedVector2D(shortPath.m_Waypoints.back().x, shortPath.m_Waypoints.back().z);
CFixedVector2D offset = target - pos;
- if (turnRate > zero && !offset.IsZero())
- {
- fixed maxRotation = turnRate.Multiply(timeLeft);
- fixed angleDiff = angle - atan2_approx(offset.X, offset.Y);
- if (angleDiff != zero)
+
+ fixed angleDiff = angle - atan2_approx(offset.X, offset.Y);
+ fixed absoluteAngleDiff = angleDiff.Absolute();
+ if (absoluteAngleDiff > entity_angle_t::Pi())
+ absoluteAngleDiff = entity_angle_t::Pi() * 2 - absoluteAngleDiff;
+
+ // We only rotate to the instantTurnAngle angle. The rest we rotate during movement.
+ if (absoluteAngleDiff > m_InstantTurnAngle)
+ {
+ // Stop moving when rotating this far.
+ speed = zero;
+ if (turnRate > zero && !offset.IsZero())
{
- fixed absoluteAngleDiff = angleDiff.Absolute();
- if (absoluteAngleDiff > entity_angle_t::Pi())
- absoluteAngleDiff = entity_angle_t::Pi() * 2 - absoluteAngleDiff;
+ fixed maxRotation = turnRate.Multiply(timeLeft);
// Figure out whether rotating will increase or decrease the angle, and how far we need to rotate in that direction.
int direction = (entity_angle_t::Zero() < angleDiff && angleDiff <= entity_angle_t::Pi()) || angleDiff < -entity_angle_t::Pi() ? -1 : 1;
// Can't rotate far enough, just rotate in the correct direction.
- if (absoluteAngleDiff > maxRotation)
+ if (absoluteAngleDiff - m_InstantTurnAngle > maxRotation)
{
angle += maxRotation * direction;
if (angle * direction > entity_angle_t::Pi())
angle -= entity_angle_t::Pi() * 2 * direction;
break;
}
// Rotate towards the next waypoint and continue moving.
angle = atan2_approx(offset.X, offset.Y);
- // Give some 'free' rotation for angles below 0.5 radians.
- timeLeft = (std::min(maxRotation, maxRotation - absoluteAngleDiff + fixed::FromInt(1)/2)) / turnRate;
+ timeLeft = std::min(maxRotation, maxRotation - absoluteAngleDiff + m_InstantTurnAngle) / turnRate;
}
}
+ else
+ {
+ // Modify the speed depending on the angle difference.
+ fixed sin, cos;
+ sincos_approx(angleDiff, sin, cos);
+ speed = speed.Multiply(cos);
+ }
// Work out how far we can travel in timeLeft.
- fixed maxdist = maxSpeed.Multiply(timeLeft);
+ fixed accelTime = std::min(timeLeft, (maxSpeed - speed) / m_Acceleration);
+ fixed accelDist = speed.Multiply(accelTime) + accelTime.Square().Multiply(m_Acceleration) / 2;
+ fixed maxdist = accelDist + maxSpeed.Multiply(timeLeft - accelTime);
// If the target is close, we can move there directly.
fixed offsetLength = offset.Length();
if (offsetLength <= maxdist)
{
if (cmpPathfinder->CheckMovement(GetObstructionFilter(specificIgnore), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass))
{
pos = target;
// Spend the rest of the time heading towards the next waypoint.
- timeLeft = (maxdist - offsetLength) / maxSpeed;
+ // Either we still need to accelerate after, or we have reached maxSpeed.
+ // The former is much less likely than the latter: usually we can reach
+ // maxSpeed within one waypoint. So the Sqrt is not too bad.
+ if (offsetLength <= accelDist)
+ {
+ fixed requiredTime = (-speed + (speed.Square() + offsetLength.Multiply(m_Acceleration).Multiply(fixed::FromInt(2))).Sqrt()) / m_Acceleration;
+ timeLeft -= requiredTime;
+ speed += m_Acceleration.Multiply(requiredTime);
+ }
+ else
+ {
+ timeLeft -= accelTime + (offsetLength - accelDist) / maxSpeed;
+ speed = maxSpeed;
+ }
if (shortPath.m_Waypoints.empty())
longPath.m_Waypoints.pop_back();
else
shortPath.m_Waypoints.pop_back();
continue;
}
else
{
// Error - path was obstructed.
return true;
}
}
else
{
// Not close enough, so just move in the right direction.
offset.Normalize(maxdist);
target = pos + offset;
+ speed = std::min(maxSpeed, speed + m_Acceleration.Multiply(timeLeft));
+
if (cmpPathfinder->CheckMovement(GetObstructionFilter(specificIgnore), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass))
pos = target;
else
return true;
break;
}
}
return false;
}
-void CCmpUnitMotion::UpdateMovementState(entity_pos_t speed)
+void CCmpUnitMotion::UpdateMovementState(entity_pos_t speed, entity_pos_t meanSpeed)
{
CmpPtr cmpVisual(GetEntityHandle());
if (cmpVisual)
{
- if (speed == fixed::Zero())
+ if (meanSpeed == fixed::Zero())
cmpVisual->SelectMovementAnimation("idle", fixed::FromInt(1));
else
- cmpVisual->SelectMovementAnimation(speed > (m_WalkSpeed / 2).Multiply(m_RunMultiplier + fixed::FromInt(1)) ? "run" : "walk", speed);
+ cmpVisual->SelectMovementAnimation(meanSpeed > (m_WalkSpeed / 2).Multiply(m_RunMultiplier + fixed::FromInt(1)) ? "run" : "walk", meanSpeed);
}
- m_CurSpeed = speed;
+ m_CurrentSpeed = speed;
}
bool CCmpUnitMotion::HandleObstructedMove(bool moved)
{
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return false;
// We failed to move, inform other components as they might handle it.
// (don't send messages on the first failure, as that would be too noisy).
// Also don't increment above the initial MoveObstructed message if we actually manage to move a little.
if (!moved || m_FailedMovements < 2)
{
if (!IncrementFailedMovementsAndMaybeNotify() && m_FailedMovements >= 2)
MoveObstructed();
}
PathGoal goal;
if (!ComputeGoal(goal, m_MoveRequest))
return false;
// At this point we have a position in the world since ComputeGoal checked for that.
CFixedVector2D pos = cmpPosition->GetPosition2D();
// Assume that we are merely obstructed and the long path is salvageable, so try going around the obstruction.
// This could be a separate function, but it doesn't really make sense to call it outside of here, and I can't find a name.
// I use an IIFE to have nice 'return' semantics still.
if ([&]() -> bool {
// If the goal is close enough, we should ignore any remaining long waypoint and just
// short path there directly, as that improves behaviour in general - see D2095).
if (InShortPathRange(goal, pos))
return false;
// On rare occasions, when following a short path, we can end up in a position where
// the short pathfinder thinks we are inside an obstruction (and can leave)
// but the CheckMovement logic doesn't. I believe the cause is a small numerical difference
// in their calculation, but haven't been able to pinpoint it precisely.
// In those cases, the solution is to back away to prevent the short-pathfinder from being confused.
// TODO: this should only be done if we're obstructed by a static entity.
if (!m_ShortPath.m_Waypoints.empty() && m_FailedMovements == BACKUP_HACK_DELAY)
{
Waypoint next = m_ShortPath.m_Waypoints.back();
CFixedVector2D backUp(pos.X - next.x, pos.Y - next.z);
backUp.Normalize();
next.x = pos.X + backUp.X;
next.z = pos.Y + backUp.Y;
m_ShortPath.m_Waypoints.push_back(next);
return true;
}
// Delete the next waypoint if it's reasonably close,
// because it might be blocked by units and thus unreachable.
// NB: this number is tricky. Make it too high, and units start going down dead ends, which looks odd (#5795)
// Make it too low, and they might get stuck behind other obstructed entities.
// It also has performance implications because it calls the short-pathfinder.
fixed skipbeyond = std::max(ShortPathSearchRange() / 3, Pathfinding::NAVCELL_SIZE * 8);
if (m_LongPath.m_Waypoints.size() > 1 &&
(pos - CFixedVector2D(m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z)).CompareLength(skipbeyond) < 0)
{
m_LongPath.m_Waypoints.pop_back();
}
else if (ShouldAlternatePathfinder())
{
// Recompute the whole thing occasionally, in case we got stuck in a dead end from removing long waypoints.
RequestLongPath(pos, goal);
return true;
}
if (m_LongPath.m_Waypoints.empty())
return false;
// Compute a short path in the general vicinity of the next waypoint, to help pathfinding in crowds.
// The goal here is to manage to move in the general direction of our target, not to be super accurate.
fixed radius = Clamp(skipbeyond/3, Pathfinding::NAVCELL_SIZE * 4, Pathfinding::NAVCELL_SIZE * 12);
PathGoal subgoal = { PathGoal::CIRCLE, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z, radius };
RequestShortPath(pos, subgoal, false);
return true;
}()) return true;
// If we couldn't use a workaround, try recomputing the entire path.
ComputePathToGoal(pos, goal);
return true;
}
bool CCmpUnitMotion::TargetHasValidPosition(const MoveRequest& moveRequest) const
{
if (moveRequest.m_Type != MoveRequest::ENTITY)
return true;
CmpPtr cmpPosition(GetSimContext(), moveRequest.m_Entity);
return cmpPosition && cmpPosition->IsInWorld();
}
bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out, const MoveRequest& moveRequest) const
{
if (moveRequest.m_Type == MoveRequest::POINT)
{
out = moveRequest.m_Position;
return true;
}
CmpPtr cmpTargetPosition(GetSimContext(), moveRequest.m_Entity);
if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld())
return false;
if (moveRequest.m_Type == MoveRequest::OFFSET)
{
// There is an offset, so compute it relative to orientation
entity_angle_t angle = cmpTargetPosition->GetRotation().Y;
CFixedVector2D offset = moveRequest.GetOffset().Rotate(angle);
out = cmpTargetPosition->GetPosition2D() + offset;
}
else
{
out = cmpTargetPosition->GetPosition2D();
// Position is only updated after all units have moved & pushed.
// Therefore, we may need to interpolate the target position, depending on when this call takes place during the turn:
// - On "Turn Start", we'll check positions directly without interpolation.
// - During movement, we'll call this for direct-pathing & we need to interpolate
// (this way, we move where the unit will end up at the end of _this_ turn, making it match on next turn start).
// - After movement, we'll call this to request paths & we need to interpolate
// (this way, we'll move where the unit ends up in the end of _next_ turn, making it a match in 2 turns).
// TODO: This does not really aim many turns in advance, with orthogonal trajectories it probably should.
CmpPtr cmpUnitMotion(GetSimContext(), moveRequest.m_Entity);
CmpPtr cmpUnitMotionManager(GetSystemEntity());
bool needInterpolation = cmpUnitMotion && cmpUnitMotion->IsMoveRequested() && cmpUnitMotionManager->ComputingMotion();
if (needInterpolation)
{
// Add predicted movement.
CFixedVector2D tempPos = out + (out - cmpTargetPosition->GetPreviousPosition2D());
out = tempPos;
}
}
return true;
}
bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from, bool updatePaths)
{
// Assume if we have short paths we want to follow them.
// Exception: offset movement (formations) generally have very short deltas
// and to look good we need them to walk-straight most of the time.
if (!IsFormationMember() && !m_ShortPath.m_Waypoints.empty())
return false;
CFixedVector2D targetPos;
if (!ComputeTargetPosition(targetPos))
return false;
CmpPtr cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return false;
// Move the goal to match the target entity's new position
PathGoal goal;
if (!ComputeGoal(goal, m_MoveRequest))
return false;
goal.x = targetPos.X;
goal.z = targetPos.Y;
// (we ignore changes to the target's rotation, since only buildings are
// square and buildings don't move)
// Find the point on the goal shape that we should head towards
CFixedVector2D goalPos = goal.NearestPointOnGoal(from);
// Fail if the target is too far away
if ((goalPos - from).CompareLength(DIRECT_PATH_RANGE) > 0)
return false;
// Check if there's any collisions on that route.
// For entity goals, skip only the specific obstruction tag or with e.g. walls we might ignore too many entities.
ICmpObstructionManager::tag_t specificIgnore;
if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
{
CmpPtr cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
if (cmpTargetObstruction)
specificIgnore = cmpTargetObstruction->GetObstruction();
}
// Check movement against units - we want to use the short pathfinder to walk around those if needed.
if (specificIgnore.valid())
{
if (!cmpPathfinder->CheckMovement(GetObstructionFilter(specificIgnore), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
return false;
}
else if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
return false;
if (!updatePaths)
return true;
// That route is okay, so update our path
m_LongPath.m_Waypoints.clear();
m_ShortPath.m_Waypoints.clear();
m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y });
return true;
}
bool CCmpUnitMotion::PathingUpdateNeeded(const CFixedVector2D& from) const
{
if (m_MoveRequest.m_Type == MoveRequest::NONE)
return false;
CFixedVector2D targetPos;
if (!ComputeTargetPosition(targetPos))
return false;
if (m_FollowKnownImperfectPathCountdown > 0 && (!m_LongPath.m_Waypoints.empty() || !m_ShortPath.m_Waypoints.empty()))
return false;
if (PossiblyAtDestination())
return false;
// Get the obstruction shape and translate it where we estimate the target to be.
ICmpObstructionManager::ObstructionSquare estimatedTargetShape;
if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
{
CmpPtr cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
if (cmpTargetObstruction)
cmpTargetObstruction->GetObstructionSquare(estimatedTargetShape);
}
estimatedTargetShape.x = targetPos.X;
estimatedTargetShape.z = targetPos.Y;
CmpPtr cmpObstruction(GetEntityHandle());
ICmpObstructionManager::ObstructionSquare shape;
if (cmpObstruction)
cmpObstruction->GetObstructionSquare(shape);
// Translate our own obstruction shape to our last waypoint or our current position, lacking that.
if (m_LongPath.m_Waypoints.empty() && m_ShortPath.m_Waypoints.empty())
{
shape.x = from.X;
shape.z = from.Y;
}
else
{
const Waypoint& lastWaypoint = m_LongPath.m_Waypoints.empty() ? m_ShortPath.m_Waypoints.front() : m_LongPath.m_Waypoints.front();
shape.x = lastWaypoint.x;
shape.z = lastWaypoint.z;
}
CmpPtr cmpObstructionManager(GetSystemEntity());
ENSURE(cmpObstructionManager);
// Increase the ranges with distance, to avoid recomputing every turn against units that are moving and far-away for example.
entity_pos_t distance = (from - CFixedVector2D(estimatedTargetShape.x, estimatedTargetShape.z)).Length();
// TODO: it could be worth computing this based on time to collision instead of linear distance.
entity_pos_t minRange = std::max(m_MoveRequest.m_MinRange - distance / TARGET_UNCERTAINTY_MULTIPLIER, entity_pos_t::Zero());
entity_pos_t maxRange = m_MoveRequest.m_MaxRange < entity_pos_t::Zero() ? m_MoveRequest.m_MaxRange :
m_MoveRequest.m_MaxRange + distance / TARGET_UNCERTAINTY_MULTIPLIER;
if (cmpObstructionManager->AreShapesInRange(shape, estimatedTargetShape, minRange, maxRange, false))
return false;
return true;
}
void CCmpUnitMotion::FaceTowardsPoint(entity_pos_t x, entity_pos_t z)
{
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return;
CFixedVector2D pos = cmpPosition->GetPosition2D();
FaceTowardsPointFromPos(pos, x, z);
}
void CCmpUnitMotion::FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z)
{
CFixedVector2D target(x, z);
CFixedVector2D offset = target - pos;
if (!offset.IsZero())
{
entity_angle_t angle = atan2_approx(offset.X, offset.Y);
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition)
return;
cmpPosition->TurnTo(angle);
}
}
// The pathfinder cannot go to "rounded rectangles" goals, which are what happens with square targets and a non-null range.
// Depending on what the best approximation is, we either pretend the target is a circle or a square.
// One needs to be careful that the approximated geometry will be in the range.
bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const
{
// Given a square, plus a target range we should reach, the shape at that distance
// is a round-cornered square which we can approximate as either a circle or as a square.
// Previously, we used the shape that minimized the worst-case error.
// However that is unsage in some situations. So let's be less clever and
// just check if our range is at least three times bigger than the circleradius
return (range > circleRadius*3);
}
bool CCmpUnitMotion::ComputeGoal(PathGoal& out, const MoveRequest& moveRequest) const
{
if (moveRequest.m_Type == MoveRequest::NONE)
return false;
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return false;
CFixedVector2D pos = cmpPosition->GetPosition2D();
CFixedVector2D targetPosition;
if (!ComputeTargetPosition(targetPosition, moveRequest))
return false;
ICmpObstructionManager::ObstructionSquare targetObstruction;
if (moveRequest.m_Type == MoveRequest::ENTITY)
{
CmpPtr cmpTargetObstruction(GetSimContext(), moveRequest.m_Entity);
if (cmpTargetObstruction)
cmpTargetObstruction->GetObstructionSquare(targetObstruction);
}
targetObstruction.x = targetPosition.X;
targetObstruction.z = targetPosition.Y;
ICmpObstructionManager::ObstructionSquare obstruction;
CmpPtr cmpObstruction(GetEntityHandle());
if (cmpObstruction)
cmpObstruction->GetObstructionSquare(obstruction);
else
{
obstruction.x = pos.X;
obstruction.z = pos.Y;
}
CmpPtr cmpObstructionManager(GetSystemEntity());
ENSURE(cmpObstructionManager);
entity_pos_t distance = cmpObstructionManager->DistanceBetweenShapes(obstruction, targetObstruction);
out.x = targetObstruction.x;
out.z = targetObstruction.z;
out.hw = targetObstruction.hw;
out.hh = targetObstruction.hh;
out.u = targetObstruction.u;
out.v = targetObstruction.v;
if (moveRequest.m_MinRange > fixed::Zero() || moveRequest.m_MaxRange > fixed::Zero() ||
targetObstruction.hw > fixed::Zero())
out.type = PathGoal::SQUARE;
else
{
out.type = PathGoal::POINT;
return true;
}
entity_pos_t circleRadius = CFixedVector2D(targetObstruction.hw, targetObstruction.hh).Length();
// TODO: because we cannot move to rounded rectangles, we have to make conservative approximations.
// This means we might end up in a situation where cons(max-range) < min range < max range < cons(min-range)
// When going outside of the min-range or inside the max-range, the unit will still go through the correct range
// but if it moves fast enough, this might not be picked up by PossiblyAtDestination().
// Fixing this involves moving to rounded rectangles, or checking more often in PerformMove().
// In the meantime, one should avoid that 'Speed over a turn' > MaxRange - MinRange, in case where
// min-range is not 0 and max-range is not infinity.
if (distance < moveRequest.m_MinRange)
{
// Distance checks are nearest edge to nearest edge, so we need to account for our clearance
// and we must make sure diagonals also fit so multiply by slightly more than sqrt(2)
entity_pos_t goalDistance = moveRequest.m_MinRange + m_Clearance * 3 / 2;
if (ShouldTreatTargetAsCircle(moveRequest.m_MinRange, circleRadius))
{
// We are safely away from the obstruction itself if we are away from the circumscribing circle
out.type = PathGoal::INVERTED_CIRCLE;
out.hw = circleRadius + goalDistance;
}
else
{
out.type = PathGoal::INVERTED_SQUARE;
out.hw = targetObstruction.hw + goalDistance;
out.hh = targetObstruction.hh + goalDistance;
}
}
else if (moveRequest.m_MaxRange >= fixed::Zero() && distance > moveRequest.m_MaxRange)
{
if (ShouldTreatTargetAsCircle(moveRequest.m_MaxRange, circleRadius))
{
entity_pos_t goalDistance = moveRequest.m_MaxRange;
// We must go in-range of the inscribed circle, not the circumscribing circle.
circleRadius = std::min(targetObstruction.hw, targetObstruction.hh);
out.type = PathGoal::CIRCLE;
out.hw = circleRadius + goalDistance;
}
else
{
// The target is large relative to our range, so treat it as a square and
// get close enough that the diagonals come within range
entity_pos_t goalDistance = moveRequest.m_MaxRange * 2 / 3; // multiply by slightly less than 1/sqrt(2)
out.type = PathGoal::SQUARE;
entity_pos_t delta = std::max(goalDistance, m_Clearance + entity_pos_t::FromInt(4)/16); // ensure it's far enough to not intersect the building itself
out.hw = targetObstruction.hw + delta;
out.hh = targetObstruction.hh + delta;
}
}
// Do nothing in particular in case we are already in range.
return true;
}
void CCmpUnitMotion::ComputePathToGoal(const CFixedVector2D& from, const PathGoal& goal)
{
#if DISABLE_PATHFINDER
{
CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY);
CFixedVector2D goalPos = m_FinalGoal.NearestPointOnGoal(from);
m_LongPath.m_Waypoints.clear();
m_ShortPath.m_Waypoints.clear();
m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y });
return;
}
#endif
// If the target is close enough, hope that we'll be able to go straight next turn.
if (!ShouldAlternatePathfinder() && TryGoingStraightToTarget(from, false))
{
// NB: since we may fail to move straight next turn, we should edge our bets.
// Since the 'go straight' logic currently fires only if there's no short path,
// we'll compute a long path regardless to make sure _that_ stays up to date.
// (it's also extremely likely to be very fast to compute, so no big deal).
m_ShortPath.m_Waypoints.clear();
RequestLongPath(from, goal);
return;
}
// Otherwise we need to compute a path.
// If it's close then just do a short path, not a long path
// TODO: If it's close on the opposite side of a river then we really
// need a long path, so we shouldn't simply check linear distance
// the check is arbitrary but should be a reasonably small distance.
// We want to occasionally compute a long path if we're computing short-paths, because the short path domain
// is bounded and thus it can't around very large static obstacles.
// Likewise, we want to compile a short-path occasionally when the target is far because we might be stuck
// on a navcell surrounded by impassable navcells, but the short-pathfinder could move us out of there.
bool shortPath = InShortPathRange(goal, from);
if (ShouldAlternatePathfinder())
shortPath = !shortPath;
if (shortPath)
{
m_LongPath.m_Waypoints.clear();
// Extend the range so that our first path is probably valid.
RequestShortPath(from, goal, true);
}
else
{
m_ShortPath.m_Waypoints.clear();
RequestLongPath(from, goal);
}
}
void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal)
{
CmpPtr cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return;
// this is by how much our waypoints will be apart at most.
// this value here seems sensible enough.
PathGoal improvedGoal = goal;
improvedGoal.maxdist = SHORT_PATH_MIN_SEARCH_RANGE - entity_pos_t::FromInt(1);
cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass);
m_ExpectedPathTicket.m_Type = Ticket::LONG_PATH;
m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId());
}
void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool extendRange)
{
CmpPtr cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return;
entity_pos_t searchRange = ShortPathSearchRange();
if (extendRange)
{
CFixedVector2D dist(from.X - goal.x, from.Y - goal.z);
if (dist.CompareLength(searchRange - entity_pos_t::FromInt(1)) >= 0)
{
searchRange = dist.Length() + fixed::FromInt(1);
if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE)
searchRange = SHORT_PATH_MAX_SEARCH_RANGE;
}
}
m_ExpectedPathTicket.m_Type = Ticket::SHORT_PATH;
m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, true, GetGroup(), GetEntityId());
}
bool CCmpUnitMotion::MoveTo(MoveRequest request)
{
PROFILE("MoveTo");
if (request.m_MinRange == request.m_MaxRange && !request.m_MinRange.IsZero())
LOGWARNING("MaxRange must be larger than MinRange; See CCmpUnitMotion.cpp for more information");
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return false;
PathGoal goal;
if (!ComputeGoal(goal, request))
return false;
m_MoveRequest = request;
m_FailedMovements = 0;
m_FollowKnownImperfectPathCountdown = 0;
ComputePathToGoal(cmpPosition->GetPosition2D(), goal);
return true;
}
bool CCmpUnitMotion::IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return false;
MoveRequest request(target, minRange, maxRange);
PathGoal goal;
if (!ComputeGoal(goal, request))
return false;
CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY);
CFixedVector2D pos = cmpPosition->GetPosition2D();
return cmpPathfinder->IsGoalReachable(pos.X, pos.Y, goal, m_PassClass);
}
void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector& lines, CColor color)
{
bool floating = false;
CmpPtr cmpPosition(GetEntityHandle());
if (cmpPosition)
floating = cmpPosition->CanFloat();
lines.clear();
std::vector waypointCoords;
for (size_t i = 0; i < path.m_Waypoints.size(); ++i)
{
float x = path.m_Waypoints[i].x.ToFloat();
float z = path.m_Waypoints[i].z.ToFloat();
waypointCoords.push_back(x);
waypointCoords.push_back(z);
lines.push_back(SOverlayLine());
lines.back().m_Color = color;
SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating);
}
float x = cmpPosition->GetPosition2D().X.ToFloat();
float z = cmpPosition->GetPosition2D().Y.ToFloat();
waypointCoords.push_back(x);
waypointCoords.push_back(z);
lines.push_back(SOverlayLine());
lines.back().m_Color = color;
SimRender::ConstructLineOnGround(GetSimContext(), waypointCoords, lines.back(), floating);
}
void CCmpUnitMotion::RenderSubmit(SceneCollector& collector)
{
if (!m_DebugOverlayEnabled)
return;
RenderPath(m_LongPath, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH);
RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOR_SHORT_PATH);
for (size_t i = 0; i < m_DebugOverlayLongPathLines.size(); ++i)
collector.Submit(&m_DebugOverlayLongPathLines[i]);
for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i)
collector.Submit(&m_DebugOverlayShortPathLines[i]);
}
#endif // INCLUDED_CCMPUNITMOTION
Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpUnitMotion.h (revision 25952)
+++ ps/trunk/source/simulation2/components/ICmpUnitMotion.h (revision 25953)
@@ -1,162 +1,173 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_ICMPUNITMOTION
#define INCLUDED_ICMPUNITMOTION
#include "simulation2/system/Interface.h"
#include "simulation2/components/ICmpPathfinder.h" // for pass_class_t
#include "simulation2/components/ICmpPosition.h" // for entity_pos_t
/**
* Motion interface for entities with complex movement capabilities.
* (Simpler motion is handled by ICmpMotion instead.)
*
* It should eventually support different movement speeds, moving to areas
* instead of points, moving as part of a group, moving as part of a formation,
* etc.
*/
class ICmpUnitMotion : public IComponent
{
public:
/**
* Attempt to walk into range of a to a given point, or as close as possible.
* The range is measured from the center of the unit.
* If cannot move anywhere at all, or if there is some other error, then returns false.
* Otherwise, returns true.
* If maxRange is negative, then the maximum range is treated as infinity.
*/
virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0;
/**
* Attempt to walk into range of a given target entity, or as close as possible.
* The range is measured between approximately the edges of the unit and the target, so that
* maxRange=0 is not unreachably close to the target.
* If the unit cannot move anywhere at all, or if there is some other error, then returns false.
* Otherwise, returns true.
* If maxRange is negative, then the maximum range is treated as infinity.
*/
virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
/**
* Join a formation, and move towards a given offset relative to the formation controller entity.
* The unit will remain 'in formation' fromthe perspective of UnitMotion
* until SetMemberOfFormation(INVALID_ENTITY) is passed.
*/
virtual void MoveToFormationOffset(entity_id_t controller, entity_pos_t x, entity_pos_t z) = 0;
/**
* Set/unset the unit as a formation member.
* @param controller - if INVALID_ENTITY, the unit is no longer a formation member. Otherwise it is and this is the controller.
*/
virtual void SetMemberOfFormation(entity_id_t controller) = 0;
/**
* Check if the target is reachable.
* Don't take this as absolute gospel since there are things that the pathfinder may not detect, such as
* entity obstructions in the way, but in general it should return satisfactory results.
* The interface is similar to MoveToTargetRange but the move is not attempted.
* @return true if the target is assumed reachable, false otherwise.
*/
virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
/**
* Turn to look towards the given point.
*/
virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0;
/**
* Stop moving immediately.
*/
virtual void StopMoving() = 0;
/**
- * Get the distance travelled over the last turn.
+ * Get the speed at the end of the current turn.
*/
virtual fixed GetCurrentSpeed() const = 0;
/**
* @returns true if the unit has a destination.
*/
virtual bool IsMoveRequested() const = 0;
/**
* Get the unit template walk speed after modifications.
*/
virtual fixed GetWalkSpeed() const = 0;
/**
* Get the unit template running (i.e. max) speed after modifications.
*/
virtual fixed GetRunMultiplier() const = 0;
/**
* Returns the ratio of GetSpeed() / GetWalkSpeed().
*/
virtual fixed GetSpeedMultiplier() const = 0;
/**
* Set the current movement speed.
* @param speed A multiplier of GetWalkSpeed().
*/
virtual void SetSpeedMultiplier(fixed multiplier) = 0;
/**
* Get the speed at which the unit intends to move.
* (regardless of whether the unit is moving or not right now).
*/
virtual fixed GetSpeed() const = 0;
/**
* @return the estimated position of the unit in @param dt seconds,
* following current paths. This is allowed to 'look into the future'.
*/
virtual CFixedVector2D EstimateFuturePosition(const fixed dt) const = 0;
/**
+ * Get the current acceleration.
+ */
+ virtual fixed GetAcceleration() const = 0;
+
+ /**
+ * Set the current acceleration.
+ * @param acceleration The acceleration.
+ */
+ virtual void SetAcceleration(fixed acceleration) = 0;
+
+ /**
* Set whether the unit will turn to face the target point after finishing moving.
*/
virtual void SetFacePointAfterMove(bool facePointAfterMove) = 0;
virtual bool GetFacePointAfterMove() const = 0;
/**
* Get the unit's passability class.
*/
virtual pass_class_t GetPassabilityClass() const = 0;
/**
* Get the passability class name (as defined in pathfinder.xml)
*/
virtual std::string GetPassabilityClassName() const = 0;
/**
* Get the unit clearance (used by the Obstruction component)
*/
virtual entity_pos_t GetUnitClearance() const = 0;
/**
* Toggle the rendering of debug info.
*/
virtual void SetDebugOverlay(bool enabled) = 0;
DECLARE_INTERFACE_TYPE(UnitMotion)
};
#endif // INCLUDED_ICMPUNITMOTION
Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp
===================================================================
--- ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp (revision 25952)
+++ ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp (revision 25953)
@@ -1,159 +1,171 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "ICmpUnitMotion.h"
#include "simulation2/system/InterfaceScripted.h"
#include "simulation2/scripting/ScriptComponent.h"
BEGIN_INTERFACE_WRAPPER(UnitMotion)
DEFINE_INTERFACE_METHOD("MoveToPointRange", ICmpUnitMotion, MoveToPointRange)
DEFINE_INTERFACE_METHOD("MoveToTargetRange", ICmpUnitMotion, MoveToTargetRange)
DEFINE_INTERFACE_METHOD("MoveToFormationOffset", ICmpUnitMotion, MoveToFormationOffset)
DEFINE_INTERFACE_METHOD("SetMemberOfFormation", ICmpUnitMotion, SetMemberOfFormation)
DEFINE_INTERFACE_METHOD("IsTargetRangeReachable", ICmpUnitMotion, IsTargetRangeReachable)
DEFINE_INTERFACE_METHOD("FaceTowardsPoint", ICmpUnitMotion, FaceTowardsPoint)
DEFINE_INTERFACE_METHOD("StopMoving", ICmpUnitMotion, StopMoving)
DEFINE_INTERFACE_METHOD("GetCurrentSpeed", ICmpUnitMotion, GetCurrentSpeed)
DEFINE_INTERFACE_METHOD("IsMoveRequested", ICmpUnitMotion, IsMoveRequested)
DEFINE_INTERFACE_METHOD("GetSpeed", ICmpUnitMotion, GetSpeed)
DEFINE_INTERFACE_METHOD("GetWalkSpeed", ICmpUnitMotion, GetWalkSpeed)
DEFINE_INTERFACE_METHOD("GetRunMultiplier", ICmpUnitMotion, GetRunMultiplier)
DEFINE_INTERFACE_METHOD("EstimateFuturePosition", ICmpUnitMotion, EstimateFuturePosition)
DEFINE_INTERFACE_METHOD("SetSpeedMultiplier", ICmpUnitMotion, SetSpeedMultiplier)
+DEFINE_INTERFACE_METHOD("GetAcceleration", ICmpUnitMotion, GetAcceleration)
+DEFINE_INTERFACE_METHOD("SetAcceleration", ICmpUnitMotion, SetAcceleration)
DEFINE_INTERFACE_METHOD("GetPassabilityClassName", ICmpUnitMotion, GetPassabilityClassName)
DEFINE_INTERFACE_METHOD("GetUnitClearance", ICmpUnitMotion, GetUnitClearance)
DEFINE_INTERFACE_METHOD("SetFacePointAfterMove", ICmpUnitMotion, SetFacePointAfterMove)
DEFINE_INTERFACE_METHOD("GetFacePointAfterMove", ICmpUnitMotion, GetFacePointAfterMove)
DEFINE_INTERFACE_METHOD("SetDebugOverlay", ICmpUnitMotion, SetDebugOverlay)
END_INTERFACE_WRAPPER(UnitMotion)
class CCmpUnitMotionScripted : public ICmpUnitMotion
{
public:
DEFAULT_SCRIPT_WRAPPER(UnitMotionScripted)
virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
{
return m_Script.Call("MoveToPointRange", x, z, minRange, maxRange);
}
virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{
return m_Script.Call("MoveToTargetRange", target, minRange, maxRange);
}
virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z)
{
m_Script.CallVoid("MoveToFormationOffset", target, x, z);
}
virtual void SetMemberOfFormation(entity_id_t controller)
{
m_Script.CallVoid("SetMemberOfFormation", controller);
}
virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{
return m_Script.Call("IsTargetRangeReachable", target, minRange, maxRange);
}
virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z)
{
m_Script.CallVoid("FaceTowardsPoint", x, z);
}
virtual void StopMoving()
{
m_Script.CallVoid("StopMoving");
}
virtual fixed GetCurrentSpeed() const
{
return m_Script.Call("GetCurrentSpeed");
}
virtual bool IsMoveRequested() const
{
return m_Script.Call("IsMoveRequested");
}
virtual fixed GetSpeed() const
{
return m_Script.Call("GetSpeed");
}
virtual fixed GetWalkSpeed() const
{
return m_Script.Call("GetWalkSpeed");
}
virtual fixed GetRunMultiplier() const
{
return m_Script.Call("GetRunMultiplier");
}
virtual void SetSpeedMultiplier(fixed multiplier)
{
m_Script.CallVoid("SetSpeedMultiplier", multiplier);
}
virtual fixed GetSpeedMultiplier() const
{
return m_Script.Call("GetSpeedMultiplier");
}
virtual CFixedVector2D EstimateFuturePosition(const fixed dt) const
{
return m_Script.Call("EstimateFuturePosition", dt);
}
+ virtual fixed GetAcceleration() const
+ {
+ return m_Script.Call("GetAcceleration");
+ }
+
+ virtual void SetAcceleration(fixed acceleration)
+ {
+ m_Script.CallVoid("SetAcceleration", acceleration);
+ }
+
virtual void SetFacePointAfterMove(bool facePointAfterMove)
{
m_Script.CallVoid("SetFacePointAfterMove", facePointAfterMove);
}
virtual bool GetFacePointAfterMove() const
{
return m_Script.Call("GetFacePointAfterMove");
}
virtual pass_class_t GetPassabilityClass() const
{
return m_Script.Call("GetPassabilityClass");
}
virtual std::string GetPassabilityClassName() const
{
return m_Script.Call("GetPassabilityClassName");
}
virtual entity_pos_t GetUnitClearance() const
{
return m_Script.Call("GetUnitClearance");
}
virtual void SetDebugOverlay(bool enabled)
{
m_Script.CallVoid("SetDebugOverlay", enabled);
}
};
REGISTER_COMPONENT_SCRIPT_WRAPPER(UnitMotionScripted)