/** * @file monstdat.cpp * * Implementation of all monster data. */ #include "monstdat.h" #include #include #include #include "data/file.hpp" #include "items.h" #include "monster.h" #include "textdat.h" #include "utils/language.h" namespace devilution { namespace { // Returns a `treasure` value for the given item. constexpr uint16_t Uniq(_unique_items item) { return static_cast(T_UNIQ) + item; } std::vector MonsterSpritePaths; } // namespace const char *MonsterData::spritePath() const { return MonsterSpritePaths[static_cast(spriteId)].c_str(); } /** Contains the data related to each monster ID. */ std::vector MonstersData; /** * Map between .DUN file value and monster type enum */ const _monster_id MonstConvTbl[] = { MT_NZOMBIE, MT_BZOMBIE, MT_GZOMBIE, MT_YZOMBIE, MT_RFALLSP, MT_DFALLSP, MT_YFALLSP, MT_BFALLSP, MT_WSKELAX, MT_TSKELAX, MT_RSKELAX, MT_XSKELAX, MT_RFALLSD, MT_DFALLSD, MT_YFALLSD, MT_BFALLSD, MT_NSCAV, MT_BSCAV, MT_WSCAV, MT_YSCAV, MT_WSKELBW, MT_TSKELBW, MT_RSKELBW, MT_XSKELBW, MT_WSKELSD, MT_TSKELSD, MT_RSKELSD, MT_XSKELSD, MT_SNEAK, MT_STALKER, MT_UNSEEN, MT_ILLWEAV, MT_NGOATMC, MT_BGOATMC, MT_RGOATMC, MT_GGOATMC, MT_FIEND, MT_GLOOM, MT_BLINK, MT_FAMILIAR, MT_NGOATBW, MT_BGOATBW, MT_RGOATBW, MT_GGOATBW, MT_NACID, MT_RACID, MT_BACID, MT_XACID, MT_SKING, MT_FAT, MT_MUDMAN, MT_TOAD, MT_FLAYED, MT_WYRM, MT_CAVSLUG, MT_DEVOUR, MT_DVLWYRM, MT_NMAGMA, MT_YMAGMA, MT_BMAGMA, MT_WMAGMA, MT_HORNED, MT_MUDRUN, MT_FROSTC, MT_OBLORD, MT_BONEDMN, MT_REDDTH, MT_LTCHDMN, MT_UDEDBLRG, MT_INVALID, MT_INVALID, MT_INVALID, MT_INVALID, MT_INCIN, MT_FLAMLRD, MT_DOOMFIRE, MT_HELLBURN, MT_INVALID, MT_INVALID, MT_INVALID, MT_INVALID, MT_RSTORM, MT_STORM, MT_STORML, MT_MAEL, MT_WINGED, MT_GARGOYLE, MT_BLOODCLW, MT_DEATHW, MT_MEGA, MT_GUARD, MT_VTEXLRD, MT_BALROG, MT_NSNAKE, MT_RSNAKE, MT_GSNAKE, MT_BSNAKE, MT_NBLACK, MT_RTBLACK, MT_BTBLACK, MT_RBLACK, MT_UNRAV, MT_HOLOWONE, MT_PAINMSTR, MT_REALWEAV, MT_SUCCUBUS, MT_SNOWWICH, MT_HLSPWN, MT_SOLBRNR, MT_COUNSLR, MT_MAGISTR, MT_CABALIST, MT_ADVOCATE, MT_INVALID, MT_DIABLO, MT_INVALID, MT_GOLEM, MT_INVALID, MT_INVALID, MT_INVALID, // Monster from blood1.dun and blood2.dun MT_INVALID, MT_INVALID, MT_INVALID, MT_INVALID, // Snotspill from banner2.dun MT_INVALID, MT_INVALID, MT_BIGFALL, MT_DARKMAGE, MT_HELLBOAR, MT_STINGER, MT_PSYCHORB, MT_ARACHNON, MT_FELLTWIN, MT_HORKSPWN, MT_VENMTAIL, MT_NECRMORB, MT_SPIDLORD, MT_LASHWORM, MT_TORCHANT, MT_HORKDMN, MT_DEFILER, MT_GRAVEDIG, MT_TOMBRAT, MT_FIREBAT, MT_SKLWING, MT_LICH, MT_CRYPTDMN, MT_HELLBAT, MT_BONEDEMN, MT_LICH, MT_BICLOPS, MT_FLESTHNG, MT_REAPER, MT_NAKRUL, MT_CLEAVER, MT_INVILORD, MT_LRDSAYTR, }; tl::expected ParseMonsterAvailability(std::string_view value) { if (value == "Always") return MonsterAvailability::Always; if (value == "Never") return MonsterAvailability::Never; if (value == "Retail") return MonsterAvailability::Retail; return tl::make_unexpected("Expected one of: Always, Never, or Retail"); } tl::expected ParseAiId(std::string_view value) { if (value == "Zombie") return MonsterAIID::Zombie; if (value == "Fat") return MonsterAIID::Fat; if (value == "SkeletonMelee") return MonsterAIID::SkeletonMelee; if (value == "SkeletonRanged") return MonsterAIID::SkeletonRanged; if (value == "Scavenger") return MonsterAIID::Scavenger; if (value == "Rhino") return MonsterAIID::Rhino; if (value == "GoatMelee") return MonsterAIID::GoatMelee; if (value == "GoatRanged") return MonsterAIID::GoatRanged; if (value == "Fallen") return MonsterAIID::Fallen; if (value == "Magma") return MonsterAIID::Magma; if (value == "SkeletonKing") return MonsterAIID::SkeletonKing; if (value == "Bat") return MonsterAIID::Bat; if (value == "Gargoyle") return MonsterAIID::Gargoyle; if (value == "Butcher") return MonsterAIID::Butcher; if (value == "Succubus") return MonsterAIID::Succubus; if (value == "Sneak") return MonsterAIID::Sneak; if (value == "Storm") return MonsterAIID::Storm; if (value == "FireMan") return MonsterAIID::FireMan; if (value == "Gharbad") return MonsterAIID::Gharbad; if (value == "Acid") return MonsterAIID::Acid; if (value == "AcidUnique") return MonsterAIID::AcidUnique; if (value == "Golem") return MonsterAIID::Golem; if (value == "Zhar") return MonsterAIID::Zhar; if (value == "Snotspill") return MonsterAIID::Snotspill; if (value == "Snake") return MonsterAIID::Snake; if (value == "Counselor") return MonsterAIID::Counselor; if (value == "Mega") return MonsterAIID::Mega; if (value == "Diablo") return MonsterAIID::Diablo; if (value == "Lazarus") return MonsterAIID::Lazarus; if (value == "LazarusSuccubus") return MonsterAIID::LazarusSuccubus; if (value == "Lachdanan") return MonsterAIID::Lachdanan; if (value == "Warlord") return MonsterAIID::Warlord; if (value == "FireBat") return MonsterAIID::FireBat; if (value == "Torchant") return MonsterAIID::Torchant; if (value == "HorkDemon") return MonsterAIID::HorkDemon; if (value == "Lich") return MonsterAIID::Lich; if (value == "ArchLich") return MonsterAIID::ArchLich; if (value == "Psychorb") return MonsterAIID::Psychorb; if (value == "Necromorb") return MonsterAIID::Necromorb; if (value == "BoneDemon") return MonsterAIID::BoneDemon; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseMonsterFlag(std::string_view value) { if (value == "HIDDEN") return MFLAG_HIDDEN; if (value == "LOCK_ANIMATION") return MFLAG_LOCK_ANIMATION; if (value == "ALLOW_SPECIAL") return MFLAG_ALLOW_SPECIAL; if (value == "TARGETS_MONSTER") return MFLAG_TARGETS_MONSTER; if (value == "GOLEM") return MFLAG_GOLEM; if (value == "QUEST_COMPLETE") return MFLAG_QUEST_COMPLETE; if (value == "KNOCKBACK") return MFLAG_KNOCKBACK; if (value == "SEARCH") return MFLAG_SEARCH; if (value == "CAN_OPEN_DOOR") return MFLAG_CAN_OPEN_DOOR; if (value == "NO_ENEMY") return MFLAG_NO_ENEMY; if (value == "BERSERK") return MFLAG_BERSERK; if (value == "NOLIFESTEAL") return MFLAG_NOLIFESTEAL; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseMonsterClass(std::string_view value) { if (value == "Undead") return MonsterClass::Undead; if (value == "Demon") return MonsterClass::Demon; if (value == "Animal") return MonsterClass::Animal; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseMonsterResistance(std::string_view value) { if (value == "RESIST_MAGIC") return RESIST_MAGIC; if (value == "RESIST_FIRE") return RESIST_FIRE; if (value == "RESIST_LIGHTNING") return RESIST_LIGHTNING; if (value == "IMMUNE_MAGIC") return IMMUNE_MAGIC; if (value == "IMMUNE_FIRE") return IMMUNE_FIRE; if (value == "IMMUNE_LIGHTNING") return IMMUNE_LIGHTNING; if (value == "IMMUNE_ACID") return IMMUNE_ACID; return tl::make_unexpected("Unknown enum value"); } void LoadMonsterData() { const std::string_view filename = "txtdata\\monsters\\monstdat.tsv"; tl::expected dataFileResult = DataFile::load(filename); if (!dataFileResult.has_value()) { DataFile::reportFatalError(dataFileResult.error(), filename); } DataFile &dataFile = dataFileResult.value(); if (tl::expected result = dataFile.skipHeader(); !result.has_value()) { DataFile::reportFatalError(result.error(), filename); } MonstersData.clear(); std::unordered_map spritePathToId; for (DataFileRecord record : dataFile) { FieldIterator fieldIt = record.begin(); const FieldIterator endField = record.end(); MonstersData.emplace_back(); MonsterData &monster = MonstersData.back(); const auto advance = [&]() { ++fieldIt; if (fieldIt == endField) { DataFile::reportFatalError(DataFile::Error::NotEnoughColumns, filename); } }; // Skip the first column (monster ID). // name advance(); monster.name = (*fieldIt).value(); // assetsSuffix advance(); { std::string assetsSuffix { (*fieldIt).value() }; const auto [it, inserted] = spritePathToId.emplace(assetsSuffix, spritePathToId.size()); if (inserted) MonsterSpritePaths.push_back(it->first); monster.spriteId = it->second; } // soundSuffix advance(); monster.soundSuffix = (*fieldIt).value(); // trnFile advance(); monster.trnFile = (*fieldIt).value(); // availability advance(); if (tl::expected result = ParseMonsterAvailability((*fieldIt).value()); result.has_value()) { monster.availability = *std::move(result); } else { DataFile::reportFatalFieldError(DataFileField::Error::InvalidValue, filename, "availability", *fieldIt, result.error()); } // width advance(); if (tl::expected result = (*fieldIt).parseInt(monster.width); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "width", *fieldIt); } // image advance(); if (tl::expected result = (*fieldIt).parseInt(monster.image); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "image", *fieldIt); } // hasSpecial advance(); if (tl::expected result = (*fieldIt).parseBool(monster.hasSpecial); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "hasSpecial", *fieldIt); } // hasSpecialSound advance(); if (tl::expected result = (*fieldIt).parseBool(monster.hasSpecialSound); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "hasSpecialSound", *fieldIt); } // frames[6] advance(); if (tl::expected result = (*fieldIt).parseIntArray(monster.frames); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "frames", *fieldIt); } // rate[6] advance(); if (tl::expected result = (*fieldIt).parseIntArray(monster.rate); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "rate", *fieldIt); } // minDunLvl advance(); if (tl::expected result = (*fieldIt).parseInt(monster.minDunLvl); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "minDunLvl", *fieldIt); } // maxDunLvl advance(); if (tl::expected result = (*fieldIt).parseInt(monster.maxDunLvl); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "maxDunLvl", *fieldIt); } // level advance(); if (tl::expected result = (*fieldIt).parseInt(monster.level); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "level", *fieldIt); } // hitPointsMinimum advance(); if (tl::expected result = (*fieldIt).parseInt(monster.hitPointsMinimum); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "hitPointsMinimum", *fieldIt); } // hitPointsMaximum advance(); if (tl::expected result = (*fieldIt).parseInt(monster.hitPointsMaximum); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "hitPointsMaximum", *fieldIt); } // ai advance(); if (tl::expected result = ParseAiId((*fieldIt).value()); result.has_value()) { monster.ai = *std::move(result); } else { DataFile::reportFatalFieldError(DataFileField::Error::InvalidValue, filename, "ai", *fieldIt, result.error()); } // abilityFlags advance(); if (tl::expected result = (*fieldIt).parseEnumList(monster.abilityFlags, ParseMonsterFlag); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "abilityFlags", *fieldIt); } // intelligence advance(); if (tl::expected result = (*fieldIt).parseInt(monster.intelligence); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "intelligence", *fieldIt); } // toHit advance(); if (tl::expected result = (*fieldIt).parseInt(monster.toHit); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "toHit", *fieldIt); } // animFrameNum advance(); if (tl::expected result = (*fieldIt).parseInt(monster.animFrameNum); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "animFrameNum", *fieldIt); } // minDamage advance(); if (tl::expected result = (*fieldIt).parseInt(monster.minDamage); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "minDamage", *fieldIt); } // maxDamage advance(); if (tl::expected result = (*fieldIt).parseInt(monster.maxDamage); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "maxDamage", *fieldIt); } // toHitSpecial advance(); if (tl::expected result = (*fieldIt).parseInt(monster.toHitSpecial); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "toHitSpecial", *fieldIt); } // animFrameNumSpecial advance(); if (tl::expected result = (*fieldIt).parseInt(monster.animFrameNumSpecial); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "animFrameNumSpecial", *fieldIt); } // minDamageSpecial advance(); if (tl::expected result = (*fieldIt).parseInt(monster.minDamageSpecial); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "minDamageSpecial", *fieldIt); } // maxDamageSpecial advance(); if (tl::expected result = (*fieldIt).parseInt(monster.maxDamageSpecial); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "maxDamageSpecial", *fieldIt); } // armorClass advance(); if (tl::expected result = (*fieldIt).parseInt(monster.armorClass); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "armorClass", *fieldIt); } // monsterClass advance(); if (tl::expected result = ParseMonsterClass((*fieldIt).value()); result.has_value()) { monster.monsterClass = *std::move(result); } else { DataFile::reportFatalFieldError(DataFileField::Error::InvalidValue, filename, "monsterClass", *fieldIt, result.error()); } // resistance advance(); if (tl::expected result = (*fieldIt).parseEnumList(monster.resistance, ParseMonsterResistance); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "resistance", *fieldIt); } // resistanceHell advance(); if (tl::expected result = (*fieldIt).parseEnumList(monster.resistanceHell, ParseMonsterResistance); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "resistanceHell", *fieldIt); } // selectionType advance(); if (tl::expected result = (*fieldIt).parseInt(monster.selectionType); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "selectionType", *fieldIt); } // treasure // TODO: Replace this hack with proper parsing once unique monsters have been migrated. advance(); { const std::string_view value = (*fieldIt).value(); if (value.empty()) { monster.treasure = 0; } else if (value == "None") { monster.treasure = T_NODROP; } else if (value == "Uniq(SKCROWN)") { monster.treasure = Uniq(UITEM_SKCROWN); } else if (value == "Uniq(CLEAVER)") { monster.treasure = Uniq(UITEM_CLEAVER); } else { DataFile::reportFatalFieldError(DataFileField::Error::InvalidValue, filename, "treasure", *fieldIt, "NOTE: Parser is incomplete"); } } // exp advance(); if (tl::expected result = (*fieldIt).parseInt(monster.exp); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename, "exp", *fieldIt); } } } size_t GetNumMonsterSprites() { return MonsterSpritePaths.size(); } /** Contains the data related to each unique monster ID. */ const UniqueMonsterData UniqueMonstersData[] = { // clang-format off // mtype, mName, mTrnName, mlevel, mmaxhp, mAi, mint, mMinDamage, mMaxDamage, mMagicRes, monsterPack, customToHit, customArmorClass, mtalkmsg // TRANSLATORS: Unique Monster Block start { MT_NGOATMC, P_("monster", "Gharbad the Weak"), "bsdb", 4, 120, MonsterAIID::Gharbad, 3, 8, 16, IMMUNE_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_GARBUD1 }, { MT_SKING, P_("monster", "Skeleton King"), "genrl", 0, 240, MonsterAIID::SkeletonKing, 3, 6, 16, IMMUNE_MAGIC | RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Independent, 0, 0, TEXT_NONE }, { MT_COUNSLR, P_("monster", "Zhar the Mad"), "general", 8, 360, MonsterAIID::Zhar, 3, 16, 40, IMMUNE_MAGIC | RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_ZHAR1 }, { MT_BFALLSP, P_("monster", "Snotspill"), "bng", 4, 220, MonsterAIID::Snotspill, 3, 10, 18, RESIST_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_BANNER10 }, { MT_ADVOCATE, P_("monster", "Arch-Bishop Lazarus"), "general", 0, 600, MonsterAIID::Lazarus, 3, 30, 50, IMMUNE_MAGIC | RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_VILE13 }, { MT_HLSPWN, P_("monster", "Red Vex"), "redv", 0, 400, MonsterAIID::LazarusSuccubus, 3, 30, 50, IMMUNE_MAGIC | RESIST_FIRE, UniqueMonsterPack::None, 0, 0, TEXT_VILE13 }, { MT_HLSPWN, P_("monster", "Black Jade"), "blkjd", 0, 400, MonsterAIID::LazarusSuccubus, 3, 30, 50, IMMUNE_MAGIC | RESIST_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_VILE13 }, { MT_RBLACK, P_("monster", "Lachdanan"), "bhka", 14, 500, MonsterAIID::Lachdanan, 3, 0, 0, 0, UniqueMonsterPack::None, 0, 0, TEXT_VEIL9 }, { MT_BTBLACK, P_("monster", "Warlord of Blood"), "general", 13, 850, MonsterAIID::Warlord, 3, 35, 50, IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_WARLRD9 }, { MT_CLEAVER, P_("monster", "The Butcher"), "genrl", 0, 220, MonsterAIID::Butcher, 3, 6, 12, RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_HORKDMN, P_("monster", "Hork Demon"), "genrl", 19, 300, MonsterAIID::HorkDemon, 3, 20, 35, RESIST_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_DEFILER, P_("monster", "The Defiler"), "genrl", 20, 480, MonsterAIID::SkeletonMelee, 3, 30, 40, RESIST_MAGIC | RESIST_FIRE | IMMUNE_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_NAKRUL, P_("monster", "Na-Krul"), "genrl", 0, 1332, MonsterAIID::SkeletonMelee, 3, 40, 50, IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_TSKELAX, P_("monster", "Bonehead Keenaxe"), "bhka", 2, 91, MonsterAIID::SkeletonMelee, 2, 4, 10, IMMUNE_MAGIC, UniqueMonsterPack::Leashed, 100, 0, TEXT_NONE }, { MT_RFALLSD, P_("monster", "Bladeskin the Slasher"), "bsts", 2, 51, MonsterAIID::Fallen, 0, 6, 18, RESIST_FIRE, UniqueMonsterPack::Leashed, 0, 45, TEXT_NONE }, { MT_NZOMBIE, P_("monster", "Soulpus"), "general", 2, 133, MonsterAIID::Zombie, 0, 4, 8, RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_RFALLSP, P_("monster", "Pukerat the Unclean"), "ptu", 2, 77, MonsterAIID::Fallen, 3, 1, 5, RESIST_FIRE, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_WSKELAX, P_("monster", "Boneripper"), "br", 2, 54, MonsterAIID::Bat, 0, 6, 15, IMMUNE_MAGIC | IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_NZOMBIE, P_("monster", "Rotfeast the Hungry"), "eth", 2, 85, MonsterAIID::SkeletonMelee, 3, 4, 12, IMMUNE_MAGIC, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_DFALLSD, P_("monster", "Gutshank the Quick"), "gtq", 3, 66, MonsterAIID::Bat, 2, 6, 16, RESIST_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_TSKELSD, P_("monster", "Brokenhead Bangshield"), "bhbs", 3, 108, MonsterAIID::SkeletonMelee, 3, 12, 20, IMMUNE_MAGIC | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_YFALLSP, P_("monster", "Bongo"), "bng", 3, 178, MonsterAIID::Fallen, 3, 9, 21, 0, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_BZOMBIE, P_("monster", "Rotcarnage"), "rcrn", 3, 102, MonsterAIID::Zombie, 3, 9, 24, IMMUNE_MAGIC | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 45, TEXT_NONE }, { MT_NSCAV, P_("monster", "Shadowbite"), "shbt", 2, 60, MonsterAIID::SkeletonMelee, 3, 3, 20, IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_WSKELBW, P_("monster", "Deadeye"), "de", 2, 49, MonsterAIID::GoatRanged, 0, 6, 9, IMMUNE_MAGIC | RESIST_FIRE, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_RSKELAX, P_("monster", "Madeye the Dead"), "mtd", 4, 75, MonsterAIID::Bat, 0, 9, 21, IMMUNE_MAGIC | IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 30, TEXT_NONE }, { MT_BSCAV, P_("monster", "El Chupacabras"), "general", 3, 120, MonsterAIID::GoatMelee, 0, 10, 18, RESIST_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_TSKELBW, P_("monster", "Skullfire"), "skfr", 3, 125, MonsterAIID::GoatRanged, 1, 6, 10, IMMUNE_FIRE, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_SNEAK, P_("monster", "Warpskull"), "tspo", 3, 117, MonsterAIID::Sneak, 2, 6, 18, RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_GZOMBIE, P_("monster", "Goretongue"), "pmr", 3, 156, MonsterAIID::SkeletonMelee, 1, 15, 30, IMMUNE_MAGIC, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_WSCAV, P_("monster", "Pulsecrawler"), "bhka", 4, 150, MonsterAIID::Scavenger, 0, 16, 20, IMMUNE_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 45, TEXT_NONE }, { MT_BLINK, P_("monster", "Moonbender"), "general", 4, 135, MonsterAIID::Bat, 0, 9, 27, IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_BLINK, P_("monster", "Wrathraven"), "general", 5, 135, MonsterAIID::Bat, 2, 9, 22, IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_YSCAV, P_("monster", "Spineeater"), "general", 4, 180, MonsterAIID::Scavenger, 1, 18, 25, IMMUNE_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_RSKELBW, P_("monster", "Blackash the Burning"), "bashtb", 4, 120, MonsterAIID::GoatRanged, 0, 6, 16, IMMUNE_MAGIC | IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_BFALLSD, P_("monster", "Shadowcrow"), "general", 5, 270, MonsterAIID::Sneak, 2, 12, 25, 0, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_LRDSAYTR, P_("monster", "Blightstone the Weak"), "bhka", 4, 360, MonsterAIID::SkeletonMelee, 0, 4, 12, IMMUNE_MAGIC | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 70, 0, TEXT_NONE }, { MT_FAT, P_("monster", "Bilefroth the Pit Master"), "bftp", 6, 210, MonsterAIID::Bat, 1, 16, 23, IMMUNE_MAGIC | IMMUNE_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_NGOATBW, P_("monster", "Bloodskin Darkbow"), "bsdb", 5, 207, MonsterAIID::GoatRanged, 0, 3, 16, RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 55, TEXT_NONE }, { MT_GLOOM, P_("monster", "Foulwing"), "db", 5, 246, MonsterAIID::Rhino, 3, 12, 28, RESIST_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_XSKELSD, P_("monster", "Shadowdrinker"), "shdr", 5, 300, MonsterAIID::Sneak, 1, 18, 26, IMMUNE_MAGIC | RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::None, 0, 45, TEXT_NONE }, { MT_UNSEEN, P_("monster", "Hazeshifter"), "bhka", 5, 285, MonsterAIID::Sneak, 3, 18, 30, IMMUNE_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_NACID, P_("monster", "Deathspit"), "bfds", 6, 303, MonsterAIID::AcidUnique, 0, 12, 32, RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_RGOATMC, P_("monster", "Bloodgutter"), "bgbl", 6, 315, MonsterAIID::Bat, 1, 24, 34, IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_BGOATMC, P_("monster", "Deathshade Fleshmaul"), "dsfm", 6, 276, MonsterAIID::Rhino, 0, 12, 24, IMMUNE_MAGIC | RESIST_FIRE, UniqueMonsterPack::None, 0, 65, TEXT_NONE }, { MT_WYRM, P_("monster", "Warmaggot the Mad"), "general", 6, 246, MonsterAIID::Bat, 3, 15, 30, RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_STORM, P_("monster", "Glasskull the Jagged"), "bhka", 7, 354, MonsterAIID::Storm, 0, 18, 30, IMMUNE_MAGIC | IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_RGOATBW, P_("monster", "Blightfire"), "blf", 7, 321, MonsterAIID::Succubus, 2, 13, 21, IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_GARGOYLE, P_("monster", "Nightwing the Cold"), "general", 7, 342, MonsterAIID::Bat, 1, 18, 26, IMMUNE_MAGIC | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_GGOATBW, P_("monster", "Gorestone"), "general", 7, 303, MonsterAIID::GoatRanged, 1, 15, 28, RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 70, 0, TEXT_NONE }, { MT_BMAGMA, P_("monster", "Bronzefist Firestone"), "general", 8, 360, MonsterAIID::Magma, 0, 30, 36, IMMUNE_MAGIC | RESIST_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_INCIN, P_("monster", "Wrathfire the Doomed"), "wftd", 8, 270, MonsterAIID::SkeletonMelee, 2, 20, 30, IMMUNE_MAGIC | RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_NMAGMA, P_("monster", "Firewound the Grim"), "bhka", 8, 303, MonsterAIID::Magma, 0, 18, 22, IMMUNE_MAGIC | RESIST_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_MUDMAN, P_("monster", "Baron Sludge"), "bsm", 8, 315, MonsterAIID::Sneak, 3, 25, 34, IMMUNE_MAGIC | RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 75, TEXT_NONE }, { MT_GGOATMC, P_("monster", "Blighthorn Steelmace"), "bhsm", 7, 250, MonsterAIID::Rhino, 0, 20, 28, RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 45, TEXT_NONE }, { MT_RACID, P_("monster", "Chaoshowler"), "general", 8, 240, MonsterAIID::AcidUnique, 0, 12, 20, 0, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_REDDTH, P_("monster", "Doomgrin the Rotting"), "general", 8, 405, MonsterAIID::Storm, 3, 25, 50, IMMUNE_MAGIC | RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_FLAMLRD, P_("monster", "Madburner"), "general", 9, 270, MonsterAIID::Storm, 0, 20, 40, IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_LTCHDMN, P_("monster", "Bonesaw the Litch"), "general", 9, 495, MonsterAIID::Storm, 2, 30, 55, IMMUNE_MAGIC | RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_MUDRUN, P_("monster", "Breakspine"), "general", 9, 351, MonsterAIID::Rhino, 0, 25, 34, RESIST_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_REDDTH, P_("monster", "Devilskull Sharpbone"), "general", 9, 444, MonsterAIID::Storm, 1, 25, 40, IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_STORM, P_("monster", "Brokenstorm"), "general", 9, 411, MonsterAIID::Storm, 2, 25, 36, IMMUNE_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_RSTORM, P_("monster", "Stormbane"), "general", 9, 555, MonsterAIID::Storm, 3, 30, 30, IMMUNE_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_TOAD, P_("monster", "Oozedrool"), "general", 9, 483, MonsterAIID::Fat, 3, 25, 30, RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_BLOODCLW, P_("monster", "Goldblight of the Flame"), "general", 10, 405, MonsterAIID::Gargoyle, 0, 15, 35, IMMUNE_MAGIC | IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 80, TEXT_NONE }, { MT_OBLORD, P_("monster", "Blackstorm"), "general", 10, 525, MonsterAIID::Rhino, 3, 20, 40, IMMUNE_MAGIC | IMMUNE_LIGHTNING, UniqueMonsterPack::Leashed, 0, 90, TEXT_NONE }, { MT_RACID, P_("monster", "Plaguewrath"), "general", 10, 450, MonsterAIID::AcidUnique, 2, 20, 30, IMMUNE_MAGIC | RESIST_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_RSTORM, P_("monster", "The Flayer"), "general", 10, 501, MonsterAIID::Storm, 1, 20, 35, RESIST_MAGIC | RESIST_FIRE | IMMUNE_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_FROSTC, P_("monster", "Bluehorn"), "general", 11, 477, MonsterAIID::Rhino, 1, 25, 30, IMMUNE_MAGIC | RESIST_FIRE, UniqueMonsterPack::Leashed, 0, 90, TEXT_NONE }, { MT_HELLBURN, P_("monster", "Warpfire Hellspawn"), "general", 11, 525, MonsterAIID::FireMan, 3, 10, 40, RESIST_MAGIC | IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_NSNAKE, P_("monster", "Fangspeir"), "general", 11, 444, MonsterAIID::SkeletonMelee, 1, 15, 32, IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_UDEDBLRG, P_("monster", "Festerskull"), "general", 11, 600, MonsterAIID::Storm, 2, 15, 30, IMMUNE_MAGIC, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_NBLACK, P_("monster", "Lionskull the Bent"), "general", 12, 525, MonsterAIID::SkeletonMelee, 2, 25, 25, IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_COUNSLR, P_("monster", "Blacktongue"), "general", 12, 360, MonsterAIID::Counselor, 3, 15, 30, RESIST_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_DEATHW, P_("monster", "Viletouch"), "general", 12, 525, MonsterAIID::Gargoyle, 3, 20, 40, IMMUNE_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_RSNAKE, P_("monster", "Viperflame"), "general", 12, 570, MonsterAIID::SkeletonMelee, 1, 25, 35, IMMUNE_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_BSNAKE, P_("monster", "Fangskin"), "bhka", 14, 681, MonsterAIID::SkeletonMelee, 2, 15, 50, IMMUNE_MAGIC | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_SUCCUBUS, P_("monster", "Witchfire the Unholy"), "general", 12, 444, MonsterAIID::Succubus, 3, 10, 20, IMMUNE_MAGIC | IMMUNE_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_BALROG, P_("monster", "Blackskull"), "bhka", 13, 750, MonsterAIID::SkeletonMelee, 3, 25, 40, IMMUNE_MAGIC | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_UNRAV, P_("monster", "Soulslash"), "general", 12, 450, MonsterAIID::SkeletonMelee, 0, 25, 25, IMMUNE_MAGIC, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_VTEXLRD, P_("monster", "Windspawn"), "general", 12, 711, MonsterAIID::SkeletonMelee, 1, 35, 40, IMMUNE_MAGIC | IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_GSNAKE, P_("monster", "Lord of the Pit"), "general", 13, 762, MonsterAIID::SkeletonMelee, 2, 25, 42, RESIST_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_RTBLACK, P_("monster", "Rustweaver"), "general", 13, 400, MonsterAIID::SkeletonMelee, 3, 1, 60, IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_HOLOWONE, P_("monster", "Howlingire the Shade"), "general", 13, 450, MonsterAIID::SkeletonMelee, 2, 40, 75, RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_MAEL, P_("monster", "Doomcloud"), "general", 13, 612, MonsterAIID::Storm, 1, 1, 60, RESIST_FIRE | IMMUNE_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_PAINMSTR, P_("monster", "Bloodmoon Soulfire"), "general", 13, 684, MonsterAIID::SkeletonMelee, 1, 15, 40, IMMUNE_MAGIC | RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_SNOWWICH, P_("monster", "Witchmoon"), "general", 13, 310, MonsterAIID::Succubus, 3, 30, 40, RESIST_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_VTEXLRD, P_("monster", "Gorefeast"), "general", 13, 771, MonsterAIID::SkeletonMelee, 3, 20, 55, RESIST_FIRE, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_RTBLACK, P_("monster", "Graywar the Slayer"), "general", 14, 672, MonsterAIID::SkeletonMelee, 1, 30, 50, RESIST_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_MAGISTR, P_("monster", "Dreadjudge"), "general", 14, 540, MonsterAIID::Counselor, 1, 30, 40, IMMUNE_MAGIC | RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_HLSPWN, P_("monster", "Stareye the Witch"), "general", 14, 726, MonsterAIID::Succubus, 2, 30, 50, IMMUNE_FIRE, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_BTBLACK, P_("monster", "Steelskull the Hunter"), "general", 14, 831, MonsterAIID::SkeletonMelee, 3, 40, 50, RESIST_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_RBLACK, P_("monster", "Sir Gorash"), "general", 16, 1050, MonsterAIID::SkeletonMelee, 1, 20, 60, 0, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_CABALIST, P_("monster", "The Vizier"), "general", 15, 850, MonsterAIID::Counselor, 2, 25, 40, IMMUNE_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_REALWEAV, P_("monster", "Zamphir"), "general", 15, 891, MonsterAIID::SkeletonMelee, 2, 30, 50, IMMUNE_MAGIC | RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_HLSPWN, P_("monster", "Bloodlust"), "general", 15, 825, MonsterAIID::Succubus, 1, 20, 55, IMMUNE_MAGIC | IMMUNE_LIGHTNING, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_HLSPWN, P_("monster", "Webwidow"), "general", 16, 774, MonsterAIID::Succubus, 1, 20, 50, IMMUNE_MAGIC | IMMUNE_FIRE, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_SOLBRNR, P_("monster", "Fleshdancer"), "general", 16, 999, MonsterAIID::Succubus, 3, 30, 50, IMMUNE_MAGIC | RESIST_FIRE, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, { MT_OBLORD, P_("monster", "Grimspike"), "general", 19, 534, MonsterAIID::Sneak, 1, 25, 40, IMMUNE_MAGIC | RESIST_FIRE, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, // TRANSLATORS: Unique Monster Block end { MT_STORML, P_("monster", "Doomlock"), "general", 28, 534, MonsterAIID::Sneak, 1, 35, 55, IMMUNE_MAGIC | RESIST_FIRE | RESIST_LIGHTNING, UniqueMonsterPack::Leashed, 0, 0, TEXT_NONE }, { MT_INVALID, nullptr, nullptr, 0, 0, MonsterAIID::Invalid, 0, 0, 0, 0, UniqueMonsterPack::None, 0, 0, TEXT_NONE }, // clang-format on }; } // namespace devilution