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 @@ -548,6 +548,32 @@ }; /** + * Ping nearby owned units to help if a unit gets attacked by aggressive animals + * @param {number} ent - The unit that gets attacked. + * @param {number} attacker - The attacker unit. + */ +Attack.prototype.PingForHelp = function(ent, attacker = this.entity) +{ + const cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); + if (!cmpOwnership) + return; + + const owner = cmpOwnership.GetOwner(); + if (owner == INVALID_PLAYER) + return; + + const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + const nearby = cmpRangeManager.ExecuteQuery(ent, 0, 60, [owner], IID_UnitAI, true); + + for (let i = 0; i < nearby.length; ++i) + { + const cmpUnitAI = Engine.QueryInterface(nearby[i], IID_UnitAI); + if (cmpUnitAI.isIdle) + cmpUnitAI["Attack"].apply(cmpUnitAI, [attacker]); + } +}; + +/** * Attack our target entity. * @param {string} data - The attack type to use. * @param {number} lateness - The offset of the actual call and when it was expected. @@ -560,6 +586,11 @@ return; } + // when a unit gets attacked by aggressive animals, ping (idle) units nearby to help out + let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); + if (cmpUnitAI.IsDangerousAnimal()) + this.PingForHelp(this.target); + // ToDo: Enable entities to keep facing a target. Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target); 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 @@ -446,6 +446,48 @@ return ACCEPT_ORDER; }, + "Order.PingForHelp": function(msg) { + let type = this.GetBestAttackAgainst(msg.data.target, msg.data.allowCapture); + if (!type) + return this.FinishOrder(); + + msg.data.attackType = type; + + this.RememberTargetPosition(); + if (msg.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather") + this.RememberTargetPosition(this.orderQueue[1].data); + + if (this.CheckTargetAttackRange(msg.data.target, msg.data.attackType)) + { + if (this.CanUnpack()) + { + this.PushOrderFront("Unpack", { "force": true }); + return ACCEPT_ORDER; + } + + // Cancel any current packing order. + if (this.EnsureCorrectPackStateForAttack(false)) + this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING"); + + return ACCEPT_ORDER; + } + + // If we're hunting, that's a special case where we should continue attacking our target. + if (this.GetStance().respondStandGround && !msg.data.force && !msg.data.hunting || !this.AbleToMove()) + return this.FinishOrder(); + + if (this.CanPack()) + { + this.PushOrderFront("Pack", { "force": true }); + return ACCEPT_ORDER; + } + + // If we're currently packing/unpacking, make sure we are packed, so we can move. + if (this.EnsureCorrectPackStateForAttack(true)) + this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING"); + return ACCEPT_ORDER; + }, + "Order.Patrol": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); @@ -6426,7 +6468,7 @@ return true; let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); - return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); + return cmpUnitAI && !cmpUnitAI.IsAnimal(); }; let entsByPreferences = {};