Browse Source

Refactor Missile direction/frame group handling

pull/7986/head
Eric Robinson 10 months ago committed by GitHub
parent
commit
e538b457bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      Source/loadsave.cpp
  2. 39
      Source/misdat.h
  3. 192
      Source/missiles.cpp
  4. 155
      Source/missiles.h
  5. 2
      Source/monster.cpp
  6. 2
      Source/portal.cpp
  7. 6
      test/missiles_test.cpp

4
Source/loadsave.cpp

@ -771,7 +771,7 @@ void LoadMissile(LoadHelper *file)
missile.position.start.y = file->NextLE<int32_t>(); missile.position.start.y = file->NextLE<int32_t>();
missile.position.traveled.deltaX = file->NextLE<int32_t>(); missile.position.traveled.deltaX = file->NextLE<int32_t>();
missile.position.traveled.deltaY = file->NextLE<int32_t>(); missile.position.traveled.deltaY = file->NextLE<int32_t>();
missile._mimfnum = file->NextLE<int32_t>(); missile.setFrameGroupRaw(file->NextLE<int32_t>());
missile._mispllvl = file->NextLE<int32_t>(); missile._mispllvl = file->NextLE<int32_t>();
missile._miDelFlag = file->NextBool32(); missile._miDelFlag = file->NextBool32();
missile._miAnimType = static_cast<MissileGraphicID>(file->NextLE<uint8_t>()); missile._miAnimType = static_cast<MissileGraphicID>(file->NextLE<uint8_t>());
@ -1541,7 +1541,7 @@ void SaveMissile(SaveHelper *file, const Missile &missile)
file->WriteLE<int32_t>(missile.position.start.y); file->WriteLE<int32_t>(missile.position.start.y);
file->WriteLE<int32_t>(missile.position.traveled.deltaX); file->WriteLE<int32_t>(missile.position.traveled.deltaX);
file->WriteLE<int32_t>(missile.position.traveled.deltaY); file->WriteLE<int32_t>(missile.position.traveled.deltaY);
file->WriteLE<int32_t>(missile._mimfnum); file->WriteLE<int32_t>(missile.getFrameGroupRaw());
file->WriteLE<int32_t>(missile._mispllvl); file->WriteLE<int32_t>(missile._mispllvl);
file->WriteLE<uint32_t>(missile._miDelFlag ? 1 : 0); file->WriteLE<uint32_t>(missile._miDelFlag ? 1 : 0);
file->WriteLE<uint8_t>(static_cast<uint8_t>(missile._miAnimType)); file->WriteLE<uint8_t>(static_cast<uint8_t>(missile._miAnimType));

39
Source/misdat.h

