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 Boolean(this.pos.length); +}; + +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 +++ binaries/data/mods/public/simulation/components/interfaces/RallyPoint.js @@ -1 +0,0 @@ -Engine.RegisterInterface("RallyPoint"); Index: source/simulation2/TypeList.h =================================================================== --- source/simulation2/TypeList.h +++ source/simulation2/TypeList.h @@ -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 @@ -25,10 +25,59 @@ #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/MessageTypes.h" #include "graphics/Terrain.h" // For TERRAIN_TILE_SIZE #include "maths/FixedVector2D.h" +#include "simulation2/helpers/Geometry.h" + +/** + * Given a grid point (x, y) on the rectangle [-x_max, x_max] x [-y_max, y_max], + * this returns the number of steps needed to reach the point starting at (x_max, 0) + * and walking counter-clockwise along the perimeter of the rectangle. + */ +int GetPerimeterSteps(int x_max, int y_max, int x, int y) +{ + int quarter = x_max + y_max; + 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; +} + +/** + * 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 counter-clockwise along + * the perimeter of the rectangle for k steps. + */ +std::pair 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); +} class CCmpFootprint : public ICmpFootprint { @@ -147,7 +196,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::Epsilon() * 10; ICmpObstructionManager::tag_t spawnedTag; CmpPtr cmpSpawnedObstruction(GetSimContext(), spawned); @@ -156,9 +206,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 +217,87 @@ 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; + + // 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()); + + // As of a22, infantry has radius 0.8m = three rows and siege has radius 3m = one row. + int rows = std::max(1, std::min(3, (fixed::FromInt(10) / gap).ToInt_RoundToNearest())); + + // 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); + std::vector offsetSteps(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 by angle/2pi to find the steps. + for (int r = 0; r < rows; ++r) + offsetSteps[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 to steps. + 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) + offsetSteps[r] = GetPerimeterSteps( + 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 steps by 2pi / 6*(a+r) to get the angle. + pos += u.Rotate(fixed::Pi() * (offsetSteps[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 steps to coordinates and scale. + std::pair p = GetPerimeterCoordinates(b + r, a + r, offsetSteps[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 =================================================================== --- source/simulation2/components/ICmpRallyPoint.h +++ source/simulation2/components/ICmpRallyPoint.h @@ -0,0 +1,38 @@ +/* Copyright (C) 2017 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 =================================================================== --- source/simulation2/components/ICmpRallyPoint.cpp +++ source/simulation2/components/ICmpRallyPoint.cpp @@ -0,0 +1,45 @@ +/* Copyright (C) 2017 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 @@ -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