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.traveled.deltaX = 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._miDelFlag = file->NextBool32();
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.traveled.deltaX);
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<uint32_t>(missile._miDelFlag ? 1 : 0);
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);
/**
* 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<size_t>(direction)] : sprites->list();
}
};

192
Source/missiles.cpp

@ -40,6 +40,38 @@ namespace devilution {
std::list<Missile> 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<Direction16>(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<Direction>(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<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)
{
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>(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 &parameter)
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 &parameter)
}
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 &parameter)
}
}
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 &parameter)
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 &parameter)
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 &parameter)
{
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<int32_t>(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 &parameter)
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 &parameter)
void AddAcid(Missile &missile, AddMissileParameter &parameter)
{
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 &parameter)
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 &parameter)
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 &parameter)
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<int>(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<Direction>(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>() != AcidPuddleFrame::Idle) {
missile._miDelFlag = true;
} else {
SetMissDir(missile, 1);
missile.setFrameGroup<AcidPuddleFrame>(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>(FireWallFrame::Idle);
missile._miAnimFrame = GenerateRnd(11) + 1;
}
if (missile.duration == missile._miAnimLen - 1) {
SetMissDir(missile, 0);
missile.setFrameGroup<FireWallFrame>(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>() == 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>(PortalFrame::Idle);
if (leveltype != DTYPE_TOWN && missile.getFrameGroup<PortalFrame>() != 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>(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>() == GuardianFrame::Attack && missile.var2 == 0)) {
missile.setFrameGroup<GuardianFrame>(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>(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>(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)
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<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 {
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 <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;
@ -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<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();
struct AddMissileParameter {

2
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<Direction>(missile._mimfnum);
monster.direction = missile.getDirection();
monster.position.tile = position;
M_StartStand(monster, monster.direction);
M_StartHit(monster, 0);

2
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>(PortalFrame::Idle);
if (leveltype != DTYPE_TOWN)
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 {};
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);

Loading…
Cancel
Save