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 @@ -642,6 +642,15 @@ this.FinishOrder(); }, + "Order.Scout": function(msg) {warn(uneval(msg)); + // Remember staring position to return to it after we finish scouting. + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld()) + return; + this.ScoutingBeginPosition = cmpPosition.GetPosition2D(); + this.SetNextState("INDIVIDUAL.SCOUTING"); + }, + // States for the special entity representing a group of units moving in formation: "FORMATIONCONTROLLER": { @@ -1405,6 +1414,136 @@ } }, + "SCOUTING": { + "enter": function() { + warn("Entered scouting state from: " + uneval(this.ScoutingBeginPosition) ); // Remove when done + + // Set up variables + let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) + { + this.FinishOrder(); + return true; + } + let owner = cmpOwnership.GetOwner(); + + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + let cmpVision = Engine.QueryInterface(this.entity, IID_Vision); + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld() || !cmpUnitMotion || !cmpVision || !cmpRangeManager) + { + this.FinishOrder(); + return true; + } + + let visionRange = cmpVision.GetRange(); + let pos = cmpPosition.GetPosition2D(); + let ang; + let targetPos; + + warn("Phase 1."); // Remove when done. + + let imax = 10; // Number of primary iterations to be done. + for (let i = 0; i < imax; ++i) + { + warn("Primary Iteration: " + i); + + let dist = visionRange + randFloat(0.8, 1.2) + 20 * i; // What is a sensible number? Let it depend on map size? + + let x; + let y; + + // This is to ensure that the entity does not always start exploring North. + let randStartAngle = randFloat(0.0, 2.0) * Math.PI; + // This is to ensure that the angle-iteration does not always goes (counter)clockwise. Is this needed, for I don't think so ;) + let rotationDirection = randBool() ? 1 : -1; + + let jmax = 5; // Number of secondary iterations to be done. + for (let j = 0; j < jmax*i; ++j) + { + warn("Secondary Iteration: " + j); + + ang = randStartAngle + rotationDirection * 2 * Math.PI * j / jmax * randFloat(0.8, 1.2); + + x = pos.x + Math.sin(ang) * dist; + y = pos.y + Math.cos(ang) * dist; + +warn("Can position ("+uneval(x)+","+uneval(y)+") be explored? " + uneval(cmpRangeManager.CanPlayerExplorePosition(x, y, owner) ) ); + + if (cmpRangeManager.CanPlayerExplorePosition(x, y, owner)) + { + let cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); + let cmpPlayer = QueryOwnerInterface(this.entity); + if (cmpTerritoryManager && cmpPlayer) + { + let tileOwner = cmpTerritoryManager.GetOwner(x, y); + if (!tileOwner || !cmpPlayer.IsEnemy(tileOwner)) + { + targetPos = {"x": x, "y": y}; + break; + } + } + } + } + + if (targetPos) + break; + } + + if (!targetPos) + { + warn("No unexplored positions left!"); // Remove when done. + + if (!this.CheckPointRangeExplicit(this.ScoutingBeginPosition.x, this.ScoutingBeginPosition.y)) + { + this.MoveToPoint(this.ScoutingBeginPosition.x, this.ScoutingBeginPosition.y); + this.order.data = {"x": this.ScoutingBeginPosition.x, "z": this.ScoutingBeginPosition.y}; + this.SetNextState("WALKING"); + return true; + } + } + else + { + warn("Sent on the move. "+ uneval(targetPos)); // Remove when done. + + if (!this.MoveToPoint(targetPos.x, targetPos.y)) + { + warn("Target not reachable!") + } + } + + }, + + "Attacked": function(msg) { + this.SetSpeedMultiplier(this.GetRunMultiplier()); + }, + + "leave": function(msg) { + this.ResetSpeedMultiplier(); + }, + + // 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; + } + }, + + "MoveStarted": function() { + this.SelectAnimation("move"); + }, + + "MoveCompleted": function() { + this.ResetSpeedMultiplier(); + this.SetNextState("SCOUTING"); + }, + }, + "IDLE": { "enter": function() { // Switch back to idle animation to guarantee we won't @@ -4835,6 +4974,7 @@ targetPositions.push(cmpTargetPosition.GetPosition2D()); return targetPositions; + case "Scout": case "Stop": return []; @@ -5020,6 +5160,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. */ 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 @@ -1,4 +1,4 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -2436,6 +2436,15 @@ return exploredVertices * 100 / m_TotalInworldVertices; } + + virtual bool CanPlayerExplorePosition(entity_pos_t x, entity_pos_t y, player_id_t player) const + { + int i = (x / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToNearest(); + int j = (y / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToNearest(); + CLosQuerier los(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); + return !los.IsExplored(i, j) && !LosIsOffWorld(i, j); + } + }; REGISTER_COMPONENT_TYPE(RangeManager) Index: source/simulation2/components/ICmpRangeManager.h =================================================================== --- source/simulation2/components/ICmpRangeManager.h +++ source/simulation2/components/ICmpRangeManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -419,6 +419,11 @@ */ virtual u8 GetUnionPercentMapExplored(const std::vector& players) const = 0; + /** + * Get whether a position can be explored by specified player. + */ + virtual bool CanPlayerExplorePosition(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 @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -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("CanPlayerExplorePosition", bool, ICmpRangeManager, CanPlayerExplorePosition, entity_pos_t, entity_pos_t, player_id_t) END_INTERFACE_WRAPPER(RangeManager)