mirror of
https://github.com/scummvm/scummvm.git
synced 2025-04-02 10:52:32 -04:00
1678 lines
42 KiB
C++
1678 lines
42 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 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "mm/mm1/game/combat.h"
|
|
#include "mm/mm1/globals.h"
|
|
#include "mm/mm1/mm1.h"
|
|
#include "mm/mm1/sound.h"
|
|
|
|
namespace MM {
|
|
namespace MM1 {
|
|
namespace Game {
|
|
|
|
#define STARTING_HANDICAP 40
|
|
|
|
Combat::Combat() : MonsterTouch() {
|
|
g_globals->_combat = this;
|
|
clear();
|
|
}
|
|
|
|
Combat::~Combat() {
|
|
g_globals->_combat = nullptr;
|
|
}
|
|
|
|
void Combat::clear() {
|
|
// Reset party character combat flags
|
|
for (uint i = 0; i < g_globals->_party.size(); ++i) {
|
|
Character &c = g_globals->_party[i];
|
|
c._checked = false;
|
|
c._canAttack = false;
|
|
}
|
|
|
|
// Reset other fields
|
|
Common::fill(&_treasureFlags[0], &_treasureFlags[MAX_PARTY_SIZE], false);
|
|
|
|
_allowFight = _allowShoot = _allowCast = _allowAttack = false;
|
|
_val1 = 0;
|
|
_val6 = _val7 = 0;
|
|
_partyIndex = _val9 = 0;
|
|
_monsterShootingCtr = _destCharCtr = 0;
|
|
_activeMonsterNum = 0;
|
|
_destAC = 0;
|
|
_numberOfTimes = 0;
|
|
_attackerLevel = 0;
|
|
_advanceIndex = 0;
|
|
_handicapThreshold = _handicapParty = 0;
|
|
_handicapMonsters = _handicapDelta = 0;
|
|
_handicap = HANDICAP_EVEN;
|
|
_monsterP = nullptr;
|
|
_monsterIndex = _currentChar = 0;
|
|
_attackersCount = 0;
|
|
_totalExperience = 0;
|
|
_advanceIndex = 0;
|
|
_monstersResistSpells = _monstersRegenerate = false;
|
|
_attackAttr1.clear();
|
|
_attackAttr2.clear();
|
|
_timesHit = 0;
|
|
_isShooting = false;
|
|
|
|
_turnUndeadUsed = false;
|
|
_divineInterventionUsed = false;
|
|
_monstersDestroyedCtr = 0;
|
|
|
|
// TODO: clear everything
|
|
|
|
_roundNum = 1;
|
|
_monstersCount = _remainingMonsters.size();
|
|
g_globals->_combatParty.clear();
|
|
for (uint i = 0; i < g_globals->_party.size(); ++i)
|
|
g_globals->_combatParty.push_back(&g_globals->_party[i]);
|
|
}
|
|
|
|
void Combat::loadMonsters() {
|
|
Game::Encounter &enc = g_globals->_encounters;
|
|
|
|
// Set up hp and ac for the encounter monsters
|
|
for (uint i = 0; i < enc._monsterList.size(); ++i) {
|
|
Monster &mon = enc._monsterList[i];
|
|
_monsterP = &mon;
|
|
int val = getRandomNumber(8);
|
|
|
|
mon._defaultHP += val;
|
|
mon._hp = mon._defaultHP;
|
|
mon._ac = mon._defaultAC;
|
|
|
|
monsterIndexOf();
|
|
}
|
|
|
|
// Now copy it into the active combat. This is kept as a
|
|
// separate list since defeated monsters are removed from it
|
|
_remainingMonsters.clear();
|
|
for (uint i = 0; i < enc._monsterList.size(); ++i)
|
|
_remainingMonsters.push_back(&enc._monsterList[i]);
|
|
}
|
|
|
|
void Combat::monsterIndexOf() {
|
|
Game::Encounter &enc = g_globals->_encounters;
|
|
|
|
_monsterIndex = MAX_COMBAT_MONSTERS;
|
|
for (uint i = 0; i < enc._monsterList.size(); ++i) {
|
|
if (_monsterP == &enc._monsterList[i]) {
|
|
_monsterIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Combat::monsterSetPtr(int monsterNum) {
|
|
_monsterP = _remainingMonsters[monsterNum];
|
|
monsterIndexOf();
|
|
}
|
|
|
|
void Combat::setupCanAttacks() {
|
|
const Encounter &enc = g_globals->_encounters;
|
|
const Maps::Map &map = *g_maps->_currentMap;
|
|
|
|
for (uint i = 0; i < g_globals->_combatParty.size(); ++i)
|
|
g_globals->_combatParty[i]->_canAttack = false;
|
|
|
|
if ((int8)map[Maps::MAP_ID] < 0) {
|
|
if (enc._encounterType != FORCE_SURPRISED) {
|
|
for (uint i = 0; i < g_globals->_combatParty.size(); ++i) {
|
|
if (i < (MAX_PARTY_SIZE - 1)) {
|
|
g_globals->_combatParty[i]->_canAttack = true;
|
|
} else {
|
|
g_globals->_combatParty[MAX_PARTY_SIZE - 1]->_canAttack =
|
|
getRandomNumber(100) <= 10;
|
|
}
|
|
}
|
|
|
|
setupAttackersCount();
|
|
return;
|
|
}
|
|
} else {
|
|
if (enc._encounterType != FORCE_SURPRISED) {
|
|
g_globals->_combatParty[0]->_canAttack = true;
|
|
if (g_globals->_combatParty.size() > 1)
|
|
g_globals->_combatParty[1]->_canAttack = true;
|
|
if (g_globals->_combatParty.size() > 2)
|
|
checkLeftWall();
|
|
if (g_globals->_combatParty.size() > 3)
|
|
checkRightWall();
|
|
if (g_globals->_combatParty.size() > 4) {
|
|
if (g_globals->_combatParty[2]->_canAttack && getRandomNumber(100) <= 5)
|
|
g_globals->_combatParty[4]->_canAttack = true;
|
|
}
|
|
if (g_globals->_combatParty.size() > 5) {
|
|
if (g_globals->_combatParty[3]->_canAttack && getRandomNumber(100) <= 5)
|
|
g_globals->_combatParty[5]->_canAttack = true;
|
|
}
|
|
|
|
setupAttackersCount();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Entire party is allowed to attack, I guess
|
|
// because the monsters are surrounding the party,
|
|
// placing them within reach
|
|
for (uint i = 0; i < g_globals->_combatParty.size(); ++i)
|
|
g_globals->_combatParty[i]->_canAttack = true;
|
|
|
|
setupAttackersCount();
|
|
}
|
|
|
|
void Combat::setupAttackersCount() {
|
|
_attackersCount = 0;
|
|
for (uint i = 0; i < g_globals->_combatParty.size(); ++i) {
|
|
if (g_globals->_combatParty[i]->_canAttack)
|
|
++_attackersCount;
|
|
}
|
|
|
|
_attackersCount += getRandomNumber(_attackersCount + 1) - 1;
|
|
}
|
|
|
|
void Combat::checkLeftWall() {
|
|
Maps::Maps &maps = *g_maps;
|
|
|
|
g_globals->_combatParty[2]->_canAttack = !(maps._currentWalls & maps._leftMask) ||
|
|
getRandomNumber(100) <= 25;
|
|
}
|
|
|
|
void Combat::checkRightWall() {
|
|
Maps::Maps &maps = *g_maps;
|
|
|
|
g_globals->_combatParty[3]->_canAttack = !(maps._currentWalls & maps._rightMask) ||
|
|
getRandomNumber(100) <= 25;
|
|
}
|
|
|
|
void Combat::setupHandicap() {
|
|
_handicapParty = _handicapMonsters = STARTING_HANDICAP;
|
|
_handicapThreshold = getRandomNumber(7);
|
|
|
|
int val = getRandomNumber(7);
|
|
if (val < _handicapThreshold) {
|
|
SWAP(val, _handicapThreshold);
|
|
_handicapDelta = val - _handicapThreshold;
|
|
|
|
if (_handicapDelta) {
|
|
_handicap = HANDICAP_MONSTER;
|
|
_handicapParty += _handicapDelta;
|
|
return;
|
|
}
|
|
} else if (val > _handicapThreshold) {
|
|
_handicapDelta = val - _handicapThreshold;
|
|
|
|
if (_handicapDelta) {
|
|
_handicap = HANDICAP_PARTY;
|
|
_handicapMonsters += _handicapDelta;
|
|
return;
|
|
}
|
|
}
|
|
|
|
_handicap = HANDICAP_EVEN;
|
|
_handicapDelta = 0;
|
|
}
|
|
|
|
bool Combat::canMonsterCast() const {
|
|
return !(g_maps->_currentState & Maps::SFLAG_SPELLS_DISALLOWED) &&
|
|
!(g_globals->_encounters._monsterList[_monsterIndex]._status
|
|
& (MONFLAG_BLIND | MONFLAG_SILENCED));
|
|
}
|
|
|
|
void Combat::dispelParty() {
|
|
g_globals->_activeSpells.clear();
|
|
Character *tmpC = g_globals->_currCharacter;
|
|
Monster *tmpM = _monsterP;
|
|
|
|
for (uint i = 0; i < g_globals->_combatParty.size(); ++i) {
|
|
Character &c = *g_globals->_combatParty[i];
|
|
g_globals->_currCharacter = &c;
|
|
c.updateAttributes();
|
|
c.updateAC();
|
|
c.updateResistances();
|
|
|
|
if (!(c._condition & BAD_CONDITION))
|
|
c._condition &= ~(BLINDED | SILENCED);
|
|
}
|
|
|
|
g_globals->_currCharacter = tmpC;
|
|
|
|
for (uint i = 0; i < _remainingMonsters.size(); ++i) {
|
|
monsterSetPtr(i);
|
|
_remainingMonsters[i]->_status = 0;
|
|
}
|
|
|
|
_monsterP = tmpM;
|
|
}
|
|
|
|
void Combat::combatLoop(bool checkMonstersFirst) {
|
|
for (;;) {
|
|
// Check if the monsters have all been defeated
|
|
if (_remainingMonsters.empty()) {
|
|
defeatedMonsters();
|
|
return;
|
|
}
|
|
|
|
// Check for initial loop where monsters can be checked first
|
|
if (!checkMonstersFirst) {
|
|
for (uint i = 0; i < g_globals->_combatParty.size(); ++i) {
|
|
Character &c = *g_globals->_combatParty[i];
|
|
int speed = c._speed._current;
|
|
|
|
if (speed && speed >= _handicapParty && !c._checked) {
|
|
_currentChar = i;
|
|
g_globals->_currCharacter = &c;
|
|
|
|
if (!(c._condition & (BAD_CONDITION | UNCONSCIOUS |
|
|
PARALYZED | ASLEEP))) {
|
|
// Character is enabled
|
|
setMode(SELECT_OPTION);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
checkMonstersFirst = false;
|
|
for (uint i = 0; i < _remainingMonsters.size(); ++i) {
|
|
_monsterP = _remainingMonsters[i];
|
|
monsterIndexOf();
|
|
|
|
if (_monsterP->_speed && _monsterP->_speed >= _handicapMonsters && !_monsterP->_checked) {
|
|
_monsterP->_checked = true;
|
|
|
|
if (!(_monsterP->_status & (MONFLAG_ASLEEP | MONFLAG_HELD |
|
|
MONFLAG_WEBBED | MONFLAG_PARALYZED))) {
|
|
monsterAction();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Decrease the handicap/speed threshold
|
|
if (_handicapParty == 1 && _handicapMonsters == 1) {
|
|
// End of the round
|
|
nextRound();
|
|
break;
|
|
} else {
|
|
if (_handicapParty != 1)
|
|
--_handicapParty;
|
|
if (_handicapMonsters != 1)
|
|
--_handicapMonsters;
|
|
}
|
|
assert(_handicapParty >= 1 && _handicapMonsters >= 1);
|
|
}
|
|
}
|
|
|
|
void Combat::defeatedMonsters() {
|
|
int activeCharCount = 0;
|
|
_totalExperience = 0;
|
|
|
|
// Count total experience from all defeated monsters
|
|
Common::Array<Monster> &monsters = g_globals->_encounters._monsterList;
|
|
for (uint i = 0; i < monsters.size(); ++i) {
|
|
_monsterP = &monsters[i];
|
|
_totalExperience += _monsterP->_experience;
|
|
setTreasure();
|
|
}
|
|
|
|
// Count number of active characters
|
|
for (uint i = 0; i < g_globals->_combatParty.size(); ++i) {
|
|
if (!(g_globals->_combatParty[i]->_condition & BAD_CONDITION))
|
|
++activeCharCount;
|
|
}
|
|
|
|
// Split the experience between the active characters
|
|
_totalExperience /= activeCharCount;
|
|
for (uint i = 0; i < g_globals->_combatParty.size(); ++i) {
|
|
if (!(g_globals->_combatParty[i]->_condition & BAD_CONDITION))
|
|
g_globals->_combatParty[i]->_exp += _totalExperience;
|
|
}
|
|
|
|
setMode(DEFEATED_MONSTERS);
|
|
}
|
|
|
|
void Combat::setTreasure() {
|
|
_val7 = _monsterP->_loot;
|
|
_val6 = MAX(_val6, _val7);
|
|
|
|
if (_val7 & 1)
|
|
// Add gems
|
|
g_globals->_treasure[8] += getRandomNumber(6);
|
|
|
|
if (_val7 & 6) {
|
|
if (!(_val7 & 2)) {
|
|
g_globals->_treasure.setGold(g_globals->_treasure.getGold() +
|
|
getRandomNumber(10));
|
|
} else if (!(_val7 & 4)) {
|
|
g_globals->_treasure.setGold(g_globals->_treasure.getGold() +
|
|
getRandomNumber(100));
|
|
} else {
|
|
g_globals->_treasure.setGold(g_globals->_treasure.getGold() +
|
|
getRandomNumber(4) * 256);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Combat::selectTreasure() {
|
|
#define SHIFT_BIT (_val6 & 0x80) != 0; _val6 <<= 1
|
|
for (int i = 0; i < 5; ++i) {
|
|
bool flag = SHIFT_BIT;
|
|
if (flag && getRandomNumber(100) <= (10 + i * 10))
|
|
selectTreasure2(5 - i);
|
|
}
|
|
#undef SHIFT_BIT
|
|
|
|
auto &treasure = g_globals->_treasure;
|
|
treasure._trapType = 0;
|
|
|
|
if (_treasureFlags[4]) {
|
|
treasure._container = 10;
|
|
} else if (_treasureFlags[3]) {
|
|
treasure._container = 5 + getRandomNumber(4);
|
|
} else if (_treasureFlags[2]) {
|
|
treasure._container = 4 + getRandomNumber(4);
|
|
} else if (_treasureFlags[1]) {
|
|
treasure._container = 3 + getRandomNumber(4);
|
|
} else if (_treasureFlags[0] || treasure[8]) {
|
|
treasure._container = 1 + getRandomNumber(4);
|
|
} else if (treasure[7]) {
|
|
treasure._container = getRandomNumber(4) - 1;
|
|
} else {
|
|
treasure._container = getRandomNumber(2) - 1;
|
|
}
|
|
}
|
|
|
|
void Combat::selectTreasure2(int count) {
|
|
_treasureFlags[count - 1] = true;
|
|
|
|
// Select item base and step offset
|
|
int idx = getRandomNumber(0, 5);
|
|
|
|
static const byte TREASURES_ARR1[6] = { 1, 61, 86, 121, 156, 171 };
|
|
static const byte TREASURES_ARR2[6] = { 12, 5, 7, 7, 3, 12 };
|
|
byte itemId = TREASURES_ARR1[idx];
|
|
byte rndCount = TREASURES_ARR2[idx];
|
|
|
|
for (idx = 0; idx < count; ++idx)
|
|
itemId += rndCount;
|
|
itemId += getRandomNumber(rndCount) - 1;
|
|
|
|
auto &treasure = g_globals->_treasure;
|
|
if (!treasure._items[0])
|
|
treasure._items[0] = itemId;
|
|
else if (!treasure._items[1])
|
|
treasure._items[1] = itemId;
|
|
else if (!treasure._items[2])
|
|
treasure._items[2] = itemId;
|
|
}
|
|
|
|
void Combat::nextRound() {
|
|
++_roundNum;
|
|
setupHandicap();
|
|
clearArrays();
|
|
g_globals->_party.updateAC();
|
|
|
|
_partyIndex = getRandomNumber(g_globals->_combatParty.size());
|
|
updateHighestLevel();
|
|
|
|
setMode(NEXT_ROUND);
|
|
}
|
|
|
|
void Combat::nextRound2() {
|
|
if (moveMonsters()) {
|
|
// Display that a monster advanced in the view
|
|
setMode(MONSTER_ADVANCES);
|
|
} else {
|
|
// No advancements, so move to next part of new round
|
|
nextRound3();
|
|
}
|
|
}
|
|
|
|
void Combat::nextRound3() {
|
|
if (monsterChanges()) {
|
|
setMode(MONSTERS_AFFECTED);
|
|
} else {
|
|
combatLoop();
|
|
}
|
|
}
|
|
|
|
void Combat::clearArrays() {
|
|
for (uint i = 0; i < g_globals->_combatParty.size(); ++i)
|
|
g_globals->_combatParty[i]->_checked = false;
|
|
|
|
for (uint i = 0; i < _remainingMonsters.size(); ++i)
|
|
_remainingMonsters[i]->_checked = false;
|
|
}
|
|
|
|
void Combat::updateHighestLevel() {
|
|
Encounter &enc = g_globals->_encounters;
|
|
|
|
for (uint i = 0; i < g_globals->_combatParty.size(); ++i) {
|
|
Character &c = g_globals->_party[i];
|
|
g_globals->_currCharacter = &c;
|
|
|
|
// This looks suspect, since it doesn't find the
|
|
// max of the party's active characters, just ends
|
|
// up set to whichever last character's level
|
|
if (!(c._condition & (ASLEEP | BLINDED | SILENCED | DISEASED | POISONED)))
|
|
enc._highestLevel = c._level._base;
|
|
}
|
|
}
|
|
|
|
bool Combat::moveMonsters() {
|
|
if (_attackersCount >= (int)_remainingMonsters.size())
|
|
return false;
|
|
|
|
bool hasAdvance = false;
|
|
for (uint i = 0; i < _remainingMonsters.size(); ++i) {
|
|
_advanceIndex = i;
|
|
|
|
if (!(_remainingMonsters[i]->_status & ~MONFLAG_SILENCED) &&
|
|
_remainingMonsters[i]->_counterFlags & COUNTER_ADVANCES) {
|
|
monsterAdvances();
|
|
hasAdvance = true;
|
|
}
|
|
}
|
|
|
|
return hasAdvance;
|
|
}
|
|
|
|
void Combat::monsterAdvances() {
|
|
// TODO: I can't understand the advancement logic at all.
|
|
// So for now, I'm simply moving the monster forward one slot
|
|
assert(_advanceIndex > 0);
|
|
Monster *mon = _remainingMonsters.remove_at(_advanceIndex);
|
|
_remainingMonsters.insert_at(_advanceIndex - 1, mon);
|
|
_monsterP = _remainingMonsters[_advanceIndex - 1];
|
|
|
|
setMode(MONSTER_ADVANCES);
|
|
}
|
|
|
|
bool Combat::monsterChanges() {
|
|
_monstersRegenerate = _monstersResistSpells = false;
|
|
|
|
for (uint i = 0; i < _remainingMonsters.size(); ++i) {
|
|
monsterSetPtr(i);
|
|
|
|
if ((_monsterP->_counterFlags & COUNTER_REGENERATE) &&
|
|
(_monsterP->_defaultHP != _remainingMonsters[i]->_hp)) {
|
|
_monstersRegenerate = true;
|
|
int newVal = _remainingMonsters[i]->_hp + 5;
|
|
_remainingMonsters[i]->_hp = (byte)newVal;
|
|
|
|
if (newVal >= 256 || newVal >= _monsterP->_defaultHP) {
|
|
_remainingMonsters[i]->_hp = _monsterP->_defaultHP;
|
|
}
|
|
}
|
|
|
|
if (_remainingMonsters[i]->_status) {
|
|
proc2();
|
|
|
|
if (_val9) {
|
|
_monstersResistSpells = true;
|
|
byte v = _remainingMonsters[i]->_status;
|
|
|
|
v &= 0x7f;
|
|
if (v != _remainingMonsters[i]->_status) {
|
|
_remainingMonsters[i]->_status = v;
|
|
} else {
|
|
v &= 0x3f;
|
|
if (v != _remainingMonsters[i]->_status) {
|
|
_remainingMonsters[i]->_status = v;
|
|
} else {
|
|
v &= 0x1f;
|
|
if (v != _remainingMonsters[i]->_status) {
|
|
_remainingMonsters[i]->_status = v;
|
|
} else {
|
|
v &= 0xf;
|
|
if (v != _remainingMonsters[i]->_status) {
|
|
_remainingMonsters[i]->_status = v;
|
|
} else {
|
|
v &= 7;
|
|
if (v != _remainingMonsters[i]->_status) {
|
|
_remainingMonsters[i]->_status = v;
|
|
} else {
|
|
v &= 3;
|
|
if (v != _remainingMonsters[i]->_status) {
|
|
_remainingMonsters[i]->_status = v;
|
|
} else {
|
|
v &= 1;
|
|
if (v != _remainingMonsters[i]->_status)
|
|
_remainingMonsters[i]->_status = v;
|
|
else
|
|
_remainingMonsters[i]->_status = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return _monstersRegenerate || _monstersResistSpells;
|
|
}
|
|
|
|
void Combat::proc2() {
|
|
int threshold = getMonsterIndex() * 8 + 20;
|
|
int val = getRandomNumber(100);
|
|
|
|
_val9 = (val != 100 && val <= threshold) ? 1 : 0;
|
|
}
|
|
|
|
void Combat::monsterAction() {
|
|
Encounter &enc = g_globals->_encounters;
|
|
byte bitset = _monsterP->_counterFlags;
|
|
int threshold = 999; // Never flee by default
|
|
|
|
_activeMonsterNum = _monsterIndex;
|
|
_monsterName = _monsterP->_name;
|
|
monsterIndexOf();
|
|
|
|
// Set up threshold to check whether monster flees
|
|
if (!(bitset & (COUNTER_THRESHOLD1 | COUNTER_THRESHOLD2))) {
|
|
if (enc._highestLevel < 4) {
|
|
} else if (enc._highestLevel < 9) {
|
|
threshold = 50;
|
|
} else if (enc._highestLevel < 14) {
|
|
threshold = 75;
|
|
} else {
|
|
threshold = 0;
|
|
}
|
|
} else if (!(bitset & COUNTER_THRESHOLD1)) {
|
|
if (enc._highestLevel < 9) {
|
|
} else if (enc._highestLevel < 14) {
|
|
threshold = 50;
|
|
} else {
|
|
threshold = 75;
|
|
}
|
|
} else if (!(bitset & COUNTER_THRESHOLD2)) {
|
|
if (enc._highestLevel < 14) {
|
|
} else {
|
|
threshold = 50;
|
|
}
|
|
}
|
|
|
|
if (getRandomNumber(100) >= threshold) {
|
|
// Monster flees from combat
|
|
_monsterP->_experience = 0;
|
|
_monsterP->_loot = 0;
|
|
_monsterP->_hp = 0;
|
|
_monsterP->_status = MONFLAG_DEAD;
|
|
removeMonster();
|
|
|
|
_activeMonsterNum = -1;
|
|
setMode(MONSTER_FLEES);
|
|
} else {
|
|
// Otherwise, move on to checking monster actions
|
|
checkMonsterActions();
|
|
}
|
|
}
|
|
|
|
bool Combat::checkMonsterSpells() {
|
|
Encounter &enc = g_globals->_encounters;
|
|
if (_remainingMonsters.empty()) {
|
|
defeatedMonsters();
|
|
return true;
|
|
}
|
|
|
|
if (enc._monsterList[_monsterIndex]._status & MONFLAG_MINDLESS) {
|
|
setMode(MONSTER_WANDERS);
|
|
return true;
|
|
}
|
|
|
|
if (!_monsterP->_specialAbility || (_monsterP->_specialAbility & 0x80) ||
|
|
(getRandomNumber(100) >= _monsterP->_specialThreshold) ||
|
|
!(_monsterP->_counterFlags & COUNTER_BITS))
|
|
return false;
|
|
|
|
_monsterP->_counterFlags--;
|
|
if (!_monsterP->_specialAbility || _monsterP->_specialAbility >= 33)
|
|
return false;
|
|
|
|
castMonsterSpell(enc._monsterList[_monsterIndex]._name,
|
|
_monsterP->_specialAbility);
|
|
setMode(MONSTER_SPELL);
|
|
return true;
|
|
}
|
|
|
|
void Combat::checkMonsterActions() {
|
|
if (checkMonsterSpells())
|
|
// Monster cast spell, so things are taken care of
|
|
return;
|
|
|
|
_destCharCtr = 0;
|
|
if (_activeMonsterNum < _attackersCount) {
|
|
selectMonsterTarget();
|
|
return;
|
|
}
|
|
|
|
if (!(_monsterP->_specialAbility & 0x80) || !(_monsterP->_counterFlags & COUNTER_BITS)) {
|
|
setMode(WAITS_FOR_OPENING);
|
|
return;
|
|
}
|
|
|
|
_monsterP->_counterFlags--;
|
|
|
|
// Pick a random character to shoot at
|
|
int charNum = getRandomNumber(g_globals->_party.size()) - 1;
|
|
Character &c = g_globals->_party[charNum];
|
|
g_globals->_currCharacter = &c;
|
|
|
|
if (!(c._condition & (BAD_CONDITION | UNCONSCIOUS))) {
|
|
monsterAttackShooting();
|
|
return;
|
|
}
|
|
|
|
// Chosen character was incapitated, so scan through party
|
|
for (uint i = 0; i < g_globals->_party.size(); ++i) {
|
|
c = g_globals->_party[i];
|
|
|
|
if (!(c._condition & (BAD_CONDITION | UNCONSCIOUS))) {
|
|
g_globals->_currCharacter = &c;
|
|
monsterAttackShooting();
|
|
return;
|
|
}
|
|
}
|
|
|
|
checkParty();
|
|
}
|
|
|
|
void Combat::removeMonster() {
|
|
bool changed;
|
|
do {
|
|
changed = false;
|
|
for (uint i = 0; i < _remainingMonsters.size(); ++i) {
|
|
_monsterP = _remainingMonsters[i];
|
|
if (_remainingMonsters[i]->_status == MONFLAG_DEAD) {
|
|
_remainingMonsters.remove_at(i);
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
} while (changed);
|
|
}
|
|
|
|
void Combat::removeDeadMonsters() {
|
|
for (int i = (int)_remainingMonsters.size() - 1; i >= 0; --i) {
|
|
if (_remainingMonsters[i]->_status == MONFLAG_DEAD)
|
|
_remainingMonsters.remove_at(i);
|
|
}
|
|
}
|
|
|
|
void Combat::checkParty() {
|
|
_monsterShootingCtr = 0;
|
|
|
|
if (g_globals->_party.checkPartyIncapacitated())
|
|
return;
|
|
|
|
// Update the array for the party
|
|
for (uint i = 0; i < g_globals->_combatParty.size(); ++i) {
|
|
const Character &c = *g_globals->_combatParty[i];
|
|
if ((c._condition & BAD_CONDITION) || !c._hpCurrent)
|
|
g_globals->_combatParty[i]->_checked = true;
|
|
}
|
|
|
|
combatLoop(true);
|
|
}
|
|
|
|
void Combat::fightMonster(int monsterNum) {
|
|
_attackAttr1 = g_globals->_currCharacter->_physicalAttr;
|
|
_isShooting = false;
|
|
attackMonster(monsterNum);
|
|
}
|
|
|
|
void Combat::shootMonster(int monsterNum) {
|
|
_attackAttr1 = g_globals->_currCharacter->_missileAttr;
|
|
_isShooting = true;
|
|
attackMonster(monsterNum);
|
|
}
|
|
|
|
void Combat::attackMonsterPhysical() {
|
|
_attackAttr1 = g_globals->_currCharacter->_physicalAttr;
|
|
_isShooting = false;
|
|
attackMonster(0);
|
|
}
|
|
|
|
void Combat::attackMonsterShooting() {
|
|
_attackAttr1 = g_globals->_currCharacter->_missileAttr;
|
|
_isShooting = true;
|
|
attackMonster(0);
|
|
}
|
|
|
|
void Combat::attackMonster(int monsterNum) {
|
|
Encounter &enc = g_globals->_encounters;
|
|
_activeMonsterNum = monsterNum;
|
|
monsterSetPtr(monsterNum);
|
|
|
|
Character &c = *g_globals->_currCharacter;
|
|
_attackerLevel = c._level._current;
|
|
_destAC = _monsterP->_ac;
|
|
|
|
if (c._class >= CLERIC) {
|
|
_attackerLevel >>= 1;
|
|
if (c._class == SORCERER)
|
|
_attackerLevel >>= 1;
|
|
}
|
|
|
|
// Affect level based on accuracy
|
|
_attackerLevel += _attackAttr1;
|
|
|
|
if (c._accuracy >= 40)
|
|
_attackerLevel += 7;
|
|
else if (c._accuracy >= 35)
|
|
_attackerLevel += 6;
|
|
else if (c._accuracy >= 30)
|
|
_attackerLevel += 5;
|
|
else if (c._accuracy >= 24)
|
|
_attackerLevel += 4;
|
|
else if (c._accuracy >= 19)
|
|
_attackerLevel += 3;
|
|
else if (c._accuracy >= 16)
|
|
_attackerLevel += 2;
|
|
else if (c._accuracy >= 23)
|
|
_attackerLevel += 1;
|
|
else if (c._accuracy >= 9)
|
|
_attackerLevel += 0;
|
|
else if (c._accuracy >= 7)
|
|
_attackerLevel = MAX((int)_attackerLevel - 1, 0);
|
|
else if (c._accuracy >= 5)
|
|
_attackerLevel = MAX((int)_attackerLevel - 2, 0);
|
|
else
|
|
_attackerLevel = MAX((int)_attackerLevel - 3, 0);
|
|
|
|
_numberOfTimes = 1;
|
|
|
|
if (c._class >= CLERIC && c._level >= 8) {
|
|
_numberOfTimes += c._level / 8;
|
|
}
|
|
|
|
// Affect level based on might
|
|
_attackAttr2 = _attackAttr1;
|
|
|
|
if (c._might >= 40)
|
|
_attackAttr2._current += 13;
|
|
else if (c._might >= 35)
|
|
_attackAttr2._current += 12;
|
|
else if (c._might >= 29)
|
|
_attackAttr2._current += 11;
|
|
else if (c._might >= 27)
|
|
_attackAttr2._current += 10;
|
|
else if (c._might >= 25)
|
|
_attackAttr2._current += 9;
|
|
else if (c._might >= 23)
|
|
_attackAttr2._current += 8;
|
|
else if (c._might >= 21)
|
|
_attackAttr2._current += 7;
|
|
else if (c._might >= 19)
|
|
_attackAttr2._current += 6;
|
|
else if (c._might >= 18)
|
|
_attackAttr2._current += 5;
|
|
else if (c._might >= 17)
|
|
_attackAttr2._current += 4;
|
|
else if (c._might >= 16)
|
|
_attackAttr2._current += 3;
|
|
else if (c._might >= 15)
|
|
_attackAttr2._current += 2;
|
|
else if (c._might >= 13)
|
|
_attackAttr2._current += 1;
|
|
else if (c._might >= 9)
|
|
_attackAttr2._current += 0;
|
|
else if (c._might >= 7)
|
|
_attackAttr2._current = MAX((int)_attackAttr2._current - 1, 0);
|
|
else if (c._might >= 5)
|
|
_attackAttr2._current = MAX((int)_attackAttr2._current - 2, 0);
|
|
else
|
|
_attackAttr2._current = MAX((int)_attackAttr2._current - 3, 0);
|
|
|
|
if (_isShooting && c._class == ARCHER)
|
|
_attackAttr2._current += (c._level + 1) / 2;
|
|
|
|
// Mark the character as having attacked this round
|
|
g_globals->_combatParty[_currentChar]->_checked = true;
|
|
|
|
// Can the attack succeed?
|
|
if (_attackAttr1._current || !(_monsterP->_resistances & MONRES_PHYSICAL_ATTACK)) {
|
|
if (enc._monsterList[_monsterIndex]._status & (MONFLAG_ASLEEP |
|
|
MONFLAG_HELD | MONFLAG_WEBBED | MONFLAG_PARALYZED))
|
|
++_attackerLevel;
|
|
|
|
if (g_globals->_activeSpells._s.bless) {
|
|
_attackerLevel++;
|
|
_attackAttr2._current++;
|
|
}
|
|
|
|
if (g_globals->_activeSpells._s.cursed) {
|
|
int destAC = _destAC + (int)g_globals->_activeSpells._s.cursed;
|
|
if (destAC > 255)
|
|
destAC = 200;
|
|
_destAC = destAC;
|
|
}
|
|
|
|
addAttackDamage();
|
|
if (_damage)
|
|
updateMonsterStatus();
|
|
|
|
setMode(CHAR_ATTACKS);
|
|
|
|
} else {
|
|
setMode(NO_EFFECT);
|
|
}
|
|
}
|
|
|
|
void Combat::addAttackDamage() {
|
|
_damage = 0;
|
|
_timesHit = 0;
|
|
_destAC += 10;
|
|
|
|
for (int i = 0; i < _numberOfTimes; ++i) {
|
|
int val = getRandomNumber(20);
|
|
if (val == 20 || (val != 1 && (val + _attackerLevel) >= _destAC)) {
|
|
_damage = MIN(_damage + (int)_attackAttr2._current +
|
|
getRandomNumber(_attackAttr2._base), 255);
|
|
++_timesHit;
|
|
}
|
|
}
|
|
|
|
_displayedDamage = _damage;
|
|
}
|
|
|
|
void Combat::updateMonsterStatus() {
|
|
int val = _monsterP->_hp - _damage;
|
|
if (val <= 0) {
|
|
_monsterP->_hp = 0;
|
|
_monsterP->_status = MONFLAG_DEAD;
|
|
|
|
} else {
|
|
_monsterP->_hp = val;
|
|
_monsterP->_status &= ~(MONFLAG_ASLEEP | MONFLAG_HELD);
|
|
}
|
|
}
|
|
|
|
bool Combat::monsterTouch(Common::String &line) {
|
|
line.clear();
|
|
|
|
if (_monsterShootingCtr || !_monsterP->_bonusOnTouch)
|
|
return false;
|
|
if (_monsterP->_bonusOnTouch & 0x80) {
|
|
proc9();
|
|
// Original returned value, but caller didn't use.
|
|
// So for ScummVM we return true only if an action is done
|
|
//return g_globals->_spellsState._mmVal7;
|
|
return false;
|
|
}
|
|
|
|
return MonsterTouch::monsterTouch(_monsterP->_bonusOnTouch, line);
|
|
}
|
|
|
|
void Combat::iterateMonsters1() {
|
|
_destMonsterNum = 0;
|
|
_spellMonsterCount = _remainingMonsters.size();
|
|
iterateMonsters1Inner();
|
|
}
|
|
|
|
void Combat::iterateMonsters1Inner() {
|
|
Common::String line1 = Common::String::format("%s %s",
|
|
g_globals->_currCharacter->_name,
|
|
STRING["spells.casts_spell"].c_str());
|
|
|
|
_monsterP = _remainingMonsters[_destMonsterNum];
|
|
const Common::String monsterName = _monsterP->_name;
|
|
bool affects = !monsterLevelThreshold();
|
|
if (affects) {
|
|
if (g_globals->_spellsState._mmVal1) {
|
|
proc2();
|
|
affects = !_val9;
|
|
}
|
|
}
|
|
|
|
byte idx = g_globals->_spellsState._resistanceIndex;
|
|
if (affects && idx) {
|
|
if (--idx >= 8)
|
|
idx = 0;
|
|
|
|
static const byte FLAGS[8] = {
|
|
0x40, 0x20, 0x60, 0x10, 8, 4, 2, 1
|
|
};
|
|
affects = (_monsterP->_resistances & FLAGS[idx]) != FLAGS[idx];
|
|
}
|
|
|
|
Common::String effect;
|
|
if (!affects) {
|
|
effect = STRING["monster_spells.not_affected"];
|
|
|
|
} else {
|
|
_monsterP->_status |= g_globals->_spellsState._damage;
|
|
|
|
byte bits = g_globals->_spellsState._damage;
|
|
int effectNum;
|
|
for (effectNum = 0; effectNum < 8; ++effectNum, bits >>= 1) {
|
|
if (bits & 1)
|
|
break;
|
|
}
|
|
|
|
effect = STRING[Common::String::format(
|
|
"spells.monster_effects.%d", effectNum)];
|
|
}
|
|
|
|
InfoMessage msg(
|
|
0, 0, line1,
|
|
0, 2, Common::String::format("%s %s",
|
|
monsterName.c_str(), effect.c_str()
|
|
));
|
|
|
|
msg._delaySeconds = 3;
|
|
|
|
bool isEnd = !--g_globals->_spellsState._resistanceTypeOrTargetCount;
|
|
if (!isEnd) {
|
|
++_destMonsterNum;
|
|
isEnd = _destMonsterNum >= (int)_remainingMonsters.size();
|
|
}
|
|
// TODO: This may not be needed in ScummVM implementation
|
|
// if (!isEnd)
|
|
// isEnd = (int)((int)_remainingMonsters.size() + _destMonsterNum - _spellMonsterCount) < 0;
|
|
|
|
if (!isEnd) {
|
|
// Move to next iteration after display timeout
|
|
msg._callback = []() {
|
|
g_globals->_combat->iterateMonsters1Inner();
|
|
};
|
|
}
|
|
|
|
displaySpellResult(msg);
|
|
}
|
|
|
|
void Combat::iterateMonsters2() {
|
|
_destMonsterNum = 0;
|
|
_spellMonsterCount = _remainingMonsters.size();
|
|
iterateMonsters2Inner();
|
|
}
|
|
|
|
void Combat::iterateMonsters2Inner() {
|
|
Common::String line1 = Common::String::format("%s %s",
|
|
g_globals->_currCharacter->_name,
|
|
STRING["spells.casts_spell"].c_str());
|
|
|
|
_monsterP = _remainingMonsters[_destMonsterNum];
|
|
const Common::String monsterName = _monsterP->_name;
|
|
|
|
_damage = g_globals->_spellsState._damage;
|
|
|
|
bool affects = !monsterLevelThreshold();
|
|
if (affects) {
|
|
if (g_globals->_spellsState._mmVal1) {
|
|
proc2();
|
|
if (_val9)
|
|
_damage >>= 1;
|
|
}
|
|
|
|
byte idx = g_globals->_spellsState._resistanceIndex;
|
|
if (idx) {
|
|
if (--idx >= 8)
|
|
idx = 0;
|
|
|
|
static const byte FLAGS[8] = {
|
|
0x40, 0x20, 0x60, 0x10, 8, 4, 2, 1
|
|
};
|
|
if ((_monsterP->_resistances & FLAGS[idx]) == FLAGS[idx])
|
|
_damage >>= 2;
|
|
}
|
|
|
|
affects = _damage > 0;
|
|
}
|
|
|
|
InfoMessage msg(0, 0, line1);
|
|
|
|
if (!affects) {
|
|
msg._lines.push_back(Line(
|
|
0, 1, Common::String::format("%s %s",
|
|
monsterName.c_str(),
|
|
STRING["monster_spells.not_affected"].c_str()
|
|
)
|
|
));
|
|
|
|
} else {
|
|
Common::String line2 = Common::String::format("%s %s %d %s %s",
|
|
monsterName.c_str(),
|
|
STRING["dialogs.combat.takes"].c_str(),
|
|
_damage,
|
|
STRING[_damage == 1 ? "dialogs.combat.point" : "dialogs.combat.points"].c_str(),
|
|
STRING["dialogs.combat.of_damage"].c_str()
|
|
);
|
|
|
|
msg._lines.push_back(Line(0, 1, line2));
|
|
|
|
if (_damage >= _monsterP->_hp) {
|
|
msg._lines.push_back(Line(0, 2, STRING["dialogs.combat.and_goes_down"]));
|
|
}
|
|
}
|
|
|
|
updateMonsterStatus();
|
|
|
|
msg._delaySeconds = 3;
|
|
|
|
bool isEnd = !--g_globals->_spellsState._resistanceTypeOrTargetCount;
|
|
if (!isEnd) {
|
|
++_destMonsterNum;
|
|
if (_destMonsterNum >= (int)_remainingMonsters.size())
|
|
isEnd = true;
|
|
}
|
|
// TODO: Is this needed with ScummVM's cleaner _remainingMonsters array?
|
|
// if (!isEnd)
|
|
// isEnd = (int)((int)_remainingMonsters.size() + _destMonsterNum - _spellMonsterCount) < 0;
|
|
|
|
if (!isEnd) {
|
|
// Move to next iteration after display timeout
|
|
msg._callback = []() {
|
|
g_globals->_combat->iterateMonsters2Inner();
|
|
};
|
|
} else {
|
|
msg._callback = []() {
|
|
g_globals->_combat->characterDone();
|
|
};
|
|
}
|
|
|
|
displaySpellResult(msg);
|
|
}
|
|
|
|
void Combat::characterDone() {
|
|
g_globals->_combatParty[_currentChar]->_checked = true;
|
|
combatLoop();
|
|
}
|
|
|
|
void Combat::resetDestMonster() {
|
|
_destMonsterNum = 0;
|
|
_monsterP = _remainingMonsters[0];
|
|
monsterIndexOf();
|
|
g_globals->_spellsState._resistanceTypeOrTargetCount = RESISTANCE_15;
|
|
}
|
|
|
|
void Combat::spellFailed() {
|
|
g_globals->_combatParty[_currentChar]->_checked = true;
|
|
|
|
SoundMessage msg(10, 2, Common::String::format("*** %s ***",
|
|
STRING["spells.failed"].c_str()));
|
|
msg._delaySeconds = 3;
|
|
displaySpellResult(msg);
|
|
}
|
|
|
|
bool Combat::monsterLevelThreshold() const {
|
|
int level = _monsterP->_resistUndead & MAGIC_RESISTANCE;
|
|
return (level != 0) &&
|
|
getRandomNumber(g_globals->_currCharacter->_level + 100) < level;
|
|
}
|
|
|
|
void Combat::turnUndead() {
|
|
if (_turnUndeadUsed) {
|
|
// Already been used in the current combat, can't be used again
|
|
displaySpellResult(InfoMessage(15, 1, STRING["spells.no_effect"]));
|
|
|
|
} else {
|
|
_turnUndeadUsed = true;
|
|
|
|
for (uint i = 0; i < _remainingMonsters.size(); ++i) {
|
|
monsterSetPtr(i);
|
|
Monster *monster = _monsterP;
|
|
|
|
if ((monster->_resistUndead & IS_UNDEAD) &&
|
|
(getRandomNumber(20) + g_globals->_currCharacter->_level) >=
|
|
(_remainingMonsters[i]->_hp * 2 + 10)) {
|
|
destroyMonster();
|
|
++_monstersDestroyedCtr;
|
|
}
|
|
}
|
|
|
|
if (_monstersDestroyedCtr)
|
|
displaySpellResult(InfoMessage(5, 1, STRING["spells.monsters_destroyed"]));
|
|
else
|
|
displaySpellResult(InfoMessage(15, 1, STRING["spells.no_effect"]));
|
|
}
|
|
|
|
g_globals->_combatParty[_currentChar]->_checked = true;
|
|
}
|
|
|
|
void Combat::destroyMonster() {
|
|
Encounter &enc = g_globals->_encounters;
|
|
enc._monsterList[getMonsterIndex()]._status = MONFLAG_DEAD;
|
|
Sound::sound2(SOUND_9);
|
|
}
|
|
|
|
void Combat::summonLightning() {
|
|
SpellsState &ss = g_globals->_spellsState;
|
|
|
|
if (_destMonsterNum < _attackersCount) {
|
|
Common::String line1 = Common::String::format("%s %s",
|
|
g_globals->_currCharacter->_name,
|
|
STRING["spells.casts_spell"].c_str());
|
|
|
|
ss._damage = g_globals->_currCharacter->_level * 2 + 4;
|
|
ss._mmVal1++;
|
|
ss._resistanceIndex++;
|
|
ss._resistanceTypeOrTargetCount = RESISTANCE_ELECTRICITY;
|
|
handlePartyDamage();
|
|
|
|
InfoMessage msg(0, 0, line1);
|
|
msg._delaySeconds = 3;
|
|
msg._callback = []() {
|
|
g_globals->_combat->summonLightning2();
|
|
};
|
|
displaySpellResult(msg);
|
|
} else {
|
|
summonLightning2();
|
|
}
|
|
}
|
|
|
|
void Combat::summonLightning2() {
|
|
SpellsState &ss = g_globals->_spellsState;
|
|
ss._mmVal1 = 1;
|
|
ss._resistanceIndex = 2;
|
|
ss._resistanceTypeOrTargetCount = RESISTANCE_ELECTRICITY;
|
|
ss._damage = getRandomNumber(29) + 3;
|
|
|
|
iterateMonsters2();
|
|
}
|
|
|
|
void Combat::fireball2() {
|
|
SpellsState &ss = g_globals->_spellsState;
|
|
ss._mmVal1 = 1;
|
|
ss._resistanceIndex = 1;
|
|
ss._resistanceTypeOrTargetCount = 5;
|
|
ss._damage = 0;
|
|
|
|
levelAdjust();
|
|
}
|
|
|
|
void Combat::levelAdjust() {
|
|
SpellsState &ss = g_globals->_spellsState;
|
|
for (uint i = 0; i < g_globals->_currCharacter->_level._current; ++i)
|
|
ss._damage += getRandomNumber(6);
|
|
|
|
iterateMonsters2();
|
|
}
|
|
|
|
void Combat::paralyze() {
|
|
SpellsState &ss = g_globals->_spellsState;
|
|
g_globals->_combat->resetDestMonster();
|
|
|
|
ss._mmVal1++;
|
|
ss._resistanceIndex = 6;
|
|
ss._resistanceTypeOrTargetCount = _attackersCount;
|
|
ss._damage = BAD_CONDITION;
|
|
|
|
iterateMonsters1();
|
|
}
|
|
|
|
bool Combat::divineIntervention() {
|
|
Character &c = *g_globals->_currCharacter;
|
|
if (c._alignment != c._alignmentInitial || _divineInterventionUsed)
|
|
return false;
|
|
|
|
_divineInterventionUsed = true;
|
|
int age = (int)c._level + 5;
|
|
if (age > 255)
|
|
return false;
|
|
|
|
c._age = age;
|
|
|
|
for (uint i = 0; i < g_globals->_party.size(); ++i) {
|
|
c = g_globals->_party[i];
|
|
|
|
if (c._condition != ERADICATED) {
|
|
c._condition = 0;
|
|
c._hpCurrent = c._hp;
|
|
c._hpMax = c._hpCurrent;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Combat::holyWord() {
|
|
_monstersDestroyedCtr = 0;
|
|
for (uint i = 0; i < _remainingMonsters.size(); ++i) {
|
|
monsterSetPtr(i);
|
|
if (_monsterP->_resistUndead & IS_UNDEAD) {
|
|
destroyMonster();
|
|
++_monstersDestroyedCtr;
|
|
}
|
|
}
|
|
|
|
if (_monstersDestroyedCtr)
|
|
displaySpellResult(InfoMessage(5, 1, STRING["spells.monsters_destroyed"]));
|
|
else
|
|
displaySpellResult(InfoMessage(15, 1, STRING["spells.no_effect"]));
|
|
g_globals->_combatParty[_currentChar]->_checked = true;
|
|
}
|
|
|
|
#define COL2 21
|
|
void Combat::identifyMonster() {
|
|
InfoMessage msg;
|
|
Common::String line;
|
|
assert(_monsterP == _remainingMonsters[_destMonsterNum]);
|
|
|
|
line = _monsterP->_name;
|
|
line += ':';
|
|
while (line.size() < COL2)
|
|
line += ' ';
|
|
line += STRING["spells.info.hp"];
|
|
line += Common::String::format("%d", _monsterP->_hp);
|
|
line += " ";
|
|
line += STRING["spells.info.ac"];
|
|
line += Common::String::format("%d", _monsterP->_ac);
|
|
msg._lines.push_back(Line(0, 0, line));
|
|
|
|
line = STRING["spells.info.speed"];
|
|
line += Common::String::format("%d", _monsterP->_speed);
|
|
while (line.size() < COL2)
|
|
line += ' ';
|
|
line += STRING["spells.info.bonus_on_touch"];
|
|
line += _monsterP->_bonusOnTouch ? 'Y' : 'N';
|
|
msg._lines.push_back(Line(0, 1, line));
|
|
|
|
line = STRING["spells.info.num_attacks"];
|
|
line += Common::String::format("%d", _monsterP->_numberOfAttacks);
|
|
while (line.size() < COL2)
|
|
line += ' ';
|
|
line += STRING["spells.info.special_ability"];
|
|
line += (_monsterP->_specialAbility & 0x80) ||
|
|
!_monsterP->_specialAbility ? 'N' : 'Y';
|
|
msg._lines.push_back(Line(0, 2, line));
|
|
|
|
line = STRING["spells.info.max_damage"];
|
|
line += Common::String::format("%d", _monsterP->_maxDamage);
|
|
while (line.size() < COL2)
|
|
line += ' ';
|
|
line += STRING["spells.info.magic_resistance"];
|
|
line += Common::String::format("%d",
|
|
_monsterP->_resistUndead & MAGIC_RESISTANCE);
|
|
msg._lines.push_back(Line(0, 3, line));
|
|
|
|
msg._callback = []() {
|
|
g_globals->_combat->characterDone();
|
|
};
|
|
|
|
displaySpellResult(msg);
|
|
}
|
|
#undef COL2
|
|
|
|
void Combat::fireball() {
|
|
SpellsState &ss = g_globals->_spellsState;
|
|
|
|
if (_destMonsterNum < _attackersCount) {
|
|
Common::String line1 = Common::String::format("%s %s",
|
|
g_globals->_currCharacter->_name,
|
|
STRING["spells.casts_spell"].c_str());
|
|
|
|
ss._damage = g_globals->_currCharacter->_level * 2 + 4;
|
|
ss._mmVal1++;
|
|
ss._resistanceIndex++;
|
|
ss._resistanceTypeOrTargetCount++;
|
|
handlePartyDamage();
|
|
|
|
InfoMessage msg(0, 0, line1);
|
|
msg._delaySeconds = 3;
|
|
msg._callback = []() {
|
|
g_globals->_combat->fireball2();
|
|
};
|
|
displaySpellResult(msg);
|
|
} else {
|
|
fireball2();
|
|
}
|
|
}
|
|
|
|
void Combat::lightningBolt() {
|
|
SpellsState &ss = g_globals->_spellsState;
|
|
ss._mmVal1++;
|
|
ss._resistanceTypeOrTargetCount = 3;
|
|
ss._resistanceIndex = 2;
|
|
|
|
levelAdjust();
|
|
}
|
|
|
|
void Combat::makeRoom() {
|
|
for (uint i = 0; i < MIN(g_globals->_combatParty.size(), 5U); ++i)
|
|
g_globals->_combatParty[i]->_canAttack = true;
|
|
}
|
|
|
|
void Combat::slow() {
|
|
for (uint i = 0; i < _remainingMonsters.size(); ++i) {
|
|
monsterSetPtr(i);
|
|
_monsterP->_speed = MAX(_monsterP->_speed / 2, 1);
|
|
}
|
|
}
|
|
|
|
void Combat::weaken() {
|
|
SpellsState &ss = g_globals->_spellsState;
|
|
|
|
for (uint i = 0; i < _remainingMonsters.size(); ++i) {
|
|
monsterSetPtr(i);
|
|
_remainingMonsters[i]->_ac = MAX((int)_remainingMonsters[i]->_ac - 1, 1);
|
|
}
|
|
|
|
resetDestMonster();
|
|
ss._mmVal1++;
|
|
ss._damage = 2;
|
|
iterateMonsters2();
|
|
}
|
|
|
|
bool Combat::web() {
|
|
SpellsState &ss = g_globals->_spellsState;
|
|
if (_destMonsterNum < _attackersCount)
|
|
return false;
|
|
|
|
ss._mmVal1++;
|
|
ss._resistanceIndex = 0;
|
|
ss._resistanceTypeOrTargetCount = 5;
|
|
ss._damage = UNCONSCIOUS;
|
|
|
|
iterateMonsters1();
|
|
return true;
|
|
}
|
|
|
|
bool Combat::acidRain() {
|
|
SpellsState &ss = g_globals->_spellsState;
|
|
if (_attackersCount >= (int)_remainingMonsters.size())
|
|
return false;
|
|
|
|
_destMonsterNum = _attackersCount;
|
|
monsterSetPtr(_destMonsterNum);
|
|
monsterIndexOf();
|
|
|
|
ss._mmVal1 = 1;
|
|
ss._resistanceIndex = 3;
|
|
ss._resistanceTypeOrTargetCount = 15;
|
|
ss._damage = 0;
|
|
|
|
for (int i = 0; i < 5; ++i)
|
|
ss._damage += getRandomNumber(10);
|
|
|
|
iterateMonsters2();
|
|
return true;
|
|
}
|
|
|
|
void Combat::fingerOfDeath() {
|
|
Common::String line1 = Common::String::format("%s %s",
|
|
g_globals->_currCharacter->_name,
|
|
STRING["spells.casts_spell"].c_str());
|
|
|
|
const Common::String monsterName = _monsterP->_name;
|
|
bool kills = !(_monsterP->_resistUndead & IS_UNDEAD) &&
|
|
!monsterLevelThreshold();
|
|
if (kills) {
|
|
proc2();
|
|
kills = !_val9;
|
|
|
|
if (kills)
|
|
destroyMonster();
|
|
}
|
|
removeMonster();
|
|
|
|
Common::String line2 = Common::String::format("%s %s",
|
|
monsterName.c_str(),
|
|
kills ? STRING["spells.char_effects.7"].c_str() :
|
|
STRING["monster_spells.not_affected"].c_str()
|
|
);
|
|
|
|
InfoMessage msg(0, 0, line1, 0, 2, line2);
|
|
msg._delaySeconds = 3;
|
|
msg._callback = []() {
|
|
g_globals->_combat->characterDone();
|
|
};
|
|
|
|
displaySpellResult(msg);
|
|
}
|
|
|
|
void Combat::disintegration() {
|
|
Common::String line1 = Common::String::format("%s %s",
|
|
g_globals->_currCharacter->_name,
|
|
STRING["spells.casts_spell"].c_str());
|
|
|
|
const Common::String monsterName = _monsterP->_name;
|
|
bool kills = !monsterLevelThreshold();
|
|
if (kills) {
|
|
proc2();
|
|
kills = !_val9;
|
|
|
|
if (kills)
|
|
destroyMonster();
|
|
}
|
|
removeMonster();
|
|
|
|
Common::String line2 = Common::String::format("%s %s",
|
|
monsterName.c_str(),
|
|
kills ? STRING["spells.char_effects.disintegrated"].c_str() :
|
|
STRING["monster_spells.not_affected"].c_str()
|
|
);
|
|
|
|
InfoMessage msg(0, 0, line1, 0, 2, line2);
|
|
msg._delaySeconds = 3;
|
|
msg._callback = []() {
|
|
g_globals->_combat->characterDone();
|
|
};
|
|
|
|
displaySpellResult(msg);
|
|
}
|
|
|
|
void Combat::monsterAttackRandom() {
|
|
Encounter &enc = g_globals->_encounters;
|
|
size_t monsterNameSize = enc._monsterList[getMonsterIndex()]._name.size() + 1;
|
|
|
|
_monsterAttackStyle = getRandomNumber((monsterNameSize < 13) ? 15 : 11);
|
|
_monsterShootingCtr = 0;
|
|
|
|
monsterAttackInner();
|
|
}
|
|
|
|
void Combat::monsterAttackShooting() {
|
|
++_monsterShootingCtr;
|
|
_monsterAttackStyle = 99; // shooting
|
|
|
|
monsterAttackInner();
|
|
}
|
|
|
|
|
|
void Combat::monsterAttackInner() {
|
|
Encounter &enc = g_globals->_encounters;
|
|
Character &c = *g_globals->_currCharacter;
|
|
|
|
_destAC = c._ac._base;
|
|
int monsterIndex = getMonsterIndex();
|
|
_attackerLevel = enc._monsterList[monsterIndex]._level * 2 + 4;
|
|
|
|
if (c._condition & (ASLEEP | BLINDED | PARALYZED))
|
|
_attackerLevel += 5;
|
|
|
|
if (_monsterShootingCtr) {
|
|
_attackAttr2._base = _monsterP->_specialAbility & 0x7f;
|
|
_numberOfTimes = 1;
|
|
|
|
} else {
|
|
_numberOfTimes = _monsterP->_numberOfAttacks;
|
|
_attackAttr2._base = _monsterP->_maxDamage;
|
|
}
|
|
|
|
if (g_globals->_activeSpells._s.invisbility)
|
|
_destAC += 3;
|
|
|
|
if (g_globals->_activeSpells._s.cursed) {
|
|
_attackAttr2._current = g_globals->_activeSpells._s.cursed;
|
|
|
|
int attackerLevel = _attackerLevel + _attackAttr2._current;
|
|
_attackerLevel = (attackerLevel > 255) ? 192 : attackerLevel;
|
|
}
|
|
|
|
// Calculate attack damage
|
|
addAttackDamage();
|
|
|
|
// Do some final damage adjustment which may reduce the
|
|
// actual damage from what gets displayed
|
|
if (g_globals->_activeSpells._s.power_shield)
|
|
_damage /= 2;
|
|
|
|
if (_monsterShootingCtr && g_globals->_activeSpells._s.shield)
|
|
_damage = MAX((int)_damage - 8, 0);
|
|
|
|
// Display the result
|
|
setMode(MONSTER_ATTACK);
|
|
}
|
|
|
|
void Combat::selectMonsterTarget() {
|
|
// Iterate through finding a character target for the monster
|
|
do {
|
|
uint idx = _partyIndex + 1;
|
|
if (idx >= g_globals->_party.size())
|
|
idx = 0;
|
|
|
|
// Find a party position that can attack
|
|
int wrapCount = 0;
|
|
while (!g_globals->_combatParty[idx]->_canAttack) {
|
|
if (++idx >= g_globals->_party.size()) {
|
|
idx = 0;
|
|
if (++wrapCount > 1)
|
|
error("No-one in party could attack. Shouldn't happen");
|
|
}
|
|
}
|
|
_partyIndex = idx;
|
|
Character &c = g_globals->_party[_partyIndex];
|
|
g_globals->_currCharacter = &c;
|
|
|
|
if (!(c._condition & BAD_CONDITION) && !(c._condition & UNCONSCIOUS)) {
|
|
// Monster will attack character using a random attack style
|
|
// message (which doesn't make any real difference)
|
|
monsterAttackRandom();
|
|
return;
|
|
}
|
|
|
|
} while (++_destCharCtr < (int)g_globals->_party.size());
|
|
|
|
// At this point, fall back on a generic display message
|
|
// that the monster infiltrates the ranks, which basically
|
|
// means enabling the whole party to be able to attack directly
|
|
for (uint i = 0; i < g_globals->_combatParty.size(); ++i)
|
|
g_globals->_combatParty[i]->_canAttack = true;
|
|
_attackersCount = g_globals->_party.size() * 2;
|
|
|
|
setMode(INFILTRATION);
|
|
}
|
|
|
|
void Combat::attack() {
|
|
if (_allowAttack)
|
|
attackMonsterPhysical();
|
|
}
|
|
|
|
void Combat::block() {
|
|
characterDone();
|
|
}
|
|
|
|
void Combat::cast() {
|
|
g_events->addView("CastSpell");
|
|
}
|
|
|
|
void Combat::exchangeWith(int charNum) {
|
|
g_globals->_combatParty[_currentChar]->_checked = true;
|
|
Character *c1 = g_globals->_combatParty[_currentChar];
|
|
Character *c2 = g_globals->_combatParty[charNum];
|
|
g_globals->_combatParty[_currentChar] = c2;
|
|
g_globals->_combatParty[charNum] = c1;
|
|
|
|
_currentChar = charNum;
|
|
combatLoop();
|
|
}
|
|
|
|
void Combat::retreat() {
|
|
Encounter &enc = g_globals->_encounters;
|
|
|
|
if (getRandomNumber(100) <= enc._fleeThreshold ||
|
|
getRandomNumber(100) < _roundNum) {
|
|
Maps::Maps &maps = g_globals->_maps;
|
|
Maps::Map &map = *maps._currentMap;
|
|
maps._mapPos = Common::Point(map[Maps::MAP_FLEE_X],
|
|
map[Maps::MAP_FLEE_Y]);
|
|
maps.visitedTile();
|
|
|
|
g_globals->_treasure.clear0();
|
|
combatDone();
|
|
|
|
} else {
|
|
if (enc._fleeThreshold)
|
|
// Increase threshold to gradually make it easier to flee
|
|
enc._fleeThreshold++;
|
|
|
|
g_globals->_combat->characterDone();
|
|
}
|
|
}
|
|
|
|
void Combat::use() {
|
|
// Show the character info view in USE mode
|
|
g_events->send("CharacterInfo", GameMessage("USE"));
|
|
}
|
|
|
|
void Combat::combatDone() {
|
|
// Reset several active spells
|
|
g_globals->_activeSpells._s.bless = 0;
|
|
g_globals->_activeSpells._s.invisbility = 0;
|
|
g_globals->_activeSpells._s.shield = 0;
|
|
g_globals->_activeSpells._s.power_shield = 0;
|
|
|
|
// Update the party's characters
|
|
g_globals->_party.combatDone();
|
|
|
|
// Rearrange the party to match the combat order
|
|
g_globals->_party.rearrange(g_globals->_combatParty);
|
|
}
|
|
|
|
Common::String Combat::subtractDamageFromChar() {
|
|
Character &c = *g_globals->_currCharacter;
|
|
int newHp = c._hpCurrent - _damage;
|
|
Common::String result;
|
|
|
|
if (newHp > 0) {
|
|
c._hpCurrent = newHp;
|
|
|
|
} else {
|
|
c._hpCurrent = 0;
|
|
|
|
if (!(c._condition & (BAD_CONDITION | UNCONSCIOUS))) {
|
|
c._condition |= UNCONSCIOUS;
|
|
|
|
result = Common::String::format("%s %s", c._name,
|
|
STRING["dialogs.combat.goes_down"].c_str());
|
|
Sound::sound2(SOUND_8);
|
|
|
|
} else {
|
|
if (c._condition & BAD_CONDITION)
|
|
c._condition = BAD_CONDITION | DEAD;
|
|
|
|
result = Common::String::format("%s %s", c._name,
|
|
STRING["dialogs.combat.dies"].c_str());
|
|
Sound::sound2(SOUND_8);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
} // namespace Game
|
|
} // namespace MM1
|
|
} // namespace MM
|