/** * @file monster.cpp * * Implementation of monster functionality, AI, actions, spawning, loading, etc. */ #include "monster.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include #include #include "automap.h" #include "control.h" #include "crawl.hpp" #include "cursor.h" #include "dead.h" #include "diablo.h" #include "dvlnet/leaveinfo.hpp" #include "effects.h" #include "engine/animationinfo.h" #include "engine/clx_sprite.hpp" #include "engine/direction.hpp" #include "engine/lighting_defs.hpp" #include "engine/load_cl2.hpp" #include "engine/load_file.hpp" #include "engine/path.h" #include "engine/point.hpp" #include "engine/points_in_rectangle_range.hpp" #include "engine/random.hpp" #include "engine/render/clx_render.hpp" #include "engine/sound.h" #include "engine/sound_position.hpp" #include "engine/world_tile.hpp" #include "function_ref.hpp" #include "game_mode.hpp" #include "headless_mode.hpp" #include "inv.h" #include "itemdat.h" #include "items.h" #include "levels/crypt.h" #include "levels/drlg_l4.h" #include "levels/dun_tile.hpp" #include "levels/gendung.h" #include "levels/gendung_defs.hpp" #include "levels/themes.h" #include "levels/tile_properties.hpp" #include "levels/trigs.h" #include "lighting.h" #include "minitext.h" #include "misdat.h" #include "missiles.h" #include "monstdat.h" #include "movie.h" #include "msg.h" #include "multi.h" #include "objdat.h" #include "objects.h" #include "options.h" #include "player.h" #include "playerdat.hpp" #include "qol/floatingnumbers.h" #include "quests.h" #include "sound_effect_enums.h" #include "spelldat.h" #include "storm/storm_net.hpp" #include "textdat.h" #include "utils/algorithm/container.hpp" #include "utils/attributes.h" #include "utils/cl2_to_clx.hpp" #include "utils/endian_swap.hpp" #include "utils/enum_traits.h" #include "utils/file_name_generator.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/log.hpp" #include "utils/pointer_value_union.hpp" #include "utils/static_vector.hpp" #include "utils/status_macros.hpp" #include "utils/str_cat.hpp" #ifdef _DEBUG #include "debug.h" #endif namespace devilution { CMonster LevelMonsterTypes[MaxLvlMTypes]; size_t LevelMonsterTypeCount; Monster Monsters[MaxMonsters]; unsigned ActiveMonsters[MaxMonsters]; size_t ActiveMonsterCount; /** Tracks the total number of monsters killed per monster_id. */ int MonsterKillCounts[NUM_MAX_MTYPES]; bool sgbSaveSoundOn; namespace { constexpr int NightmareToHitBonus = 85; constexpr int HellToHitBonus = 120; constexpr int NightmareAcBonus = 50; constexpr int HellAcBonus = 80; /** @brief Reserved some entries in @Monster for golems. For vanilla compatibility, this must remain 4. */ constexpr int ReservedMonsterSlotsForGolems = 4; /** Tracks which missile files are already loaded */ size_t totalmonsters; int monstimgtot; int uniquetrans; constexpr const 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, }; /** Maps from monster action to monster animation letter. */ constexpr char Animletter[7] = "nwahds"; size_t GetNumAnims(const MonsterData &monsterData) { return monsterData.hasSpecial ? 6 : 5; } size_t GetNumAnimsWithGraphics(const MonsterData &monsterData) { // Monster graphics can be missing for certain actions, // e.g. Golem has no standing graphics. const size_t numAnims = GetNumAnims(monsterData); size_t result = 0; for (size_t i = 0; i < numAnims; ++i) { if (monsterData.hasAnim(i)) ++result; } return result; } void InitMonsterTRN(CMonster &monst) { char path[64]; *BufCopy(path, "monsters\\", monst.data().trnFile, ".trn") = '\0'; std::array colorTranslations; LoadFileInMem(path, colorTranslations); std::replace(colorTranslations.begin(), colorTranslations.end(), 255, 0); const size_t numAnims = GetNumAnims(monst.data()); for (size_t i = 0; i < numAnims; i++) { if (i == 1 && IsAnyOf(monst.type, MT_COUNSLR, MT_MAGISTR, MT_CABALIST, MT_ADVOCATE)) { continue; } AnimStruct &anim = monst.anims[i]; if (anim.sprites->isSheet()) { ClxApplyTrans(ClxSpriteSheet { anim.sprites->sheet() }, colorTranslations.data()); } else { ClxApplyTrans(ClxSpriteList { anim.sprites->list() }, colorTranslations.data()); } } } void InitMonster(Monster &monster, Direction rd, size_t typeIndex, Point position) { monster.direction = rd; monster.position.tile = position; monster.position.future = position; monster.position.old = position; monster.levelType = static_cast(typeIndex); monster.mode = MonsterMode::Stand; monster.animInfo = {}; monster.changeAnimationData(MonsterGraphic::Stand); monster.animInfo.tickCounterOfCurrentFrame = GenerateRnd(monster.animInfo.ticksPerFrame - 1); monster.animInfo.currentFrame = GenerateRnd(monster.animInfo.numberOfFrames - 1); const int maxhp = RandomIntBetween(monster.data().hitPointsMinimum, monster.data().hitPointsMaximum); monster.maxHitPoints = maxhp << 6; if (!gbIsMultiplayer) monster.maxHitPoints = std::max(monster.maxHitPoints / 2, 64); monster.hitPoints = monster.maxHitPoints; monster.ai = monster.data().ai; monster.intelligence = monster.data().intelligence; monster.goal = MonsterGoal::Normal; monster.goalVar1 = 0; monster.goalVar2 = 0; monster.goalVar3 = 0; monster.pathCount = 0; monster.enemy = 0; monster.isInvalid = false; monster.uniqueType = UniqueMonsterType::None; monster.activeForTicks = 0; monster.lightId = NO_LIGHT; monster.rndItemSeed = AdvanceRndSeed(); monster.aiSeed = AdvanceRndSeed(); monster.whoHit = 0; monster.minDamage = monster.data().minDamage; monster.maxDamage = monster.data().maxDamage; monster.minDamageSpecial = monster.data().minDamageSpecial; monster.maxDamageSpecial = monster.data().maxDamageSpecial; monster.armorClass = monster.data().armorClass; monster.resistance = monster.data().resistance; monster.leader = Monster::NoLeader; monster.leaderRelation = LeaderRelation::None; monster.flags = monster.data().abilityFlags; monster.talkMsg = TEXT_NONE; if (monster.ai == MonsterAIID::Gargoyle) { monster.changeAnimationData(MonsterGraphic::Special); monster.animInfo.currentFrame = 0; monster.flags |= MFLAG_ALLOW_SPECIAL; monster.mode = MonsterMode::SpecialMeleeAttack; } if (sgGameInitInfo.nDifficulty == DIFF_NIGHTMARE) { monster.maxHitPoints = 3 * monster.maxHitPoints; if (gbIsHellfire) monster.maxHitPoints += (gbIsMultiplayer ? 100 : 50) << 6; else monster.maxHitPoints += 100 << 6; monster.hitPoints = monster.maxHitPoints; monster.minDamage = 2 * (monster.minDamage + 2); monster.maxDamage = 2 * (monster.maxDamage + 2); monster.minDamageSpecial = 2 * (monster.minDamageSpecial + 2); monster.maxDamageSpecial = 2 * (monster.maxDamageSpecial + 2); monster.armorClass += NightmareAcBonus; } else if (sgGameInitInfo.nDifficulty == DIFF_HELL) { monster.maxHitPoints = 4 * monster.maxHitPoints; if (gbIsHellfire) monster.maxHitPoints += (gbIsMultiplayer ? 200 : 100) << 6; else monster.maxHitPoints += 200 << 6; monster.hitPoints = monster.maxHitPoints; monster.minDamage = 4 * monster.minDamage + 6; monster.maxDamage = 4 * monster.maxDamage + 6; monster.minDamageSpecial = 4 * monster.minDamageSpecial + 6; monster.maxDamageSpecial = 4 * monster.maxDamageSpecial + 6; monster.armorClass += HellAcBonus; monster.resistance = monster.data().resistanceHell; } } bool CanPlaceMonster(Point position) { return InDungeonBounds(position) && dMonster[position.x][position.y] == 0 && dPlayer[position.x][position.y] == 0 && !IsTileVisible(position) && !TileContainsSetPiece(position) && !IsTileOccupied(position); } void PlaceMonster(size_t i, size_t typeIndex, Point position) { if (LevelMonsterTypes[typeIndex].type == MT_NAKRUL) { for (size_t j = 0; j < ActiveMonsterCount; j++) { if (Monsters[j].levelType == typeIndex) { return; } } } Monster &monster = Monsters[i]; monster.occupyTile(position, false); auto rd = static_cast(GenerateRnd(8)); InitMonster(monster, rd, typeIndex, position); } void PlaceGroup(size_t typeIndex, size_t num, Monster *leader = nullptr, bool leashed = false) { uint8_t placed = 0; for (int try1 = 0; try1 < 10; try1++) { while (placed != 0) { ActiveMonsterCount--; placed--; const Point &position = Monsters[ActiveMonsterCount].position.tile; dMonster[position.x][position.y] = 0; } int xp; int yp; if (leader != nullptr) { const int offset = GenerateRnd(8); auto position = leader->position.tile + static_cast(offset); xp = position.x; yp = position.y; } else { do { xp = GenerateRnd(80) + 16; yp = GenerateRnd(80) + 16; } while (!CanPlaceMonster({ xp, yp })); } const int x1 = xp; const int y1 = yp; if (num + ActiveMonsterCount > totalmonsters) { num = totalmonsters - ActiveMonsterCount; } unsigned j = 0; for (unsigned try2 = 0; j < num && try2 < 100; xp += Displacement(static_cast(GenerateRnd(8))).deltaX, yp += Displacement(static_cast(GenerateRnd(8))).deltaX) { /// BUGFIX: `yp += Point.y` if (!CanPlaceMonster({ xp, yp }) || (dTransVal[xp][yp] != dTransVal[x1][y1]) || (leashed && (std::abs(xp - x1) >= 4 || std::abs(yp - y1) >= 4))) { try2++; continue; } PlaceMonster(ActiveMonsterCount, typeIndex, { xp, yp }); if (leader != nullptr) { Monster &minion = Monsters[ActiveMonsterCount]; minion.maxHitPoints *= 2; minion.hitPoints = minion.maxHitPoints; minion.intelligence = leader->intelligence; if (leashed) { minion.setLeader(leader); } if (minion.ai != MonsterAIID::Gargoyle) { minion.changeAnimationData(MonsterGraphic::Stand); minion.animInfo.currentFrame = GenerateRnd(minion.animInfo.numberOfFrames - 1); minion.flags &= ~MFLAG_ALLOW_SPECIAL; minion.mode = MonsterMode::Stand; } } ActiveMonsterCount++; placed++; j++; } if (placed >= num) { break; } } if (leashed) { leader->packSize = placed; } } size_t GetMonsterTypeIndex(_monster_id type) { for (size_t i = 0; i < LevelMonsterTypeCount; i++) { if (LevelMonsterTypes[i].type == type) return i; } return LevelMonsterTypeCount; } Point GetUniqueMonstPosition(UniqueMonsterType uniqindex) { if (setlevel) { switch (uniqindex) { case UniqueMonsterType::Lazarus: return { 32, 46 }; case UniqueMonsterType::RedVex: return { 40, 45 }; case UniqueMonsterType::BlackJade: return { 38, 49 }; case UniqueMonsterType::SkeletonKing: return { 35, 47 }; default: break; } } switch (uniqindex) { case UniqueMonsterType::SnotSpill: return SetPiece.position.megaToWorld() + Displacement { 8, 12 }; case UniqueMonsterType::WarlordOfBlood: return SetPiece.position.megaToWorld() + Displacement { 6, 7 }; case UniqueMonsterType::Zhar: for (int i = 0; i < themeCount; i++) { if (i == zharlib) { return themeLoc[i].room.position.megaToWorld() + Displacement { 4, 4 }; } } break; case UniqueMonsterType::Lazarus: return SetPiece.position.megaToWorld() + Displacement { 3, 6 }; case UniqueMonsterType::RedVex: return SetPiece.position.megaToWorld() + Displacement { 5, 3 }; case UniqueMonsterType::BlackJade: return SetPiece.position.megaToWorld() + Displacement { 5, 9 }; case UniqueMonsterType::Butcher: return SetPiece.position.megaToWorld() + Displacement { 4, 4 }; case UniqueMonsterType::NaKrul: if (UberRow == 0 || UberCol == 0) { UberDiabloMonsterIndex = -1; break; } UberDiabloMonsterIndex = static_cast(ActiveMonsterCount); return { UberRow - 2, UberCol }; default: break; } Point position; int count = 0; do { position = Point { GenerateRnd(80), GenerateRnd(80) } + Displacement { 16, 16 }; int count2 = 0; for (int x = position.x - 3; x < position.x + 3; x++) { for (int y = position.y - 3; y < position.y + 3; y++) { if (InDungeonBounds({ x, y }) && CanPlaceMonster({ x, y })) { count2++; } } } if (count2 < 9) { count++; if (count < 1000) { continue; } } } while (!CanPlaceMonster(position)); return position; } tl::expected PlaceUniqueMonst(UniqueMonsterType uniqindex, size_t minionType, int bosspacksize) { const auto &uniqueMonsterData = UniqueMonstersData[static_cast(uniqindex)]; const size_t typeIndex = GetMonsterTypeIndex(uniqueMonsterData.mtype); const Point position = GetUniqueMonstPosition(uniqindex); PlaceMonster(ActiveMonsterCount, typeIndex, position); Monster &monster = Monsters[ActiveMonsterCount]; ActiveMonsterCount++; return PrepareUniqueMonst(monster, uniqindex, minionType, bosspacksize, uniqueMonsterData); } void ClearMVars(Monster &monster) { monster.var1 = 0; monster.var2 = 0; monster.var3 = 0; monster.position.temp = { 0, 0 }; } void ClrAllMonsters() { for (auto &monster : Monsters) { ClearMVars(monster); monster.goal = MonsterGoal::None; monster.mode = MonsterMode::Stand; monster.var1 = 0; monster.var2 = 0; monster.position.tile = { 0, 0 }; monster.position.future = { 0, 0 }; monster.position.old = { 0, 0 }; monster.direction = static_cast(GenerateRnd(8)); monster.animInfo = {}; monster.flags = MFLAG_NO_ENEMY; monster.isInvalid = false; monster.enemy = 0; monster.enemyPosition = {}; DiscardRandomValues(1); } } tl::expected PlaceUniqueMonsters() { for (size_t u = 0; u < UniqueMonstersData.size(); ++u) { if (UniqueMonstersData[u].mlevel != currlevel) continue; const size_t minionType = GetMonsterTypeIndex(UniqueMonstersData[u].mtype); if (minionType == LevelMonsterTypeCount) continue; const auto uniqueType = static_cast(u); if (uniqueType == UniqueMonsterType::Garbud && Quests[Q_GARBUD]._qactive == QUEST_NOTAVAIL) continue; if (uniqueType == UniqueMonsterType::Zhar && Quests[Q_ZHAR]._qactive == QUEST_NOTAVAIL) continue; if (uniqueType == UniqueMonsterType::SnotSpill && Quests[Q_LTBANNER]._qactive == QUEST_NOTAVAIL) continue; if (uniqueType == UniqueMonsterType::Lachdan && Quests[Q_VEIL]._qactive == QUEST_NOTAVAIL) continue; if (uniqueType == UniqueMonsterType::WarlordOfBlood && Quests[Q_WARLORD]._qactive == QUEST_NOTAVAIL) continue; RETURN_IF_ERROR(PlaceUniqueMonst(uniqueType, minionType, 8)); } return {}; } tl::expected PlaceQuestMonsters() { if (!setlevel) { if (Quests[Q_BUTCHER].IsAvailable()) { RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::Butcher, 0, 0)); } if (currlevel == Quests[Q_SKELKING]._qlevel && UseMultiplayerQuests()) { for (size_t i = 0; i < LevelMonsterTypeCount; i++) { if (IsSkel(LevelMonsterTypes[i].type)) { RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::SkeletonKing, i, 30)); break; } } } if (Quests[Q_LTBANNER].IsAvailable()) { auto dunData = LoadFileInMem("levels\\l1data\\banner1.dun"); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), SetPiece.position.megaToWorld())); } if (Quests[Q_BLOOD].IsAvailable()) { auto dunData = LoadFileInMem("levels\\l2data\\blood2.dun"); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), SetPiece.position.megaToWorld())); } if (Quests[Q_BLIND].IsAvailable()) { auto dunData = LoadFileInMem("levels\\l2data\\blind2.dun"); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), SetPiece.position.megaToWorld())); } if (Quests[Q_ANVIL].IsAvailable()) { auto dunData = LoadFileInMem("levels\\l3data\\anvil.dun"); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), SetPiece.position.megaToWorld() + Displacement { 2, 2 })); } if (Quests[Q_WARLORD].IsAvailable()) { auto dunData = LoadFileInMem("levels\\l4data\\warlord.dun"); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), SetPiece.position.megaToWorld())); RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::WarlordOfBlood, PLACE_SCATTER)); } if (Quests[Q_VEIL].IsAvailable()) { RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::Lachdan, PLACE_SCATTER)); } if (Quests[Q_ZHAR].IsAvailable() && zharlib == -1) { Quests[Q_ZHAR]._qactive = QUEST_NOTAVAIL; } if (currlevel == Quests[Q_BETRAYER]._qlevel && UseMultiplayerQuests()) { RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::Lazarus, PLACE_UNIQUE)); RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::RedVex, PLACE_UNIQUE)); RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::Lazarus, 0, 0)); RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::RedVex, 0, 0)); RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::BlackJade, 0, 0)); auto dunData = LoadFileInMem("levels\\l4data\\vile1.dun"); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), SetPiece.position.megaToWorld())); } if (currlevel == 24) { UberDiabloMonsterIndex = -1; const size_t typeIndex = GetMonsterTypeIndex(MT_NAKRUL); if (typeIndex < LevelMonsterTypeCount) { for (size_t i = 0; i < ActiveMonsterCount; i++) { const Monster &monster = Monsters[i]; if (monster.isUnique() || monster.levelType == typeIndex) { UberDiabloMonsterIndex = static_cast(i); break; } } } if (UberDiabloMonsterIndex == -1) RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::NaKrul, 0, 0)); } } else if (setlvlnum == SL_SKELKING) { RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::SkeletonKing, 0, 0)); } else if (setlvlnum == SL_VILEBETRAYER) { RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::Lazarus, PLACE_UNIQUE)); RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::RedVex, PLACE_UNIQUE)); RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::BlackJade, PLACE_UNIQUE)); RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::Lazarus, 0, 0)); RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::RedVex, 0, 0)); RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::BlackJade, 0, 0)); } return {}; } tl::expected LoadDiabMonsts() { { ASSIGN_OR_RETURN(auto dunData, LoadFileInMemWithStatus("levels\\l4data\\diab1.dun")); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), DiabloQuad1.megaToWorld())); } { ASSIGN_OR_RETURN(auto dunData, LoadFileInMemWithStatus("levels\\l4data\\diab2a.dun")); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), DiabloQuad2.megaToWorld())); } { ASSIGN_OR_RETURN(auto dunData, LoadFileInMemWithStatus("levels\\l4data\\diab3a.dun")); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), DiabloQuad3.megaToWorld())); } { ASSIGN_OR_RETURN(auto dunData, LoadFileInMemWithStatus("levels\\l4data\\diab4a.dun")); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), DiabloQuad4.megaToWorld())); } return {}; } void DeleteMonster(size_t activeIndex) { const unsigned monsterId = ActiveMonsters[activeIndex]; const Monster &monster = Monsters[monsterId]; if ((monster.flags & MFLAG_BERSERK) != 0) { AddUnLight(monster.lightId); } ActiveMonsterCount--; std::swap(ActiveMonsters[activeIndex], ActiveMonsters[ActiveMonsterCount]); // This ensures alive monsters are before ActiveMonsterCount in the array and any deleted monster after for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &activeMonster = Monsters[ActiveMonsters[i]]; if ((activeMonster.flags & MFLAG_TARGETS_MONSTER) != 0 && activeMonster.enemy == monsterId) { activeMonster.flags |= MFLAG_NO_ENEMY; } } } void NewMonsterAnim(Monster &monster, MonsterGraphic graphic, Direction md, AnimationDistributionFlags flags = AnimationDistributionFlags::None, int8_t numSkippedFrames = 0, int8_t distributeFramesBeforeFrame = 0) { const auto &animData = monster.type().getAnimData(graphic); monster.animInfo.setNewAnimation(animData.spritesForDirection(md), animData.frames, animData.rate, flags, numSkippedFrames, distributeFramesBeforeFrame); monster.flags &= ~(MFLAG_LOCK_ANIMATION | MFLAG_ALLOW_SPECIAL); monster.direction = md; } void StartMonsterGotHit(Monster &monster) { if (monster.type().type != MT_GOLEM) { auto animationFlags = gGameLogicStep < GameLogicStep::ProcessMonsters ? AnimationDistributionFlags::ProcessAnimationPending : AnimationDistributionFlags::None; NewMonsterAnim(monster, MonsterGraphic::GotHit, monster.direction, animationFlags); monster.mode = MonsterMode::HitRecovery; } monster.position.tile = monster.position.old; monster.position.future = monster.position.old; M_ClearSquares(monster); monster.occupyTile(monster.position.tile, false); } DVL_ALWAYS_INLINE bool IsRanged(Monster &monster) { return IsAnyOf(monster.ai, MonsterAIID::SkeletonRanged, MonsterAIID::GoatRanged, MonsterAIID::Succubus, MonsterAIID::LazarusSuccubus); } void UpdateEnemy(Monster &monster) { WorldTilePosition target; int menemy = -1; int bestDist = -1; bool bestsameroom = false; const WorldTilePosition position = monster.position.tile; const bool isPlayerMinion = monster.isPlayerMinion(); if (!isPlayerMinion) { for (size_t pnum = 0; pnum < Players.size(); pnum++) { const Player &player = Players[pnum]; if (!player.plractive || !player.isOnActiveLevel() || player._pLvlChanging || (player.hasNoLife() && gbIsMultiplayer)) continue; const bool sameroom = (dTransVal[position.x][position.y] == dTransVal[player.position.tile.x][player.position.tile.y]); const int dist = position.WalkingDistance(player.position.tile); if ((sameroom && !bestsameroom) || ((sameroom || !bestsameroom) && dist < bestDist) || (menemy == -1)) { monster.flags &= ~MFLAG_TARGETS_MONSTER; menemy = static_cast(pnum); target = player.position.future; bestDist = dist; bestsameroom = sameroom; } } } for (size_t i = 0; i < ActiveMonsterCount; i++) { const unsigned monsterId = ActiveMonsters[i]; Monster &otherMonster = Monsters[monsterId]; if (&otherMonster == &monster) continue; if ((otherMonster.hitPoints >> 6) <= 0) continue; if (otherMonster.position.tile == GolemHoldingCell) continue; if (otherMonster.talkMsg != TEXT_NONE && M_Talker(otherMonster)) continue; if (isPlayerMinion && otherMonster.isPlayerMinion()) // prevent golems from fighting each other continue; const int dist = otherMonster.position.tile.WalkingDistance(position); if (((monster.flags & MFLAG_GOLEM) == 0 && (monster.flags & MFLAG_BERSERK) == 0 && dist >= 2 && !IsRanged(monster)) || ((monster.flags & MFLAG_GOLEM) == 0 && (monster.flags & MFLAG_BERSERK) == 0 && (otherMonster.flags & MFLAG_GOLEM) == 0)) { continue; } const 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.flags |= MFLAG_TARGETS_MONSTER; menemy = static_cast(monsterId); target = otherMonster.position.future; bestDist = dist; bestsameroom = sameroom; } } if (menemy != -1) { monster.flags &= ~MFLAG_NO_ENEMY; monster.enemy = menemy; monster.enemyPosition = target; } else { monster.flags |= MFLAG_NO_ENEMY; } } /** * @brief Make the AI wait a bit before thinking again * @param monster The monster that will wait * @param len */ void AiDelay(Monster &monster, int len) { if (len <= 0) { return; } if (monster.ai == MonsterAIID::Lazarus) { return; } monster.var2 = len; monster.mode = 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.mode = MonsterMode::SpecialStand; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; } void WalkInDirection(Monster &monster, Direction endDir) { Point dir = { 0, 0 }; dir += endDir; const auto fx = static_cast(monster.position.tile.x + dir.x); const auto fy = static_cast(monster.position.tile.y + dir.y); MonsterMode mode; switch (endDir) { case Direction::NorthWest: case Direction::North: case Direction::NorthEast: mode = MonsterMode::MoveNorthwards; break; case Direction::West: case Direction::East: mode = MonsterMode::MoveSideways; break; case Direction::SouthWest: case Direction::South: case Direction::SouthEast: mode = MonsterMode::MoveSouthwards; break; case Direction::NoDirection: DVL_UNREACHABLE(); break; } monster.mode = mode; monster.position.old = monster.position.tile; monster.position.future = { fx, fy }; monster.occupyTile(monster.position.future, true); monster.var1 = dir.x; monster.var2 = dir.y; monster.var3 = static_cast(endDir); NewMonsterAnim(monster, MonsterGraphic::Walk, endDir, AnimationDistributionFlags::ProcessAnimationPending, -1); } void StartAttack(Monster &monster) { const Direction md = GetMonsterDirection(monster); NewMonsterAnim(monster, MonsterGraphic::Attack, md, AnimationDistributionFlags::ProcessAnimationPending); monster.mode = MonsterMode::MeleeAttack; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; } void StartRangedAttack(Monster &monster, MissileID missileType, int dam) { const Direction md = GetMonsterDirection(monster); NewMonsterAnim(monster, MonsterGraphic::Attack, md, AnimationDistributionFlags::ProcessAnimationPending); monster.mode = MonsterMode::RangedAttack; monster.var1 = static_cast(missileType); monster.var2 = dam; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; } void StartRangedSpecialAttack(Monster &monster, MissileID missileType, int dam) { const Direction md = GetMonsterDirection(monster); int8_t distributeFramesBeforeFrame = 0; if (monster.ai == MonsterAIID::Mega) distributeFramesBeforeFrame = monster.data().animFrameNumSpecial; NewMonsterAnim(monster, MonsterGraphic::Special, md, AnimationDistributionFlags::ProcessAnimationPending, 0, distributeFramesBeforeFrame); monster.mode = MonsterMode::SpecialRangedAttack; monster.var1 = static_cast(missileType); monster.var2 = 0; monster.var3 = dam; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; } void StartSpecialAttack(Monster &monster) { const Direction md = GetMonsterDirection(monster); NewMonsterAnim(monster, MonsterGraphic::Special, md); monster.mode = MonsterMode::SpecialMeleeAttack; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; } void StartEating(Monster &monster) { NewMonsterAnim(monster, MonsterGraphic::Special, monster.direction); monster.mode = MonsterMode::SpecialMeleeAttack; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; } void DiabloDeath(Monster &diablo, bool sendmsg) { PlaySFX(SfxID::DiabloDeath); auto &quest = Quests[Q_DIABLO]; quest._qactive = QUEST_DONE; if (sendmsg) NetSendCmdQuest(true, quest); sgbSaveSoundOn = gbSoundOn; gbProcessPlayers = false; for (size_t i = 0; i < ActiveMonsterCount; i++) { const int monsterId = ActiveMonsters[i]; Monster &monster = Monsters[monsterId]; if (monster.type().type == MT_DIABLO || diablo.activeForTicks == 0) continue; NewMonsterAnim(monster, MonsterGraphic::Death, monster.direction); monster.mode = MonsterMode::Death; monster.var1 = 0; monster.position.tile = monster.position.old; monster.position.future = monster.position.tile; M_ClearSquares(monster); monster.occupyTile(monster.position.tile, false); } AddLight(diablo.position.tile, 8); DoVision(diablo.position.tile, 8, MAP_EXP_NONE, true); int dist = diablo.position.tile.WalkingDistance(ViewPosition); dist = std::min(dist, 20); diablo.var3 = ViewPosition.x << 16; diablo.position.temp.x = ViewPosition.y << 16; diablo.position.temp.y = (int)((diablo.var3 - (diablo.position.tile.x << 16)) / (float)dist); if (!gbIsMultiplayer) { Player &myPlayer = *MyPlayer; myPlayer.pDiabloKillLevel = std::max(myPlayer.pDiabloKillLevel, static_cast(sgGameInitInfo.nDifficulty + 1)); } } void SpawnLoot(Monster &monster, bool sendmsg) { if (monster.type().type == MT_HORKSPWN) { return; } if (Quests[Q_GARBUD].IsAvailable() && monster.uniqueType == UniqueMonsterType::Garbud) { CreateTypeItem(monster.position.tile + Displacement { 1, 1 }, true, ItemType::Mace, IMISC_NONE, sendmsg, false); } else if (monster.uniqueType == UniqueMonsterType::Defiler) { if (effect_is_playing(SfxID::Defiler8)) stream_stop(); SpawnMapOfDoom(monster.position.tile, sendmsg); Quests[Q_DEFILER]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_DEFILER]); } else if (monster.uniqueType == UniqueMonsterType::HorkDemon) { if (sgGameInitInfo.bTheoQuest != 0) { SpawnTheodore(monster.position.tile, sendmsg); } else { CreateAmulet(monster.position.tile, 13, sendmsg, false); } } else if (monster.type().type == MT_NAKRUL) { SfxID nSFX = IsUberRoomOpened ? SfxID::NaKrul4 : SfxID::NaKrul5; if (sgGameInitInfo.bCowQuest != 0) nSFX = SfxID::NaKrul6; if (effect_is_playing(nSFX)) stream_stop(); 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, SpellID::Apocalypse, sendmsg, false); } else if (!monster.isPlayerMinion()) { SpawnItem(monster, monster.position.tile, sendmsg); } } std::optional GetTeleportTile(const Monster &monster) { const int mx = monster.enemyPosition.x; const int my = monster.enemyPosition.y; const int rx = PickRandomlyAmong({ -1, 1 }); const int ry = PickRandomlyAmong({ -1, 1 }); for (int j = -1; j <= 1; j++) { for (int k = -1; k < 1; k++) { if (j == 0 && k == 0) continue; const int x = mx + (rx * j); const int y = my + (ry * k); if (!InDungeonBounds({ x, y }) || x == monster.position.tile.x || y == monster.position.tile.y) continue; if (IsTileAvailable(monster, { x, y })) return Point { x, y }; } } return {}; } void Teleport(Monster &monster) { if (monster.mode == MonsterMode::Petrified) return; std::optional position = GetTeleportTile(monster); if (!position) return; M_ClearSquares(monster); dMonster[monster.position.tile.x][monster.position.tile.y] = 0; monster.occupyTile(*position, false); monster.position.old = *position; monster.direction = GetMonsterDirection(monster); ChangeLightXY(monster.lightId, *position); } bool IsHardHit(Monster &target, unsigned dam) { switch (target.type().type) { case MT_SNEAK: case MT_STALKER: case MT_UNSEEN: case MT_ILLWEAV: return true; default: return (dam >> 6) >= target.level(sgGameInitInfo.nDifficulty) + 3; } } void MonsterHitMonster(Monster &attacker, Monster &target, int dam) { if (IsHardHit(target, dam)) { target.direction = Opposite(attacker.direction); } M_StartHit(target, dam); } void StartDeathFromMonster(Monster &attacker, Monster &target) { const Direction md = GetDirection(target.position.tile, attacker.position.tile); MonsterDeath(target, md, true); if (gbIsHellfire) M_StartStand(attacker, attacker.direction); } void StartFadein(Monster &monster, Direction md, bool backwards) { NewMonsterAnim(monster, MonsterGraphic::Special, md); monster.mode = MonsterMode::FadeIn; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; monster.flags &= ~MFLAG_HIDDEN; if (backwards) { monster.flags |= MFLAG_LOCK_ANIMATION; monster.animInfo.currentFrame = monster.animInfo.numberOfFrames - 1; } } void StartFadeout(Monster &monster, Direction md, bool backwards) { NewMonsterAnim(monster, MonsterGraphic::Special, md); monster.mode = MonsterMode::FadeOut; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; if (backwards) { monster.flags |= MFLAG_LOCK_ANIMATION; monster.animInfo.currentFrame = monster.animInfo.numberOfFrames - 1; } } /** * @brief Starts the monster healing procedure. * * The monster will be healed between 1.47% and 25% of its max HP. The healing amount is stored in _mVar1. * * This is only used by Gargoyles. * * @param monster The monster that will be healed. */ void StartHeal(Monster &monster) { monster.changeAnimationData(MonsterGraphic::Special); monster.animInfo.currentFrame = monster.type().getAnimData(MonsterGraphic::Special).frames - 1; monster.flags |= MFLAG_LOCK_ANIMATION; monster.mode = MonsterMode::Heal; monster.var1 = monster.maxHitPoints / (16 * (GenerateRnd(5) + 4)); } void SyncLightPosition(Monster &monster) { if (monster.lightId == NO_LIGHT) return; const WorldTileDisplacement offset = monster.isWalking() ? monster.position.CalculateWalkingOffset(monster.direction, monster.animInfo) : WorldTileDisplacement {}; ChangeLightOffset(monster.lightId, offset.screenToLight()); } void MonsterIdle(Monster &monster) { if (monster.type().type == MT_GOLEM) monster.changeAnimationData(MonsterGraphic::Walk); else monster.changeAnimationData(MonsterGraphic::Stand); if (monster.animInfo.isLastFrame()) UpdateEnemy(monster); if (monster.var2 < std::numeric_limits::max()) monster.var2++; } /** * @brief Continue movement towards new tile */ bool MonsterWalk(Monster &monster) { // Check if we reached new tile const bool isAnimationEnd = monster.animInfo.isLastFrame(); if (isAnimationEnd) { dMonster[monster.position.tile.x][monster.position.tile.y] = 0; monster.position.tile.x += monster.var1; monster.position.tile.y += monster.var2; // dMonster is set here for backwards compatibility; without it, the monster would be invisible if loaded from a vanilla save. monster.occupyTile(monster.position.tile, false); ChangeLightXY(monster.lightId, monster.position.tile); M_StartStand(monster, monster.direction); } 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.type().type == MT_FLESTHNG) PlayEffect(monster, MonsterSound::Special); } } SyncLightPosition(monster); return isAnimationEnd; } void MonsterAttackMonster(Monster &attacker, Monster &target, int hper, int mind, int maxd) { if (!target.isPossibleToHit()) return; int hit = GenerateRnd(100); if (target.mode == MonsterMode::Petrified) hit = 0; if (target.tryLiftGargoyle()) return; if (hit >= hper) return; const int dam = RandomIntBetween(mind, maxd) << 6; ApplyMonsterDamage(DamageType::Physical, target, dam); if (attacker.isPlayerMinion()) { const auto playerId = static_cast(attacker.goalVar3); const Player &player = Players[playerId]; target.tag(player); } if (target.hitPoints >> 6 <= 0) { StartDeathFromMonster(attacker, target); } else { MonsterHitMonster(attacker, target, dam); } if (target.activeForTicks == 0) { target.activeForTicks = UINT8_MAX; target.position.last = attacker.position.tile; } } int CheckReflect(Monster &monster, Player &player, int dam) { player.wReflections--; if (player.wReflections <= 0) NetSendCmdParam1(true, CMD_SETREFLECT, 0); // reflects 20-30% damage const int mdam = dam * RandomIntBetween(20, 30, true) / 100; ApplyMonsterDamage(DamageType::Physical, monster, mdam); if (monster.hitPoints >> 6 <= 0) M_StartKill(monster, player); else M_StartHit(monster, player, mdam); return mdam; } int GetMinHit() { switch (currlevel) { case 16: return 30; case 15: return 25; case 14: return 20; default: return 15; } } void MonsterAttackPlayer(Monster &monster, Player &player, int hit, int minDam, int maxDam) { if (player.hasNoLife() || 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.data().monsterClass == MonsterClass::Demon) ac += 40; if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::ACAgainstUndead) && monster.data().monsterClass == MonsterClass::Undead) ac += 20; hit += 2 * (monster.level(sgGameInitInfo.nDifficulty) - player.getCharacterLevel()) + 30 - ac; const int minhit = GetMinHit(); 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.level(sgGameInitInfo.nDifficulty) * 2); blk = std::clamp(blk, 0, 100); if (hper >= hit) return; if (blkper < blk) { const Direction dir = GetDirection(player.position.tile, monster.position.tile); StartPlrBlock(player, dir); if (&player == MyPlayer && player.wReflections > 0) { int dam = GenerateRnd(((maxDam - minDam) << 6) + 1) + (minDam << 6); dam = std::max(dam + (player._pIGetHit << 6), 64); CheckReflect(monster, player, dam); } return; } if (monster.type().type == MT_YZOMBIE && &player == MyPlayer) { if (player._pMaxHP > 64) { if (player._pMaxHPBase > 64) { player._pMaxHP -= 64; player._pHitPoints = std::min(player._pHitPoints, player._pMaxHP); player._pMaxHPBase -= 64; player._pHPBase = std::min(player._pHPBase, player._pMaxHPBase); } } } // New method fixes a bug which caused the maximum possible damage value to be 63/64ths too low. int dam = RandomIntBetween(minDam << 6, maxDam << 6); dam = std::max(dam + (player._pIGetHit << 6), 64); if (&player == MyPlayer) { if (player.wReflections > 0) { const int reflectedDamage = CheckReflect(monster, player, dam); dam = std::max(dam - reflectedDamage, 0); } ApplyPlrDamage(DamageType::Physical, player, 0, 0, dam); } // Reflect can also kill a monster, so make sure the monster is still alive if (HasAnyOf(player._pIFlags, ItemSpecialEffect::Thorns) && monster.mode != MonsterMode::Death) { const int mdam = (GenerateRnd(3) + 1) << 6; ApplyMonsterDamage(DamageType::Physical, monster, mdam); if (monster.hitPoints >> 6 <= 0) M_StartKill(monster, player); else M_StartHit(monster, player, mdam); } if ((monster.flags & MFLAG_NOLIFESTEAL) == 0 && monster.type().type == MT_SKING && gbIsMultiplayer) monster.hitPoints += dam; if (player.hasNoLife()) { if (gbIsHellfire) M_StartStand(monster, monster.direction); return; } StartPlrHit(player, dam, false); if ((monster.flags & MFLAG_KNOCKBACK) != 0) { if (player._pmode != PM_GOTHIT) StartPlrHit(player, 0, true); const Point newPosition = player.position.tile + monster.direction; if (PosOkPlayer(player, newPosition)) { player.position.tile = newPosition; FixPlayerLocation(player, player._pdir); FixPlrWalkTags(player); player.occupyTile(newPosition, false); SetPlayerOld(player); } } } void MonsterAttackEnemy(Monster &monster, int hit, int minDam, int maxDam) { if ((monster.flags & MFLAG_NO_ENEMY) == 0) { if ((monster.flags & MFLAG_TARGETS_MONSTER) != 0) MonsterAttackMonster(monster, Monsters[monster.enemy], hit, minDam, maxDam); else MonsterAttackPlayer(monster, Players[monster.enemy], hit, minDam, maxDam); } } bool MonsterAttack(Monster &monster) { if (monster.animInfo.currentFrame == monster.data().animFrameNum - 1) { MonsterAttackEnemy(monster, monster.toHit(sgGameInitInfo.nDifficulty), monster.minDamage, monster.maxDamage); if (monster.ai != MonsterAIID::Snake) PlayEffect(monster, MonsterSound::Attack); } if (IsAnyOf(monster.type().type, MT_NMAGMA, MT_YMAGMA, MT_BMAGMA, MT_WMAGMA) && monster.animInfo.currentFrame == 8) { MonsterAttackEnemy(monster, monster.toHit(sgGameInitInfo.nDifficulty) + 10, monster.minDamage - 2, monster.maxDamage - 2); PlayEffect(monster, MonsterSound::Attack); } if (IsAnyOf(monster.type().type, MT_STORM, MT_RSTORM, MT_STORML, MT_MAEL) && monster.animInfo.currentFrame == 12) { MonsterAttackEnemy(monster, monster.toHit(sgGameInitInfo.nDifficulty) - 20, monster.minDamage + 4, monster.maxDamage + 4); PlayEffect(monster, MonsterSound::Attack); } if (monster.ai == MonsterAIID::Snake && monster.animInfo.currentFrame == 0) PlayEffect(monster, MonsterSound::Attack); if (monster.animInfo.isLastFrame()) { M_StartStand(monster, monster.direction); return true; } return false; } bool MonsterRangedAttack(Monster &monster) { if (monster.animInfo.currentFrame == monster.data().animFrameNum - 1) { const auto &missileType = static_cast(monster.var1); if (missileType != MissileID::Null) { int multimissiles = 1; if (missileType == MissileID::ChargedBolt) multimissiles = 3; for (int mi = 0; mi < multimissiles; mi++) { AddMissile( monster.position.tile, monster.enemyPosition, monster.direction, missileType, TARGET_PLAYERS, monster, monster.var2, 0); } } PlayEffect(monster, MonsterSound::Attack); } if (monster.animInfo.isLastFrame()) { M_StartStand(monster, monster.direction); return true; } return false; } bool MonsterRangedSpecialAttack(Monster &monster) { if (monster.animInfo.currentFrame == monster.data().animFrameNumSpecial - 1 && monster.animInfo.tickCounterOfCurrentFrame == 0 && (monster.ai != MonsterAIID::Mega || monster.var2 == 0)) { if (AddMissile( monster.position.tile, monster.enemyPosition, monster.direction, static_cast(monster.var1), TARGET_PLAYERS, monster, monster.var3, 0) != nullptr) { PlayEffect(monster, MonsterSound::Special); } } if (monster.ai == MonsterAIID::Mega && monster.animInfo.currentFrame == monster.data().animFrameNumSpecial - 1) { if (monster.var2++ == 0) { monster.flags |= MFLAG_ALLOW_SPECIAL; } else if (monster.var2 == 15) { monster.flags &= ~MFLAG_ALLOW_SPECIAL; } } if (monster.animInfo.isLastFrame()) { M_StartStand(monster, monster.direction); return true; } return false; } bool MonsterSpecialAttack(Monster &monster) { if (monster.animInfo.currentFrame == monster.data().animFrameNumSpecial - 1) { MonsterAttackEnemy(monster, monster.toHitSpecial(sgGameInitInfo.nDifficulty), monster.minDamageSpecial, monster.maxDamageSpecial); } if (monster.animInfo.isLastFrame()) { M_StartStand(monster, monster.direction); return true; } return false; } bool MonsterFadein(Monster &monster) { if (((monster.flags & MFLAG_LOCK_ANIMATION) == 0 || monster.animInfo.currentFrame != 0) && ((monster.flags & MFLAG_LOCK_ANIMATION) != 0 || monster.animInfo.currentFrame != monster.animInfo.numberOfFrames - 1)) { return false; } M_StartStand(monster, monster.direction); monster.flags &= ~MFLAG_LOCK_ANIMATION; return true; } bool MonsterFadeout(Monster &monster) { if (((monster.flags & MFLAG_LOCK_ANIMATION) == 0 || monster.animInfo.currentFrame != 0) && ((monster.flags & MFLAG_LOCK_ANIMATION) != 0 || monster.animInfo.currentFrame != monster.animInfo.numberOfFrames - 1)) { return false; } monster.flags &= ~MFLAG_LOCK_ANIMATION; monster.flags |= MFLAG_HIDDEN; M_StartStand(monster, monster.direction); return true; } /** * @brief Applies the healing effect on the monster. * * This is triggered by StartHeal() * * @param monster The monster that will be healed. * @return */ void MonsterHeal(Monster &monster) { if (monster.animInfo.currentFrame == 0) { monster.flags &= ~MFLAG_LOCK_ANIMATION; monster.flags |= MFLAG_ALLOW_SPECIAL; if (monster.var1 + monster.hitPoints < monster.maxHitPoints) { monster.hitPoints = monster.var1 + monster.hitPoints; } else { monster.hitPoints = monster.maxHitPoints; monster.flags &= ~MFLAG_ALLOW_SPECIAL; monster.mode = MonsterMode::SpecialMeleeAttack; } } } void MonsterTalk(Monster &monster) { M_StartStand(monster, monster.direction); monster.goal = MonsterGoal::Talking; if (effect_is_playing(Speeches[monster.talkMsg].sfxnr)) return; InitQTextMsg(monster.talkMsg); if (monster.uniqueType == UniqueMonsterType::SnotSpill) { if (monster.talkMsg == TEXT_BANNER10 && (monster.flags & MFLAG_QUEST_COMPLETE) == 0) { ObjChangeMap(SetPiece.position.x, SetPiece.position.y, SetPiece.position.x + (SetPiece.size.width / 2) + 2, SetPiece.position.y + (SetPiece.size.height / 2) - 2); auto tren = TransVal; TransVal = 9; DRLG_MRectTrans({ SetPiece.position, WorldTileSize((SetPiece.size.width / 2) + 4, SetPiece.size.height / 2) }); TransVal = tren; Quests[Q_LTBANNER]._qvar1 = 2; if (Quests[Q_LTBANNER]._qactive == QUEST_INIT) Quests[Q_LTBANNER]._qactive = QUEST_ACTIVE; monster.flags |= MFLAG_QUEST_COMPLETE; NetSendCmdQuest(true, Quests[Q_LTBANNER]); } if (Quests[Q_LTBANNER]._qvar1 < 2) { app_fatal(StrCat("SS Talk = ", monster.talkMsg, ", Flags = ", monster.flags)); } } if (monster.uniqueType == UniqueMonsterType::Lachdan) { if (monster.talkMsg == TEXT_VEIL9) { Quests[Q_VEIL]._qactive = QUEST_ACTIVE; Quests[Q_VEIL]._qlog = true; NetSendCmdQuest(true, Quests[Q_VEIL]); } } if (monster.uniqueType == UniqueMonsterType::WarlordOfBlood) { Quests[Q_WARLORD]._qvar1 = QS_WARLORD_TALKING; NetSendCmdQuest(true, Quests[Q_WARLORD]); } if (monster.uniqueType == UniqueMonsterType::Lazarus && UseMultiplayerQuests()) { Quests[Q_BETRAYER]._qvar1 = 6; monster.goal = MonsterGoal::Normal; monster.activeForTicks = UINT8_MAX; monster.talkMsg = TEXT_NONE; } } bool MonsterGotHit(Monster &monster) { if (monster.animInfo.isLastFrame()) { M_StartStand(monster, monster.direction); return true; } return false; } void ReleaseMinions(const Monster &leader) { for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &minion = Monsters[ActiveMonsters[i]]; if (minion.leaderRelation == LeaderRelation::Leashed && minion.getLeader() == &leader) { minion.setLeader(nullptr); } } } void ShrinkLeaderPacksize(const Monster &monster) { if (monster.leaderRelation == LeaderRelation::Leashed) { monster.getLeader()->packSize--; } } void MonsterDeath(Monster &monster) { monster.var1++; if (monster.type().type == 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.var1 == 140) PrepDoEnding(); } else if (monster.animInfo.isLastFrame()) { if (monster.isUnique()) AddCorpse(monster.position.tile, monster.corpseId, monster.direction); else AddCorpse(monster.position.tile, monster.type().corpseId, monster.direction); dMonster[monster.position.tile.x][monster.position.tile.y] = 0; monster.isInvalid = true; M_UpdateRelations(monster); } } bool MonsterSpecialStand(Monster &monster) { if (monster.animInfo.currentFrame == monster.data().animFrameNumSpecial - 1) PlayEffect(monster, MonsterSound::Special); if (monster.animInfo.isLastFrame()) { M_StartStand(monster, monster.direction); return true; } return false; } bool MonsterDelay(Monster &monster) { monster.changeAnimationData(MonsterGraphic::Stand, GetMonsterDirection(monster)); if (monster.ai == MonsterAIID::Lazarus) { if (monster.var2 > 8 || monster.var2 < 0) monster.var2 = 8; } if (monster.var2-- == 0) { const int oFrame = monster.animInfo.currentFrame; M_StartStand(monster, monster.direction); monster.animInfo.currentFrame = oFrame; return true; } return false; } void MonsterPetrified(Monster &monster) { if (monster.hitPoints <= 0) { dMonster[monster.position.tile.x][monster.position.tile.y] = 0; monster.isInvalid = true; } } std::optional GetRandomSkeletonTypeIndex() { int32_t typeCount = 0; size_t skeletonIndexes[SkeletonTypes.size()]; for (size_t i = 0; i < LevelMonsterTypeCount; i++) { if (IsSkel(LevelMonsterTypes[i].type)) { skeletonIndexes[typeCount++] = i; } } if (typeCount == 0) { return {}; } const size_t typeIndex = skeletonIndexes[GenerateRnd(typeCount)]; return typeIndex; } Monster *AddSkeleton(Point position, Direction dir, bool inMap) { auto typeIndex = GetRandomSkeletonTypeIndex(); if (!typeIndex) return nullptr; return AddMonster(position, dir, *typeIndex, inMap); } bool IsLineNotSolid(Point startPoint, Point endPoint) { return LineClear(IsTileNotSolid, startPoint, endPoint); } void FollowTheLeader(Monster &monster) { if (monster.leaderRelation != LeaderRelation::Leashed) return; Monster *leader = monster.getLeader(); if (leader == nullptr) return; if (leader->activeForTicks > monster.activeForTicks) { monster.position.last = leader->position.tile; monster.activeForTicks = leader->activeForTicks - 1; } if (monster.ai != MonsterAIID::Gargoyle || (monster.flags & MFLAG_ALLOW_SPECIAL) == 0) return; if (leader->mode == MonsterMode::SpecialMeleeAttack) return; monster.flags &= ~MFLAG_ALLOW_SPECIAL; monster.mode = MonsterMode::SpecialMeleeAttack; } void GroupUnity(Monster &monster) { if (monster.leaderRelation == LeaderRelation::None) return; // No unique monster would be a minion of someone else! assert(!monster.isUnique()); // Someone with a leaderRelation should have a leader, if we end up trying to access a nullptr then the relation was already broken... auto &leader = *monster.getLeader(); 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.activeForTicks > leader.activeForTicks) { leader.position.last = monster.position.tile; leader.activeForTicks = monster.activeForTicks - 1; } if (leader.ai == MonsterAIID::Gargoyle && (leader.flags & MFLAG_ALLOW_SPECIAL) != 0) { leader.flags &= ~MFLAG_ALLOW_SPECIAL; leader.mode = MonsterMode::SpecialMeleeAttack; } } } bool RandomWalk(Monster &monster, Direction md) { const Direction mdtemp = md; bool ok = DirOK(monster, md); if (FlipCoin()) ok = ok || (md = Right(mdtemp), DirOK(monster, md)) || (md = Left(mdtemp), DirOK(monster, md)); else ok = ok || (md = Left(mdtemp), DirOK(monster, md)) || (md = Right(mdtemp), DirOK(monster, md)); if (FlipCoin()) { ok = ok || (md = Left(Left(mdtemp)), DirOK(monster, md)) || (md = Right(Right(mdtemp)), DirOK(monster, md)); } else { ok = ok || (md = Right(Right(mdtemp)), DirOK(monster, md)) || (md = Left(Left(mdtemp)), DirOK(monster, md)); } if (ok) Walk(monster, md); return ok; } bool RandomWalk2(Monster &monster, Direction md) { Direction mdtemp = md; bool ok = DirOK(monster, md); // Can we continue in the same direction // Randomly go left or right if (FlipCoin()) { ok = ok || (mdtemp = Right(md), DirOK(monster, Right(md))) || (mdtemp = Left(md), DirOK(monster, Left(md))); } else { ok = ok || (mdtemp = Left(md), DirOK(monster, Left(md))) || (mdtemp = Right(md), DirOK(monster, Right(md))); } if (ok) Walk(monster, mdtemp); return ok; } /** * @brief Check if a tile is affected by a spell we are vulnerable to */ bool IsTileSafe(const Monster &monster, Point position) { if (!InDungeonBounds(position)) return false; const bool fearsFire = (monster.resistance & IMMUNE_FIRE) == 0 || monster.type().type == MT_DIABLO; const bool fearsLightning = (monster.resistance & IMMUNE_LIGHTNING) == 0 || monster.type().type == MT_DIABLO; return !(fearsFire && HasAnyOf(dFlags[position.x][position.y], DungeonFlag::MissileFireWall)) && !(fearsLightning && HasAnyOf(dFlags[position.x][position.y], DungeonFlag::MissileLightningWall)); } /** * @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.flags & MFLAG_CAN_OPEN_DOOR) != 0)) return false; return IsTileSafe(monster, position); } bool AiPlanWalk(Monster &monster) { int8_t path[MaxPathLengthMonsters]; /** 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 }; if (FindPath(CanStep, [&monster](Point position) { return IsTileAccessible(monster, position); }, monster.position.tile, monster.enemyPosition, path, MaxPathLengthMonsters) == 0) { return false; } RandomWalk(monster, plr2monst[path[0]]); return true; } Direction Turn(Direction direction, bool turnLeft) { return turnLeft ? Left(direction) : Right(direction); } bool RoundWalk(Monster &monster, Direction direction, int8_t *dir) { const Direction turn45deg = Turn(direction, *dir != 0); const Direction turn90deg = Turn(turn45deg, *dir != 0); // Turn 90 degrees if (Walk(monster, turn90deg)) { return true; } // Only do a small turn if (Walk(monster, turn45deg)) { return true; } // Continue straight if (Walk(monster, direction)) { return true; } // Try 90 degrees in the opposite than desired direction *dir = (*dir == 0) ? 1 : 0; return RandomWalk(monster, Opposite(turn90deg)); } bool AiPlanPath(Monster &monster) { if (monster.type().type != MT_GOLEM) { if (monster.activeForTicks == 0) return false; if (monster.mode != MonsterMode::Stand) return false; if (IsNoneOf(monster.goal, MonsterGoal::Normal, MonsterGoal::Move, MonsterGoal::Attack)) return false; if (monster.position.tile == GolemHoldingCell) return false; } const bool clear = LineClear( [&monster](Point position) { return (IsTileWalkable(position) && IsTileSafe(monster, position)); }, monster.position.tile, monster.enemyPosition); if (!clear || (monster.pathCount >= 5 && monster.pathCount < 8)) { if ((monster.flags & MFLAG_CAN_OPEN_DOOR) != 0) MonstCheckDoors(monster); monster.pathCount++; if (monster.pathCount < 5) return false; if (AiPlanWalk(monster)) return true; } if (monster.type().type != MT_GOLEM) monster.pathCount = 0; return false; } void AiAvoidance(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); const int v = GenerateRnd(100); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy >= 2 && monster.activeForTicks == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { if (monster.goal == MonsterGoal::Move || (distanceToEnemy >= 4 && FlipCoin(4))) { if (monster.goal != MonsterGoal::Move) { monster.goalVar1 = 0; monster.goalVar2 = GenerateRnd(2); } monster.goal = MonsterGoal::Move; if ((monster.goalVar1++ >= static_cast(2 * distanceToEnemy) && DirOK(monster, md)) || dTransVal[monster.position.tile.x][monster.position.tile.y] != dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { monster.goal = MonsterGoal::Normal; } else if (!RoundWalk(monster, md, &monster.goalVar2)) { AiDelay(monster, GenerateRnd(10) + 10); } } } else { monster.goal = MonsterGoal::Normal; } if (monster.goal == MonsterGoal::Normal) { if (distanceToEnemy >= 2) { if ((monster.var2 > 20 && v < 2 * monster.intelligence + 28) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 2 * monster.intelligence + 78)) { RandomWalk(monster, md); } } else if (v < 2 * monster.intelligence + 23) { monster.direction = md; if (IsAnyOf(monster.ai, MonsterAIID::GoatMelee, MonsterAIID::Gharbad) && monster.hitPoints < (monster.maxHitPoints / 2) && !FlipCoin()) StartSpecialAttack(monster); else StartAttack(monster); } } monster.checkStandAnimationIsLoaded(md); } MissileID GetMissileType(MonsterAIID ai) { switch (ai) { case MonsterAIID::GoatRanged: return MissileID::Arrow; case MonsterAIID::Succubus: case MonsterAIID::LazarusSuccubus: return MissileID::BloodStar; case MonsterAIID::Acid: case MonsterAIID::AcidUnique: return MissileID::Acid; case MonsterAIID::FireBat: return MissileID::Firebolt; case MonsterAIID::Torchant: return MissileID::Fireball; case MonsterAIID::Lich: return MissileID::OrangeFlare; case MonsterAIID::ArchLich: return MissileID::YellowFlare; case MonsterAIID::Psychorb: return MissileID::BlueFlare; case MonsterAIID::Necromorb: return MissileID::RedFlare; case MonsterAIID::Magma: return MissileID::MagmaBall; case MonsterAIID::Storm: return MissileID::ThinLightningControl; case MonsterAIID::Diablo: return MissileID::DiabloApocalypse; case MonsterAIID::BoneDemon: return MissileID::BlueFlare2; default: return MissileID::Arrow; } } void AiRanged(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } if (monster.activeForTicks == UINT8_MAX || (monster.flags & MFLAG_TARGETS_MONSTER) != 0) { const Direction md = GetMonsterDirection(monster); if (monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); monster.direction = md; if (static_cast(monster.var1) == MonsterMode::RangedAttack) { AiDelay(monster, GenerateRnd(20)); } else if (monster.distanceToEnemy() < 4) { if (GenerateRnd(100) < 10 * (monster.intelligence + 7)) RandomWalk(monster, Opposite(md)); } if (monster.mode == MonsterMode::Stand) { if (LineClearMissile(monster.position.tile, monster.enemyPosition)) { const MissileID missileType = GetMissileType(monster.ai); if (monster.ai == MonsterAIID::AcidUnique) StartRangedSpecialAttack(monster, missileType, 0); else StartRangedAttack(monster, missileType, 0); } else { monster.checkStandAnimationIsLoaded(md); } } return; } if (monster.activeForTicks != 0) { const Direction md = GetDirection(monster.position.tile, monster.position.last); RandomWalk(monster, md); } } void AiRangedAvoidance(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (IsAnyOf(monster.ai, MonsterAIID::Magma, MonsterAIID::Storm, MonsterAIID::BoneDemon) && monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); const int lessmissiles = (monster.ai == MonsterAIID::Acid) ? 1 : 0; const int dam = (monster.ai == MonsterAIID::Diablo) ? 40 : 0; const MissileID missileType = GetMissileType(monster.ai); int v = GenerateRnd(10000); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy >= 2 && monster.activeForTicks == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { if (monster.goal == MonsterGoal::Move || (distanceToEnemy >= 3 && FlipCoin(4 << lessmissiles))) { if (monster.goal != MonsterGoal::Move) { monster.goalVar1 = 0; monster.goalVar2 = GenerateRnd(2); } monster.goal = MonsterGoal::Move; if (monster.goalVar1++ >= static_cast(2 * distanceToEnemy) && DirOK(monster, md)) { monster.goal = MonsterGoal::Normal; } else if (v < (500 * (monster.intelligence + 1) >> lessmissiles) && (LineClearMissile(monster.position.tile, monster.enemyPosition))) { StartRangedSpecialAttack(monster, missileType, dam); } else { RoundWalk(monster, md, &monster.goalVar2); } } } else { monster.goal = MonsterGoal::Normal; } if (monster.goal == MonsterGoal::Normal) { if (((distanceToEnemy >= 3 && v < ((500 * (monster.intelligence + 2)) >> lessmissiles)) || v < ((500 * (monster.intelligence + 1)) >> lessmissiles)) && LineClearMissile(monster.position.tile, monster.enemyPosition)) { StartRangedSpecialAttack(monster, missileType, dam); } else if (distanceToEnemy >= 2) { v = GenerateRnd(100); if (v < 1000 * (monster.intelligence + 5) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 1000 * (monster.intelligence + 8))) { RandomWalk(monster, md); } } else if (v < 1000 * (monster.intelligence + 6)) { monster.direction = md; StartAttack(monster); } } if (monster.mode == MonsterMode::Stand) { AiDelay(monster, GenerateRnd(10) + 5); } } void ZombieAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } if (!IsTileVisible(monster.position.tile)) { return; } if (GenerateRnd(100) < 2 * monster.intelligence + 10) { const int dist = monster.enemyPosition.WalkingDistance(monster.position.tile); if (dist >= 2) { if (dist >= 2 * monster.intelligence + 4) { Direction md = monster.direction; if (GenerateRnd(100) < 2 * monster.intelligence + 20) { md = static_cast(GenerateRnd(8)); } Walk(monster, md); } else { RandomWalk(monster, GetMonsterDirection(monster)); } } else { StartAttack(monster); } } monster.checkStandAnimationIsLoaded(monster.direction); } void OverlordAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetMonsterDirection(monster); monster.direction = md; const int v = GenerateRnd(100); if (monster.distanceToEnemy() >= 2) { if ((monster.var2 > 20 && v < 4 * monster.intelligence + 20) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 4 * monster.intelligence + 70)) { RandomWalk(monster, md); } } else if (v < 4 * monster.intelligence + 15) { StartAttack(monster); } else if (v < 4 * monster.intelligence + 20) { StartSpecialAttack(monster); } monster.checkStandAnimationIsLoaded(md); } void SkeletonAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); monster.direction = md; if (monster.distanceToEnemy() >= 2) { if (static_cast(monster.var1) == MonsterMode::Delay || (GenerateRnd(100) >= 35 - 4 * monster.intelligence)) { RandomWalk(monster, md); } else { AiDelay(monster, 15 - (2 * monster.intelligence) + GenerateRnd(10)); } } else { if (static_cast(monster.var1) == MonsterMode::Delay || (GenerateRnd(100) < 2 * monster.intelligence + 20)) { StartAttack(monster); } else { AiDelay(monster, (2 * (5 - monster.intelligence)) + GenerateRnd(10)); } } monster.checkStandAnimationIsLoaded(md); } void SkeletonBowAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetMonsterDirection(monster); monster.direction = md; const int v = GenerateRnd(100); bool walking = false; if (monster.distanceToEnemy() < 4) { if ((monster.var2 > 20 && v < 2 * monster.intelligence + 13) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 2 * monster.intelligence + 63)) { walking = Walk(monster, Opposite(md)); } } if (!walking) { if (GenerateRnd(100) < 2 * monster.intelligence + 3) { if (LineClearMissile(monster.position.tile, monster.enemyPosition)) StartRangedAttack(monster, MissileID::Arrow, 4); } } monster.checkStandAnimationIsLoaded(md); } std::optional ScavengerFindCorpse(const Monster &scavenger) { const bool reverseSearch = FlipCoin(); const int first = reverseSearch ? 4 : -4; const int last = reverseSearch ? -4 : 4; const int increment = reverseSearch ? -1 : 1; for (int y = first; y <= last; y += increment) { for (int x = first; x <= last; x += increment) { Point position = scavenger.position.tile + Displacement { x, y }; if (!InDungeonBounds(position)) continue; if (dCorpse[position.x][position.y] == 0) continue; if (!IsLineNotSolid(scavenger.position.tile, position)) continue; return position; } } return {}; } void ScavengerAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) return; if (monster.hitPoints < (monster.maxHitPoints / 2) && monster.goal != MonsterGoal::Healing) { if (monster.leaderRelation != LeaderRelation::None) { ShrinkLeaderPacksize(monster); monster.leaderRelation = LeaderRelation::None; } monster.goal = MonsterGoal::Healing; monster.goalVar3 = 10; } if (monster.goal == MonsterGoal::Healing && monster.goalVar3 != 0) { monster.goalVar3--; if (dCorpse[monster.position.tile.x][monster.position.tile.y] != 0) { StartEating(monster); if (gbIsHellfire) { const int mMaxHP = monster.maxHitPoints; monster.hitPoints += mMaxHP / 8; monster.hitPoints = std::min(monster.hitPoints, monster.maxHitPoints); if (monster.goalVar3 <= 0 || monster.hitPoints == monster.maxHitPoints) dCorpse[monster.position.tile.x][monster.position.tile.y] = 0; } else { monster.hitPoints += 64; } int targetHealth = monster.maxHitPoints; if (!gbIsHellfire) targetHealth = (monster.maxHitPoints / 2) + (monster.maxHitPoints / 4); if (monster.hitPoints >= targetHealth) { monster.goal = MonsterGoal::Normal; monster.goalVar1 = 0; monster.goalVar2 = 0; } } else { if (monster.goalVar1 == 0) { std::optional position = ScavengerFindCorpse(monster); if (position) { monster.goalVar1 = position->x + 1; monster.goalVar2 = position->y + 1; } } if (monster.goalVar1 != 0) { const int x = monster.goalVar1 - 1; const int y = monster.goalVar2 - 1; monster.direction = GetDirection(monster.position.tile, { x, y }); RandomWalk(monster, monster.direction); } } } if (monster.mode == MonsterMode::Stand) SkeletonAi(monster); } void RhinoAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); int v = GenerateRnd(100); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy >= 2) { if (monster.goal == MonsterGoal::Move || (distanceToEnemy >= 5 && !FlipCoin(4))) { if (monster.goal != MonsterGoal::Move) { monster.goalVar1 = 0; monster.goalVar2 = GenerateRnd(2); } monster.goal = MonsterGoal::Move; if (monster.goalVar1++ >= static_cast(2 * distanceToEnemy) || dTransVal[monster.position.tile.x][monster.position.tile.y] != dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { monster.goal = MonsterGoal::Normal; } else if (!RoundWalk(monster, md, &monster.goalVar2)) { AiDelay(monster, GenerateRnd(10) + 10); } } } else { monster.goal = MonsterGoal::Normal; } if (monster.goal == MonsterGoal::Normal) { if (distanceToEnemy >= 5 && v < 2 * monster.intelligence + 43 && LineClear([&monster](Point position) { return IsTileAvailable(monster, position); }, monster.position.tile, monster.enemyPosition)) { if (AddMissile(monster.position.tile, monster.enemyPosition, md, MissileID::Rhino, TARGET_PLAYERS, monster, 0, 0) != nullptr) { if (monster.data().hasSpecialSound) PlayEffect(monster, MonsterSound::Special); monster.occupyTile(monster.position.tile, true); monster.mode = MonsterMode::Charge; } } else { if (distanceToEnemy >= 2) { v = GenerateRnd(100); if (v >= 2 * monster.intelligence + 33 && (IsNoneOf(static_cast(monster.var1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways) || monster.var2 != 0 || v >= 2 * monster.intelligence + 83)) { AiDelay(monster, GenerateRnd(10) + 10); } else { RandomWalk(monster, md); } } else if (v < 2 * monster.intelligence + 28) { monster.direction = md; StartAttack(monster); } } } monster.checkStandAnimationIsLoaded(monster.direction); } void FallenAi(Monster &monster) { if (monster.goal == MonsterGoal::Attack) { if (monster.goalVar1 != 0) monster.goalVar1--; else monster.goal = MonsterGoal::Normal; } if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } if (monster.goal == MonsterGoal::Retreat) { if (monster.goalVar1-- == 0) { monster.goal = MonsterGoal::Normal; M_StartStand(monster, Opposite(static_cast(monster.goalVar2))); } } if (monster.animInfo.isLastFrame()) { if (!FlipCoin(4)) { return; } StartSpecialStand(monster, monster.direction); if (monster.maxHitPoints - (2 * monster.intelligence + 2) >= monster.hitPoints) monster.hitPoints += 2 * monster.intelligence + 2; else monster.hitPoints = monster.maxHitPoints; const int rad = (2 * monster.intelligence) + 4; for (int y = -rad; y <= rad; y++) { for (int x = -rad; x <= rad; x++) { const int xpos = monster.position.tile.x + x; const int ypos = monster.position.tile.y + y; if (InDungeonBounds({ xpos, ypos })) { const int m = dMonster[xpos][ypos]; if (m <= 0) continue; Monster &otherMonster = Monsters[m - 1]; if (otherMonster.ai != MonsterAIID::Fallen) continue; otherMonster.goal = MonsterGoal::Attack; otherMonster.goalVar1 = 30 * monster.intelligence + 105; } } } } else if (monster.goal == MonsterGoal::Retreat) { monster.direction = static_cast(monster.goalVar2); RandomWalk(monster, monster.direction); } else if (monster.goal == MonsterGoal::Attack) { if (monster.distanceToEnemy() < 2) StartAttack(monster); else RandomWalk(monster, GetMonsterDirection(monster)); } else { SkeletonAi(monster); } } void LeoricAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); int v = GenerateRnd(100); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy >= 2 && monster.activeForTicks == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { if (monster.goal == MonsterGoal::Move || (distanceToEnemy >= 3 && FlipCoin(4))) { if (monster.goal != MonsterGoal::Move) { monster.goalVar1 = 0; monster.goalVar2 = GenerateRnd(2); } monster.goal = MonsterGoal::Move; if ((monster.goalVar1++ >= static_cast(2 * distanceToEnemy) && DirOK(monster, md)) || dTransVal[monster.position.tile.x][monster.position.tile.y] != dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { monster.goal = MonsterGoal::Normal; } else if (!RoundWalk(monster, md, &monster.goalVar2)) { AiDelay(monster, GenerateRnd(10) + 10); } } } else { monster.goal = MonsterGoal::Normal; } if (monster.goal == MonsterGoal::Normal) { if (!UseMultiplayerQuests() && ((distanceToEnemy >= 3 && v < 4 * monster.intelligence + 35) || v < 6) && LineClearMissile(monster.position.tile, monster.enemyPosition)) { const Point newPosition = monster.position.tile + md; if (IsTileAvailable(monster, newPosition) && ActiveMonsterCount < MaxMonsters) { auto typeIndex = GetRandomSkeletonTypeIndex(); if (typeIndex) { SpawnMonster(newPosition, md, *typeIndex); } StartSpecialStand(monster, md); } } else { if (distanceToEnemy >= 2) { v = GenerateRnd(100); if (v >= monster.intelligence + 25 && (IsNoneOf(static_cast(monster.var1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways) || monster.var2 != 0 || (v >= monster.intelligence + 75))) { AiDelay(monster, GenerateRnd(10) + 10); } else { RandomWalk(monster, md); } } else if (v < monster.intelligence + 20) { monster.direction = md; StartAttack(monster); } } } monster.checkStandAnimationIsLoaded(md); } void BatAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); monster.direction = md; const int v = GenerateRnd(100); if (monster.goal == MonsterGoal::Retreat) { if (monster.goalVar1 == 0) { RandomWalk(monster, Opposite(md)); monster.goalVar1++; } else { RandomWalk(monster, PickRandomlyAmong({ Right(md), Left(md) })); monster.goal = MonsterGoal::Normal; } return; } const unsigned distanceToEnemy = monster.distanceToEnemy(); if (monster.type().type == MT_GLOOM && distanceToEnemy >= 5 && v < 4 * monster.intelligence + 33 && LineClear([&monster](Point position) { return IsTileAvailable(monster, position); }, monster.position.tile, monster.enemyPosition)) { if (AddMissile(monster.position.tile, monster.enemyPosition, md, MissileID::Rhino, TARGET_PLAYERS, monster, 0, 0) != nullptr) { monster.occupyTile(monster.position.tile, true); monster.mode = MonsterMode::Charge; } } else if (distanceToEnemy >= 2) { if ((monster.var2 > 20 && v < monster.intelligence + 13) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < monster.intelligence + 63)) { RandomWalk(monster, md); } } else if (v < 4 * monster.intelligence + 8) { StartAttack(monster); monster.goal = MonsterGoal::Retreat; monster.goalVar1 = 0; if (monster.type().type == MT_FAMILIAR) { AddMissile(monster.enemyPosition, monster.enemyPosition + Direction::SouthEast, Direction::South, MissileID::Lightning, TARGET_PLAYERS, monster, GenerateRnd(10) + 1, 0); } } monster.checkStandAnimationIsLoaded(md); } void GargoyleAi(Monster &monster) { const Direction md = GetMonsterDirection(monster); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (monster.activeForTicks != 0 && (monster.flags & MFLAG_ALLOW_SPECIAL) != 0) { UpdateEnemy(monster); if (distanceToEnemy < monster.intelligence + 2U) { monster.flags &= ~MFLAG_ALLOW_SPECIAL; } return; } if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } if (monster.hitPoints < (monster.maxHitPoints / 2)) monster.goal = MonsterGoal::Retreat; if (monster.goal == MonsterGoal::Retreat) { if (distanceToEnemy >= monster.intelligence + 2U) { monster.goal = MonsterGoal::Normal; StartHeal(monster); } else if (!RandomWalk(monster, Opposite(md))) { monster.goal = MonsterGoal::Normal; } } AiAvoidance(monster); } void ButcherAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); monster.direction = md; if (monster.distanceToEnemy() >= 2) RandomWalk(monster, md); else StartAttack(monster); monster.checkStandAnimationIsLoaded(md); } void SneakAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const unsigned dist = 5 - monster.intelligence; const unsigned distanceToEnemy = monster.distanceToEnemy(); if (static_cast(monster.var1) == MonsterMode::HitRecovery) { monster.goal = MonsterGoal::Retreat; monster.goalVar1 = 0; } else if (distanceToEnemy >= dist + 3 || monster.goalVar1 > 8) { monster.goal = MonsterGoal::Normal; monster.goalVar1 = 0; } Direction md = GetMonsterDirection(monster); if (monster.goal == MonsterGoal::Retreat && (monster.flags & MFLAG_NO_ENEMY) == 0) { if ((monster.flags & MFLAG_TARGETS_MONSTER) != 0) md = GetDirection(monster.position.tile, Monsters[monster.enemy].position.tile); else md = GetDirection(monster.position.tile, Players[monster.enemy].position.last); md = Opposite(md); if (monster.type().type == MT_UNSEEN) { md = PickRandomlyAmong({ Right(md), Left(md) }); } } monster.direction = md; const int v = GenerateRnd(100); if (distanceToEnemy < dist && (monster.flags & MFLAG_HIDDEN) != 0) { StartFadein(monster, md, false); } else { if ((distanceToEnemy >= dist + 1) && (monster.flags & MFLAG_HIDDEN) == 0) { StartFadeout(monster, md, true); } else { if (monster.goal == MonsterGoal::Retreat || (distanceToEnemy >= 2 && ((monster.var2 > 20 && v < 4 * monster.intelligence + 14) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 4 * monster.intelligence + 64)))) { monster.goalVar1++; RandomWalk(monster, md); } } } if (monster.mode == MonsterMode::Stand) { if (distanceToEnemy >= 2 || v >= 4 * monster.intelligence + 10) monster.changeAnimationData(MonsterGraphic::Stand); else StartAttack(monster); } } void GharbadAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } const Direction md = GetMonsterDirection(monster); if (monster.talkMsg >= TEXT_GARBUD1 && monster.talkMsg <= TEXT_GARBUD3 && !IsTileVisible(monster.position.tile) && monster.goal == MonsterGoal::Talking) { monster.goal = MonsterGoal::Inquiring; switch (monster.talkMsg) { case TEXT_GARBUD1: monster.talkMsg = TEXT_GARBUD2; Quests[Q_GARBUD]._qvar1 = QS_GHARBAD_FIRST_ITEM_READY; NetSendCmdQuest(true, Quests[Q_GARBUD]); break; case TEXT_GARBUD2: monster.talkMsg = TEXT_GARBUD3; Quests[Q_GARBUD]._qvar1 = QS_GHARBAD_SECOND_ITEM_NEARLY_DONE; NetSendCmdQuest(true, Quests[Q_GARBUD]); break; case TEXT_GARBUD3: monster.talkMsg = TEXT_GARBUD4; Quests[Q_GARBUD]._qvar1 = QS_GHARBAD_SECOND_ITEM_READY; NetSendCmdQuest(true, Quests[Q_GARBUD]); break; default: break; } } if (IsTileVisible(monster.position.tile)) { if (monster.talkMsg == TEXT_GARBUD4) { if (!effect_is_playing(SfxID::Gharbad4) && monster.goal == MonsterGoal::Talking) { monster.goal = MonsterGoal::Normal; monster.activeForTicks = UINT8_MAX; monster.talkMsg = TEXT_NONE; Quests[Q_GARBUD]._qvar1 = QS_GHARBAD_ATTACKING; NetSendCmdQuest(true, Quests[Q_GARBUD]); } } } if (IsAnyOf(monster.goal, MonsterGoal::Normal, MonsterGoal::Move)) AiAvoidance(monster); monster.checkStandAnimationIsLoaded(md); } void SnotSpilAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } const Direction md = GetMonsterDirection(monster); if (monster.talkMsg == TEXT_BANNER10 && !IsTileVisible(monster.position.tile) && monster.goal == MonsterGoal::Talking) { monster.talkMsg = TEXT_BANNER11; monster.goal = MonsterGoal::Inquiring; } if (monster.talkMsg == TEXT_BANNER11 && Quests[Q_LTBANNER]._qvar1 == 3) { monster.talkMsg = TEXT_NONE; monster.goal = MonsterGoal::Normal; } if (IsTileVisible(monster.position.tile)) { if (monster.talkMsg == TEXT_BANNER12) { if (!effect_is_playing(SfxID::Snotspill3) && monster.goal == MonsterGoal::Talking) { ObjChangeMap(SetPiece.position.x, SetPiece.position.y, SetPiece.position.x + SetPiece.size.width + 1, SetPiece.position.y + SetPiece.size.height + 1); Quests[Q_LTBANNER]._qvar1 = 3; NetSendCmdQuest(true, Quests[Q_LTBANNER]); RedoPlayerVision(); monster.activeForTicks = UINT8_MAX; monster.talkMsg = TEXT_NONE; monster.goal = MonsterGoal::Normal; } } if (Quests[Q_LTBANNER]._qvar1 == 3) { if (IsAnyOf(monster.goal, MonsterGoal::Normal, MonsterGoal::Attack)) FallenAi(monster); } } monster.checkStandAnimationIsLoaded(md); } void SnakeAi(Monster &monster) { const int8_t pattern[6] = { 1, 1, 0, -1, -1, 0 }; if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) return; Direction md = GetDirection(monster.position.tile, monster.position.last); monster.direction = md; const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy >= 2) { if (distanceToEnemy < 3 && LineClear([&monster](Point position) { return IsTileAvailable(monster, position); }, monster.position.tile, monster.enemyPosition) && static_cast(monster.var1) != MonsterMode::Charge) { if (AddMissile(monster.position.tile, monster.enemyPosition, md, MissileID::Rhino, TARGET_PLAYERS, monster, 0, 0) != nullptr) { PlayEffect(monster, MonsterSound::Attack); monster.occupyTile(monster.position.tile, true); monster.mode = MonsterMode::Charge; } } else if (static_cast(monster.var1) == MonsterMode::Delay || GenerateRnd(100) >= 35 - 2 * monster.intelligence) { if (pattern[monster.goalVar1] == -1) md = Left(md); else if (pattern[monster.goalVar1] == 1) md = Right(md); monster.goalVar1++; if (monster.goalVar1 > 5) monster.goalVar1 = 0; const auto targetDirection = static_cast(monster.goalVar2); if (md != targetDirection) { int drift = static_cast(md) - monster.goalVar2; if (drift < 0) drift += 8; if (drift < 4) md = Right(targetDirection); else if (drift > 4) md = Left(targetDirection); monster.goalVar2 = static_cast(md); } if (!Walk(monster, md)) RandomWalk2(monster, monster.direction); } else { AiDelay(monster, 15 - monster.intelligence + GenerateRnd(10)); } } else { if (IsAnyOf(static_cast(monster.var1), MonsterMode::Delay, MonsterMode::Charge) || (GenerateRnd(100) < monster.intelligence + 20)) { StartAttack(monster); } else { AiDelay(monster, 10 - monster.intelligence + GenerateRnd(10)); } } monster.checkStandAnimationIsLoaded(monster.direction); } void CounselorAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); const int v = GenerateRnd(100); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (monster.goal == MonsterGoal::Retreat) { if (monster.goalVar1++ <= 3) RandomWalk(monster, Opposite(md)); else { monster.goal = MonsterGoal::Normal; StartFadein(monster, md, true); } } else if (monster.goal == MonsterGoal::Move) { if (distanceToEnemy >= 2 && monster.activeForTicks == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { if (monster.goalVar1++ < static_cast(2 * distanceToEnemy) || !DirOK(monster, md)) { RoundWalk(monster, md, &monster.goalVar2); } else { monster.goal = MonsterGoal::Normal; StartFadein(monster, md, true); } } else { monster.goal = MonsterGoal::Normal; StartFadein(monster, md, true); } } else if (monster.goal == MonsterGoal::Normal) { if (distanceToEnemy >= 2) { if (v < 5 * (monster.intelligence + 10) && LineClearMissile(monster.position.tile, monster.enemyPosition)) { constexpr MissileID MissileTypes[4] = { MissileID::Firebolt, MissileID::ChargedBolt, MissileID::LightningControl, MissileID::Fireball }; StartRangedAttack(monster, MissileTypes[monster.intelligence], RandomIntBetween(monster.minDamage, monster.maxDamage)); } else if (GenerateRnd(100) < 30) { monster.goal = MonsterGoal::Move; monster.goalVar1 = 0; StartFadeout(monster, md, false); } else { AiDelay(monster, GenerateRnd(10) + (2 * (5 - monster.intelligence))); } } else { monster.direction = md; if (monster.hitPoints < (monster.maxHitPoints / 2)) { monster.goal = MonsterGoal::Retreat; monster.goalVar1 = 0; StartFadeout(monster, md, false); } else if (static_cast(monster.var1) == MonsterMode::Delay || GenerateRnd(100) < 2 * monster.intelligence + 20) { StartRangedAttack(monster, MissileID::Null, 0); AddMissile(monster.position.tile, { 0, 0 }, monster.direction, MissileID::FlashBottom, TARGET_PLAYERS, monster, 4, 0); AddMissile(monster.position.tile, { 0, 0 }, monster.direction, MissileID::FlashTop, TARGET_PLAYERS, monster, 4, 0); } else { AiDelay(monster, GenerateRnd(10) + (2 * (5 - monster.intelligence))); } } } if (monster.mode == MonsterMode::Stand) { AiDelay(monster, GenerateRnd(10) + 5); } } void ZharAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } const Direction md = GetMonsterDirection(monster); if (monster.talkMsg == TEXT_ZHAR1 && !IsTileVisible(monster.position.tile) && monster.goal == MonsterGoal::Talking) { monster.talkMsg = TEXT_ZHAR2; monster.goal = MonsterGoal::Inquiring; Quests[Q_ZHAR]._qvar1 = QS_ZHAR_ANGRY; NetSendCmdQuest(true, Quests[Q_ZHAR]); } if (IsTileVisible(monster.position.tile)) { if (monster.talkMsg == TEXT_ZHAR2) { if (!effect_is_playing(SfxID::Zhar2) && monster.goal == MonsterGoal::Talking) { monster.activeForTicks = UINT8_MAX; monster.talkMsg = TEXT_NONE; monster.goal = MonsterGoal::Normal; Quests[Q_ZHAR]._qvar1 = QS_ZHAR_ATTACKING; NetSendCmdQuest(true, Quests[Q_ZHAR]); } } } if (IsAnyOf(monster.goal, MonsterGoal::Normal, MonsterGoal::Retreat, MonsterGoal::Move)) CounselorAi(monster); monster.checkStandAnimationIsLoaded(md); } void MegaAi(Monster &monster) { const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy >= 5) { SkeletonAi(monster); return; } if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); int v = GenerateRnd(100); if (distanceToEnemy >= 2 && monster.activeForTicks == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { if (monster.goal == MonsterGoal::Move || distanceToEnemy >= 3) { if (monster.goal != MonsterGoal::Move) { monster.goalVar1 = 0; monster.goalVar2 = GenerateRnd(2); } monster.goal = MonsterGoal::Move; monster.goalVar3 = 4; if (monster.goalVar1++ < static_cast(2 * distanceToEnemy) || !DirOK(monster, md)) { if (v < 5 * (monster.intelligence + 16)) RoundWalk(monster, md, &monster.goalVar2); } else { monster.goal = MonsterGoal::Normal; } } } else { monster.goal = MonsterGoal::Normal; } if (monster.goal == MonsterGoal::Normal) { if (((distanceToEnemy >= 3 && v < 5 * (monster.intelligence + 2)) || v < 5 * (monster.intelligence + 1) || monster.goalVar3 == 4) && LineClearMissile(monster.position.tile, monster.enemyPosition)) { StartRangedSpecialAttack(monster, MissileID::InfernoControl, 0); } else if (distanceToEnemy >= 2) { v = GenerateRnd(100); if (v < 2 * (5 * monster.intelligence + 25) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 2 * (5 * monster.intelligence + 40))) { RandomWalk(monster, md); } } else { if (GenerateRnd(100) < 10 * (monster.intelligence + 4)) { monster.direction = md; if (FlipCoin()) StartRangedSpecialAttack(monster, MissileID::InfernoControl, 0); else StartAttack(monster); } } monster.goalVar3 = 1; } if (monster.mode == MonsterMode::Stand) { AiDelay(monster, GenerateRnd(10) + 5); } } void LazarusAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } const Direction md = GetMonsterDirection(monster); if (IsTileVisible(monster.position.tile)) { if (!UseMultiplayerQuests()) { const Player &myPlayer = *MyPlayer; if (monster.talkMsg == TEXT_VILE13 && monster.goal == MonsterGoal::Inquiring && myPlayer.position.tile == Point { 35, 46 }) { if (!gbIsMultiplayer) { // Playing ingame movies is currently not supported in multiplayer PlayInGameMovie("gendata\\fprst3.smk"); } monster.mode = MonsterMode::Talk; Quests[Q_BETRAYER]._qvar1 = 5; NetSendCmdQuest(true, Quests[Q_BETRAYER]); } if (monster.talkMsg == TEXT_VILE13 && !effect_is_playing(SfxID::LazarusGreeting) && monster.goal == MonsterGoal::Talking) { ObjChangeMap(1, 18, 20, 24); RedoPlayerVision(); Quests[Q_BETRAYER]._qvar1 = 6; monster.goal = MonsterGoal::Normal; monster.activeForTicks = UINT8_MAX; monster.talkMsg = TEXT_NONE; NetSendCmdQuest(true, Quests[Q_BETRAYER]); } } if (UseMultiplayerQuests() && monster.talkMsg == TEXT_VILE13 && monster.goal == MonsterGoal::Inquiring && Quests[Q_BETRAYER]._qvar1 <= 3) { monster.mode = MonsterMode::Talk; } } if (IsAnyOf(monster.goal, MonsterGoal::Normal, MonsterGoal::Retreat, MonsterGoal::Move)) { if (!UseMultiplayerQuests() && Quests[Q_BETRAYER]._qvar1 == 4 && monster.talkMsg == TEXT_NONE) { // Fix save games affected by teleport bug ObjChangeMapResync(1, 18, 20, 24); RedoPlayerVision(); Quests[Q_BETRAYER]._qvar1 = 6; } monster.talkMsg = TEXT_NONE; CounselorAi(monster); } monster.checkStandAnimationIsLoaded(md); } void LazarusMinionAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) return; const Direction md = GetMonsterDirection(monster); if (IsTileVisible(monster.position.tile)) { if (!UseMultiplayerQuests()) { if (Quests[Q_BETRAYER]._qvar1 <= 5) { monster.goal = MonsterGoal::Inquiring; } else { monster.goal = MonsterGoal::Normal; monster.talkMsg = TEXT_NONE; } } else { monster.goal = MonsterGoal::Normal; } } if (monster.goal == MonsterGoal::Normal) AiRanged(monster); monster.checkStandAnimationIsLoaded(md); } void LachdananAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } const Direction md = GetMonsterDirection(monster); if (monster.talkMsg == TEXT_VEIL9 && !IsTileVisible(monster.position.tile) && monster.goal == MonsterGoal::Talking) { monster.talkMsg = TEXT_VEIL10; monster.goal = MonsterGoal::Inquiring; Quests[Q_VEIL]._qvar2 = QS_VEIL_EARLY_RETURN; NetSendCmdQuest(true, Quests[Q_VEIL]); } if (IsTileVisible(monster.position.tile)) { if (monster.talkMsg == TEXT_VEIL11) { if (!effect_is_playing(SfxID::Lachdanan3) && monster.goal == MonsterGoal::Talking) { monster.talkMsg = TEXT_NONE; Quests[Q_VEIL]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_VEIL]); MonsterDeath(monster, monster.direction, true); delta_kill_monster(monster, monster.position.tile, *MyPlayer); NetSendCmdLocParam1(false, CMD_MONSTDEATH, monster.position.tile, static_cast(monster.getId())); } } } monster.checkStandAnimationIsLoaded(md); } void WarlordAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } const Direction md = GetMonsterDirection(monster); if (IsTileVisible(monster.position.tile)) { if (monster.talkMsg == TEXT_WARLRD9 && monster.goal == MonsterGoal::Inquiring) monster.mode = MonsterMode::Talk; if (monster.talkMsg == TEXT_WARLRD9 && !effect_is_playing(SfxID::Warlord) && monster.goal == MonsterGoal::Talking) { monster.activeForTicks = UINT8_MAX; monster.talkMsg = TEXT_NONE; monster.goal = MonsterGoal::Normal; Quests[Q_WARLORD]._qvar1 = QS_WARLORD_ATTACKING; NetSendCmdQuest(true, Quests[Q_WARLORD]); } } if (monster.goal == MonsterGoal::Normal) SkeletonAi(monster); monster.checkStandAnimationIsLoaded(md); } void HorkDemonAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (monster.activeForTicks < 255) { MonstCheckDoors(monster); } int v = GenerateRnd(100); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy < 2) { monster.goal = MonsterGoal::Normal; } else if (monster.goal == MonsterGoal::Move || (distanceToEnemy >= 5 && !FlipCoin(4))) { if (monster.goal != MonsterGoal::Move) { monster.goalVar1 = 0; monster.goalVar2 = GenerateRnd(2); } monster.goal = MonsterGoal::Move; if (monster.goalVar1++ >= static_cast(2 * distanceToEnemy) || dTransVal[monster.position.tile.x][monster.position.tile.y] != dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { monster.goal = MonsterGoal::Normal; } else if (!RoundWalk(monster, md, &monster.goalVar2)) { AiDelay(monster, GenerateRnd(10) + 10); } } if (monster.goal == MonsterGoal::Normal) { if ((distanceToEnemy >= 3) && v < 2 * monster.intelligence + 43) { const Point position = monster.position.tile + monster.direction; if (IsTileAvailable(monster, position) && ActiveMonsterCount < MaxMonsters) { StartRangedSpecialAttack(monster, MissileID::HorkSpawn, 0); } } else if (distanceToEnemy < 2) { if (v < 2 * monster.intelligence + 28) { monster.direction = md; StartAttack(monster); } } else { v = GenerateRnd(100); if (v < 2 * monster.intelligence + 33 || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 2 * monster.intelligence + 83)) { RandomWalk(monster, md); } else { AiDelay(monster, GenerateRnd(10) + 10); } } } monster.checkStandAnimationIsLoaded(monster.direction); } std::string_view GetMonsterTypeText(const MonsterData &monsterData) { switch (monsterData.monsterClass) { case MonsterClass::Animal: return _("Animal"); case MonsterClass::Demon: return _("Demon"); case MonsterClass::Undead: return _("Undead"); } app_fatal(StrCat("Unknown monsterClass ", static_cast(monsterData.monsterClass))); } void ActivateSpawn(Monster &monster, Point position, Direction dir) { monster.occupyTile(position, false); 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[])(Monster &monster) = { /*MonsterAIID::Zombie */ &ZombieAi, /*MonsterAIID::Fat */ &OverlordAi, /*MonsterAIID::SkeletonMelee */ &SkeletonAi, /*MonsterAIID::SkeletonRanged */ &SkeletonBowAi, /*MonsterAIID::Scavenger */ &ScavengerAi, /*MonsterAIID::Rhino */ &RhinoAi, /*MonsterAIID::GoatMelee */ &AiAvoidance, /*MonsterAIID::GoatRanged */ &AiRanged, /*MonsterAIID::Fallen */ &FallenAi, /*MonsterAIID::Magma */ &AiRangedAvoidance, /*MonsterAIID::SkeletonKing */ &LeoricAi, /*MonsterAIID::Bat */ &BatAi, /*MonsterAIID::Gargoyle */ &GargoyleAi, /*MonsterAIID::Butcher */ &ButcherAi, /*MonsterAIID::Succubus */ &AiRanged, /*MonsterAIID::Sneak */ &SneakAi, /*MonsterAIID::Storm */ &AiRangedAvoidance, /*MonsterAIID::FireMan */ nullptr, /*MonsterAIID::Gharbad */ &GharbadAi, /*MonsterAIID::Acid */ &AiRangedAvoidance, /*MonsterAIID::AcidUnique */ &AiRanged, /*MonsterAIID::Golem */ &GolumAi, /*MonsterAIID::Zhar */ &ZharAi, /*MonsterAIID::Snotspill */ &SnotSpilAi, /*MonsterAIID::Snake */ &SnakeAi, /*MonsterAIID::Counselor */ &CounselorAi, /*MonsterAIID::Mega */ &MegaAi, /*MonsterAIID::Diablo */ &AiRangedAvoidance, /*MonsterAIID::Lazarus */ &LazarusAi, /*MonsterAIID::LazarusSuccubus*/ &LazarusMinionAi, /*MonsterAIID::Lachdanan */ &LachdananAi, /*MonsterAIID::Warlord */ &WarlordAi, /*MonsterAIID::FireBat */ &AiRanged, /*MonsterAIID::Torchant */ &AiRanged, /*MonsterAIID::HorkDemon */ &HorkDemonAi, /*MonsterAIID::Lich */ &AiRanged, /*MonsterAIID::ArchLich */ &AiRanged, /*MonsterAIID::Psychorb */ &AiRanged, /*MonsterAIID::Necromorb */ &AiRanged, /*MonsterAIID::BoneDemon */ &AiRangedAvoidance }; bool IsRelativeMoveOK(const Monster &monster, Point position, Direction mdir) { const 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 IsMonsterAvailable(const MonsterData &monsterData) { if (monsterData.availability == MonsterAvailability::Never) return false; if (gbIsSpawn && monsterData.availability == MonsterAvailability::Retail) return false; return currlevel >= monsterData.minDunLvl && currlevel <= monsterData.maxDunLvl; } bool UpdateModeStance(Monster &monster) { switch (monster.mode) { case MonsterMode::Stand: MonsterIdle(monster); return false; case MonsterMode::MoveNorthwards: case MonsterMode::MoveSouthwards: case MonsterMode::MoveSideways: return MonsterWalk(monster); case MonsterMode::MeleeAttack: return MonsterAttack(monster); case MonsterMode::HitRecovery: return MonsterGotHit(monster); case MonsterMode::Death: MonsterDeath(monster); return false; case MonsterMode::SpecialMeleeAttack: return MonsterSpecialAttack(monster); case MonsterMode::FadeIn: return MonsterFadein(monster); case MonsterMode::FadeOut: return MonsterFadeout(monster); case MonsterMode::RangedAttack: return MonsterRangedAttack(monster); case MonsterMode::SpecialStand: return MonsterSpecialStand(monster); case MonsterMode::SpecialRangedAttack: return MonsterRangedSpecialAttack(monster); case MonsterMode::Delay: return MonsterDelay(monster); case MonsterMode::Petrified: MonsterPetrified(monster); return false; case MonsterMode::Heal: MonsterHeal(monster); return false; case MonsterMode::Talk: MonsterTalk(monster); return false; default: return false; } } MonsterSpritesData LoadMonsterSpritesData(const MonsterData &monsterData) { const size_t numAnims = GetNumAnims(monsterData); MonsterSpritesData result; result.data = MultiFileLoader {}( numAnims, FileNameWithCharAffixGenerator({ "monsters\\", monsterData.spritePath() }, DEVILUTIONX_CL2_EXT, Animletter), result.offsets.data(), [&monsterData](size_t index) { return monsterData.hasAnim(index); }); #ifndef UNPACKED_MPQS // Convert CL2 to CLX: std::vector> clxData; size_t accumulatedSize = 0; for (size_t i = 0, j = 0; i < numAnims; ++i) { if (!monsterData.hasAnim(i)) continue; const uint32_t begin = result.offsets[j]; const uint32_t end = result.offsets[j + 1]; clxData.emplace_back(); Cl2ToClx(reinterpret_cast(&result.data[begin]), end - begin, PointerOrValue { monsterData.width }, clxData.back()); result.offsets[j] = static_cast(accumulatedSize); accumulatedSize += clxData.back().size(); ++j; } result.offsets[clxData.size()] = static_cast(accumulatedSize); result.data = nullptr; result.data = std::unique_ptr(new std::byte[accumulatedSize]); for (size_t i = 0; i < clxData.size(); ++i) { memcpy(&result.data[result.offsets[i]], clxData[i].data(), clxData[i].size()); } #endif return result; } void EnsureMonsterIndexIsActive(size_t monsterId) { assert(monsterId < MaxMonsters); for (size_t index = 0; index < MaxMonsters; index++) { if (ActiveMonsters[index] != monsterId) continue; if (index < ActiveMonsterCount) return; // monster is already active const unsigned oldId = ActiveMonsters[ActiveMonsterCount]; ActiveMonsters[ActiveMonsterCount] = static_cast(monsterId); ActiveMonsters[index] = oldId; ActiveMonsterCount += 1; } } void InitGolem(devilution::Monster &monster, uint8_t golemOwnerPlayerId, int16_t golemSpellLevel) { monster.flags |= MFLAG_GOLEM; monster.goalVar3 = static_cast(golemOwnerPlayerId); const Player &player = Players[golemOwnerPlayerId]; monster.maxHitPoints = 2 * (320 * golemSpellLevel + player._pMaxMana / 3); monster.hitPoints = monster.maxHitPoints; monster.armorClass = 25; monster.golemToHit = 5 * (golemSpellLevel + 8) + 2 * player.getCharacterLevel(); monster.minDamage = 2 * (golemSpellLevel + 4); monster.maxDamage = 2 * (golemSpellLevel + 8); UpdateEnemy(monster); } } // namespace tl::expected AddMonsterType(_monster_id type, placeflag placeflag) { const size_t typeIndex = GetMonsterTypeIndex(type); CMonster &monsterType = LevelMonsterTypes[typeIndex]; if (typeIndex == LevelMonsterTypeCount) { LevelMonsterTypeCount++; monsterType.type = type; const MonsterData &monsterData = MonstersData[type]; monstimgtot += monsterData.image; const size_t numAnims = GetNumAnims(monsterData); for (size_t i = 0; i < numAnims; ++i) { AnimStruct &anim = monsterType.anims[i]; anim.frames = monsterData.frames[i]; if (monsterData.hasAnim(i)) { anim.rate = monsterData.rate[i]; anim.width = monsterData.width; } } RETURN_IF_ERROR(InitMonsterSND(monsterType)); } monsterType.placeFlags |= placeflag; return typeIndex; } tl::expected InitTRNForUniqueMonster(Monster &monster) { char filestr[64]; *BufCopy(filestr, R"(monsters\monsters\)", UniqueMonstersData[static_cast(monster.uniqueType)].mTrnName, ".trn") = '\0'; ASSIGN_OR_RETURN(monster.uniqueMonsterTRN, LoadFileInMemWithStatus(filestr)); return {}; } tl::expected PrepareUniqueMonst(Monster &monster, UniqueMonsterType monsterType, size_t minionType, int bosspacksize, const UniqueMonsterData &uniqueMonsterData) { monster.uniqueType = monsterType; monster.maxHitPoints = uniqueMonsterData.mmaxhp << 6; if (!gbIsMultiplayer) monster.maxHitPoints = std::max(monster.maxHitPoints / 2, 64); monster.hitPoints = monster.maxHitPoints; monster.ai = uniqueMonsterData.mAi; monster.intelligence = uniqueMonsterData.mint; monster.minDamage = uniqueMonsterData.mMinDamage; monster.maxDamage = uniqueMonsterData.mMaxDamage; monster.minDamageSpecial = uniqueMonsterData.mMinDamage; monster.maxDamageSpecial = uniqueMonsterData.mMaxDamage; monster.resistance = uniqueMonsterData.mMagicRes; monster.talkMsg = uniqueMonsterData.mtalkmsg; if (monsterType == UniqueMonsterType::HorkDemon) monster.lightId = NO_LIGHT; else monster.lightId = AddLight(monster.position.tile, 3); if (UseMultiplayerQuests()) { if (monster.ai == MonsterAIID::LazarusSuccubus) monster.talkMsg = TEXT_NONE; if (monster.ai == MonsterAIID::Lazarus && Quests[Q_BETRAYER]._qvar1 > 3) { monster.goal = MonsterGoal::Normal; } else if (monster.talkMsg != TEXT_NONE) { monster.goal = MonsterGoal::Inquiring; } } else if (monster.talkMsg != TEXT_NONE) { monster.goal = MonsterGoal::Inquiring; } if (sgGameInitInfo.nDifficulty == DIFF_NIGHTMARE) { monster.maxHitPoints = 3 * monster.maxHitPoints; if (gbIsHellfire) monster.maxHitPoints += (gbIsMultiplayer ? 100 : 50) << 6; else monster.maxHitPoints += 100 << 6; monster.hitPoints = monster.maxHitPoints; monster.minDamage = 2 * (monster.minDamage + 2); monster.maxDamage = 2 * (monster.maxDamage + 2); monster.minDamageSpecial = 2 * (monster.minDamageSpecial + 2); monster.maxDamageSpecial = 2 * (monster.maxDamageSpecial + 2); } else if (sgGameInitInfo.nDifficulty == DIFF_HELL) { monster.maxHitPoints = 4 * monster.maxHitPoints; if (gbIsHellfire) monster.maxHitPoints += (gbIsMultiplayer ? 200 : 100) << 6; else monster.maxHitPoints += 200 << 6; monster.hitPoints = monster.maxHitPoints; monster.minDamage = 4 * monster.minDamage + 6; monster.maxDamage = 4 * monster.maxDamage + 6; monster.minDamageSpecial = 4 * monster.minDamageSpecial + 6; monster.maxDamageSpecial = 4 * monster.maxDamageSpecial + 6; } RETURN_IF_ERROR(InitTRNForUniqueMonster(monster)); monster.uniqTrans = uniquetrans++; if (uniqueMonsterData.customArmorClass != 0) { monster.armorClass = uniqueMonsterData.customArmorClass; if (sgGameInitInfo.nDifficulty == DIFF_NIGHTMARE) { monster.armorClass += NightmareAcBonus; } else if (sgGameInitInfo.nDifficulty == DIFF_HELL) { monster.armorClass += HellAcBonus; } } if (uniqueMonsterData.monsterPack != UniqueMonsterPack::None) { PlaceGroup(minionType, bosspacksize, &monster, uniqueMonsterData.monsterPack == UniqueMonsterPack::Leashed); } if (monster.ai != MonsterAIID::Gargoyle) { monster.changeAnimationData(MonsterGraphic::Stand); monster.animInfo.currentFrame = GenerateRnd(monster.animInfo.numberOfFrames - 1); monster.flags &= ~MFLAG_ALLOW_SPECIAL; monster.mode = MonsterMode::Stand; } return {}; } void InitLevelMonsters() { LevelMonsterTypeCount = 0; monstimgtot = 0; for (CMonster &levelMonsterType : LevelMonsterTypes) { levelMonsterType.placeFlags = 0; } ClrAllMonsters(); ActiveMonsterCount = 0; totalmonsters = MaxMonsters; std::iota(std::begin(ActiveMonsters), std::end(ActiveMonsters), 0U); uniquetrans = 0; } tl::expected GetLevelMTypes() { RETURN_IF_ERROR(AddMonsterType(MT_GOLEM, PLACE_SPECIAL)); if (currlevel == 16) { RETURN_IF_ERROR(AddMonsterType(MT_ADVOCATE, PLACE_SCATTER)); RETURN_IF_ERROR(AddMonsterType(MT_RBLACK, PLACE_SCATTER)); RETURN_IF_ERROR(AddMonsterType(MT_DIABLO, PLACE_SPECIAL)); return {}; } if (currlevel == 18) RETURN_IF_ERROR(AddMonsterType(MT_HORKSPWN, PLACE_SCATTER)); if (currlevel == 19) { RETURN_IF_ERROR(AddMonsterType(MT_HORKSPWN, PLACE_SCATTER)); RETURN_IF_ERROR(AddMonsterType(MT_HORKDMN, PLACE_UNIQUE)); } if (currlevel == 20) RETURN_IF_ERROR(AddMonsterType(MT_DEFILER, PLACE_UNIQUE)); if (currlevel == 24) { RETURN_IF_ERROR(AddMonsterType(MT_ARCHLICH, PLACE_SCATTER)); RETURN_IF_ERROR(AddMonsterType(MT_NAKRUL, PLACE_SPECIAL)); } if (!setlevel) { if (Quests[Q_BUTCHER].IsAvailable()) RETURN_IF_ERROR(AddMonsterType(MT_CLEAVER, PLACE_SPECIAL)); if (Quests[Q_GARBUD].IsAvailable()) RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::Garbud, PLACE_UNIQUE)); if (Quests[Q_ZHAR].IsAvailable()) RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::Zhar, PLACE_UNIQUE)); if (Quests[Q_LTBANNER].IsAvailable()) RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::SnotSpill, PLACE_UNIQUE)); if (Quests[Q_VEIL].IsAvailable()) RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::Lachdan, PLACE_UNIQUE)); if (Quests[Q_WARLORD].IsAvailable()) RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::WarlordOfBlood, PLACE_UNIQUE)); if (UseMultiplayerQuests() && currlevel == Quests[Q_SKELKING]._qlevel) { RETURN_IF_ERROR(AddMonsterType(MT_SKING, PLACE_UNIQUE)); int skeletonTypeCount = 0; _monster_id skeltypes[NUM_MAX_MTYPES]; for (const _monster_id skeletonType : SkeletonTypes) { if (!IsMonsterAvailable(MonstersData[skeletonType])) continue; skeltypes[skeletonTypeCount++] = skeletonType; } RETURN_IF_ERROR(AddMonsterType(skeltypes[GenerateRnd(skeletonTypeCount)], PLACE_SCATTER)); } _monster_id typelist[MaxMonsters]; int nt = 0; for (size_t i = 0; i < MonstersData.size(); i++) { if (!IsMonsterAvailable(MonstersData[i])) continue; typelist[nt++] = (_monster_id)i; } while (nt > 0 && LevelMonsterTypeCount < MaxLvlMTypes && monstimgtot < 4000) { for (int i = 0; i < nt;) { if (MonstersData[typelist[i]].image > 4000 - monstimgtot) { typelist[i] = typelist[--nt]; continue; } i++; } if (nt != 0) { const int i = GenerateRnd(nt); RETURN_IF_ERROR(AddMonsterType(typelist[i], PLACE_SCATTER)); typelist[i] = typelist[--nt]; } } } else { if (setlvlnum == SL_SKELKING) { RETURN_IF_ERROR(AddMonsterType(MT_SKING, PLACE_UNIQUE)); } } return {}; } tl::expected InitMonsterSND(CMonster &monsterType) { if (!gbSndInited) return {}; const char *prefixes[] { "a", // Attack "h", // Hit "d", // Death "s", // Special }; const MonsterData &data = MonstersData[monsterType.type]; const std::string_view soundSuffix = data.soundPath(); for (int i = 0; i < 4; i++) { const std::string_view prefix = prefixes[i]; if (prefix == "s" && !data.hasSpecialSound) continue; for (int j = 0; j < 2; j++) { char path[64]; *BufCopy(path, "monsters\\", soundSuffix, prefix, j + 1, ".wav") = '\0'; ASSIGN_OR_RETURN(monsterType.sounds[i][j], SoundFileLoadWithStatus(path)); } } return {}; } tl::expected InitMonsterGFX(CMonster &monsterType, MonsterSpritesData &&spritesData) { if (HeadlessMode) return {}; const _monster_id mtype = monsterType.type; const MonsterData &monsterData = MonstersData[mtype]; if (spritesData.data == nullptr) spritesData = LoadMonsterSpritesData(monsterData); monsterType.animData = std::move(spritesData.data); const size_t numAnims = GetNumAnims(monsterData); for (size_t i = 0, j = 0; i < numAnims; ++i) { if (!monsterData.hasAnim(i)) { monsterType.anims[i].sprites = std::nullopt; continue; } const uint32_t begin = spritesData.offsets[j]; const uint32_t end = spritesData.offsets[j + 1]; auto *animSpritesData = reinterpret_cast(&monsterType.animData[begin]); const uint16_t numLists = GetNumListsFromClxListOrSheetBuffer(animSpritesData, end - begin); monsterType.anims[i].sprites = ClxSpriteListOrSheet { animSpritesData, numLists }; ++j; } if (!monsterData.trnFile.empty()) { InitMonsterTRN(monsterType); } if (IsAnyOf(mtype, MT_NMAGMA, MT_YMAGMA, MT_BMAGMA, MT_WMAGMA)) RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::MagmaBall).LoadGFX()); if (IsAnyOf(mtype, MT_STORM, MT_RSTORM, MT_STORML, MT_MAEL)) RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::ThinLightning).LoadGFX()); if (mtype == MT_SNOWWICH) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BloodStarBlue).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BloodStarBlueExplosion).LoadGFX()); } if (mtype == MT_HLSPWN) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BloodStarRed).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BloodStarRedExplosion).LoadGFX()); } if (mtype == MT_SOLBRNR) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BloodStarYellow).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BloodStarYellowExplosion).LoadGFX()); } if (IsAnyOf(mtype, MT_NACID, MT_RACID, MT_BACID, MT_XACID, MT_SPIDLORD)) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::Acid).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::AcidSplat).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::AcidPuddle).LoadGFX()); } if (mtype == MT_LICH) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::OrangeFlare).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::OrangeFlareExplosion).LoadGFX()); } if (mtype == MT_ARCHLICH) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::YellowFlare).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::YellowFlareExplosion).LoadGFX()); } if (IsAnyOf(mtype, MT_PSYCHORB, MT_BONEDEMN)) RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BlueFlare2).LoadGFX()); if (mtype == MT_NECRMORB) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::RedFlare).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::RedFlareExplosion).LoadGFX()); } if (mtype == MT_PSYCHORB) RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BlueFlareExplosion).LoadGFX()); if (mtype == MT_BONEDEMN) RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BlueFlareExplosion2).LoadGFX()); if (mtype == MT_DIABLO) RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::DiabloApocalypseBoom).LoadGFX()); return {}; } tl::expected InitAllMonsterGFX() { if (HeadlessMode) return {}; using LevelMonsterTypeIndices = StaticVector; std::vector monstersBySprite(GetNumMonsterSprites()); for (size_t i = 0; i < LevelMonsterTypeCount; ++i) { monstersBySprite[static_cast(LevelMonsterTypes[i].data().spriteId)].emplace_back(i); } size_t totalUniqueBytes = 0; size_t totalBytes = 0; for (const LevelMonsterTypeIndices &monsterTypes : monstersBySprite) { if (monsterTypes.empty()) continue; CMonster &firstMonster = LevelMonsterTypes[monsterTypes[0]]; if (firstMonster.animData != nullptr) continue; MonsterSpritesData spritesData = LoadMonsterSpritesData(firstMonster.data()); const size_t spritesDataSize = spritesData.offsets[GetNumAnimsWithGraphics(firstMonster.data())]; for (size_t i = 1; i < monsterTypes.size(); ++i) { MonsterSpritesData spritesDataCopy { std::unique_ptr { new std::byte[spritesDataSize] }, spritesData.offsets }; memcpy(spritesDataCopy.data.get(), spritesData.data.get(), spritesDataSize); RETURN_IF_ERROR(InitMonsterGFX(LevelMonsterTypes[monsterTypes[i]], std::move(spritesDataCopy))); } LogVerbose("Loaded monster graphics: {:15s} {:>4d} KiB x{:d}", firstMonster.data().spritePath(), spritesDataSize / 1024, monsterTypes.size()); totalUniqueBytes += spritesDataSize; totalBytes += spritesDataSize * monsterTypes.size(); RETURN_IF_ERROR(InitMonsterGFX(firstMonster, std::move(spritesData))); } LogVerbose(" Total monster graphics: {:>4d} KiB {:>4d} KiB", totalUniqueBytes / 1024, totalBytes / 1024); if (totalUniqueBytes > 0) { // we loaded new sprites, check if we need to update existing monsters for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &monster = Monsters[ActiveMonsters[i]]; if (!monster.animInfo.sprites) RETURN_IF_ERROR(SyncMonsterAnim(monster)); } } return {}; } void WeakenNaKrul() { if (currlevel != 24 || static_cast(UberDiabloMonsterIndex) >= ActiveMonsterCount) return; Monster &monster = Monsters[UberDiabloMonsterIndex]; PlayEffect(monster, MonsterSound::Death); monster.armorClass -= 50; const int hp = monster.maxHitPoints / 2; monster.resistance = 0; monster.hitPoints = hp; monster.maxHitPoints = hp; } void InitGolems() { if (!setlevel) { for (int i = 0; i < ReservedMonsterSlotsForGolems; i++) AddMonster(GolemHoldingCell, Direction::South, 0, false); } } tl::expected 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) RETURN_IF_ERROR(PlaceQuestMonsters()); if (!setlevel) { if (!gbIsSpawn) RETURN_IF_ERROR(PlaceUniqueMonsters()); size_t na = 0; for (int s = 16; s < 96; s++) { for (int t = 16; t < 96; t++) { if (!IsTileSolid({ s, t })) na++; } } size_t numplacemonsters = na / 30; if (gbIsMultiplayer) numplacemonsters += numplacemonsters / 2; if (ActiveMonsterCount + numplacemonsters > MaxMonsters - 10) numplacemonsters = MaxMonsters - 10 - ActiveMonsterCount; totalmonsters = ActiveMonsterCount + numplacemonsters; int numscattypes = 0; size_t scattertypes[NUM_MAX_MTYPES]; for (size_t i = 0; i < LevelMonsterTypeCount; i++) { if ((LevelMonsterTypes[i].placeFlags & PLACE_SCATTER) != 0) { scattertypes[numscattypes] = i; numscattypes++; } } if (numscattypes > 0) { while (ActiveMonsterCount < totalmonsters) { const size_t typeIndex = scattertypes[GenerateRnd(numscattypes)]; if (currlevel == 1 || FlipCoin()) na = 1; else if (currlevel == 2 || leveltype == DTYPE_CRYPT) na = GenerateRnd(2) + 2; else na = GenerateRnd(3) + 3; PlaceGroup(typeIndex, na); } } } 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); } } return InitAllMonsterGFX(); } tl::expected SetMapMonsters(const uint16_t *dunData, Point startPosition) { RETURN_IF_ERROR(AddMonsterType(MT_GOLEM, PLACE_SPECIAL)); if (setlevel) for (int i = 0; i < ReservedMonsterSlotsForGolems; i++) AddMonster(GolemHoldingCell, Direction::South, 0, false); WorldTileSize size = GetDunSize(dunData); const int layer2Offset = 2 + (size.width * size.height); // The rest of the layers are at dPiece scale size *= static_cast(2); const uint16_t *monsterLayer = &dunData[layer2Offset + (size.width * size.height)]; for (WorldTileCoord j = 0; j < size.height; j++) { for (WorldTileCoord i = 0; i < size.width; i++) { auto monsterId = static_cast(Swap16LE(monsterLayer[j * size.width + i])); if (monsterId != 0) { ASSIGN_OR_RETURN(const size_t typeIndex, AddMonsterType(MonstConvTbl[monsterId - 1], PLACE_SPECIAL)); PlaceMonster(ActiveMonsterCount++, typeIndex, startPosition + Displacement { i, j }); } } } return {}; } Monster *AddMonster(Point position, Direction dir, size_t typeIndex, bool inMap) { if (ActiveMonsterCount < MaxMonsters) { Monster &monster = Monsters[ActiveMonsters[ActiveMonsterCount++]]; if (inMap) monster.occupyTile(position, false); InitMonster(monster, dir, typeIndex, position); return &monster; } return nullptr; } void SpawnMonster(Point position, Direction dir, size_t typeIndex) { if (ActiveMonsterCount >= MaxMonsters) return; // The command is only executed for the level owner, to prevent desyncs in multiplayer. if (!MyPlayer->isLevelOwnedByLocalClient()) return; const size_t monsterIndex = ActiveMonsters[ActiveMonsterCount]; ActiveMonsterCount += 1; const uint32_t seed = GetLCGEngineState(); // Update local state immediately to increase ActiveMonsterCount instantly (this allows multiple monsters to be spawned in one game tick) InitializeSpawnedMonster(position, dir, typeIndex, monsterIndex, seed, 0, 0); NetSendCmdSpawnMonster(position, dir, static_cast(typeIndex), static_cast(monsterIndex), seed, 0, 0); } void LoadDeltaSpawnedMonster(size_t typeIndex, size_t monsterId, uint32_t seed, uint8_t golemOwnerPlayerId, int16_t golemSpellLevel) { SetRndSeed(seed); EnsureMonsterIndexIsActive(monsterId); const WorldTilePosition position = GolemHoldingCell; Monster &monster = Monsters[monsterId]; M_ClearSquares(monster); InitMonster(monster, Direction::South, typeIndex, position); if (monster.type().type == MT_GOLEM) { InitGolem(monster, golemOwnerPlayerId, golemSpellLevel); } } void InitializeSpawnedMonster(Point position, Direction dir, size_t typeIndex, size_t monsterId, uint32_t seed, uint8_t golemOwnerPlayerId, int16_t golemSpellLevel) { SetRndSeed(seed); EnsureMonsterIndexIsActive(monsterId); Monster &monster = Monsters[monsterId]; M_ClearSquares(monster); // When we receive a network message, the position we got for the new monster may already be occupied. // That's why we check for the next free tile for the monster. auto freePosition = Crawl(0, MaxCrawlRadius, [&](Displacement displacement) -> std::optional { Point posToCheck = position + displacement; if (IsTileAvailable(posToCheck)) return posToCheck; return {}; }); assert(freePosition); assert(!MyPlayer->isLevelOwnedByLocalClient() || (freePosition && position == *freePosition)); position = freePosition.value_or(position); monster.occupyTile(position, false); InitMonster(monster, dir, typeIndex, position); if (monster.type().type == MT_GOLEM) { InitGolem(monster, golemOwnerPlayerId, golemSpellLevel); StartSpecialStand(monster, dir); } else if (IsSkel(monster.type().type)) { StartSpecialStand(monster, dir); } else { M_StartStand(monster, dir); } } void AddDoppelganger(Monster &monster) { Point target = { 0, 0 }; for (int d = 0; d < 8; d++) { const Point position = monster.position.tile + static_cast(d); if (!IsTileAvailable(position)) continue; target = position; } if (target != Point { 0, 0 }) { const size_t typeIndex = GetMonsterTypeIndex(monster.type().type); SpawnMonster(target, monster.direction, typeIndex); } } void ApplyMonsterDamage(DamageType damageType, Monster &monster, int damage) { AddFloatingNumber(damageType, monster, damage); monster.hitPoints -= damage; if (monster.hitPoints >> 6 <= 0) { delta_kill_monster(monster, monster.position.tile, *MyPlayer); NetSendCmdLocParam1(false, CMD_MONSTDEATH, monster.position.tile, static_cast(monster.getId())); return; } delta_monster_hp(monster, *MyPlayer); NetSendCmdMonDmg(false, static_cast(monster.getId()), damage); } bool M_Talker(const Monster &monster) { return IsAnyOf(monster.ai, MonsterAIID::Lazarus, MonsterAIID::Warlord, MonsterAIID::Gharbad, MonsterAIID::Zhar, MonsterAIID::Snotspill, MonsterAIID::Lachdanan, MonsterAIID::LazarusSuccubus); } void M_StartStand(Monster &monster, Direction md) { ClearMVars(monster); if (monster.type().type == MT_GOLEM) NewMonsterAnim(monster, MonsterGraphic::Walk, md); else NewMonsterAnim(monster, MonsterGraphic::Stand, md); monster.var1 = static_cast(monster.mode); monster.var2 = 0; monster.mode = MonsterMode::Stand; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; UpdateEnemy(monster); } void M_ClearSquares(const Monster &monster) { for (const Point searchTile : PointsInRectangle(Rectangle { monster.position.old, 1 })) { if (FindMonsterAtPosition(searchTile) == &monster) dMonster[searchTile.x][searchTile.y] = 0; } } void M_GetKnockback(Monster &monster, WorldTilePosition attackerStartPos) { const Direction dir = GetDirection(attackerStartPos, monster.position.tile); if (!IsRelativeMoveOK(monster, monster.position.old, dir)) { return; } M_ClearSquares(monster); monster.position.old += dir; StartMonsterGotHit(monster); ChangeLightXY(monster.lightId, monster.position.tile); } void M_StartHit(Monster &monster, int dam) { PlayEffect(monster, MonsterSound::Hit); if (IsHardHit(monster, dam)) { if (monster.type().type == MT_BLINK) { Teleport(monster); } else if (IsAnyOf(monster.type().type, MT_NSCAV, MT_BSCAV, MT_WSCAV, MT_YSCAV, MT_GRAVEDIG)) { monster.goal = MonsterGoal::Normal; monster.goalVar1 = 0; monster.goalVar2 = 0; } if (monster.mode != MonsterMode::Petrified) { StartMonsterGotHit(monster); } } } void M_StartHit(Monster &monster, const Player &player, int dam) { monster.tag(player); if (IsHardHit(monster, dam)) { monster.enemy = player.getId(); monster.enemyPosition = player.position.future; monster.flags &= ~MFLAG_TARGETS_MONSTER; if (monster.mode != MonsterMode::Petrified) { monster.direction = GetMonsterDirection(monster); } } M_StartHit(monster, dam); } void MonsterDeath(Monster &monster, Direction md, bool sendmsg) { if (!monster.isPlayerMinion()) AddPlrMonstExper(monster.level(sgGameInitInfo.nDifficulty), monster.exp(sgGameInitInfo.nDifficulty), monster.whoHit); MonsterKillCounts[monster.type().type]++; monster.hitPoints = 0; monster.flags &= ~MFLAG_HIDDEN; SetRndSeed(monster.rndItemSeed); SpawnLoot(monster, sendmsg); if (monster.type().type == MT_DIABLO) DiabloDeath(monster, true); else PlayEffect(monster, MonsterSound::Death); if (monster.mode != MonsterMode::Petrified) { if (monster.type().type == MT_GOLEM) md = Direction::South; NewMonsterAnim(monster, MonsterGraphic::Death, md, gGameLogicStep < GameLogicStep::ProcessMonsters ? AnimationDistributionFlags::ProcessAnimationPending : AnimationDistributionFlags::None); monster.mode = MonsterMode::Death; } else if (monster.isUnique()) { AddUnLight(monster.lightId); } monster.goal = MonsterGoal::None; monster.var1 = 0; monster.position.tile = monster.position.old; monster.position.future = monster.position.old; M_ClearSquares(monster); monster.occupyTile(monster.position.tile, false); CheckQuestKill(monster, sendmsg); M_FallenFear(monster.position.tile); if (IsAnyOf(monster.type().type, MT_NACID, MT_RACID, MT_BACID, MT_XACID, MT_SPIDLORD)) AddMissile(monster.position.tile, { 0, 0 }, Direction::South, MissileID::AcidPuddle, TARGET_PLAYERS, monster, monster.intelligence + 1, 0); } void StartMonsterDeath(Monster &monster, const Player &player, bool sendmsg) { monster.tag(player); const Direction md = GetDirection(monster.position.tile, player.position.tile); MonsterDeath(monster, md, sendmsg); } void KillGolem(Monster &golem) { delta_kill_monster(golem, golem.position.tile, *MyPlayer); NetSendCmdLocParam1(false, CMD_MONSTDEATH, golem.position.tile, static_cast(golem.getId())); M_StartKill(golem, *MyPlayer); } void M_StartKill(Monster &monster, const Player &player) { StartMonsterDeath(monster, player, true); } void M_SyncStartKill(Monster &monster, Point position, const Player &player) { if (monster.hitPoints == 0 || monster.mode == MonsterMode::Death) { return; } if (dMonster[position.x][position.y] == 0) { M_ClearSquares(monster); monster.position.tile = position; monster.position.old = position; } StartMonsterDeath(monster, player, false); } void M_UpdateRelations(const Monster &monster) { if (monster.hasLeashedMinions()) ReleaseMinions(monster); ShrinkLeaderPacksize(monster); } void DoEnding() { if (gbIsMultiplayer) { SNetLeaveGame(leaveinfo_t::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); const bool bMusicOn = gbMusicOn; gbMusicOn = true; const 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(sgGameInitInfo.nDifficulty + 1)); for (Player &player : Players) { player._pmode = PM_QUIT; player._pInvincible = true; if (gbIsMultiplayer) { if (player.hasNoLife()) player._pHitPoints = 64; if (player.hasNoMana()) player._pMana = 64; } } } bool Walk(Monster &monster, Direction md) { if (!DirOK(monster, md)) { return false; } if (md == Direction::NoDirection) return true; WalkInDirection(monster, md); return true; } void GolumAi(Monster &golem) { if (golem.position.tile.x == 1 && golem.position.tile.y == 0) { return; } if (IsAnyOf(golem.mode, MonsterMode::Death, MonsterMode::SpecialStand) || golem.isWalking()) { return; } if ((golem.flags & MFLAG_TARGETS_MONSTER) == 0) UpdateEnemy(golem); if (golem.mode == MonsterMode::MeleeAttack) { return; } if ((golem.flags & MFLAG_NO_ENEMY) == 0) { Monster &enemy = Monsters[golem.enemy]; const int mex = golem.position.tile.x - enemy.position.future.x; const int mey = golem.position.tile.y - enemy.position.future.y; golem.direction = GetDirection(golem.position.tile, enemy.position.tile); if (std::abs(mex) < 2 && std::abs(mey) < 2) { golem.enemyPosition = enemy.position.tile; if (enemy.activeForTicks == 0) { enemy.activeForTicks = UINT8_MAX; enemy.position.last = golem.position.tile; for (int j = 0; j < 5; j++) { for (int k = 0; k < 5; k++) { const int mx = golem.position.tile.x + k - 2; const int my = golem.position.tile.y + j - 2; if (!InDungeonBounds({ mx, my })) continue; const int enemyId = dMonster[mx][my]; if (enemyId > 0) Monsters[enemyId - 1].activeForTicks = UINT8_MAX; } } } StartAttack(golem); return; } if (AiPlanPath(golem)) return; } golem.pathCount++; if (golem.pathCount > 8) golem.pathCount = 5; if (RandomWalk(golem, Players[golem.goalVar3]._pdir)) return; Direction md = Left(golem.direction); for (int j = 0; j < 8; j++) { md = Right(md); if (Walk(golem, md)) { break; } } } void DeleteMonsterList() { for (int i = 0; i < ReservedMonsterSlotsForGolems; i++) { Monster &golem = Monsters[i]; if (!golem.isInvalid) continue; golem.position.tile = GolemHoldingCell; golem.position.future = { 0, 0 }; golem.position.old = { 0, 0 }; golem.isInvalid = false; } for (size_t i = ReservedMonsterSlotsForGolems; i < ActiveMonsterCount;) { if (Monsters[ActiveMonsters[i]].isInvalid) { if (pcursmonst == static_cast(ActiveMonsters[i])) // Unselect monster if player highlighted it pcursmonst = -1; DeleteMonster(i); } else { i++; } } } void RemoveEnemyReferences(const Player &player) { if (&player == MyPlayer || !player.isOnActiveLevel()) return; const size_t playerId = player.getId(); for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &activeMonster = Monsters[ActiveMonsters[i]]; if ((activeMonster.flags & MFLAG_TARGETS_MONSTER) == 0 && activeMonster.enemy == playerId) { activeMonster.flags |= MFLAG_NO_ENEMY; } } } void ProcessMonsters() { DeleteMonsterList(); assert(ActiveMonsterCount <= MaxMonsters); for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &monster = Monsters[ActiveMonsters[i]]; FollowTheLeader(monster); if (gbIsMultiplayer) { SetRndSeed(monster.aiSeed); monster.aiSeed = AdvanceRndSeed(); } if (monster.hitPoints < monster.maxHitPoints && monster.hitPoints >> 6 > 0) { if (monster.level(sgGameInitInfo.nDifficulty) > 1) { monster.hitPoints += monster.level(sgGameInitInfo.nDifficulty) / 2; } else { monster.hitPoints += monster.level(sgGameInitInfo.nDifficulty); } monster.hitPoints = std::min(monster.hitPoints, monster.maxHitPoints); // prevent going over max HP with part of a single regen tick } const bool isMonsterVisible = IsTileVisible(monster.position.tile); if (isMonsterVisible && monster.activeForTicks == 0) { if (monster.type().type == MT_CLEAVER) { PlaySFX(SfxID::ButcherGreeting); } if (monster.type().type == MT_NAKRUL) { if (sgGameInitInfo.bCowQuest != 0) { PlaySFX(SfxID::NaKrul6); } else { if (IsUberRoomOpened) PlaySFX(SfxID::NaKrul4); else PlaySFX(SfxID::NaKrul5); } } if (monster.type().type == MT_DEFILER) PlaySFX(SfxID::Defiler8); UpdateEnemy(monster); } if ((monster.flags & MFLAG_NO_ENEMY) == 0) { if ((monster.flags & MFLAG_TARGETS_MONSTER) != 0) { assert(monster.enemy >= 0 && monster.enemy < MaxMonsters); monster.position.last = Monsters[monster.enemy].position.future; monster.enemyPosition = monster.position.last; } else { assert(monster.enemy >= 0 && monster.enemy < MAX_PLRS); const Player &player = Players[monster.enemy]; monster.enemyPosition = player.position.future; if (isMonsterVisible) { monster.position.last = player.position.future; } } } if ((monster.flags & MFLAG_TARGETS_MONSTER) == 0) { if (isMonsterVisible) { monster.activeForTicks = UINT8_MAX; } else if (monster.activeForTicks != 0 && monster.type().type != MT_DIABLO) { monster.activeForTicks--; } } while (true) { if ((monster.flags & MFLAG_SEARCH) == 0 || !AiPlanPath(monster)) { AiProc[static_cast(monster.ai)](monster); } if (!UpdateModeStance(monster)) break; GroupUnity(monster); } if (monster.mode != MonsterMode::Petrified && (monster.flags & MFLAG_ALLOW_SPECIAL) == 0) { monster.animInfo.processAnimation((monster.flags & MFLAG_LOCK_ANIMATION) != 0); } } DeleteMonsterList(); } void FreeMonsters() { for (CMonster &monsterType : LevelMonsterTypes) { monsterType.animData = nullptr; monsterType.corpseId = 0; for (AnimStruct &animData : monsterType.anims) { animData.sprites = std::nullopt; } for (auto &variants : monsterType.sounds) { for (auto &sound : variants) { sound = nullptr; } } } } bool DirOK(const Monster &monster, Direction mdir) { const Point position = monster.position.tile; const Point futurePosition = position + mdir; if (!IsRelativeMoveOK(monster, position, mdir)) return false; if (monster.leaderRelation == LeaderRelation::Leashed) { return futurePosition.WalkingDistance(monster.getLeader()->position.future) < 4; } if (!monster.hasLeashedMinions()) 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; Monster *minion = FindMonsterAtPosition({ x, y }, true); if (minion == nullptr) continue; if (minion->leaderRelation == LeaderRelation::Leashed && minion->getLeader() == &monster) { mcount++; } } } return mcount == monster.packSize; } bool PosOkMissile(Point position) { return !TileHasAny(position, TileProperties::BlockMissile); } bool LineClearMissile(Point startPoint, Point endPoint) { return LineClear(PosOkMissile, startPoint, endPoint); } bool LineClear(tl::function_ref clear, Point startPoint, Point endPoint) { Point position = startPoint; int dx = endPoint.x - position.x; int dy = endPoint.y - position.y; if (std::abs(dx) > std::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; } tl::expected SyncMonsterAnim(Monster &monster) { #ifdef _DEBUG // fix for saves with debug monsters having type originally not on the level CMonster &monsterType = LevelMonsterTypes[monster.levelType]; if (monsterType.corpseId == 0) { RETURN_IF_ERROR(InitMonsterGFX(monsterType)); monsterType.corpseId = 1; } #endif if (monster.isUnique()) { RETURN_IF_ERROR(InitTRNForUniqueMonster(monster)); } MonsterGraphic graphic = MonsterGraphic::Stand; switch (monster.getVisualMonsterMode()) { 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); return {}; } void M_FallenFear(Point position) { const Rectangle fearArea = Rectangle { position, 4 }; for (const Point tile : PointsInRectangle(fearArea)) { if (!InDungeonBounds(tile)) continue; const int m = dMonster[tile.x][tile.y]; if (m == 0) continue; Monster &monster = Monsters[std::abs(m) - 1]; if (monster.ai != MonsterAIID::Fallen || monster.hitPoints >> 6 <= 0) continue; const int runDistance = std::max((8 - monster.data().level), 2); monster.goal = MonsterGoal::Retreat; monster.goalVar1 = runDistance; monster.goalVar2 = static_cast(GetDirection(position, monster.position.tile)); } } void PrintMonstHistory(int mt) { if (*GetOptions().Gameplay.showMonsterType) { AddInfoBoxString(fmt::format(fmt::runtime(_("Type: {:s} Kills: {:d}")), GetMonsterTypeText(MonstersData[mt]), MonsterKillCounts[mt])); } else { AddInfoBoxString(fmt::format(fmt::runtime(_("Total kills: {:d}")), MonsterKillCounts[mt])); } if (MonsterKillCounts[mt] >= 30) { int minHP = MonstersData[mt].hitPointsMinimum; int maxHP = MonstersData[mt].hitPointsMaximum; if (!gbIsMultiplayer) { minHP /= 2; maxHP /= 2; } minHP = std::max(minHP, 1); maxHP = std::max(maxHP, 1); int hpBonusNightmare = 100; int hpBonusHell = 200; 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; } AddInfoBoxString(fmt::format(fmt::runtime(_("Hit Points: {:d}-{:d}")), minHP, maxHP)); } if (MonsterKillCounts[mt] >= 15) { const int res = (sgGameInitInfo.nDifficulty != DIFF_HELL) ? MonstersData[mt].resistance : MonstersData[mt].resistanceHell; if ((res & (RESIST_MAGIC | RESIST_FIRE | RESIST_LIGHTNING | IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING)) == 0) { AddInfoBoxString(_("No magic resistance")); } else { if ((res & (RESIST_MAGIC | RESIST_FIRE | RESIST_LIGHTNING)) != 0) { std::string resists = std::string(_("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")); AddInfoBoxString(resists); } if ((res & (IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING)) != 0) { std::string immune = std::string(_("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")); AddInfoBoxString(immune); } } } } void PrintUniqueHistory() { const Monster &monster = Monsters[pcursmonst]; if (*GetOptions().Gameplay.showMonsterType) { AddInfoBoxString(fmt::format(fmt::runtime(_("Type: {:s}")), GetMonsterTypeText(monster.data()))); } const int res = monster.resistance & (RESIST_MAGIC | RESIST_FIRE | RESIST_LIGHTNING | IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING); if (res == 0) { AddInfoBoxString(_("No resistances")); AddInfoBoxString(_("No Immunities")); } else { if ((res & (RESIST_MAGIC | RESIST_FIRE | RESIST_LIGHTNING)) != 0) AddInfoBoxString(_("Some Magic Resistances")); else AddInfoBoxString(_("No resistances")); if ((res & (IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING)) != 0) { AddInfoBoxString(_("Some Magic Immunities")); } else { AddInfoBoxString(_("No Immunities")); } } } void PlayEffect(Monster &monster, MonsterSound mode) { if (MyPlayer->pLvlLoad != 0) { return; } const int sndIdx = GenerateRnd(2); if (!gbSndInited || !gbSoundOn || gbBufferMsgs != 0) { return; } TSnd *snd = monster.type().sounds[static_cast(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) { assert(static_cast(missile._misource) < MaxMonsters); Monster &monster = Monsters[missile._misource]; const Point oldPosition = missile.position.tile; monster.occupyTile(position, false); monster.direction = missile.getDirection(); monster.position.tile = position; M_StartStand(monster, monster.direction); M_StartHit(monster, 0); if (monster.type().type == MT_GLOOM) return; if ((monster.flags & MFLAG_TARGETS_MONSTER) == 0) { Player *player = PlayerAtPosition(oldPosition, true); if (player == nullptr) return; MonsterAttackPlayer(monster, *player, 500, monster.minDamageSpecial, monster.maxDamageSpecial); if (IsAnyOf(monster.type().type, MT_NSNAKE, MT_RSNAKE, MT_BSNAKE, MT_GSNAKE)) return; if (player->_pmode != PM_GOTHIT && player->_pmode != PM_DEATH) StartPlrHit(*player, 0, true); const Point newPosition = oldPosition + GetDirection(missile.position.start, oldPosition); if (PosOkPlayer(*player, newPosition)) { player->position.tile = newPosition; FixPlayerLocation(*player, player->_pdir); FixPlrWalkTags(*player); player->occupyTile(newPosition, false); SetPlayerOld(*player); } return; } Monster *target = FindMonsterAtPosition(oldPosition, true); if (target == nullptr) return; MonsterAttackMonster(monster, *target, 500, monster.minDamageSpecial, monster.maxDamageSpecial); if (IsAnyOf(monster.type().type, MT_NSNAKE, MT_RSNAKE, MT_BSNAKE, MT_GSNAKE)) return; const Point newPosition = oldPosition + GetDirection(missile.position.start, oldPosition); if (IsTileAvailable(*target, newPosition)) { monster.occupyTile(newPosition, false); dMonster[oldPosition.x][oldPosition.y] = 0; monster.position.tile = newPosition; monster.position.future = newPosition; } } Monster *FindMonsterAtPosition(Point position, bool ignoreMovingMonsters) { if (!InDungeonBounds(position)) { return nullptr; } auto monsterId = dMonster[position.x][position.y]; if (monsterId == 0 || (ignoreMovingMonsters && monsterId < 0)) { // nothing at this position, return a nullptr return nullptr; } return &Monsters[std::abs(monsterId) - 1]; } Monster *FindUniqueMonster(UniqueMonsterType monsterType) { for (size_t i = 0; i < ActiveMonsterCount; i++) { const int monsterId = ActiveMonsters[i]; Monster &monster = Monsters[monsterId]; if (monster.uniqueType == monsterType) return &monster; } return nullptr; } Monster *FindGolemForPlayer(const Player &player) { for (size_t i = 0; i < ActiveMonsterCount; i++) { const int monsterId = ActiveMonsters[i]; Monster &monster = Monsters[monsterId]; if (monster.type().type != MT_GOLEM) continue; if (monster.position.tile == GolemHoldingCell) continue; if (monster.goalVar3 != player.getId()) continue; if (monster.hitPoints == 0) continue; return &monster; } return nullptr; } bool IsTileAvailable(const Monster &monster, Point position) { if (!IsTileAvailable(position)) return false; return IsTileSafe(monster, position); } bool IsSkel(_monster_id mt) { return c_find(SkeletonTypes, mt) != SkeletonTypes.end(); } bool IsGoat(_monster_id mt) { return IsAnyOf(mt, MT_NGOATMC, MT_BGOATMC, MT_RGOATMC, MT_GGOATMC, MT_NGOATBW, MT_BGOATBW, MT_RGOATBW, MT_GGOATBW); } void ActivateSkeleton(Monster &monster, Point position) { if (IsTileAvailable(position)) { ActivateSpawn(monster, position, Direction::SouthWest); return; } constexpr std::array spawnDirections { Direction::North, Direction::NorthEast, Direction::East, Direction::NorthWest, Direction::SouthEast, Direction::West, Direction::SouthWest, Direction::South }; std::bitset<8> spawnOk; for (size_t i = 0; i < spawnDirections.size(); i++) { if (IsTileAvailable(position + spawnDirections[i])) spawnOk.set(i); } if (spawnOk.none()) return; // this is used in the following loop to find the nth set bit. int spawnChoice = GenerateRnd(15) % spawnOk.count(); for (size_t i = 0; i < spawnOk.size(); i++) { if (!spawnOk.test(i)) continue; if (spawnChoice > 0) { spawnChoice--; continue; } ActivateSpawn(monster, position + spawnDirections[i], Opposite(spawnDirections[i])); return; } } Monster *PreSpawnSkeleton() { Monster *skeleton = AddSkeleton({ 0, 0 }, Direction::South, false); if (skeleton != nullptr) M_StartStand(*skeleton, Direction::South); return skeleton; } void TalktoMonster(Player &player, Monster &monster) { if (&player == MyPlayer) monster.mode = MonsterMode::Talk; if (monster.uniqueType == UniqueMonsterType::SnotSpill && Quests[Q_LTBANNER].IsAvailable() && Quests[Q_LTBANNER]._qvar1 == 2) { if (RemoveInventoryItemById(player, IDI_BANNER)) { Quests[Q_LTBANNER]._qactive = QUEST_DONE; monster.talkMsg = TEXT_BANNER12; monster.goal = MonsterGoal::Inquiring; NetSendCmdQuest(true, Quests[Q_LTBANNER]); } } if (monster.uniqueType == UniqueMonsterType::Lachdan && Quests[Q_VEIL].IsAvailable() && monster.talkMsg >= TEXT_VEIL9) { if (RemoveInventoryItemById(player, IDI_GLDNELIX) && (monster.flags & MFLAG_QUEST_COMPLETE) == 0) { monster.talkMsg = TEXT_VEIL11; monster.goal = MonsterGoal::Inquiring; monster.flags |= MFLAG_QUEST_COMPLETE; if (MyPlayer == &player) { SpawnUnique(UITEM_STEELVEIL, monster.position.tile + Direction::South); Quests[Q_VEIL]._qvar2 = QS_VEIL_ITEM_SPAWNED; NetSendCmdQuest(true, Quests[Q_VEIL]); } } } if (monster.uniqueType == UniqueMonsterType::Zhar && monster.talkMsg == TEXT_ZHAR1 && (monster.flags & MFLAG_QUEST_COMPLETE) == 0) { if (MyPlayer == &player) { Quests[Q_ZHAR]._qactive = QUEST_ACTIVE; Quests[Q_ZHAR]._qlog = true; Quests[Q_ZHAR]._qvar1 = QS_ZHAR_ITEM_SPAWNED; SetRndSeed(monster.rndItemSeed); DiscardRandomValues(10); CreateTypeItem(monster.position.tile + Displacement { 1, 1 }, false, ItemType::Misc, IMISC_BOOK, false, false, true); monster.flags |= MFLAG_QUEST_COMPLETE; NetSendCmdQuest(true, Quests[Q_ZHAR]); } } if (monster.uniqueType == UniqueMonsterType::Garbud && MyPlayer == &player) { if (monster.talkMsg == TEXT_GARBUD1) { Quests[Q_GARBUD]._qactive = QUEST_ACTIVE; Quests[Q_GARBUD]._qlog = true; NetSendCmdQuest(true, Quests[Q_GARBUD]); } if (monster.talkMsg == TEXT_GARBUD2 && (monster.flags & MFLAG_QUEST_COMPLETE) == 0) { SetRndSeed(monster.rndItemSeed); DiscardRandomValues(10); SpawnItem(monster, monster.position.tile + Displacement { 1, 1 }, false, true); monster.flags |= MFLAG_QUEST_COMPLETE; Quests[Q_GARBUD]._qvar1 = QS_GHARBAD_FIRST_ITEM_SPAWNED; NetSendCmdQuest(true, Quests[Q_GARBUD]); } } } void SpawnGolem(const Player &player, Point position, uint8_t spellLevel) { // Search monster index to use for the new golem Monster *golem = nullptr; // 1. Prefer MonsterIndex = PlayerIndex for vanilla compatibility if (player.getId() < ReservedMonsterSlotsForGolems) { Monster &reservedGolem = Monsters[player.getId()]; if (reservedGolem.position.tile == GolemHoldingCell || reservedGolem.hitPoints == 0) golem = &reservedGolem; } // 2. Use reserved slots, so additional Monsters can spawn if (golem == nullptr) { for (int i = 0; i < ReservedMonsterSlotsForGolems; i++) { Monster &reservedGolem = Monsters[i]; if (reservedGolem.position.tile == GolemHoldingCell || reservedGolem.hitPoints == 0) { golem = &reservedGolem; break; } } } // 3. Use normal monster slot if (golem == nullptr) { if (ActiveMonsterCount >= MaxMonsters) return; const size_t monsterIndex = ActiveMonsters[ActiveMonsterCount]; ActiveMonsterCount += 1; golem = &Monsters[monsterIndex]; } if (golem == nullptr) return; const size_t monsterIndex = golem->getId(); const uint32_t seed = GetLCGEngineState(); // Update local state immediately to increase ActiveMonsterCount instantly (this allows multiple monsters to be spawned in one game tick) InitializeSpawnedMonster(position, Direction::South, 0, monsterIndex, seed, player.getId(), spellLevel); NetSendCmdSpawnMonster(position, Direction::South, 0, static_cast(monsterIndex), seed, player.getId(), spellLevel); } bool CanTalkToMonst(const Monster &monster) { return IsAnyOf(monster.goal, MonsterGoal::Inquiring, MonsterGoal::Talking); } uint8_t encode_enemy(Monster &monster) { if ((monster.flags & MFLAG_TARGETS_MONSTER) != 0) return monster.enemy; return monster.enemy + MaxMonsters; } void decode_enemy(Monster &monster, uint8_t enemyId) { if (enemyId >= MaxMonsters) { enemyId -= MaxMonsters; monster.flags &= ~MFLAG_TARGETS_MONSTER; monster.enemy = enemyId; monster.enemyPosition = Players[enemyId].position.future; } else { monster.flags |= MFLAG_TARGETS_MONSTER; monster.enemy = enemyId; monster.enemyPosition = Monsters[enemyId].position.future; } } [[nodiscard]] size_t Monster::getId() const { return std::distance(&Monsters[0], this); } Monster *Monster::getLeader() const { if (leader == Monster::NoLeader) return nullptr; return &Monsters[leader]; } void Monster::setLeader(const Monster *newLeader) { if (newLeader == nullptr) { // really we should update this->leader to NoLeader to avoid leaving a dangling reference to a dead monster // when passed nullptr. So that buffed minions are drawn with a distinct colour in monhealthbar we leave the // reference and hope that no code tries to modify the leader through this instance later. leaderRelation = LeaderRelation::None; return; } this->leader = static_cast(newLeader->getId()); leaderRelation = LeaderRelation::Leashed; ai = newLeader->ai; } [[nodiscard]] unsigned Monster::distanceToEnemy() const { const int mx = position.tile.x - enemyPosition.x; const int my = position.tile.y - enemyPosition.y; return std::max(std::abs(mx), std::abs(my)); } void Monster::checkStandAnimationIsLoaded(Direction mdir) { if (IsAnyOf(mode, MonsterMode::Stand, MonsterMode::Talk)) { direction = mdir; changeAnimationData(MonsterGraphic::Stand); } } void Monster::petrify() { mode = MonsterMode::Petrified; animInfo.isPetrified = true; } bool Monster::isWalking() const { switch (getVisualMonsterMode()) { case MonsterMode::MoveNorthwards: case MonsterMode::MoveSouthwards: case MonsterMode::MoveSideways: return true; default: return false; } } bool Monster::isImmune(MissileID missileType, DamageType missileElement) const { if (((resistance & IMMUNE_MAGIC) != 0 && missileElement == DamageType::Magic) || ((resistance & IMMUNE_FIRE) != 0 && missileElement == DamageType::Fire) || ((resistance & IMMUNE_LIGHTNING) != 0 && missileElement == DamageType::Lightning) || ((resistance & IMMUNE_ACID) != 0 && missileElement == DamageType::Acid)) return true; if (missileType == MissileID::HolyBolt && type().type != MT_DIABLO && data().monsterClass != MonsterClass::Undead) return true; return false; } bool Monster::isResistant(MissileID missileType, DamageType missileElement) const { if (((resistance & RESIST_MAGIC) != 0 && missileElement == DamageType::Magic) || ((resistance & RESIST_FIRE) != 0 && missileElement == DamageType::Fire) || ((resistance & RESIST_LIGHTNING) != 0 && missileElement == DamageType::Lightning)) return true; if (gbIsHellfire && missileType == MissileID::HolyBolt && IsAnyOf(type().type, MT_DIABLO, MT_BONEDEMN)) return true; return false; } bool Monster::isPlayerMinion() const { return (flags & MFLAG_GOLEM) != 0 && (flags & MFLAG_BERSERK) == 0; } bool Monster::isPossibleToHit() const { return hitPoints >> 6 > 0 && talkMsg == TEXT_NONE && (type().type != MT_ILLWEAV || goal != MonsterGoal::Retreat) && !(IsAnyOf(mode, MonsterMode::Charge, MonsterMode::Death)) && (!IsAnyOf(type().type, MT_COUNSLR, MT_MAGISTR, MT_CABALIST, MT_ADVOCATE) || goal == MonsterGoal::Normal); } void Monster::tag(const Player &tagger) { whoHit |= 1 << tagger.getId(); } bool Monster::tryLiftGargoyle() { if (ai == MonsterAIID::Gargoyle && (flags & MFLAG_ALLOW_SPECIAL) != 0) { flags &= ~MFLAG_ALLOW_SPECIAL; mode = MonsterMode::SpecialMeleeAttack; return true; } return false; } MonsterMode Monster::getVisualMonsterMode() const { if (mode != MonsterMode::Petrified) return mode; const size_t monsterId = this->getId(); for (auto &missile : Missiles) { // Search the missile that will restore the original monster mode and use the saved/original monster mode from it if (missile._mitype == MissileID::StoneCurse && static_cast(missile.var2) == monsterId) { return static_cast(missile.var1); } } return MonsterMode::Petrified; } unsigned int Monster::toHit(_difficulty difficulty) const { if (isPlayerMinion()) return golemToHit; unsigned int baseToHit = data().toHit; if (isUnique() && UniqueMonstersData[static_cast(uniqueType)].customToHit != 0) { baseToHit = UniqueMonstersData[static_cast(uniqueType)].customToHit; } if (difficulty == DIFF_NIGHTMARE) { baseToHit += NightmareToHitBonus; } else if (difficulty == DIFF_HELL) { baseToHit += HellToHitBonus; } return baseToHit; } unsigned int Monster::toHitSpecial(_difficulty difficulty) const { unsigned int baseToHitSpecial = data().toHitSpecial; if (isUnique() && UniqueMonstersData[static_cast(uniqueType)].customToHit != 0) { baseToHitSpecial = UniqueMonstersData[static_cast(uniqueType)].customToHit; } if (difficulty == DIFF_NIGHTMARE) { baseToHitSpecial += NightmareToHitBonus; } else if (difficulty == DIFF_HELL) { baseToHitSpecial += HellToHitBonus; } return baseToHitSpecial; } void Monster::occupyTile(Point tile, bool isMoving) const { const auto id = static_cast(this->getId() + 1); dMonster[tile.x][tile.y] = isMoving ? -id : id; } } // namespace devilution