Index: binaries/data/mods/public/gui/common/tooltips.js
===================================================================
--- binaries/data/mods/public/gui/common/tooltips.js
+++ binaries/data/mods/public/gui/common/tooltips.js
@@ -254,6 +254,8 @@
let tooltips = [];
for (let type in template.attack)
{
+ if (type == "attackGround")
+ continue; // Because the ability to attack ground is not per se set per type...
if (type == "Slaughter")
continue; // Slaughter is used to kill animals, so do not show it.
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;
@@ -899,6 +900,10 @@
var 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,6 +917,18 @@
inputState = INPUT_NORMAL;
break;
}
+
+ case "hotkeydown":
+ if (preSelectedAction == ACTION_ATTACKGROUND && ev.hotkey == "camera.zoom.wheel.in" || ev.hotkey == "camera.zoom.in")
+ {
+ OnAttackGroundMouseWheel(-1);
+ warn("Called down! " + uneval(g_AttackGroundSize));
+ }
+ else if (preSelectedAction == ACTION_ATTACKGROUND && ev.hotkey == "camera.zoom.wheel.out" || ev.hotkey == "camera.zoom.out")
+ {
+ OnAttackGroundMouseWheel(1);
+ warn("Called up! " + uneval(g_AttackGroundSize));
+ }
// else
default:
// Slight hack: If selection is empty, reset the input state
@@ -1322,6 +1339,34 @@
}
}
+// Attack ground:
+// When the user uses the hotkey the radius of the bombarded area is increased/decreased.
+var g_AttackGroundSize = getDefaultAttackGroundSize();
+function OnAttackGroundMouseWheel(dir)
+{
+ g_AttackGroundSize += dir / +(Engine.ConfigDB_GetValue("user", "gui.session.scrollattackgroundsizeratio") || 1);
+ if (g_AttackGroundSize < 1 || !Number.isFinite(g_AttackGroundSize))
+ g_AttackGroundSize = 1;
+
+ updateSelectionDetails();
+}
+
+function getDefaultAttackGroundSize()
+{
+ let num = +Engine.ConfigDB_GetValue("user", "gui.session.attackgroundsize");
+ return Number.isInteger(num) && num > 0 ? num : 5;
+}
+
+function getAttackGroundSize()
+{
+ return Math.max(Math.round(g_AttackGroundSize), 1);
+}
+
+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,40 @@
"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)
+ {
+ return { "possible": true };
+ },
+ "preSelectedActionCheck": function(target, selection)
+ {
+ if (preSelectedAction != ACTION_ATTACKGROUND || !getActionInfo("attack-ground", target, selection).possible)
+ return false;
+
+ return {
+ "type": "attack-ground",
+ "cursor": "action-attack",
+ "target": target
+ };
+ },
+ "specificness": 50,
+ },
+
+
"patrol":
{
"execute": function(target, action, selection, queued)
@@ -1095,6 +1129,25 @@
},
},
+ "attack-ground": {
+ "getInfo": function(entStates)
+ {
+ if (entStates.every(entState => !entState.attack || !entState.attack.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)
{
@@ -1513,7 +1566,7 @@
}
return {
- "possible": ["move", "attack-move", "remove-guard", "patrol"].indexOf(action) != -1
+ "possible": ["move", "attack-move", "remove-guard", "patrol", "attack-ground"].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
@@ -111,6 +111,7 @@
"10.0" +
"0.0" +
"" +
+ "" +
"" +
"" +
"1000.0" +
@@ -178,6 +179,9 @@
"" +
"" +
"" +
+ ""+
+ "" +
+ "" +
"" +
"" +
"" +
@@ -261,6 +265,14 @@
(!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1));
};
+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.GetPreferredClasses = function(type)
{
if (this.template[type] && this.template[type].PreferredClasses &&
@@ -281,6 +293,9 @@
Attack.prototype.CanAttack = function(target, wantedTypes)
{
+ if (target instanceof Vector3D)
+ return this.CanAttackGround();
+
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
return true;
@@ -381,6 +396,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)
{
@@ -504,12 +523,15 @@
};
/**
- * 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)
{
+ let attackGround = target instanceof Vector3D;
let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner();
let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
@@ -530,16 +552,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 (attackGround)
+ predictedPosition = target;
+ else
+ {
+ 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;
+ }
// Add inaccuracy based on spread.
let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", +this.template[type].Projectile.Spread, this.entity) *
@@ -549,11 +578,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.
@@ -106,6 +106,8 @@
if (!data.position)
return;
+ let attackGround = data.target instanceof Vector3D;
+
let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager);
if (cmpSoundManager && data.attackImpactSound)
cmpSoundManager.PlaySoundGroupAtPosition(data.attackImpactSound, data.position);
@@ -129,27 +131,32 @@
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))
+ if (!attackGround)
{
- 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;
+ }
}
- let targetPosition = this.InterpolatedLocation(data.target, lateness);
+ let targetPosition = attackGround ? data.target :
+ 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
@@ -430,6 +430,8 @@
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
+
+ ret.attack.attackGround = cmpAttack.CanAttackGround();
}
let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
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
@@ -393,7 +393,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
@@ -414,34 +414,34 @@
return;
}
- if (this.IsAnimal())
- this.SetNextState("ANIMAL.COMBAT.ATTACKING");
- else
- this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
+ this.IsAnimal() ? this.SetNextState("ANIMAL.COMBAT.ATTACKING") :
+ this.SetNextState("INDIVIDUAL.COMBAT");
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.IsAnimal() ? this.SetNextState("ANIMAL.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;
- // 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;
- }
+ // Distribute the attacks.
+ 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);
- if (this.IsAnimal())
- this.SetNextState("ANIMAL.COMBAT.APPROACHING");
- else
- this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
+ this.SetNextState("INDIVIDUAL.COMBAT");
},
"Order.Patrol": function(msg) {
@@ -697,15 +697,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))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
@@ -716,9 +716,22 @@
return;
}
this.CallMemberFunction("Attack", [target, allowCapture, false]);
- if (cmpAttack.CanAttackAsFormation())
- this.SetNextState("COMBAT.ATTACKING");
- else
+ cmpAttack.CanAttackAsFormation() ? this.SetNextState("COMBAT.ATTACKING") :
+ 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]);
+ cmpAttack.CanAttackAsFormation() ? this.SetNextState("COMBAT.ATTACKING") :
this.SetNextState("MEMBER");
},
@@ -1082,14 +1095,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() {
@@ -1097,9 +1113,13 @@
},
"MovementUpdate": function(msg) {
+ let target = this.order.data.target;
+ let attackGround = target instanceof Vector3D;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
- this.CallMemberFunction("Attack", [this.order.data.target, this.order.data.allowCapture, false]);
- if (cmpAttack.CanAttackAsFormation())
+ attackGround ? this.CallMemberFunction("AttackGround", [target, false]) :
+ this.CallMemberFunction("Attack", [target, this.order.data.allowCapture, false]);
+
+ if (cmpAttack && cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
else
this.SetNextState("MEMBER");
@@ -1109,39 +1129,46 @@
"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))
+ let target = this.order.data.target;
+ let attackGround = target instanceof Vector3D;
+ let allowCapture = this.order.data.allowCapture;
+ // Check if we are already in range, otherwise walk there.
+ if (!this.CheckFormationTargetAttackRange(target))
{
- if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
+ if (this.TargetIsAlive(target) && this.CheckTargetVisible(target) || attackGround)
{
this.FinishOrder();
- this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
+ attackGround ? this.PushOrderFront("AttackGround", { "target": target, "force": false }) :
+ this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
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;
- // Check if we are already in range, otherwise walk there
- if (!this.CheckTargetAttackRange(target, target))
+ let target = this.order.data.target;
+ let attackGround = target instanceof Vector3D;
+ let allowCapture = this.order.data.allowCapture;
+ // Check if we are already in range, otherwise walk there.
+ if (!this.CheckFormationTargetAttackRange(target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
this.FinishOrder();
- this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
+ attackGround ? this.PushOrderFront("AttackGround", { "target": target, "force": false }) :
+ this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
return;
}
this.FinishOrder();
@@ -1151,7 +1178,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);
},
@@ -1722,6 +1749,43 @@
},
"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;
+ }
+
+ // 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;
+ }
+
+ this.SetNextState("APPROACHING");
+ },
+
"Order.LeaveFoundation": function(msg) {
// Ignore the order as we're busy.
return { "discardOrder": true };
@@ -1746,7 +1810,10 @@
this.SetAnimationVariant("combat");
this.SelectAnimation("move");
- this.StartTimer(1000, 1000);
+
+ // If attack ground is asked do not start the timer (ground does not run away).
+ if (!(this.order.data.target instanceof Vector3D))
+ this.StartTimer(1000, 1000);
},
"leave": function() {
@@ -1783,53 +1850,56 @@
"ATTACKING": {
"enter": function() {
- var target = this.order.data.target;
- var cmpFormation = Engine.QueryInterface(target, IID_Formation);
- // if the target is a formation, save the attacking formation, and pick a member
- if (cmpFormation)
+ let target = this.order.data.target;
+ let attackGround = target instanceof Vector3D;
+
+ if (!attackGround)
{
- this.order.data.formationTarget = target;
- target = cmpFormation.GetClosestMember(this.entity);
- this.order.data.target = 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;
+ }
}
- // Check the target is still alive and attackable
- if (this.CanAttack(target) && !this.CheckTargetAttackRange(target, this.order.data.attackType))
+
+ // Check the target is still alive and attackable.
+ // If out of range, try to chase after it.
+ if (this.CanAttack(target) && !this.CheckTargetAttackRange(target, this.order.data.attackType) && this.ShouldChaseTarget(target, this.order.data.force))
{
- // Can't reach it - try to chase after it
- if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
+ if (this.CanPack())
{
- if (this.CanPack())
- {
- this.PushOrderFront("Pack", { "force": true });
- return;
- }
-
- this.SetNextState("COMBAT.CHASING");
- return true;
+ this.PushOrderFront("Pack", { "force": true });
+ return;
}
+
+ this.SetNextState("COMBAT.CHASING");
+ return true;
}
this.StopMoving();
- var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
+ 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.
- var prepare = this.attackTimers.prepare;
+ let prepare = this.attackTimers.prepare;
if (this.lastAttacked)
{
- var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
- var repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime();
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ let repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime();
prepare = Math.max(prepare, repeatLeft);
}
this.oldAttackType = this.order.data.attackType;
// add prefix + no capital first letter for attackType
- var animationName = "attack_" + this.order.data.attackType.toLowerCase();
+ let animationName = "attack_" + this.order.data.attackType.toLowerCase();
if (this.IsFormationMember())
{
- var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
+ let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
animationName = cmpFormation.GetFormationAnimation(this.entity, animationName);
}
@@ -1844,13 +1914,19 @@
this.FaceTowardsTarget(this.order.data.target);
- var cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
+ let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
+ // Attack ground with BuildingAI not yet implemented.
+ if (cmpBuildingAI && attackGround)
+ {
+ this.FinishOrder();
+ return true;
+ }
if (cmpBuildingAI)
cmpBuildingAI.SetUnitAITarget(this.order.data.target);
},
"leave": function() {
- var cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
+ let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
if (cmpBuildingAI)
cmpBuildingAI.SetUnitAITarget(0);
this.StopTimer();
@@ -1858,18 +1934,23 @@
},
"Timer": function(msg) {
- var target = this.order.data.target;
- var cmpFormation = Engine.QueryInterface(target, IID_Formation);
- // if the target is a formation, save the attacking formation, and pick a member
- if (cmpFormation)
+ let target = this.order.data.target;
+ let attackGround = target instanceof Vector3D;
+
+ if (!attackGround)
{
- var thisObject = this;
- var filter = function(t) {
- return thisObject.CanAttack(t);
- };
- this.order.data.formationTarget = target;
- target = cmpFormation.GetClosestMember(this.entity, filter);
- this.order.data.target = 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 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
if (this.CanAttack(target))
@@ -1877,7 +1958,7 @@
// If we are hunting, first update the target position of the gather order so we know where will be the killed animal
if (this.order.data.hunting && this.orderQueue[1] && this.orderQueue[1].data.lastPos)
{
- var cmpPosition = Engine.QueryInterface(this.order.data.target, IID_Position);
+ let cmpPosition = Engine.QueryInterface(this.order.data.target, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
// Store the initial position, so that we can find the rest of the herd later
@@ -1889,13 +1970,13 @@
}
}
- var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastAttacked = cmpTimer.GetTime() - msg.lateness;
this.FaceTowardsTarget(target);
// BuildingAI has it's own attack-routine
- var cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
+ let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
if (!cmpBuildingAI)
{
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
@@ -1914,7 +1995,7 @@
}
// Can't reach it - try to chase after it
- if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
+ if (this.ShouldChaseTarget(target, this.order.data.force))
{
if (this.CanPack())
{
@@ -1924,10 +2005,15 @@
this.SetNextState("COMBAT.CHASING");
return;
}
+ else if (this.MoveToTargetAttackRange(target, this.order.data.attackType))
+ {
+ this.SetNextState("COMBAT.APPROACHING");
+ return;
+ }
}
// if we're targetting a formation, find a new member of that formation
- var cmpTargetFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation);
+ let cmpTargetFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation);
// if there is no target, it means previously searching for the target inside the target formation failed, so don't repeat the search
if (target && cmpTargetFormation)
{
@@ -1937,7 +2023,7 @@
}
// 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 ennemies around before moving again
+ // Except if in WalkAndFight mode where we look for more enemies around before moving again
if (this.FinishOrder())
{
if (this.IsWalkingAndFighting())
@@ -2377,7 +2463,7 @@
return;
}
// Can't reach it - try to chase after it
- if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
+ if (this.ShouldChaseTarget(target, this.order.data.force))
{
if (this.CanPack())
{
@@ -4145,76 +4231,88 @@
UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
{
+ let attackGround = target instanceof Vector3D;
+
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);
- var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ return attackGround ? cmpUnitMotion.MoveToPointRange(target.x, target.z, range.min, range.max, true) :
+ 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
+ *
+ * @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)
{
+ let attackGround = target instanceof Vector3D;
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
{
- var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
+ let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation())
return false;
}
- var cmpFormation = Engine.QueryInterface(target, IID_Formation);
- if (cmpFormation)
- target = cmpFormation.GetClosestMember(this.entity);
+ if (!attackGround)
+ {
+ 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))
+ if (!attackGround && !this.CheckTargetVisible(target))
return false;
- var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
- var range = cmpAttack.GetRange(type);
+ let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
+ let range = cmpAttack.GetRange(type);
- var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- if (!thisCmpPosition.IsInWorld())
+ let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
+ if (!cmpThisPosition.IsInWorld())
return false;
- var s = thisCmpPosition.GetPosition();
+ let selfPosition = cmpThisPosition.GetPosition();
- var targetCmpPosition = Engine.QueryInterface(target, IID_Position);
- if (!targetCmpPosition.IsInWorld())
+ let targetPosition = this.GetTargetPosition3D(target);
+ if (!targetPosition)
return false;
- var t = targetCmpPosition.GetPosition();
- // h is positive when I'm higher than the target
- var 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;
- // No negative roots please
- if (h>-range.max/2)
- var parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h);
- else
- // return false? Or hope you come close enough?
- var parabolicMaxRange = 0;
- //return false;
+ // No negative roots please.
+ let parabolicMaxRange = 0;
+ if (h > -range.max / 2)
+ parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h);
- // the parabole changes while walking, take something in the middle
- var guessedMaxRange = (range.max + parabolicMaxRange)/2;
+ // The parabole changes while walking, take something in the middle.
+ let guessedMaxRange = (range.max + parabolicMaxRange) / 2;
- var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- if (cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange))
- return true;
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ if (!cmpUnitMotion)
+ return false;
- // if that failed, try closer
- return cmpUnitMotion.MoveToTargetRange(target, range.min, Math.min(range.max, parabolicMaxRange));
+ // If guessedMaxRange fails, try closer.
+ if (attackGround)
+ return cmpUnitMotion.MoveToPointRange(target.x, target.z, range.min, guessedMaxRange) ||
+ cmpUnitMotion.MoveToPointRange(target.x, target.z, range.min, Math.min(range.max, parabolicMaxRange));
+
+ return cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange) ||
+ cmpUnitMotion.MoveToTargetRange(target, range.min, Math.min(range.max, parabolicMaxRange));
};
UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
@@ -4226,6 +4324,45 @@
return cmpUnitMotion.MoveToTargetRange(target, min, max);
};
+/**
+ * Move unit so we hope the target is in 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 order to move has succeeded?
+ */
+UnitAI.prototype.MoveFormationToTargetAttackRange = function(target)
+{
+ let attackGround = target instanceof Vector3D;
+ // 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 (!attackGround)
+ {
+ 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)
+ return attackGround ? cmpUnitMotion.MoveToPointRange(target.x, target.z, range.min, range.max, true) :
+ cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
+ return false;
+};
+
UnitAI.prototype.MoveToGarrisonRange = function(target)
{
if (!this.CheckTargetVisible(target))
@@ -4268,64 +4405,83 @@
return cmpObstructionManager.IsInPointRange(this.entity, x, z, min, max, true);
};
+/**
+ * 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 attackGround = target instanceof Vector3D;
+ 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, true);
+ return attackGround ? cmpObstructionManager.IsInPointRange(this.entity, target.x, target.z, range.min, range.max, true) :
+ cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, true);
};
/**
* Check if the target is inside the attack range
- * For melee attacks, this goes straigt to the regular range calculation
+ * 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
+ *
+ * @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)
{
+ let attackGround = target instanceof Vector3D;
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
{
- var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
+ let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation()
&& cmpFormationUnitAI.order.data.target == target)
return true;
}
- var cmpFormation = Engine.QueryInterface(target, IID_Formation);
- if (cmpFormation)
- target = cmpFormation.GetClosestMember(this.entity);
+ if (!attackGround)
+ {
+ let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation);
+ if (cmpTargetFormation)
+ target = cmpTargetFormation.GetClosestMember(this.entity);
+ }
if (type != "Ranged")
return this.CheckTargetRange(target, IID_Attack, type);
- var targetCmpPosition = Engine.QueryInterface(target, IID_Position);
- if (!targetCmpPosition || !targetCmpPosition.IsInWorld())
+ let targetPosition = this.GetTargetPosition3D(target);
+ if (!targetPosition)
return false;
- var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
- var range = cmpAttack.GetRange(type);
+ let range;
+ let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
+ if (cmpAttack)
+ range = cmpAttack.GetRange(type);
- var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- if (!thisCmpPosition.IsInWorld())
+ let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
+ if (!cmpThisPosition.IsInWorld())
return false;
- var s = thisCmpPosition.GetPosition();
-
- var t = targetCmpPosition.GetPosition();
+ let selfPosition = cmpThisPosition.GetPosition();
- var h = s.y-t.y+range.elevationBonus;
- var maxRangeSq = 2*range.max*(h + range.max/2);
+ let h = selfPosition.y - targetPosition.y + range.elevationBonus;
+ let maxRangeSq = 2 * range.max * (h + range.max / 2);
if (maxRangeSq < 0)
return false;
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
- return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, Math.sqrt(maxRangeSq), true);
+ return attackGround ? cmpObstructionManager.IsInPointRange(this.entity, target.x, target.z, range.min, Math.sqrt(maxRangeSq), true) :
+ cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, Math.sqrt(maxRangeSq), true);
};
UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
@@ -4334,6 +4490,42 @@
return cmpObstructionManager.IsInTargetRange(this.entity, target, min, max, true);
};
+/**
+ * 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 attackGround = target instanceof Vector3D;
+ // 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 (!attackGround)
+ {
+ 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);
+ return attackGround ? cmpObstructionManager.IsInPointRange(this.entity, target.x, target.z, range.min, range.max, true) :
+ cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, true);
+};
+
+
UnitAI.prototype.CheckGarrisonRange = function(target)
{
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
@@ -4354,16 +4546,20 @@
*/
UnitAI.prototype.CheckTargetVisible = function(target)
{
- var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ // Assume an attackground target is '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;
@@ -4376,22 +4572,16 @@
UnitAI.prototype.FaceTowardsTarget = function(target)
{
- var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
- var cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
- if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+
+ let targetPosition = this.GetTargetPosition2D(target);
+ if (!targetPosition)
return;
- var targetpos = cmpTargetPosition.GetPosition2D();
- var angle = cmpPosition.GetPosition2D().angleTo(targetpos);
- var rot = cmpPosition.GetRotation();
- var delta = (rot.y - angle + Math.PI) % (2 * Math.PI) - Math.PI;
- if (Math.abs(delta) > 0.2)
- {
- var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- if (cmpUnitMotion)
- cmpUnitMotion.FaceTowardsPoint(targetpos.x, targetpos.y);
- }
+
+ let pos = targetPosition.sub(cmpPosition.GetPosition2D());
+ cmpPosition.TurnTo(Math.atan2(pos.x, pos.y));
};
UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type)
@@ -4555,9 +4745,10 @@
* Returns whether we should chase the targeted entity,
* given our current stance.
*/
-UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
+UnitAI.prototype.ShouldChaseTarget = 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)
@@ -4627,6 +4818,40 @@
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 (target instanceof Vector3D)
+ return target;
+
+ let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
+ if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+ return undefined;
+
+ return cmpTargetPosition.GetPosition();
+};
+
+/*
+ * 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 (target instanceof Vector3D)
+ return Vector2D.from3D(target);
+
+ let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
+ if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+ return undefined;
+
+ return cmpTargetPosition.GetPosition2D();
+};
+
UnitAI.prototype.GetTargetPositions = function()
{
var targetPositions = [];
@@ -4644,6 +4869,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":
@@ -4910,6 +5139,19 @@
};
/**
+ * 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)
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_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
+