Index: binaries/data/config/default.cfg
===================================================================
--- binaries/data/config/default.cfg
+++ binaries/data/config/default.cfg
@@ -313,6 +313,8 @@
deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting
rotate.cw = RightBracket ; Rotate building placement preview clockwise
rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise
+attackgroundbombardradius.decrease = PgDn ; Decrease attack ground bombard radius.
+attackgroundbombardradius.increase = PgUp ; Increase attack ground bombard radius.
[hotkey.session.gui]
toggle = "Alt+G" ; Toggle visibility of session GUI
@@ -372,6 +374,8 @@
rankabovestatusbar = true ; Show rank icons above status bars
experiencestatusbar = true ; Show an experience status bar above each selected unit
respoptooltipsort = 0 ; Sorting players in the resources and population tooltip by value (0 - no sort, -1 - ascending, 1 - descending)
+attackgroundbombardradius = 2.0 ; Default radius of the area to bombard with attack ground (m).
+attackgroundbombardchange = 0.5 ; Value to add/subtract from the radius with each push of the hotkey.
[gui.session.minimap]
blinkduration = 1.7 ; The blink duration while pinging
Index: binaries/data/mods/public/gui/options/options.json
===================================================================
--- binaries/data/mods/public/gui/options/options.json
+++ binaries/data/mods/public/gui/options/options.json
@@ -435,6 +435,23 @@
"max": 30
},
{
+ "type": "number",
+ "label": "Attack-Ground Bombard Size",
+ "tooltip": "The default value of the bombard radius (m).",
+ "config": "gui.session.attackgroundbombardradius",
+ "callback": "updateDefaultAttackGroundSize",
+ "min": 1,
+ "max": 20
+ },
+ {
+ "type": "slider",
+ "label": "Attack-Ground Increment value",
+ "tooltip": "Value to add/subtract from the radius with each push of the hotkey.",
+ "config": "gui.session.attackgroundbombardchange",
+ "min": 0.1,
+ "max": 10
+ },
+ {
"type": "boolean",
"label": "Chat Notification Attack",
"tooltip": "Show a chat notification if you are attacked by another player.",
Index: binaries/data/mods/public/gui/session/input.js
===================================================================
--- binaries/data/mods/public/gui/session/input.js
+++ binaries/data/mods/public/gui/session/input.js
@@ -17,6 +17,7 @@
const ACTION_REPAIR = 2;
const ACTION_GUARD = 3;
const ACTION_PATROL = 4;
+const ACTION_ATTACKGROUND = 5;
var preSelectedAction = ACTION_NONE;
const INPUT_NORMAL = 0;
@@ -896,9 +897,13 @@
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE)
{
- var action = determineAction(ev.x, ev.y);
+ let action = determineAction(ev.x, ev.y);
if (!action)
break;
+
+ if (preSelectedAction == ACTION_ATTACKGROUND)
+ action.radius = g_AttackGroundSize;
+
if (!Engine.HotkeyIsPressed("session.queue"))
{
preSelectedAction = ACTION_NONE;
@@ -912,7 +917,17 @@
inputState = INPUT_NORMAL;
break;
}
- // else
+
+ case "hotkeydown":
+ if (preSelectedAction == ACTION_ATTACKGROUND && ev.hotkey == "session.attackgroundbombardradius.decrease")
+ {
+ AttackGroundBombardRadiusChange(-1);
+ }
+ else if (preSelectedAction == ACTION_ATTACKGROUND && ev.hotkey == "session.attackgroundbombardradius.increase")
+ {
+ AttackGroundBombardRadiusChange(1);
+ }
+
default:
// Slight hack: If selection is empty, reset the input state
if (g_Selection.toList().length == 0)
@@ -1322,6 +1337,34 @@
}
}
+// Attack ground:
+// When the user uses the hotkey the radius of the bombarded area is increased/decreased.
+var g_AttackGroundSize = getDefaultAttackGroundSize();
+function AttackGroundBombardRadiusChange(dir)
+{
+ g_AttackGroundSize += dir * +Engine.ConfigDB_GetValue("user", "gui.session.attackgroundbombardchange");
+ if (g_AttackGroundSize < 0 || !Number.isFinite(g_AttackGroundSize))
+ g_AttackGroundSize = 0;
+
+ updateSelectionDetails();
+}
+
+function getDefaultAttackGroundSize()
+{
+ let num = +Engine.ConfigDB_GetValue("user", "gui.session.attackgroundbombardradius");
+ return Number.isFinite(num) && num >= 0 ? num : 0;
+}
+
+function getAttackGroundSize()
+{
+ return Math.max(g_AttackGroundSize, 0);
+}
+
+function updateDefaultAttackGroundSize()
+{
+ g_AttackGroundSize = getDefaultAttackGroundSize();
+}
+
// Batch training:
// When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING
// When the user releases shift, or clicks on a different training button, we create the batched units
Index: binaries/data/mods/public/gui/session/unit_actions.js
===================================================================
--- binaries/data/mods/public/gui/session/unit_actions.js
+++ binaries/data/mods/public/gui/session/unit_actions.js
@@ -236,6 +236,47 @@
"specificness": 10,
},
+ "attack-ground": {
+ "execute": function(target, action, selection, queued)
+ {
+ Engine.PostNetworkCommand({
+ "type": "attack-ground",
+ "entities": selection,
+ "target": target,
+ "radius": action.radius,
+ "queued": queued
+ });
+
+ DrawTargetMarker(target);
+
+ return true;
+ },
+ "getActionInfo": function(selection, target)
+ {
+ let tooltip = sprintf(translate("Bombard radius: %(radius)s"), {
+ "radius": g_AttackGroundSize.toFixed(1)
+ });
+
+ return {
+ "possible": true,
+ "tooltip": tooltip
+ };
+ },
+ "preSelectedActionCheck": function(target, selection)
+ {
+ if (preSelectedAction != ACTION_ATTACKGROUND)
+ return false;
+
+ return {
+ "type": "attack-ground",
+ "cursor": "action-attack",
+ "tooltip": getActionInfo("attack-ground", target, selection).tooltip,
+ "target": target
+ };
+ },
+ "specificness": 50,
+ },
+
"patrol":
{
"execute": function(target, action, selection, queued)
@@ -1095,6 +1136,26 @@
},
},
+ "attack-ground": {
+ "getInfo": function(entStates)
+ {
+ if (entStates.every(entState => !entState.attack ||
+ Object.keys(entState.attack).every(type => !entState.attack[type].attackGround)))
+ return false;
+
+ return {
+ "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.attackground") +
+ translate("Attack the selected ground."),
+ "icon": "attack-request.png"
+ };
+ },
+ "execute": function(entStates)
+ {
+ inputState = INPUT_PRESELECTEDACTION;
+ preSelectedAction = ACTION_ATTACKGROUND;
+ },
+ },
+
"garrison": {
"getInfo": function(entStates)
{
@@ -1511,6 +1572,11 @@
"cursor": cursor
};
}
+ if (action == "attack-ground")
+ return {
+ "possible": true,
+ "tooltip": g_UnitActions[action].getActionInfo("attack-ground", selection).tooltip
+ }
return {
"possible": ["move", "attack-move", "remove-guard", "patrol"].indexOf(action) != -1
Index: binaries/data/mods/public/simulation/components/Attack.js
===================================================================
--- binaries/data/mods/public/simulation/components/Attack.js
+++ binaries/data/mods/public/simulation/components/Attack.js
@@ -117,6 +117,7 @@
"0.0" +
"" +
"" +
+ "" +
"" +
"" +
"" +
@@ -186,6 +187,9 @@
"" +
"" +
"" +
+ ""+
+ "" +
+ "" +
"" +
"" +
"" +
@@ -287,8 +291,24 @@
return [];
};
+/**
+ * Whether the entity is able to attack ground (with the requested attack type).
+ * @param {string | undefined} attackType - The attack type requested.
+ * @return {boolean} Whether the entity is able to attack ground.
+ */
+Attack.prototype.CanAttackGround = function(attackType)
+{
+ if (attackType)
+ return "AttackGround" in this.template[attackType];
+
+ return this.GetAttackTypes().some(type => "AttackGround" in this.template[type]);
+};
+
Attack.prototype.CanAttack = function(target, wantedTypes)
{
+ if (target instanceof Vector3D)
+ return this.CanAttackGround();
+
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
return true;
@@ -389,6 +409,10 @@
Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
{
+ // ToDo: Add support for more attack types based on some sort of preference (more splash is more better, DPS, range or something else?).
+ if (target instanceof Vector3D)
+ return this.GetAttackTypes().find(type => this.CanAttackGround(type));
+
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
{
@@ -512,9 +536,11 @@
};
/**
- * Attack the target entity. This should only be called after a successful range check,
+ * Attack the target. 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.
+ * @param {string} type - The type of the attack (e.g. "Melee", "Ranged", "Capture", "Slaughter").
+ * @param {number | Vector3D} target - Either the target entity ID, or a Vector3D of a position to attack.
*/
Attack.prototype.PerformAttack = function(type, target)
{
@@ -538,16 +564,23 @@
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 predictedPosition;
+ if (typeof target == "number")
+ {
+ 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;
+ let timeToTarget = this.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity);
+ predictedPosition = timeToTarget ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition;
+ }
+ else
+ predictedPosition = target;
// Add inaccuracy based on spread.
let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", +this.template[type].Projectile.Spread, this.entity) *
@@ -557,11 +590,11 @@
let offsetX = randNorm[0] * distanceModifiedSpread;
let offsetZ = randNorm[1] * distanceModifiedSpread;
- let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, targetPosition.y, predictedPosition.z + offsetZ);
+ let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, predictedPosition.y, predictedPosition.z + offsetZ);
// Recalculate when the missile will hit the target position.
let realHorizDistance = realTargetPosition.horizDistanceTo(selfPosition);
- timeToTarget = realHorizDistance / horizSpeed;
+ let timeToTarget = realHorizDistance / horizSpeed;
let missileDirection = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance);
Index: binaries/data/mods/public/simulation/components/Damage.js
===================================================================
--- binaries/data/mods/public/simulation/components/Damage.js
+++ binaries/data/mods/public/simulation/components/Damage.js
@@ -82,7 +82,7 @@
* 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 {number | Vector3D} data.target - Either the target entity ID, or a Vector3D of a position to attack.
* @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.
@@ -129,27 +129,33 @@
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))
+ let targetPosition;
+ if (typeof data.target == "number")
{
- 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);
+ // 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;
+ return;
+ }
+ targetPosition = this.InterpolatedLocation(data.target, lateness);
}
+ else
+ targetPosition = data.target;
- let targetPosition = this.InterpolatedLocation(data.target, lateness);
if (!targetPosition)
return;
- // If we didn't hit the main target look for nearby units
+ // 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)
Index: binaries/data/mods/public/simulation/components/FormationAttack.js
===================================================================
--- binaries/data/mods/public/simulation/components/FormationAttack.js
+++ binaries/data/mods/public/simulation/components/FormationAttack.js
@@ -23,44 +23,53 @@
FormationAttack.prototype.GetRange = function(target)
{
- var result = {"min": 0, "max": this.canAttackAsFormation ? -1 : 0};
- var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+let result = {
+ "min": 0,
+ "max": this.canAttackAsFormation ? -1 : 0,
+ "elevationBonus": this.canAttackAsFormation ? -1 : 0
+ };
+ let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
{
warn("FormationAttack component used on a non-formation entity");
return result;
}
- var members = cmpFormation.GetMembers();
- for (var ent of members)
+ let members = cmpFormation.GetMembers();
+ for (let ent of members)
{
- var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
+ let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (!cmpAttack)
continue;
- var type = cmpAttack.GetBestAttackAgainst(target);
+ let type = cmpAttack.GetBestAttackAgainst(target);
if (!type)
continue;
// if the formation can attack, take the minimum max range (so units are certainly in range),
// If the formation can't attack, take the maximum max range as the point where the formation will be disbanded
// Always take the minimum min range (to not get impossible situations)
- var range = cmpAttack.GetRange(type);
-
+ let range = cmpAttack.GetRange(type);
if (this.canAttackAsFormation)
{
if (range.max < result.max || result.max < 0)
result.max = range.max;
+
+ if (range.elevationBonus < result.elevationBonus || result.max < 0)
+ result.elevationBonus = range.elevationBonus;
}
else
{
if (range.max > result.max || range.max < 0)
result.max = range.max;
+
+ if (range.elevationBonus > result.elevationBonus)
+ result.elevationBonus = range.elevationBonus;
}
if (range.min < result.min)
result.min = range.min;
}
// add half the formation size, so it counts as the range for the units on the first row
- var extraRange = cmpFormation.GetSize().depth/2;
+ let extraRange = cmpFormation.GetSize().depth / 2;
if (result.max >= 0)
result.max += extraRange;
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js
+++ binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -429,6 +429,8 @@
// not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
+
+ ret.attack[type].attackGround = cmpAttack.CanAttackGround(type);
}
}
Index: binaries/data/mods/public/simulation/components/Identity.js
===================================================================
--- binaries/data/mods/public/simulation/components/Identity.js
+++ binaries/data/mods/public/simulation/components/Identity.js
@@ -17,7 +17,15 @@
"" +
"" +
"" +
- "" +
+ "" +
+ "" +
+ "tokens" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
"" +
"" +
"" +
@@ -95,15 +103,10 @@
{
this.classesList = GetIdentityClasses(this.template);
this.visibleClassesList = GetVisibleIdentityClasses(this.template);
+ if (this.template.Phenotype)
+ this.phenotype = this.template.Phenotype == "random" ? pickRandom(this.GetPossiblePhenotypes()) : this.template.Phenotype;
};
-Identity.prototype.Deserialize = function ()
-{
- this.Init();
-};
-
-Identity.prototype.Serialize = null; // we have no dynamic state to save
-
Identity.prototype.GetCiv = function()
{
return this.template.Civ;
@@ -114,9 +117,22 @@
return this.template.Lang || "greek"; // ugly default
};
-Identity.prototype.GetGender = function()
+/**
+ * Get a list of possible Phenotypes.
+ * @return {string[]} A list of possible phenotypes.
+ */
+Identity.prototype.GetPossiblePhenotypes = function()
+{
+ return !!this.template.PossiblePhenotypes ? this.template.PossiblePhenotypes._string.split(/\s+/) : [];
+};
+
+/**
+ * Get the current Phenotype.
+ * @return {string} The current phenotype.
+ */
+Identity.prototype.GetPhenotype = function()
{
- return this.template.Gender || "male"; // ugly default
+ return this.phenotype;
};
Identity.prototype.GetRank = function()
Index: binaries/data/mods/public/simulation/components/Sound.js
===================================================================
--- binaries/data/mods/public/simulation/components/Sound.js
+++ binaries/data/mods/public/simulation/components/Sound.js
@@ -35,16 +35,16 @@
if (name in this.template.SoundGroups)
{
// Replace the "{lang}" codes with this entity's civ ID
- var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
+ let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
if (!cmpIdentity)
return;
- var lang = cmpIdentity.GetLang();
- // Replace the "{gender}" codes with this entity's gender ID
- var gender = cmpIdentity.GetGender();
-
- var soundName = this.template.SoundGroups[name].replace(/\{lang\}/g, lang)
- .replace(/\{gender\}/g, gender);
- var cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager);
+ let lang = cmpIdentity.GetLang();
+ // Replace the "{phenotype}" codes with this entity's phenotype ID
+ let phenotype = cmpIdentity.GetPhenotype();
+
+ let soundName = this.template.SoundGroups[name].replace(/\{lang\}/g, lang)
+ .replace(/\{phenotype\}/g, phenotype);
+ let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager);
if (cmpSoundManager)
cmpSoundManager.PlaySoundGroup(soundName, this.entity);
}
Index: binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitAI.js
+++ binaries/data/mods/public/simulation/components/UnitAI.js
@@ -399,7 +399,7 @@
}
// Work out how to attack the given target
- var type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
+ let type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
if (!type)
{
// Oops, we can't attack at all
@@ -427,7 +427,7 @@
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.ATTACKING");
else
- this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
+ this.SetNextState("INDIVIDUAL.COMBAT");
return;
}
@@ -451,7 +451,28 @@
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.APPROACHING");
else
- this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
+ this.SetNextState("INDIVIDUAL.COMBAT");
+ },
+
+ "Order.AttackGround": function(msg) {
+ // Work out how to attack the given target
+ let target = this.order.data.target;
+ let type = this.GetBestAttackAgainst(target);
+ if (!type)
+ {
+ // Oops, we can't attack at all
+ this.FinishOrder();
+ return;
+ }
+ this.order.data.attackType = type;
+
+ // Distribute the attacks over the area.
+ let randNorm = randomNormal2D();
+ let offsetX = randNorm[0] * this.order.data.radius;
+ let offsetZ = randNorm[1] * this.order.data.radius;
+ this.order.data.target = new Vector3D(target.x + offsetX, target.y, target.z + offsetZ);
+
+ this.SetNextState("INDIVIDUAL.COMBAT");
},
"Order.Patrol": function(msg) {
@@ -716,15 +737,15 @@
},
"Order.Attack": function(msg) {
- var target = msg.data.target;
- var allowCapture = msg.data.allowCapture;
- var cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
+ let target = msg.data.target;
+ let allowCapture = msg.data.allowCapture;
+ let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
target = cmpTargetUnitAI.GetFormationController();
- var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
+ let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
// Check if we are already in range, otherwise walk there
- if (!this.CheckTargetAttackRange(target, target))
+ if (!this.CheckFormationTargetAttackRange(target, target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
@@ -741,6 +762,23 @@
this.SetNextState("MEMBER");
},
+ "Order.AttackGround": function(msg) {
+ let target = msg.data.target;
+
+ let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
+ // Check if we are already in range, otherwise walk there
+ if (!this.CheckFormationTargetAttackRange(target))
+ {
+ this.SetNextState("COMBAT.APPROACHING");
+ return;
+ }
+ this.CallMemberFunction("AttackGround", [target, msg.data.radius, false]);
+ if (cmpAttack.CanAttackAsFormation())
+ this.SetNextState("COMBAT.ATTACKING");
+ else
+ this.SetNextState("MEMBER");
+ },
+
"Order.Garrison": function(msg) {
if (!Engine.QueryInterface(msg.data.target, IID_GarrisonHolder))
{
@@ -1108,14 +1146,17 @@
"COMBAT": {
"APPROACHING": {
"enter": function() {
- if (!this.MoveTo(this.order.data))
+ if (!this.MoveFormationToTargetAttackRange(this.order.data.target))
{
this.FinishOrder();
return true;
}
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
- cmpFormation.SetRearrange(true);
- cmpFormation.MoveMembersIntoFormation(true, true);
+ if (cmpFormation)
+ {
+ cmpFormation.SetRearrange(true);
+ cmpFormation.MoveMembersIntoFormation(true, true);
+ }
},
"leave": function() {
@@ -1123,8 +1164,13 @@
},
"MovementUpdate": function(msg) {
+ let target = this.order.data.target;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
- this.CallMemberFunction("Attack", [this.order.data.target, this.order.data.allowCapture, false]);
+ if (typeof target == "number")
+ this.CallMemberFunction("Attack", [target, this.order.data.allowCapture, false]);
+ else
+ this.CallMemberFunction("AttackGround", [target, this.order.data.radius, false]);
+
if (cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
else
@@ -1135,39 +1181,68 @@
"ATTACKING": {
// Wait for individual members to finish
"enter": function(msg) {
- var target = this.order.data.target;
- var allowCapture = this.order.data.allowCapture;
+ let target = this.order.data.target;
+ let allowCapture = this.order.data.allowCapture;
+ let attackEntity = typeof target == "number";
+
// Check if we are already in range, otherwise walk there
- if (!this.CheckTargetAttackRange(target, target))
+ if (!this.CheckFormationTargetAttackRange(target))
{
- if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
+ if (!attackEntity || this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
this.FinishOrder();
- this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
+ if (attackEntity)
+ this.PushOrderFront("Attack", {
+ "target": target,
+ "force": false,
+ "allowCapture": allowCapture
+ });
+ else
+ this.PushOrderFront("AttackGround", {
+ "target": target,
+ "radius": this.order.data.radius,
+ "force": false
+ });
return true;
}
this.FinishOrder();
return true;
}
- var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+ let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// TODO fix the rearranging while attacking as formation
- cmpFormation.SetRearrange(!this.IsAttackingAsFormation());
- cmpFormation.MoveMembersIntoFormation(false, false);
+ if (cmpFormation)
+ {
+ 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;
+ let target = this.order.data.target;
+ let allowCapture = this.order.data.allowCapture;
+ let attackEntity = typeof target == "number";
+
// Check if we are already in range, otherwise walk there
- if (!this.CheckTargetAttackRange(target, target))
+ if (!this.CheckFormationTargetAttackRange(target))
{
- if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
+ if (!attackEntity || this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
this.FinishOrder();
- this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
+ if (attackEntity)
+ this.PushOrderFront("Attack", {
+ "target": target,
+ "force": false,
+ "allowCapture": allowCapture
+ });
+ else
+ this.PushOrderFront("AttackGround", {
+ "target": target,
+ "radius": this.order.data.radius,
+ "force": false
+ });
return;
}
this.FinishOrder();
@@ -1177,7 +1252,7 @@
"leave": function(msg) {
this.StopTimer();
- var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+ let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (cmpFormation)
cmpFormation.SetRearrange(true);
},
@@ -1744,6 +1819,35 @@
},
"COMBAT": {
+ "enter": function() {
+ // 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;
+ }
+
+ this.SetNextState("ATTACKING");
+ 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;
+ }
+
+ this.SetNextState("APPROACHING");
+ },
+
"Order.LeaveFoundation": function(msg) {
// Ignore the order as we're busy.
return { "discardOrder": true };
@@ -1767,7 +1871,9 @@
// Show weapons rather than carried resources.
this.SetAnimationVariant("combat");
- this.StartTimer(1000, 1000);
+ // If attack ground is asked do not start the timer (ground does not run away).
+ if (typeof this.order.data.target == "number")
+ this.StartTimer(1000, 1000);
},
"leave": function() {
@@ -1838,13 +1944,18 @@
"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)
+ let attackEntity = typeof target == "number";
+
+ if (attackEntity)
{
- this.order.data.formationTarget = target;
- target = cmpFormation.GetClosestMember(this.entity);
- this.order.data.target = target;
+ // If the target is a formation, save the attacking formation and pick a member.
+ let cmpFormation = Engine.QueryInterface(target, IID_Formation);
+ if (cmpFormation)
+ {
+ this.order.data.formationTarget = target;
+ target = cmpFormation.GetClosestMember(this.entity);
+ this.order.data.target = target;
+ }
}
if (!this.CanAttack(target))
@@ -1895,7 +2006,13 @@
this.FaceTowardsTarget(this.order.data.target);
+ // Attack ground with BuildingAI not yet implemented.
let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
+ if (cmpBuildingAI && !attackEntity)
+ {
+ this.FinishOrder();
+ return true;
+ }
if (cmpBuildingAI)
cmpBuildingAI.SetUnitAITarget(this.order.data.target);
},
@@ -1911,18 +2028,22 @@
"Timer": function(msg) {
let target = this.order.data.target;
- let cmpFormation = Engine.QueryInterface(target, IID_Formation);
+ let attackEntity = typeof target == "number";
- // if the target is a formation, save the attacking formation, and pick a member
- if (cmpFormation)
+ if (attackEntity)
{
- let thisObject = this;
- let filter = function(t) {
- return thisObject.CanAttack(t);
- };
- this.order.data.formationTarget = target;
- target = cmpFormation.GetClosestMember(this.entity, filter);
- this.order.data.target = target;
+ // If the target is a formation, save the attacking formation and pick a member.
+ let cmpFormation = Engine.QueryInterface(target, IID_Formation);
+ if (cmpFormation)
+ {
+ let thisObject = this;
+ let filter = function(t) {
+ return thisObject.CanAttack(t);
+ };
+ this.order.data.formationTarget = target;
+ target = cmpFormation.GetClosestMember(this.entity, filter);
+ this.order.data.target = target;
+ }
}
// Check the target is still alive and attackable
@@ -1971,6 +2092,11 @@
this.SetNextState("COMBAT.CHASING");
return;
}
+ else if (this.MoveToTargetAttackRange(target, this.order.data.attackType))
+ {
+ this.SetNextState("COMBAT.APPROACHING");
+ return;
+ }
},
// TODO: respond to target deaths immediately, rather than waiting
@@ -4248,28 +4374,47 @@
return cmpUnitMotion.MoveToTargetRange(target, 0, 1);
};
+/**
+ * Move the entity so we hope the target is in range.
+ * @param {number | Vector3D} target - Either the target entity ID, or a Vector3D of a position to attack.
+ * @param {number} iid - The interface ID to check the range for.
+ * @param {string} type - The type for which the range is to be checked.
+ * @return {boolean} - Whether the order to move has succeeded?
+ */
UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
{
+ let attackEntity = typeof target == "number";
+
if (!this.CheckTargetVisible(target) || this.IsTurret())
return false;
- var cmpRanged = Engine.QueryInterface(this.entity, iid);
+ let cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return false;
- var range = cmpRanged.GetRange(type);
+ let range = cmpRanged.GetRange(type);
+
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ if (attackEntity)
+ return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
+
+ return cmpUnitMotion.MoveToPointRange(target.x, target.z, range.min, range.max, true);
- 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
+ * 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.
+ *
+ * @param {number | Vector3D} target - Either the target entity ID, or a Vector3D of a position to attack.
+ * @param {string} type - The type of the attack which is to be used.
+ * @return {boolean} - Whether the order to move has succeeded?
*/
UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
{
- // for formation members, the formation will take care of the range check
+ let attackEntity = typeof target == "number";
+
+ // For formation members, the formation will take care of the range check.
if (this.IsFormationMember())
{
let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
@@ -4277,9 +4422,12 @@
return false;
}
- let cmpFormation = Engine.QueryInterface(target, IID_Formation);
- if (cmpFormation)
- target = cmpFormation.GetClosestMember(this.entity);
+ if (attackEntity)
+ {
+ let cmpFormation = Engine.QueryInterface(target, IID_Formation);
+ if (cmpFormation)
+ target = cmpFormation.GetClosestMember(this.entity);
+ }
if (type != "Ranged")
return this.MoveToTargetRange(target, IID_Attack, type);
@@ -4290,22 +4438,23 @@
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
let range = cmpAttack.GetRange(type);
- let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- if (!thisCmpPosition.IsInWorld())
+ let cmpSelfPosition = Engine.QueryInterface(this.entity, IID_Position);
+ if (!cmpSelfPosition.IsInWorld())
return false;
- let s = thisCmpPosition.GetPosition();
+ let selfPosition = cmpSelfPosition.GetPosition();
- let targetCmpPosition = Engine.QueryInterface(target, IID_Position);
- if (!targetCmpPosition.IsInWorld())
+ let targetPosition = this.GetTargetPosition3D(target);
+ if (!targetPosition)
return false;
- let t = targetCmpPosition.GetPosition();
- // h is positive when I'm higher than the target
- let h = s.y - t.y + range.elevationBonus;
+ // h Is positive when I'm higher than the target.
+ let h = selfPosition.y - targetPosition.y + range.elevationBonus;
- let parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h);
- // No negative roots please
- if (h <= -range.max / 2)
+ // No negative roots please.
+ let parabolicMaxRange;
+ if (h > -range.max / 2)
+ parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h);
+ else
// return false? Or hope you come close enough?
parabolicMaxRange = 0;
@@ -4313,7 +4462,11 @@
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);
+
+ if (attackEntity)
+ return cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange);
+
+ return cmpUnitMotion.MoveToPointRange(target.x, target.z, range.min, guessedMaxRange);
};
UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
@@ -4325,6 +4478,50 @@
return cmpUnitMotion.MoveToTargetRange(target, min, max);
};
+/**
+ * Move formation so we hope the target is within the attack range.
+ *
+ * @param {number | Vector3D} target - Either the target entity ID, or a Vector3D of a position to attack.
+ * @return {boolean} - Whether the order to move has succeeded?
+ */
+UnitAI.prototype.MoveFormationToTargetAttackRange = function(target)
+{
+ let attackEntity = typeof target == "number";
+ // 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;
+ }
+
+ if (attackEntity)
+ {
+ let cmpFormation = Engine.QueryInterface(target, IID_Formation);
+ if (cmpFormation)
+ target = cmpFormation.GetClosestMember(this.entity);
+ }
+
+ if (!this.CheckTargetVisible(target) || this.IsTurret())
+ return false;
+
+ let cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack);
+ if (!cmpFormationAttack)
+ return false;
+ let range = cmpFormationAttack.GetRange(target);
+
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ if (cmpUnitMotion)
+ {
+ if (attackEntity)
+ return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
+ else
+ return cmpUnitMotion.MoveToPointRange(target.x, target.z, range.min, range.max, true);
+ }
+
+ return false;
+};
+
UnitAI.prototype.MoveToGarrisonRange = function(target)
{
if (!this.CheckTargetVisible(target))
@@ -4367,26 +4564,44 @@
return cmpObstructionManager.IsInPointRange(this.entity, x, z, min, max, false);
};
+/**
+ * Check if the target is inside the range.
+ *
+ * @param {number | Vector3D} target - Either the target entity ID, or a Vector3D of a position to attack.
+ * @param {iid} number - The type of the interface which ought to be querried.
+ * @param {string} type - The type of the attack which is to be used.
+ * @return {boolean} - Whether the location is within range.
+ */
UnitAI.prototype.CheckTargetRange = function(target, iid, type)
{
- var cmpRanged = Engine.QueryInterface(this.entity, iid);
+ let attackEntity = typeof target == "number";
+ let cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return false;
- var range = cmpRanged.GetRange(type);
+ let range = cmpRanged.GetRange(type);
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
- return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
+ if (attackEntity)
+ return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
+
+ return cmpObstructionManager.IsInPointRange(this.entity, target.x, target.z, 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
+ * Check if the target is inside the attack range.
+ * For melee attacks, this goes straight 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
+ * when the target is lower and smaller ranges when the target is higher.
+ *
+ * @param {number | Vector3D} target - Either the target entity ID, or a Vector3D of a position to attack.
+ * @param {string} type - The type of the attack which is to be used.
+ * @return {boolean} - Whether the attack-location is within attacking distance.
*/
UnitAI.prototype.CheckTargetAttackRange = function(target, type)
{
- // for formation members, the formation will take care of the range check
+ let attackEntity = typeof target == "number";
+
+ // For formation members, the formation will take care of the range check.
if (this.IsFormationMember())
{
let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
@@ -4395,36 +4610,40 @@
return true;
}
- let cmpFormation = Engine.QueryInterface(target, IID_Formation);
- if (cmpFormation)
- target = cmpFormation.GetClosestMember(this.entity);
+ if (attackEntity)
+ {
+ let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation);
+ if (cmpTargetFormation)
+ target = cmpTargetFormation.GetClosestMember(this.entity);
+ }
if (type != "Ranged")
return this.CheckTargetRange(target, IID_Attack, type);
- let targetCmpPosition = Engine.QueryInterface(target, IID_Position);
- if (!targetCmpPosition || !targetCmpPosition.IsInWorld())
+ let targetPosition = this.GetTargetPosition3D(target);
+ if (!targetPosition)
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())
+ let cmpSelfPosition = Engine.QueryInterface(this.entity, IID_Position);
+ if (!cmpSelfPosition.IsInWorld())
return false;
+ let selfPosition = cmpSelfPosition.GetPosition();
- let s = thisCmpPosition.GetPosition();
-
- let t = targetCmpPosition.GetPosition();
-
- let h = s.y - t.y + range.elevationBonus;
+ let h = selfPosition.y - targetPosition.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);
+
+ if (attackEntity)
+ return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, maxRange, false);
+
+ return cmpObstructionManager.IsInPointRange(this.entity, target.x, target.z, range.min, maxRange, false);
};
UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
@@ -4433,6 +4652,43 @@
return cmpObstructionManager.IsInTargetRange(this.entity, target, min, max, false);
};
+/**
+ * Check if the target is inside the attack range of the formation.
+ *
+ * @param {number | Vector3D} target - Either the target entity ID, or a Vector3D of a position to attack.
+ * @return {boolean} - Whether the attack-location is within attacking distance.
+ */
+UnitAI.prototype.CheckFormationTargetAttackRange = function(target)
+{
+ let attackEntity = typeof target == "number";
+ // 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;
+ }
+
+ if (attackEntity)
+ {
+ let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation);
+ if (cmpTargetFormation)
+ target = cmpTargetFormation.GetClosestMember(this.entity);
+ }
+
+ let cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack);
+ if (!cmpFormationAttack)
+ return false;
+ let range = cmpFormationAttack.GetRange(target);
+
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ if (attackEntity)
+ return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
+
+ return cmpObstructionManager.IsInPointRange(this.entity, target.x, target.z, range.min, range.max, false);
+};
+
UnitAI.prototype.CheckGarrisonRange = function(target)
{
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
@@ -4450,19 +4706,25 @@
/**
* Returns true if the target entity is visible through the FoW/SoD.
+ * @param {number | Vector3D} target - Either an entity ID, or a Vector3D of a position.
+ * @return {boolean} Whether the target is visible.
*/
UnitAI.prototype.CheckTargetVisible = function(target)
{
- var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ // Assume an attackground target is either visible or not important whether it is.
+ if (target instanceof Vector3D)
+ return true;
+
+ let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
- var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ let 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);
+ let cmpFogging = Engine.QueryInterface(target, IID_Fogging);
if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner()))
return true;
@@ -4499,12 +4761,10 @@
*/
UnitAI.prototype.FaceTowardsTarget = function(target)
{
- let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
- if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+ let targetPosition = this.GetTargetPosition2D(target);
+ if (!targetPosition)
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)
@@ -4681,7 +4941,8 @@
*/
UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
{
- if (this.IsTurret())
+ // When attacking ground or are turret, we can't chase.
+ if (target instanceof Vector3D || this.IsTurret())
return false;
if (this.GetStance().respondChase)
@@ -4751,6 +5012,43 @@
this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true });
};
+/*
+ * Returns the 3D target position.
+ * @param {number | Vector3D} target - Either the target entity ID, or a Vector3D of a position to attack.
+ * @return {Vector3D} - The 3D-position of the target.
+ */
+UnitAI.prototype.GetTargetPosition3D = function(target)
+{
+ if (typeof target == "number")
+ {
+ let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
+ if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+ return undefined;
+
+ return cmpTargetPosition.GetPosition();
+ }
+
+ return target;
+};
+
+/*
+ * Returns the 2D target position.
+ * @param {number | Vector3D} target - Either the target entity ID, or a Vector3D of a position to attack.
+ * @return {Vector3D} - The 3D-position of the target.
+ */
+UnitAI.prototype.GetTargetPosition2D = function(target)
+{
+ if (typeof target == "number")
+ {
+ let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
+ if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+ return undefined;
+
+ return cmpTargetPosition.GetPosition2D();
+ }
+ return Vector2D.from3D(target);
+};
+
UnitAI.prototype.GetTargetPositions = function()
{
var targetPositions = [];
@@ -4768,6 +5066,10 @@
targetPositions.push(new Vector2D(order.data.x, order.data.z));
break; // and continue the loop
+ case "AttackGround":
+ targetPositions.push(Vector2D.from3D(order.data.target));
+ break;
+
case "WalkToTarget":
case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
case "Guard":
@@ -5043,6 +5345,23 @@
};
/**
+ * Adds AttackGround order to queue, forced by the player.
+ *
+ * @param {Vector3D} target - The x,y,z-values where the entities need to attack ground.
+ * @param {number} radius - The radius from the target in which the entities need to attack ground.
+ * @param {boolean} queued - Whether the order is queued or not.
+ */
+UnitAI.prototype.AttackGround = function(target, radius, queued)
+{
+ if (this.CanAttack(target))
+ this.AddOrder("AttackGround", {
+ "target": target,
+ "radius": radius,
+ "force": true
+ }, queued);
+};
+
+/**
* Adds garrison order to the queue, forced by the player.
*/
UnitAI.prototype.Garrison = function(target, queued)
@@ -5651,9 +5970,10 @@
{
if (!orderData)
orderData = this.order.data;
- let cmpPosition = Engine.QueryInterface(orderData.target, IID_Position);
- if (cmpPosition && cmpPosition.IsInWorld())
- orderData.lastPos = cmpPosition.GetPosition();
+
+ let targetPosition = this.GetTargetPosition3D(orderData.target);
+ if (targetPosition)
+ orderData.lastPos = targetPosition;
};
UnitAI.prototype.SetHeldPosition = function(x, z)
Index: binaries/data/mods/public/simulation/components/tests/test_Attack.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_Attack.js
+++ binaries/data/mods/public/simulation/components/tests/test_Attack.js
@@ -101,7 +101,8 @@
"Multiplier": 3
}
}
- }
+ },
+ "AttackGround": {}
},
"Capture": {
"Value": 8,
@@ -152,6 +153,10 @@
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80 });
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackStrengths("Capture"), { "value": 8 });
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.CanAttackGround("Capture"), false);
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.CanAttackGround("Ranged"), true);
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.CanAttackGround(), true);
+
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackStrengths("Ranged"), {
"Hack": 0,
"Pierce": 10,
Index: binaries/data/mods/public/simulation/components/tests/test_Identity.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_Identity.js
+++ binaries/data/mods/public/simulation/components/tests/test_Identity.js
@@ -2,12 +2,13 @@
let cmpIdentity = ConstructComponent(5, "Identity", {
"Civ": "iber",
- "GenericName": "Iberian Skirmisher"
+ "GenericName": "Iberian Skirmisher",
+ "Phenotype": "male",
});
TS_ASSERT_EQUALS(cmpIdentity.GetCiv(), "iber");
TS_ASSERT_EQUALS(cmpIdentity.GetLang(), "greek");
-TS_ASSERT_EQUALS(cmpIdentity.GetGender(), "male");
+TS_ASSERT_EQUALS(cmpIdentity.GetPhenotype(), "male");
TS_ASSERT_EQUALS(cmpIdentity.GetRank(), "");
TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetClassesList(), []);
TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetVisibleClassesList(), []);
@@ -20,7 +21,7 @@
cmpIdentity = ConstructComponent(6, "Identity", {
"Civ": "iber",
"Lang": "iberian",
- "Gender": "female",
+ "Phenotype": "female",
"GenericName": "Iberian Skirmisher",
"SpecificName": "Lusitano Ezpatari",
"SelectionGroupName": "units/iber_infantry_javelinist_b",
@@ -39,7 +40,7 @@
TS_ASSERT_EQUALS(cmpIdentity.GetCiv(), "iber");
TS_ASSERT_EQUALS(cmpIdentity.GetLang(), "iberian");
-TS_ASSERT_EQUALS(cmpIdentity.GetGender(), "female");
+TS_ASSERT_EQUALS(cmpIdentity.GetPhenotype(), "female");
TS_ASSERT_EQUALS(cmpIdentity.GetRank(), "Basic");
TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetClassesList(), ["CitizenSoldier", "Human", "Organic", "Javelin", "Basic"]);
TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetVisibleClassesList(), ["Javelin"]);
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js
+++ binaries/data/mods/public/simulation/helpers/Commands.js
@@ -202,6 +202,21 @@
});
},
+ "attack-ground": function(player, cmd, data)
+ {
+ let entities = [];
+ let target = new Vector3D(cmd.target.x, cmd.target.y, cmd.target.z);
+ for (let ent of data.entities)
+ {
+ let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
+ if (cmpAttack && cmpAttack.CanAttackGround())
+ entities.push(ent);
+ }
+ GetFormationUnitAIs(entities, player).forEach(cmpUnitAI => {
+ cmpUnitAI.AttackGround(target, cmd.radius, cmd.queued);
+ });
+ },
+
"patrol": function(player, cmd, data)
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
Index: binaries/data/mods/public/simulation/templates/template_gaia.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_gaia.xml
+++ binaries/data/mods/public/simulation/templates/template_gaia.xml
@@ -5,6 +5,7 @@
gaia
Gaia
+ default
true
Index: binaries/data/mods/public/simulation/templates/template_structure.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_structure.xml
+++ binaries/data/mods/public/simulation/templates/template_structure.xml
@@ -60,6 +60,7 @@
gaia
Structure
Structure
+ default
false
Index: binaries/data/mods/public/simulation/templates/template_unit.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit.xml
+++ binaries/data/mods/public/simulation/templates/template_unit.xml
@@ -51,6 +51,7 @@
special/formations/flank
special/formations/battle_line
+ male
false
Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml
@@ -80,11 +80,11 @@
-
- voice/{lang}/civ/civ_{gender}_walk.xml
- voice/{lang}/civ/civ_{gender}_attack.xml
- voice/{lang}/civ/civ_{gender}_gather.xml
- voice/{lang}/civ/civ_{gender}_garrison.xml
+
+ voice/{lang}/civ/civ_{phenotype}_walk.xml
+ voice/{lang}/civ/civ_{phenotype}_attack.xml
+ voice/{lang}/civ/civ_{phenotype}_gather.xml
+ voice/{lang}/civ/civ_{phenotype}_garrison.xml
actor/mounted/movement/walk.xml
actor/mounted/movement/walk.xml
attack/impact/arrow_metal.xml
Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml
@@ -42,11 +42,11 @@
-
- voice/{lang}/civ/civ_{gender}_walk.xml
- voice/{lang}/civ/civ_{gender}_attack.xml
- voice/{lang}/civ/civ_{gender}_gather.xml
- voice/{lang}/civ/civ_{gender}_garrison.xml
+
+ voice/{lang}/civ/civ_{phenotype}_walk.xml
+ voice/{lang}/civ/civ_{phenotype}_attack.xml
+ voice/{lang}/civ/civ_{phenotype}_gather.xml
+ voice/{lang}/civ/civ_{phenotype}_garrison.xml
actor/mounted/movement/walk.xml
actor/mounted/movement/walk.xml
attack/weapon/sword.xml
Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry.xml
@@ -32,16 +32,16 @@
interface/alarm/alarm_create_infantry.xml
-
- voice/{lang}/civ/civ_{gender}_walk.xml
- voice/{lang}/civ/civ_{gender}_attack.xml
- voice/{lang}/civ/civ_{gender}_gather.xml
- voice/{lang}/civ/civ_{gender}_repair.xml
- voice/{lang}/civ/civ_{gender}_garrison.xml
+
+ voice/{lang}/civ/civ_{phenotype}_walk.xml
+ voice/{lang}/civ/civ_{phenotype}_attack.xml
+ voice/{lang}/civ/civ_{phenotype}_gather.xml
+ voice/{lang}/civ/civ_{phenotype}_repair.xml
+ voice/{lang}/civ/civ_{phenotype}_garrison.xml
actor/human/movement/walk.xml
actor/human/movement/walk.xml
attack/weapon/sword.xml
- actor/human/death/{gender}_death.xml
+ actor/human/death/{phenotype}_death.xml
Index: binaries/data/mods/public/simulation/templates/template_unit_hero.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_hero.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_hero.xml
@@ -54,20 +54,20 @@
-
+
interface/alarm/alarm_create_infantry.xml
- voice/{lang}/civ/civ_{gender}_heal.xml
- voice/{lang}/civ/civ_{gender}_walk.xml
- voice/{lang}/civ/civ_{gender}_attack.xml
- voice/{lang}/civ/civ_{gender}_gather.xml
- voice/{lang}/civ/civ_{gender}_repair.xml
- voice/{lang}/civ/civ_{gender}_garrison.xml
+ voice/{lang}/civ/civ_{phenotype}_heal.xml
+ voice/{lang}/civ/civ_{phenotype}_walk.xml
+ voice/{lang}/civ/civ_{phenotype}_attack.xml
+ voice/{lang}/civ/civ_{phenotype}_gather.xml
+ voice/{lang}/civ/civ_{phenotype}_repair.xml
+ voice/{lang}/civ/civ_{phenotype}_garrison.xml
attack/weapon/sword.xml
attack/impact/arrow_metal.xml
attack/weapon/arrowfly.xml
actor/human/movement/walk.xml
actor/human/movement/walk.xml
- actor/human/death/{gender}_death.xml
+ actor/human/death/{phenotype}_death.xml
Index: binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
@@ -100,18 +100,18 @@
-
- voice/{lang}/civ/civ_{gender}_walk.xml
- voice/{lang}/civ/civ_{gender}_attack.xml
- voice/{lang}/civ/civ_{gender}_gather.xml
- voice/{lang}/civ/civ_{gender}_repair.xml
- voice/{lang}/civ/civ_{gender}_garrison.xml
+
+ voice/{lang}/civ/civ_{phenotype}_walk.xml
+ voice/{lang}/civ/civ_{phenotype}_attack.xml
+ voice/{lang}/civ/civ_{phenotype}_gather.xml
+ voice/{lang}/civ/civ_{phenotype}_repair.xml
+ voice/{lang}/civ/civ_{phenotype}_garrison.xml
actor/human/movement/walk.xml
actor/human/movement/run.xml
attack/impact/arrow_metal.xml
attack/weapon/sword.xml
attack/weapon/arrowfly.xml
- actor/human/death/{gender}_death.xml
+ actor/human/death/{phenotype}_death.xml
resource/construction/con_wood.xml
resource/foraging/forage_leaves.xml
resource/farming/farm.xml
Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml
@@ -24,6 +24,7 @@
Human
+
Index: binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml
@@ -48,7 +48,7 @@
Female Citizen
- female
+ female
FemaleCitizen
Citizen Worker
@@ -70,15 +70,15 @@
interface/alarm/alarm_create_female.xml
-
- voice/{lang}/civ/civ_{gender}_walk.xml
- voice/{lang}/civ/civ_{gender}_attack.xml
- voice/{lang}/civ/civ_{gender}_build.xml
- voice/{lang}/civ/civ_{gender}_gather.xml
- voice/{lang}/civ/civ_{gender}_repair.xml
- voice/{lang}/civ/civ_{gender}_garrison.xml
+
+ voice/{lang}/civ/civ_{phenotype}_walk.xml
+ voice/{lang}/civ/civ_{phenotype}_attack.xml
+ voice/{lang}/civ/civ_{phenotype}_build.xml
+ voice/{lang}/civ/civ_{phenotype}_gather.xml
+ voice/{lang}/civ/civ_{phenotype}_repair.xml
+ voice/{lang}/civ/civ_{phenotype}_garrison.xml
attack/weapon/sword.xml
- actor/human/death/{gender}_death.xml
+ actor/human/death/{phenotype}_death.xml
resource/construction/con_wood.xml
resource/foraging/forage_leaves.xml
resource/farming/farm.xml
Index: binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml
@@ -42,16 +42,16 @@
interface/alarm/alarm_create_infantry.xml
-
- voice/{lang}/civ/civ_{gender}_walk.xml
- voice/{lang}/civ/civ_{gender}_attack.xml
- voice/{lang}/civ/civ_{gender}_gather.xml
- voice/{lang}/civ/civ_{gender}_repair.xml
- voice/{lang}/civ/civ_{gender}_heal.xml
- voice/{lang}/civ/civ_{gender}_garrison.xml
+
+ voice/{lang}/civ/civ_{phenotype}_walk.xml
+ voice/{lang}/civ/civ_{phenotype}_attack.xml
+ voice/{lang}/civ/civ_{phenotype}_gather.xml
+ voice/{lang}/civ/civ_{phenotype}_repair.xml
+ voice/{lang}/civ/civ_{phenotype}_heal.xml
+ voice/{lang}/civ/civ_{phenotype}_garrison.xml
actor/human/movement/walk.xml
actor/human/movement/run.xml
- actor/human/death/{gender}_death.xml
+ actor/human/death/{phenotype}_death.xml
Index: binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml
@@ -65,14 +65,14 @@
-
- voice/{lang}/civ/civ_{gender}_walk.xml
- voice/{lang}/civ/civ_{gender}_gather.xml
- voice/{lang}/civ/civ_{gender}_repair.xml
- voice/{lang}/civ/civ_{gender}_garrison.xml
+
+ voice/{lang}/civ/civ_{phenotype}_walk.xml
+ voice/{lang}/civ/civ_{phenotype}_gather.xml
+ voice/{lang}/civ/civ_{phenotype}_repair.xml
+ voice/{lang}/civ/civ_{phenotype}_garrison.xml
actor/human/movement/walk.xml
actor/human/movement/run.xml
- actor/human/death/{gender}_death.xml
+ actor/human/death/{phenotype}_death.xml
resource/construction/con_wood.xml
resource/foraging/forage_leaves.xml
resource/farming/farm.xml
Index: binaries/data/mods/public/simulation/templates/template_unit_support_trader.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_support_trader.xml
+++ binaries/data/mods/public/simulation/templates/template_unit_support_trader.xml
@@ -19,16 +19,16 @@
-
- voice/{lang}/civ/civ_{gender}_trade.xml
- voice/{lang}/civ/civ_{gender}_walk.xml
- voice/{lang}/civ/civ_{gender}_attack.xml
- voice/{lang}/civ/civ_{gender}_gather.xml
- voice/{lang}/civ/civ_{gender}_repair.xml
+
+ voice/{lang}/civ/civ_{phenotype}_trade.xml
+ voice/{lang}/civ/civ_{phenotype}_walk.xml
+ voice/{lang}/civ/civ_{phenotype}_attack.xml
+ voice/{lang}/civ/civ_{phenotype}_gather.xml
+ voice/{lang}/civ/civ_{phenotype}_repair.xml
actor/human/movement/walk.xml
actor/human/movement/run.xml
attack/weapon/sword.xml
- actor/human/death/{gender}_death.xml
+ actor/human/death/{phenotype}_death.xml
resource/construction/con_wood.xml
resource/foraging/forage_leaves.xml
resource/farming/farm.xml
Index: binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca.xml
+++ binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca.xml
@@ -12,7 +12,7 @@
Chariot
Boudicca (Chariot)
Boadicea
- female
+ female
units/brit_hero_boudicca.png
Index: binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca_cavalry_javelinist.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca_cavalry_javelinist.xml
+++ binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca_cavalry_javelinist.xml
@@ -7,7 +7,7 @@
brit
Boudicca (Sword)
Boadicea
- female
+ female
units/brit_hero_boudicca.png
Index: binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca_sword.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca_sword.xml
+++ binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca_sword.xml
@@ -7,7 +7,7 @@
brit
Boudicca (Sword)
Boadicea
- female
+ female
units/brit_hero_boudicca.png
Index: binaries/data/mods/public/simulation/templates/units/cart_support_healer_b.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/cart_support_healer_b.xml
+++ binaries/data/mods/public/simulation/templates/units/cart_support_healer_b.xml
@@ -2,7 +2,7 @@
cart
- female
+ female
units/cart_support_healer_b
Kehinit
units/cart_support_healer.png
Index: binaries/data/mods/public/simulation/templates/units/iber_support_healer_b.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/iber_support_healer_b.xml
+++ binaries/data/mods/public/simulation/templates/units/iber_support_healer_b.xml
@@ -2,7 +2,7 @@
iber
- female
+ female
units/iber_support_healer_b
Priestess of Ataekina
Emakumezko Apaiz de Ataekina
Index: binaries/data/mods/public/simulation/templates/units/kush_hero_amanirenas.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/kush_hero_amanirenas.xml
+++ binaries/data/mods/public/simulation/templates/units/kush_hero_amanirenas.xml
@@ -7,7 +7,7 @@
kush
Amanirenas
Amnirense qore li kdwe li
- female
+ female
units/kush_hero_amanirenas.png
Index: binaries/data/mods/public/simulation/templates/units/kush_hero_amanirenas_chariot.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/kush_hero_amanirenas_chariot.xml
+++ binaries/data/mods/public/simulation/templates/units/kush_hero_amanirenas_chariot.xml
@@ -7,7 +7,7 @@
kush
Amanirenas
Amnirense qore li kdwe li
- female
+ female
units/kush_hero_amanirenas.png
Index: binaries/data/mods/public/simulation/templates/units/maur_champion_maiden.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/maur_champion_maiden.xml
+++ binaries/data/mods/public/simulation/templates/units/maur_champion_maiden.xml
@@ -2,7 +2,7 @@
maur
- female
+ female
Maiden Guard
Visha Kanya
units/maur_champion_maiden
Index: binaries/data/mods/public/simulation/templates/units/maur_champion_maiden_archer.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/maur_champion_maiden_archer.xml
+++ binaries/data/mods/public/simulation/templates/units/maur_champion_maiden_archer.xml
@@ -2,7 +2,7 @@
maur
- female
+ female
Maiden Guard Archer
Visha Kanya
units/maur_champion_maiden_archer.png
Index: binaries/data/mods/public/simulation/templates/units/ptol_hero_cleopatra.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/ptol_hero_cleopatra.xml
+++ binaries/data/mods/public/simulation/templates/units/ptol_hero_cleopatra.xml
@@ -7,7 +7,7 @@
ptol
- female
+ female
Cleopatra VII
Kleopatra H' Philopator
units/ptol_hero_cleopatra.png
Index: source/simulation2/components/CCmpVisualActor.cpp
===================================================================
--- source/simulation2/components/CCmpVisualActor.cpp
+++ source/simulation2/components/CCmpVisualActor.cpp
@@ -23,6 +23,7 @@
#include "simulation2/MessageTypes.h"
#include "ICmpFootprint.h"
+#include "ICmpIdentity.h"
#include "ICmpUnitRenderer.h"
#include "ICmpOwnership.h"
#include "ICmpPosition.h"
@@ -35,6 +36,7 @@
#include "simulation2/serialization/SerializeTemplates.h"
+#include
#include "graphics/Decal.h"
#include "graphics/Frustum.h"
#include "graphics/Model.h"
@@ -198,7 +200,16 @@
if (m_IsFoundationActor)
m_BaseActorName = m_ActorName = paramNode.GetChild("FoundationActor").ToString();
else
- m_BaseActorName = m_ActorName = paramNode.GetChild("Actor").ToString();
+ {
+ std::wstring baseActorString = paramNode.GetChild("Actor").ToString();
+
+ CmpPtr cmpIdentity(GetEntityHandle());
+ const std::wstring pattern = L"{phenotype}";
+ if (cmpIdentity)
+ boost::replace_all(baseActorString , pattern, cmpIdentity->GetPhenotype());
+
+ m_BaseActorName = m_ActorName = baseActorString;
+ }
m_VisibleInAtlasOnly = paramNode.GetChild("VisibleInAtlasOnly").ToBool();
m_IsActorOnly = paramNode.GetChild("ActorOnly").IsOk();
Index: source/simulation2/components/ICmpIdentity.h
===================================================================
--- source/simulation2/components/ICmpIdentity.h
+++ source/simulation2/components/ICmpIdentity.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2011 Wildfire Games.
+/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -29,6 +29,8 @@
public:
virtual std::string GetSelectionGroupName() = 0;
+ virtual std::wstring GetPhenotype() = 0;
+
DECLARE_INTERFACE_TYPE(Identity)
};
Index: source/simulation2/components/ICmpIdentity.cpp
===================================================================
--- source/simulation2/components/ICmpIdentity.cpp
+++ source/simulation2/components/ICmpIdentity.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2011 Wildfire Games.
+/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -35,6 +35,11 @@
{
return m_Script.Call("GetSelectionGroupName");
}
+
+ virtual std::wstring GetPhenotype()
+ {
+ return m_Script.Call("GetPhenotype");
+ }
};
REGISTER_COMPONENT_SCRIPT_WRAPPER(IdentityScripted)