Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/missile class composition #475

Merged
merged 10 commits into from
Mar 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/freeablo/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ add_library(freeablo_lib # split into a library so I can link to it from tests
faworld/missile/missile.cpp
faworld/missile/missile.h
faworld/missile/missileactorengagement.cpp
faworld/missile/missileattributes.cpp
faworld/missile/missilecreation.cpp
faworld/missile/missileenums.h
faworld/missile/missilegraphic.cpp
Expand Down
25 changes: 8 additions & 17 deletions apps/freeablo/faworld/gamelevel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ namespace FAWorld
actorMapInsert(mActors[i]);
}

Misc::Point GameLevel::getFreeSpotNear(Misc::Point point, int32_t radius) const
Misc::Point GameLevel::getFreeSpotNear(Misc::Point point, int32_t radius, const std::function<bool(const Misc::Point& point)>& additionalConstraints) const
{
// partially based on https://stackoverflow.com/a/398302

Expand All @@ -192,7 +192,7 @@ namespace FAWorld
Misc::Point targetPoint = point + Misc::Point{xOffset, yOffset};
if (targetPoint.x >= 0 && targetPoint.x < width() && targetPoint.y >= 0 && targetPoint.y < height())
{
if (isPassable(targetPoint, nullptr))
if (isPassable(targetPoint, nullptr) && (additionalConstraints == nullptr || additionalConstraints(targetPoint)))
grantramsay marked this conversation as resolved.
Show resolved Hide resolved
return targetPoint;
}

Expand Down Expand Up @@ -260,22 +260,13 @@ namespace FAWorld
}
}

for (const auto& actor : mActors)
for (const auto& graphic : mMissileGraphics)
{
for (const auto& missile : actor->getMissiles())
{
// Only display missiles for this (the currently displayed) level.
if (missile->getLevel() != this)
continue;
for (const auto& graphic : missile->mGraphics)
{
auto tmp = graphic->getCurrentFrame();
auto spriteGroup = tmp.first;
auto frame = tmp.second;
if (spriteGroup)
state->mObjects.push_back({spriteGroup, static_cast<uint32_t>(frame), graphic->mCurPos, std::nullopt});
}
}
auto tmp = graphic->getCurrentFrame();
auto spriteGroup = tmp.first;
auto frame = tmp.second;
if (spriteGroup)
state->mObjects.push_back({spriteGroup, static_cast<uint32_t>(frame), graphic->mCurPos, std::nullopt});
}

for (auto& p : mItemMap->mItems)
Expand Down
15 changes: 14 additions & 1 deletion apps/freeablo/faworld/gamelevel.h
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#pragma once
#include "hoverstate.h"
#include "itemmap.h" // TODO: remove, only included for the Tile type
#include <functional>
#include <level/level.h>
#include <misc/stdhashes.h>
#include <unordered_map>
#include <unordered_set>