@ -129,6 +129,41 @@ enum class MissileDataFlags : uint8_t {
}; };
use_enum_as_flags(MissileDataFlags); 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 { struct MissileData {
using AddFn = void (*)(Missile &, AddMissileParameter &); using AddFn = void (*)(Missile &, AddMissileParameter &);
using ProcessFn = void (*)(Missile &); using ProcessFn = void (*)(Missile &);
@ -198,11 +233,11 @@ struct MissileFileData {
* @param direction One of the 16 directions. Valid range: [0, 15]. * @param direction One of the 16 directions. Valid range: [0, 15].
* @return OptionalClxSpriteList * @return OptionalClxSpriteList
*/ */
[[nodiscard]] OptionalClxSpriteList spritesForDirection(size_t direction) const [[nodiscard]] OptionalClxSpriteList spritesForDirection(Direction16 direction) const
{ {
if (!sprites) if (!sprites)
return std::nullopt; return std::nullopt;
return sprites->isSheet() ? sprites->sheet()[direction] : sprites->list(); return sprites->isSheet() ? sprites->sheet()[static_cast<size_t>(direction)] : sprites->list();
} }
}; };

192
Source/missiles.cpp

@ -40,6 +40,38 @@ namespace devilution {
std::list<Missile> Missiles; std::list<Missile> Missiles;
bool MissilePreFlag; 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<Direction16>(dir));
}
_miAnimDelay = missileData.animDelay(dir);
_miAnimLen = missileData.animLen(dir);
_miAnimWidth = missileData.animWidth;
_miAnimWidth2 = missileData.animWidth2;
_miAnimCnt = 0;
_miAnimFrame = 1;
}
namespace { namespace {
int AddClassHealingBonus(int hp, HeroClass heroClass) int AddClassHealingBonus(int hp, HeroClass heroClass)
@ -175,7 +207,7 @@ void MoveMissilePos(Missile &missile)
{ {
Direction moveDirection; Direction moveDirection;
switch (static_cast<Direction>(missile._mimfnum)) { switch (missile.getDirection()) {
case Direction::East: case Direction::East:
moveDirection = Direction::SouthEast; moveDirection = Direction::SouthEast;
break; break;
@ -399,14 +431,14 @@ void RotateBlockedMissile(Missile &missile)
return; return;
} }
int dir = missile._mimfnum + rotation; int dir = missile.getFrameGroupRaw() + rotation;
int mAnimFAmt = GetMissileSpriteData(missile._miAnimType).animFAmt; int mAnimFAmt = GetMissileSpriteData(missile._miAnimType).animFAmt;
if (dir < 0) if (dir < 0)
dir = mAnimFAmt - 1; dir = mAnimFAmt - 1;
else if (dir >= mAnimFAmt) else if (dir >= mAnimFAmt)
dir = 0; dir = 0;
SetMissDir(missile, dir); missile.setFrameGroupRaw(dir);
} }
void CheckMissileCol(Missile &missile, DamageType damageType, int minDamage, int maxDamage, bool isDamageShifted, Point position, bool dontDeleteOnCollision) 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; 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<size_t>(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) void AddRune(Missile &missile, Point dst, MissileID missileID)
{ {
if (LineClearMissile(missile.position.start, dst)) { if (LineClearMissile(missile.position.start, dst)) {
@ -692,7 +692,7 @@ bool GuardianTryFireAt(Missile &missile, Point target)
Direction dir = GetDirection(position, target); Direction dir = GetDirection(position, target);
AddMissile(position, target, dir, MissileID::Firebolt, TARGET_MONSTERS, missile._misource, missile._midam, missile.sourcePlayer()->GetSpellLevel(SpellID::Guardian), &missile); AddMissile(position, target, dir, MissileID::Firebolt, TARGET_MONSTERS, missile._misource, missile._midam, missile.sourcePlayer()->GetSpellLevel(SpellID::Guardian), &missile);
SetMissDir(missile, 2); missile.setFrameGroup<GuardianFrame>(GuardianFrame::Attack);
missile.var2 = 3; missile.var2 = 3;
return true; return true;
@ -1122,12 +1122,6 @@ bool PlayerMHit(Player &player, Monster *monster, int dist, int mind, int maxd,
return true; return true;
} }
void SetMissDir(Missile &missile, int dir)
{
missile._mimfnum = dir;
SetMissAnim(missile, missile._miAnimType);
}
void InitMissiles() void InitMissiles()
{ {
Player &myPlayer = *MyPlayer; Player &myPlayer = *MyPlayer;
@ -1540,7 +1534,7 @@ void AddBigExplosion(Missile &missile, AddMissileParameter & /*parameter*/)
CheckMissileCol(missile, damageType, dmg, dmg, false, position, true); CheckMissileCol(missile, damageType, dmg, dmg, false, position, true);
} }
missile._mlid = AddLight(missile.position.start, 8); missile._mlid = AddLight(missile.position.start, 8);
SetMissDir(missile, 0); missile.setDefaultFrameGroup();
missile.duration = missile._miAnimLen - 1; missile.duration = missile._miAnimLen - 1;
} }
@ -1555,7 +1549,7 @@ void AddImmolation(Missile &missile, AddMissileParameter &parameter)
sp += std::min(missile._mispllvl, 34); sp += std::min(missile._mispllvl, 34);
} }
UpdateMissileVelocity(missile, dst, sp); UpdateMissileVelocity(missile, dst, sp);
SetMissDir(missile, GetDirection16(missile.position.start, dst)); missile.setDirection(GetDirection16(missile.position.start, dst));
missile.duration = 256; missile.duration = 256;
missile._mlid = AddLight(missile.position.start, 8); missile._mlid = AddLight(missile.position.start, 8);
} }
@ -1693,7 +1687,7 @@ void AddElementalArrow(Missile &missile, AddMissileParameter &parameter)
} }
UpdateMissileVelocity(missile, dst, av); UpdateMissileVelocity(missile, dst, av);
SetMissDir(missile, GetDirection16(missile.position.start, dst)); missile.setDirection(GetDirection16(missile.position.start, dst));
missile.duration = 256; missile.duration = 256;
missile.var1 = missile.position.start.x; missile.var1 = missile.position.start.x;
missile.var2 = missile.position.start.y; missile.var2 = missile.position.start.y;
@ -1802,7 +1796,7 @@ void AddFirebolt(Missile &missile, AddMissileParameter &parameter)
} }
} }
UpdateMissileVelocity(missile, dst, sp); UpdateMissileVelocity(missile, dst, sp);
SetMissDir(missile, GetDirection16(missile.position.start, dst)); missile.setDirection(GetDirection16(missile.position.start, dst));
missile.duration = 256; missile.duration = 256;
missile.var1 = missile.position.start.x; missile.var1 = missile.position.start.x;
missile.var2 = missile.position.start.y; missile.var2 = missile.position.start.y;
@ -1913,7 +1907,7 @@ void AddFireball(Missile &missile, AddMissileParameter &parameter)
missile._midam = ScaleSpellEffect(dmg, missile._mispllvl); missile._midam = ScaleSpellEffect(dmg, missile._mispllvl);
} }
UpdateMissileVelocity(missile, dst, sp); UpdateMissileVelocity(missile, dst, sp);
SetMissDir(missile, GetDirection16(missile.position.start, dst)); missile.setDirection(GetDirection16(missile.position.start, dst));
missile.duration = 256; missile.duration = 256;
missile.var1 = missile.position.start.x; missile.var1 = missile.position.start.x;
missile.var2 = missile.position.start.y; missile.var2 = missile.position.start.y;
@ -1953,16 +1947,16 @@ void AddMissileExplosion(Missile &missile, AddMissileParameter &parameter)
if (missile._micaster != TARGET_MONSTERS && missile._misource >= 0) { if (missile._micaster != TARGET_MONSTERS && missile._misource >= 0) {
switch (Monsters[missile._misource].type().type) { switch (Monsters[missile._misource].type().type) {
case MT_SUCCUBUS: case MT_SUCCUBUS:
SetMissAnim(missile, MissileGraphicID::BloodStarExplosion); missile.setAnimation(MissileGraphicID::BloodStarExplosion);
break; break;
case MT_SNOWWICH: case MT_SNOWWICH:
SetMissAnim(missile, MissileGraphicID::BloodStarBlueExplosion); missile.setAnimation(MissileGraphicID::BloodStarBlueExplosion);
break; break;
case MT_HLSPWN: case MT_HLSPWN:
SetMissAnim(missile, MissileGraphicID::BloodStarRedExplosion); missile.setAnimation(MissileGraphicID::BloodStarRedExplosion);
break; break;
case MT_SOLBRNR: case MT_SOLBRNR:
SetMissAnim(missile, MissileGraphicID::BloodStarYellowExplosion); missile.setAnimation(MissileGraphicID::BloodStarYellowExplosion);
break; break;
default: default:
break; break;
@ -1982,9 +1976,9 @@ void AddWeaponExplosion(Missile &missile, AddMissileParameter &parameter)
{ {
missile.var2 = parameter.dst.x; missile.var2 = parameter.dst.x;
if (parameter.dst.x == 1) if (parameter.dst.x == 1)
SetMissAnim(missile, MissileGraphicID::MagmaBallExplosion); missile.setAnimation(MissileGraphicID::MagmaBallExplosion);
else else
SetMissAnim(missile, MissileGraphicID::ChargedBolt); missile.setAnimation(MissileGraphicID::ChargedBolt);
missile.duration = missile._miAnimLen - 1; missile.duration = missile._miAnimLen - 1;
} }
@ -2164,7 +2158,7 @@ namespace {
void InitMissileAnimationFromMonster(Missile &mis, Direction midir, const Monster &mon, MonsterGraphic graphic) void InitMissileAnimationFromMonster(Missile &mis, Direction midir, const Monster &mon, MonsterGraphic graphic)
{ {
const AnimStruct &anim = mon.type().getAnimData(graphic); const AnimStruct &anim = mon.type().getAnimData(graphic);
mis._mimfnum = static_cast<int32_t>(midir); mis.setDirection(midir);
mis._miAnimFlags = MissileGraphicsFlags::None; mis._miAnimFlags = MissileGraphicsFlags::None;
ClxSpriteList sprites = *anim.spritesForDirection(midir); ClxSpriteList sprites = *anim.spritesForDirection(midir);
const uint16_t width = sprites[0].width(); const uint16_t width = sprites[0].width();
@ -2215,17 +2209,17 @@ void AddGenericMagicMissile(Missile &missile, AddMissileParameter &parameter)
if (missile._micaster != TARGET_MONSTERS && missile._misource > 0) { if (missile._micaster != TARGET_MONSTERS && missile._misource > 0) {
Monster &monster = Monsters[missile._misource]; Monster &monster = Monsters[missile._misource];
if (monster.type().type == MT_SUCCUBUS) if (monster.type().type == MT_SUCCUBUS)
SetMissAnim(missile, MissileGraphicID::BloodStar); missile.setAnimation(MissileGraphicID::BloodStar);
if (monster.type().type == MT_SNOWWICH) if (monster.type().type == MT_SNOWWICH)
SetMissAnim(missile, MissileGraphicID::BloodStarBlue); missile.setAnimation(MissileGraphicID::BloodStarBlue);
if (monster.type().type == MT_HLSPWN) if (monster.type().type == MT_HLSPWN)
SetMissAnim(missile, MissileGraphicID::BloodStarRed); missile.setAnimation(MissileGraphicID::BloodStarRed);
if (monster.type().type == MT_SOLBRNR) if (monster.type().type == MT_SOLBRNR)
SetMissAnim(missile, MissileGraphicID::BloodStarYellow); missile.setAnimation(MissileGraphicID::BloodStarYellow);
} }
if (GetMissileSpriteData(missile._miAnimType).animFAmt == 16) { if (GetMissileSpriteData(missile._miAnimType).animFAmt == 16) {
SetMissDir(missile, GetDirection16(missile.position.start, dst)); missile.setDirection(GetDirection16(missile.position.start, dst));
} }
if (missile._midam == 0) { if (missile._midam == 0) {
@ -2248,7 +2242,7 @@ void AddGenericMagicMissile(Missile &missile, AddMissileParameter &parameter)
void AddAcid(Missile &missile, AddMissileParameter &parameter) void AddAcid(Missile &missile, AddMissileParameter &parameter)
{ {
UpdateMissileVelocity(missile, parameter.dst, 16); 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) if (!gbIsHellfire || (missile.position.velocity.deltaX & 0xFFFF0000) != 0 || (missile.position.velocity.deltaY & 0xFFFF0000) != 0)
missile.duration = 5 * (Monsters[missile._misource].intelligence + 4); missile.duration = 5 * (Monsters[missile._misource].intelligence + 4);
else else
@ -2427,7 +2421,7 @@ void AddElemental(Missile &missile, AddMissileParameter &parameter)
missile._midam = ScaleSpellEffect(dmg, missile._mispllvl) / 2; missile._midam = ScaleSpellEffect(dmg, missile._mispllvl) / 2;
UpdateMissileVelocity(missile, dst, 16); UpdateMissileVelocity(missile, dst, 16);
SetMissDir(missile, GetDirection(missile.position.start, dst)); missile.setDirection(GetDirection(missile.position.start, dst));
missile.duration = 256; missile.duration = 256;
missile.var1 = missile.position.start.x; missile.var1 = missile.position.start.x;
missile.var2 = missile.position.start.y; missile.var2 = missile.position.start.y;
@ -2654,7 +2648,7 @@ void AddHolyBolt(Missile &missile, AddMissileParameter &parameter)
Player &player = Players[missile._misource]; Player &player = Players[missile._misource];
UpdateMissileVelocity(missile, dst, sp); UpdateMissileVelocity(missile, dst, sp);
SetMissDir(missile, GetDirection16(missile.position.start, dst)); missile.setDirection(GetDirection16(missile.position.start, dst));
missile.duration = 256; missile.duration = 256;
missile.var1 = missile.position.start.x; missile.var1 = missile.position.start.x;
missile.var2 = missile.position.start.y; missile.var2 = missile.position.start.y;
@ -2697,7 +2691,7 @@ void AddBoneSpirit(Missile &missile, AddMissileParameter &parameter)
dst += parameter.midir; dst += parameter.midir;
} }
UpdateMissileVelocity(missile, dst, 16); UpdateMissileVelocity(missile, dst, 16);
SetMissDir(missile, GetDirection(missile.position.start, dst)); missile.setDirection(GetDirection(missile.position.start, dst));
missile.duration = 256; missile.duration = 256;
missile.var1 = missile.position.start.x; missile.var1 = missile.position.start.x;
missile.var2 = missile.position.start.y; 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) if (missile._miAnimType == MissileGraphicID::None || GetMissileSpriteData(missile._miAnimType).animFAmt < 8)
SetMissDir(missile, 0); missile.setDefaultFrameGroup();
else else
SetMissDir(missile, midir); missile.setDirection(midir);
if (!lSFX) { if (!lSFX) {
lSFX = missileData.castSound; lSFX = missileData.castSound;
@ -2809,7 +2803,7 @@ void ProcessElementalArrow(Missile &missile)
} }
MoveMissileAndCheckMissileCol(missile, DamageType::Physical, mind, maxd, true, false); MoveMissileAndCheckMissileCol(missile, DamageType::Physical, mind, maxd, true, false);
if (missile.duration == 0) { if (missile.duration == 0) {
missile._mimfnum = 0; missile.setDefaultFrameGroup();
missile.duration = missile._miAnimLen - 1; missile.duration = missile._miAnimLen - 1;
missile.position.StopMissile(); missile.position.StopMissile();
@ -2848,7 +2842,7 @@ void ProcessElementalArrow(Missile &missile)
app_fatal(StrCat("wrong missile ID ", static_cast<int>(missile._mitype))); app_fatal(StrCat("wrong missile ID ", static_cast<int>(missile._mitype)));
break; break;
} }
SetMissAnim(missile, eAnim); missile.setAnimation(eAnim);
CheckMissileCol(missile, damageType, eMind, eMaxd, false, missile.position.tile, true); CheckMissileCol(missile, damageType, eMind, eMaxd, false, missile.position.tile, true);
} else { } else {
if (missile.position.tile != Point { missile.var1, missile.var2 }) { if (missile.position.tile != Point { missile.var1, missile.var2 }) {
@ -2904,7 +2898,7 @@ void ProcessGenericProjectile(Missile &missile)
if (missile.duration == 0) { if (missile.duration == 0) {
missile._miDelFlag = true; missile._miDelFlag = true;
Point dst = { 0, 0 }; Point dst = { 0, 0 };
auto dir = static_cast<Direction>(missile._mimfnum); Direction dir = missile.getDirection();
switch (missile._mitype) { switch (missile._mitype) {
case MissileID::Firebolt: case MissileID::Firebolt:
case MissileID::MagmaBall: 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); CheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, true, missile.position.tile, false);
missile.duration = range; missile.duration = range;
if (range == 0) { if (range == 0) {
if (missile._mimfnum != 0) { if (missile.getFrameGroup<AcidPuddleFrame>() != AcidPuddleFrame::Idle) {
missile._miDelFlag = true; missile._miDelFlag = true;
} else { } else {
SetMissDir(missile, 1); missile.setFrameGroup<AcidPuddleFrame>(AcidPuddleFrame::End);
missile.duration = missile._miAnimLen; missile.duration = missile._miAnimLen;
} }
} }
@ -2992,11 +2986,11 @@ void ProcessFireWall(Missile &missile)
missile.duration--; missile.duration--;
if (missile.duration == missile.var1) { if (missile.duration == missile.var1) {
SetMissDir(missile, 1); missile.setFrameGroup<FireWallFrame>(FireWallFrame::Idle);
missile._miAnimFrame = GenerateRnd(11) + 1; missile._miAnimFrame = GenerateRnd(11) + 1;
} }
if (missile.duration == missile._miAnimLen - 1) { if (missile.duration == missile._miAnimLen - 1) {
SetMissDir(missile, 0); missile.setFrameGroup<FireWallFrame>(FireWallFrame::Start);
missile._miAnimFrame = 13; missile._miAnimFrame = 13;
missile._miAnimAdd = -1; missile._miAnimAdd = -1;
} }
@ -3006,7 +3000,7 @@ void ProcessFireWall(Missile &missile)
AddUnLight(missile._mlid); AddUnLight(missile._mlid);
} }
constexpr int MaxExpLightIndex = ExpLightLen - 1; constexpr int MaxExpLightIndex = ExpLightLen - 1;
if (missile._mimfnum == 0 && missile.duration != 0 && missile.var2 <= MaxExpLightIndex * 2) { if (missile.getFrameGroup<FireWallFrame>() == FireWallFrame::Start && missile.duration != 0 && missile.var2 <= MaxExpLightIndex * 2) {
if (missile.var2 == 0) if (missile.var2 == 0)
missile._mlid = AddLight(missile.position.tile, ExpLight[0]); missile._mlid = AddLight(missile.position.tile, ExpLight[0]);
int expLightIndex = MaxExpLightIndex - std::abs(missile.var2 - MaxExpLightIndex); 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)))) { || (TransList[dTransVal[missilePosition.x][missilePosition.y - 1]] && TileHasAny(missilePosition + Direction::NorthEast, TileProperties::Solid)))) {
missile.position.offset.deltaX -= 32; missile.position.offset.deltaX -= 32;
} }
missile._mimfnum = 0; missile.setDefaultFrameGroup();
SetMissAnim(missile, MissileGraphicID::BigExplosion); missile.setAnimation(MissileGraphicID::BigExplosion);
missile.duration = missile._miAnimLen - 1; missile.duration = missile._miAnimLen - 1;
missile.position.velocity = {}; missile.position.velocity = {};
} else if (missile.position.tile != Point { missile.var1, missile.var2 }) { } else if (missile.position.tile != Point { missile.var1, missile.var2 }) {
@ -3314,8 +3308,8 @@ void ProcessTownPortal(Missile &missile)
if (missile.duration > 1) if (missile.duration > 1)
missile.duration--; missile.duration--;
if (missile.duration == missile.var1) if (missile.duration == missile.var1)
SetMissDir(missile, 1); missile.setFrameGroup<PortalFrame>(PortalFrame::Idle);
if (leveltype != DTYPE_TOWN && missile._mimfnum != 1 && missile.duration != 0) { if (leveltype != DTYPE_TOWN && missile.getFrameGroup<PortalFrame>() != PortalFrame::Idle && missile.duration != 0) {
if (missile.var2 == 0) if (missile.var2 == 0)
missile._mlid = AddLight(missile.position.tile, 1); missile._mlid = AddLight(missile.position.tile, 1);
ChangeLight(missile._mlid, missile.position.tile, expLight[missile.var2]); ChangeLight(missile._mlid, missile.position.tile, expLight[missile.var2]);
@ -3405,7 +3399,7 @@ void ProcessFlameWave(Missile &missile)
missile.var1++; missile.var1++;
if (missile.var1 == missile._miAnimLen) { if (missile.var1 == missile._miAnimLen) {
SetMissDir(missile, 1); missile.setFrameGroup<FireWallFrame>(FireWallFrame::Idle);
missile._miAnimFrame = GenerateRnd(11) + 1; missile._miAnimFrame = GenerateRnd(11) + 1;
} }
int j = missile.duration; int j = missile.duration;
@ -3435,8 +3429,8 @@ void ProcessGuardian(Missile &missile)
if (missile.var2 > 0) { if (missile.var2 > 0) {
missile.var2--; missile.var2--;
} }
if (missile.duration == missile.var1 || (missile._mimfnum == 2 && missile.var2 == 0)) { if (missile.duration == missile.var1 || (missile.getFrameGroup<GuardianFrame>() == GuardianFrame::Attack && missile.var2 == 0)) {
SetMissDir(missile, 1); missile.setFrameGroup<GuardianFrame>(GuardianFrame::Idle);
} }
Point position = missile.position.tile; Point position = missile.position.tile;
@ -3478,7 +3472,7 @@ void ProcessGuardian(Missile &missile)
} }
if (missile.duration == 14) { if (missile.duration == 14) {
SetMissDir(missile, 0); missile.setFrameGroup<GuardianFrame>(GuardianFrame::Start);
missile._miAnimFrame = 15; missile._miAnimFrame = 15;
missile._miAnimAdd = -1; missile._miAnimAdd = -1;
} }
@ -3633,9 +3627,9 @@ void ProcessStoneCurse(Missile &missile)
missile.duration--; missile.duration--;
Monster &monster = Monsters[missile.var2]; Monster &monster = Monsters[missile.var2];
if (monster.hitPoints == 0 && missile._miAnimType != MissileGraphicID::StoneCurseShatter) { if (monster.hitPoints == 0 && missile._miAnimType != MissileGraphicID::StoneCurseShatter) {
missile._mimfnum = 0; missile.setDefaultFrameGroup();
missile._miDrawFlag = true; missile._miDrawFlag = true;
SetMissAnim(missile, MissileGraphicID::StoneCurseShatter); missile.setAnimation(MissileGraphicID::StoneCurseShatter);
missile.duration = 11; missile.duration = 11;
} }
if (monster.mode != MonsterMode::Petrified) { 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); MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, false, false);
if (missile._miHitFlag) { if (missile._miHitFlag) {
missile.var1 = 8; missile.var1 = 8;
missile._mimfnum = 0; missile.setDefaultFrameGroup();
missile.position.offset = { 0, 0 }; missile.position.offset = { 0, 0 };
missile.position.velocity = {}; missile.position.velocity = {};
SetMissAnim(missile, MissileGraphicID::Lightning); missile.setAnimation(MissileGraphicID::Lightning);
missile.duration = missile._miAnimLen; missile.duration = missile._miAnimLen;
} }
ChangeLight(missile._mlid, missile.position.tile, missile.var1); ChangeLight(missile._mlid, missile.position.tile, missile.var1);
@ -3987,8 +3981,8 @@ void ProcessHolyBolt(Missile &missile)
int dam = missile._midam; int dam = missile._midam;
MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), dam, dam, true, true); MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), dam, dam, true, true);
if (missile.duration == 0) { if (missile.duration == 0) {
missile._mimfnum = 0; missile.setDefaultFrameGroup();
SetMissAnim(missile, MissileGraphicID::HolyBoltExplosion); missile.setAnimation(MissileGraphicID::HolyBoltExplosion);
missile.duration = missile._miAnimLen - 1; missile.duration = missile._miAnimLen - 1;
missile.position.StopMissile(); missile.position.StopMissile();
} else { } else {
@ -4047,11 +4041,11 @@ void ProcessElemental(Missile &missile)
auto *nextMonster = FindClosest(missilePosition, 19); auto *nextMonster = FindClosest(missilePosition, 19);
if (nextMonster != nullptr) { if (nextMonster != nullptr) {
Direction sd = GetDirection(missilePosition, nextMonster->position.tile); Direction sd = GetDirection(missilePosition, nextMonster->position.tile);
SetMissDir(missile, sd); missile.setDirection(sd);
UpdateMissileVelocity(missile, nextMonster->position.tile, 16); UpdateMissileVelocity(missile, nextMonster->position.tile, 16);
} else { } else {
Direction sd = Players[missile._misource]._pdir; Direction sd = Players[missile._misource]._pdir;
SetMissDir(missile, sd); missile.setDirection(sd);
UpdateMissileVelocity(missile, missilePosition + sd, 16); UpdateMissileVelocity(missile, missilePosition + sd, 16);
} }
} }
@ -4061,8 +4055,8 @@ void ProcessElemental(Missile &missile)
ChangeLight(missile._mlid, missilePosition, 8); ChangeLight(missile._mlid, missilePosition, 8);
} }
if (missile.duration == 0) { if (missile.duration == 0) {
missile._mimfnum = 0; missile.setDefaultFrameGroup();
SetMissAnim(missile, MissileGraphicID::BigExplosion); missile.setAnimation(MissileGraphicID::BigExplosion);
missile.duration = missile._miAnimLen - 1; missile.duration = missile._miAnimLen - 1;
missile.position.StopMissile(); missile.position.StopMissile();
} }
@ -4074,7 +4068,7 @@ void ProcessBoneSpirit(Missile &missile)
{ {
missile.duration--; missile.duration--;
int dam = missile._midam; int dam = missile._midam;
if (missile._mimfnum == 8) { if (missile.getDirection() == Direction::NoDirection) {
ChangeLight(missile._mlid, missile.position.tile, missile._miAnimFrame); ChangeLight(missile._mlid, missile.position.tile, missile._miAnimFrame);
if (missile.duration == 0) { if (missile.duration == 0) {
missile._miDelFlag = true; missile._miDelFlag = true;
@ -4092,11 +4086,11 @@ void ProcessBoneSpirit(Missile &missile)
auto *monster = FindClosest(c, 19); auto *monster = FindClosest(c, 19);
if (monster != nullptr) { if (monster != nullptr) {
missile._midam = monster->hitPoints >> 7; 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); UpdateMissileVelocity(missile, monster->position.tile, 16);
} else { } else {
Direction sd = Players[missile._misource]._pdir; Direction sd = Players[missile._misource]._pdir;
SetMissDir(missile, sd); missile.setDirection(sd);
UpdateMissileVelocity(missile, c + sd, 16); UpdateMissileVelocity(missile, c + sd, 16);
} }
} }
@ -4106,7 +4100,7 @@ void ProcessBoneSpirit(Missile &missile)
ChangeLight(missile._mlid, c, 8); ChangeLight(missile._mlid, c, 8);
} }
if (missile.duration == 0) { if (missile.duration == 0) {
SetMissDir(missile, 8); missile.setDirection(Direction::NoDirection);
missile.position.velocity = {}; missile.position.velocity = {};
missile.duration = 7; missile.duration = 7;
} }
@ -4129,9 +4123,9 @@ void ProcessRedPortal(Missile &missile)
if (missile.duration > 1) if (missile.duration > 1)
missile.duration--; missile.duration--;
if (missile.duration == missile.var1) if (missile.duration == missile.var1)
SetMissDir(missile, 1); missile.setFrameGroup<RedPortalFrame>(RedPortalFrame::Idle);
if (leveltype != DTYPE_TOWN && missile._mimfnum != 1 && missile.duration != 0) { if (leveltype != DTYPE_TOWN && missile.getFrameGroup<RedPortalFrame>() != RedPortalFrame::Idle && missile.duration != 0) {
if (missile.var2 == 0) if (missile.var2 == 0)
missile._mlid = AddLight(missile.position.tile, 1); missile._mlid = AddLight(missile.position.tile, 1);
ChangeLight(missile._mlid, missile.position.tile, expLight[missile.var2]); ChangeLight(missile._mlid, missile.position.tile, expLight[missile.var2]);
@ -4203,7 +4197,7 @@ void SetUpMissileAnimationData()
continue; continue;
if (missile._mitype != MissileID::Rhino) { if (missile._mitype != MissileID::Rhino) {
missile._miAnimData = GetMissileSpriteData(missile._miAnimType).spritesForDirection(missile._mimfnum); missile._miAnimData = GetMissileSpriteData(missile._miAnimType).spritesForDirection(missile.getDirection16());
continue; continue;
} }
@ -4217,7 +4211,7 @@ void SetUpMissileAnimationData()
} else { } else {
graphic = MonsterGraphic::Walk; graphic = MonsterGraphic::Walk;
} }
missile._miAnimData = mon.getAnimData(graphic).spritesForDirection(static_cast<Direction>(missile._mimfnum)); missile._miAnimData = mon.getAnimData(graphic).spritesForDirection(missile.getDirection());
} }
} }

