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;
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,39 @@
"specificness": 10,
},
+ "attack-ground": {
+ "execute": function(target, action, selection, queued)
+ {
+ Engine.PostNetworkCommand({
+ "type": "attack-ground",
+ "entities": selection,
+ "target": target,
+ "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 +1128,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 +1565,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,15 +293,34 @@
Attack.prototype.CanAttack = function(target, wantedTypes)
{
- let cmpFormation = Engine.QueryInterface(target, IID_Formation);
- if (cmpFormation)
- return true;
+ let attackGround = typeof target == "object";
let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position);
- let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
- if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld())
+ if (!cmpThisPosition || !cmpThisPosition.IsInWorld())
return false;
+ let targetHeight = attackGround ? target.y : undefined;
+ if (!attackGround)
+ {
+ let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
+ if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+ return false;
+ targetHeight = cmpTargetPosition.GetHeightFixed();
+ }
+
+ // Check if the height difference is larger than the attack range
+ // If the relative height is bigger, it means they will never be able to
+ // reach each other, no matter how close they come.
+ let heightDiff = Math.abs(cmpThisPosition.GetHeightFixed() - targetHeight);
+
+ let types = this.GetAttackTypes(wantedTypes);
+ if (attackGround)
+ return types.some(type => heightDiff <= this.GetRange(type).max && "AttackGround" in this.template[type]);
+
+ let cmpFormation = Engine.QueryInterface(target, IID_Formation);
+ if (cmpFormation)
+ return true;
+
let cmpIdentity = QueryMiragedInterface(target, IID_Identity);
if (!cmpIdentity)
return false;
@@ -305,16 +336,10 @@
if (!cmpTargetPlayer || !cmpEntityPlayer)
return false;
- let types = this.GetAttackTypes(wantedTypes);
let entityOwner = cmpEntityPlayer.GetPlayerID();
let targetOwner = cmpTargetPlayer.GetPlayerID();
let cmpCapturable = QueryMiragedInterface(target, IID_Capturable);
- // Check if the relative height difference is larger than the attack range
- // If the relative height is bigger, it means they will never be able to
- // reach each other, no matter how close they come.
- let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset());
-
for (let type of types)
{
if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints()))
@@ -504,12 +529,16 @@
};
/**
- * Attack the target entity. This should only be called after a successful range check,
+ * Attack the target (entity). This should only be called after a successful range check,
* and should only be called after GetTimers().repeat msec has passed since the last
* call to PerformAttack.
+ * @param {string} type - The type of the attack (e.g. "Melee", "Ranged", "Capture", "Slaughter").
+ * @param {number | Vector3D} target - Either the entityID of the targetet entity, or,
+ * when AttackGround calls, the 3D position of the targeted ground.
*/
Attack.prototype.PerformAttack = function(type, target)
{
+ let attackGround = typeof target == "object";
let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner();
let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
@@ -530,16 +559,25 @@
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 targetPosition = attackGround ? new Vector3D(target.x, target.y, target.z) : undefined;
+ if (!attackGround)
+ {
+ let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
+ if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+ return;
+ targetPosition = cmpTargetPosition.GetPosition();
+ }
+
+ let predictedPosition = targetPosition;
+ if (!attackGround)
+ {
+ 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) *
@@ -553,7 +591,7 @@
// 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,8 @@
* 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 entity id of the target for a normal attack, or
+ * when attackGround calls, the 3D position of the targetted ground.
* @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 +107,8 @@
if (!data.position)
return;
+ let attackGround = typeof data.target == "object";
+
let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager);
if (cmpSoundManager && data.attackImpactSound)
cmpSoundManager.PlaySoundGroupAtPosition(data.attackImpactSound, data.position);
@@ -129,23 +132,28 @@
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 ? new Vector3D(data.target.x, data.target.y, data.target.z) :
+ this.InterpolatedLocation(data.target, lateness);
+
if (!targetPosition)
return;
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,29 +23,29 @@
FormationAttack.prototype.GetRange = function(target)
{
- var result = {"min": 0, "max": this.canAttackAsFormation ? -1 : 0};
- var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+warn("FormationAttack.js; GetRange init: " + uneval(target));
+ let result = {"min": 0, "max": this.canAttackAsFormation ? -1 : 0, "elevationBonus": 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 = target == "Ranged" ? "Ranged" : 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)
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
@@ -417,31 +417,20 @@
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.ATTACKING");
else
- this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
- return;
- }
-
- // If we can't reach the target, but are standing ground, then abandon this attack order.
- // Unless we're hunting, that's a special case where we should continue attacking our target.
- if (this.GetStance().respondStandGround && !this.order.data.force && !this.order.data.hunting || this.IsTurret())
- {
- this.FinishOrder();
- return;
- }
-
- // For packable units out of attack range:
- // 1. If packed, we need to move to attack range and then unpack.
- // 2. If unpacked, we first need to pack, then follow case 1.
- if (this.CanPack())
- {
- this.PushOrderFront("Pack", { "force": true });
+ this.SetNextState("INDIVIDUAL.COMBAT");
return;
}
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.APPROACHING");
else
- this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
+ this.SetNextState("INDIVIDUAL.COMBAT");
+ },
+
+ "Order.AttackGround": function(msg) {
+ // In the current system, we can only attack-ground with "Ranged" attacks.
+ this.order.data.attackType = "Ranged";
+ this.SetNextState("INDIVIDUAL.COMBAT");
},
"Order.Patrol": function(msg) {
@@ -697,13 +686,13 @@
},
"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))
{
@@ -722,6 +711,29 @@
this.SetNextState("MEMBER");
},
+ "Order.AttackGround": function(msg) {
+warn("UnitAI.js; Order.AttackGround init: " + uneval(msg));
+ // In the current system, we can only attack-ground with "Ranged" attacks.
+ this.order.data.attackType = "Ranged";
+ 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.CheckTargetAttackRange(target, this.order.data.attackType))
+ {
+warn("UnitAI.js; Order.AttackGround need to move: " + uneval(target));
+ this.SetNextState("ATTACKGROUND.APPROACHING");
+ return;
+ }
+warn("UnitAI.js; Order.AttackGround no need to move: " + uneval(this.order));
+ this.CallMemberFunction("AttackGround", [target, false]);
+warn("UnitAI.js; Order.AttackGround no need to move: " + uneval(target));
+ if (cmpAttack.CanAttackAsFormation())
+ this.SetNextState("ATTACKGROUND.ATTACKING");
+ else
+ this.SetNextState("MEMBER");
+ },
+
"Order.Garrison": function(msg) {
if (!Engine.QueryInterface(msg.data.target, IID_GarrisonHolder))
{
@@ -1158,6 +1170,76 @@
},
},
+ "ATTACKGROUND": {
+ "APPROACHING": {
+ "enter": function() {
+warn("UnitAI.js; FormationAttackGroundApproaching-state entered: " + uneval(this.order));
+ if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
+ {
+ this.FinishOrder();
+ return true;
+ }
+ let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+ cmpFormation.SetRearrange(true);
+ cmpFormation.MoveMembersIntoFormation(true, true);
+ },
+
+ "leave": function() {
+ this.StopMoving();
+ },
+
+ "MovementUpdate": function(msg) {
+warn("UnitAI.js; FormationAttackGroundApproaching-state: " + uneval(this.order));
+ let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
+ this.CallMemberFunction("AttackGround", [this.order.data.target, false]);
+ if (cmpAttack.CanAttackAsFormation())
+ this.SetNextState("ATTACKGROUND.ATTACKING");
+ else
+ this.SetNextState("MEMBER");
+ },
+ },
+
+ "ATTACKING": {
+ // Wait for individual members to finish
+ "enter": function(msg) {
+warn("UnitAI.js; FormationAttackGroundAttacking-state: " + uneval(this.order));
+ let target = this.order.data.target;
+ // Check if we are already in range, otherwise walk there
+ if (!this.CheckTargetAttackRange(target, this.order.data.attackType))
+ {
+ this.FinishOrder();
+ this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
+ return true;
+ }
+
+ let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+ // TODO fix the rearranging while attacking as formation
+ cmpFormation.SetRearrange(!this.IsAttackingAsFormation());
+ cmpFormation.MoveMembersIntoFormation(false, false);
+ this.StartTimer(200, 200);
+ return false;
+ },
+
+ "Timer": function(msg) {
+ let target = this.order.data.target;
+ // Check if we are already in range, otherwise walk there
+ if (!this.CheckTargetAttackRange(target, this.order.data.attackType))
+ {
+ this.FinishOrder();
+ this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
+ return;
+ }
+ },
+
+ "leave": function(msg) {
+ this.StopTimer();
+ let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+ if (cmpFormation)
+ cmpFormation.SetRearrange(true);
+ },
+ },
+ },
+
"MEMBER": {
// Wait for individual members to finish
"enter": function(msg) {
@@ -1722,6 +1804,49 @@
},
"COMBAT": {
+ "enter": function() {
+ if (!this.CanAttack(this.order.data.target))
+ {
+ this.FinishOrder();
+ return true;
+ }
+
+ // 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 +1871,10 @@
this.SetAnimationVariant("combat");
this.SelectAnimation("move");
- this.StartTimer(1000, 1000);
+
+ // If attack ground is asked (target type == "object") do not start the timer (ground does not run away).
+ if (typeof this.order.data.target == "number")
+ this.StartTimer(1000, 1000);
},
"leave": function() {
@@ -1783,53 +1911,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 = typeof target == "object";
+
+ 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 +1975,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 +1995,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 = typeof target == "object";
+
+ 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 +2019,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 +2031,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 +2056,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 +2066,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 +2084,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 +2524,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())
{
@@ -4161,60 +4308,75 @@
* 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 | object} target - Either the entity-ID of the entity which ought to be attacked
+ * or the 3D-location when attackGround is asking.
+ * @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 = typeof target == "object";
// 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())
- return false;
+ let targetPosition = target;
+ if (!attackGround)
+ {
+ let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
+ if (!cmpTargetPosition.IsInWorld())
+ return false;
+ targetPosition = cmpTargetPosition.GetPosition();
+ }
- 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;
+ let parabolicMaxRange = 0;
+ // No negative roots please.
+ 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 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));
- // if that failed, try closer
- return cmpUnitMotion.MoveToTargetRange(target, 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)
@@ -4284,48 +4446,63 @@
* For melee attacks, this goes straigt to the regular range calculation
* For ranged attacks, the parabolic formula is used to accout for bigger ranges
* when the target is lower, and smaller ranges when the target is higher
+ *
+ * @param {number | object} target - Either the entity-ID of the entity which ought to be attacked
+ * or the 3D-location when attackGround is asking.
+ * @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 = typeof target == "object";
// 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())
- return false;
+ let targetPosition = target;
+ if (!attackGround)
+ {
+ let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
+ if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+ return false;
+ targetPosition = cmpTargetPosition.GetPosition();
+ }
- 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();
+ let selfPosition = cmpThisPosition.GetPosition();
- var t = targetCmpPosition.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)
@@ -4376,22 +4553,23 @@
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())
- 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 attackGround = typeof target == "object"
+ let targetPosition = attackGround ? new Vector2D(target.x, target.z) : undefined;
+
+ if (!attackGround)
+ {
+ let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
+ if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
+ return;
+ targetPosition = cmpTargetPosition.GetPosition2D();
}
+
+ let pos = targetPosition.sub(cmpPosition.GetPosition2D());
+ cmpPosition.TurnTo(Math.atan2(pos.x, pos.y));
};
UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type)
@@ -4555,8 +4733,12 @@
* Returns whether we should chase the targeted entity,
* given our current stance.
*/
-UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
+UnitAI.prototype.ShouldChaseTarget = function(target, force)
{
+ // When attacking ground we can't chase.
+ if (typeof target == "object")
+ return false;
+
if (this.IsTurret())
return false;
@@ -4644,6 +4826,10 @@
targetPositions.push(new Vector2D(order.data.x, order.data.z));
break; // and continue the loop
+ case "AttackGround":
+ targetPositions.push(new Vector3D(order.data.target.x, order.data.target.y, order.data.target.z));
+ break;
+
case "WalkToTarget":
case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
case "Guard":
@@ -4910,6 +5096,18 @@
};
/**
+ * Adds AttackGround order to queue, forced by the player.
+ *
+ * @param {float[]} target.x, target.y, target.z - The x,y,z-values where the units need to attack-ground
+ * @param {boolean} queued - Whether the order is queued or not
+ */
+UnitAI.prototype.AttackGround = function(target, queued)
+{
+ if (this.CanAttack(target))
+ this.AddOrder("AttackGround", { "target": target, "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/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
@@ -34,7 +34,7 @@
AddMock(attacker, IID_Position, {
"IsInWorld": () => true,
- "GetHeightOffset": () => 5,
+ "GetHeightFixed": () => 5,
"GetPosition2D": () => new Vector2D(1, 2)
});
@@ -123,7 +123,7 @@
AddMock(defender, IID_Position, {
"IsInWorld": () => true,
- "GetHeightOffset": () => 0
+ "GetHeightFixed": () => 0
});
AddMock(defender, IID_Health, {
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,22 @@
});
},
+ "attack-ground": function(player, cmd, data)
+ {
+ let entities = [];
+ for (let ent of data.entities)
+ {
+ let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
+ if (cmpAttack && cmpAttack.CanAttackGround()){
+ entities.push(ent);
+// cmpUnitAI.AttackGround(cmd.target, cmd.queued);
+ }
+ }
+ GetFormationUnitAIs(entities, player).forEach(cmpUnitAI => {
+ cmpUnitAI.AttackGround(cmd.target, 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
+