Files
2026-02-02 04:50:13 +01:00

1173 lines
28 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/player.h"
#include "ultima/ultima4/game/armor.h"
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/game/game.h"
#include "ultima/ultima4/game/names.h"
#include "ultima/ultima4/game/weapon.h"
#include "ultima/ultima4/controllers/combat_controller.h"
#include "ultima/ultima4/core/debugger.h"
#include "ultima/ultima4/core/types.h"
#include "ultima/ultima4/core/utils.h"
#include "ultima/ultima4/map/annotation.h"
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/map/mapmgr.h"
#include "ultima/ultima4/map/tilemap.h"
#include "ultima/ultima4/map/tileset.h"
namespace Ultima {
namespace Ultima4 {
bool isPartyMember(Object *punknown) {
PartyMember *pm;
if ((pm = dynamic_cast<PartyMember *>(punknown)) != nullptr)
return true;
else
return false;
}
PartyMember::PartyMember(Party *p, SaveGamePlayerRecord *pr) :
Creature(tileForClass(pr->_class)),
_player(pr),
_party(p) {
/* FIXME: we need to rename movement behaviors */
setMovementBehavior(MOVEMENT_ATTACK_AVATAR);
this->_ranged = g_weapons->get(pr->_weapon)->getRange() ? 1 : 0;
setStatus(pr->_status);
}
PartyMember::~PartyMember() {
}
void PartyMember::notifyOfChange() {
if (_party) {
_party->notifyOfChange(this);
}
}
Common::String PartyMember::translate(Std::vector<Common::String> &parts) {
if (parts.size() == 0)
return "";
else if (parts.size() == 1) {
if (parts[0] == "hp")
return xu4_to_string(getHp());
else if (parts[0] == "max_hp")
return xu4_to_string(getMaxHp());
else if (parts[0] == "mp")
return xu4_to_string(getMp());
else if (parts[0] == "max_mp")
return xu4_to_string(getMaxMp());
else if (parts[0] == "str")
return xu4_to_string(getStr());
else if (parts[0] == "dex")
return xu4_to_string(getDex());
else if (parts[0] == "int")
return xu4_to_string(getInt());
else if (parts[0] == "exp")
return xu4_to_string(getExp());
else if (parts[0] == "name")
return getName();
else if (parts[0] == "weapon")
return getWeapon()->getName();
else if (parts[0] == "armor")
return getArmor()->getName();
else if (parts[0] == "sex") {
Common::String var((char)getSex());
return var;
} else if (parts[0] == "class")
return getClassName(getClass());
else if (parts[0] == "level")
return xu4_to_string(getRealLevel());
} else if (parts.size() == 2) {
if (parts[0] == "needs") {
if (parts[1] == "cure") {
if (getStatus() == STAT_POISONED)
return "true";
else
return "false";
} else if (parts[1] == "heal" || parts[1] == "fullheal") {
if (getHp() < getMaxHp())
return "true";
else
return "false";
} else if (parts[1] == "resurrect") {
if (getStatus() == STAT_DEAD)
return "true";
else
return "false";
}
}
}
return "";
}
int PartyMember::getHp() const {
return _player->_hp;
}
int PartyMember::getMaxMp() const {
int max_mp = -1;
switch (_player->_class) {
case CLASS_MAGE: /* mage: 200% of int */
max_mp = _player->_intel * 2;
break;
case CLASS_DRUID: /* druid: 150% of int */
max_mp = _player->_intel * 3 / 2;
break;
case CLASS_BARD: /* bard, paladin, ranger: 100% of int */
case CLASS_PALADIN:
case CLASS_RANGER:
max_mp = _player->_intel;
break;
case CLASS_TINKER: /* tinker: 50% of int */
max_mp = _player->_intel / 2;
break;
case CLASS_FIGHTER: /* fighter, shepherd: no mp at all */
case CLASS_SHEPHERD:
max_mp = 0;
break;
default:
error("invalid player class: %d", _player->_class);
}
/* mp always maxes out at 99 */
if (max_mp > 99)
max_mp = 99;
return max_mp;
}
const Weapon *PartyMember::getWeapon() const {
return g_weapons->get(_player->_weapon);
}
const Armor *PartyMember::getArmor() const {
return g_armors->get(_player->_armor);
}
Common::String PartyMember::getName() const {
return _player->_name;
}
SexType PartyMember::getSex() const {
return _player->_sex;
}
ClassType PartyMember::getClass() const {
return _player->_class;
}
CreatureStatus PartyMember::getState() const {
if (getHp() <= 0)
return MSTAT_DEAD;
else if (getHp() < 24)
return MSTAT_FLEEING;
else
return MSTAT_BARELYWOUNDED;
}
int PartyMember::getRealLevel() const {
return _player->_hpMax / 100;
}
int PartyMember::getMaxLevel() const {
int level = 1;
int next = 100;
while (_player->_xp >= next && level < 8) {
level++;
next <<= 1;
}
return level;
}
void PartyMember::addStatus(StatusType s) {
Creature::addStatus(s);
_player->_status = _status.back();
notifyOfChange();
}
void PartyMember::adjustMp(int pts) {
AdjustValueMax(_player->_mp, pts, getMaxMp());
notifyOfChange();
}
void PartyMember::advanceLevel() {
if (getRealLevel() == getMaxLevel())
return;
setStatus(STAT_GOOD);
_player->_hpMax = getMaxLevel() * 100;
_player->_hp = _player->_hpMax;
/* improve stats by 1-8 each */
_player->_str += xu4_random(8) + 1;
_player->_dex += xu4_random(8) + 1;
_player->_intel += xu4_random(8) + 1;
if (_player->_str > 50) _player->_str = 50;
if (_player->_dex > 50) _player->_dex = 50;
if (_player->_intel > 50) _player->_intel = 50;
if (_party) {
_party->setChanged();
PartyEvent event(PartyEvent::ADVANCED_LEVEL, this);
event._player = this;
_party->notifyObservers(event);
}
}
void PartyMember::applyEffect(TileEffect effect) {
if (getStatus() == STAT_DEAD)
return;
switch (effect) {
case EFFECT_NONE:
break;
case EFFECT_LAVA:
case EFFECT_FIRE:
applyDamage(16 + (xu4_random(32)));
/*else if (player == ALL_PLAYERS && xu4_random(2) == 0)
playerApplyDamage(&(c->saveGame->_players[i]), 10 + (xu4_random(25)));*/
break;
case EFFECT_SLEEP:
putToSleep();
break;
case EFFECT_POISONFIELD:
case EFFECT_POISON:
if (getStatus() != STAT_POISONED) {
soundPlay(SOUND_POISON_EFFECT, false);
addStatus(STAT_POISONED);
}
break;
case EFFECT_ELECTRICITY:
break;
default:
error("invalid effect: %d", effect);
}
if (effect != EFFECT_NONE)
notifyOfChange();
}
void PartyMember::awardXp(int xp) {
AdjustValueMax(_player->_xp, xp, 9999);
notifyOfChange();
}
bool PartyMember::heal(HealType type) {
switch (type) {
case HT_NONE:
return true;
case HT_CURE:
if (getStatus() != STAT_POISONED)
return false;
removeStatus(STAT_POISONED);
break;
case HT_FULLHEAL:
if (getStatus() == STAT_DEAD ||
_player->_hp == _player->_hpMax)
return false;
_player->_hp = _player->_hpMax;
break;
case HT_RESURRECT:
if (getStatus() != STAT_DEAD)
return false;
setStatus(STAT_GOOD);
break;
case HT_HEAL:
if (getStatus() == STAT_DEAD ||
_player->_hp == _player->_hpMax)
return false;
_player->_hp += 75 + (xu4_random(0x100) % 0x19);
break;
case HT_CAMPHEAL:
if (getStatus() == STAT_DEAD ||
_player->_hp == _player->_hpMax)
return false;
_player->_hp += 99 + (xu4_random(0x100) & 0x77);
break;
case HT_INNHEAL:
if (getStatus() == STAT_DEAD ||
_player->_hp == _player->_hpMax)
return false;
_player->_hp += 100 + (xu4_random(50) * 2);
break;
default:
return false;
}
if (_player->_hp > _player->_hpMax)
_player->_hp = _player->_hpMax;
notifyOfChange();
return true;
}
void PartyMember::removeStatus(StatusType s) {
Creature::removeStatus(s);
_player->_status = _status.back();
notifyOfChange();
}
void PartyMember::setHp(int hp) {
_player->_hp = hp;
notifyOfChange();
}
void PartyMember::setMp(int mp) {
_player->_mp = mp;
notifyOfChange();
}
EquipError PartyMember::setArmor(const Armor *a) {
ArmorType type = a->getType();
if (type != ARMR_NONE && _party->_saveGame->_armor[type] < 1)
return EQUIP_NONE_LEFT;
if (!a->canWear(getClass()))
return EQUIP_CLASS_RESTRICTED;
ArmorType oldArmorType = getArmor()->getType();
if (oldArmorType != ARMR_NONE)
_party->_saveGame->_armor[oldArmorType]++;
if (type != ARMR_NONE)
_party->_saveGame->_armor[type]--;
_player->_armor = type;
notifyOfChange();
return EQUIP_SUCCEEDED;
}
EquipError PartyMember::setWeapon(const Weapon *w) {
WeaponType type = w->getType();
if (type != WEAP_HANDS && _party->_saveGame->_weapons[type] < 1)
return EQUIP_NONE_LEFT;
if (!w->canReady(getClass()))
return EQUIP_CLASS_RESTRICTED;
WeaponType old = getWeapon()->getType();
if (old != WEAP_HANDS)
_party->_saveGame->_weapons[old]++;
if (type != WEAP_HANDS)
_party->_saveGame->_weapons[type]--;
_player->_weapon = type;
notifyOfChange();
return EQUIP_SUCCEEDED;
}
bool PartyMember::applyDamage(int damage, bool) {
int newHp = _player->_hp;
if (getStatus() == STAT_DEAD)
return false;
newHp -= damage;
if (newHp < 0) {
setStatus(STAT_DEAD);
newHp = 0;
}
_player->_hp = newHp;
notifyOfChange();
if (isCombatMap(g_context->_location->_map) && getStatus() == STAT_DEAD) {
Coords p = getCoords();
Map *map = getMap();
assert(_party);
map->_annotations->add(p, g_tileSets->findTileByName("corpse")->getId())->setTTL(_party->size() * 2);
{
_party->setChanged();
PartyEvent event(PartyEvent::PLAYER_KILLED, this);
event._player = this;
_party->notifyObservers(event);
}
/* remove yourself from the map */
remove();
return false;
}
return true;
}
int PartyMember::getAttackBonus() const {
if (g_weapons->get(_player->_weapon)->alwaysHits() || _player->_dex >= 40)
return 255;
return _player->_dex;
}
int PartyMember::getDefense() const {
return g_armors->get(_player->_armor)->getDefense();
}
bool PartyMember::dealDamage(Creature *m, int damage) {
/* we have to record these now, because if we
kill the target, it gets destroyed */
int m_xp = m->getXp();
if (!Creature::dealDamage(m, damage)) {
/* half the time you kill an evil creature you get a karma boost */
awardXp(m_xp);
return false;
}
return true;
}
int PartyMember::getDamage() {
int maxDamage;
maxDamage = g_weapons->get(_player->_weapon)->getDamage();
maxDamage += _player->_str;
if (maxDamage > 255)
maxDamage = 255;
return xu4_random(maxDamage);
}
const Common::String &PartyMember::getHitTile() const {
return getWeapon()->getHitTile();
}
const Common::String &PartyMember::getMissTile() const {
return getWeapon()->getMissTile();
}
bool PartyMember::isDead() {
return getStatus() == STAT_DEAD;
}
bool PartyMember::isDisabled() {
return (getStatus() == STAT_GOOD ||
getStatus() == STAT_POISONED) ? false : true;
}
int PartyMember::loseWeapon() {
int weapon = _player->_weapon;
notifyOfChange();
if (_party->_saveGame->_weapons[weapon] > 0)
return (--_party->_saveGame->_weapons[weapon]) + 1;
else {
_player->_weapon = WEAP_HANDS;
return 0;
}
}
void PartyMember::putToSleep() {
if (getStatus() != STAT_DEAD) {
soundPlay(SOUND_SLEEP, false);
addStatus(STAT_SLEEPING);
setTile(g_tileSets->findTileByName("corpse")->getId());
}
}
void PartyMember::wakeUp() {
removeStatus(STAT_SLEEPING);
setTile(tileForClass(getClass()));
}
MapTile PartyMember::tileForClass(int klass) {
const char *name = nullptr;
switch (klass) {
case CLASS_MAGE:
name = "mage";
break;
case CLASS_BARD:
name = "bard";
break;
case CLASS_FIGHTER:
name = "fighter";
break;
case CLASS_DRUID:
name = "druid";
break;
case CLASS_TINKER:
name = "tinker";
break;
case CLASS_PALADIN:
name = "paladin";
break;
case CLASS_RANGER:
name = "ranger";
break;
case CLASS_SHEPHERD:
name = "shepherd";
break;
default:
error("invalid class %d in tileForClass", klass);
}
const Tile *tile = g_tileSets->get("base")->getByName(name);
assertMsg(tile, "no tile found for class %d", klass);
return tile->getId();
}
/*-------------------------------------------------------------------*/
Party::Party(SaveGame *s) : _saveGame(s), _transport(0), _torchDuration(0), _activePlayer(-1) {
MapId map = _saveGame->_positions.back()._map;
if (map >= MAP_DECEIT && map <= MAP_ABYSS)
_torchDuration = _saveGame->_torchDuration;
for (int i = 0; i < _saveGame->_members; i++) {
// add the members to the party
_members.push_back(new PartyMember(this, &_saveGame->_players[i]));
}
// set the party's transport (transport value stored in savegame
// hardcoded to index into base tilemap)
setTransport(g_tileMaps->get("base")->translate(_saveGame->_transport));
}
Party::~Party() {
for (uint idx = 0; idx < _members.size(); ++idx)
delete _members[idx];
}
void Party::notifyOfChange(PartyMember *pm, PartyEvent::Type eventType) {
setChanged();
PartyEvent event(eventType, pm);
notifyObservers(event);
}
Common::String Party::translate(Std::vector<Common::String> &parts) {
if (parts.size() == 0)
return "";
else if (parts.size() == 1) {
// Translate some different items for the script
if (parts[0] == "transport") {
if (g_context->_transportContext & TRANSPORT_FOOT)
return "foot";
if (g_context->_transportContext & TRANSPORT_HORSE)
return "horse";
if (g_context->_transportContext & TRANSPORT_SHIP)
return "ship";
if (g_context->_transportContext & TRANSPORT_BALLOON)
return "balloon";
} else if (parts[0] == "gold")
return xu4_to_string(_saveGame->_gold);
else if (parts[0] == "food")
return xu4_to_string(_saveGame->_food);
else if (parts[0] == "members")
return xu4_to_string(size());
else if (parts[0] == "keys")
return xu4_to_string(_saveGame->_keys);
else if (parts[0] == "torches")
return xu4_to_string(_saveGame->_torches);
else if (parts[0] == "gems")
return xu4_to_string(_saveGame->_gems);
else if (parts[0] == "sextants")
return xu4_to_string(_saveGame->_sextants);
else if (parts[0] == "food")
return xu4_to_string((_saveGame->_food / 100));
else if (parts[0] == "gold")
return xu4_to_string(_saveGame->_gold);
else if (parts[0] == "party_members")
return xu4_to_string(_saveGame->_members);
else if (parts[0] == "moves")
return xu4_to_string(_saveGame->_moves);
} else if (parts.size() >= 2) {
if (parts[0].findFirstOf("member") == 0) {
// Make a new parts list, but remove the first item
Std::vector<Common::String> new_parts = parts;
new_parts.erase(new_parts.begin());
// Find the member we'll be working with
Common::String str = parts[0];
size_t pos = str.findFirstOf("1234567890");
if (pos != Common::String::npos) {
str = str.substr(pos);
int p_member = (int)strtol(str.c_str(), nullptr, 10);
// Make the party member translate its own stuff
if (p_member > 0)
return member(p_member - 1)->translate(new_parts);
}
}
else if (parts.size() == 2) {
if (parts[0] == "weapon") {
const Weapon *w = g_weapons->get(parts[1]);
if (w)
return xu4_to_string(_saveGame->_weapons[w->getType()]);
} else if (parts[0] == "armor") {
const Armor *a = g_armors->get(parts[1]);
if (a)
return xu4_to_string(_saveGame->_armor[a->getType()]);
}
}
}
return "";
}
void Party::adjustFood(int food) {
// Check for cheat that disables party hunger
if (food < 0 && g_debugger->_disableHunger)
return;
int oldFood = _saveGame->_food;
AdjustValue(_saveGame->_food, food, 999900, 0);
if ((_saveGame->_food / 100) != (oldFood / 100)) {
notifyOfChange();
}
}
void Party::adjustGold(int gold) {
AdjustValue(_saveGame->_gold, gold, 9999, 0);
notifyOfChange();
}
void Party::adjustKarma(KarmaAction action) {
bool timeLimited = false;
int v, newKarma[VIRT_MAX], maxVal[VIRT_MAX];
/*
* make a local copy of all virtues, and adjust it according to
* the game rules
*/
for (v = 0; v < VIRT_MAX; v++) {
newKarma[v] = _saveGame->_karma[v] == 0 ? 100 : _saveGame->_karma[v];
maxVal[v] = _saveGame->_karma[v] == 0 ? 100 : 99;
}
switch (action) {
case KA_FOUND_ITEM:
AdjustValueMax(newKarma[VIRT_HONOR], 5, maxVal[VIRT_HONOR]);
break;
case KA_STOLE_CHEST:
AdjustValueMin(newKarma[VIRT_HONESTY], -1, 1);
AdjustValueMin(newKarma[VIRT_JUSTICE], -1, 1);
AdjustValueMin(newKarma[VIRT_HONOR], -1, 1);
break;
case KA_GAVE_ALL_TO_BEGGAR:
// When donating all, you get +3 HONOR in Apple 2, but not in in U4DOS.
// TODO: Make this a configuration option.
// AdjustValueMax(newKarma[VIRT_HONOR], 3, maxVal[VIRT_HONOR]);
case KA_GAVE_TO_BEGGAR:
// In U4DOS, we only get +2 COMPASSION, no HONOR or SACRIFICE even if
// donating all.
timeLimited = true;
AdjustValueMax(newKarma[VIRT_COMPASSION], 2, maxVal[VIRT_COMPASSION]);
break;
case KA_BRAGGED:
AdjustValueMin(newKarma[VIRT_HUMILITY], -5, 1);
break;
case KA_HUMBLE:
timeLimited = true;
AdjustValueMax(newKarma[VIRT_HUMILITY], 10, maxVal[VIRT_HUMILITY]);
break;
case KA_HAWKWIND:
case KA_MEDITATION:
timeLimited = true;
AdjustValueMax(newKarma[VIRT_SPIRITUALITY], 3, maxVal[VIRT_SPIRITUALITY]);
break;
case KA_BAD_MANTRA:
AdjustValueMin(newKarma[VIRT_SPIRITUALITY], -3, 1);
break;
case KA_ATTACKED_GOOD:
AdjustValueMin(newKarma[VIRT_COMPASSION], -5, 1);
AdjustValueMin(newKarma[VIRT_JUSTICE], -5, 1);
AdjustValueMin(newKarma[VIRT_HONOR], -5, 1);
break;
case KA_FLED_EVIL:
AdjustValueMin(newKarma[VIRT_VALOR], -2, 1);
break;
case KA_HEALTHY_FLED_EVIL:
AdjustValueMin(newKarma[VIRT_VALOR], -2, 1);
AdjustValueMin(newKarma[VIRT_SACRIFICE], -2, 1);
break;
case KA_KILLED_EVIL:
AdjustValueMax(newKarma[VIRT_VALOR], xu4_random(2), maxVal[VIRT_VALOR]); /* gain one valor half the time, zero the rest */
break;
case KA_FLED_GOOD:
AdjustValueMax(newKarma[VIRT_COMPASSION], 2, maxVal[VIRT_COMPASSION]);
AdjustValueMax(newKarma[VIRT_JUSTICE], 2, maxVal[VIRT_JUSTICE]);
break;
case KA_SPARED_GOOD:
AdjustValueMax(newKarma[VIRT_COMPASSION], 1, maxVal[VIRT_COMPASSION]);
AdjustValueMax(newKarma[VIRT_JUSTICE], 1, maxVal[VIRT_JUSTICE]);
break;
case KA_DONATED_BLOOD:
AdjustValueMax(newKarma[VIRT_SACRIFICE], 5, maxVal[VIRT_SACRIFICE]);
break;
case KA_DIDNT_DONATE_BLOOD:
AdjustValueMin(newKarma[VIRT_SACRIFICE], -5, 1);
break;
case KA_CHEAT_REAGENTS:
AdjustValueMin(newKarma[VIRT_HONESTY], -10, 1);
AdjustValueMin(newKarma[VIRT_JUSTICE], -10, 1);
AdjustValueMin(newKarma[VIRT_HONOR], -10, 1);
break;
case KA_DIDNT_CHEAT_REAGENTS:
timeLimited = true;
AdjustValueMax(newKarma[VIRT_HONESTY], 2, maxVal[VIRT_HONESTY]);
AdjustValueMax(newKarma[VIRT_JUSTICE], 2, maxVal[VIRT_JUSTICE]);
AdjustValueMax(newKarma[VIRT_HONOR], 2, maxVal[VIRT_HONOR]);
break;
case KA_USED_SKULL:
/* using the skull is very, very bad... */
for (v = 0; v < VIRT_MAX; v++)
AdjustValueMin(newKarma[v], -5, 1);
break;
case KA_DESTROYED_SKULL:
/* ...but destroying it is very, very good */
for (v = 0; v < VIRT_MAX; v++)
AdjustValueMax(newKarma[v], 10, maxVal[v]);
break;
}
/*
* check if enough time has passed since last virtue award if
* action is time limited -- if not, throw away new values
*/
if (timeLimited) {
if (((_saveGame->_moves / 16) >= 0x10000) || (((_saveGame->_moves / 16) & 0xFFFF) != _saveGame->_lastVirtue))
_saveGame->_lastVirtue = (_saveGame->_moves / 16) & 0xFFFF;
else
return;
}
/* something changed */
notifyOfChange();
/*
* return to u4dos compatibility and handle losing of eighths
*/
for (v = 0; v < VIRT_MAX; v++) {
if (maxVal[v] == 100) { /* already an avatar */
if (newKarma[v] < 100) { /* but lost it */
_saveGame->_karma[v] = newKarma[v];
setChanged();
PartyEvent event(PartyEvent::LOST_EIGHTH, 0);
notifyObservers(event);
} else _saveGame->_karma[v] = 0; /* return to u4dos compatibility */
} else _saveGame->_karma[v] = newKarma[v];
}
}
void Party::applyEffect(TileEffect effect) {
int i;
for (i = 0; i < size(); i++) {
switch (effect) {
case EFFECT_NONE:
case EFFECT_ELECTRICITY:
_members[i]->applyEffect(effect);
break;
case EFFECT_LAVA:
case EFFECT_FIRE:
case EFFECT_SLEEP:
if (xu4_random(2) == 0)
_members[i]->applyEffect(effect);
break;
case EFFECT_POISONFIELD:
case EFFECT_POISON:
if (xu4_random(5) == 0)
_members[i]->applyEffect(effect);
break;
default:
break;
}
}
}
bool Party::attemptElevation(Virtue virtue) {
if (_saveGame->_karma[virtue] == 99) {
_saveGame->_karma[virtue] = 0;
notifyOfChange();
return true;
} else
return false;
}
void Party::burnTorch(int turns) {
_torchDuration -= turns;
if (_torchDuration <= 0)
_torchDuration = 0;
_saveGame->_torchDuration = _torchDuration;
notifyOfChange();
}
bool Party::canEnterShrine(Virtue virtue) {
if (_saveGame->_runes & (1 << (int) virtue))
return true;
else
return false;
}
bool Party::canPersonJoin(Common::String name, Virtue *v) {
int i;
if (name.empty())
return 0;
for (i = 1; i < 8; i++) {
if (name == _saveGame->_players[i]._name) {
if (v)
*v = (Virtue) _saveGame->_players[i]._class;
return true;
}
}
return false;
}
void Party::damageShip(uint pts) {
_saveGame->_shipHull -= pts;
if ((short)_saveGame->_shipHull < 0)
_saveGame->_shipHull = 0;
notifyOfChange();
}
bool Party::donate(int quantity) {
if (quantity > _saveGame->_gold)
return false;
adjustGold(-quantity);
if (_saveGame->_gold > 0)
adjustKarma(KA_GAVE_TO_BEGGAR);
else adjustKarma(KA_GAVE_ALL_TO_BEGGAR);
return true;
}
void Party::endTurn() {
int i;
_saveGame->_moves++;
for (i = 0; i < size(); i++) {
/* Handle player status (only for non-combat turns) */
if ((g_context->_location->_context & CTX_NON_COMBAT) == g_context->_location->_context) {
/* party members eat food (also non-combat) */
if (!_members[i]->isDead())
adjustFood(-1);
switch (_members[i]->getStatus()) {
case STAT_SLEEPING:
if (xu4_random(5) == 0)
_members[i]->wakeUp();
break;
case STAT_POISONED:
/* SOLUS
* shouldn't play poison damage sound in combat,
* yet if the PC takes damage just befor combat
* begins, the sound is played after the combat
* screen appears
*/
soundPlay(SOUND_POISON_DAMAGE, false);
_members[i]->applyDamage(2);
break;
default:
break;
}
}
/* regenerate magic points */
if (!_members[i]->isDisabled() && _members[i]->getMp() < _members[i]->getMaxMp())
_saveGame->_players[i]._mp++;
}
/* The party is starving! */
if ((_saveGame->_food == 0) && ((g_context->_location->_context & CTX_NON_COMBAT) == g_context->_location->_context)) {
setChanged();
PartyEvent event(PartyEvent::STARVING, 0);
notifyObservers(event);
}
/* heal ship (25% chance it is healed each turn) */
if ((g_context->_location->_context == CTX_WORLDMAP) && (_saveGame->_shipHull < 50) && xu4_random(4) == 0)
healShip(1);
}
int Party::getChest() {
int gold = xu4_random(50) + xu4_random(8) + 10;
adjustGold(gold);
return gold;
}
int Party::getTorchDuration() const {
return _torchDuration;
}
void Party::healShip(uint pts) {
_saveGame->_shipHull += pts;
if (_saveGame->_shipHull > 50)
_saveGame->_shipHull = 50;
notifyOfChange();
}
bool Party::isFlying() const {
return (_saveGame->_balloonState && _torchDuration <= 0);
}
bool Party::isImmobilized() {
int i;
bool immobile = true;
for (i = 0; i < _saveGame->_members; i++) {
if (!_members[i]->isDisabled())
immobile = false;
}
return immobile;
}
bool Party::isDead() {
int i;
bool dead = true;
for (i = 0; i < _saveGame->_members; i++) {
if (!_members[i]->isDead()) {
dead = false;
}
}
return dead;
}
bool Party::isPersonJoined(Common::String name) {
int i;
if (name.empty())
return false;
for (i = 1; i < _saveGame->_members; i++) {
if (name == _saveGame->_players[i]._name)
return true;
}
return false;
}
CannotJoinError Party::join(Common::String name) {
int i;
SaveGamePlayerRecord tmp;
for (i = _saveGame->_members; i < 8; i++) {
if (name == _saveGame->_players[i]._name) {
/* ensure avatar is experienced enough */
if (_saveGame->_members + 1 > (_saveGame->_players[0]._hpMax / 100))
return JOIN_NOT_EXPERIENCED;
/* ensure character has enough karma */
if ((_saveGame->_karma[_saveGame->_players[i]._class] > 0) &&
(_saveGame->_karma[_saveGame->_players[i]._class] < 40))
return JOIN_NOT_VIRTUOUS;
tmp = _saveGame->_players[_saveGame->_members];
_saveGame->_players[_saveGame->_members] = _saveGame->_players[i];
_saveGame->_players[i] = tmp;
_members.push_back(new PartyMember(this, &_saveGame->_players[_saveGame->_members++]));
setChanged();
PartyEvent event(PartyEvent::MEMBER_JOINED, _members.back());
notifyObservers(event);
return JOIN_SUCCEEDED;
}
}
return JOIN_NOT_EXPERIENCED;
}
bool Party::lightTorch(int duration, bool loseTorch) {
if (loseTorch) {
if (g_ultima->_saveGame->_torches <= 0)
return false;
g_ultima->_saveGame->_torches--;
}
_torchDuration += duration;
_saveGame->_torchDuration = _torchDuration;
notifyOfChange();
return true;
}
void Party::quenchTorch() {
_torchDuration = _saveGame->_torchDuration = 0;
notifyOfChange();
}
void Party::reviveParty() {
int i;
for (i = 0; i < size(); i++) {
_members[i]->wakeUp();
_members[i]->setStatus(STAT_GOOD);
_saveGame->_players[i]._hp = _saveGame->_players[i]._hpMax;
}
for (i = ARMR_NONE + 1; i < ARMR_MAX; i++)
_saveGame->_armor[i] = 0;
for (i = WEAP_HANDS + 1; i < WEAP_MAX; i++)
_saveGame->_weapons[i] = 0;
_saveGame->_food = 20099;
_saveGame->_gold = 200;
setTransport(g_tileSets->findTileByName("avatar")->getId());
setChanged();
PartyEvent event(PartyEvent::PARTY_REVIVED, 0);
notifyObservers(event);
}
MapTile Party::getTransport() const {
return _transport;
}
void Party::setTransport(MapTile tile) {
// transport value stored in savegame hardcoded to index into base tilemap
_saveGame->_transport = g_tileMaps->get("base")->untranslate(tile);
assertMsg(_saveGame->_transport != 0, "could not generate valid savegame transport for tile with id %d\n", tile._id);
_transport = tile;
if (tile.getTileType()->isHorse())
g_context->_transportContext = TRANSPORT_HORSE;
else if (tile.getTileType()->isShip())
g_context->_transportContext = TRANSPORT_SHIP;
else if (tile.getTileType()->isBalloon())
g_context->_transportContext = TRANSPORT_BALLOON;
else g_context->_transportContext = TRANSPORT_FOOT;
notifyOfChange();
}
void Party::setShipHull(int str) {
int newStr = str;
AdjustValue(newStr, 0, 99, 0);
if (_saveGame->_shipHull != newStr) {
_saveGame->_shipHull = newStr;
notifyOfChange();
}
}
Direction Party::getDirection() const {
return _transport.getDirection();
}
void Party::setDirection(Direction dir) {
_transport.setDirection(dir);
}
void Party::adjustReagent(int reagent, int amt) {
int oldVal = g_ultima->_saveGame->_reagents[reagent];
AdjustValue(g_ultima->_saveGame->_reagents[reagent], amt, 99, 0);
if (oldVal != g_ultima->_saveGame->_reagents[reagent]) {
notifyOfChange();
}
}
int Party::getReagent(int reagent) const {
return g_ultima->_saveGame->_reagents[reagent];
}
short *Party::getReagentPtr(int reagent) const {
return &g_ultima->_saveGame->_reagents[reagent];
}
void Party::setActivePlayer(int p) {
_activePlayer = p;
setChanged();
PartyEvent event(PartyEvent::ACTIVE_PLAYER_CHANGED, _activePlayer < 0 ? 0 : _members[_activePlayer]);
notifyObservers(event);
}
int Party::getActivePlayer() const {
return _activePlayer;
}
void Party::swapPlayers(int p1, int p2) {
assertMsg(p1 < _saveGame->_members, "p1 out of range: %d", p1);
assertMsg(p2 < _saveGame->_members, "p2 out of range: %d", p2);
SaveGamePlayerRecord tmp = _saveGame->_players[p1];
_saveGame->_players[p1] = g_ultima->_saveGame->_players[p2];
g_ultima->_saveGame->_players[p2] = tmp;
syncMembers();
if (p1 == _activePlayer)
_activePlayer = p2;
else if (p2 == _activePlayer)
_activePlayer = p1;
notifyOfChange(0);
}
void Party::syncMembers() {
_members.clear();
for (int i = 0; i < _saveGame->_members; i++) {
// add the members to the party
_members.push_back(new PartyMember(this, &_saveGame->_players[i]));
}
}
int Party::size() const {
return _members.size();
}
PartyMember *Party::member(int index) const {
return _members[index];
}
} // End of namespace Ultima4
} // End of namespace Ultima