scummvm/engines/xeen/combat.cpp
2018-03-10 07:14:43 -05:00

2102 lines
55 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 PowType MONSTER_SHOOT_POW[7] = {
POW_MAGIC_ARROW, POW_SPARKLES, POW_FIREBALL,
POW_MEGAVOLTS, POW_COLD_RAY, POW_SPRAY, POW_ENERGY_BLAST
};
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") {
Common::fill(&_attackMonsters[0], &_attackMonsters[26], 0);
Common::fill(&_shootingRow[0], &_shootingRow[MAX_PARTY_COUNT], 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;
_attackDurationCtr = 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;
_combatTarget = 0;
}
void Combat::clearAttackers() {
Common::fill(&_attackMonsters[0], &_attackMonsters[ATTACK_MONSTERS_COUNT], -1);
}
void Combat::clearBlocked() {
Common::fill(_charsBlocked, _charsBlocked + PARTY_AND_MONSTERS, false);
}
void Combat::clearShooting() {
Common::fill(_shootingRow, _shootingRow + MAX_PARTY_COUNT, 0);
}
void Combat::giveCharDamage(int damage, DamageType attackType, int charIndex) {
EventsManager &events = *_vm->_events;
Interface &intf = *_vm->_interface;
Party &party = *_vm->_party;
Sound &sound = *_vm->_sound;
Windows &windows = *_vm->_windows;
int charIndex1 = charIndex + 1;
int selectedIndex1 = 0;
int selectedIndex2 = 0;
bool breakFlag = false;
windows.closeAll();
int idx = (int)party._activeParty.size();
if (_combatTarget == 2) {
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;
--selectedIndex1;
break;
}
}
}
}
if (idx == (int)party._activeParty.size()) {
if (!_combatTarget)
selectedIndex1 = 0;
}
for (;;) {
for (; selectedIndex1 < (_combatTarget ? charIndex1 : (int)party._activeParty.size()); ++selectedIndex1) {
Character &c = party._activeParty[selectedIndex1];
c._conditions[ASLEEP] = 0; // Force attacked 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);
intf._charPowSprites.draw(0, frame, Common::Point(Res.CHAR_FACES_X[selectedIndex1], 150));
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;
// Attacked characters which are asleep are killed
if (attackType == DT_SLEEP) {
damage = c._currentHp;
c._conditions[DEAD] = 1;
}
// Subtract the hit points from the character
c.subtractHitPoints(damage);
if (selectedIndex2)
break;
}
// Break check and if not, move to other index
if (!selectedIndex2 || breakFlag)
break;
selectedIndex1 = selectedIndex2 - 1;
breakFlag = true;
}
events.ipause(5);
intf.drawParty(true);
party.checkPartyDead();
}
void Combat::doCharDamage(Character &c, int charNum, int monsterDataIndex) {
Debugger &debugger = *g_vm->_debugger;
EventsManager &events = *_vm->_events;
Interface &intf = *_vm->_interface;
Map &map = *_vm->_map;
Party &party = *_vm->_party;
Sound &sound = *_vm->_sound;
Windows &windows = *_vm->_windows;
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 != DT_PHYSICAL) {
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(0, frame, Common::Point(Res.CHAR_FACES_X[charNum], 150));
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;
}
if (debugger._invincible)
// Invincibility mode is on, so reset conditions that were set
c.clearConditions();
else
// Standard gameplay, deal out the damage
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 ((uint)monster._position.y < 32) {
assert((uint)monster._position.x < 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 == DT_PHYSICAL) {
// 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;
PowType powNum = POW_INVALID;
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 != POW_MAGIC_ARROW)
break;
}
}
_powSprites.load(Common::String::format("pow%d.icn", (int)powNum));
sound.playFX(ATTACK_TYPE_FX[monsterData->_attackType]);
for (int charNum = 0; charNum < MAX_PARTY_COUNT; ++charNum) {
if (!_shootingRow[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 (_shootingRow[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 (_shootingRow[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->shouldExit() && 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 (!_shootingRow[charNum]) {
_shootingRow[charNum] = COMBAT_SHOOTING[result - 1];
break;
}
}
}
}
break;
}
}
}
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 == DT_PHYSICAL && _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;
}
}
clearShooting();
}
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 = DT_PHYSICAL;
}
}
}
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;
}
assert(monsterIndex != -1);
MazeMonster &monster = map._mobData._monsters[monsterIndex];
MonsterStruct &monsterData = *monster._monsterData;
if (monster._damageType != DT_PHYSICAL)
return;
monster._frame = 8;
monster._postAttackDelay = 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) {
// Critical Save
sound.playFX(6);
} else {
if (v == 20)
// Critical failure
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;
int oldSpeed = hasSpeed && _whosSpeed < (int)_speedTable.size() ? _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 (_whosSpeed = 0; _whosSpeed < (int)charSpeeds.size(); ++_whosSpeed) {
if (oldSpeed == _speedTable[_whosSpeed])
break;
}
if (_whosSpeed == (int)charSpeeds.size())
error("Could not reset next speedy character. Beep beep.");
}
}
}
bool Combat::allHaveGone() const {
int monsCount = (_attackMonsters[0] != -1 ? 1 : 0)
+ (_attackMonsters[1] != -1 ? 1 : 0)
+ (_attackMonsters[2] != -1 ? 1 : 0);
for (uint idx = 0; idx < (_combatParty.size() + monsCount); ++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 (_attackDurationCtr == 2 && _attackMonsters[2] != -1) {
_monster2Attack = _attackMonsters[2];
} if (_attackDurationCtr == 1 && _attackMonsters[1] != -1) {
_monster2Attack = _attackMonsters[1];
} else {
_monster2Attack = _attackMonsters[0];
_attackDurationCtr = 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 != RT_SINGLE) {
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);
} 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);
}
break;
case DT_HOLYWORD:
if (monsterData._monsterType == MONSTER_UNDEAD) {
attack2(monster._hp, RT_ALL);
}
break;
case DT_MASS_DISTORTION:
attack2(MAX(monster._hp / 2, 1), RT_ALL);
break;
case DT_UNDEAD:
if (monsterData._monsterType == MONSTER_UNDEAD)
damage = 25;
else
rangeType = RT_ALL;
attack2(damage, rangeType);
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);
}
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);
}
break;
case DT_MAGIC_ARROW:
attack2(8, rangeType);
break;
default:
break;
}
}
}
} else {
_pow.resetElementals();
damage = 0;
for (uint charIndex = 0; charIndex < party._activeParty.size(); ++charIndex) {
Character &ch = party._activeParty[charIndex];
if (_shootingRow[charIndex] && !_missedShot[charIndex]) {
if (!hitMonster(ch, rangeType)) {
++_missedShot[charIndex];
} else {
damage = _monsterDamage ? _monsterDamage : _weaponDamage;
_shootingRow[charIndex] = 0;
attack2(damage, RT_HIT);
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:
error("Invalid class");
}
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 != RT_SINGLE && 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 == RT_SINGLE || _damageType == DT_PHYSICAL) && _attackWeaponId != 34) {
if (monsterData._phsyicalResistence != 0) {
if (monsterData._phsyicalResistence == 100) {
// Completely immune to the damage
damage = 0;
} else {
// Reduce the damage based on physical resistance
damage = damage * (100 - monsterData._phsyicalResistence) / 100;
}
}
}
if (damage) {
_pow[_attackDurationCtr]._duration = 3;
_pow[_attackDurationCtr]._active = _damageType == DT_PHYSICAL && (rangeType == RT_HIT || rangeType == RT_SINGLE);
monster._frame = 11;
monster._postAttackDelay = 5;
}
int monsterResist = getMonsterResistence(rangeType);
damage += monsterResist;
if (monsterResist > 0) {
_pow[_attackDurationCtr]._elemFrame = _attackWeapon->getElementalCategory();
_pow[_attackDurationCtr]._elemScale = getDamageScale(monsterResist);
} else if (rangeType != RT_HIT) {
_pow[_attackDurationCtr]._elemFrame = 0;
}
if (rangeType != RT_SINGLE && rangeType != RT_HIT) {
monster._effect2 = DAMAGE_TYPE_EFFECTS[_damageType];
monster._effect1 = 0;
}
if (rangeType != RT_SINGLE && 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 {
_pow[_attackDurationCtr]._scale = 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[0][11] = true;
if (_monster2Attack == 8 && party._mazeId == 78) {
party._gameFlags[0][60] = true;
party._questFlags[0][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[0][104] = true;
}
}
giveExperience(monsterData._experience);
if (party._mazeId != 85) {
party._treasure._gold = monsterData._gold;
party._treasure._gems = monsterData._gems;
if (!isDarkCc && monster._spriteId == 89) {
// Xeen's Scepter of Temporal Distortion
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 = true;
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 = true;
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 = true;
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 = true;
break;
}
}
break;
}
}
}
}
monster._position = Common::Point(0x80, 0x80);
_pow[_attackDurationCtr]._duration = 0;
_monster2Attack = -1;
intf.draw3d(true);
if (_attackMonsters[0] != -1) {
_monster2Attack = _attackMonsters[0];
_attackDurationCtr = 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._armorClass + 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) {
XeenItem &weapon = c._weapons[idx];
bool flag;
if (rangeType != RT_SINGLE) {
flag = weapon._frame == 4;
} else {
flag = weapon._frame == 1 || weapon._frame == 13;
}
if (flag) {
if (!(weapon._bonusFlags & (ITEMFLAG_BROKEN | ITEMFLAG_CURSED))) {
_attackWeapon = &weapon;
if (weapon._material >= 37 && weapon._material < 59) {
_hitChanceBonus = Res.METAL_DAMAGE_PERCENT[weapon._material - 37];
_weaponDamage = Res.METAL_DAMAGE[weapon._material - 37];
}
}
_hitChanceBonus += party._heroism;
_attackWeaponId = weapon._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 == ADVENTURER) {
_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_HIT) {
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 && _vm->getGameID() != GType_Clouds)
exp *= 2;
c._experience += exp;
}
}
}
}
}
void Combat::rangedAttack(PowType 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(&_shootingRow[0], &_shootingRow[MAX_ACTIVE_PARTY], 1);
} else if (powNum == POW_ARROW) {
_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()) {
_shootingRow[idx] = 1;
flag = true;
}
}
} else {
_shootingRow[0] = 1;
flag = true;
}
if (!flag) {
sound.playFX(21);
return;
}
sound.playFX(49);
} else {
_shootingRow[0] = 1;
_shootType = ST_0;
}
intf._charsShooting = true;
_powSprites.load(Common::String::format("pow%d.icn", (int)powNum));
int attackDurationCtr = _attackDurationCtr;
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]);
}
_attackDurationCtr = -1;
if (_monster2Attack != -1) {
_attackDurationCtr--;
if (attackMonsters.empty())
attackMonsters.resize(1);
attackMonsters[0] = monster2Attack;
}
for (uint idx = 0; idx < party._activeParty.size(); ++idx) {
if (_shootingRow[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);
// Iterate through the three possible monster positions in the first row
for (uint monIdx = 0; monIdx < 3; ++monIdx) {
++_attackDurationCtr;
if (monIdx < attackMonsters.size()) {
Common::fill(&_missedShot[0], &_missedShot[MAX_PARTY_COUNT], 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]);
}
// Iterate through the three possible monster positions in the second row
for (uint monIdx = 0; monIdx < 3; ++monIdx) {
++_attackDurationCtr;
if (monIdx < attackMonsters.size()) {
Common::fill(&_missedShot[0], &_missedShot[MAX_PARTY_COUNT], 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]);
}
// Iterate through the three possible monster positions in the third row
for (uint monIdx = 0; monIdx < 3; ++monIdx) {
++_attackDurationCtr;
if (monIdx < attackMonsters.size()) {
Common::fill(&_missedShot[0], &_missedShot[MAX_PARTY_COUNT], 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]);
}
// Iterate through the three possible monster positions in the fourth row
for (uint monIdx = 0; monIdx < 3; ++monIdx) {
++_attackDurationCtr;
if (monIdx < attackMonsters.size()) {
Common::fill(&_missedShot[0], &_missedShot[MAX_PARTY_COUNT], 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:
clearShooting();
_monster2Attack = monster2Attack;
_attackDurationCtr = attackDurationCtr;
party.giveTreasure();
}
void Combat::shootRangedWeapon() {
_rangeType = RT_ALL;
_damageType = DT_PHYSICAL;
rangedAttack(POW_ARROW);
}
} // End of namespace Xeen