/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "common/algorithm.h" #include "mm/mm1/data/character.h" #include "mm/shared/utils/strings.h" #include "mm/mm1/mm1.h" namespace MM { namespace MM1 { static const int CLASS_HP_PER_LEVEL[6] = { 12, 10, 10, 8, 6, 8 }; Resistances::Resistances() { for (int i = 0; i < 8; ++i) _arr[i].clear(); } void Resistances::synchronize(Common::Serializer &s) { for (int i = 0; i < 8; ++i) _arr[i].synchronize(s); } size_t Resistances::getPerformanceTotal() const { size_t total = 0; for (int i = 0; i < 8; ++i) total += _arr[i].getPerformanceTotal(); return total; } void Inventory::clear() { _items.clear(); _items.resize(INVENTORY_COUNT); } void Inventory::synchronize(Common::Serializer &s, bool ids) { for (int i = 0; i < INVENTORY_COUNT; ++i) s.syncAsByte(ids ? _items[i]._id : _items[i]._charges); } bool Inventory::empty() const { for (uint i = 0; i < INVENTORY_COUNT; ++i) { if (_items[i]) return false; } return true; } bool Inventory::full() const { for (uint i = 0; i < INVENTORY_COUNT; ++i) { if (!_items[i]) return false; } return true; } uint Inventory::size() const { for (int i = INVENTORY_COUNT - 1; i >= 0; --i) { if (_items[i]) return i + 1; } return 0; } uint Inventory::add(byte id, byte charges) { uint idx = getFreeSlot(); _items[idx]._id = id; _items[idx]._charges = charges; return idx; } int Inventory::getFreeSlot() const { for (uint i = 0; i < INVENTORY_COUNT; ++i) { if (!_items[i]) return i; } error("Inventory is full"); return -1; } void Inventory::removeAt(uint idx) { _items.remove_at(idx); _items.push_back(Entry()); } void Inventory::remove(Entry *e) { int index = indexOf(e); assert(index >= 0); removeAt(index); } int Inventory::indexOf(Entry *e) const { for (uint i = 0; i < INVENTORY_COUNT; ++i) { if (e == &_items[i]) return i; } return -1; } int Inventory::indexOf(byte itemId) const { for (uint i = 0; i < INVENTORY_COUNT; ++i) { if (_items[i]._id == itemId) return i; } return -1; } bool Inventory::hasCategory(CategoryFn fn) const { for (uint i = 0; i < INVENTORY_COUNT; ++i) { if (fn(_items[i]._id)) return true; } return false; } void Inventory::removeCharge(Entry *e) { if (e->_charges) { if (--e->_charges == 0) remove(e); } } size_t Inventory::getPerformanceTotal() const { size_t total = 0; for (uint i = 0; i < size(); ++i) total += (size_t)(*this)[i]._id + (size_t)(*this)[i]._charges; return total; } /*------------------------------------------------------------------------*/ Character::Character() : PrimaryAttributes() { Common::fill(&_flags[0], &_flags[14], 0); } void Character::synchronize(Common::Serializer &s, int portraitNum) { char name[16]; if (s.isSaving()) { // Save the name in uppercase to match original Common::strlcpy(name, uppercase(_name).c_str(), 16); s.syncBytes((byte *)name, 16); } else { s.syncBytes((byte *)name, 16); name[15] = '\0'; if (g_engine->isEnhanced()) Common::strlcpy(_name, camelCase(name).c_str(), 16); else Common::strlcpy(_name, uppercase(name).c_str(), 16); } s.syncAsByte(_sex); s.syncAsByte(_alignmentInitial); s.syncAsByte(_alignment); s.syncAsByte(_race); s.syncAsByte(_class); _intelligence.synchronize(s); _might.synchronize(s); _personality.synchronize(s); _endurance.synchronize(s); _speed.synchronize(s); _accuracy.synchronize(s); _luck.synchronize(s); _level.synchronize(s); s.syncAsByte(_age); s.syncAsByte(_ageDayCtr); s.syncAsUint32LE(_exp); s.syncAsUint16LE(_sp._current); s.syncAsUint16LE(_sp._base); _spellLevel.synchronize(s); s.syncAsUint16LE(_gems); s.syncAsUint16LE(_hpCurrent); s.syncAsUint16LE(_hp); s.syncAsUint16LE(_hpMax); // Gold field is annoying by being 3 bytes uint goldLo = _gold & 0xffff; uint goldHi = _gold >> 16; s.syncAsUint16LE(goldLo); s.syncAsByte(goldHi); if (s.isLoading()) _gold = goldLo | (goldHi << 16); _ac.synchronize(s); s.syncAsByte(_food); s.syncAsByte(_condition); _equipped.synchronize(s, true); _backpack.synchronize(s, true); _equipped.synchronize(s, false); _backpack.synchronize(s, false); _resistances.synchronize(s); _physicalAttr.synchronize(s); _missileAttr.synchronize(s); s.syncAsByte(_trapCtr); s.syncAsByte(_quest); s.syncAsByte(_worthiness); s.syncAsByte(_alignmentCtr); s.syncBytes(_flags, 14); s.syncAsByte(_portrait); if (s.isLoading()) { if (portraitNum != -1) _portrait = portraitNum; if (_portrait >= NUM_PORTRAITS) // Ensure portrait number is valid _portrait = 0; } if (s.isLoading()) loadFaceSprites(); } void Character::loadFaceSprites() { if (_portrait != 0xff && g_engine->isEnhanced()) { Common::Path cname(Common::String::format("char%02d.fac", _portrait * 2 + (_sex == MALE ? 0 : 1) + 1)); _faceSprites.load(cname); } } void Character::clear() { Common::fill(_name, _name + 16, 0); _sex = (Sex)0; _alignmentInitial = (Alignment)0; _alignment = (Alignment)0; _race = (Race)0; _class = (CharacterClass)0; _intelligence = _might = _personality = _endurance = 0; _speed = _accuracy = _luck = 0; _level = 1; _age = _ageDayCtr = 0; _exp = 0; _sp = 0; _spellLevel = 0; _gems = 0; _hpCurrent = _hp = _hpMax = 0; _gold = 0; _ac = 0; _food = 0; _condition = 0; _quest = 0; _equipped.clear(); _backpack.clear(); _alignmentInitial = GOOD; _alignment = GOOD; _resistances._s._magic.clear(); _resistances._s._fear.clear(); _resistances._s._poison.clear(); _resistances._s._psychic.clear(); _trapCtr = _alignmentCtr = 0; Common::fill(&_flags[0], &_flags[8], 0); } void Character::gatherGold() { uint total = 0; for (uint i = 0; i < g_globals->_party.size(); ++i) { total += g_globals->_party[i]._gold; g_globals->_party[i]._gold = 0; } _gold = total; } Character::TradeResult Character::trade(int whoTo, int itemIndex) { Character &dest = g_globals->_party[whoTo]; if (&dest == this) return TRADE_SUCCESS; if (dest._backpack.full()) return TRADE_FULL; if (!_backpack[itemIndex]) return TRADE_NO_ITEM; Inventory::Entry e = _backpack[itemIndex]; _backpack.removeAt(itemIndex); dest._backpack.add(e._id, e._charges); return TRADE_SUCCESS; } Character::LevelIncrease Character::increaseLevel() { ++_level; ++_age; if (_age > 220) _age = 220; _trapCtr += 2; int classNum = _class == NONE ? ROBBER : _class; int newHP = g_engine->getRandomNumber(CLASS_HP_PER_LEVEL[classNum - 1]); if (_endurance._base >= 40) newHP += 10; else if (_endurance._base >= 35) newHP += 9; else if (_endurance._base >= 30) newHP += 8; else if (_endurance._base >= 27) newHP += 7; else if (_endurance._base >= 24) newHP += 6; else if (_endurance._base >= 21) newHP += 5; else if (_endurance._base >= 19) newHP += 4; else if (_endurance._base >= 17) newHP += 3; else if (_endurance._base >= 15) newHP += 2; else if (_endurance._base >= 13) newHP += 1; else if (_endurance._base >= 9) newHP += 0; else if (_endurance._base >= 7) newHP = MAX(newHP - 1, 1); else if (_endurance._base >= 5) newHP = MAX(newHP - 2, 1); else newHP = MAX(newHP - 3, 1); _hpCurrent += newHP; _hp = _hpMax = _hpCurrent; int gainedSpells = 0; if (classNum < ARCHER) { if (_level._base < 7) gainedSpells = 0; else if (_level._base == 7) gainedSpells = 1; else if (_level._base == 9) gainedSpells = 2; else if (_level._base == 11) gainedSpells = 3; else if (_level._base == 13) gainedSpells = 4; } else if (classNum < SORCERER) { if (_level._base == 3) gainedSpells = 2; else if (_level._base == 5) gainedSpells = 3; else if (_level._base == 7) gainedSpells = 4; else if (_level._base == 9) gainedSpells = 5; else if (_level._base == 11) gainedSpells = 6; else if (_level._base == 13) gainedSpells = 7; } LevelIncrease result; result._numHP = newHP; result._numSpells = gainedSpells; return result; } Character::BuyResult Character::buyItem(byte itemId) { // Check if backpack is full if (_backpack.full()) return BUY_BACKPACK_FULL; // Check character has enough gold g_globals->_items.getItem(itemId); Item &item = g_globals->_currItem; if (_gold < item._cost) return BUY_NOT_ENOUGH_GOLD; // Add the item _gold -= item._cost; _backpack.add(itemId, item._maxCharges); return BUY_SUCCESS; } void Character::updateAttributes() { _intelligence.reset(); _might.reset(); _personality.reset(); _endurance.reset(); _speed.reset(); _personality.reset(); _endurance.reset(); _speed.reset(); _accuracy.reset(); _luck.reset(); _level.reset(); _spellLevel.reset(); } void Character::updateAC() { int ac = _ac._base; if (_speed >= 40) ac += 9; else if (_speed >= 35) ac += 8; else if (_speed >= 30) ac += 7; else if (_speed >= 25) ac += 6; else if (_speed >= 21) ac += 5; else if (_speed >= 19) ac += 4; else if (_speed >= 17) ac += 3; else if (_speed >= 15) ac += 2; else if (_speed >= 13) ac += 1; else if (_speed >= 9) ac += 0; else if (_speed >= 7) ac = MAX(ac - 1, 0); else if (_speed >= 5) ac = MAX(ac - 2, 0); else ac = MAX(ac - 3, 0); _ac._current = ac; } void Character::updateSP() { int intelligence = _intelligence._current; int personality = _personality._current; int level = _level._current; int index = 3; AttributePair newSP; // Spell points only relevant for spell casters if (_spellLevel._current) { int threshold = -1; if (_class == CLERIC) threshold = personality; else if (_class == SORCERER) threshold = intelligence; else if (level < 7) threshold = -1; else { level -= 6; threshold = (_class == PALADIN) ? personality : intelligence; } if (threshold >= 40) index += 10; else if (threshold >= 35) index += 9; else if (threshold >= 30) index += 8; else if (threshold >= 27) index += 7; else if (threshold >= 24) index += 6; else if (threshold >= 21) index += 5; else if (threshold >= 19) index += 4; else if (threshold >= 17) index += 3; else if (threshold >= 15) index += 2; else if (threshold >= 13) index += 1; else if (threshold < 5) index -= 3; else if (threshold < 7) index -= 2; else if (threshold < 9) index -= 1; // Calculate the SP newSP._base += index * level; newSP._current = newSP._base; } // Set the character's new SP _sp = newSP; } void Character::updateResistances() { for (int i = 0; i < 8; ++i) _resistances._arr[i]._current = _resistances._arr[i]._base; } Common::String Character::getConditionString() const { Common::String result; int cond = _condition; if (cond == 0) { result += STRING["stats.conditions.good"]; } else if (cond == ERADICATED) { result += STRING["stats.conditions.eradicated"]; } else { if (cond & BAD_CONDITION) { // Fatal conditions if (cond & DEAD) result += STRING["stats.conditions.dead"] + ","; if (cond & STONE) result += STRING["stats.conditions.stone"] + ","; } else { if (cond & UNCONSCIOUS) result += STRING["stats.conditions.unconscious"] + ","; if (cond & PARALYZED) result += STRING["stats.conditions.paralyzed"] + ","; if (cond & POISONED) result += STRING["stats.conditions.poisoned"] + ","; if (cond & DISEASED) result += STRING["stats.conditions.diseased"] + ","; if (cond & SILENCED) result += STRING["stats.conditions.silenced"] + ","; if (cond & BLINDED) result += STRING["stats.conditions.blinded"] + ","; if (cond & ASLEEP) result += STRING["stats.conditions.asleep"] + ","; } result.deleteLastChar(); } return result; } void Character::rest() { // Characters with a bad condition like // being stoned can't rest if (_condition & BAD_CONDITION) return; updateSP(); updateAttributes(); updateAC(); updateResistances(); _condition &= ~(ASLEEP | BLINDED | SILENCED | PARALYZED | UNCONSCIOUS); if (_hpCurrent == 0) _hpCurrent = 1; // Increment the day counter. When it overflows, // it's time to increment the character's age by a year if (_ageDayCtr++ > 255) { _ageDayCtr = 0; if (_age < 255) ++_age; } if ((g_engine->getRandomNumber(70) + 80) < _age) { // Older characters have a chance of falling unconscious _condition = UNCONSCIOUS | BAD_CONDITION; return; } // Fun fact: in the original if any of the attributes reach zero, // then it jumps to an instruction that jumps to itself, freezing the game. // For ScummVM, I just limit the minimum to 1 instead if (_age >= 60) { _might._current = MAX(_might._current - 1, 1); _endurance._current = MAX(_endurance._current - 1, 1); _speed._current = MAX(_speed._current - 1, 1); } if (_age >= 70) { _might._current = MAX(_might._current - 1, 1); _endurance._current = MAX(_endurance._current - 1, 1); _speed._current = MAX(_speed._current - 1, 1); } if (_age >= 80) { _might._current = MAX((int)_might._current - 2, 1); } if (_food > 0) { --_food; if (_condition & POISONED) { _hpMax /= 2; } else { _hpMax = _hp; } if (!(_condition & DISEASED)) { _hpCurrent = _hpMax; _sp._current = _sp._base; } } } bool Character::hasItem(byte itemId) const { return _backpack.indexOf(itemId) != -1 || _equipped.indexOf(itemId) != -1; } #define PERF16(x) ((x & 0xff) + ((x >> 8) & 0xff)) #define PERF32(x) ((x & 0xff) + ((x >> 8) & 0xff) + \ ((x >> 16) & 0xff) + ((x >> 24) & 0xff)) size_t Character::getPerformanceTotal() const { size_t totalFlags = 0; for (int i = 0; i < 14; ++i) totalFlags += _flags[i]; return (int)_sex + _alignmentInitial + _alignment + _race + _class + _intelligence.getPerformanceTotal() + _might.getPerformanceTotal() + _personality.getPerformanceTotal() + _endurance.getPerformanceTotal() + _speed.getPerformanceTotal() + _accuracy.getPerformanceTotal() + _luck.getPerformanceTotal() + _level.getPerformanceTotal() + (int)_age + (int)_ageDayCtr + PERF32(_exp) + _sp.getPerformanceTotal() + _spellLevel.getPerformanceTotal() + PERF16(_gems) + PERF16(_hpCurrent) + PERF16(_hp) + PERF16(_hpMax) + PERF32(_gold) + _ac + _food + _condition + _equipped.getPerformanceTotal() + _backpack.getPerformanceTotal() + _resistances.getPerformanceTotal() + _physicalAttr.getPerformanceTotal() + _missileAttr.getPerformanceTotal() + _trapCtr + _quest + _worthiness + _alignmentCtr + totalFlags; } byte Character::statColor(int amount, int threshold) const { if (amount < 1) return 6; else if (amount > threshold) return 2; else if (amount == threshold) return 15; else if (amount >= (threshold / 4)) return 9; else return 32; } byte Character::conditionColor() const { if (_condition == ERADICATED) return 32; else if (_condition == FINE) return 15; else if (_condition & BAD_CONDITION) return 6; else return 9; } ConditionEnum Character::worstCondition() const { if (_condition == ERADICATED) { return C_ERADICATED; } else if (_condition & BAD_CONDITION) { if (_condition & DEAD) return C_DEAD; if (_condition & STONE) return C_STONE; if (_condition & UNCONSCIOUS) return C_UNCONSCIOUS; } else { if (_condition & PARALYZED) return C_PARALYZED; if (_condition & POISONED) return C_POISONED; if (_condition & DISEASED) return C_DISEASED; if (_condition & SILENCED) return C_SILENCED; if (_condition & BLINDED) return C_BLINDED; if (_condition & ASLEEP) return C_ASLEEP; } return C_GOOD; } Common::String Character::getConditionString(ConditionEnum cond) { switch (cond) { case C_ERADICATED: return STRING["stats.conditions.eradicated"]; case C_DEAD: return STRING["stats.conditions.dead"]; case C_STONE: return STRING["stats.conditions.stone"]; case C_UNCONSCIOUS: return STRING["stats.conditions.unconscious"]; case C_PARALYZED: return STRING["stats.conditions.paralyzed"]; case C_POISONED: return STRING["stats.conditions.poisoned"]; case C_DISEASED: return STRING["stats.conditions.diseased"]; case C_SILENCED: return STRING["stats.conditions.silenced"]; case C_BLINDED: return STRING["stats.conditions.blinded"]; case C_ASLEEP: return STRING["stats.conditions.asleep"]; default: return STRING["stats.conditions.good"]; } } int Character::spellNumber() const { return g_events->isInCombat() ? _combatSpell : _nonCombatSpell; } void Character::setSpellNumber(int spellNum) { if (g_events->isInCombat()) _combatSpell = spellNum; else _nonCombatSpell = spellNum; } } // namespace MM1 } // namespace MM