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 @@ -497,8 +497,10 @@ * 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 probableDancer - if true, assume the target is moving erratically ("dancing") */ -Attack.prototype.PerformAttack = function(type, target) +Attack.prototype.PerformAttack = function(type, target, probableDancer = false) { let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(); @@ -527,6 +529,17 @@ let previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition(); let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength); + // To work around 'dancing' units, we'll sometimes revert our prediction, assuming that the unit + // will turn around. This avoids the trap where exact velocity prediction never pan out. + if (probableDancer) + { + let choice = Math.random(); + if (choice < 0.33) + targetVelocity *= -1; + else if (choice > 0.66) + targetVelocity = 0.0; + } + let timeToTarget = PositionHelper.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); let predictedPosition = (timeToTarget !== false) ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition; Index: binaries/data/mods/public/simulation/components/DelayedDamage.js =================================================================== --- binaries/data/mods/public/simulation/components/DelayedDamage.js +++ binaries/data/mods/public/simulation/components/DelayedDamage.js @@ -65,15 +65,21 @@ if (cmpMirage) target = cmpMirage.GetParent(); + let cmpAttackerAI = Engine.QueryInterface(data.attacker, IID_UnitAI); + // Deal direct damage if we hit the main target // and we could handle the attack. - if (PositionHelper.TestCollision(target, data.position, lateness) && - Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner)) + if (PositionHelper.TestCollision(target, data.position, lateness)) { - cmpProjectileManager.RemoveProjectile(data.projectileId); + cmpAttackerAI.MissileHit(true); + + if (Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner)) + cmpProjectileManager.RemoveProjectile(data.projectileId); return; } + cmpAttackerAI.MissileHit(false); + // If we didn't hit the main target look for nearby units. let ents = PositionHelper.EntitiesNearPoint(Vector2D.from3D(data.position), this.MISSILE_HIT_RADIUS, Attacking.GetPlayersToDamage(data.attackerOwner, data.friendlyFire)); 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 @@ -180,6 +180,10 @@ // Ignore newly-seen enemy units by default. }, + "MissileHit": function(msg) { + // Ignore by default. + }, + "Attacked": function(msg) { // ignore attacker }, @@ -1921,6 +1925,8 @@ if (!this.formationAnimationVariant) this.SetAnimationVariant("combat"); + this.failedAttacks = 0; + this.oldAttackType = this.order.data.attackType; this.SelectAnimation("attack_" + this.order.data.attackType.toLowerCase()); this.SetAnimationSync(prepare, this.attackTimers.repeat); @@ -1953,6 +1959,7 @@ cmpBuildingAI.SetUnitAITarget(0); this.StopTimer(); this.ResetAnimation(); + delete this.failedAttacks; }, "Timer": function(msg) { @@ -1979,7 +1986,7 @@ if (!cmpBuildingAI) { let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - cmpAttack.PerformAttack(attackType, target); + cmpAttack.PerformAttack(attackType, target, this.failedAttacks > 5); } // PerformAttack might have triggered messages that moved us to another state. @@ -2021,6 +2028,11 @@ && this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, true) != "Capture") this.RespondToTargetedEntities([msg.data.attacker]); }, + + "MissileHit": function(msg) { + if (!msg.success) + ++this.failedAttacks; + }, }, "FINDINGNEWTARGET": { @@ -4148,6 +4160,11 @@ Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; +UnitAI.prototype.MissileHit = function(success) +{ + this.UnitFsm.ProcessMessage(this, {"type": "MissileHit", "success": success}); +}; + UnitAI.prototype.OnAttacked = function(msg) { if (msg.fromStatusEffect)