diff --git a/binaries/data/mods/public/simulation/components/BuildingAI.js b/binaries/data/mods/public/simulation/components/BuildingAI.js index 1e2abe0cb9..97285930c3 100644 --- a/binaries/data/mods/public/simulation/components/BuildingAI.js +++ b/binaries/data/mods/public/simulation/components/BuildingAI.js @@ -32,6 +32,10 @@ BuildingAI.prototype.OnOwnershipChanged = function(msg) { if (msg.to != -1) this.SetupRangeQuery(msg.to); + + // Non-Gaia buildings should attack certain Gaia units. + if (msg.to != 0 || this.gaiaUnitsQuery) + this.SetupGaiaRangeQuery(msg.to); }; /** @@ -50,6 +54,8 @@ BuildingAI.prototype.OnDestroy = function() var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.enemyUnitsQuery) cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery); + if (this.gaiaUnitsQuery) + cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery); }; /** @@ -81,12 +87,58 @@ BuildingAI.prototype.SetupRangeQuery = function(owner) } }; +// Set up a range query for Gaia units within LOS range which can be attacked. +// This should be called whenever our ownership changes. +BuildingAI.prototype.SetupGaiaRangeQuery = function() +{ + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + var owner = cmpOwnership.GetOwner(); + + var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + + if (this.gaiaUnitsQuery) + { + rangeMan.DestroyActiveQuery(this.gaiaUnitsQuery); + this.gaiaUnitsQuery = undefined; + } + + if (owner == -1) + return; + + var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player); + if (!cmpPlayer.IsEnemy(0)) + return; + + var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + if (cmpAttack) + { + var range = cmpAttack.GetRange("Ranged"); + + // This query is only interested in Gaia entities that can attack. + this.gaiaUnitsQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, [0], IID_Attack, rangeMan.GetEntityFlagMask("normal")); + rangeMan.EnableActiveQuery(this.gaiaUnitsQuery); + } +}; + /** * Called when units enter or leave range */ BuildingAI.prototype.OnRangeUpdate = function(msg) { - if (msg.tag != this.enemyUnitsQuery) + if (msg.tag == this.gaiaUnitsQuery) + { + const filter = function(e) { + var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); + return (cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal())); + }; + + if (msg.added.length) + msg.added = msg.added.filter(filter); + if (msg.removed.length) + msg.removed = msg.removed.filter(filter); + } + else if (msg.tag != this.enemyUnitsQuery) return; if (msg.added.length > 0) diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index 45d79a7cd1..a7b7ce97c7 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -140,6 +140,10 @@ var UnitFsmSpec = { // ignore newly-seen units by default }, + "LosGaiaRangeUpdate": function(msg) { + // ignore newly-seen Gaia units by default + }, + "LosHealRangeUpdate": function(msg) { // ignore newly-seen injured units by default }, @@ -188,6 +192,26 @@ var UnitFsmSpec = { // Individual orders: // (these will switch the unit out of formation mode) + "Order.Stop": function(msg) { + // We have no control over non-domestic animals. + if (this.IsAnimal() && !this.IsDomestic()) + { + this.FinishOrder(); + return; + } + + // Stop moving immediately. + this.StopMoving(); + this.FinishOrder(); + + // No orders left, we're an individual now + if (this.IsAnimal()) + this.SetNextState("ANIMAL.IDLE"); + else + this.SetNextState("INDIVIDUAL.IDLE"); + + }, + "Order.Walk": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic()) @@ -458,6 +482,12 @@ var UnitFsmSpec = { this.FinishOrder(); }, + "Order.Stop": function(msg) { + var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + cmpFormation.CallMemberFunction("Stop", [false]); + cmpFormation.Disband(); + }, + "Order.Attack": function(msg) { // TODO: we should move in formation towards the target, // then break up into individuals when close enough to it @@ -698,6 +728,14 @@ var UnitFsmSpec = { } }, + "LosGaiaRangeUpdate": function(msg) { + if (this.GetStance().targetVisibleEnemies) + { + // Start attacking one of the newly-seen enemy (if any) + this.RespondToTargetedEntities(this.GetAttackableEntitiesByPreference(msg.data.added, true)); + } + }, + "LosHealRangeUpdate": function(msg) { this.RespondToHealableEntities(msg.data.added); }, @@ -1614,9 +1652,7 @@ var UnitFsmSpec = { { this.Flee(msg.data.attacker, false); } - else if (this.template.NaturalBehaviour == "violent" || - this.template.NaturalBehaviour == "aggressive" || - this.template.NaturalBehaviour == "defensive") + else if (this.IsDangerousAnimal() || this.template.NaturalBehaviour == "defensive") { if (this.CanAttack(msg.data.attacker)) this.Attack(msg.data.attacker, false); @@ -1691,8 +1727,7 @@ var UnitFsmSpec = { } } // Start attacking one of the newly-seen enemy (if any) - else if (this.template.NaturalBehaviour == "violent" || - this.template.NaturalBehaviour == "aggressive") + else if (this.IsDangerousAnimal()) { this.AttackVisibleEntity(msg.data.added); } @@ -1781,6 +1816,12 @@ UnitAI.prototype.IsAnimal = function() return (this.template.NaturalBehaviour ? true : false); }; +UnitAI.prototype.IsDangerousAnimal = function() +{ + return (this.IsAnimal() && (this.template.NaturalBehaviour == "violent" || + this.template.NaturalBehaviour == "aggressive")); +}; + UnitAI.prototype.IsDomestic = function() { var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); @@ -1804,6 +1845,19 @@ UnitAI.prototype.IsGarrisoned = function() return this.isGarrisoned; }; +UnitAI.prototype.CanAttackGaia = function() +{ + var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + if (!cmpAttack) + return false; + + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership || cmpOwnership.GetOwner() == 0) + return false; + + return true; +}; + UnitAI.prototype.OnCreate = function() { if (this.IsAnimal()) @@ -1816,9 +1870,14 @@ UnitAI.prototype.OnCreate = function() UnitAI.prototype.OnOwnershipChanged = function(msg) { - this.SetupRangeQuery(); - if (this.IsHealer()) - this.SetupHealRangeQuery(); + this.SetupRangeQueries(); + + // If the unit isn't being created or dying, clear orders and reset stance. + if (msg.to != -1 && msg.from != -1) + { + this.SetStance(this.template.DefaultStance); + this.Stop(false); + } }; UnitAI.prototype.OnDestroy = function() @@ -1832,8 +1891,22 @@ UnitAI.prototype.OnDestroy = function() rangeMan.DestroyActiveQuery(this.losRangeQuery); if (this.losHealRangeQuery) rangeMan.DestroyActiveQuery(this.losHealRangeQuery); + if (this.losGaiaRangeQuery) + rangeMan.DestroyActiveQuery(this.losGaiaRangeQuery); }; +// Wrapper function that sets up the normal, healer, and Gaia range queries. +UnitAI.prototype.SetupRangeQueries = function() +{ + this.SetupRangeQuery(); + + if (this.IsHealer()) + this.SetupHealRangeQuery(); + + if (this.CanAttackGaia() || this.losGaiaRangeQuery) + this.SetupGaiaRangeQuery(); +} + // Set up a range query for all enemy units within LOS range // which can be attacked. // This should be called whenever our ownership changes. @@ -1846,7 +1919,10 @@ UnitAI.prototype.SetupRangeQuery = function() var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (this.losRangeQuery) + { rangeMan.DestroyActiveQuery(this.losRangeQuery); + this.losRangeQuery = undefined; + } var players = []; @@ -1905,6 +1981,40 @@ UnitAI.prototype.SetupHealRangeQuery = function() rangeMan.EnableActiveQuery(this.losHealRangeQuery); }; +// Set up a range query for Gaia units within LOS range which can be attacked. +// This should be called whenever our ownership changes. +UnitAI.prototype.SetupGaiaRangeQuery = function() +{ + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + var owner = cmpOwnership.GetOwner(); + + var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + + if (this.losGaiaRangeQuery) + { + rangeMan.DestroyActiveQuery(this.losGaiaRangeQuery); + this.losGaiaRangeQuery = undefined; + } + + if (owner == -1) + return; + + var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player); + if (!cmpPlayer.IsEnemy(0)) + return; + + // Only create the query if Gaia is our enemy and we can attack. + if (this.CanAttackGaia()) + { + var range = this.GetQueryRange(IID_Attack); + + // This query is only interested in Gaia entities that can attack. + this.losGaiaRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, [0], IID_Attack, rangeMan.GetEntityFlagMask("normal")); + rangeMan.EnableActiveQuery(this.losGaiaRangeQuery); + } +}; + //// FSM linkage functions //// UnitAI.prototype.SetNextState = function(state) @@ -2157,6 +2267,8 @@ UnitAI.prototype.OnRangeUpdate = function(msg) { if (msg.tag == this.losRangeQuery) UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg}); + else if (msg.tag == this.losGaiaRangeQuery) + UnitFsm.ProcessMessage(this, {"type": "LosGaiaRangeUpdate", "data": msg}); else if (msg.tag == this.losHealRangeQuery) UnitFsm.ProcessMessage(this, {"type": "LosHealRangeUpdate", "data": msg}); }; @@ -2743,6 +2855,9 @@ UnitAI.prototype.ComputeWalkingDistance = function() // Return the total distance to the target return distance; + case "Stop": + return 0; + default: error("ComputeWalkingDistance: Unrecognised order type '"+order.type+"'"); return distance; @@ -2769,6 +2884,14 @@ UnitAI.prototype.Walk = function(x, z, queued) this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued); }; +/** + * Adds stop order to queue, forced by the player. + */ +UnitAI.prototype.Stop = function(queued) +{ + this.AddOrder("Stop", undefined, queued); +}; + /** * Adds walk-to-target order to queue, this only occurs in response * to a player order, and so is forced. @@ -3046,12 +3169,8 @@ UnitAI.prototype.SwitchToStance = function(stance) if (stance == "standground") this.StopMoving(); - // Reset the range query, since the range depends on stance - this.SetupRangeQuery(); - // Just if we are a healer - // TODO maybe move those two to a SetupRangeQuerys() - if (this.IsHealer()) - this.SetupHealRangeQuery(); + // Reset the range queries, since the range depends on stance. + this.SetupRangeQueries(); }; /** @@ -3060,7 +3179,7 @@ UnitAI.prototype.SwitchToStance = function(stance) */ UnitAI.prototype.FindNewTargets = function() { - if (!this.losRangeQuery) + if (!this.losRangeQuery && !this.losGaiaRangeQuery) return false; var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); @@ -3069,6 +3188,13 @@ UnitAI.prototype.FindNewTargets = function() if (!this.GetStance().targetVisibleEnemies) return false; + // If no regular enemies were found, attempt to attack a hostile Gaia entity. + if (this.losGaiaRangeQuery && (!ents || !ents.length)) + { + ents = rangeMan.ResetActiveQuery(this.losGaiaRangeQuery); + return this.RespondToTargetedEntities(this.GetAttackableEntitiesByPreference(ents, true)); + } + return this.RespondToTargetedEntities(this.GetAttackableEntitiesByPreference(ents)); }; @@ -3410,12 +3536,24 @@ UnitAI.prototype.MoveRandomly = function(distance) cmpMotion.MoveToPointRange(tx, tz, distance, distance); }; -UnitAI.prototype.GetAttackableEntitiesByPreference = function(ents) +UnitAI.prototype.GetAttackableEntitiesByPreference = function(ents, filterGaia) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return []; + if (filterGaia) + { + const filter = function(e) { + var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); + return (cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal())); + }; + + return ents + .filter(function (v, i, a) { return cmpAttack.CanAttack(v) && filter(v); }) + .sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); }); + } + return ents .filter(function (v, i, a) { return cmpAttack.CanAttack(v); }) .sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); }); diff --git a/binaries/data/mods/public/simulation/helpers/Player.js b/binaries/data/mods/public/simulation/helpers/Player.js index 1f8c751d2f..09d19193fd 100644 --- a/binaries/data/mods/public/simulation/helpers/Player.js +++ b/binaries/data/mods/public/simulation/helpers/Player.js @@ -128,8 +128,11 @@ function LoadPlayerSettings(settings, newPlayers) cmpPlayer.SetCiv(pDefs.Civ); cmpPlayer.SetColour(pDefs.Colour.r, pDefs.Colour.g, pDefs.Colour.b); + // Gaia should be its own ally. + cmpPlayer.SetAlly(0); + // Gaia is everyone's enemy - for (var j = 0; j < numPlayers; ++j) + for (var j = 1; j < numPlayers; ++j) cmpPlayer.SetEnemy(j); } }