You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

5024 lines
147 KiB

/**
* @file monster.cpp
*
* Implementation of monster functionality, AI, actions, spawning, loading, etc.
*/
#include "monster.h"
#include <algorithm>
#include <array>
#include <climits>
#include <fmt/format.h>
#include "control.h"
#include "cursor.h"
#include "dead.h"
#include "drlg_l1.h"
#include "drlg_l4.h"
#include "engine/cel_header.hpp"
#include "engine/load_file.hpp"
#include "engine/random.hpp"
#include "engine/render/cl2_render.hpp"
#include "init.h"
#include "lighting.h"
#include "minitext.h"
#include "missiles.h"
#include "movie.h"
#include "options.h"
#include "spelldat.h"
#include "storm/storm_net.hpp"
#include "themes.h"
#include "towners.h"
#include "trigs.h"
#include "utils/file_name_generator.hpp"
#include "utils/language.h"
#include "utils/stdcompat/string_view.hpp"
#include "utils/utf8.hpp"
#ifdef _DEBUG
#include "debug.h"
#endif
namespace devilution {
CMonster LevelMonsterTypes[MAX_LVLMTYPES];
int LevelMonsterTypeCount;
Monster Monsters[MAXMONSTERS];
int ActiveMonsters[MAXMONSTERS];
int ActiveMonsterCount;
// BUGFIX: replace MonsterKillCounts[MAXMONSTERS] with MonsterKillCounts[NUM_MTYPES].
/** Tracks the total number of monsters killed per monster_id. */
int MonsterKillCounts[MAXMONSTERS];
bool sgbSaveSoundOn;
namespace {
#define NIGHTMARE_TO_HIT_BONUS 85
#define HELL_TO_HIT_BONUS 120
#define NIGHTMARE_AC_BONUS 50
#define HELL_AC_BONUS 80
/** Tracks which missile files are already loaded */
int totalmonsters;
int monstimgtot;
int uniquetrans;
constexpr std::array<_monster_id, 12> SkeletonTypes {
MT_WSKELAX,
MT_TSKELAX,
MT_RSKELAX,
MT_XSKELAX,
MT_WSKELBW,
MT_TSKELBW,
MT_RSKELBW,
MT_XSKELBW,
MT_WSKELSD,
MT_TSKELSD,
MT_RSKELSD,
MT_XSKELSD,
};
// BUGFIX: MWVel velocity values are not rounded consistently. The correct
// formula for monster walk velocity is calculated as follows (for 16, 32 and 64
// pixel distances, respectively):
//
// vel16 = (16 << monsterWalkShift) / nframes
// vel32 = (32 << monsterWalkShift) / nframes
// vel64 = (64 << monsterWalkShift) / nframes
//
// The correct monster walk velocity table is as follows:
//
// int MWVel[24][3] = {
// { 256, 512, 1024 },
// { 128, 256, 512 },
// { 85, 171, 341 },
// { 64, 128, 256 },
// { 51, 102, 205 },
// { 43, 85, 171 },
// { 37, 73, 146 },
// { 32, 64, 128 },
// { 28, 57, 114 },
// { 26, 51, 102 },
// { 23, 47, 93 },
// { 21, 43, 85 },
// { 20, 39, 79 },
// { 18, 37, 73 },
// { 17, 34, 68 },
// { 16, 32, 64 },
// { 15, 30, 60 },
// { 14, 28, 57 },
// { 13, 27, 54 },
// { 13, 26, 51 },
// { 12, 24, 49 },
// { 12, 23, 47 },
// { 11, 22, 45 },
// { 11, 21, 43 }
// };
/** Maps from monster walk animation frame num to monster velocity. */
int MWVel[24][3] = {
{ 256, 512, 1024 },
{ 128, 256, 512 },
{ 85, 170, 341 },
{ 64, 128, 256 },
{ 51, 102, 204 },
{ 42, 85, 170 },
{ 36, 73, 146 },
{ 32, 64, 128 },
{ 28, 56, 113 },
{ 26, 51, 102 },
{ 23, 46, 93 },
{ 21, 42, 85 },
{ 19, 39, 78 },
{ 18, 36, 73 },
{ 17, 34, 68 },
{ 16, 32, 64 },
{ 15, 30, 60 },
{ 14, 28, 57 },
{ 13, 26, 54 },
{ 12, 25, 51 },
{ 12, 24, 48 },
{ 11, 23, 46 },
{ 11, 22, 44 },
{ 10, 21, 42 }
};
/** Maps from monster action to monster animation letter. */
char animletter[7] = "nwahds";
size_t GetNumAnims(const MonsterData &monsterData)
{
return monsterData.has_special ? 6 : 5;
}
bool IsDirectionalAnim(const CMonster &monster, size_t animIndex)
{
return monster.mtype != MT_GOLEM || animIndex < 4;
}
void InitMonsterTRN(CMonster &monst)
{
std::array<uint8_t, 256> colorTranslations;
LoadFileInMem(monst.MData->TransFile, colorTranslations);
std::replace(colorTranslations.begin(), colorTranslations.end(), 255, 0);
const size_t numAnims = GetNumAnims(*monst.MData);
for (size_t i = 0; i < numAnims; i++) {
if (i == 1 && IsAnyOf(monst.mtype, MT_COUNSLR, MT_MAGISTR, MT_CABALIST, MT_ADVOCATE)) {
continue;
}
AnimStruct &anim = monst.Anims[i];
if (IsDirectionalAnim(monst, i)) {
for (size_t j = 0; j < 8; ++j) {
Cl2ApplyTrans(anim.CelSpritesForDirections[j], colorTranslations, anim.Frames);
}
} else {
byte *frames[8];
CelGetDirectionFrames(anim.CelSpritesForDirections[0], frames);
for (byte *frame : frames) {
Cl2ApplyTrans(frame, colorTranslations, anim.Frames);
}
}
}
}
void InitMonster(Monster &monster, Direction rd, int mtype, Point position)
{
monster._mdir = rd;
monster.position.tile = position;
monster.position.future = position;
monster.position.old = position;
monster._mMTidx = mtype;
monster._mmode = MonsterMode::Stand;
monster.MType = &LevelMonsterTypes[mtype];
monster.MData = monster.MType->MData;
monster.mName = pgettext("monster", monster.MData->mName).c_str();
monster.AnimInfo = {};
monster.ChangeAnimationData(MonsterGraphic::Stand);
monster.AnimInfo.TickCounterOfCurrentFrame = GenerateRnd(monster.AnimInfo.TicksPerFrame - 1);
monster.AnimInfo.CurrentFrame = GenerateRnd(monster.AnimInfo.NumberOfFrames - 1);
monster.mLevel = monster.MData->mLevel;
int maxhp = monster.MData->mMinHP + GenerateRnd(monster.MData->mMaxHP - monster.MData->mMinHP + 1);
if (monster.MType->mtype == MT_DIABLO && !gbIsHellfire) {
maxhp /= 2;
monster.mLevel -= 15;
}
monster._mmaxhp = maxhp << 6;
if (!gbIsMultiplayer)
monster._mmaxhp = std::max(monster._mmaxhp / 2, 64);
monster._mhitpoints = monster._mmaxhp;
monster._mAi = monster.MData->mAi;
monster._mint = monster.MData->mInt;
monster._mgoal = MGOAL_NORMAL;
monster._mgoalvar1 = 0;
monster._mgoalvar2 = 0;
monster._mgoalvar3 = 0;
monster._pathcount = 0;
monster._mDelFlag = false;
monster._uniqtype = 0;
monster._msquelch = 0;
monster.mlid = NO_LIGHT; // BUGFIX monsters initial light id should be -1 (fixed)
monster._mRndSeed = AdvanceRndSeed();
monster._mAISeed = AdvanceRndSeed();
monster.mWhoHit = 0;
monster.mExp = monster.MData->mExp;
monster.mHit = monster.MData->mHit;
monster.mMinDamage = monster.MData->mMinDamage;
monster.mMaxDamage = monster.MData->mMaxDamage;
monster.mHit2 = monster.MData->mHit2;
monster.mMinDamage2 = monster.MData->mMinDamage2;
monster.mMaxDamage2 = monster.MData->mMaxDamage2;
monster.mArmorClass = monster.MData->mArmorClass;
monster.mMagicRes = monster.MData->mMagicRes;
monster.leader = 0;
monster.leaderRelation = LeaderRelation::None;
monster._mFlags = monster.MData->mFlags;
monster.mtalkmsg = TEXT_NONE;
if (monster._mAi == AI_GARG) {
monster.ChangeAnimationData(MonsterGraphic::Special);
monster.AnimInfo.CurrentFrame = 0;
monster._mFlags |= MFLAG_ALLOW_SPECIAL;
monster._mmode = MonsterMode::SpecialMeleeAttack;
}
if (sgGameInitInfo.nDifficulty == DIFF_NIGHTMARE) {
monster._mmaxhp = 3 * monster._mmaxhp;
if (gbIsHellfire)
monster._mmaxhp += (gbIsMultiplayer ? 100 : 50) << 6;
else
monster._mmaxhp += 64;
monster._mhitpoints = monster._mmaxhp;
monster.mLevel += 15;
monster.mExp = 2 * (monster.mExp + 1000);
monster.mHit += NIGHTMARE_TO_HIT_BONUS;
monster.mMinDamage = 2 * (monster.mMinDamage + 2);
monster.mMaxDamage = 2 * (monster.mMaxDamage + 2);
monster.mHit2 += NIGHTMARE_TO_HIT_BONUS;
monster.mMinDamage2 = 2 * (monster.mMinDamage2 + 2);
monster.mMaxDamage2 = 2 * (monster.mMaxDamage2 + 2);
monster.mArmorClass += NIGHTMARE_AC_BONUS;
} else if (sgGameInitInfo.nDifficulty == DIFF_HELL) {
monster._mmaxhp = 4 * monster._mmaxhp;
if (gbIsHellfire)
monster._mmaxhp += (gbIsMultiplayer ? 200 : 100) << 6;
else
monster._mmaxhp += 192;
monster._mhitpoints = monster._mmaxhp;
monster.mLevel += 30;
monster.mExp = 4 * (monster.mExp + 1000);
monster.mHit += HELL_TO_HIT_BONUS;
monster.mMinDamage = 4 * monster.mMinDamage + 6;
monster.mMaxDamage = 4 * monster.mMaxDamage + 6;
monster.mHit2 += HELL_TO_HIT_BONUS;
monster.mMinDamage2 = 4 * monster.mMinDamage2 + 6;
monster.mMaxDamage2 = 4 * monster.mMaxDamage2 + 6;
monster.mArmorClass += HELL_AC_BONUS;
monster.mMagicRes = monster.MData->mMagicRes2;
}
}
bool CanPlaceMonster(int xp, int yp)
{
if (!InDungeonBounds({ xp, yp })
|| dMonster[xp][yp] != 0
|| dPlayer[xp][yp] != 0) {
return false;
}
if (IsTileVisible({ xp, yp })) {
return false;
}
if (TileContainsSetPiece({ xp, yp })) {
return false;
}
return !IsTileOccupied({ xp, yp });
}
void PlaceMonster(int i, int mtype, int x, int y)
{
if (LevelMonsterTypes[mtype].mtype == MT_NAKRUL) {
for (int j = 0; j < ActiveMonsterCount; j++) {
if (Monsters[j]._mMTidx == mtype) {
return;
}
if (Monsters[j].MType->mtype == MT_NAKRUL) {
return;
}
}
}
dMonster[x][y] = i + 1;
auto rd = static_cast<Direction>(GenerateRnd(8));
InitMonster(Monsters[i], rd, mtype, { x, y });
}
void PlaceGroup(int mtype, int num, UniqueMonsterPack uniqueMonsterPack, int leaderId)
{
int placed = 0;
auto &leader = Monsters[leaderId];
for (int try1 = 0; try1 < 10; try1++) {
while (placed != 0) {
ActiveMonsterCount--;
placed--;
const auto &position = Monsters[ActiveMonsterCount].position.tile;
dMonster[position.x][position.y] = 0;
}
int xp;
int yp;
if (uniqueMonsterPack != UniqueMonsterPack::None) {
int offset = GenerateRnd(8);
auto position = leader.position.tile + static_cast<Direction>(offset);
xp = position.x;
yp = position.y;
} else {
do {
xp = GenerateRnd(80) + 16;
yp = GenerateRnd(80) + 16;
} while (!CanPlaceMonster(xp, yp));
}
int x1 = xp;
int y1 = yp;
if (num + ActiveMonsterCount > totalmonsters) {
num = totalmonsters - ActiveMonsterCount;
}
int j = 0;
for (int try2 = 0; j < num && try2 < 100; xp += Displacement(static_cast<Direction>(GenerateRnd(8))).deltaX, yp += Displacement(static_cast<Direction>(GenerateRnd(8))).deltaX) { /// BUGFIX: `yp += Point.y`
if (!CanPlaceMonster(xp, yp)
|| (dTransVal[xp][yp] != dTransVal[x1][y1])
|| (uniqueMonsterPack == UniqueMonsterPack::Leashed && (abs(xp - x1) >= 4 || abs(yp - y1) >= 4))) {
try2++;
continue;
}
PlaceMonster(ActiveMonsterCount, mtype, xp, yp);
if (uniqueMonsterPack != UniqueMonsterPack::None) {
auto &minion = Monsters[ActiveMonsterCount];
minion._mmaxhp *= 2;
minion._mhitpoints = minion._mmaxhp;
minion._mint = leader._mint;
if (uniqueMonsterPack == UniqueMonsterPack::Leashed) {
minion.leader = leaderId;
minion.leaderRelation = LeaderRelation::Leashed;
minion._mAi = leader._mAi;
}
if (minion._mAi != AI_GARG) {
minion.ChangeAnimationData(MonsterGraphic::Stand);
minion.AnimInfo.CurrentFrame = GenerateRnd(minion.AnimInfo.NumberOfFrames - 1);
minion._mFlags &= ~MFLAG_ALLOW_SPECIAL;
minion._mmode = MonsterMode::Stand;
}
}
ActiveMonsterCount++;
placed++;
j++;
}
if (placed >= num) {
break;
}
}
if (uniqueMonsterPack == UniqueMonsterPack::Leashed) {
leader.packsize = placed;
}
}
void PlaceUniqueMonst(int uniqindex, int miniontype, int bosspacksize)
{
auto &monster = Monsters[ActiveMonsterCount];
const auto &uniqueMonsterData = UniqueMonstersData[uniqindex];
int uniqtype;
for (uniqtype = 0; uniqtype < LevelMonsterTypeCount; uniqtype++) {
if (LevelMonsterTypes[uniqtype].mtype == uniqueMonsterData.mtype) {
break;
}
}
int count = 0;
int xp;
int yp;
while (true) {
xp = GenerateRnd(80) + 16;
yp = GenerateRnd(80) + 16;
int count2 = 0;
for (int x = xp - 3; x < xp + 3; x++) {
for (int y = yp - 3; y < yp + 3; y++) {
if (InDungeonBounds({ x, y }) && CanPlaceMonster(x, y)) {
count2++;
}
}
}
if (count2 < 9) {
count++;
if (count < 1000) {
continue;
}
}
if (CanPlaceMonster(xp, yp)) {
break;
}
}
if (uniqindex == UMT_SNOTSPIL) {
xp = 2 * setpc_x + 24;
yp = 2 * setpc_y + 28;
}
if (uniqindex == UMT_WARLORD) {
xp = 2 * setpc_x + 22;
yp = 2 * setpc_y + 23;
}
if (uniqindex == UMT_ZHAR) {
for (int i = 0; i < themeCount; i++) {
if (i == zharlib) {
xp = 2 * themeLoc[i].x + 20;
yp = 2 * themeLoc[i].y + 20;
break;
}
}
}
if (!gbIsMultiplayer) {
if (uniqindex == UMT_LAZARUS) {
xp = 32;
yp = 46;
}
if (uniqindex == UMT_RED_VEX) {
xp = 40;
yp = 45;
}
if (uniqindex == UMT_BLACKJADE) {
xp = 38;
yp = 49;
}
if (uniqindex == UMT_SKELKING) {
xp = 35;
yp = 47;
}
} else {
if (uniqindex == UMT_LAZARUS) {
xp = 2 * setpc_x + 19;
yp = 2 * setpc_y + 22;
}
if (uniqindex == UMT_RED_VEX) {
xp = 2 * setpc_x + 21;
yp = 2 * setpc_y + 19;
}
if (uniqindex == UMT_BLACKJADE) {
xp = 2 * setpc_x + 21;
yp = 2 * setpc_y + 25;
}
}
if (uniqindex == UMT_BUTCHER) {
bool done = false;
for (yp = 0; yp < MAXDUNY && !done; yp++) {
for (xp = 0; xp < MAXDUNX && !done; xp++) {
done = dPiece[xp][yp] == 367;
}
}
}
if (uniqindex == UMT_NAKRUL) {
if (UberRow == 0 || UberCol == 0) {
UberDiabloMonsterIndex = -1;
return;
}
xp = UberRow - 2;
yp = UberCol;
UberDiabloMonsterIndex = ActiveMonsterCount;
}
PlaceMonster(ActiveMonsterCount, uniqtype, xp, yp);
PrepareUniqueMonst(monster, uniqindex, miniontype, bosspacksize, uniqueMonsterData);
}
int AddMonsterType(_monster_id type, placeflag placeflag)
{
bool done = false;
int i;
for (i = 0; i < LevelMonsterTypeCount && !done; i++) {
done = LevelMonsterTypes[i].mtype == type;
}
i--;
if (!done) {
i = LevelMonsterTypeCount;
LevelMonsterTypeCount++;
LevelMonsterTypes[i].mtype = type;
monstimgtot += MonstersData[type].mImage;
InitMonsterGFX(i);
InitMonsterSND(i);
}
LevelMonsterTypes[i].mPlaceFlags |= placeflag;
return i;
}
void ClearMVars(Monster &monster)
{
monster._mVar1 = 0;
monster._mVar2 = 0;
monster._mVar3 = 0;
monster.position.temp = { 0, 0 };
monster.position.offset2 = { 0, 0 };
}
void ClrAllMonsters()
{
for (auto &monster : Monsters) {
ClearMVars(monster);
monster.mName = "Invalid Monster";
monster._mgoal = MGOAL_NONE;
monster._mmode = MonsterMode::Stand;
monster._mVar1 = 0;
monster._mVar2 = 0;
monster.position.tile = { 0, 0 };
monster.position.future = { 0, 0 };
monster.position.old = { 0, 0 };
monster._mdir = static_cast<Direction>(GenerateRnd(8));
monster.position.velocity = { 0, 0 };
monster.AnimInfo = {};
monster._mFlags = 0;
monster._mDelFlag = false;
monster._menemy = GenerateRnd(gbActivePlayers);
monster.enemyPosition = Players[monster._menemy].position.future;
}
}
void PlaceUniqueMonsters()
{
for (int u = 0; UniqueMonstersData[u].mtype != -1; u++) {
if (UniqueMonstersData[u].mlevel != currlevel)
continue;
bool done = false;
int mt;
for (mt = 0; mt < LevelMonsterTypeCount; mt++) {
done = (LevelMonsterTypes[mt].mtype == UniqueMonstersData[u].mtype);
if (done)
break;
}
if (u == UMT_GARBUD && Quests[Q_GARBUD]._qactive == QUEST_NOTAVAIL)
done = false;
if (u == UMT_ZHAR && Quests[Q_ZHAR]._qactive == QUEST_NOTAVAIL)
done = false;
if (u == UMT_SNOTSPIL && Quests[Q_LTBANNER]._qactive == QUEST_NOTAVAIL)
done = false;
if (u == UMT_LACHDAN && Quests[Q_VEIL]._qactive == QUEST_NOTAVAIL)
done = false;
if (u == UMT_WARLORD && Quests[Q_WARLORD]._qactive == QUEST_NOTAVAIL)
done = false;
if (done)
PlaceUniqueMonst(u, mt, 8);
}
}
void PlaceQuestMonsters()
{
if (!setlevel) {
if (Quests[Q_BUTCHER].IsAvailable()) {
PlaceUniqueMonst(UMT_BUTCHER, 0, 0);
}
if (currlevel == Quests[Q_SKELKING]._qlevel && gbIsMultiplayer) {
for (int i = 0; i < LevelMonsterTypeCount; i++) {
if (IsSkel(LevelMonsterTypes[i].mtype)) {
PlaceUniqueMonst(UMT_SKELKING, i, 30);
break;
}
}
}
if (Quests[Q_LTBANNER].IsAvailable()) {
auto dunData = LoadFileInMem<uint16_t>("Levels\\L1Data\\Banner1.DUN");
SetMapMonsters(dunData.get(), Point { setpc_x, setpc_y } * 2);
}
if (Quests[Q_BLOOD].IsAvailable()) {
auto dunData = LoadFileInMem<uint16_t>("Levels\\L2Data\\Blood2.DUN");
SetMapMonsters(dunData.get(), Point { setpc_x, setpc_y } * 2);
}
if (Quests[Q_BLIND].IsAvailable()) {
auto dunData = LoadFileInMem<uint16_t>("Levels\\L2Data\\Blind2.DUN");
SetMapMonsters(dunData.get(), Point { setpc_x, setpc_y } * 2);
}
if (Quests[Q_ANVIL].IsAvailable()) {
auto dunData = LoadFileInMem<uint16_t>("Levels\\L3Data\\Anvil.DUN");
SetMapMonsters(dunData.get(), Point { setpc_x + 1, setpc_y + 1 } * 2);
}
if (Quests[Q_WARLORD].IsAvailable()) {
auto dunData = LoadFileInMem<uint16_t>("Levels\\L4Data\\Warlord.DUN");
SetMapMonsters(dunData.get(), Point { setpc_x, setpc_y } * 2);
AddMonsterType(UniqueMonstersData[UMT_WARLORD].mtype, PLACE_SCATTER);
}
if (Quests[Q_VEIL].IsAvailable()) {
AddMonsterType(UniqueMonstersData[UMT_LACHDAN].mtype, PLACE_SCATTER);
}
if (Quests[Q_ZHAR].IsAvailable() && zharlib == -1) {
Quests[Q_ZHAR]._qactive = QUEST_NOTAVAIL;
}
if (currlevel == Quests[Q_BETRAYER]._qlevel && gbIsMultiplayer) {
AddMonsterType(UniqueMonstersData[UMT_LAZARUS].mtype, PLACE_UNIQUE);
AddMonsterType(UniqueMonstersData[UMT_RED_VEX].mtype, PLACE_UNIQUE);
PlaceUniqueMonst(UMT_LAZARUS, 0, 0);
PlaceUniqueMonst(UMT_RED_VEX, 0, 0);
PlaceUniqueMonst(UMT_BLACKJADE, 0, 0);
auto dunData = LoadFileInMem<uint16_t>("Levels\\L4Data\\Vile1.DUN");
SetMapMonsters(dunData.get(), Point { setpc_x, setpc_y } * 2);
}
if (currlevel == 24) {
UberDiabloMonsterIndex = -1;
int i1;
for (i1 = 0; i1 < LevelMonsterTypeCount; i1++) {
if (LevelMonsterTypes[i1].mtype == UniqueMonstersData[UMT_NAKRUL].mtype)
break;
}
if (i1 < LevelMonsterTypeCount) {
for (int i2 = 0; i2 < ActiveMonsterCount; i2++) {
auto &monster = Monsters[i2];
if (monster._uniqtype != 0 || monster._mMTidx == i1) {
UberDiabloMonsterIndex = i2;
break;
}
}
}
if (UberDiabloMonsterIndex == -1)
PlaceUniqueMonst(UMT_NAKRUL, 0, 0);
}
} else if (setlvlnum == SL_SKELKING) {
PlaceUniqueMonst(UMT_SKELKING, 0, 0);
}
}
void LoadDiabMonsts()
{
{
auto dunData = LoadFileInMem<uint16_t>("Levels\\L4Data\\diab1.DUN");
SetMapMonsters(dunData.get(), Point { diabquad1x, diabquad1y } * 2);
}
{
auto dunData = LoadFileInMem<uint16_t>("Levels\\L4Data\\diab2a.DUN");
SetMapMonsters(dunData.get(), Point { diabquad2x, diabquad2y } * 2);
}
{
auto dunData = LoadFileInMem<uint16_t>("Levels\\L4Data\\diab3a.DUN");
SetMapMonsters(dunData.get(), Point { diabquad3x, diabquad3y } * 2);
}
{
auto dunData = LoadFileInMem<uint16_t>("Levels\\L4Data\\diab4a.DUN");
SetMapMonsters(dunData.get(), Point { diabquad4x, diabquad4y } * 2);
}
}
void DeleteMonster(int i)
{
const auto &monster = Monsters[ActiveMonsters[i]];
if ((monster._mFlags & MFLAG_BERSERK) != 0) {
AddUnLight(monster.mlid);
}
ActiveMonsterCount--;
std::swap(ActiveMonsters[i], ActiveMonsters[ActiveMonsterCount]); // This ensures alive monsters are before ActiveMonsterCount in the array and any deleted monster after
}
void NewMonsterAnim(Monster &monster, MonsterGraphic graphic, Direction md, AnimationDistributionFlags flags = AnimationDistributionFlags::None, int numSkippedFrames = 0, int distributeFramesBeforeFrame = 0)
{
const auto &animData = monster.MType->GetAnimData(graphic);
monster.AnimInfo.SetNewAnimation(animData.GetCelSpritesForDirection(md), animData.Frames, animData.Rate, flags, numSkippedFrames, distributeFramesBeforeFrame);
monster._mFlags &= ~(MFLAG_LOCK_ANIMATION | MFLAG_ALLOW_SPECIAL);
monster._mdir = md;
}
void StartMonsterGotHit(int monsterId)
{
auto &monster = Monsters[monsterId];
if (monster.MType->mtype != MT_GOLEM) {
auto animationFlags = gGameLogicStep < GameLogicStep::ProcessMonsters ? AnimationDistributionFlags::ProcessAnimationPending : AnimationDistributionFlags::None;
int numSkippedFrames = (gbIsHellfire && monster.MType->mtype == MT_DIABLO) ? 4 : 0;
NewMonsterAnim(monster, MonsterGraphic::GotHit, monster._mdir, animationFlags, numSkippedFrames);
monster._mmode = MonsterMode::HitRecovery;
}
monster.position.offset = { 0, 0 };
monster.position.tile = monster.position.old;
monster.position.future = monster.position.old;
M_ClearSquares(monsterId);
dMonster[monster.position.tile.x][monster.position.tile.y] = monsterId + 1;
}
bool IsRanged(Monster &monster)
{
return IsAnyOf(monster._mAi, AI_SKELBOW, AI_GOATBOW, AI_SUCC, AI_LAZHELP);
}
void UpdateEnemy(Monster &monster)
{
Point target;
int menemy = -1;
int bestDist = -1;
bool bestsameroom = false;
const auto &position = monster.position.tile;
if ((monster._mFlags & MFLAG_BERSERK) != 0 || (monster._mFlags & MFLAG_GOLEM) == 0) {
for (int pnum = 0; pnum < MAX_PLRS; pnum++) {
Player &player = Players[pnum];
if (!player.plractive || currlevel != player.plrlevel || player._pLvlChanging
|| (((player._pHitPoints >> 6) == 0) && gbIsMultiplayer))
continue;
bool sameroom = (dTransVal[position.x][position.y] == dTransVal[player.position.tile.x][player.position.tile.y]);
int dist = position.WalkingDistance(player.position.tile);
if ((sameroom && !bestsameroom)
|| ((sameroom || !bestsameroom) && dist < bestDist)
|| (menemy == -1)) {
monster._mFlags &= ~MFLAG_TARGETS_MONSTER;
menemy = pnum;
target = player.position.future;
bestDist = dist;
bestsameroom = sameroom;
}
}
}
for (int j = 0; j < ActiveMonsterCount; j++) {
int mi = ActiveMonsters[j];
auto &otherMonster = Monsters[mi];
if (&otherMonster == &monster)
continue;
if ((otherMonster._mhitpoints >> 6) <= 0)
continue;
if (otherMonster.position.tile == GolemHoldingCell)
continue;
if (M_Talker(otherMonster) && otherMonster.mtalkmsg != TEXT_NONE)
continue;
bool isBerserked = (monster._mFlags & MFLAG_BERSERK) != 0 || (otherMonster._mFlags & MFLAG_BERSERK) != 0;
if ((monster._mFlags & MFLAG_GOLEM) != 0 && (otherMonster._mFlags & MFLAG_GOLEM) != 0 && !isBerserked) // prevent golems from fighting each other
continue;
int dist = otherMonster.position.tile.WalkingDistance(position);
if (((monster._mFlags & MFLAG_GOLEM) == 0
&& (monster._mFlags & MFLAG_BERSERK) == 0
&& dist >= 2
&& !IsRanged(monster))
|| ((monster._mFlags & MFLAG_GOLEM) == 0
&& (monster._mFlags & MFLAG_BERSERK) == 0
&& (otherMonster._mFlags & MFLAG_GOLEM) == 0)) {
continue;
}
bool sameroom = dTransVal[position.x][position.y] == dTransVal[otherMonster.position.tile.x][otherMonster.position.tile.y];
if ((sameroom && !bestsameroom)
|| ((sameroom || !bestsameroom) && dist < bestDist)
|| (menemy == -1)) {
monster._mFlags |= MFLAG_TARGETS_MONSTER;
menemy = mi;
target = otherMonster.position.future;
bestDist = dist;
bestsameroom = sameroom;
}
}
if (menemy != -1) {
monster._mFlags &= ~MFLAG_NO_ENEMY;
monster._menemy = menemy;
monster.enemyPosition = target;
} else {
monster._mFlags |= MFLAG_NO_ENEMY;
}
}
/**
* @brief Make the AI wait a bit before thinking again
* @param len
*/
void AiDelay(Monster &monster, int len)
{
if (len <= 0) {
return;
}
if (monster._mAi == AI_LAZARUS) {
return;
}
monster._mVar2 = len;
monster._mmode = MonsterMode::Delay;
}
/**
* @brief Get the direction from the monster to its current enemy
*/
Direction GetMonsterDirection(Monster &monster)
{
return GetDirection(monster.position.tile, monster.enemyPosition);
}
void StartSpecialStand(Monster &monster, Direction md)
{
NewMonsterAnim(monster, MonsterGraphic::Special, md);
monster._mmode = MonsterMode::SpecialStand;
monster.position.offset = { 0, 0 };
monster.position.future = monster.position.tile;
monster.position.old = monster.position.tile;
}
void StartWalk(int i, int xvel, int yvel, int xadd, int yadd, Direction endDir)
{
auto &monster = Monsters[i];
int fx = xadd + monster.position.tile.x;
int fy = yadd + monster.position.tile.y;
dMonster[fx][fy] = -(i + 1);
monster._mmode = MonsterMode::MoveNorthwards;
monster.position.old = monster.position.tile;
monster.position.future = { fx, fy };
monster.position.velocity = { xvel, yvel };
monster._mVar1 = xadd;
monster._mVar2 = yadd;
monster._mVar3 = static_cast<int>(endDir);
NewMonsterAnim(monster, MonsterGraphic::Walk, endDir, AnimationDistributionFlags::ProcessAnimationPending, -1);
monster.position.offset2 = { 0, 0 };
}
void StartWalk2(int i, int xvel, int yvel, int xoff, int yoff, int xadd, int yadd, Direction endDir)
{
auto &monster = Monsters[i];
int fx = xadd + monster.position.tile.x;
int fy = yadd + monster.position.tile.y;
dMonster[monster.position.tile.x][monster.position.tile.y] = -(i + 1);
monster._mVar1 = monster.position.tile.x;
monster._mVar2 = monster.position.tile.y;
monster.position.old = monster.position.tile;
monster.position.tile = { fx, fy };
monster.position.future = { fx, fy };
dMonster[fx][fy] = i + 1;
if (monster.mlid != NO_LIGHT)
ChangeLightXY(monster.mlid, monster.position.tile);
monster.position.offset = { xoff, yoff };
monster._mmode = MonsterMode::MoveSouthwards;
monster.position.velocity = { xvel, yvel };
monster._mVar3 = static_cast<int>(endDir);
NewMonsterAnim(monster, MonsterGraphic::Walk, endDir, AnimationDistributionFlags::ProcessAnimationPending, -1);
monster.position.offset2 = { 16 * xoff, 16 * yoff };
}
void StartWalk3(int i, int xvel, int yvel, int xoff, int yoff, int xadd, int yadd, int mapx, int mapy, Direction endDir)
{
auto &monster = Monsters[i];
int fx = xadd + monster.position.tile.x;
int fy = yadd + monster.position.tile.y;
int x = mapx + monster.position.tile.x;
int y = mapy + monster.position.tile.y;
if (monster.mlid != NO_LIGHT)
ChangeLightXY(monster.mlid, { x, y });
dMonster[monster.position.tile.x][monster.position.tile.y] = -(i + 1);
dMonster[fx][fy] = i + 1;
monster.position.temp = { x, y };
monster.position.old = monster.position.tile;
monster.position.future = { fx, fy };
monster.position.offset = { xoff, yoff };
monster._mmode = MonsterMode::MoveSideways;
monster.position.velocity = { xvel, yvel };
monster._mVar1 = fx;
monster._mVar2 = fy;
monster._mVar3 = static_cast<int>(endDir);
NewMonsterAnim(monster, MonsterGraphic::Walk, endDir, AnimationDistributionFlags::ProcessAnimationPending, -1);
monster.position.offset2 = { 16 * xoff, 16 * yoff };
}
void StartAttack(Monster &monster)
{
Direction md = GetMonsterDirection(monster);
NewMonsterAnim(monster, MonsterGraphic::Attack, md, AnimationDistributionFlags::ProcessAnimationPending);
monster._mmode = MonsterMode::MeleeAttack;
monster.position.offset = { 0, 0 };
monster.position.future = monster.position.tile;
monster.position.old = monster.position.tile;
}
void StartRangedAttack(Monster &monster, missile_id missileType, int dam)
{
Direction md = GetMonsterDirection(monster);
NewMonsterAnim(monster, MonsterGraphic::Attack, md, AnimationDistributionFlags::ProcessAnimationPending);
monster._mmode = MonsterMode::RangedAttack;
monster._mVar1 = missileType;
monster._mVar2 = dam;
monster.position.offset = { 0, 0 };
monster.position.future = monster.position.tile;
monster.position.old = monster.position.tile;
}
void StartRangedSpecialAttack(Monster &monster, missile_id missileType, int dam)
{
Direction md = GetMonsterDirection(monster);
int distributeFramesBeforeFrame = 0;
if (monster._mAi == AI_MEGA)
distributeFramesBeforeFrame = monster.MData->mAFNum2;
NewMonsterAnim(monster, MonsterGraphic::Special, md, AnimationDistributionFlags::ProcessAnimationPending, 0, distributeFramesBeforeFrame);
monster._mmode = MonsterMode::SpecialRangedAttack;
monster._mVar1 = missileType;
monster._mVar2 = 0;
monster._mVar3 = dam;
monster.position.offset = { 0, 0 };
monster.position.future = monster.position.tile;
monster.position.old = monster.position.tile;
}
void StartSpecialAttack(Monster &monster)
{
Direction md = GetMonsterDirection(monster);
NewMonsterAnim(monster, MonsterGraphic::Special, md);
monster._mmode = MonsterMode::SpecialMeleeAttack;
monster.position.offset = { 0, 0 };
monster.position.future = monster.position.tile;
monster.position.old = monster.position.tile;
}
void StartEating(Monster &monster)
{
NewMonsterAnim(monster, MonsterGraphic::Special, monster._mdir);
monster._mmode = MonsterMode::SpecialMeleeAttack;
monster.position.offset = { 0, 0 };
monster.position.future = monster.position.tile;
monster.position.old = monster.position.tile;
}
void DiabloDeath(Monster &diablo, bool sendmsg)
{
PlaySFX(USFX_DIABLOD);
auto &quest = Quests[Q_DIABLO];
quest._qactive = QUEST_DONE;
if (sendmsg)
NetSendCmdQuest(true, quest);
sgbSaveSoundOn = gbSoundOn;
gbProcessPlayers = false;
for (int j = 0; j < ActiveMonsterCount; j++) {
int k = ActiveMonsters[j];
auto &monster = Monsters[k];
if (monster.MType->mtype == MT_DIABLO || diablo._msquelch == 0)
continue;
NewMonsterAnim(monster, MonsterGraphic::Death, monster._mdir);
monster._mmode = MonsterMode::Death;
monster.position.offset = { 0, 0 };
monster._mVar1 = 0;
monster.position.tile = monster.position.old;
monster.position.future = monster.position.tile;
M_ClearSquares(k);
dMonster[monster.position.tile.x][monster.position.tile.y] = k + 1;
}
AddLight(diablo.position.tile, 8);
DoVision(diablo.position.tile, 8, MAP_EXP_NONE, true);
int dist = diablo.position.tile.WalkingDistance(ViewPosition);
if (dist > 20)
dist = 20;
diablo._mVar3 = ViewPosition.x << 16;
diablo.position.temp.x = ViewPosition.y << 16;
diablo.position.temp.y = (int)((diablo._mVar3 - (diablo.position.tile.x << 16)) / (double)dist);
diablo.position.offset2.deltaX = (int)((diablo.position.temp.x - (diablo.position.tile.y << 16)) / (double)dist);
}
void SpawnLoot(Monster &monster, bool sendmsg)
{
if (monster.MType->mtype == MT_HORKSPWN) {
return;
}
if (Quests[Q_GARBUD].IsAvailable() && monster._uniqtype - 1 == UMT_GARBUD) {
CreateTypeItem(monster.position.tile + Displacement { 1, 1 }, true, ItemType::Mace, IMISC_NONE, sendmsg, false);
} else if (monster._uniqtype - 1 == UMT_DEFILER) {
if (effect_is_playing(USFX_DEFILER8))
stream_stop();
Quests[Q_DEFILER]._qlog = false;
SpawnMapOfDoom(monster.position.tile, sendmsg);
} else if (monster._uniqtype - 1 == UMT_HORKDMN) {
if (sgGameInitInfo.bTheoQuest != 0) {
SpawnTheodore(monster.position.tile, sendmsg);
} else {
CreateAmulet(monster.position.tile, 13, sendmsg, false);
}
} else if (monster.MType->mtype == MT_NAKRUL) {
int nSFX = IsUberRoomOpened ? USFX_NAKRUL4 : USFX_NAKRUL5;
if (sgGameInitInfo.bCowQuest != 0)
nSFX = USFX_NAKRUL6;
if (effect_is_playing(nSFX))
stream_stop();
Quests[Q_NAKRUL]._qlog = false;
UberDiabloMonsterIndex = -2;
CreateMagicWeapon(monster.position.tile, ItemType::Sword, ICURS_GREAT_SWORD, sendmsg, false);
CreateMagicWeapon(monster.position.tile, ItemType::Staff, ICURS_WAR_STAFF, sendmsg, false);
CreateMagicWeapon(monster.position.tile, ItemType::Bow, ICURS_LONG_WAR_BOW, sendmsg, false);
CreateSpellBook(monster.position.tile, SPL_APOCA, sendmsg, false);
} else if (monster.MType->mtype != MT_GOLEM) {
SpawnItem(monster, monster.position.tile, sendmsg);
}
}
void Teleport(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode == MonsterMode::Petrified)
return;
int mx = monster.enemyPosition.x;
int my = monster.enemyPosition.y;
int rx = 2 * GenerateRnd(2) - 1;
int ry = 2 * GenerateRnd(2) - 1;
bool done = false;
int x;
int y;
for (int j = -1; j <= 1 && !done; j++) {
for (int k = -1; k < 1 && !done; k++) {
if (j != 0 || k != 0) {
x = mx + rx * j;
y = my + ry * k;
if (InDungeonBounds({ x, y }) && x != monster.position.tile.x && y != monster.position.tile.y) {
if (IsTileAvailable(monster, { x, y }))
done = true;
}
}
}
}
if (done) {
M_ClearSquares(i);
dMonster[monster.position.tile.x][monster.position.tile.y] = 0;
dMonster[x][y] = i + 1;
monster.position.old = { x, y };
monster._mdir = GetMonsterDirection(monster);
if (monster.mlid != NO_LIGHT) {
ChangeLightXY(monster.mlid, { x, y });
}
}
}
void MonsterHitMonster(int mid, int i, int dam)
{
assert(mid >= 0 && mid < MAXMONSTERS);
auto &monster = Monsters[mid];
assert(monster.MType != nullptr);
if (i >= 0 && i < MAX_PLRS)
monster.mWhoHit |= 1 << i;
delta_monster_hp(mid, monster._mhitpoints, currlevel);
NetSendCmdMonDmg(false, mid, dam);
PlayEffect(monster, 1);
if (IsAnyOf(monster.MType->mtype, MT_SNEAK, MT_STALKER, MT_UNSEEN, MT_ILLWEAV) || dam >> 6 >= monster.mLevel + 3) {
if (i >= 0)
monster._mdir = Opposite(Monsters[i]._mdir);
if (monster.MType->mtype == MT_BLINK) {
Teleport(mid);
} else if (IsAnyOf(monster.MType->mtype, MT_NSCAV, MT_BSCAV, MT_WSCAV, MT_YSCAV, MT_GRAVEDIG)) {
monster._mgoal = MGOAL_NORMAL;
monster._mgoalvar1 = 0;
monster._mgoalvar2 = 0;
}
if (monster._mmode != MonsterMode::Petrified) {
StartMonsterGotHit(mid);
}
}
}
void MonsterDeath(int mid, int pnum, Direction md, bool sendmsg)
{
assert(mid >= 0 && mid < MAXMONSTERS);
auto &monster = Monsters[mid];
assert(monster.MType != nullptr);
if (pnum < MAX_PLRS) {
if (pnum >= 0)
monster.mWhoHit |= 1 << pnum;
if (monster.MType->mtype != MT_GOLEM)
AddPlrMonstExper(monster.mLevel, monster.mExp, monster.mWhoHit);
}
MonsterKillCounts[monster.MType->mtype]++;
monster._mhitpoints = 0;
SetRndSeed(monster._mRndSeed);
SpawnLoot(monster, sendmsg);
if (monster.MType->mtype == MT_DIABLO)
DiabloDeath(monster, true);
else
PlayEffect(monster, 2);
if (monster._mmode != MonsterMode::Petrified) {
if (monster.MType->mtype == MT_GOLEM)
md = Direction::South;
NewMonsterAnim(monster, MonsterGraphic::Death, md, gGameLogicStep < GameLogicStep::ProcessMonsters ? AnimationDistributionFlags::ProcessAnimationPending : AnimationDistributionFlags::None);
monster._mmode = MonsterMode::Death;
}
monster._mgoal = MGOAL_NONE;
monster._mVar1 = 0;
monster.position.offset = { 0, 0 };
monster.position.tile = monster.position.old;
monster.position.future = monster.position.old;
M_ClearSquares(mid);
dMonster[monster.position.tile.x][monster.position.tile.y] = mid + 1;
CheckQuestKill(monster, sendmsg);
M_FallenFear(monster.position.tile);
if (IsAnyOf(monster.MType->mtype, MT_NACID, MT_RACID, MT_BACID, MT_XACID, MT_SPIDLORD))
AddMissile(monster.position.tile, { 0, 0 }, Direction::South, MIS_ACIDPUD, TARGET_PLAYERS, mid, monster._mint + 1, 0);
}
void StartMonsterDeath(int mid, int pnum, bool sendmsg)
{
assert(mid >= 0 && mid < MAXMONSTERS);
Monster &monster = Monsters[mid];
Direction md = pnum >= 0 ? GetDirection(monster.position.tile, Players[pnum].position.tile) : monster._mdir;
MonsterDeath(mid, pnum, md, sendmsg);
}
void StartDeathFromMonster(int i, int mid)
{
assert(i >= 0 && i < MAXMONSTERS);
Monster &killer = Monsters[i];
assert(mid >= 0 && mid < MAXMONSTERS);
Monster &monster = Monsters[mid];
delta_kill_monster(mid, monster.position.tile, currlevel);
NetSendCmdLocParam1(false, CMD_MONSTDEATH, monster.position.tile, mid);
Direction md = GetDirection(monster.position.tile, killer.position.tile);
MonsterDeath(mid, i, md, true);
if (gbIsHellfire)
M_StartStand(killer, killer._mdir);
}
void StartFadein(Monster &monster, Direction md, bool backwards)
{
NewMonsterAnim(monster, MonsterGraphic::Special, md);
monster._mmode = MonsterMode::FadeIn;
monster.position.offset = { 0, 0 };
monster.position.future = monster.position.tile;
monster.position.old = monster.position.tile;
monster._mFlags &= ~MFLAG_HIDDEN;
if (backwards) {
monster._mFlags |= MFLAG_LOCK_ANIMATION;
monster.AnimInfo.CurrentFrame = monster.AnimInfo.NumberOfFrames - 1;
}
}
void StartFadeout(Monster &monster, Direction md, bool backwards)
{
NewMonsterAnim(monster, MonsterGraphic::Special, md);
monster._mmode = MonsterMode::FadeOut;
monster.position.offset = { 0, 0 };
monster.position.future = monster.position.tile;
monster.position.old = monster.position.tile;
if (backwards) {
monster._mFlags |= MFLAG_LOCK_ANIMATION;
monster.AnimInfo.CurrentFrame = monster.AnimInfo.NumberOfFrames - 1;
}
}
void StartHeal(Monster &monster)
{
monster.ChangeAnimationData(MonsterGraphic::Special);
monster.AnimInfo.CurrentFrame = monster.MType->GetAnimData(MonsterGraphic::Special).Frames - 1;
monster._mFlags |= MFLAG_LOCK_ANIMATION;
monster._mmode = MonsterMode::Heal;
monster._mVar1 = monster._mmaxhp / (16 * (GenerateRnd(5) + 4));
}
void SyncLightPosition(Monster &monster)
{
int lx = (monster.position.offset.deltaX + 2 * monster.position.offset.deltaY) / 8;
int ly = (2 * monster.position.offset.deltaY - monster.position.offset.deltaX) / 8;
if (monster.mlid != NO_LIGHT)
ChangeLightOffset(monster.mlid, { lx, ly });
}
bool MonsterIdle(Monster &monster)
{
if (monster.MType->mtype == MT_GOLEM)
monster.ChangeAnimationData(MonsterGraphic::Walk);
else
monster.ChangeAnimationData(MonsterGraphic::Stand);
if (monster.AnimInfo.CurrentFrame == monster.AnimInfo.NumberOfFrames - 1)
UpdateEnemy(monster);
monster._mVar2++;
return false;
}
/**
* @brief Continue movement towards new tile
*/
bool MonsterWalk(int i, MonsterMode variant)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
assert(monster.MType != nullptr);
// Check if we reached new tile
const bool isAnimationEnd = monster.AnimInfo.CurrentFrame == monster.AnimInfo.NumberOfFrames - 1;
if (isAnimationEnd) {
switch (variant) {
case MonsterMode::MoveNorthwards:
dMonster[monster.position.tile.x][monster.position.tile.y] = 0;
monster.position.tile.x += monster._mVar1;
monster.position.tile.y += monster._mVar2;
dMonster[monster.position.tile.x][monster.position.tile.y] = i + 1;
break;
case MonsterMode::MoveSouthwards:
dMonster[monster._mVar1][monster._mVar2] = 0;
break;
case MonsterMode::MoveSideways:
dMonster[monster.position.tile.x][monster.position.tile.y] = 0;
monster.position.tile = { monster._mVar1, monster._mVar2 };
// dMonster is set here for backwards comparability, without it the monster would be invisible if loaded from a vanilla save.
dMonster[monster.position.tile.x][monster.position.tile.y] = i + 1;
break;
default:
break;
}
if (monster.mlid != NO_LIGHT)
ChangeLightXY(monster.mlid, monster.position.tile);
M_StartStand(monster, monster._mdir);
} else { // We didn't reach new tile so update monster's "sub-tile" position
if (monster.AnimInfo.TickCounterOfCurrentFrame == 0) {
if (monster.AnimInfo.CurrentFrame == 0 && monster.MType->mtype == MT_FLESTHNG)
PlayEffect(monster, 3);
monster.position.offset2 += monster.position.velocity;
monster.position.offset.deltaX = monster.position.offset2.deltaX >> 4;
monster.position.offset.deltaY = monster.position.offset2.deltaY >> 4;
}
}
if (monster.mlid != NO_LIGHT) // BUGFIX: change uniqtype check to mlid check like it is in all other places (fixed)
SyncLightPosition(monster);
return isAnimationEnd;
}
void MonsterAttackMonster(int i, int mid, int hper, int mind, int maxd)
{
assert(mid >= 0 && mid < MAXMONSTERS);
auto &monster = Monsters[mid];
assert(monster.MType != nullptr);
if (!monster.IsPossibleToHit())
return;
int hit = GenerateRnd(100);
if (monster._mmode == MonsterMode::Petrified)
hit = 0;
if (monster.TryLiftGargoyle())
return;
if (hit >= hper)
return;
int dam = (mind + GenerateRnd(maxd - mind + 1)) << 6;
monster._mhitpoints -= dam;
if (monster._mhitpoints >> 6 <= 0) {
StartDeathFromMonster(i, mid);
} else {
MonsterHitMonster(mid, i, dam);
}
}
void CheckReflect(int mon, int pnum, int dam)
{
auto &monster = Monsters[mon];
Player &player = Players[pnum];
player.wReflections--;
if (player.wReflections <= 0)
NetSendCmdParam1(true, CMD_SETREFLECT, 0);
// reflects 20-30% damage
int mdam = dam * (GenerateRnd(10) + 20L) / 100;
monster._mhitpoints -= mdam;
dam = std::max(dam - mdam, 0);
if (monster._mhitpoints >> 6 <= 0)
M_StartKill(mon, pnum);
else
M_StartHit(mon, pnum, mdam);
}
void MonsterAttackPlayer(int i, int pnum, int hit, int minDam, int maxDam)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
assert(monster.MType != nullptr);
if ((monster._mFlags & MFLAG_TARGETS_MONSTER) != 0) {
MonsterAttackMonster(i, pnum, hit, minDam, maxDam);
return;
}
Player &player = Players[pnum];
if (player._pHitPoints >> 6 <= 0 || player._pInvincible || HasAnyOf(player._pSpellFlags, SpellFlag::Etherealize))
return;
if (monster.position.tile.WalkingDistance(player.position.tile) >= 2)
return;
int hper = GenerateRnd(100);
#ifdef _DEBUG
if (DebugGodMode)
hper = 1000;
#endif
int ac = player.GetArmor();
if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::ACAgainstDemons) && monster.MData->mMonstClass == MonsterClass::Demon)
ac += 40;
if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::ACAgainstUndead) && monster.MData->mMonstClass == MonsterClass::Undead)
ac += 20;
hit += 2 * (monster.mLevel - player._pLevel)
+ 30
- ac;
int minhit = 15;
if (currlevel == 14)
minhit = 20;
if (currlevel == 15)
minhit = 25;
if (currlevel == 16)
minhit = 30;
hit = std::max(hit, minhit);
int blkper = 100;
if ((player._pmode == PM_STAND || player._pmode == PM_ATTACK) && player._pBlockFlag) {
blkper = GenerateRnd(100);
}
int blk = player.GetBlockChance() - (monster.mLevel * 2);
blk = clamp(blk, 0, 100);
if (hper >= hit)
return;
if (blkper < blk) {
Direction dir = GetDirection(player.position.tile, monster.position.tile);
StartPlrBlock(pnum, dir);
if (pnum == MyPlayerId && player.wReflections > 0) {
int dam = GenerateRnd(((maxDam - minDam) << 6) + 1) + (minDam << 6);
dam = std::max(dam + (player._pIGetHit << 6), 64);
CheckReflect(i, pnum, dam);
}
return;
}
if (monster.MType->mtype == MT_YZOMBIE && pnum == MyPlayerId) {
if (player._pMaxHP > 64) {
if (player._pMaxHPBase > 64) {
player._pMaxHP -= 64;
if (player._pHitPoints > player._pMaxHP) {
player._pHitPoints = player._pMaxHP;
}
player._pMaxHPBase -= 64;
if (player._pHPBase > player._pMaxHPBase) {
player._pHPBase = player._pMaxHPBase;
}
}
}
}
int dam = (minDam << 6) + GenerateRnd(((maxDam - minDam) << 6) + 1);
dam = std::max(dam + (player._pIGetHit << 6), 64);
if (pnum == MyPlayerId) {
if (player.wReflections > 0)
CheckReflect(i, pnum, dam);
ApplyPlrDamage(pnum, 0, 0, dam);
}
// Reflect can also kill a monster, so make sure the monster is still alive
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::Thorns) && monster._mmode != MonsterMode::Death) {
int mdam = (GenerateRnd(3) + 1) << 6;
monster._mhitpoints -= mdam;
if (monster._mhitpoints >> 6 <= 0)
M_StartKill(i, pnum);
else
M_StartHit(i, pnum, mdam);
}
if ((monster._mFlags & MFLAG_NOLIFESTEAL) == 0 && monster.MType->mtype == MT_SKING && gbIsMultiplayer)
monster._mhitpoints += dam;
if (player._pHitPoints >> 6 <= 0) {
if (gbIsHellfire)
M_StartStand(monster, monster._mdir);
return;
}
StartPlrHit(pnum, dam, false);
if ((monster._mFlags & MFLAG_KNOCKBACK) != 0) {
if (player._pmode != PM_GOTHIT)
StartPlrHit(pnum, 0, true);
Point newPosition = player.position.tile + monster._mdir;
if (PosOkPlayer(player, newPosition)) {
player.position.tile = newPosition;
FixPlayerLocation(pnum, player._pdir);
FixPlrWalkTags(pnum);
dPlayer[newPosition.x][newPosition.y] = pnum + 1;
SetPlayerOld(player);
}
}
}
bool MonsterAttack(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
assert(monster.MType != nullptr);
assert(monster.MData != nullptr);
if (monster.AnimInfo.CurrentFrame == monster.MData->mAFNum - 1) {
MonsterAttackPlayer(i, monster._menemy, monster.mHit, monster.mMinDamage, monster.mMaxDamage);
if (monster._mAi != AI_SNAKE)
PlayEffect(monster, 0);
}
if (IsAnyOf(monster.MType->mtype, MT_NMAGMA, MT_YMAGMA, MT_BMAGMA, MT_WMAGMA) && monster.AnimInfo.CurrentFrame == 8) {
MonsterAttackPlayer(i, monster._menemy, monster.mHit + 10, monster.mMinDamage - 2, monster.mMaxDamage - 2);
PlayEffect(monster, 0);
}
if (IsAnyOf(monster.MType->mtype, MT_STORM, MT_RSTORM, MT_STORML, MT_MAEL) && monster.AnimInfo.CurrentFrame == 12) {
MonsterAttackPlayer(i, monster._menemy, monster.mHit - 20, monster.mMinDamage + 4, monster.mMaxDamage + 4);
PlayEffect(monster, 0);
}
if (monster._mAi == AI_SNAKE && monster.AnimInfo.CurrentFrame == 0)
PlayEffect(monster, 0);
if (monster.AnimInfo.CurrentFrame == monster.AnimInfo.NumberOfFrames - 1) {
M_StartStand(monster, monster._mdir);
return true;
}
return false;
}
bool MonaterRangedAttack(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
assert(monster.MType != nullptr);
assert(monster.MData != nullptr);
if (monster.AnimInfo.CurrentFrame == monster.MData->mAFNum - 1) {
const auto &missileType = static_cast<missile_id>(monster._mVar1);
if (missileType != MIS_NULL) {
int multimissiles = 1;
if (missileType == MIS_CBOLT)
multimissiles = 3;
for (int mi = 0; mi < multimissiles; mi++) {
AddMissile(
monster.position.tile,
monster.enemyPosition,
monster._mdir,
missileType,
TARGET_PLAYERS,
i,
monster._mVar2,
0);
}
}
PlayEffect(monster, 0);
}
if (monster.AnimInfo.CurrentFrame == monster.AnimInfo.NumberOfFrames - 1) {
M_StartStand(monster, monster._mdir);
return true;
}
return false;
}
bool MonsterRangedSpecialAttack(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
assert(monster.MType != nullptr);
assert(monster.MData != nullptr);
if (monster.AnimInfo.CurrentFrame == monster.MData->mAFNum2 - 1 && monster.AnimInfo.TickCounterOfCurrentFrame == 0) {
if (AddMissile(
monster.position.tile,
monster.enemyPosition,
monster._mdir,
static_cast<missile_id>(monster._mVar1),
TARGET_PLAYERS,
i,
monster._mVar3,
0)
!= nullptr) {
PlayEffect(monster, 3);
}
}
if (monster._mAi == AI_MEGA && monster.AnimInfo.CurrentFrame == monster.MData->mAFNum2 - 1) {
if (monster._mVar2++ == 0) {
monster._mFlags |= MFLAG_ALLOW_SPECIAL;
} else if (monster._mVar2 == 15) {
monster._mFlags &= ~MFLAG_ALLOW_SPECIAL;
}
}
if (monster.AnimInfo.CurrentFrame == monster.AnimInfo.NumberOfFrames - 1) {
M_StartStand(monster, monster._mdir);
return true;
}
return false;
}
bool MonsterSpecialAttack(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
assert(monster.MType != nullptr);
assert(monster.MData != nullptr);
if (monster.AnimInfo.CurrentFrame == monster.MData->mAFNum2 - 1)
MonsterAttackPlayer(i, monster._menemy, monster.mHit2, monster.mMinDamage2, monster.mMaxDamage2);
if (monster.AnimInfo.CurrentFrame == monster.AnimInfo.NumberOfFrames - 1) {
M_StartStand(monster, monster._mdir);
return true;
}
return false;
}
bool MonsterFadein(Monster &monster)
{
if (((monster._mFlags & MFLAG_LOCK_ANIMATION) == 0 || monster.AnimInfo.CurrentFrame != 0)
&& ((monster._mFlags & MFLAG_LOCK_ANIMATION) != 0 || monster.AnimInfo.CurrentFrame != monster.AnimInfo.NumberOfFrames - 1)) {
return false;
}
M_StartStand(monster, monster._mdir);
monster._mFlags &= ~MFLAG_LOCK_ANIMATION;
return true;
}
bool MonsterFadeout(Monster &monster)
{
if (((monster._mFlags & MFLAG_LOCK_ANIMATION) == 0 || monster.AnimInfo.CurrentFrame != 0)
&& ((monster._mFlags & MFLAG_LOCK_ANIMATION) != 0 || monster.AnimInfo.CurrentFrame != monster.AnimInfo.NumberOfFrames - 1)) {
return false;
}
monster._mFlags &= ~MFLAG_LOCK_ANIMATION;
monster._mFlags |= MFLAG_HIDDEN;
M_StartStand(monster, monster._mdir);
return true;
}
bool MonsterHeal(Monster &monster)
{
if ((monster._mFlags & MFLAG_NOHEAL) != 0) {
monster._mFlags &= ~MFLAG_ALLOW_SPECIAL;
monster._mmode = MonsterMode::SpecialMeleeAttack;
return false;
}
if (monster.AnimInfo.CurrentFrame == 0) {
monster._mFlags &= ~MFLAG_LOCK_ANIMATION;
monster._mFlags |= MFLAG_ALLOW_SPECIAL;
if (monster._mVar1 + monster._mhitpoints < monster._mmaxhp) {
monster._mhitpoints = monster._mVar1 + monster._mhitpoints;
} else {
monster._mhitpoints = monster._mmaxhp;
monster._mFlags &= ~MFLAG_ALLOW_SPECIAL;
monster._mmode = MonsterMode::SpecialMeleeAttack;
}
}
return false;
}
bool MonsterTalk(Monster &monster)
{
M_StartStand(monster, monster._mdir);
monster._mgoal = MGOAL_TALKING;
if (effect_is_playing(Speeches[monster.mtalkmsg].sfxnr))
return false;
InitQTextMsg(monster.mtalkmsg);
if (monster._uniqtype - 1 == UMT_GARBUD) {
if (monster.mtalkmsg == TEXT_GARBUD1) {
Quests[Q_GARBUD]._qactive = QUEST_ACTIVE;
Quests[Q_GARBUD]._qlog = true; // BUGFIX: (?) for other quests qactive and qlog go together, maybe this should actually go into the if above (fixed)
}
if (monster.mtalkmsg == TEXT_GARBUD2 && (monster._mFlags & MFLAG_QUEST_COMPLETE) == 0) {
SpawnItem(monster, monster.position.tile + Displacement { 1, 1 }, true);
monster._mFlags |= MFLAG_QUEST_COMPLETE;
}
}
if (monster._uniqtype - 1 == UMT_ZHAR
&& monster.mtalkmsg == TEXT_ZHAR1
&& (monster._mFlags & MFLAG_QUEST_COMPLETE) == 0) {
Quests[Q_ZHAR]._qactive = QUEST_ACTIVE;
Quests[Q_ZHAR]._qlog = true;
CreateTypeItem(monster.position.tile + Displacement { 1, 1 }, false, ItemType::Misc, IMISC_BOOK, true, false);
monster._mFlags |= MFLAG_QUEST_COMPLETE;
}
if (monster._uniqtype - 1 == UMT_SNOTSPIL) {
if (monster.mtalkmsg == TEXT_BANNER10 && (monster._mFlags & MFLAG_QUEST_COMPLETE) == 0) {
ObjChangeMap(setpc_x, setpc_y, (setpc_w / 2) + setpc_x + 2, (setpc_h / 2) + setpc_y - 2);
auto tren = TransVal;
TransVal = 9;
DRLG_MRectTrans(setpc_x, setpc_y, (setpc_w / 2) + setpc_x + 4, setpc_y + (setpc_h / 2));
TransVal = tren;
Quests[Q_LTBANNER]._qvar1 = 2;
if (Quests[Q_LTBANNER]._qactive == QUEST_INIT)
Quests[Q_LTBANNER]._qactive = QUEST_ACTIVE;
monster._mFlags |= MFLAG_QUEST_COMPLETE;
}
if (Quests[Q_LTBANNER]._qvar1 < 2) {
app_fatal("SS Talk = %i, Flags = %i", monster.mtalkmsg, monster._mFlags);
}
}
if (monster._uniqtype - 1 == UMT_LACHDAN) {
if (monster.mtalkmsg == TEXT_VEIL9) {
Quests[Q_VEIL]._qactive = QUEST_ACTIVE;
Quests[Q_VEIL]._qlog = true;
}
if (monster.mtalkmsg == TEXT_VEIL11 && (monster._mFlags & MFLAG_QUEST_COMPLETE) == 0) {
SpawnUnique(UITEM_STEELVEIL, monster.position.tile + Direction::South);
monster._mFlags |= MFLAG_QUEST_COMPLETE;
}
}
if (monster._uniqtype - 1 == UMT_WARLORD)
Quests[Q_WARLORD]._qvar1 = 2;
if (monster._uniqtype - 1 == UMT_LAZARUS && gbIsMultiplayer) {
Quests[Q_BETRAYER]._qvar1 = 6;
monster._mgoal = MGOAL_NORMAL;
monster._msquelch = UINT8_MAX;
monster.mtalkmsg = TEXT_NONE;
}
return false;
}
bool MonsterGotHit(Monster &monster)
{
if (monster.AnimInfo.CurrentFrame == monster.AnimInfo.NumberOfFrames - 1) {
M_StartStand(monster, monster._mdir);
return true;
}
return false;
}
bool MonsterDeath(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
assert(monster.MType != nullptr);
monster._mVar1++;
if (monster.MType->mtype == MT_DIABLO) {
if (monster.position.tile.x < ViewPosition.x) {
ViewPosition.x--;
} else if (monster.position.tile.x > ViewPosition.x) {
ViewPosition.x++;
}
if (monster.position.tile.y < ViewPosition.y) {
ViewPosition.y--;
} else if (monster.position.tile.y > ViewPosition.y) {
ViewPosition.y++;
}
if (monster._mVar1 == 140)
PrepDoEnding();
} else if (monster.AnimInfo.CurrentFrame == monster.AnimInfo.NumberOfFrames - 1) {
if (monster._uniqtype == 0)
AddCorpse(monster.position.tile, monster.MType->mdeadval, monster._mdir);
else
AddCorpse(monster.position.tile, monster._udeadval, monster._mdir);
dMonster[monster.position.tile.x][monster.position.tile.y] = 0;
monster._mDelFlag = true;
M_UpdateLeader(i);
}
return false;
}
bool MonsterSpecialStand(Monster &monster)
{
if (monster.AnimInfo.CurrentFrame == monster.MData->mAFNum2 - 1)
PlayEffect(monster, 3);
if (monster.AnimInfo.CurrentFrame == monster.AnimInfo.NumberOfFrames - 1) {
M_StartStand(monster, monster._mdir);
return true;
}
return false;
}
bool MonsterDelay(Monster &monster)
{
monster.ChangeAnimationData(MonsterGraphic::Stand, GetMonsterDirection(monster));
if (monster._mAi == AI_LAZARUS) {
if (monster._mVar2 > 8 || monster._mVar2 < 0)
monster._mVar2 = 8;
}
if (monster._mVar2-- == 0) {
int oFrame = monster.AnimInfo.CurrentFrame;
M_StartStand(monster, monster._mdir);
monster.AnimInfo.CurrentFrame = oFrame;
return true;
}
return false;
}
bool MonsterPetrified(Monster &monster)
{
if (monster._mhitpoints <= 0) {
dMonster[monster.position.tile.x][monster.position.tile.y] = 0;
monster._mDelFlag = true;
}
return false;
}
int AddSkeleton(Point position, Direction dir, bool inMap)
{
int j = 0;
for (int i = 0; i < LevelMonsterTypeCount; i++) {
if (IsSkel(LevelMonsterTypes[i].mtype))
j++;
}
if (j == 0) {
return -1;
}
int skeltypes = GenerateRnd(j);
int m = 0;
for (int i = 0; m < LevelMonsterTypeCount && i <= skeltypes; m++) {
if (IsSkel(LevelMonsterTypes[m].mtype))
i++;
}
return AddMonster(position, dir, m - 1, inMap);
}
void SpawnSkeleton(Point position, Direction dir)
{
int skel = AddSkeleton(position, dir, true);
if (skel != -1)
StartSpecialStand(Monsters[skel], dir);
}
bool IsLineNotSolid(Point startPoint, Point endPoint)
{
return LineClear(IsTileNotSolid, startPoint, endPoint);
}
void FollowTheLeader(Monster &monster)
{
if (monster.leader == 0)
return;
if (monster.leaderRelation != LeaderRelation::Leashed)
return;
auto &leader = Monsters[monster.leader];
if (monster._msquelch >= leader._msquelch)
return;
monster.position.last = leader.position.tile;
monster._msquelch = leader._msquelch - 1;
}
void GroupUnity(Monster &monster)
{
if (monster.leaderRelation == LeaderRelation::None)
return;
// Someone with a leaderRelation should have a leader ...
assert(monster.leader >= 0);
// And no unique monster would be a minion of someone else!
assert(monster._uniqtype == 0);
auto &leader = Monsters[monster.leader];
if (IsLineNotSolid(monster.position.tile, leader.position.future)) {
if (monster.leaderRelation == LeaderRelation::Separated
&& monster.position.tile.WalkingDistance(leader.position.future) < 4) {
// Reunite the separated monster with the pack
leader.packsize++;
monster.leaderRelation = LeaderRelation::Leashed;
}
} else if (monster.leaderRelation == LeaderRelation::Leashed) {
leader.packsize--;
monster.leaderRelation = LeaderRelation::Separated;
}
if (monster.leaderRelation == LeaderRelation::Leashed) {
if (monster._msquelch > leader._msquelch) {
leader.position.last = monster.position.tile;
leader._msquelch = monster._msquelch - 1;
}
if (leader._mAi == AI_GARG && (leader._mFlags & MFLAG_ALLOW_SPECIAL) != 0) {
leader._mFlags &= ~MFLAG_ALLOW_SPECIAL;
leader._mmode = MonsterMode::SpecialMeleeAttack;
}
}
}
bool RandomWalk(int i, Direction md)
{
Direction mdtemp = md;
bool ok = DirOK(i, md);
if (GenerateRnd(2) != 0)
ok = ok || (md = Left(mdtemp), DirOK(i, md)) || (md = Right(mdtemp), DirOK(i, md));
else
ok = ok || (md = Right(mdtemp), DirOK(i, md)) || (md = Left(mdtemp), DirOK(i, md));
if (GenerateRnd(2) != 0) {
ok = ok
|| (md = Right(Right(mdtemp)), DirOK(i, md))
|| (md = Left(Left(mdtemp)), DirOK(i, md));
} else {
ok = ok
|| (md = Left(Left(mdtemp)), DirOK(i, md))
|| (md = Right(Right(mdtemp)), DirOK(i, md));
}
if (ok)
M_WalkDir(i, md);
return ok;
}
bool RandomWalk2(int i, Direction md)
{
Direction mdtemp = md;
bool ok = DirOK(i, md); // Can we continue in the same direction
if (GenerateRnd(2) != 0) { // Randomly go left or right
ok = ok || (mdtemp = Left(md), DirOK(i, Left(md))) || (mdtemp = Right(md), DirOK(i, Right(md)));
} else {
ok = ok || (mdtemp = Right(md), DirOK(i, Right(md))) || (mdtemp = Left(md), DirOK(i, Left(md)));
}
if (ok)
M_WalkDir(i, mdtemp);
return ok;
}
/**
* @brief Check if a tile is affected by a spell we are vunerable to
*/
bool IsTileSafe(const Monster &monster, Point position)
{
if (!TileContainsMissile(position)) {
return true;
}
bool fearsFire = (monster.mMagicRes & IMMUNE_FIRE) == 0 || monster.MType->mtype == MT_DIABLO;
bool fearsLightning = (monster.mMagicRes & IMMUNE_LIGHTNING) == 0 || monster.MType->mtype == MT_DIABLO;
for (auto &missile : Missiles) {
if (missile.position.tile == position) {
if (fearsFire && missile._mitype == MIS_FIREWALL) {
return false;
}
if (fearsLightning && missile._mitype == MIS_LIGHTWALL) {
return false;
}
}
}
return true;
}
/**
* @brief Check that the given tile is not currently blocked
*/
bool IsTileAvailable(Point position)
{
if (dPlayer[position.x][position.y] != 0 || dMonster[position.x][position.y] != 0)
return false;
if (!IsTileWalkable(position))
return false;
return true;
}
/**
* @brief If a monster can access the given tile (possibly by opening a door)
*/
bool IsTileAccessible(const Monster &monster, Point position)
{
if (dPlayer[position.x][position.y] != 0 || dMonster[position.x][position.y] != 0)
return false;
if (!IsTileWalkable(position, (monster._mFlags & MFLAG_CAN_OPEN_DOOR) != 0))
return false;
return IsTileSafe(monster, position);
}
bool AiPlanWalk(int i)
{
int8_t path[MAX_PATH_LENGTH];
/** Maps from walking path step to facing direction. */
const Direction plr2monst[9] = { Direction::South, Direction::NorthEast, Direction::NorthWest, Direction::SouthEast, Direction::SouthWest, Direction::North, Direction::East, Direction::South, Direction::West };
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (FindPath([&monster](Point position) { return IsTileAccessible(monster, position); }, monster.position.tile, monster.enemyPosition, path) == 0) {
return false;
}
RandomWalk(i, plr2monst[path[0]]);
return true;
}
bool DumbWalk(int i, Direction md)
{
bool ok = DirOK(i, md);
if (ok)
M_WalkDir(i, md);
return ok;
}
Direction Turn(Direction direction, bool turnLeft)
{
return turnLeft ? Left(direction) : Right(direction);
}
bool RoundWalk(int i, Direction direction, int *dir)
{
Direction turn45deg = Turn(direction, *dir != 0);
Direction turn90deg = Turn(turn45deg, *dir != 0);
if (DirOK(i, turn90deg)) {
// Turn 90 degrees
M_WalkDir(i, turn90deg);
return true;
}
if (DirOK(i, turn45deg)) {
// Only do a small turn
M_WalkDir(i, turn45deg);
return true;
}
if (DirOK(i, direction)) {
// Continue straight
M_WalkDir(i, direction);
return true;
}
// Try 90 degrees in the opposite than desired direction
*dir = (*dir == 0) ? 1 : 0;
return RandomWalk(i, Opposite(turn90deg));
}
bool AiPlanPath(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster.MType->mtype != MT_GOLEM) {
if (monster._msquelch == 0)
return false;
if (monster._mmode != MonsterMode::Stand)
return false;
if (monster._mgoal != MGOAL_NORMAL && monster._mgoal != MGOAL_MOVE && monster._mgoal != MGOAL_ATTACK2)
return false;
if (monster.position.tile.x == 1 && monster.position.tile.y == 0)
return false;
}
bool clear = LineClear(
[&monster](Point position) { return IsTileAvailable(monster, position); },
monster.position.tile,
monster.enemyPosition);
if (!clear || (monster._pathcount >= 5 && monster._pathcount < 8)) {
if ((monster._mFlags & MFLAG_CAN_OPEN_DOOR) != 0)
MonstCheckDoors(monster);
monster._pathcount++;
if (monster._pathcount < 5)
return false;
if (AiPlanWalk(i))
return true;
}
if (monster.MType->mtype != MT_GOLEM)
monster._pathcount = 0;
return false;
}
void AiAvoidance(int i, bool special)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
int fx = monster.enemyPosition.x;
int fy = monster.enemyPosition.y;
int mx = monster.position.tile.x - fx;
int my = monster.position.tile.y - fy;
Direction md = GetDirection(monster.position.tile, monster.position.last);
if (monster._msquelch < UINT8_MAX)
MonstCheckDoors(monster);
int v = GenerateRnd(100);
if ((abs(mx) >= 2 || abs(my) >= 2) && monster._msquelch == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[fx][fy]) {
if (monster._mgoal == MGOAL_MOVE || ((abs(mx) >= 4 || abs(my) >= 4) && GenerateRnd(4) == 0)) {
if (monster._mgoal != MGOAL_MOVE) {
monster._mgoalvar1 = 0;
monster._mgoalvar2 = GenerateRnd(2);
}
monster._mgoal = MGOAL_MOVE;
int dist = std::max(abs(mx), abs(my));
if ((monster._mgoalvar1++ >= 2 * dist && DirOK(i, md)) || dTransVal[monster.position.tile.x][monster.position.tile.y] != dTransVal[fx][fy]) {
monster._mgoal = MGOAL_NORMAL;
} else if (!RoundWalk(i, md, &monster._mgoalvar2)) {
AiDelay(monster, GenerateRnd(10) + 10);
}
}
} else {
monster._mgoal = MGOAL_NORMAL;
}
if (monster._mgoal == MGOAL_NORMAL) {
if (abs(mx) >= 2 || abs(my) >= 2) {
if ((monster._mVar2 > 20 && v < 2 * monster._mint + 28)
|| (IsAnyOf(static_cast<MonsterMode>(monster._mVar1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways)
&& monster._mVar2 == 0
&& v < 2 * monster._mint + 78)) {
RandomWalk(i, md);
}
} else if (v < 2 * monster._mint + 23) {
monster._mdir = md;
if (special && monster._mhitpoints < (monster._mmaxhp / 2) && GenerateRnd(2) != 0)
StartSpecialAttack(monster);
else
StartAttack(monster);
}
}
monster.CheckStandAnimationIsLoaded(md);
}
void AiRanged(int i, missile_id missileType, bool special)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand) {
return;
}
if (monster._msquelch == UINT8_MAX || (monster._mFlags & MFLAG_TARGETS_MONSTER) != 0) {
int fx = monster.enemyPosition.x;
int fy = monster.enemyPosition.y;
int mx = monster.position.tile.x - fx;
int my = monster.position.tile.y - fy;
Direction md = GetMonsterDirection(monster);
if (monster._msquelch < UINT8_MAX)
MonstCheckDoors(monster);
monster._mdir = md;
if (static_cast<MonsterMode>(monster._mVar1) == MonsterMode::RangedAttack) {
AiDelay(monster, GenerateRnd(20));
} else if (abs(mx) < 4 && abs(my) < 4) {
if (GenerateRnd(100) < 10 * (monster._mint + 7))
RandomWalk(i, Opposite(md));
}
if (monster._mmode == MonsterMode::Stand) {
if (LineClearMissile(monster.position.tile, { fx, fy })) {
if (special)
StartRangedSpecialAttack(monster, missileType, 4);
else
StartRangedAttack(monster, missileType, 4);
} else {
monster.CheckStandAnimationIsLoaded(md);
}
}
return;
}
if (monster._msquelch != 0) {
int fx = monster.position.last.x;
int fy = monster.position.last.y;
Direction md = GetDirection(monster.position.tile, { fx, fy });
RandomWalk(i, md);
}
}
void AiRangedAvoidance(int i, missile_id missileType, bool checkdoors, int dam, int lessmissiles)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
int fx = monster.enemyPosition.x;
int fy = monster.enemyPosition.y;
int mx = monster.position.tile.x - fx;
int my = monster.position.tile.y - fy;
Direction md = GetDirection(monster.position.tile, monster.position.last);
if (checkdoors && monster._msquelch < UINT8_MAX)
MonstCheckDoors(monster);
int v = GenerateRnd(10000);
int dist = std::max(abs(mx), abs(my));
if (dist >= 2 && monster._msquelch == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[fx][fy]) {
if (monster._mgoal == MGOAL_MOVE || (dist >= 3 && GenerateRnd(4 << lessmissiles) == 0)) {
if (monster._mgoal != MGOAL_MOVE) {
monster._mgoalvar1 = 0;
monster._mgoalvar2 = GenerateRnd(2);
}
monster._mgoal = MGOAL_MOVE;
if (monster._mgoalvar1++ >= 2 * dist && DirOK(i, md)) {
monster._mgoal = MGOAL_NORMAL;
} else if (v < (500 * (monster._mint + 1) >> lessmissiles)
&& (LineClearMissile(monster.position.tile, { fx, fy }))) {
StartRangedSpecialAttack(monster, missileType, dam);
} else {
RoundWalk(i, md, &monster._mgoalvar2);
}
}
} else {
monster._mgoal = MGOAL_NORMAL;
}
if (monster._mgoal == MGOAL_NORMAL) {
if (((dist >= 3 && v < ((500 * (monster._mint + 2)) >> lessmissiles))
|| v < ((500 * (monster._mint + 1)) >> lessmissiles))
&& LineClearMissile(monster.position.tile, { fx, fy })) {
StartRangedSpecialAttack(monster, missileType, dam);
} else if (dist >= 2) {
v = GenerateRnd(100);
if (v < 1000 * (monster._mint + 5)
|| (IsAnyOf(static_cast<MonsterMode>(monster._mVar1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways) && monster._mVar2 == 0 && v < 1000 * (monster._mint + 8))) {
RandomWalk(i, md);
}
} else if (v < 1000 * (monster._mint + 6)) {
monster._mdir = md;
StartAttack(monster);
}
}
if (monster._mmode == MonsterMode::Stand) {
AiDelay(monster, GenerateRnd(10) + 5);
}
}
void ZombieAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand) {
return;
}
if (!IsTileVisible(monster.position.tile)) {
return;
}
if (GenerateRnd(100) < 2 * monster._mint + 10) {
int dist = monster.enemyPosition.WalkingDistance(monster.position.tile);
if (dist >= 2) {
if (dist >= 2 * monster._mint + 4) {
Direction md = monster._mdir;
if (GenerateRnd(100) < 2 * monster._mint + 20) {
md = static_cast<Direction>(GenerateRnd(8));
}
DumbWalk(i, md);
} else {
RandomWalk(i, GetMonsterDirection(monster));
}
} else {
StartAttack(monster);
}
}
monster.CheckStandAnimationIsLoaded(monster._mdir);
}
void OverlordAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
int mx = monster.position.tile.x - monster.enemyPosition.x;
int my = monster.position.tile.y - monster.enemyPosition.y;
Direction md = GetMonsterDirection(monster);
monster._mdir = md;
int v = GenerateRnd(100);
if (abs(mx) >= 2 || abs(my) >= 2) {
if ((monster._mVar2 > 20 && v < 4 * monster._mint + 20)
|| (IsAnyOf(static_cast<MonsterMode>(monster._mVar1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways)
&& monster._mVar2 == 0
&& v < 4 * monster._mint + 70)) {
RandomWalk(i, md);
}
} else if (v < 4 * monster._mint + 15) {
StartAttack(monster);
} else if (v < 4 * monster._mint + 20) {
StartSpecialAttack(monster);
}
monster.CheckStandAnimationIsLoaded(md);
}
void SkeletonAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
int x = monster.position.tile.x - monster.enemyPosition.x;
int y = monster.position.tile.y - monster.enemyPosition.y;
Direction md = GetDirection(monster.position.tile, monster.position.last);
monster._mdir = md;
if (abs(x) >= 2 || abs(y) >= 2) {
if (static_cast<MonsterMode>(monster._mVar1) == MonsterMode::Delay || (GenerateRnd(100) >= 35 - 4 * monster._mint)) {
RandomWalk(i, md);
} else {
AiDelay(monster, 15 - 2 * monster._mint + GenerateRnd(10));
}
} else {
if (static_cast<MonsterMode>(monster._mVar1) == MonsterMode::Delay || (GenerateRnd(100) < 2 * monster._mint + 20)) {
StartAttack(monster);
} else {
AiDelay(monster, 2 * (5 - monster._mint) + GenerateRnd(10));
}
}
monster.CheckStandAnimationIsLoaded(md);
}
void SkeletonBowAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
int mx = monster.position.tile.x - monster.enemyPosition.x;
int my = monster.position.tile.y - monster.enemyPosition.y;
Direction md = GetMonsterDirection(monster);
monster._mdir = md;
int v = GenerateRnd(100);
bool walking = false;
if (abs(mx) < 4 && abs(my) < 4) {
if ((monster._mVar2 > 20 && v < 2 * monster._mint + 13)
|| (IsAnyOf(static_cast<MonsterMode>(monster._mVar1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways)
&& monster._mVar2 == 0
&& v < 2 * monster._mint + 63)) {
walking = DumbWalk(i, Opposite(md));
}
}
if (!walking) {
if (GenerateRnd(100) < 2 * monster._mint + 3) {
if (LineClearMissile(monster.position.tile, monster.enemyPosition))
StartRangedAttack(monster, MIS_ARROW, 4);
}
}
monster.CheckStandAnimationIsLoaded(md);
}
void ScavengerAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand)
return;
if (monster._mhitpoints < (monster._mmaxhp / 2) && monster._mgoal != MGOAL_HEALING) {
if (monster.leaderRelation != LeaderRelation::None) {
if (monster.leaderRelation == LeaderRelation::Leashed)
Monsters[monster.leader].packsize--;
monster.leaderRelation = LeaderRelation::None;
}
monster._mgoal = MGOAL_HEALING;
monster._mgoalvar3 = 10;
}
if (monster._mgoal == MGOAL_HEALING && monster._mgoalvar3 != 0) {
monster._mgoalvar3--;
if (dCorpse[monster.position.tile.x][monster.position.tile.y] != 0) {
StartEating(monster);
if ((monster._mFlags & MFLAG_NOHEAL) == 0) {
if (gbIsHellfire) {
int mMaxHP = monster._mmaxhp; // BUGFIX use _mmaxhp or we loose health when difficulty isn't normal (fixed)
monster._mhitpoints += mMaxHP / 8;
if (monster._mhitpoints > monster._mmaxhp)
monster._mhitpoints = monster._mmaxhp;
if (monster._mgoalvar3 <= 0 || monster._mhitpoints == monster._mmaxhp)
dCorpse[monster.position.tile.x][monster.position.tile.y] = 0;
} else {
monster._mhitpoints += 64;
}
}
int targetHealth = monster._mmaxhp;
if (!gbIsHellfire)
targetHealth = (monster._mmaxhp / 2) + (monster._mmaxhp / 4);
if (monster._mhitpoints >= targetHealth) {
monster._mgoal = MGOAL_NORMAL;
monster._mgoalvar1 = 0;
monster._mgoalvar2 = 0;
}
} else {
if (monster._mgoalvar1 == 0) {
bool done = false;
int x;
int y;
if (GenerateRnd(2) != 0) {
for (y = -4; y <= 4 && !done; y++) {
for (x = -4; x <= 4 && !done; x++) {
// BUGFIX: incorrect check of offset against limits of the dungeon (fixed)
if (!InDungeonBounds(monster.position.tile + Displacement { x, y }))
continue;
done = dCorpse[monster.position.tile.x + x][monster.position.tile.y + y] != 0
&& IsLineNotSolid(
monster.position.tile,
monster.position.tile + Displacement { x, y });
}
}
x--;
y--;
} else {
for (y = 4; y >= -4 && !done; y--) {
for (x = 4; x >= -4 && !done; x--) {
// BUGFIX: incorrect check of offset against limits of the dungeon (fixed)
if (!InDungeonBounds(monster.position.tile + Displacement { x, y }))
continue;
done = dCorpse[monster.position.tile.x + x][monster.position.tile.y + y] != 0
&& IsLineNotSolid(
monster.position.tile,
monster.position.tile + Displacement { x, y });
}
}
x++;
y++;
}
if (done) {
monster._mgoalvar1 = x + monster.position.tile.x + 1;
monster._mgoalvar2 = y + monster.position.tile.y + 1;
}
}
if (monster._mgoalvar1 != 0) {
int x = monster._mgoalvar1 - 1;
int y = monster._mgoalvar2 - 1;
monster._mdir = GetDirection(monster.position.tile, { x, y });
RandomWalk(i, monster._mdir);
}
}
}
if (monster._mmode == MonsterMode::Stand)
SkeletonAi(i);
}
void RhinoAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
int fx = monster.enemyPosition.x;
int fy = monster.enemyPosition.y;
int mx = monster.position.tile.x - fx;
int my = monster.position.tile.y - fy;
Direction md = GetDirection(monster.position.tile, monster.position.last);
if (monster._msquelch < UINT8_MAX)
MonstCheckDoors(monster);
int v = GenerateRnd(100);
int dist = std::max(abs(mx), abs(my));
if (dist >= 2) {
if (monster._mgoal == MGOAL_MOVE || (dist >= 5 && GenerateRnd(4) != 0)) {
if (monster._mgoal != MGOAL_MOVE) {
monster._mgoalvar1 = 0;
monster._mgoalvar2 = GenerateRnd(2);
}
monster._mgoal = MGOAL_MOVE;
if (monster._mgoalvar1++ >= 2 * dist || dTransVal[monster.position.tile.x][monster.position.tile.y] != dTransVal[fx][fy]) {
monster._mgoal = MGOAL_NORMAL;
} else if (!RoundWalk(i, md, &monster._mgoalvar2)) {
AiDelay(monster, GenerateRnd(10) + 10);
}
}
} else {
monster._mgoal = MGOAL_NORMAL;
}
if (monster._mgoal == MGOAL_NORMAL) {
if (dist >= 5
&& v < 2 * monster._mint + 43
&& LineClear([&monster](Point position) { return IsTileAvailable(monster, position); }, monster.position.tile, { fx, fy })) {
if (AddMissile(monster.position.tile, { fx, fy }, md, MIS_RHINO, TARGET_PLAYERS, i, 0, 0) != nullptr) {
if (monster.MData->snd_special)
PlayEffect(monster, 3);
dMonster[monster.position.tile.x][monster.position.tile.y] = -(i + 1);
monster._mmode = MonsterMode::Charge;
}
} else {
if (dist >= 2) {
v = GenerateRnd(100);
if (v >= 2 * monster._mint + 33
&& (IsNoneOf(static_cast<MonsterMode>(monster._mVar1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways)
|| monster._mVar2 != 0
|| v >= 2 * monster._mint + 83)) {
AiDelay(monster, GenerateRnd(10) + 10);
} else {
RandomWalk(i, md);
}
} else if (v < 2 * monster._mint + 28) {
monster._mdir = md;
StartAttack(monster);
}
}
}
monster.CheckStandAnimationIsLoaded(monster._mdir);
}
void GoatAi(int i)
{
AiAvoidance(i, true);
}
void GoatBowAi(int i)
{
AiRanged(i, MIS_ARROW, false);
}
void FallenAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mgoal == MGOAL_ATTACK2) {
if (monster._mgoalvar1 != 0)
monster._mgoalvar1--;
else
monster._mgoal = MGOAL_NORMAL;
}
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
if (monster._mgoal == MGOAL_RETREAT) {
if (monster._mgoalvar1-- == 0) {
monster._mgoal = MGOAL_NORMAL;
M_StartStand(monster, Opposite(static_cast<Direction>(monster._mgoalvar2)));
}
}
if (monster.AnimInfo.CurrentFrame == monster.AnimInfo.NumberOfFrames - 1) {
if (GenerateRnd(4) != 0) {
return;
}
if ((monster._mFlags & MFLAG_NOHEAL) == 0) {
StartSpecialStand(monster, monster._mdir);
if (monster._mmaxhp - (2 * monster._mint + 2) >= monster._mhitpoints)
monster._mhitpoints += 2 * monster._mint + 2;
else
monster._mhitpoints = monster._mmaxhp;
}
int rad = 2 * monster._mint + 4;
for (int y = -rad; y <= rad; y++) {
for (int x = -rad; x <= rad; x++) {
int xpos = monster.position.tile.x + x;
int ypos = monster.position.tile.y + y;
if (InDungeonBounds({ x, y })) {
int m = dMonster[xpos][ypos];
if (m <= 0)
continue;
auto &otherMonster = Monsters[m - 1];
if (otherMonster._mAi != AI_FALLEN)
continue;
otherMonster._mgoal = MGOAL_ATTACK2;
otherMonster._mgoalvar1 = 30 * monster._mint + 105;
}
}
}
} else if (monster._mgoal == MGOAL_RETREAT) {
RandomWalk(i, monster._mdir);
} else if (monster._mgoal == MGOAL_ATTACK2) {
int xpos = monster.position.tile.x - monster.enemyPosition.x;
int ypos = monster.position.tile.y - monster.enemyPosition.y;
if (abs(xpos) < 2 && abs(ypos) < 2)
StartAttack(monster);
else
RandomWalk(i, GetMonsterDirection(monster));
} else
SkeletonAi(i);
}
void MagmaAi(int i)
{
AiRangedAvoidance(i, MIS_MAGMABALL, true, 4, 0);
}
void LeoricAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
int fx = monster.enemyPosition.x;
int fy = monster.enemyPosition.y;
int mx = monster.position.tile.x - fx;
int my = monster.position.tile.y - fy;
Direction md = GetDirection(monster.position.tile, monster.position.last);
if (monster._msquelch < UINT8_MAX)
MonstCheckDoors(monster);
int v = GenerateRnd(100);
int dist = std::max(abs(mx), abs(my));
if (dist >= 2 && monster._msquelch == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[fx][fy]) {
if (monster._mgoal == MGOAL_MOVE || ((abs(mx) >= 3 || abs(my) >= 3) && GenerateRnd(4) == 0)) {
if (monster._mgoal != MGOAL_MOVE) {
monster._mgoalvar1 = 0;
monster._mgoalvar2 = GenerateRnd(2);
}
monster._mgoal = MGOAL_MOVE;
if ((monster._mgoalvar1++ >= 2 * dist && DirOK(i, md)) || dTransVal[monster.position.tile.x][monster.position.tile.y] != dTransVal[fx][fy]) {
monster._mgoal = MGOAL_NORMAL;
} else if (!RoundWalk(i, md, &monster._mgoalvar2)) {
AiDelay(monster, GenerateRnd(10) + 10);
}
}
} else {
monster._mgoal = MGOAL_NORMAL;
}
if (monster._mgoal == MGOAL_NORMAL) {
if (!gbIsMultiplayer
&& ((dist >= 3 && v < 4 * monster._mint + 35) || v < 6)
&& LineClearMissile(monster.position.tile, { fx, fy })) {
Point newPosition = monster.position.tile + md;
if (IsTileAvailable(monster, newPosition) && ActiveMonsterCount < MAXMONSTERS) {
SpawnSkeleton(newPosition, md);
StartSpecialStand(monster, md);
}
} else {
if (dist >= 2) {
v = GenerateRnd(100);
if (v >= monster._mint + 25
&& (IsNoneOf(static_cast<MonsterMode>(monster._mVar1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways) || monster._mVar2 != 0 || (v >= monster._mint + 75))) {
AiDelay(monster, GenerateRnd(10) + 10);
} else {
RandomWalk(i, md);
}
} else if (v < monster._mint + 20) {
monster._mdir = md;
StartAttack(monster);
}
}
}
monster.CheckStandAnimationIsLoaded(md);
}
void BatAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
int xd = monster.position.tile.x - monster.enemyPosition.x;
int yd = monster.position.tile.y - monster.enemyPosition.y;
Direction md = GetDirection(monster.position.tile, monster.position.last);
monster._mdir = md;
int v = GenerateRnd(100);
if (monster._mgoal == MGOAL_RETREAT) {
if (monster._mgoalvar1 == 0) {
RandomWalk(i, Opposite(md));
monster._mgoalvar1++;
} else {
if (GenerateRnd(2) != 0)
RandomWalk(i, Left(md));
else
RandomWalk(i, Right(md));
monster._mgoal = MGOAL_NORMAL;
}
return;
}
int fx = monster.enemyPosition.x;
int fy = monster.enemyPosition.y;
if (monster.MType->mtype == MT_GLOOM
&& (abs(xd) >= 5 || abs(yd) >= 5)
&& v < 4 * monster._mint + 33
&& LineClear([&monster](Point position) { return IsTileAvailable(monster, position); }, monster.position.tile, { fx, fy })) {
if (AddMissile(monster.position.tile, { fx, fy }, md, MIS_RHINO, TARGET_PLAYERS, i, 0, 0) != nullptr) {
dMonster[monster.position.tile.x][monster.position.tile.y] = -(i + 1);
monster._mmode = MonsterMode::Charge;
}
} else if (abs(xd) >= 2 || abs(yd) >= 2) {
if ((monster._mVar2 > 20 && v < monster._mint + 13)
|| (IsAnyOf(static_cast<MonsterMode>(monster._mVar1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways)
&& monster._mVar2 == 0
&& v < monster._mint + 63)) {
RandomWalk(i, md);
}
} else if (v < 4 * monster._mint + 8) {
StartAttack(monster);
monster._mgoal = MGOAL_RETREAT;
monster._mgoalvar1 = 0;
if (monster.MType->mtype == MT_FAMILIAR) {
AddMissile(monster.enemyPosition, { monster.enemyPosition.x + 1, 0 }, Direction::South, MIS_LIGHTNING, TARGET_PLAYERS, i, GenerateRnd(10) + 1, 0);
}
}
monster.CheckStandAnimationIsLoaded(md);
}
void GargoyleAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
int dx = monster.position.tile.x - monster.position.last.x;
int dy = monster.position.tile.y - monster.position.last.y;
Direction md = GetMonsterDirection(monster);
if (monster._msquelch != 0 && (monster._mFlags & MFLAG_ALLOW_SPECIAL) != 0) {
UpdateEnemy(monster);
int mx = monster.position.tile.x - monster.enemyPosition.x;
int my = monster.position.tile.y - monster.enemyPosition.y;
if (abs(mx) < monster._mint + 2 && abs(my) < monster._mint + 2) {
monster._mFlags &= ~MFLAG_ALLOW_SPECIAL;
}
return;
}
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
if (monster._mhitpoints < (monster._mmaxhp / 2))
if ((monster._mFlags & MFLAG_NOHEAL) == 0)
monster._mgoal = MGOAL_RETREAT;
if (monster._mgoal == MGOAL_RETREAT) {
if (abs(dx) >= monster._mint + 2 || abs(dy) >= monster._mint + 2) {
monster._mgoal = MGOAL_NORMAL;
StartHeal(monster);
} else if (!RandomWalk(i, Opposite(md))) {
monster._mgoal = MGOAL_NORMAL;
}
}
AiAvoidance(i, false);
}
void ButcherAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
int mx = monster.position.tile.x;
int my = monster.position.tile.y;
int x = mx - monster.enemyPosition.x;
int y = my - monster.enemyPosition.y;
Direction md = GetDirection({ mx, my }, monster.position.last);
monster._mdir = md;
if (abs(x) >= 2 || abs(y) >= 2)
RandomWalk(i, md);
else
StartAttack(monster);
monster.CheckStandAnimationIsLoaded(md);
}
void SuccubusAi(int i)
{
AiRanged(i, MIS_FLARE, false);
}
void SneakAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand) {
return;
}
int mx = monster.position.tile.x;
int my = monster.position.tile.y;
if (dLight[mx][my] == LightsMax) {
return;
}
mx -= monster.enemyPosition.x;
my -= monster.enemyPosition.y;
int dist = 5 - monster._mint;
if (static_cast<MonsterMode>(monster._mVar1) == MonsterMode::HitRecovery) {
monster._mgoal = MGOAL_RETREAT;
monster._mgoalvar1 = 0;
} else if (abs(mx) >= dist + 3 || abs(my) >= dist + 3 || monster._mgoalvar1 > 8) {
monster._mgoal = MGOAL_NORMAL;
monster._mgoalvar1 = 0;
}
Direction md = GetMonsterDirection(monster);
if (monster._mgoal == MGOAL_RETREAT && (monster._mFlags & MFLAG_NO_ENEMY) == 0) {
if ((monster._mFlags & MFLAG_TARGETS_MONSTER) != 0)
md = GetDirection(monster.position.tile, Monsters[monster._menemy].position.tile);
else
md = GetDirection(monster.position.tile, Players[monster._menemy].position.last);
md = Opposite(md);
if (monster.MType->mtype == MT_UNSEEN) {
if (GenerateRnd(2) != 0)
md = Left(md);
else
md = Right(md);
}
}
monster._mdir = md;
int v = GenerateRnd(100);
if (abs(mx) < dist && abs(my) < dist && (monster._mFlags & MFLAG_HIDDEN) != 0) {
StartFadein(monster, md, false);
} else {
if ((abs(mx) >= dist + 1 || abs(my) >= dist + 1) && (monster._mFlags & MFLAG_HIDDEN) == 0) {
StartFadeout(monster, md, true);
} else {
if (monster._mgoal == MGOAL_RETREAT
|| ((abs(mx) >= 2 || abs(my) >= 2) && ((monster._mVar2 > 20 && v < 4 * monster._mint + 14) || (IsAnyOf(static_cast<MonsterMode>(monster._mVar1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways) && monster._mVar2 == 0 && v < 4 * monster._mint + 64)))) {
monster._mgoalvar1++;
RandomWalk(i, md);
}
}
}
if (monster._mmode == MonsterMode::Stand) {
if (abs(mx) >= 2 || abs(my) >= 2 || v >= 4 * monster._mint + 10)
monster.ChangeAnimationData(MonsterGraphic::Stand);
else
StartAttack(monster);
}
}
void StormAi(int i)
{
AiRangedAvoidance(i, MIS_LIGHTCTRL2, true, 4, 0);
}
void GharbadAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand) {
return;
}
Direction md = GetMonsterDirection(monster);
if (monster.mtalkmsg >= TEXT_GARBUD1
&& monster.mtalkmsg <= TEXT_GARBUD3
&& !IsTileVisible(monster.position.tile)
&& monster._mgoal == MGOAL_TALKING) {
monster._mgoal = MGOAL_INQUIRING;
switch (monster.mtalkmsg) {
case TEXT_GARBUD1:
monster.mtalkmsg = TEXT_GARBUD2;
break;
case TEXT_GARBUD2:
monster.mtalkmsg = TEXT_GARBUD3;
break;
case TEXT_GARBUD3:
monster.mtalkmsg = TEXT_GARBUD4;
break;
default:
break;
}
}
if (IsTileVisible(monster.position.tile)) {
if (monster.mtalkmsg == TEXT_GARBUD4) {
if (!effect_is_playing(USFX_GARBUD4) && monster._mgoal == MGOAL_TALKING) {
monster._mgoal = MGOAL_NORMAL;
monster._msquelch = UINT8_MAX;
monster.mtalkmsg = TEXT_NONE;
}
}
}
if (monster._mgoal == MGOAL_NORMAL || monster._mgoal == MGOAL_MOVE)
AiAvoidance(i, true);
monster.CheckStandAnimationIsLoaded(md);
}
void AcidAvoidanceAi(int i)
{
AiRangedAvoidance(i, MIS_ACID, false, 4, 1);
}
void AcidAi(int i)
{
AiRanged(i, MIS_ACID, true);
}
void SnotSpilAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand) {
return;
}
Direction md = GetMonsterDirection(monster);
if (monster.mtalkmsg == TEXT_BANNER10 && !IsTileVisible(monster.position.tile) && monster._mgoal == MGOAL_TALKING) {
monster.mtalkmsg = TEXT_BANNER11;
monster._mgoal = MGOAL_INQUIRING;
}
if (monster.mtalkmsg == TEXT_BANNER11 && Quests[Q_LTBANNER]._qvar1 == 3) {
monster.mtalkmsg = TEXT_NONE;
monster._mgoal = MGOAL_NORMAL;
}
if (IsTileVisible(monster.position.tile)) {
if (monster.mtalkmsg == TEXT_BANNER12) {
if (!effect_is_playing(USFX_SNOT3) && monster._mgoal == MGOAL_TALKING) {
ObjChangeMap(setpc_x, setpc_y, setpc_x + setpc_w + 1, setpc_y + setpc_h + 1);
Quests[Q_LTBANNER]._qvar1 = 3;
RedoPlayerVision();
monster._msquelch = UINT8_MAX;
monster.mtalkmsg = TEXT_NONE;
monster._mgoal = MGOAL_NORMAL;
}
}
if (Quests[Q_LTBANNER]._qvar1 == 3) {
if (monster._mgoal == MGOAL_NORMAL || monster._mgoal == MGOAL_ATTACK2)
FallenAi(i);
}
}
monster.CheckStandAnimationIsLoaded(md);
}
void SnakeAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
char pattern[6] = { 1, 1, 0, -1, -1, 0 };
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0)
return;
int fx = monster.enemyPosition.x;
int fy = monster.enemyPosition.y;
int mx = monster.position.tile.x - fx;
int my = monster.position.tile.y - fy;
Direction md = GetDirection(monster.position.tile, monster.position.last);
monster._mdir = md;
if (abs(mx) >= 2 || abs(my) >= 2) {
if (abs(mx) < 3 && abs(my) < 3 && LineClear([&monster](Point position) { return IsTileAvailable(monster, position); }, monster.position.tile, { fx, fy }) && static_cast<MonsterMode>(monster._mVar1) != MonsterMode::Charge) {
if (AddMissile(monster.position.tile, { fx, fy }, md, MIS_RHINO, TARGET_PLAYERS, i, 0, 0) != nullptr) {
PlayEffect(monster, 0);
dMonster[monster.position.tile.x][monster.position.tile.y] = -(i + 1);
monster._mmode = MonsterMode::Charge;
}
} else if (static_cast<MonsterMode>(monster._mVar1) == MonsterMode::Delay || GenerateRnd(100) >= 35 - 2 * monster._mint) {
if (pattern[monster._mgoalvar1] == -1)
md = Left(md);
else if (pattern[monster._mgoalvar1] == 1)
md = Right(md);
monster._mgoalvar1++;
if (monster._mgoalvar1 > 5)
monster._mgoalvar1 = 0;
Direction targetDirection = static_cast<Direction>(monster._mgoalvar2);
if (md != targetDirection) {
int drift = static_cast<int>(md) - monster._mgoalvar2;
if (drift < 0)
drift += 8;
if (drift < 4)
md = Right(targetDirection);
else if (drift > 4)
md = Left(targetDirection);
monster._mgoalvar2 = static_cast<int>(md);
}
if (!DumbWalk(i, md))
RandomWalk2(i, monster._mdir);
} else {
AiDelay(monster, 15 - monster._mint + GenerateRnd(10));
}
} else {
if (IsAnyOf(static_cast<MonsterMode>(monster._mVar1), MonsterMode::Delay, MonsterMode::Charge)
|| (GenerateRnd(100) < monster._mint + 20)) {
StartAttack(monster);
} else
AiDelay(monster, 10 - monster._mint + GenerateRnd(10));
}
monster.CheckStandAnimationIsLoaded(monster._mdir);
}
void CounselorAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
int fx = monster.enemyPosition.x;
int fy = monster.enemyPosition.y;
int mx = monster.position.tile.x - fx;
int my = monster.position.tile.y - fy;
Direction md = GetDirection(monster.position.tile, monster.position.last);
if (monster._msquelch < UINT8_MAX)
MonstCheckDoors(monster);
int v = GenerateRnd(100);
if (monster._mgoal == MGOAL_RETREAT) {
if (monster._mgoalvar1++ <= 3)
RandomWalk(i, Opposite(md));
else {
monster._mgoal = MGOAL_NORMAL;
StartFadein(monster, md, true);
}
} else if (monster._mgoal == MGOAL_MOVE) {
int dist = std::max(abs(mx), abs(my));
if (dist >= 2 && monster._msquelch == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[fx][fy]) {
if (monster._mgoalvar1++ < 2 * dist || !DirOK(i, md)) {
RoundWalk(i, md, &monster._mgoalvar2);
} else {
monster._mgoal = MGOAL_NORMAL;
StartFadein(monster, md, true);
}
} else {
monster._mgoal = MGOAL_NORMAL;
StartFadein(monster, md, true);
}
} else if (monster._mgoal == MGOAL_NORMAL) {
if (abs(mx) >= 2 || abs(my) >= 2) {
if (v < 5 * (monster._mint + 10) && LineClearMissile(monster.position.tile, { fx, fy })) {
constexpr missile_id MissileTypes[4] = { MIS_FIREBOLT, MIS_CBOLT, MIS_LIGHTCTRL, MIS_FIREBALL };
StartRangedAttack(monster, MissileTypes[monster._mint], monster.mMinDamage + GenerateRnd(monster.mMaxDamage - monster.mMinDamage + 1));
} else if (GenerateRnd(100) < 30) {
monster._mgoal = MGOAL_MOVE;
monster._mgoalvar1 = 0;
StartFadeout(monster, md, false);
} else
AiDelay(monster, GenerateRnd(10) + 2 * (5 - monster._mint));
} else {
monster._mdir = md;
if (monster._mhitpoints < (monster._mmaxhp / 2)) {
monster._mgoal = MGOAL_RETREAT;
monster._mgoalvar1 = 0;
StartFadeout(monster, md, false);
} else if (static_cast<MonsterMode>(monster._mVar1) == MonsterMode::Delay
|| GenerateRnd(100) < 2 * monster._mint + 20) {
StartRangedAttack(monster, MIS_NULL, 0);
AddMissile(monster.position.tile, { 0, 0 }, monster._mdir, MIS_FLASH, TARGET_PLAYERS, i, 4, 0);
AddMissile(monster.position.tile, { 0, 0 }, monster._mdir, MIS_FLASH2, TARGET_PLAYERS, i, 4, 0);
} else
AiDelay(monster, GenerateRnd(10) + 2 * (5 - monster._mint));
}
}
if (monster._mmode == MonsterMode::Stand) {
AiDelay(monster, GenerateRnd(10) + 5);
}
}
void ZharAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand) {
return;
}
Direction md = GetMonsterDirection(monster);
if (monster.mtalkmsg == TEXT_ZHAR1 && !IsTileVisible(monster.position.tile) && monster._mgoal == MGOAL_TALKING) {
monster.mtalkmsg = TEXT_ZHAR2;
monster._mgoal = MGOAL_INQUIRING;
}
if (IsTileVisible(monster.position.tile)) {
if (monster.mtalkmsg == TEXT_ZHAR2) {
if (!effect_is_playing(USFX_ZHAR2) && monster._mgoal == MGOAL_TALKING) {
monster._msquelch = UINT8_MAX;
monster.mtalkmsg = TEXT_NONE;
monster._mgoal = MGOAL_NORMAL;
}
}
}
if (monster._mgoal == MGOAL_NORMAL || monster._mgoal == MGOAL_RETREAT || monster._mgoal == MGOAL_MOVE)
CounselorAi(i);
monster.CheckStandAnimationIsLoaded(md);
}
void MegaAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
int mx = monster.position.tile.x - monster.enemyPosition.x;
int my = monster.position.tile.y - monster.enemyPosition.y;
if (abs(mx) >= 5 || abs(my) >= 5) {
SkeletonAi(i);
return;
}
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
int fx = monster.enemyPosition.x;
int fy = monster.enemyPosition.y;
mx = monster.position.tile.x - fx;
my = monster.position.tile.y - fy;
Direction md = GetDirection(monster.position.tile, monster.position.last);
if (monster._msquelch < UINT8_MAX)
MonstCheckDoors(monster);
int v = GenerateRnd(100);
int dist = std::max(abs(mx), abs(my));
if (dist >= 2 && monster._msquelch == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[fx][fy]) {
if (monster._mgoal == MGOAL_MOVE || dist >= 3) {
if (monster._mgoal != MGOAL_MOVE) {
monster._mgoalvar1 = 0;
monster._mgoalvar2 = GenerateRnd(2);
}
monster._mgoal = MGOAL_MOVE;
monster._mgoalvar3 = 4;
if (monster._mgoalvar1++ < 2 * dist || !DirOK(i, md)) {
if (v < 5 * (monster._mint + 16))
RoundWalk(i, md, &monster._mgoalvar2);
} else
monster._mgoal = MGOAL_NORMAL;
}
} else {
monster._mgoal = MGOAL_NORMAL;
}
if (monster._mgoal == MGOAL_NORMAL) {
if (((dist >= 3 && v < 5 * (monster._mint + 2)) || v < 5 * (monster._mint + 1) || monster._mgoalvar3 == 4) && LineClearMissile(monster.position.tile, { fx, fy })) {
StartRangedSpecialAttack(monster, MIS_FLAMEC, 0);
} else if (dist >= 2) {
v = GenerateRnd(100);
if (v < 2 * (5 * monster._mint + 25)
|| (IsAnyOf(static_cast<MonsterMode>(monster._mVar1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways)
&& monster._mVar2 == 0
&& v < 2 * (5 * monster._mint + 40))) {
RandomWalk(i, md);
}
} else {
if (GenerateRnd(100) < 10 * (monster._mint + 4)) {
monster._mdir = md;
if (GenerateRnd(2) != 0)
StartAttack(monster);
else
StartRangedSpecialAttack(monster, MIS_FLAMEC, 0);
}
}
monster._mgoalvar3 = 1;
}
if (monster._mmode == MonsterMode::Stand) {
AiDelay(monster, GenerateRnd(10) + 5);
}
}
void DiabloAi(int i)
{
AiRangedAvoidance(i, MIS_DIABAPOCA, false, 40, 0);
}
void LazarusAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand) {
return;
}
Direction md = GetMonsterDirection(monster);
if (IsTileVisible(monster.position.tile)) {
if (!gbIsMultiplayer) {
Player &myPlayer = *MyPlayer;
if (monster.mtalkmsg == TEXT_VILE13 && monster._mgoal == MGOAL_INQUIRING && myPlayer.position.tile == Point { 35, 46 }) {
PlayInGameMovie("gendata\\fprst3.smk");
monster._mmode = MonsterMode::Talk;
Quests[Q_BETRAYER]._qvar1 = 5;
}
if (monster.mtalkmsg == TEXT_VILE13 && !effect_is_playing(USFX_LAZ1) && monster._mgoal == MGOAL_TALKING) {
ObjChangeMap(1, 18, 20, 24);
RedoPlayerVision();
Quests[Q_BETRAYER]._qvar1 = 6;
monster._mgoal = MGOAL_NORMAL;
monster._msquelch = UINT8_MAX;
monster.mtalkmsg = TEXT_NONE;
}
}
if (gbIsMultiplayer && monster.mtalkmsg == TEXT_VILE13 && monster._mgoal == MGOAL_INQUIRING && Quests[Q_BETRAYER]._qvar1 <= 3) {
monster._mmode = MonsterMode::Talk;
}
}
if (monster._mgoal == MGOAL_NORMAL || monster._mgoal == MGOAL_RETREAT || monster._mgoal == MGOAL_MOVE) {
if (!gbIsMultiplayer && Quests[Q_BETRAYER]._qvar1 == 4 && monster.mtalkmsg == TEXT_NONE) { // Fix save games affected by teleport bug
ObjChangeMapResync(1, 18, 20, 24);
RedoPlayerVision();
Quests[Q_BETRAYER]._qvar1 = 6;
}
monster.mtalkmsg = TEXT_NONE;
CounselorAi(i);
}
monster.CheckStandAnimationIsLoaded(md);
}
void LazarusMinionAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand)
return;
Direction md = GetMonsterDirection(monster);
if (IsTileVisible(monster.position.tile)) {
if (!gbIsMultiplayer) {
if (Quests[Q_BETRAYER]._qvar1 <= 5) {
monster._mgoal = MGOAL_INQUIRING;
} else {
monster._mgoal = MGOAL_NORMAL;
monster.mtalkmsg = TEXT_NONE;
}
} else
monster._mgoal = MGOAL_NORMAL;
}
if (monster._mgoal == MGOAL_NORMAL)
SuccubusAi(i);
monster.CheckStandAnimationIsLoaded(md);
}
void LachdananAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand) {
return;
}
Direction md = GetMonsterDirection(monster);
if (monster.mtalkmsg == TEXT_VEIL9 && !IsTileVisible(monster.position.tile) && monster._mgoal == MGOAL_TALKING) {
monster.mtalkmsg = TEXT_VEIL10;
monster._mgoal = MGOAL_INQUIRING;
}
if (IsTileVisible(monster.position.tile)) {
if (monster.mtalkmsg == TEXT_VEIL11) {
if (!effect_is_playing(USFX_LACH3) && monster._mgoal == MGOAL_TALKING) {
monster.mtalkmsg = TEXT_NONE;
Quests[Q_VEIL]._qactive = QUEST_DONE;
M_StartKill(i, -1);
}
}
}
monster.CheckStandAnimationIsLoaded(md);
}
void WarlordAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand) {
return;
}
Direction md = GetMonsterDirection(monster);
if (IsTileVisible(monster.position.tile)) {
if (monster.mtalkmsg == TEXT_WARLRD9 && monster._mgoal == MGOAL_INQUIRING)
monster._mmode = MonsterMode::Talk;
if (monster.mtalkmsg == TEXT_WARLRD9 && !effect_is_playing(USFX_WARLRD1) && monster._mgoal == MGOAL_TALKING) {
monster._msquelch = UINT8_MAX;
monster.mtalkmsg = TEXT_NONE;
monster._mgoal = MGOAL_NORMAL;
}
}
if (monster._mgoal == MGOAL_NORMAL)
SkeletonAi(i);
monster.CheckStandAnimationIsLoaded(md);
}
void FirebatAi(int i)
{
AiRanged(i, MIS_FIREBOLT, false);
}
void TorchantAi(int i)
{
AiRanged(i, MIS_FIREBALL, false);
}
void HorkDemonAi(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mmode != MonsterMode::Stand || monster._msquelch == 0) {
return;
}
int fx = monster.enemyPosition.x;
int fy = monster.enemyPosition.y;
int mx = monster.position.tile.x - fx;
int my = monster.position.tile.y - fy;
Direction md = GetDirection(monster.position.tile, monster.position.last);
if (monster._msquelch < 255) {
MonstCheckDoors(monster);
}
int v = GenerateRnd(100);
if (abs(mx) < 2 && abs(my) < 2) {
monster._mgoal = MGOAL_NORMAL;
} else if (monster._mgoal == 4 || ((abs(mx) >= 5 || abs(my) >= 5) && GenerateRnd(4) != 0)) {
if (monster._mgoal != 4) {
monster._mgoalvar1 = 0;
monster._mgoalvar2 = GenerateRnd(2);
}
monster._mgoal = MGOAL_MOVE;
int dist = std::max(abs(mx), abs(my));
if (monster._mgoalvar1++ >= 2 * dist || dTransVal[monster.position.tile.x][monster.position.tile.y] != dTransVal[fx][fy]) {
monster._mgoal = MGOAL_NORMAL;
} else if (!RoundWalk(i, md, &monster._mgoalvar2)) {
AiDelay(monster, GenerateRnd(10) + 10);
}
}
if (monster._mgoal == 1) {
if ((abs(mx) >= 3 || abs(my) >= 3) && v < 2 * monster._mint + 43) {
Point position = monster.position.tile + monster._mdir;
if (IsTileAvailable(monster, position) && ActiveMonsterCount < MAXMONSTERS) {
StartRangedSpecialAttack(monster, MIS_HORKDMN, 0);
}
} else if (abs(mx) < 2 && abs(my) < 2) {
if (v < 2 * monster._mint + 28) {
monster._mdir = md;
StartAttack(monster);
}
} else {
v = GenerateRnd(100);
if (v < 2 * monster._mint + 33
|| (IsAnyOf(static_cast<MonsterMode>(monster._mVar1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways) && monster._mVar2 == 0 && v < 2 * monster._mint + 83)) {
RandomWalk(i, md);
} else {
AiDelay(monster, GenerateRnd(10) + 10);
}
}
}
monster.CheckStandAnimationIsLoaded(monster._mdir);
}
void LichAi(int i)
{
AiRanged(i, MIS_LICH, false);
}
void ArchLichAi(int i)
{
AiRanged(i, MIS_ARCHLICH, false);
}
void PsychorbAi(int i)
{
AiRanged(i, MIS_PSYCHORB, false);
}
void NecromorbAi(int i)
{
AiRanged(i, MIS_NECROMORB, false);
}
void BoneDemonAi(int i)
{
AiRangedAvoidance(i, MIS_BONEDEMON, true, 4, 0);
}
string_view GetMonsterTypeText(const MonsterData &monsterData)
{
switch (monsterData.mMonstClass) {
case MonsterClass::Animal:
return _("Animal");
case MonsterClass::Demon:
return _("Demon");
case MonsterClass::Undead:
return _("Undead");
}
app_fatal("Unknown mMonstClass %i", static_cast<int>(monsterData.mMonstClass));
}
void ActivateSpawn(int i, Point position, Direction dir)
{
auto &monster = Monsters[i];
dMonster[position.x][position.y] = i + 1;
monster.position.tile = position;
monster.position.future = position;
monster.position.old = position;
StartSpecialStand(monster, dir);
}
/** Maps from monster AI ID to monster AI function. */
void (*AiProc[])(int i) = {
/*AI_ZOMBIE */ &ZombieAi,
/*AI_FAT */ &OverlordAi,
/*AI_SKELSD */ &SkeletonAi,
/*AI_SKELBOW */ &SkeletonBowAi,
/*AI_SCAV */ &ScavengerAi,
/*AI_RHINO */ &RhinoAi,
/*AI_GOATMC */ &GoatAi,
/*AI_GOATBOW */ &GoatBowAi,
/*AI_FALLEN */ &FallenAi,
/*AI_MAGMA */ &MagmaAi,
/*AI_SKELKING */ &LeoricAi,
/*AI_BAT */ &BatAi,
/*AI_GARG */ &GargoyleAi,
/*AI_CLEAVER */ &ButcherAi,
/*AI_SUCC */ &SuccubusAi,
/*AI_SNEAK */ &SneakAi,
/*AI_STORM */ &StormAi,
/*AI_FIREMAN */ nullptr,
/*AI_GARBUD */ &GharbadAi,
/*AI_ACID */ &AcidAvoidanceAi,
/*AI_ACIDUNIQ */ &AcidAi,
/*AI_GOLUM */ &GolumAi,
/*AI_ZHAR */ &ZharAi,
/*AI_SNOTSPIL */ &SnotSpilAi,
/*AI_SNAKE */ &SnakeAi,
/*AI_COUNSLR */ &CounselorAi,
/*AI_MEGA */ &MegaAi,
/*AI_DIABLO */ &DiabloAi,
/*AI_LAZARUS */ &LazarusAi,
/*AI_LAZHELP */ &LazarusMinionAi,
/*AI_LACHDAN */ &LachdananAi,
/*AI_WARLORD */ &WarlordAi,
/*AI_FIREBAT */ &FirebatAi,
/*AI_TORCHANT */ &TorchantAi,
/*AI_HORKDMN */ &HorkDemonAi,
/*AI_LICH */ &LichAi,
/*AI_ARCHLICH */ &ArchLichAi,
/*AI_PSYCHORB */ &PsychorbAi,
/*AI_NECROMORB*/ &NecromorbAi,
/*AI_BONEDEMON*/ &BoneDemonAi
};
bool IsRelativeMoveOK(const Monster &monster, Point position, Direction mdir)
{
Point futurePosition = position + mdir;
if (!InDungeonBounds(futurePosition) || !IsTileAvailable(monster, futurePosition))
return false;
if (mdir == Direction::East) {
if (IsTileSolid(position + Direction::SouthEast))
return false;
} else if (mdir == Direction::West) {
if (IsTileSolid(position + Direction::SouthWest))
return false;
} else if (mdir == Direction::North) {
if (IsTileSolid(position + Direction::NorthEast) || IsTileSolid(position + Direction::NorthWest))
return false;
} else if (mdir == Direction::South)
if (IsTileSolid(position + Direction::SouthWest) || IsTileSolid(position + Direction::SouthEast))
return false;
return true;
}
bool IsMonsterAvalible(const MonsterData &monsterData)
{
if (monsterData.availability == MonsterAvailability::Never)
return false;
if (gbIsSpawn && monsterData.availability == MonsterAvailability::Retail)
return false;
return currlevel >= monsterData.mMinDLvl && currlevel <= monsterData.mMaxDLvl;
}
} // namespace
void InitTRNForUniqueMonster(Monster &monster)
{
char filestr[64];
sprintf(filestr, "Monsters\\Monsters\\%s.TRN", UniqueMonstersData[monster._uniqtype - 1].mTrnName);
monster.uniqueTRN = LoadFileInMem<uint8_t>(filestr);
}
void PrepareUniqueMonst(Monster &monster, int uniqindex, int miniontype, int bosspacksize, const UniqueMonsterData &uniqueMonsterData)
{
monster._uniqtype = uniqindex + 1;
if (uniqueMonsterData.mlevel != 0) {
monster.mLevel = 2 * uniqueMonsterData.mlevel;
} else {
monster.mLevel = monster.MData->mLevel + 5;
}
monster.mExp *= 2;
monster.mName = pgettext("monster", uniqueMonsterData.mName).c_str();
monster._mmaxhp = uniqueMonsterData.mmaxhp << 6;
if (!gbIsMultiplayer)
monster._mmaxhp = std::max(monster._mmaxhp / 2, 64);
monster._mhitpoints = monster._mmaxhp;
monster._mAi = uniqueMonsterData.mAi;
monster._mint = uniqueMonsterData.mint;
monster.mMinDamage = uniqueMonsterData.mMinDamage;
monster.mMaxDamage = uniqueMonsterData.mMaxDamage;
monster.mMinDamage2 = uniqueMonsterData.mMinDamage;
monster.mMaxDamage2 = uniqueMonsterData.mMaxDamage;
monster.mMagicRes = uniqueMonsterData.mMagicRes;
monster.mtalkmsg = uniqueMonsterData.mtalkmsg;
if (uniqindex == UMT_HORKDMN)
monster.mlid = NO_LIGHT; // BUGFIX monsters initial light id should be -1 (fixed)
else
monster.mlid = AddLight(monster.position.tile, 3);
if (gbIsMultiplayer) {
if (monster._mAi == AI_LAZHELP)
monster.mtalkmsg = TEXT_NONE;
if (monster._mAi == AI_LAZARUS && Quests[Q_BETRAYER]._qvar1 > 3) {
monster._mgoal = MGOAL_NORMAL;
} else if (monster.mtalkmsg != TEXT_NONE) {
monster._mgoal = MGOAL_INQUIRING;
}
} else if (monster.mtalkmsg != TEXT_NONE) {
monster._mgoal = MGOAL_INQUIRING;
}
if (sgGameInitInfo.nDifficulty == DIFF_NIGHTMARE) {
monster._mmaxhp = 3 * monster._mmaxhp;
if (gbIsHellfire)
monster._mmaxhp += (gbIsMultiplayer ? 100 : 50) << 6;
else
monster._mmaxhp += 64;
monster.mLevel += 15;
monster._mhitpoints = monster._mmaxhp;
monster.mExp = 2 * (monster.mExp + 1000);
monster.mMinDamage = 2 * (monster.mMinDamage + 2);
monster.mMaxDamage = 2 * (monster.mMaxDamage + 2);
monster.mMinDamage2 = 2 * (monster.mMinDamage2 + 2);
monster.mMaxDamage2 = 2 * (monster.mMaxDamage2 + 2);
} else if (sgGameInitInfo.nDifficulty == DIFF_HELL) {
monster._mmaxhp = 4 * monster._mmaxhp;
if (gbIsHellfire)
monster._mmaxhp += (gbIsMultiplayer ? 200 : 100) << 6;
else
monster._mmaxhp += 192;
monster.mLevel += 30;
monster._mhitpoints = monster._mmaxhp;
monster.mExp = 4 * (monster.mExp + 1000);
monster.mMinDamage = 4 * monster.mMinDamage + 6;
monster.mMaxDamage = 4 * monster.mMaxDamage + 6;
monster.mMinDamage2 = 4 * monster.mMinDamage2 + 6;
monster.mMaxDamage2 = 4 * monster.mMaxDamage2 + 6;
}
InitTRNForUniqueMonster(monster);
monster._uniqtrans = uniquetrans++;
if (uniqueMonsterData.customToHit != 0) {
monster.mHit = uniqueMonsterData.customToHit;
monster.mHit2 = uniqueMonsterData.customToHit;
if (sgGameInitInfo.nDifficulty == DIFF_NIGHTMARE) {
monster.mHit += NIGHTMARE_TO_HIT_BONUS;
monster.mHit2 += NIGHTMARE_TO_HIT_BONUS;
} else if (sgGameInitInfo.nDifficulty == DIFF_HELL) {
monster.mHit += HELL_TO_HIT_BONUS;
monster.mHit2 += HELL_TO_HIT_BONUS;
}
}
if (uniqueMonsterData.customArmorClass != 0) {
monster.mArmorClass = uniqueMonsterData.customArmorClass;
if (sgGameInitInfo.nDifficulty == DIFF_NIGHTMARE) {
monster.mArmorClass += NIGHTMARE_AC_BONUS;
} else if (sgGameInitInfo.nDifficulty == DIFF_HELL) {
monster.mArmorClass += HELL_AC_BONUS;
}
}
ActiveMonsterCount++;
if (uniqueMonsterData.monsterPack != UniqueMonsterPack::None) {
PlaceGroup(miniontype, bosspacksize, uniqueMonsterData.monsterPack, ActiveMonsterCount - 1);
}
if (monster._mAi != AI_GARG) {
monster.ChangeAnimationData(MonsterGraphic::Stand);
monster.AnimInfo.CurrentFrame = GenerateRnd(monster.AnimInfo.NumberOfFrames - 1);
monster._mFlags &= ~MFLAG_ALLOW_SPECIAL;
monster._mmode = MonsterMode::Stand;
}
}
void InitLevelMonsters()
{
LevelMonsterTypeCount = 0;
monstimgtot = 0;
for (auto &levelMonsterType : LevelMonsterTypes) {
levelMonsterType.mPlaceFlags = 0;
}
ClrAllMonsters();
ActiveMonsterCount = 0;
totalmonsters = MAXMONSTERS;
for (int i = 0; i < MAXMONSTERS; i++) {
ActiveMonsters[i] = i;
}
uniquetrans = 0;
}
void GetLevelMTypes()
{
AddMonsterType(MT_GOLEM, PLACE_SPECIAL);
if (currlevel == 16) {
AddMonsterType(MT_ADVOCATE, PLACE_SCATTER);
AddMonsterType(MT_RBLACK, PLACE_SCATTER);
AddMonsterType(MT_DIABLO, PLACE_SPECIAL);
return;
}
if (currlevel == 18)
AddMonsterType(MT_HORKSPWN, PLACE_SCATTER);
if (currlevel == 19) {
AddMonsterType(MT_HORKSPWN, PLACE_SCATTER);
AddMonsterType(MT_HORKDMN, PLACE_UNIQUE);
}
if (currlevel == 20)
AddMonsterType(MT_DEFILER, PLACE_UNIQUE);
if (currlevel == 24) {
AddMonsterType(MT_ARCHLICH, PLACE_SCATTER);
AddMonsterType(MT_NAKRUL, PLACE_SPECIAL);
}
if (!setlevel) {
if (Quests[Q_BUTCHER].IsAvailable())
AddMonsterType(MT_CLEAVER, PLACE_SPECIAL);
if (Quests[Q_GARBUD].IsAvailable())
AddMonsterType(UniqueMonstersData[UMT_GARBUD].mtype, PLACE_UNIQUE);
if (Quests[Q_ZHAR].IsAvailable())
AddMonsterType(UniqueMonstersData[UMT_ZHAR].mtype, PLACE_UNIQUE);
if (Quests[Q_LTBANNER].IsAvailable())
AddMonsterType(UniqueMonstersData[UMT_SNOTSPIL].mtype, PLACE_UNIQUE);
if (Quests[Q_VEIL].IsAvailable())
AddMonsterType(UniqueMonstersData[UMT_LACHDAN].mtype, PLACE_UNIQUE);
if (Quests[Q_WARLORD].IsAvailable())
AddMonsterType(UniqueMonstersData[UMT_WARLORD].mtype, PLACE_UNIQUE);
if (gbIsMultiplayer && currlevel == Quests[Q_SKELKING]._qlevel) {
AddMonsterType(MT_SKING, PLACE_UNIQUE);
int skeletonTypeCount = 0;
_monster_id skeltypes[NUM_MTYPES];
for (_monster_id skeletonType : SkeletonTypes) {
if (!IsMonsterAvalible(MonstersData[skeletonType]))
continue;
skeltypes[skeletonTypeCount++] = skeletonType;
}
AddMonsterType(skeltypes[GenerateRnd(skeletonTypeCount)], PLACE_SCATTER);
}
_monster_id typelist[MAXMONSTERS];
int nt = 0;
for (int i = MT_NZOMBIE; i < NUM_MTYPES; i++) {
if (!IsMonsterAvalible(MonstersData[i]))
continue;
typelist[nt++] = (_monster_id)i;
}
while (nt > 0 && LevelMonsterTypeCount < MAX_LVLMTYPES && monstimgtot < 4000) {
for (int i = 0; i < nt;) {
if (MonstersData[typelist[i]].mImage > 4000 - monstimgtot) {
typelist[i] = typelist[--nt];
continue;
}
i++;
}
if (nt != 0) {
int i = GenerateRnd(nt);
AddMonsterType(typelist[i], PLACE_SCATTER);
typelist[i] = typelist[--nt];
}
}
} else {
if (setlvlnum == SL_SKELKING) {
AddMonsterType(MT_SKING, PLACE_UNIQUE);
}
}
}
void InitMonsterGFX(int monst)
{
CMonster &monster = LevelMonsterTypes[monst];
const _monster_id mtype = monster.mtype;
const MonsterData &monsterData = MonstersData[mtype];
const int width = monsterData.width;
constexpr size_t MaxAnims = sizeof(animletter) / sizeof(animletter[0]) - 1;
const size_t numAnims = GetNumAnims(monsterData);
const auto hasAnim = [&monsterData](size_t i) {
return monsterData.Frames[i] != 0;
};
std::array<uint32_t, MaxAnims> animOffsets;
monster.animData = MultiFileLoader<MaxAnims> {}(
numAnims,
FileNameWithCharAffixGenerator({ "Monsters\\", monsterData.GraphicType }, ".CL2", &animletter[0]),
&animOffsets[0],
hasAnim);
for (unsigned animIndex = 0; animIndex < numAnims; animIndex++) {
AnimStruct &anim = monster.Anims[animIndex];
if (!hasAnim(animIndex)) {
anim.Frames = 0;
continue;
}
anim.Frames = monsterData.Frames[animIndex];
anim.Rate = monsterData.Rate[animIndex];
anim.Width = width;
byte *cl2Data = &monster.animData[animOffsets[animIndex]];
if (IsDirectionalAnim(monster, animIndex)) {
CelGetDirectionFrames(cl2Data, anim.CelSpritesForDirections.data());
} else {
for (size_t i = 0; i < 8; ++i) {
anim.CelSpritesForDirections[i] = cl2Data;
}
}
}
monster.MData = &monsterData;
if (monsterData.has_trans) {
InitMonsterTRN(monster);
}
if (IsAnyOf(mtype, MT_NMAGMA, MT_YMAGMA, MT_BMAGMA, MT_WMAGMA))
MissileSpriteData[MFILE_MAGBALL].LoadGFX();
if (IsAnyOf(mtype, MT_STORM, MT_RSTORM, MT_STORML, MT_MAEL))
MissileSpriteData[MFILE_THINLGHT].LoadGFX();
if (mtype == MT_SNOWWICH) {
MissileSpriteData[MFILE_SCUBMISB].LoadGFX();
MissileSpriteData[MFILE_SCBSEXPB].LoadGFX();
}
if (mtype == MT_HLSPWN) {
MissileSpriteData[MFILE_SCUBMISD].LoadGFX();
MissileSpriteData[MFILE_SCBSEXPD].LoadGFX();
}
if (mtype == MT_SOLBRNR) {
MissileSpriteData[MFILE_SCUBMISC].LoadGFX();
MissileSpriteData[MFILE_SCBSEXPC].LoadGFX();
}
if (IsAnyOf(mtype, MT_NACID, MT_RACID, MT_BACID, MT_XACID, MT_SPIDLORD)) {
MissileSpriteData[MFILE_ACIDBF].LoadGFX();
MissileSpriteData[MFILE_ACIDSPLA].LoadGFX();
MissileSpriteData[MFILE_ACIDPUD].LoadGFX();
}
if (mtype == MT_LICH) {
MissileSpriteData[MFILE_LICH].LoadGFX();
MissileSpriteData[MFILE_EXORA1].LoadGFX();
}
if (mtype == MT_ARCHLICH) {
MissileSpriteData[MFILE_ARCHLICH].LoadGFX();
MissileSpriteData[MFILE_EXYEL2].LoadGFX();
}
if (IsAnyOf(mtype, MT_PSYCHORB, MT_BONEDEMN))
MissileSpriteData[MFILE_BONEDEMON].LoadGFX();
if (mtype == MT_NECRMORB) {
MissileSpriteData[MFILE_NECROMORB].LoadGFX();
MissileSpriteData[MFILE_EXRED3].LoadGFX();
}
if (mtype == MT_PSYCHORB)
MissileSpriteData[MFILE_EXBL2].LoadGFX();
if (mtype == MT_BONEDEMN)
MissileSpriteData[MFILE_EXBL3].LoadGFX();
if (mtype == MT_DIABLO)
MissileSpriteData[MFILE_FIREPLAR].LoadGFX();
}
void monster_some_crypt()
{
if (currlevel != 24 || UberDiabloMonsterIndex < 0 || UberDiabloMonsterIndex >= ActiveMonsterCount)
return;
auto &monster = Monsters[UberDiabloMonsterIndex];
PlayEffect(monster, 2);
Quests[Q_NAKRUL]._qlog = false;
monster.mArmorClass -= 50;
int hp = monster._mmaxhp / 2;
monster.mMagicRes = 0;
monster._mhitpoints = hp;
monster._mmaxhp = hp;
}
void InitGolems()
{
if (!setlevel) {
for (int i = 0; i < MAX_PLRS; i++)
AddMonster(GolemHoldingCell, Direction::South, 0, false);
}
}
void InitMonsters()
{
if (!gbIsSpawn && !setlevel && currlevel == 16)
LoadDiabMonsts();
int nt = numtrigs;
if (currlevel == 15)
nt = 1;
for (int i = 0; i < nt; i++) {
for (int s = -2; s < 2; s++) {
for (int t = -2; t < 2; t++)
DoVision(trigs[i].position + Displacement { s, t }, 15, MAP_EXP_NONE, false);
}
}
if (!gbIsSpawn)
PlaceQuestMonsters();
if (!setlevel) {
if (!gbIsSpawn)
PlaceUniqueMonsters();
int na = 0;
for (int s = 16; s < 96; s++) {
for (int t = 16; t < 96; t++) {
if (!IsTileSolid({ s, t }))
na++;
}
}
int numplacemonsters = na / 30;
if (gbIsMultiplayer)
numplacemonsters += numplacemonsters / 2;
if (ActiveMonsterCount + numplacemonsters > MAXMONSTERS - 10)
numplacemonsters = MAXMONSTERS - 10 - ActiveMonsterCount;
totalmonsters = ActiveMonsterCount + numplacemonsters;
int numscattypes = 0;
int scattertypes[NUM_MTYPES];
for (int i = 0; i < LevelMonsterTypeCount; i++) {
if ((LevelMonsterTypes[i].mPlaceFlags & PLACE_SCATTER) != 0) {
scattertypes[numscattypes] = i;
numscattypes++;
}
}
while (ActiveMonsterCount < totalmonsters) {
int mtype = scattertypes[GenerateRnd(numscattypes)];
if (currlevel == 1 || GenerateRnd(2) == 0)
na = 1;
else if (currlevel == 2 || (currlevel >= 21 && currlevel <= 24))
na = GenerateRnd(2) + 2;
else
na = GenerateRnd(3) + 3;
PlaceGroup(mtype, na, UniqueMonsterPack::None, 0);
}
}
for (int i = 0; i < nt; i++) {
for (int s = -2; s < 2; s++) {
for (int t = -2; t < 2; t++)
DoUnVision(trigs[i].position + Displacement { s, t }, 15);
}
}
}
void SetMapMonsters(const uint16_t *dunData, Point startPosition)
{
AddMonsterType(MT_GOLEM, PLACE_SPECIAL);
if (setlevel)
for (int i = 0; i < MAX_PLRS; i++)
AddMonster(GolemHoldingCell, Direction::South, 0, false);
if (setlevel && setlvlnum == SL_VILEBETRAYER) {
AddMonsterType(UniqueMonstersData[UMT_LAZARUS].mtype, PLACE_UNIQUE);
AddMonsterType(UniqueMonstersData[UMT_RED_VEX].mtype, PLACE_UNIQUE);
AddMonsterType(UniqueMonstersData[UMT_BLACKJADE].mtype, PLACE_UNIQUE);
PlaceUniqueMonst(UMT_LAZARUS, 0, 0);
PlaceUniqueMonst(UMT_RED_VEX, 0, 0);
PlaceUniqueMonst(UMT_BLACKJADE, 0, 0);
}
int width = SDL_SwapLE16(dunData[0]);
int height = SDL_SwapLE16(dunData[1]);
int layer2Offset = 2 + width * height;
// The rest of the layers are at dPiece scale
width *= 2;
height *= 2;
const uint16_t *monsterLayer = &dunData[layer2Offset + width * height];
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
auto monsterId = static_cast<uint8_t>(SDL_SwapLE16(monsterLayer[j * width + i]));
if (monsterId != 0) {
int mtype = AddMonsterType(MonstConvTbl[monsterId - 1], PLACE_SPECIAL);
PlaceMonster(ActiveMonsterCount++, mtype, i + startPosition.x + 16, j + startPosition.y + 16);
}
}
}
}
int AddMonster(Point position, Direction dir, int mtype, bool inMap)
{
if (ActiveMonsterCount < MAXMONSTERS) {
int i = ActiveMonsters[ActiveMonsterCount++];
if (inMap)
dMonster[position.x][position.y] = i + 1;
InitMonster(Monsters[i], dir, mtype, position);
return i;
}
return -1;
}
void AddDoppelganger(Monster &monster)
{
if (monster.MType == nullptr) {
return;
}
Point target = { 0, 0 };
for (int d = 0; d < 8; d++) {
const Point position = monster.position.tile + static_cast<Direction>(d);
if (!IsTileAvailable(position))
continue;
target = position;
}
if (target != Point { 0, 0 }) {
for (int j = 0; j < MAX_LVLMTYPES; j++) {
if (LevelMonsterTypes[j].mtype == monster.MType->mtype) {
AddMonster(target, monster._mdir, j, true);
break;
}
}
}
}
bool M_Talker(const Monster &monster)
{
return IsAnyOf(monster._mAi, AI_LAZARUS, AI_WARLORD, AI_GARBUD, AI_ZHAR, AI_SNOTSPIL, AI_LACHDAN, AI_LAZHELP);
}
void M_StartStand(Monster &monster, Direction md)
{
ClearMVars(monster);
if (monster.MType->mtype == MT_GOLEM)
NewMonsterAnim(monster, MonsterGraphic::Walk, md);
else
NewMonsterAnim(monster, MonsterGraphic::Stand, md);
monster._mVar1 = static_cast<int>(monster._mmode);
monster._mVar2 = 0;
monster._mmode = MonsterMode::Stand;
monster.position.offset = { 0, 0 };
monster.position.future = monster.position.tile;
monster.position.old = monster.position.tile;
UpdateEnemy(monster);
}
void M_ClearSquares(int i)
{
auto &monster = Monsters[i];
int mx = monster.position.old.x;
int my = monster.position.old.y;
int m1 = -(i + 1);
int m2 = i + 1;
for (int y = my - 1; y <= my + 1; y++) {
for (int x = mx - 1; x <= mx + 1; x++) {
if (InDungeonBounds({ x, y }) && (dMonster[x][y] == m1 || dMonster[x][y] == m2))
dMonster[x][y] = 0;
}
}
}
void M_GetKnockback(int i)
{
auto &monster = Monsters[i];
Direction dir = Opposite(monster._mdir);
if (!IsRelativeMoveOK(monster, monster.position.old, dir)) {
return;
}
M_ClearSquares(i);
monster.position.old += dir;
StartMonsterGotHit(i);
}
void M_StartHit(int i, int pnum, int dam)
{
auto &monster = Monsters[i];
if (pnum >= 0)
monster.mWhoHit |= 1 << pnum;
if (pnum == MyPlayerId) {
delta_monster_hp(i, monster._mhitpoints, currlevel);
NetSendCmdMonDmg(false, i, dam);
}
PlayEffect(monster, 1);
if (IsAnyOf(monster.MType->mtype, MT_SNEAK, MT_STALKER, MT_UNSEEN, MT_ILLWEAV) || dam >> 6 >= monster.mLevel + 3) {
if (pnum >= 0) {
monster._menemy = pnum;
monster.enemyPosition = Players[pnum].position.future;
monster._mFlags &= ~MFLAG_TARGETS_MONSTER;
monster._mdir = GetMonsterDirection(monster);
}
if (monster.MType->mtype == MT_BLINK) {
Teleport(i);
} else if (IsAnyOf(monster.MType->mtype, MT_NSCAV, MT_BSCAV, MT_WSCAV, MT_YSCAV)
|| monster.MType->mtype == MT_GRAVEDIG) {
monster._mgoal = MGOAL_NORMAL;
monster._mgoalvar1 = 0;
monster._mgoalvar2 = 0;
}
if (monster._mmode != MonsterMode::Petrified) {
StartMonsterGotHit(i);
}
}
}
void M_StartKill(int i, int pnum)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (MyPlayerId == pnum) {
delta_kill_monster(i, monster.position.tile, currlevel);
if (i != pnum) {
NetSendCmdLocParam1(false, CMD_MONSTDEATH, monster.position.tile, i);
} else {
NetSendCmdLocParam1(false, CMD_KILLGOLEM, monster.position.tile, currlevel);
}
}
StartMonsterDeath(i, pnum, true);
}
void M_SyncStartKill(int i, Point position, int pnum)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
if (monster._mhitpoints == 0 || monster._mmode == MonsterMode::Death) {
return;
}
if (dMonster[position.x][position.y] == 0) {
M_ClearSquares(i);
monster.position.tile = position;
monster.position.old = position;
}
StartMonsterDeath(i, pnum, false);
}
void M_UpdateLeader(int i)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
for (int j = 0; j < ActiveMonsterCount; j++) {
auto &minion = Monsters[ActiveMonsters[j]];
if (minion.leaderRelation == LeaderRelation::Leashed && minion.leader == i)
minion.leaderRelation = LeaderRelation::None;
}
if (monster.leaderRelation == LeaderRelation::Leashed) {
Monsters[monster.leader].packsize--;
}
}
void DoEnding()
{
if (gbIsMultiplayer) {
SNetLeaveGame(LEAVE_ENDING);
}
music_stop();
if (gbIsMultiplayer) {
SDL_Delay(1000);
}
if (gbIsSpawn)
return;
switch (MyPlayer->_pClass) {
case HeroClass::Sorcerer:
case HeroClass::Monk:
play_movie("gendata\\DiabVic1.smk", false);
break;
case HeroClass::Warrior:
case HeroClass::Barbarian:
play_movie("gendata\\DiabVic2.smk", false);
break;
default:
play_movie("gendata\\DiabVic3.smk", false);
break;
}
play_movie("gendata\\Diabend.smk", false);
bool bMusicOn = gbMusicOn;
gbMusicOn = true;
int musicVolume = sound_get_or_set_music_volume(1);
sound_get_or_set_music_volume(0);
music_start(TMUSIC_CATACOMBS);
loop_movie = true;
play_movie("gendata\\loopdend.smk", true);
loop_movie = false;
music_stop();
sound_get_or_set_music_volume(musicVolume);
gbMusicOn = bMusicOn;
}
void PrepDoEnding()
{
gbSoundOn = sgbSaveSoundOn;
gbRunGame = false;
MyPlayerIsDead = false;
cineflag = true;
Player &myPlayer = *MyPlayer;
myPlayer.pDiabloKillLevel = std::max(myPlayer.pDiabloKillLevel, static_cast<uint8_t>(sgGameInitInfo.nDifficulty + 1));
for (Player &player : Players) {
player._pmode = PM_QUIT;
player._pInvincible = true;
if (gbIsMultiplayer) {
if (player._pHitPoints >> 6 == 0)
player._pHitPoints = 64;
if (player._pMana >> 6 == 0)
player._pMana = 64;
}
}
}
void M_WalkDir(int i, Direction md)
{
assert(i >= 0 && i < MAXMONSTERS);
int mwi = Monsters[i].MType->GetAnimData(MonsterGraphic::Walk).Frames - 1;
switch (md) {
case Direction::North:
StartWalk(i, 0, -MWVel[mwi][1], -1, -1, Direction::North);
break;
case Direction::NorthEast:
StartWalk(i, MWVel[mwi][1], -MWVel[mwi][0], 0, -1, Direction::NorthEast);
break;
case Direction::East:
StartWalk3(i, MWVel[mwi][2], 0, -32, -16, 1, -1, 1, 0, Direction::East);
break;
case Direction::SouthEast:
StartWalk2(i, MWVel[mwi][1], MWVel[mwi][0], -32, -16, 1, 0, Direction::SouthEast);
break;
case Direction::South:
StartWalk2(i, 0, MWVel[mwi][1], 0, -32, 1, 1, Direction::South);
break;
case Direction::SouthWest:
StartWalk2(i, -MWVel[mwi][1], MWVel[mwi][0], 32, -16, 0, 1, Direction::SouthWest);
break;
case Direction::West:
StartWalk3(i, -MWVel[mwi][2], 0, 32, -16, -1, 1, 0, 1, Direction::West);
break;
case Direction::NorthWest:
StartWalk(i, -MWVel[mwi][1], -MWVel[mwi][0], -1, 0, Direction::NorthWest);
break;
}
}
void GolumAi(int i)
{
assert(i >= 0 && i < MAX_PLRS);
auto &golem = Monsters[i];
if (golem.position.tile.x == 1 && golem.position.tile.y == 0) {
return;
}
if (IsAnyOf(golem._mmode, MonsterMode::Death, MonsterMode::SpecialStand) || golem.IsWalking()) {
return;
}
if ((golem._mFlags & MFLAG_TARGETS_MONSTER) == 0)
UpdateEnemy(golem);
if (golem._mmode == MonsterMode::MeleeAttack) {
return;
}
if ((golem._mFlags & MFLAG_NO_ENEMY) == 0) {
auto &enemy = Monsters[golem._menemy];
int mex = golem.position.tile.x - enemy.position.future.x;
int mey = golem.position.tile.y - enemy.position.future.y;
golem._mdir = GetDirection(golem.position.tile, enemy.position.tile);
if (abs(mex) < 2 && abs(mey) < 2) {
golem.enemyPosition = enemy.position.tile;
if (enemy._msquelch == 0) {
enemy._msquelch = UINT8_MAX;
enemy.position.last = golem.position.tile;
for (int j = 0; j < 5; j++) {
for (int k = 0; k < 5; k++) {
int enemyId = dMonster[golem.position.tile.x + k - 2][golem.position.tile.y + j - 2]; // BUGFIX: Check if indexes are between 0 and 112
if (enemyId > 0)
Monsters[enemyId - 1]._msquelch = UINT8_MAX; // BUGFIX: should be `Monsters[_menemy-1]`, not Monsters[_menemy]. (fixed)
}
}
}
StartAttack(golem);
return;
}
if (AiPlanPath(i))
return;
}
golem._pathcount++;
if (golem._pathcount > 8)
golem._pathcount = 5;
if (RandomWalk(i, Players[i]._pdir))
return;
Direction md = Left(golem._mdir);
bool ok = false;
for (int j = 0; j < 8 && !ok; j++) {
md = Right(md);
ok = DirOK(i, md);
}
if (ok)
M_WalkDir(i, md);
}
void DeleteMonsterList()
{
for (int i = 0; i < MAX_PLRS; i++) {
auto &golem = Monsters[i];
if (!golem._mDelFlag)
continue;
golem.position.tile = GolemHoldingCell;
golem.position.future = { 0, 0 };
golem.position.old = { 0, 0 };
golem._mDelFlag = false;
}
for (int i = MAX_PLRS; i < ActiveMonsterCount;) {
if (Monsters[ActiveMonsters[i]]._mDelFlag) {
if (pcursmonst == ActiveMonsters[i]) // Unselect monster if player highlighted it
pcursmonst = -1;
DeleteMonster(i);
} else {
i++;
}
}
}
void ProcessMonsters()
{
DeleteMonsterList();
assert(ActiveMonsterCount >= 0 && ActiveMonsterCount <= MAXMONSTERS);
for (int i = 0; i < ActiveMonsterCount; i++) {
int mi = ActiveMonsters[i];
auto &monster = Monsters[mi];
FollowTheLeader(monster);
bool raflag = false;
if (gbIsMultiplayer) {
SetRndSeed(monster._mAISeed);
monster._mAISeed = AdvanceRndSeed();
}
if ((monster._mFlags & MFLAG_NOHEAL) == 0 && monster._mhitpoints < monster._mmaxhp && monster._mhitpoints >> 6 > 0) {
if (monster.mLevel > 1) {
monster._mhitpoints += monster.mLevel / 2;
} else {
monster._mhitpoints += monster.mLevel;
}
monster._mhitpoints = std::min(monster._mhitpoints, monster._mmaxhp); // prevent going over max HP with part of a single regen tick
}
if (IsTileVisible(monster.position.tile) && monster._msquelch == 0) {
if (monster.MType->mtype == MT_CLEAVER) {
PlaySFX(USFX_CLEAVER);
}
if (monster.MType->mtype == MT_NAKRUL) {
if (sgGameInitInfo.bCowQuest != 0) {
PlaySFX(USFX_NAKRUL6);
} else {
if (IsUberRoomOpened)
PlaySFX(USFX_NAKRUL4);
else
PlaySFX(USFX_NAKRUL5);
}
}
if (monster.MType->mtype == MT_DEFILER)
PlaySFX(USFX_DEFILER8);
UpdateEnemy(monster);
}
if ((monster._mFlags & MFLAG_TARGETS_MONSTER) != 0) {
assert(monster._menemy >= 0 && monster._menemy < MAXMONSTERS);
monster.position.last = Monsters[monster._menemy].position.future;
monster.enemyPosition = monster.position.last;
} else {
assert(monster._menemy >= 0 && monster._menemy < MAX_PLRS);
Player &player = Players[monster._menemy];
monster.enemyPosition = player.position.future;
if (IsTileVisible(monster.position.tile)) {
monster._msquelch = UINT8_MAX;
monster.position.last = player.position.future;
} else if (monster._msquelch != 0 && monster.MType->mtype != MT_DIABLO) {
monster._msquelch--;
}
}
do {
if ((monster._mFlags & MFLAG_SEARCH) == 0 || !AiPlanPath(mi)) {
AiProc[monster._mAi](mi);
}
switch (monster._mmode) {
case MonsterMode::Stand:
raflag = MonsterIdle(monster);
break;
case MonsterMode::MoveNorthwards:
case MonsterMode::MoveSouthwards:
case MonsterMode::MoveSideways:
raflag = MonsterWalk(mi, monster._mmode);
break;
case MonsterMode::MeleeAttack:
raflag = MonsterAttack(mi);
break;
case MonsterMode::HitRecovery:
raflag = MonsterGotHit(monster);
break;
case MonsterMode::Death:
raflag = MonsterDeath(mi);
break;
case MonsterMode::SpecialMeleeAttack:
raflag = MonsterSpecialAttack(mi);
break;
case MonsterMode::FadeIn:
raflag = MonsterFadein(monster);
break;
case MonsterMode::FadeOut:
raflag = MonsterFadeout(monster);
break;
case MonsterMode::RangedAttack:
raflag = MonaterRangedAttack(mi);
break;
case MonsterMode::SpecialStand:
raflag = MonsterSpecialStand(monster);
break;
case MonsterMode::SpecialRangedAttack:
raflag = MonsterRangedSpecialAttack(mi);
break;
case MonsterMode::Delay:
raflag = MonsterDelay(monster);
break;
case MonsterMode::Charge:
raflag = false;
break;
case MonsterMode::Petrified:
raflag = MonsterPetrified(monster);
break;
case MonsterMode::Heal:
raflag = MonsterHeal(monster);
break;
case MonsterMode::Talk:
raflag = MonsterTalk(monster);
break;
}
if (raflag) {
GroupUnity(monster);
}
} while (raflag);
if (monster._mmode != MonsterMode::Petrified) {
monster.AnimInfo.ProcessAnimation((monster._mFlags & MFLAG_LOCK_ANIMATION) != 0, (monster._mFlags & MFLAG_ALLOW_SPECIAL) != 0);
}
}
DeleteMonsterList();
}
void FreeMonsters()
{
for (int i = 0; i < LevelMonsterTypeCount; i++) {
LevelMonsterTypes[i].animData = nullptr;
}
}
bool DirOK(int i, Direction mdir)
{
assert(i >= 0 && i < MAXMONSTERS);
auto &monster = Monsters[i];
Point position = monster.position.tile;
Point futurePosition = position + mdir;
if (!IsRelativeMoveOK(monster, position, mdir))
return false;
if (monster.leaderRelation == LeaderRelation::Leashed) {
return futurePosition.WalkingDistance(Monsters[monster.leader].position.future) < 4;
}
if (monster._uniqtype == 0 || UniqueMonstersData[monster._uniqtype - 1].monsterPack != UniqueMonsterPack::Leashed)
return true;
int mcount = 0;
for (int x = futurePosition.x - 3; x <= futurePosition.x + 3; x++) {
for (int y = futurePosition.y - 3; y <= futurePosition.y + 3; y++) {
if (!InDungeonBounds({ x, y }))
continue;
int mi = dMonster[x][y];
if (mi <= 0)
continue;
auto &minion = Monsters[mi - 1];
if (minion.leaderRelation == LeaderRelation::Leashed && minion.leader == i) {
mcount++;
}
}
}
return mcount == monster.packsize;
}
bool PosOkMissile(Point position)
{
return !nMissileTable[dPiece[position.x][position.y]];
}
bool LineClearMissile(Point startPoint, Point endPoint)
{
return LineClear(PosOkMissile, startPoint, endPoint);
}
bool LineClear(const std::function<bool(Point)> &clear, Point startPoint, Point endPoint)
{
Point position = startPoint;
int dx = endPoint.x - position.x;
int dy = endPoint.y - position.y;
if (abs(dx) > abs(dy)) {
if (dx < 0) {
std::swap(position, endPoint);
dx = -dx;
dy = -dy;
}
int d;
int yincD;
int dincD;
int dincH;
if (dy > 0) {
d = 2 * dy - dx;
dincD = 2 * dy;
dincH = 2 * (dy - dx);
yincD = 1;
} else {
d = 2 * dy + dx;
dincD = 2 * dy;
dincH = 2 * (dx + dy);
yincD = -1;
}
bool done = false;
while (!done && position != endPoint) {
if ((d <= 0) ^ (yincD < 0)) {
d += dincD;
} else {
d += dincH;
position.y += yincD;
}
position.x++;
done = position != startPoint && !clear(position);
}
} else {
if (dy < 0) {
std::swap(position, endPoint);
dy = -dy;
dx = -dx;
}
int d;
int xincD;
int dincD;
int dincH;
if (dx > 0) {
d = 2 * dx - dy;
dincD = 2 * dx;
dincH = 2 * (dx - dy);
xincD = 1;
} else {
d = 2 * dx + dy;
dincD = 2 * dx;
dincH = 2 * (dy + dx);
xincD = -1;
}
bool done = false;
while (!done && position != endPoint) {
if ((d <= 0) ^ (xincD < 0)) {
d += dincD;
} else {
d += dincH;
position.x += xincD;
}
position.y++;
done = position != startPoint && !clear(position);
}
}
return position == endPoint;
}
void SyncMonsterAnim(Monster &monster)
{
monster.MType = &LevelMonsterTypes[monster._mMTidx];
#ifdef _DEBUG
// fix for saves with debug monsters having type originally not on the level
if (LevelMonsterTypes[monster._mMTidx].MData == nullptr) {
InitMonsterGFX(monster._mMTidx);
LevelMonsterTypes[monster._mMTidx].mdeadval = 1;
}
#endif
monster.MData = LevelMonsterTypes[monster._mMTidx].MData;
if (monster._uniqtype != 0)
monster.mName = pgettext("monster", UniqueMonstersData[monster._uniqtype - 1].mName).c_str();
else
monster.mName = pgettext("monster", monster.MData->mName).c_str();
if (monster._uniqtype != 0)
InitTRNForUniqueMonster(monster);
MonsterGraphic graphic = MonsterGraphic::Stand;
switch (monster._mmode) {
case MonsterMode::Stand:
case MonsterMode::Delay:
case MonsterMode::Talk:
break;
case MonsterMode::MoveNorthwards:
case MonsterMode::MoveSouthwards:
case MonsterMode::MoveSideways:
graphic = MonsterGraphic::Walk;
break;
case MonsterMode::MeleeAttack:
case MonsterMode::RangedAttack:
graphic = MonsterGraphic::Attack;
break;
case MonsterMode::HitRecovery:
graphic = MonsterGraphic::GotHit;
break;
case MonsterMode::Death:
graphic = MonsterGraphic::Death;
break;
case MonsterMode::SpecialMeleeAttack:
case MonsterMode::FadeIn:
case MonsterMode::FadeOut:
case MonsterMode::SpecialStand:
case MonsterMode::SpecialRangedAttack:
case MonsterMode::Heal:
graphic = MonsterGraphic::Special;
break;
case MonsterMode::Charge:
graphic = MonsterGraphic::Attack;
monster.AnimInfo.CurrentFrame = 0;
break;
default:
monster.AnimInfo.CurrentFrame = 0;
break;
}
monster.ChangeAnimationData(graphic);
}
void M_FallenFear(Point position)
{
for (int i = 0; i < ActiveMonsterCount; i++) {
auto &monster = Monsters[ActiveMonsters[i]];
if (monster._mAi != AI_FALLEN)
continue;
if (position.WalkingDistance(monster.position.tile) >= 5)
continue;
if (monster._mhitpoints >> 6 <= 0)
continue;
int rundist;
switch (monster.MType->mtype) {
case MT_RFALLSP:
case MT_RFALLSD:
rundist = 7;
break;
case MT_DFALLSP:
case MT_DFALLSD:
rundist = 5;
break;
case MT_YFALLSP:
case MT_YFALLSD:
rundist = 3;
break;
case MT_BFALLSP:
case MT_BFALLSD:
rundist = 2;
break;
default:
continue;
}
monster._mgoal = MGOAL_RETREAT;
monster._mgoalvar1 = rundist;
monster._mgoalvar2 = static_cast<int>(GetDirection(position, monster.position.tile));
}
}
void PrintMonstHistory(int mt)
{
if (*sgOptions.Gameplay.showMonsterType) {
AddPanelString(fmt::format(_("Type: {:s} Kills: {:d}"), GetMonsterTypeText(MonstersData[mt]), MonsterKillCounts[mt]));
} else {
AddPanelString(fmt::format(_("Total kills: {:d}"), MonsterKillCounts[mt]));
}
if (MonsterKillCounts[mt] >= 30) {
int minHP = MonstersData[mt].mMinHP;
int maxHP = MonstersData[mt].mMaxHP;
if (!gbIsHellfire && mt == MT_DIABLO) {
minHP /= 2;
maxHP /= 2;
}
if (!gbIsMultiplayer) {
minHP /= 2;
maxHP /= 2;
}
if (minHP < 1)
minHP = 1;
if (maxHP < 1)
maxHP = 1;
int hpBonusNightmare = 1;
int hpBonusHell = 3;
if (gbIsHellfire) {
hpBonusNightmare = (!gbIsMultiplayer ? 50 : 100);
hpBonusHell = (!gbIsMultiplayer ? 100 : 200);
}
if (sgGameInitInfo.nDifficulty == DIFF_NIGHTMARE) {
minHP = 3 * minHP + hpBonusNightmare;
maxHP = 3 * maxHP + hpBonusNightmare;
} else if (sgGameInitInfo.nDifficulty == DIFF_HELL) {
minHP = 4 * minHP + hpBonusHell;
maxHP = 4 * maxHP + hpBonusHell;
}
AddPanelString(fmt::format(_("Hit Points: {:d}-{:d}"), minHP, maxHP));
}
if (MonsterKillCounts[mt] >= 15) {
int res = (sgGameInitInfo.nDifficulty != DIFF_HELL) ? MonstersData[mt].mMagicRes : MonstersData[mt].mMagicRes2;
if ((res & (RESIST_MAGIC | RESIST_FIRE | RESIST_LIGHTNING | IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING)) == 0) {
AddPanelString(_("No magic resistance"));
} else {
if ((res & (RESIST_MAGIC | RESIST_FIRE | RESIST_LIGHTNING)) != 0) {
std::string resists = _("Resists:");
if ((res & RESIST_MAGIC) != 0)
resists.append(_(" Magic"));
if ((res & RESIST_FIRE) != 0)
resists.append(_(" Fire"));
if ((res & RESIST_LIGHTNING) != 0)
resists.append(_(" Lightning"));
AddPanelString(resists);
}
if ((res & (IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING)) != 0) {
std::string immune = _("Immune:");
if ((res & IMMUNE_MAGIC) != 0)
immune.append(_(" Magic"));
if ((res & IMMUNE_FIRE) != 0)
immune.append(_(" Fire"));
if ((res & IMMUNE_LIGHTNING) != 0)
immune.append(_(" Lightning"));
AddPanelString(immune);
}
}
}
}
void PrintUniqueHistory()
{
auto &monster = Monsters[pcursmonst];
if (*sgOptions.Gameplay.showMonsterType) {
AddPanelString(fmt::format(_("Type: {:s}"), GetMonsterTypeText(*monster.MData)));
}
int res = monster.mMagicRes & (RESIST_MAGIC | RESIST_FIRE | RESIST_LIGHTNING | IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING);
if (res == 0) {
AddPanelString(_("No resistances"));
AddPanelString(_("No Immunities"));
} else {
if ((res & (RESIST_MAGIC | RESIST_FIRE | RESIST_LIGHTNING)) != 0)
AddPanelString(_("Some Magic Resistances"));
else
AddPanelString(_("No resistances"));
if ((res & (IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING)) != 0) {
AddPanelString(_("Some Magic Immunities"));
} else {
AddPanelString(_("No Immunities"));
}
}
}
void PlayEffect(Monster &monster, int mode)
{
if (MyPlayer->pLvlLoad != 0) {
return;
}
int sndIdx = GenerateRnd(2);
if (!gbSndInited || !gbSoundOn || gbBufferMsgs != 0) {
return;
}
int mi = monster._mMTidx;
TSnd *snd = LevelMonsterTypes[mi].Snds[mode][sndIdx].get();
if (snd == nullptr || snd->isPlaying()) {
return;
}
int lVolume = 0;
int lPan = 0;
if (!CalculateSoundPosition(monster.position.tile, &lVolume, &lPan))
return;
snd_play_snd(snd, lVolume, lPan);
}
void MissToMonst(Missile &missile, Point position)
{
int m = missile._misource;
assert(m >= 0 && m < MAXMONSTERS);
auto &monster = Monsters[m];
Point oldPosition = missile.position.tile;
dMonster[position.x][position.y] = m + 1;
monster._mdir = static_cast<Direction>(missile._mimfnum);
monster.position.tile = position;
M_StartStand(monster, monster._mdir);
if ((monster._mFlags & MFLAG_TARGETS_MONSTER) == 0)
M_StartHit(m, -1, 0);
else
MonsterHitMonster(m, -1, 0);
if (monster.MType->mtype == MT_GLOOM)
return;
if ((monster._mFlags & MFLAG_TARGETS_MONSTER) == 0) {
if (dPlayer[oldPosition.x][oldPosition.y] <= 0)
return;
int pnum = dPlayer[oldPosition.x][oldPosition.y] - 1;
MonsterAttackPlayer(m, pnum, 500, monster.mMinDamage2, monster.mMaxDamage2);
if (IsAnyOf(monster.MType->mtype, MT_NSNAKE, MT_RSNAKE, MT_BSNAKE, MT_GSNAKE))
return;
Player &player = Players[pnum];
if (player._pmode != PM_GOTHIT && player._pmode != PM_DEATH)
StartPlrHit(pnum, 0, true);
Point newPosition = oldPosition + monster._mdir;
if (PosOkPlayer(player, newPosition)) {
player.position.tile = newPosition;
FixPlayerLocation(pnum, player._pdir);
FixPlrWalkTags(pnum);
dPlayer[newPosition.x][newPosition.y] = pnum + 1;
SetPlayerOld(player);
}
return;
}
if (dMonster[oldPosition.x][oldPosition.y] <= 0)
return;
int mid = dMonster[oldPosition.x][oldPosition.y] - 1;
MonsterAttackMonster(m, mid, 500, monster.mMinDamage2, monster.mMaxDamage2);
if (IsAnyOf(monster.MType->mtype, MT_NSNAKE, MT_RSNAKE, MT_BSNAKE, MT_GSNAKE))
return;
Point newPosition = oldPosition + monster._mdir;
if (IsTileAvailable(Monsters[mid], newPosition)) {
m = dMonster[oldPosition.x][oldPosition.y];
dMonster[newPosition.x][newPosition.y] = m;
dMonster[oldPosition.x][oldPosition.y] = 0;
m--;
monster.position.tile = newPosition;
monster.position.future = newPosition;
}
}
bool IsTileAvailable(const Monster &monster, Point position)
{
if (!IsTileAvailable(position))
return false;
return IsTileSafe(monster, position);
}
bool IsSkel(_monster_id mt)
{
return std::find(std::begin(SkeletonTypes), std::end(SkeletonTypes), mt) != std::end(SkeletonTypes);
}
bool IsGoat(_monster_id mt)
{
return IsAnyOf(mt,
MT_NGOATMC, MT_BGOATMC, MT_RGOATMC, MT_GGOATMC,
MT_NGOATBW, MT_BGOATBW, MT_RGOATBW, MT_GGOATBW);
}
bool SpawnSkeleton(int ii, Point position)
{
if (ii == -1)
return false;
if (IsTileAvailable(position)) {
Direction dir = GetDirection(position, position); // TODO useless calculation
ActivateSpawn(ii, position, dir);
return true;
}
bool monstok[3][3];
bool savail = false;
int yy = 0;
for (int j = position.y - 1; j <= position.y + 1; j++) {
int xx = 0;
for (int k = position.x - 1; k <= position.x + 1; k++) {
monstok[xx][yy] = IsTileAvailable({ k, j });
savail = savail || monstok[xx][yy];
xx++;
}
yy++;
}
if (!savail) {
return false;
}
int rs = GenerateRnd(15) + 1;
int x2 = 0;
int y2 = 0;
while (rs > 0) {
if (monstok[x2][y2])
rs--;
if (rs > 0) {
x2++;
if (x2 == 3) {
x2 = 0;
y2++;
if (y2 == 3)
y2 = 0;
}
}
}
Point spawn = position + Displacement { x2 - 1, y2 - 1 };
Direction dir = GetDirection(spawn, position);
ActivateSpawn(ii, spawn, dir);
return true;
}
int PreSpawnSkeleton()
{
int skel = AddSkeleton({ 0, 0 }, Direction::South, false);
if (skel != -1)
M_StartStand(Monsters[skel], Direction::South);
return skel;
}
void TalktoMonster(Monster &monster)
{
Player &player = Players[monster._menemy];
monster._mmode = MonsterMode::Talk;
if (monster._mAi != AI_SNOTSPIL && monster._mAi != AI_LACHDAN) {
return;
}
if (Quests[Q_LTBANNER].IsAvailable() && Quests[Q_LTBANNER]._qvar1 == 2) {
if (player.TryRemoveInvItemById(IDI_BANNER)) {
Quests[Q_LTBANNER]._qactive = QUEST_DONE;
monster.mtalkmsg = TEXT_BANNER12;
monster._mgoal = MGOAL_INQUIRING;
}
}
if (Quests[Q_VEIL].IsAvailable() && monster.mtalkmsg >= TEXT_VEIL9) {
if (player.TryRemoveInvItemById(IDI_GLDNELIX)) {
monster.mtalkmsg = TEXT_VEIL11;
monster._mgoal = MGOAL_INQUIRING;
}
}
}
void SpawnGolem(int i, Point position, Missile &missile)
{
assert(i >= 0 && i < MAX_PLRS);
Player &player = Players[i];
auto &golem = Monsters[i];
dMonster[position.x][position.y] = i + 1;
golem.position.tile = position;
golem.position.future = position;
golem.position.old = position;
golem._pathcount = 0;
golem._mmaxhp = 2 * (320 * missile._mispllvl + player._pMaxMana / 3);
golem._mhitpoints = golem._mmaxhp;
golem.mArmorClass = 25;
golem.mHit = 5 * (missile._mispllvl + 8) + 2 * player._pLevel;
golem.mMinDamage = 2 * (missile._mispllvl + 4);
golem.mMaxDamage = 2 * (missile._mispllvl + 8);
golem._mFlags |= MFLAG_GOLEM;
StartSpecialStand(golem, Direction::South);
UpdateEnemy(golem);
if (i == MyPlayerId) {
NetSendCmdGolem(
golem.position.tile.x,
golem.position.tile.y,
golem._mdir,
golem._menemy,
golem._mhitpoints,
currlevel);
}
}
bool CanTalkToMonst(const Monster &monster)
{
return IsAnyOf(monster._mgoal, MGOAL_INQUIRING, MGOAL_TALKING);
}
int encode_enemy(Monster &monster)
{
if ((monster._mFlags & MFLAG_TARGETS_MONSTER) != 0)
return monster._menemy + MAX_PLRS;
return monster._menemy;
}
void decode_enemy(Monster &monster, int enemyId)
{
if (enemyId < MAX_PLRS) {
monster._mFlags &= ~MFLAG_TARGETS_MONSTER;
monster._menemy = enemyId;
monster.enemyPosition = Players[enemyId].position.future;
} else {
monster._mFlags |= MFLAG_TARGETS_MONSTER;
enemyId -= MAX_PLRS;
monster._menemy = enemyId;
monster.enemyPosition = Monsters[enemyId].position.future;
}
}
void Monster::CheckStandAnimationIsLoaded(Direction mdir)
{
if (IsAnyOf(_mmode, MonsterMode::Stand, MonsterMode::Talk)) {
_mdir = mdir;
ChangeAnimationData(MonsterGraphic::Stand);
}
}
void Monster::Petrify()
{
_mmode = MonsterMode::Petrified;
AnimInfo.IsPetrified = true;
}
bool Monster::IsWalking() const
{
switch (_mmode) {
case MonsterMode::MoveNorthwards:
case MonsterMode::MoveSouthwards:
case MonsterMode::MoveSideways:
return true;
default:
return false;
}
}
bool Monster::IsImmune(missile_id mName) const
{
missile_resistance missileElement = MissilesData[mName].mResist;
if (((mMagicRes & IMMUNE_MAGIC) != 0 && missileElement == MISR_MAGIC)
|| ((mMagicRes & IMMUNE_FIRE) != 0 && missileElement == MISR_FIRE)
|| ((mMagicRes & IMMUNE_LIGHTNING) != 0 && missileElement == MISR_LIGHTNING)
|| ((mMagicRes & IMMUNE_ACID) != 0 && missileElement == MISR_ACID))
return true;
if (mName == MIS_HBOLT && MType->mtype != MT_DIABLO && MData->mMonstClass != MonsterClass::Undead)
return true;
return false;
}
bool Monster::IsResistant(missile_id mName) const
{
missile_resistance missileElement = MissilesData[mName].mResist;
if (((mMagicRes & RESIST_MAGIC) != 0 && missileElement == MISR_MAGIC)
|| ((mMagicRes & RESIST_FIRE) != 0 && missileElement == MISR_FIRE)
|| ((mMagicRes & RESIST_LIGHTNING) != 0 && missileElement == MISR_LIGHTNING))
return true;
if (gbIsHellfire && mName == MIS_HBOLT && IsAnyOf(MType->mtype, MT_DIABLO, MT_BONEDEMN))
return true;
return false;
}
bool Monster::IsPossibleToHit() const
{
return !(_mhitpoints >> 6 <= 0
|| mtalkmsg != TEXT_NONE
|| (MType->mtype == MT_ILLWEAV && _mgoal == MGOAL_RETREAT)
|| _mmode == MonsterMode::Charge
|| (IsAnyOf(MType->mtype, MT_COUNSLR, MT_MAGISTR, MT_CABALIST, MT_ADVOCATE) && _mgoal != MGOAL_NORMAL));
}
bool Monster::TryLiftGargoyle()
{
if (_mAi == AI_GARG && (_mFlags & MFLAG_ALLOW_SPECIAL) != 0) {
_mFlags &= ~MFLAG_ALLOW_SPECIAL;
_mmode = MonsterMode::SpecialMeleeAttack;
return true;
}
return false;
}
} // namespace devilution