1173 lines
28 KiB
C++
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
|