diff --git a/src/Bin/CodeGen/CMakeLists.txt b/src/Bin/CodeGen/CMakeLists.txt index c70a359040f7..9b107493a943 100644 --- a/src/Bin/CodeGen/CMakeLists.txt +++ b/src/Bin/CodeGen/CMakeLists.txt @@ -8,6 +8,7 @@ set(BIN_CODEGEN_SOURCES set(BIN_CODEGEN_HEADERS CodeGenOptions.h CodeGenEnums.h + CodeGenFunctions.h CodeGenMap.h) if(NOT BUILD_PLATFORM STREQUAL "android") diff --git a/src/Bin/CodeGen/CodeGen.cpp b/src/Bin/CodeGen/CodeGen.cpp index 59d30a28d321..6e74575985fc 100644 --- a/src/Bin/CodeGen/CodeGen.cpp +++ b/src/Bin/CodeGen/CodeGen.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -14,7 +15,10 @@ #include "Engine/GameResourceManager.h" #include "Engine/MapInfo.h" +#include "GUI/UI/Houses/TownHall.h" + #include "Library/Lod/LodReader.h" +#include "Library/Random/Random.h" #include "Library/Serialization/EnumSerialization.h" #include "Utility/Format.h" @@ -30,19 +34,6 @@ static auto contains = [](const std::string &haystack, const std::string &needle return haystack.find(needle) != std::string::npos; }; -static std::string toUpperCaseEnum(const std::string &string) { - std::string result; - for (char c : trim(string)) { - if (isalnum(c)) { - result += static_cast(toupper(c)); - } else if (isspace(c) || c == '/' || c == '-') { - if (!result.ends_with('_')) - result += '_'; - } - } - return result; -} - int runItemIdCodeGen(CodeGenOptions options, GameResourceManager *resourceManager) { ItemTable itemTable; itemTable.Initialize(resourceManager); @@ -256,9 +247,26 @@ int runHouseIdCodeGen(CodeGenOptions options, GameResourceManager *resourceManag return 0; } -std::string cleanupMonsterEnumName(std::string enumName) { - if (enumName.starts_with("ZBLASTERGUY") || enumName.starts_with("ZULTRA_DRAGON")) - enumName = enumName.substr(1); +MonsterStats loadMonsterStats(GameResourceManager *resourceManager) { + TriBlob dmonBlobs; + dmonBlobs.mm7 = resourceManager->getEventsFile("dmonlist.bin"); + + pMonsterList = new MonsterList; + deserialize(dmonBlobs, pMonsterList); + + MonsterStats result; + result.Initialize(resourceManager->getEventsFile("monsters.txt")); + return result; +} + +std::string cleanupMonsterIdEnumName(std::string enumName) { + for (const char *prefix : {"ZBLASTERGUY", "ZULTRA_DRAGON", }) + if (enumName.starts_with(prefix)) + enumName = enumName.substr(1); + + for (const char *prefix : {"ZCAT", "ZCHICKEN", "ZDOG", "ZRAT"}) + if (enumName.starts_with(prefix)) + enumName = "UNUSED_" + enumName.substr(1); enumName = replaceAll(enumName, "MALEA", "MALE_A"); enumName = replaceAll(enumName, "MALEB", "MALE_B"); @@ -268,23 +276,17 @@ std::string cleanupMonsterEnumName(std::string enumName) { } int runMonsterIdCodeGen(CodeGenOptions options, GameResourceManager *resourceManager) { - TriBlob dmonBlobs; - dmonBlobs.mm7 = resourceManager->getEventsFile("dmonlist.bin"); - - pMonsterList = new MonsterList; - deserialize(dmonBlobs, pMonsterList); - - MonsterStats monsterStats; - monsterStats.Initialize(resourceManager->getEventsFile("monsters.txt")); + MonsterStats monsterStats = loadMonsterStats(resourceManager); CodeGenMap map; map.insert(MONSTER_INVALID, "INVALID", ""); - for (const MonsterId i : monsterStats.pInfos.indices()) { - const MonsterInfo &desc = monsterStats.pInfos[i]; - std::string enumName = cleanupMonsterEnumName(toUpperCaseEnum(desc.pPictureName)); + for (const MonsterId i : allMonsters()) { + const MonsterDesc &desc = pMonsterList->pMonsters[i]; + const MonsterInfo &info = monsterStats.pInfos[i]; + std::string enumName = cleanupMonsterIdEnumName(toUpperCaseEnum(desc.pMonsterName)); - std::string comment = desc.pName; + std::string comment = info.pName; if (comment == "peasant") comment = "Peasant"; if (!comment.empty()) @@ -297,32 +299,31 @@ int runMonsterIdCodeGen(CodeGenOptions options, GameResourceManager *resourceMan return 0; } -int runMonsterTypeCodeGen(CodeGenOptions options, GameResourceManager *resourceManager) { - TriBlob dmonBlobs; - dmonBlobs.mm7 = resourceManager->getEventsFile("dmonlist.bin"); +std::string cleanupMonsterTypeEnumName(std::string enumName) { + enumName = cleanupMonsterIdEnumName(enumName); - pMonsterList = new MonsterList; - deserialize(dmonBlobs, pMonsterList); + if (enumName.ends_with("_A")) { + enumName.resize(enumName.size() - 2); + } else if (!enumName.empty()) { + throw Exception("Invalid monster id name"); + } + + return enumName; +} - MonsterStats monsterStats; - monsterStats.Initialize(resourceManager->getEventsFile("monsters.txt")); +int runMonsterTypeCodeGen(CodeGenOptions options, GameResourceManager *resourceManager) { + MonsterStats monsterStats = loadMonsterStats(resourceManager); CodeGenMap map; map.insert(MONSTER_TYPE_INVALID, "INVALID", ""); int counter = 0; - for (const MonsterId i : monsterStats.pInfos.indices()) { + for (const MonsterId i : allMonsters()) { if (++counter % 3 != 1) continue; - const MonsterInfo &desc = monsterStats.pInfos[i]; - std::string enumName = cleanupMonsterEnumName(toUpperCaseEnum(desc.pPictureName)); - - if (enumName.ends_with("_A")) { - enumName.resize(enumName.size() - 2); - } else if (!enumName.empty()) { - throw Exception("Invalid monster id name"); - } + const MonsterDesc &desc = pMonsterList->pMonsters[i]; + std::string enumName = cleanupMonsterTypeEnumName(toUpperCaseEnum(desc.pMonsterName)); map.insert(monsterTypeForMonsterId(i), enumName, ""); } @@ -331,6 +332,57 @@ int runMonsterTypeCodeGen(CodeGenOptions options, GameResourceManager *resourceM return 0; } +int runBountyHuntCodeGen(CodeGenOptions options, GameResourceManager *resourceManager) { + // Fill bounty hunt map. + grng = RandomEngine::create(RANDOM_ENGINE_SEQUENTIAL); + IndexedArray, HOUSE_FIRST_TOWN_HALL, HOUSE_LAST_TOWN_HALL> monstersByTownHall; + for (const HouseId townHall : allTownhallHouses()) { + grng->seed(0); + while(true) { + MonsterId monsterId = GUIWindow_TownHall::randomMonsterForHunting(townHall); + if (!monstersByTownHall[townHall].insert(monsterId).second) + break; + } + } + + // Invert the map. + std::unordered_map> townHallsByMonster; + for (const HouseId townHall : allTownhallHouses()) + for (const MonsterId monsterId : monstersByTownHall[townHall]) + townHallsByMonster[monsterId].insert(townHall); + + // Reduce the map to monster types & check that it's actually reducible. + std::unordered_map> townHallsByMonsterType; + for (const MonsterId monsterId : allMonsters()) { + MonsterType monsterType = monsterTypeForMonsterId(monsterId); + if (townHallsByMonsterType.contains(monsterType) && townHallsByMonsterType[monsterType] != townHallsByMonster[monsterId]) + throw Exception("Invalid bounty hunt record"); + + townHallsByMonsterType[monsterType] = townHallsByMonster[monsterId]; + } + + // Prepare output table. + std::vector> table; + for (const MonsterType monsterType : allMonsterTypes()) { + auto &line = table.emplace_back(); + line[0] = fmt::format("{{{}, ", toString(monsterType)); + line[1] = "{"; + for (const HouseId townHall : townHallsByMonsterType[monsterType]) + line[2 + std::to_underlying(townHall) - std::to_underlying(HOUSE_FIRST_TOWN_HALL)] = toString(townHall) + ", "; + for (size_t i = 6; i >= 2; i--) { + if (!line[i].empty()) { + line[i].resize(line[i].size() - 2); // Drop the last ", ". + break; + } + } + line[7] = "}},"; + } + + // Dump! + dumpAligned(stdout, " ", table); + return 0; +} + int platformMain(int argc, char **argv) { try { CodeGenOptions options = CodeGenOptions::parse(argc, argv); @@ -349,6 +401,7 @@ int platformMain(int argc, char **argv) { case CodeGenOptions::SUBCOMMAND_HOUSE_ID: return runHouseIdCodeGen(std::move(options), &resourceManager); case CodeGenOptions::SUBCOMMAND_MONSTER_ID: return runMonsterIdCodeGen(std::move(options), &resourceManager); case CodeGenOptions::SUBCOMMAND_MONSTER_TYPE: return runMonsterTypeCodeGen(std::move(options), &resourceManager); + case CodeGenOptions::SUBCOMMAND_BOUNTY_HUNT: return runBountyHuntCodeGen(std::move(options), &resourceManager); default: assert(false); return 1; diff --git a/src/Bin/CodeGen/CodeGenEnums.cpp b/src/Bin/CodeGen/CodeGenEnums.cpp index e760407bab09..7463b3412125 100644 --- a/src/Bin/CodeGen/CodeGenEnums.cpp +++ b/src/Bin/CodeGen/CodeGenEnums.cpp @@ -39,3 +39,6 @@ MM_DEFINE_ENUM_SERIALIZATION_FUNCTIONS(BuildingType, CASE_INSENSITIVE, { {BUILDING_SHADOW_GUILD, "SHADOW_GUILD"}, {BUILDING_ADVENTURERS_INN, "ADVENTURERS_INN"}, }) + +MM_DEFINE_ENUM_MAGIC_SERIALIZATION_FUNCTIONS(HouseId) +MM_DEFINE_ENUM_MAGIC_SERIALIZATION_FUNCTIONS(MonsterType) diff --git a/src/Bin/CodeGen/CodeGenEnums.h b/src/Bin/CodeGen/CodeGenEnums.h index 50180bb9dfe4..6f7f7db4292e 100644 --- a/src/Bin/CodeGen/CodeGenEnums.h +++ b/src/Bin/CodeGen/CodeGenEnums.h @@ -1,7 +1,10 @@ #pragma once #include "Engine/Tables/BuildingTable.h" +#include "Engine/Objects/MonsterEnums.h" #include "Library/Serialization/SerializationFwd.h" MM_DECLARE_SERIALIZATION_FUNCTIONS(BuildingType) +MM_DECLARE_SERIALIZATION_FUNCTIONS(HouseId) +MM_DECLARE_SERIALIZATION_FUNCTIONS(MonsterType) diff --git a/src/Bin/CodeGen/CodeGenFunctions.h b/src/Bin/CodeGen/CodeGenFunctions.h new file mode 100644 index 000000000000..d7e06cbc7e37 --- /dev/null +++ b/src/Bin/CodeGen/CodeGenFunctions.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "Utility/Format.h" + +inline std::string toUpperCaseEnum(const std::string &string) { + std::string result; + for (char c : trim(string)) { + if (std::isalnum(c)) { + result += static_cast(toupper(c)); + } else if (std::isspace(c) || c == '/' || c == '-') { + if (!result.ends_with('_')) + result += '_'; + } + } + return result; +} + +template +void dumpAligned(FILE *file, std::string_view prefix, const std::vector> &table) { + std::array maxLengths; + maxLengths.fill(0); + + for (const auto &line : table) + for (size_t i = 0; i < line.size(); i++) + maxLengths[i] = std::max(maxLengths[i], line[i].size()); + + for (const auto &line : table) { + std::string output = std::string(prefix); + for (size_t i = 0; i < line.size(); i++) { + output += line[i]; + output += std::string(maxLengths[i] - line[i].size(), ' '); + } + while (output.ends_with(' ')) // Note that we don't trim front. + output.pop_back(); + fmt::println(file, "{}", output); + } +} diff --git a/src/Bin/CodeGen/CodeGenMap.h b/src/Bin/CodeGen/CodeGenMap.h index d3149e993f67..323483b88723 100644 --- a/src/Bin/CodeGen/CodeGenMap.h +++ b/src/Bin/CodeGen/CodeGenMap.h @@ -11,6 +11,8 @@ #include "Utility/Workaround/ToUnderlying.h" #include "Utility/Format.h" +#include "CodeGenFunctions.h" + class CodeGenMap { public: template @@ -40,25 +42,16 @@ class CodeGenMap { } void dump(FILE *file, const std::string &prefix) { - std::vector> linesAndComments; + std::vector> linesAndComments; for (const auto &[value, name] : _nameByValue) { std::string comment = _commentByValue[value]; if (!comment.empty()) - comment = " // " + comment; + comment = "// " + comment; - linesAndComments.emplace_back(fmt::format("{}{} = {}", prefix, name, value), comment); + linesAndComments.push_back({fmt::format("{}{} = {}, ", prefix, name, value), comment}); } - size_t maxLineLen = 0; - for (const auto &[line, _] : linesAndComments) - maxLineLen = std::max(maxLineLen, line.size()); - - for (const auto &[line, comment] : linesAndComments) { - std::string padding; - if (!comment.empty()) - padding = std::string(maxLineLen - line.size(), ' '); - fmt::println(file, " {},{}{}", line, padding, comment); - } + dumpAligned(file, " ", linesAndComments); } private: diff --git a/src/Bin/CodeGen/CodeGenOptions.cpp b/src/Bin/CodeGen/CodeGenOptions.cpp index e7dcabfe9720..54105cd8bc78 100644 --- a/src/Bin/CodeGen/CodeGenOptions.cpp +++ b/src/Bin/CodeGen/CodeGenOptions.cpp @@ -36,16 +36,21 @@ CodeGenOptions CodeGenOptions::parse(int argc, char **argv) { CLI::App *monsterTypes = app->add_subcommand("monster_types", "Generate monster types enum")->fallthrough(); monsterTypes->callback([&] { result.subcommand = SUBCOMMAND_MONSTER_TYPE; }); + CLI::App *bountyHunt = app->add_subcommand("bounty_hunt", "Generate monster type / town hall table for bounty hunts")->fallthrough(); + bountyHunt->callback([&] { result.subcommand = SUBCOMMAND_BOUNTY_HUNT; }); + try { app->parse(argc, argv); } catch (const CLI::ParseError &e) { + // TODO(captainurist): this is getting out of hand. bool isHelp = app->get_help_ptr()->as() || items->get_help_ptr()->as() || maps->get_help_ptr()->as() || beacons->get_help_ptr()->as() || monsters->get_help_ptr()->as() || - monsterTypes->get_help_ptr()->as(); + monsterTypes->get_help_ptr()->as() || + bountyHunt->get_help_ptr()->as(); if (isHelp) { app->exit(e); result.helpPrinted = true; diff --git a/src/Bin/CodeGen/CodeGenOptions.h b/src/Bin/CodeGen/CodeGenOptions.h index 00bf8f191432..bf172725c481 100644 --- a/src/Bin/CodeGen/CodeGenOptions.h +++ b/src/Bin/CodeGen/CodeGenOptions.h @@ -11,6 +11,7 @@ struct CodeGenOptions : GameStarterOptions { SUBCOMMAND_HOUSE_ID, SUBCOMMAND_MONSTER_ID, SUBCOMMAND_MONSTER_TYPE, + SUBCOMMAND_BOUNTY_HUNT, }; using enum Subcommand; diff --git a/src/Engine/Engine.cpp b/src/Engine/Engine.cpp index 3768c251c427..ef3cc6e1b01d 100644 --- a/src/Engine/Engine.cpp +++ b/src/Engine/Engine.cpp @@ -997,10 +997,7 @@ void Engine::_461103_load_level_sub() { //{ // v3 = pActors[i].pMonsterInfo.uID; v17 = 0; - if (pActors[i].monsterInfo.uID >= MONSTER_PEASANT_DWARF_FEMALE_A_A && - pActors[i].monsterInfo.uID <= MONSTER_PEASANT_HUMAN2_FEMALE_C_C || - pActors[i].monsterInfo.uID >= MONSTER_PEASANT_GOBLIN_FEMALE_A_A && - pActors[i].monsterInfo.uID <= MONSTER_PEASANT_GOBLIN_MALE_C_C) + if (isPeasant(pActors[i].monsterInfo.uID)) v17 = 1; // v1 = 0; v4 = (std::to_underlying(pActors[i].monsterInfo.uID) - 1) % 3; // TODO(captainurist): encapsulate monster tier calculation. diff --git a/src/Engine/Graphics/Outdoor.cpp b/src/Engine/Graphics/Outdoor.cpp index 83db382f3b7f..8daece0449d0 100644 --- a/src/Engine/Graphics/Outdoor.cpp +++ b/src/Engine/Graphics/Outdoor.cpp @@ -2317,7 +2317,7 @@ void UpdateActors_ODM() { pActors[Actor_ITR].aiState == Summoned || !pActors[Actor_ITR].moveSpeed) continue; - bool Water_Walk = MonsterStats::BelongsToSupertype(pActors[Actor_ITR].monsterInfo.uID, MONSTER_SUPERTYPE_WATER_ELEMENTAL); + bool Water_Walk = supertypeForMonsterId(pActors[Actor_ITR].monsterInfo.uID) == MONSTER_SUPERTYPE_WATER_ELEMENTAL; pActors[Actor_ITR].sectorId = 0; diff --git a/src/Engine/Graphics/Vis.cpp b/src/Engine/Graphics/Vis.cpp index 63927359ce60..01260dae453b 100644 --- a/src/Engine/Graphics/Vis.cpp +++ b/src/Engine/Graphics/Vis.cpp @@ -791,7 +791,7 @@ bool Vis::isBillboardPartOfSelection(int billboardId, Vis_SelectionFilter *filte return false; auto only_target_undead = filter->select_flags & TargetUndead; - auto target_not_undead = MonsterStats::BelongsToSupertype(pActors[object_idx].monsterInfo.uID, MONSTER_SUPERTYPE_UNDEAD) == 0; + auto target_not_undead = supertypeForMonsterId(pActors[object_idx].monsterInfo.uID) != MONSTER_SUPERTYPE_UNDEAD; if (only_target_undead && target_not_undead) return false; diff --git a/src/Engine/Objects/Actor.cpp b/src/Engine/Objects/Actor.cpp index 3b4f77aebe2a..ddaeb9849a0b 100644 --- a/src/Engine/Objects/Actor.cpp +++ b/src/Engine/Objects/Actor.cpp @@ -1611,8 +1611,7 @@ void Actor::AI_RandomMove(unsigned int uActor_id, Pid uTarget_id, absx = absy + (absx / 2); else absx = absx + absy / 2; - if (MonsterStats::BelongsToSupertype(pActors[uActor_id].monsterInfo.uID, - MONSTER_SUPERTYPE_TREANT)) { + if (supertypeForMonsterId(pActors[uActor_id].monsterInfo.uID) == MONSTER_SUPERTYPE_TREANT) { if (!uActionLength) uActionLength = 256; Actor::AI_StandOrBored(uActor_id, Pid(OBJECT_Character, 0), uActionLength, &doNotInitializeBecauseShouldBeRandom); @@ -1842,37 +1841,7 @@ void Actor::Die(unsigned int uActorID) { ItemGen drop; drop.Reset(); - switch (actor->monsterInfo.uID) { - case MONSTER_HARPY_A: - case MONSTER_HARPY_B: - case MONSTER_HARPY_C: - drop.uItemID = ITEM_REAGENT_HARPY_FEATHER; - break; - - case MONSTER_OOZE_A: - case MONSTER_OOZE_B: - case MONSTER_OOZE_C: - drop.uItemID = ITEM_REAGENT_VIAL_OF_OOZE_ENDOPLASM; - break; - - case MONSTER_TROLL_A: - case MONSTER_TROLL_B: - case MONSTER_TROLL_C: - drop.uItemID = ITEM_REAGENT_VIAL_OF_TROLL_BLOOD; - break; - - case MONSTER_DEVIL_A: - case MONSTER_DEVIL_B: - case MONSTER_DEVIL_C: - drop.uItemID = ITEM_REAGENT_VIAL_OF_DEVIL_ICHOR; - break; - - case MONSTER_DRAGON_A: - case MONSTER_DRAGON_B: - case MONSTER_DRAGON_C: - drop.uItemID = ITEM_REAGENT_DRAGONS_EYE; - break; - } + drop.uItemID = itemDropForMonsterType(monsterTypeForMonsterId(actor->monsterInfo.uID)); if (grng->random(100) < 20 && drop.uItemID != ITEM_NULL) { SpriteObject::dropItemAt(pItemTable->pItems[drop.uItemID].uSpriteID, @@ -1935,8 +1904,7 @@ void Actor::AI_Pursue1(unsigned int uActorID, Pid a2, signed int arg0, } else { v10 = pDir; } - if (MonsterStats::BelongsToSupertype(v7->monsterInfo.uID, - MONSTER_SUPERTYPE_TREANT)) { + if (supertypeForMonsterId(v7->monsterInfo.uID) == MONSTER_SUPERTYPE_TREANT) { if (!uActionLength) uActionLength = 256; Actor::AI_StandOrBored(uActorID, Pid::character(0), uActionLength, v10); return; @@ -1985,8 +1953,7 @@ void Actor::AI_Flee(unsigned int uActorID, Pid sTargetPid, } Actor::GetDirectionInfo(v7, Pid::character(0), &v10, 0); v13 = &v10; - if (MonsterStats::BelongsToSupertype(v5->monsterInfo.uID, - MONSTER_SUPERTYPE_TREANT) || + if (supertypeForMonsterId(v5->monsterInfo.uID) == MONSTER_SUPERTYPE_TREANT || sTargetPid.type() == OBJECT_Actor && v13->uDistance < 307.2) { if (!uActionLength) uActionLength = 256; Actor::AI_StandOrBored(uActorID, Pid::character(0), uActionLength, v13); @@ -2037,8 +2004,7 @@ void Actor::AI_Pursue2(unsigned int uActorID, Pid a2, Actor::GetDirectionInfo(v8, a2, &a3, v6); v10 = &a3; } - if (MonsterStats::BelongsToSupertype(v7->monsterInfo.uID, - MONSTER_SUPERTYPE_TREANT)) { + if (supertypeForMonsterId(v7->monsterInfo.uID) == MONSTER_SUPERTYPE_TREANT) { if (!uActionLength) uActionLength = 256; Actor::AI_StandOrBored(uActorID, Pid::character(0), uActionLength, v10); return; @@ -2093,8 +2059,7 @@ void Actor::AI_Pursue3(unsigned int uActorID, Pid a2, Actor::GetDirectionInfo(v7, a2, &a3, v5); v20 = &a3; } - if (MonsterStats::BelongsToSupertype(v6->monsterInfo.uID, - MONSTER_SUPERTYPE_TREANT)) { + if (supertypeForMonsterId(v6->monsterInfo.uID) == MONSTER_SUPERTYPE_TREANT) { if (!uActionLength) uActionLength = 256; return Actor::AI_StandOrBored(uActorID, Pid::character(0), uActionLength, a4); } @@ -2442,9 +2407,7 @@ void Actor::ActorDamageFromMonster(Pid attacker_id, pushDistance = 20 * finalDmg / pActors[actor_id].monsterInfo.uHP; if (pushDistance > 10) pushDistance = 10; - if (!MonsterStats::BelongsToSupertype( - pActors[actor_id].monsterInfo.uID, - MONSTER_SUPERTYPE_TREANT)) { + if (supertypeForMonsterId(pActors[actor_id].monsterInfo.uID) != MONSTER_SUPERTYPE_TREANT) { pVelocity->x = (int32_t)fixpoint_mul(pushDistance, pVelocity->x); pVelocity->y = @@ -3316,8 +3279,7 @@ void Actor::DamageMonsterFromParty(Pid a1, unsigned int uActorID_Monster, } } if (knockbackValue > 10) knockbackValue = 10; - if (!MonsterStats::BelongsToSupertype(pMonster->monsterInfo.uID, - MONSTER_SUPERTYPE_TREANT)) { + if (supertypeForMonsterId(pMonster->monsterInfo.uID) != MONSTER_SUPERTYPE_TREANT) { pVelocity->x = fixpoint_mul(knockbackValue, pVelocity->x); pVelocity->y = fixpoint_mul(knockbackValue, pVelocity->y); pVelocity->z = fixpoint_mul(knockbackValue, pVelocity->z); @@ -4811,9 +4773,7 @@ void ItemDamageFromActor(Pid uObjID, unsigned int uActorID, (signed int)pActors[uActorID].monsterInfo.uHP > 10) a2a = 10; - if (!MonsterStats::BelongsToSupertype( - pActors[uActorID].monsterInfo.uID, - MONSTER_SUPERTYPE_TREANT)) { + if (supertypeForMonsterId(pActors[uActorID].monsterInfo.uID) != MONSTER_SUPERTYPE_TREANT) { pVelocity->x = fixpoint_mul(a2a, pVelocity->x); pVelocity->y = fixpoint_mul(a2a, pVelocity->y); pVelocity->z = fixpoint_mul(a2a, pVelocity->z); diff --git a/src/Engine/Objects/Character.cpp b/src/Engine/Objects/Character.cpp index 1d0dc00efe17..a916c0ad43f2 100644 --- a/src/Engine/Objects/Character.cpp +++ b/src/Engine/Objects/Character.cpp @@ -1145,21 +1145,20 @@ int Character::CalculateMeleeDmgToEnemyWithWeapon(ItemGen *weapon, ITEM_ENCHANTMENT enchType = weapon->special_enchantment; // check against enchantments - if (MonsterStats::BelongsToSupertype(uTargetActorID, - MONSTER_SUPERTYPE_UNDEAD) && + if (supertypeForMonsterId(uTargetActorID) == MONSTER_SUPERTYPE_UNDEAD && (enchType == ITEM_ENCHANTMENT_UNDEAD_SLAYING || itemId == ITEM_ARTIFACT_GHOULSBANE || itemId == ITEM_ARTIFACT_GIBBET || itemId == ITEM_RELIC_JUSTICE)) { totalDmg *= 2; // double damage vs undead - } else if (MonsterStats::BelongsToSupertype(uTargetActorID, MONSTER_SUPERTYPE_KREEGAN) && + } else if (supertypeForMonsterId(uTargetActorID) == MONSTER_SUPERTYPE_KREEGAN && (enchType == ITEM_ENCHANTMENT_DEMON_SLAYING || itemId == ITEM_ARTIFACT_GIBBET)) { totalDmg *= 2; // double damage vs devils - } else if (MonsterStats::BelongsToSupertype(uTargetActorID, MONSTER_SUPERTYPE_DRAGON) && + } else if (supertypeForMonsterId(uTargetActorID) == MONSTER_SUPERTYPE_DRAGON && (enchType == ITEM_ENCHANTMENT_DRAGON_SLAYING || itemId == ITEM_ARTIFACT_GIBBET)) { totalDmg *= 2; // double damage vs dragons - } else if (MonsterStats::BelongsToSupertype(uTargetActorID, MONSTER_SUPERTYPE_ELF) && + } else if (supertypeForMonsterId(uTargetActorID) == MONSTER_SUPERTYPE_ELF && (enchType == ITEM_ENCHANTMENT_ELF_SLAYING || itemId == ITEM_RELIC_OLD_NICK)) { totalDmg *= 2; // double damage vs elf - } else if (MonsterStats::BelongsToSupertype(uTargetActorID, MONSTER_SUPERTYPE_TITAN) && + } else if (supertypeForMonsterId(uTargetActorID) == MONSTER_SUPERTYPE_TITAN && (enchType == ITEM_ENCHANTMENT_TITAN_SLAYING)) { totalDmg *= 2; // double damage vs titan } @@ -1247,16 +1246,16 @@ int Character::CalculateRangedDamageTo(MonsterId uMonsterInfoID) { if (uMonsterInfoID != MONSTER_INVALID) { // check against bow enchantments if (itemenchant == ITEM_ENCHANTMENT_UNDEAD_SLAYING && - MonsterStats::BelongsToSupertype(uMonsterInfoID, MONSTER_SUPERTYPE_UNDEAD)) { // double damage vs undead + supertypeForMonsterId(uMonsterInfoID) == MONSTER_SUPERTYPE_UNDEAD) { // double damage vs undead damage *= 2; } else if (itemenchant == ITEM_ENCHANTMENT_DEMON_SLAYING && - MonsterStats::BelongsToSupertype(uMonsterInfoID, MONSTER_SUPERTYPE_KREEGAN)) { // double vs devils + supertypeForMonsterId(uMonsterInfoID) == MONSTER_SUPERTYPE_KREEGAN) { // double vs devils damage *= 2; } else if (itemenchant == ITEM_ENCHANTMENT_DRAGON_SLAYING && - MonsterStats::BelongsToSupertype(uMonsterInfoID, MONSTER_SUPERTYPE_DRAGON)) { // double vs dragons + supertypeForMonsterId(uMonsterInfoID) == MONSTER_SUPERTYPE_DRAGON) { // double vs dragons damage *= 2; } else if (itemenchant == ITEM_ENCHANTMENT_ELF_SLAYING && - MonsterStats::BelongsToSupertype(uMonsterInfoID, MONSTER_SUPERTYPE_ELF)) { // double vs elf + supertypeForMonsterId(uMonsterInfoID) == MONSTER_SUPERTYPE_ELF) { // double vs elf damage *= 2; } } diff --git a/src/Engine/Objects/MonsterEnums.cpp b/src/Engine/Objects/MonsterEnums.cpp index fd60798d10d6..b9542c1d4bb0 100644 --- a/src/Engine/Objects/MonsterEnums.cpp +++ b/src/Engine/Objects/MonsterEnums.cpp @@ -99,9 +99,13 @@ static constexpr IndexedArray {MONSTER_TYPE_TREANT, {SEX_MALE, RACE_HUMAN}}, {MONSTER_TYPE_GHOUL, {SEX_MALE, RACE_HUMAN}}, - // These two weren't in the original data tables. + // OE addition, these weren't in the original data tables: {MONSTER_TYPE_BLASTERGUY, {SEX_MALE, RACE_HUMAN}}, - {MONSTER_TYPE_ULTRA_DRAGON, {SEX_MALE, RACE_HUMAN}} + {MONSTER_TYPE_ULTRA_DRAGON, {SEX_MALE, RACE_HUMAN}}, + {MONSTER_TYPE_UNUSED_CAT, {SEX_MALE, RACE_HUMAN}}, + {MONSTER_TYPE_UNUSED_CHICKEN, {SEX_MALE, RACE_HUMAN}}, + {MONSTER_TYPE_UNUSED_DOG, {SEX_MALE, RACE_HUMAN}}, + {MONSTER_TYPE_UNUSED_RAT, {SEX_MALE, RACE_HUMAN}}, }; CharacterSex sexForMonsterType(MonsterType monsterType) { @@ -111,3 +115,175 @@ CharacterSex sexForMonsterType(MonsterType monsterType) { Race raceForMonsterType(MonsterType monsterType) { return sexAndRaceByMonsterType[monsterType].race; } + +struct BountyHuntableMask : IndexedArray { + constexpr BountyHuntableMask() { + fill(false); + } + + constexpr BountyHuntableMask(std::initializer_list townHalls) { + fill(false); + for (const HouseId townHall : townHalls) + (*this)[townHall] = true; + } +}; + +// TODO(captainurist): Tbh the table still makes little sense. Why are Angels bounty-huntable in Celeste? +/** + * Table of monster types that can be targeted in bounty hunts in each of the game's town halls. + * + * This is autogenerated code. It was, however, generated from code that was subsequently deleted, and then the data + * here was edited. Feel free to edit. + * + * @see runBountyHuntCodeGen + */ +static constexpr IndexedArray bountyHuntableMaskByMonsterType = { + {MONSTER_TYPE_ANGEL, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_ARCHER, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_BAT, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_BEHEMOTH, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_BEHOLDER, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_CLERIC_MOON, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE }}, + {MONSTER_TYPE_CLERIC_SUN, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_DEVIL, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_DRAGON, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_DRAGONFLY, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_DWARF, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_ELEMENTAL_AIR, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_ELEMENTAL_EARTH, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_ELEMENTAL_FIRE, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_ELEMENTAL_LIGHT, { }}, + {MONSTER_TYPE_ELEMENTAL_WATER, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_ELF_ARCHER, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_ELF_SPEARMAN, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_FIGHTER_CHAIN, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_FIGHTER_LEATHER, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_FIGHTER_PLATE, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_GARGOYLE, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_GENIE, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_GHOST, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE }}, + {MONSTER_TYPE_GOBLIN, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE }}, + {MONSTER_TYPE_GOG, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_GOLEM, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_GRIFFIN, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_HARPY, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_HYDRA, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_LICH, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE }}, + {MONSTER_TYPE_MAGE, { HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_MANTICORE, { }}, + {MONSTER_TYPE_MEDUSA, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_MINOTAUR, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_MONK, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_NECROMANCER, { HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE }}, + {MONSTER_TYPE_OOZE, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_PEASANT_DWARF_FEMALE_A, { }}, + {MONSTER_TYPE_PEASANT_DWARF_FEMALE_B, { }}, + {MONSTER_TYPE_PEASANT_DWARF_FEMALE_C, { }}, + {MONSTER_TYPE_PEASANT_DWARF_MALE_A, { }}, + {MONSTER_TYPE_PEASANT_DWARF_MALE_B, { }}, + {MONSTER_TYPE_PEASANT_DWARF_MALE_C, { }}, + {MONSTER_TYPE_PEASANT_ELF_FEMALE_A, { }}, + {MONSTER_TYPE_PEASANT_ELF_FEMALE_B, { }}, + {MONSTER_TYPE_PEASANT_ELF_FEMALE_C, { }}, + {MONSTER_TYPE_PEASANT_ELF_MALE_A, { }}, + {MONSTER_TYPE_PEASANT_ELF_MALE_B, { }}, + {MONSTER_TYPE_PEASANT_ELF_MALE_C, { }}, + {MONSTER_TYPE_PEASANT_HUMAN1_FEMALE_A, { }}, + {MONSTER_TYPE_PEASANT_HUMAN1_FEMALE_B, { }}, + {MONSTER_TYPE_PEASANT_HUMAN1_FEMALE_C, { }}, + {MONSTER_TYPE_PEASANT_HUMAN1_MALE_A, { }}, + {MONSTER_TYPE_PEASANT_HUMAN1_MALE_B, { }}, + {MONSTER_TYPE_PEASANT_HUMAN1_MALE_C, { }}, + {MONSTER_TYPE_PEASANT_HUMAN2_MALE_A, { }}, + {MONSTER_TYPE_PEASANT_HUMAN2_MALE_B, { }}, + {MONSTER_TYPE_PEASANT_HUMAN2_MALE_C, { }}, + {MONSTER_TYPE_PEASANT_HUMAN2_FEMALE_A, { }}, + {MONSTER_TYPE_PEASANT_HUMAN2_FEMALE_B, { }}, + {MONSTER_TYPE_PEASANT_HUMAN2_FEMALE_C, { }}, + {MONSTER_TYPE_RAT, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_ROBOT, { }}, + {MONSTER_TYPE_ROC, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_SEA_MONSTER, { }}, + {MONSTER_TYPE_SKELETON_WARRIOR, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE }}, + {MONSTER_TYPE_SPIDER, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_SWORDSMAN, { HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_THIEF, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_TITAN, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_TROGLODYTE, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_VAMPIRE, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE }}, + {MONSTER_TYPE_WARLOCK, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_WIGHT, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE }}, + {MONSTER_TYPE_WYVERN, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_ZOMBIE, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE }}, + + // OE fix: in original binary female goblin peasant could become a bounty hunt target in Harmondale. + {MONSTER_TYPE_PEASANT_GOBLIN_FEMALE_A, { }}, + {MONSTER_TYPE_PEASANT_GOBLIN_FEMALE_B, { }}, + {MONSTER_TYPE_PEASANT_GOBLIN_FEMALE_C, { }}, + {MONSTER_TYPE_PEASANT_GOBLIN_MALE_A, { }}, + {MONSTER_TYPE_PEASANT_GOBLIN_MALE_B, { }}, + {MONSTER_TYPE_PEASANT_GOBLIN_MALE_C, { }}, + {MONSTER_TYPE_TROLL, { HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE, HOUSE_TOWN_HALL_PIT}}, + {MONSTER_TYPE_TREANT, { }}, + {MONSTER_TYPE_GHOUL, {HOUSE_TOWN_HALL_HARMONDALE, HOUSE_TOWN_HALL_ERATHIA, HOUSE_TOWN_HALL_TULAREAN_FOREST, HOUSE_TOWN_HALL_CELESTE }}, + {MONSTER_TYPE_BLASTERGUY, { }}, + {MONSTER_TYPE_ULTRA_DRAGON, { }}, + {MONSTER_TYPE_UNUSED_CAT, { }}, + {MONSTER_TYPE_UNUSED_CHICKEN, { }}, + {MONSTER_TYPE_UNUSED_DOG, { }}, + {MONSTER_TYPE_UNUSED_RAT, { }}, +}; + +bool isBountyHuntable(MonsterType monsterType, HouseId townHall) { + return bountyHuntableMaskByMonsterType[monsterType][townHall]; +} + +ItemId itemDropForMonsterType(MonsterType monsterType) { + switch (monsterType) { + case MONSTER_TYPE_HARPY: return ITEM_REAGENT_HARPY_FEATHER; + case MONSTER_TYPE_OOZE: return ITEM_REAGENT_VIAL_OF_OOZE_ENDOPLASM; + case MONSTER_TYPE_TROLL: return ITEM_REAGENT_VIAL_OF_TROLL_BLOOD; + case MONSTER_TYPE_DEVIL: return ITEM_REAGENT_VIAL_OF_DEVIL_ICHOR; + case MONSTER_TYPE_DRAGON: return ITEM_REAGENT_DRAGONS_EYE; + default: return ITEM_NULL; + } +} + +MONSTER_SUPERTYPE supertypeForMonsterType(MonsterType monsterType) { + switch (monsterType) { + case MONSTER_TYPE_GHOST: + case MONSTER_TYPE_LICH: + case MONSTER_TYPE_SKELETON_WARRIOR: + case MONSTER_TYPE_VAMPIRE: + case MONSTER_TYPE_WIGHT: + case MONSTER_TYPE_ZOMBIE: + case MONSTER_TYPE_GHOUL: + return MONSTER_SUPERTYPE_UNDEAD; + case MONSTER_TYPE_DEVIL: + return MONSTER_SUPERTYPE_KREEGAN; + case MONSTER_TYPE_PEASANT_ELF_FEMALE_A: + case MONSTER_TYPE_PEASANT_ELF_FEMALE_B: + case MONSTER_TYPE_PEASANT_ELF_FEMALE_C: + case MONSTER_TYPE_PEASANT_ELF_MALE_A: + case MONSTER_TYPE_PEASANT_ELF_MALE_B: + case MONSTER_TYPE_PEASANT_ELF_MALE_C: + case MONSTER_TYPE_ELF_ARCHER: + case MONSTER_TYPE_ELF_SPEARMAN: + return MONSTER_SUPERTYPE_ELF; + + // TODO(captainurist): should also include mega-dragon? + case MONSTER_TYPE_DRAGON: + return MONSTER_SUPERTYPE_DRAGON; + + // TODO(captainurist): This needs some reworking it seems. Water elemental supertype is about water walking, + // treant supertype is about being a tree that can't move. The rest are about "of X slaying". + case MONSTER_TYPE_ELEMENTAL_WATER: + return MONSTER_SUPERTYPE_WATER_ELEMENTAL; + case MONSTER_TYPE_TREANT: + return MONSTER_SUPERTYPE_TREANT; + case MONSTER_TYPE_TITAN: + return MONSTER_SUPERTYPE_TITAN; + default: + return MONSTER_SUPERTYPE_NONE; + } +} diff --git a/src/Engine/Objects/MonsterEnums.h b/src/Engine/Objects/MonsterEnums.h index 8de8e40ab6bf..bbd1ee56838d 100644 --- a/src/Engine/Objects/MonsterEnums.h +++ b/src/Engine/Objects/MonsterEnums.h @@ -3,6 +3,8 @@ #include #include "Engine/Objects/CharacterEnums.h" +#include "Engine/Objects/ItemEnums.h" +#include "GUI/UI/UIHouseEnums.h" #include "Utility/Workaround/ToUnderlying.h" #include "Utility/Segment.h" @@ -280,28 +282,31 @@ enum class MonsterId { MONSTER_ULTRA_DRAGON_A = 262, // "Mega-Dragon". MONSTER_ULTRA_DRAGON_B = 263, // "Mega-Dragon". MONSTER_ULTRA_DRAGON_C = 264, // "Mega-Dragon". - MONSTER_265 = 265, - MONSTER_266 = 266, - MONSTER_267 = 267, - MONSTER_268 = 268, - MONSTER_269 = 269, - MONSTER_270 = 270, - MONSTER_271 = 271, - MONSTER_272 = 272, - MONSTER_273 = 273, - MONSTER_274 = 274, - MONSTER_275 = 275, - MONSTER_276 = 276, - MONSTER_277 = 277, + MONSTER_UNUSED_CAT_A = 265, + MONSTER_UNUSED_CAT_B = 266, + MONSTER_UNUSED_CAT_C = 267, + MONSTER_UNUSED_CHICKEN_A = 268, + MONSTER_UNUSED_CHICKEN_B = 269, + MONSTER_UNUSED_CHICKEN_C = 270, + MONSTER_UNUSED_DOG_A = 271, + MONSTER_UNUSED_DOG_B = 272, + MONSTER_UNUSED_DOG_C = 273, + MONSTER_UNUSED_RAT_A = 274, + MONSTER_UNUSED_RAT_B = 275, + MONSTER_UNUSED_RAT_C = 276, MONSTER_FIRST = MONSTER_ANGEL_A, - MONSTER_LAST = MONSTER_277, + MONSTER_LAST = MONSTER_UNUSED_RAT_C, MONSTER_FIRST_ARENA = MONSTER_ANGEL_A, MONSTER_LAST_ARENA = MONSTER_GHOUL_C }; using enum MonsterId; +inline Segment allMonsters() { + return {MONSTER_FIRST, MONSTER_LAST}; +} + inline Segment allArenaMonsters() { return {MONSTER_FIRST_ARENA, MONSTER_LAST_ARENA}; } @@ -404,16 +409,15 @@ enum class MonsterType { MONSTER_TYPE_GHOUL = 86, MONSTER_TYPE_BLASTERGUY = 87, MONSTER_TYPE_ULTRA_DRAGON = 88, - MONSTER_TYPE_89 = 89, - MONSTER_TYPE_90 = 90, - MONSTER_TYPE_91 = 91, - MONSTER_TYPE_92 = 92, - MONSTER_TYPE_93 = 93, + MONSTER_TYPE_UNUSED_CAT = 89, + MONSTER_TYPE_UNUSED_CHICKEN = 90, + MONSTER_TYPE_UNUSED_DOG = 91, + MONSTER_TYPE_UNUSED_RAT = 92, MONSTER_TYPE_9999 = 9999, // TODO(captainurist): eh? MONSTER_TYPE_FIRST = MONSTER_TYPE_ANGEL, - MONSTER_TYPE_LAST = MONSTER_TYPE_ULTRA_DRAGON, + MONSTER_TYPE_LAST = MONSTER_TYPE_UNUSED_RAT, MONSTER_TYPE_FIRST_PEASANT_DWARF = MONSTER_TYPE_PEASANT_DWARF_FEMALE_A, MONSTER_TYPE_LAST_PEASANT_DWARF = MONSTER_TYPE_PEASANT_DWARF_MALE_C, @@ -429,6 +433,10 @@ enum class MonsterType { }; using enum MonsterType; +inline Segment allMonsterTypes() { + return {MONSTER_TYPE_FIRST, MONSTER_TYPE_LAST}; +} + inline MonsterType monsterTypeForMonsterId(MonsterId monsterId) { return static_cast((std::to_underlying(monsterId) - 1) / 3 + 1); } @@ -441,10 +449,18 @@ inline bool isPeasant(MonsterType monsterType) { (monsterType >= MONSTER_TYPE_FIRST_PEASANT_GOBLIN && monsterType <= MONSTER_TYPE_LAST_PEASANT_GOBLIN); } +inline bool isPeasant(MonsterId monsterId) { + return isPeasant(monsterTypeForMonsterId(monsterId)); +} + CharacterSex sexForMonsterType(MonsterType monsterType); Race raceForMonsterType(MonsterType monsterType); +bool isBountyHuntable(MonsterType monsterType, HouseId townHall); + +ItemId itemDropForMonsterType(MonsterType monsterType); + /* 335 */ enum class MONSTER_SPECIAL_ABILITY_TYPE { MONSTER_SPECIAL_ABILITY_NONE = 0x0, @@ -474,10 +490,22 @@ enum class MONSTER_SUPERTYPE { MONSTER_SUPERTYPE_WATER_ELEMENTAL = 0x5, MONSTER_SUPERTYPE_TREANT = 0x6, MONSTER_SUPERTYPE_TITAN = 0x7, - MONSTER_SUPERTYPE_8 = 0x8, + MONSTER_SUPERTYPE_8 = 0x8, // TODO(captainurist): not an arena monster? Drop? }; using enum MONSTER_SUPERTYPE; +/** + * @offset 0x00438BDF + * + * @param monsterType Monster type to check. + * @return Supertype for the provided monster type. + */ +MONSTER_SUPERTYPE supertypeForMonsterType(MonsterType monsterType); + +inline MONSTER_SUPERTYPE supertypeForMonsterId(MonsterId monsterId) { + return supertypeForMonsterType(monsterTypeForMonsterId(monsterId)); +} + enum class SPECIAL_ATTACK_TYPE : uint8_t { SPECIAL_ATTACK_NONE = 0, SPECIAL_ATTACK_CURSE = 1, @@ -517,3 +545,4 @@ enum class MonsterHostility { HOSTILITY_LAST = HOSTILITY_LONG }; using enum MonsterHostility; + diff --git a/src/Engine/Objects/Monsters.cpp b/src/Engine/Objects/Monsters.cpp index 3ca799f2b46a..bcbe685d548b 100644 --- a/src/Engine/Objects/Monsters.cpp +++ b/src/Engine/Objects/Monsters.cpp @@ -1081,71 +1081,3 @@ MonsterId MonsterList::GetMonsterIDByName(const std::string &pMonsterName) { } Error("Monster not found: %s", pMonsterName.c_str()); } -//----- (00438BDF) -------------------------------------------------------- -bool MonsterStats::BelongsToSupertype(MonsterId uMonsterInfoID, - enum MONSTER_SUPERTYPE eSupertype) { - switch (eSupertype) { - case MONSTER_SUPERTYPE_UNDEAD: - if (uMonsterInfoID >= MONSTER_GHOST_A && - uMonsterInfoID <= MONSTER_GHOST_C // 70<=id<=72 - || uMonsterInfoID >= MONSTER_LICH_A && - uMonsterInfoID <= MONSTER_LICH_C // 91-93 - || - uMonsterInfoID >= MONSTER_SKELETON_WARRIOR_A && - uMonsterInfoID <= MONSTER_SKELETON_WARRIOR_C // 199-201 - || - uMonsterInfoID >= MONSTER_VAMPIRE_A && - uMonsterInfoID <= MONSTER_VAMPIRE_C // 217-219 - || uMonsterInfoID >= MONSTER_WIGHT_A && - uMonsterInfoID <= MONSTER_WIGHT_C // 223-225 - || - uMonsterInfoID >= MONSTER_ZOMBIE_A && - uMonsterInfoID <= MONSTER_ZOMBIE_C // 229-231 - || - uMonsterInfoID >= MONSTER_GHOUL_A && - uMonsterInfoID <= MONSTER_GHOUL_C) // 256-258 - return true; - return false; - case MONSTER_SUPERTYPE_KREEGAN: - if (uMonsterInfoID >= MONSTER_DEVIL_A && - uMonsterInfoID <= MONSTER_DEVIL_C) // 22-24 - return true; - return false; - case MONSTER_SUPERTYPE_ELF: - if (uMonsterInfoID >= MONSTER_PEASANT_ELF_FEMALE_A_A && - uMonsterInfoID <= - MONSTER_PEASANT_ELF_MALE_C_C // 133 - 150 - || - uMonsterInfoID >= MONSTER_ELF_ARCHER_A && - uMonsterInfoID <= MONSTER_ELF_ARCHER_C // 49-51 - || uMonsterInfoID >= MONSTER_ELF_SPEARMAN_A && - uMonsterInfoID <= - MONSTER_ELF_SPEARMAN_C) // 52-54 - return true; - return false; - case MONSTER_SUPERTYPE_DRAGON: - if (uMonsterInfoID >= MONSTER_DRAGON_A && - uMonsterInfoID <= MONSTER_DRAGON_C) // 25-27 - return true; - return false; - case MONSTER_SUPERTYPE_WATER_ELEMENTAL: - if (uMonsterInfoID >= MONSTER_ELEMENTAL_WATER_A && - uMonsterInfoID <= - MONSTER_ELEMENTAL_WATER_C) // 46-48 - return true; - return false; - case MONSTER_SUPERTYPE_TREANT: - if (uMonsterInfoID >= MONSTER_TREANT_A && - uMonsterInfoID <= MONSTER_TREANT_C) // 253-255 - return true; - return false; - case MONSTER_SUPERTYPE_TITAN: - if (uMonsterInfoID >= MONSTER_TITAN_A && - uMonsterInfoID <= MONSTER_TITAN_C) // 211-213 - return true; - return false; - default: - return false; - } - return false; -} diff --git a/src/Engine/Objects/Monsters.h b/src/Engine/Objects/Monsters.h index 82c1f2667237..3bf93dc6c3fe 100644 --- a/src/Engine/Objects/Monsters.h +++ b/src/Engine/Objects/Monsters.h @@ -84,9 +84,6 @@ struct MonsterStats { void InitializePlacements(const Blob &placements); MonsterId FindMonsterByTextureName(const std::string &Str2); - static bool BelongsToSupertype(MonsterId uMonsterInfoID, - MONSTER_SUPERTYPE eSupertype); - IndexedArray pInfos; // 0 - 5b18h std::array pPlaceStrings; // 5B18h placement counts from 1 unsigned int uNumMonsters; // 5B94h // TODO(captainurist): can drop? diff --git a/src/Engine/Objects/SpriteObject.cpp b/src/Engine/Objects/SpriteObject.cpp index ceb58e890b43..41addccfb24e 100644 --- a/src/Engine/Objects/SpriteObject.cpp +++ b/src/Engine/Objects/SpriteObject.cpp @@ -1036,7 +1036,7 @@ bool processSpellImpact(unsigned int uLayingItemID, Pid pid) { case SPRITE_SPELL_LIGHT_DESTROY_UNDEAD: { if (pid.type() == OBJECT_Actor && - MonsterStats::BelongsToSupertype(pActors[pid.id()].monsterInfo.uID, MONSTER_SUPERTYPE_UNDEAD)) { + supertypeForMonsterId(pActors[pid.id()].monsterInfo.uID) == MONSTER_SUPERTYPE_UNDEAD) { applySpellSpriteDamage(uLayingItemID, pid); } updateSpriteOnImpact(object); diff --git a/src/Engine/Snapshots/TableSerialization.cpp b/src/Engine/Snapshots/TableSerialization.cpp index 5f9cde2a8758..70498adc806a 100644 --- a/src/Engine/Snapshots/TableSerialization.cpp +++ b/src/Engine/Snapshots/TableSerialization.cpp @@ -15,6 +15,8 @@ #include "Library/Snapshots/SnapshotSerialization.h" +#include "Utility/Exception.h" + #include "EntitySnapshots.h" #include "CompositeSnapshots.h" @@ -70,23 +72,17 @@ void deserialize(const TriBlob &src, IconFrameTable *dst) { void deserialize(const TriBlob &src, MonsterList *dst) { std::vector monsters; - if (src.mm6) - deserialize(src.mm6, &monsters, tags::append, tags::via); if (src.mm7) deserialize(src.mm7, &monsters, tags::append, tags::via); - if (src.mm8) - deserialize(src.mm8, &monsters, tags::append, tags::via); - assert(monsters.size() <= dst->pMonsters.size()); // TODO(captainurist): this shouldn't be an assertion. + if (monsters.size() != 277) + throw Exception("Invalid monster list size, expected {}, got {}", 277, monsters.size()); + monsters.pop_back(); // Last one is unused. + assert(monsters.size() == dst->pMonsters.size()); dst->pMonsters.fill(MonsterDesc()); - for (size_t i = 0; MonsterId index : dst->pMonsters.indices()) { - if (i >= monsters.size()) - break; + for (size_t i = 0; MonsterId index : dst->pMonsters.indices()) dst->pMonsters[index] = monsters[i++]; - } - - assert(!dst->pMonsters.empty()); } void deserialize(const TriBlob &src, ObjectList *dst) { diff --git a/src/Engine/Spells/CastSpellInfo.cpp b/src/Engine/Spells/CastSpellInfo.cpp index 93f82f3d7ece..5f872c735f3a 100644 --- a/src/Engine/Spells/CastSpellInfo.cpp +++ b/src/Engine/Spells/CastSpellInfo.cpp @@ -446,7 +446,7 @@ void CastSpellInfoHelpers::castSpell() { pSpellSprite.uFacing = target_direction.uYawAngle; pSpellSprite.uAttributes |= SPRITE_ATTACHED_TO_HEAD; int obj_id = pSpellSprite.Create(0, 0, 0, 0); - if (!MonsterStats::BelongsToSupertype(pActors[monster_id].monsterInfo.uID, MONSTER_SUPERTYPE_UNDEAD)) { + if (supertypeForMonsterId(pActors[monster_id].monsterInfo.uID) != MONSTER_SUPERTYPE_UNDEAD) { spellFailed(pCastSpell, LSTR_SPELL_FAILED); pPlayer->SpendMana(uRequiredMana); // decrease mana on failure setSpellRecovery(pCastSpell, recoveryTime); @@ -1751,7 +1751,7 @@ void CastSpellInfoHelpers::castSpell() { pSpellSprite.uType = SPRITE_SPELL_SPIRIT_TURN_UNDEAD_1; initSpellSprite(&pSpellSprite, spell_level, spell_mastery, pCastSpell); for (Actor *actor : render->getActorsInViewport(4096)) { - if (MonsterStats::BelongsToSupertype(actor->monsterInfo.uID, MONSTER_SUPERTYPE_UNDEAD)) { + if (supertypeForMonsterId(actor->monsterInfo.uID) == MONSTER_SUPERTYPE_UNDEAD) { pSpellSprite.vPosition = actor->pos - Vec3i(0, 0, actor->height * -0.8); pSpellSprite.spell_target_pid = Pid(OBJECT_Actor, actor->id); pSpellSprite.Create(0, 0, 0, 0); @@ -2059,7 +2059,7 @@ void CastSpellInfoHelpers::castSpell() { GameTime spell_duration = GameTime::FromMinutes(10 * spell_level); int monster_id = spell_targeted_at.id(); // v730 = 836 * monster_id; - if (MonsterStats::BelongsToSupertype(pActors[monster_id].monsterInfo.uID, MONSTER_SUPERTYPE_UNDEAD)) { + if (supertypeForMonsterId(pActors[monster_id].monsterInfo.uID) == MONSTER_SUPERTYPE_UNDEAD) { spellFailed(pCastSpell, LSTR_SPELL_FAILED); pPlayer->SpendMana(uRequiredMana); // decrease mana on failure setSpellRecovery(pCastSpell, recoveryTime); @@ -2104,7 +2104,7 @@ void CastSpellInfoHelpers::castSpell() { initSpellSprite(&pSpellSprite, spell_level, spell_mastery, pCastSpell); for (Actor *actor : render->getActorsInViewport(4096)) { // Change: do not exit loop when first undead monster is found - if (!MonsterStats::BelongsToSupertype(actor->monsterInfo.uID, MONSTER_SUPERTYPE_UNDEAD)) { + if (supertypeForMonsterId(actor->monsterInfo.uID) != MONSTER_SUPERTYPE_UNDEAD) { pSpellSprite.vPosition = actor->pos - Vec3i(0, 0, actor->height * -0.8); pSpellSprite.spell_target_pid = Pid(OBJECT_Actor, actor->id); pSpellSprite.Create(0, 0, 0, 0); @@ -2742,7 +2742,7 @@ void CastSpellInfoHelpers::castSpell() { assert(false); } int monster_id = spell_targeted_at.id(); - if (!MonsterStats::BelongsToSupertype(pActors[monster_id].monsterInfo.uID, MONSTER_SUPERTYPE_UNDEAD)) { + if (supertypeForMonsterId(pActors[monster_id].monsterInfo.uID) != MONSTER_SUPERTYPE_UNDEAD) { spellFailed(pCastSpell, LSTR_SPELL_FAILED); pPlayer->SpendMana(uRequiredMana); // decrease mana on failure setSpellRecovery(pCastSpell, recoveryTime); diff --git a/src/Engine/Tables/FactionTable.cpp b/src/Engine/Tables/FactionTable.cpp index 914581464bc8..0771dc1f3aa5 100644 --- a/src/Engine/Tables/FactionTable.cpp +++ b/src/Engine/Tables/FactionTable.cpp @@ -18,6 +18,9 @@ void FactionTable::Initialize(const Blob &factions) { int decode_step; // int item_counter; + for (auto &line : relations) + line.fill(HOSTILITY_FRIENDLY); + std::string txtRaw(factions.string_view()); strtok(txtRaw.data(), "\r"); for (i = 0; i < 89; ++i) { diff --git a/src/Engine/Tables/FactionTable.h b/src/Engine/Tables/FactionTable.h index 986d3b1b91ec..d9c3b6003cb3 100644 --- a/src/Engine/Tables/FactionTable.h +++ b/src/Engine/Tables/FactionTable.h @@ -9,6 +9,7 @@ class Blob; struct FactionTable { void Initialize(const Blob &factions); + // Original table was 89x89 elements, in OE it was expanded to include unused monster types. IndexedArray, MONSTER_TYPE_INVALID, MONSTER_TYPE_LAST> relations; }; diff --git a/src/Engine/Tables/NPCTable.cpp b/src/Engine/Tables/NPCTable.cpp index 476c14b11b90..78f51d4a0b2b 100644 --- a/src/Engine/Tables/NPCTable.cpp +++ b/src/Engine/Tables/NPCTable.cpp @@ -478,7 +478,6 @@ void NPCStats::InitializeAdditionalNPCs(NPCData *pNPCDataBuff, MonsterId npc_uid int uPortretMin; // [sp+24h] [bp+Ch]@1 int uPortretMax; - // TODO(captainurist): encapsulate enum arithmetic. MonsterType monsterType = monsterTypeForMonsterId(npc_uid); CharacterSex uNPCSex = sexForMonsterType(monsterType); uRace = raceForMonsterType(monsterType); diff --git a/src/GUI/UI/Houses/TownHall.cpp b/src/GUI/UI/Houses/TownHall.cpp index fbed5f963861..5f606ac7ddd8 100644 --- a/src/GUI/UI/Houses/TownHall.cpp +++ b/src/GUI/UI/Houses/TownHall.cpp @@ -145,106 +145,9 @@ std::vector GUIWindow_TownHall::listDialogueOptions() { MonsterId GUIWindow_TownHall::randomMonsterForHunting(HouseId townhall) { while (true) { - // TODO(captainurist): I got lazy here. Use actual enum values. - int result = grng->random(258) + 1; - switch (townhall) { - case HOUSE_TOWN_HALL_HARMONDALE: - if ((result < 115 || result > 132) && - (result < 235 || result > 252) && - (result < 133 || result > 150) && - (result < 0x97u || result > 0xBAu) && - (result < 0xBEu || result > 0xC0u) && - (result < 0xC4u || result > 0xC6u) && - (result < 0x2Bu || result > 0x2Du) && - (result < 0xCDu || result > 0xCFu) && - (result < 0x5Eu || result > 0x60u) && - (result < 0xFDu || result > 0xFFu) && - (result < 0x6Du || result > 0x6Fu) && - (result < 0x61u || result > 0x63u)) - return static_cast(result); - break; - - case HOUSE_TOWN_HALL_ERATHIA: - if ((result < 115 || result > 132) && - (result < 0xE8u || result > 0xF9u) && - (result < 0x85u || result > 0x96u) && - (result < 0x97u || result > 0xBAu) && - (result < 0xBEu || result > 0xC0u) && - (result < 0xC4u || result > 0xC6u) && - (result < 0x2Bu || result > 0x2Du) && - (result < 0x52u || result > 0x54u) && - (result < 4 || result > 6) && - (result < 0x37u || result > 0x39u) && - (result < 0x3Au || result > 0x3Cu) && - (result < 0x3Du || result > 0x3Fu) && - (result < 0xFDu || result > 0xFFu) && - (result < 0x61u || result > 0x63u) && - (result < 0xCDu || result > 0xCFu)) - return static_cast(result); - break; - - case HOUSE_TOWN_HALL_TULAREAN_FOREST: - if ((result < 0x73u || result > 0x84u) && - (result < 0xE8u || result > 0xF9u) && - (result < 0x85u || result > 0x96u) && - (result < 0x97u || result > 0xBAu) && - (result < 0xBEu || result > 0xC0u) && - (result < 0xC4u || result > 0xC6u) && - (result < 0x2Bu || result > 0x2Du) && - (result < 0x31u || result > 0x33u) && - (result < 0x34u || result > 0x36u) && - (result < 0xFDu || result > 0xFFu) && - (result < 0x61u || result > 0x63u) && - (result < 0x1Cu || result > 0x1Eu)) - return static_cast(result); - break; - - case HOUSE_TOWN_HALL_CELESTE: - if ((result < 0x73u || result > 0x84u) && - (result < 0xE8u || result > 0xF9u) && - (result < 0x85u || result > 0x96u) && - (result < 0x97u || result > 0xBAu) && - (result < 0xBEu || result > 0xC0u) && - (result < 0xC4u || result > 0xC6u) && - (result < 0x2Bu || result > 0x2Du) && - (result < 0x5Eu || result > 0x60u) && - (result < 0x43u || result > 0x45u) && - (result < 0x4Fu || result > 0x51u) && - (result < 0xC1u || result > 0xC3u) && - (result < 0x13u || result > 0x15u) && - (result < 0xFDu || result > 0xFFu) && - (result < 0x61u || result > 0x63u) && - (result < 0x6Au || result > 0x6Cu)) - return static_cast(result); - break; - - case HOUSE_TOWN_HALL_PIT: - if ((result < 0x73u || result > 0x84u) && - (result < 0xE8u || result > 0xF9u) && - (result < 0x85u || result > 0x96u) && - (result < 0x97u || result > 0xBAu) && - (result < 0xBEu || result > 0xC0u) && - (result < 0xC4u || result > 0xC6u) && - (result < 0x2Bu || result > 0x2Du) && - (result < 0x6Du || result > 0x6Fu) && - (result < 0x46u || result > 0x48u) && - (result < 0x100u || result > 0x102u) && - (result < 0xD9u || result > 0xDBu) && - (result < 0xC7u || result > 0xC9u) && - (result < 0xE5u || result > 0xE7u) && - (result < 0xDFu || result > 0xE1u) && - (result < 0x5Bu || result > 0x5Du) && - (result < 0x49u || result > 0x4Bu) && - (result < 0xFDu || result > 0xFFu) && - (result < 0x61u || result > 0x63u) && - (result < 0x10u || result > 0x12u)) - return static_cast(result); - break; - - default: - assert(false); - return MONSTER_INVALID; - } + MonsterId result = grng->randomSample(allMonsters()); + if (isBountyHuntable(monsterTypeForMonsterId(result), townhall)) + return result; } } diff --git a/src/GUI/UI/Houses/TownHall.h b/src/GUI/UI/Houses/TownHall.h index abc992daab61..91c9e78e5967 100644 --- a/src/GUI/UI/Houses/TownHall.h +++ b/src/GUI/UI/Houses/TownHall.h @@ -22,14 +22,14 @@ class GUIWindow_TownHall : public GUIWindow_House { */ std::string bountyHuntingText(); + static MonsterId randomMonsterForHunting(HouseId townhall); + protected: void mainDialogue(); void bountyHuntDialogue(); void payFineDialogue(); private: - MonsterId randomMonsterForHunting(HouseId townhall); - /** * Handler for the "Bounty Hunt" dialogue option in a town hall. * diff --git a/src/GUI/UI/NPCTopics.cpp b/src/GUI/UI/NPCTopics.cpp index 70d5a1eaf033..1ed6fc537dc7 100644 --- a/src/GUI/UI/NPCTopics.cpp +++ b/src/GUI/UI/NPCTopics.cpp @@ -326,7 +326,7 @@ void prepareArenaFight(DIALOGUE_TYPE dialogue) { std::vector candidateIds; for (MonsterId i : allArenaMonsters()) { if (pMonsterStats->pInfos[i].uAIType != 1) { - if (!MonsterStats::BelongsToSupertype(pMonsterStats->pInfos[i].uID, MONSTER_SUPERTYPE_8)) { + if (supertypeForMonsterId(pMonsterStats->pInfos[i].uID) != MONSTER_SUPERTYPE_8) { if (pMonsterStats->pInfos[i].uLevel >= monsterMinLevel && pMonsterStats->pInfos[i].uLevel <= monsterMaxLevel) { candidateIds.push_back(i); diff --git a/src/GUI/UI/UIPopup.cpp b/src/GUI/UI/UIPopup.cpp index 00ce72ac2610..124901ced4b7 100644 --- a/src/GUI/UI/UIPopup.cpp +++ b/src/GUI/UI/UIPopup.cpp @@ -170,6 +170,12 @@ IndexedArray monster_popup_y_offsets {MONSTER_TYPE_GHOUL, 0}, {MONSTER_TYPE_BLASTERGUY, 0}, {MONSTER_TYPE_ULTRA_DRAGON, 0}, + + // OE addition, original data table was smaller: + {MONSTER_TYPE_UNUSED_CAT, 0}, + {MONSTER_TYPE_UNUSED_CHICKEN, 0}, + {MONSTER_TYPE_UNUSED_DOG, 0}, + {MONSTER_TYPE_UNUSED_RAT, 0}, }; void Inventory_ItemPopupAndAlchemy(); @@ -660,10 +666,7 @@ void MonsterPopup_Draw(unsigned int uActorID, GUIWindow *pWindow) { } else { // rand(); pMonsterInfoUI_Doll.currentActionAnimation = ANIM_Bored; - if ((pMonsterInfoUI_Doll.monsterInfo.uID < MONSTER_PEASANT_DWARF_FEMALE_A_A || - pMonsterInfoUI_Doll.monsterInfo.uID > MONSTER_PEASANT_HUMAN2_FEMALE_C_C) && - (pMonsterInfoUI_Doll.monsterInfo.uID < MONSTER_PEASANT_GOBLIN_FEMALE_A_A || - pMonsterInfoUI_Doll.monsterInfo.uID > MONSTER_PEASANT_GOBLIN_MALE_C_C) && vrng->random(30) < 100) + if (!isPeasant(pMonsterInfoUI_Doll.monsterInfo.uID) && vrng->random(30) < 100) pMonsterInfoUI_Doll.currentActionAnimation = ANIM_AtkMelee; pMonsterInfoUI_Doll.currentActionLength = 8 * @@ -1029,7 +1032,7 @@ void MonsterPopup_Draw(unsigned int uActorID, GUIWindow *pWindow) { } /** - * @offset 0x00417BB5. + * @offset 0x00417BB5 * * @brief Generating message for skill description popup. * diff --git a/test/Bin/GameTest/CMakeLists.txt b/test/Bin/GameTest/CMakeLists.txt index 88500115d87e..92e2b1961567 100644 --- a/test/Bin/GameTest/CMakeLists.txt +++ b/test/Bin/GameTest/CMakeLists.txt @@ -19,7 +19,7 @@ if(OE_BUILD_TESTS) ExternalProject_Add(OpenEnroth_TestData PREFIX ${CMAKE_CURRENT_BINARY_DIR}/test_data_tmp GIT_REPOSITORY https://github.com/OpenEnroth/OpenEnroth_TestData.git - GIT_TAG 32290d6df02775193fe4345e8f5c0c4ff76dbeae + GIT_TAG f4b686c7775de2726d7e2a6097fab0fef6b36f27 SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/test_data CONFIGURE_COMMAND "" BUILD_COMMAND "" diff --git a/test/Bin/GameTest/GameTests.cpp b/test/Bin/GameTest/GameTests.cpp index f607a8730fc1..23980b7ffa8c 100644 --- a/test/Bin/GameTest/GameTests.cpp +++ b/test/Bin/GameTest/GameTests.cpp @@ -649,12 +649,7 @@ GAME_TEST(Issues, Issue488) { GAME_TEST(Issues, Issue489) { // Test that AOE version of Shrinking Ray spell works. - auto chibisTape = tapes.custom([] { - return std::count_if(pActors.begin(), pActors.end(), [] (const Actor &actor) { - return actor.buffs[ACTOR_BUFF_SHRINK].Active(); - }); - }); - + auto chibisTape = tapes.actorCountByBuff(ACTOR_BUFF_SHRINK); test.playTraceFromTestData("issue_489.mm7", "issue_489.json"); EXPECT_EQ(chibisTape, tape(0, 21)); } @@ -1389,9 +1384,7 @@ GAME_TEST(Issues, Issue830) { GAME_TEST(Issues, Issue832) { // Death Blossom + ice blast crash - auto deathsTape = tapes.custom([] { - return std::count_if(pActors.begin(), pActors.end(), [] (auto &&actor) { return actor.aiState == AIState::Dead; }); - }); + auto deathsTape = tapes.actorCountByState(AIState::Dead); test.playTraceFromTestData("issue_832.mm7", "issue_832.json"); EXPECT_EQ(deathsTape.firstLast(), tape(0, 3)); } @@ -1643,14 +1636,10 @@ GAME_TEST(Issues, Issue1197) { // 1200 GAME_TEST(Issues, Issue1251) { - // Make sure charm wand doesnt assert - auto charmedactors = tapes.custom([] { - return std::count_if(pActors.begin(), pActors.end(), [](const Actor& actor) { - return actor.buffs[ACTOR_BUFF_CHARM].Active(); - }); - }); + // Make sure charm wand doesn't assert + auto charmedActors = tapes.actorCountByBuff(ACTOR_BUFF_CHARM); test.playTraceFromTestData("issue_1251b.mm7", "issue_1251b.json"); - EXPECT_EQ(charmedactors.delta(), 3); + EXPECT_EQ(charmedActors.delta(), 3); } GAME_TEST(Issues, Issue1255) { @@ -1683,13 +1672,7 @@ GAME_TEST(Issues, Issue1281) { GAME_TEST(Issues, Issue1282) { // Picking up an item asserts. auto itemTape = tapes.hasItem(ITEM_LEATHER_ARMOR); - auto totalObjectsTape = tapes.custom([] { - int result = 0; - for (const SpriteObject &spriteObject : pSpriteObjects) - if (spriteObject.containing_item.uItemID != ITEM_NULL) - result++; - return result; - }); + auto totalObjectsTape = tapes.mapItemCount(); test.playTraceFromTestData("issue_1282.mm7", "issue_1282.json"); EXPECT_EQ(itemTape, tape(false, true)); EXPECT_EQ(totalObjectsTape.delta(), -1); @@ -1708,3 +1691,12 @@ GAME_TEST(Issues, Issue1315) { std::tuple(false, GAME_STATE_PARTY_DIED), // Instant switch from turn-based & alive into realtime & dead, std::tuple(false, GAME_STATE_PLAYING))); // meaning that the party died in turn-based mode. } + +GAME_TEST(Prs, Pr1325) { + // Trolls drop vials of troll blood. + auto vialsTape = tapes.mapItemCount(ITEM_REAGENT_VIAL_OF_TROLL_BLOOD); + auto deadTape = tapes.actorCountByState(AIState::Dead); + test.playTraceFromTestData("pr_1325.mm7", "pr_1325.json"); + EXPECT_EQ(vialsTape.delta(), +6); + EXPECT_EQ(deadTape.delta(), +84); // Too much armageddon... +} diff --git a/test/Testing/Game/CommonTapeRecorder.cpp b/test/Testing/Game/CommonTapeRecorder.cpp index c61e815a59fe..856579958102 100644 --- a/test/Testing/Game/CommonTapeRecorder.cpp +++ b/test/Testing/Game/CommonTapeRecorder.cpp @@ -1,6 +1,10 @@ #include "CommonTapeRecorder.h" +#include + #include "Engine/Objects/Character.h" +#include "Engine/Objects/Actor.h" +#include "Engine/Objects/SpriteObject.h" #include "Engine/mm7_data.h" #include "Engine/Party.h" #include "Engine/Engine.h" @@ -44,8 +48,8 @@ TestTape CommonTapeRecorder::totalItemCount() { }); } -TestTape CommonTapeRecorder::hasItem(ItemId item) { - return custom([item] { return pParty->hasItem(item); }); +TestTape CommonTapeRecorder::hasItem(ItemId itemId) { + return custom([itemId] { return pParty->hasItem(itemId); }); } TestTape CommonTapeRecorder::gold() { @@ -91,3 +95,33 @@ TestTape CommonTapeRecorder::time() { TestTape CommonTapeRecorder::turnBasedMode() { return custom([] { return pParty->bTurnBasedModeOn; }); } + +TestTape CommonTapeRecorder::actorCountByState(AIState state) { + return custom([state] { + return static_cast(std::ranges::count(pActors, state, &Actor::aiState)); + }); +} + +TestTape CommonTapeRecorder::actorCountByBuff(ACTOR_BUFF_INDEX buff) { + return custom([buff] { + return static_cast(std::ranges::count_if(pActors, [buff] (const Actor &actor) { + return actor.buffs[buff].Active(); + })); + }); +} + +TestTape CommonTapeRecorder::mapItemCount() { + return custom([] { + return static_cast(std::ranges::count_if(pSpriteObjects, [] (const SpriteObject &object) { + return object.containing_item.uItemID != ITEM_NULL; + })); + }); +} + +TestTape CommonTapeRecorder::mapItemCount(ItemId itemId) { + return custom([itemId] { + return static_cast(std::ranges::count_if(pSpriteObjects, [itemId] (const SpriteObject &object) { + return object.containing_item.uItemID == itemId; + })); + }); +} diff --git a/test/Testing/Game/CommonTapeRecorder.h b/test/Testing/Game/CommonTapeRecorder.h index f1930423a0ab..6cd72ce2a64b 100644 --- a/test/Testing/Game/CommonTapeRecorder.h +++ b/test/Testing/Game/CommonTapeRecorder.h @@ -6,6 +6,7 @@ #include #include "Engine/Objects/ItemEnums.h" +#include "Engine/Objects/ActorEnums.h" #include "Engine/Time.h" #include "GUI/GUIEnums.h" #include "GUI/GUIDialogues.h" @@ -43,7 +44,7 @@ class CommonTapeRecorder { TestTape totalItemCount(); - TestTape hasItem(ItemId item); + TestTape hasItem(ItemId itemId); TestTape gold(); @@ -68,6 +69,14 @@ class CommonTapeRecorder { return custom([&] { return entry.value(); }); } + TestTape actorCountByState(AIState state); + + TestTape actorCountByBuff(ACTOR_BUFF_INDEX buff); + + TestTape mapItemCount(); + + TestTape mapItemCount(ItemId itemId); + private: TestController *_controller = nullptr; };