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

1014 lines
27 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/game/creature.h"
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/game/game.h"
#include "ultima/ultima4/game/player.h"
#include "ultima/ultima4/controllers/combat_controller.h"
#include "ultima/ultima4/core/config.h"
#include "ultima/ultima4/core/settings.h"
#include "ultima/ultima4/core/utils.h"
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/ultima4/gfx/screen.h"
#include "ultima/ultima4/gfx/textcolor.h"
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/map/map.h"
#include "ultima/ultima4/map/tileset.h"
namespace Ultima {
namespace Ultima4 {
CreatureMgr *CreatureMgr::_instance = nullptr;
bool isCreature(Object *punknown) {
Creature *m;
if ((m = dynamic_cast<Creature *>(punknown)) != nullptr)
return true;
else
return false;
}
Creature::Creature(MapTile tile) : Object(Object::CREATURE),
_id(0), _leader(0), _baseHp(0), _hp(0), _xp(0), _ranged(0),
_leavesTile(false), _mAttr(MATTR_STEALFOOD),
_movementAttr(MATTR_STATIONARY), _slowedType(SLOWED_BY_NOTHING),
_encounterSize(0), _resists(0), _spawn(0) {
const Creature *m = creatureMgr->getByTile(tile);
if (m)
*this = *m;
}
void Creature::load(const ConfigElement &conf) {
uint idx;
static const struct {
const char *name;
uint mask;
} booleanAttributes[] = {
{ "undead", MATTR_UNDEAD },
{ "good", MATTR_GOOD },
{ "swims", MATTR_WATER },
{ "sails", MATTR_WATER },
{ "cantattack", MATTR_NONATTACKABLE },
{ "camouflage", MATTR_CAMOUFLAGE },
{ "wontattack", MATTR_NOATTACK },
{ "ambushes", MATTR_AMBUSHES },
{ "incorporeal", MATTR_INCORPOREAL },
{ "nochest", MATTR_NOCHEST },
{ "divides", MATTR_DIVIDES },
{ "forceOfNature", MATTR_FORCE_OF_NATURE }
};
/* steals="" */
static const struct {
const char *name;
uint mask;
} steals[] = {
{ "food", MATTR_STEALFOOD },
{ "gold", MATTR_STEALGOLD }
};
/* casts="" */
static const struct {
const char *name;
uint mask;
} casts[] = {
{ "sleep", MATTR_CASTS_SLEEP },
{ "negate", MATTR_NEGATE }
};
/* movement="" */
static const struct {
const char *name;
uint mask;
} movement[] = {
{ "none", MATTR_STATIONARY },
{ "wanders", MATTR_WANDERS }
};
/* boolean attributes that affect movement */
static const struct {
const char *name;
uint mask;
} movementBoolean[] = {
{ "swims", MATTR_SWIMS },
{ "sails", MATTR_SAILS },
{ "flies", MATTR_FLIES },
{ "teleports", MATTR_TELEPORT },
{ "canMoveOntoCreatures", MATTR_CANMOVECREATURES },
{ "canMoveOntoAvatar", MATTR_CANMOVEAVATAR }
};
static const struct {
const char *name;
TileEffect effect;
} effects[] = {
{ "fire", EFFECT_FIRE },
{ "poison", EFFECT_POISONFIELD },
{ "sleep", EFFECT_SLEEP }
};
_name = conf.getString("name");
_id = static_cast<unsigned short>(conf.getInt("id"));
/* Get the leader if it's been included, otherwise the leader is itself */
_leader = static_cast<byte>(conf.getInt("leader", _id));
_xp = static_cast<unsigned short>(conf.getInt("exp"));
_ranged = conf.getBool("ranged");
setTile(g_tileSets->findTileByName(conf.getString("tile")));
setHitTile("hit_flash");
setMissTile("miss_flash");
_mAttr = static_cast<CreatureAttrib>(0);
_movementAttr = static_cast<CreatureMovementAttrib>(0);
_resists = 0;
/* get the encounter size */
_encounterSize = conf.getInt("encounterSize", 0);
/* get the base hp */
_baseHp = conf.getInt("basehp", 0);
/* adjust basehp according to battle difficulty setting */
if (settings._battleDiff == "Hard")
_baseHp *= 2;
if (settings._battleDiff == "Expert")
_baseHp *= 4;
/* get the camouflaged tile */
if (conf.exists("camouflageTile"))
_camouflageTile = conf.getString("camouflageTile");
/* get the ranged tile for world map attacks */
if (conf.exists("worldrangedtile"))
_worldRangedTile = conf.getString("worldrangedtile");
/* get ranged hit tile */
if (conf.exists("rangedhittile")) {
if (conf.getString("rangedhittile") == "random")
_mAttr = static_cast<CreatureAttrib>(_mAttr | MATTR_RANDOMRANGED);
else
setHitTile(conf.getString("rangedhittile"));
}
/* get ranged miss tile */
if (conf.exists("rangedmisstile")) {
if (conf.getString("rangedmisstile") == "random")
_mAttr = static_cast<CreatureAttrib>(_mAttr | MATTR_RANDOMRANGED);
else
setMissTile(conf.getString("rangedmisstile"));
}
/* find out if the creature leaves a tile behind on ranged attacks */
_leavesTile = conf.getBool("leavestile");
/* get effects that this creature is immune to */
for (idx = 0; idx < sizeof(effects) / sizeof(effects[0]); idx++) {
if (conf.getString("resists") == effects[idx].name) {
_resists = effects[idx].effect;
}
}
/* Load creature attributes */
for (idx = 0; idx < sizeof(booleanAttributes) / sizeof(booleanAttributes[0]); idx++) {
if (conf.getBool(booleanAttributes[idx].name)) {
_mAttr = static_cast<CreatureAttrib>(_mAttr | booleanAttributes[idx].mask);
}
}
/* Load boolean attributes that affect movement */
for (idx = 0; idx < sizeof(movementBoolean) / sizeof(movementBoolean[0]); idx++) {
if (conf.getBool(movementBoolean[idx].name)) {
_movementAttr = static_cast<CreatureMovementAttrib>(_movementAttr | movementBoolean[idx].mask);
}
}
/* steals="" */
for (idx = 0; idx < sizeof(steals) / sizeof(steals[0]); idx++) {
if (conf.getString("steals") == steals[idx].name) {
_mAttr = static_cast<CreatureAttrib>(_mAttr | steals[idx].mask);
}
}
/* casts="" */
for (idx = 0; idx < sizeof(casts) / sizeof(casts[0]); idx++) {
if (conf.getString("casts") == casts[idx].name) {
_mAttr = static_cast<CreatureAttrib>(_mAttr | casts[idx].mask);
}
}
/* movement="" */
for (idx = 0; idx < sizeof(movement) / sizeof(movement[0]); idx++) {
if (conf.getString("movement") == movement[idx].name) {
_movementAttr = static_cast<CreatureMovementAttrib>(_movementAttr | movement[idx].mask);
}
}
if (conf.exists("spawnsOnDeath")) {
_mAttr = static_cast<CreatureAttrib>(_mAttr | MATTR_SPAWNSONDEATH);
_spawn = static_cast<byte>(conf.getInt("spawnsOnDeath"));
}
/* Figure out which 'slowed' function to use */
_slowedType = SLOWED_BY_TILE;
if (sails())
/* sailing creatures (pirate ships) */
_slowedType = SLOWED_BY_WIND;
else if (flies() || isIncorporeal())
/* flying creatures (dragons, bats, etc.) and incorporeal creatures (ghosts, zorns) */
_slowedType = SLOWED_BY_NOTHING;
}
bool Creature::isAttackable() const {
if (_mAttr & MATTR_NONATTACKABLE)
return false;
/* can't attack horse transport */
if (_tile.getTileType()->isHorse() && getMovementBehavior() == MOVEMENT_FIXED)
return false;
return true;
}
int Creature::getDamage() const {
int damage, val, x;
val = _baseHp;
x = xu4_random(val >> 2);
damage = (x >> 4) * 10;
damage += x % 10;
return damage;
}
int Creature::setInitialHp(int points) {
if (points < 0)
_hp = xu4_random(_baseHp) | (_baseHp / 2);
else
_hp = points;
/* make sure the creature doesn't flee initially */
if (_hp < 24) _hp = 24;
return _hp;
}
void Creature::setRandomRanged() {
switch (xu4_random(4)) {
case 0:
_rangedHitTile = _rangedMissTile = "poison_field";
break;
case 1:
_rangedHitTile = _rangedMissTile = "energy_field";
break;
case 2:
_rangedHitTile = _rangedMissTile = "fire_field";
break;
case 3:
_rangedHitTile = _rangedMissTile = "sleep_field";
break;
}
}
CreatureStatus Creature::getState() const {
int heavy_threshold, light_threshold, crit_threshold;
crit_threshold = _baseHp >> 2; /* (basehp / 4) */
heavy_threshold = _baseHp >> 1; /* (basehp / 2) */
light_threshold = crit_threshold + heavy_threshold;
if (_hp <= 0)
return MSTAT_DEAD;
else if (_hp < 24)
return MSTAT_FLEEING;
else if (_hp < crit_threshold)
return MSTAT_CRITICAL;
else if (_hp < heavy_threshold)
return MSTAT_HEAVILYWOUNDED;
else if (_hp < light_threshold)
return MSTAT_LIGHTLYWOUNDED;
else
return MSTAT_BARELYWOUNDED;
}
bool Creature::specialAction() {
bool retval = false;
int dx = abs(g_context->_location->_coords.x - _coords.x);
int dy = abs(g_context->_location->_coords.y - _coords.y);
int mapdist = g_context->_location->_coords.distance(_coords, g_context->_location->_map);
/* find out which direction the avatar is in relation to the creature */
MapCoords mapcoords(_coords);
int dir = mapcoords.getRelativeDirection(g_context->_location->_coords, g_context->_location->_map);
//Init outside of switch
int broadsidesDirs = 0;
switch (_id) {
case LAVA_LIZARD_ID:
case SEA_SERPENT_ID:
case HYDRA_ID:
case DRAGON_ID:
/* A 50/50 chance they try to range attack when you're close enough
and not in a city
Note: Monsters in settlements in U3 do fire on party
*/
if (mapdist <= 3 && xu4_random(2) == 0 && (g_context->_location->_context & CTX_CITY) == 0) {
Std::vector<Coords> path = gameGetDirectionalActionPath(dir, MASK_DIR_ALL, _coords,
1, 3, nullptr, false);
for (const auto &coords : path) {
if (creatureRangeAttack(coords, this))
break;
}
}
break;
case PIRATE_ID:
/* Fire cannon: Pirates only fire broadsides and only when they can hit you :) */
retval = true;
broadsidesDirs = dirGetBroadsidesDirs(_tile.getDirection());
if ((((dx == 0) && (dy <= 3)) || /* avatar is close enough and on the same column, OR */
((dy == 0) && (dx <= 3))) && /* avatar is close enough and on the same row, AND */
((broadsidesDirs & dir) > 0)) { /* pirate ship is firing broadsides */
// nothing (not even mountains!) can block cannonballs
Std::vector<Coords> path = gameGetDirectionalActionPath(dir, broadsidesDirs, _coords,
1, 3, nullptr, false);
for (const auto &coords : path) {
if (fireAt(coords, false))
break;
}
} else
retval = false;
break;
default:
break;
}
return retval;
}
bool Creature::specialEffect() {
Object *obj;
bool retval = false;
switch (_id) {
case STORM_ID: {
ObjectDeque::iterator i;
if (_coords == g_context->_location->_coords) {
/* damage the ship */
if (g_context->_transportContext == TRANSPORT_SHIP) {
/* FIXME: Check actual damage from u4dos */
gameDamageShip(10, 30);
}
/* anything else but balloon damages the party */
else if (g_context->_transportContext != TRANSPORT_BALLOON) {
/* FIXME: formula for twister damage is guesstimated from u4dos */
gameDamageParty(0, 75);
}
return true;
}
/* See if the storm is on top of any objects and destroy them! */
for (i = g_context->_location->_map->_objects.begin();
i != g_context->_location->_map->_objects.end();) {
obj = *i;
if (this != obj &&
obj->getCoords() == _coords) {
/* Converged with an object, destroy the object! */
i = g_context->_location->_map->removeObject(i);
retval = true;
} else i++;
}
}
break;
case WHIRLPOOL_ID: {
ObjectDeque::iterator i;
if (_coords == g_context->_location->_coords && (g_context->_transportContext == TRANSPORT_SHIP)) {
/* Deal 10 damage to the ship */
gameDamageShip(-1, 10);
/* Send the party to Locke Lake */
g_context->_location->_coords = g_context->_location->_map->getLabel("lockelake");
/* Teleport the whirlpool that sent you there far away from lockelake */
this->setCoords(Coords(0, 0, 0));
retval = true;
break;
}
/* See if the whirlpool is on top of any objects and destroy them! */
for (i = g_context->_location->_map->_objects.begin();
i != g_context->_location->_map->_objects.end();) {
obj = *i;
if (this != obj &&
obj->getCoords() == _coords) {
Creature *m = dynamic_cast<Creature *>(obj);
/* Make sure the object isn't a flying creature or object */
if (!m || (m && (m->swims() || m->sails()) && !m->flies())) {
/* Destroy the object it met with */
i = g_context->_location->_map->removeObject(i);
retval = true;
} else {
i++;
}
} else i++;
}
}
default:
break;
}
return retval;
}
void Creature::act(CombatController *controller) {
int dist;
CombatAction action;
Creature *target;
/* see if creature wakes up if it is asleep */
if ((getStatus() == STAT_SLEEPING) && (xu4_random(8) == 0))
wakeUp();
/* if the creature is still asleep, then do nothing */
if (getStatus() == STAT_SLEEPING)
return;
if (negates())
g_context->_aura->set(Aura::NEGATE, 2);
/*
* figure out what to do
*/
// creatures who teleport do so 1/8 of the time
if (teleports() && xu4_random(8) == 0)
action = CA_TELEPORT;
// creatures who ranged attack do so 1/4 of the time. Make sure
// their ranged attack is not negated!
else if (_ranged != 0 && xu4_random(4) == 0 &&
(_rangedHitTile != "magic_flash" || (*g_context->_aura != Aura::NEGATE)))
action = CA_RANGED;
// creatures who cast sleep do so 1/4 of the time they don't ranged attack
else if (castsSleep() && (*g_context->_aura != Aura::NEGATE) && (xu4_random(4) == 0))
action = CA_CAST_SLEEP;
else if (getState() == MSTAT_FLEEING)
action = CA_FLEE;
// default action: attack (or move towards) closest target
else
action = CA_ATTACK;
/*
* now find out who to do it to
*/
target = nearestOpponent(&dist, action == CA_RANGED);
if (target == nullptr)
return;
if (action == CA_ATTACK && dist > 1)
action = CA_ADVANCE;
/* let's see if the creature blends into the background, or if he appears... */
if (camouflages() && !hideOrShow())
return; /* creature is hidden -- no action! */
switch (action) {
case CA_ATTACK:
soundPlay(SOUND_NPC_ATTACK, false); // NPC_ATTACK, melee
if (controller->attackHit(this, target)) {
soundPlay(SOUND_PC_STRUCK, false); // PC_STRUCK, melee and ranged
GameController::flashTile(target->getCoords(), "hit_flash", 4);
if (!dealDamage(target, getDamage()))
target = nullptr;
if (target && isPartyMember(target)) {
/* steal gold if the creature steals gold */
if (stealsGold() && xu4_random(4) == 0) {
soundPlay(SOUND_ITEM_STOLEN, false); // ITEM_STOLEN, gold
g_context->_party->adjustGold(-(xu4_random(0x3f)));
}
/* steal food if the creature steals food */
if (stealsFood()) {
soundPlay(SOUND_ITEM_STOLEN, false); // ITEM_STOLEN, food
g_context->_party->adjustFood(-2500);
}
}
} else {
GameController::flashTile(target->getCoords(), "miss_flash", 1);
}
break;
case CA_CAST_SLEEP: {
g_screen->screenMessage("\nSleep!\n");
gameSpellEffect('s', -1, static_cast<Sound>(SOUND_MAGIC)); /* show the sleep spell effect */
/* Apply the sleep spell to party members still in combat */
if (!isPartyMember(this)) {
PartyMemberVector party = controller->getMap()->getPartyMembers();
for (auto *member : party) {
if (xu4_random(2) == 0)
member->putToSleep();
}
}
break;
}
case CA_TELEPORT: {
Coords new_c;
bool valid = false;
bool firstTry = true;
while (!valid) {
Map *map = getMap();
new_c = Coords(xu4_random(map->_width), xu4_random(map->_height), g_context->_location->_coords.z);
const Tile *tile = map->tileTypeAt(new_c, WITH_OBJECTS);
if (tile->isCreatureWalkable()) {
/* If the tile would slow me down, try again! */
if (firstTry && tile->getSpeed() != FAST)
firstTry = false;
/* OK, good enough! */
else
valid = true;
}
}
/* Teleport! */
setCoords(new_c);
break;
}
case CA_RANGED: {
// if the creature has a random tile for a ranged weapon,
// let's switch it now!
if (hasRandomRanged())
setRandomRanged();
MapCoords m_coords = getCoords(),
p_coords = target->getCoords();
// figure out which direction to fire the weapon
int dir = m_coords.getRelativeDirection(p_coords);
soundPlay(SOUND_NPC_ATTACK, false); // NPC_ATTACK, ranged
Std::vector<Coords> path = gameGetDirectionalActionPath(dir, MASK_DIR_ALL, m_coords,
1, 11, &Tile::canAttackOverTile, false);
bool hit = false;
for (const auto &coords : path) {
if (controller->rangedAttack(coords, this)) {
hit = true;
break;
}
}
if (!hit && path.size() > 0)
controller->rangedMiss(path[path.size() - 1], this);
break;
}
case CA_FLEE:
case CA_ADVANCE: {
Map *map = getMap();
if (moveCombatObject(action, map, this, target->getCoords())) {
Coords coords = getCoords();
if (MAP_IS_OOB(map, coords)) {
g_screen->screenMessage("\n%c%s Flees!%c\n", FG_YELLOW, _name.c_str(), FG_WHITE);
/* Congrats, you have a heart! */
if (isGood())
g_context->_party->adjustKarma(KA_SPARED_GOOD);
map->removeObject(this);
}
}
break;
}
}
this->animateMovement();
}
void Creature::addStatus(StatusType s) {
if (_status.size() && _status.back() > s) {
StatusType prev = _status.back();
_status.pop_back();
_status.push_back(s);
_status.push_back(prev);
} else _status.push_back(s);
}
void Creature::applyTileEffect(TileEffect effect) {
if (effect != EFFECT_NONE) {
gameUpdateScreen();
switch (effect) {
case EFFECT_SLEEP:
/* creature fell asleep! */
if ((_resists != EFFECT_SLEEP) &&
(xu4_random(0xFF) >= _hp))
putToSleep();
break;
case EFFECT_LAVA:
case EFFECT_FIRE:
/* deal 0 - 127 damage to the creature if it is not immune to fire damage */
if ((_resists != EFFECT_FIRE) && (_resists != EFFECT_LAVA))
applyDamage(xu4_random(0x7F), false);
break;
case EFFECT_POISONFIELD:
/* deal 0 - 127 damage to the creature if it is not immune to poison field damage */
if (_resists != EFFECT_POISONFIELD)
applyDamage(xu4_random(0x7F), false);
break;
case EFFECT_POISON:
default:
break;
}
}
}
int Creature::getAttackBonus() const {
return 0;
}
int Creature::getDefense() const {
return 128;
}
bool Creature::divide() {
Map *map = getMap();
int dirmask = map->getValidMoves(getCoords(), getTile());
Direction d = dirRandomDir(dirmask);
/* this is a game enhancement, make sure it's turned on! */
if (!settings._enhancements || !settings._enhancementsOptions._slimeDivides)
return false;
/* make sure there's a place to put the divided creature! */
if (d != DIR_NONE) {
MapCoords coords(getCoords());
g_screen->screenMessage("%s Divides!\n", _name.c_str());
/* find a spot to put our new creature */
coords.move(d, map);
/* create our new creature! */
Creature *addedCreature = map->addCreature(this, coords);
int dividedHp = (this->_hp + 1) / 2;
addedCreature->_hp = dividedHp;
this->_hp = dividedHp;
return true;
}
return false;
}
bool Creature::spawnOnDeath() {
Map *map = getMap();
/* this is a game enhancement, make sure it's turned on! */
if (!settings._enhancements || !settings._enhancementsOptions._gazerSpawnsInsects)
return false;
/* make sure there's a place to put the divided creature! */
MapCoords coords(getCoords());
/* create our new creature! */
map->addCreature(creatureMgr->getById(_spawn), coords);
return true;
}
StatusType Creature::getStatus() const {
return _status.back();
}
bool Creature::isAsleep() const {
for (StatusList::const_iterator itr = this->_status.begin();
itr != this->_status.end();
++itr)
if (*itr == STAT_SLEEPING)
return true;
return false;
}
bool Creature::hideOrShow() {
/* find the nearest opponent */
int dist;
/* ok, now we've got the nearest party member. Now, see if they're close enough */
if (nearestOpponent(&dist, false) != nullptr) {
if ((dist < 5) && !isVisible())
setVisible(); /* show yourself */
else if (dist >= 5)
setVisible(false); /* hide and take no action! */
}
return isVisible();
}
Creature *Creature::nearestOpponent(int *dist, bool ranged) {
Creature *opponent = nullptr;
int d, leastDist = 0xFFFF;
bool jinx = (*g_context->_aura == Aura::JINX);
Map *map = getMap();
for (auto *obj : map->_objects) {
if (!isCreature(obj))
continue;
bool amPlayer = isPartyMember(this);
bool fightingPlayer = isPartyMember(obj);
/* if a party member, find a creature. If a creature, find a party member */
/* if jinxed is false, find anything that isn't self */
if ((amPlayer != fightingPlayer) ||
(jinx && !amPlayer && obj != this)) {
MapCoords objCoords = obj->getCoords();
/* if ranged, get the distance using diagonals, otherwise get movement distance */
if (ranged)
d = objCoords.distance(getCoords());
else d = objCoords.movementDistance(getCoords());
/* skip target 50% of time if same distance */
if (d < leastDist || (d == leastDist && xu4_random(2) == 0)) {
opponent = dynamic_cast<Creature *>(obj);
leastDist = d;
}
}
}
if (opponent)
*dist = leastDist;
return opponent;
}
void Creature::putToSleep() {
if (getStatus() != STAT_DEAD) {
addStatus(STAT_SLEEPING);
setAnimated(false); /* freeze creature */
}
}
void Creature::removeStatus(StatusType s) {
StatusList::iterator i;
for (i = _status.begin(); i != _status.end();) {
if (*i == s)
i = _status.erase(i);
else
i++;
}
// Just to be sure, if a player is poisoned from a savegame, then they won't have
// a STAT_GOOD in the stack yet.
if (_status.empty())
addStatus(STAT_GOOD);
}
void Creature::setStatus(StatusType s) {
_status.clear();
this->addStatus(s);
}
void Creature::wakeUp() {
removeStatus(STAT_SLEEPING);
setAnimated(); /* reanimate creature */
}
bool Creature::applyDamage(int damage, bool byplayer) {
/* deal the damage */
if (_id != LORDBRITISH_ID)
AdjustValueMin(_hp, -damage, 0);
switch (getState()) {
case MSTAT_DEAD:
if (byplayer)
g_screen->screenMessage("%c%s Killed!%c\nExp. %d\n", FG_RED, _name.c_str(), FG_WHITE, _xp);
else
g_screen->screenMessage("%c%s Killed!%c\n", FG_RED, _name.c_str(), FG_WHITE);
/*
* the creature is dead; let it spawns something else on
* death (e.g. a gazer that spawns insects like in u5)
* then remove it
*/
if (spawnsOnDeath())
spawnOnDeath();
// Remove yourself from the map
remove();
return false;
case MSTAT_FLEEING:
g_screen->screenMessage("%c%s Fleeing!%c\n", FG_YELLOW, _name.c_str(), FG_WHITE);
break;
case MSTAT_CRITICAL:
g_screen->screenMessage("%s Critical!\n", _name.c_str());
break;
case MSTAT_HEAVILYWOUNDED:
g_screen->screenMessage("%s Heavily Wounded!\n", _name.c_str());
break;
case MSTAT_LIGHTLYWOUNDED:
g_screen->screenMessage("%s Lightly Wounded!\n", _name.c_str());
break;
case MSTAT_BARELYWOUNDED:
g_screen->screenMessage("%s Barely Wounded!\n", _name.c_str());
break;
}
/* creature is still alive and has the chance to divide - xu4 enhancement */
if (divides() && xu4_random(2) == 0)
divide();
return true;
}
bool Creature::dealDamage(Creature *m, int damage) {
return m->applyDamage(damage, isPartyMember(this));
}
/**
* CreatureMgr class implementation
*/
CreatureMgr *CreatureMgr::getInstance() {
if (_instance == nullptr) {
_instance = new CreatureMgr();
_instance->loadAll();
}
return _instance;
}
void CreatureMgr::loadAll() {
const Config *config = Config::getInstance();
Std::vector<ConfigElement> creatureConfs = config->getElement("creatures").getChildren();
for (const auto &i : creatureConfs) {
if (i.getName() != "creature")
continue;
Creature *m = new Creature(0);
m->load(i);
/* add the creature to the list */
_creatures[m->getId()] = m;
}
}
Creature *CreatureMgr::getByTile(MapTile tile) {
for (const auto &c : _creatures) {
if (c._value->getTile() == tile)
return c._value;
}
// if (tile.id)
// warning("Did not find creature for tile %d", tile.id);
return nullptr;
}
Creature *CreatureMgr::getById(CreatureId id) {
CreatureMap::const_iterator i = _creatures.find(id);
if (i != _creatures.end())
return i->_value;
else
return nullptr;
}
Creature *CreatureMgr::getByName(Common::String name) {
for (const auto &c : _creatures) {
if (scumm_stricmp(c._value->getName().c_str(), name.c_str()) == 0)
return c._value;
}
return nullptr;
}
Creature *CreatureMgr::randomForTile(const Tile *tile) {
/* FIXME: this is too dependent on the tile system, and easily
broken when tileset changes are made. Methinks the logic
behind this should be moved to monsters.xml or another conf
file */
int era;
TileId randTile;
if (tile->isSailable()) {
randTile = _creatures.find(PIRATE_ID)->_value->getTile().getId();
randTile += xu4_random(7);
return getByTile(randTile);
} else if (tile->isSwimable()) {
randTile = _creatures.find(NIXIE_ID)->_value->getTile().getId();
randTile += xu4_random(5);
return getByTile(randTile);
}
if (!tile->isCreatureWalkable())
return nullptr;
//if (c->saveGame->_moves > 100000) // FIXME: what's 100,000 moves all about (if anything)?
if (g_ultima->_saveGame->_moves > 30000)
era = 0x0f;
else if (g_ultima->_saveGame->_moves > 20000)
era = 0x07;
else
era = 0x03;
randTile = _creatures.find(ORC_ID)->_value->getTile().getId();
randTile += era & xu4_random(0x10) & xu4_random(0x10);
return getByTile(randTile);
}
Creature *CreatureMgr::randomForDungeon(int dngLevel) {
int adjustedDngLevel = dngLevel + 1;
size_t range = adjustedDngLevel < 5 ? 3 : 4;
CreatureId monster = STORM_ID + adjustedDngLevel + xu4_random(range);
if (monster >= MIMIC_ID)
++monster;
return getById(monster);
}
Creature *CreatureMgr::randomAmbushing() {
int numAmbushingCreatures = 0,
randCreature;
/* first, find out how many creatures exist that might ambush you */
for (const auto &c : _creatures) {
if (c._value->ambushes())
numAmbushingCreatures++;
}
if (numAmbushingCreatures > 0) {
/* now, randomely select one of them */
randCreature = xu4_random(numAmbushingCreatures);
numAmbushingCreatures = 0;
/* now, find the one we selected */
for (const auto &c : _creatures) {
if (c._value->ambushes()) {
/* found the creature - return it! */
if (numAmbushingCreatures == randCreature)
return c._value;
/* move on to the next creature */
else
numAmbushingCreatures++;
}
}
}
error("failed to find an ambushing creature");
return nullptr;
}
} // End of namespace Ultima4
} // End of namespace Ultima