/** * @file monster.h * * Interface of monster functionality, AI, actions, spawning, loading, etc. */ #pragma once #include #include #include #include #include "engine.h" #include "engine/actor_position.hpp" #include "engine/animationinfo.h" #include "engine/cel_sprite.hpp" #include "engine/point.hpp" #include "engine/sound.h" #include "engine/world_tile.hpp" #include "monstdat.h" #include "spelldat.h" #include "textdat.h" namespace devilution { struct Missile; constexpr size_t MaxMonsters = 200; constexpr size_t MaxLvlMTypes = 24; enum monster_flag : uint16_t { // clang-format off MFLAG_HIDDEN = 1 << 0, MFLAG_LOCK_ANIMATION = 1 << 1, MFLAG_ALLOW_SPECIAL = 1 << 2, MFLAG_NOHEAL = 1 << 3, MFLAG_TARGETS_MONSTER = 1 << 4, MFLAG_GOLEM = 1 << 5, MFLAG_QUEST_COMPLETE = 1 << 6, MFLAG_KNOCKBACK = 1 << 7, MFLAG_SEARCH = 1 << 8, MFLAG_CAN_OPEN_DOOR = 1 << 9, MFLAG_NO_ENEMY = 1 << 10, MFLAG_BERSERK = 1 << 11, MFLAG_NOLIFESTEAL = 1 << 12, // clang-format on }; /** Indexes from UniqueMonstersData array for special unique monsters (usually quest related) */ enum : uint8_t { UMT_GARBUD, UMT_SKELKING, UMT_ZHAR, UMT_SNOTSPIL, UMT_LAZARUS, UMT_RED_VEX, UMT_BLACKJADE, UMT_LACHDAN, UMT_WARLORD, UMT_BUTCHER, UMT_HORKDMN, UMT_DEFILER, UMT_NAKRUL, }; enum class MonsterMode : uint8_t { Stand, /** Movement towards N, NW, or NE */ MoveNorthwards, /** Movement towards S, SW, or SE */ MoveSouthwards, /** Movement towards W or E */ MoveSideways, MeleeAttack, HitRecovery, Death, SpecialMeleeAttack, FadeIn, FadeOut, RangedAttack, SpecialStand, SpecialRangedAttack, Delay, Charge, Petrified, Heal, Talk, }; enum class MonsterGraphic : uint8_t { Stand, Walk, Attack, GotHit, Death, Special, }; enum class MonsterGoal : uint8_t { None, Normal, Retreat, Healing, Move, Attack, Inquiring, Talking, }; enum placeflag : uint8_t { // clang-format off PLACE_SCATTER = 1 << 0, PLACE_SPECIAL = 1 << 1, PLACE_UNIQUE = 1 << 2, // clang-format on }; /** * @brief Defines the relation of the monster to a monster pack. * If value is different from Individual Monster, the leader must also be set */ enum class LeaderRelation : uint8_t { None, /** * @brief Minion that sticks to the leader */ Leashed, /** * @brief Minion that was separated from the leader and acts individually until it reaches the leader again */ Separated, }; struct AnimStruct { [[nodiscard]] OptionalCelSprite getCelSpritesForDirection(Direction direction) const { const byte *spriteData = celSpritesForDirections[static_cast(direction)]; if (spriteData == nullptr) return std::nullopt; return CelSprite(spriteData, width); } std::array celSpritesForDirections; uint16_t width; int8_t frames; int8_t rate; }; struct CMonster { _monster_id type; /** placeflag enum as a flags*/ uint8_t placeFlags; std::unique_ptr animData; AnimStruct anims[6]; /** * @brief Returns AnimStruct for specified graphic */ const AnimStruct &getAnimData(MonsterGraphic graphic) const { return anims[static_cast(graphic)]; } std::unique_ptr sounds[4][2]; int8_t corpseId; const MonsterData *data; }; extern CMonster LevelMonsterTypes[MaxLvlMTypes]; struct Monster { // note: missing field _mAFNum const char *name; std::unique_ptr uniqueMonsterTRN; /** * @brief Contains information for current animation */ AnimationInfo animInfo; /** Specifies current goal of the monster */ MonsterGoal goal; /** Specifies monster's behaviour regarding moving and changing goals. */ int goalVar1; /** * @brief Specifies turning direction for @p RoundWalk in most cases. * Used in custom way by @p FallenAi, @p SnakeAi, @p M_FallenFear and @p FallenAi. */ int goalVar2; /** * @brief Controls monster's behaviour regarding special actions. * Used only by @p ScavengerAi and @p MegaAi. */ int goalVar3; int var1; int var2; int var3; int maxHitPoints; int hitPoints; uint32_t flags; /** Seed used to determine item drops on death */ uint32_t rndItemSeed; /** Seed used to determine AI behaviour/sync sounds in multiplayer games? */ uint32_t aiSeed; uint16_t exp; uint16_t hit; uint16_t hit2; uint16_t magicResistance; _speech_id talkMsg; ActorPosition position; /** Usually corresponds to the enemy's future position */ WorldTilePosition enemyPosition; uint8_t levelType; MonsterMode mode; uint8_t pathCount; /** Direction faced by monster (direction enum) */ Direction direction; /** The current target of the monster. An index in to either the player or monster array based on the _meflag value. */ uint8_t enemy; bool isInvalid; _mai_id ai; /** * @brief Specifies monster's behaviour across various actions. * Generally, when monster thinks it decides what to do based on this value, among other things. * Higher values should result in more aggressive behaviour (e.g. some monsters use this to calculate the @p AiDelay). */ uint8_t intelligence; /** Stores information for how many ticks the monster will remain active */ uint8_t activeForTicks; uint8_t uniqType; uint8_t uniqTrans; int8_t corpseId; int8_t whoHit; int8_t level; uint8_t minDamage; uint8_t maxDamage; uint8_t minDamage2; uint8_t maxDamage2; uint8_t armorClass; uint8_t leader; LeaderRelation leaderRelation; uint8_t packSize; int8_t lightId; /** * @brief Sets the current cell sprite to match the desired desiredDirection and animation sequence * @param graphic Animation sequence of interest * @param desiredDirection Desired desiredDirection the monster should be visually facing */ void changeAnimationData(MonsterGraphic graphic, Direction desiredDirection) { auto &animationData = type().getAnimData(graphic); // Passing the Frames and rate properties here is only relevant when initialising a monster, but doesn't cause any harm when switching animations. this->animInfo.changeAnimationData(animationData.getCelSpritesForDirection(desiredDirection), animationData.frames, animationData.rate); } /** * @brief Sets the current cell sprite to match the desired animation sequence using the direction the monster is currently facing * @param graphic Animation sequence of interest */ void changeAnimationData(MonsterGraphic graphic) { this->changeAnimationData(graphic, this->direction); } /** * @brief Check if the correct stand Animation is loaded. This is needed if direction is changed (monster stands and looks at the player). * @param dir direction of the monster */ void checkStandAnimationIsLoaded(Direction dir); /** * @brief Sets mode to MonsterMode::Petrified */ void petrify(); const CMonster &type() const { return LevelMonsterTypes[levelType]; } const MonsterData &data() const { return *type().data; } /** * @brief Returns the network identifier for this monster * * This is currently the index into the Monsters array, but may change in the future. */ [[nodiscard]] size_t getId() const; /** * @brief Is the monster currently walking? */ bool isWalking() const; bool isImmune(missile_id mitype) const; bool isResistant(missile_id mitype) const; bool isPossibleToHit() const; bool tryLiftGargoyle(); }; extern size_t LevelMonsterTypeCount; extern Monster Monsters[MaxMonsters]; extern int ActiveMonsters[MaxMonsters]; extern size_t ActiveMonsterCount; extern int MonsterKillCounts[MaxMonsters]; extern bool sgbSaveSoundOn; void PrepareUniqueMonst(Monster &monster, int uniqindex, int miniontype, int bosspacksize, const UniqueMonsterData &uniqueMonsterData); void InitLevelMonsters(); void GetLevelMTypes(); void InitMonsterGFX(size_t monsterTypeIndex); void WeakenNaKrul(); void InitGolems(); void InitMonsters(); void SetMapMonsters(const uint16_t *dunData, Point startPosition); Monster *AddMonster(Point position, Direction dir, size_t mtype, bool inMap); void AddDoppelganger(Monster &monster); bool M_Talker(const Monster &monster); void M_StartStand(Monster &monster, Direction md); void M_ClearSquares(const Monster &monster); void M_GetKnockback(Monster &monster); void M_StartHit(Monster &monster, int dam); void M_StartHit(Monster &monster, int pnum, int dam); void StartMonsterDeath(Monster &monster, int pnum, bool sendmsg); void M_StartKill(int monsterId, int pnum); void M_SyncStartKill(int monsterId, Point position, int pnum); void M_UpdateLeader(int monsterId); void DoEnding(); void PrepDoEnding(); bool Walk(Monster &monster, Direction md); void GolumAi(int monsterId); void DeleteMonsterList(); void ProcessMonsters(); void FreeMonsters(); bool DirOK(int monsterId, Direction mdir); bool PosOkMissile(Point position); bool LineClearMissile(Point startPoint, Point endPoint); bool LineClear(const std::function &clear, Point startPoint, Point endPoint); void SyncMonsterAnim(Monster &monster); void M_FallenFear(Point position); void PrintMonstHistory(int mt); void PrintUniqueHistory(); void PlayEffect(Monster &monster, int mode); void MissToMonst(Missile &missile, Point position); Monster *MonsterAtPosition(Point position); /** * @brief Check that the given tile is available to the monster */ bool IsTileAvailable(const Monster &monster, Point position); bool IsSkel(_monster_id mt); bool IsGoat(_monster_id mt); bool SpawnSkeleton(Monster *monster, Point position); Monster *PreSpawnSkeleton(); void TalktoMonster(Monster &monster); void SpawnGolem(int id, Point position, Missile &missile); bool CanTalkToMonst(const Monster &monster); int encode_enemy(Monster &monster); void decode_enemy(Monster &monster, int enemyId); } // namespace devilution