Index: ps/trunk/binaries/data/mods/public/simulation/components/Damage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Damage.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Damage.js (nonexistent)
@@ -1,312 +0,0 @@
-function Damage() {}
-
-Damage.prototype.Schema =
- "";
-
-Damage.prototype.Init = function()
-{
-};
-
-/**
- * Gives the position of the given entity, taking the lateness into account.
- * @param {number} ent - Entity id of the entity we are finding the location for.
- * @param {number} lateness - The time passed since the expected time to fire the function.
- * @return {Vector3D} The location of the entity.
- */
-Damage.prototype.InterpolatedLocation = function(ent, lateness)
-{
- let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
- if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
- return undefined;
- let curPos = cmpTargetPosition.GetPosition();
- let prevPos = cmpTargetPosition.GetPreviousPosition();
- let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
- let turnLength = cmpTimer.GetLatestTurnLength();
- return new Vector3D(
- (curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength,
- 0,
- (curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength);
-};
-
-/**
- * Test if a point is inside of an entity's footprint.
- * @param {number} ent - Id of the entity we are checking with.
- * @param {Vector3D} point - The point we are checking with.
- * @param {number} lateness - The time passed since the expected time to fire the function.
- * @return {boolean} True if the point is inside of the entity's footprint.
- */
-Damage.prototype.TestCollision = function(ent, point, lateness)
-{
- let targetPosition = this.InterpolatedLocation(ent, lateness);
- if (!targetPosition)
- return false;
-
- let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
- if (!cmpFootprint)
- return false;
-
- let targetShape = cmpFootprint.GetShape();
-
- if (!targetShape)
- return false;
-
- if (targetShape.type == "circle")
- return targetPosition.horizDistanceToSquared(point) < targetShape.radius * targetShape.radius;
-
- if (targetShape.type == "square")
- {
- let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
- let distance = Vector2D.from3D(Vector3D.sub(point, targetPosition)).rotate(-angle);
- return Math.abs(distance.x) < targetShape.width / 2 && Math.abs(distance.y) < targetShape.depth / 2;
- }
-
- warn("TestCollision called with an invalid footprint shape");
- return false;
-};
-
-/**
- * Get the list of players affected by the damage.
- * @param {number} attackerOwner - The player id of the attacker.
- * @param {boolean} friendlyFire - A flag indicating if allied entities are also damaged.
- * @return {number[]} The ids of players need to be damaged.
- */
-Damage.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire)
-{
- if (!friendlyFire)
- return QueryPlayerIDInterface(attackerOwner).GetEnemies();
-
- return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
-};
-
-/**
- * Handles hit logic after the projectile travel time has passed.
- * @param {Object} data - The data sent by the caller.
- * @param {number} data.attacker - The entity id of the attacker.
- * @param {number} data.target - The entity id of the target.
- * @param {Vector2D} data.origin - The origin of the projectile hit.
- * @param {Object} data.strengths - Data of the form { 'hack': number, 'pierce': number, 'crush': number }.
- * @param {string} data.type - The type of damage.
- * @param {number} data.attackerOwner - The player id of the owner of the attacker.
- * @param {boolean} data.isSplash - A flag indicating if it's splash damage.
- * @param {Vector3D} data.position - The expected position of the target.
- * @param {number} data.projectileId - The id of the projectile.
- * @param {Vector3D} data.direction - The unit vector defining the direction.
- * @param {Object} data.bonus - The attack bonus template from the attacker.
- * @param {string} data.attackImpactSound - The name of the sound emited on impact.
- * @param {Object} data.statusEffects - Status effects eg. poisoning, burning etc.
- * ***When splash damage***
- * @param {boolean} data.friendlyFire - A flag indicating if allied entities are also damaged.
- * @param {number} data.radius - The radius of the splash damage.
- * @param {string} data.shape - The shape of the splash range.
- * @param {Object} data.splashBonus - The attack bonus template from the attacker.
- * @param {Object} data.splashStrengths - Data of the form { 'hack': number, 'pierce': number, 'crush': number }.
- */
-Damage.prototype.MissileHit = function(data, lateness)
-{
- if (!data.position)
- return;
-
- let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager);
- if (cmpSoundManager && data.attackImpactSound)
- cmpSoundManager.PlaySoundGroupAtPosition(data.attackImpactSound, data.position);
-
- // Do this first in case the direct hit kills the target
- if (data.isSplash)
- {
- this.CauseDamageOverArea({
- "attacker": data.attacker,
- "origin": Vector2D.from3D(data.position),
- "radius": data.radius,
- "shape": data.shape,
- "strengths": data.splashStrengths,
- "splashBonus": data.splashBonus,
- "direction": data.direction,
- "playersToDamage": this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire),
- "type": data.type,
- "attackerOwner": data.attackerOwner
- });
- }
-
- let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
-
- // Deal direct damage if we hit the main target
- // and if the target has DamageReceiver (not the case for a mirage for example)
- let cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
- if (cmpDamageReceiver && this.TestCollision(data.target, data.position, lateness))
- {
- data.multiplier = GetDamageBonus(data.attacker, data.target, data.type, data.bonus);
- this.CauseDamage(data);
- cmpProjectileManager.RemoveProjectile(data.projectileId);
-
- let cmpStatusReceiver = Engine.QueryInterface(data.target, IID_StatusEffectsReceiver);
- if (cmpStatusReceiver && data.statusEffects)
- cmpStatusReceiver.InflictEffects(data.statusEffects);
-
- return;
- }
-
- let targetPosition = this.InterpolatedLocation(data.target, lateness);
- if (!targetPosition)
- return;
-
- // If we didn't hit the main target look for nearby units
- let cmpPlayer = QueryPlayerIDInterface(data.attackerOwner);
- let ents = this.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies());
- for (let ent of ents)
- {
- if (!this.TestCollision(ent, data.position, lateness))
- continue;
-
- this.CauseDamage({
- "strengths": data.strengths,
- "target": ent,
- "attacker": data.attacker,
- "multiplier": GetDamageBonus(data.attacker, ent, data.type, data.bonus),
- "type": data.type,
- "attackerOwner": data.attackerOwner
- });
- cmpProjectileManager.RemoveProjectile(data.projectileId);
- break;
- }
-};
-
-/**
- * Damages units around a given origin.
- * @param {Object} data - The data sent by the caller.
- * @param {number} data.attacker - The entity id of the attacker.
- * @param {Vector2D} data.origin - The origin of the projectile hit.
- * @param {number} data.radius - The radius of the splash damage.
- * @param {string} data.shape - The shape of the radius.
- * @param {Object} data.strengths - Data of the form { 'hack': number, 'pierce': number, 'crush': number }.
- * @param {string} data.type - The type of damage.
- * @param {number} data.attackerOwner - The player id of the attacker.
- * @param {Vector3D} [data.direction] - The unit vector defining the direction. Needed for linear splash damage.
- * @param {Object} data.splashBonus - The attack bonus template from the attacker.
- * @param {number[]} data.playersToDamage - The array of player id's to damage.
- */
-Damage.prototype.CauseDamageOverArea = function(data)
-{
- // Get nearby entities and define variables
- let nearEnts = this.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage);
- let damageMultiplier = 1;
-
- // Cycle through all the nearby entities and damage it appropriately based on its distance from the origin.
- for (let ent of nearEnts)
- {
- let entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D();
- if (data.shape == 'Circular') // circular effect with quadratic falloff in every direction
- damageMultiplier = 1 - data.origin.distanceToSquared(entityPosition) / (data.radius * data.radius);
- else if (data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles)
- {
- // Get position of entity relative to splash origin.
- let relativePos = entityPosition.sub(data.origin);
-
- // Get the position relative to the missile direction.
- let direction = Vector2D.from3D(data.direction);
- let parallelPos = relativePos.dot(direction);
- let perpPos = relativePos.cross(direction);
-
- // The width of linear splash is one fifth of the normal splash radius.
- let width = data.radius / 5;
-
- // Check that the unit is within the distance splash width of the line starting at the missile's
- // landing point which extends in the direction of the missile for length splash radius.
- if (parallelPos >= 0 && Math.abs(perpPos) < width) // If in radius, quadratic falloff in both directions
- damageMultiplier = (1 - parallelPos * parallelPos / (data.radius * data.radius)) *
- (1 - perpPos * perpPos / (width * width));
- else
- damageMultiplier = 0;
- }
- else // In case someone calls this function with an invalid shape.
- {
- warn("The " + data.shape + " splash damage shape is not implemented!");
- }
-
- if (data.splashBonus)
- damageMultiplier *= GetDamageBonus(data.attacker, ent, data.type, data.splashBonus);
-
- // Call CauseDamage which reduces the hitpoints, posts network command, plays sounds....
- this.CauseDamage({
- "strengths": data.strengths,
- "target": ent,
- "attacker": data.attacker,
- "multiplier": damageMultiplier,
- "type": data.type + ".Splash",
- "attackerOwner": data.attackerOwner
- });
- }
-};
-
-/**
- * Causes damage on a given unit.
- * @param {Object} data - The data passed by the caller.
- * @param {Object} data.strengths - Data in the form of { 'hack': number, 'pierce': number, 'crush': number }.
- * @param {number} data.target - The entity id of the target.
- * @param {number} data.attacker - The entity id of the attacker.
- * @param {number} data.multiplier - The damage multiplier.
- * @param {string} data.type - The type of damage.
- * @param {number} data.attackerOwner - The player id of the attacker.
- */
-Damage.prototype.CauseDamage = function(data)
-{
- let cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
- if (!cmpDamageReceiver)
- return;
-
- let targetState = cmpDamageReceiver.TakeDamage(data.strengths, data.multiplier);
-
- let cmpPromotion = Engine.QueryInterface(data.attacker, IID_Promotion);
- let cmpLoot = Engine.QueryInterface(data.target, IID_Loot);
- let cmpHealth = Engine.QueryInterface(data.target, IID_Health);
- if (cmpPromotion && cmpLoot && cmpLoot.GetXp() > 0)
- cmpPromotion.IncreaseXp(cmpLoot.GetXp() * -targetState.change / cmpHealth.GetMaxHitpoints());
-
- if (targetState.killed)
- this.TargetKilled(data.attacker, data.target, data.attackerOwner);
-
- Engine.PostMessage(data.target, MT_Attacked, { "attacker": data.attacker, "target": data.target, "type": data.type, "damage": -targetState.change, "attackerOwner": data.attackerOwner });
-};
-
-/**
- * Gets entities near a give point for given players.
- * @param {Vector2D} origin - The point to check around.
- * @param {number} radius - The radius around the point to check.
- * @param {number[]} players - The players of which we need to check entities.
- * @return {number[]} The id's of the entities in range of the given point.
- */
-Damage.prototype.EntitiesNearPoint = function(origin, radius, players)
-{
- // If there is insufficient data return an empty array.
- if (!origin || !radius || !players)
- return [];
-
- return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQueryAroundPos(origin, 0, radius, players, IID_DamageReceiver);
-};
-
-/**
- * Called when a unit kills something (another unit, building, animal etc).
- * @param {number} attacker - The entity id of the killer.
- * @param {number} target - The entity id of the target.
- * @param {number} attackerOwner - The player id of the attacker.
- */
-Damage.prototype.TargetKilled = function(attacker, target, attackerOwner)
-{
- let cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership);
- let atkOwner = cmpAttackerOwnership && cmpAttackerOwnership.GetOwner() != INVALID_PLAYER ? cmpAttackerOwnership.GetOwner() : attackerOwner;
-
- // Add to killer statistics.
- let cmpKillerPlayerStatisticsTracker = QueryPlayerIDInterface(atkOwner, IID_StatisticsTracker);
- if (cmpKillerPlayerStatisticsTracker)
- cmpKillerPlayerStatisticsTracker.KilledEntity(target);
- // Add to loser statistics.
- let cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(target, IID_StatisticsTracker);
- if (cmpTargetPlayerStatisticsTracker)
- cmpTargetPlayerStatisticsTracker.LostEntity(target);
-
- // If killer can collect loot, let's try to collect it.
- let cmpLooter = Engine.QueryInterface(attacker, IID_Looter);
- if (cmpLooter)
- cmpLooter.Collect(target);
-};
-
-Engine.RegisterSystemComponentType(IID_Damage, "Damage", Damage);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/Damage.js
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Damage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Damage.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Damage.js (nonexistent)
@@ -1 +0,0 @@
-Engine.RegisterInterface("Damage");
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Damage.js
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DamageReceiver.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DamageReceiver.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DamageReceiver.js (nonexistent)
@@ -1,6 +0,0 @@
-Engine.RegisterInterface("DamageReceiver");
-
-/**
- * Message of the form { "entity": entity, "invulnerability": true/false }
- */
-Engine.RegisterMessageType("InvulnerabilityChanged");
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DamageReceiver.js
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/DamageTypes.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/DamageTypes.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/DamageTypes.js (nonexistent)
@@ -1,22 +0,0 @@
-/**
- * Builds a RelaxRNG schema based on currently valid elements.
- *
- * To prevent validation errors, disabled damage types are included in the schema.
- *
- * @param {string} helptext - Text displayed as help
- * @return {string} - RelaxNG schema string
- */
-function BuildDamageTypesSchema(helptext = "")
-{
- return "" +
- "" +
- "" +
- // Armour requires Foundation to not be a damage type.
- "Foundation" +
- "" +
- "" +
- "" +
- "";
-}
-
-Engine.RegisterGlobal("BuildDamageTypesSchema", BuildDamageTypesSchema);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/helpers/DamageTypes.js
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/globalscripts/AttackEffects.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/AttackEffects.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/globalscripts/AttackEffects.js (revision 22754)
@@ -0,0 +1,16 @@
+// TODO: could be worth putting this in json files someday
+const g_EffectTypes = ["Damage", "Capture", "GiveStatus"];
+const g_EffectReceiver = {
+ "Damage": {
+ "IID": "IID_Health",
+ "method": "TakeDamage"
+ },
+ "Capture": {
+ "IID": "IID_Capturable",
+ "method": "Capture"
+ },
+ "GiveStatus": {
+ "IID": "IID_StatusEffectsReceiver",
+ "method": "GiveStatus"
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/globalscripts/AttackEffects.js
___________________________________________________________________
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 22754)
@@ -1,545 +1,551 @@
/**
* 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", "Factions", "CivBonuses", "TeamBonuses",
"Structures", "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;
}
/**
* Gets an array of all classes for this identity template
*/
function GetIdentityClasses(template)
{
var classList = [];
if (template.Classes && template.Classes._string)
classList = classList.concat(template.Classes._string.split(/\s+/));
if (template.VisibleClasses && template.VisibleClasses._string)
classList = classList.concat(template.VisibleClasses._string.split(/\s+/));
if (template.Rank)
classList = classList.concat(template.Rank);
return classList;
}
/**
* Gets an array with all classes for this identity template
* that should be shown in the GUI
*/
function GetVisibleIdentityClasses(template)
{
if (template.VisibleClasses && template.VisibleClasses._string)
return template.VisibleClasses._string.split(/\s+/);
return [];
}
/**
* Check if 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)
current_value = GetTechModifiedProperty(modifiers, GetIdentityClasses(template.Identity), mod_key, 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} resources - An instance of the Resources prototype.
* @param {object} damageTypes - An instance of the DamageTypes prototype.
* @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, resources, damageTypes, 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.Armour)
{
ret.armour = {};
for (let damageType in template.Armour)
if (damageType != "Foundation")
ret.armour[damageType] = getEntityValue("Armour/" + damageType);
}
+ 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);
+ }
+
+ // TODO: status effects
+ return effects;
+ };
+
if (template.Attack)
{
ret.attack = {};
for (let type in template.Attack)
{
let getAttackStat = function(stat) {
return getEntityValue("Attack/" + type + "/" + stat);
};
- if (type == "Capture")
- ret.attack.Capture = {
- "value": getAttackStat("Value")
- };
- else
- {
- ret.attack[type] = {
- "minRange": getAttackStat("MinRange"),
- "maxRange": getAttackStat("MaxRange"),
- "elevationBonus": getAttackStat("ElevationBonus"),
- "damage": {}
- };
- for (let damageType in template.Attack[type].Damage)
- ret.attack[type].damage[damageType] = getAttackStat("Damage/" + damageType);
+ ret.attack[type] = {
+ "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].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange *
- (2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange));
- }
ret.attack[type].repeatTime = getAttackStat("RepeatTime");
+ Object.assign(ret.attack[type], getAttackEffects(template.Attack[type], "Attack/" + type));
+
if (template.Attack[type].Splash)
{
ret.attack[type].splash = {
// true if undefined
"friendlyFire": template.Attack[type].Splash.FriendlyFire != "false",
"shape": template.Attack[type].Splash.Shape,
- "damage": {}
};
- for (let damageType in template.Attack[type].Splash.Damage)
- ret.attack[type].splash.damage[damageType] = getAttackStat("Splash/Damage/" + damageType);
+ Object.assign(ret.attack[type].splash, getAttackEffects(template.Attack[type].Splash, "Attack/" + type + "/Splash"));
}
}
}
if (template.DeathDamage)
{
ret.deathDamage = {
"friendlyFire": template.DeathDamage.FriendlyFire != "false",
- "damage": {}
};
- for (let damageType in template.DeathDamage.Damage)
- ret.deathDamage.damage[damageType] = getEntityValue("DeathDamage/Damage/" + damageType);
+
+ Object.assign(ret.deathDamage, getAttackEffects(template.DeathDamage, "DeathDamage"));
}
if (template.Auras && auraTemplates)
{
ret.auras = {};
for (let auraID of template.Auras._string.split(/\s+/))
{
let aura = auraTemplates[auraID];
ret.auras[auraID] = {
"name": aura.auraName,
"description": aura.auraDescription || null,
"radius": aura.radius || null
};
}
}
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.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.PopulationBonus)
ret.cost.populationBonus = getEntityValue("Cost/PopulationBonus");
if (template.Cost.BuildTime)
ret.cost.time = getEntityValue("Cost/BuildTime");
}
if (template.Footprint)
{
ret.footprint = { "height": template.Footprint.Height };
if (template.Footprint.Square)
ret.footprint.square = {
"width": +template.Footprint.Square["@width"],
"depth": +template.Footprint.Square["@depth"]
};
else if (template.Footprint.Circle)
ret.footprint.circle = { "radius": +template.Footprint.Circle["@radius"] };
else
warn("GetTemplateDataHelper(): Unrecognized Footprint type");
}
if (template.GarrisonHolder)
{
ret.garrisonHolder = {
"buffHeal": getEntityValue("GarrisonHolder/BuffHeal")
};
if (template.GarrisonHolder.Max)
ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max");
}
if (template.Heal)
ret.heal = {
"hp": getEntityValue("Heal/HP"),
"range": getEntityValue("Heal/Range"),
"rate": getEntityValue("Heal/Rate")
};
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.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.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)
{
ret.speed = {
"walk": getEntityValue("UnitMotion/WalkSpeed"),
};
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.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(" ");
}
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;
}
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 22753)
+++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 22754)
@@ -1,832 +1,832 @@
var g_TooltipTextFormats = {
"unit": { "font": "sans-10", "color": "orange" },
"header": { "font": "sans-bold-13" },
"body": { "font": "sans-13" },
"comma": { "font": "sans-12" },
"nameSpecificBig": { "font": "sans-bold-16" },
"nameSpecificSmall": { "font": "sans-bold-12" },
"nameGeneric": { "font": "sans-bold-16" }
};
var g_AttackTypes = {
"Melee": translate("Melee Attack:"),
"Ranged": translate("Ranged Attack:"),
"Capture": translate("Capture Attack:")
};
var g_SplashDamageTypes = {
"Circular": translate("Circular Splash Damage"),
"Linear": translate("Linear Splash Damage")
};
var g_RangeTooltipString = {
"relative": {
// Translation: For example: Ranged Attack: 12.0 Pierce, Range: 2 to 10 (+2) meters, Interval: 3 arrows / 2 seconds
"minRange": translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(minRange)s to %(maxRange)s (%(relativeRange)s) %(rangeUnit)s, %(rate)s"),
// Translation: For example: Ranged Attack: 12.0 Pierce, Range: 10 (+2) meters, Interval: 3 arrows / 2 seconds
"no-minRange": translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(maxRange)s (%(relativeRange)s) %(rangeUnit)s, %(rate)s"),
},
"non-relative": {
// Translation: For example: Ranged Attack: 12.0 Pierce, Range: 2 to 10 meters, Interval: 3 arrows / 2 seconds
"minRange": translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(minRange)s to %(maxRange)s %(rangeUnit)s, %(rate)s"),
// Translation: For example: Ranged Attack: 12.0 Pierce, Range: 10 meters, Interval: 3 arrows / 2 seconds
"no-minRange": translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(maxRange)s %(rangeUnit)s, %(rate)s"),
}
};
function getCostTypes()
{
return g_ResourceData.GetCodes().concat(["population", "populationBonus", "time"]);
}
/**
* 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 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 attackRateDetails(template, type)
{
// Either one arrow shot by UnitAI,
let timeString = getSecondsString(template.attack[type].repeatTime / 1000);
// or multiple arrows shot by BuildingAI
if (!template.buildingAI || type != "Ranged")
return timeString;
// Show either current rate from simulation or default count if the sim is not running
let arrows = template.buildingAI.arrowCount || template.buildingAI.defaultArrowCount;
let arrowString = sprintf(translatePlural("%(arrowcount)s %(arrows)s", "%(arrowcount)s %(arrows)s", arrows), {
"arrowcount": arrows,
"arrows": unitFont(translatePlural("arrow", "arrows", arrows))
});
return sprintf(translate("%(arrowString)s / %(timeString)s"), {
"arrowString": arrowString,
"timeString": timeString
});
}
/**
* Converts an armor level into the actual reduction percentage
*/
function armorLevelToPercentageString(level)
{
return sprintf(translate("%(percentage)s%%"), {
"percentage": (100 - Math.round(Math.pow(0.9, level) * 100))
});
}
function getArmorTooltip(template)
{
if (!template.armour)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Armor:")),
"details":
Object.keys(template.armour).map(
dmgType => sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), {
"damage": template.armour[dmgType].toFixed(1),
"damageType": unitFont(translateWithContext("damage type", dmgType)),
"armorPercentage":
'[font="sans-10"]' +
sprintf(translate("(%(armorPercentage)s)"), {
"armorPercentage": armorLevelToPercentageString(template.armour[dmgType])
}) + '[/font]'
})
).join(commaFont(translate(", ")))
});
}
function damageTypesToText(dmg)
{
if (!dmg)
return '[font="sans-12"]' + translate("(None)") + '[/font]';
return Object.keys(dmg).filter(dmgType => dmg[dmgType]).map(
dmgType => sprintf(translate("%(damage)s %(damageType)s"), {
"damage": dmg[dmgType].toFixed(1),
"damageType": unitFont(translateWithContext("damage type", dmgType))
})).join(commaFont(translate(", ")));
}
function getAttackTooltip(template)
{
if (!template.attack)
return "";
let tooltips = [];
for (let type in template.attack)
{
if (type == "Slaughter")
continue; // Slaughter is used to kill animals, so do not show it.
let rate = sprintf(translate("%(label)s %(details)s"), {
"label":
headerFont(
template.buildingAI && type == "Ranged" ?
translate("Interval:") :
translate("Rate:")),
"details": attackRateDetails(template, type)
});
let attackLabel = headerFont(g_AttackTypes[type]);
if (type == "Capture" || type != "Ranged")
{
tooltips.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), {
"attackLabel": attackLabel,
"details":
type == "Capture" ?
- template.attack.Capture.value :
- damageTypesToText(template.attack[type].damage),
+ template.attack.Capture.Capture :
+ damageTypesToText(template.attack[type].Damage),
"rate": rate
}));
continue;
}
let minRange = Math.round(template.attack[type].minRange);
let maxRange = Math.round(template.attack[type].maxRange);
let realRange = template.attack[type].elevationAdaptedRange;
let relativeRange = realRange ? Math.round(realRange - maxRange) : 0;
tooltips.push(sprintf(g_RangeTooltipString[relativeRange ? "relative" : "non-relative"][minRange ? "minRange" : "no-minRange"], {
"attackLabel": attackLabel,
- "damageTypes": damageTypesToText(template.attack[type].damage),
+ "damageTypes": damageTypesToText(template.attack[type].Damage),
"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)),
"rate": rate,
}));
}
return tooltips.join("\n");
}
function getSplashDamageTooltip(template)
{
if (!template.attack)
return "";
let tooltips = [];
for (let type in template.attack)
{
let splash = template.attack[type].splash;
if (!splash)
continue;
let splashDamageTooltip = sprintf(translate("%(label)s: %(value)s"), {
"label": headerFont(g_SplashDamageTypes[splash.shape]),
- "value": damageTypesToText(splash.damage)
+ "value": damageTypesToText(splash.Damage)
});
if (g_AlwaysDisplayFriendlyFire || splash.friendlyFire)
splashDamageTooltip += commaFont(translate(", ")) + sprintf(translate("Friendly Fire: %(enabled)s"), {
"enabled": splash.friendlyFire ? translate("Yes") : translate("No")
});
tooltips.push(splashDamageTooltip);
}
// If multiple attack types deal splash damage, the attack type should be shown to differentiate.
return tooltips.join("\n");
}
function getGarrisonTooltip(template)
{
if (!template.garrisonHolder)
return "";
let tooltips = [
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(translate("Health")),
"second": unitFont(translate("second")),
})
);
return tooltips.join(commaFont(translate(", ")));
}
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)
{
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Number of repairers:")),
"details": entState.repairable.numBuilders
}) + "\n" + (entState.repairable.numBuilders ?
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.",
Math.round(entState.repairable.buildTime.timeRemaining - entState.repairable.buildTime.timeRemainingNew)),
{
"second": Math.round(entState.repairable.buildTime.timeRemaining - entState.repairable.buildTime.timeRemainingNew)
}) :
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)
}));
}
function getBuildTimeTooltip(entState)
{
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Number of builders:")),
"details": entState.foundation.numBuilders
}) + "\n" + (entState.foundation.numBuilders ?
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.",
Math.round(entState.foundation.buildTime.timeRemaining - entState.foundation.buildTime.timeRemainingNew)),
{
"second": Math.round(entState.foundation.buildTime.timeRemaining - entState.foundation.buildTime.timeRemainingNew)
}) :
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)
}));
}
/**
* 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)
{
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())
// Population bonus is shown in the tooltip
if (type != "populationBonus" && 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 "";
// Average the resource rates (TODO: distinguish between subtypes)
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 and skew the results
if (subtype !== "ruins")
types.push(resource.code + "." + subtype);
let [rate, count] = types.reduce((sum, t) => {
let r = template.resourceGatherRates[t];
return [sum[0] + (r > 0 ? r : 0), sum[1] + (r > 0 ? 1 : 0)];
}, [0, 0]);
if (rate > 0)
rates[resource.code] = +(rate / count).toFixed(1);
}
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]
})
).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)
})
});
}
/**
* 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, 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);
let templateMedium = GetTemplateData(template.wallSet.templates.medium);
let templateShort = GetTemplateData(template.wallSet.templates.short);
let templateTower = GetTemplateData(template.wallSet.templates.tower);
let wallCosts = getWallPieceTooltip([templateShort, templateMedium, templateLong]);
let towerCosts = getEntityCostComponentsTooltipString(templateTower);
return sprintf(translate("Walls: %(costs)s"), { "costs": wallCosts.join(" ") }) + "\n" +
sprintf(translate("Towers: %(costs)s"), { "costs": towerCosts.join(" ") });
}
if (template.cost)
{
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)
{
let popBonus = "";
if (template.cost && template.cost.populationBonus)
popBonus = sprintf(translate("%(label)s %(populationBonus)s"), {
"label": headerFont(translate("Population Bonus:")),
"populationBonus": template.cost.populationBonus
});
return popBonus;
}
/**
* Returns a message with the amount of each resource needed to create an entity.
*/
function getNeededResourcesTooltip(resources)
{
if (!resources)
return "";
let formatted = [];
for (let resource in resources)
formatted.push(sprintf(translate("%(component)s %(cost)s"), {
"component": '[font="sans-12"]' + 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);
if (walk == 0 && run == 0)
return "";
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"))
})
});
}
function getHealerTooltip(template)
{
if (!template.heal)
return "";
let hp = +(template.heal.hp.toFixed(1));
let range = +(template.heal.range.toFixed(0));
let rate = +((template.heal.rate / 1000).toFixed(1));
return [
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", hp), {
"label": headerFont(translate("Heal:")),
"val": hp,
// Translation: Short for hit points (or health points) that are healed in one healing action
"unit": unitFont(translatePlural("HP", "HP", hp))
}),
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", range), {
"label": headerFont(translate("Range:")),
"val": range,
"unit": unitFont(translatePlural("meter", "meters", range))
}),
sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", rate), {
"label": headerFont(translate("Rate:")),
"val": rate,
"unit": unitFont(translatePlural("second", "seconds", rate))
})
].join(translate(", "));
}
function getAurasTooltip(template)
{
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": translate(auras[auraID].name)
})),
"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;
return sprintf(translate("%(specificName)s (%(genericName)s)"), {
"specificName": template.name.specific,
"genericName": template.name.generic
});
}
function getEntityNamesFormatted(template)
{
if (!template.name.specific)
return setStringTags(template.name.generic, g_TooltipTextFormats.nameSpecificBig);
// Translation: Example: "Epibátēs Athēnaîos [font="sans-bold-16"](Athenian Marine)[/font]"
return sprintf(translate("%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s"), {
"specificName":
setStringTags(template.name.specific[0], g_TooltipTextFormats.nameSpecificBig) +
setStringTags(template.name.specific.slice(1).toUpperCase(), g_TooltipTextFormats.nameSpecificSmall),
"genericName": template.name.generic,
"fontStart": '[font="' + g_TooltipTextFormats.nameGeneric.font + '"]',
"fontEnd": '[/font]'
});
}
function getVisibleEntityClassesFormatted(template)
{
if (!template.visibleIdentityClasses || !template.visibleIdentityClasses.length)
return "";
return headerFont(translate("Classes:")) + ' ' +
bodyFont(template.visibleIdentityClasses.map(c => translate(c)).join(translate(", ")));
}
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 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.");
}
Index: ps/trunk/binaries/data/mods/public/maps/random/polar_sea_triggers.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/polar_sea_triggers.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/maps/random/polar_sea_triggers.js (revision 22754)
@@ -1,113 +1,112 @@
const debugLog = false;
var attackerTemplate = "gaia/fauna_arctic_wolf_domestic";
var minWaveSize = 1;
var maxWaveSize = 3;
var firstWaveTime = 5;
var minWaveTime = 2;
var maxWaveTime = 4;
/**
* Attackers will focus the targetCount closest units that have the targetClasses type.
*/
var targetClasses = "Organic+!Domestic";
var targetCount = 3;
var disabledTechnologies = [
"gather_lumbering_ironaxes",
"gather_lumbering_sharpaxes",
"gather_lumbering_strongeraxes"
];
Trigger.prototype.InitDisableTechnologies = function()
{
for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i)
QueryPlayerIDInterface(i).SetDisabledTechnologies(disabledTechnologies);
};
Trigger.prototype.SpawnWolvesAndAttack = function()
{
let waveSize = Math.round(Math.random() * (maxWaveSize - minWaveSize) + minWaveSize);
let attackers = TriggerHelper.SpawnUnitsFromTriggerPoints("A", attackerTemplate, waveSize, 0);
if (debugLog)
print("Spawned " + waveSize + " " + attackerTemplate + " at " + Object.keys(attackers).length + " points\n");
let allTargets;
- let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
let players = new Array(TriggerHelper.GetNumberOfPlayers()).fill(0).map((v, i) => i + 1);
for (let spawnPoint in attackers)
{
// TriggerHelper.SpawnUnits is guaranteed to spawn
let firstAttacker = attackers[spawnPoint][0];
if (!firstAttacker)
continue;
let attackerPos = TriggerHelper.GetEntityPosition2D(firstAttacker);
if (!attackerPos)
continue;
// The returned entities are sorted by RangeManager already
- let targets = cmpDamage.EntitiesNearPoint(attackerPos, 200, players).filter(ent => {
+ let targets = Attack.EntitiesNearPoint(attackerPos, 200, players).filter(ent => {
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses);
});
let goodTargets = targets.slice(0, targetCount);
// Look through all targets if there aren't enough nearby ones
if (goodTargets.length < targetCount)
{
if (!allTargets)
allTargets = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities().filter(ent => {
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses);
});
let getDistance = target => {
let targetPos = TriggerHelper.GetEntityPosition2D(target);
return targetPos ? attackerPos.distanceToSquared(targetPos) : Infinity;
};
goodTargets = [];
let goodDists = [];
for (let target of allTargets)
{
let dist = getDistance(target);
let i = goodDists.findIndex(element => dist < element);
if (i != -1 || goodTargets.length < targetCount)
{
if (i == -1)
i = goodTargets.length;
goodTargets.splice(i, 0, target);
goodDists.splice(i, 0, dist);
if (goodTargets.length > targetCount)
{
goodTargets.pop();
goodDists.pop();
}
}
}
}
for (let target of goodTargets)
ProcessCommand(0, {
"type": "attack",
"entities": attackers[spawnPoint],
"target": target,
"queued": true
});
}
this.DoAfterDelay((Math.random() * (maxWaveTime - minWaveTime) + minWaveTime) * 60 * 1000, "SpawnWolvesAndAttack", {});
};
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.InitDisableTechnologies();
cmpTrigger.DoAfterDelay(firstWaveTime * 60 * 1000, "SpawnWolvesAndAttack", {});
}
Index: ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js (revision 22754)
@@ -1,308 +1,308 @@
/**
* If set to true, it will print how many templates would be spawned if the players were not defeated.
*/
const dryRun = false;
/**
* If enabled, prints the number of units to the command line output.
*/
const debugLog = false;
/**
* Get the number of minutes to pass between spawning new treasures.
*/
var treasureTime = () => randFloat(3, 5);
/**
* Get the time in minutes when the first wave of attackers will be spawned.
*/
var firstWaveTime = () => randFloat(4, 6);
/**
* Maximum time in minutes between two consecutive waves.
*/
var maxWaveTime = 4;
/**
* Get the next attacker wave delay.
*/
var waveTime = () => randFloat(0.5, 1) * maxWaveTime;
/**
* Roughly the number of attackers on the first wave.
*/
var initialAttackers = 5;
/**
* Increase the number of attackers exponentially, by this percent value per minute.
*/
var percentPerMinute = 1.05;
/**
* Greatest amount of attackers that can be spawned.
*/
var totalAttackerLimit = 200;
/**
* Least and greatest amount of siege engines per wave.
*/
var siegeFraction = () => randFloat(0.2, 0.5);
/**
* Potentially / definitely spawn a gaia hero after this number of minutes.
*/
var heroTime = () => randFloat(20, 60);
/**
* The following templates can't be built by any player.
*/
var disabledTemplates = (civ) => [
// Economic structures
"structures/" + civ + "_corral",
"structures/" + civ + "_farmstead",
"structures/" + civ + "_field",
"structures/" + civ + "_storehouse",
"structures/" + civ + "_rotarymill",
"units/maur_support_elephant",
// Expansions
"structures/" + civ + "_civil_centre",
"structures/" + civ + "_military_colony",
// Walls
"structures/" + civ + "_wallset_stone",
"structures/rome_wallset_siege",
"other/wallset_palisade",
// Shoreline
"structures/" + civ + "_dock",
"structures/brit_crannog",
"structures/cart_super_dock",
"structures/ptol_lighthouse"
];
/**
* Spawn these treasures in regular intervals.
*/
var treasures = [
"gaia/treasure/food_barrel",
"gaia/treasure/food_bin",
"gaia/treasure/food_crate",
"gaia/treasure/food_jars",
"gaia/treasure/metal",
"gaia/treasure/stone",
"gaia/treasure/wood",
"gaia/treasure/wood",
"gaia/treasure/wood"
];
/**
* An object that maps from civ [f.e. "spart"] to an object
* that has the keys "champions", "siege" and "heroes",
* which is an array containing all these templates,
* trainable from a building or not.
*/
var attackerUnitTemplates = {};
Trigger.prototype.InitSurvival = function()
{
this.InitStartingUnits();
this.LoadAttackerTemplates();
this.SetDisableTemplates();
this.PlaceTreasures();
this.InitializeEnemyWaves();
};
Trigger.prototype.debugLog = function(txt)
{
if (!debugLog)
return;
print("DEBUG [" + Math.round(TriggerHelper.GetMinutes()) + "] " + txt + "\n");
};
Trigger.prototype.LoadAttackerTemplates = function()
{
for (let civ of ["gaia", ...Object.keys(loadCivFiles(false))])
attackerUnitTemplates[civ] = {
"heroes": TriggerHelper.GetTemplateNamesByClasses("Hero", civ, undefined, true),
"champions": TriggerHelper.GetTemplateNamesByClasses("Champion+!Elephant", civ, undefined, true),
"siege": TriggerHelper.GetTemplateNamesByClasses("Siege Champion+Elephant", civ, "packed", undefined)
};
this.debugLog("Attacker templates:");
this.debugLog(uneval(attackerUnitTemplates));
};
Trigger.prototype.SetDisableTemplates = function()
{
for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
cmpPlayer.SetDisabledTemplates(disabledTemplates(cmpPlayer.GetCiv()));
}
};
/**
* Remember civic centers and make women invincible.
*/
Trigger.prototype.InitStartingUnits = function()
{
for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
{
this.playerCivicCenter[playerID] = TriggerHelper.GetPlayerEntitiesByClass(playerID, "CivilCentre")[0];
this.treasureFemale[playerID] = TriggerHelper.GetPlayerEntitiesByClass(playerID, "FemaleCitizen")[0];
- Engine.QueryInterface(this.treasureFemale[playerID], IID_DamageReceiver).SetInvulnerability(true);
+ Engine.QueryInterface(this.treasureFemale[playerID], IID_Resistance).SetInvulnerability(true);
}
};
Trigger.prototype.InitializeEnemyWaves = function()
{
let time = firstWaveTime() * 60 * 1000;
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).AddTimeNotification({
"message": markForTranslation("The first wave will start in %(time)s!"),
"translateMessage": true
}, time);
this.DoAfterDelay(time, "StartAnEnemyWave", {});
};
Trigger.prototype.StartAnEnemyWave = function()
{
let currentMin = TriggerHelper.GetMinutes();
let nextWaveTime = waveTime();
let civ = pickRandom(Object.keys(attackerUnitTemplates));
// Determine total attacker count of the current wave.
// Exponential increase with time, capped to the limit and fluctuating proportionally with the current wavetime.
let totalAttackers = Math.ceil(Math.min(totalAttackerLimit,
initialAttackers * Math.pow(percentPerMinute, currentMin) * nextWaveTime / maxWaveTime));
let siegeRatio = siegeFraction();
this.debugLog("Spawning " + totalAttackers + " attackers, siege ratio " + siegeRatio.toFixed(2));
let attackerCount = TriggerHelper.BalancedTemplateComposition(
[
{
"templates": attackerUnitTemplates[civ].heroes,
"count": currentMin > heroTime() && attackerUnitTemplates[civ].heroes.length ? 1 : 0
},
{
"templates": attackerUnitTemplates[civ].siege,
"frequency": siegeRatio
},
{
"templates": attackerUnitTemplates[civ].champions,
"frequency": 1 - siegeRatio
}
],
totalAttackers);
this.debugLog("Templates: " + uneval(attackerCount));
// Spawn the templates
let spawned = false;
for (let point of this.GetTriggerPoints("A"))
{
if (dryRun)
{
spawned = true;
break;
}
// Don't spawn attackers for defeated players and players that lost their cc after win
let playerID = QueryOwnerInterface(point, IID_Player).GetPlayerID();
let civicCentre = this.playerCivicCenter[playerID];
if (!civicCentre)
continue;
// Check if the cc is garrisoned in another building
let targetPos = TriggerHelper.GetEntityPosition2D(civicCentre);
if (!targetPos)
continue;
for (let templateName in attackerCount)
{
let isHero = attackerUnitTemplates[civ].heroes.indexOf(templateName) != -1;
// Don't spawn gaia hero if the previous one is still alive
if (this.gaiaHeroes[playerID] && isHero)
{
let cmpHealth = Engine.QueryInterface(this.gaiaHeroes[playerID], IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() != 0)
{
this.debugLog("Not spawning hero for player " + playerID + " as the previous one is still alive");
continue;
}
}
if (dryRun)
continue;
let entities = TriggerHelper.SpawnUnits(point, templateName, attackerCount[templateName], 0);
ProcessCommand(0, {
"type": "attack-walk",
"entities": entities,
"x": targetPos.x,
"z": targetPos.y,
"targetClasses": undefined,
"allowCapture": false,
"queued": true
});
if (isHero)
this.gaiaHeroes[playerID] = entities[0];
}
spawned = true;
}
if (!spawned)
return;
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
"message": markForTranslation("An enemy wave is attacking!"),
"translateMessage": true
});
this.DoAfterDelay(nextWaveTime * 60 * 1000, "StartAnEnemyWave", {});
};
Trigger.prototype.PlaceTreasures = function()
{
let point = pickRandom(["B", "C", "D"]);
let triggerPoints = this.GetTriggerPoints(point);
for (let point of triggerPoints)
TriggerHelper.SpawnUnits(point, pickRandom(treasures), 1, 0);
this.DoAfterDelay(treasureTime() * 60 * 1000, "PlaceTreasures", {});
};
Trigger.prototype.OnOwnershipChanged = function(data)
{
if (data.entity == this.playerCivicCenter[data.from])
{
this.playerCivicCenter[data.from] = undefined;
TriggerHelper.DefeatPlayer(
data.from,
markForTranslation("%(player)s has been defeated (lost civic center)."));
}
else if (data.entity == this.treasureFemale[data.from])
{
this.treasureFemale[data.from] = undefined;
Engine.DestroyEntity(data.entity);
}
};
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.treasureFemale = [];
cmpTrigger.playerCivicCenter = [];
cmpTrigger.gaiaHeroes = [];
cmpTrigger.RegisterTrigger("OnInitGame", "InitSurvival", { "enabled": true });
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "OnOwnershipChanged", { "enabled": true });
}
Index: ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js (revision 22754)
@@ -1,190 +1,190 @@
Trigger.prototype.InitCaptureTheRelic = function()
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let catafalqueTemplates = shuffleArray(cmpTemplateManager.FindAllTemplates(false).filter(
name => GetIdentityClasses(cmpTemplateManager.GetTemplate(name).Identity || {}).indexOf("Relic") != -1));
let potentialSpawnPoints = TriggerHelper.GetLandSpawnPoints();
if (!potentialSpawnPoints.length)
{
error("No gaia entities found on this map that could be used as spawn points!");
return;
}
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
let numSpawnedRelics = cmpEndGameManager.GetGameSettings().relicCount;
this.playerRelicsCount = new Array(TriggerHelper.GetNumberOfPlayers()).fill(0, 1);
this.playerRelicsCount[0] = numSpawnedRelics;
for (let i = 0; i < numSpawnedRelics; ++i)
{
this.relics[i] = TriggerHelper.SpawnUnits(pickRandom(potentialSpawnPoints), catafalqueTemplates[i], 1, 0)[0];
- let cmpDamageReceiver = Engine.QueryInterface(this.relics[i], IID_DamageReceiver);
- cmpDamageReceiver.SetInvulnerability(true);
+ let cmpResistance = Engine.QueryInterface(this.relics[i], IID_Resistance);
+ cmpResistance.SetInvulnerability(true);
let cmpPositionRelic = Engine.QueryInterface(this.relics[i], IID_Position);
cmpPositionRelic.SetYRotation(randomAngle());
}
};
Trigger.prototype.CheckCaptureTheRelicVictory = function(data)
{
let cmpIdentity = Engine.QueryInterface(data.entity, IID_Identity);
if (!cmpIdentity || !cmpIdentity.HasClass("Relic") || data.from == INVALID_PLAYER)
return;
--this.playerRelicsCount[data.from];
if (data.to == -1)
{
warn("Relic entity " + data.entity + " has been destroyed");
this.relics.splice(this.relics.indexOf(data.entity), 1);
}
else
++this.playerRelicsCount[data.to];
this.DeleteCaptureTheRelicVictoryMessages();
this.CheckCaptureTheRelicCountdown();
};
/**
* Check if a group of mutually allied players have acquired all relics.
* The winning players are the relic owners and all players mutually allied to all relic owners.
* Reset the countdown if the group of winning players changes or extends.
*/
Trigger.prototype.CheckCaptureTheRelicCountdown = function()
{
if (this.playerRelicsCount[0])
{
this.DeleteCaptureTheRelicVictoryMessages();
return;
}
let activePlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetActivePlayers();
let relicOwners = activePlayers.filter(playerID => this.playerRelicsCount[playerID]);
if (!relicOwners.length)
{
this.DeleteCaptureTheRelicVictoryMessages();
return;
}
let winningPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager).GetAlliedVictory() ?
activePlayers.filter(playerID => relicOwners.every(owner => QueryPlayerIDInterface(playerID).IsMutualAlly(owner))) :
[relicOwners[0]];
// All relicOwners should be mutually allied
if (relicOwners.some(owner => winningPlayers.indexOf(owner) == -1))
{
this.DeleteCaptureTheRelicVictoryMessages();
return;
}
// Reset the timer when playerAndAllies isn't the same as this.relicsVictoryCountdownPlayers
if (winningPlayers.length != this.relicsVictoryCountdownPlayers.length ||
winningPlayers.some(player => this.relicsVictoryCountdownPlayers.indexOf(player) == -1))
{
this.relicsVictoryCountdownPlayers = winningPlayers;
this.StartCaptureTheRelicCountdown(winningPlayers);
}
};
Trigger.prototype.DeleteCaptureTheRelicVictoryMessages = function()
{
if (!this.relicsVictoryTimer)
return;
Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).CancelTimer(this.relicsVictoryTimer);
this.relicsVictoryTimer = undefined;
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.DeleteTimeNotification(this.ownRelicsVictoryMessage);
cmpGuiInterface.DeleteTimeNotification(this.othersRelicsVictoryMessage);
this.relicsVictoryCountdownPlayers = [];
};
Trigger.prototype.StartCaptureTheRelicCountdown = function(winningPlayers)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
if (this.relicsVictoryTimer)
{
cmpTimer.CancelTimer(this.relicsVictoryTimer);
cmpGuiInterface.DeleteTimeNotification(this.ownRelicsVictoryMessage);
cmpGuiInterface.DeleteTimeNotification(this.othersRelicsVictoryMessage);
}
if (!this.relics.length)
return;
let others = [-1];
for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
{
let cmpPlayer = QueryPlayerIDInterface(playerID);
if (cmpPlayer.GetState() == "won")
return;
if (winningPlayers.indexOf(playerID) == -1)
others.push(playerID);
}
let cmpPlayer = QueryOwnerInterface(this.relics[0], IID_Player);
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
let captureTheRelicDuration = cmpEndGameManager.GetGameSettings().relicDuration;
let isTeam = winningPlayers.length > 1;
this.ownRelicsVictoryMessage = cmpGuiInterface.AddTimeNotification({
"message": isTeam ?
markForTranslation("%(_player_)s and their allies have captured all relics and will win in %(time)s.") :
markForTranslation("%(_player_)s has captured all relics and will win in %(time)s."),
"players": others,
"parameters": {
"_player_": cmpPlayer.GetPlayerID()
},
"translateMessage": true,
"translateParameters": []
}, captureTheRelicDuration);
this.othersRelicsVictoryMessage = cmpGuiInterface.AddTimeNotification({
"message": isTeam ?
markForTranslation("You and your allies have captured all relics and will win in %(time)s.") :
markForTranslation("You have captured all relics and will win in %(time)s."),
"players": winningPlayers,
"translateMessage": true
}, captureTheRelicDuration);
this.relicsVictoryTimer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger,
"CaptureTheRelicVictorySetWinner", captureTheRelicDuration, winningPlayers);
};
Trigger.prototype.CaptureTheRelicVictorySetWinner = function(winningPlayers)
{
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
cmpEndGameManager.MarkPlayersAsWon(
winningPlayers,
n => markForPluralTranslation(
"%(lastPlayer)s has won (Capture the Relic).",
"%(players)s and %(lastPlayer)s have won (Capture the Relic).",
n),
n => markForPluralTranslation(
"%(lastPlayer)s has been defeated (Capture the Relic).",
"%(players)s and %(lastPlayer)s have been defeated (Capture the Relic).",
n));
};
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.relics = [];
cmpTrigger.playerRelicsCount = [];
cmpTrigger.relicsVictoryTimer = undefined;
cmpTrigger.ownRelicsVictoryMessage = undefined;
cmpTrigger.othersRelicsVictoryMessage = undefined;
cmpTrigger.relicsVictoryCountdownPlayers = [];
cmpTrigger.DoAfterDelay(0, "InitCaptureTheRelic", {});
cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "CheckCaptureTheRelicCountdown", { "enabled": true });
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "CheckCaptureTheRelicVictory", { "enabled": true });
cmpTrigger.RegisterTrigger("OnPlayerWon", "DeleteCaptureTheRelicVictoryMessages", { "enabled": true });
cmpTrigger.RegisterTrigger("OnPlayerDefeated", "CheckCaptureTheRelicCountdown", { "enabled": true });
}
Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 22754)
@@ -1,948 +1,948 @@
var API3 = function(m)
{
// defines a template.
m.Template = m.Class({
"_init": function(sharedAI, templateName, template)
{
this._templateName = templateName;
this._template = template;
// save a reference to the template tech modifications
if (!sharedAI._templatesModifications[this._templateName])
sharedAI._templatesModifications[this._templateName] = {};
this._templateModif = sharedAI._templatesModifications[this._templateName];
this._tpCache = new Map();
},
// helper function to return a template value, optionally adjusting for tech.
// TODO: there's no support for "_string" values here.
"get": function(string)
{
let value = this._template;
if (this._entityModif && this._entityModif.has(string))
return this._entityModif.get(string);
else if (this._templateModif)
{
let owner = this._entity ? this._entity.owner : PlayerID;
if (this._templateModif[owner] && this._templateModif[owner].has(string))
return this._templateModif[owner].get(string);
}
if (!this._tpCache.has(string))
{
let args = string.split("/");
for (let arg of args)
{
if (value[arg])
value = value[arg];
else
{
value = undefined;
break;
}
}
this._tpCache.set(string, value);
}
return this._tpCache.get(string);
},
"templateName": function() { return this._templateName; },
"genericName": function() { return this.get("Identity/GenericName"); },
"civ": function() { return this.get("Identity/Civ"); },
"classes": function() {
let template = this.get("Identity");
if (!template)
return undefined;
return GetIdentityClasses(template);
},
"hasClass": function(name) {
if (!this._classes)
this._classes = this.classes();
let classes = this._classes;
return classes && classes.indexOf(name) != -1;
},
"hasClasses": function(array) {
if (!this._classes)
this._classes = this.classes();
let classes = this._classes;
if (!classes)
return false;
for (let cls of array)
if (classes.indexOf(cls) == -1)
return false;
return true;
},
"requiredTech": function() { return this.get("Identity/RequiredTechnology"); },
"available": function(gameState) {
let techRequired = this.requiredTech();
if (!techRequired)
return true;
return gameState.isResearched(techRequired);
},
// specifically
"phase": function() {
let techRequired = this.requiredTech();
if (!techRequired)
return 0;
if (techRequired == "phase_village")
return 1;
if (techRequired == "phase_town")
return 2;
if (techRequired == "phase_city")
return 3;
if (techRequired.startsWith("phase_"))
return 4;
return 0;
},
"cost": function(productionQueue) {
if (!this.get("Cost"))
return undefined;
let ret = {};
for (let type in this.get("Cost/Resources"))
ret[type] = +this.get("Cost/Resources/" + type);
return ret;
},
"costSum": function(productionQueue) {
let cost = this.cost(productionQueue);
if (!cost)
return undefined;
let ret = 0;
for (let type in cost)
ret += cost[type];
return ret;
},
"techCostMultiplier": function(type) {
return +(this.get("ProductionQueue/TechCostMultiplier/"+type) || 1);
},
/**
* Returns { "max": max, "min": min } or undefined if no obstruction.
* max: radius of the outer circle surrounding this entity's obstruction shape
* min: radius of the inner circle
*/
"obstructionRadius": function() {
if (!this.get("Obstruction"))
return undefined;
if (this.get("Obstruction/Static"))
{
let w = +this.get("Obstruction/Static/@width");
let h = +this.get("Obstruction/Static/@depth");
return { "max": Math.sqrt(w*w + h*h) / 2, "min": Math.min(h, w) / 2 };
}
if (this.get("Obstruction/Unit"))
{
let r = +this.get("Obstruction/Unit/@radius");
return { "max": r, "min": r };
}
let right = this.get("Obstruction/Obstructions/Right");
let left = this.get("Obstruction/Obstructions/Left");
if (left && right)
{
let w = +right["@x"] + right["@width"]/2 - left["@x"] + left["@width"]/2;
let h = Math.max(+right["@z"] + right["@depth"]/2, +left["@z"] + left["@depth"]/2) -
Math.min(+right["@z"] - right["@depth"]/2, +left["@z"] - left["@depth"]/2);
return { "max": Math.sqrt(w*w + h*h) / 2, "min": Math.min(h, w) / 2 };
}
return { "max": 0, "min": 0 }; // Units have currently no obstructions
},
/**
* Returns the radius of a circle surrounding this entity's footprint.
*/
"footprintRadius": function() {
if (!this.get("Footprint"))
return undefined;
if (this.get("Footprint/Square"))
{
let w = +this.get("Footprint/Square/@width");
let h = +this.get("Footprint/Square/@depth");
return Math.sqrt(w*w + h*h) / 2;
}
if (this.get("Footprint/Circle"))
return +this.get("Footprint/Circle/@radius");
return 0; // this should never happen
},
"maxHitpoints": function() { return +(this.get("Health/Max") || 0); },
"isHealable": function()
{
if (this.get("Health") !== undefined)
return this.get("Health/Unhealable") !== "true";
return false;
},
"isRepairable": function() { return this.get("Repairable") !== undefined; },
"getPopulationBonus": function() { return +this.get("Cost/PopulationBonus"); },
"armourStrengths": function() {
let armourDamageTypes = this.get("Armour");
if (!armourDamageTypes)
return undefined;
let armour = {};
for (let damageType in armourDamageTypes)
if (damageType != "Foundation")
armour[damageType] = +armourDamageTypes[damageType];
return armour;
},
"attackTypes": function() {
if (!this.get("Attack"))
return undefined;
let ret = [];
for (let type in this.get("Attack"))
ret.push(type);
return ret;
},
"attackRange": function(type) {
if (!this.get("Attack/" + type +""))
return undefined;
return {
"max": +this.get("Attack/" + type +"/MaxRange"),
"min": +(this.get("Attack/" + type +"/MinRange") || 0)
};
},
"attackStrengths": function(type) {
let attackDamageTypes = this.get("Attack/" + type + "/Damage");
if (!attackDamageTypes)
return undefined;
let damage = {};
for (let damageType in attackDamageTypes)
damage[damageType] = +attackDamageTypes[damageType];
return damage;
},
"captureStrength": function() {
if (!this.get("Attack/Capture"))
return undefined;
- return +this.get("Attack/Capture/Value") || 0;
+ return +this.get("Attack/Capture/Capture") || 0;
},
"attackTimes": function(type) {
if (!this.get("Attack/" + type +""))
return undefined;
return {
"prepare": +(this.get("Attack/" + type + "/PrepareTime") || 0),
"repeat": +(this.get("Attack/" + type + "/RepeatTime") || 1000)
};
},
// returns the classes this templates counters:
// Return type is [ [-neededClasses- , multiplier], … ].
"getCounteredClasses": function() {
if (!this.get("Attack"))
return undefined;
let Classes = [];
for (let type in this.get("Attack"))
{
let bonuses = this.get("Attack/" + type + "/Bonuses");
if (!bonuses)
continue;
for (let b in bonuses)
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (bonusClasses)
Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses/" + b +"/Multiplier")]);
}
}
return Classes;
},
// returns true if the entity counters those classes.
// TODO: refine using the multiplier
"countersClasses": function(classes) {
if (!this.get("Attack"))
return false;
let mcounter = [];
for (let type in this.get("Attack"))
{
let bonuses = this.get("Attack/" + type + "/Bonuses");
if (!bonuses)
continue;
for (let b in bonuses)
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (bonusClasses)
mcounter.concat(bonusClasses.split(" "));
}
}
for (let i in classes)
if (mcounter.indexOf(classes[i]) != -1)
return true;
return false;
},
// returns, if it exists, the multiplier from each attack against a given class
"getMultiplierAgainst": function(type, againstClass) {
if (!this.get("Attack/" + type +""))
return undefined;
if (this.get("Attack/" + type + "/Bonuses"))
{
for (let b in this.get("Attack/" + type + "/Bonuses"))
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (!bonusClasses)
continue;
for (let bcl of bonusClasses.split(" "))
if (bcl == againstClass)
return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier");
}
}
return 1;
},
"buildableEntities": function(civ) {
let templates = this.get("Builder/Entities/_string");
if (!templates)
return [];
return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
},
"trainableEntities": function(civ) {
let templates = this.get("ProductionQueue/Entities/_string");
if (!templates)
return undefined;
return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
},
"researchableTechs": function(gameState, civ) {
let templates = this.get("ProductionQueue/Technologies/_string");
if (!templates)
return undefined;
let techs = templates.split(/\s+/);
for (let i = 0; i < techs.length; ++i)
{
let tech = techs[i];
if (tech.indexOf("{civ}") == -1)
continue;
let civTech = tech.replace("{civ}", civ);
techs[i] = TechnologyTemplates.Has(civTech) ?
civTech : tech.replace("{civ}", "generic");
}
return techs;
},
"resourceSupplyType": function() {
if (!this.get("ResourceSupply"))
return undefined;
let [type, subtype] = this.get("ResourceSupply/Type").split('.');
return { "generic": type, "specific": subtype };
},
// will return either "food", "wood", "stone", "metal" and not treasure.
"getResourceType": function() {
if (!this.get("ResourceSupply"))
return undefined;
let [type, subtype] = this.get("ResourceSupply/Type").split('.');
if (type == "treasure")
return subtype;
return type;
},
"getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); },
"resourceSupplyMax": function() { return +this.get("ResourceSupply/Amount"); },
"maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); },
"resourceGatherRates": function() {
if (!this.get("ResourceGatherer"))
return undefined;
let ret = {};
let baseSpeed = +this.get("ResourceGatherer/BaseSpeed");
for (let r in this.get("ResourceGatherer/Rates"))
ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed;
return ret;
},
"resourceDropsiteTypes": function() {
if (!this.get("ResourceDropsite"))
return undefined;
let types = this.get("ResourceDropsite/Types");
return types ? types.split(/\s+/) : [];
},
"garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); },
"garrisonMax": function() { return this.get("GarrisonHolder/Max"); },
"garrisonEjectHealth": function() { return +this.get("GarrisonHolder/EjectHealth"); },
"getDefaultArrow": function() { return +this.get("BuildingAI/DefaultArrowCount"); },
"getArrowMultiplier": function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); },
"getGarrisonArrowClasses": function() {
if (!this.get("BuildingAI"))
return undefined;
return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/);
},
"buffHeal": function() { return +this.get("GarrisonHolder/BuffHeal"); },
"promotion": function() { return this.get("Promotion/Entity"); },
"isPackable": function() { return this.get("Pack") != undefined; },
/**
* Returns whether this is an animal that is too difficult to hunt.
*/
"isHuntable": function() {
if(!this.get("ResourceSupply/KillBeforeGather"))
return false;
// do not hunt retaliating animals (animals without UnitAI are dead animals)
let behaviour = this.get("UnitAI/NaturalBehaviour");
return !behaviour ||
behaviour != "violent" && behaviour != "aggressive" && behaviour != "defensive";
},
"walkSpeed": function() { return +this.get("UnitMotion/WalkSpeed"); },
"trainingCategory": function() { return this.get("TrainingRestrictions/Category"); },
"buildTime": function(productionQueue) {
let time = +this.get("Cost/BuildTime");
if (productionQueue)
time *= productionQueue.techCostMultiplier("time");
return time;
},
"buildCategory": function() { return this.get("BuildRestrictions/Category"); },
"buildDistance": function() {
let distance = this.get("BuildRestrictions/Distance");
if (!distance)
return undefined;
let ret = {};
for (let key in distance)
ret[key] = this.get("BuildRestrictions/Distance/" + key);
return ret;
},
"buildPlacementType": function() { return this.get("BuildRestrictions/PlacementType"); },
"buildTerritories": function() {
if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Territory"))
return undefined;
return this.get("BuildRestrictions/Territory").split(/\s+/);
},
"hasBuildTerritory": function(territory) {
let territories = this.buildTerritories();
return territories && territories.indexOf(territory) != -1;
},
"hasTerritoryInfluence": function() {
return this.get("TerritoryInfluence") !== undefined;
},
"hasDefensiveFire": function() {
if (!this.get("Attack/Ranged"))
return false;
return this.getDefaultArrow() || this.getArrowMultiplier();
},
"territoryInfluenceRadius": function() {
if (this.get("TerritoryInfluence") !== undefined)
return +this.get("TerritoryInfluence/Radius");
return -1;
},
"territoryInfluenceWeight": function() {
if (this.get("TerritoryInfluence") !== undefined)
return +this.get("TerritoryInfluence/Weight");
return -1;
},
"territoryDecayRate": function() {
return +(this.get("TerritoryDecay/DecayRate") || 0);
},
"defaultRegenRate": function() {
return +(this.get("Capturable/RegenRate") || 0);
},
"garrisonRegenRate": function() {
return +(this.get("Capturable/GarrisonRegenRate") || 0);
},
"visionRange": function() { return +this.get("Vision/Range"); },
"gainMultiplier": function() { return +this.get("Trader/GainMultiplier"); },
"isBuilder": function() { return this.get("Builder") !== undefined; },
"isGatherer": function() { return this.get("ResourceGatherer") !== undefined; },
"canGather": function(type) {
let gatherRates = this.get("ResourceGatherer/Rates");
if (!gatherRates)
return false;
for (let r in gatherRates)
if (r.split('.')[0] === type)
return true;
return false;
},
"isGarrisonHolder": function() { return this.get("GarrisonHolder") !== undefined; },
/**
* returns true if the tempalte can capture the given target entity
* if no target is given, returns true if the template has the Capture attack
*/
"canCapture": function(target)
{
if (!this.get("Attack/Capture"))
return false;
if (!target)
return true;
if (!target.get("Capturable"))
return false;
let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string");
return !restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses);
},
"isCapturable": function() { return this.get("Capturable") !== undefined; },
"canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; },
"canGarrison": function() { return "Garrisonable" in this._template; },
});
// defines an entity, with a super Template.
// also redefines several of the template functions where the only change is applying aura and tech modifications.
m.Entity = m.Class({
"_super": m.Template,
"_init": function(sharedAI, entity)
{
this._super.call(this, sharedAI, entity.template, sharedAI.GetTemplate(entity.template));
this._entity = entity;
this._ai = sharedAI;
// save a reference to the template tech modifications
if (!sharedAI._templatesModifications[this._templateName])
sharedAI._templatesModifications[this._templateName] = {};
this._templateModif = sharedAI._templatesModifications[this._templateName];
// save a reference to the entity tech/aura modifications
if (!sharedAI._entitiesModifications.has(entity.id))
sharedAI._entitiesModifications.set(entity.id, new Map());
this._entityModif = sharedAI._entitiesModifications.get(entity.id);
},
"toString": function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; },
"id": function() { return this._entity.id; },
/**
* Returns extra data that the AI scripts have associated with this entity,
* for arbitrary local annotations.
* (This data should not be shared with any other AI scripts.)
*/
"getMetadata": function(player, key) { return this._ai.getMetadata(player, this, key); },
/**
* Sets extra data to be associated with this entity.
*/
"setMetadata": function(player, key, value) { this._ai.setMetadata(player, this, key, value); },
"deleteAllMetadata": function(player) { delete this._ai._entityMetadata[player][this.id()]; },
"deleteMetadata": function(player, key) { this._ai.deleteMetadata(player, this, key); },
"position": function() { return this._entity.position; },
"angle": function() { return this._entity.angle; },
"isIdle": function() {
if (typeof this._entity.idle === "undefined")
return undefined;
return this._entity.idle;
},
"getStance": function() { return this._entity.stance !== undefined ? this._entity.stance : undefined; },
"unitAIState": function() { return this._entity.unitAIState !== undefined ? this._entity.unitAIState : undefined; },
"unitAIOrderData": function() { return this._entity.unitAIOrderData !== undefined ? this._entity.unitAIOrderData : undefined; },
"hitpoints": function() { return this._entity.hitpoints !== undefined ? this._entity.hitpoints : undefined; },
"isHurt": function() { return this.hitpoints() < this.maxHitpoints(); },
"healthLevel": function() { return this.hitpoints() / this.maxHitpoints(); },
"needsHeal": function() { return this.isHurt() && this.isHealable(); },
"needsRepair": function() { return this.isHurt() && this.isRepairable(); },
"decaying": function() { return this._entity.decaying !== undefined ? this._entity.decaying : undefined; },
"capturePoints": function() {return this._entity.capturePoints !== undefined ? this._entity.capturePoints : undefined; },
"isInvulnerable": function() { return this._entity.invulnerability || false; },
"isSharedDropsite": function() { return this._entity.sharedDropsite === true; },
/**
* Returns the current training queue state, of the form
* [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ]
*/
"trainingQueue": function() {
let queue = this._entity.trainingQueue;
return queue;
},
"trainingQueueTime": function() {
let queue = this._entity.trainingQueue;
if (!queue)
return undefined;
let time = 0;
for (let item of queue)
time += item.timeRemaining;
return time/1000;
},
"foundationProgress": function() {
if (this._entity.foundationProgress === undefined)
return undefined;
return this._entity.foundationProgress;
},
"getBuilders": function() {
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return [];
return this._entity.foundationBuilders;
},
"getBuildersNb": function() {
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return 0;
return this._entity.foundationBuilders.length;
},
"owner": function() {
return this._entity.owner;
},
"isOwn": function(player) {
if (typeof this._entity.owner === "undefined")
return false;
return this._entity.owner === player;
},
"resourceSupplyAmount": function() {
if (this._entity.resourceSupplyAmount === undefined)
return undefined;
return this._entity.resourceSupplyAmount;
},
"resourceSupplyNumGatherers": function()
{
if (this._entity.resourceSupplyNumGatherers !== undefined)
return this._entity.resourceSupplyNumGatherers;
return undefined;
},
"isFull": function()
{
if (this._entity.resourceSupplyNumGatherers !== undefined)
return this.maxGatherers() === this._entity.resourceSupplyNumGatherers;
return undefined;
},
"resourceCarrying": function() {
if (this._entity.resourceCarrying === undefined)
return undefined;
return this._entity.resourceCarrying;
},
"currentGatherRate": function() {
// returns the gather rate for the current target if applicable.
if (!this.get("ResourceGatherer"))
return undefined;
if (this.unitAIOrderData().length &&
(this.unitAIState().split(".")[1] == "GATHER" || this.unitAIState().split(".")[1] == "RETURNRESOURCE"))
{
let res;
// this is an abuse of "_ai" but it works.
if (this.unitAIState().split(".")[1] == "GATHER" && this.unitAIOrderData()[0].target !== undefined)
res = this._ai._entities.get(this.unitAIOrderData()[0].target);
else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined)
res = this._ai._entities.get(this.unitAIOrderData()[1].target);
if (!res)
return 0;
let type = res.resourceSupplyType();
if (!type)
return 0;
if (type.generic == "treasure")
return 1000;
let tstring = type.generic + "." + type.specific;
let rate = +this.get("ResourceGatherer/BaseSpeed");
rate *= +this.get("ResourceGatherer/Rates/" +tstring);
if (rate)
return rate;
return 0;
}
return undefined;
},
"garrisoned": function() { return this._entity.garrisoned; },
"canGarrisonInside": function() { return this._entity.garrisoned.length < this.garrisonMax(); },
/**
* returns true if the entity can attack (including capture) the given class.
*/
"canAttackClass": function(aClass)
{
if (!this.get("Attack"))
return false;
for (let type in this.get("Attack"))
{
if (type == "Slaughter")
continue;
let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses))
return true;
}
return false;
},
"move": function(x, z, queued = false) {
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued });
return this;
},
"moveToRange": function(x, z, min, max, queued = false) {
Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued });
return this;
},
"attackMove": function(x, z, targetClasses, allowCapture = true, queued = false) {
Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued });
return this;
},
// violent, aggressive, defensive, passive, standground
"setStance": function(stance, queued = false) {
if (this.getStance() === undefined)
return undefined;
Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance, "queued": queued });
return this;
},
"stopMoving": function() {
Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": false });
},
"unload": function(id) {
if (!this.get("GarrisonHolder"))
return undefined;
Engine.PostCommand(PlayerID, { "type": "unload", "garrisonHolder": this.id(), "entities": [id] });
return this;
},
// Unloads all owned units, don't unload allies
"unloadAll": function() {
if (!this.get("GarrisonHolder"))
return undefined;
Engine.PostCommand(PlayerID, { "type": "unload-all-by-owner", "garrisonHolders": [this.id()] });
return this;
},
"garrison": function(target, queued = false) {
Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued });
return this;
},
"attack": function(unitId, allowCapture = true, queued = false) {
Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued });
return this;
},
// moveApart from a point in the opposite direction with a distance dist
"moveApart": function(point, dist) {
if (this.position() !== undefined) {
let direction = [this.position()[0] - point[0], this.position()[1] - point[1]];
let norm = m.VectorDistance(point, this.position());
if (norm === 0)
direction = [1, 0];
else
{
direction[0] /= norm;
direction[1] /= norm;
}
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false });
}
return this;
},
// Flees from a unit in the opposite direction.
"flee": function(unitToFleeFrom) {
if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) {
let FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0],
this.position()[1] - unitToFleeFrom.position()[1]];
let dist = m.VectorDistance(unitToFleeFrom.position(), this.position());
FleeDirection[0] = 40 * FleeDirection[0]/dist;
FleeDirection[1] = 40 * FleeDirection[1]/dist;
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false });
}
return this;
},
"gather": function(target, queued = false) {
Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued });
return this;
},
"repair": function(target, autocontinue = false, queued = false) {
Engine.PostCommand(PlayerID, { "type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued });
return this;
},
"returnResources": function(target, queued = false) {
Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued });
return this;
},
"destroy": function() {
Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": [this.id()] });
return this;
},
"barter": function(buyType, sellType, amount) {
Engine.PostCommand(PlayerID, { "type": "barter", "sell": sellType, "buy": buyType, "amount": amount });
return this;
},
"tradeRoute": function(target, source) {
Engine.PostCommand(PlayerID, { "type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false });
return this;
},
"setRallyPoint": function(target, command) {
let data = { "command": command, "target": target.id() };
Engine.PostCommand(PlayerID, { "type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data });
return this;
},
"unsetRallyPoint": function() {
Engine.PostCommand(PlayerID, { "type": "unset-rallypoint", "entities": [this.id()] });
return this;
},
"train": function(civ, type, count, metadata, promotedTypes)
{
let trainable = this.trainableEntities(civ);
if (!trainable)
{
error("Called train("+type+", "+count+") on non-training entity "+this);
return this;
}
if (trainable.indexOf(type) == -1)
{
error("Called train("+type+", "+count+") on entity "+this+" which can't train that");
return this;
}
Engine.PostCommand(PlayerID, {
"type": "train",
"entities": [this.id()],
"template": type,
"count": count,
"metadata": metadata,
"promoted": promotedTypes
});
return this;
},
"construct": function(template, x, z, angle, metadata) {
// TODO: verify this unit can construct this, just for internal
// sanity-checking and error reporting
Engine.PostCommand(PlayerID, {
"type": "construct",
"entities": [this.id()],
"template": template,
"x": x,
"z": z,
"angle": angle,
"autorepair": false,
"autocontinue": false,
"queued": false,
"metadata": metadata // can be undefined
});
return this;
},
"research": function(template) {
Engine.PostCommand(PlayerID, { "type": "research", "entity": this.id(), "template": template });
return this;
},
"stopProduction": function(id) {
Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": id });
return this;
},
"stopAllProduction": function(percentToStopAt) {
let queue = this._entity.trainingQueue;
if (!queue)
return true; // no queue, so technically we stopped all production.
for (let item of queue)
if (item.progress < percentToStopAt)
Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": item.id });
return this;
},
"guard": function(target, queued = false) {
Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued });
return this;
},
"removeGuard": function() {
Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] });
return this;
}
});
return m;
}(API3);
Index: ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js (revision 22754)
@@ -1,393 +1,393 @@
function AIProxy() {}
AIProxy.prototype.Schema =
"";
/**
* AIProxy passes its entity's state data to AI scripts.
*
* Efficiency is critical: there can be many thousands of entities,
* and the data returned by this component is serialized and copied to
* the AI thread every turn, so it can be quite expensive.
*
* We omit all data that can be derived statically from the template XML
* files - the AI scripts can parse the templates themselves.
* This violates the component interface abstraction and is potentially
* fragile if the template formats change (since both the component code
* and the AI will have to be updated in sync), but it's not *that* bad
* really and it helps performance significantly.
*
* We also add an optimisation to avoid copying non-changing values.
* The first call to GetRepresentation calls GetFullRepresentation,
* which constructs the complete entity state representation.
* After that, we simply listen to events from the rest of the gameplay code,
* and store the changed data in this.changes.
* Properties in this.changes will override those previously returned
* from GetRepresentation; if a property isn't overridden then the AI scripts
* will keep its old value.
*
* The event handlers should set this.changes.whatever to exactly the
* same as GetFullRepresentation would set.
*/
AIProxy.prototype.Init = function()
{
this.changes = null;
this.needsFullGet = true;
this.cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
};
AIProxy.prototype.Serialize = null; // we have no dynamic state to save
AIProxy.prototype.Deserialize = function()
{
this.Init();
};
AIProxy.prototype.GetRepresentation = function()
{
// Return the full representation the first time we're called
let ret;
if (this.needsFullGet)
ret = this.GetFullRepresentation();
else
ret = this.changes;
// Initialise changes to null instead of {}, to avoid memory allocations in the
// common case where there will be no changes; event handlers should each reset
// it to {} if needed
this.changes = null;
return ret;
};
AIProxy.prototype.NotifyChange = function()
{
if (this.needsFullGet)
{
// not yet notified, be sure that the owner is set before doing so
// as the Create event is sent only on first ownership changed
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() < 0)
return false;
}
if (!this.changes)
{
this.changes = {};
this.cmpAIInterface.ChangedEntity(this.entity);
}
return true;
};
// AI representation-updating event handlers:
AIProxy.prototype.OnPositionChanged = function(msg)
{
if (!this.NotifyChange())
return;
if (msg.inWorld)
{
this.changes.position = [msg.x, msg.z];
this.changes.angle = msg.a;
}
else
{
this.changes.position = undefined;
this.changes.angle = undefined;
}
};
AIProxy.prototype.OnHealthChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.hitpoints = msg.to;
};
AIProxy.prototype.OnCapturePointsChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.capturePoints = msg.capturePoints;
};
AIProxy.prototype.OnInvulnerabilityChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.invulnerability = msg.invulnerability;
};
AIProxy.prototype.OnUnitIdleChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.idle = msg.idle;
};
AIProxy.prototype.OnUnitStanceChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.stance = msg.to;
};
AIProxy.prototype.OnUnitAIStateChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.unitAIState = msg.to;
};
AIProxy.prototype.OnUnitAIOrderDataChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.unitAIOrderData = msg.to;
};
AIProxy.prototype.OnProductionQueueChanged = function(msg)
{
if (!this.NotifyChange())
return;
let cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue);
this.changes.trainingQueue = cmpProductionQueue.GetQueue();
};
AIProxy.prototype.OnGarrisonedUnitsChanged = function(msg)
{
if (!this.NotifyChange())
return;
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
this.changes.garrisoned = cmpGarrisonHolder.GetEntities();
// Send a message telling a unit garrisoned or ungarrisoned.
// I won't check if the unit is still alive so it'll be up to the AI.
for (let ent of msg.added)
this.cmpAIInterface.PushEvent("Garrison", { "entity": ent, "holder": this.entity });
for (let ent of msg.removed)
this.cmpAIInterface.PushEvent("UnGarrison", { "entity": ent, "holder": this.entity });
};
AIProxy.prototype.OnResourceSupplyChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.resourceSupplyAmount = msg.to;
};
AIProxy.prototype.OnResourceSupplyNumGatherersChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.resourceSupplyNumGatherers = msg.to;
};
AIProxy.prototype.OnResourceCarryingChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.resourceCarrying = msg.to;
};
AIProxy.prototype.OnFoundationProgressChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.foundationProgress = msg.to;
};
AIProxy.prototype.OnFoundationBuildersChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.foundationBuilders = msg.to;
};
AIProxy.prototype.OnDropsiteSharingChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.sharedDropsite = msg.shared;
};
AIProxy.prototype.OnTerritoryDecayChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.decaying = msg.to;
this.cmpAIInterface.PushEvent("TerritoryDecayChanged", msg);
};
// TODO: event handlers for all the other things
AIProxy.prototype.GetFullRepresentation = function()
{
this.needsFullGet = false;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let ret = {
// These properties are constant and won't need to be updated
"id": this.entity,
"template": cmpTemplateManager.GetCurrentTemplateName(this.entity)
};
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition)
{
// Updated by OnPositionChanged
if (cmpPosition.IsInWorld())
{
let pos = cmpPosition.GetPosition2D();
ret.position = [pos.x, pos.y];
ret.angle = cmpPosition.GetRotation().y;
}
else
{
ret.position = undefined;
ret.angle = undefined;
}
}
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
if (cmpHealth)
{
// Updated by OnHealthChanged
ret.hitpoints = cmpHealth.GetHitpoints();
}
- let cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
- if (cmpDamageReceiver)
- ret.invulnerability = cmpDamageReceiver.IsInvulnerable();
+ let cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance);
+ if (cmpResistance)
+ ret.invulnerability = cmpResistance.IsInvulnerable();
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership)
{
// Updated by OnOwnershipChanged
ret.owner = cmpOwnership.GetOwner();
}
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
if (cmpUnitAI)
{
// Updated by OnUnitIdleChanged
ret.idle = cmpUnitAI.IsIdle();
// Updated by OnUnitStanceChanged
ret.stance = cmpUnitAI.GetStanceName();
// Updated by OnUnitAIStateChanged
ret.unitAIState = cmpUnitAI.GetCurrentState();
// Updated by OnUnitAIOrderDataChanged
ret.unitAIOrderData = cmpUnitAI.GetOrderData();
}
let cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue);
if (cmpProductionQueue)
{
// Updated by OnProductionQueueChanged
ret.trainingQueue = cmpProductionQueue.GetQueue();
}
let cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation);
if (cmpFoundation)
{
// Updated by OnFoundationProgressChanged
ret.foundationProgress = cmpFoundation.GetBuildPercentage();
}
let cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply);
if (cmpResourceSupply)
{
// Updated by OnResourceSupplyChanged
ret.resourceSupplyAmount = cmpResourceSupply.GetCurrentAmount();
ret.resourceSupplyNumGatherers = cmpResourceSupply.GetNumGatherers();
}
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
// Updated by OnResourceCarryingChanged
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
}
let cmpResourceDropsite = Engine.QueryInterface(this.entity, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
// Updated by OnDropsiteSharingChanged
ret.sharedDropsite = cmpResourceDropsite.IsShared();
}
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
// Updated by OnGarrisonedUnitsChanged
ret.garrisoned = cmpGarrisonHolder.GetEntities();
}
let cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay);
if (cmpTerritoryDecay)
ret.decaying = cmpTerritoryDecay.IsDecaying();
let cmpCapturable = Engine.QueryInterface(this.entity, IID_Capturable);
if (cmpCapturable)
ret.capturePoints = cmpCapturable.GetCapturePoints();
return ret;
};
// AI event handlers:
// (These are passed directly as events to the AI scripts, rather than updating
// our proxy representation.)
// (This shouldn't include extremely high-frequency events, like PositionChanged,
// because that would be very expensive and AI will rarely care about all those
// events.)
// special case: this changes the state and sends an event.
AIProxy.prototype.OnOwnershipChanged = function(msg)
{
this.NotifyChange();
if (msg.from == INVALID_PLAYER)
{
this.cmpAIInterface.PushEvent("Create", { "entity": msg.entity });
return;
}
if (msg.to == INVALID_PLAYER)
{
this.cmpAIInterface.PushEvent("Destroy", { "entity": msg.entity });
return;
}
this.changes.owner = msg.to;
this.cmpAIInterface.PushEvent("OwnershipChanged", msg);
};
AIProxy.prototype.OnAttacked = function(msg)
{
this.cmpAIInterface.PushEvent("Attacked", msg);
};
AIProxy.prototype.OnConstructionFinished = function(msg)
{
this.cmpAIInterface.PushEvent("ConstructionFinished", msg);
};
AIProxy.prototype.OnTrainingStarted = function(msg)
{
this.cmpAIInterface.PushEvent("TrainingStarted", msg);
};
AIProxy.prototype.OnTrainingFinished = function(msg)
{
this.cmpAIInterface.PushEvent("TrainingFinished", msg);
};
AIProxy.prototype.OnAIMetadata = function(msg)
{
this.cmpAIInterface.PushEvent("AIMetadata", msg);
};
Engine.RegisterComponentType(IID_AIProxy, "AIProxy", AIProxy);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Armour.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Armour.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Armour.js (revision 22754)
@@ -1,86 +1,73 @@
function Armour() {}
+Armour.prototype.DamageResistanceSchema = "" +
+ "" +
+ "" +
+ "" +
+ "Foundation" +
+ "" +
+ "" +
+ "" +
+ "";
+
Armour.prototype.Schema =
"Controls the damage resistance of the unit." +
"" +
"10.0" +
"0.0" +
"5.0" +
"" +
- BuildDamageTypesSchema("damage protection") +
+ Armour.prototype.DamageResistanceSchema +
"" +
"" +
- "" +
- BuildDamageTypesSchema("damage protection") +
- "" +
+ Armour.prototype.DamageResistanceSchema +
"" +
"";
Armour.prototype.Init = function()
{
this.invulnerable = false;
};
Armour.prototype.IsInvulnerable = function()
{
return this.invulnerable;
};
Armour.prototype.SetInvulnerability = function(invulnerability)
{
this.invulnerable = invulnerability;
Engine.PostMessage(this.entity, MT_InvulnerabilityChanged, { "entity": this.entity, "invulnerability": invulnerability });
};
-/**
- * Take damage according to the entity's armor.
- * @param {Object} strengths - { "hack": number, "pierce": number, "crush": number } or something like that.
- * @param {number} multiplier - the damage multiplier.
- * Returns object of the form { "killed": false, "change": -12 }.
- */
-Armour.prototype.TakeDamage = function(strengths, multiplier = 1)
-{
- if (this.invulnerable)
- return { "killed": false, "change": 0 };
-
- // Adjust damage values based on armour; exponential armour: damage = attack * 0.9^armour
- var armourStrengths = this.GetArmourStrengths();
-
- // Total is sum of individual damages
- // Don't bother rounding, since HP is no longer integral.
- var total = 0;
- for (let type in strengths)
- total += strengths[type] * multiplier * Math.pow(0.9, armourStrengths[type] || 0);
-
- // Reduce health
- var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
- return cmpHealth.Reduce(total);
-};
-
-Armour.prototype.GetArmourStrengths = function()
+Armour.prototype.GetArmourStrengths = function(effectType)
{
// Work out the armour values with technology effects.
let applyMods = (type, foundation) => {
let strength;
if (foundation)
{
strength = +this.template.Foundation[type];
type = "Foundation/" + type;
}
else
strength = +this.template[type];
- return ApplyValueModificationsToEntity("Armour/" + type, strength, this.entity);
+ return ApplyValueModificationsToEntity("Armour/" + effectType + "/" + type, strength, this.entity);
};
let foundation = Engine.QueryInterface(this.entity, IID_Foundation) && this.template.Foundation;
let ret = {};
+
+ if (effectType != "Damage")
+ return ret;
+
for (let damageType in this.template)
if (damageType != "Foundation")
ret[damageType] = applyMods(damageType, foundation);
return ret;
};
-Engine.RegisterComponentType(IID_DamageReceiver, "Armour", Armour);
+Engine.RegisterComponentType(IID_Resistance, "Armour", Armour);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 22754)
@@ -1,725 +1,611 @@
function Attack() {}
var g_AttackTypes = ["Melee", "Ranged", "Capture"];
-Attack.prototype.statusEffectsSchema =
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "";
-
-Attack.prototype.bonusesSchema =
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "";
-
Attack.prototype.preferredClassesSchema =
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"";
Attack.prototype.restrictedClassesSchema =
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"";
Attack.prototype.Schema =
"Controls the attack abilities and strengths of the unit." +
"" +
"" +
"" +
"10.0" +
"0.0" +
"5.0" +
"" +
"4.0" +
"1000" +
"" +
"" +
"pers" +
"Infantry" +
"1.5" +
"" +
"" +
"Cavalry Melee" +
"1.5" +
"" +
"" +
"Champion" +
"Cavalry Infantry" +
"" +
"" +
"" +
"0.0" +
"10.0" +
"0.0" +
"" +
"44.0" +
"20.0" +
"15.0" +
"800" +
"1600" +
"1000" +
"" +
"" +
"Cavalry" +
"2" +
"" +
"" +
"" +
"50.0" +
"2.5" +
"props/units/weapons/rock_flaming.xml" +
"props/units/weapons/rock_explosion.xml" +
"0.1" +
"" +
"Champion" +
"" +
"Circular" +
"20" +
"false" +
"" +
"0.0" +
"10.0" +
"0.0" +
"" +
"" +
"" +
"" +
"" +
"1000.0" +
"0.0" +
"0.0" +
"" +
"4.0" +
"" +
"" +
"" +
"" +
"" +
- "" +
- BuildDamageTypesSchema("damage strength") +
- "" +
+ Attacking.BuildAttackEffectsSchema() +
"" +
"" +
"" +
"" +
"" + // TODO: it shouldn't be stretched
"" +
"" +
- Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
- "" +
- BuildDamageTypesSchema("damage strength") +
- "" +
+ Attacking.BuildAttackEffectsSchema() +
"" +
"" +
""+
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
- "" +
- BuildDamageTypesSchema("damage strength") +
- "" +
- Attack.prototype.bonusesSchema +
+ Attacking.BuildAttackEffectsSchema() +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
- Attack.prototype.statusEffectsSchema +
- Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
- "" +
+ Attacking.BuildAttackEffectsSchema() +
"" +
"" + // TODO: it shouldn't be stretched
"" +
"" +
- Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"" +
"" +
"" +
"" +
- "" +
- BuildDamageTypesSchema("damage strength") +
- "" +
+ Attacking.BuildAttackEffectsSchema() +
"" + // TODO: how do these work?
- Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"" +
"" +
"";
Attack.prototype.Init = function()
{
};
Attack.prototype.Serialize = null; // we have no dynamic state to save
Attack.prototype.GetAttackTypes = function(wantedTypes)
{
let types = g_AttackTypes.filter(type => !!this.template[type]);
if (!wantedTypes)
return types;
let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0);
return types.filter(type => wantedTypes.indexOf("!" + type) == -1 &&
(!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1));
};
Attack.prototype.GetPreferredClasses = function(type)
{
if (this.template[type] && this.template[type].PreferredClasses &&
this.template[type].PreferredClasses._string)
return this.template[type].PreferredClasses._string.split(/\s+/);
return [];
};
Attack.prototype.GetRestrictedClasses = function(type)
{
if (this.template[type] && this.template[type].RestrictedClasses &&
this.template[type].RestrictedClasses._string)
return this.template[type].RestrictedClasses._string.split(/\s+/);
return [];
};
Attack.prototype.CanAttack = function(target, wantedTypes)
{
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
return true;
let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
return false;
let cmpIdentity = QueryMiragedInterface(target, IID_Identity);
if (!cmpIdentity)
return false;
let cmpHealth = QueryMiragedInterface(target, IID_Health);
let targetClasses = cmpIdentity.GetClassesList();
if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && cmpHealth && cmpHealth.GetHitpoints() &&
(!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length))
return true;
let cmpEntityPlayer = QueryOwnerInterface(this.entity);
let cmpTargetPlayer = QueryOwnerInterface(target);
if (!cmpTargetPlayer || !cmpEntityPlayer)
return false;
let types = this.GetAttackTypes(wantedTypes);
let entityOwner = cmpEntityPlayer.GetPlayerID();
let targetOwner = cmpTargetPlayer.GetPlayerID();
let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
// Check if the relative height difference is larger than the attack range
// If the relative height is bigger, it means they will never be able to
// reach each other, no matter how close they come.
let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
for (let type of types)
{
if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints()))
continue;
if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner)))
continue;
if (heightDiff > this.GetRange(type).max)
continue;
let restrictedClasses = this.GetRestrictedClasses(type);
if (!restrictedClasses.length)
return true;
if (!MatchesClassList(targetClasses, restrictedClasses))
return true;
}
return false;
};
/**
* Returns null if we have no preference or the lowest index of a preferred class.
*/
Attack.prototype.GetPreference = function(target)
{
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return undefined;
let targetClasses = cmpIdentity.GetClassesList();
let minPref = null;
for (let type of this.GetAttackTypes())
{
let preferredClasses = this.GetPreferredClasses(type);
for (let targetClass of targetClasses)
{
let pref = preferredClasses.indexOf(targetClass);
if (pref === 0)
return pref;
if (pref != -1 && (minPref === null || minPref > pref))
minPref = pref;
}
}
return minPref;
};
/**
* Get the full range of attack using all available attack types.
*/
Attack.prototype.GetFullAttackRange = function()
{
let ret = { "min": Infinity, "max": 0 };
for (let type of this.GetAttackTypes())
{
let range = this.GetRange(type);
ret.min = Math.min(ret.min, range.min);
ret.max = Math.max(ret.max, range.max);
}
return ret;
};
+Attack.prototype.GetAttackEffectsData = function(type, splash)
+{
+ let tp = this.template[type];
+ if (splash)
+ tp = tp.Splash;
+ return Attacking.GetAttackEffectsData("Attack/" + type + splash ? "/Splash" : "", tp, this.entity);
+};
+
Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
{
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
{
// TODO: Formation against formation needs review
let types = this.GetAttackTypes();
return g_AttackTypes.find(attack => types.indexOf(attack) != -1);
}
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return undefined;
let targetClasses = cmpIdentity.GetClassesList();
let isTargetClass = className => targetClasses.indexOf(className) != -1;
// Always slaughter domestic animals instead of using a normal attack
if (isTargetClass("Domestic") && this.template.Slaughter)
return "Slaughter";
let types = this.GetAttackTypes().filter(type => this.CanAttack(target, [type]));
// Check whether the target is capturable and prefer that when it is allowed.
let captureIndex = types.indexOf("Capture");
if (captureIndex != -1)
{
if (allowCapture)
return "Capture";
types.splice(captureIndex, 1);
}
let isPreferred = className => this.GetPreferredClasses(className).some(isTargetClass);
return types.sort((a, b) =>
(types.indexOf(a) + (isPreferred(a) ? types.length : 0)) -
(types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop();
};
Attack.prototype.CompareEntitiesByPreference = function(a, b)
{
let aPreference = this.GetPreference(a);
let bPreference = this.GetPreference(b);
if (aPreference === null && bPreference === null) return 0;
if (aPreference === null) return 1;
if (bPreference === null) return -1;
return aPreference - bPreference;
};
Attack.prototype.GetTimers = function(type)
{
let prepare = +(this.template[type].PrepareTime || 0);
prepare = ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", prepare, this.entity);
let repeat = +(this.template[type].RepeatTime || 1000);
repeat = ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeat, this.entity);
return { "prepare": prepare, "repeat": repeat };
};
-Attack.prototype.GetAttackStrengths = function(type)
-{
- // Work out the attack values with technology effects
- let template = this.template[type];
- let splash = "";
- if (!template)
- {
- template = this.template[type.split(".")[0]].Splash;
- splash = "/Splash";
- }
-
- let applyMods = damageType =>
- ApplyValueModificationsToEntity("Attack/" + type + splash + "/Damage/" + damageType, +(template.Damage[damageType] || 0), this.entity);
-
- if (type == "Capture")
- return { "value": ApplyValueModificationsToEntity("Attack/Capture/Value", +(template.Value || 0), this.entity) };
-
- let ret = {};
- for (let damageType in template.Damage)
- ret[damageType] = applyMods(damageType);
-
- return ret;
-};
-
Attack.prototype.GetSplashDamage = function(type)
{
if (!this.template[type].Splash)
return false;
- let splash = {};
- splash.damage = this.GetAttackStrengths(type + ".Splash");
- splash.friendlyFire = this.template[type].Splash.FriendlyFire != "false";
- splash.shape = this.template[type].Splash.Shape;
- return splash;
+ return {
+ "attackData": this.GetAttackEffectsData(type, true),
+ "friendlyFire": this.template[type].Splash.FriendlyFire != "false",
+ "shape": this.template[type].Splash.Shape,
+ };
};
Attack.prototype.GetRange = function(type)
{
let max = +this.template[type].MaxRange;
max = ApplyValueModificationsToEntity("Attack/" + type + "/MaxRange", max, this.entity);
let min = +(this.template[type].MinRange || 0);
min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity);
let elevationBonus = +(this.template[type].ElevationBonus || 0);
elevationBonus = ApplyValueModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity);
return { "max": max, "min": min, "elevationBonus": elevationBonus };
};
-Attack.prototype.GetBonusTemplate = function(type)
-{
- let template = this.template[type];
- if (!template)
- template = this.template[type.split(".")[0]].Splash;
-
- return template.Bonuses || null;
-};
-
/**
* Attack the target entity. This should only be called after a successful range check,
* and should only be called after GetTimers().repeat msec has passed since the last
* call to PerformAttack.
*/
Attack.prototype.PerformAttack = function(type, target)
{
let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner();
- let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
// If this is a ranged attack, then launch a projectile
if (type == "Ranged")
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let turnLength = cmpTimer.GetLatestTurnLength()/1000;
// In the future this could be extended:
// * Obstacles like trees could reduce the probability of the target being hit
// * Obstacles like walls should block projectiles entirely
let horizSpeed = +this.template[type].Projectile.Speed;
let gravity = +this.template[type].Projectile.Gravity;
- //horizSpeed /= 2; gravity /= 2; // slow it down for testing
+ // horizSpeed /= 2; gravity /= 2; // slow it down for testing
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
let selfPosition = cmpPosition.GetPosition();
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return;
let targetPosition = cmpTargetPosition.GetPosition();
let previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition();
let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength);
let timeToTarget = this.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity);
let predictedPosition = (timeToTarget !== false) ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition;
// Add inaccuracy based on spread.
let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", +this.template[type].Projectile.Spread, this.entity) *
predictedPosition.horizDistanceTo(selfPosition) / 100;
let randNorm = randomNormal2D();
let offsetX = randNorm[0] * distanceModifiedSpread;
let offsetZ = randNorm[1] * distanceModifiedSpread;
let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, targetPosition.y, predictedPosition.z + offsetZ);
// Recalculate when the missile will hit the target position.
let realHorizDistance = realTargetPosition.horizDistanceTo(selfPosition);
timeToTarget = realHorizDistance / horizSpeed;
let missileDirection = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance);
// Launch the graphical projectile.
let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
let actorName = "";
let impactActorName = "";
let impactAnimationLifetime = 0;
actorName = this.template[type].Projectile.ActorName || "";
impactActorName = this.template[type].Projectile.ImpactActorName || "";
impactAnimationLifetime = this.template[type].Projectile.ImpactAnimationLifetime || 0;
// TODO: Use unit rotation to implement x/z offsets.
let deltaLaunchPoint = new Vector3D(0, this.template[type].Projectile.LaunchPoint["@y"], 0.0);
let launchPoint = Vector3D.add(selfPosition, deltaLaunchPoint);
-
+
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
{
// if the projectile definition is missing from the template
// then fallback to the projectile name and launchpoint in the visual actor
if (!actorName)
actorName = cmpVisual.GetProjectileActor();
let visualActorLaunchPoint = cmpVisual.GetProjectileLaunchPoint();
if (visualActorLaunchPoint.length() > 0)
launchPoint = visualActorLaunchPoint;
}
let id = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, realTargetPosition, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime);
let attackImpactSound = "";
let cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
if (cmpSound)
attackImpactSound = cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase());
let data = {
"type": type,
- "attacker": this.entity,
+ "attackData": this.GetAttackEffectsData(type),
"target": target,
- "strengths": this.GetAttackStrengths(type),
+ "attacker": this.entity,
+ "attackerOwner": attackerOwner,
"position": realTargetPosition,
"direction": missileDirection,
"projectileId": id,
- "bonus": this.GetBonusTemplate(type),
- "isSplash": false,
- "attackerOwner": attackerOwner,
- "attackImpactSound": attackImpactSound,
- "statusEffects": this.template[type].StatusEffects
+ "attackImpactSound": attackImpactSound
};
if (this.template[type].Splash)
- {
- data.friendlyFire = this.template[type].Splash.FriendlyFire != "false";
- data.radius = +this.template[type].Splash.Range;
- data.shape = this.template[type].Splash.Shape;
- data.isSplash = true;
- data.splashStrengths = this.GetAttackStrengths(type + ".Splash");
- data.splashBonus = this.GetBonusTemplate(type + ".Splash");
- }
- cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", timeToTarget * 1000 + +this.template[type].Delay, data);
- }
- else if (type == "Capture")
- {
- if (attackerOwner == INVALID_PLAYER)
- return;
-
- let multiplier = GetDamageBonus(this.entity, target, type, this.GetBonusTemplate(type));
- let cmpHealth = Engine.QueryInterface(target, IID_Health);
- if (!cmpHealth || cmpHealth.GetHitpoints() == 0)
- return;
- multiplier *= cmpHealth.GetMaxHitpoints() / (0.1 * cmpHealth.GetMaxHitpoints() + 0.9 * cmpHealth.GetHitpoints());
-
- let cmpCapturable = Engine.QueryInterface(target, IID_Capturable);
- if (!cmpCapturable || !cmpCapturable.CanCapture(attackerOwner))
- return;
-
- let strength = this.GetAttackStrengths("Capture").value * multiplier;
- if (cmpCapturable.Reduce(strength, attackerOwner) && IsOwnedByEnemyOfPlayer(attackerOwner, target))
- Engine.PostMessage(target, MT_Attacked, {
- "attacker": this.entity,
- "target": target,
- "type": type,
- "damage": strength,
- "attackerOwner": attackerOwner
- });
+ data.splash = {
+ "friendlyFire": this.template[type].Splash.FriendlyFire != "false",
+ "radius": +this.template[type].Splash.Range,
+ "shape": this.template[type].Splash.Shape,
+ "attackData": this.GetAttackEffectsData(type, true),
+ };
+ cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "MissileHit", +this.template[type].Delay + timeToTarget * 1000, data);
}
else
- {
- // Melee attack - hurt the target immediately
- cmpDamage.CauseDamage({
- "strengths": this.GetAttackStrengths(type),
- "target": target,
- "attacker": this.entity,
- "multiplier": GetDamageBonus(this.entity, target, type, this.GetBonusTemplate(type)),
- "type": type,
- "attackerOwner": attackerOwner
- });
- }
+ Attacking.HandleAttackEffects(type, this.GetAttackEffectsData(type), target, this.entity, attackerOwner);
};
/**
* Get the predicted time of collision between a projectile (or a chaser)
* and its target, assuming they both move in straight line at a constant speed.
* Vertical component of movement is ignored.
* @param {Vector3D} selfPosition - the 3D position of the projectile (or chaser).
* @param {number} horizSpeed - the horizontal speed of the projectile (or chaser).
* @param {Vector3D} targetPosition - the 3D position of the target.
* @param {Vector3D} targetVelocity - the 3D velocity vector of the target.
* @return {Vector3D|boolean} - the 3D predicted position or false if the collision will not happen.
*/
Attack.prototype.PredictTimeToTarget = function(selfPosition, horizSpeed, targetPosition, targetVelocity)
{
let relativePosition = new Vector3D.sub(targetPosition, selfPosition);
let a = targetVelocity.x * targetVelocity.x + targetVelocity.z * targetVelocity.z - horizSpeed * horizSpeed;
let b = relativePosition.x * targetVelocity.x + relativePosition.z * targetVelocity.z;
let c = relativePosition.x * relativePosition.x + relativePosition.z * relativePosition.z;
// The predicted time to reach the target is the smallest non negative solution
// (when it exists) of the equation a t^2 + 2 b t + c = 0.
// Using c>=0, we can straightly compute the right solution.
if (c == 0)
return 0;
let disc = b * b - a * c;
if (a < 0 || b < 0 && disc >= 0)
return c / (Math.sqrt(disc) - b);
return false;
};
Attack.prototype.OnValueModification = function(msg)
{
if (msg.component != "Attack")
return;
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
if (!cmpUnitAI)
return;
if (this.GetAttackTypes().some(type =>
msg.valueNames.indexOf("Attack/" + type + "/MaxRange") != -1))
cmpUnitAI.UpdateRangeQueries();
};
Attack.prototype.GetRangeOverlays = function()
{
if (!this.template.Ranged || !this.template.Ranged.RangeOverlay)
return [];
let range = this.GetRange("Ranged");
let rangeOverlays = [];
for (let i in range)
if ((i == "min" || i == "max") && range[i])
rangeOverlays.push({
"radius": range[i],
"texture": this.template.Ranged.RangeOverlay.LineTexture,
"textureMask": this.template.Ranged.RangeOverlay.LineTextureMask,
"thickness": +this.template.Ranged.RangeOverlay.LineThickness,
});
return rangeOverlays;
};
Engine.RegisterComponentType(IID_Attack, "Attack", Attack);
Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 22754)
@@ -1,379 +1,379 @@
//Number of rounds of firing per 2 seconds
const roundCount = 10;
const attackType = "Ranged";
function BuildingAI() {}
BuildingAI.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
BuildingAI.prototype.MAX_PREFERENCE_BONUS = 2;
BuildingAI.prototype.Init = function()
{
this.currentRound = 0;
this.archersGarrisoned = 0;
this.arrowsLeft = 0;
this.targetUnits = [];
};
BuildingAI.prototype.OnGarrisonedUnitsChanged = function(msg)
{
let classes = this.template.GarrisonArrowClasses;
for (let ent of msg.added)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity)
continue;
if (MatchesClassList(cmpIdentity.GetClassesList(), classes))
++this.archersGarrisoned;
}
for (let ent of msg.removed)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity)
continue;
if (MatchesClassList(cmpIdentity.GetClassesList(), classes))
--this.archersGarrisoned;
}
};
BuildingAI.prototype.OnOwnershipChanged = function(msg)
{
this.targetUnits = [];
this.SetupRangeQuery();
this.SetupGaiaRangeQuery();
};
BuildingAI.prototype.OnDiplomacyChanged = function(msg)
{
if (!IsOwnedByPlayer(msg.player, this.entity))
return;
// Remove maybe now allied/neutral units
this.targetUnits = [];
this.SetupRangeQuery();
this.SetupGaiaRangeQuery();
};
BuildingAI.prototype.OnDestroy = function()
{
if (this.timer)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
}
// Clean up range queries
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.enemyUnitsQuery)
cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
if (this.gaiaUnitsQuery)
cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
};
/**
* React on Attack value modifications, as it might influence the range
*/
BuildingAI.prototype.OnValueModification = function(msg)
{
if (msg.component != "Attack")
return;
this.targetUnits = [];
this.SetupRangeQuery();
this.SetupGaiaRangeQuery();
};
/**
* Setup the Range Query to detect units coming in & out of range
*/
BuildingAI.prototype.SetupRangeQuery = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.enemyUnitsQuery)
{
cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
this.enemyUnitsQuery = undefined;
}
var cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return;
var enemies = cmpPlayer.GetEnemies();
if (enemies.length && enemies[0] == 0)
enemies.shift(); // remove gaia
if (!enemies.length)
return;
var range = cmpAttack.GetRange(attackType);
this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
this.entity, range.min, range.max, range.elevationBonus,
- enemies, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal"));
+ enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"));
cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
};
// Set up a range query for Gaia units within LOS range which can be attacked.
// This should be called whenever our ownership changes.
BuildingAI.prototype.SetupGaiaRangeQuery = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.gaiaUnitsQuery)
{
cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
this.gaiaUnitsQuery = undefined;
}
var cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer || !cmpPlayer.IsEnemy(0))
return;
var range = cmpAttack.GetRange(attackType);
// This query is only interested in Gaia entities that can attack.
this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
this.entity, range.min, range.max, range.elevationBonus,
[0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal"));
cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery);
};
/**
* Called when units enter or leave range
*/
BuildingAI.prototype.OnRangeUpdate = function(msg)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
// Target enemy units except non-dangerous animals
if (msg.tag == this.gaiaUnitsQuery)
{
msg.added = msg.added.filter(e => {
let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
});
}
else if (msg.tag != this.enemyUnitsQuery)
return;
// Add new targets
for (let entity of msg.added)
if (cmpAttack.CanAttack(entity))
this.targetUnits.push(entity);
// Remove targets outside of vision-range
for (let entity of msg.removed)
{
let index = this.targetUnits.indexOf(entity);
if (index > -1)
this.targetUnits.splice(index, 1);
}
if (this.targetUnits.length)
this.StartTimer();
};
BuildingAI.prototype.StartTimer = function()
{
if (this.timer)
return;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var attackTimers = cmpAttack.GetTimers(attackType);
this.timer = cmpTimer.SetInterval(this.entity, IID_BuildingAI, "FireArrows",
attackTimers.prepare, attackTimers.repeat / roundCount, null);
};
BuildingAI.prototype.GetDefaultArrowCount = function()
{
var arrowCount = +this.template.DefaultArrowCount;
return Math.round(ApplyValueModificationsToEntity("BuildingAI/DefaultArrowCount", arrowCount, this.entity));
};
BuildingAI.prototype.GetMaxArrowCount = function()
{
if (!this.template.MaxArrowCount)
return Infinity;
let maxArrowCount = +this.template.MaxArrowCount;
return Math.round(ApplyValueModificationsToEntity("BuildingAI/MaxArrowCount", maxArrowCount, this.entity));
};
BuildingAI.prototype.GetGarrisonArrowMultiplier = function()
{
var arrowMult = +this.template.GarrisonArrowMultiplier;
return ApplyValueModificationsToEntity("BuildingAI/GarrisonArrowMultiplier", arrowMult, this.entity);
};
BuildingAI.prototype.GetGarrisonArrowClasses = function()
{
var string = this.template.GarrisonArrowClasses;
if (string)
return string.split(/\s+/);
return [];
};
/**
* Returns the number of arrows which needs to be fired.
* DefaultArrowCount + Garrisoned Archers(ie., any unit capable
* of shooting arrows from inside buildings)
*/
BuildingAI.prototype.GetArrowCount = function()
{
let count = this.GetDefaultArrowCount() +
Math.round(this.archersGarrisoned * this.GetGarrisonArrowMultiplier());
return Math.min(count, this.GetMaxArrowCount());
};
BuildingAI.prototype.SetUnitAITarget = function(ent)
{
this.unitAITarget = ent;
if (ent)
this.StartTimer();
};
/**
* Fire arrows with random temporal distribution on prefered targets.
* Called 'roundCount' times every 'RepeatTime' seconds when there are units in the range.
*/
BuildingAI.prototype.FireArrows = function()
{
if (!this.targetUnits.length && !this.unitAITarget)
{
if (!this.timer)
return;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
return;
}
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
if (this.currentRound > roundCount - 1)
this.currentRound = 0;
if (this.currentRound == 0)
this.arrowsLeft = this.GetArrowCount();
let arrowsToFire = 0;
if (this.currentRound == roundCount - 1)
arrowsToFire = this.arrowsLeft;
else
arrowsToFire = Math.min(
randIntInclusive(0, 2 * this.GetArrowCount() / roundCount),
this.arrowsLeft
);
if (arrowsToFire <= 0)
{
++this.currentRound;
return;
}
// Add targets to a weighted list, to allow preferences
let targets = new WeightedList();
let maxPreference = this.MAX_PREFERENCE_BONUS;
let addTarget = function(target)
{
let preference = cmpAttack.GetPreference(target);
let weight = 1;
if (preference !== null && preference !== undefined)
weight += maxPreference / (1 + preference);
targets.push(target, weight);
};
// Add the UnitAI target separately, as the UnitMotion and RangeManager implementations differ
if (this.unitAITarget && this.targetUnits.indexOf(this.unitAITarget) == -1)
addTarget(this.unitAITarget);
for (let target of this.targetUnits)
addTarget(target);
for (let i = 0; i < arrowsToFire; ++i)
{
let selectedIndex = targets.randomIndex();
let selectedTarget = targets.itemAt(selectedIndex);
if (selectedTarget && this.CheckTargetVisible(selectedTarget))
{
cmpAttack.PerformAttack(attackType, selectedTarget);
PlaySound("attack_" + attackType.toLowerCase(), this.entity);
continue;
}
// Could not attack target, retry
targets.removeAt(selectedIndex);
--i;
if (!targets.length())
{
this.arrowsLeft += arrowsToFire;
break;
}
}
this.arrowsLeft -= arrowsToFire;
this.currentRound++;
};
/**
* Returns true if the target entity is visible through the FoW/SoD.
*/
BuildingAI.prototype.CheckTargetVisible = function(target)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
// Entities that are hidden and miraged are considered visible
var cmpFogging = Engine.QueryInterface(target, IID_Fogging);
if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner()))
return true;
// Either visible directly, or visible in fog
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) != "hidden";
};
Engine.RegisterComponentType(IID_BuildingAI, "BuildingAI", BuildingAI);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js (revision 22754)
@@ -1,326 +1,344 @@
function Capturable() {}
Capturable.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
Capturable.prototype.Init = function()
{
// Cache this value
this.maxCp = +this.template.CapturePoints;
this.cp = [];
};
//// Interface functions ////
/**
* Returns the current capture points array
*/
Capturable.prototype.GetCapturePoints = function()
{
return this.cp;
};
Capturable.prototype.GetMaxCapturePoints = function()
{
return this.maxCp;
};
Capturable.prototype.GetGarrisonRegenRate = function()
{
return ApplyValueModificationsToEntity("Capturable/GarrisonRegenRate", +this.template.GarrisonRegenRate, this.entity);
};
/**
* Set the new capture points, used for cloning entities
* The caller should assure that the sum of capture points
* matches the max.
*/
Capturable.prototype.SetCapturePoints = function(capturePointsArray)
{
this.cp = capturePointsArray;
};
+Capturable.prototype.Capture = function(effectData, attacker, attackerOwner, bonusMultiplier)
+{
+ let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
+
+ if (attackerOwner == INVALID_PLAYER || !this.CanCapture(attackerOwner) ||
+ !cmpHealth || cmpHealth.GetHitpoints() == 0)
+ return {};
+
+ bonusMultiplier *= cmpHealth.GetMaxHitpoints() / (0.1 * cmpHealth.GetMaxHitpoints() + 0.9 * cmpHealth.GetHitpoints());
+
+ let total = Attacking.GetTotalAttackEffects({ "Capture": effectData }, "Capture") * bonusMultiplier;
+
+ let change = this.Reduce(total, attackerOwner);
+ // TODO: implement loot
+
+ return { "captureChange": change };
+};
+
/**
* Reduces the amount of capture points of an entity,
* in favour of the player of the source
* Returns the number of capture points actually taken
*/
Capturable.prototype.Reduce = function(amount, playerID)
{
if (amount <= 0)
return 0;
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return 0;
var cmpPlayerSource = QueryPlayerIDInterface(playerID);
if (!cmpPlayerSource)
return 0;
// Before changing the value, activate Fogging if necessary to hide changes
var cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
if (cmpFogging)
cmpFogging.Activate();
var numberOfEnemies = this.cp.filter((v, i) => v > 0 && cmpPlayerSource.IsEnemy(i)).length;
if (numberOfEnemies == 0)
return 0;
// Distribute the capture points over all enemies.
let distributedAmount = amount / numberOfEnemies;
let removedAmount = 0;
while (distributedAmount > 0.0001)
{
numberOfEnemies = 0;
for (let i in this.cp)
{
if (!this.cp[i] || !cmpPlayerSource.IsEnemy(i))
continue;
if (this.cp[i] > distributedAmount)
{
removedAmount += distributedAmount;
this.cp[i] -= distributedAmount;
++numberOfEnemies;
}
else
{
removedAmount += this.cp[i];
this.cp[i] = 0;
}
}
distributedAmount = numberOfEnemies ? (amount - removedAmount) / numberOfEnemies : 0;
}
// give all cp taken to the player
var takenCp = this.maxCp - this.cp.reduce((a, b) => a + b);
this.cp[playerID] += takenCp;
this.CheckTimer();
this.RegisterCapturePointsChanged();
return takenCp;
};
/**
* Check if the source can (re)capture points from this building
*/
Capturable.prototype.CanCapture = function(playerID)
{
var cmpPlayerSource = QueryPlayerIDInterface(playerID);
if (!cmpPlayerSource)
warn(playerID + " has no player component defined on its id");
var cp = this.GetCapturePoints();
var sourceEnemyCp = 0;
for (let i in this.GetCapturePoints())
if (cmpPlayerSource.IsEnemy(i))
sourceEnemyCp += cp[i];
return sourceEnemyCp > 0;
};
//// Private functions ////
/**
* This has to be called whenever the capture points are changed.
* It notifies other components of the change, and switches ownership when needed.
*/
Capturable.prototype.RegisterCapturePointsChanged = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return;
Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.cp });
var owner = cmpOwnership.GetOwner();
if (owner == INVALID_PLAYER || this.cp[owner] > 0)
return;
// If all cp has been taken from the owner, convert it to the best player.
var bestPlayer = 0;
for (let i in this.cp)
if (this.cp[i] >= this.cp[bestPlayer])
bestPlayer = +i;
let cmpLostPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
if (cmpLostPlayerStatisticsTracker)
cmpLostPlayerStatisticsTracker.LostEntity(this.entity);
cmpOwnership.SetOwner(bestPlayer);
let cmpCapturedPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
if (cmpCapturedPlayerStatisticsTracker)
cmpCapturedPlayerStatisticsTracker.CapturedEntity(this.entity);
};
Capturable.prototype.GetRegenRate = function()
{
var regenRate = +this.template.RegenRate;
regenRate = ApplyValueModificationsToEntity("Capturable/RegenRate", regenRate, this.entity);
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
var garrisonRegenRate = this.GetGarrisonRegenRate() * cmpGarrisonHolder.GetEntities().length;
else
var garrisonRegenRate = 0;
return regenRate + garrisonRegenRate;
};
Capturable.prototype.TimerTick = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return;
var owner = cmpOwnership.GetOwner();
var modifiedCp = 0;
// Special handle for the territory decay.
// Reduce cp from the owner in favour of all neighbours (also allies).
var cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay);
if (cmpTerritoryDecay && cmpTerritoryDecay.IsDecaying())
{
var neighbours = cmpTerritoryDecay.GetConnectedNeighbours();
var totalNeighbours = neighbours.reduce((a, b) => a + b);
var decay = Math.min(cmpTerritoryDecay.GetDecayRate(), this.cp[owner]);
this.cp[owner] -= decay;
if (totalNeighbours)
for (let p in neighbours)
this.cp[p] += decay * neighbours[p] / totalNeighbours;
else // decay to gaia as default
this.cp[0] += decay;
modifiedCp += decay;
this.RegisterCapturePointsChanged();
}
var regenRate = this.GetRegenRate();
if (regenRate < 0)
modifiedCp += this.Reduce(-regenRate, 0);
else if (regenRate > 0)
modifiedCp += this.Reduce(regenRate, owner);
if (modifiedCp)
return;
// Nothing changed, stop the timer.
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = 0;
Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": false, "regenRate": 0, "territoryDecay": 0 });
};
/**
* Start the regeneration timer when no timer exists.
* When nothing can be modified (f.e. because it is fully regenerated), the
* timer stops automatically after one execution.
*/
Capturable.prototype.CheckTimer = function()
{
if (this.timer)
return;
var regenRate = this.GetRegenRate();
var cmpDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay);
var decay = cmpDecay && cmpDecay.IsDecaying() ? cmpDecay.GetDecayRate() : 0;
if (regenRate == 0 && decay == 0)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetInterval(this.entity, IID_Capturable, "TimerTick", 1000, 1000, null);
Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": true, "regenRate": regenRate, "territoryDecay": decay });
};
//// Message Listeners ////
Capturable.prototype.OnValueModification = function(msg)
{
if (msg.component != "Capturable")
return;
var oldMaxCp = this.GetMaxCapturePoints();
this.maxCp = ApplyValueModificationsToEntity("Capturable/CapturePoints", +this.template.CapturePoints, this.entity);
if (oldMaxCp == this.maxCp)
return;
var scale = this.maxCp / oldMaxCp;
for (let i in this.cp)
this.cp[i] *= scale;
Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.cp });
this.CheckTimer();
};
Capturable.prototype.OnGarrisonedUnitsChanged = function(msg)
{
this.CheckTimer();
};
Capturable.prototype.OnTerritoryDecayChanged = function(msg)
{
if (msg.to)
this.CheckTimer();
};
Capturable.prototype.OnDiplomacyChanged = function(msg)
{
this.CheckTimer();
};
Capturable.prototype.OnOwnershipChanged = function(msg)
{
if (msg.to == INVALID_PLAYER)
return; // we're dead
if (this.cp.length)
{
if (!this.cp[msg.from])
return; // nothing to change
// Was already initialised, this happens on defeat or wololo
// transfer the points of the old owner to the new one
this.cp[msg.to] += this.cp[msg.from];
this.cp[msg.from] = 0;
this.RegisterCapturePointsChanged();
}
else
{
// Initialise the capture points when created.
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
if (i == msg.to)
this.cp[i] = this.maxCp;
else
this.cp[i] = 0;
}
this.CheckTimer();
};
/**
* When a player is defeated, reassign the cp of non-owned entities to gaia.
* Those owned by the defeated player are dealt with onOwnershipChanged.
*/
Capturable.prototype.OnGlobalPlayerDefeated = function(msg)
{
if (!this.cp[msg.playerId])
return;
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() == msg.playerId)
return;
this.cp[0] += this.cp[msg.playerId];
this.cp[msg.playerId] = 0;
this.RegisterCapturePointsChanged();
this.CheckTimer();
};
Engine.RegisterComponentType(IID_Capturable, "Capturable", Capturable);
Index: ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js (revision 22754)
@@ -1,95 +1,77 @@
function DeathDamage() {}
DeathDamage.prototype.bonusesSchema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
DeathDamage.prototype.Schema =
"When a unit or building is destroyed, it inflicts damage to nearby units." +
"" +
"Circular" +
"20" +
"false" +
"" +
"0.0" +
"10.0" +
"50.0" +
"" +
"" +
"" +
"" +
"" +
- "" +
- BuildDamageTypesSchema("damage strength") +
- "" +
- DeathDamage.prototype.bonusesSchema;
+ Attacking.BuildAttackEffectsSchema();
DeathDamage.prototype.Init = function()
{
};
DeathDamage.prototype.Serialize = null; // we have no dynamic state to save
-DeathDamage.prototype.GetDeathDamageStrengths = function()
+DeathDamage.prototype.GetDeathDamageEffects = function()
{
- // Work out the damage values with technology effects
- let applyMods = damageType =>
- ApplyValueModificationsToEntity("DeathDamage/Damage/" + damageType, +(this.template.Damage[damageType] || 0), this.entity);
-
- let ret = {};
- for (let damageType in this.template.Damage)
- ret[damageType] = applyMods(damageType);
-
- return ret;
-};
-
-DeathDamage.prototype.GetBonusTemplate = function()
-{
- return this.template.Bonuses || null;
+ return Attacking.GetAttackEffectsData("DeathDamage", this.template, this.entity);
};
DeathDamage.prototype.CauseDeathDamage = function()
{
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
let pos = cmpPosition.GetPosition2D();
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
let owner = cmpOwnership.GetOwner();
if (owner == INVALID_PLAYER)
warn("Unit causing death damage does not have any owner.");
- let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
- let playersToDamage = cmpDamage.GetPlayersToDamage(owner, this.template.FriendlyFire);
+ let playersToDamage = Attacking.GetPlayersToDamage(owner, this.template.FriendlyFire);
let radius = ApplyValueModificationsToEntity("DeathDamage/Range", +this.template.Range, this.entity);
- cmpDamage.CauseDamageOverArea({
+ Attacking.CauseDamageOverArea({
+ "type": "Death",
+ "attackData": this.GetDeathDamageEffects(),
"attacker": this.entity,
+ "attackerOwner": owner,
"origin": pos,
"radius": radius,
"shape": this.template.Shape,
- "strengths": this.GetDeathDamageStrengths(),
- "splashBonus": this.GetBonusTemplate(),
"playersToDamage": playersToDamage,
- "type": "Death",
- "attackerOwner": owner
});
};
Engine.RegisterComponentType(IID_DeathDamage, "DeathDamage", DeathDamage);
Index: ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js (revision 22754)
@@ -0,0 +1,85 @@
+function DelayedDamage() {}
+
+DelayedDamage.prototype.Schema =
+ "";
+
+DelayedDamage.prototype.Init = function()
+{
+};
+
+/**
+ * Handles hit logic after the projectile travel time has passed.
+ * @param {Object} data - The data sent by the caller.
+ * @param {string} data.type - The type of damage.
+ * @param {Object} data.attackData - Data of the form { 'effectType': { ...opaque effect data... }, 'Bonuses': {...} }.
+ * @param {number} data.target - The entity id of the target.
+ * @param {number} data.attacker - The entity id of the attacker.
+ * @param {number} data.attackerOwner - The player id of the owner of the attacker.
+ * @param {Vector2D} data.origin - The origin of the projectile hit.
+ * @param {Vector3D} data.position - The expected position of the target.
+ * @param {number} data.projectileId - The id of the projectile.
+ * @param {Vector3D} data.direction - The unit vector defining the direction.
+ * @param {string} data.attackImpactSound - The name of the sound emited on impact.
+ * ***When splash damage***
+ * @param {boolean} data.splash.friendlyFire - A flag indicating if allied entities are also damaged.
+ * @param {number} data.splash.radius - The radius of the splash damage.
+ * @param {string} data.splash.shape - The shape of the splash range.
+ * @param {Object} data.splash.attackData - same as attackData, for splash.
+ */
+DelayedDamage.prototype.MissileHit = function(data, lateness)
+{
+ if (!data.position)
+ return;
+
+ let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager);
+ if (cmpSoundManager && data.attackImpactSound)
+ cmpSoundManager.PlaySoundGroupAtPosition(data.attackImpactSound, data.position);
+
+ // Do this first in case the direct hit kills the target
+ if (data.splash)
+ {
+ Attacking.CauseDamageOverArea({
+ "type": data.type,
+ "attackData": data.splash.attackData,
+ "attacker": data.attacker,
+ "attackerOwner": data.attackerOwner,
+ "origin": Vector2D.from3D(data.position),
+ "radius": data.splash.radius,
+ "shape": data.splash.shape,
+ "direction": data.direction,
+ "playersToDamage": Attacking.GetPlayersToDamage(data.attackerOwner, data.splash.friendlyFire)
+ });
+ }
+
+ let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
+
+ // Deal direct damage if we hit the main target
+ // and if the target has Resistance (not the case for a mirage for example)
+ if (Attacking.TestCollision(data.target, data.position, lateness))
+ {
+ cmpProjectileManager.RemoveProjectile(data.projectileId);
+
+ Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner);
+ return;
+ }
+
+ let targetPosition = Attacking.InterpolatedLocation(data.target, lateness);
+ if (!targetPosition)
+ return;
+
+ // If we didn't hit the main target look for nearby units
+ let cmpPlayer = QueryPlayerIDInterface(data.attackerOwner);
+ let ents = Attacking.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies());
+ for (let ent of ents)
+ {
+ if (!Attacking.TestCollision(ent, data.position, lateness))
+ continue;
+
+ Attacking.HandleAttackEffects(data.type, data.attackData, ent, data.attacker, data.attackerOwner);
+
+ cmpProjectileManager.RemoveProjectile(data.projectileId);
+ break;
+ }
+};
+
+Engine.RegisterSystemComponentType(IID_DelayedDamage, "DelayedDamage", DelayedDamage);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 22754)
@@ -1,1980 +1,1980 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronised for the biggest part
// So most of the attributes shouldn't be serialized
// Return an object with a small selection of deterministic data
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
this.templateModified = {};
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
// Work out what 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";
}
// store player ally/neutral/enemy data as arrays
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).PlayerHasMarket(i),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(i)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = cmpTerrain.GetMapSize();
// Add timeElapsed
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
// Add ceasefire info
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
// Add cinema path info
let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
// Add the game type and allied victory
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
// Add basic statistics to each player
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()
{
// Get basic simulation info
let ret = this.GetSimulationState();
// Add statistics to each player
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;
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"player": INVALID_PLAYER,
"template": template
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"visibleClasses": cmpIdentity.GetVisibleClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable()
};
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(),
};
var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
ret.upgrade = {
"upgrades": cmpUpgrade.GetUpgrades(),
"progress": cmpUpgrade.GetProgress(),
"template": cmpUpgrade.GetUpgradingTo()
};
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"queue": cmpProductionQueue.GetQueue()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods()
};
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"numBuilders": cmpFoundation.GetNumBuilders(),
"buildTime": cmpFoundation.GetBuildTime()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = {
"numBuilders": cmpRepairable.GetNumBuilders(),
"buildTime": cmpRepairable.GetBuildTime()
};
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
};
ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"canPatrol": cmpUnitAI.CanPatrol(),
"selectableStances": cmpUnitAI.GetSelectableStances(),
"isIdle": cmpUnitAI.IsIdle(),
};
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities(),
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
}
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked(),
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = true;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = {};
- if (type == "Capture")
- ret.attack[type] = cmpAttack.GetAttackStrengths(type);
- else
- ret.attack[type].damage = cmpAttack.GetAttackStrengths(type);
+
+ Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type));
ret.attack[type].splash = cmpAttack.GetSplashDamage(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 (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
{
// For units, take the range in front of it, no spread. So angle = 0
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
}
else if(cmpPosition && cmpPosition.IsInWorld())
{
// For buildings, take the average elevation around it. So angle = 2*pi
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI);
}
else
{
// not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
}
- let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
+ let cmpArmour = Engine.QueryInterface(ent, IID_Resistance);
if (cmpArmour)
- ret.armour = cmpArmour.GetArmourStrengths();
+ ret.armour = cmpArmour.GetArmourStrengths("Damage");
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
ret.isBarterMarket = true;
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"hp": cmpHeal.GetHP(),
"range": cmpHeal.GetRange().max,
"rate": cmpHeal.GetRate(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses(),
};
let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
if (cmpLoot)
{
ret.loot = cmpLoot.GetResources();
ret.loot.xp = cmpLoot.GetXp();
}
let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
if (cmpResourceTrickle)
ret.resourceTrickle = {
"interval": cmpResourceTrickle.GetTimer(),
"rates": cmpResourceTrickle.GetRates()
};
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
"run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier()
};
return ret;
};
GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
{
return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, templateName)
{
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, player, aurasTemplate, Resources);
let auraNames = template.Auras._string.split(/\s+/);
for (let name of auraNames)
aurasTemplate[name] = AuraTemplates.Get(name);
return GetTemplateDataHelper(template, player, aurasTemplate, Resources);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
// Checks whether the requirements for this technology have been met
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
// Returns technologies that are being actively researched, along with
// which entity is researching them and how far along the research is.
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech of cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
{
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
ret[tech].timeRemaining = cmpProductionQueue.GetQueue()[0].timeRemaining;
}
else
{
ret[tech].progress = 0;
ret[tech].timeRemaining = 0;
}
}
return ret;
};
// Returns the battle state of the player.
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
// Returns a list of ongoing attacks against the player.
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks();
};
// Used to show a red square over GUI elements you can't yet afford.
GuiInterface.prototype.GetNeededResources = function(player, data)
{
return QueryPlayerIDInterface(data.player || player).GetNeededResources(data.cost);
};
/**
* State of the templateData (player dependent): true when some template values have been modified
* and need to be reloaded by the gui.
*/
GuiInterface.prototype.OnTemplateModification = function(msg)
{
this.templateModified[msg.player] = true;
};
GuiInterface.prototype.IsTemplateModified = function(player)
{
return this.templateModified[player] || false;
};
GuiInterface.prototype.ResetTemplateModified = function()
{
this.templateModified = {};
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default
if (!notification.players)
{
notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
notification.players[0] = -1;
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// filter on players and time, since the delete timer might be executed with a delay
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
return QueryPlayerIDInterface(wantedPlayer).GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
return data.ents.some(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate;
});
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data)
{
let updateEntityColor = (iids, entities) => {
for (let ent of entities)
for (let iid of iids)
{
let cmp = Engine.QueryInterface(ent, iid);
if (cmp)
cmp.UpdateColor();
}
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 1; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i, IID_Player);
if (!cmpPlayer)
continue;
cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors);
if (data.displayDiplomacyColors)
cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]);
updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ?
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] :
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer],
cmpRangeManager.GetEntitiesByPlayer(i));
}
updateEntityColor([IID_Selectable, IID_StatusBars], data.selected);
Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors();
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
let playerColors = {}; // cache of owner -> color map
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
pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set
if (pos)
{
// Only update the position if we changed it (cmd.queued is set)
if ("queued" in cmd)
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z
else
cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
// rebuild the renderer when not set (when reading saved game or in case of building update)
else if (!cmpRallyPointRenderer.IsSet())
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z });
cmpRallyPointRenderer.SetDisplayed(true);
// remember which entities have their rally points displayed so we can hide them again
this.entsRallyPointsDisplayed.push(ent);
}
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": [],
};
// See if we're changing template
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
// Destroy the old preview if there was one
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
// Load the new template
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
// Move the preview into the right location
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
// Set it to a red shade if this is an invalid location
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* 'populationBonus': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
// --------------------------------------------------------------------------------
// do some entity cache management and check for snapping
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// we're clearing the preview, clear the entity cache and bail
for (let tpl in this.placementWallEntities)
{
for (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;
}
// Move all existing cached entities outside of the world and reset their use count
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, tpl),
};
// ensure that the loaded template data contains a wallPiece component
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
// prevent division by zero errors further on if the start and end positions are the same
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// clear the single-building preview entity (we'll be rolling our own)
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// calculate wall placement and position preview entities
let result = {
"pieces": [],
"cost": { "population": 0, "populationBonus": 0, "time": 0 },
};
for (let res of Resources.GetCodes())
result.cost[res] = 0;
let previewEntities = [];
if (end.pos)
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length > 0 && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true, // preview only, must not appear in the result
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle
});
}
if (end.pos)
{
// Analogous to the starting side case above
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length-1].controlGroups = previewEntities[previewEntities.length-1].controlGroups || [];
previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// if we're snapping to a foundation, add an extra preview tower and also set it to the same control group
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
// allocate new entity
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
// reuse an existing one
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// move piece to right location
// TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
// check whether this wall piece can be validly positioned here
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region
// TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta
let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement
validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: we should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
for (let res of Resources.GetCodes().concat(["population", "populationBonus", "time"]))
result.cost[res] += tplData.cost[res];
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest
// (TODO: break unlikely ties by choosing the lowest entity ID)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for (let ent of data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (template.BuildRestrictions.PlacementType == "shore")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
/**
* Find any idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
* @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
* @param data.excludeUnits Array of units to exclude.
*
* Returns an array of idle units.
* If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
*/
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let idleUnits = [];
// The general case is that only the 'first' idle unit is required; filtering would examine every unit.
// This loop imitates a grouping/aggregation on the first matching idle class.
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
{
let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
if (!filtered.idle)
continue;
// If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
// By adding to the 'end', there is no pause if the series of units loops.
var bucket = filtered.bucket;
if(bucket == 0 && data.prevUnit && entity <= data.prevUnit)
bucket = data.idleClasses.length;
if (!idleUnits[bucket])
idleUnits[bucket] = [];
idleUnits[bucket].push(entity);
// If enough units have been collected in the first bucket, go ahead and return them.
if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
return idleUnits[0];
}
let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
if (data.limit && reduced.length > data.limit)
return reduced.slice(0, data.limit);
return reduced;
};
/**
* Discover if the player has idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.excludeUnits Array of units to exclude.
*
* Returns a boolean of whether the player has any idle units
*/
GuiInterface.prototype.HasIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
};
/**
* Whether to filter an idle unit
*
* @param unit The unit to filter.
* @param idleclasses Array of class names to include.
* @param excludeUnits Array of units to exclude.
*
* Returns an object with the following fields:
* - idle - true if the unit is considered idle by the filter, false otherwise.
* - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
*/
GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
return { "idle": false };
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
if(!cmpIdentity)
return { "idle": false };
let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
return { "idle": false };
return { "idle": true, "bucket": bucket };
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
{
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
}
else if (!firstMarket)
{
result = { "type": "set first" };
}
else if (!secondMarket)
{
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
}
else
{
// Else both markets are not null and target is different from them
result = { "type": "set first" };
}
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for (let ent of data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for (let ent of traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player)
{
return QueryPlayerIDInterface(player).GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
// List the GuiInterface functions that can be safely called by GUI scripts.
// (GUI scripts are non-deterministic and untrusted, so these functions must be
// appropriately careful. They are called with a first argument "player", which is
// trusted and indicates the player associated with the current client; no data should
// be returned unless this player is meant to be able to see it.)
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetMultipleEntityStates": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"UpdateDisplayedPlayerColors": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"AddTargetMarker": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"EnableVisualRangeOverlayType": 1,
"SetRangeOverlays": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
"IsTemplateModified": 1,
"ResetTemplateModified": 1
};
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/Health.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Health.js (revision 22754)
@@ -1,418 +1,443 @@
function Health() {}
Health.prototype.Schema =
"Deals with hitpoints and death." +
"" +
"100" +
"1.0" +
"0" +
"corpse" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"0" +
"1" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"vanish" +
"corpse" +
"remain" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
Health.prototype.Init = function()
{
// Cache this value so it allows techs to maintain previous health level
this.maxHitpoints = +this.template.Max;
// Default to , but use if it's undefined or zero
// (Allowing 0 initial HP would break our death detection code)
this.hitpoints = +(this.template.Initial || this.GetMaxHitpoints());
this.regenRate = ApplyValueModificationsToEntity("Health/RegenRate", +this.template.RegenRate, this.entity);
this.idleRegenRate = ApplyValueModificationsToEntity("Health/IdleRegenRate", +this.template.IdleRegenRate, this.entity);
this.CheckRegenTimer();
this.UpdateActor();
};
/**
* Returns the current hitpoint value.
* This is 0 if (and only if) the unit is dead.
*/
Health.prototype.GetHitpoints = function()
{
return this.hitpoints;
};
Health.prototype.GetMaxHitpoints = function()
{
return this.maxHitpoints;
};
/**
* @return {boolean} Whether the units are injured. Dead units are not considered injured.
*/
Health.prototype.IsInjured = function()
{
return this.hitpoints > 0 && this.hitpoints < this.GetMaxHitpoints();
};
Health.prototype.SetHitpoints = function(value)
{
// If we're already dead, don't allow resurrection
if (this.hitpoints == 0)
return;
// Before changing the value, activate Fogging if necessary to hide changes
let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
if (cmpFogging)
cmpFogging.Activate();
let old = this.hitpoints;
this.hitpoints = Math.max(1, Math.min(this.GetMaxHitpoints(), value));
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
cmpRangeManager.SetEntityFlag(this.entity, "injured", this.IsInjured());
this.RegisterHealthChanged(old);
};
Health.prototype.IsRepairable = function()
{
return Engine.QueryInterface(this.entity, IID_Repairable) != null;
};
Health.prototype.IsUnhealable = function()
{
return this.template.Unhealable == "true" ||
this.hitpoints <= 0 || !this.IsInjured();
};
Health.prototype.GetIdleRegenRate = function()
{
return this.idleRegenRate;
};
Health.prototype.GetRegenRate = function()
{
return this.regenRate;
};
Health.prototype.ExecuteRegeneration = function()
{
let regen = this.GetRegenRate();
if (this.GetIdleRegenRate() != 0)
{
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
if (cmpUnitAI && (cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned() && !cmpUnitAI.IsTurret()))
regen += this.GetIdleRegenRate();
}
if (regen > 0)
this.Increase(regen);
else
this.Reduce(-regen);
};
/*
* Check if the regeneration timer needs to be started or stopped
*/
Health.prototype.CheckRegenTimer = function()
{
// check if we need a timer
if (this.GetRegenRate() == 0 && this.GetIdleRegenRate() == 0 ||
!this.IsInjured() && this.GetRegenRate() >= 0 && this.GetIdleRegenRate() >= 0 ||
this.hitpoints == 0)
{
// we don't need a timer, disable if one exists
if (this.regenTimer)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.regenTimer);
this.regenTimer = undefined;
}
return;
}
// we need a timer, enable if one doesn't exist
if (this.regenTimer)
return;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.regenTimer = cmpTimer.SetInterval(this.entity, IID_Health, "ExecuteRegeneration", 1000, 1000, null);
};
Health.prototype.Kill = function()
{
this.Reduce(this.hitpoints);
};
/**
+ * Take damage according to the entity's resistance.
+ * @param {Object} strengths - { "hack": number, "pierce": number, "crush": number } or something like that.
+ * @param {number} bonusMultiplier - the damage multiplier.
+ * Returns object of the form { "killed": false, "change": -12 }.
+ */
+Health.prototype.TakeDamage = function(effectData, attacker, attackerOwner, bonusMultiplier)
+{
+ let cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance);
+
+ if (cmpResistance && cmpResistance.IsInvulnerable())
+ return { "killed": false };
+
+ let total = Attacking.GetTotalAttackEffects(effectData, "Damage", cmpResistance) * bonusMultiplier;
+
+ // Reduce health
+ let change = this.Reduce(total);
+
+ let cmpLoot = Engine.QueryInterface(this.entity, IID_Loot);
+ if (cmpLoot && cmpLoot.GetXp() > 0 && change.HPchange < 0)
+ change.xp = cmpLoot.GetXp() * -change.HPchange / this.GetMaxHitpoints();
+
+ return change;
+};
+
+/**
* @param {number} amount - The amount of hitpoints to substract. Kills the entity if required.
- * @return {{killed:boolean, change:number}} - Number of health points lost and whether the entity was killed.
+ * @return {{killed:boolean, HPchange:number}} - Number of health points lost and whether the entity was killed.
*/
Health.prototype.Reduce = function(amount)
{
// If we are dead, do not do anything
// (The entity will exist a little while after calling DestroyEntity so this
// might get called multiple times)
// Likewise if the amount is 0.
if (!amount || !this.hitpoints)
- return { "killed": false, "change": 0 };
+ return { "killed": false, "HPchange": 0 };
// Before changing the value, activate Fogging if necessary to hide changes
let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
if (cmpFogging)
cmpFogging.Activate();
let oldHitpoints = this.hitpoints;
// If we reached 0, then die.
if (amount >= this.hitpoints)
{
this.hitpoints = 0;
this.RegisterHealthChanged(oldHitpoints);
this.HandleDeath();
- return { "killed": true, "change": -oldHitpoints };
+ return { "killed": true, "HPchange": -oldHitpoints };
}
// If we are not marked as injured, do it now
if (!this.IsInjured())
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
cmpRangeManager.SetEntityFlag(this.entity, "injured", true);
}
this.hitpoints -= amount;
this.RegisterHealthChanged(oldHitpoints);
- return { "killed": false, "change": this.hitpoints - oldHitpoints };
+ return { "killed": false, "HPchange": this.hitpoints - oldHitpoints };
};
/**
* Handle what happens when the entity dies.
*/
Health.prototype.HandleDeath = function()
{
let cmpDeathDamage = Engine.QueryInterface(this.entity, IID_DeathDamage);
if (cmpDeathDamage)
cmpDeathDamage.CauseDeathDamage();
PlaySound("death", this.entity);
if (this.template.SpawnEntityOnDeath)
this.CreateDeathSpawnedEntity();
switch (this.template.DeathType)
{
case "corpse":
this.CreateCorpse();
break;
case "remain":
{
let resource = this.CreateCorpse(true);
if (resource != INVALID_ENTITY)
Engine.PostMessage(this.entity, MT_EntityRenamed, { "entity": this.entity, "newentity": resource });
break;
}
case "vanish":
break;
default:
error("Invalid template.DeathType: " + this.template.DeathType);
break;
}
Engine.DestroyEntity(this.entity);
};
Health.prototype.Increase = function(amount)
{
// Before changing the value, activate Fogging if necessary to hide changes
let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
if (cmpFogging)
cmpFogging.Activate();
if (!this.IsInjured())
return { "old": this.hitpoints, "new": this.hitpoints };
// If we're already dead, don't allow resurrection
if (this.hitpoints == 0)
return undefined;
let old = this.hitpoints;
this.hitpoints = Math.min(this.hitpoints + amount, this.GetMaxHitpoints());
if (!this.IsInjured())
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
cmpRangeManager.SetEntityFlag(this.entity, "injured", false);
}
this.RegisterHealthChanged(old);
return { "old": old, "new": this.hitpoints };
};
Health.prototype.CreateCorpse = function(leaveResources)
{
// If the unit died while not in the world, don't create any corpse for it
// since there's nowhere for the corpse to be placed
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition.IsInWorld())
return INVALID_ENTITY;
// Either creates a static local version of the current entity, or a
// persistent corpse retaining the ResourceSupply element of the parent.
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let templateName = cmpTemplateManager.GetCurrentTemplateName(this.entity);
let corpse;
if (leaveResources)
corpse = Engine.AddEntity("resource|" + templateName);
else
corpse = Engine.AddLocalEntity("corpse|" + templateName);
// Copy various parameters so it looks just like us
let cmpCorpsePosition = Engine.QueryInterface(corpse, IID_Position);
let pos = cmpPosition.GetPosition();
cmpCorpsePosition.JumpTo(pos.x, pos.z);
let rot = cmpPosition.GetRotation();
cmpCorpsePosition.SetYRotation(rot.y);
cmpCorpsePosition.SetXZRotation(rot.x, rot.z);
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
let cmpCorpseOwnership = Engine.QueryInterface(corpse, IID_Ownership);
cmpCorpseOwnership.SetOwner(cmpOwnership.GetOwner());
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
let cmpCorpseVisual = Engine.QueryInterface(corpse, IID_Visual);
cmpCorpseVisual.SetActorSeed(cmpVisual.GetActorSeed());
// Make it fall over
cmpCorpseVisual.SelectAnimation("death", true, 1.0);
return corpse;
};
Health.prototype.CreateDeathSpawnedEntity = function()
{
// If the unit died while not in the world, don't spawn a death entity for it
// since there's nowhere for it to be placed
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition.IsInWorld())
return INVALID_ENTITY;
// Create SpawnEntityOnDeath entity
let spawnedEntity = Engine.AddLocalEntity(this.template.SpawnEntityOnDeath);
// Move to same position
let cmpSpawnedPosition = Engine.QueryInterface(spawnedEntity, IID_Position);
let pos = cmpPosition.GetPosition();
cmpSpawnedPosition.JumpTo(pos.x, pos.z);
let rot = cmpPosition.GetRotation();
cmpSpawnedPosition.SetYRotation(rot.y);
cmpSpawnedPosition.SetXZRotation(rot.x, rot.z);
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
let cmpSpawnedOwnership = Engine.QueryInterface(spawnedEntity, IID_Ownership);
if (cmpOwnership && cmpSpawnedOwnership)
cmpSpawnedOwnership.SetOwner(cmpOwnership.GetOwner());
return spawnedEntity;
};
Health.prototype.UpdateActor = function()
{
if (!this.template.DamageVariants)
return;
let ratio = this.hitpoints / this.GetMaxHitpoints();
let newDamageVariant = "alive";
if (ratio > 0)
{
let minTreshold = 1;
for (let key in this.template.DamageVariants)
{
let treshold = +this.template.DamageVariants[key];
if (treshold < ratio || treshold > minTreshold)
continue;
newDamageVariant = key;
minTreshold = treshold;
}
}
else
newDamageVariant = "death";
if (this.damageVariant && this.damageVariant == newDamageVariant)
return;
this.damageVariant = newDamageVariant;
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SetVariant("health", newDamageVariant);
};
Health.prototype.OnValueModification = function(msg)
{
if (msg.component != "Health")
return;
let oldMaxHitpoints = this.GetMaxHitpoints();
let newMaxHitpoints = ApplyValueModificationsToEntity("Health/Max", +this.template.Max, this.entity);
if (oldMaxHitpoints != newMaxHitpoints)
{
let newHitpoints = this.hitpoints * newMaxHitpoints/oldMaxHitpoints;
this.maxHitpoints = newMaxHitpoints;
this.SetHitpoints(newHitpoints);
}
let oldRegenRate = this.regenRate;
this.regenRate = ApplyValueModificationsToEntity("Health/RegenRate", +this.template.RegenRate, this.entity);
let oldIdleRegenRate = this.idleRegenRate;
this.idleRegenRate = ApplyValueModificationsToEntity("Health/IdleRegenRate", +this.template.IdleRegenRate, this.entity);
if (this.regenRate != oldRegenRate || this.idleRegenRate != oldIdleRegenRate)
this.CheckRegenTimer();
};
Health.prototype.RegisterHealthChanged = function(from)
{
this.CheckRegenTimer();
this.UpdateActor();
Engine.PostMessage(this.entity, MT_HealthChanged, { "from": from, "to": this.hitpoints });
};
Engine.RegisterComponentType(IID_Health, "Health", Health);
Index: ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js (revision 22754)
@@ -1,69 +1,65 @@
function StatusEffectsReceiver() {}
StatusEffectsReceiver.prototype.Init = function()
{
this.activeStatusEffects = {};
};
-StatusEffectsReceiver.prototype.InflictEffects = function(statusEffects)
+// Called by attacking effects.
+StatusEffectsReceiver.prototype.GiveStatus = function(effectData, attacker, attackerOwner, bonusMultiplier)
{
- for (let effect in statusEffects)
- this.InflictEffect(effect, statusEffects[effect]);
+ for (let effect in effectData)
+ this.AddStatus(effect, effectData[effect]);
+
+ // TODO: implement loot / resistance.
+
+ return { "inflictedStatuses": Object.keys(effectData) };
};
-StatusEffectsReceiver.prototype.InflictEffect = function(statusName, data)
+StatusEffectsReceiver.prototype.AddStatus = function(statusName, data)
{
if (this.activeStatusEffects[statusName])
return;
this.activeStatusEffects[statusName] = {};
let status = this.activeStatusEffects[statusName];
status.duration = +data.Duration;
status.interval = +data.Interval;
status.damage = +data.Damage;
status.timeElapsed = 0;
status.firstTime = true;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
status.timer = cmpTimer.SetInterval(this.entity, IID_StatusEffectsReceiver, "ExecuteEffect", 0, +status.interval, statusName);
};
-StatusEffectsReceiver.prototype.RemoveEffect = function(statusName) {
+StatusEffectsReceiver.prototype.RemoveStatus = function(statusName) {
if (!this.activeStatusEffects[statusName])
return;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.activeStatusEffects[statusName].timer);
this.activeStatusEffects[statusName] = undefined;
};
StatusEffectsReceiver.prototype.ExecuteEffect = function(statusName, lateness)
{
let status = this.activeStatusEffects[statusName];
if (!status)
return;
if (status.firstTime)
{
status.firstTime = false;
status.timeElapsed += lateness;
}
else
status.timeElapsed += status.interval + lateness;
- let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
-
- cmpDamage.CauseDamage({
- "strengths": { [statusName]: status.damage },
- "target": this.entity,
- "attacker": -1,
- "multiplier": 1,
- "type": statusName,
- "attackerOwner": -1
- });
+ Attacking.HandleAttackEffects(statusName, { "Damage": { [statusName]: status.damage } }, this.entity, -1, -1);
if (status.timeElapsed >= status.duration)
- this.RemoveEffect(statusName);
+ this.RemoveStatus(statusName);
};
Engine.RegisterComponentType(IID_StatusEffectsReceiver, "StatusEffectsReceiver", StatusEffectsReceiver);
Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 22754)
@@ -1,6035 +1,6035 @@
function UnitAI() {}
UnitAI.prototype.Schema =
"Controls the unit's movement, attacks, etc, in response to commands from the player." +
"" +
"" +
"" +
"violent" +
"aggressive" +
"defensive" +
"passive" +
"standground" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"violent" +
"aggressive" +
"defensive" +
"passive" +
"skittish" +
"domestic" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
""+
"" +
"";
// Unit stances.
// There some targeting options:
// targetVisibleEnemies: anything in vision range is a viable target
// targetAttackersAlways: anything that hurts us is a viable target,
// possibly overriding user orders!
// There are some response options, triggered when targets are detected:
// respondFlee: run away
// respondChase: start chasing after the enemy
// respondChaseBeyondVision: start chasing, and don't stop even if it's out
// of this unit's vision range (though still visible to the player)
// respondStandGround: attack enemy but don't move at all
// respondHoldGround: attack enemy but don't move far from current position
// TODO: maybe add targetAggressiveEnemies (don't worry about lone scouts,
// do worry around armies slaughtering the guy standing next to you), etc.
var g_Stances = {
"violent": {
"targetVisibleEnemies": true,
"targetAttackersAlways": true,
"respondFlee": false,
"respondChase": true,
"respondChaseBeyondVision": true,
"respondStandGround": false,
"respondHoldGround": false,
"selectable": true
},
"aggressive": {
"targetVisibleEnemies": true,
"targetAttackersAlways": false,
"respondFlee": false,
"respondChase": true,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": false,
"selectable": true
},
"defensive": {
"targetVisibleEnemies": true,
"targetAttackersAlways": false,
"respondFlee": false,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": true,
"selectable": true
},
"passive": {
"targetVisibleEnemies": false,
"targetAttackersAlways": false,
"respondFlee": true,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": false,
"selectable": true
},
"standground": {
"targetVisibleEnemies": true,
"targetAttackersAlways": false,
"respondFlee": false,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": true,
"respondHoldGround": false,
"selectable": true
},
"none": {
// Only to be used by AI or trigger scripts
"targetVisibleEnemies": false,
"targetAttackersAlways": false,
"respondFlee": false,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": false,
"selectable": false
}
};
// See ../helpers/FSM.js for some documentation of this FSM specification syntax
UnitAI.prototype.UnitFsmSpec = {
// Default event handlers:
"MovementUpdate": function(msg) {
// ignore spurious movement messages
// (these can happen when stopping moving at the same time
// as switching states)
},
"ConstructionFinished": function(msg) {
// ignore uninteresting construction messages
},
"LosRangeUpdate": function(msg) {
// ignore newly-seen units by default
},
"LosHealRangeUpdate": function(msg) {
// ignore newly-seen injured units by default
},
"Attacked": function(msg) {
// ignore attacker
},
"HealthChanged": function(msg) {
// ignore
},
"PackFinished": function(msg) {
// ignore
},
"PickupCanceled": function(msg) {
// ignore
},
"TradingCanceled": function(msg) {
// ignore
},
"GuardedAttacked": function(msg) {
// ignore
},
// Formation handlers:
"FormationLeave": function(msg) {
// ignore when we're not in FORMATIONMEMBER
},
// Called when being told to walk as part of a formation
"Order.FormationWalk": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("FORMATIONMEMBER.WALKING");
},
// Special orders:
// (these will be overridden by various states)
"Order.LeaveFoundation": function(msg) {
// If foundation is not ally of entity, or if entity is unpacked siege,
// ignore the order
if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) && !Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() ||
this.IsPacking() || this.CanPack() || this.IsTurret())
{
this.FinishOrder();
return;
}
// Move a tile outside the building if necessary.
let range = 4;
if (this.CheckTargetRangeExplicit(msg.data.target, range, -1))
this.FinishOrder();
else
{
this.order.data.min = range;
this.SetNextState("INDIVIDUAL.WALKING");
}
},
// Individual orders:
// (these will switch the unit out of formation mode)
"Order.Stop": function(msg) {
// We have no control over non-domestic animals.
if (this.IsAnimal() && !this.IsDomestic())
{
this.FinishOrder();
return;
}
// Stop moving immediately.
this.StopMoving();
this.FinishOrder();
// No orders left, we're an individual now
if (this.IsAnimal())
this.SetNextState("ANIMAL.IDLE");
else
this.SetNextState("INDIVIDUAL.IDLE");
},
"Order.Walk": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetHeldPosition(this.order.data.x, this.order.data.z);
// It's not too bad if we don't arrive at exactly the right position.
this.order.data.relaxed = true;
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING");
else
this.SetNextState("INDIVIDUAL.WALKING");
},
"Order.WalkAndFight": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetHeldPosition(this.order.data.x, this.order.data.z);
// It's not too bad if we don't arrive at exactly the right position.
this.order.data.relaxed = true;
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING"); // WalkAndFight not applicable for animals
else
this.SetNextState("INDIVIDUAL.WALKINGANDFIGHTING");
},
"Order.WalkToTarget": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
if (this.CheckRange(this.order.data))
{
// We are already at the target, or can't move at all
this.FinishOrder();
return true;
}
// It's not too bad if we don't arrive at exactly the right position.
this.order.data.relaxed = true;
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING");
else
this.SetNextState("INDIVIDUAL.WALKING");
},
"Order.PickupUnit": function(msg) {
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
{
this.FinishOrder();
return;
}
if (this.CheckRange(this.order.data))
{
this.FinishOrder();
return;
}
// Check if we need to move
// TODO implement a better way to know if we are on the shoreline
let needToMove = true;
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (this.lastShorelinePosition && cmpPosition && (this.lastShorelinePosition.x == cmpPosition.GetPosition().x) &&
(this.lastShorelinePosition.z == cmpPosition.GetPosition().z))
// we were already on the shoreline, and have not moved since
if (DistanceBetweenEntities(this.entity, this.order.data.target) < 50)
needToMove = false;
if (needToMove)
this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING");
else
this.SetNextState("INDIVIDUAL.PICKUP.LOADING");
},
"Order.Guard": function(msg) {
if (!this.AddGuard(this.order.data.target))
{
this.FinishOrder();
return;
}
if (!this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("INDIVIDUAL.GUARD.ESCORTING");
else
this.SetNextState("INDIVIDUAL.GUARD.GUARDING");
},
"Order.Flee": function(msg) {
if (this.IsAnimal())
this.SetNextState("ANIMAL.FLEEING");
else
this.SetNextState("INDIVIDUAL.FLEEING");
},
"Order.Attack": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.order.data.target))
{
this.FinishOrder();
return;
}
// Work out how to attack the given target
var type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
if (!type)
{
// Oops, we can't attack at all
this.FinishOrder();
return;
}
this.order.data.attackType = type;
this.RememberTargetPosition();
if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather")
this.RememberTargetPosition(this.orderQueue[1].data);
// If we are already at the target, try attacking it from here
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
// For packable units within attack range:
// 1. If unpacked, we can attack the target.
// 2. If packed, we first need to unpack, then follow case 1.
if (this.CanUnpack())
{
this.PushOrderFront("Unpack", { "force": true });
return;
}
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.ATTACKING");
else
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
return;
}
// If we can't reach the target, but are standing ground, then abandon this attack order.
// Unless we're hunting, that's a special case where we should continue attacking our target.
if (this.GetStance().respondStandGround && !this.order.data.force && !this.order.data.hunting || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units out of attack range:
// 1. If packed, we need to move to attack range and then unpack.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.APPROACHING");
else
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
},
"Order.Patrol": function(msg) {
if (this.IsAnimal() || this.IsTurret())
{
this.FinishOrder();
return;
}
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
// It's not too bad if we don't arrive at exactly the right position.
this.order.data.relaxed = true;
this.SetNextState("INDIVIDUAL.PATROL");
},
"Order.Heal": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.order.data.target))
{
this.FinishOrder();
return;
}
// Healers can't heal themselves.
if (this.order.data.target == this.entity)
{
this.FinishOrder();
return;
}
// Check if the target is in range
if (this.CheckTargetRange(this.order.data.target, IID_Heal))
{
this.SetNextState("INDIVIDUAL.HEAL.HEALING");
return;
}
// If we can't reach the target, but are standing ground,
// then abandon this heal order
if (this.GetStance().respondStandGround && !this.order.data.force)
{
this.FinishOrder();
return;
}
this.SetNextState("INDIVIDUAL.HEAL.APPROACHING");
},
"Order.Gather": function(msg) {
// If the target is still alive, we need to kill it first
if (this.MustKillGatherTarget(this.order.data.target))
{
// Make sure we can attack the target, else we'll get very stuck
if (!this.GetBestAttackAgainst(this.order.data.target, false))
{
// Oops, we can't attack at all - give up
// TODO: should do something so the player knows why this failed
this.FinishOrder();
return;
}
// The target was visible when this order was issued,
// but could now be invisible again.
if (!this.CheckTargetVisible(this.order.data.target))
{
if (this.order.data.secondTry === undefined)
{
this.order.data.secondTry = true;
this.PushOrderFront("Walk", this.order.data.lastPos);
}
// We couldn't move there, or the target moved away
else
{
let data = this.order.data;
if (!this.FinishOrder())
this.PushOrderFront("GatherNearPosition", {
"x": data.lastPos.x,
"z": data.lastPos.z,
"type": data.type,
"template": data.template
});
}
return;
}
this.PushOrderFront("Attack", { "target": this.order.data.target, "force": !!this.order.data.force, "hunting": true, "allowCapture": false });
return;
}
this.RememberTargetPosition();
if (!this.order.data.initPos)
this.order.data.initPos = this.order.data.lastPos;
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
this.SetNextState("INDIVIDUAL.GATHER.GATHERING");
else
this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
},
"Order.GatherNearPosition": function(msg) {
this.SetNextState("INDIVIDUAL.GATHER.WALKING");
this.order.data.initPos = { 'x': this.order.data.x, 'z': this.order.data.z };
this.order.data.relaxed = true;
},
"Order.ReturnResource": function(msg) {
// Check if the dropsite is already in range
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
{
var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
Engine.QueryInterface(this.entity, IID_ResourceGatherer).CommitResources(dropsiteTypes);
// Stop showing the carried resource animation.
this.SetDefaultAnimationVariant();
// Our next order should always be a Gather,
// so just switch back to that order
this.FinishOrder();
return;
}
}
this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING");
},
"Order.Trade": function(msg) {
// We must check if this trader has both markets in case it was a back-to-work order
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader || !cmpTrader.HasBothMarkets())
{
this.FinishOrder();
return;
}
// TODO find the nearest way-point from our position, and start with it
this.waypoints = undefined;
this.SetNextState("TRADE.APPROACHINGMARKET");
},
"Order.Repair": function(msg) {
// Try to move within range
if (this.CheckTargetRange(this.order.data.target, IID_Builder))
this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING");
else
this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING");
},
"Order.Garrison": function(msg) {
if (this.IsTurret())
{
this.SetNextState("IDLE");
return;
}
else if (this.IsGarrisoned())
{
this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED");
return;
}
// For packable units:
// 1. If packed, we can move to the garrison target.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING");
},
"Order.Ungarrison": function() {
this.FinishOrder();
this.isGarrisoned = false;
},
"Order.Cheering": function(msg) {
this.SetNextState("INDIVIDUAL.CHEERING");
},
"Order.Pack": function(msg) {
if (this.CanPack())
this.SetNextState("INDIVIDUAL.PACKING");
},
"Order.Unpack": function(msg) {
if (this.CanUnpack())
this.SetNextState("INDIVIDUAL.UNPACKING");
},
"Order.CancelPack": function(msg) {
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked())
cmpPack.CancelPack();
this.FinishOrder();
},
"Order.CancelUnpack": function(msg) {
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked())
cmpPack.CancelPack();
this.FinishOrder();
},
// States for the special entity representing a group of units moving in formation:
"FORMATIONCONTROLLER": {
"Order.Walk": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("WALKING");
},
"Order.WalkAndFight": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("WALKINGANDFIGHTING");
},
"Order.MoveIntoFormation": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("FORMING");
},
// Only used by other orders to walk there in formation
"Order.WalkToTargetRange": function(msg) {
if (!this.CheckRange(this.order.data))
this.SetNextState("WALKING");
else
this.FinishOrder();
},
"Order.WalkToTarget": function(msg) {
if (!this.CheckRange(this.order.data))
this.SetNextState("WALKING");
else
this.FinishOrder();
},
"Order.WalkToPointRange": function(msg) {
if (!this.CheckRange(this.order.data))
this.SetNextState("WALKING");
else
this.FinishOrder();
},
"Order.Patrol": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("PATROL");
},
"Order.Guard": function(msg) {
this.CallMemberFunction("Guard", [msg.data.target, false]);
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.Disband();
},
"Order.Stop": function(msg) {
if (!this.IsAttackingAsFormation())
this.CallMemberFunction("Stop", [false]);
this.StopMoving();
this.FinishOrder();
},
"Order.Attack": function(msg) {
var target = msg.data.target;
var allowCapture = msg.data.allowCapture;
var cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
target = cmpTargetUnitAI.GetFormationController();
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetAttackRange(target, target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
this.SetNextState("COMBAT.APPROACHING");
return;
}
this.FinishOrder();
return;
}
this.CallMemberFunction("Attack", [target, allowCapture, false]);
if (cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
else
this.SetNextState("MEMBER");
},
"Order.Garrison": function(msg) {
if (!Engine.QueryInterface(msg.data.target, IID_GarrisonHolder))
{
this.FinishOrder();
return;
}
// Check if we are already in range, otherwise walk there
if (!this.CheckGarrisonRange(msg.data.target))
{
if (!this.CheckTargetVisible(msg.data.target))
{
this.FinishOrder();
return;
}
else
{
this.SetNextState("GARRISON.APPROACHING");
return;
}
}
this.SetNextState("GARRISON.GARRISONING");
},
"Order.Gather": function(msg) {
if (this.MustKillGatherTarget(msg.data.target))
{
// The target was visible when this order was given,
// but could now be invisible.
if (!this.CheckTargetVisible(msg.data.target))
{
if (msg.data.secondTry === undefined)
{
msg.data.secondTry = true;
this.PushOrderFront("Walk", msg.data.lastPos);
}
// We couldn't move there, or the target moved away
else
{
let data = msg.data;
if (!this.FinishOrder())
this.PushOrderFront("GatherNearPosition", {
"x": data.lastPos.x,
"z": data.lastPos.z,
"type": data.type,
"template": data.template
});
}
return;
}
this.PushOrderFront("Attack", { "target": msg.data.target, "force": !!msg.data.force, "hunting": true, "allowCapture": false, "min": 0, "max": 10 });
return;
}
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The target isn't gatherable or not visible any more.
this.FinishOrder();
// TODO: Should we issue a gather-near-position order
// if the target isn't gatherable/doesn't exist anymore?
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("Gather", [msg.data.target, false]);
this.SetNextState("MEMBER");
},
"Order.GatherNearPosition": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20))
{
// Out of range; move there in formation
this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 });
return;
}
this.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]);
this.SetNextState("MEMBER");
},
"Order.Heal": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The target was destroyed
this.FinishOrder();
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("Heal", [msg.data.target, false]);
this.SetNextState("MEMBER");
},
"Order.Repair": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The building was finished or destroyed
this.FinishOrder();
else
// Out of range move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]);
this.SetNextState("MEMBER");
},
"Order.ReturnResource": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The target was destroyed
this.FinishOrder();
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("ReturnResource", [msg.data.target, false]);
this.SetNextState("MEMBER");
},
"Order.Pack": function(msg) {
this.CallMemberFunction("Pack", [false]);
this.SetNextState("MEMBER");
},
"Order.Unpack": function(msg) {
this.CallMemberFunction("Unpack", [false]);
this.SetNextState("MEMBER");
},
"IDLE": {
"enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(false);
},
},
"WALKING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || this.CheckRange(this.order.data))
{
this.FinishOrder();
this.CallMemberFunction("ResetFinishOrder", []);
}
},
},
"WALKINGANDFIGHTING": {
"enter": function(msg) {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
this.StartTimer(0, 1000);
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"leave": function() {
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
// check if there are no enemies to attack
this.FindWalkAndFightTargets();
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || this.CheckRange(this.order.data))
{
this.FinishOrder();
this.CallMemberFunction("ResetFinishOrder", []);
}
},
},
"PATROL": {
"enter": function(msg) {
// Memorize the origin position in case that we want to go back
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
{
this.FinishOrder();
return;
}
if (!this.patrolStartPosOrder)
{
this.patrolStartPosOrder = cmpPosition.GetPosition();
this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses;
this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture;
}
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
this.StartTimer(0, 1000);
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"Timer": function(msg) {
// Check if there are no enemies to attack
this.FindWalkAndFightTargets();
},
"leave": function(msg) {
this.StopTimer();
this.StopMoving();
delete this.patrolStartPosOrder;
},
"MovementUpdate": function(msg) {
if (!msg.likelyFailure && !this.CheckRange(this.order.data))
return;
/**
* A-B-A-B-..:
* if the user only commands one patrol order, the patrol will be between
* the last position and the defined waypoint
* A-B-C-..-A-B-..:
* otherwise, the patrol is only between the given patrol commands and the
* last position is not included (last position = the position where the unit
* is located at the time of the first patrol order)
*/
if (this.orderQueue.length == 1)
this.PushOrder("Patrol", this.patrolStartPosOrder);
this.PushOrder(this.order.type, this.order.data);
this.FinishOrder();
},
},
"GARRISON":{
"enter": function() {
// If the garrisonholder should pickup, warn it so it can take needed action
var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target; // temporary, deleted in "leave"
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
}
},
"leave": function() {
// If a pickup has been requested and not yet canceled, cancel it
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
},
"APPROACHING": {
"enter": function() {
if (!this.MoveToGarrisonRange(this.order.data.target))
{
this.FinishOrder();
return true;
}
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || msg.likelySuccess)
this.SetNextState("GARRISONING");
},
},
"GARRISONING": {
"enter": function() {
// If a pickup has been requested, cancel it as it will be requested by members
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
this.CallMemberFunction("Garrison", [this.order.data.target, false]);
this.SetNextState("MEMBER");
},
},
},
"FORMING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (!msg.likelyFailure && !this.CheckRange(this.order.data))
return;
if (this.FinishOrder())
{
this.CallMemberFunction("ResetFinishOrder", []);
return;
}
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.FindInPosition();
}
},
"COMBAT": {
"APPROACHING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
this.CallMemberFunction("Attack", [this.order.data.target, this.order.data.allowCapture, false]);
if (cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
else
this.SetNextState("MEMBER");
},
},
"ATTACKING": {
// Wait for individual members to finish
"enter": function(msg) {
var target = this.order.data.target;
var allowCapture = this.order.data.allowCapture;
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetAttackRange(target, target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
this.FinishOrder();
this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
return true;
}
this.FinishOrder();
return true;
}
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// TODO fix the rearranging while attacking as formation
cmpFormation.SetRearrange(!this.IsAttackingAsFormation());
cmpFormation.MoveMembersIntoFormation(false, false);
this.StartTimer(200, 200);
return false;
},
"Timer": function(msg) {
var target = this.order.data.target;
var allowCapture = this.order.data.allowCapture;
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetAttackRange(target, target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
this.FinishOrder();
this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
return;
}
this.FinishOrder();
return;
}
},
"leave": function(msg) {
this.StopTimer();
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (cmpFormation)
cmpFormation.SetRearrange(true);
},
},
},
"MEMBER": {
// Wait for individual members to finish
"enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(false);
this.StopMoving();
this.StartTimer(1000, 1000);
},
"Timer": function(msg) {
// Have all members finished the task?
if (!this.TestAllMemberFunction("HasFinishedOrder", []))
return;
this.CallMemberFunction("ResetFinishOrder", []);
// Execute the next order
if (this.FinishOrder())
{
// if WalkAndFight order, look for new target before moving again
if (this.IsWalkingAndFighting())
this.FindWalkAndFightTargets();
return;
}
},
"leave": function(msg) {
this.StopTimer();
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.MoveToMembersCenter();
},
},
},
// States for entities moving as part of a formation:
"FORMATIONMEMBER": {
"FormationLeave": function(msg) {
// We're not in a formation anymore, so no need to track this.
this.finishedOrder = false;
// Stop moving as soon as the formation disbands
this.StopMoving();
// If the controller handled an order but some members rejected it,
// they will have no orders and be in the FORMATIONMEMBER.IDLE state.
if (this.orderQueue.length)
{
// We're leaving the formation, so stop our FormationWalk order
if (this.FinishOrder())
return;
}
// No orders left, we're an individual now
this.SetNextState("INDIVIDUAL.IDLE");
},
// Override the LeaveFoundation order since we're not doing
// anything more important (and we might be stuck in the WALKING
// state forever and need to get out of foundations in that case)
"Order.LeaveFoundation": function(msg) {
// If foundation is not ally of entity, or if entity is unpacked siege,
// ignore the order
if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) && !Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() ||
this.IsPacking() || this.CanPack() || this.IsTurret())
{
this.FinishOrder();
return;
}
// Move a tile outside the building
let range = 4;
if (this.CheckTargetRangeExplicit(msg.data.target, range, -1))
{
// We are already at the target, or can't move at all
this.FinishOrder();
}
else
{
this.order.data.min = range;
this.SetNextState("WALKINGTOPOINT");
}
},
"enter": function() {
if (this.IsAnimal())
{
// Animals can't go in formation.
warn("Entity " + this.entity + " was put in FORMATIONMEMBER state but is an animal");
this.FinishOrder();
this.SetNextState("ANIMAL.IDLE");
return true;
}
let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
this.SetAnimationVariant(cmpFormation.GetFormationAnimation(this.entity));
},
"leave": function() {
this.SetDefaultAnimationVariant();
},
"IDLE": "INDIVIDUAL.IDLE",
"WALKING": {
"enter": function() {
this.formationOffset = { "x": this.order.data.x, "z": this.order.data.z };
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.MoveToFormationOffset(this.order.data.target, this.order.data.x, this.order.data.z);
},
"leave": function() {
this.StopMoving();
},
// Occurs when the unit has reached its destination and the controller
// is done moving. The controller is notified.
"MovementUpdate": function(msg) {
// We can only finish this order if the move was really completed.
let cmpPosition = Engine.QueryInterface(this.formationController, IID_Position);
let atDestination = cmpPosition && cmpPosition.IsInWorld();
if (!atDestination && cmpPosition)
{
let pos = cmpPosition.GetPosition2D();
atDestination = this.CheckPointRangeExplicit(pos.X + this.order.data.x, pos.Y + this.order.data.z, 0, 1);
}
if (!atDestination && !msg.likelyFailure)
return;
if (this.FinishOrder())
return;
let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
cmpFormation.SetInPosition(this.entity);
},
},
// Special case used by Order.LeaveFoundation
"WALKINGTOPOINT": {
"enter": function() {
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
cmpFormation.UnsetInPosition(this.entity);
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
},
"MovementUpdate": function() {
if (!this.CheckRange(this.order.data))
return;
this.StopMoving();
this.FinishOrder();
},
},
},
// States for entities not part of a formation:
"INDIVIDUAL": {
"enter": function() {
// Sanity-checking
if (this.IsAnimal())
error("Animal got moved into INDIVIDUAL.* state");
},
"Attacked": function(msg) {
// Respond to attack if we always target attackers or during unforced orders
if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force)
this.RespondToTargetedEntities([msg.data.attacker]);
},
"GuardedAttacked": function(msg) {
// do nothing if we have a forced order in queue before the guard order
for (var i = 0; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].type == "Guard")
break;
if (this.orderQueue[i].data && this.orderQueue[i].data.force)
return;
}
// if we already are targeting another unit still alive, finish with it first
if (this.order && (this.order.type == "WalkAndFight" || this.order.type == "Attack"))
if (this.order.data.target != msg.data.attacker && this.TargetIsAlive(msg.data.attacker))
return;
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
if (cmpIdentity && cmpIdentity.HasClass("Support") &&
cmpHealth && cmpHealth.IsInjured())
{
if (this.CanHeal(this.isGuardOf))
this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
else if (this.CanRepair(this.isGuardOf))
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
return;
}
// if the attacker is a building and we can repair the guarded, repair it rather than attacking
var cmpBuildingAI = Engine.QueryInterface(msg.data.attacker, IID_BuildingAI);
if (cmpBuildingAI && this.CanRepair(this.isGuardOf))
{
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
return;
}
// target the unit
if (this.CheckTargetVisible(msg.data.attacker))
this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "allowCapture": true });
else
{
var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.PushOrderFront("WalkAndFight", { "x": pos.x, "z": pos.z, "target": msg.data.attacker, "force": false });
// if we already had a WalkAndFight, keep only the most recent one in case the target has moved
if (this.orderQueue[1] && this.orderQueue[1].type == "WalkAndFight")
{
this.orderQueue.splice(1, 1);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
}
}
},
"IDLE": {
"enter": function() {
// Switch back to idle animation to guarantee we won't
// get stuck with an incorrect animation
this.SelectAnimation("idle");
// Idle is the default state. If units try, from the IDLE.enter sub-state, to
// begin another order, and that order fails (calling FinishOrder), they might
// end up in an infinite loop. To avoid this, all methods that could put the unit in
// a new state are done on the next turn.
// This wastes a turn but avoids infinite loops.
// Further, the GUI and AI want to know when a unit is idle,
// but sending this info in Idle.enter will send spurious messages.
// Pick 100 to execute on the next turn in SP and MP.
this.StartTimer(100);
},
"leave": function() {
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
cmpRangeManager.DisableActiveQuery(this.losRangeQuery);
if (this.losHealRangeQuery)
cmpRangeManager.DisableActiveQuery(this.losHealRangeQuery);
this.StopTimer();
if (this.isIdle)
{
this.isIdle = false;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
"LosRangeUpdate": function(msg) {
if (this.GetStance().targetVisibleEnemies)
{
// Start attacking one of the newly-seen enemy (if any)
this.AttackEntitiesByPreference(msg.data.added);
}
},
"LosHealRangeUpdate": function(msg) {
this.RespondToHealableEntities(msg.data.added);
},
"Timer": function(msg) {
// If the unit is guarding/escorting, go back to its duty
if (this.isGuardOf)
{
this.Guard(this.isGuardOf, false);
return;
}
// If a unit can heal and attack we first want to heal wounded units,
// so check if we are a healer and find whether there's anybody nearby to heal.
// (If anyone approaches later it'll be handled via LosHealRangeUpdate.)
// If anyone in sight gets hurt that will be handled via LosHealRangeUpdate.
if (this.IsHealer() && this.FindNewHealTargets())
return; // (abort the FSM transition since we may have already switched state)
// If we entered the idle state we must have nothing better to do,
// so immediately check whether there's anybody nearby to attack.
// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
if (this.FindNewTargets())
return; // (abort the FSM transition since we may have already switched state)
if (this.formationOffset && this.formationController)
{
this.PushOrder("FormationWalk", {
"target": this.formationController,
"x": this.formationOffset.x,
"z": this.formationOffset.z,
});
return;
}
if (!this.isIdle)
{
this.isIdle = true;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
},
"WALKING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
// If it looks like the path is failing, and we are close enough (3 tiles)
// stop anyways. This avoids pathing for an unreachable goal and reduces lag considerably.
if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) ||
this.CheckRange(this.order.data))
this.FinishOrder();
},
},
"WALKINGANDFIGHTING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
// Show weapons rather than carried resources.
this.SetAnimationVariant("combat");
this.StartTimer(0, 1000);
},
"Timer": function(msg) {
this.FindWalkAndFightTargets();
},
"leave": function(msg) {
this.StopMoving();
this.StopTimer();
this.SetDefaultAnimationVariant();
},
"MovementUpdate": function(msg) {
// If it looks like the path is failing, and we are close enough (3 tiles)
// stop anyways. This avoids pathing for an unreachable goal and reduces lag considerably.
if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) ||
this.CheckRange(this.order.data))
this.FinishOrder();
},
},
"PATROL": {
"enter": function() {
// Memorize the origin position in case that we want to go back
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld() ||
!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
if (!this.patrolStartPosOrder)
{
this.patrolStartPosOrder = cmpPosition.GetPosition();
this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses;
this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture;
}
this.StartTimer(0, 1000);
this.SetAnimationVariant("combat");
},
"leave": function() {
this.StopMoving();
this.StopTimer();
delete this.patrolStartPosOrder;
this.SetDefaultAnimationVariant();
},
"Timer": function(msg) {
this.FindWalkAndFightTargets();
},
"MovementUpdate": function(msg) {
if (!msg.likelyFailure && !msg.likelySuccess && !this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange))
return;
if (this.orderQueue.length == 1)
this.PushOrder("Patrol", this.patrolStartPosOrder);
this.PushOrder(this.order.type, this.order.data);
this.FinishOrder();
},
},
"GUARD": {
"RemoveGuard": function() {
this.StopMoving();
this.FinishOrder();
},
"ESCORTING": {
"enter": function() {
if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
{
this.FinishOrder();
return true;
}
// Show weapons rather than carried resources.
this.SetAnimationVariant("combat");
this.StartTimer(0, 1000);
this.SetHeldPositionOnEntity(this.isGuardOf);
return false;
},
"Timer": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.isGuardOf))
{
this.FinishOrder();
return;
}
// Adapt the speed to the one of the target if needed
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
if (cmpObstructionManager.IsInTargetRange(this.entity, this.isGuardOf, 0, 3 * this.guardRange, false))
{
let cmpUnitAI = Engine.QueryInterface(this.isGuardOf, IID_UnitAI);
if (cmpUnitAI)
{
let speed = cmpUnitAI.GetWalkSpeed();
if (speed < this.GetWalkSpeed())
this.SetSpeedMultiplier(speed / this.GetWalkSpeed());
}
}
this.SetHeldPositionOnEntity(this.isGuardOf);
},
"leave": function(msg) {
this.StopMoving();
this.ResetSpeedMultiplier();
this.StopTimer();
this.SetDefaultAnimationVariant();
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("GUARDING");
},
},
"GUARDING": {
"enter": function() {
this.StopMoving();
this.StartTimer(1000, 1000);
this.SetHeldPositionOnEntity(this.entity);
this.SetAnimationVariant("combat");
this.FaceTowardsTarget(this.order.data.target);
return false;
},
"LosRangeUpdate": function(msg) {
// Start attacking one of the newly-seen enemy (if any)
if (this.GetStance().targetVisibleEnemies)
this.AttackEntitiesByPreference(msg.data.added);
},
"Timer": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.isGuardOf))
{
this.FinishOrder();
return;
}
// Then check is the target has moved and try following it.
// TODO: find out what to do if we cannot move.
if (!this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange) &&
this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("ESCORTING");
else
{
this.FaceTowardsTarget(this.order.data.target);
// if nothing better to do, check if the guarded needs to be healed or repaired
var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
if (cmpHealth && cmpHealth.IsInjured())
{
if (this.CanHeal(this.isGuardOf))
this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
else if (this.CanRepair(this.isGuardOf))
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
}
}
},
"leave": function(msg) {
this.StopTimer();
this.SetDefaultAnimationVariant();
},
},
},
"FLEEING": {
"enter": function() {
// We use the distance between the entities to account for ranged attacks
this.order.data.distanceToFlee = DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance);
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
// Use unit motion directly to ignore the visibility check. TODO: change this if we add LOS to fauna.
if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1) ||
!cmpUnitMotion || !cmpUnitMotion.MoveToTargetRange(this.order.data.target, this.order.data.distanceToFlee, -1))
{
this.FinishOrder();
return true;
}
this.PlaySound("panic");
// Run quickly
this.SetSpeedMultiplier(this.GetRunMultiplier());
},
"HealthChanged": function() {
this.SetSpeedMultiplier(this.GetRunMultiplier());
},
"leave": function() {
this.ResetSpeedMultiplier();
this.StopMoving();
},
"MovementUpdate": function(msg) {
// When we've run far enough, stop fleeing
if (msg.likelyFailure || this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1))
this.FinishOrder();
},
// TODO: what if we run into more enemies while fleeing?
},
"COMBAT": {
"Order.LeaveFoundation": function(msg) {
// Ignore the order as we're busy.
return { "discardOrder": true };
},
"Attacked": function(msg) {
// If we're already in combat mode, ignore anyone else who's attacking us
// unless it's a melee attack since they may be blocking our way to the target
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || !this.order.data.force))
this.RespondToTargetedEntities([msg.data.attacker]);
},
"APPROACHING": {
"enter": function() {
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
this.FinishOrder();
return true;
}
// Show weapons rather than carried resources.
this.SetAnimationVariant("combat");
this.StartTimer(1000, 1000);
},
"leave": function() {
// Show carried resources when walking.
this.SetDefaultAnimationVariant();
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
else
{
this.RememberTargetPosition();
if (this.order.data.hunting && this.orderQueue.length > 1 &&
this.orderQueue[1].type === "Gather")
this.RememberTargetPosition(this.orderQueue[1].data);
}
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure)
{
// This also handles hunting.
if (this.orderQueue.length > 1)
{
this.FinishOrder();
return;
}
else if (!this.order.data.force)
{
this.SetNextState("COMBAT.FINDINGNEWTARGET");
return;
}
// Go to the last known position and try to find enemies there.
let lastPos = this.order.data.lastPos;
this.PushOrder("WalkAndFight", { "x": lastPos.x, "z": lastPos.z, "force": false });
return;
}
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
// If the unit needs to unpack, do so
if (this.CanUnpack())
{
this.PushOrderFront("Unpack", { "force": true });
return;
}
this.SetNextState("ATTACKING");
}
else if (msg.likelySuccess)
// Try moving again,
// attack range uses a height-related formula and our actual max range might have changed.
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
this.FinishOrder();
},
},
"ATTACKING": {
"enter": function() {
let target = this.order.data.target;
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
// if the target is a formation, save the attacking formation, and pick a member
if (cmpFormation)
{
this.order.data.formationTarget = target;
target = cmpFormation.GetClosestMember(this.entity);
this.order.data.target = target;
}
if (!this.CanAttack(target))
{
this.SetNextState("COMBAT.FINDINGNEWTARGET");
return true;
}
if (!this.CheckTargetAttackRange(target, this.order.data.attackType))
{
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return true;
}
this.SetNextState("COMBAT.APPROACHING");
return true;
}
this.StopMoving();
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
this.attackTimers = cmpAttack.GetTimers(this.order.data.attackType);
// If the repeat time since the last attack hasn't elapsed,
// delay this attack to avoid attacking too fast.
let prepare = this.attackTimers.prepare;
if (this.lastAttacked)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime();
prepare = Math.max(prepare, repeatLeft);
}
if (!this.IsFormationMember())
this.SetAnimationVariant("combat");
this.oldAttackType = this.order.data.attackType;
// add prefix + no capital first letter for attackType
this.SelectAnimation("attack_" + this.order.data.attackType.toLowerCase());
this.SetAnimationSync(prepare, this.attackTimers.repeat);
this.StartTimer(prepare, this.attackTimers.repeat);
// TODO: we should probably only bother syncing projectile attacks, not melee
// If using a non-default prepare time, re-sync the animation when the timer runs.
this.resyncAnimation = prepare != this.attackTimers.prepare;
this.FaceTowardsTarget(this.order.data.target);
let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
if (cmpBuildingAI)
cmpBuildingAI.SetUnitAITarget(this.order.data.target);
},
"leave": function() {
let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
if (cmpBuildingAI)
cmpBuildingAI.SetUnitAITarget(0);
this.StopTimer();
this.SetDefaultAnimationVariant();
this.ResetAnimation();
},
"Timer": function(msg) {
let target = this.order.data.target;
// Check the target is still alive and attackable
if (!this.CanAttack(target))
{
this.SetNextState("COMBAT.FINDINGNEWTARGET");
return;
}
this.RememberTargetPosition();
if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather")
this.RememberTargetPosition(this.orderQueue[1].data);
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastAttacked = cmpTimer.GetTime() - msg.lateness;
this.FaceTowardsTarget(target);
// BuildingAI has it's own attack-routine
let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
if (!cmpBuildingAI)
{
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
cmpAttack.PerformAttack(this.order.data.attackType, target);
}
// Check we can still reach the target for the next attack
if (this.CheckTargetAttackRange(target, this.order.data.attackType))
{
if (this.resyncAnimation)
{
this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat);
this.resyncAnimation = false;
}
return;
}
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("COMBAT.CHASING");
return;
}
this.SetNextState("FINDINGNEWTARGET");
},
// TODO: respond to target deaths immediately, rather than waiting
// until the next Timer event
"Attacked": function(msg) {
// If we are capturing and are attacked by something that we would not capture, attack that entity instead
if (this.order.data.attackType == "Capture" && (this.GetStance().targetAttackersAlways || !this.order.data.force)
&& this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, true) != "Capture")
this.RespondToTargetedEntities([msg.data.attacker]);
},
},
"FINDINGNEWTARGET": {
"enter": function() {
// Try to find the formation the target was a part of.
let cmpFormation = Engine.QueryInterface(this.order.data.target, IID_Formation);
if (!cmpFormation)
cmpFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation);
// If the target is a formation, pick closest member.
if (cmpFormation)
{
let filter = (t) => this.CanAttack(t);
this.order.data.formationTarget = this.order.data.target;
let target = cmpFormation.GetClosestMember(this.entity, filter);
this.order.data.target = target;
this.SetNextState("COMBAT.ATTACKING");
return true;
}
// Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up
// except if in WalkAndFight mode where we look for more enemies around before moving again.
if (this.FinishOrder())
{
if (this.IsWalkingAndFighting())
this.FindWalkAndFightTargets();
return true;
}
// See if we can switch to a new nearby enemy
if (this.FindNewTargets())
return true;
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
return true;
},
},
"CHASING": {
"enter": function() {
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
this.FinishOrder();
return true;
}
// Show weapons rather than carried resources.
this.SetAnimationVariant("combat");
var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.IsFleeing())
{
// Run after a fleeing target
this.SetSpeedMultiplier(this.GetRunMultiplier());
}
this.StartTimer(1000, 1000);
},
"leave": function() {
this.ResetSpeedMultiplier();
// Show carried resources when walking.
this.SetDefaultAnimationVariant();
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MovementUpdate": function(msg) {
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
// If the unit needs to unpack, do so
if (this.CanUnpack())
{
this.PushOrderFront("Unpack", { "force": true });
return;
}
this.SetNextState("ATTACKING");
}
else if (msg.likelySuccess)
// Try moving again,
// attack range uses a height-related formula and our actual max range might have changed.
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
this.FinishOrder();
},
},
},
"GATHER": {
"APPROACHING": {
"enter": function() {
this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave".
// check that we can gather from the resource we're supposed to gather from.
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
var cmpMirage = Engine.QueryInterface(this.gatheringTarget, IID_Mirage);
if ((!cmpMirage || !cmpMirage.Mirages(IID_ResourceSupply)) &&
(!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity)) ||
!this.MoveTo(this.order.data, IID_ResourceGatherer))
{
// The GATHERING timer will handle finding a valid resource.
this.SetNextState("GATHERING");
return true;
}
return false;
},
"MovementUpdate": function(msg) {
// The GATHERING timer will handle finding a valid resource.
if (msg.likelyFailure || this.CheckRange(this.order.data, IID_ResourceGatherer))
this.SetNextState("GATHERING");
},
"leave": function() {
this.StopMoving();
this.SetDefaultAnimationVariant();
if (!this.gatheringTarget)
return;
// don't use ownership because this is called after a conversion/resignation
// and the ownership would be invalid then.
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply)
cmpSupply.RemoveGatherer(this.entity);
delete this.gatheringTarget;
},
},
// Walking to a good place to gather resources near, used by GatherNearPosition
"WALKING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
// If we failed, the GATHERING timer will handle finding a valid resource.
if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) ||
this.CheckRange(this.order.data))
this.SetNextState("GATHERING");
},
},
"GATHERING": {
"enter": function() {
this.gatheringTarget = this.order.data.target || INVALID_ENTITY; // deleted in "leave".
// Check if the resource is full.
// Will only be added if we're not already in.
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
let cmpSupply;
if (cmpOwnership)
cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity))
{
this.StartTimer(0);
return false;
}
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
this.order.data.force = false;
this.order.data.autoharvest = true;
// Calculate timing based on gather rates
// This allows the gather rate to control how often we gather, instead of how much.
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
var rate = cmpResourceGatherer.GetTargetGatherRate(this.gatheringTarget);
if (!rate)
{
// Try to find another target if the current one stopped existing
if (!Engine.QueryInterface(this.gatheringTarget, IID_Identity))
{
// Let the Timer logic handle this
this.StartTimer(0);
return false;
}
// No rate, give up on gathering
this.FinishOrder();
return true;
}
// Scale timing interval based on rate, and start timer
// The offset should be at least as long as the repeat time so we use the same value for both.
var offset = 1000/rate;
var repeat = offset;
this.StartTimer(offset, repeat);
// We want to start the gather animation as soon as possible,
// but only if we're actually at the target and it's still alive
// (else it'll look like we're chopping empty air).
// (If it's not alive, the Timer handler will deal with sending us
// off to a different target.)
if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer))
{
this.StopMoving();
this.SetDefaultAnimationVariant();
this.FaceTowardsTarget(this.order.data.target);
this.SelectAnimation("gather_" + this.order.data.type.specific);
}
return false;
},
"leave": function() {
this.StopTimer();
// don't use ownership because this is called after a conversion/resignation
// and the ownership would be invalid then.
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply)
cmpSupply.RemoveGatherer(this.entity);
delete this.gatheringTarget;
// Show the carried resource, if we've gathered anything.
this.ResetAnimation();
this.SetDefaultAnimationVariant();
},
"Timer": function(msg) {
let resourceTemplate = this.order.data.template;
let resourceType = this.order.data.type;
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return;
let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply && cmpSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity))
// Check we can still reach and gather from the target
if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer) && this.CanGather(this.gatheringTarget))
{
// Gather the resources:
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
// Try to gather treasure
if (cmpResourceGatherer.TryInstantGather(this.gatheringTarget))
return;
// If we've already got some resources but they're the wrong type,
// drop them first to ensure we're only ever carrying one type
if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic))
cmpResourceGatherer.DropResources();
this.FaceTowardsTarget(this.order.data.target);
// Collect from the target
let status = cmpResourceGatherer.PerformGather(this.gatheringTarget);
// If we've collected as many resources as possible,
// return to the nearest dropsite
if (status.filled)
{
let nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
{
// (Keep this Gather order on the stack so we'll
// continue gathering after returning)
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// Oh no, couldn't find any drop sites. Give up on gathering.
this.FinishOrder();
return;
}
// We can gather more from this target, do so in the next timer
if (!status.exhausted)
return;
}
else
{
// Try to follow the target
if (this.MoveToTargetRange(this.gatheringTarget, IID_ResourceGatherer))
{
this.SetNextState("APPROACHING");
return;
}
// Our target is no longer visible - go to its last known position first
// and then hopefully it will become visible.
if (!this.CheckTargetVisible(this.gatheringTarget) && this.order.data.lastPos)
{
this.PushOrderFront("Walk", {
"x": this.order.data.lastPos.x,
"z": this.order.data.lastPos.z,
"force": this.order.data.force
});
return;
}
}
// We're already in range, can't get anywhere near it or the target is exhausted.
// Give up on this order and try our next queued order
// but first check what is our next order and, if needed, insert a returnResource order
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer.IsCarrying(resourceType.generic) &&
this.orderQueue.length > 1 && this.orderQueue[1] !== "ReturnResource" &&
(this.orderQueue[1].type !== "Gather" || this.orderQueue[1].data.type.generic !== resourceType.generic))
{
let nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
this.orderQueue.splice(1, 0, { "type": "ReturnResource", "data": { "target": nearby, "force": false } });
}
// Must go before FinishOrder or this.order will be undefined.
let initPos = this.order.data.initPos;
if (this.FinishOrder())
return;
// No remaining orders - pick a useful default behaviour
if (!initPos)
{
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
let pos = cmpPosition.GetPosition();
initPos = { 'x': pos.X, 'z': pos.Z };
}
}
if (initPos)
{
// Try to find a new resource of the same specific type near the initial resource position:
// Also don't switch to a different type of huntable animal
let nearby = this.FindNearbyResource(function(ent, type, template) {
return (
(type.generic == "treasure" && resourceType.generic == "treasure") ||
(type.specific == resourceType.specific &&
(type.specific != "meat" || resourceTemplate == template))
);
}, new Vector2D(initPos.x, initPos.z));
if (nearby)
{
this.PerformGather(nearby, false, false);
return;
}
// Failing that, try to move there and se if we are more lucky: maybe there are resources in FOW.
// Only move if we are some distance away (TODO: pick the distance better?)
if (!this.CheckPointRangeExplicit(initPos.x, initPos.z, 0, 10))
{
this.GatherNearPosition(initPos.x, initPos.z, resourceType, resourceTemplate);
return;
}
}
// Nothing else to gather - if we're carrying anything then we should
// drop it off, and if not then we might as well head to the dropsite
// anyway because that's a nice enough place to congregate and idle
let nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
{
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// No dropsites - just give up
},
},
},
"HEAL": {
"Attacked": function(msg) {
// If we stand ground we will rather die than flee
if (!this.GetStance().respondStandGround && !this.order.data.force)
this.Flee(msg.data.attacker, false);
},
"APPROACHING": {
"enter": function() {
if (this.CheckRange(this.order.data, IID_Heal))
{
this.SetNextState("HEALING");
return true;
}
if (!this.MoveTo(this.order.data, IID_Heal))
{
this.SetNextState("FINDINGNEWTARGET");
return true;
}
this.StartTimer(1000, 1000);
},
"leave": function() {
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null))
this.SetNextState("FINDINGNEWTARGET");
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || this.CheckRange(this.order.data, IID_Heal))
this.SetNextState("HEALING");
},
},
"HEALING": {
"enter": function() {
if (!this.CheckRange(this.order.data, IID_Heal))
{
this.SetNextState("APPROACHING");
return true;
}
if (!this.TargetIsAlive(this.order.data.target) ||
!this.CanHeal(this.order.data.target))
{
this.SetNextState("FINDINGNEWTARGET");
return true;
}
let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
this.healTimers = cmpHeal.GetTimers();
// If the repeat time since the last heal hasn't elapsed,
// delay the action to avoid healing too fast.
var prepare = this.healTimers.prepare;
if (this.lastHealed)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime();
prepare = Math.max(prepare, repeatLeft);
}
this.SelectAnimation("heal");
this.SetAnimationSync(prepare, this.healTimers.repeat);
this.StartTimer(prepare, this.healTimers.repeat);
// If using a non-default prepare time, re-sync the animation when the timer runs.
this.resyncAnimation = prepare != this.healTimers.prepare;
this.FaceTowardsTarget(this.order.data.target);
},
"leave": function() {
this.ResetAnimation();
this.StopTimer();
},
"Timer": function(msg) {
let target = this.order.data.target;
// Check the target is still alive and healable
if (!this.TargetIsAlive(target) || !this.CanHeal(target))
{
this.SetNextState("FINDINGNEWTARGET");
return;
}
// Check if we can still reach the target
if (!this.CheckRange(this.order.data, IID_Heal))
{
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
// Can't reach it - try to chase after it
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("HEAL.APPROACHING");
}
else
this.SetNextState("FINDINGNEWTARGET");
return;
}
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastHealed = cmpTimer.GetTime() - msg.lateness;
this.FaceTowardsTarget(target);
let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
cmpHeal.PerformHeal(target);
if (this.resyncAnimation)
{
this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat);
this.resyncAnimation = false;
}
},
},
"FINDINGNEWTARGET": {
"enter": function() {
// If we have another order, do that instead.
if (this.FinishOrder())
return true;
// Heal another one
if (this.FindNewHealTargets())
return true;
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
// We quit this state right away.
return true;
},
},
},
// Returning to dropsite
"RETURNRESOURCE": {
"APPROACHING": {
"enter": function() {
if (!this.MoveTo(this.order.data, IID_ResourceGatherer))
{
this.FinishOrder();
return true;
}
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
// Check the dropsite is in range and we can return our resource there
// (we didn't get stopped before reaching it)
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
{
let cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
// Dump any resources we can
let dropsiteTypes = cmpResourceDropsite.GetTypes();
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
cmpResourceGatherer.CommitResources(dropsiteTypes);
// Stop showing the carried resource animation.
this.SetDefaultAnimationVariant();
// Our next order should always be a Gather,
// so just switch back to that order
this.FinishOrder();
return;
}
}
if (msg.obstructed)
return;
// If we are here: we are in range but not carrying the right resources (or resources at all),
// the dropsite was destroyed, or we couldn't reach it, or ownership changed.
// Look for a new one.
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
let genericType = cmpResourceGatherer.GetMainCarryingType();
let nearby = this.FindNearestDropsite(genericType);
if (nearby)
{
this.FinishOrder();
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// Oh no, couldn't find any drop sites. Give up on returning.
this.FinishOrder();
},
},
},
"TRADE": {
"Attacked": function(msg) {
// Ignore attack
// TODO: Inform player
},
"APPROACHINGMARKET": {
"enter": function() {
if (!this.MoveToMarket(this.order.data.target))
{
this.FinishOrder();
return true;
}
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (!msg.likelyFailure && !this.CheckTargetRange(this.order.data.target, IID_Trader))
return;
if (this.waypoints && this.waypoints.length)
{
if (!this.MoveToMarket(this.order.data.target))
this.StopTrading();
}
else
this.PerformTradeAndMoveToNextMarket(this.order.data.target);
},
},
"TradingCanceled": function(msg) {
if (msg.market != this.order.data.target)
return;
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
let otherMarket = cmpTrader && cmpTrader.GetFirstMarket();
this.StopTrading();
if (otherMarket)
this.WalkToTarget(otherMarket);
},
},
"REPAIR": {
"APPROACHING": {
"enter": function() {
if (!this.MoveTo(this.order.data, IID_Builder))
{
this.FinishOrder();
return true;
}
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || msg.likelySuccess)
this.SetNextState("REPAIRING");
},
},
"REPAIRING": {
"enter": function() {
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
if (this.order.data.force)
this.order.data.autoharvest = true;
this.order.data.force = false;
this.repairTarget = this.order.data.target; // temporary, deleted in "leave".
// Check we can still reach and repair the target
if (!this.CanRepair(this.repairTarget))
{
// Can't reach it, no longer owned by ally, or it doesn't exist any more
this.FinishOrder();
return true;
}
if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
{
this.SetNextState("APPROACHING");
return true;
}
// Check if the target is still repairable
var cmpHealth = Engine.QueryInterface(this.repairTarget, IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() >= cmpHealth.GetMaxHitpoints())
{
// The building was already finished/fully repaired before we arrived;
// let the ConstructionFinished handler handle this.
this.OnGlobalConstructionFinished({"entity": this.repairTarget, "newentity": this.repairTarget});
return true;
}
this.StopMoving();
let cmpBuilderList = QueryBuilderListInterface(this.repairTarget);
if (cmpBuilderList)
cmpBuilderList.AddBuilder(this.entity);
this.FaceTowardsTarget(this.order.data.target);
this.SelectAnimation("build");
this.StartTimer(1000, 1000);
return false;
},
"leave": function() {
let cmpBuilderList = QueryBuilderListInterface(this.repairTarget);
if (cmpBuilderList)
cmpBuilderList.RemoveBuilder(this.entity);
delete this.repairTarget;
this.StopTimer();
this.ResetAnimation();
},
"Timer": function(msg) {
// Check we can still reach and repair the target
if (!this.CanRepair(this.repairTarget))
{
// No longer owned by ally, or it doesn't exist any more
this.FinishOrder();
return;
}
this.FaceTowardsTarget(this.order.data.target);
let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
cmpBuilder.PerformBuilding(this.repairTarget);
// if the building is completed, the leave() function will be called
// by the ConstructionFinished message
// in that case, the repairTarget is deleted, and we can just return
if (!this.repairTarget)
return;
if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
this.SetNextState("APPROACHING");
},
},
"ConstructionFinished": function(msg) {
if (msg.data.entity != this.order.data.target)
return; // ignore other buildings
// Save the current order's data in case we need it later
let oldData = this.order.data;
// Save the current state so we can continue walking if necessary
// FinishOrder() below will switch to IDLE if there's no order, which sets the idle animation.
// Idle animation while moving towards finished construction looks weird (ghosty).
let oldState = this.GetCurrentState();
// Drop any resource we can if we are in range when the construction finishes
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
let cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite);
if (cmpResourceGatherer && cmpResourceDropsite && this.CheckTargetRange(msg.data.newentity, IID_Builder) &&
this.CanReturnResource(msg.data.newentity, true))
{
let dropsiteTypes = cmpResourceDropsite.GetTypes();
cmpResourceGatherer.CommitResources(dropsiteTypes);
this.SetDefaultAnimationVariant();
}
// We finished building it.
// Switch to the next order (if any)
if (this.FinishOrder())
{
if (this.CanReturnResource(msg.data.newentity, true))
{
this.SetDefaultAnimationVariant();
this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false });
}
return;
}
// No remaining orders - pick a useful default behaviour
// If autocontinue explicitly disabled (e.g. by AI) then
// do nothing automatically
if (!oldData.autocontinue)
return;
// If this building was e.g. a farm of ours, the entities that received
// the build command should start gathering from it
if ((oldData.force || oldData.autoharvest) && this.CanGather(msg.data.newentity))
{
if (this.CanReturnResource(msg.data.newentity, true))
{
this.SetDefaultAnimationVariant();
this.PushOrder("ReturnResource", { "target": msg.data.newentity, "force": false });
}
this.PerformGather(msg.data.newentity, true, false);
return;
}
// If this building was e.g. a farmstead of ours, entities that received
// the build command should look for nearby resources to gather
if ((oldData.force || oldData.autoharvest) && this.CanReturnResource(msg.data.newentity, false))
{
let types = cmpResourceDropsite.GetTypes();
let pos;
let cmpPosition = Engine.QueryInterface(msg.data.newentity, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
pos = cmpPosition.GetPosition2D();
// TODO: Slightly undefined behavior here, we don't know what type of resource will be collected,
// may cause problems for AIs (especially hunting fast animals), but avoid ugly hacks to fix that!
let nearby = this.FindNearbyResource(function(ent, type, template) {
return (types.indexOf(type.generic) != -1);
}, pos);
if (nearby)
{
this.PerformGather(nearby, true, false);
return;
}
}
// Look for a nearby foundation to help with
let nearbyFoundation = this.FindNearbyFoundation();
if (nearbyFoundation)
{
this.AddOrder("Repair", { "target": nearbyFoundation, "autocontinue": oldData.autocontinue, "force": false }, true);
return;
}
// Unit was approaching and there's nothing to do now, so switch to walking
if (oldState === "INDIVIDUAL.REPAIR.APPROACHING")
// We're already walking to the given point, so add this as a order.
this.WalkToTarget(msg.data.newentity, true);
},
},
"GARRISON": {
"enter": function() {
// If the garrisonholder should pickup, warn it so it can take needed action
var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target; // temporary, deleted in "leave"
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
}
},
"leave": function() {
// If a pickup has been requested and not yet canceled, cancel it
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
},
"APPROACHING": {
"enter": function() {
if (!this.MoveToGarrisonRange(this.order.data.target))
{
this.FinishOrder();
return true;
}
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || msg.likelySuccess)
this.SetNextState("GARRISONED");
},
},
"GARRISONED": {
"enter": function() {
let target = this.order.data.target;
if (!target)
{
this.FinishOrder();
return true;
}
if (this.IsGarrisoned())
return false;
// Check that we can garrison here
if (this.CanGarrison(target))
// Check that we're in range of the garrison target
if (this.CheckGarrisonRange(target))
{
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
// Check that garrisoning succeeds
if (cmpGarrisonHolder.Garrison(this.entity))
{
this.isGarrisoned = true;
if (this.formationController)
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
{
// disable rearrange for this removal,
// but enable it again for the next
// move command
var rearrange = cmpFormation.rearrange;
cmpFormation.SetRearrange(false);
cmpFormation.RemoveMembers([this.entity]);
cmpFormation.SetRearrange(rearrange);
}
}
// Check if we are garrisoned in a dropsite
var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (cmpResourceDropsite && this.CanReturnResource(target, true))
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
cmpResourceGatherer.CommitResources(dropsiteTypes);
this.SetDefaultAnimationVariant();
}
}
// If a pickup has been requested, remove it
if (this.pickup)
{
var cmpHolderPosition = Engine.QueryInterface(target, IID_Position);
var cmpHolderUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderPosition)
cmpHolderUnitAI.lastShorelinePosition = cmpHolderPosition.GetPosition();
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
if (this.IsTurret())
{
this.SetNextState("IDLE");
return true;
}
return false;
}
}
else
{
// Unable to reach the target, try again (or follow if it is a moving target)
// except if the does not exits anymore or its orders have changed
if (this.pickup)
{
var cmpUnitAI = Engine.QueryInterface(this.pickup, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.HasPickupOrder(this.entity))
{
this.FinishOrder();
return true;
}
}
this.SetNextState("APPROACHING");
return true;
}
// Garrisoning failed for some reason, so finish the order
this.FinishOrder();
return true;
},
"leave": function() {
}
},
},
"CHEERING": {
"enter": function() {
// Unit is invulnerable while cheering
- var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
- cmpDamageReceiver.SetInvulnerability(true);
+ var cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance);
+ cmpResistance.SetInvulnerability(true);
this.SelectAnimation("promotion");
this.StartTimer(2800, 2800);
return false;
},
"leave": function() {
this.StopTimer();
this.ResetAnimation();
- var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
- cmpDamageReceiver.SetInvulnerability(false);
+ var cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance);
+ cmpResistance.SetInvulnerability(false);
},
"Timer": function(msg) {
this.FinishOrder();
},
},
"PACKING": {
"enter": function() {
this.StopMoving();
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.Pack();
},
"PackFinished": function(msg) {
this.FinishOrder();
},
"leave": function() {
},
"Attacked": function(msg) {
// Ignore attacks while packing
},
},
"UNPACKING": {
"enter": function() {
this.StopMoving();
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.Unpack();
},
"PackFinished": function(msg) {
this.FinishOrder();
},
"leave": function() {
},
"Attacked": function(msg) {
// Ignore attacks while unpacking
},
},
"PICKUP": {
"APPROACHING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (msg.likelyFailure || msg.likelySuccess)
this.SetNextState("LOADING");
},
"PickupCanceled": function() {
this.StopMoving();
this.FinishOrder();
},
},
"LOADING": {
"enter": function() {
this.StopMoving();
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
{
this.FinishOrder();
return true;
}
return false;
},
"PickupCanceled": function() {
this.FinishOrder();
},
},
},
},
"ANIMAL": {
"Attacked": function(msg) {
if (this.template.NaturalBehaviour == "skittish" ||
this.template.NaturalBehaviour == "passive")
{
this.Flee(msg.data.attacker, false);
}
else if (this.IsDangerousAnimal() || this.template.NaturalBehaviour == "defensive")
{
if (this.CanAttack(msg.data.attacker))
this.Attack(msg.data.attacker, false);
}
else if (this.template.NaturalBehaviour == "domestic")
{
// Never flee, stop what we were doing
this.SetNextState("IDLE");
}
},
"Order.LeaveFoundation": function(msg) {
// Move a tile outside the building
let range = 4;
if (this.CheckTargetRangeExplicit(msg.data.target, range, -1))
{
this.FinishOrder();
return;
}
this.order.data.min = range;
this.SetNextState("WALKING");
},
"IDLE": {
// (We need an IDLE state so that FinishOrder works)
"enter": function() {
// Start feeding immediately
this.SetNextState("FEEDING");
return true;
},
},
"ROAMING": {
"enter": function() {
// Walk in a random direction
this.SetFacePointAfterMove(false);
this.MoveRandomly(+this.template.RoamDistance);
// Set a random timer to switch to feeding state
this.StartTimer(randIntInclusive(+this.template.RoamTimeMin, +this.template.RoamTimeMax));
},
"leave": function() {
this.StopMoving();
this.StopTimer();
this.SetFacePointAfterMove(true);
},
"LosRangeUpdate": function(msg) {
if (this.template.NaturalBehaviour == "skittish")
{
if (msg.data.added.length > 0)
{
this.Flee(msg.data.added[0], false);
return;
}
}
// Start attacking one of the newly-seen enemy (if any)
else if (this.IsDangerousAnimal())
{
this.AttackVisibleEntity(msg.data.added);
}
// TODO: if two units enter our range together, we'll attack the
// first and then the second won't trigger another LosRangeUpdate
// so we won't notice it. Probably we should do something with
// ResetActiveQuery in ROAMING.enter/FEEDING.enter in order to
// find any units that are already in range.
},
"Timer": function(msg) {
this.SetNextState("FEEDING");
},
"MovementUpdate": function() {
this.MoveRandomly(+this.template.RoamDistance);
},
},
"FEEDING": {
"enter": function() {
// Stop and eat for a while
this.SelectAnimation("feeding");
this.StopMoving();
this.StartTimer(randIntInclusive(+this.template.FeedTimeMin, +this.template.FeedTimeMax));
},
"leave": function() {
this.ResetAnimation();
this.StopTimer();
},
"LosRangeUpdate": function(msg) {
if (this.template.NaturalBehaviour == "skittish")
{
if (msg.data.added.length > 0)
{
this.Flee(msg.data.added[0], false);
return;
}
}
// Start attacking one of the newly-seen enemy (if any)
else if (this.template.NaturalBehaviour == "violent")
{
this.AttackVisibleEntity(msg.data.added);
}
},
"Timer": function(msg) {
this.SetNextState("ROAMING");
},
},
"FLEEING": "INDIVIDUAL.FLEEING", // reuse the same fleeing behaviour for animals
"COMBAT": "INDIVIDUAL.COMBAT", // reuse the same combat behaviour for animals
"WALKING": "INDIVIDUAL.WALKING", // reuse the same walking behaviour for animals
// only used for domestic animals
},
};
UnitAI.prototype.Init = function()
{
this.orderQueue = []; // current order is at the front of the list
this.order = undefined; // always == this.orderQueue[0]
this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
this.isGarrisoned = false;
this.isIdle = false;
this.finishedOrder = false; // used to find if all formation members finished the order
this.heldPosition = undefined;
// Queue of remembered works
this.workOrders = [];
this.isGuardOf = undefined;
// For preventing increased action rate due to Stop orders or target death.
this.lastAttacked = undefined;
this.lastHealed = undefined;
this.SetStance(this.template.DefaultStance);
};
UnitAI.prototype.IsTurret = function()
{
if (!this.IsGarrisoned())
return false;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
return cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY;
};
UnitAI.prototype.IsFormationController = function()
{
return (this.template.FormationController == "true");
};
UnitAI.prototype.IsFormationMember = function()
{
return (this.formationController != INVALID_ENTITY);
};
UnitAI.prototype.HasFinishedOrder = function()
{
return this.finishedOrder;
};
UnitAI.prototype.ResetFinishOrder = function()
{
this.finishedOrder = false;
};
UnitAI.prototype.IsAnimal = function()
{
return (this.template.NaturalBehaviour ? true : false);
};
UnitAI.prototype.IsDangerousAnimal = function()
{
return (this.IsAnimal() && (this.template.NaturalBehaviour == "violent" ||
this.template.NaturalBehaviour == "aggressive"));
};
UnitAI.prototype.IsDomestic = function()
{
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
return cmpIdentity && cmpIdentity.HasClass("Domestic");
};
UnitAI.prototype.IsHealer = function()
{
return Engine.QueryInterface(this.entity, IID_Heal);
};
UnitAI.prototype.IsIdle = function()
{
return this.isIdle;
};
UnitAI.prototype.IsGarrisoned = function()
{
return this.isGarrisoned;
};
UnitAI.prototype.SetGarrisoned = function()
{
this.isGarrisoned = true;
};
UnitAI.prototype.GetGarrisonHolder = function()
{
if (this.IsGarrisoned())
{
for (let order of this.orderQueue)
if (order.type == "Garrison")
return order.data.target;
}
return INVALID_ENTITY;
};
UnitAI.prototype.ShouldRespondToEndOfAlert = function()
{
return !this.orderQueue.length || this.orderQueue[0].type == "Garrison";
};
UnitAI.prototype.IsFleeing = function()
{
var state = this.GetCurrentState().split(".").pop();
return (state == "FLEEING");
};
UnitAI.prototype.IsWalking = function()
{
var state = this.GetCurrentState().split(".").pop();
return (state == "WALKING");
};
/**
* Return true if the current order is WalkAndFight or Patrol.
*/
UnitAI.prototype.IsWalkingAndFighting = function()
{
if (this.IsFormationMember())
return false;
return this.orderQueue.length > 0 && (this.orderQueue[0].type == "WalkAndFight" || this.orderQueue[0].type == "Patrol");
};
UnitAI.prototype.OnCreate = function()
{
if (this.IsAnimal())
this.UnitFsm.Init(this, "ANIMAL.FEEDING");
else if (this.IsFormationController())
this.UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
else
this.UnitFsm.Init(this, "INDIVIDUAL.IDLE");
this.isIdle = true;
};
UnitAI.prototype.OnDiplomacyChanged = function(msg)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() == msg.player)
this.SetupRangeQueries();
if (this.isGuardOf && !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf))
this.RemoveGuard();
};
UnitAI.prototype.OnOwnershipChanged = function(msg)
{
this.SetupRangeQueries();
if (this.isGuardOf && (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf)))
this.RemoveGuard();
// If the unit isn't being created or dying, reset stance and clear orders
if (msg.to != INVALID_PLAYER && msg.from != INVALID_PLAYER)
{
// Switch to a virgin state to let states execute their leave handlers.
// except if garrisoned or cheering or (un)packing, in which case we only clear the order queue
if (this.isGarrisoned || this.IsPacking() || this.orderQueue[0] && this.orderQueue[0].type == "Cheering")
{
this.orderQueue.length = Math.min(this.orderQueue.length, 1);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
}
else
{
let index = this.GetCurrentState().indexOf(".");
if (index != -1)
this.UnitFsm.SwitchToNextState(this, this.GetCurrentState().slice(0,index));
this.Stop(false);
}
this.workOrders = [];
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (cmpTrader)
cmpTrader.StopTrading();
this.SetStance(this.template.DefaultStance);
if (this.IsTurret())
this.SetTurretStance();
}
};
UnitAI.prototype.OnDestroy = function()
{
// Switch to an empty state to let states execute their leave handlers.
this.UnitFsm.SwitchToNextState(this, "");
// Clean up range queries
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
cmpRangeManager.DestroyActiveQuery(this.losRangeQuery);
if (this.losHealRangeQuery)
cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery);
};
UnitAI.prototype.OnVisionRangeChanged = function(msg)
{
// Update range queries
if (this.entity == msg.entity)
this.SetupRangeQueries();
};
UnitAI.prototype.HasPickupOrder = function(entity)
{
return this.orderQueue.some(order => order.type == "PickupUnit" && order.data.target == entity);
};
UnitAI.prototype.OnPickupRequested = function(msg)
{
// First check if we already have such a request
if (this.HasPickupOrder(msg.entity))
return;
// Otherwise, insert the PickUp order after the last forced order
this.PushOrderAfterForced("PickupUnit", { "target": msg.entity });
};
UnitAI.prototype.OnPickupCanceled = function(msg)
{
for (let i = 0; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].type != "PickupUnit" || this.orderQueue[i].data.target != msg.entity)
continue;
if (i == 0)
this.UnitFsm.ProcessMessage(this, {"type": "PickupCanceled", "data": msg});
else
this.orderQueue.splice(i, 1);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
break;
}
};
// Wrapper function that sets up the normal and healer range queries.
UnitAI.prototype.SetupRangeQueries = function()
{
this.SetupRangeQuery();
if (this.IsHealer())
this.SetupHealRangeQuery();
};
UnitAI.prototype.UpdateRangeQueries = function()
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
this.SetupRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losRangeQuery));
if (this.IsHealer() && this.losHealRangeQuery)
this.SetupHealRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losHealRangeQuery));
};
// Set up a range query for all enemy and gaia units within LOS range
// which can be attacked.
// This should be called whenever our ownership changes.
UnitAI.prototype.SetupRangeQuery = function(enable = true)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
{
cmpRangeManager.DestroyActiveQuery(this.losRangeQuery);
this.losRangeQuery = undefined;
}
var cmpPlayer = QueryOwnerInterface(this.entity);
// If we are being destructed (owner -1), creating a range query is pointless
if (!cmpPlayer)
return;
// Exclude allies, and self
// TODO: How to handle neutral players - Special query to attack military only?
var players = cmpPlayer.GetEnemies();
var range = this.GetQueryRange(IID_Attack);
- this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal"));
+ this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"));
if (enable)
cmpRangeManager.EnableActiveQuery(this.losRangeQuery);
};
// Set up a range query for all own or ally units within LOS range
// which can be healed.
// This should be called whenever our ownership changes.
UnitAI.prototype.SetupHealRangeQuery = function(enable = true)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losHealRangeQuery)
{
cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery);
this.losHealRangeQuery = undefined;
}
var cmpPlayer = QueryOwnerInterface(this.entity);
// If we are being destructed (owner -1), creating a range query is pointless
if (!cmpPlayer)
return;
var players = cmpPlayer.GetAllies();
var range = this.GetQueryRange(IID_Heal);
this.losHealRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health, cmpRangeManager.GetEntityFlagMask("injured"));
if (enable)
cmpRangeManager.EnableActiveQuery(this.losHealRangeQuery);
};
//// FSM linkage functions ////
// Setting the next state to the current state will leave/re-enter the top-most substate.
UnitAI.prototype.SetNextState = function(state)
{
this.UnitFsm.SetNextState(this, state);
};
UnitAI.prototype.DeferMessage = function(msg)
{
this.UnitFsm.DeferMessage(this, msg);
};
UnitAI.prototype.GetCurrentState = function()
{
return this.UnitFsm.GetCurrentState(this);
};
UnitAI.prototype.FsmStateNameChanged = function(state)
{
Engine.PostMessage(this.entity, MT_UnitAIStateChanged, { "to": state });
};
/**
* Call when the current order has been completed (or failed).
* Removes the current order from the queue, and processes the
* next one (if any). Returns false and defaults to IDLE
* if there are no remaining orders or if the unit is not
* inWorld and not garrisoned (thus usually waiting to be destroyed).
*/
UnitAI.prototype.FinishOrder = function()
{
if (!this.orderQueue.length)
{
let stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetCurrentTemplateName(this.entity);
error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack);
}
this.orderQueue.shift();
this.order = this.orderQueue[0];
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (this.orderQueue.length && (this.IsGarrisoned() || cmpPosition && cmpPosition.IsInWorld()))
{
let ret = this.UnitFsm.ProcessMessage(this,
{ "type": "Order."+this.order.type, "data": this.order.data }
);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret && ret.discardOrder)
return this.FinishOrder();
// Otherwise we've successfully processed a new order
return true;
}
this.orderQueue = [];
this.order = undefined;
// Switch to IDLE as a default state.
this.SetNextState("IDLE");
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// Check if there are queued formation orders
if (this.IsFormationMember())
{
let cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpUnitAI)
{
// Inform the formation controller that we finished this task
this.finishedOrder = true;
// We don't want to carry out the default order
// if there are still queued formation orders left
if (cmpUnitAI.GetOrders().length > 1)
return true;
}
}
return false;
};
/**
* Add an order onto the back of the queue,
* and execute it if we didn't already have an order.
*/
UnitAI.prototype.PushOrder = function(type, data)
{
var order = { "type": type, "data": data };
this.orderQueue.push(order);
// If we didn't already have an order, then process this new one
if (this.orderQueue.length == 1)
{
this.order = order;
let ret = this.UnitFsm.ProcessMessage(this,
{ "type": "Order."+this.order.type, "data": this.order.data }
);
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret && ret.discardOrder)
this.FinishOrder();
}
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
/**
* Add an order onto the front of the queue,
* and execute it immediately.
*/
UnitAI.prototype.PushOrderFront = function(type, data)
{
var order = { "type": type, "data": data };
// If current order is cheering then add new order after it
// same thing if current order if packing/unpacking
if (this.order && this.order.type == "Cheering")
{
var cheeringOrder = this.orderQueue.shift();
this.orderQueue.unshift(cheeringOrder, order);
}
else if (this.order && this.IsPacking())
{
var packingOrder = this.orderQueue.shift();
this.orderQueue.unshift(packingOrder, order);
}
else
{
this.orderQueue.unshift(order);
this.order = order;
let ret = this.UnitFsm.ProcessMessage(this,
{ "type": "Order."+this.order.type, "data": this.order.data }
);
// If the order was rejected then immediately take it off again;
// assume the previous active order is still valid (the short-lived
// new order hasn't changed state or anything) so we can carry on
// as if nothing had happened
if (ret && ret.discardOrder)
{
this.orderQueue.shift();
this.order = this.orderQueue[0];
}
}
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
/**
* Insert an order after the last forced order onto the queue
* and after the other orders of the same type
*/
UnitAI.prototype.PushOrderAfterForced = function(type, data)
{
if (!this.order || ((!this.order.data || !this.order.data.force) && this.order.type != type))
this.PushOrderFront(type, data);
else
{
for (let i = 1; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].data && this.orderQueue[i].data.force)
continue;
if (this.orderQueue[i].type == type)
continue;
this.orderQueue.splice(i, 0, {"type": type, "data": data});
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
return;
}
this.PushOrder(type, data);
}
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
UnitAI.prototype.ReplaceOrder = function(type, data)
{
// Remember the previous work orders to be able to go back to them later if required
if (data && data.force)
{
if (this.IsFormationController())
this.CallMemberFunction("UpdateWorkOrders", [type]);
else
this.UpdateWorkOrders(type);
}
let garrisonHolder = this.IsGarrisoned() && type != "Ungarrison" ? this.GetGarrisonHolder() : null;
// Special cases of orders that shouldn't be replaced:
// 1. Cheering - we're invulnerable, add order after we finish
// 2. Packing/unpacking - we're immobile, add order after we finish (unless it's cancel)
// TODO: maybe a better way of doing this would be to use priority levels
if (this.order && this.order.type == "Cheering")
{
var order = { "type": type, "data": data };
var cheeringOrder = this.orderQueue.shift();
this.orderQueue = [cheeringOrder, order];
}
else if (this.IsPacking() && type != "CancelPack" && type != "CancelUnpack")
{
var order = { "type": type, "data": data };
var packingOrder = this.orderQueue.shift();
this.orderQueue = [packingOrder, order];
}
else
{
this.orderQueue = [];
this.PushOrder(type, data);
}
if (garrisonHolder)
this.PushOrder("Garrison", { "target": garrisonHolder });
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
UnitAI.prototype.GetOrders = function()
{
return this.orderQueue.slice();
};
UnitAI.prototype.AddOrders = function(orders)
{
orders.forEach(order => this.PushOrder(order.type, order.data));
};
UnitAI.prototype.GetOrderData = function()
{
var orders = [];
for (let order of this.orderQueue)
if (order.data)
orders.push(clone(order.data));
return orders;
};
UnitAI.prototype.UpdateWorkOrders = function(type)
{
var isWorkType = type => type == "Gather" || type == "Trade" || type == "Repair" || type == "ReturnResource";
// If we are being re-affected to a work order, forget the previous ones
if (isWorkType(type))
{
this.workOrders = [];
return;
}
// Then if we already have work orders, keep them
if (this.workOrders.length)
return;
// First if the unit is in a formation, get its workOrders from it
if (this.IsFormationMember())
{
var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpUnitAI)
{
for (var i = 0; i < cmpUnitAI.orderQueue.length; ++i)
{
if (isWorkType(cmpUnitAI.orderQueue[i].type))
{
this.workOrders = cmpUnitAI.orderQueue.slice(i);
return;
}
}
}
}
// If nothing found, take the unit orders
for (var i = 0; i < this.orderQueue.length; ++i)
{
if (isWorkType(this.orderQueue[i].type))
{
this.workOrders = this.orderQueue.slice(i);
return;
}
}
};
UnitAI.prototype.BackToWork = function()
{
if (this.workOrders.length == 0)
return false;
if (this.IsGarrisoned())
{
let cmpGarrisonHolder = Engine.QueryInterface(this.GetGarrisonHolder(), IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.PerformEject([this.entity], false))
return false;
}
// Clear the order queue considering special orders not to avoid
if (this.order && this.order.type == "Cheering")
{
var cheeringOrder = this.orderQueue.shift();
this.orderQueue = [cheeringOrder];
}
else
this.orderQueue = [];
this.AddOrders(this.workOrders);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// And if the unit is in a formation, remove it from the formation
if (this.IsFormationMember())
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers([this.entity]);
}
this.workOrders = [];
return true;
};
UnitAI.prototype.HasWorkOrders = function()
{
return this.workOrders.length > 0;
};
UnitAI.prototype.GetWorkOrders = function()
{
return this.workOrders;
};
UnitAI.prototype.SetWorkOrders = function(orders)
{
this.workOrders = orders;
};
UnitAI.prototype.TimerHandler = function(data, lateness)
{
// Reset the timer
if (data.timerRepeat === undefined)
this.timer = undefined;
this.UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
};
/**
* Set up the UnitAI timer to run after 'offset' msecs, and then
* every 'repeat' msecs until StopTimer is called. A "Timer" message
* will be sent each time the timer runs.
*/
UnitAI.prototype.StartTimer = function(offset, repeat)
{
if (this.timer)
error("Called StartTimer when there's already an active timer");
var data = { "timerRepeat": repeat };
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
if (repeat === undefined)
this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data);
else
this.timer = cmpTimer.SetInterval(this.entity, IID_UnitAI, "TimerHandler", offset, repeat, data);
};
/**
* Stop the current UnitAI timer.
*/
UnitAI.prototype.StopTimer = function()
{
if (!this.timer)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
};
UnitAI.prototype.OnMotionUpdate = function(msg)
{
this.UnitFsm.ProcessMessage(this, Object.assign({ "type": "MovementUpdate" }, msg));
};
UnitAI.prototype.OnGlobalConstructionFinished = function(msg)
{
// TODO: This is a bit inefficient since every unit listens to every
// construction message - ideally we could scope it to only the one we're building
this.UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg});
};
UnitAI.prototype.OnGlobalEntityRenamed = function(msg)
{
let changed = false;
for (let order of this.orderQueue)
{
if (order.data && order.data.target && order.data.target == msg.entity)
{
changed = true;
order.data.target = msg.newentity;
}
if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity)
{
changed = true;
order.data.formationTarget = msg.newentity;
}
}
if (changed)
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
UnitAI.prototype.OnAttacked = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
};
UnitAI.prototype.OnGuardedAttacked = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data});
};
UnitAI.prototype.OnHealthChanged = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to});
};
UnitAI.prototype.OnRangeUpdate = function(msg)
{
if (msg.tag == this.losRangeQuery)
this.UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
else if (msg.tag == this.losHealRangeQuery)
this.UnitFsm.ProcessMessage(this, {"type": "LosHealRangeUpdate", "data": msg});
};
UnitAI.prototype.OnPackFinished = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed});
};
//// Helper functions to be called by the FSM ////
UnitAI.prototype.GetWalkSpeed = function()
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!cmpUnitMotion)
return 0;
return cmpUnitMotion.GetWalkSpeed();
};
UnitAI.prototype.GetRunMultiplier = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!cmpUnitMotion)
return 0;
return cmpUnitMotion.GetRunMultiplier();
};
/**
* Returns true if the target exists and has non-zero hitpoints.
*/
UnitAI.prototype.TargetIsAlive = function(ent)
{
var cmpFormation = Engine.QueryInterface(ent, IID_Formation);
if (cmpFormation)
return true;
var cmpHealth = QueryMiragedInterface(ent, IID_Health);
return cmpHealth && cmpHealth.GetHitpoints() != 0;
};
/**
* Returns true if the target exists and needs to be killed before
* beginning to gather resources from it.
*/
UnitAI.prototype.MustKillGatherTarget = function(ent)
{
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (!cmpResourceSupply)
return false;
if (!cmpResourceSupply.GetKillBeforeGather())
return false;
return this.TargetIsAlive(ent);
};
/**
* Returns the entity ID of the nearest resource supply where the given
* filter returns true, or undefined if none can be found.
* if position (as a vector2D) is given, the nearest is computed versus this position.
* TODO: extend this to exclude resources that already have lots of
* gatherers.
*/
UnitAI.prototype.FindNearbyResource = function(filter, position)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return undefined;
let owner = cmpOwnership.GetOwner();
// We accept resources owned by Gaia or any player
let players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
let range = 64; // TODO: what's a sensible number?
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let pos = position;
if (!pos)
{
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
pos = cmpPosition.GetPosition2D();
}
let nearby = cmpRangeManager.ExecuteQueryAroundPos(pos, 0, range, players, IID_ResourceSupply);
return nearby.find(ent => {
if (!this.CanGather(ent) || !this.CheckTargetVisible(ent))
return false;
let cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
let type = cmpResourceSupply.GetType();
let amount = cmpResourceSupply.GetCurrentAmount();
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
// Remove "resource|" prefix from template names, if present.
if (template.indexOf("resource|") != -1)
template = template.slice(9);
return amount > 0 && cmpResourceSupply.IsAvailable(owner, this.entity) && filter(ent, type, template);
});
};
/**
* Returns the entity ID of the nearest resource dropsite that accepts
* the given type, or undefined if none can be found.
*/
UnitAI.prototype.FindNearestDropsite = function(genericType)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return undefined;
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return undefined;
let pos = cmpPosition.GetPosition2D();
let bestDropsite;
let bestDist = Infinity;
// Maximum distance a point on an obstruction can be from the center of the obstruction.
let maxDifference = 40;
// Find dropsites owned by this unit's player or allied ones if allowed.
let owner = cmpOwnership.GetOwner();
let cmpPlayer = QueryOwnerInterface(this.entity);
let players = cmpPlayer && cmpPlayer.HasSharedDropsites() ? cmpPlayer.GetMutualAllies() : [owner];
let nearbyDropsites = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite);
let isShip = Engine.QueryInterface(this.entity, IID_Identity).HasClass("Ship");
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
for (let dropsite of nearbyDropsites)
{
// Ships are unable to reach land dropsites and shouldn't attempt to do so.
if (isShip && !Engine.QueryInterface(dropsite, IID_Identity).HasClass("Naval"))
continue;
let cmpResourceDropsite = Engine.QueryInterface(dropsite, IID_ResourceDropsite);
if (!cmpResourceDropsite.AcceptsType(genericType) || !this.CheckTargetVisible(dropsite))
continue;
if (Engine.QueryInterface(dropsite, IID_Ownership).GetOwner() != owner && !cmpResourceDropsite.IsShared())
continue;
// The range manager sorts entities by the distance to their center,
// but we want the distance to the point where resources will be dropped off.
let dist = cmpObstructionManager.DistanceToPoint(dropsite, pos.x, pos.y);
if (dist == -1)
continue;
if (dist < bestDist)
{
bestDropsite = dropsite;
bestDist = dist;
}
else if (dist > bestDist + maxDifference)
break;
}
return bestDropsite;
};
/**
* Returns the entity ID of the nearest building that needs to be constructed,
* or undefined if none can be found close enough.
*/
UnitAI.prototype.FindNearbyFoundation = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return undefined;
// Find buildings owned by this unit's player
var players = [cmpOwnership.GetOwner()];
var range = 64; // TODO: what's a sensible number?
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, IID_Foundation);
// Skip foundations that are already complete. (This matters since
// we process the ConstructionFinished message before the foundation
// we're working on has been deleted.)
return nearby.find(ent => !Engine.QueryInterface(ent, IID_Foundation).IsFinished());
};
/**
* Play a sound appropriate to the current entity.
*/
UnitAI.prototype.PlaySound = function(name)
{
// If we're a formation controller, use the sounds from our first member
if (this.IsFormationController())
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
var member = cmpFormation.GetPrimaryMember();
if (member)
PlaySound(name, member);
}
else
{
// Otherwise use our own sounds
PlaySound(name, this.entity);
}
};
/*
* Set a visualActor animation variant.
* By changing the animation variant, you can change animations based on unitAI state.
* If there are no specific variants or the variant doesn't exist in the actor,
* the actor fallbacks to any existing animation.
* @param type if present, switch to a specific animation variant.
*/
UnitAI.prototype.SetAnimationVariant = function(type)
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetVariant("animationVariant", type);
return;
};
/*
* Reset the animation variant to default behavior
* Default behavior is to pick a resource-carrying variant if resources are being carried.
* Otherwise pick nothing in particular.
*/
UnitAI.prototype.SetDefaultAnimationVariant = function()
{
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
{
this.SetAnimationVariant("");
return;
}
let type = cmpResourceGatherer.GetLastCarriedType();
if (type)
{
let typename = "carry_" + type.generic;
// Special case for meat
if (type.specific == "meat")
typename = "carry_" + type.specific;
this.SetAnimationVariant(typename);
return;
}
this.SetAnimationVariant("");
};
UnitAI.prototype.ResetAnimation = function()
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SelectAnimation("idle", false, 1.0);
};
UnitAI.prototype.SelectAnimation = function(name, once = false, speed = 1.0)
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SelectAnimation(name, once, speed);
};
UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetAnimationSyncRepeat(repeattime);
cmpVisual.SetAnimationSyncOffset(actiontime);
};
UnitAI.prototype.StopMoving = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.StopMoving();
};
/**
* Generic dispatcher for other MoveTo functions.
* @param iid - Interface ID (optional) implementing GetRange
* @param type - Range type for the interface call
* @returns whether the move succeeded or failed.
*/
UnitAI.prototype.MoveTo = function(data, iid, type)
{
if (data.target)
{
if (data.min || data.max)
return this.MoveToTargetRangeExplicit(data.target, data.min || -1, data.max || -1);
else if (!iid)
return this.MoveToTarget(data.target);
return this.MoveToTargetRange(data.target, iid, type);
}
else if (data.min || data.max)
return this.MoveToPointRange(data.x, data.z, data.min || -1, data.max || -1);
return this.MoveToPoint(data.x, data.z);
};
UnitAI.prototype.MoveToPoint = function(x, z)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToPointRange(x, z, 0, 0); // For point goals, allow a max range of 0.
};
UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax);
};
UnitAI.prototype.MoveToTarget = function(target)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, 0, 1);
};
UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
{
if (!this.CheckTargetVisible(target) || this.IsTurret())
return false;
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return false;
var range = cmpRanged.GetRange(type);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
/**
* Move unit so we hope the target is in the attack range
* for melee attacks, this goes straight to the default range checks
* for ranged attacks, the parabolic range is used
*/
UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
{
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
{
let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation())
return false;
}
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
target = cmpFormation.GetClosestMember(this.entity);
if (type != "Ranged")
return this.MoveToTargetRange(target, IID_Attack, type);
if (!this.CheckTargetVisible(target))
return false;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
let range = cmpAttack.GetRange(type);
let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
return false;
let s = thisCmpPosition.GetPosition();
let targetCmpPosition = Engine.QueryInterface(target, IID_Position);
if (!targetCmpPosition.IsInWorld())
return false;
let t = targetCmpPosition.GetPosition();
// h is positive when I'm higher than the target
let h = s.y - t.y + range.elevationBonus;
let parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h);
// No negative roots please
if (h <= -range.max / 2)
// return false? Or hope you come close enough?
parabolicMaxRange = 0;
// The parabole changes while walking so be cautious:
let guessedMaxRange = parabolicMaxRange > range.max ? (range.max + parabolicMaxRange) / 2 : parabolicMaxRange;
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange);
};
UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, min, max);
};
UnitAI.prototype.MoveToGarrisonRange = function(target)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
var range = cmpGarrisonHolder.GetLoadingRange();
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
/**
* Generic dispatcher for other Check...Range functions.
* @param iid - Interface ID (optional) implementing GetRange
* @param type - Range type for the interface call
*/
UnitAI.prototype.CheckRange = function(data, iid, type)
{
if (data.target)
{
if (data.min || data.max)
return this.CheckTargetRangeExplicit(data.target, data.min || -1, data.max || -1);
else if (!iid)
return this.CheckTargetRangeExplicit(data.target, 0, 1);
return this.CheckTargetRange(data.target, iid, type);
}
else if (data.min || data.max)
return this.CheckPointRangeExplicit(data.x, data.z, data.min || -1, data.max || -1);
return this.CheckPointRangeExplicit(data.x, data.z, 0, 0);
};
UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max)
{
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInPointRange(this.entity, x, z, min, max, false);
};
UnitAI.prototype.CheckTargetRange = function(target, iid, type)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return false;
var range = cmpRanged.GetRange(type);
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
};
/**
* Check if the target is inside the attack range
* For melee attacks, this goes straigt to the regular range calculation
* For ranged attacks, the parabolic formula is used to accout for bigger ranges
* when the target is lower, and smaller ranges when the target is higher
*/
UnitAI.prototype.CheckTargetAttackRange = function(target, type)
{
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
{
let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation() &&
cmpFormationUnitAI.order.data.target == target)
return true;
}
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
target = cmpFormation.GetClosestMember(this.entity);
if (type != "Ranged")
return this.CheckTargetRange(target, IID_Attack, type);
let targetCmpPosition = Engine.QueryInterface(target, IID_Position);
if (!targetCmpPosition || !targetCmpPosition.IsInWorld())
return false;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
let range = cmpAttack.GetRange(type);
let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
return false;
let s = thisCmpPosition.GetPosition();
let t = targetCmpPosition.GetPosition();
let h = s.y - t.y + range.elevationBonus;
let maxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h);
if (maxRange < 0)
return false;
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, maxRange, false);
};
UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
{
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, min, max, false);
};
UnitAI.prototype.CheckGarrisonRange = function(target)
{
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
var range = cmpGarrisonHolder.GetLoadingRange();
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
range.max += cmpObstruction.GetUnitRadius()*1.5; // multiply by something larger than sqrt(2)
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
};
/**
* Returns true if the target entity is visible through the FoW/SoD.
*/
UnitAI.prototype.CheckTargetVisible = function(target)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
return false;
// Entities that are hidden and miraged are considered visible
var cmpFogging = Engine.QueryInterface(target, IID_Fogging);
if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner()))
return true;
if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) == "hidden")
return false;
// Either visible directly, or visible in fog
return true;
};
/**
* How close to our goal do we consider it's OK to stop if the goal appears unreachable.
* Currently 3 terrain tiles as that's relatively close but helps pathfinding.
*/
UnitAI.prototype.DefaultRelaxedMaxRange = 12;
/**
* @returns true if the unit is in the relaxed-range from the target.
*/
UnitAI.prototype.RelaxedMaxRangeCheck = function(data, relaxedRange)
{
if (!data.relaxed)
return false;
let ndata = data;
ndata.min = 0;
ndata.max = relaxedRange;
return this.CheckRange(ndata);
};
/**
* Let an entity face its target.
* @param {number} target - The entity-ID of the target.
*/
UnitAI.prototype.FaceTowardsTarget = function(target)
{
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return;
let targetPosition = cmpTargetPosition.GetPosition2D();
// Use cmpUnitMotion for units that support that, otherwise try cmpPosition (e.g. turrets)
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
{
cmpUnitMotion.FaceTowardsPoint(targetPosition.x, targetPosition.y);
return;
}
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
cmpPosition.TurnTo(cmpPosition.GetPosition2D().angleTo(targetPosition));
};
UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(type);
var cmpPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return false;
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
var halfvision = cmpVision.GetRange() / 2;
var pos = cmpPosition.GetPosition();
var heldPosition = this.heldPosition;
if (heldPosition === undefined)
heldPosition = { "x": pos.x, "z": pos.z };
return Math.euclidDistance2D(pos.x, pos.z, heldPosition.x, heldPosition.z) < halfvision + range.max;
};
UnitAI.prototype.CheckTargetIsInVisionRange = function(target)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
var range = cmpVision.GetRange();
var distance = DistanceBetweenEntities(this.entity, target);
return distance < range;
};
UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return undefined;
return cmpAttack.GetBestAttackAgainst(target, allowCapture);
};
/**
* Try to find one of the given entities which can be attacked,
* and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackVisibleEntity = function(ents)
{
var target = ents.find(target => this.CanAttack(target));
if (!target)
return false;
this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true });
return true;
};
/**
* Try to find one of the given entities which can be attacked
* and which is close to the hold position, and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackEntityInZone = function(ents)
{
var target = ents.find(target =>
this.CanAttack(target)
&& this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true))
&& (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target))
);
if (!target)
return false;
this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true });
return true;
};
/**
* Try to respond appropriately given our current stance,
* given a list of entities that match our stance's target criteria.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToTargetedEntities = function(ents)
{
if (!ents.length)
return false;
if (this.GetStance().respondChase)
return this.AttackVisibleEntity(ents);
if (this.GetStance().respondStandGround)
return this.AttackVisibleEntity(ents);
if (this.GetStance().respondHoldGround)
return this.AttackEntityInZone(ents);
if (this.GetStance().respondFlee)
{
this.PushOrderFront("Flee", { "target": ents[0], "force": false });
return true;
}
return false;
};
/**
* Try to respond to healable entities.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToHealableEntities = function(ents)
{
var ent = ents.find(ent => this.CanHeal(ent));
if (!ent)
return false;
this.PushOrderFront("Heal", { "target": ent, "force": false });
return true;
};
/**
* Returns true if we should stop following the target entity.
*/
UnitAI.prototype.ShouldAbandonChase = function(target, force, iid, type)
{
// Forced orders shouldn't be interrupted.
if (force)
return false;
// If we are guarding/escorting, don't abandon as long as the guarded unit is in target range of the attacker
if (this.isGuardOf)
{
var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
var cmpAttack = Engine.QueryInterface(target, IID_Attack);
if (cmpUnitAI && cmpAttack &&
cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type)))
return false;
}
// Stop if we're in hold-ground mode and it's too far from the holding point
if (this.GetStance().respondHoldGround)
{
if (!this.CheckTargetDistanceFromHeldPosition(target, iid, type))
return true;
}
// Stop if it's left our vision range, unless we're especially persistent
if (!this.GetStance().respondChaseBeyondVision)
{
if (!this.CheckTargetIsInVisionRange(target))
return true;
}
// (Note that CCmpUnitMotion will detect if the target is lost in FoW,
// and will continue moving to its last seen position and then stop)
return false;
};
/*
* Returns whether we should chase the targeted entity,
* given our current stance.
*/
UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
{
if (this.IsTurret())
return false;
if (this.GetStance().respondChase)
return true;
// If we are guarding/escorting, chase at least as long as the guarded unit is in target range of the attacker
if (this.isGuardOf)
{
let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
let cmpAttack = Engine.QueryInterface(target, IID_Attack);
if (cmpUnitAI && cmpAttack &&
cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type)))
return true;
}
if (force)
return true;
return false;
};
//// External interface functions ////
UnitAI.prototype.SetFormationController = function(ent)
{
this.formationController = ent;
// Set obstruction group, so we can walk through members
// of our own formation (or ourself if not in formation)
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
{
if (ent == INVALID_ENTITY)
cmpObstruction.SetControlGroup(this.entity);
else
cmpObstruction.SetControlGroup(ent);
}
// If we were removed from a formation, let the FSM switch back to INDIVIDUAL
if (ent == INVALID_ENTITY)
this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
};
UnitAI.prototype.GetFormationController = function()
{
return this.formationController;
};
UnitAI.prototype.GetFormationTemplate = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(this.formationController) || "special/formations/null";
};
UnitAI.prototype.MoveIntoFormation = function(cmd)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
// Add new order to move into formation at the current position
this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true });
};
UnitAI.prototype.GetTargetPositions = function()
{
var targetPositions = [];
for (var i = 0; i < this.orderQueue.length; ++i)
{
var order = this.orderQueue[i];
switch (order.type)
{
case "Walk":
case "WalkAndFight":
case "WalkToPointRange":
case "MoveIntoFormation":
case "GatherNearPosition":
case "Patrol":
targetPositions.push(new Vector2D(order.data.x, order.data.z));
break; // and continue the loop
case "WalkToTarget":
case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
case "Guard":
case "Flee":
case "LeaveFoundation":
case "Attack":
case "Heal":
case "Gather":
case "ReturnResource":
case "Repair":
case "Garrison":
// Find the target unit's position
var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return targetPositions;
targetPositions.push(cmpTargetPosition.GetPosition2D());
return targetPositions;
case "Stop":
return [];
default:
error("GetTargetPositions: Unrecognised order type '"+order.type+"'");
return [];
}
}
return targetPositions;
};
/**
* Returns the estimated distance that this unit will travel before either
* finishing all of its orders, or reaching a non-walk target (attack, gather, etc).
* Intended for Formation to switch to column layout on long walks.
*/
UnitAI.prototype.ComputeWalkingDistance = function()
{
var distance = 0;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return 0;
// Keep track of the position at the start of each order
var pos = cmpPosition.GetPosition2D();
var targetPositions = this.GetTargetPositions();
for (var i = 0; i < targetPositions.length; ++i)
{
distance += pos.distanceTo(targetPositions[i]);
// Remember this as the start position for the next order
pos = targetPositions[i];
}
// Return the total distance to the end of the order queue
return distance;
};
UnitAI.prototype.AddOrder = function(type, data, queued)
{
if (this.expectedRoute)
this.expectedRoute = undefined;
if (queued)
this.PushOrder(type, data);
else
{
// May happen if an order arrives on the same turn the unit is garrisoned
// in that case, just forget the order as this will lead to an infinite loop
if (this.IsGarrisoned() && !this.IsTurret() && type != "Ungarrison")
return;
this.ReplaceOrder(type, data);
}
};
/**
* Adds guard/escort order to the queue, forced by the player.
*/
UnitAI.prototype.Guard = function(target, queued)
{
if (!this.CanGuard())
{
this.WalkToTarget(target, queued);
return;
}
// if we already had an old guard order, do nothing if the target is the same
// and the order is running, otherwise remove the previous order
if (this.isGuardOf)
{
if (this.isGuardOf == target && this.order && this.order.type == "Guard")
return;
else
this.RemoveGuard();
}
this.AddOrder("Guard", { "target": target, "force": false }, queued);
};
UnitAI.prototype.AddGuard = function(target)
{
if (!this.CanGuard())
return false;
var cmpGuard = Engine.QueryInterface(target, IID_Guard);
if (!cmpGuard)
return false;
// Do not allow to guard a unit already guarding
var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.IsGuardOf())
return false;
this.isGuardOf = target;
this.guardRange = cmpGuard.GetRange(this.entity);
cmpGuard.AddGuard(this.entity);
return true;
};
UnitAI.prototype.RemoveGuard = function()
{
if (!this.isGuardOf)
return;
let cmpGuard = Engine.QueryInterface(this.isGuardOf, IID_Guard);
if (cmpGuard)
cmpGuard.RemoveGuard(this.entity);
this.guardRange = undefined;
this.isGuardOf = undefined;
if (!this.order)
return;
if (this.order.type == "Guard")
this.UnitFsm.ProcessMessage(this, { "type": "RemoveGuard" });
else
for (let i = 1; i < this.orderQueue.length; ++i)
if (this.orderQueue[i].type == "Guard")
this.orderQueue.splice(i, 1);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
UnitAI.prototype.IsGuardOf = function()
{
return this.isGuardOf;
};
UnitAI.prototype.SetGuardOf = function(entity)
{
// entity may be undefined
this.isGuardOf = entity;
};
UnitAI.prototype.CanGuard = function()
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Do not let a unit already guarded to guard. This would work in principle,
// but would clutter the gui with too much buttons to take all cases into account
var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard);
if (cmpGuard && cmpGuard.GetEntities().length)
return false;
return this.template.CanGuard == "true";
};
UnitAI.prototype.CanPatrol = function()
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
return this.IsFormationController() || this.template.CanPatrol == "true";
};
/**
* Adds walk order to queue, forced by the player.
*/
UnitAI.prototype.Walk = function(x, z, queued)
{
if (this.expectedRoute && queued)
this.expectedRoute.push({ "x": x, "z": z });
else
this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued);
};
/**
* Adds walk to point range order to queue, forced by the player.
*/
UnitAI.prototype.WalkToPointRange = function(x, z, min, max, queued)
{
this.AddOrder("Walk", { "x": x, "z": z, "min": min, "max": max, "force": true }, queued);
};
/**
* Adds stop order to queue, forced by the player.
*/
UnitAI.prototype.Stop = function(queued)
{
this.AddOrder("Stop", { "force": true }, queued);
};
/**
* Adds walk-to-target order to queue, this only occurs in response
* to a player order, and so is forced.
*/
UnitAI.prototype.WalkToTarget = function(target, queued)
{
this.AddOrder("WalkToTarget", { "target": target, "force": true }, queued);
};
/**
* Adds walk-and-fight order to queue, this only occurs in response
* to a player order, and so is forced.
* If targetClasses is given, only entities matching the targetClasses can be attacked.
*/
UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, allowCapture = true, queued = false)
{
this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued);
};
UnitAI.prototype.Patrol = function(x, z, targetClasses, allowCapture = true, queued = false)
{
if (!this.CanPatrol())
{
this.Walk(x, z, queued);
return;
}
this.AddOrder("Patrol", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued);
};
/**
* Adds leave foundation order to queue, treated as forced.
*/
UnitAI.prototype.LeaveFoundation = function(target)
{
// If we're already being told to leave a foundation, then
// ignore this new request so we don't end up being too indecisive
// to ever actually move anywhere
// Ignore also the request if we are packing
if (this.order && (this.order.type == "LeaveFoundation" || (this.order.type == "Flee" && this.order.data.target == target) || this.IsPacking()))
return;
this.PushOrderFront("LeaveFoundation", { "target": target, "force": true });
};
/**
* Adds attack order to the queue, forced by the player.
*/
UnitAI.prototype.Attack = function(target, allowCapture = true, queued = false)
{
if (!this.CanAttack(target))
{
// We don't want to let healers walk to the target unit so they can be easily killed.
// Instead we just let them get into healing range.
if (this.IsHealer())
this.MoveToTargetRange(target, IID_Heal);
else
this.WalkToTarget(target, queued);
return;
}
let order = {
"target": target,
"force": true,
"allowCapture": allowCapture,
};
this.RememberTargetPosition(order);
this.AddOrder("Attack", order, queued);
};
/**
* Adds garrison order to the queue, forced by the player.
*/
UnitAI.prototype.Garrison = function(target, queued)
{
if (target == this.entity)
return;
if (!this.CanGarrison(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Garrison", { "target": target, "force": true }, queued);
};
/**
* Adds ungarrison order to the queue.
*/
UnitAI.prototype.Ungarrison = function()
{
if (this.IsGarrisoned())
this.AddOrder("Ungarrison", null, false);
};
/**
* Adds a garrison order for units that are already garrisoned in the garrison holder.
*/
UnitAI.prototype.Autogarrison = function(target)
{
this.isGarrisoned = true;
this.PushOrderFront("Garrison", { "target": target });
};
/**
* Adds gather order to the queue, forced by the player
* until the target is reached
*/
UnitAI.prototype.Gather = function(target, queued)
{
this.PerformGather(target, queued, true);
};
/**
* Internal function to abstract the force parameter.
*/
UnitAI.prototype.PerformGather = function(target, queued, force)
{
if (!this.CanGather(target))
{
this.WalkToTarget(target, queued);
return;
}
// Save the resource type now, so if the resource gets destroyed
// before we process the order then we still know what resource
// type to look for more of
var type;
var cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply);
if (cmpResourceSupply)
type = cmpResourceSupply.GetType();
else
error("CanGather allowed gathering from invalid entity");
// Also save the target entity's template, so that if it's an animal,
// we won't go from hunting slow safe animals to dangerous fast ones
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(target);
// Remove "resource|" prefix from template name, if present.
if (template.indexOf("resource|") != -1)
template = template.slice(9);
let order = {
"target": target,
"type": type,
"template": template,
"force": force,
};
this.RememberTargetPosition(order);
order.initPos = order.lastPos;
this.AddOrder("Gather", order, queued);
};
/**
* Adds gather-near-position order to the queue, not forced, so it can be
* interrupted by attacks.
*/
UnitAI.prototype.GatherNearPosition = function(x, z, type, template, queued)
{
// Remove "resource|" prefix from template name, if present.
if (template.indexOf("resource|") != -1)
template = template.slice(9);
if (this.IsFormationController() || Engine.QueryInterface(this.entity, IID_ResourceGatherer))
this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued);
else
this.AddOrder("Walk", { "x": x, "z": z, "force": false }, queued);
};
/**
* Adds heal order to the queue, forced by the player.
*/
UnitAI.prototype.Heal = function(target, queued)
{
if (!this.CanHeal(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Heal", { "target": target, "force": true }, queued);
};
/**
* Adds return resource order to the queue, forced by the player.
*/
UnitAI.prototype.ReturnResource = function(target, queued)
{
if (!this.CanReturnResource(target, true))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("ReturnResource", { "target": target, "force": true }, queued);
};
/**
* Adds trade order to the queue. Either walk to the first market, or
* start a new route. Not forced, so it can be interrupted by attacks.
* The possible route may be given directly as a SetupTradeRoute argument
* if coming from a RallyPoint, or through this.expectedRoute if a user command.
*/
UnitAI.prototype.SetupTradeRoute = function(target, source, route, queued)
{
if (!this.CanTrade(target))
{
this.WalkToTarget(target, queued);
return;
}
// AI has currently no access to BackToWork
let cmpPlayer = QueryOwnerInterface(this.entity);
if (cmpPlayer && cmpPlayer.IsAI() && !this.IsFormationController() &&
this.workOrders.length && this.workOrders[0].type == "Trade")
{
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (cmpTrader.HasBothMarkets() &&
(cmpTrader.GetFirstMarket() == target && cmpTrader.GetSecondMarket() == source ||
cmpTrader.GetFirstMarket() == source && cmpTrader.GetSecondMarket() == target))
{
this.BackToWork();
return;
}
}
var marketsChanged = this.SetTargetMarket(target, source);
if (!marketsChanged)
return;
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (cmpTrader.HasBothMarkets())
{
let data = {
"target": cmpTrader.GetFirstMarket(),
"route": route,
"force": false
};
if (this.expectedRoute)
{
if (!route && this.expectedRoute.length)
data.route = this.expectedRoute.slice();
this.expectedRoute = undefined;
}
if (this.IsFormationController())
{
this.CallMemberFunction("AddOrder", ["Trade", data, queued]);
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (cmpFormation)
cmpFormation.Disband();
}
else
this.AddOrder("Trade", data, queued);
}
else
{
if (this.IsFormationController())
this.CallMemberFunction("WalkToTarget", [cmpTrader.GetFirstMarket(), queued]);
else
this.WalkToTarget(cmpTrader.GetFirstMarket(), queued);
this.expectedRoute = [];
}
};
UnitAI.prototype.SetTargetMarket = function(target, source)
{
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader)
return false;
var marketsChanged = cmpTrader.SetTargetMarket(target, source);
if (this.IsFormationController())
this.CallMemberFunction("SetTargetMarket", [target, source]);
return marketsChanged;
};
UnitAI.prototype.SwitchMarketOrder = function(oldMarket, newMarket)
{
if (this.order && this.order.data && this.order.data.target && this.order.data.target == oldMarket)
this.order.data.target = newMarket;
};
UnitAI.prototype.MoveToMarket = function(targetMarket)
{
if (this.waypoints && this.waypoints.length > 1)
{
let point = this.waypoints.pop();
return this.MoveToPoint(point.x, point.z) || this.MoveToMarket(targetMarket);
}
this.waypoints = undefined;
return this.MoveToTargetRange(targetMarket, IID_Trader);
};
UnitAI.prototype.PerformTradeAndMoveToNextMarket = function(currentMarket)
{
if (!this.CanTrade(currentMarket))
{
this.StopTrading();
return;
}
if (!this.CheckTargetRange(currentMarket, IID_Trader))
{
if (!this.MoveToMarket(currentMarket)) // If the current market is not reached try again
this.StopTrading();
return;
}
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
let nextMarket = cmpTrader.PerformTrade(currentMarket);
let amount = cmpTrader.GetGoods().amount;
if (!nextMarket || !amount || !amount.traderGain)
{
this.StopTrading();
return;
}
this.order.data.target = nextMarket;
if (this.order.data.route && this.order.data.route.length)
{
this.waypoints = this.order.data.route.slice();
if (this.order.data.target == cmpTrader.GetSecondMarket())
this.waypoints.reverse();
this.waypoints.unshift(null); // additionnal dummy point for the market
}
if (this.MoveToMarket(nextMarket)) // We've started walking to the next market
this.SetNextState("APPROACHINGMARKET");
else
this.StopTrading();
};
UnitAI.prototype.MarketRemoved = function(market)
{
if (this.order && this.order.data && this.order.data.target && this.order.data.target == market)
this.UnitFsm.ProcessMessage(this, { "type": "TradingCanceled", "market": market });
};
UnitAI.prototype.StopTrading = function()
{
this.StopMoving();
this.FinishOrder();
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
cmpTrader.StopTrading();
};
/**
* Adds repair/build order to the queue, forced by the player
* until the target is reached
*/
UnitAI.prototype.Repair = function(target, autocontinue, queued)
{
if (!this.CanRepair(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Repair", { "target": target, "autocontinue": autocontinue, "force": true }, queued);
};
/**
* Adds flee order to the queue, not forced, so it can be
* interrupted by attacks.
*/
UnitAI.prototype.Flee = function(target, queued)
{
this.AddOrder("Flee", { "target": target, "force": false }, queued);
};
/**
* Adds cheer order to the queue. Forced so it won't be interrupted by attacks.
*/
UnitAI.prototype.Cheer = function()
{
this.AddOrder("Cheering", { "force": true }, false);
};
UnitAI.prototype.Pack = function(queued)
{
// Check that we can pack
if (this.CanPack())
this.AddOrder("Pack", { "force": true }, queued);
};
UnitAI.prototype.Unpack = function(queued)
{
// Check that we can unpack
if (this.CanUnpack())
this.AddOrder("Unpack", { "force": true }, queued);
};
UnitAI.prototype.CancelPack = function(queued)
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked())
this.AddOrder("CancelPack", { "force": true }, queued);
};
UnitAI.prototype.CancelUnpack = function(queued)
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked())
this.AddOrder("CancelUnpack", { "force": true }, queued);
};
UnitAI.prototype.SetStance = function(stance)
{
if (g_Stances[stance])
{
this.stance = stance;
Engine.PostMessage(this.entity, MT_UnitStanceChanged, { "to": this.stance });
}
else
error("UnitAI: Setting to invalid stance '"+stance+"'");
};
UnitAI.prototype.SwitchToStance = function(stance)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.SetHeldPosition(pos.x, pos.z);
this.SetStance(stance);
// Stop moving if switching to stand ground
// TODO: Also stop existing orders in a sensible way
if (stance == "standground")
this.StopMoving();
// Reset the range queries, since the range depends on stance.
this.SetupRangeQueries();
};
UnitAI.prototype.SetTurretStance = function()
{
this.previousStance = undefined;
if (this.GetStance().respondStandGround)
return;
for (let stance in g_Stances)
{
if (!g_Stances[stance].respondStandGround)
continue;
this.previousStance = this.GetStanceName();
this.SwitchToStance(stance);
return;
}
};
UnitAI.prototype.ResetTurretStance = function()
{
if (!this.previousStance)
return;
this.SwitchToStance(this.previousStance);
this.previousStance = undefined;
};
/**
* Resets losRangeQuery, and if there are some targets in range that we can
* attack then we start attacking and this returns true; otherwise, returns false.
*/
UnitAI.prototype.FindNewTargets = function()
{
if (!this.losRangeQuery)
return false;
if (!this.GetStance().targetVisibleEnemies)
return false;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return this.AttackEntitiesByPreference(cmpRangeManager.ResetActiveQuery(this.losRangeQuery));
};
UnitAI.prototype.FindWalkAndFightTargets = function()
{
if (this.IsFormationController())
{
var cmpUnitAI;
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
for (var ent of cmpFormation.members)
{
if (!(cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI)))
continue;
var targets = cmpUnitAI.GetTargetsFromUnit();
for (var targ of targets)
{
if (!cmpUnitAI.CanAttack(targ))
continue;
if (this.order.data.targetClasses)
{
var cmpIdentity = Engine.QueryInterface(targ, IID_Identity);
var targetClasses = this.order.data.targetClasses;
if (targetClasses.attack && cmpIdentity
&& !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack))
continue;
if (targetClasses.avoid && cmpIdentity
&& MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid))
continue;
// Only used by the AIs to prevent some choices of targets
if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
continue;
}
this.PushOrderFront("Attack", { "target": targ, "force": false, "allowCapture": this.order.data.allowCapture });
return true;
}
}
return false;
}
var targets = this.GetTargetsFromUnit();
for (var targ of targets)
{
if (!this.CanAttack(targ))
continue;
if (this.order.data.targetClasses)
{
var cmpIdentity = Engine.QueryInterface(targ, IID_Identity);
var targetClasses = this.order.data.targetClasses;
if (cmpIdentity && targetClasses.attack
&& !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack))
continue;
if (cmpIdentity && targetClasses.avoid
&& MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid))
continue;
// Only used by the AIs to prevent some choices of targets
if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
continue;
}
this.PushOrderFront("Attack", { "target": targ, "force": false, "allowCapture": this.order.data.allowCapture });
return true;
}
// healers on a walk-and-fight order should heal injured units
if (this.IsHealer())
return this.FindNewHealTargets();
return false;
};
UnitAI.prototype.GetTargetsFromUnit = function()
{
if (!this.losRangeQuery)
return [];
if (!this.GetStance().targetVisibleEnemies)
return [];
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return [];
var attackfilter = function(e) {
var cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() > 0)
return true;
var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
};
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var entities = cmpRangeManager.ResetActiveQuery(this.losRangeQuery);
var targets = entities.filter(function(v) { return cmpAttack.CanAttack(v) && attackfilter(v); })
.sort(function(a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); });
return targets;
};
/**
* Resets losHealRangeQuery, and if there are some targets in range that we can heal
* then we start healing and this returns true; otherwise, returns false.
*/
UnitAI.prototype.FindNewHealTargets = function()
{
if (!this.losHealRangeQuery)
return false;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return this.RespondToHealableEntities(cmpRangeManager.ResetActiveQuery(this.losHealRangeQuery));
};
UnitAI.prototype.GetQueryRange = function(iid)
{
var ret = { "min": 0, "max": 0 };
if (this.GetStance().respondStandGround)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return ret;
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetFullAttackRange();
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
ret.min = range.min;
ret.max = Math.min(range.max, cmpVision.GetRange());
}
else if (this.GetStance().respondChase)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var range = cmpVision.GetRange();
ret.max = range;
}
else if (this.GetStance().respondHoldGround)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return ret;
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetFullAttackRange();
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var vision = cmpVision.GetRange();
ret.max = Math.min(range.max + vision / 2, vision);
}
// We probably have stance 'passive' and we wouldn't have a range,
// but as it is the default for healers we need to set it to something sane.
else if (iid === IID_Heal)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var range = cmpVision.GetRange();
ret.max = range;
}
return ret;
};
UnitAI.prototype.GetStance = function()
{
return g_Stances[this.stance];
};
UnitAI.prototype.GetSelectableStances = function()
{
if (this.IsTurret())
return [];
return Object.keys(g_Stances).filter(key => g_Stances[key].selectable);
};
UnitAI.prototype.GetStanceName = function()
{
return this.stance;
};
/*
* Make the unit walk at its normal pace.
*/
UnitAI.prototype.ResetSpeedMultiplier = function()
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetSpeedMultiplier(1);
};
UnitAI.prototype.SetSpeedMultiplier = function(speed)
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetSpeedMultiplier(speed);
};
/*
* Remember the position of the target (in lastPos), if any, in case it disappears later
* and we want to head to its last known position.
* @param orderData - The order data to set this on. Defaults to this.order.data
*/
UnitAI.prototype.RememberTargetPosition = function(orderData)
{
if (!orderData)
orderData = this.order.data;
let cmpPosition = Engine.QueryInterface(orderData.target, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
orderData.lastPos = cmpPosition.GetPosition();
};
UnitAI.prototype.SetHeldPosition = function(x, z)
{
this.heldPosition = {"x": x, "z": z};
};
UnitAI.prototype.SetHeldPositionOnEntity = function(entity)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.SetHeldPosition(pos.x, pos.z);
};
UnitAI.prototype.GetHeldPosition = function()
{
return this.heldPosition;
};
UnitAI.prototype.WalkToHeldPosition = function()
{
if (this.heldPosition)
{
this.AddOrder("Walk", { "x": this.heldPosition.x, "z": this.heldPosition.z, "force": false }, false);
return true;
}
return false;
};
//// Helper functions ////
UnitAI.prototype.CanAttack = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(target);
};
UnitAI.prototype.CanGarrison = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
// Verify that the target is owned by this entity's player or a mutual ally of this player
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target)))
return false;
// Don't let animals garrison for now
// (If we want to support that, we'll need to change Order.Garrison so it
// doesn't move the animal into an INVIDIDUAL.* state)
if (this.IsAnimal())
return false;
return true;
};
UnitAI.prototype.CanGather = function(target)
{
if (this.IsTurret())
return false;
// The target must be a valid resource supply, or the mirage of one.
var cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply);
if (!cmpResourceSupply)
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Gather commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return false;
// Verify that we can gather from this target
if (!cmpResourceGatherer.GetTargetGatherRate(target))
return false;
// No need to verify ownership as we should be able to gather from
// a target regardless of ownership.
// No need to call "cmpResourceSupply.IsAvailable()" either because that
// would cause units to walk to full entities instead of choosing another one
// nearby to gather from, which is undesirable.
return true;
};
UnitAI.prototype.CanHeal = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Heal commands
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
if (!cmpHeal)
return false;
// Verify that the target is alive
if (!this.TargetIsAlive(target))
return false;
// Verify that the target is owned by the same player as the entity or of an ally
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)))
return false;
// Verify that the target is not unhealable (or at max health)
var cmpHealth = Engine.QueryInterface(target, IID_Health);
if (!cmpHealth || cmpHealth.IsUnhealable())
return false;
// Verify that the target has no unhealable class
var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return false;
if (MatchesClassList(cmpIdentity.GetClassesList(), cmpHeal.GetUnhealableClasses()))
return false;
// Verify that the target is a healable class
if (MatchesClassList(cmpIdentity.GetClassesList(), cmpHeal.GetHealableClasses()))
return true;
return false;
};
UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to ReturnResource commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return false;
// Verify that the target is a dropsite
var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (!cmpResourceDropsite)
return false;
if (checkCarriedResource)
{
// Verify that we are carrying some resources,
// and can return our current resource to this target
var type = cmpResourceGatherer.GetMainCarryingType();
if (!type || !cmpResourceDropsite.AcceptsType(type))
return false;
}
// Verify that the dropsite is owned by this entity's player (or a mutual ally's if allowed)
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && IsOwnedByPlayer(cmpOwnership.GetOwner(), target))
return true;
var cmpPlayer = QueryOwnerInterface(this.entity);
return cmpPlayer && cmpPlayer.HasSharedDropsites() && cmpResourceDropsite.IsShared() &&
cmpOwnership && IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target);
};
UnitAI.prototype.CanTrade = function(target)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Trade commands
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
return cmpTrader && cmpTrader.CanTrade(target);
};
UnitAI.prototype.CanRepair = function(target)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Repair (Builder) commands
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (!cmpBuilder)
return false;
// Verify that the target can be either built or repaired
var cmpFoundation = QueryMiragedInterface(target, IID_Foundation);
var cmpRepairable = Engine.QueryInterface(target, IID_Repairable);
if (!cmpFoundation && !cmpRepairable)
return false;
// Verify that the target is owned by an ally of this entity's player
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
return cmpOwnership && IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target);
};
UnitAI.prototype.CanPack = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return cmpPack && !cmpPack.IsPacking() && !cmpPack.IsPacked();
};
UnitAI.prototype.CanUnpack = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return cmpPack && !cmpPack.IsPacking() && cmpPack.IsPacked();
};
UnitAI.prototype.IsPacking = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return cmpPack && cmpPack.IsPacking();
};
//// Formation specific functions ////
UnitAI.prototype.IsAttackingAsFormation = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttackAsFormation()
&& this.GetCurrentState() == "FORMATIONCONTROLLER.COMBAT.ATTACKING";
};
//// Animal specific functions ////
UnitAI.prototype.MoveRandomly = function(distance)
{
// To minimize drift all across the map, animals describe circles
// approximated by polygons.
// And to avoid getting stuck in obstacles or narrow spaces, each side
// of the polygon is obtained by trying to go away from a point situated
// half a meter backwards of the current position, after rotation.
// We also add a fluctuation on the length of each side of the polygon (dist)
// which, in addition to making the move more random, helps escaping narrow spaces
// with bigger values of dist.
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!cmpPosition || !cmpPosition.IsInWorld() || !cmpUnitMotion)
return;
let pos = cmpPosition.GetPosition();
let ang = cmpPosition.GetRotation().y;
if (!this.roamAngle)
{
this.roamAngle = (randBool() ? 1 : -1) * Math.PI / 6;
ang -= this.roamAngle / 2;
this.startAngle = ang;
}
else if (Math.abs((ang - this.startAngle + Math.PI) % (2 * Math.PI) - Math.PI) < Math.abs(this.roamAngle / 2))
this.roamAngle *= randBool() ? 1 : -1;
let halfDelta = randFloat(this.roamAngle / 4, this.roamAngle * 3 / 4);
// First half rotation to decrease the impression of immediate rotation
ang += halfDelta;
cmpUnitMotion.FaceTowardsPoint(pos.x + 0.5 * Math.sin(ang), pos.z + 0.5 * Math.cos(ang));
// Then second half of the rotation
ang += halfDelta;
let dist = randFloat(0.5, 1.5) * distance;
cmpUnitMotion.MoveToPointRange(pos.x - 0.5 * Math.sin(ang), pos.z - 0.5 * Math.cos(ang), dist, -1);
};
UnitAI.prototype.SetFacePointAfterMove = function(val)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpMotion)
cmpMotion.SetFacePointAfterMove(val);
};
UnitAI.prototype.AttackEntitiesByPreference = function(ents)
{
if (!ents.length)
return false;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return false;
var attackfilter = function(e) {
var cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() > 0)
return true;
var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
};
let entsByPreferences = {};
let preferences = [];
let entsWithoutPref = [];
for (let ent of ents)
{
if (!attackfilter(ent))
continue;
let pref = cmpAttack.GetPreference(ent);
if (pref === null || pref === undefined)
entsWithoutPref.push(ent);
else if (!entsByPreferences[pref])
{
preferences.push(pref);
entsByPreferences[pref] = [ent];
}
else
entsByPreferences[pref].push(ent);
}
if (preferences.length)
{
preferences.sort((a, b) => a - b);
for (let pref of preferences)
if (this.RespondToTargetedEntities(entsByPreferences[pref]))
return true;
}
return this.RespondToTargetedEntities(entsWithoutPref);
};
/**
* Call obj.funcname(args) on UnitAI components of all formation members.
*/
UnitAI.prototype.CallMemberFunction = function(funcname, args)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
cmpFormation.GetMembers().forEach(ent => {
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI[funcname].apply(cmpUnitAI, args);
});
};
/**
* Call obj.functname(args) on UnitAI components of all formation members,
* and return true if all calls return true.
*/
UnitAI.prototype.TestAllMemberFunction = function(funcname, args)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return false;
return cmpFormation.GetMembers().every(ent => {
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI[funcname].apply(cmpUnitAI, args);
});
};
UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DelayedDamage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DelayedDamage.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DelayedDamage.js (revision 22754)
@@ -0,0 +1 @@
+Engine.RegisterInterface("DelayedDamage");
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/DelayedDamage.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Resistance.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Resistance.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Resistance.js (revision 22754)
@@ -0,0 +1,6 @@
+Engine.RegisterInterface("Resistance");
+
+/**
+ * Message of the form { "entity": entity, "invulnerability": true/false }
+ */
+Engine.RegisterMessageType("InvulnerabilityChanged");
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Resistance.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 22754)
@@ -1,305 +1,323 @@
Engine.LoadHelperScript("DamageBonus.js");
-Engine.LoadHelperScript("DamageTypes.js");
+Engine.LoadHelperScript("Attacking.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/AuraManager.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/Formation.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("Attack.js");
let entityID = 903;
function attackComponentTest(defenderClass, isEnemy, test_function)
{
ResetState();
{
let playerEnt1 = 5;
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": () => playerEnt1
});
AddMock(playerEnt1, IID_Player, {
"GetPlayerID": () => 1,
"IsEnemy": () => isEnemy
});
}
let attacker = entityID;
AddMock(attacker, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 5,
"GetPosition2D": () => new Vector2D(1, 2)
});
AddMock(attacker, IID_Ownership, {
"GetOwner": () => 1
});
let cmpAttack = ConstructComponent(attacker, "Attack", {
"Melee": {
"Damage": {
"Hack": 11,
"Pierce": 5,
"Crush": 0
},
"MinRange": 3,
"MaxRange": 5,
"PreferredClasses": {
"_string": "FemaleCitizen"
},
"RestrictedClasses": {
"_string": "Elephant Archer"
},
"Bonuses":
{
"BonusCav": {
"Classes": "Cavalry",
"Multiplier": 2
}
}
},
"Ranged": {
"Damage": {
"Hack": 0,
"Pierce": 10,
"Crush": 0
},
"MinRange": 10,
"MaxRange": 80,
"PrepareTime": 300,
"RepeatTime": 500,
"Projectile": {
"Speed": 10,
"Spread": 2,
"Gravity": 1
},
"PreferredClasses": {
"_string": "Archer"
},
"RestrictedClasses": {
"_string": "Elephant"
},
"Splash": {
"Shape": "Circular",
"Range": 10,
"FriendlyFire": "false",
"Damage": {
"Hack": 0.0,
"Pierce": 15.0,
"Crush": 35.0
},
"Bonuses": {
"BonusCav": {
"Classes": "Cavalry",
"Multiplier": 3
}
}
}
},
"Capture": {
- "Value": 8,
+ "Capture": 8,
"MaxRange": 10,
},
"Slaughter": {}
});
let defender = ++entityID;
AddMock(defender, IID_Identity, {
"GetClassesList": () => [defenderClass],
"HasClass": className => className == defenderClass
});
AddMock(defender, IID_Ownership, {
"GetOwner": () => 1
});
AddMock(defender, IID_Position, {
"IsInWorld": () => true,
"GetHeightOffset": () => 0
});
AddMock(defender, IID_Health, {
"GetHitpoints": () => 100
});
test_function(attacker, cmpAttack, defender);
}
// Validate template getter functions
attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => {
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(), ["Melee", "Ranged", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes([]), ["Melee", "Ranged", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged", "Capture"]), ["Melee", "Ranged", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged"]), ["Melee", "Ranged"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture"]), ["Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "!Melee"]), []);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee"]), ["Ranged", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee", "!Ranged"]), ["Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "!Ranged"]), ["Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "Melee", "!Ranged"]), ["Melee", "Capture"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["FemaleCitizen"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80 });
- TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackStrengths("Capture"), { "value": 8 });
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Capture"), { "Capture": 8 });
- TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackStrengths("Ranged"), {
- "Hack": 0,
- "Pierce": 10,
- "Crush": 0
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), {
+ "Damage": {
+ "Hack": 0,
+ "Pierce": 10,
+ "Crush": 0
+ }
});
- TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackStrengths("Ranged.Splash"), {
- "Hack": 0.0,
- "Pierce": 15.0,
- "Crush": 35.0
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged", true), {
+ "Damage": {
+ "Hack": 0.0,
+ "Pierce": 15.0,
+ "Crush": 35.0
+ },
+ "Bonuses": {
+ "BonusCav": {
+ "Classes": "Cavalry",
+ "Multiplier": 3
+ }
+ }
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Ranged"), {
"prepare": 300,
"repeat": 500
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Capture"), {
"prepare": 0,
"repeat": 1000
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetSplashDamage("Ranged"), {
- "damage": {
- "Hack": 0,
- "Pierce": 15,
- "Crush": 35,
+ "attackData": {
+ "Damage": {
+ "Hack": 0,
+ "Pierce": 15,
+ "Crush": 35,
+ },
+ "Bonuses": {
+ "BonusCav": {
+ "Classes": "Cavalry",
+ "Multiplier": 3
+ }
+ }
},
"friendlyFire": false,
"shape": "Circular"
});
});
for (let className of ["Infantry", "Cavalry"])
attackComponentTest(className, true, (attacker, cmpAttack, defender) => {
- TS_ASSERT_EQUALS(cmpAttack.GetBonusTemplate("Melee").BonusCav.Multiplier, 2);
+ TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Melee").Bonuses.BonusCav.Multiplier, 2);
- TS_ASSERT(cmpAttack.GetBonusTemplate("Capture") === null);
+ TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Capture").Bonuses || null, null);
- let getAttackBonus = (s, t, e) => GetDamageBonus(s, e, t, cmpAttack.GetBonusTemplate(t));
+ let getAttackBonus = (s, t, e, splash) => GetAttackBonus(s, e, t, cmpAttack.GetAttackEffectsData(t, splash).Bonuses || null);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Melee", defender), className == "Cavalry" ? 2 : 1);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender), 1);
- TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged.Splash", defender), className == "Cavalry" ? 3 : 1);
+ TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender, true), className == "Cavalry" ? 3 : 1);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Capture", defender), 1);
TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Slaughter", defender), 1);
});
// CanAttack rejects elephant attack due to RestrictedClasses
attackComponentTest("Elephant", true, (attacker, cmpAttack, defender) => {
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), false);
});
function testGetBestAttackAgainst(defenderClass, bestAttack, bestAllyAttack, isBuilding = false)
{
attackComponentTest(defenderClass, true, (attacker, cmpAttack, defender) => {
if (isBuilding)
AddMock(defender, IID_Capturable, {
"CanCapture": playerID => {
TS_ASSERT_EQUALS(playerID, 1);
return true;
}
});
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), defenderClass != "Archer");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), true);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false);
let allowCapturing = [true];
if (!isBuilding)
allowCapturing.push(false);
for (let ac of allowCapturing)
TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack);
});
attackComponentTest(defenderClass, false, (attacker, cmpAttack, defender) => {
if (isBuilding)
AddMock(defender, IID_Capturable, {
"CanCapture": playerID => {
TS_ASSERT_EQUALS(playerID, 1);
return true;
}
});
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), false);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), isBuilding);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), isBuilding);
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic");
TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false);
let allowCapturing = [true];
if (!isBuilding)
allowCapturing.push(false);
for (let ac of allowCapturing)
TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAllyAttack);
});
}
testGetBestAttackAgainst("FemaleCitizen", "Melee", undefined);
testGetBestAttackAgainst("Archer", "Ranged", undefined);
testGetBestAttackAgainst("Domestic", "Slaughter", "Slaughter");
testGetBestAttackAgainst("Structure", "Capture", "Capture", true);
testGetBestAttackAgainst("Structure", "Ranged", undefined, false);
function testPredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity)
{
ResetState();
let cmpAttack = ConstructComponent(1, "Attack", {});
let timeToTarget = cmpAttack.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity);
if (timeToTarget === false)
return;
// Position of the target after that time.
let targetPos = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition);
// Time that the projectile need to reach it.
let time = targetPos.horizDistanceTo(selfPosition) / horizSpeed;
TS_ASSERT_EQUALS(timeToTarget.toFixed(1), time.toFixed(1));
}
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(0, 0, 0), new Vector3D(0, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(16, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-16, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 1));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 4));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 16));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 1));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(2, 0, 2));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(8, 0, 8));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 1));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-2, 0, 2));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-8, 0, 8));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 2));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 2));
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 22754)
@@ -1,551 +1,548 @@
Engine.LoadHelperScript("DamageBonus.js");
-Engine.LoadHelperScript("DamageTypes.js");
+Engine.LoadHelperScript("Attacking.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Sound.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/AttackDetection.js");
Engine.LoadComponentScript("interfaces/AuraManager.js");
-Engine.LoadComponentScript("interfaces/Damage.js");
-Engine.LoadComponentScript("interfaces/DamageReceiver.js");
+Engine.LoadComponentScript("interfaces/DelayedDamage.js");
+Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Player.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("Attack.js");
-Engine.LoadComponentScript("Damage.js");
+Engine.LoadComponentScript("DelayedDamage.js");
Engine.LoadComponentScript("Timer.js");
function Test_Generic()
{
ResetState();
- let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage");
let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer");
- cmpTimer.OnUpdate({ turnLength: 1 });
+ cmpTimer.OnUpdate({ "turnLength": 1 });
let attacker = 11;
let atkPlayerEntity = 1;
let attackerOwner = 6;
let cmpAttack = ConstructComponent(attacker, "Attack",
{
"Ranged": {
+ "Damage": {
+ "Crush": 5,
+ },
"MaxRange": 50,
"MinRange": 0,
"Delay": 0,
"Projectile": {
"Speed": 75.0,
"Spread": 0.5,
"Gravity": 9.81,
"LaunchPoint": { "@y": 3 }
}
}
});
let damage = 5;
let target = 21;
let targetOwner = 7;
let targetPos = new Vector3D(3, 0, 3);
let type = "Melee";
let damageTaken = false;
- cmpAttack.GetAttackStrengths = attackType => ({ "hack": 0, "pierce": 0, "crush": damage });
+ cmpAttack.GetAttackStrengths = attackType => ({ "Hack": 0, "Pierce": 0, "Crush": damage });
let data = {
- "attacker": attacker,
- "target": target,
"type": "Melee",
- "strengths": { "hack": 0, "pierce": 0, "crush": damage },
- "multiplier": 1.0,
+ "attackData": {
+ "Damage": { "Hack": 0, "Pierce": 0, "Crush": damage },
+ },
+ "target": target,
+ "attacker": attacker,
"attackerOwner": attackerOwner,
"position": targetPos,
- "isSplash": false,
"projectileId": 9,
- "direction": new Vector3D(1,0,0)
+ "direction": new Vector3D(1, 0, 0)
};
AddMock(atkPlayerEntity, IID_Player, {
"GetEnemies": () => [targetOwner]
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => atkPlayerEntity,
"GetAllPlayers": () => [0, 1, 2, 3, 4]
});
AddMock(SYSTEM_ENTITY, IID_ProjectileManager, {
"RemoveProjectile": () => {},
"LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {},
});
AddMock(target, IID_Position, {
"GetPosition": () => targetPos,
"GetPreviousPosition": () => targetPos,
"GetPosition2D": () => Vector2D.From(targetPos),
"IsInWorld": () => true,
});
- AddMock(target, IID_Health, {});
+ AddMock(target, IID_Health, {
+ "TakeDamage": (effectData, __, ___, bonusMultiplier) => {
+ damageTaken = true;
+ return { "killed": false, "HPchange": -bonusMultiplier * effectData.Crush };
+ },
+ });
- AddMock(target, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => { damageTaken = true; return { "killed": false, "change": -multiplier * strengths.crush }; },
+ AddMock(SYSTEM_ENTITY, IID_DelayedDamage, {
+ "MissileHit": () => {
+ damageTaken = true;
+ },
});
Engine.PostMessage = function(ent, iid, message)
{
- TS_ASSERT_UNEVAL_EQUALS({ "attacker": attacker, "target": target, "type": type, "damage": damage, "attackerOwner": attackerOwner }, message);
+ TS_ASSERT_UNEVAL_EQUALS({ "type": type, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "damage": damage, "capture": 0, "statusEffects": [] }, message);
};
AddMock(target, IID_Footprint, {
"GetShape": () => ({ "type": "circle", "radius": 20 }),
});
AddMock(attacker, IID_Ownership, {
"GetOwner": () => attackerOwner,
});
AddMock(attacker, IID_Position, {
"GetPosition": () => new Vector3D(2, 0, 3),
"GetRotation": () => new Vector3D(1, 2, 3),
"IsInWorld": () => true,
});
function TestDamage()
{
- cmpTimer.OnUpdate({ turnLength: 1 });
+ cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT(damageTaken);
damageTaken = false;
}
- cmpDamage.CauseDamage(data);
+ Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner);
TestDamage();
- type = data.type = "Ranged";
- cmpDamage.CauseDamage(data);
+ data.type = "Ranged";
+ type = data.type;
+ Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner);
TestDamage();
// Check for damage still being dealt if the attacker dies
cmpAttack.PerformAttack("Ranged", target);
Engine.DestroyEntity(attacker);
TestDamage();
atkPlayerEntity = 1;
AddMock(atkPlayerEntity, IID_Player, {
"GetEnemies": () => [2, 3]
});
- TS_ASSERT_UNEVAL_EQUALS(cmpDamage.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]);
- TS_ASSERT_UNEVAL_EQUALS(cmpDamage.GetPlayersToDamage(atkPlayerEntity, false), [2, 3]);
+ TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]);
+ TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, false), [2, 3]);
}
Test_Generic();
function TestLinearSplashDamage()
{
ResetState();
Engine.PostMessage = (ent, iid, message) => {};
const attacker = 50;
const attackerOwner = 1;
const origin = new Vector2D(0, 0);
let data = {
+ "type": "Ranged",
+ "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } },
"attacker": attacker,
+ "attackerOwner": attackerOwner,
"origin": origin,
"radius": 10,
"shape": "Linear",
- "strengths": { "hack" : 100, "pierce" : 0, "crush": 0 },
"direction": new Vector3D(1, 747, 0),
"playersToDamage": [2],
- "type": "Ranged",
- "attackerOwner": attackerOwner
};
- let fallOff = function(x,y)
+ let fallOff = function(x, y)
{
return (1 - x * x / (data.radius * data.radius)) * (1 - 25 * y * y / (data.radius * data.radius));
};
let hitEnts = new Set();
- let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage");
-
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [60, 61, 62],
});
AddMock(60, IID_Position, {
"GetPosition2D": () => new Vector2D(2.2, -0.4),
});
AddMock(61, IID_Position, {
"GetPosition2D": () => new Vector2D(0, 0),
});
AddMock(62, IID_Position, {
"GetPosition2D": () => new Vector2D(5, 2),
});
- AddMock(60, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
+ AddMock(60, IID_Health, {
+ "TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(60);
- TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(2.2, -0.4));
- return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
+ TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(2.2, -0.4));
+ return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
- AddMock(61, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
+ AddMock(61, IID_Health, {
+ "TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(61);
- TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(0, 0));
- return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
+ TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(0, 0));
+ return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
- AddMock(62, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
+ AddMock(62, IID_Health, {
+ "TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(62);
- TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 0);
- return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
+ TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 0);
+ return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
- cmpDamage.CauseDamageOverArea(data);
+ Attacking.CauseDamageOverArea(data);
TS_ASSERT(hitEnts.has(60));
TS_ASSERT(hitEnts.has(61));
TS_ASSERT(hitEnts.has(62));
hitEnts.clear();
data.direction = new Vector3D(0.6, 747, 0.8);
- AddMock(60, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
+ AddMock(60, IID_Health, {
+ "TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(60);
- TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(1, 2));
- return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
+ TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(1, 2));
+ return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
- cmpDamage.CauseDamageOverArea(data);
+ Attacking.CauseDamageOverArea(data);
TS_ASSERT(hitEnts.has(60));
TS_ASSERT(hitEnts.has(61));
TS_ASSERT(hitEnts.has(62));
hitEnts.clear();
}
TestLinearSplashDamage();
function TestCircularSplashDamage()
{
ResetState();
Engine.PostMessage = (ent, iid, message) => {};
const radius = 10;
let fallOff = function(r)
{
return 1 - r * r / (radius * radius);
};
- let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage");
-
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [60, 61, 62, 64],
});
AddMock(60, IID_Position, {
"GetPosition2D": () => new Vector2D(3, 4),
});
AddMock(61, IID_Position, {
"GetPosition2D": () => new Vector2D(0, 0),
});
AddMock(62, IID_Position, {
"GetPosition2D": () => new Vector2D(3.6, 3.2),
});
AddMock(63, IID_Position, {
"GetPosition2D": () => new Vector2D(10, -10),
});
// Target on the frontier of the shape
AddMock(64, IID_Position, {
"GetPosition2D": () => new Vector2D(9, -4),
});
- AddMock(60, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
- TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(0));
- return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
+ AddMock(60, IID_Resistance, {
+ "TakeDamage": (effectData, __, ___, mult) => {
+ TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(0));
+ return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
- AddMock(61, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
- TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(5));
- return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
+ AddMock(61, IID_Resistance, {
+ "TakeDamage": (effectData, __, ___, mult) => {
+ TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(5));
+ return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
- AddMock(62, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
- TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(1));
- return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
+ AddMock(62, IID_Resistance, {
+ "TakeDamage": (effectData, __, ___, mult) => {
+ TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(1));
+ return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
- AddMock(63, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
+ AddMock(63, IID_Resistance, {
+ "TakeDamage": (effectData, __, ___, mult) => {
TS_ASSERT(false);
}
});
- AddMock(64, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
- TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 0);
- return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
+ AddMock(64, IID_Resistance, {
+ "TakeDamage": (effectData, __, ___, mult) => {
+ TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 0);
+ return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
- cmpDamage.CauseDamageOverArea({
+ Attacking.CauseDamageOverArea({
+ "type": "Ranged",
+ "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } },
"attacker": 50,
+ "attackerOwner": 1,
"origin": new Vector2D(3, 4),
"radius": radius,
"shape": "Circular",
- "strengths": { "hack" : 100, "pierce" : 0, "crush": 0 },
"playersToDamage": [2],
- "type": "Ranged",
- "attackerOwner": 1
});
}
TestCircularSplashDamage();
function Test_MissileHit()
{
ResetState();
Engine.PostMessage = (ent, iid, message) => {};
- let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage");
+ let cmpDelayedDamage = ConstructComponent(SYSTEM_ENTITY, "DelayedDamage");
let target = 60;
let targetOwner = 1;
let targetPos = new Vector3D(3, 10, 0);
let hitEnts = new Set();
AddMock(SYSTEM_ENTITY, IID_Timer, {
"GetLatestTurnLength": () => 500
});
const radius = 10;
let data = {
"type": "Ranged",
- "attacker": 70,
+ "attackData": { "Damage": { "Hack": 0, "Pierce": 100, "Crush": 0 } },
"target": 60,
- "strengths": { "hack": 0, "pierce": 100, "crush": 0 },
+ "attacker": 70,
+ "attackerOwner": 1,
"position": targetPos,
"direction": new Vector3D(1, 0, 0),
"projectileId": 9,
- "bonus": undefined,
- "isSplash": false,
- "attackerOwner": 1
};
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => id == 1 ? 10 : 11,
"GetAllPlayers": () => [0, 1]
});
AddMock(SYSTEM_ENTITY, IID_ProjectileManager, {
"RemoveProjectile": () => {},
"LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {},
});
AddMock(60, IID_Position, {
"GetPosition": () => targetPos,
"GetPreviousPosition": () => targetPos,
"GetPosition2D": () => Vector2D.From(targetPos),
"IsInWorld": () => true,
});
- AddMock(60, IID_Health, {});
-
- AddMock(60, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
+ AddMock(60, IID_Health, {
+ "TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(60);
- TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100);
- return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
+ TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100);
+ return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
AddMock(60, IID_Footprint, {
"GetShape": () => ({ "type": "circle", "radius": 20 }),
});
AddMock(70, IID_Ownership, {
"GetOwner": () => 1,
});
AddMock(70, IID_Position, {
"GetPosition": () => new Vector3D(0, 0, 0),
"GetRotation": () => new Vector3D(0, 0, 0),
"IsInWorld": () => true,
});
AddMock(10, IID_Player, {
"GetEnemies": () => [2]
});
- cmpDamage.MissileHit(data, 0);
+ cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(60));
hitEnts.clear();
// The main target is not hit but another one is hit.
AddMock(60, IID_Position, {
"GetPosition": () => new Vector3D(900, 10, 0),
"GetPreviousPosition": () => new Vector3D(900, 10, 0),
"GetPosition2D": () => new Vector2D(900, 0),
"IsInWorld": () => true,
});
- AddMock(60, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
+ AddMock(60, IID_Health, {
+ "TakeDamage": (effectData, __, ___, mult) => {
TS_ASSERT_EQUALS(false);
- return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
+ return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [61]
});
AddMock(61, IID_Position, {
"GetPosition": () => targetPos,
"GetPreviousPosition": () => targetPos,
"GetPosition2D": () => Vector2D.from3D(targetPos),
"IsInWorld": () => true,
});
- AddMock(61, IID_Health, {});
-
- AddMock(61, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
- TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100);
+ AddMock(61, IID_Health, {
+ "TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(61);
- return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
+ TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100);
+ return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
AddMock(61, IID_Footprint, {
"GetShape": () => ({ "type": "circle", "radius": 20 }),
});
- cmpDamage.MissileHit(data, 0);
+ cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
hitEnts.clear();
// Add a splash damage.
-
- data.friendlyFire = false;
- data.radius = 10;
- data.shape = "Circular";
- data.isSplash = true;
- data.splashStrengths = { "hack": 0, "pierce": 0, "crush": 200 };
+ data.splash = {};
+ data.splash.friendlyFire = false;
+ data.splash.radius = 10;
+ data.splash.shape = "Circular";
+ data.splash.attackData = { "Damage": { "Hack": 0, "Pierce": 0, "Crush": 200 } };
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [61, 62]
});
let dealtDamage = 0;
- AddMock(61, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
- dealtDamage += multiplier * (strengths.hack + strengths.pierce + strengths.crush);
+ AddMock(61, IID_Health, {
+ "TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(61);
- return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
+ dealtDamage += mult * (effectData.Hack + effectData.Pierce + effectData.Crush);
+ return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
AddMock(62, IID_Position, {
"GetPosition": () => new Vector3D(8, 10, 0),
"GetPreviousPosition": () => new Vector3D(8, 10, 0),
"GetPosition2D": () => new Vector2D(8, 0),
"IsInWorld": () => true,
});
- AddMock(62, IID_Health, {});
-
- AddMock(62, IID_DamageReceiver, {
- "TakeDamage": (strengths, multiplier) => {
- TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 200 * 0.75);
+ AddMock(62, IID_Health, {
+ "TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(62);
- return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
+ TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 200 * 0.75);
+ return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
AddMock(62, IID_Footprint, {
"GetShape": () => ({ "type": "circle", "radius": 20 }),
});
- cmpDamage.MissileHit(data, 0);
+ cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 100 + 200);
dealtDamage = 0;
TS_ASSERT(hitEnts.has(62));
hitEnts.clear();
// Add some hard counters bonus.
Engine.DestroyEntity(62);
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [61]
});
let bonus= { "BonusCav": { "Classes": "Cavalry", "Multiplier": 400 } };
let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 10000 } };
AddMock(61, IID_Identity, {
"HasClass": cl => cl == "Cavalry"
});
- data.bonus = bonus;
- cmpDamage.MissileHit(data, 0);
+ data.attackData.Bonuses = bonus;
+ cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 200);
dealtDamage = 0;
hitEnts.clear();
- data.splashBonus = splashBonus;
- cmpDamage.MissileHit(data, 0);
+ data.splash.attackData.Bonuses = splashBonus;
+ cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 10000 * 200);
dealtDamage = 0;
hitEnts.clear();
- data.bonus = undefined;
- cmpDamage.MissileHit(data, 0);
+ data.attackData.Bonuses = undefined;
+ cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200);
dealtDamage = 0;
hitEnts.clear();
- data.bonus = null;
- cmpDamage.MissileHit(data, 0);
+ data.attackData.Bonuses = null;
+ cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200);
dealtDamage = 0;
hitEnts.clear();
- data.bonus = {};
- cmpDamage.MissileHit(data, 0);
+ data.attackData.Bonuses = {};
+ cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200);
dealtDamage = 0;
hitEnts.clear();
}
Test_MissileHit();
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js (revision 22754)
@@ -1,77 +1,75 @@
Engine.LoadHelperScript("DamageBonus.js");
-Engine.LoadHelperScript("DamageTypes.js");
+Engine.LoadHelperScript("Attacking.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/AuraManager.js");
-Engine.LoadComponentScript("interfaces/Damage.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("DeathDamage.js");
let deadEnt = 60;
let player = 1;
ApplyValueModificationsToEntity = function(value, stat, ent)
{
if (value == "DeathDamage/Damage/Pierce" && ent == deadEnt)
return stat + 200;
return stat;
};
let template = {
"Shape": "Circular",
"Range": 10.7,
"FriendlyFire": "false",
"Damage": {
"Hack": 0.0,
"Pierce": 15.0,
"Crush": 35.0
}
};
-let modifiedDamage = {
- "Hack": 0.0,
- "Pierce": 215.0,
- "Crush": 35.0
+let effects = {
+ "Damage": {
+ "Hack": 0.0,
+ "Pierce": 215.0,
+ "Crush": 35.0
+ }
};
let cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template);
let playersToDamage = [2, 3, 7];
let pos = new Vector2D(3, 4.2);
let result = {
+ "type": "Death",
+ "attackData": effects,
"attacker": deadEnt,
+ "attackerOwner": player,
"origin": pos,
"radius": template.Range,
"shape": template.Shape,
- "strengths": modifiedDamage,
- "splashBonus": null,
- "playersToDamage": playersToDamage,
- "type": "Death",
- "attackerOwner": player
+ "playersToDamage": playersToDamage
};
-AddMock(SYSTEM_ENTITY, IID_Damage, {
- "CauseDamageOverArea": data => TS_ASSERT_UNEVAL_EQUALS(data, result),
- "GetPlayersToDamage": (owner, friendlyFire) => playersToDamage
-});
+Attacking.CauseDamageOverArea = data => TS_ASSERT_UNEVAL_EQUALS(data, result);
+Attacking.GetPlayersToDamage = () => playersToDamage;
AddMock(deadEnt, IID_Position, {
"GetPosition2D": () => pos,
"IsInWorld": () => true
});
AddMock(deadEnt, IID_Ownership, {
"GetOwner": () => player
});
-TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageStrengths(), modifiedDamage);
+TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageEffects(), effects);
cmpDeathDamage.CauseDeathDamage();
// Test splash damage bonus
-let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } };
-template.Bonuses = splashBonus;
+effects.Bonuses = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } };
+template.Bonuses = effects.Bonuses;
cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template);
-result.splashBonus = splashBonus;
-TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageStrengths(), modifiedDamage);
+result.attackData.Bonuses = effects.Bonuses;
+TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageEffects(), effects);
cmpDeathDamage.CauseDeathDamage();
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 22754)
@@ -1,590 +1,590 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/AlertRaiser.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Barter.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/CeasefireManager.js");
-Engine.LoadComponentScript("interfaces/DamageReceiver.js");
+Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/EndGameManager.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Gate.js");
Engine.LoadComponentScript("interfaces/Guard.js");
Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Market.js");
Engine.LoadComponentScript("interfaces/Pack.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/Repairable.js");
Engine.LoadComponentScript("interfaces/ResourceDropsite.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/ResourceTrickle.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Trader.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("GuiInterface.js");
Resources = {
"GetCodes": () => ["food", "metal", "stone", "wood"],
"GetNames": () => ({
"food": "Food",
"metal": "Metal",
"stone": "Stone",
"wood": "Wood"
}),
"GetResource": resource => ({
"aiAnalysisInfluenceGroup":
resource == "food" ? "ignore" :
resource == "wood" ? "abundant" : "sparse"
})
};
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
AddMock(SYSTEM_ENTITY, IID_Barter, {
GetPrices: function() {
return {
"buy": { "food": 150 },
"sell": { "food": 25 }
};
},
PlayerHasMarket: function () { return false; }
});
AddMock(SYSTEM_ENTITY, IID_EndGameManager, {
GetVictoryConditions: () => ["conquest", "wonder"],
GetAlliedVictory: function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
GetNumPlayers: function() { return 2; },
GetPlayerByID: function(id) { TS_ASSERT(id === 0 || id === 1); return 100+id; }
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
GetLosVisibility: function(ent, player) { return "visible"; },
GetLosCircular: function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
GetCurrentTemplateName: function(ent) { return "example"; },
GetTemplate: function(name) { return ""; }
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
GetTime: function() { return 0; },
SetTimeout: function(ent, iid, funcname, time, data) { return 0; }
});
AddMock(100, IID_Player, {
GetName: function() { return "Player 1"; },
GetCiv: function() { return "gaia"; },
GetColor: function() { return { r: 1, g: 1, b: 1, a: 1}; },
CanControlAllUnits: function() { return false; },
GetPopulationCount: function() { return 10; },
GetPopulationLimit: function() { return 20; },
GetMaxPopulation: function() { return 200; },
GetResourceCounts: function() { return { food: 100 }; },
GetPanelEntities: function() { return []; },
IsTrainingBlocked: function() { return false; },
GetState: function() { return "active"; },
GetTeam: function() { return -1; },
GetLockTeams: function() { return false; },
GetCheatsEnabled: function() { return false; },
GetDiplomacy: function() { return [-1, 1]; },
IsAlly: function() { return false; },
IsMutualAlly: function() { return false; },
IsNeutral: function() { return false; },
IsEnemy: function() { return true; },
GetDisabledTemplates: function() { return {}; },
GetDisabledTechnologies: function() { return {}; },
GetSpyCostMultiplier: function() { return 1; },
HasSharedDropsites: function() { return false; },
HasSharedLos: function() { return false; }
});
AddMock(100, IID_EntityLimits, {
GetLimits: function() { return {"Foo": 10}; },
GetCounts: function() { return {"Foo": 5}; },
GetLimitChangers: function() {return {"Foo": {}}; }
});
AddMock(100, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(100, IID_StatisticsTracker, {
GetBasicStatistics: function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
GetSequences: function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
IncreaseTrainedUnitsCounter: function() { return 1; },
IncreaseConstructedBuildingsCounter: function() { return 1; },
IncreaseBuiltCivCentresCounter: function() { return 1; }
});
AddMock(101, IID_Player, {
GetName: function() { return "Player 2"; },
GetCiv: function() { return "mace"; },
GetColor: function() { return { r: 1, g: 0, b: 0, a: 1}; },
CanControlAllUnits: function() { return true; },
GetPopulationCount: function() { return 40; },
GetPopulationLimit: function() { return 30; },
GetMaxPopulation: function() { return 300; },
GetResourceCounts: function() { return { food: 200 }; },
GetPanelEntities: function() { return []; },
IsTrainingBlocked: function() { return false; },
GetState: function() { return "active"; },
GetTeam: function() { return -1; },
GetLockTeams: function() {return false; },
GetCheatsEnabled: function() { return false; },
GetDiplomacy: function() { return [-1, 1]; },
IsAlly: function() { return true; },
IsMutualAlly: function() {return false; },
IsNeutral: function() { return false; },
IsEnemy: function() { return false; },
GetDisabledTemplates: function() { return {}; },
GetDisabledTechnologies: function() { return {}; },
GetSpyCostMultiplier: function() { return 1; },
HasSharedDropsites: function() { return false; },
HasSharedLos: function() { return false; }
});
AddMock(101, IID_EntityLimits, {
GetLimits: function() { return {"Bar": 20}; },
GetCounts: function() { return {"Bar": 0}; },
GetLimitChangers: function() {return {"Bar": {}}; }
});
AddMock(101, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(101, IID_StatisticsTracker, {
GetBasicStatistics: function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
GetSequences: function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
IncreaseTrainedUnitsCounter: function() { return 1; },
IncreaseConstructedBuildingsCounter: function() { return 1; },
IncreaseBuiltCivCentresCounter: function() { return 1; }
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
players: [
{
name: "Player 1",
civ: "gaia",
color: { r:1, g:1, b:1, a:1 },
controlsAll: false,
popCount: 10,
popLimit: 20,
popMax: 200,
panelEntities: [],
resourceCounts: { food: 100 },
trainingBlocked: false,
state: "active",
team: -1,
teamsLocked: false,
cheatsEnabled: false,
disabledTemplates: {},
disabledTechnologies: {},
hasSharedDropsites: false,
hasSharedLos: false,
spyCostMultiplier: 1,
phase: "village",
isAlly: [false, false],
isMutualAlly: [false, false],
isNeutral: [false, false],
isEnemy: [true, true],
entityLimits: {"Foo": 10},
entityCounts: {"Foo": 5},
entityLimitChangers: {"Foo": {}},
researchQueued: new Map(),
researchStarted: new Set(),
researchedTechs: new Set(),
classCounts: {},
typeCountsByClass: {},
canBarter: false,
barterPrices: {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
statistics: {
resourcesGathered: {
food: 100,
wood: 0,
metal: 0,
stone: 0,
vegetarianFood: 0
},
percentMapExplored: 10
}
},
{
name: "Player 2",
civ: "mace",
color: { r:1, g:0, b:0, a:1 },
controlsAll: true,
popCount: 40,
popLimit: 30,
popMax: 300,
panelEntities: [],
resourceCounts: { food: 200 },
trainingBlocked: false,
state: "active",
team: -1,
teamsLocked: false,
cheatsEnabled: false,
disabledTemplates: {},
disabledTechnologies: {},
hasSharedDropsites: false,
hasSharedLos: false,
spyCostMultiplier: 1,
phase: "village",
isAlly: [true, true],
isMutualAlly: [false, false],
isNeutral: [false, false],
isEnemy: [false, false],
entityLimits: {"Bar": 20},
entityCounts: {"Bar": 0},
entityLimitChangers: {"Bar": {}},
researchQueued: new Map(),
researchStarted: new Set(),
researchedTechs: new Set(),
classCounts: {},
typeCountsByClass: {},
canBarter: false,
barterPrices: {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
statistics: {
resourcesGathered: {
food: 100,
wood: 0,
metal: 0,
stone: 0,
vegetarianFood: 0
},
percentMapExplored: 10
}
}
],
circularMap: false,
timeElapsed: 0,
"victoryConditions": ["conquest", "wonder"],
alliedVictory: false
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
"players": [
{
"name": "Player 1",
"civ": "gaia",
"color": { "r":1, "g":1, "b":1, "a":1 },
"controlsAll": false,
"popCount": 10,
"popLimit": 20,
"popMax": 200,
"panelEntities": [],
"resourceCounts": { "food": 100 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [false, false],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [true, true],
"entityLimits": {"Foo": 10},
"entityCounts": {"Foo": 5},
"entityLimitChangers": {"Foo": {}},
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
},
{
"name": "Player 2",
"civ": "mace",
"color": { "r":1, "g":0, "b":0, "a":1 },
"controlsAll": true,
"popCount": 40,
"popLimit": 30,
"popMax": 300,
"panelEntities": [],
"resourceCounts": { "food": 200 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [true, true],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [false, false],
"entityLimits": {"Bar": 20},
"entityCounts": {"Bar": 0},
"entityLimitChangers": {"Bar": {}},
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
}
],
"circularMap": false,
"timeElapsed": 0,
"victoryConditions": ["conquest", "wonder"],
"alliedVictory": false
});
AddMock(10, IID_Builder, {
GetEntitiesList: function() {
return ["test1", "test2"];
},
});
AddMock(10, IID_Health, {
GetHitpoints: function() { return 50; },
GetMaxHitpoints: function() { return 60; },
IsRepairable: function() { return false; },
IsUnhealable: function() { return false; }
});
AddMock(10, IID_Identity, {
GetClassesList: function() { return ["class1", "class2"]; },
GetVisibleClassesList: function() { return ["class3", "class4"]; },
GetRank: function() { return "foo"; },
GetSelectionGroupName: function() { return "Selection Group Name"; },
HasClass: function() { return true; },
IsUndeletable: function() { return false; }
});
AddMock(10, IID_Position, {
GetTurretParent: function() {return INVALID_ENTITY;},
GetPosition: function() {
return {x:1, y:2, z:3};
},
IsInWorld: function() {
return true;
}
});
AddMock(10, IID_ResourceTrickle, {
"GetTimer": () => 1250,
"GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 })
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), {
"id": 10,
"player": INVALID_PLAYER,
"template": "example",
"identity": {
"rank": "foo",
"classes": ["class1", "class2"],
"visibleClasses": ["class3", "class4"],
"selectionGroupName": "Selection Group Name",
"canDelete": true
},
"position": {x:1, y:2, z:3},
"hitpoints": 50,
"maxHitpoints": 60,
"needsRepair": false,
"needsHeal": true,
"builder": true,
"canGarrison": false,
"visibility": "visible",
"isBarterMarket":true,
"resourceTrickle": {
"interval": 1250,
"rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 }
}
});
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Health.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Health.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Health.js (revision 22754)
@@ -1,150 +1,150 @@
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/AuraManager.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadHelperScript("Sound.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("Health.js");
const entity_id = 5;
const corpse_id = entity_id + 1;
const health_template = {
"Max": 50,
"RegenRate": 0,
"IdleRegenRate": 0,
"DeathType": "corpse",
"Unhealable": false
};
var injured_flag = false;
var corpse_entity;
function setEntityUp()
{
let cmpHealth = ConstructComponent(entity_id, "Health", health_template);
AddMock(entity_id, IID_DeathDamage, {
"CauseDeathDamage": () => {}
});
AddMock(entity_id, IID_Position, {
"IsInWorld": () => true,
"GetPosition": () => ({ "x": 0, "z": 0 }),
"GetRotation": () => ({ "x": 0, "y": 0, "z": 0 })
});
AddMock(entity_id, IID_Ownership, {
"GetOwner": () => 1
});
AddMock(entity_id, IID_Visual, {
"GetActorSeed": () => 1
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetCurrentTemplateName": () => "test"
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"SetEntityFlag": (ent, flag, value) => (injured_flag = value)
});
return cmpHealth;
}
var cmpHealth = setEntityUp();
TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false);
TS_ASSERT_EQUALS(cmpHealth.IsUnhealable(), true);
var change = cmpHealth.Reduce(25);
TS_ASSERT_EQUALS(injured_flag, true);
TS_ASSERT_EQUALS(change.killed, false);
-TS_ASSERT_EQUALS(change.change, -25);
+TS_ASSERT_EQUALS(change.HPchange, -25);
TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 25);
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.IsInjured(), true);
TS_ASSERT_EQUALS(cmpHealth.IsUnhealable(), false);
change = cmpHealth.Increase(25);
TS_ASSERT_EQUALS(injured_flag, false);
TS_ASSERT_EQUALS(change.new, 50);
TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false);
TS_ASSERT_EQUALS(cmpHealth.IsUnhealable(), true);
// Check death.
Engine.AddLocalEntity = function(template) {
corpse_entity = template;
AddMock(corpse_id, IID_Position, {
"JumpTo": () => {},
"SetYRotation": () => {},
"SetXZRotation": () => {},
});
AddMock(corpse_id, IID_Ownership, {
"SetOwner": () => {},
});
AddMock(corpse_id, IID_Visual, {
"SetActorSeed": () => {},
"SelectAnimation": () => {},
});
return corpse_id;
};
change = cmpHealth.Reduce(50);
// Assert we create a corpse with the proper template.
TS_ASSERT_EQUALS(corpse_entity, "corpse|test");
// Check that we are not marked as injured.
TS_ASSERT_EQUALS(injured_flag, false);
TS_ASSERT_EQUALS(change.killed, true);
-TS_ASSERT_EQUALS(change.change, -50);
+TS_ASSERT_EQUALS(change.HPchange, -50);
TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0);
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false);
// Check that we can't be revived once dead.
change = cmpHealth.Increase(25);
TS_ASSERT_EQUALS(change.new, 0);
TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0);
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false);
// Check that we can't die twice.
change = cmpHealth.Reduce(50);
TS_ASSERT_EQUALS(change.killed, false);
-TS_ASSERT_EQUALS(change.change, 0);
+TS_ASSERT_EQUALS(change.HPchange, 0);
TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0);
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false);
cmpHealth = setEntityUp();
// Check that we still die with > Max HP of damage.
change = cmpHealth.Reduce(60);
TS_ASSERT_EQUALS(change.killed, true);
-TS_ASSERT_EQUALS(change.change, -50);
+TS_ASSERT_EQUALS(change.HPchange, -50);
TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0);
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false);
cmpHealth = setEntityUp();
// Check that increasing by more than required puts us at the max HP
change = cmpHealth.Reduce(30);
change = cmpHealth.Increase(30);
TS_ASSERT_EQUALS(injured_flag, false);
TS_ASSERT_EQUALS(change.new, 50);
TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false);
TS_ASSERT_EQUALS(cmpHealth.IsUnhealable(), true);
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 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js (revision 22754)
@@ -1,329 +1,329 @@
Engine.LoadHelperScript("FSM.js");
Engine.LoadHelperScript("Entity.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
-Engine.LoadComponentScript("interfaces/DamageReceiver.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");
/* 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) {
return 1;
},
EnableActiveQuery: function(id) { },
ResetActiveQuery: function(id) { if (mode == 0) return []; else 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 []; },
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": (ent, target, min, max, opposite) => true
});
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,
"MoveToFormationOffset": (target, x, z) => {},
"MoveToTargetRange": (target, min, max) => true,
"StopMoving": () => {},
"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; },
});
unitAI.OnCreate();
unitAI.SetupRangeQuery(1);
if (mode == 1)
{
AddMock(enemy, IID_Health, {
GetHitpoints: function() { return 10; },
});
AddMock(enemy, IID_UnitAI, {
IsAnimal: function() { return false; }
});
}
else if (mode == 2)
AddMock(enemy, IID_Health, {
GetHitpoints: function() { return 0; },
});
var controllerFormation = ConstructComponent(controller, "Formation", {"FormationName": "Line Closed", "FormationShape": "square", "ShiftRows": "false", "SortingClasses": "", "WidthDepthRatio": 1, "UnitSeparationWidthMultiplier": 1, "UnitSeparationDepthMultiplier": 1, "SpeedMultiplier": 1, "Sloppyness": 0});
var 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; },
});
AddMock(controller, IID_UnitMotion, {
"GetWalkSpeed": () => 1,
"StopMoving": () => {},
"SetSpeedMultiplier": () => {},
"MoveToPointRange": () => 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) {
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 []; },
});
// 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,
"MoveToFormationOffset": (target, x, z) => {},
"MoveToTargetRange": (target, min, max) => true,
"StopMoving": () => {},
"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; },
});
unitAI.OnCreate();
unitAI.SetupRangeQuery(1);
unitAIs.push(unitAI);
}
// create enemy
AddMock(enemy, IID_Health, {
GetHitpoints: function() { return 40; },
});
var controllerFormation = ConstructComponent(controller, "Formation", {"FormationName": "Line Closed", "FormationShape": "square", "ShiftRows": "false", "SortingClasses": "", "WidthDepthRatio": 1, "UnitSeparationWidthMultiplier": 1, "UnitSeparationDepthMultiplier": 1, "SpeedMultiplier": 1, "Sloppyness": 0});
var controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" });
AddMock(controller, IID_Position, {
GetTurretParent: function() { return 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: function() { return { "y": 0 }; },
IsInWorld: function() { return true; },
});
AddMock(controller, IID_UnitMotion, {
"GetWalkSpeed": () => 1,
"SetSpeedMultiplier": (speed) => {},
"MoveToPointRange": (x, z, minRange, maxRange) => {},
"StopMoving": () => {},
"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 (var ent of unitAIs)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
controllerAI.MoveIntoFormation({"name": "Circle"});
// let all units be in position
for (var ent of unitAIs)
controllerFormation.SetInPosition(ent);
for (var ent of unitAIs)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
controllerFormation.Disband();
}
TestFormationExiting(0);
TestFormationExiting(1);
TestFormationExiting(2);
TestMoveIntoFormationWhileAttacking();
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/DamageBonus.js (revision 22754)
@@ -1,31 +1,31 @@
/**
* Calculates the attack damage multiplier against a target.
* @param {entity_id_t} source - The source entity's id.
* @param {entity_id_t} target - The target entity's id.
* @param {string} type - The type of attack.
* @param {Object} template - The bonus' template.
* @return {number} - The source entity's attack bonus against the specified target.
*/
-function GetDamageBonus(source, target, type, template)
+function GetAttackBonus(source, target, type, template)
{
let attackBonus = 1;
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return 1;
// Multiply the bonuses for all matching classes
for (let key in template)
{
let bonus = template[key];
if (bonus.Civ && bonus.Civ !== cmpIdentity.GetCiv())
continue;
if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !cmpIdentity.HasClass(cls)))
continue;
attackBonus *= ApplyValueModificationsToEntity("Attack/" + type + "/Bonuses/" + key + "/Multiplier", +bonus.Multiplier, source);
}
return attackBonus;
}
-Engine.RegisterGlobal("GetDamageBonus", GetDamageBonus);
+Engine.RegisterGlobal("GetAttackBonus", GetAttackBonus);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml (revision 22754)
@@ -1,30 +1,30 @@
- 5
+ 541000Field Palisade SiegeWall StoneWallChampion UnitOrganic HumanChampion Soldierphase_city1501010020attack/impact/arrow_metal.xmlattack/weapon/arrowfly.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml (revision 22754)
@@ -1,128 +1,128 @@
2415
- 2
+ 241000Field Palisade SiegeWall StoneWall50.00.00.021.0
structures/{civ}_house
structures/{civ}_storehouse
structures/{civ}_farmstead
structures/{civ}_field
structures/{civ}_corral
structures/{civ}_outpost
other/wallset_palisade
structures/{civ}_sentry_tower
structures/{civ}_dock
structures/{civ}_barracks
structures/{civ}_blacksmith
structures/{civ}_temple
structures/{civ}_market
structures/{civ}_defense_tower
structures/{civ}_wallset_stone
structures/{civ}_civil_centre
structures/{civ}_fortress
structures/{civ}_wonder
10500002.580InfantryCitizenSoldier Human OrganicCitizen Infantry Soldier WorkerBasic10050001002.01.00.50.2510.7550.520.5circle/128x128.pngcircle/128x128_mask.pngvoice/{lang}/civ/civ_{phenotype}_walk.xmlvoice/{lang}/civ/civ_{phenotype}_attack.xmlvoice/{lang}/civ/civ_{phenotype}_gather.xmlvoice/{lang}/civ/civ_{phenotype}_repair.xmlvoice/{lang}/civ/civ_{phenotype}_garrison.xmlactor/human/movement/walk.xmlactor/human/movement/run.xmlattack/impact/arrow_metal.xmlattack/weapon/sword.xmlattack/weapon/arrowfly.xmlactor/human/death/{phenotype}_death.xmlresource/construction/con_wood.xmlresource/foraging/forage_leaves.xmlresource/farming/farm.xmlresource/lumbering/lumbering.xmlresource/mining/pickaxe.xmlresource/mining/mining.xmlresource/mining/mining.xmlinterface/alarm/alarm_create_infantry.xml80
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js (revision 22754)
@@ -1,119 +1,117 @@
-Engine.LoadComponentScript("interfaces/Damage.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("StatusEffectsReceiver.js");
Engine.LoadComponentScript("Timer.js");
var target = 42;
var cmpStatusReceiver;
var cmpTimer;
var dealtDamage;
function setup()
{
cmpStatusReceiver = ConstructComponent(target, "StatusEffectsReceiver");
cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer");
dealtDamage = 0;
}
function testInflictEffects()
{
setup();
let statusName = "Burn";
- AddMock(SYSTEM_ENTITY, IID_Damage, {
- "CauseDamage": (data) => { dealtDamage += data.strengths[statusName]; }
- });
+ let Attacking = {
+ "HandleAttackEffects": (_, attackData) => { dealtDamage += attackData.Damage[statusName]; }
+ };
+ Engine.RegisterGlobal("Attacking", Attacking);
// damage scheduled: 0, 10, 20 sec
- cmpStatusReceiver.InflictEffects({
- [statusName]: {
- "Duration": 20000,
- "Interval": 10000,
- "Damage": 1
- }
+ cmpStatusReceiver.AddStatus(statusName, {
+ "Duration": 20000,
+ "Interval": 10000,
+ "Damage": 1
});
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 1 sec
cmpTimer.OnUpdate({ "turnLength": 8 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 9 sec
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 2); // 10 sec
cmpTimer.OnUpdate({ "turnLength": 10 });
TS_ASSERT_EQUALS(dealtDamage, 3); // 20 sec
cmpTimer.OnUpdate({ "turnLength": 10 });
TS_ASSERT_EQUALS(dealtDamage, 3); // 30 sec
}
testInflictEffects();
function testMultipleEffects()
{
setup();
- AddMock(SYSTEM_ENTITY, IID_Damage, {
- "CauseDamage": (data) => {
- if (data.strengths.Burn) dealtDamage += data.strengths.Burn;
- if (data.strengths.Poison) dealtDamage += data.strengths.Poison;
- }
- });
+ let Attacking = {
+ "HandleAttackEffects": (_, attackData) => {
+ if (attackData.Damage.Burn) dealtDamage += attackData.Damage.Burn;
+ if (attackData.Damage.Poison) dealtDamage += attackData.Damage.Poison;
+ },
+ };
+ Engine.RegisterGlobal("Attacking", Attacking);
// damage scheduled: 0, 1, 2, 10 sec
- cmpStatusReceiver.InflictEffects({
+ cmpStatusReceiver.GiveStatus({
"Burn": {
"Duration": 20000,
"Interval": 10000,
"Damage": 10
},
"Poison": {
"Duration": 3000,
"Interval": 1000,
"Damage": 1
}
});
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 12); // 1 sec
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 13); // 2 sec
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 13); // 3 sec
cmpTimer.OnUpdate({ "turnLength": 7 });
TS_ASSERT_EQUALS(dealtDamage, 23); // 10 sec
}
testMultipleEffects();
-function testRemoveEffect()
+function testRemoveStatus()
{
setup();
let statusName = "Poison";
- AddMock(SYSTEM_ENTITY, IID_Damage, {
- "CauseDamage": (data) => { dealtDamage += data.strengths[statusName]; }
- });
+ let Attacking = {
+ "HandleAttackEffects": (_, attackData) => { dealtDamage += attackData.Damage[statusName]; }
+ };
+ Engine.RegisterGlobal("Attacking", Attacking);
// damage scheduled: 0, 10, 20 sec
- cmpStatusReceiver.InflictEffects({
- [statusName]: {
- "Duration": 20000,
- "Interval": 10000,
- "Damage": 1
- }
+ cmpStatusReceiver.AddStatus(statusName, {
+ "Duration": 20000,
+ "Interval": 10000,
+ "Damage": 1
});
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 1 sec
- cmpStatusReceiver.RemoveEffect(statusName);
+ cmpStatusReceiver.RemoveStatus(statusName);
cmpTimer.OnUpdate({ "turnLength": 10 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 11 sec
}
-testRemoveEffect();
+testRemoveStatus();
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js (revision 22754)
@@ -0,0 +1,308 @@
+/**
+ * Provides attack and damage-related helpers under the Attacking umbrella (to avoid name ambiguity with the component).
+ */
+function Attacking() {}
+
+/**
+ * Builds a RelaxRNG schema of possible attack effects.
+ * Currently harcoded to "Damage", "Capture" and "StatusEffects".
+ * Attacks may also have a "Bonuses" element.
+ *
+ * @return {string} - RelaxNG schema string
+ */
+Attacking.prototype.BuildAttackEffectsSchema = function()
+{
+ return "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ // Armour requires Foundation to not be a damage type.
+ "Foundation" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+};
+
+/**
+ * Returns a template-like object of attack effects.
+ */
+Attacking.prototype.GetAttackEffectsData = function(valueModifRoot, template, entity)
+{
+ let ret = {};
+
+ if (template.Damage)
+ {
+ ret.Damage = {};
+ let applyMods = damageType =>
+ ApplyValueModificationsToEntity(valueModifRoot + "/Damage/" + damageType, +(template.Damage[damageType] || 0), entity);
+ for (let damageType in template.Damage)
+ ret.Damage[damageType] = applyMods(damageType);
+ }
+ if (template.Capture)
+ ret.Capture = ApplyValueModificationsToEntity(valueModifRoot + "/Capture", +(template.Capture || 0), entity);
+
+ if (template.StatusEffects)
+ ret.StatusEffects = template.StatusEffects;
+
+ if (template.Bonuses)
+ ret.Bonuses = template.Bonuses;
+
+ return ret;
+};
+
+Attacking.prototype.GetTotalAttackEffects = function(effectData, effectType, cmpResistance)
+{
+ let total = 0;
+ let armourStrengths = cmpResistance ? cmpResistance.GetArmourStrengths(effectType) : {};
+
+ for (let type in effectData)
+ total += effectData[type] * Math.pow(0.9, armourStrengths[type] || 0);
+
+ return total;
+};
+
+/**
+ * Gives the position of the given entity, taking the lateness into account.
+ * @param {number} ent - Entity id of the entity we are finding the location for.
+ * @param {number} lateness - The time passed since the expected time to fire the function.
+ * @return {Vector3D} The location of the entity.
+ */
+Attacking.prototype.InterpolatedLocation = function(ent, lateness)
+{
+ let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
+ if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
+ return undefined;
+ let curPos = cmpTargetPosition.GetPosition();
+ let prevPos = cmpTargetPosition.GetPreviousPosition();
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ let turnLength = cmpTimer.GetLatestTurnLength();
+ return new Vector3D(
+ (curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength,
+ 0,
+ (curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength
+ );
+};
+
+/**
+ * Test if a point is inside of an entity's footprint.
+ * @param {number} ent - Id of the entity we are checking with.
+ * @param {Vector3D} point - The point we are checking with.
+ * @param {number} lateness - The time passed since the expected time to fire the function.
+ * @return {boolean} True if the point is inside of the entity's footprint.
+ */
+Attacking.prototype.TestCollision = function(ent, point, lateness)
+{
+ let targetPosition = this.InterpolatedLocation(ent, lateness);
+ if (!targetPosition)
+ return false;
+
+ let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
+ if (!cmpFootprint)
+ return false;
+
+ let targetShape = cmpFootprint.GetShape();
+
+ if (!targetShape)
+ return false;
+
+ if (targetShape.type == "circle")
+ return targetPosition.horizDistanceToSquared(point) < targetShape.radius * targetShape.radius;
+
+ if (targetShape.type == "square")
+ {
+ let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
+ let distance = Vector2D.from3D(Vector3D.sub(point, targetPosition)).rotate(-angle);
+ return Math.abs(distance.x) < targetShape.width / 2 && Math.abs(distance.y) < targetShape.depth / 2;
+ }
+
+ warn("TestCollision called with an invalid footprint shape");
+ return false;
+};
+
+/**
+ * Get the list of players affected by the damage.
+ * @param {number} attackerOwner - The player id of the attacker.
+ * @param {boolean} friendlyFire - A flag indicating if allied entities are also damaged.
+ * @return {number[]} The ids of players need to be damaged.
+ */
+Attacking.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire)
+{
+ if (!friendlyFire)
+ return QueryPlayerIDInterface(attackerOwner).GetEnemies();
+
+ return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
+};
+
+/**
+ * Damages units around a given origin.
+ * @param {Object} data - The data sent by the caller.
+ * @param {string} data.type - The type of damage.
+ * @param {Object} data.attackData - The attack data.
+ * @param {number} data.attacker - The entity id of the attacker.
+ * @param {number} data.attackerOwner - The player id of the attacker.
+ * @param {Vector2D} data.origin - The origin of the projectile hit.
+ * @param {number} data.radius - The radius of the splash damage.
+ * @param {string} data.shape - The shape of the radius.
+ * @param {Vector3D} [data.direction] - The unit vector defining the direction. Needed for linear splash damage.
+ * @param {number[]} data.playersToDamage - The array of player id's to damage.
+ */
+Attacking.prototype.CauseDamageOverArea = function(data)
+{
+ // Get nearby entities and define variables
+ let nearEnts = this.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage);
+ let damageMultiplier = 1;
+
+ // Cycle through all the nearby entities and damage it appropriately based on its distance from the origin.
+ for (let ent of nearEnts)
+ {
+ let entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D();
+ if (data.shape == 'Circular') // circular effect with quadratic falloff in every direction
+ damageMultiplier = 1 - data.origin.distanceToSquared(entityPosition) / (data.radius * data.radius);
+ else if (data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles)
+ {
+ // Get position of entity relative to splash origin.
+ let relativePos = entityPosition.sub(data.origin);
+
+ // Get the position relative to the missile direction.
+ let direction = Vector2D.from3D(data.direction);
+ let parallelPos = relativePos.dot(direction);
+ let perpPos = relativePos.cross(direction);
+
+ // The width of linear splash is one fifth of the normal splash radius.
+ let width = data.radius / 5;
+
+ // Check that the unit is within the distance splash width of the line starting at the missile's
+ // landing point which extends in the direction of the missile for length splash radius.
+ if (parallelPos >= 0 && Math.abs(perpPos) < width) // If in radius, quadratic falloff in both directions
+ damageMultiplier = (1 - parallelPos * parallelPos / (data.radius * data.radius)) *
+ (1 - perpPos * perpPos / (width * width));
+ else
+ damageMultiplier = 0;
+ }
+ else // In case someone calls this function with an invalid shape.
+ {
+ warn("The " + data.shape + " splash damage shape is not implemented!");
+ }
+
+ this.HandleAttackEffects(data.type + ".Splash", data.attackData, ent, data.attacker, data.attackerOwner, damageMultiplier);
+ }
+};
+
+Attacking.prototype.HandleAttackEffects = function(attackType, attackData, target, attacker, attackerOwner, bonusMultiplier = 1)
+{
+ let targetState = {};
+ for (let effectType of g_EffectTypes)
+ {
+ if (!attackData[effectType])
+ continue;
+
+ bonusMultiplier *= !attackData.Bonuses ? 1 : GetAttackBonus(attacker, target, attackType, attackData.Bonuses);
+
+ let receiver = g_EffectReceiver[effectType];
+ let cmpReceiver = Engine.QueryInterface(target, global[receiver.IID]);
+ if (!cmpReceiver)
+ return;
+
+ Object.assign(targetState, cmpReceiver[receiver.method](attackData[effectType], attacker, attackerOwner, bonusMultiplier));
+ }
+
+ let cmpPromotion = Engine.QueryInterface(attacker, IID_Promotion);
+ if (cmpPromotion && targetState.xp)
+ cmpPromotion.IncreaseXp(targetState.xp);
+
+ if (targetState.killed)
+ this.TargetKilled(attacker, target, attackerOwner);
+
+ Engine.PostMessage(target, MT_Attacked, {
+ "type": attackType,
+ "target": target,
+ "attacker": attacker,
+ "attackerOwner": attackerOwner,
+ "damage": -(targetState.HPchange || 0),
+ "capture": targetState.captureChange || 0,
+ "statusEffects": targetState.inflictedStatuses || [],
+ });
+};
+
+/**
+ * Gets entities near a give point for given players.
+ * @param {Vector2D} origin - The point to check around.
+ * @param {number} radius - The radius around the point to check.
+ * @param {number[]} players - The players of which we need to check entities.
+ * @return {number[]} The id's of the entities in range of the given point.
+ */
+Attacking.prototype.EntitiesNearPoint = function(origin, radius, players)
+{
+ // If there is insufficient data return an empty array.
+ if (!origin || !radius || !players)
+ return [];
+
+ return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQueryAroundPos(origin, 0, radius, players, IID_Resistance);
+};
+
+/**
+ * Called when a unit kills something (another unit, building, animal etc).
+ * @param {number} attacker - The entity id of the killer.
+ * @param {number} target - The entity id of the target.
+ * @param {number} attackerOwner - The player id of the attacker.
+ */
+Attacking.prototype.TargetKilled = function(attacker, target, attackerOwner)
+{
+ let cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership);
+ let atkOwner = cmpAttackerOwnership && cmpAttackerOwnership.GetOwner() != INVALID_PLAYER ? cmpAttackerOwnership.GetOwner() : attackerOwner;
+
+ // Add to killer statistics.
+ let cmpKillerPlayerStatisticsTracker = QueryPlayerIDInterface(atkOwner, IID_StatisticsTracker);
+ if (cmpKillerPlayerStatisticsTracker)
+ cmpKillerPlayerStatisticsTracker.KilledEntity(target);
+ // Add to loser statistics.
+ let cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(target, IID_StatisticsTracker);
+ if (cmpTargetPlayerStatisticsTracker)
+ cmpTargetPlayerStatisticsTracker.LostEntity(target);
+
+ // If killer can collect loot, let's try to collect it.
+ let cmpLooter = Engine.QueryInterface(attacker, IID_Looter);
+ if (cmpLooter)
+ cmpLooter.Collect(target);
+};
+
+var AttackingInstance = new Attacking();
+Engine.RegisterGlobal("Attacking", AttackingInstance);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/helpers/Attacking.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml (revision 22754)
@@ -1,106 +1,106 @@
3115
- 2
+ 241000Field Palisade SiegeWall StoneWall100.00.00.021151007.5160CitizenSoldier Human OrganicCavalry Citizen SoldierCavalryBasic
special/formations/wedge
13010500pitch1502.01.0520202020circle/128x128.pngcircle/128x128_mask.pngvoice/{lang}/civ/civ_{phenotype}_walk.xmlvoice/{lang}/civ/civ_{phenotype}_attack.xmlvoice/{lang}/civ/civ_{phenotype}_gather.xmlvoice/{lang}/civ/civ_{phenotype}_garrison.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlattack/impact/arrow_metal.xmlattack/weapon/sword.xmlattack/weapon/arrowfly.xmlactor/fauna/death/death_horse.xmlinterface/alarm/alarm_create_cav.xml6.51.9592
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml (revision 22753)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml (revision 22754)
@@ -1,79 +1,79 @@
10.020.015.0
- 15
+ 1541000Field Palisade SiegeWall StoneWall
units/heroes/hero_garrison
240100250600HeroOrganic HumanHero Soldierphase_city400100025herostar/256x256.pngstar/256x256_mask.pnginterface/alarm/alarm_create_infantry.xmlvoice/{lang}/civ/civ_{phenotype}_heal.xmlvoice/{lang}/civ/civ_{phenotype}_walk.xmlvoice/{lang}/civ/civ_{phenotype}_attack.xmlvoice/{lang}/civ/civ_{phenotype}_gather.xmlvoice/{lang}/civ/civ_{phenotype}_repair.xmlvoice/{lang}/civ/civ_{phenotype}_garrison.xmlattack/weapon/sword.xmlattack/impact/arrow_metal.xmlattack/weapon/arrowfly.xmlactor/human/movement/walk.xmlactor/human/movement/walk.xmlactor/human/death/{phenotype}_death.xmlHero88