/* 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 2 * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "xeen/character.h" #include "xeen/dialogs_query.h" #include "xeen/dialogs_message.h" #include "xeen/resources.h" #include "xeen/xeen.h" namespace Xeen { XeenItem::XeenItem() { clear(); } void XeenItem::clear() { _material = _id = _bonusFlags = 0; _frame = 0; } void XeenItem::synchronize(Common::Serializer &s) { s.syncAsByte(_material); s.syncAsByte(_id); s.syncAsByte(_bonusFlags); s.syncAsByte(_frame); } ElementalCategory XeenItem::getElementalCategory() const { int idx; for (idx = 0; Res.ELEMENTAL_CATEGORIES[idx] < _material; ++idx) ; return (ElementalCategory)idx; } AttributeCategory XeenItem::getAttributeCategory() const { int m = _material - 59; int idx; for (idx = 0; Res.ATTRIBUTE_CATEGORIES[idx] < m; ++idx) ; return (AttributeCategory)idx; } const char *XeenItem::getItemName(ItemCategory category, uint id) { if (id < 82) return Res.ITEM_NAMES[category][id]; switch (category) { case CATEGORY_WEAPON: return Res.QUEST_ITEM_NAMES[id - 82]; case CATEGORY_ARMOR: return Res.QUEST_ITEM_NAMES[id - 82 + 35]; case CATEGORY_ACCESSORY: return Res.QUEST_ITEM_NAMES[id - 82 + 35 + 14]; default: return Res.QUEST_ITEM_NAMES[id - 82 + 35 + 14 + 11]; } } /*------------------------------------------------------------------------*/ InventoryItems::InventoryItems(Character *character, ItemCategory category): _character(character), _category(category) { resize(INV_ITEMS_TOTAL); _names = Res.ITEM_NAMES[category]; } void InventoryItems::clear() { for (uint idx = 0; idx < size(); ++idx) operator[](idx).clear(); } bool InventoryItems::passRestrictions(int itemId, bool showError) const { CharacterClass charClass = _character->_class; switch (charClass) { case CLASS_KNIGHT: case CLASS_PALADIN: return true; case CLASS_ARCHER: case CLASS_CLERIC: case CLASS_SORCERER: case CLASS_ROBBER: case CLASS_NINJA: case CLASS_BARBARIAN: case CLASS_DRUID: case CLASS_RANGER: { if (!(Res.ITEM_RESTRICTIONS[itemId + Res.RESTRICTION_OFFSETS[_category]] & (1 << (charClass - CLASS_ARCHER)))) return true; break; } default: break; } Common::String name = _names[itemId]; if (showError) { Common::String msg = Common::String::format(Res.NOT_PROFICIENT, Res.CLASS_NAMES[charClass], name.c_str()); ErrorScroll::show(Party::_vm, msg, WT_FREEZE_WAIT); } return false; } Common::String InventoryItems::getName(int itemIndex) { int id = operator[](itemIndex)._id; return _names[id]; } Common::String InventoryItems::getIdentifiedDetails(int itemIndex) { XeenItem &item = operator[](itemIndex); Common::String classes; for (int charClass = CLASS_KNIGHT; charClass <= CLASS_RANGER; ++charClass) { if (passRestrictions(charClass, true)) { const char *const name = Res.CLASS_NAMES[charClass]; classes += name[0]; classes += name[1]; classes += " "; } } if (classes.size() == 30) classes = Res.ALL; return getAttributes(item, classes); } bool InventoryItems::discardItem(int itemIndex) { XeenItem &item = operator[](itemIndex); XeenEngine *vm = Party::_vm; if (item._bonusFlags & ITEMFLAG_CURSED) { ErrorScroll::show(vm, Res.CANNOT_DISCARD_CURSED_ITEM); } else { Common::String itemDesc = getFullDescription(itemIndex, 4); Common::String msg = Common::String::format(Res.PERMANENTLY_DISCARD, itemDesc.c_str()); if (Confirm::show(vm, msg)) { operator[](itemIndex).clear(); sort(); return true; } } return true; } void InventoryItems::sort() { for (uint idx = 0; idx < size(); ++idx) { if (operator[](idx)._id == 0) { // Found empty slot operator[](idx).clear(); // Scan through the rest of the list to find any item for (uint idx2 = idx + 1; idx2 < size(); ++idx2) { if (operator[](idx2)._id) { // Found an item, so move it into the blank slot operator[](idx) = operator[](idx2); operator[](idx2).clear(); break; } } } } } void InventoryItems::removeItem(int itemIndex) { XeenItem &item = operator[](itemIndex); XeenEngine *vm = Party::_vm; if (item._bonusFlags & ITEMFLAG_CURSED) ErrorScroll::show(vm, Res.CANNOT_REMOVE_CURSED_ITEM); else item._frame = 0; } XeenEngine *InventoryItems::getVm() { return Party::_vm; } void InventoryItems::equipError(int itemIndex1, ItemCategory category1, int itemIndex2, ItemCategory category2) { XeenEngine *vm = Party::_vm; if (itemIndex1 >= 0) { Common::String itemName1 = _character->_items[category1].getName(itemIndex1); Common::String itemName2 = _character->_items[category2].getName(itemIndex2); MessageDialog::show(vm, Common::String::format(Res.REMOVE_X_TO_EQUIP_Y, itemName1.c_str(), itemName2.c_str())); } else { MessageDialog::show(vm, Common::String::format(Res.EQUIPPED_ALL_YOU_CAN, (itemIndex1 == -1) ? Res.RING : Res.MEDAL)); } } void InventoryItems::enchantItem(int itemIndex, int amount) { XeenEngine *vm = Party::_vm; vm->_sound->playFX(21); ErrorScroll::show(vm, Common::String::format(Res.NOT_ENCHANTABLE, Res.SPELL_FAILED)); } bool InventoryItems::isFull() const { return operator[](size() - 1)._id != 0; } /*------------------------------------------------------------------------*/ void WeaponItems::equipItem(int itemIndex) { XeenItem &item = operator[](itemIndex); if (item._id <= 17) { if (passRestrictions(item._id, false)) { for (uint idx = 0; idx < size(); ++idx) { XeenItem &i = operator[](idx); if (i._frame == 13 || i._frame == 1) { equipError(itemIndex, CATEGORY_WEAPON, idx, CATEGORY_WEAPON); return; } } item._frame = 1; } } else if (item._id >= 30 && item._id <= 33) { if (passRestrictions(item._id, false)) { for (uint idx = 0; idx < size(); ++idx) { XeenItem &i = operator[](idx); if (i._frame == 4) { equipError(itemIndex, CATEGORY_WEAPON, idx, CATEGORY_WEAPON); return; } } item._frame = 4; } } else { if (passRestrictions(item._id, false)) { for (uint idx = 0; idx < size(); ++idx) { XeenItem &i = operator[](idx); if (i._frame == 13 || i._frame == 1) { equipError(itemIndex, CATEGORY_WEAPON, idx, CATEGORY_WEAPON); return; } } for (uint idx = 0; idx < _character->_armor.size(); ++idx) { XeenItem &i = _character->_armor[idx]; if (i._frame == 2) { equipError(itemIndex, CATEGORY_WEAPON, idx, CATEGORY_ARMOR); return; } } item._frame = 13; } } } Common::String WeaponItems::getFullDescription(int itemIndex, int displayNum) { XeenItem &i = operator[](itemIndex); Resources &res = *getVm()->_resources; return Common::String::format("\f%02u%s%s%s\f%02u%s%s%s", displayNum, !i._bonusFlags ? res._maeNames[i._material].c_str() : "", (i._bonusFlags & ITEMFLAG_BROKEN) ? Res.ITEM_BROKEN : "", (i._bonusFlags & ITEMFLAG_CURSED) ? Res.ITEM_CURSED : "", displayNum, Res.WEAPON_NAMES[i._id], !i._bonusFlags ? "" : Res.BONUS_NAMES[i._bonusFlags & ITEMFLAG_BONUS_MASK], (i._bonusFlags & (ITEMFLAG_BROKEN | ITEMFLAG_CURSED)) || !i._bonusFlags ? "\b " : "" ); } void WeaponItems::enchantItem(int itemIndex, int amount) { Sound &sound = *getVm()->_sound; XeenItem &item = operator[](itemIndex); Character tempCharacter; if (item._material == 0 && item._bonusFlags == 0 && item._id != 34) { tempCharacter.makeItem(amount, 0, 1); XeenItem &tempItem = tempCharacter._weapons[0]; item._material = tempItem._material; item._bonusFlags = tempItem._bonusFlags; sound.playFX(19); } else { InventoryItems::enchantItem(itemIndex, amount); } } /* * Returns a text string listing all the stats/attributes of a given item */ Common::String WeaponItems::getAttributes(XeenItem &item, const Common::String &classes) { Common::String attrBonus, elemDamage, physDamage, toHit, specialPower; attrBonus = elemDamage = physDamage = toHit = specialPower = Res.FIELD_NONE; // First calculate physical damage int minVal = Res.WEAPON_DAMAGE_BASE[item._id]; int maxVal = minVal * Res.WEAPON_DAMAGE_MULTIPLIER[item._id]; if (item._material >= 37 && item._material <= 58) { minVal += Res.METAL_DAMAGE[item._material - 37]; maxVal += Res.METAL_DAMAGE[item._material - 37]; toHit = Common::String::format("%+d", Res.METAL_DAMAGE_PERCENT[item._material - 37]); } physDamage = Common::String::format(Res.DAMAGE_X_TO_Y, minVal, maxVal); // Next handle elemental/attribute damage if (item._material < 37) { int damage = Res.ELEMENTAL_DAMAGE[item._material]; if (damage > 0) { ElementalCategory elemCategory = item.getElementalCategory(); elemDamage = Common::String::format(Res.ELEMENTAL_XY_DAMAGE, damage, Res.ELEMENTAL_NAMES[elemCategory]); } } else if (item._material >= 59) { int bonus = Res.ATTRIBUTE_BONUSES[item._material - 59]; AttributeCategory attrCategory = item.getAttributeCategory(); attrBonus = Common::String::format(Res.ATTR_XY_BONUS, bonus, Res.ATTRIBUTE_NAMES[attrCategory]); } // Handle weapon effective against int effective = item._bonusFlags & ITEMFLAG_BONUS_MASK; if (effective) { specialPower = Common::String::format(Res.EFFECTIVE_AGAINST, Res.EFFECTIVENESS_NAMES[effective]); } return Common::String::format(Res.ITEM_DETAILS, classes.c_str(), toHit.c_str(), physDamage.c_str(), elemDamage.c_str(), Res.FIELD_NONE, Res.FIELD_NONE, attrBonus.c_str(), specialPower.c_str() ); } /*------------------------------------------------------------------------*/ void ArmorItems::equipItem(int itemIndex) { XeenItem &item = operator[](itemIndex); if (item._id <= 7) { if (passRestrictions(item._id, false)) { for (uint idx = 0; idx < size(); ++idx) { XeenItem &i = operator[](idx); if (i._frame == 9) { equipError(itemIndex, CATEGORY_ARMOR, idx, CATEGORY_ARMOR); return; } } item._frame = 3; } } else if (item._id == 8) { if (passRestrictions(item._id, false)) { for (uint idx = 0; idx < size(); ++idx) { XeenItem &i = operator[](idx); if (i._frame == 2) { equipError(itemIndex, CATEGORY_ARMOR, idx, CATEGORY_ARMOR); return; } } for (uint idx = 0; idx < _character->_weapons.size(); ++idx) { XeenItem &i = _character->_weapons[idx]; if (i._frame == 13) { equipError(itemIndex, CATEGORY_ARMOR, idx, CATEGORY_WEAPON); return; } } item._frame = 2; } } else if (item._id == 9) { for (uint idx = 0; idx < size(); ++idx) { XeenItem &i = operator[](idx); if (i._frame == 5) { equipError(itemIndex, CATEGORY_ARMOR, idx, CATEGORY_ARMOR); return; } } item._frame = 5; } else if (item._id == 10) { for (uint idx = 0; idx < size(); ++idx) { XeenItem &i = operator[](idx); if (i._frame == 9) { equipError(itemIndex, CATEGORY_ARMOR, idx, CATEGORY_ARMOR); return; } } item._frame = 9; } else if (item._id <= 12) { for (uint idx = 0; idx < size(); ++idx) { XeenItem &i = operator[](idx); if (i._frame == 10) { equipError(itemIndex, CATEGORY_ARMOR, idx, CATEGORY_ARMOR); return; } } item._frame = 10; } else { for (uint idx = 0; idx < size(); ++idx) { XeenItem &i = operator[](idx); if (i._frame == 6) { equipError(itemIndex, CATEGORY_ARMOR, idx, CATEGORY_ARMOR); return; } } item._frame = 6; } } Common::String ArmorItems::getFullDescription(int itemIndex, int displayNum) { XeenItem &i = operator[](itemIndex); Resources &res = *getVm()->_resources; return Common::String::format("\f%02u%s%s%s\f%02u%s%s", displayNum, !i._bonusFlags ? "" : res._maeNames[i._material].c_str(), (i._bonusFlags & ITEMFLAG_BROKEN) ? Res.ITEM_BROKEN : "", (i._bonusFlags & ITEMFLAG_CURSED) ? Res.ITEM_CURSED : "", displayNum, Res.ARMOR_NAMES[i._id], (i._bonusFlags & (ITEMFLAG_BROKEN | ITEMFLAG_CURSED)) || !i._bonusFlags ? "\b " : "" ); } void ArmorItems::enchantItem(int itemIndex, int amount) { Sound &sound = *getVm()->_sound; XeenItem &item = operator[](itemIndex); Character tempCharacter; if (item._material == 0 && item._bonusFlags == 0) { tempCharacter.makeItem(amount, 0, 2); XeenItem &tempItem = tempCharacter._armor[0]; item._material = tempItem._material; item._bonusFlags = tempItem._bonusFlags; sound.playFX(19); } else { InventoryItems::enchantItem(itemIndex, amount); } } /* * Returns a text string listing all the stats/attributes of a given item */ Common::String ArmorItems::getAttributes(XeenItem &item, const Common::String &classes) { Common::String elemResist, attrBonus, acBonus; elemResist = attrBonus = acBonus = Res.FIELD_NONE; if (item._material < 36) { int resistence = Res.ELEMENTAL_RESISTENCES[item._material]; if (resistence > 0) { int eCategory = ELEM_FIRE; while (eCategory < ELEM_MAGIC && Res.ELEMENTAL_CATEGORIES[eCategory] < item._material) ++eCategory; elemResist = Common::String::format(Res.ATTR_XY_BONUS, resistence, Res.ELEMENTAL_NAMES[eCategory]); } } else if (item._material >= 59) { int bonus = Res.ATTRIBUTE_BONUSES[item._material - 59]; AttributeCategory aCategory = item.getAttributeCategory(); attrBonus = Common::String::format(Res.ATTR_XY_BONUS, bonus, Res.ATTRIBUTE_NAMES[aCategory]); } int strength = Res.ARMOR_STRENGTHS[item._id]; if (item._material >= 37 && item._material <= 58) { strength += Res.METAL_LAC[item._material - 37]; } acBonus = Common::String::format("%+d", strength); return Common::String::format(Res.ITEM_DETAILS, classes.c_str(), Res.FIELD_NONE, Res.FIELD_NONE, Res.FIELD_NONE, elemResist.c_str(), acBonus.c_str(), attrBonus.c_str(), Res.FIELD_NONE); } /*------------------------------------------------------------------------*/ void AccessoryItems::equipItem(int itemIndex) { XeenItem &item = operator[](itemIndex); if (item._id == 1) { int count = 0; for (uint idx = 0; idx < size(); ++idx) { XeenItem &i = operator[](idx); if (i._frame == 8) ++count; } if (count <= 1) item._frame = 8; else equipError(-1, CATEGORY_ACCESSORY, itemIndex, CATEGORY_ACCESSORY); } else if (item._id == 2) { for (uint idx = 0; idx < size(); ++idx) { XeenItem &i = operator[](idx); if (i._frame == 12) { equipError(itemIndex, CATEGORY_ACCESSORY, idx, CATEGORY_ACCESSORY); return; } } } else if (item._id <= 7) { int count = 0; for (uint idx = 0; idx < size(); ++idx) { XeenItem &i = operator[](idx); if (i._frame == 7) ++count; } if (count <= 1) item._frame = 7; else equipError(-2, CATEGORY_ACCESSORY, itemIndex, CATEGORY_ACCESSORY); } else { for (uint idx = 0; idx < size(); ++idx) { XeenItem &i = operator[](idx); if (i._frame == 11) { equipError(itemIndex, CATEGORY_ACCESSORY, idx, CATEGORY_ACCESSORY); return; } } item._frame = 11; } } Common::String AccessoryItems::getFullDescription(int itemIndex, int displayNum) { XeenItem &i = operator[](itemIndex); Resources &res = *getVm()->_resources; return Common::String::format("\f%02u%s%s%s\f%02u%s%s", displayNum, !i._bonusFlags ? "" : res._maeNames[i._material].c_str(), (i._bonusFlags & ITEMFLAG_BROKEN) ? Res.ITEM_BROKEN : "", (i._bonusFlags & ITEMFLAG_CURSED) ? Res.ITEM_CURSED : "", displayNum, Res.ARMOR_NAMES[i._id], (i._bonusFlags & (ITEMFLAG_BROKEN | ITEMFLAG_CURSED)) || !i._bonusFlags ? "\b " : "" ); } /* * Returns a text string listing all the stats/attributes of a given item */ Common::String AccessoryItems::getAttributes(XeenItem &item, const Common::String &classes) { Common::String elemResist, attrBonus; elemResist = attrBonus = Res.FIELD_NONE; if (item._material < 36) { int resistence = Res.ELEMENTAL_RESISTENCES[item._material]; if (resistence > 0) { int eCategory = ELEM_FIRE; while (eCategory < ELEM_MAGIC && Res.ELEMENTAL_CATEGORIES[eCategory] < item._material) ++eCategory; elemResist = Common::String::format(Res.ATTR_XY_BONUS, resistence, Res.ELEMENTAL_NAMES[eCategory]); } } else if (item._material >= 59) { int bonus = Res.ATTRIBUTE_BONUSES[item._material - 59]; AttributeCategory aCategory = item.getAttributeCategory(); attrBonus = Common::String::format(Res.ATTR_XY_BONUS, bonus, Res.ATTRIBUTE_NAMES[aCategory]); } return Common::String::format(Res.ITEM_DETAILS, classes.c_str(), Res.FIELD_NONE, Res.FIELD_NONE, Res.FIELD_NONE, elemResist.c_str(), Res.FIELD_NONE, attrBonus.c_str(), Res.FIELD_NONE); } /*------------------------------------------------------------------------*/ Common::String MiscItems::getFullDescription(int itemIndex, int displayNum) { XeenItem &i = operator[](itemIndex); Resources &res = *getVm()->_resources; return Common::String::format("\f%02u%s%s%s\f%02u%s%s", displayNum, !i._bonusFlags ? "" : res._maeNames[i._material].c_str(), (i._bonusFlags & ITEMFLAG_BROKEN) ? Res.ITEM_BROKEN : "", (i._bonusFlags & ITEMFLAG_CURSED) ? Res.ITEM_CURSED : "", displayNum, Res.ARMOR_NAMES[i._id], (i._bonusFlags & (ITEMFLAG_BROKEN | ITEMFLAG_CURSED)) || !i._id ? "\b " : "" ); } /* * Returns a text string listing all the stats/attributes of a given item */ Common::String MiscItems::getAttributes(XeenItem &item, const Common::String &classes) { Common::String specialPower = Res.FIELD_NONE; Spells &spells = *getVm()->_spells; if (item._id) { specialPower = spells._spellNames[Res.MISC_SPELL_INDEX[item._id]]; } return Common::String::format(Res.ITEM_DETAILS, classes.c_str(), Res.FIELD_NONE, Res.FIELD_NONE, Res.FIELD_NONE, Res.FIELD_NONE, Res.FIELD_NONE, Res.FIELD_NONE, specialPower.c_str()); } /*------------------------------------------------------------------------*/ InventoryItemsGroup::InventoryItemsGroup(InventoryItems &weapons, InventoryItems &armor, InventoryItems &accessories, InventoryItems &misc) { _itemSets[0] = &weapons; _itemSets[1] = &armor; _itemSets[2] = &accessories; _itemSets[3] = &misc; } InventoryItems &InventoryItemsGroup::operator[](ItemCategory category) { return *_itemSets[category]; } void InventoryItemsGroup::breakAllItems() { for (int idx = 0; idx < INV_ITEMS_TOTAL; ++idx) { if ((*_itemSets[0])[idx]._id != 34) { (*_itemSets[0])[idx]._bonusFlags |= ITEMFLAG_BROKEN; (*_itemSets[0])[idx]._frame = 0; } (*_itemSets[1])[idx]._bonusFlags |= ITEMFLAG_BROKEN; (*_itemSets[2])[idx]._bonusFlags |= ITEMFLAG_BROKEN; (*_itemSets[3])[idx]._bonusFlags |= ITEMFLAG_BROKEN; (*_itemSets[1])[idx]._frame = 0; (*_itemSets[2])[idx]._frame = 0; } } /*------------------------------------------------------------------------*/ void AttributePair::synchronize(Common::Serializer &s) { s.syncAsByte(_permanent); s.syncAsByte(_temporary); } /*------------------------------------------------------------------------*/ AttributePair::AttributePair() { _temporary = _permanent = 0; } /*------------------------------------------------------------------------*/ Character::Character(): _weapons(this), _armor(this), _accessories(this), _misc(this), _items(_weapons, _armor, _accessories, _misc) { clear(); _faceSprites = nullptr; _rosterId = -1; } void Character::clear() { _sex = MALE; _race = HUMAN; _xeenSide = 0; _class = CLASS_KNIGHT; _ACTemp = 0; _birthDay = 0; _tempAge = 0; Common::fill(&_skills[0], &_skills[18], 0); Common::fill(&_awards[0], &_awards[128], 0); Common::fill(&_spells[0], &_spells[39], false); _lloydMap = 0; _hasSpells = false; _currentSpell = 0; _quickOption = QUICK_ATTACK; _lloydSide = 0; Common::fill(&_conditions[0], &_conditions[16], 0); _townUnknown = 0; _savedMazeId = 0; _currentHp = 0; _currentSp = 0; _birthYear = 0; _experience = 0; _currentAdventuringSpell = 0; _currentCombatSpell = 0; _might._permanent = _might._temporary = 0; _intellect._permanent = _intellect._temporary = 0; _personality._permanent = _personality._temporary = 0; _endurance._permanent = _endurance._temporary = 0; _speed._permanent = _speed._temporary = 0; _accuracy._permanent = _accuracy._temporary = 0; _luck._permanent = _luck._temporary = 0; _fireResistence._permanent = _fireResistence._temporary = 0; _coldResistence._permanent = _coldResistence._temporary = 0; _electricityResistence._permanent = _electricityResistence._temporary = 0; _poisonResistence._permanent = _poisonResistence._temporary = 0; _energyResistence._permanent = _energyResistence._temporary = 0; _magicResistence._permanent = _magicResistence._temporary = 0; _weapons.clear(); _armor.clear(); _accessories.clear(); _misc.clear(); } void Character::synchronize(Common::Serializer &s) { char name[16]; Common::fill(&name[0], &name[16], '\0'); Common::strlcpy(name, _name.c_str(), 16); s.syncBytes((byte *)name, 16); if (s.isLoading()) _name = Common::String(name); s.syncAsByte(_sex); s.syncAsByte(_race); s.syncAsByte(_xeenSide); s.syncAsByte(_class); _might.synchronize(s); _intellect.synchronize(s); _personality.synchronize(s); _endurance.synchronize(s); _speed.synchronize(s); _accuracy.synchronize(s); _luck.synchronize(s); s.syncAsByte(_ACTemp); _level.synchronize(s); s.syncAsByte(_birthDay); s.syncAsByte(_tempAge); // Synchronize the skill list for (int idx = 0; idx < 18; ++idx) s.syncAsByte(_skills[idx]); // Synchronize character awards. The original packed awards 64..127 in the // upper nibble of the first 64 bytes. Except for award 9, which was a full // byte counter counting the number of times the warzone was awarded for (int idx = 0; idx < 64; ++idx) { byte b = (idx == WARZONE_AWARD) ? _awards[idx] : (_awards[idx] ? 0x1 : 0) | (_awards[idx + 64] ? 0x10 : 0); s.syncAsByte(b); if (s.isLoading()) { _awards[idx] = (idx == WARZONE_AWARD) ? b : b & 0xF; _awards[idx + 64] = (idx == WARZONE_AWARD) ? 0 : (b >> 4) & 0xf; } } // Synchronize spell list for (int i = 0; i < MAX_SPELLS_PER_CLASS; ++i) s.syncAsByte(_spells[i]); s.syncAsByte(_lloydMap); s.syncAsByte(_lloydPosition.x); s.syncAsByte(_lloydPosition.y); s.syncAsByte(_hasSpells); s.syncAsByte(_currentSpell); s.syncAsByte(_quickOption); for (int i = 0; i < 9; ++i) _weapons[i].synchronize(s); for (int i = 0; i < 9; ++i) _armor[i].synchronize(s); for (int i = 0; i < 9; ++i) _accessories[i].synchronize(s); for (int i = 0; i < 9; ++i) _misc[i].synchronize(s); s.syncAsByte(_lloydSide); _fireResistence.synchronize(s); _coldResistence.synchronize(s); _electricityResistence.synchronize(s); _poisonResistence.synchronize(s); _energyResistence.synchronize(s); _magicResistence.synchronize(s); for (int i = 0; i < 16; ++i) s.syncAsByte(_conditions[i]); s.syncAsUint16LE(_townUnknown); s.syncAsByte(_savedMazeId); s.syncAsUint16LE(_currentHp); s.syncAsUint16LE(_currentSp); s.syncAsUint16LE(_birthYear); s.syncAsUint32LE(_experience); s.syncAsByte(_currentAdventuringSpell); s.syncAsByte(_currentCombatSpell); } Condition Character::worstCondition() const { for (int cond = ERADICATED; cond >= CURSED; --cond) { if (_conditions[cond]) return (Condition)cond; } return NO_CONDITION; } bool Character::isDisabled() const { Condition condition = worstCondition(); return condition == ASLEEP || condition == PARALYZED || condition == UNCONSCIOUS || condition == STONED || condition == ERADICATED; } bool Character::isDisabledOrDead() const { Condition condition = worstCondition(); return condition == ASLEEP || (condition >= PARALYZED && condition <= ERADICATED); } bool Character::isDead() const { Condition condition = worstCondition(); return condition >= DEAD && condition <= ERADICATED; } int Character::getAge(bool ignoreTemp) const { int year = MIN(Party::_vm->_party->_year - _birthYear, (uint)254); return ignoreTemp ? year : year + _tempAge; } int Character::getMaxHP() const { int hp = Res.BASE_HP_BY_CLASS[_class]; hp += statBonus(getStat(ENDURANCE)); hp += Res.RACE_HP_BONUSES[_race]; if (_skills[BODYBUILDER]) ++hp; if (hp < 1) hp = 1; hp *= getCurrentLevel(); hp += itemScan(7); return MAX(hp, 0); } int Character::getMaxSP() const { int result = 0; bool flag = false; int amount = 0; Attribute attrib; Skill skill; if (!_hasSpells) return 0; if (_class == CLASS_SORCERER || _class == CLASS_ARCHER) { attrib = INTELLECT; skill = PRESTIDIGITATION; } else { attrib = PERSONALITY; skill = PRAYER_MASTER; } if (_class == CLASS_DRUID || _class == CLASS_RANGER) skill = ASTROLOGER; for (;;) { // Get the base number of spell points result = statBonus(getStat(attrib)) + 3; result += Res.RACE_SP_BONUSES[_race][attrib - 1]; if (_skills[skill]) result += 2; if (result < 1) result = 1; // Multiply it by the character's level result *= getCurrentLevel(); // Classes other than sorcerer, clerics, and druids only get half the SP if (_class != CLASS_SORCERER && _class != CLASS_CLERIC && _class != CLASS_DRUID) result /= 2; if (flag || (_class != CLASS_DRUID && _class != CLASS_RANGER)) break; // Druids and rangers get bonuses averaged on both personality and intellect attrib = INTELLECT; flag = true; amount = result; } if (flag) result = (amount + result) / 2; result += itemScan(8); if (result < 0) result = 0; return result; } uint Character::getStat(Attribute attrib, bool baseOnly) const { AttributePair attr; int mode = 0; switch (attrib) { case MIGHT: attr = _might; break; case INTELLECT: attr = _intellect; mode = 1; break; case PERSONALITY: attr = _personality; mode = 1; break; case ENDURANCE: attr = _endurance; break; case SPEED: attr = _speed; break; case ACCURACY: attr = _accuracy; break; case LUCK: attr = _luck; mode = 2; break; default: return 0; } // All the attributes except luck are affected by the character's age if (mode < 2) { int age = getAge(false); int ageIndex = 0; while (Res.AGE_RANGES[ageIndex] <= age) ++ageIndex; attr._permanent += Res.AGE_RANGES_ADJUST[mode][ageIndex]; } attr._permanent += itemScan((int)attrib); if (!baseOnly) { attr._permanent += conditionMod(attrib); attr._permanent += attr._temporary; } return MAX(attr._permanent, (uint)0); } int Character::statColor(int amount, int threshold) { 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; } int Character::statBonus(uint statValue) const { int idx; for (idx = 0; Res.STAT_VALUES[idx] <= statValue; ++idx) ; return Res.STAT_BONUSES[idx]; } bool Character::charSavingThrow(DamageType attackType) const { int v, vMax; if (attackType == DT_PHYSICAL) { v = statBonus(getStat(LUCK)) + getCurrentLevel(); vMax = v + 20; } else { switch (attackType) { case DT_MAGICAL: v = _magicResistence._permanent + _magicResistence._temporary + itemScan(16); break; case DT_FIRE: v = _fireResistence._permanent + _fireResistence._temporary + itemScan(11); break; case DT_ELECTRICAL: v = _electricityResistence._permanent + _electricityResistence._temporary + itemScan(12); break; case DT_COLD: v = _coldResistence._permanent + _coldResistence._temporary + itemScan(13); break; case DT_POISON: v = _poisonResistence._permanent + _poisonResistence._temporary + itemScan(14); break; case DT_ENERGY: v = _energyResistence._permanent + _energyResistence._temporary + itemScan(15); break; default: v = 0; break; } vMax = v + 40; } return Party::_vm->getRandomNumber(1, vMax) <= v; } bool Character::noActions() { Condition condition = worstCondition(); switch (condition) { case CURSED: case POISONED: case DISEASED: case INSANE: case IN_LOVE: case DRUNK: { Common::String msg = Common::String::format(Res.IN_NO_CONDITION, _name.c_str()); ErrorScroll::show(Party::_vm, msg, Party::_vm->_mode == 17 ? WT_LOC_WAIT : WT_NONFREEZED_WAIT); return true; } default: return false; } } void Character::setAward(int awardId, bool value) { int v = awardId; if (awardId == 73) v = 126; else if (awardId == 81) v = 127; _awards[v] = value ? 1 : 0; } bool Character::hasAward(int awardId) const { int v = awardId; if (awardId == 73) v = 126; else if (awardId == 81) v = 127; return _awards[v]; } int Character::getArmorClass(bool baseOnly) const { Party &party = *Party::_vm->_party; int result = statBonus(getStat(SPEED)) + itemScan(9); if (!baseOnly) result += party._blessed + _ACTemp; return MAX(result, 0); } int Character::getThievery() const { int result = getCurrentLevel() * 2; if (_class == CLASS_NINJA) result += 15; else if (_class == CLASS_ROBBER) result += 30; switch (_race) { case ELF: case GNOME: result += 10; break; case DWARF: result += 5; break; case HALF_ORC: result -= 10; break; default: break; } result += itemScan(10); // If the character doesn't have a thievery skill, then don't allow any result if (!_skills[THIEVERY]) result = 0; return MAX(result, 0); } uint Character::getCurrentLevel() const { return MAX(_level._permanent + _level._temporary, (uint)0); } int Character::itemScan(int itemId) const { int result = 0; // Weapons for (int idx = 0; idx < INV_ITEMS_TOTAL; ++idx) { const XeenItem &item = _weapons[idx]; if (item._frame && !(item._bonusFlags & 0xC0) && itemId < 11 && itemId != 3 && item._material >= 59 && item._material <= 130) { int mIndex = (int)item.getAttributeCategory(); if (mIndex > PERSONALITY) ++mIndex; if (mIndex == itemId) result += Res.ATTRIBUTE_BONUSES[item._material - 59]; } } // Armor for (int idx = 0; idx < INV_ITEMS_TOTAL; ++idx) { const XeenItem &item = _armor[idx]; if (item._frame && !(item._bonusFlags & 0xC0)) { if (itemId < 11 && itemId != 3 && item._material >= 59 && item._material <= 130) { int mIndex = (int)item.getAttributeCategory(); if (mIndex > PERSONALITY) ++mIndex; if (mIndex == itemId) result += Res.ATTRIBUTE_BONUSES[item._material - 59]; } if (itemId > 10 && item._material < 37) { int mIndex = item.getElementalCategory() + 11; if (mIndex == itemId) { result += Res.ELEMENTAL_RESISTENCES[item._material]; } } if (itemId == 9) { result += Res.ARMOR_STRENGTHS[item._id]; if (item._material >= 37 && item._material <= 58) result += Res.METAL_LAC[item._material - 37]; } } } // Accessories for (int idx = 0; idx < INV_ITEMS_TOTAL; ++idx) { const XeenItem &item = _accessories[idx]; if (item._frame && !(item._bonusFlags & 0xC0)) { if (itemId < 11 && itemId != 3 && item._material >= 59 && item._material <= 130) { int mIndex = (int)item.getAttributeCategory(); if (mIndex > PERSONALITY) ++mIndex; if (mIndex == itemId) { result += Res.ATTRIBUTE_BONUSES[item._material - 59]; } } if (itemId > 10 && item._material < 37) { int mIndex = item.getElementalCategory() + 11; if (mIndex == itemId) result += Res.ELEMENTAL_RESISTENCES[item._material]; } } } return result; } int Character::conditionMod(Attribute attrib) const { if (_conditions[DEAD] || _conditions[STONED] || _conditions[ERADICATED]) return 0; int v[7]; Common::fill(&v[0], &v[7], 0); if (_conditions[CURSED]) v[6] -= _conditions[CURSED]; if (_conditions[INSANE]) { v[2] -= _conditions[INSANE]; v[1] -= _conditions[INSANE]; v[5] -= _conditions[INSANE]; v[0] -= _conditions[INSANE]; v[4] -= _conditions[INSANE]; } if (_conditions[POISONED]) { v[0] -= _conditions[POISONED]; v[4] -= _conditions[POISONED]; v[5] -= _conditions[POISONED]; } if (_conditions[DISEASED]) { v[3] -= _conditions[DISEASED]; v[2] -= _conditions[DISEASED]; v[1] -= _conditions[DISEASED]; } for (int idx = 0; idx < 7; ++idx) { v[idx] -= _conditions[HEART_BROKEN]; v[idx] -= _conditions[IN_LOVE]; v[idx] -= _conditions[WEAK]; v[idx] -= _conditions[DRUNK]; } return v[attrib]; } void Character::setValue(int id, uint value) { Party &party = *Party::_vm->_party; Scripts &scripts = *Party::_vm->_scripts; switch (id) { case 3: // Set character sex _sex = (Sex)value; break; case 4: // Set race _race = (Race)value; break; case 5: // Set class _class = (CharacterClass)value; break; case 8: // Set the current Hp _currentHp = value; break; case 9: // Set the current Sp _currentSp = value; break; case 10: case 77: // Set temporary armor class _ACTemp = value; break; case 11: // Set temporary level _level._temporary = value; break; case 12: // Set the character's temporary age _tempAge = value; break; case 16: // Set character experience _experience = value; break; case 17: // Set party poison resistence party._poisonResistence = value; break; case 18: // Set condition if (value == 16) { // Clear all the conditions clearConditions(); } else if (value == 6) { _conditions[value] = 1; } else { ++_conditions[value]; } if (value >= DEAD && value <= ERADICATED && _currentHp > 0) _currentHp = 0; break; case 25: // Set time of day in minutes (0-1440) party._minutes = value; break; case 34: // Set party gold party._gold = value; break; case 35: // Set party gems party._gems = value; break; case 37: _might._temporary = value; break; case 38: _intellect._temporary = value; break; case 39: _personality._temporary = value; break; case 40: _endurance._temporary = value; break; case 41: _speed._temporary = value; break; case 42: _accuracy._temporary = value; break; case 43: _luck._temporary = value; break; case 45: _might._permanent = value; break; case 46: _intellect._permanent = value; break; case 47: _personality._permanent = value; break; case 48: _endurance._permanent = value; break; case 49: _speed._permanent = value; break; case 50: _accuracy._permanent = value; break; case 51: _luck._permanent = value; break; case 52: _fireResistence._permanent = value; break; case 53: _electricityResistence._permanent = value; break; case 54: _coldResistence._permanent = value; break; case 55: _poisonResistence._permanent = value; break; case 56: _energyResistence._permanent = value; break; case 57: _magicResistence._permanent = value; break; case 58: _fireResistence._temporary = value; break; case 59: _electricityResistence._temporary = value; break; case 60: _coldResistence._temporary = value; break; case 61: _poisonResistence._temporary = value; break; case 62: _energyResistence._temporary = value; break; case 63: _magicResistence._temporary = value; break; case 64: _level._permanent = value; break; case 65: // Set party food party._food = value; break; case 69: party._levitateCount = value; break; case 70: party._lightCount = value; break; case 71: party._fireResistence = value; break; case 72: party._electricityResistence = value; break; case 73: party._coldResistence = value; break; case 74: party._walkOnWaterActive = value != 0; party._poisonResistence = value; party._wizardEyeActive = value != 0; party._coldResistence = value; party._electricityResistence = value; party._fireResistence = value; party._lightCount = value; party._levitateCount = value; break; case 76: // Set day of the year (0-99) party._day = value; break; case 79: party._wizardEyeActive = true; break; case 83: scripts._nEdamageType = (DamageType)value; break; case 84: party._mazeDirection = (Direction)value; break; case 85: party._year = value; break; case 94: party._walkOnWaterActive = value != 0; break; default: break; } } bool Character::guildMember() const { FileManager &files = *g_vm->_files; Party &party = *g_vm->_party; if (party._mazeId == 49 && !files._isDarkCc) { return hasAward(SHANGRILA_GUILD_MEMBER); } switch (party._mazeId) { case 29: return hasAward(CASTLEVIEW_GUILD_MEMBER); case 31: return hasAward(SANDCASTER_GUILD_MEMBER); case 33: return hasAward(LAKESIDE_GUILD_MEMBER); case 35: return hasAward(NECROPOLIS_GUILD_MEMBER); default: return hasAward(OLYMPUS_GUILD_MEMBER); } } uint Character::experienceToNextLevel() const { uint next = nextExperienceLevel(); uint curr = getCurrentExperience(); return (curr >= next) ? 0 : next - curr; } uint Character::nextExperienceLevel() const { int shift, base; if (_level._permanent >= 12) { base = _level._permanent - 12; shift = 10; } else { base = 0; assert(_level._permanent > 0); shift = _level._permanent - 1; } return (base * 1024000) + (Res.CLASS_EXP_LEVELS[_class] << shift); } uint Character::getCurrentExperience() const { int lev = _level._permanent - 1; int shift, base; if (lev == 0) return _experience; if (lev >= 12) { base = lev - 12; shift = 10; } else { base = 0; shift = lev - 1; } return (base * 1024000) + (Res.CLASS_EXP_LEVELS[_class] << shift) + _experience; } int Character::getNumSkills() const { int total = 0; for (int idx = THIEVERY; idx <= DANGER_SENSE; ++idx) { if (_skills[idx]) ++total; } return total; } int Character::getNumAwards() const { int total = 0; for (int idx = 0; idx < AWARDS_TOTAL; ++idx) { if (hasAward(idx)) ++total; } return total; } ItemCategory Character::makeItem(int p1, int itemIndex, int p3) { XeenEngine *vm = Party::_vm; Scripts &scripts = *vm->_scripts; if (!p1) return CATEGORY_WEAPON; int itemId = 0; int v4 = vm->getRandomNumber(100); int v6 = vm->getRandomNumber(p1 < 6 ? 100 : 80); ItemCategory category; int v16 = 0, v14 = 0, miscBonus = 0, miscId = 0, v8 = 0, v12 = 0; // Randomly pick a category and item Id if (p3 == 12) { if (scripts._itemType < 35) { category = CATEGORY_WEAPON; itemId = scripts._itemType; } else if (scripts._itemType < 49) { category = CATEGORY_ARMOR; itemId = scripts._itemType - 35; } else if (scripts._itemType < 60) { category = CATEGORY_ACCESSORY; itemId = scripts._itemType - 49; } else { category = CATEGORY_MISC; itemId = scripts._itemType - 60; } } else { switch (p3) { case 1: v4 = 35; break; case 2: v4 = 60; break; case 3: v4 = 100; break; default: break; } if (p1 == 1) { if (v4 <= 40) { category = CATEGORY_WEAPON; if (v6 <= 30) { itemId = vm->getRandomNumber(1, 6); } else if (v6 <= 60) { itemId = vm->getRandomNumber(7, 17); } else if (v6 <= 85) { itemId = vm->getRandomNumber(18, 29); } else { itemId = vm->getRandomNumber(30, 33); } } else if (v4 <= 85) { category = CATEGORY_ARMOR; itemId = vm->getRandomNumber(1, 7); } else { category = CATEGORY_MISC; itemId = vm->getRandomNumber(1, 9); } } else if (v4 <= 35) { category = CATEGORY_WEAPON; if (v6 <= 30) { itemId = vm->getRandomNumber(1, 6); } else if (v6 <= 60) { itemId = vm->getRandomNumber(7, 17); } else if (v6 <= 85) { itemId = vm->getRandomNumber(18, 29); } else { itemId = vm->getRandomNumber(30, 33); } } else if (v4 <= 60) { category = CATEGORY_ARMOR; itemId = (v6 > 70) ? 8 : vm->getRandomNumber(1, 7); } else if (v6 <= 10) { category = CATEGORY_ARMOR; itemId = 9; } else if (v6 <= 20) { category = CATEGORY_ARMOR; itemId = 13; } else if (v6 <= 35) { category = CATEGORY_ACCESSORY; itemId = 1; } else if (v6 <= 45) { category = CATEGORY_ARMOR; itemId = 10; } else if (v6 <= 55) { category = CATEGORY_ARMOR; itemId = vm->getRandomNumber(11, 12); } else if (v6 <= 65) { category = CATEGORY_ACCESSORY; itemId = 2; } else if (v6 <= 75) { category = CATEGORY_ACCESSORY; itemId = vm->getRandomNumber(3, 7); } else if (v6 <= 80) { category = CATEGORY_ACCESSORY; itemId = vm->getRandomNumber(8, 10); } else { category = CATEGORY_MISC; itemId = vm->getRandomNumber(1, 9); } } XeenItem &newItem = _items[category][itemIndex]; newItem.clear(); newItem._id = itemId; v4 = vm->getRandomNumber(1, 100); switch (category) { case CATEGORY_WEAPON: case CATEGORY_ARMOR: if (p1 != 1) { if (v4 <= 70) { v8 = 3; } else if (v4 <= 98) { v8 = 1; } else { v8 = 2; } } break; case CATEGORY_ACCESSORY: if (v4 <= 20) { v8 = 3; } else if (v4 <= 60) { v8 = 1; } else { v8 = 2; } break; case CATEGORY_MISC: v8 = 4; break; default: break; } if (p1 != 1 || category == CATEGORY_MISC) { int rval, mult; switch (v8) { case 1: rval = vm->getRandomNumber(1, 100); if (rval <= 25) { mult = 0; } else if (rval <= 45) { mult = 1; } else if (rval <= 60) { mult = 2; } else if (rval <= 75) { mult = 3; } else if (rval <= 95) { mult = 4; } else { mult = 5; } v12 = Res.MAKE_ITEM_ARR1[vm->getRandomNumber(Res.MAKE_ITEM_ARR2[mult][p1][0], Res.MAKE_ITEM_ARR2[mult][p1][1])]; break; case 2: rval = vm->getRandomNumber(1, 100); if (rval <= 15) { mult = 0; } else if (rval <= 25) { mult = 1; } else if (rval <= 35) { mult = 2; } else if (rval <= 50) { mult = 3; } else if (rval <= 65) { mult = 4; } else if (rval <= 80) { mult = 5; } else if (rval <= 85) { mult = 6; } else if (rval <= 90) { mult = 7; } else if (rval <= 95) { mult = 8; } else { mult = 9; } v14 = Res.MAKE_ITEM_ARR1[vm->getRandomNumber(Res.MAKE_ITEM_ARR3[mult][p1][0], Res.MAKE_ITEM_ARR3[mult][p1][1])]; break; case 3: mult = p1 == 7 || vm->getRandomNumber(1, 100) > 70 ? 1 : 0; v16 = vm->getRandomNumber(Res.MAKE_ITEM_ARR4[mult][p1 - 1][0], Res.MAKE_ITEM_ARR4[mult][p1 - 1][1]); if (mult) v16 += 9; break; case 4: miscBonus = vm->getRandomNumber(Res.MAKE_ITEM_ARR5[p1][0], Res.MAKE_ITEM_ARR5[p1][1]); break; default: break; } } switch (category) { case CATEGORY_WEAPON: if (p1 != 1) { newItem._material = (v14 ? v14 + 58 : 0) + (v16 ? v16 + 36 : 0) + v12; if (vm->getRandomNumber(20) == 10) newItem._bonusFlags = vm->getRandomNumber(1, 6); } break; case CATEGORY_ARMOR: case CATEGORY_ACCESSORY: if (p1 != 1) { newItem._material = (v14 ? v14 + 58 : 0) + (v16 ? v16 + 36 : 0) + v12; } break; case CATEGORY_MISC: newItem._id = miscId; newItem._bonusFlags = miscBonus; break; default: break; } return category; } void Character::addHitPoints(int amount) { Interface &intf = *Party::_vm->_interface; Common::fill(&intf._charFX[0], &intf._charFX[MAX_ACTIVE_PARTY], 0); if (!isDead()) { int maxHp = getMaxHP(); if (_currentHp <= maxHp) { _currentHp = MIN(_currentHp + amount, maxHp); intf.spellFX(this); } if (_currentHp > 0) _conditions[UNCONSCIOUS] = 0; intf.drawParty(true); } Common::fill(&intf._charFX[0], &intf._charFX[MAX_ACTIVE_PARTY], 0); } void Character::subtractHitPoints(int amount) { Debugger &debugger = *g_vm->_debugger; Sound &sound = *g_vm->_sound; // If invincibility is turned on in the debugger, ignore all damage if (debugger._invincible) return; // Subtract the given HP amount _currentHp -= amount; bool flag = _currentHp <= 10; if (_currentHp < 1) { int v = getMaxHP() + _currentHp; if (v >= 1) { _conditions[UNCONSCIOUS] = 1; sound.playFX(38); } else { _conditions[DEAD] = 1; flag = true; if (_currentHp > 0) _currentHp = 0; } if (flag) { // Check for breaking equipped armor for (int idx = 0; idx < INV_ITEMS_TOTAL; ++idx) { XeenItem &item = _armor[idx]; if (item._id && item._frame) item._bonusFlags |= ITEMFLAG_BROKEN; } } } } bool Character::hasSlayerSword() const { for (uint idx = 0; idx < INV_ITEMS_TOTAL; ++idx) { if (_weapons[idx]._id == 34) // Character has Xeen Slayer sword return true; } return false; } bool Character::hasMissileWeapon() const { for (uint idx = 0; idx < INV_ITEMS_TOTAL; ++idx) { if (_weapons[idx]._frame == 4) { return !isDisabledOrDead(); } } return false; } int Character::getClassCategory() const { switch (_class) { case CLASS_ARCHER: case CLASS_SORCERER: return 1; case CLASS_DRUID: case CLASS_RANGER: return 2; default: return 0; } } void Character::clearConditions() { Common::fill(&_conditions[CURSED], &_conditions[NO_CONDITION], false); } } // End of namespace Xeen