The open source repository for the action RPG game in development by Sig Productions titled 'Adventures in Lestoria'!
https://forums.lestoria.net
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
312 lines
11 KiB
312 lines
11 KiB
#pragma region License
|
|
/*
|
|
License (OLC-3)
|
|
~~~~~~~~~~~~~~~
|
|
|
|
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
|
|
|
|
Redistribution and use in source and binary forms, with or without modification,
|
|
are permitted provided that the following conditions are met:
|
|
|
|
1. Redistributions or derivations of source code must retain the above copyright
|
|
notice, this list of conditions and the following disclaimer.
|
|
|
|
2. Redistributions or derivative works in binary form must reproduce the above
|
|
copyright notice. This list of conditions and the following disclaimer must be
|
|
reproduced in the documentation and/or other materials provided with the distribution.
|
|
|
|
3. Neither the name of the copyright holder nor the names of its contributors may
|
|
be used to endorse or promote products derived from this software without specific
|
|
prior written permission.
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
|
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
|
|
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
|
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
SUCH DAMAGE.
|
|
|
|
Portions of this software are copyright © 2024 The FreeType
|
|
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
|
|
All rights reserved.
|
|
*/
|
|
#pragma endregion
|
|
#include "Pathfinding.h"
|
|
#include "DEFINES.h"
|
|
#include "AdventuresInLestoria.h"
|
|
|
|
INCLUDE_game
|
|
|
|
const vi2d Pathfinding::gridSpacing=vf2d{12,12};
|
|
|
|
void Pathfinding::Initialize(){
|
|
nodes.clear();
|
|
sNode*lastNodeAdded=nullptr;
|
|
for (int x = 0; x < game->GetCurrentMapData().width*24; x+=gridSpacing.x)
|
|
for (int y = 0; y < game->GetCurrentMapData().height*24; y+=gridSpacing.y)
|
|
{
|
|
bool tileIsEmpty=true;
|
|
for(const LayerTag&layer:game->GetCurrentMap().GetLayers()){
|
|
int tileID=layer.tiles[y/24][x/24]-1;
|
|
if(tileID!=-1){
|
|
tileIsEmpty=false;
|
|
break;
|
|
}
|
|
}
|
|
if(tileIsEmpty)continue; //Don't add the node since it's outside the map.
|
|
|
|
sNode&node=nodes[{x,y}];
|
|
node.x = x; // ...because we give each node its own coordinates
|
|
node.y = y; // ...because we give each node its own coordinates
|
|
geom2d::rect<float>tile=game->GetTileCollision(game->GetCurrentLevel(),{float(x),float(y)});
|
|
if(tile==game->NO_COLLISION){
|
|
node.bObstacle=false;
|
|
}else{
|
|
node.bObstacle=geom2d::overlaps(vf2d{fmodf(x+gridSpacing.x/2,24),fmodf(y+gridSpacing.y/2,24)},tile);
|
|
}
|
|
tile=game->GetTileCollision(game->GetCurrentLevel(),{float(x),float(y)},true);
|
|
if(tile==game->NO_COLLISION){
|
|
node.bObstacleUpper=false;
|
|
}else{
|
|
node.bObstacleUpper=geom2d::overlaps(vf2d{fmodf(x,24),fmodf(y,24)},tile);
|
|
}
|
|
node.parent = nullptr;
|
|
node.bVisited = false;
|
|
if(nodes.size()==1){//This is the first node added, set as the start node.
|
|
nodeStart=&node;
|
|
}
|
|
nodeEnd=&node;
|
|
}
|
|
|
|
for (auto&[key,node]:nodes){
|
|
if(nodes.find({node.x,node.y-gridSpacing.y})!=nodes.end()){
|
|
node.vecNeighbours.push_back(&nodes[{node.x,node.y-gridSpacing.y}]);
|
|
}
|
|
if(nodes.find({node.x,node.y+gridSpacing.y})!=nodes.end()){
|
|
node.vecNeighbours.push_back(&nodes[{node.x,node.y+gridSpacing.y}]);
|
|
}
|
|
if(nodes.find({node.x-gridSpacing.x,node.y})!=nodes.end()){
|
|
node.vecNeighbours.push_back(&nodes[{node.x-gridSpacing.x,node.y}]);
|
|
}
|
|
if(nodes.find({node.x+gridSpacing.x,node.y})!=nodes.end()){
|
|
node.vecNeighbours.push_back(&nodes[{node.x+gridSpacing.x,node.y}]);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<vf2d> Pathfinding::Solve_AStar(vf2d startPos,vf2d endPos,float maxRange,bool upperLevel){
|
|
float dist=float(sqrt(pow(endPos.x-startPos.x,2)+pow(endPos.y-startPos.y,2)));
|
|
if(dist>maxRange*game->GetCurrentMapData().tilewidth)return{};
|
|
|
|
startPos=vi2d(startPos/gridSpacing.x)*gridSpacing.x;
|
|
endPos=vi2d(endPos/gridSpacing.x)*gridSpacing.x;
|
|
|
|
if(nodes.find(startPos)==nodes.end())return{};
|
|
if(nodes.find(endPos)==nodes.end())return{};
|
|
|
|
nodeStart=&nodes[startPos];
|
|
nodeEnd=&nodes[endPos];
|
|
|
|
geom2d::rect<int>posPerimeter{{int(std::min(startPos.x,endPos.x)),int(std::min(startPos.y,endPos.y))},{int(abs(endPos.x-startPos.x)),int(abs(endPos.y-startPos.y))}};
|
|
posPerimeter.pos={int(std::clamp(posPerimeter.pos.x-maxRange*game->GetCurrentMapData().tilewidth,0.f,game->GetCurrentMapData().width*float(game->GetCurrentMapData().tilewidth))),int(std::clamp(posPerimeter.pos.y-maxRange*game->GetCurrentMapData().tilewidth,0.f,game->GetCurrentMapData().height*float(game->GetCurrentMapData().tilewidth)))};
|
|
posPerimeter.size={int(std::clamp(posPerimeter.size.x+maxRange*game->GetCurrentMapData().tilewidth*2,0.f,game->GetCurrentMapData().width*float(game->GetCurrentMapData().tilewidth)-posPerimeter.pos.x)),int(std::clamp(posPerimeter.size.y+maxRange*game->GetCurrentMapData().tilewidth*2,0.f,game->GetCurrentMapData().height*float(game->GetCurrentMapData().tilewidth)-posPerimeter.pos.y))};
|
|
|
|
for (auto&[key,node]:nodes){
|
|
if(geom2d::overlaps(posPerimeter,vi2d{node.x,node.y})){
|
|
node.bVisited = false;
|
|
}else{
|
|
node.bVisited = true;
|
|
}
|
|
node.fGlobalGoal = INFINITY;
|
|
node.fLocalGoal = INFINITY;
|
|
node.parent = nullptr; // No parents
|
|
}
|
|
|
|
auto distance = [](sNode* a, sNode* b) // For convenience
|
|
{
|
|
return sqrtf(float((a->x - b->x)*(a->x - b->x) + (a->y - b->y)*(a->y - b->y)));
|
|
};
|
|
|
|
auto heuristic = [distance](sNode* a, sNode* b)
|
|
{
|
|
return distance(a, b);
|
|
};
|
|
|
|
sNode *nodeCurrent = nodeStart;
|
|
nodeStart->fLocalGoal = 0.0f;
|
|
nodeStart->fGlobalGoal = heuristic(nodeStart, nodeEnd);
|
|
|
|
std::list<sNode*> listNotTestedNodes;
|
|
//if((!upperLevel && nodeStart->bObstacle)||(upperLevel && nodeStart->bObstacleUpper))return {};
|
|
listNotTestedNodes.push_back(nodeStart);
|
|
|
|
while (!listNotTestedNodes.empty() && nodeCurrent != nodeEnd)
|
|
{
|
|
listNotTestedNodes.sort([](const sNode* lhs, const sNode* rhs){ return lhs->fGlobalGoal < rhs->fGlobalGoal; } );
|
|
|
|
while(!listNotTestedNodes.empty() && listNotTestedNodes.front()->bVisited)
|
|
listNotTestedNodes.pop_front();
|
|
if (listNotTestedNodes.empty())
|
|
break;
|
|
|
|
nodeCurrent = listNotTestedNodes.front();
|
|
nodeCurrent->bVisited = true;
|
|
for (auto nodeNeighbour : nodeCurrent->vecNeighbours)
|
|
{
|
|
if (!nodeNeighbour->bVisited && ((!upperLevel && !nodeNeighbour->bObstacle)||(upperLevel && !nodeNeighbour->bObstacleUpper)))
|
|
listNotTestedNodes.push_back(nodeNeighbour);
|
|
|
|
float fPossiblyLowerGoal = nodeCurrent->fLocalGoal + distance(nodeCurrent, nodeNeighbour);
|
|
|
|
if (fPossiblyLowerGoal < nodeNeighbour->fLocalGoal)
|
|
{
|
|
nodeNeighbour->parent = nodeCurrent;
|
|
nodeNeighbour->fLocalGoal = fPossiblyLowerGoal;
|
|
nodeNeighbour->fGlobalGoal = nodeNeighbour->fLocalGoal + heuristic(nodeNeighbour, nodeEnd);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nodeEnd != nullptr)
|
|
{
|
|
int pathLength=INFINITE;
|
|
sNode *p = nodeEnd;
|
|
std::vector<vf2d>finalPath;
|
|
while (p->parent != nullptr)
|
|
{
|
|
if(pathLength==INFINITE){
|
|
pathLength=1;
|
|
} else {
|
|
pathLength++;
|
|
}
|
|
finalPath.push_back({float((*p).x),float((*p).y)});
|
|
p = p->parent;
|
|
}
|
|
std::reverse(finalPath.begin(),finalPath.end());
|
|
return finalPath;
|
|
} else {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
Pathfinding::sSpline Pathfinding::Solve_WalkPath(vf2d startPos,vf2d endPos,float maxRange,bool upperLevel){
|
|
maxRange*=(24/game->pathfinder.gridSpacing.x);
|
|
Pathfinding::sSpline newSpline{};
|
|
newSpline.Initialize(Solve_AStar(startPos,endPos,maxRange,upperLevel));
|
|
return newSpline;
|
|
}
|
|
|
|
void Pathfinding::sSpline::Initialize(const std::vector<vf2d>&points){
|
|
this->points.clear();
|
|
for(const vf2d&point:points){
|
|
this->points.push_back({vf2d{point}+gridSpacing/2});
|
|
}
|
|
fTotalSplineLength=0.f;
|
|
for (int i = 0; i < this->points.size(); i++)
|
|
{
|
|
fTotalSplineLength += (this->points[i].length = CalculateSegmentLength(i, true));
|
|
}
|
|
}
|
|
|
|
Pathfinding::sPoint2D Pathfinding::sSpline::GetSplinePoint(float t, bool bLooped)const{
|
|
int p0, p1, p2, p3;
|
|
float t1=t;
|
|
if (!bLooped)
|
|
{
|
|
p1 = (int)t1 + 1;
|
|
p2 = p1 + 1;
|
|
p3 = p2 + 1;
|
|
p0 = p1 - 1;
|
|
}
|
|
else
|
|
{
|
|
p1 = (int)t1;
|
|
p2 = (p1 + 1) % points.size();
|
|
p3 = (p2 + 1) % points.size();
|
|
p0 = p1 >= 1 ? p1 - 1 : points.size() - 1;
|
|
}
|
|
|
|
t = t - (int)t;
|
|
|
|
float tt = t * t;
|
|
float ttt = tt * t;
|
|
|
|
float q1 = -ttt + 2.0f*tt - t;
|
|
float q2 = 3.0f*ttt - 5.0f*tt + 2.0f;
|
|
float q3 = -3.0f*ttt + 4.0f*tt + t;
|
|
float q4 = ttt - tt;
|
|
|
|
float tx = 0.5f * (points[p0].pos.x * q1 + points[p1].pos.x * q2 + points[p2].pos.x * q3 + points[p3].pos.x * q4);
|
|
float ty = 0.5f * (points[p0].pos.y * q1 + points[p1].pos.y * q2 + points[p2].pos.y * q3 + points[p3].pos.y * q4);
|
|
|
|
return{ {tx, ty} };
|
|
}
|
|
|
|
Pathfinding::sPoint2D Pathfinding::sSpline::GetSplineGradient(float t, bool bLooped){
|
|
int p0, p1, p2, p3;
|
|
float t1=t;
|
|
if (!bLooped)
|
|
{
|
|
p1 = (int)t1 + 1;
|
|
p2 = p1 + 1;
|
|
p3 = p2 + 1;
|
|
p0 = p1 - 1;
|
|
}
|
|
else
|
|
{
|
|
p1 = (int)t1;
|
|
p2 = (p1 + 1) % points.size();
|
|
p3 = (p2 + 1) % points.size();
|
|
p0 = p1 >= 1 ? p1 - 1 : points.size() - 1;
|
|
}
|
|
|
|
t = t - (int)t;
|
|
|
|
float tt = t * t;
|
|
float ttt = tt * t;
|
|
|
|
float q1 = -3.0f * tt + 4.0f*t - 1;
|
|
float q2 = 9.0f*tt - 10.0f*t;
|
|
float q3 = -9.0f*tt + 8.0f*t + 1.0f;
|
|
float q4 = 3.0f*tt - 2.0f*t;
|
|
|
|
float tx = 0.5f * (points[p0].pos.x * q1 + points[p1].pos.x * q2 + points[p2].pos.x * q3 + points[p3].pos.x * q4);
|
|
float ty = 0.5f * (points[p0].pos.y * q1 + points[p1].pos.y * q2 + points[p2].pos.y * q3 + points[p3].pos.y * q4);
|
|
|
|
return{ {tx, ty} };
|
|
}
|
|
|
|
float Pathfinding::sSpline::CalculateSegmentLength(int node, bool bLooped){
|
|
float fLength = 0.0f;
|
|
float fStepSize = 0.005;
|
|
|
|
sPoint2D old_point, new_point;
|
|
old_point = GetSplinePoint((float)node, bLooped);
|
|
|
|
for (float t = 0; t < 1.0f; t += fStepSize)
|
|
{
|
|
new_point = GetSplinePoint(fmod((float)node + t,1.0f), bLooped);
|
|
fLength += sqrtf((new_point.pos.x - old_point.pos.x)*(new_point.pos.x - old_point.pos.x)
|
|
+ (new_point.pos.y - old_point.pos.y)*(new_point.pos.y - old_point.pos.y));
|
|
old_point = new_point;
|
|
}
|
|
|
|
return fLength;
|
|
}
|
|
|
|
float Pathfinding::sSpline::GetNormalisedOffset(float p){
|
|
// Which node is the base?
|
|
int i = 0;
|
|
while (p > points[i].length)
|
|
{
|
|
p -= points[i].length;
|
|
i++;
|
|
}
|
|
|
|
// The fractional is the offset
|
|
return (float)i + (p / points[i].length);
|
|
} |