diff options
Diffstat (limited to 'engines/xeen/combat.cpp')
-rw-r--r-- | engines/xeen/combat.cpp | 2090 |
1 files changed, 2090 insertions, 0 deletions
diff --git a/engines/xeen/combat.cpp b/engines/xeen/combat.cpp new file mode 100644 index 0000000000..1d03a5128d --- /dev/null +++ b/engines/xeen/combat.cpp @@ -0,0 +1,2090 @@ +/* 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 "common/algorithm.h" +#include "common/rect.h" +#include "xeen/character.h" +#include "xeen/combat.h" +#include "xeen/interface.h" +#include "xeen/xeen.h" + +namespace Xeen { + +static const int MONSTER_GRID_X[48] = { + 1, 1, 1, 0, -1, -1, -1, 1, 1, 1, 0, -1, + -1, -1, 1, 1, 1, 0, -1, -1, -1, 1, 1, 1, + 0, -1, -1, -1, 1, 1, 1, 0, -1, -1, -1, 1, + 1, 1, 0, -1, -1, -1, 1, 1, 1, 0, -1, -1 +}; + +static const int MONSTER_GRID_Y[48] = { + 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, + 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0 +}; + +static const int MONSTER_GRID3[48] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + - 1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 1, 1, + 0, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 +}; + +static const int MONSTER_GRID_BITINDEX1[48] = { + 1, 1, 1, 2, 3, 3, 3, 1, 1, 1, 2, 3, + 3, 3, 1, 1, 1, 2, 3, 3, 3, 1, 1, 1, + 0, 3, 3, 3, 1, 1, 1, 0, 3, 3, 3, 1, + 1, 1, 0, 3, 3, 3, 1, 1, 1, 0, 3, 3 +}; + +static const int MONSTER_GRID_BITINDEX2[48] = { + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, + 0, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +static const int ATTACK_TYPE_FX[23] = { + 49, 18, 13, 14, 15, 17, 16, 0, 6, 1, 2, 3, + 4, 5, 4, 9, 27, 29, 44, 51, 53, 61, 71 +}; + +static const int MONSTER_SHOOT_POW[7] = { 12, 14, 0, 4, 8, 10, 13 }; + +static const int COMBAT_SHOOTING[4] = { 1, 1, 2, 3 }; + +static const int DAMAGE_TYPE_EFFECTS[19] = { + 3, 10, 4, 11, 1, 2, 5, 9, 5, 14, 5, 14, 10, 8, 3, 9, 2, 2, 3 +}; + +static const int POW_WEAPON_VOCS[35] = { + 0, 5, 4, 5, 5, 5, 5, 2, 4, 5, 3, 5, 4, 2, 3, 2, 2, 4, 5, 5, + 5, 5, 5, 1, 3, 2, 5, 1, 1, 1, 0, 0, 0, 2, 2 +}; + +static const int MONSTER_ITEM_RANGES[6] = { 10, 20, 50, 100, 100, 100 }; + +#define monsterSavingThrow(MONINDEX) (_vm->getRandomNumber(1, 50 + (MONINDEX)) <= (MONINDEX)) + +/*------------------------------------------------------------------------*/ + +Combat::Combat(XeenEngine *vm): _vm(vm), _missVoc("miss.voc"), _pow1Voc("pow1.voc") { + Common::fill(&_attackMonsters[0], &_attackMonsters[26], 0); + Common::fill(&_charsArray1[0], &_charsArray1[12], 0); + Common::fill(&_monPow[0], &_monPow[12], 0); + Common::fill(&_monsterScale[0], &_monsterScale[12], 0); + Common::fill(&_elemPow[0], &_elemPow[12], ELEM_FIRE); + Common::fill(&_elemScale[0], &_elemScale[12], 0); + Common::fill(&_shooting[0], &_shooting[8], 0); + Common::fill(&_monsterMap[0][0], &_monsterMap[32][32], 0); + Common::fill(&_monsterMoved[0], &_monsterMoved[MAX_NUM_MONSTERS], false); + Common::fill(&_rangeAttacking[0], &_rangeAttacking[MAX_NUM_MONSTERS], false); + Common::fill(&_gmonHit[0], &_gmonHit[36], 0); + Common::fill(&_missedShot[0], &_missedShot[MAX_PARTY_COUNT], 0); + _globalCombat = 0; + _whosTurn = -1; + _itemFlag = false; + _monstersAttacking = false; + _combatMode = COMBATMODE_0; + _monsterIndex = 0; + _partyRan = false; + _monster2Attack = -1; + _whosSpeed = 0; + _damageType = DT_PHYSICAL; + _oldCharacter = nullptr; + _shootType = ST_0; + _monsterDamage = 0; + _weaponDamage = 0; + _weaponDie = _weaponDice = 0; + _attackWeapon = nullptr; + _attackWeaponId = 0; + _hitChanceBonus = 0; + _dangerPresent = false; + _moveMonsters = false; + _rangeType = RT_SINGLE; +} + +void Combat::clear() { + Common::fill(&_attackMonsters[0], &_attackMonsters[26], -1); +} + +void Combat::giveCharDamage(int damage, DamageType attackType, int charIndex) { + Party &party = *_vm->_party; + Screen &screen = *_vm->_screen; + Scripts &scripts = *_vm->_scripts; + SoundManager &sound = *_vm->_sound; + int charIndex1 = charIndex + 1; + int selectedIndex1 = 0; + int selectedIndex2 = 0; + bool breakFlag = false; + + screen.closeWindows(); + + int idx = (int)party._activeParty.size(); + if (!scripts._v2) { + for (idx = 0; idx < (int)party._activeParty.size(); ++idx) { + Character &c = party._activeParty[idx]; + Condition condition = c.worstCondition(); + + if (!(condition >= UNCONSCIOUS && condition <= ERADICATED)) { + if (!selectedIndex1) { + selectedIndex1 = idx + 1; + } else { + selectedIndex2 = idx + 1; + break; + } + } + } + } + if (idx == (int)party._activeParty.size()) { + selectedIndex1 = scripts._v2 ? charIndex : 0; + goto loop; + } + + for (;;) { + // The if below is to get around errors due to the + // goto I was forced to use when reimplementing this method + if (true) { + Character &c = party._activeParty[selectedIndex1]; + c._conditions[ASLEEP] = 0; // Force character to be awake + + int frame = 0, fx = 0; + switch (attackType) { + case DT_PHYSICAL: + fx = 29; + break; + case DT_MAGICAL: + frame = 6; + fx = 27; + break; + case DT_FIRE: + damage -= party._fireResistence; + frame = 1; + fx = 22; + break; + case DT_ELECTRICAL: + damage -= party._electricityResistence; + frame = 2; + fx = 23; + break; + case DT_COLD: + damage -= party._coldResistence; + frame = 3; + fx = 24; + break; + case DT_POISON: + damage -= party._poisonResistence; + frame = 4; + fx = 26; + break; + case DT_ENERGY: + frame = 5; + fx = 25; + break; + case DT_SLEEP: + fx = 38; + break; + default: + break; + } + + // All attack types other than physical allow for saving + // throws to reduce the damage + if (attackType != DT_PHYSICAL) { + while (c.charSavingThrow(attackType) && damage > 0) + damage /= 2; + } + + // Draw the attack effect on the character sprite + sound.playFX(fx); + _powSprites.draw(screen, frame, + Common::Point(CHAR_FACES_X[selectedIndex1], 150)); + screen._windows[33].update(); + + // Reduce damage if power shield active, and set it zero + // if the damage amount has become negative.. you wouldn't + // want attacks healing the characters + if (party._powerShield) + damage -= party._powerShield; + if (damage < 0) + damage = 0; + + // TODO: This seems weird.. maybe I've got attack types wrong.. + // why should attack type 7 (DT_SLEEP) set the dead condition? + if (attackType == DT_SLEEP) { + damage = c._currentHp; + c._conditions[DEAD] = 1; + } + + // Subtract the hit points from the character + c.subtractHitPoints(damage); + } + + if (selectedIndex2) { + ++selectedIndex1; +loop: + if ((scripts._v2 ? charIndex1 : (int)party._activeParty.size()) > selectedIndex1) + break; + } + + // Break check and if not, move to other index + if (!selectedIndex2 || breakFlag) + break; + + selectedIndex1 = selectedIndex2 - 1; + breakFlag = true; + } +} + +/** + * Do damage to a specific character + */ +void Combat::doCharDamage(Character &c, int charNum, int monsterDataIndex) { + EventsManager &events = *_vm->_events; + Interface &intf = *_vm->_interface; + Map &map = *_vm->_map; + Party &party = *_vm->_party; + Screen &screen = *_vm->_screen; + SoundManager &sound = *_vm->_sound; + MonsterStruct &monsterData = map._monsterData[monsterDataIndex]; + + // Attacked characters are automatically woken up + c._conditions[ASLEEP] = 0; + + // Figure out the damage amount + int damage = 0; + for (int idx = 0; idx < monsterData._strikes; ++idx) + damage += _vm->getRandomNumber(1, monsterData._dmgPerStrike); + + + int fx = 29, frame = 0; + if (monsterData._attackType) { + if (c.charSavingThrow(monsterData._attackType)) + damage /= 2; + + switch (monsterData._attackType) { + case DT_MAGICAL: + frame = 6; + fx = 27; + break; + case DT_FIRE: + damage -= party._fireResistence; + frame = 1; + fx = 22; + break; + case DT_ELECTRICAL: + damage -= party._electricityResistence; + frame = 2; + fx = 23; + break; + case DT_COLD: + damage -= party._coldResistence; + frame = 3; + fx = 24; + break; + case DT_POISON: + damage -= party._poisonResistence; + frame = 4; + fx = 26; + break; + case DT_ENERGY: + frame = 5; + fx = 25; + break; + default: + break; + } + + while (damage > 0 && c.charSavingThrow(monsterData._attackType)) + damage /= 2; + } + + sound.playFX(fx); + intf._charPowSprites.draw(screen, frame, Common::Point(CHAR_FACES_X[charNum], 150)); + screen._windows[33].update(); + + damage -= party._powerShield; + if (damage > 0 && monsterData._specialAttack && !c.charSavingThrow(DT_PHYSICAL)) { + switch (monsterData._specialAttack) { + case SA_POISON: + if (!++c._conditions[POISONED]) + c._conditions[POISONED] = -1; + sound.playFX(26); + break; + case SA_DISEASE: + if (!++c._conditions[DISEASED]) + c._conditions[DISEASED] = -1; + sound.playFX(26); + break; + case SA_INSANE: + if (!++c._conditions[INSANE]) + c._conditions[INSANE] = -1; + sound.playFX(28); + break; + case SA_SLEEP: + if (!++c._conditions[ASLEEP]) + c._conditions[ASLEEP] = -1; + sound.playFX(36); + break; + case SA_CURSEITEM: + for (int idx = 0; idx < INV_ITEMS_TOTAL; ++idx) { + if (c._weapons[idx]._id != 34) + c._weapons[idx]._bonusFlags |= ITEMFLAG_CURSED; + c._armor[idx]._bonusFlags |= ITEMFLAG_CURSED; + c._accessories[idx]._bonusFlags |= ITEMFLAG_CURSED; + c._misc[idx]._bonusFlags |= ITEMFLAG_CURSED;; + } + sound.playFX(37); + break; + case SA_DRAINSP: + c._currentSp = 0; + sound.playFX(37); + break; + case SA_CURSE: + if (!++c._conditions[CURSED]) + c._conditions[CURSED] = -1; + sound.playFX(37); + break; + case SA_PARALYZE: + if (!++c._conditions[PARALYZED]) + c._conditions[PARALYZED] = -1; + sound.playFX(37); + break; + case SA_UNCONSCIOUS: + if (!++c._conditions[UNCONSCIOUS]) + c._conditions[UNCONSCIOUS] = -1; + sound.playFX(37); + break; + case SA_CONFUSE: + if (!++c._conditions[CONFUSED]) + c._conditions[CONFUSED] = -1; + sound.playFX(28); + break; + case SA_BREAKWEAPON: + for (int idx = 0; idx < INV_ITEMS_TOTAL; ++idx) { + XeenItem &weapon = c._weapons[idx]; + if (weapon._id != 34 && weapon._id != 0 && weapon._frame != 0) { + weapon._bonusFlags |= ITEMFLAG_BROKEN; + weapon._frame = 0; + } + } + sound.playFX(37); + break; + case SA_WEAKEN: + if (!++c._conditions[WEAK]) + c._conditions[WEAK] = -1; + sound.playFX(36); + break; + case SA_ERADICATE: + if (!++c._conditions[ERADICATED]) + c._conditions[ERADICATED] = -1; + c._items.breakAllItems(); + sound.playFX(37); + + if (c._currentHp > 0) + c._currentHp = 0; + break; + case SA_AGING: + ++c._tempAge; + sound.playFX(37); + break; + case SA_DEATH: + if (!++c._conditions[DEAD]) + c._conditions[DEAD] = -1; + sound.playFX(38); + if (c._currentHp > 0) + c._currentHp = 0; + break; + case SA_STONE: + if (!++c._conditions[STONED]) + c._conditions[STONED] = -1; + sound.playFX(38); + if (c._currentHp > 0) + c._currentHp = 0; + break; + } + + c.subtractHitPoints(damage); + } + + events.ipause(2); + intf.drawParty(true); +} + +void Combat::moveMonsters() { + Interface &intf = *_vm->_interface; + Map &map = *_vm->_map; + Party &party = *_vm->_party; + + if (!_moveMonsters) + return; + + intf._tillMove = 0; + if (intf._charsShooting) + return; + + Common::fill(&_monsterMap[0][0], &_monsterMap[32][32], 0); + Common::fill(&_monsterMoved[0], &_monsterMoved[MAX_NUM_MONSTERS], false); + Common::fill(&_rangeAttacking[0], &_rangeAttacking[MAX_NUM_MONSTERS], false); + Common::fill(&_gmonHit[0], &_gmonHit[36], -1); + _dangerPresent = false; + + for (uint idx = 0; idx < map._mobData._monsters.size(); ++idx) { + MazeMonster &monster = map._mobData._monsters[idx]; + if (monster._position.y < 32) { + _monsterMap[monster._position.y][monster._position.x]++; + } + } + + for (int loopNum = 0; loopNum < 2; ++loopNum) { + int arrIndex = -1; + for (int yDiff = 3; yDiff >= -3; --yDiff) { + for (int xDiff = -3; xDiff <= 3; ++xDiff) { + Common::Point pt = party._mazePosition + Common::Point(xDiff, yDiff); + ++arrIndex; + + for (int idx = 0; idx < (int)map._mobData._monsters.size(); ++idx) { + MazeMonster &monster = map._mobData._monsters[idx]; + MonsterStruct &monsterData = *monster._monsterData; + + if (pt == monster._position) { + _dangerPresent = true; + if ((monster._isAttacking || _vm->_mode == MODE_SLEEPING) + && !_monsterMoved[idx]) { + if (party._mazePosition.x == pt.x || party._mazePosition.y == pt.y) { + // Check for range attacks + if (monsterData._rangeAttack && !_rangeAttacking[idx] + && _attackMonsters[0] != idx && _attackMonsters[1] != idx + && _attackMonsters[2] != idx && !monster._damageType) { + // Setup monster for attacking + setupMonsterAttack(monster._spriteId, pt); + _rangeAttacking[idx] = true; + } + } + + switch (party._mazeDirection) { + case DIR_NORTH: + case DIR_SOUTH: + if (monsterCanMove(pt, MONSTER_GRID_BITMASK[MONSTER_GRID_BITINDEX1[arrIndex]], + MONSTER_GRID_X[arrIndex], MONSTER_GRID_Y[arrIndex], idx)) { + // Move the monster + moveMonster(idx, Common::Point(MONSTER_GRID_X[arrIndex], MONSTER_GRID_Y[arrIndex])); + } else { + if (monsterCanMove(pt, MONSTER_GRID_BITMASK[MONSTER_GRID_BITINDEX2[arrIndex]], + arrIndex >= 21 && arrIndex <= 27 ? MONSTER_GRID3[arrIndex] : 0, + arrIndex >= 21 && arrIndex <= 27 ? 0 : MONSTER_GRID3[arrIndex], + idx)) + if (arrIndex >= 21 && arrIndex <= 27) { + moveMonster(idx, Common::Point(MONSTER_GRID3[arrIndex], 0)); + } else { + moveMonster(idx, Common::Point(0, MONSTER_GRID3[arrIndex])); + } + } + break; + + case DIR_EAST: + case DIR_WEST: + if (monsterCanMove(pt, MONSTER_GRID_BITMASK[MONSTER_GRID_BITINDEX2[arrIndex]], + arrIndex >= 21 && arrIndex <= 27 ? MONSTER_GRID3[arrIndex] : 0, + arrIndex >= 21 && arrIndex <= 27 ? 0 : MONSTER_GRID3[arrIndex], + idx)) { + if (arrIndex >= 21 && arrIndex <= 27) { + moveMonster(idx, Common::Point(MONSTER_GRID3[arrIndex], 0)); + } else { + moveMonster(idx, Common::Point(0, MONSTER_GRID3[arrIndex])); + } + } else if (monsterCanMove(pt, MONSTER_GRID_BITMASK[MONSTER_GRID_BITINDEX1[arrIndex]], + MONSTER_GRID_X[arrIndex], MONSTER_GRID_Y[arrIndex], idx)) { + moveMonster(idx, Common::Point(MONSTER_GRID_X[arrIndex], MONSTER_GRID_Y[arrIndex])); + } + } + } + } + } + } + } + } + + monsterOvercome(); + if (_monstersAttacking) + monstersAttack(); +} + +void Combat::monstersAttack() { + EventsManager &events = *_vm->_events; + Interface &intf = *_vm->_interface; + Map &map = *_vm->_map; + Party &party = *_vm->_party; + SoundManager &sound = *_vm->_sound; + int powNum = -1; + MonsterStruct *monsterData = nullptr; + OutdoorDrawList &outdoorList = intf._outdoorList; + IndoorDrawList &indoorList = intf._indoorList; + + for (int idx = 0; idx < 36; ++idx) { + if (_gmonHit[idx] != -1) { + monsterData = &map._monsterData[_gmonHit[idx]]; + powNum = MONSTER_SHOOT_POW[monsterData->_attackType]; + if (powNum != 12) + break; + } + } + + _powSprites.load(Common::String::format("pow%d.icn", powNum)); + sound.playFX(ATTACK_TYPE_FX[monsterData->_attackType]); + + for (int charNum = 0; charNum < MAX_PARTY_COUNT; ++charNum) { + if (!_shooting[charNum]) + continue; + + if (map._isOutdoors) { + outdoorList._attackImgs1[charNum]._scale = 3; + outdoorList._attackImgs2[charNum]._scale = 7; + outdoorList._attackImgs3[charNum]._scale = 11; + outdoorList._attackImgs4[charNum]._scale = 15; + outdoorList._attackImgs1[charNum]._sprites = nullptr; + outdoorList._attackImgs2[charNum]._sprites = nullptr; + outdoorList._attackImgs3[charNum]._sprites = nullptr; + outdoorList._attackImgs4[charNum]._sprites = nullptr; + + switch (_shooting[charNum]) { + case 1: + outdoorList._attackImgs1[charNum]._sprites = &_powSprites; + break; + case 2: + outdoorList._attackImgs2[charNum]._sprites = &_powSprites; + break; + default: + outdoorList._attackImgs3[charNum]._sprites = &_powSprites; + break; + } + } else { + indoorList._attackImgs1[charNum]._scale = 3; + indoorList._attackImgs2[charNum]._scale = 7; + indoorList._attackImgs3[charNum]._scale = 11; + indoorList._attackImgs4[charNum]._scale = 15; + indoorList._attackImgs1[charNum]._sprites = nullptr; + indoorList._attackImgs2[charNum]._sprites = nullptr; + indoorList._attackImgs3[charNum]._sprites = nullptr; + indoorList._attackImgs4[charNum]._sprites = nullptr; + + switch (_shooting[charNum]) { + case 1: + indoorList._attackImgs1[charNum]._sprites = &_powSprites; + break; + case 2: + indoorList._attackImgs2[charNum]._sprites = &_powSprites; + break; + default: + indoorList._attackImgs3[charNum]._sprites = &_powSprites; + break; + } + } + } + + // Wait whilst the attacking effect is done + do { + intf.draw3d(true); + events.pollEventsAndWait(); + } while (!_vm->shouldQuit() && intf._isAttacking); + + endAttack(); + + if (_vm->_mode != MODE_COMBAT) { + // Combat wasn't previously active, but it is now. Set up + // the combat party from the currently active party + setupCombatParty(); + } + + for (int idx = 0; idx < 36; ++idx) { + if (_gmonHit[idx] != -1) + doMonsterTurn(_gmonHit[idx]); + } + + _monstersAttacking = false; + + if (_vm->_mode != MODE_SLEEPING) { + for (uint charNum = 0; charNum < party._activeParty.size(); ++charNum) { + Condition condition = party._activeParty[charNum].worstCondition(); + + if (condition != ASLEEP && (condition < PARALYZED || condition == NO_CONDITION)) { + _vm->_mode = MODE_1; + break; + } + } + } +} + +void Combat::setupMonsterAttack(int monsterDataIndex, const Common::Point &pt) { + Party &party = *_vm->_party; + + for (int idx = 0; idx < 36; ++idx) { + if (_gmonHit[idx] != -1) { + int result = stopAttack(pt - party._mazePosition); + if (result) { + _monstersAttacking = true; + _gmonHit[idx] = monsterDataIndex; + + if (result != 1) { + for (int charNum = 0; charNum < MAX_PARTY_COUNT; ++charNum) { + if (!_shooting[charNum]) { + _shooting[charNum] = COMBAT_SHOOTING[result - 1]; + } + } + } + } + } + } +} + +/** + * Determines whether a given monster can move + */ +bool Combat::monsterCanMove(const Common::Point &pt, int wallShift, + int xDiff, int yDiff, int monsterId) { + Map &map = *_vm->_map; + MazeMonster &monster = map._mobData._monsters[monsterId]; + MonsterStruct &monsterData = *monster._monsterData; + + Common::Point tempPos = pt; + if (map._isOutdoors) { + tempPos += Common::Point(xDiff, yDiff); + wallShift = 4; + } + int v = map.mazeLookup(tempPos, wallShift); + + if (!map._isOutdoors) { + return v <= map.mazeData()._difficulties._wallNoPass; + } else { + SurfaceType surfaceType; + switch (v) { + case 0: + case 2: + case 3: + case 4: + case 5: + case 6: + case 8: + case 11: + case 13: + case 14: + surfaceType = (SurfaceType)map.mazeData()._surfaceTypes[map._currentSurfaceId]; + if (surfaceType == SURFTYPE_WATER || surfaceType == SURFTYPE_DWATER) { + return monsterData._flying || monster._spriteId == 59; + } else if (surfaceType == SURFTYPE_SPACE) { + return monsterData._flying; + } else { + return _vm->_files->_isDarkCc || monster._spriteId != 59; + } + default: + return v <= map.mazeData()._difficulties._wallNoPass; + } + } +} + +/** + * Moves a monster by a given delta amount if it's a valid move + */ +void Combat::moveMonster(int monsterId, const Common::Point &moveDelta) { + Map &map = *_vm->_map; + MazeMonster &monster = map._mobData._monsters[monsterId]; + Common::Point newPos = monster._position + moveDelta; + + if (_monsterMap[newPos.y][newPos.x] < 3 && !monster._damageType && _moveMonsters) { + // Adjust monster's position + ++_monsterMap[newPos.y][newPos.x]; + --_monsterMap[monster._position.y][monster._position.x]; + monster._position = newPos; + _monsterMoved[monsterId] = true; + } +} + +void Combat::endAttack() { + Interface &intf = *_vm->_interface; + Map &map = *_vm->_map; + Party &party = *_vm->_party; + intf._isAttacking = false; + IndoorDrawList &indoorList = intf._indoorList; + OutdoorDrawList &outdoorList = intf._outdoorList; + + for (uint idx = 0; idx < party._activeParty.size(); ++idx) { + if (map._isOutdoors) { + outdoorList._attackImgs1[idx]._scale = 0; + outdoorList._attackImgs2[idx]._scale = 0; + outdoorList._attackImgs3[idx]._scale = 0; + outdoorList._attackImgs4[idx]._scale = 0; + outdoorList._attackImgs1[idx]._sprites = nullptr; + outdoorList._attackImgs2[idx]._sprites = nullptr; + outdoorList._attackImgs3[idx]._sprites = nullptr; + outdoorList._attackImgs4[idx]._sprites = nullptr; + } else { + indoorList._attackImgs1[idx]._scale = 0; + indoorList._attackImgs2[idx]._scale = 0; + indoorList._attackImgs3[idx]._scale = 0; + indoorList._attackImgs4[idx]._scale = 0; + indoorList._attackImgs1[idx]._sprites = nullptr; + indoorList._attackImgs2[idx]._sprites = nullptr; + indoorList._attackImgs3[idx]._sprites = nullptr; + indoorList._attackImgs4[idx]._sprites = nullptr; + } + } + + Common::fill(&_shooting[0], &_shooting[MAX_PARTY_COUNT], false); +} + +void Combat::monsterOvercome() { + Map &map = *_vm->_map; + + for (uint idx = 0; idx < map._mobData._monsters.size(); ++idx) { + MazeMonster &monster = map._mobData._monsters[idx]; + int dataIndex = monster._spriteId; + + if (monster._damageType != DT_PHYSICAL && monster._damageType != DT_DRAGONSLEEP) { + // Do a saving throw for monster + if (dataIndex <= _vm->getRandomNumber(1, dataIndex + 50)) + monster._damageType = 0; + } + } +} + +void Combat::doMonsterTurn(int monsterId) { + Interface &intf = *_vm->_interface; + Map &map = *_vm->_map; + Party &party = *_vm->_party; + SoundManager &sound = *_vm->_sound; + + if (_monstersAttacking) { + int monsterIndex; + switch (_whosTurn - _combatParty.size()) { + case 0: + monsterIndex = _attackMonsters[0]; + intf._indoorList[156]._scale = 0; + break; + case 1: + monsterIndex = _attackMonsters[1]; + intf._indoorList[150]._scale = 0; + break; + case 2: + default: + monsterIndex = _attackMonsters[2]; + intf._indoorList[153]._scale = 0; + } + + MazeMonster &monster = map._mobData._monsters[monsterIndex]; + MonsterStruct &monsterData = *monster._monsterData; + if (monster._damageType) + return; + + monster._frame = 8; + monster._fieldA = 3; + monster._field9 = 0; + intf.draw3d(true); + intf.draw3d(true); + + File f(Common::String::format("%s.voc", monsterData._attackVoc.c_str())); + sound.playSample(&f, 0); + monsterId = monster._spriteId; + } + + MonsterStruct &monsterData = map._monsterData[monsterId]; + bool flag = false; + for (int attackNum = 0; attackNum < monsterData._numberOfAttacks; ++attackNum) { + int charNum = -1; + bool isHated = false; + + if (monsterData._hatesClass != -1) { + if (monsterData._hatesClass == 15) + // Monster hates all classes + goto loop; + + for (uint charIndex = 0; charIndex < _combatParty.size(); ++charIndex) { + Character &c = *_combatParty[charIndex]; + Condition cond = c.worstCondition(); + if (cond >= PARALYZED && cond <= ERADICATED) + continue; + + bool isHated = false; + switch (monsterData._hatesClass) { + case CLASS_KNIGHT: + case CLASS_PALADIN: + case CLASS_ARCHER: + case CLASS_CLERIC: + case CLASS_SORCERER: + case CLASS_ROBBER: + case CLASS_NINJA: + case CLASS_BARBARIAN: + case CLASS_DRUID: + case CLASS_RANGER: + isHated = c._class == monsterData._hatesClass; + break; + case 12: + isHated = c._race == DWARF; + break; + default: + break; + } + + if (isHated) { + charNum = charIndex; + break; + } + } + } + + if (!isHated) { + // No particularly hated foe, so decide which character to start with + switch (_combatParty.size()) { + case 1: + charNum = 0; + break; + case 2: + case 3: + case 4: + case 5: + charNum = _vm->getRandomNumber(0, _combatParty.size() - 1); + break; + case 6: + if (_vm->getRandomNumber(1, 6) == 6) + charNum = 5; + else + charNum = _vm->getRandomNumber(0, 4); + break; + } + } + + // Attacking loop + do { + if (!flag) { + Condition cond = _combatParty[charNum]->worstCondition(); + + if (cond >= PARALYZED && cond <= ERADICATED) { + Common::Array<int> ableChars; + bool skip = false; + + for (uint idx = 0; idx < _combatParty.size() && !skip; ++idx) { + switch (_combatParty[idx]->worstCondition()) { + case PARALYZED: + case UNCONSCIOUS: + if (flag) + skip = true; + break; + case DEAD: + case STONED: + case ERADICATED: + break; + default: + ableChars.push_back(idx); + break; + } + } + + if (!skip) { + if (ableChars.size() == 0) { + party._dead = true; + _vm->_mode = MODE_1; + return; + } + + charNum = ableChars[_vm->getRandomNumber(0, ableChars.size() - 1)]; + } + } + } + + // Unconditional if to get around goto initialization errors + if (true) { + Character &c = *_combatParty[charNum]; + if (monsterData._attackType != DT_PHYSICAL || c._conditions[ASLEEP]) { + doCharDamage(c, charNum, monsterId); + } else { + int v = _vm->getRandomNumber(1, 20); + if (v == 1) { + sound.playFX(6); + } else { + if (v == 20) + doCharDamage(c, charNum, monsterId); + v += monsterData._hitChance / 4 + _vm->getRandomNumber(1, + monsterData._hitChance); + + int ac = c.getArmorClass() + (!_charsBlocked[charNum] ? 10 : + c.getCurrentLevel() / 2 + 15); + if (ac > v) { + sound.playFX(6); + } else { + doCharDamage(c, charNum, monsterId); + } + } + } + + if (flag) + break; + } +loop: + flag = true; + } while (++charNum < (int)_combatParty.size()); + } + + intf.drawParty(true); +} + +int Combat::stopAttack(const Common::Point &diffPt) { + Map &map = *_vm->_map; + Party &party = *_vm->_party; + Direction dir = party._mazeDirection; + const Common::Point &mazePos = party._mazePosition; + + if (map._isOutdoors) { + if (diffPt.x > 0) { + for (int x = 1; x <= diffPt.x; ++x) { + int v = map.mazeLookup(Common::Point(mazePos.x + x, mazePos.y), 0, 8); + if (v) + return 0; + } + return (dir == DIR_EAST) ? diffPt.x + 1 : 1; + + } else if (diffPt.x < 0) { + for (int x = diffPt.x; x < 0; ++x) { + int v = map.mazeLookup(Common::Point(mazePos.x + x, mazePos.y), 4); + switch (v) { + case 0: + case 2: + case 4: + case 5: + case 8: + case 11: + case 13: + case 14: + break; + default: + return 0; + } + } + return dir == DIR_WEST ? diffPt.x * -1 + 1 : 1; + + } else if (diffPt.y <= 0) { + for (int y = diffPt.y; y < 0; ++y) { + int v = map.mazeLookup(Common::Point(mazePos.x, mazePos.y + y), 4); + switch (v) { + case 0: + case 2: + case 4: + case 5: + case 8: + case 11: + case 13: + case 14: + break; + default: + return 0; + } + } + return party._mazeDirection == DIR_SOUTH ? diffPt.y * -1 + 1 : 1; + + } else { + for (int y = 1; y <= diffPt.y; ++y) { + int v = map.mazeLookup(Common::Point(mazePos.x, mazePos.y + y), 4); + switch (v) { + case 0: + case 2: + case 4: + case 5: + case 8: + case 11: + case 13: + case 14: + break; + default: + return 0; + } + } + return dir == DIR_NORTH ? diffPt.y + 1 : 1; + } + } else { + // Indoors + if (diffPt.x > 0) { + for (int x = 1; x <= diffPt.x; ++x) { + int v = map.mazeLookup(Common::Point(mazePos.x + x, mazePos.y), 0, 8); + if (v) + return 0; + } + return dir == DIR_EAST ? diffPt.x + 1 : 1; + + } else if (diffPt.x < 0) { + for (int x = diffPt.x; x < 0; ++x) { + int v = map.mazeLookup(Common::Point(mazePos.x + x, mazePos.y), 0, 0x800); + if (v) + return 0; + } + return dir == DIR_WEST ? diffPt.x * -1 + 1 : 1; + + } else if (diffPt.y <= 0) { + for (int y = diffPt.y; y < 0; ++y) { + int v = map.mazeLookup(Common::Point(mazePos.x, mazePos.y + y), 0, 0x8000); + if (v) + return 0; + } + return dir == DIR_SOUTH ? diffPt.y * -1 + 1 : 1; + + } else { + for (int y = 1; y <= diffPt.y; ++y) { + int v = map.mazeLookup(Common::Point(mazePos.x, mazePos.y + y), 0, 0x80); + if (v) + return 0; + } + return dir == DIR_NORTH ? diffPt.y + 1 : 1; + } + } +} + +/** + * Setup the combat party with a copy of the currently active party + */ +void Combat::setupCombatParty() { + Party &party = *_vm->_party; + + _combatParty.clear(); + for (uint idx = 0; idx < party._activeParty.size(); ++idx) + _combatParty.push_back(&party._activeParty[idx]); +} + +void Combat::setSpeedTable() { + Map &map = *_vm->_map; + Common::Array<int> charSpeeds; + bool hasSpeed = _whosSpeed != -1 && _whosSpeed < (int)_speedTable.size(); + int oldSpeed = hasSpeed ? _speedTable[_whosSpeed] : 0; + + // Set up speeds for party membres + int maxSpeed = 0; + for (uint charNum = 0; charNum < _combatParty.size(); ++charNum) { + Character &c = *_combatParty[charNum]; + charSpeeds.push_back(c.getStat(SPEED)); + + maxSpeed = MAX(charSpeeds[charNum], maxSpeed); + } + + // Add in speeds of attacking monsters + for (int monsterNum = 0; monsterNum < 3; ++monsterNum) { + if (_attackMonsters[monsterNum] != -1) { + MazeMonster &monster = map._mobData._monsters[_attackMonsters[monsterNum]]; + MonsterStruct &monsterData = *monster._monsterData; + charSpeeds.push_back(monsterData._speed); + + maxSpeed = MAX(maxSpeed, monsterData._speed); + } else { + charSpeeds.push_back(0); + } + } + + // Populate the _speedTable list with the character/monster indexes + // in order of attacking speed + _speedTable.clear(); + for (; maxSpeed >= 0; --maxSpeed) { + for (uint idx = 0; idx < charSpeeds.size(); ++idx) { + if (charSpeeds[idx] == maxSpeed) + _speedTable.push_back(idx); + } + } + + if (hasSpeed) { + if (_speedTable[_whosSpeed] != oldSpeed) { + for (uint idx = 0; idx < charSpeeds.size(); ++idx) { + if (oldSpeed == _speedTable[idx]) { + _whosSpeed = idx; + break; + } + } + } + } +} + +/** + * Returns true if all participants in the combat are disabled + */ +bool Combat::allHaveGone() const { + for (uint idx = 0; idx < _charsGone.size(); ++idx) { + if (!_charsGone[idx]) { + if (idx >= _combatParty.size()) { + return false; + } else { + Condition condition = _combatParty[idx]->worstCondition(); + if (condition < PARALYZED || condition == NO_CONDITION) + return false; + } + } + } + + return true; +} + +/** + * Returns true if all the characters of the party are disabled + */ +bool Combat::charsCantAct() const { + for (uint idx = 0; idx < _combatParty.size(); ++idx) { + if (!_combatParty[idx]->isDisabledOrDead()) + return false; + } + + return true; +} + +/** + * Return a description of the monsters being faced + */ +Common::String Combat::getMonsterDescriptions() { + Map &map = *_vm->_map; + Common::String lines[3]; + + // Get names of monsters attacking, if any + for (int idx = 0; idx < 3; ++idx) { + if (_attackMonsters[idx] != -1) { + MazeMonster &monster = map._mobData._monsters[_attackMonsters[idx]]; + MonsterStruct &monsterData = *monster._monsterData; + int textColor = monster.getTextColor(); + + Common::String format = "\n\v020\f%2u%s\fd"; + format.setChar('2' + idx, 3); + lines[idx] = Common::String::format(format.c_str(), textColor, + monsterData._name.c_str()); + } + } + + if (_monsterIndex == 2 && _attackMonsters[2] != -1) { + _monster2Attack = _attackMonsters[2]; + } if (_monsterIndex == 1 && _attackMonsters[1] != -1) { + _monster2Attack = _attackMonsters[1]; + } else { + _monster2Attack = _attackMonsters[0]; + _monsterIndex = 0; + } + + return Common::String::format(COMBAT_DETAILS, lines[0].c_str(), + lines[1].c_str(), lines[2].c_str()); +} + +void Combat::attack(Character &c, RangeType rangeType) { + Interface &intf = *_vm->_interface; + Map &map = *_vm->_map; + Party &party = *_vm->_party; + int damage = _monsterDamage; + + if (_monster2Attack == -1) + return; + + MazeMonster &monster = map._mobData._monsters[_monster2Attack]; + int monsterDataIndex = monster._spriteId; + MonsterStruct &monsterData = map._monsterData[monsterDataIndex]; + + if (rangeType) { + if (_shootType != ST_1 || _damageType == DT_MAGIC_ARROW) { + if (!monsterData._magicResistence || monsterData._magicResistence <= + _vm->getRandomNumber(1, 100 + _oldCharacter->getCurrentLevel())) { + if (_monsterDamage != 0) { + attack2(damage, rangeType); + setSpeedTable(); + } else { + switch (_damageType) { + case DT_SLEEP: + if (monsterData._monsterType == MONSTER_ANIMAL || monsterData._monsterType == MONSTER_HUMANOID) { + if (_vm->getRandomNumber(1, 50 + monsterDataIndex) > monsterDataIndex) + monster._damageType = DT_SLEEP; + } + break; + case DT_FINGEROFDEATH: + if ((monsterData._monsterType == MONSTER_ANIMAL || monsterData._monsterType == MONSTER_HUMANOID) + && !monsterSavingThrow(monsterDataIndex)) { + damage = MIN(monster._hp, 50); + attack2(damage, RT_ALL); + setSpeedTable(); + } + break; + case DT_HOLYWORD: + if (monsterData._monsterType == MONSTER_UNDEAD) { + attack2(monster._hp, RT_ALL); + setSpeedTable(); + } + break; + case DT_MASS_DISTORTION: + attack2(MAX(monster._hp / 2, 1), RT_ALL); + setSpeedTable(); + break; + case DT_UNDEAD: + if (monsterData._monsterType == MONSTER_UNDEAD) + damage = 25; + else + rangeType = RT_ALL; + attack2(damage, rangeType); + setSpeedTable(); + break; + case DT_BEASTMASTER: + if ((monsterData._monsterType == MONSTER_ANIMAL || monsterData._monsterType == MONSTER_HUMANOID) + && !monsterSavingThrow(monsterDataIndex)) { + monster._damageType = DT_BEASTMASTER; + } + break; + case DT_DRAGONSLEEP: + if (monsterData._monsterType == MONSTER_DRAGON && !monsterSavingThrow(monsterDataIndex)) + monster._damageType = DT_DRAGONSLEEP; + break; + case DT_GOLEMSTOPPER: + if (monsterData._monsterType == MONSTER_GOLEM) { + attack2(100, rangeType); + setSpeedTable(); + } + break; + case DT_HYPNOTIZE: + if ((monsterData._monsterType == MONSTER_ANIMAL || monsterData._monsterType == MONSTER_HUMANOID) + && !monsterSavingThrow(monsterDataIndex)) { + monster._damageType = _damageType; + } + break; + case DT_INSECT_SPRAY: + if (monsterData._monsterType == MONSTER_INSECT) { + attack2(25, rangeType); + setSpeedTable(); + } + break; + case DT_MAGIC_ARROW: + attack2(8, rangeType); + setSpeedTable(); + break; + default: + break; + } + } + } + } else { + Common::fill(&_elemPow[0], &_elemPow[PARTY_AND_MONSTERS], ELEM_FIRE); + damage = 0; + + for (uint charIndex = 0; charIndex < party._activeParty.size(); ++charIndex) { + Character &c = party._activeParty[charIndex]; + + if (_shooting[charIndex] && !_missedShot[charIndex]) { + if (!hitMonster(c, rangeType)) { + ++_missedShot[charIndex]; + } else { + damage = _monsterDamage ? _monsterDamage : _weaponDamage; + _shooting[charIndex] = 0; + attack2(damage, rangeType); + + if (map._isOutdoors) { + intf._outdoorList._attackImgs1[charIndex]._scale = 0; + intf._outdoorList._attackImgs1[charIndex]._sprites = nullptr; + intf._outdoorList._attackImgs2[charIndex]._scale = 0; + intf._outdoorList._attackImgs2[charIndex]._sprites = nullptr; + intf._outdoorList._attackImgs3[charIndex]._scale = 0; + intf._outdoorList._attackImgs3[charIndex]._sprites = nullptr; + intf._outdoorList._attackImgs4[charIndex]._scale = 0; + intf._outdoorList._attackImgs4[charIndex]._sprites = nullptr; + } else { + intf._indoorList._attackImgs1[charIndex]._scale = 0; + intf._indoorList._attackImgs1[charIndex]._sprites = nullptr; + intf._indoorList._attackImgs2[charIndex]._scale = 0; + intf._indoorList._attackImgs2[charIndex]._sprites = nullptr; + intf._indoorList._attackImgs3[charIndex]._scale = 0; + intf._indoorList._attackImgs3[charIndex]._sprites = nullptr; + intf._indoorList._attackImgs4[charIndex]._scale = 0; + intf._indoorList._attackImgs4[charIndex]._sprites = nullptr; + } + + if (_monster2Attack == -1) + return; + } + } + } + } + } else { + _damageType = DT_PHYSICAL; + int divisor = 0; + switch (c._class) { + case CLASS_BARBARIAN: + divisor = 4; + break; + case CLASS_KNIGHT: + case CLASS_NINJA: + divisor = 5; + break; + case CLASS_PALADIN: + case CLASS_ARCHER: + case CLASS_ROBBER: + case CLASS_RANGER: + divisor = 6; + break; + case CLASS_CLERIC: + case CLASS_DRUID: + divisor = 7; + break; + case CLASS_SORCERER: + divisor = 8; + break; + } + + int numberOfAttacks = c.getCurrentLevel() / divisor + 1; + damage = 0; + + while (numberOfAttacks-- > 0) { + if (hitMonster(c, RT_SINGLE)) + damage += getMonsterDamage(c); + } + + for (int itemIndex = 0; itemIndex < INV_ITEMS_TOTAL; ++itemIndex) { + XeenItem &weapon = c._weapons[itemIndex]; + if (weapon._frame != 0) { + switch (weapon._bonusFlags & ITEMFLAG_BONUS_MASK) { + case 1: + if (monsterData._monsterType == MONSTER_DRAGON) + damage *= 3; + break; + case 2: + if (monsterData._monsterType == MONSTER_UNDEAD) + damage *= 3; + break; + case 3: + if (monsterData._monsterType == MONSTER_GOLEM) + damage *= 3; + break; + case 4: + if (monsterData._monsterType == MONSTER_INSECT) + damage *= 3; + break; + case 5: + if (monsterData._monsterType == MONSTER_0) + damage *= 3; + break; + case 6: + if (monsterData._monsterType == MONSTER_ANIMAL) + damage *= 3; + break; + } + } + } + + attack2(damage, rangeType); + setSpeedTable(); + } +} + +void Combat::attack2(int damage, RangeType rangeType) { + Interface &intf = *_vm->_interface; + Map &map = *_vm->_map; + Party &party = *_vm->_party; + SoundManager &sound = *_vm->_sound; + bool isDarkCc = _vm->_files->_isDarkCc; + MazeMonster &monster = map._mobData._monsters[_monster2Attack]; + MonsterStruct &monsterData = *monster._monsterData; + bool monsterDied = false; + + if (!isDarkCc && damage && rangeType && monster._spriteId == 89) + damage = 0; + + if (!damage) { + sound.playSample(&_missVoc, 1); + sound.playFX(6); + } else { + if (!isDarkCc && monster._spriteId == 89) + damage += 100; + if (monster._damageType == DT_SLEEP || monster._damageType == DT_DRAGONSLEEP) + monster._damageType = DT_PHYSICAL; + + if ((!rangeType || !_damageType) && _attackWeaponId != 34) { + if (monsterData._phsyicalResistence != 0) { + if (monsterData._phsyicalResistence == 100) { + damage = 0; + } else { + // This doesn't seem to have any effect? + damage = (damage * 100) / 100; + } + } + } + + if (damage) { + _charsArray1[_monsterIndex] = 3; + _monPow[_monsterIndex] = _damageType == DT_PHYSICAL && (rangeType == 3 || rangeType == 0); + monster._frame = 11; + monster._fieldA = 5; + } + + int monsterResist = getMonsterResistence(rangeType); + damage += monsterResist; + if (monsterResist > 0) { + _elemPow[_monsterIndex] = _attackWeapon->getElementalCategory(); + _elemScale[_monsterIndex] = getDamageScale(monsterResist); + } else if (rangeType != 3) { + _elemPow[_monsterIndex] = ELEM_FIRE; + } + + if (rangeType != 0 && rangeType != 3) { + monster._effect2 = DAMAGE_TYPE_EFFECTS[_damageType]; + monster._effect1 = 0; + } + + if (rangeType && monsterSavingThrow(monster._spriteId)) { + switch (_damageType) { + case DT_FINGEROFDEATH: + case DT_MASS_DISTORTION: + damage = 5; + break; + case DT_SLEEP: + case DT_HOLYWORD: + case DT_UNDEAD: + case DT_BEASTMASTER: + case DT_DRAGONSLEEP: + case DT_GOLEMSTOPPER: + case DT_HYPNOTIZE: + case DT_INSECT_SPRAY: + case DT_MAGIC_ARROW: + break; + default: + damage /= 2; + break; + } + } + + if (damage < 1) { + sound.playSample(&_missVoc, 1); + sound.playFX(6); + } else { + _monsterScale[_monsterIndex] = getDamageScale(damage); + intf.draw3d(true); + + sound.playSample(nullptr, 0); + File powVoc(Common::String::format("pow%d.voc", + POW_WEAPON_VOCS[_attackWeaponId])); + sound.playFX(60 + POW_WEAPON_VOCS[_attackWeaponId]); + sound.playSample(&powVoc, 1); + + if (monster._hp > damage) { + monster._hp -= damage; + } else { + monster._hp = 0; + monsterDied = true; + } + } + } + + intf.draw3d(true); + + if (monsterDied) { + if (!isDarkCc) { + if (_monster2Attack == 20 && party._mazeId == 41) + party._gameFlags[11] = true; + if (_monster2Attack == 8 && party._mazeId == 78) { + party._gameFlags[60] = true; + party._quests[23] = false; + + for (uint idx = 0; idx < party._activeParty.size(); ++idx) + party._activeParty[idx].setAward(42, true); + + if (_monster2Attack == 27 && party._mazeId == 29) + party._gameFlags[104] = true; + } + } + + giveExperience(monsterData._experience); + + if (party._mazeId != 85) { + party._treasure._gold = monsterData._gold; + party._treasure._gems = monsterData._gems; + + if (!isDarkCc && monster._spriteId == 89) { + party._treasure._weapons[0]._id = 90; + party._treasure._weapons[0]._bonusFlags = 0; + party._treasure._weapons[0]._material = 0; + party._treasure._hasItems = true; + party._questItems[8]++; + } + + int itemDrop = monsterData._itemDrop; + if (itemDrop) { + if (MONSTER_ITEM_RANGES[itemDrop] >= _vm->getRandomNumber(1, 100)) { + Character tempChar; + int category = tempChar.makeItem(itemDrop, 0, 0); + + switch (category) { + case CATEGORY_WEAPON: + for (int idx = 0; idx < MAX_TREASURE_ITEMS; ++idx) { + if (party._treasure._weapons[idx]._id == 0) { + party._treasure._weapons[idx] = tempChar._weapons[0]; + party._treasure._hasItems = 1; + break; + } + } + break; + case CATEGORY_ARMOR: + for (int idx = 0; idx < MAX_TREASURE_ITEMS; ++idx) { + if (party._treasure._armor[idx]._id == 0) { + party._treasure._armor[idx] = tempChar._armor[0]; + party._treasure._hasItems = 1; + break; + } + } + break; + case CATEGORY_ACCESSORY: + for (int idx = 0; idx < MAX_TREASURE_ITEMS; ++idx) { + if (party._treasure._accessories[idx]._id == 0) { + party._treasure._accessories[idx] = tempChar._accessories[0]; + party._treasure._hasItems = 1; + break; + } + } + break; + case CATEGORY_MISC: + for (int idx = 0; idx < MAX_TREASURE_ITEMS; ++idx) { + if (party._treasure._accessories[idx]._id == 0) { + party._treasure._accessories[idx] = tempChar._accessories[0]; + party._treasure._hasItems = 1; + break; + } + } + break; + } + } + } + } + + monster._position = Common::Point(0x80, 0x80); + _charsArray1[_monsterIndex] = 0; + _monster2Attack = -1; + intf.draw3d(true); + + if (_attackMonsters[0] != -1) { + _monster2Attack = _attackMonsters[0]; + _monsterIndex = 0; + } + } +} + +/** + * Flag the currently active character as blocking/defending + */ +void Combat::block() { + _charsBlocked[_whosTurn] = true; +} + +/** + * Perform whatever the current combat character's quick action is + */ +void Combat::quickFight() { + Spells &spells = *_vm->_spells; + Character *c = _combatParty[_whosTurn]; + + switch (c->_quickOption) { + case QUICK_ATTACK: + attack(*c, RT_SINGLE); + break; + case QUICK_SPELL: + if (c->_currentSpell != -1) { + spells.castSpell(c, (MagicSpell)SPELLS_ALLOWED[c->getClassCategory()][c->_currentSpell]); + } + break; + case QUICK_BLOCK: + block(); + break; + case QUICK_RUN: + run(); + break; + default: + break; + } +} + +/** + * Current selected character is trying to run away + */ +void Combat::run() { + Map &map = *_vm->_map; + SoundManager &sound = *_vm->_sound; + + if (_vm->getRandomNumber(1, 100) < map.mazeData()._difficulties._chance2Run) { + // Remove the character from the combat party + _combatParty.remove_at(_whosTurn); + setSpeedTable(); + --_whosSpeed; + _whosTurn = -1; + _partyRan = true; + sound.playFX(51); + } +} + +bool Combat::hitMonster(Character &c, RangeType rangeType) { + Map &map = *_vm->_map; + getWeaponDamage(c, rangeType); + int chance = c.statBonus(c.getStat(ACCURACY)) + _hitChanceBonus; + int divisor = 0; + + switch (c._class) { + case CLASS_KNIGHT: + case CLASS_BARBARIAN: + divisor = 1; + break; + case CLASS_PALADIN : + case CLASS_ARCHER: + case CLASS_ROBBER: + case CLASS_NINJA: + case CLASS_RANGER: + divisor = 2; + break; + case CLASS_CLERIC: + case CLASS_DRUID: + divisor = 3; + break; + case CLASS_SORCERER: + divisor = 4; + break; + } + + chance += c.getCurrentLevel() / divisor; + chance -= c._conditions[CURSED]; + + // Add on a random amount + int v; + do { + v = _vm->getRandomNumber(1, 20); + chance += v; + } while (v == 20); + + assert(_monster2Attack != -1); + MazeMonster &monster = map._mobData._monsters[_monster2Attack]; + MonsterStruct &monsterData = *monster._monsterData; + + if (monster._damageType != DT_PHYSICAL) + chance += 20; + + return chance >= (monsterData._accuracy + 10); +} + +void Combat::getWeaponDamage(Character &c, RangeType rangeType) { + Party &party = *_vm->_party; + _attackWeapon = nullptr; + _weaponDie = _weaponDice = 0; + _weaponDamage = 0; + _hitChanceBonus = 0; + + for (int idx = 0; idx < INV_ITEMS_TOTAL; ++idx) { + bool flag; + if (rangeType) { + flag = c._weapons[idx]._frame == 4; + } else { + flag = c._weapons[idx]._frame == 1 || c._weapons[idx]._frame == 13; + } + + if (flag) { + if (!(c._weapons[idx]._bonusFlags & (ITEMFLAG_BROKEN | ITEMFLAG_CURSED))) { + _attackWeapon = &c._weapons[idx]; + + if (c._weapons[idx]._material >= 37 && c._weapons[idx]._material < 59) { + _hitChanceBonus = METAL_DAMAGE_PERCENT[c._weapons[idx]._material - 37]; + _weaponDamage = METAL_DAMAGE[c._weapons[idx]._material - 37]; + } + } + + _hitChanceBonus += party._heroism; + _attackWeaponId = c._weapons[idx]._id; + _weaponDice = WEAPON_DAMAGE_BASE[_attackWeaponId]; + _weaponDie = WEAPON_DAMAGE_MULTIPLIER[_attackWeaponId]; + + for (int diceIdx = 0; diceIdx < _weaponDice; ++diceIdx) + _weaponDamage += _vm->getRandomNumber(1, _weaponDie); + } + } + + if (_weaponDamage < 1) + _weaponDamage = 0; + if (!party._difficulty) { + _hitChanceBonus += 5; + _weaponDamage *= 3; + } +} + +int Combat::getMonsterDamage(Character &c) { + return MAX(c.statBonus(c.getStat(MIGHT)) + _weaponDamage, 1); +} + +int Combat::getDamageScale(int v) { + if (v < 10) + return 5; + else if (v < 100) + return 0; + else + return 0x8000; +} + +int Combat::getMonsterResistence(RangeType rangeType) { + Map &map = *_vm->_map; + assert(_monster2Attack != -1); + MazeMonster &monster = map._mobData._monsters[_monster2Attack]; + MonsterStruct &monsterData = *monster._monsterData; + int resistence = 0, damage = 0; + + if (rangeType != RT_SINGLE && rangeType != RT_3) { + switch (_damageType) { + case DT_PHYSICAL: + resistence = monsterData._phsyicalResistence; + break; + case DT_MAGICAL: + resistence = monsterData._magicResistence; + break; + case DT_FIRE: + resistence = monsterData._fireResistence; + break; + case DT_ELECTRICAL: + resistence = monsterData._electricityResistence; + break; + case DT_COLD: + resistence = monsterData._coldResistence; + break; + case DT_POISON: + resistence = monsterData._poisonResistence; + break; + case DT_ENERGY: + resistence = monsterData._energyResistence; + break; + default: + break; + } + } else { + int material = !_attackWeapon ? 0 : _attackWeapon->_material; + damage = ELEMENTAL_DAMAGE[material]; + + if (material != 0) { + if (material < 9) + resistence = monsterData._fireResistence; + else if (material < 16) + resistence = monsterData._electricityResistence; + else if (material < 21) + resistence = monsterData._coldResistence; + else if (material < 26) + resistence = monsterData._poisonResistence; + else if (material < 34) + resistence = monsterData._energyResistence; + else + resistence = monsterData._magicResistence; + } + } + + if (resistence != 0) { + if (resistence == 100) + return 0; + else + return ((100 - resistence) * damage) / 100; + } + + return damage; +} + +/** + * Distribute experience between active party members + */ +void Combat::giveExperience(int experience) { + Party &party = *_vm->_party; + bool inCombat = _vm->_mode == MODE_COMBAT; + int count = 0; + + // Two loops: first to figure out how many active characters there are, + // and the second to distribute the experience between them + for (int loopNum = 0; loopNum < 2; ++loopNum) { + for (uint charIndex = 0; charIndex < (inCombat ? _combatParty.size() : + party._activeParty.size()); ++charIndex) { + Character &c = inCombat ? *_combatParty[charIndex] : party._activeParty[charIndex]; + Condition condition = c.worstCondition(); + + if (condition != DEAD && condition != STONED && condition != ERADICATED) { + if (loopNum == 0) { + ++count; + } else { + int exp = experience / count; + if (c._level._permanent < 15) + exp /= 2; + c._experience += exp; + } + } + } + } +} + +void Combat::multiAttack(int powNum) { + Interface &intf = *_vm->_interface; + Map &map = *_vm->_map; + Party &party = *_vm->_party; + SoundManager &sound = *_vm->_sound; + + if (_damageType == DT_POISON_VOLLEY) { + _damageType = DT_POISON; + _shootType = ST_1; + Common::fill(&_shooting[0], &_shooting[6], 1); + } else if (powNum == 11) { + _shootType = ST_1; + bool flag = false; + + if (_damageType == DT_PHYSICAL) { + for (uint idx = 0; idx < party._activeParty.size(); ++idx) { + Character &c = party._activeParty[idx]; + if (c.hasMissileWeapon()) { + _shooting[idx] = 1; + flag = true; + } + } + } else { + _shooting[0] = 1; + flag = true; + } + + if (!flag) { + sound.playFX(21); + return; + } + } else { + _shooting[0] = 1; + _shootType = ST_0; + } + + intf._charsShooting = true; + _powSprites.load(Common::String::format("pow%d.icn", powNum)); + int monsterIndex = _monsterIndex; + int monster2Attack = _monster2Attack; + bool attackedFlag = false; + + Common::Array<int> attackMonsters; + for (int idx = 0; idx < 3; ++idx) { + if (_attackMonsters[idx] != -1) + attackMonsters.push_back(_attackMonsters[idx]); + } + + _monsterIndex = -1; + if (_monster2Attack != -1) { + _monsterIndex--; + if (attackMonsters.empty()) + attackMonsters.resize(1); + attackMonsters[0] = monster2Attack; + } + + for (uint idx = 0; idx < party._activeParty.size(); ++idx) { + if (_shooting[idx]) { + if (map._isOutdoors) { + intf._outdoorList._attackImgs1[idx]._scale = 0; + intf._outdoorList._attackImgs2[idx]._scale = 4; + intf._outdoorList._attackImgs3[idx]._scale = 8; + intf._outdoorList._attackImgs4[idx]._scale = 12; + intf._outdoorList._attackImgs1[idx]._sprites = &_powSprites; + intf._outdoorList._attackImgs2[idx]._sprites = nullptr; + intf._outdoorList._attackImgs3[idx]._sprites = nullptr; + intf._outdoorList._attackImgs4[idx]._sprites = nullptr; + } else { + intf._indoorList._attackImgs1[idx]._scale = 0; + intf._indoorList._attackImgs2[idx]._scale = 4; + intf._indoorList._attackImgs3[idx]._scale = 8; + intf._indoorList._attackImgs4[idx]._scale = 12; + intf._indoorList._attackImgs1[idx]._sprites = &_powSprites; + intf._indoorList._attackImgs2[idx]._sprites = nullptr; + intf._indoorList._attackImgs3[idx]._sprites = nullptr; + intf._indoorList._attackImgs4[idx]._sprites = nullptr; + } + } + } + + intf.draw3d(true); + + ++_monsterIndex; + for (uint monIdx = 0; monIdx < attackMonsters.size(); ++monIdx, ++_monsterIndex) { + Common::fill(&_missedShot[0], &_missedShot[8], false); + _monster2Attack = attackMonsters[monIdx]; + attack(*_oldCharacter, RT_GROUP); + attackedFlag = true; + + if (_rangeType == RT_SINGLE) + // Only single shot, so exit now that the attack is done + goto finished; + } + + if (attackedFlag && _rangeType == RT_GROUP) + // Finished group attack, so exit + goto finished; + + if (map._isOutdoors) { + map.getCell(7); + switch (map._currentWall) { + case 1: + case 3: + case 6: + case 7: + case 9: + case 10: + case 12: + sound.playFX(46); + goto finished; + default: + break; + } + } else { + int cell = map.getCell(2); + if (cell < map.mazeData()._difficulties._wallNoPass) { + sound.playFX(46); + goto finished; + } + } + if (!intf._isAttacking) + goto finished; + + intf.draw3d(true); + + // Start handling second teir of monsters in the back + attackMonsters.clear(); + for (uint idx = 3; idx < 6; ++idx) { + if (_attackMonsters[idx] != -1) + attackMonsters.push_back(_attackMonsters[idx]); + } + + ++_monsterIndex; + for (uint monIdx = 0; monIdx < attackMonsters.size(); ++monIdx, ++_monsterIndex) { + Common::fill(&_missedShot[0], &_missedShot[8], false); + _monster2Attack = attackMonsters[monIdx]; + attack(*_oldCharacter, RT_GROUP); + attackedFlag = true; + + if (_rangeType == RT_SINGLE) + // Only single shot, so exit now that the attack is done + goto finished; + } + + if (attackedFlag && _rangeType == RT_GROUP) + // Finished group attack, so exit + goto finished; + + if (map._isOutdoors) { + map.getCell(14); + switch (map._currentWall) { + case 1: + case 3: + case 6: + case 7: + case 9: + case 10: + case 12: + sound.playFX(46); + goto finished; + default: + break; + } + } else { + int cell = map.getCell(7); + if (cell < map.mazeData()._difficulties._wallNoPass) { + sound.playFX(46); + goto finished; + } + } + if (!intf._isAttacking) + goto finished; + + intf.draw3d(true); + + // Start handling third teir of monsters in the back + attackMonsters.clear(); + for (uint idx = 6; idx < 9; ++idx) { + if (_attackMonsters[idx] != -1) + attackMonsters.push_back(_attackMonsters[idx]); + } + + ++_monsterIndex; + for (uint monIdx = 0; monIdx < attackMonsters.size(); ++monIdx, ++_monsterIndex) { + Common::fill(&_missedShot[0], &_missedShot[8], false); + _monster2Attack = attackMonsters[monIdx]; + attack(*_oldCharacter, RT_GROUP); + attackedFlag = true; + + if (_rangeType == RT_SINGLE) + // Only single shot, so exit now that the attack is done + goto finished; + } + + if (attackedFlag && _rangeType == RT_GROUP) + // Finished group attack, so exit + goto finished; + + if (map._isOutdoors) { + map.getCell(27); + switch (map._currentWall) { + case 1: + case 3: + case 6: + case 7: + case 9: + case 10: + case 12: + sound.playFX(46); + goto finished; + default: + break; + } + } else { + int cell = map.getCell(14); + if (cell < map.mazeData()._difficulties._wallNoPass) { + sound.playFX(46); + goto finished; + } + } + if (!intf._isAttacking) + goto finished; + + intf.draw3d(true); + + // Fourth tier + attackMonsters.clear(); + for (uint idx = 9; idx < 12; ++idx) { + if (_attackMonsters[idx] != -1) + attackMonsters.push_back(_attackMonsters[idx]); + } + + ++_monsterIndex; + for (uint monIdx = 0; monIdx < attackMonsters.size(); ++monIdx, ++_monsterIndex) { + Common::fill(&_missedShot[0], &_missedShot[8], false); + _monster2Attack = attackMonsters[monIdx]; + attack(*_oldCharacter, RT_GROUP); + attackedFlag = true; + + if (_rangeType == RT_SINGLE) + // Only single shot, so exit now that the attack is done + goto finished; + } + + if (!(attackedFlag && _rangeType == RT_GROUP)) + goto done; + +finished: + endAttack(); +done: + Common::fill(&_shooting[0], &_shooting[MAX_PARTY_COUNT], 0); + _monster2Attack = monster2Attack; + _monsterIndex = monsterIndex; + party.giveTreasure(); +} + +/** + * Fires off a ranged attack at all oncoming monsters + */ +void Combat::shootRangedWeapon() { + _rangeType = RT_ALL; + _damageType = DT_PHYSICAL; + multiAttack(11); +} + +} // End of namespace Xeen |