/* 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 "common/algorithm.h"
#include "common/rect.h"
#include "mm/xeen/character.h"
#include "mm/xeen/combat.h"
#include "mm/xeen/interface.h"
#include "mm/xeen/xeen.h"

namespace MM {
namespace Xeen {

#define GRID_SIZE  (7 * 7)

static const int MONSTER_GRID_X[GRID_SIZE] = {
	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, -1,
};

static const int MONSTER_GRID_Y[GRID_SIZE] = {
	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, 0,
};

static const int MONSTER_GRID3[GRID_SIZE] = {
	-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, +1,
};

static const int MONSTER_GRID_BITINDEX1[GRID_SIZE] = {
	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, 3,
};

static const int MONSTER_GRID_BITINDEX2[GRID_SIZE] = {
	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, 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[21] = {
	10, 20, 50, 100, 100, 100, 0, 0,
	0, 0, 0, 0, 0, 0, 0, 0,
	0, 0, 0, 0, 0
};

#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[31][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_STARTUP;
	_attackDurationCtr = 0;
	_partyRan = false;
	_monster2Attack = -1;
	_whosSpeed = 0;
	_damageType = DT_PHYSICAL;
	_oldCharacter = nullptr;
	_shootType = ST_0;
	_monsterDamage = 0;
	_weaponDamage = 0;
	_weaponDie = _weaponDice = 0;
	_weaponElemMaterial = 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 endIndex = charIndex + 1;
	int selectedIndex = 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 (!charIndex) {
					charIndex = idx + 1;
				} else {
					selectedIndex = idx + 1;
					--charIndex;
					break;
				}
			}
		}
	}
	if (idx == (int)party._activeParty.size()) {
		if (!_combatTarget)
			charIndex = 0;
	}

	for (;;) {
		for (; charIndex < (_combatTarget ? endIndex : (int)party._activeParty.size()); ++charIndex) {
			Character &c = party._activeParty[charIndex];
			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._fireResistance;
				frame = 1;
				fx = 22;
				break;
			case DT_ELECTRICAL:
				damage -= party._electricityResistance;
				frame = 2;
				fx = 23;
				break;
			case DT_COLD:
				damage -= party._coldResistance;
				frame = 3;
				fx = 24;
				break;
			case DT_POISON:
				damage -= party._poisonResistance;
				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[charIndex], 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;

			if (attackType == DT_SLEEP) {
				damage = c._currentHp;
				c._conditions[DEAD] = 1;
			}

			// Subtract the hit points from the character
			c.subtractHitPoints(damage);
			if (selectedIndex)
				break;
		}

		// Break check and if not, move to other index
		if (!selectedIndex || breakFlag)
			break;

		charIndex = selectedIndex - 1;
		breakFlag = true;
	}

	// WORKAROUND: Flag a script in progress when pausing to prevent any pending combat starting prematurely
	Mode oldMode = _vm->_mode;
	_vm->_mode = MODE_SCRIPT_IN_PROGRESS;
	events.ipause(5);
	_vm->_mode = oldMode;

	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._fireResistance;
			frame = 1;
			fx = 22;
			break;
		case DT_ELECTRICAL:
			damage -= party._electricityResistance;
			frame = 2;
			fx = 23;
			break;
		case DT_COLD:
			damage -= party._coldResistance;
			frame = 3;
			fx = 24;
			break;
		case DT_POISON:
			damage -= party._poisonResistance;
			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 = MAX(damage - party._powerShield, 0);
	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:
			c._items.curseUncurse(true);
			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 < XEEN_SLAYER_SWORD && weapon._id != 0 && weapon._frame != 0) {
					weapon._state._broken = true;
					// WORKAROUND: For consistency, we don't de-equip broken items
					//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[31][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];

		// WORKAROUND: Original only checked on y, but some monsters have an invalid X instead
		if ((uint)monster._position.x < 32 && (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 (canMonsterMove(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 (canMonsterMove(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 (canMonsterMove(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 (canMonsterMove(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::Path(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 == DEPRESSED || condition == CONFUSED || condition == NO_CONDITION) {
				_vm->_mode = MODE_INTERACTIVE;
				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::canMonsterMove(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->_ccNum || 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;

	// FIXME: Monster moved outside mapping area. Which shouldn't happen, so ignore the move if it does
	if ((uint)newPos.x >= 32 || (uint)newPos.y >= 32)
		return;

	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::Path(Common::String::format("%s.voc", monsterData._attackVoc.c_str())));
		monsterId = monster._spriteId;
	}

	MonsterStruct &monsterData = map._monsterData[monsterId];
	for (int attackNum = 0; attackNum < monsterData._numberOfAttacks; ++attackNum) {
		int charNum = -1;
		bool isHated = false;

		if (monsterData._hatesClass != CLASS_PALADIN) {
			if (monsterData._hatesClass == HATES_PARTY) {
				// Monster hates entire party, even the disabled/dead
				for (uint idx = 0; idx < _combatParty.size(); ++idx) {
					doMonsterTurn(monsterId, idx);
				}

				// Move onto monster's next attack (if any)
				continue;
			}

			for (uint charIndex = 0; charIndex < _combatParty.size(); ++charIndex) {
				Character &c = *_combatParty[charIndex];
				Condition cond = c.worstCondition();
				if (cond >= PARALYZED && cond <= ERADICATED)
					continue;

				switch (monsterData._hatesClass) {
				case CLASS_KNIGHT:
				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 HATES_DWARF:
					isHated = c._race == DWARF;
					break;
				default:
					break;
				}

				if (isHated) {
					charNum = charIndex;
					break;
				}
			}
		}

		if (!isHated) {
			// No particularly hated foe, so pick a random character to start with
			// Note: Original had a whole switch statement depending on party size, that boiled down to
			// picking a random character in all cases anyway
			charNum = _vm->getRandomNumber(0, _combatParty.size() - 1);
		}

		// If the chosen character is already disabled, we need to pick a still able body character
		// from the remainder of the combat party
		Condition cond = _combatParty[charNum]->worstCondition();
		if (cond >= PARALYZED && cond <= ERADICATED) {
			Common::Array<int> ableChars;

			for (uint idx = 0; idx < _combatParty.size(); ++idx) {
				switch (_combatParty[idx]->worstCondition()) {
				case PARALYZED:
				case UNCONSCIOUS:
				case DEAD:
				case STONED:
				case ERADICATED:
					break;
				default:
					ableChars.push_back(idx);
					break;
				}
			}

			if (ableChars.size() == 0) {
				party._dead = true;
				_vm->_mode = MODE_INTERACTIVE;
				return;
			}

			charNum = ableChars[_vm->getRandomNumber(0, ableChars.size() - 1)];
		}

		doMonsterTurn(monsterId, charNum);
	}

	intf.drawParty(true);
}

void Combat::doMonsterTurn(int monsterId, int charNum) {
	Map &map = *_vm->_map;
	Sound &sound = *_vm->_sound;
	MonsterStruct &monsterData = map._monsterData[monsterId];
	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);
			}
		}
	}
}

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 members
	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.empty()) {
			_whosSpeed = 0;
		} else if (_whosSpeed >= (int)_speedTable.size() || _speedTable[_whosSpeed] != oldSpeed) {
			for (_whosSpeed = 0; _whosSpeed < (int)_speedTable.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];
	} else 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._magicResistance || monsterData._magicResistance <=
					_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.isEquipped()) {
				switch (weapon._state._counter) {
				case EFFECTIVE_DRAGON:
					if (monsterData._monsterType == MONSTER_DRAGON)
						damage *= 3;
					break;
				case EFFECTIVE_UNDEAD	:
					if (monsterData._monsterType == MONSTER_UNDEAD)
						damage *= 3;
					break;
				case EFFECTIVE_GOLEM:
					if (monsterData._monsterType == MONSTER_GOLEM)
						damage *= 3;
					break;
				case EFFECTIVE_INSECT:
					if (monsterData._monsterType == MONSTER_INSECT)
						damage *= 3;
					break;
				case EFFEctIVE_MONSTERS:
					if (monsterData._monsterType == MONSTER_MONSTERS)
						damage *= 3;
					break;
				case EFFECTIVE_ANIMAL:
					if (monsterData._monsterType == MONSTER_ANIMAL)
						damage *= 3;
					break;
				default:
					break;
				}
			}
		}

		attack2(damage, rangeType);
	}

	setSpeedTable();
}

void Combat::attack2(int damage, RangeType rangeType) {
	Debugger &debugger = *_vm->_debugger;
	Interface &intf = *_vm->_interface;
	Map &map = *_vm->_map;
	Party &party = *_vm->_party;
	Sound &sound = *_vm->_sound;
	int ccNum = _vm->_files->_ccNum;
	MazeMonster &monster = map._mobData._monsters[_monster2Attack];
	MonsterStruct &monsterData = *monster._monsterData;
	bool monsterDied = false;

	if (!ccNum && damage && rangeType != RT_SINGLE && monster._spriteId == 89)
		damage = 0;
	if (debugger._superStrength)
		damage = 10000;

	if (!damage) {
		sound.playSound(_missVoc, 1);
		sound.playFX(6);
	} else {
		if (!ccNum && 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 < XEEN_SLAYER_SWORD) {
			if (monsterData._phsyicalResistance != 0) {
				if (monsterData._phsyicalResistance == 100) {
					// Completely immune to the damage
					damage = 0;
				} else {
					// Reduce the damage based on physical resistance
					damage = damage * (100 - monsterData._phsyicalResistance) / 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 = getMonsterResistance(rangeType);
		damage += monsterResist;
		if (monsterResist > 0) {
			_pow[_attackDurationCtr]._elemFrame = XeenItem::getElementalCategory(_weaponElemMaterial) + 1;
			_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();
			int powNum = (_attackWeaponId > XEEN_SLAYER_SWORD) ? 0 : POW_WEAPON_VOCS[_attackWeaponId];
			File powVoc(Common::Path(Common::String::format("pow%d.voc", powNum)));
			sound.playFX(60 + powNum);
			sound.playSound(powVoc, 1);

			if (monster._hp > damage) {
				monster._hp -= damage;
			} else {
				monster._hp = 0;
				monsterDied = true;
			}
		}
	}

	intf.draw3d(true);

	if (monsterDied) {
		if (!ccNum) {
			if (_monster2Attack == 20 && party._mazeId == 41)
				party._gameFlags[0][11] = true;
			if (_monster2Attack == 8 && party._mazeId == 78) {
				party._gameFlags[0][60] = true;
				party._questFlags[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 (!ccNum && monster._spriteId == 89) {
				// Xeen's Scepter of Temporal Distortion
				party._treasure._weapons[0]._id = 90;
				party._treasure._weapons[0]._material = 0;
				party._treasure._weapons[0]._state.clear();
				party._treasure._hasItems = true;
				party._questItems[8]++;
			}

			int itemDrop = monsterData._itemDrop;
			if (itemDrop) {
				if (MONSTER_ITEM_RANGES[itemDrop - 1] >= _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].empty()) {
								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].empty()) {
								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].empty()) {
								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].empty()) {
								party._treasure._accessories[idx] = tempChar._accessories[0];
								party._treasure._hasItems = true;
								break;
							}
						}
						break;
					default:
						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->getSpellsCategory()][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_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;
	case CLASS_KNIGHT:
	case CLASS_BARBARIAN:
	default:
		divisor = 1;
		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;
	_weaponElemMaterial = 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.isBad()) {
				_attackWeapon = &weapon;

				if (weapon._material < 37) {
					_weaponElemMaterial = weapon._material;
				} else if (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::getMonsterResistance(RangeType rangeType) {
	Map &map = *_vm->_map;
	assert(_monster2Attack != -1);
	MazeMonster &monster = map._mobData._monsters[_monster2Attack];
	MonsterStruct &monsterData = *monster._monsterData;
	int resistance = 0, damage = 0;

	if (rangeType != RT_SINGLE && rangeType != RT_HIT) {
		switch (_damageType) {
		case DT_PHYSICAL:
			resistance = monsterData._phsyicalResistance;
			break;
		case DT_MAGICAL:
			resistance = monsterData._magicResistance;
			break;
		case DT_FIRE:
			resistance = monsterData._fireResistance;
			break;
		case DT_ELECTRICAL:
			resistance = monsterData._electricityResistance;
			break;
		case DT_COLD:
			resistance = monsterData._coldResistance;
			break;
		case DT_POISON:
			resistance = monsterData._poisonResistance;
			break;
		case DT_ENERGY:
			resistance = monsterData._energyResistance;
			break;
		default:
			break;
		}
	} else {
		int material = _weaponElemMaterial;
		damage = Res.ELEMENTAL_DAMAGE[material];

		if (material != 0) {
			if (material < 9)
				resistance = monsterData._fireResistance;
			else if (material < 16)
				resistance = monsterData._electricityResistance;
			else if (material < 21)
				resistance = monsterData._coldResistance;
			else if (material < 26)
				resistance = monsterData._poisonResistance;
			else if (material < 34)
				resistance = monsterData._energyResistance;
			else
				resistance = monsterData._magicResistance;
		}
	}

	if (resistance != 0) {
		if (resistance == 100)
			return 0;
		else
			return ((100 - resistance) * 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::Path(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 = attackDurationCtr - 1;
		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);
}

bool Combat::areMonstersPresent() const {
	for (int idx = 0; idx < 26; ++idx) {
		if (_attackMonsters[idx] != -1)
			return true;
	}

	return false;
}

void Combat::reset() {
	clearShooting();
	setupCombatParty();

	_combatMode = COMBATMODE_INTERACTIVE;
	_monster2Attack = -1;
}

} // End of namespace Xeen
} // End of namespace MM