namespace FARender
{
Expand All @@ -30,6 +32,7 @@ namespace FAWorld
namespace Missile
{
class Missile;
class MissileGraphic;
}

class GameLevelImpl
Expand Down Expand Up @@ -83,7 +86,11 @@ namespace FAWorld

void actorMapRefresh();

Misc::Point getFreeSpotNear(Misc::Point point, int32_t radius = std::numeric_limits<int32_t>::max()) const;
// TODO: Remove the additionalConstraints parameter, it is currently only used as a bit of a hack to not
// place a player on a town portal when teleporting (see https://github.com/wheybags/freeablo/issues/478)
Misc::Point getFreeSpotNear(Misc::Point point,
int32_t radius = std::numeric_limits<int32_t>::max(),
const std::function<bool(const Misc::Point& point)>& additionalConstraints = nullptr) const;

virtual bool isPassable(const Misc::Point& point, const FAWorld::Actor* forActor) const;

Expand All @@ -108,6 +115,12 @@ namespace FAWorld

World* getWorld() { return &mWorld; }

// This list avoids having to check every actor in world to find missile graphics on a level.
// It is not saved, items are added/removed in MissileGraphic constructor/destructor.
// This is currently only intended for rendering so order is unimportant, hence using std::unordered_set
// and not saving/loading. Since order is not maintained this should not use be used for game logic!
std::unordered_set<Missile::MissileGraphic*> mMissileGraphics;

private:
GameLevel(World& world);

Expand Down
172 changes: 79 additions & 93 deletions apps/freeablo/faworld/missile/missile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,120 +5,106 @@
#include "fasavegame/gameloader.h"
#include "faworld/actor.h"

namespace FAWorld
namespace FAWorld::Missile
{
namespace Missile
Missile::Missile(MissileId missileId, Actor& creator, Misc::Point dest)
: mCreator(&creator), mMissileId(missileId), mSrcPoint(creator.getPos().current()), mAttr(Attributes::fromId(missileId))
{
Missile::Missile(MissileId missileId, Actor& creator, Misc::Point dest)
: mCreator(&creator), mMissileId(missileId), mLevel(creator.getLevel()), mSrcPoint(creator.getPos().current())
{
MissileCreation::get(missileId)(*this, dest);
mAttr.mCreation(*this, dest, creator.getLevel());

if (!missileData().mSoundEffect.empty())
Engine::ThreadManager::get()->playSound(missileData().mSoundEffect);
}
if (!missileData().mSoundEffect.empty())
Engine::ThreadManager::get()->playSound(missileData().mSoundEffect);
}

Missile::Missile(FASaveGame::GameLoader& loader)
{
auto creatorId = loader.load<int32_t>();
auto levelIndex = loader.load<int32_t>();
auto world = loader.currentlyLoadingWorld;
loader.addFunctionToRunAtEnd([this, world, creatorId, levelIndex]() {
mCreator = world->getActorById(creatorId);
mLevel = world->getLevel(levelIndex);
});

mMissileId = static_cast<MissileId>(loader.load<int32_t>());
mSrcPoint = Misc::Point(loader);
mComplete = loader.load<bool>();

auto graphicsSize = loader.load<uint32_t>();
mGraphics.reserve(graphicsSize);
for (uint32_t i = 0; i < graphicsSize; i++)
mGraphics.push_back(std::make_unique<MissileGraphic>(loader));
}
Missile::Missile(FASaveGame::GameLoader& loader) : mMissileId(static_cast<MissileId>(loader.load<int32_t>())), mAttr(Attributes::fromId(mMissileId))
{
auto creatorId = loader.load<int32_t>();
auto world = loader.currentlyLoadingWorld;
loader.addFunctionToRunAtEnd([this, world, creatorId]() { mCreator = world->getActorById(creatorId); });

void Missile::save(FASaveGame::GameSaver& saver)
{
Serial::ScopedCategorySaver cat("Missile", saver);
mSrcPoint = Misc::Point(loader);
mComplete = loader.load<bool>();

saver.save(mCreator->getId());
saver.save(mLevel->getLevelIndex());
auto graphicsSize = loader.load<uint32_t>();
mGraphics.reserve(graphicsSize);
for (uint32_t i = 0; i < graphicsSize; i++)
mGraphics.push_back(std::make_unique<MissileGraphic>(loader));
}

saver.save(static_cast<int32_t>(mMissileId));
mSrcPoint.save(saver);
saver.save(mComplete);
void Missile::save(FASaveGame::GameSaver& saver)
{
Serial::ScopedCategorySaver cat("Missile", saver);

saver.save(static_cast<uint32_t>(mGraphics.size()));
for (auto& graphic : mGraphics)
graphic->save(saver);
}
saver.save(static_cast<int32_t>(mMissileId));
saver.save(mCreator->getId());

const DiabloExe::MissileData& Missile::missileData() const
{
const auto& missileDataTable = Engine::EngineMain::get()->exe().getMissileDataTable();
return missileDataTable.at((size_t)mMissileId);
}
mSrcPoint.save(saver);
saver.save(mComplete);

const DiabloExe::MissileGraphics& Missile::missileGraphics() const
{
const auto& missileGraphicsTable = Engine::EngineMain::get()->exe().getMissileGraphicsTable();
return missileGraphicsTable.at((size_t)missileData().mMissileGraphicsId);
}
saver.save(static_cast<uint32_t>(mGraphics.size()));
for (auto& graphic : mGraphics)
graphic->save(saver);
}

std::string Missile::getGraphicsPath(int32_t i) const
{
release_assert(i >= 0 && i < missileGraphics().mNumAnimationFiles);
std::stringstream path;
path << "missiles/" << missileGraphics().mFilename;
if (missileGraphics().mNumAnimationFiles > 1)
path << i + 1;
path << ".cl2";
return path.str();
}
const DiabloExe::MissileData& Missile::missileData() const
{
const auto& missileDataTable = Engine::EngineMain::get()->exe().getMissileDataTable();
return missileDataTable.at((size_t)mMissileId);
}

void Missile::playImpactSound()
{
if (!missileData().mImpactSoundEffect.empty())
Engine::ThreadManager::get()->playSound(missileData().mImpactSoundEffect);
}
const DiabloExe::MissileGraphics& Missile::missileGraphics() const
{
const auto& missileGraphicsTable = Engine::EngineMain::get()->exe().getMissileGraphicsTable();
return missileGraphicsTable.at((size_t)missileData().mMissileGraphicsId);
}

void Missile::update()
{
for (auto& graphic : mGraphics)
{
if (graphic->isComplete())
continue;
std::string Missile::getGraphicsPath(int32_t i) const
{
release_assert(i >= 0 && i < missileGraphics().mNumAnimationFiles);
std::stringstream path;
path << "missiles/" << missileGraphics().mFilename;
if (missileGraphics().mNumAnimationFiles > 1)
path << i + 1;
path << ".cl2";
return path.str();
}

graphic->update();
void Missile::playImpactSound()
{
if (!missileData().mImpactSoundEffect.empty())
Engine::ThreadManager::get()->playSound(missileData().mImpactSoundEffect);
}

MissileMovement::get(mMissileId)(*this, *graphic);
void Missile::update()
{
for (auto& graphic : mGraphics)
{
if (graphic->isComplete())
continue;

auto curPoint = graphic->mCurPos.current();
graphic->update();

// Check if actor is hit.
auto actor = mLevel->getActorAt(curPoint);
if (actor)
MissileActorEngagement::get(mMissileId)(*this, *graphic, *actor);
mAttr.mMovement(*this, *graphic);

// Stop when walls are hit.
if (!actor && !mLevel->isPassable(curPoint, mCreator))
{
playImpactSound();
graphic->stop();
}
auto curPoint = graphic->mCurPos.current();

// Stop after max range is exceeded.
auto distance = (Vec2Fix(curPoint.x, curPoint.y) - Vec2Fix(mSrcPoint.x, mSrcPoint.y)).magnitude();
if (distance > MissileMaxRange::get(mMissileId))
graphic->stop();
// Check if actor is hit.
auto actor = graphic->getLevel()->getActorAt(curPoint);
if (actor)
mAttr.mActorEngagement(*this, *graphic, *actor);

// Stop after "time to live" has expired.
if (graphic->getTicksSinceStarted() > MissileTimeToLive::get(mMissileId))
graphic->stop();
// Stop when walls are hit.
if (!actor && !graphic->getLevel()->isPassable(curPoint, mCreator))
{
playImpactSound();
graphic->stop();
}
// Set complete flag when all graphics are finished.
mComplete = std::all_of(mGraphics.begin(), mGraphics.end(), [](const std::unique_ptr<MissileGraphic>& graphic) { return graphic->isComplete(); });

// Stop after "time to live" has expired.
if (graphic->getTicksSinceStarted() > mAttr.mTimeToLive)
graphic->stop();
}
// Set complete flag when all graphics are finished.
mComplete = std::all_of(mGraphics.begin(), mGraphics.end(), [](const std::unique_ptr<MissileGraphic>& graphic) { return graphic->isComplete(); });
}
}
Loading