Index: binaries/data/mods/public/simulation/components/RallyPoint.js =================================================================== --- binaries/data/mods/public/simulation/components/RallyPoint.js +++ binaries/data/mods/public/simulation/components/RallyPoint.js @@ -17,6 +17,16 @@ }); }; +RallyPoint.prototype.HasPositions = function() +{ + return this.pos.length > 0; +}; + +RallyPoint.prototype.GetFirstPosition = function() +{ + return this.pos.length ? { "x": this.pos[0].x, "y": this.pos[0].z } : { "x": -1, "y": -1 }; +}; + RallyPoint.prototype.GetPositions = function() { // Update positions for moving target entities Index: binaries/data/mods/public/simulation/components/interfaces/RallyPoint.js =================================================================== --- binaries/data/mods/public/simulation/components/interfaces/RallyPoint.js +++ /dev/null @@ -1 +0,0 @@ -Engine.RegisterInterface("RallyPoint"); Index: binaries/data/mods/public/simulation/components/tests/test_Foundation.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Foundation.js +++ binaries/data/mods/public/simulation/components/tests/test_Foundation.js @@ -3,7 +3,6 @@ Engine.LoadComponentScript("interfaces/Cost.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/Health.js"); -Engine.LoadComponentScript("interfaces/RallyPoint.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/TerritoryDecay.js"); Engine.LoadComponentScript("interfaces/Trigger.js"); Index: binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js +++ binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js @@ -10,7 +10,6 @@ Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); -Engine.LoadComponentScript("interfaces/RallyPoint.js"); const garrisonedEntitiesList = [25, 26, 27, 28, 29, 30, 31, 32, 33]; const garrisonHolderId = 15; Index: binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -22,7 +22,6 @@ Engine.LoadComponentScript("interfaces/Pack.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); -Engine.LoadComponentScript("interfaces/RallyPoint.js"); Engine.LoadComponentScript("interfaces/Repairable.js"); Engine.LoadComponentScript("interfaces/ResourceDropsite.js"); Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); Index: binaries/data/mods/public/simulation/components/tests/test_RallyPoint.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_RallyPoint.js +++ binaries/data/mods/public/simulation/components/tests/test_RallyPoint.js @@ -1,7 +1,6 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Health.js"); -Engine.LoadComponentScript("interfaces/RallyPoint.js"); Engine.LoadComponentScript("RallyPoint.js"); function initialRallyPointTest(test_function) Index: source/simulation2/TypeList.h =================================================================== --- source/simulation2/TypeList.h +++ source/simulation2/TypeList.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -144,6 +144,9 @@ INTERFACE(ProjectileManager) COMPONENT(ProjectileManager) +INTERFACE(RallyPoint) +COMPONENT(RallyPointScripted) + INTERFACE(RallyPointRenderer) COMPONENT(RallyPointRenderer) Index: source/simulation2/components/CCmpFootprint.cpp =================================================================== --- source/simulation2/components/CCmpFootprint.cpp +++ source/simulation2/components/CCmpFootprint.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -25,9 +25,10 @@ #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpPosition.h" +#include "simulation2/components/ICmpRallyPoint.h" #include "simulation2/components/ICmpUnitMotion.h" +#include "simulation2/helpers/Geometry.h" #include "simulation2/MessageTypes.h" -#include "graphics/Terrain.h" // For TERRAIN_TILE_SIZE #include "maths/FixedVector2D.h" class CCmpFootprint : public ICmpFootprint @@ -43,6 +44,7 @@ entity_pos_t m_Size0; // width/radius entity_pos_t m_Size1; // height/radius entity_pos_t m_Height; + entity_pos_t m_MaxSpawnDistance; static std::string GetSchema() { @@ -80,7 +82,12 @@ "" "" "" - ""; + "" + "" + "" + "" + "" + ""; } virtual void Init(const CParamNode& paramNode) @@ -104,6 +111,12 @@ } m_Height = paramNode.GetChild("Height").ToFixed(); + + if (paramNode.GetChild("MaxSpawnDistance").IsOk()) + m_MaxSpawnDistance = paramNode.GetChild("MaxSpawnDistance").ToFixed(); + else + // Pick some default + m_MaxSpawnDistance = entity_pos_t::FromInt(8); } virtual void Deinit() @@ -147,7 +160,8 @@ if (!cmpObstructionManager) return error; - entity_pos_t spawnedRadius; + // If no spawned obstruction, use a positive radius to avoid division by zero errors. + entity_pos_t spawnedRadius = fixed::FromInt(1); ICmpObstructionManager::tag_t spawnedTag; CmpPtr cmpSpawnedObstruction(GetSimContext(), spawned); @@ -156,9 +170,8 @@ spawnedRadius = cmpSpawnedObstruction->GetUnitRadius(); spawnedTag = cmpSpawnedObstruction->GetObstruction(); } - // else use zero radius - // Get passability class from UnitMotion + // Get passability class from UnitMotion. CmpPtr cmpUnitMotion(GetSimContext(), spawned); if (!cmpUnitMotion) return error; @@ -168,94 +181,85 @@ if (!cmpPathfinder) return error; + // Ignore collisions with the spawned entity. + SkipTagObstructionFilter filter(spawnedTag); + CFixedVector2D initialPos = cmpPosition->GetPosition2D(); entity_angle_t initialAngle = cmpPosition->GetRotation().Y; - // Max spawning distance in tiles - const i32 maxSpawningDistance = 4; + CFixedVector2D u = CFixedVector2D(fixed::Zero(), fixed::FromInt(1)).Rotate(initialAngle); + CFixedVector2D v = u.Perpendicular(); - if (m_Shape == CIRCLE) + // Obstructions are squares, so multiply its radius by 2*sqrt(2) ~= 3 to determine the distance between units. + entity_pos_t gap = spawnedRadius * 3; + int rows = std::max(1, (m_MaxSpawnDistance / gap).ToInt_RoundToNegInfinity()); + + // The first row of units will be half a gap away from the footprint. + CFixedVector2D halfSize = m_Shape == CIRCLE ? + CFixedVector2D(m_Size1 + gap / 2, m_Size0 + gap / 2) : + CFixedVector2D((m_Size1 + gap) / 2, (m_Size0 + gap) / 2); + + // Figure out how many units can fit on each halfside of the rectangle. + // Since 2*pi/6 ~= 1, this is also how many units can fit on a sixth of the circle. + int a = std::max(1, (halfSize.X / gap).ToInt_RoundToNegInfinity()); + int b = std::max(1, (halfSize.Y / gap).ToInt_RoundToNegInfinity()); + + // Try more spawning points for large units in case some of them are partially blocked. + if (rows == 1) { - // Expand outwards from foundation - for (i32 dist = 0; dist <= maxSpawningDistance; ++dist) - { - // The spawn point should be far enough from this footprint to fit the unit, plus a little gap - entity_pos_t clearance = spawnedRadius + entity_pos_t::FromInt(2 + (int)TERRAIN_TILE_SIZE*dist); - entity_pos_t radius = m_Size0 + clearance; + a *= 2; + b *= 2; + } - // Try equally-spaced points around the circle in alternating directions, starting from the front - const i32 numPoints = 31 + 2*dist; - for (i32 i = 0; i < (numPoints+1)/2; i = (i > 0 ? -i : 1-i)) // [0, +1, -1, +2, -2, ... (np-1)/2, -(np-1)/2] - { - entity_angle_t angle = initialAngle + (entity_angle_t::Pi()*2).Multiply(entity_angle_t::FromInt(i)/(int)numPoints); + // Store the position of the spawning point within each row that's closest to the spawning angle. + std::vector offsetPoints(rows, 0); - fixed s, c; - sincos_approx(angle, s, c); + CmpPtr cmpRallyPoint(GetEntityHandle()); + if (cmpRallyPoint && cmpRallyPoint->HasPositions()) + { + CFixedVector2D rallyPointPos = cmpRallyPoint->GetFirstPosition(); + if (m_Shape == CIRCLE) + { + entity_angle_t offsetAngle = atan2_approx(rallyPointPos.X - initialPos.X, rallyPointPos.Y - initialPos.Y) - initialAngle; - CFixedVector3D pos (initialPos.X + s.Multiply(radius), fixed::Zero(), initialPos.Y + c.Multiply(radius)); + // There are 6*(a+r) points in row r, so multiply that by angle/2pi to find the offset within the row. + for (int r = 0; r < rows; ++r) + offsetPoints[r] = (offsetAngle * 3 * (a + r) / fixed::Pi()).ToInt_RoundToNearest(); + } + else + { + CFixedVector2D offsetPos = Geometry::NearestPointOnSquare(rallyPointPos - initialPos, u, v, halfSize); - SkipTagObstructionFilter filter(spawnedTag); // ignore collisions with the spawned entity - if (cmpPathfinder->CheckUnitPlacement(filter, pos.X, pos.Z, spawnedRadius, spawnedPass) == ICmpObstruction::FOUNDATION_CHECK_SUCCESS) - return pos; // this position is okay, so return it - } + // Scale and convert the perimeter coordinates of the point to its offset within the row. + int x = (offsetPos.Dot(u) * b / halfSize.X).ToInt_RoundToNearest(); + int y = (offsetPos.Dot(v) * a / halfSize.Y).ToInt_RoundToNearest(); + for (int r = 0; r < rows; ++r) + offsetPoints[r] = Geometry::GetPerimeterDistance( + b + r, + a + r, + x >= b ? b + r : x <= -b ? -b - r : x, + y >= a ? a + r : y <= -a ? -a - r : y); } } - else - { - fixed s, c; - sincos_approx(initialAngle, s, c); - // Expand outwards from foundation - for (i32 dist = 0; dist <= maxSpawningDistance; ++dist) + for (int k = 0; k < 2 * (a + b + 2 * rows); k = k > 0 ? -k : 1 - k) + for (int r = 0; r < rows; ++r) { - // The spawn point should be far enough from this footprint to fit the unit, plus a little gap - entity_pos_t clearance = spawnedRadius + entity_pos_t::FromInt(2 + (int)TERRAIN_TILE_SIZE*dist); - - for (i32 edge = 0; edge < 4; ++edge) + CFixedVector2D pos = initialPos; + if (m_Shape == CIRCLE) + // Multiply the point by 2pi / 6*(a+r) to get the angle. + pos += u.Rotate(fixed::Pi() * (offsetPoints[r] + k) / (3 * (a + r))).Multiply(halfSize.X + gap * r ); + else { - // Try equally-spaced points along the edge in alternating directions, starting from the middle - const i32 numPoints = 9 + 2*dist; - - // Compute the direction and length of the current edge - CFixedVector2D dir; - fixed sx, sy; - switch (edge) - { - case 0: - dir = CFixedVector2D(c, -s); - sx = m_Size0; - sy = m_Size1; - break; - case 1: - dir = CFixedVector2D(-s, -c); - sx = m_Size1; - sy = m_Size0; - break; - case 2: - dir = CFixedVector2D(s, c); - sx = m_Size1; - sy = m_Size0; - break; - case 3: - dir = CFixedVector2D(-c, s); - sx = m_Size0; - sy = m_Size1; - break; - } - CFixedVector2D center = initialPos - dir.Perpendicular().Multiply(sy/2 + clearance); - dir = dir.Multiply((sx + clearance*2) / (int)(numPoints-1)); - - for (i32 i = 0; i < (numPoints+1)/2; i = (i > 0 ? -i : 1-i)) // [0, +1, -1, +2, -2, ... (np-1)/2, -(np-1)/2] - { - CFixedVector2D pos (center + dir*i); - - SkipTagObstructionFilter filter(spawnedTag); // ignore collisions with the spawned entity - if (cmpPathfinder->CheckUnitPlacement(filter, pos.X, pos.Y, spawnedRadius, spawnedPass) == ICmpObstruction::FOUNDATION_CHECK_SUCCESS) - return CFixedVector3D(pos.X, fixed::Zero(), pos.Y); // this position is okay, so return it - } + // Convert the point to coordinates and scale. + std::pair p = Geometry::GetPerimeterCoordinates(b + r, a + r, offsetPoints[r] + k); + pos += u.Multiply((halfSize.X + gap * r) * p.first / (b + r)) + + v.Multiply((halfSize.Y + gap * r) * p.second / (a + r)); } + + if (cmpPathfinder->CheckUnitPlacement(filter, pos.X, pos.Y, spawnedRadius, spawnedPass) == ICmpObstruction::FOUNDATION_CHECK_SUCCESS) + return CFixedVector3D(pos.X, fixed::Zero(), pos.Y); } - } return error; } Index: source/simulation2/components/ICmpRallyPoint.h =================================================================== --- /dev/null +++ source/simulation2/components/ICmpRallyPoint.h @@ -0,0 +1,38 @@ +/* Copyright (C) 2018 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_ICMPRALLYPOINT +#define INCLUDED_ICMPRALLYPOINT + +#include "simulation2/system/Interface.h" + +class CFixedVector2D; + +/** + * Rally point. + */ +class ICmpRallyPoint : public IComponent +{ +public: + virtual bool HasPositions() = 0; + + virtual CFixedVector2D GetFirstPosition() = 0; + + DECLARE_INTERFACE_TYPE(RallyPoint) +}; + +#endif // INCLUDED_ICMPRALLYPOINT Index: source/simulation2/components/ICmpRallyPoint.cpp =================================================================== --- /dev/null +++ source/simulation2/components/ICmpRallyPoint.cpp @@ -0,0 +1,45 @@ +/* Copyright (C) 2018 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "ICmpRallyPoint.h" + +#include "maths/FixedVector2D.h" +#include "simulation2/system/InterfaceScripted.h" +#include "simulation2/scripting/ScriptComponent.h" + +BEGIN_INTERFACE_WRAPPER(RallyPoint) +END_INTERFACE_WRAPPER(RallyPoint) + +class CCmpRallyPointScripted : public ICmpRallyPoint +{ +public: + DEFAULT_SCRIPT_WRAPPER(RallyPointScripted) + + virtual bool HasPositions() + { + return m_Script.Call("HasPositions"); + } + + virtual CFixedVector2D GetFirstPosition() + { + return m_Script.Call("GetFirstPosition"); + } +}; + +REGISTER_COMPONENT_SCRIPT_WRAPPER(RallyPointScripted) Index: source/simulation2/components/ICmpRallyPointRenderer.h =================================================================== --- source/simulation2/components/ICmpRallyPointRenderer.h +++ source/simulation2/components/ICmpRallyPointRenderer.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -15,8 +15,8 @@ * along with 0 A.D. If not, see . */ -#ifndef INCLUDED_ICMPRALLYPOINT -#define INCLUDED_ICMPRALLYPOINT +#ifndef INCLUDED_ICMPRALLYPOINTRENDERER +#define INCLUDED_ICMPRALLYPOINTRENDERER #include "maths/FixedVector2D.h" #include "simulation2/helpers/Position.h" @@ -53,4 +53,4 @@ DECLARE_INTERFACE_TYPE(RallyPointRenderer) }; -#endif // INCLUDED_ICMPRALLYPOINT +#endif // INCLUDED_ICMPRALLYPOINTRENDERER Index: source/simulation2/helpers/Geometry.h =================================================================== --- source/simulation2/helpers/Geometry.h +++ source/simulation2/helpers/Geometry.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -99,6 +99,22 @@ const CFixedVector2D& c0, const CFixedVector2D& u0, const CFixedVector2D& v0, const CFixedVector2D& halfSize0, const CFixedVector2D& c1, const CFixedVector2D& u1, const CFixedVector2D& v1, const CFixedVector2D& halfSize1); +/** + * Used in Footprint when spawning units: + * Given a grid point (x, y) on the rectangle [-x_max, x_max] x [-y_max, y_max], + * this returns the distance travelled in moving from (x_max, 0) to the the point + * and walking counter-clockwise along the perimeter of the rectangle. + */ +int GetPerimeterDistance(int x_max, int y_max, int x, int y); + +/** + * Used in Footprint when spawning units: + * This returns the grid point on the rectangle [-x_max, x_max] x [-y_max, y_max] + * reached after starting at (x_max, 0) and walking a distance k + * counter-clockwise along the perimeter of the rectangle. + */ +std::pair GetPerimeterCoordinates(int x_max, int y_max, int k); + } // namespace #endif // INCLUDED_HELPER_GEOMETRY Index: source/simulation2/helpers/Geometry.cpp =================================================================== --- source/simulation2/helpers/Geometry.cpp +++ source/simulation2/helpers/Geometry.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2018 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -317,3 +317,43 @@ return true; } + +int Geometry::GetPerimeterDistance(int x_max, int y_max, int x, int y) +{ + int quarter = x_max + y_max; + if (quarter <= 0) + return 0; + + if (x == x_max && y >= 0) + return y; + else if (y == y_max) + return quarter - x; + else if (x == -x_max) + return 2 * quarter - y; + else if (y == -y_max) + return 3 * quarter + x; + else if (x == x_max) + return 4 * quarter + y; + return 0; +} + +std::pair Geometry::GetPerimeterCoordinates(int x_max, int y_max, int k) +{ + int quarter = x_max + y_max; + if (quarter <= 0) + return std::pair(0, 0); + + k %= 4 * quarter; + if (k < 0) + k += 4 * quarter; + + if (k < y_max) + return std::pair(x_max, k); + else if (k < quarter + x_max) + return std::pair(quarter - k, y_max); + else if (k < 2 * quarter + y_max) + return std::pair(-x_max, 2 * quarter - k); + else if (k < 3 * quarter + x_max) + return std::pair(k - 3 * quarter, -y_max); + return std::pair(x_max, k - 4 * quarter); +}