Index: binaries/data/mods/public/gui/session/selection_panels_helpers.js =================================================================== --- binaries/data/mods/public/gui/session/selection_panels_helpers.js +++ binaries/data/mods/public/gui/session/selection_panels_helpers.js @@ -360,6 +360,15 @@ }); } +function setScouting(entities) +{ + Engine.PostNetworkCommand({ + "type": "scout", + "entities": entities, + "queued": false + }); +} + function unloadTemplate(template, owner) { Engine.PostNetworkCommand({ 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 @@ -1095,6 +1095,25 @@ }, }, + "scout": { + "getInfo": function(entStates) + { + if (entStates.every(entState => !entState.unitAI)) + return false; + + return { + "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.scout") + + translate("Let the unit(s) scout the map."), + "icon": "objectives.png" + }; + }, + "execute": function(entStates) + { + if (entStates.length) + setScouting(entStates.map(entState => entState.id)); + }, + }, + "garrison": { "getInfo": function(entStates) { 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 @@ -756,6 +756,10 @@ this.FinishOrder(); }, + "Order.Scout": function(msg) { + this.SetNextState("INDIVIDUAL.SCOUTING"); + }, + // States for the special entity representing a group of units moving in formation: "FORMATIONCONTROLLER": { @@ -1462,6 +1466,158 @@ } }, + "SCOUTING": { + "enter": function() { + + warn("Entered scouting state."); // Remove when done + + this.SelectAnimation("walk", false, this.GetWalkSpeed()); + + // Query nearby entities + let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) + return; + var owner = cmpOwnership.GetOwner(); + + warn("Phase 1."); // Remove when done. + + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld()) + return; + let pos = cmpPosition.GetPosition2D(); + let targetPos = undefined; + + warn("Phase 2."); // Remove when done. + + let players = []; + players.push(0); // player 0 = GAIA + let nearEnts = this.EntitiesNearPoint(pos, 500, players); // Radius ought to be large + let target = undefined; + + // Cycle through all the nearby GAIA entities and pick the first entity that is hidden from sight + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); +//cmpRangeManager ? error("Trying function: " + uneval(cmpRangeManager.IsTileExplored(pos, 1) ) ) : error("Nothing"); + for (let ent of nearEnts) + { + warn("Cycling nearby entities. "+ uneval(ent)); // Remove when done. + + if (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, cmpOwnership.GetOwner()) == "hidden") + { + target = ent; + + warn("Hidden entity found! "+ uneval(target)); // Remove when done. + + let cmpIdentity = Engine.QueryInterface(target, IID_Identity); + if (cmpIdentity && cmpIdentity.HasClass("SeaCreature")) + { + warn("Entity was a sea creature ;( "); + continue; + } + + let cmpPositionTarget = Engine.QueryInterface(target, IID_Position); + targetPos = cmpPositionTarget.GetPosition(); + break; + } + } + + if (!targetPos) + { + warn("No hidden entities left!"); // Remove when done. + + this.SetNextState("IDLE"); + } + else + { + warn("Sent on the move. "+ uneval(targetPos)); // Remove when done. + + this.MoveToPoint(targetPos.x, targetPos.z); + } + + }, + + "Attacked": function(msg) { + this.SetSpeedMultiplier(this.GetRunMultiplier()); + //this.Flee(msg.data.attacker, false); + }, + + // This does not work apparently. + // I want the entity to flee when seeing an enemy. + // Although it might be caught in an infinite loop then. + "LosRangeUpdate": function(msg) { + if (msg.data.added.length > 0) + { + this.Flee(msg.data.added[0], false); + return; + } + }, + + "MoveCompleted": function() { + this.ResetSpeedMultiplier(); + + warn("Move completed, going for the homerun!"); // Remove when done. + + // Query nearby entities + let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) + return; + var owner = cmpOwnership.GetOwner(); + + warn("Phase 1 again."); // Remove when done. + + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld()) + return; + let pos = cmpPosition.GetPosition2D(); + let targetPos = undefined; + + warn("Phase 2 again."); // Remove when done. + + let players = []; + players.push(0); // player 0 = GAIA + let nearEnts = this.EntitiesNearPoint(pos, 200, players); // Radius ought to be large + let target = undefined; + + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + // Cycle through all the nearby entities and pick the first entity that is hidden from sight + for (let ent of nearEnts) + { + + warn("Cycling nearby entities yet again. "+ uneval(ent)); // Remove when done. + + if (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, cmpOwnership.GetOwner()) == "hidden") + { + target = ent; + + warn("Another hidden entity found! "+ uneval(target)); // Remove when done. + + let cmpIdentity = Engine.QueryInterface(target, IID_Identity); + if (cmpIdentity && cmpIdentity.HasClass("SeaCreature")) + { + warn("Entity was a sea creature ;( "); + continue; + } + + let cmpPositionTarget = Engine.QueryInterface(target, IID_Position); + targetPos = cmpPositionTarget.GetPosition(); + break; + } + } + + if (!targetPos) + { + warn("No hidden entities left!"); // Remove when done. + + this.SetNextState("IDLE"); + } + else + { + warn("Getting tired going to: "+ uneval(targetPos)); // Remove when done. + + this.MoveToPoint(targetPos.x, targetPos.z); + } + }, + }, + "IDLE": { "enter": function() { // Switch back to idle animation to guarantee we won't @@ -4752,6 +4908,7 @@ targetPositions.push(cmpTargetPosition.GetPosition2D()); return targetPositions; + case "Scout": case "Stop": return []; @@ -4937,6 +5094,15 @@ }; /** + * Adds scout order to queue, forced by the player. + * @param {boolean} queued - Whether the order is queued or not + */ +UnitAI.prototype.Scout = function(queued) +{ + this.AddOrder("Scout", { "force": true }, queued); +}; + +/** * Adds walk-to-target order to queue, this only occurs in response * to a player order, and so is forced. */ @@ -5562,6 +5728,22 @@ return ret; }; +/** + * Gets entities near a give point for given players. + * @param {Vector2D} origin - the point to check around. + * @param {number} radius - the radius around the point to check. + * @param {number[]} players - the players of which we need to check entities. + * @return {number[]} - the id's of the entities in range of the given point. + */ +UnitAI.prototype.EntitiesNearPoint = function(origin, radius, players) +{ + // If there is insufficient data return an empty array. + if (!origin || !radius || !players) + return []; + + return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQueryAroundPos(origin, 0, radius, players, IID_Identity); +}; + UnitAI.prototype.GetStance = function() { return g_Stances[this.stance]; 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 @@ -503,6 +503,13 @@ }); }, + "scout": function(player, cmd, data) + { + GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { + cmpUnitAI.Scout(cmd.queued); + }); + }, + "unload": function(player, cmd, data) { // Verify that the building can be controlled by the player or is mutualAlly Index: source/simulation2/components/CCmpRangeManager.cpp =================================================================== --- source/simulation2/components/CCmpRangeManager.cpp +++ source/simulation2/components/CCmpRangeManager.cpp @@ -2436,6 +2436,15 @@ return exploredVertices * 100 / m_TotalInworldVertices; } + + virtual bool IsTileExplored(entity_pos_t x, entity_pos_t y, player_id_t player) const + { + int i = (x / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); + int j = (y / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); + CLosQuerier los(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); + return los.IsExplored(i, j); + } + }; REGISTER_COMPONENT_TYPE(RangeManager) Index: source/simulation2/components/ICmpRangeManager.h =================================================================== --- source/simulation2/components/ICmpRangeManager.h +++ source/simulation2/components/ICmpRangeManager.h @@ -419,6 +419,11 @@ */ virtual u8 GetUnionPercentMapExplored(const std::vector& players) const = 0; + /** + * Get whether a tile is explored for specified player. + */ + virtual bool IsTileExplored(entity_pos_t x, entity_pos_t y, player_id_t player) const = 0; + /** * Perform some internal consistency checks for testing/debugging. Index: source/simulation2/components/ICmpRangeManager.cpp =================================================================== --- source/simulation2/components/ICmpRangeManager.cpp +++ source/simulation2/components/ICmpRangeManager.cpp @@ -62,4 +62,5 @@ DEFINE_INTERFACE_METHOD_2("SetSharedLos", void, ICmpRangeManager, SetSharedLos, player_id_t, std::vector) DEFINE_INTERFACE_METHOD_CONST_1("GetPercentMapExplored", u8, ICmpRangeManager, GetPercentMapExplored, player_id_t) DEFINE_INTERFACE_METHOD_CONST_1("GetUnionPercentMapExplored", u8, ICmpRangeManager, GetUnionPercentMapExplored, std::vector) +DEFINE_INTERFACE_METHOD_CONST_3("IsTileExplored", bool, ICmpRangeManager, IsTileExplored, entity_pos_t, entity_pos_t, player_id_t) END_INTERFACE_WRAPPER(RangeManager)