From e538b457bfca65adb39625c27057aa2800720726 Mon Sep 17 00:00:00 2001 From: Eric Robinson <68359262+kphoenix137@users.noreply.github.com> Date: Fri, 9 May 2025 12:15:48 -0400 Subject: [PATCH] Refactor Missile direction/frame group handling --- Source/loadsave.cpp | 4 +- Source/misdat.h | 39 ++++++++- Source/missiles.cpp | 192 ++++++++++++++++++++--------------------- Source/missiles.h | 155 ++++++++++++++++++++------------- Source/monster.cpp | 2 +- Source/portal.cpp | 2 +- test/missiles_test.cpp | 6 +- 7 files changed, 230 insertions(+), 170 deletions(-) diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index a57e7c9ed..a4f645cae 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -771,7 +771,7 @@ void LoadMissile(LoadHelper *file) missile.position.start.y = file->NextLE(); missile.position.traveled.deltaX = file->NextLE(); missile.position.traveled.deltaY = file->NextLE(); - missile._mimfnum = file->NextLE(); + missile.setFrameGroupRaw(file->NextLE()); missile._mispllvl = file->NextLE(); missile._miDelFlag = file->NextBool32(); missile._miAnimType = static_cast(file->NextLE()); @@ -1541,7 +1541,7 @@ void SaveMissile(SaveHelper *file, const Missile &missile) file->WriteLE(missile.position.start.y); file->WriteLE(missile.position.traveled.deltaX); file->WriteLE(missile.position.traveled.deltaY); - file->WriteLE(missile._mimfnum); + file->WriteLE(missile.getFrameGroupRaw()); file->WriteLE(missile._mispllvl); file->WriteLE(missile._miDelFlag ? 1 : 0); file->WriteLE(static_cast(missile._miAnimType)); diff --git a/Source/misdat.h b/Source/misdat.h index 4d50bb9b7..3be438c6a 100644 --- a/Source/misdat.h +++ b/Source/misdat.h @@ -129,6 +129,41 @@ enum class MissileDataFlags : uint8_t { }; use_enum_as_flags(MissileDataFlags); +/** + * Represent a more fine-grained direction than the 8 value Direction enum. + * + * This is used when rendering projectiles like arrows which have additional sprites for "half-winds" on a 16-point compass. + * The sprite sheets are typically 0-indexed and use the following layout (relative to the screen projection) + * + * W WSW SW SSW S + * ^ + * WNW | SSE + * | + * NW -------+------> SE + * | + * NNW | ESE + * | + * N NNE NE ENE E + */ +enum class Direction16 : uint8_t { + South, + South_SouthWest, + SouthWest, + West_SouthWest, + West, + West_NorthWest, + NorthWest, + North_NorthWest, + North, + North_NorthEast, + NorthEast, + East_NorthEast, + East, + East_SouthEast, + SouthEast, + South_SouthEast, +}; + struct MissileData { using AddFn = void (*)(Missile &, AddMissileParameter &); using ProcessFn = void (*)(Missile &); @@ -198,11 +233,11 @@ struct MissileFileData { * @param direction One of the 16 directions. Valid range: [0, 15]. * @return OptionalClxSpriteList */ - [[nodiscard]] OptionalClxSpriteList spritesForDirection(size_t direction) const + [[nodiscard]] OptionalClxSpriteList spritesForDirection(Direction16 direction) const { if (!sprites) return std::nullopt; - return sprites->isSheet() ? sprites->sheet()[direction] : sprites->list(); + return sprites->isSheet() ? sprites->sheet()[static_cast(direction)] : sprites->list(); } }; diff --git a/Source/missiles.cpp b/Source/missiles.cpp index ccc367006..57c8e7a04 100644 --- a/Source/missiles.cpp +++ b/Source/missiles.cpp @@ -40,6 +40,38 @@ namespace devilution { std::list Missiles; bool MissilePreFlag; +void Missile::setAnimation(MissileGraphicID animtype) +{ + const int dir = _mimfnum; + + if (animtype >= MissileGraphicID::None) { + _miAnimType = MissileGraphicID::None; + _miAnimData = std::nullopt; + _miAnimWidth = 0; + _miAnimWidth2 = 0; + _miAnimFlags = MissileGraphicsFlags::None; + _miAnimDelay = 0; + _miAnimLen = 0; + _miAnimCnt = 0; + _miAnimFrame = 1; + return; + } + + const MissileFileData &missileData = GetMissileSpriteData(animtype); + + _miAnimType = animtype; + _miAnimFlags = missileData.flags; + if (!HeadlessMode) { + _miAnimData = missileData.spritesForDirection(static_cast(dir)); + } + _miAnimDelay = missileData.animDelay(dir); + _miAnimLen = missileData.animLen(dir); + _miAnimWidth = missileData.animWidth; + _miAnimWidth2 = missileData.animWidth2; + _miAnimCnt = 0; + _miAnimFrame = 1; +} + namespace { int AddClassHealingBonus(int hp, HeroClass heroClass) @@ -175,7 +207,7 @@ void MoveMissilePos(Missile &missile) { Direction moveDirection; - switch (static_cast(missile._mimfnum)) { + switch (missile.getDirection()) { case Direction::East: moveDirection = Direction::SouthEast; break; @@ -399,14 +431,14 @@ void RotateBlockedMissile(Missile &missile) return; } - int dir = missile._mimfnum + rotation; + int dir = missile.getFrameGroupRaw() + rotation; int mAnimFAmt = GetMissileSpriteData(missile._miAnimType).animFAmt; if (dir < 0) dir = mAnimFAmt - 1; else if (dir >= mAnimFAmt) dir = 0; - SetMissDir(missile, dir); + missile.setFrameGroupRaw(dir); } void CheckMissileCol(Missile &missile, DamageType damageType, int minDamage, int maxDamage, bool isDamageShifted, Point position, bool dontDeleteOnCollision) @@ -598,38 +630,6 @@ void MoveMissileAndCheckMissileCol(Missile &missile, DamageType damageType, int missile.lastCollisionTargetHash = tileTargetHash; } -void SetMissAnim(Missile &missile, MissileGraphicID animtype) -{ - const int dir = missile._mimfnum; - - if (animtype >= MissileGraphicID::None) { - missile._miAnimType = MissileGraphicID::None; - missile._miAnimData = std::nullopt; - missile._miAnimWidth = 0; - missile._miAnimWidth2 = 0; - missile._miAnimFlags = MissileGraphicsFlags::None; - missile._miAnimDelay = 0; - missile._miAnimLen = 0; - missile._miAnimCnt = 0; - missile._miAnimFrame = 1; - return; - } - - const MissileFileData &missileData = GetMissileSpriteData(animtype); - - missile._miAnimType = animtype; - missile._miAnimFlags = missileData.flags; - if (!HeadlessMode) { - missile._miAnimData = missileData.spritesForDirection(static_cast(dir)); - } - missile._miAnimDelay = missileData.animDelay(dir); - missile._miAnimLen = missileData.animLen(dir); - missile._miAnimWidth = missileData.animWidth; - missile._miAnimWidth2 = missileData.animWidth2; - missile._miAnimCnt = 0; - missile._miAnimFrame = 1; -} - void AddRune(Missile &missile, Point dst, MissileID missileID) { if (LineClearMissile(missile.position.start, dst)) { @@ -692,7 +692,7 @@ bool GuardianTryFireAt(Missile &missile, Point target) Direction dir = GetDirection(position, target); AddMissile(position, target, dir, MissileID::Firebolt, TARGET_MONSTERS, missile._misource, missile._midam, missile.sourcePlayer()->GetSpellLevel(SpellID::Guardian), &missile); - SetMissDir(missile, 2); + missile.setFrameGroup(GuardianFrame::Attack); missile.var2 = 3; return true; @@ -1122,12 +1122,6 @@ bool PlayerMHit(Player &player, Monster *monster, int dist, int mind, int maxd, return true; } -void SetMissDir(Missile &missile, int dir) -{ - missile._mimfnum = dir; - SetMissAnim(missile, missile._miAnimType); -} - void InitMissiles() { Player &myPlayer = *MyPlayer; @@ -1540,7 +1534,7 @@ void AddBigExplosion(Missile &missile, AddMissileParameter & /*parameter*/) CheckMissileCol(missile, damageType, dmg, dmg, false, position, true); } missile._mlid = AddLight(missile.position.start, 8); - SetMissDir(missile, 0); + missile.setDefaultFrameGroup(); missile.duration = missile._miAnimLen - 1; } @@ -1555,7 +1549,7 @@ void AddImmolation(Missile &missile, AddMissileParameter ¶meter) sp += std::min(missile._mispllvl, 34); } UpdateMissileVelocity(missile, dst, sp); - SetMissDir(missile, GetDirection16(missile.position.start, dst)); + missile.setDirection(GetDirection16(missile.position.start, dst)); missile.duration = 256; missile._mlid = AddLight(missile.position.start, 8); } @@ -1693,7 +1687,7 @@ void AddElementalArrow(Missile &missile, AddMissileParameter ¶meter) } UpdateMissileVelocity(missile, dst, av); - SetMissDir(missile, GetDirection16(missile.position.start, dst)); + missile.setDirection(GetDirection16(missile.position.start, dst)); missile.duration = 256; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; @@ -1802,7 +1796,7 @@ void AddFirebolt(Missile &missile, AddMissileParameter ¶meter) } } UpdateMissileVelocity(missile, dst, sp); - SetMissDir(missile, GetDirection16(missile.position.start, dst)); + missile.setDirection(GetDirection16(missile.position.start, dst)); missile.duration = 256; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; @@ -1913,7 +1907,7 @@ void AddFireball(Missile &missile, AddMissileParameter ¶meter) missile._midam = ScaleSpellEffect(dmg, missile._mispllvl); } UpdateMissileVelocity(missile, dst, sp); - SetMissDir(missile, GetDirection16(missile.position.start, dst)); + missile.setDirection(GetDirection16(missile.position.start, dst)); missile.duration = 256; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; @@ -1953,16 +1947,16 @@ void AddMissileExplosion(Missile &missile, AddMissileParameter ¶meter) if (missile._micaster != TARGET_MONSTERS && missile._misource >= 0) { switch (Monsters[missile._misource].type().type) { case MT_SUCCUBUS: - SetMissAnim(missile, MissileGraphicID::BloodStarExplosion); + missile.setAnimation(MissileGraphicID::BloodStarExplosion); break; case MT_SNOWWICH: - SetMissAnim(missile, MissileGraphicID::BloodStarBlueExplosion); + missile.setAnimation(MissileGraphicID::BloodStarBlueExplosion); break; case MT_HLSPWN: - SetMissAnim(missile, MissileGraphicID::BloodStarRedExplosion); + missile.setAnimation(MissileGraphicID::BloodStarRedExplosion); break; case MT_SOLBRNR: - SetMissAnim(missile, MissileGraphicID::BloodStarYellowExplosion); + missile.setAnimation(MissileGraphicID::BloodStarYellowExplosion); break; default: break; @@ -1982,9 +1976,9 @@ void AddWeaponExplosion(Missile &missile, AddMissileParameter ¶meter) { missile.var2 = parameter.dst.x; if (parameter.dst.x == 1) - SetMissAnim(missile, MissileGraphicID::MagmaBallExplosion); + missile.setAnimation(MissileGraphicID::MagmaBallExplosion); else - SetMissAnim(missile, MissileGraphicID::ChargedBolt); + missile.setAnimation(MissileGraphicID::ChargedBolt); missile.duration = missile._miAnimLen - 1; } @@ -2164,7 +2158,7 @@ namespace { void InitMissileAnimationFromMonster(Missile &mis, Direction midir, const Monster &mon, MonsterGraphic graphic) { const AnimStruct &anim = mon.type().getAnimData(graphic); - mis._mimfnum = static_cast(midir); + mis.setDirection(midir); mis._miAnimFlags = MissileGraphicsFlags::None; ClxSpriteList sprites = *anim.spritesForDirection(midir); const uint16_t width = sprites[0].width(); @@ -2215,17 +2209,17 @@ void AddGenericMagicMissile(Missile &missile, AddMissileParameter ¶meter) if (missile._micaster != TARGET_MONSTERS && missile._misource > 0) { Monster &monster = Monsters[missile._misource]; if (monster.type().type == MT_SUCCUBUS) - SetMissAnim(missile, MissileGraphicID::BloodStar); + missile.setAnimation(MissileGraphicID::BloodStar); if (monster.type().type == MT_SNOWWICH) - SetMissAnim(missile, MissileGraphicID::BloodStarBlue); + missile.setAnimation(MissileGraphicID::BloodStarBlue); if (monster.type().type == MT_HLSPWN) - SetMissAnim(missile, MissileGraphicID::BloodStarRed); + missile.setAnimation(MissileGraphicID::BloodStarRed); if (monster.type().type == MT_SOLBRNR) - SetMissAnim(missile, MissileGraphicID::BloodStarYellow); + missile.setAnimation(MissileGraphicID::BloodStarYellow); } if (GetMissileSpriteData(missile._miAnimType).animFAmt == 16) { - SetMissDir(missile, GetDirection16(missile.position.start, dst)); + missile.setDirection(GetDirection16(missile.position.start, dst)); } if (missile._midam == 0) { @@ -2248,7 +2242,7 @@ void AddGenericMagicMissile(Missile &missile, AddMissileParameter ¶meter) void AddAcid(Missile &missile, AddMissileParameter ¶meter) { UpdateMissileVelocity(missile, parameter.dst, 16); - SetMissDir(missile, GetDirection16(missile.position.start, parameter.dst)); + missile.setDirection(GetDirection16(missile.position.start, parameter.dst)); if (!gbIsHellfire || (missile.position.velocity.deltaX & 0xFFFF0000) != 0 || (missile.position.velocity.deltaY & 0xFFFF0000) != 0) missile.duration = 5 * (Monsters[missile._misource].intelligence + 4); else @@ -2427,7 +2421,7 @@ void AddElemental(Missile &missile, AddMissileParameter ¶meter) missile._midam = ScaleSpellEffect(dmg, missile._mispllvl) / 2; UpdateMissileVelocity(missile, dst, 16); - SetMissDir(missile, GetDirection(missile.position.start, dst)); + missile.setDirection(GetDirection(missile.position.start, dst)); missile.duration = 256; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; @@ -2654,7 +2648,7 @@ void AddHolyBolt(Missile &missile, AddMissileParameter ¶meter) Player &player = Players[missile._misource]; UpdateMissileVelocity(missile, dst, sp); - SetMissDir(missile, GetDirection16(missile.position.start, dst)); + missile.setDirection(GetDirection16(missile.position.start, dst)); missile.duration = 256; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; @@ -2697,7 +2691,7 @@ void AddBoneSpirit(Missile &missile, AddMissileParameter ¶meter) dst += parameter.midir; } UpdateMissileVelocity(missile, dst, 16); - SetMissDir(missile, GetDirection(missile.position.start, dst)); + missile.setDirection(GetDirection(missile.position.start, dst)); missile.duration = 256; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; @@ -2760,9 +2754,9 @@ Missile *AddMissile(WorldTilePosition src, WorldTilePosition dst, Direction midi } if (missile._miAnimType == MissileGraphicID::None || GetMissileSpriteData(missile._miAnimType).animFAmt < 8) - SetMissDir(missile, 0); + missile.setDefaultFrameGroup(); else - SetMissDir(missile, midir); + missile.setDirection(midir); if (!lSFX) { lSFX = missileData.castSound; @@ -2809,7 +2803,7 @@ void ProcessElementalArrow(Missile &missile) } MoveMissileAndCheckMissileCol(missile, DamageType::Physical, mind, maxd, true, false); if (missile.duration == 0) { - missile._mimfnum = 0; + missile.setDefaultFrameGroup(); missile.duration = missile._miAnimLen - 1; missile.position.StopMissile(); @@ -2848,7 +2842,7 @@ void ProcessElementalArrow(Missile &missile) app_fatal(StrCat("wrong missile ID ", static_cast(missile._mitype))); break; } - SetMissAnim(missile, eAnim); + missile.setAnimation(eAnim); CheckMissileCol(missile, damageType, eMind, eMaxd, false, missile.position.tile, true); } else { if (missile.position.tile != Point { missile.var1, missile.var2 }) { @@ -2904,7 +2898,7 @@ void ProcessGenericProjectile(Missile &missile) if (missile.duration == 0) { missile._miDelFlag = true; Point dst = { 0, 0 }; - auto dir = static_cast(missile._mimfnum); + Direction dir = missile.getDirection(); switch (missile._mitype) { case MissileID::Firebolt: case MissileID::MagmaBall: @@ -2975,10 +2969,10 @@ void ProcessAcidPuddle(Missile &missile) CheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, true, missile.position.tile, false); missile.duration = range; if (range == 0) { - if (missile._mimfnum != 0) { + if (missile.getFrameGroup() != AcidPuddleFrame::Idle) { missile._miDelFlag = true; } else { - SetMissDir(missile, 1); + missile.setFrameGroup(AcidPuddleFrame::End); missile.duration = missile._miAnimLen; } } @@ -2992,11 +2986,11 @@ void ProcessFireWall(Missile &missile) missile.duration--; if (missile.duration == missile.var1) { - SetMissDir(missile, 1); + missile.setFrameGroup(FireWallFrame::Idle); missile._miAnimFrame = GenerateRnd(11) + 1; } if (missile.duration == missile._miAnimLen - 1) { - SetMissDir(missile, 0); + missile.setFrameGroup(FireWallFrame::Start); missile._miAnimFrame = 13; missile._miAnimAdd = -1; } @@ -3006,7 +3000,7 @@ void ProcessFireWall(Missile &missile) AddUnLight(missile._mlid); } constexpr int MaxExpLightIndex = ExpLightLen - 1; - if (missile._mimfnum == 0 && missile.duration != 0 && missile.var2 <= MaxExpLightIndex * 2) { + if (missile.getFrameGroup() == FireWallFrame::Start && missile.duration != 0 && missile.var2 <= MaxExpLightIndex * 2) { if (missile.var2 == 0) missile._mlid = AddLight(missile.position.tile, ExpLight[0]); int expLightIndex = MaxExpLightIndex - std::abs(missile.var2 - MaxExpLightIndex); @@ -3071,8 +3065,8 @@ void ProcessFireball(Missile &missile) || (TransList[dTransVal[missilePosition.x][missilePosition.y - 1]] && TileHasAny(missilePosition + Direction::NorthEast, TileProperties::Solid)))) { missile.position.offset.deltaX -= 32; } - missile._mimfnum = 0; - SetMissAnim(missile, MissileGraphicID::BigExplosion); + missile.setDefaultFrameGroup(); + missile.setAnimation(MissileGraphicID::BigExplosion); missile.duration = missile._miAnimLen - 1; missile.position.velocity = {}; } else if (missile.position.tile != Point { missile.var1, missile.var2 }) { @@ -3314,8 +3308,8 @@ void ProcessTownPortal(Missile &missile) if (missile.duration > 1) missile.duration--; if (missile.duration == missile.var1) - SetMissDir(missile, 1); - if (leveltype != DTYPE_TOWN && missile._mimfnum != 1 && missile.duration != 0) { + missile.setFrameGroup(PortalFrame::Idle); + if (leveltype != DTYPE_TOWN && missile.getFrameGroup() != PortalFrame::Idle && missile.duration != 0) { if (missile.var2 == 0) missile._mlid = AddLight(missile.position.tile, 1); ChangeLight(missile._mlid, missile.position.tile, expLight[missile.var2]); @@ -3405,7 +3399,7 @@ void ProcessFlameWave(Missile &missile) missile.var1++; if (missile.var1 == missile._miAnimLen) { - SetMissDir(missile, 1); + missile.setFrameGroup(FireWallFrame::Idle); missile._miAnimFrame = GenerateRnd(11) + 1; } int j = missile.duration; @@ -3435,8 +3429,8 @@ void ProcessGuardian(Missile &missile) if (missile.var2 > 0) { missile.var2--; } - if (missile.duration == missile.var1 || (missile._mimfnum == 2 && missile.var2 == 0)) { - SetMissDir(missile, 1); + if (missile.duration == missile.var1 || (missile.getFrameGroup() == GuardianFrame::Attack && missile.var2 == 0)) { + missile.setFrameGroup(GuardianFrame::Idle); } Point position = missile.position.tile; @@ -3478,7 +3472,7 @@ void ProcessGuardian(Missile &missile) } if (missile.duration == 14) { - SetMissDir(missile, 0); + missile.setFrameGroup(GuardianFrame::Start); missile._miAnimFrame = 15; missile._miAnimAdd = -1; } @@ -3633,9 +3627,9 @@ void ProcessStoneCurse(Missile &missile) missile.duration--; Monster &monster = Monsters[missile.var2]; if (monster.hitPoints == 0 && missile._miAnimType != MissileGraphicID::StoneCurseShatter) { - missile._mimfnum = 0; + missile.setDefaultFrameGroup(); missile._miDrawFlag = true; - SetMissAnim(missile, MissileGraphicID::StoneCurseShatter); + missile.setAnimation(MissileGraphicID::StoneCurseShatter); missile.duration = 11; } if (monster.mode != MonsterMode::Petrified) { @@ -3965,10 +3959,10 @@ void ProcessChargedBolt(Missile &missile) MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, false, false); if (missile._miHitFlag) { missile.var1 = 8; - missile._mimfnum = 0; + missile.setDefaultFrameGroup(); missile.position.offset = { 0, 0 }; missile.position.velocity = {}; - SetMissAnim(missile, MissileGraphicID::Lightning); + missile.setAnimation(MissileGraphicID::Lightning); missile.duration = missile._miAnimLen; } ChangeLight(missile._mlid, missile.position.tile, missile.var1); @@ -3987,8 +3981,8 @@ void ProcessHolyBolt(Missile &missile) int dam = missile._midam; MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), dam, dam, true, true); if (missile.duration == 0) { - missile._mimfnum = 0; - SetMissAnim(missile, MissileGraphicID::HolyBoltExplosion); + missile.setDefaultFrameGroup(); + missile.setAnimation(MissileGraphicID::HolyBoltExplosion); missile.duration = missile._miAnimLen - 1; missile.position.StopMissile(); } else { @@ -4047,11 +4041,11 @@ void ProcessElemental(Missile &missile) auto *nextMonster = FindClosest(missilePosition, 19); if (nextMonster != nullptr) { Direction sd = GetDirection(missilePosition, nextMonster->position.tile); - SetMissDir(missile, sd); + missile.setDirection(sd); UpdateMissileVelocity(missile, nextMonster->position.tile, 16); } else { Direction sd = Players[missile._misource]._pdir; - SetMissDir(missile, sd); + missile.setDirection(sd); UpdateMissileVelocity(missile, missilePosition + sd, 16); } } @@ -4061,8 +4055,8 @@ void ProcessElemental(Missile &missile) ChangeLight(missile._mlid, missilePosition, 8); } if (missile.duration == 0) { - missile._mimfnum = 0; - SetMissAnim(missile, MissileGraphicID::BigExplosion); + missile.setDefaultFrameGroup(); + missile.setAnimation(MissileGraphicID::BigExplosion); missile.duration = missile._miAnimLen - 1; missile.position.StopMissile(); } @@ -4074,7 +4068,7 @@ void ProcessBoneSpirit(Missile &missile) { missile.duration--; int dam = missile._midam; - if (missile._mimfnum == 8) { + if (missile.getDirection() == Direction::NoDirection) { ChangeLight(missile._mlid, missile.position.tile, missile._miAnimFrame); if (missile.duration == 0) { missile._miDelFlag = true; @@ -4092,11 +4086,11 @@ void ProcessBoneSpirit(Missile &missile) auto *monster = FindClosest(c, 19); if (monster != nullptr) { missile._midam = monster->hitPoints >> 7; - SetMissDir(missile, GetDirection(c, monster->position.tile)); + missile.setDirection(GetDirection(c, monster->position.tile)); UpdateMissileVelocity(missile, monster->position.tile, 16); } else { Direction sd = Players[missile._misource]._pdir; - SetMissDir(missile, sd); + missile.setDirection(sd); UpdateMissileVelocity(missile, c + sd, 16); } } @@ -4106,7 +4100,7 @@ void ProcessBoneSpirit(Missile &missile) ChangeLight(missile._mlid, c, 8); } if (missile.duration == 0) { - SetMissDir(missile, 8); + missile.setDirection(Direction::NoDirection); missile.position.velocity = {}; missile.duration = 7; } @@ -4129,9 +4123,9 @@ void ProcessRedPortal(Missile &missile) if (missile.duration > 1) missile.duration--; if (missile.duration == missile.var1) - SetMissDir(missile, 1); + missile.setFrameGroup(RedPortalFrame::Idle); - if (leveltype != DTYPE_TOWN && missile._mimfnum != 1 && missile.duration != 0) { + if (leveltype != DTYPE_TOWN && missile.getFrameGroup() != RedPortalFrame::Idle && missile.duration != 0) { if (missile.var2 == 0) missile._mlid = AddLight(missile.position.tile, 1); ChangeLight(missile._mlid, missile.position.tile, expLight[missile.var2]); @@ -4203,7 +4197,7 @@ void SetUpMissileAnimationData() continue; if (missile._mitype != MissileID::Rhino) { - missile._miAnimData = GetMissileSpriteData(missile._miAnimType).spritesForDirection(missile._mimfnum); + missile._miAnimData = GetMissileSpriteData(missile._miAnimType).spritesForDirection(missile.getDirection16()); continue; } @@ -4217,7 +4211,7 @@ void SetUpMissileAnimationData() } else { graphic = MonsterGraphic::Walk; } - missile._miAnimData = mon.getAnimData(graphic).spritesForDirection(static_cast(missile._mimfnum)); + missile._miAnimData = mon.getAnimData(graphic).spritesForDirection(missile.getDirection()); } } diff --git a/Source/missiles.h b/Source/missiles.h index b9af0d8a2..1b7e110a2 100644 --- a/Source/missiles.h +++ b/Source/missiles.h @@ -54,52 +54,47 @@ struct MissilePosition { } }; -/** - * Represent a more fine-grained direction than the 8 value Direction enum. - * - * This is used when rendering projectiles like arrows which have additional sprites for "half-winds" on a 16-point compass. - * The sprite sheets are typically 0-indexed and use the following layout (relative to the screen projection) - * - * W WSW SW SSW S - * ^ - * WNW | SSE - * | - * NW -------+------> SE - * | - * NNW | ESE - * | - * N NNE NE ENE E - */ -enum class Direction16 : uint8_t { - South, - South_SouthWest, - SouthWest, - West_SouthWest, - West, - West_NorthWest, - NorthWest, - North_NorthWest, - North, - North_NorthEast, - NorthEast, - East_NorthEast, - East, - East_SouthEast, - SouthEast, - South_SouthEast, -}; - enum class MissileSource : uint8_t { Player, Monster, Trap, }; +enum class GuardianFrame : uint8_t { + Start = 0, + Idle = 1, + Attack = 2, +}; + +enum class AcidPuddleFrame : uint8_t { + Idle = 0, + End = 1, +}; + +enum class FireWallFrame : uint8_t { + Start = 0, + Idle = 1, +}; + +enum class PortalFrame : uint8_t { + Start = 0, + Idle = 1, +}; + +enum class RedPortalFrame : uint8_t { + Start = 0, + Idle = 1, +}; + struct Missile { /** Type of projectile */ MissileID _mitype; MissilePosition position; + +private: int _mimfnum; // The direction of the missile (direction enum) + +public: int _mispllvl; bool _miDelFlag; // Indicate whether the missile should be deleted MissileGraphicID _miAnimType; @@ -175,6 +170,69 @@ struct Missile { return MissileSource::Monster; return MissileSource::Player; } + + void setAnimation(MissileGraphicID animtype); + + /** + * @brief Sets the missile sprite to the given sheet frame + * @param dir Sprite frame + */ + void setFrameGroupRaw(int frameGroup) + { + _mimfnum = frameGroup; + setAnimation(_miAnimType); + } + + void setDefaultFrameGroup() + { + setFrameGroupRaw(0); + } + + template + void setFrameGroup(FrameEnum frameGroup) + { + setFrameGroupRaw(static_cast(frameGroup)); + } + + /** + * @brief Sets the sprite for this missile so it matches the given Direction + * @param dir Desired facing + */ + void setDirection(Direction dir) + { + setFrameGroupRaw(static_cast(dir)); + } + + /** + * @brief Sets the sprite for this missile so it matches the given Direction16 + * @param dir Desired facing at a 22.8125 degree resolution + */ + void setDirection(Direction16 dir) + { + setFrameGroupRaw(static_cast(dir)); + } + + int getFrameGroupRaw() const + { + return _mimfnum; + } + + template + FrameEnum getFrameGroup() const + { + static_assert(std::is_enum_v, "Frame group must be an enum"); + return static_cast(_mimfnum); + } + + [[nodiscard]] Direction getDirection() const + { + return static_cast(_mimfnum); + } + + [[nodiscard]] Direction16 getDirection16() const + { + return static_cast(_mimfnum); + } }; extern std::list Missiles; @@ -214,33 +272,6 @@ bool PlayerMHit(Player &player, Monster *monster, int dist, int mind, int maxd, */ bool IsMissileBlockedByTile(Point position); -/** - * @brief Sets the missile sprite to the given sheet frame - * @param missile this object - * @param dir Sprite frame, typically representing a direction but there are some exceptions (arrows being 1 indexed, directionless spells) - */ -void SetMissDir(Missile &missile, int dir); - -/** - * @brief Sets the sprite for this missile so it matches the given Direction - * @param missile this object - * @param dir Desired facing - */ -inline void SetMissDir(Missile &missile, Direction dir) -{ - SetMissDir(missile, static_cast(dir)); -} - -/** - * @brief Sets the sprite for this missile so it matches the given Direction16 - * @param missile this object - * @param dir Desired facing at a 22.8125 degree resolution - */ -inline void SetMissDir(Missile &missile, Direction16 dir) -{ - SetMissDir(missile, static_cast(dir)); -} - void InitMissiles(); struct AddMissileParameter { diff --git a/Source/monster.cpp b/Source/monster.cpp index a4c050341..8c5078022 100644 --- a/Source/monster.cpp +++ b/Source/monster.cpp @@ -4452,7 +4452,7 @@ void MissToMonst(Missile &missile, Point position) Point oldPosition = missile.position.tile; monster.occupyTile(position, false); - monster.direction = static_cast(missile._mimfnum); + monster.direction = missile.getDirection(); monster.position.tile = position; M_StartStand(monster, monster.direction); M_StartHit(monster, 0); diff --git a/Source/portal.cpp b/Source/portal.cpp index c206bcf41..4609a912a 100644 --- a/Source/portal.cpp +++ b/Source/portal.cpp @@ -53,7 +53,7 @@ void AddPortalMissile(const Player &player, Point position, bool sync) if (missile != nullptr) { // Don't show portal opening animation if we sync existing portals if (sync) - SetMissDir(*missile, 1); + missile->setFrameGroup(PortalFrame::Idle); if (leveltype != DTYPE_TOWN) missile->_mlid = AddLight(missile->position.tile, 15); diff --git a/test/missiles_test.cpp b/test/missiles_test.cpp index 09b1c5904..f8b5f283d 100644 --- a/test/missiles_test.cpp +++ b/test/missiles_test.cpp @@ -29,9 +29,9 @@ void TestAnimatedMissileRotatesUniformly(Missile &missile, int startingDir, int { ankerl::unordered_dense::map observed {}; for (auto i = 0; i < 100; i++) { - missile._mimfnum = startingDir; + missile.setFrameGroupRaw(startingDir); TestRotateBlockedMissile(missile); - observed[missile._mimfnum]++; + observed[missile.getFrameGroupRaw()]++; } EXPECT_THAT(observed, UnorderedElementsAre(Pair(leftDir, AllOf(Gt(30U), Lt(70U))), Pair(rightDir, AllOf(Gt(30U), Lt(70U))))) << "Animated missiles should rotate either direction roughly 50% of the time"; @@ -58,7 +58,7 @@ TEST(Missiles, RotateBlockedMissileArrow) // All other missiles use the number of 0-indexed sprites defined in MissileSpriteData missile = *AddMissile({ 0, 0 }, { 0, 0 }, Direction::South, MissileID::Firebolt, TARGET_MONSTERS, player, 0, 0); - EXPECT_EQ(missile._mimfnum, 0); + EXPECT_EQ(missile.getFrameGroupRaw(), 0); TestAnimatedMissileRotatesUniformly(missile, 5, 4, 6); TestAnimatedMissileRotatesUniformly(missile, 0, 15, 1); TestAnimatedMissileRotatesUniformly(missile, 15, 14, 0);