155
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 { enum class MissileSource : uint8_t {
Player, Player,
Monster, Monster,
Trap, 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 { struct Missile {
/** Type of projectile */ /** Type of projectile */
MissileID _mitype; MissileID _mitype;
MissilePosition position; MissilePosition position;
private:
int _mimfnum; // The direction of the missile (direction enum) int _mimfnum; // The direction of the missile (direction enum)
public:
int _mispllvl; int _mispllvl;
bool _miDelFlag; // Indicate whether the missile should be deleted bool _miDelFlag; // Indicate whether the missile should be deleted
MissileGraphicID _miAnimType; MissileGraphicID _miAnimType;
@ -175,6 +170,69 @@ struct Missile {
return MissileSource::Monster; return MissileSource::Monster;
return MissileSource::Player; 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 <typename FrameEnum>
void setFrameGroup(FrameEnum frameGroup)
{
setFrameGroupRaw(static_cast<int>(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<int>(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<int>(dir));
}
int getFrameGroupRaw() const
{
return _mimfnum;
}
template <typename FrameEnum>
FrameEnum getFrameGroup() const
{
static_assert(std::is_enum_v<FrameEnum>, "Frame group must be an enum");
return static_cast<FrameEnum>(_mimfnum);
}
[[nodiscard]] Direction getDirection() const
{
return static_cast<Direction>(_mimfnum);
}
[[nodiscard]] Direction16 getDirection16() const
{
return static_cast<Direction16>(_mimfnum);
}
}; };
extern std::list<Missile> Missiles; extern std::list<Missile> Missiles;
@ -214,33 +272,6 @@ bool PlayerMHit(Player &player, Monster *monster, int dist, int mind, int maxd,
*/ */
bool IsMissileBlockedByTile(Point position); 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<int>(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<int>(dir));
}
void InitMissiles(); void InitMissiles();
struct AddMissileParameter { struct AddMissileParameter {

2
Source/monster.cpp

@ -4452,7 +4452,7 @@ void MissToMonst(Missile &missile, Point position)
Point oldPosition = missile.position.tile; Point oldPosition = missile.position.tile;
monster.occupyTile(position, false); monster.occupyTile(position, false);
monster.direction = static_cast<Direction>(missile._mimfnum); monster.direction = missile.getDirection();
monster.position.tile = position; monster.position.tile = position;
M_StartStand(monster, monster.direction); M_StartStand(monster, monster.direction);
M_StartHit(monster, 0); M_StartHit(monster, 0);

2
Source/portal.cpp

@ -53,7 +53,7 @@ void AddPortalMissile(const Player &player, Point position, bool sync)
if (missile != nullptr) { if (missile != nullptr) {
// Don't show portal opening animation if we sync existing portals // Don't show portal opening animation if we sync existing portals
if (sync) if (sync)
SetMissDir(*missile, 1); missile->setFrameGroup<PortalFrame>(PortalFrame::Idle);
if (leveltype != DTYPE_TOWN) if (leveltype != DTYPE_TOWN)
missile->_mlid = AddLight(missile->position.tile, 15); missile->_mlid = AddLight(missile->position.tile, 15);

6
test/missiles_test.cpp

@ -29,9 +29,9 @@ void TestAnimatedMissileRotatesUniformly(Missile &missile, int startingDir, int
{ {
ankerl::unordered_dense::map<int, unsigned> observed {}; ankerl::unordered_dense::map<int, unsigned> observed {};
for (auto i = 0; i < 100; i++) { for (auto i = 0; i < 100; i++) {
missile._mimfnum = startingDir; missile.setFrameGroupRaw(startingDir);
TestRotateBlockedMissile(missile); 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"; 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 // 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); 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, 5, 4, 6);
TestAnimatedMissileRotatesUniformly(missile, 0, 15, 1); TestAnimatedMissileRotatesUniformly(missile, 0, 15, 1);
TestAnimatedMissileRotatesUniformly(missile, 15, 14, 0); TestAnimatedMissileRotatesUniformly(missile, 15, 14, 0);

Loading…
Cancel
Save