scummvm/engines/xeen/combat.cpp
Paul Gilbert 4515d7a1ac XEEN: Create Resources class to encapsulate all the static resources
This will make it easier later on to handle things like translations,
and if the other games have different values for some arrays
2016-09-22 20:00:04 -04:00

2064 lines
54 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 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;
Sound &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(Res.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;
}
}
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;
Sound &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(Res.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;
default:
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, Res.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, Res.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, Res.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, Res.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]));
}
default:
break;
}
}
}
}
}
}
}
monsterOvercome();
if (_monstersAttacking)
monstersAttack();
}
void Combat::monstersAttack() {
EventsManager &events = *_vm->_events;
Interface &intf = *_vm->_interface;
Map &map = *_vm->_map;
Party &party = *_vm->_party;
Sound &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];
}
}
}
}
}
}
}
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;
}
}
}
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;
Sound &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);
sound.playSound(Common::String::format("%s.voc", monsterData._attackVoc.c_str()));
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;
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;
}
}
}
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;
}
}
}
}
}
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;
}
bool Combat::charsCantAct() const {
for (uint idx = 0; idx < _combatParty.size(); ++idx) {
if (!_combatParty[idx]->isDisabledOrDead())
return false;
}
return true;
}
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(Res.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 &ch = party._activeParty[charIndex];
if (_shooting[charIndex] && !_missedShot[charIndex]) {
if (!hitMonster(ch, 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;
default:
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;
Sound &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.playSound(_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.playSound(_missVoc, 1);
sound.playFX(6);
} else {
_monsterScale[_monsterIndex] = getDamageScale(damage);
intf.draw3d(true);
sound.stopSound();
File powVoc(Common::String::format("pow%d.voc",
POW_WEAPON_VOCS[_attackWeaponId]));
sound.playFX(60 + POW_WEAPON_VOCS[_attackWeaponId]);
sound.playSound(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;
}
}
}
void Combat::block() {
_charsBlocked[_whosTurn] = true;
}
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)Res.SPELLS_ALLOWED[c->getClassCategory()][c->_currentSpell]);
}
break;
case QUICK_BLOCK:
block();
break;
case QUICK_RUN:
run();
break;
default:
break;
}
}
void Combat::run() {
Map &map = *_vm->_map;
Sound &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;
default:
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 = Res.METAL_DAMAGE_PERCENT[c._weapons[idx]._material - 37];
_weaponDamage = Res.METAL_DAMAGE[c._weapons[idx]._material - 37];
}
}
_hitChanceBonus += party._heroism;
_attackWeaponId = c._weapons[idx]._id;
_weaponDice = Res.WEAPON_DAMAGE_BASE[_attackWeaponId];
_weaponDie = Res.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 = Res.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;
}
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;
Sound &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();
}
void Combat::shootRangedWeapon() {
_rangeType = RT_ALL;
_damageType = DT_PHYSICAL;
multiAttack(11);
}
} // End of namespace Xeen