Files
scummvm-cursorfix/engines/ultima/ultima4/map/map.cpp
2026-02-02 04:50:13 +01:00

779 lines
21 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "ultima/ultima4/ultima4.h"
#include "ultima/ultima4/map/map.h"
#include "ultima/ultima4/map/annotation.h"
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/map/direction.h"
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/map/movement.h"
#include "ultima/ultima4/game/object.h"
#include "ultima/ultima4/game/person.h"
#include "ultima/ultima4/game/player.h"
#include "ultima/ultima4/game/portal.h"
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/ultima4/map/tileset.h"
#include "ultima/ultima4/map/tilemap.h"
#include "ultima/ultima4/core/types.h"
#include "ultima/ultima4/core/utils.h"
#include "ultima/ultima4/core/settings.h"
namespace Ultima {
namespace Ultima4 {
bool MapCoords::operator==(const MapCoords &a) const {
return (x == a.x) && (y == a.y) && (z == a.z);
}
bool MapCoords::operator!=(const MapCoords &a) const {
return !operator==(a);
}
bool MapCoords::operator<(const MapCoords &a) const {
if (x > a.x)
return false;
if (y > a.y)
return false;
return z < a.z;
}
MapCoords &MapCoords::wrap(const Map *map) {
if (map && map->_borderBehavior == Map::BORDER_WRAP) {
while (x < 0)
x += map->_width;
while (y < 0)
y += map->_height;
while (x >= (int)map->_width)
x -= map->_width;
while (y >= (int)map->_height)
y -= map->_height;
}
return *this;
}
MapCoords &MapCoords::putInBounds(const Map *map) {
if (map) {
if (x < 0)
x = 0;
if (x >= (int) map->_width)
x = map->_width - 1;
if (y < 0)
y = 0;
if (y >= (int) map->_height)
y = map->_height - 1;
if (z < 0)
z = 0;
if (z >= (int) map->_levels)
z = map->_levels - 1;
}
return *this;
}
MapCoords &MapCoords::move(Direction d, const Map *map) {
switch (d) {
case DIR_NORTH:
y--;
break;
case DIR_EAST:
x++;
break;
case DIR_SOUTH:
y++;
break;
case DIR_WEST:
x--;
break;
default:
break;
}
// Wrap the coordinates if necessary
wrap(map);
return *this;
}
MapCoords &MapCoords::move(int dx, int dy, const Map *map) {
x += dx;
y += dy;
// Wrap the coordinates if necessary
wrap(map);
return *this;
}
int MapCoords::getRelativeDirection(const MapCoords &c, const Map *map) const {
int dx, dy, dirmask;
dirmask = DIR_NONE;
if (z != c.z)
return dirmask;
// Adjust our coordinates to find the closest path
if (map && map->_borderBehavior == Map::BORDER_WRAP) {
MapCoords me = *this;
if (abs(int(me.x - c.x)) > abs(int(me.x + map->_width - c.x)))
me.x += map->_width;
else if (abs(int(me.x - c.x)) > abs(int(me.x - map->_width - c.x)))
me.x -= map->_width;
if (abs(int(me.y - c.y)) > abs(int(me.y + map->_width - c.y)))
me.y += map->_height;
else if (abs(int(me.y - c.y)) > abs(int(me.y - map->_width - c.y)))
me.y -= map->_height;
dx = me.x - c.x;
dy = me.y - c.y;
} else {
dx = x - c.x;
dy = y - c.y;
}
// Add x directions that lead towards to_x to the mask
if (dx < 0) dirmask |= MASK_DIR(DIR_EAST);
else if (dx > 0) dirmask |= MASK_DIR(DIR_WEST);
// Add y directions that lead towards to_y to the mask
if (dy < 0) dirmask |= MASK_DIR(DIR_SOUTH);
else if (dy > 0) dirmask |= MASK_DIR(DIR_NORTH);
// Return the result
return dirmask;
}
Direction MapCoords::pathTo(const MapCoords &c, int valid_directions, bool towards, const Map *map) const {
int directionsToObject;
// Find the directions that lead [to/away from] our target
directionsToObject = towards ? getRelativeDirection(c, map) : ~getRelativeDirection(c, map);
// Make sure we eliminate impossible options
directionsToObject &= valid_directions;
// Get the new direction to move
if (directionsToObject > DIR_NONE)
return dirRandomDir(directionsToObject);
// There are no valid directions that lead to our target, just move wherever we can!
else
return dirRandomDir(valid_directions);
}
Direction MapCoords::pathAway(const MapCoords &c, int valid_directions) const {
return pathTo(c, valid_directions, false);
}
int MapCoords::movementDistance(const MapCoords &c, const Map *map) const {
int dirmask = DIR_NONE;
int dist = 0;
MapCoords me = *this;
if (z != c.z)
return -1;
// Get the direction(s) to the coordinates
dirmask = getRelativeDirection(c, map);
while ((me.x != c.x) || (me.y != c.y)) {
if (me.x != c.x) {
if (dirmask & MASK_DIR_WEST)
me.move(DIR_WEST, map);
else
me.move(DIR_EAST, map);
dist++;
}
if (me.y != c.y) {
if (dirmask & MASK_DIR_NORTH)
me.move(DIR_NORTH, map);
else
me.move(DIR_SOUTH, map);
dist++;
}
}
return dist;
}
int MapCoords::distance(const MapCoords &c, const Map *map) const {
int dist = movementDistance(c, map);
if (dist <= 0)
return dist;
// Calculate how many fewer movements there would have been
dist -= abs(x - c.x) < abs(y - c.y) ? abs(x - c.x) : abs(y - c.y);
return dist;
}
/*-------------------------------------------------------------------*/
Map::Map() : _id(0), _type(WORLD), _width(0), _height(0), _levels(1),
_chunkWidth(0), _chunkHeight(0), _offset(0), _flags(0),
_borderBehavior(BORDER_WRAP), _music(Music::NONE),
_tileSet(nullptr), _tileMap(nullptr), _blank(0) {
_annotations = new AnnotationMgr();
}
Map::~Map() {
for (auto *i : _portals)
delete i;
delete _annotations;
}
Common::String Map::getName() {
return _baseSource._fname;
}
Object *Map::objectAt(const Coords &coords) {
// FIXME: return a list instead of one object
ObjectDeque::const_iterator i;
Object *objAt = nullptr;
for (i = _objects.begin(); i != _objects.end(); i++) {
Object *obj = *i;
if (obj->getCoords() == coords) {
// Get the most visible object
if (objAt && (objAt->getType() == Object::UNKNOWN) && (obj->getType() != Object::UNKNOWN))
objAt = obj;
// Give priority to objects that have the focus
else if (objAt && (!objAt->hasFocus()) && (obj->hasFocus()))
objAt = obj;
else if (!objAt)
objAt = obj;
}
}
return objAt;
}
const Portal *Map::portalAt(const Coords &coords, int actionFlags) {
PortalList::const_iterator i;
for (i = _portals.begin(); i != _portals.end(); i++) {
if (((*i)->_coords == coords) &&
((*i)->_triggerAction & actionFlags))
return *i;
}
return nullptr;
}
MapTile *Map::getTileFromData(const Coords &coords) {
if (MAP_IS_OOB(this, coords))
return &_blank;
int index = coords.x + (coords.y * _width) + (_width * _height * coords.z);
return &_data[index];
}
MapTile *Map::tileAt(const Coords &coords, int withObjects) {
// FIXME: this should return a list of tiles, with the most visible at the front
MapTile *tile;
Common::List<Annotation *> a = _annotations->ptrsToAllAt(coords);
Common::List<Annotation *>::iterator i;
Object *obj = objectAt(coords);
tile = getTileFromData(coords);
// FIXME: this only returns the first valid annotation it can find
if (a.size() > 0) {
for (i = a.begin(); i != a.end(); i++) {
if (!(*i)->isVisualOnly())
return &(*i)->getTile();
}
}
if ((withObjects == WITH_OBJECTS) && obj)
tile = &obj->getTile();
else if ((withObjects == WITH_GROUND_OBJECTS) &&
obj &&
obj->getTile().getTileType()->isWalkable())
tile = &obj->getTile();
return tile;
}
const Tile *Map::tileTypeAt(const Coords &coords, int withObjects) {
MapTile *tile = tileAt(coords, withObjects);
return tile->getTileType();
}
bool Map::isWorldMap() {
return _type == WORLD;
}
bool Map::isEnclosed(const Coords &party) {
uint x, y;
int *path_data;
if (_borderBehavior != BORDER_WRAP)
return true;
path_data = new int[_width * _height];
memset(path_data, -1, sizeof(int) * _width * _height);
// Determine what's walkable (1), and what's border-walkable (2)
findWalkability(party, path_data);
// Find two connecting pathways where the avatar can reach both without wrapping
for (x = 0; x < _width; x++) {
int index = x;
if (path_data[index] == 2 && path_data[index + ((_height - 1)*_width)] == 2)
return false;
}
for (y = 0; y < _width; y++) {
int index = (y * _width);
if (path_data[index] == 2 && path_data[index + _width - 1] == 2)
return false;
}
return true;
}
void Map::findWalkability(Coords coords, int *path_data) {
const Tile *mt = tileTypeAt(coords, WITHOUT_OBJECTS);
int index = coords.x + (coords.y * _width);
if (mt->isWalkable()) {
bool isBorderTile = (coords.x == 0) || (coords.x == signed(_width - 1)) || (coords.y == 0) || (coords.y == signed(_height - 1));
path_data[index] = isBorderTile ? 2 : 1;
if ((coords.x > 0) && path_data[coords.x - 1 + (coords.y * _width)] < 0)
findWalkability(Coords(coords.x - 1, coords.y, coords.z), path_data);
if ((coords.x < signed(_width - 1)) && path_data[coords.x + 1 + (coords.y * _width)] < 0)
findWalkability(Coords(coords.x + 1, coords.y, coords.z), path_data);
if ((coords.y > 0) && path_data[coords.x + ((coords.y - 1) * _width)] < 0)
findWalkability(Coords(coords.x, coords.y - 1, coords.z), path_data);
if ((coords.y < signed(_height - 1)) && path_data[coords.x + ((coords.y + 1) * _width)] < 0)
findWalkability(Coords(coords.x, coords.y + 1, coords.z), path_data);
} else {
path_data[index] = 0;
}
}
Creature *Map::addCreature(const Creature *creature, Coords coords) {
Creature *m = new Creature();
// Make a copy of the creature before placing it
*m = *creature;
m->setInitialHp();
m->setStatus(STAT_GOOD);
m->setCoords(coords);
m->setMap(this);
// initialize the creature before placing it
if (m->wanders())
m->setMovementBehavior(MOVEMENT_WANDER);
else if (m->isStationary())
m->setMovementBehavior(MOVEMENT_FIXED);
else m->setMovementBehavior(MOVEMENT_ATTACK_AVATAR);
// Hide camouflaged creatures from view during combat
if (m->camouflages() && (_type == COMBAT))
m->setVisible(false);
// place the creature on the map
_objects.push_back(m);
return m;
}
Object *Map::addObject(Object *obj, Coords coords) {
_objects.push_front(obj);
return obj;
}
Object *Map::addObject(MapTile tile, MapTile prevtile, Coords coords) {
Object *obj = new Object();
obj->setTile(tile);
obj->setPrevTile(prevtile);
obj->setCoords(coords);
obj->setPrevCoords(coords);
obj->setMap(this);
_objects.push_front(obj);
return obj;
}
void Map::removeObject(const Object *rem, bool deleteObject) {
ObjectDeque::iterator i;
for (i = _objects.begin(); i != _objects.end(); i++) {
if (*i == rem) {
// Party members persist through different maps, so don't delete them!
if (!isPartyMember(*i) && deleteObject)
delete(*i);
_objects.erase(i);
return;
}
}
}
ObjectDeque::iterator Map::removeObject(ObjectDeque::iterator rem, bool deleteObject) {
// Party members persist through different maps, so don't delete them!
if (!isPartyMember(*rem) && deleteObject)
delete(*rem);
return _objects.erase(rem);
}
Creature *Map::moveObjects(MapCoords avatar) {
Creature *attacker = nullptr;
for (auto *object : _objects) {
Creature *m = dynamic_cast<Creature *>(object);
if (m) {
/* check if the object is an attacking creature and not
just a normal, docile person in town or an inanimate object */
if ((m->getType() == Object::PERSON && m->getMovementBehavior() == MOVEMENT_ATTACK_AVATAR) ||
(m->getType() == Object::CREATURE && m->willAttack())) {
MapCoords o_coords = m->getCoords();
// Don't move objects that aren't on the same level as us
if (o_coords.z != avatar.z)
continue;
if (o_coords.movementDistance(avatar, this) <= 1) {
attacker = m;
continue;
}
}
// Before moving, Enact any special effects of the creature (such as storms eating objects, whirlpools teleporting, etc.)
m->specialEffect();
// Perform any special actions (such as pirate ships firing cannons, sea serpents' fireblast attect, etc.)
if (!m->specialAction()) {
if (moveObject(this, m, avatar)) {
m->animateMovement();
// After moving, Enact any special effects of the creature (such as storms eating objects, whirlpools teleporting, etc.)
m->specialEffect();
}
}
}
}
return attacker;
}
void Map::resetObjectAnimations() {
ObjectDeque::iterator i;
for (i = _objects.begin(); i != _objects.end(); i++) {
Object *obj = *i;
if (obj->getType() == Object::CREATURE)
obj->setPrevTile(creatureMgr->getByTile(obj->getTile())->getTile());
}
}
void Map::clearObjects() {
_objects.clear();
}
int Map::getNumberOfCreatures() {
ObjectDeque::const_iterator i;
int n = 0;
for (i = _objects.begin(); i != _objects.end(); i++) {
Object *obj = *i;
if (obj->getType() == Object::CREATURE)
n++;
}
return n;
}
int Map::getValidMoves(MapCoords from, MapTile transport) {
int retval;
Direction d;
Object *obj;
const Creature *m, *to_m;
int ontoAvatar, ontoCreature;
MapCoords coords = from;
// Get the creature object, if it exists (the one that's moving)
m = creatureMgr->getByTile(transport);
bool isAvatar = (g_context->_location->_coords == coords);
if (m && m->canMoveOntoPlayer())
isAvatar = false;
retval = 0;
for (d = DIR_WEST; d <= DIR_SOUTH; d = (Direction)(d + 1)) {
coords = from;
ontoAvatar = 0;
ontoCreature = 0;
// Move the coordinates in the current direction and test it
coords.move(d, this);
// You can always walk off the edge of the map
if (MAP_IS_OOB(this, coords)) {
retval = DIR_ADD_TO_MASK(d, retval);
continue;
}
obj = objectAt(coords);
// See if it's trying to move onto the avatar
if ((_flags & SHOW_AVATAR) && (coords == g_context->_location->_coords))
ontoAvatar = 1;
// See if it's trying to move onto a person or creature
else if (obj && (obj->getType() != Object::UNKNOWN))
ontoCreature = 1;
// Get the destination tile
MapTile tile;
if (ontoAvatar)
tile = g_context->_party->getTransport();
else if (ontoCreature)
tile = obj->getTile();
else
tile = *tileAt(coords, WITH_OBJECTS);
MapTile prev_tile = *tileAt(from, WITHOUT_OBJECTS);
// Get the other creature object, if it exists (the one that's being moved onto)
to_m = dynamic_cast<Creature *>(obj);
// Move on if unable to move onto the avatar or another creature
if (m && !isAvatar) { // some creatures/persons have the same tile as the avatar, so we have to adjust
// If moving onto the avatar, the creature must be able to move onto the player
// If moving onto another creature, it must be able to move onto other creatures,
// and the creature must be able to have others move onto it. If either of
// these conditions are not met, the creature cannot move onto another.
if ((ontoAvatar && m->canMoveOntoPlayer()) || (ontoCreature && m->canMoveOntoCreatures()))
tile = *tileAt(coords, WITHOUT_OBJECTS); //Ignore all objects, and just consider terrain
if ((ontoAvatar && !m->canMoveOntoPlayer())
|| (
ontoCreature &&
(
(!m->canMoveOntoCreatures() && !to_m->canMoveOntoCreatures())
|| (m->isForceOfNature() && to_m->isForceOfNature())
)
)
)
continue;
}
// Avatar movement
if (isAvatar) {
// if the transport is a ship, check sailable
if (transport.getTileType()->isShip() && tile.getTileType()->isSailable())
retval = DIR_ADD_TO_MASK(d, retval);
// if it is a balloon, check flyable
else if (transport.getTileType()->isBalloon() && tile.getTileType()->isFlyable())
retval = DIR_ADD_TO_MASK(d, retval);
// avatar or horseback: check walkable
else if (transport == _tileSet->getByName("avatar")->getId() || transport.getTileType()->isHorse()) {
if (tile.getTileType()->canWalkOn(d) &&
(!transport.getTileType()->isHorse() || tile.getTileType()->isCreatureWalkable()) &&
prev_tile.getTileType()->canWalkOff(d))
retval = DIR_ADD_TO_MASK(d, retval);
}
// else if (ontoCreature && to_m->canMoveOntoPlayer()) {
// retval = DIR_ADD_TO_MASK(d, retval);
// }
}
// Creature movement
else if (m) {
// Flying creatures
if (tile.getTileType()->isFlyable() && m->flies()) {
// FIXME: flying creatures behave differently on the world map?
if (isWorldMap())
retval = DIR_ADD_TO_MASK(d, retval);
else if (tile.getTileType()->isWalkable() ||
tile.getTileType()->isSwimable() ||
tile.getTileType()->isSailable())
retval = DIR_ADD_TO_MASK(d, retval);
}
// Swimming creatures and sailing creatures
else if (tile.getTileType()->isSwimable() ||
tile.getTileType()->isSailable() ||
tile.getTileType()->isShip()) {
if (m->swims() && tile.getTileType()->isSwimable())
retval = DIR_ADD_TO_MASK(d, retval);
if (m->sails() && tile.getTileType()->isSailable())
retval = DIR_ADD_TO_MASK(d, retval);
if (m->canMoveOntoPlayer() && tile.getTileType()->isShip())
retval = DIR_ADD_TO_MASK(d, retval);
}
// Ghosts and other incorporeal creatures
else if (m->isIncorporeal()) {
// can move anywhere but onto water, unless of course the creature can swim
if (!(tile.getTileType()->isSwimable() ||
tile.getTileType()->isSailable()))
retval = DIR_ADD_TO_MASK(d, retval);
}
// Walking creatures
else if (m->walks()) {
if (tile.getTileType()->canWalkOn(d) &&
prev_tile.getTileType()->canWalkOff(d) &&
tile.getTileType()->isCreatureWalkable())
retval = DIR_ADD_TO_MASK(d, retval);
}
// Creatures that can move onto player
else if (ontoAvatar && m->canMoveOntoPlayer()) {
// Tile should be transport
if (tile.getTileType()->isShip() && m->swims())
retval = DIR_ADD_TO_MASK(d, retval);
}
}
}
return retval;
}
bool Map::move(Object *obj, Direction d) {
MapCoords new_coords = obj->getCoords();
if (new_coords.move(d) != obj->getCoords()) {
obj->setCoords(new_coords);
return true;
}
return false;
}
void Map::alertGuards() {
ObjectDeque::iterator i;
const Creature *m;
// Switch all the guards to attack mode
for (i = _objects.begin(); i != _objects.end(); i++) {
m = creatureMgr->getByTile((*i)->getTile());
if (m && (m->getId() == GUARD_ID || m->getId() == LORDBRITISH_ID))
(*i)->setMovementBehavior(MOVEMENT_ATTACK_AVATAR);
}
}
MapCoords Map::getLabel(const Common::String &name) const {
Common::HashMap<Common::String, MapCoords>::const_iterator i = _labels.find(name);
if (i == _labels.end())
return MapCoords::nowhere();
return i->_value;
}
bool Map::fillMonsterTable() {
ObjectDeque::iterator current;
Object *obj;
ObjectDeque monsters;
ObjectDeque other_creatures;
ObjectDeque inanimate_objects;
Object empty;
int nCreatures = 0;
int nObjects = 0;
for (int idx = 0; idx < MONSTERTABLE_SIZE; ++idx)
_monsterTable[idx].clear();
/**
* First, categorize all the objects we have
*/
for (current = _objects.begin(); current != _objects.end(); current++) {
obj = *current;
// Moving objects first
if ((obj->getType() == Object::CREATURE) && (obj->getMovementBehavior() != MOVEMENT_FIXED)) {
Creature *c = dynamic_cast<Creature *>(obj);
assert(c);
// Whirlpools and storms are separated from other moving objects
if (c->getId() == WHIRLPOOL_ID || c->getId() == STORM_ID)
monsters.push_back(obj);
else
other_creatures.push_back(obj);
} else inanimate_objects.push_back(obj);
}
/**
* Add other monsters to our whirlpools and storms
*/
while (other_creatures.size() && nCreatures < MONSTERTABLE_CREATURES_SIZE) {
monsters.push_back(other_creatures.front());
other_creatures.pop_front();
}
/**
* Add empty objects to our list to fill things up
*/
while (monsters.size() < MONSTERTABLE_CREATURES_SIZE)
monsters.push_back(&empty);
/**
* Finally, add inanimate objects
*/
while (inanimate_objects.size() && nObjects < MONSTERTABLE_OBJECTS_SIZE) {
monsters.push_back(inanimate_objects.front());
inanimate_objects.pop_front();
}
/**
* Fill in the blanks
*/
while (monsters.size() < MONSTERTABLE_SIZE)
monsters.push_back(&empty);
/**
* Fill in our monster table
*/
int i = 0;
TileMap *base = g_tileMaps->get("base");
for (auto *monster : monsters) {
Coords c = monster->getCoords(),
prevc = monster->getPrevCoords();
_monsterTable[i]._tile = base->untranslate(monster->getTile());
_monsterTable[i]._x = c.x;
_monsterTable[i]._y = c.y;
_monsterTable[i]._prevTile = base->untranslate(monster->getPrevTile());
_monsterTable[i]._prevX = prevc.x;
_monsterTable[i]._prevY = prevc.y;
i++;
}
return true;
}
MapTile Map::translateFromRawTileIndex(int raw) const {
assertMsg(_tileMap != nullptr, "tilemap hasn't been set");
return _tileMap->translate(raw);
}
uint Map::translateToRawTileIndex(MapTile &tile) const {
return _tileMap->untranslate(tile);
}
} // End of namespace Ultima4
} // End of namespace Ultima