/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see .
*
*/
#include "common/system.h"
#include "ultima/ultima.h"
#include "ultima/ultima8/misc/direction_util.h"
#include "ultima/ultima8/world/actors/actor.h"
#include "ultima/ultima8/world/actors/animation_tracker.h"
#ifdef DEBUG_PATHFINDER
#include "graphics/screen.h"
#include "ultima/ultima8/gumps/game_map_gump.h"
#endif
namespace Ultima {
namespace Ultima8 {
#ifdef DEBUG_PATHFINDER
ObjId Pathfinder::_visualDebugActor = 0xFFFF;
#endif
struct PathNode {
PathfindingState state;
unsigned int depth;
unsigned int cost;
unsigned int heuristicTotalCost;
PathNode *parent;
uint32 stepsfromparent;
};
// NOTE: this is just to keep some statistics
static unsigned int expandednodes = 0;
void PathfindingState::load(const Actor *_actor) {
_point = _actor->getLocation();
_lastAnim = _actor->getLastAnim();
_direction = _actor->getDir();
_firstStep = _actor->hasActorFlags(Actor::ACT_FIRSTSTEP);
_flipped = _actor->hasFlags(Item::FLG_FLIPPED);
_combat = _actor->isInCombat();
}
bool PathfindingState::checkPoint(const Point3 &pt,
int sqr_range) const {
int distance = _point.sqrDist(pt);
return distance < sqr_range;
}
bool PathfindingState::checkItem(const Item *item, int xyRange, int zRange) const {
int32 itemXd, itemYd, itemZd;
int32 itemXmin, itemYmin;
Point3 pt = item->getLocationAbsolute();
item->getFootpadWorld(itemXd, itemYd, itemZd);
itemXmin = pt.x - itemXd;
itemYmin = pt.y - itemYd;
int range = 0;
if (_point.x - pt.x > range)
range = _point.x - pt.x;
if (itemXmin - _point.x > range)
range = itemXmin - _point.x;
if (_point.y - pt.y > range)
range = _point.y - pt.y;
if (itemYmin - _point.y > range)
range = itemYmin - _point.y;
// FIXME: check _point.z properly
return (range <= xyRange);
}
bool PathfindingState::checkHit(const Actor *_actor, const Item *target) const {
assert(target);
debugC(kDebugPath, "Trying hit in _direction %d", _actor->getDirToItemCentre(*target));
AnimationTracker tracker;
if (!tracker.init(_actor, Animation::attack,
_actor->getDirToItemCentre(*target), this)) {
return false;
}
while (tracker.step()) {
if (tracker.hitSomething()) break;
}
ObjId hit = tracker.hitSomething();
if (hit == target->getObjId()) return true;
return false;
}
bool PathNodeCmp::operator()(const PathNode *n1, const PathNode *n2) const {
return (n1->heuristicTotalCost > n2->heuristicTotalCost);
}
Pathfinder::Pathfinder() : _actor(nullptr), _targetItem(nullptr),
_hitMode(false), _expandTime(0), _target(),
_actorXd(0), _actorYd(0), _actorZd(0) {
expandednodes = 0;
_visited.reserve(1500);
}
Pathfinder::~Pathfinder() {
debugC(kDebugPath, "~Pathfinder: %u nodes to clean up, visited %u and %u expanded nodes in %dms.",
_cleanupNodes.size(), _visited.size(), expandednodes, _expandTime);
// clean up _nodes
for (auto *node : _cleanupNodes)
delete node;
_cleanupNodes.clear();
}
void Pathfinder::init(Actor *actor, PathfindingState *state) {
_actor = actor;
_actor->getFootpadWorld(_actorXd, _actorYd, _actorZd);
if (state)
_start = *state;
else
_start.load(_actor);
}
void Pathfinder::setTarget(const Point3 &pt) {
_target = pt;
_targetItem = 0;
_hitMode = false;
}
void Pathfinder::setTarget(Item *item, bool hit) {
Container *root = item->getRootContainer();
_targetItem = root ? root : item;
// set target to centre of item for the cost heuristic
_target = item->getCentre();
_target.z = item->getZ();
if (hit) {
assert(_start._combat);
assert(dynamic_cast(_targetItem));
_hitMode = true;
} else {
_hitMode = false;
}
}
bool Pathfinder::canReach() {
Std::vector path;
return pathfind(path);
}
bool Pathfinder::alreadyVisited(const Point3 &pt) const {
//
// There are more efficient search structures we could use for
// this, but for the number of points we end up having even on
// pathfind failure (~1200) the fancy structures don't justify
// their extra overhead.
//
// Linear search of an array is just as fast, or slightly faster.
//
for (const auto &i : _visited) {
if (i.checkPoint(pt, 8*8))
return true;
}
return false;
}
bool Pathfinder::checkTarget(const PathNode *node) const {
// TODO: these ranges are probably a bit too high,
// but otherwise it won't work properly yet -wjp
if (_targetItem) {
if (_hitMode) {
return node->state.checkHit(_actor, _targetItem);
} else {
return node->state.checkItem(_targetItem, 32, 8);
}
} else {
return node->state.checkPoint(_target, 48*48);
}
}
unsigned int Pathfinder::costHeuristic(PathNode *node) const {
unsigned int cost = node->cost;
#if 0
double sqrddist;
sqrddist = (_target.x - node->state._point.x + _actorXd / 2) *
(_target.x - node->state._point.x + _actorXd / 2);
sqrddist += (_target.y - node->state._point.y + _actorYd / 2) *
(_target.y - node->state._point.y + _actorYd / 2);
unsigned int dist = static_cast(sqrt(sqrddist));
#else
// This calculates the distance to the target using only lines in
// the 8 available directions (instead of the straight line above)
int xd = ABS(_target.x - node->state._point.x + _actorXd / 2);
int yd = ABS(_target.y - node->state._point.y + _actorYd / 2);
double m = (xd < yd) ? xd : yd;
unsigned int dist = ABS(xd - yd) + static_cast(m * 1.41421356);
#endif
#if 0
//!! TODO: divide dist by walking distance
// (using 32 for now)
dist /= 32;
node->heuristicTotalCost = cost + (dist * 4); //!! constant
#else
// Weigh remaining distance more than already travelled distance,
// to try to explore more nodes closer to the target.
node->heuristicTotalCost = 2 * cost + 3 * dist;
#endif
return node->heuristicTotalCost;
}
#ifdef DEBUG_PATHFINDER
static void drawbox(Graphics::ManagedSurface *screen, const Item *item) {
int32 cx, cy, cz;
Ultima8Engine::get_instance()->getGameMapGump()->GetCameraLocation(cx, cy, cz);
Common::Rect d = screen->getBounds();
int32 ix, iy, iz;
item->getLocation(ix, iy, iz);
int32 xd, yd, zd;
item->getFootpadWorld(xd, yd, zd);
ix -= cx;
iy -= cy;
iz -= cz;
int32 x0, y0, x1, y1, x2, y2, x3, y3;
x0 = (d.width() / 2) + (ix - iy) / 4;
y0 = (d.height() / 2) + (ix + iy) / 8 - iz;
x1 = (d.width() / 2) + (ix - iy) / 4;
y1 = (d.height() / 2) + (ix + iy) / 8 - (iz + zd);
x2 = (d.width() / 2) + (ix - xd - iy) / 4;
y2 = (d.height() / 2) + (ix - xd + iy) / 8 - iz;
x3 = (d.width() / 2) + (ix - iy + yd) / 4;
y3 = (d.height() / 2) + (ix + iy - yd) / 8 - iz;
uint32 color = screen->format.RGBToColor(0x00, 0x00, 0xFF);
screen->fillRect(Common::Rect(x0 - 1, y0 - 1, x0 + 2, y0 + 2), color);
color = screen->format.RGBToColor(0x00, 0xFF, 0x00);
screen->drawLine(x0, y0, x1, y1, color);
screen->drawLine(x0, y0, x2, y2, color);
screen->drawLine(x0, y0, x3, y3, color);
}
static void drawdot(Graphics::ManagedSurface *screen, int32 x, int32 y, int32 Z, int size, uint32 rgb) {
int32 cx, cy, cz;
Ultima8Engine::get_instance()->getGameMapGump()->GetCameraLocation(cx, cy, cz);
Common::Rect d = screen->getBounds();
x -= cx;
y -= cy;
Z -= cz;
int32 x0, y0;
x0 = (d.width() / 2) + (x - y) / 4;
y0 = (d.height() / 2) + (x + y) / 8 - Z;
screen->fillRect(Common::Rect(x0 - size, y0 - size, x0 + size + 1, y0 + size + 1), rgb);
}
static void drawedge(Graphics::ManagedSurface *screen, const PathNode *from, const PathNode *to, uint32 rgb) {
int32 cx, cy, cz;
Ultima8Engine::get_instance()->getGameMapGump()->GetCameraLocation(cx, cy, cz);
Common::Rect d = screen->getBounds();
int32 x0, y0, x1, y1;
cx = from->state._point.x - cx;
cy = from->state._point.y - cy;
cz = from->state._point.z - cz;
x0 = (d.width() / 2) + (cx - cy) / 4;
y0 = (d.height() / 2) + (cx + cy) / 8 - cz;
Ultima8Engine::get_instance()->getGameMapGump()->GetCameraLocation(cx, cy, cz);
cx = to->state._point.x - cx;
cy = to->state._point.y - cy;
cz = to->state._point.z - cz;
x1 = (d.width() / 2) + (cx - cy) / 4;
y1 = (d.height() / 2) + (cx + cy) / 8 - cz;
screen->drawLine(x0, y0, x1, y1, rgb);
}
static void drawpath(Graphics::ManagedSurface *screen, PathNode *to, uint32 rgb, bool done) {
PathNode *n1 = to;
PathNode *n2 = to->parent;
uint32 color1 = screen->format.RGBToColor(0xFF, 0x00, 0x00);
uint32 color2 = screen->format.RGBToColor(0xFF, 0xFF, 0xFF);
while (n2) {
drawedge(screen, n1, n2, rgb);
if (done && n1 == to)
drawdot(screen, n1->state._point.x, n1->state._point.y, n1->state._point.z, 2, color1);
else
drawdot(screen, n1->state._point.x, n1->state._point.y, n1->state._point.z, 1, color2);
drawdot(screen, n2->state._point.x, n2->state._point.y, n2->state._point.z, 2, color2);
n1 = n2;
n2 = n1->parent;
}
}
#endif
void Pathfinder::newNode(PathNode *oldnode, PathfindingState &state,
unsigned int steps) {
PathNode *newnode = new PathNode();
newnode->state = state;
newnode->parent = oldnode;
newnode->depth = oldnode->depth + 1;
newnode->stepsfromparent = 0;
double sqrddist;
sqrddist = ((newnode->state._point.x - oldnode->state._point.x) *
(newnode->state._point.x - oldnode->state._point.x));
sqrddist += ((newnode->state._point.y - oldnode->state._point.y) *
(newnode->state._point.y - oldnode->state._point.y));
sqrddist += ((newnode->state._point.z - oldnode->state._point.z) *
(newnode->state._point.z - oldnode->state._point.z));
unsigned int dist;
dist = static_cast(sqrt(sqrddist));
int turn = 0;
if (oldnode->depth > 0) {
turn = state._direction - oldnode->state._direction;
if (turn < 0) turn = -turn;
if (turn > 8) turn = 16 - turn;
}
newnode->cost = oldnode->cost + dist + 32 * turn; //!! constant
bool done = checkTarget(newnode);
if (done)
newnode->heuristicTotalCost = 0;
else
costHeuristic(newnode);
debugC(kDebugPath, "Trying dir %d, steps %d from (%d, %d) to (%d, %d), cost %d, heurtotcost %d",
state._direction, steps,
oldnode->state._point.x, oldnode->state._point.y, newnode->state._point.x, newnode->state._point.y,
newnode->cost, newnode->heuristicTotalCost);
#ifdef DEBUG_PATHFINDER
if (_actor->getObjId() == _visualDebugActor) {
Graphics::Screen *screen = Ultima8Engine::get_instance()->getScreen();
uint32 color = screen->format.RGBToColor(0xFF, 0xFF, 0x00);
drawpath(screen, newnode, color, done);
screen->update();
g_system->delayMillis(50);
if (!done) {
color = screen->format.RGBToColor(0xB0, 0xB0, 0x00);
drawpath(screen, newnode, color, done);
screen->update();
}
}
#endif
_nodes.push(newnode);
}
void Pathfinder::expandNode(PathNode *node) {
Animation::Sequence walkanim = Animation::walk;
PathfindingState state, closeststate;
AnimationTracker tracker;
expandednodes++;
if (_actor->isInCombat())
walkanim = Animation::advance;
// try walking in all 8 directions - TODO: should this support 16 dirs?
Direction dir = dir_north;
for (int i = 0; i < 8; i++) {
dir = Direction_OneRight(dir, dirmode_8dirs);
state = node->state;
state._lastAnim = walkanim;
state._direction = dir;
state._combat = _actor->isInCombat();
if (!tracker.init(_actor, walkanim, dir, &state)) continue;
// determine how far the _actor will travel if the animation runs to completion
Point3 max_end;
tracker.evaluateMaxAnimTravel(max_end.x, max_end.y, dir);
max_end.z = state._point.z;
if (alreadyVisited(max_end))
continue;
const int x_travel = ABS(max_end.x - state._point.x);
const int y_travel = ABS(max_end.y - state._point.y);
const int xy_maxtravel = MAX(x_travel, y_travel);
int sqrddist = x_travel * x_travel + y_travel * y_travel;
if (sqrddist > 400) {
// range is greater than 20; see if a node has been visited at range 10
Point3 pt = state._point;
pt.x += x_travel * 10 / xy_maxtravel;
pt.y += y_travel * 10 / xy_maxtravel;
if (alreadyVisited(pt)) {
continue;
}
}
uint32 steps = 0, beststeps = 0;
int bestsqdist;
bestsqdist = (_target.x - node->state._point.x + _actorXd / 2) *
(_target.x - node->state._point.x + _actorXd / 2);
bestsqdist += (_target.y - node->state._point.y + _actorYd / 2) *
(_target.y - node->state._point.y + _actorYd / 2);
while (tracker.step()) {
steps++;
tracker.updateState(state);
sqrddist = (_target.x - state._point.x + _actorXd / 2) *
(_target.x - state._point.x + _actorXd / 2);
sqrddist += (_target.y - state._point.y + _actorYd / 2) *
(_target.y - state._point.y + _actorYd / 2);
if (sqrddist < bestsqdist) {
bestsqdist = sqrddist;
beststeps = steps;
closeststate = state;
}
}
if (tracker.isDone()) {
tracker.updateState(state);
if (!alreadyVisited(state._point)) {
newNode(node, state, 0);
_visited.push_back(state);
}
} else {
// an obstruction was encountered, so generate a visited node to block
// future evaluation at the endpoint.
_visited.push_back(state);
}
// TODO: maybe only allow partial steps close to target?
if (beststeps != 0 && (beststeps != steps ||
(!tracker.isDone() && _targetItem))) {
newNode(node, closeststate, beststeps);
_visited.push_back(closeststate);
}
}
}
bool Pathfinder::pathfind(Std::vector &path) {
if (_targetItem) {
debugC(kDebugPath, "Actor %u pathfinding to item %u", _actor->getObjId(), _targetItem->getObjId());
debugC(kDebugPath, "Target Item: %s", _targetItem->dumpInfo().c_str());
} else {
debugC(kDebugPath, "Actor %u pathfinding to (%d, %d, %d)", _actor->getObjId(), _target.x, _target.y, _target.z);
}
#ifdef DEBUG_PATHFINDER
if (_actor->getObjId() == _visualDebugActor) {
Graphics::Screen *screen = Ultima8Engine::get_instance()->getScreen();
if (_targetItem) {
drawbox(screen, _targetItem);
} else {
uint32 color = screen->format.RGBToColor(0x00, 0x00, 0xFF);
drawdot(screen, _targetX, _targetY, _targetZ, 2, color);
}
screen->update();
}
#endif
path.clear();
PathNode *startnode = new PathNode();
startnode->state = _start;
startnode->cost = 0;
startnode->parent = nullptr;
startnode->depth = 0;
startnode->stepsfromparent = 0;
_nodes.push(startnode);
unsigned int expandedNodes = 0;
const unsigned int NODELIMIT_MIN = 30; //! constant
const unsigned int NODELIMIT_MAX = 200; //! constant
bool found = false;
uint32 starttime = g_system->getMillis();
while (expandedNodes < NODELIMIT_MAX && !_nodes.empty() && !found) {
// Take a copy here as the pop() below deletes the old node
PathNode *node = new PathNode(*_nodes.top());
_cleanupNodes.push_back(node);
_nodes.pop();
debugC(kDebugPath, "Trying node: (%d, %d, %d) target=(%d, %d, %d)",
node->state._point.x, node->state._point.y, node->state._point.z,
_target.x, _target.y, _target.z);
if (checkTarget(node)) {
// done!
// find path length
const PathNode *n = node;
unsigned int length = 0;
while (n->parent) {
n = n->parent;
length++;
}
debugC(kDebugPath, "Pathfinder: path found (length = %u)", length);
unsigned int i = length;
if (length > 0) length++; // add space for final 'stand' action
path.resize(length);
// now backtrack through the _nodes to assemble the final animation
while (node->parent) {
PathfindingAction action;
action._action = node->state._lastAnim;
action._direction = node->state._direction;
action._steps = node->stepsfromparent;
path[--i] = action;
debugC(kDebugPath, "anim = %d, dir = %d, steps = %d",
node->state._lastAnim, node->state._direction, node->stepsfromparent);
//TODO: check how turns work
//TODO: append final 'stand' animation
node = node->parent;
}
if (length) {
if (node->state._combat)
path[length - 1]._action = Animation::combatStand;
else
path[length - 1]._action = Animation::stand;
path[length - 1]._direction = path[length - 2]._direction;
}
_expandTime = g_system->getMillis() - starttime;
return true;
}
expandNode(node);
expandedNodes++;
if (expandedNodes >= NODELIMIT_MIN && ((expandedNodes) % 5) == 0) {
uint32 elapsed_ms = g_system->getMillis() - starttime;
if (elapsed_ms > 350) break;
}
}
_expandTime = g_system->getMillis() - starttime;
static int32 pfcalls = 0;
static int32 pftotaltime = 0;
pfcalls++;
pftotaltime += _expandTime;
debugC(kDebugPath, "maxout average = %dms.", pftotaltime / pfcalls);
return false;
}
} // End of namespace Ultima8
} // End of namespace Ultima