Implemented Freeze Ground spell. Refactored Entity class to support hashing and added std::unreachable to kill the function after computing the variant. Converted buff getter functions to use views/ranges. Added unit tests for no damage number flag for player tests. 237/237 unit tests passing. Release Build 12891.
All checks were successful
Emscripten Build / Build_and_Deploy_Web_Build (push) Successful in 7m47s

This commit is contained in:
sigonasr2 2026-03-04 15:59:18 -06:00
parent 04a9323db6
commit 7032a71746
32 changed files with 363 additions and 144 deletions

View File

@ -172,5 +172,22 @@ namespace BuffTests
Game::Update(0.f);
Assert::AreEqual(true,game->GetPlayer()->HasBuff(BuffType::SPEEDBOOST),L"Player should now have a speedboost buff.");
}
TEST_METHOD(MonsterGetBuffFunctionTest){
Monster&m{game->SpawnMonster({},MONSTER_DATA.at("TestName"))};
Game::Update(0.f);
Assert::AreEqual(false,bool(m.GetBuff(BuffType::ADRENALINE_RUSH)),L"Monster should not have an adrenaline buff.");
m.AddBuff(BuffType::ADRENALINE_RUSH,1.f,1.f);
m.AddBuff(BuffType::SPEEDBOOST,1.f,1.f);
m.AddBuff(BuffType::SPEEDBOOST,2.f,1.f);
Game::Update(0.f);
Assert::AreEqual(true,bool(m.GetBuff(BuffType::ADRENALINE_RUSH)),L"Monster should now have an adrenaline buff.");
Assert::AreEqual(true,bool(m.GetBuff(BuffType::SPEEDBOOST)),L"Monster should now have an adrenaline buff.");
Assert::AreEqual(1.f,(*m.GetBuff(BuffType::SPEEDBOOST)).lifetime,L"GetBuff should reference the first buff in the list.");
Game::Update(1.f);
Assert::AreEqual(1.f,(*m.GetBuff(BuffType::SPEEDBOOST)).lifetime,L"GetBuff should reference the first buff in the list (Second speedboost).");
Game::Update(1.f);
Assert::AreEqual(false,bool(m.GetBuff(BuffType::SPEEDBOOST)),L"Monster should no longer have a speedboost buff.");
}
};
}

View File

@ -668,7 +668,7 @@ namespace EnchantTests
Game::ChangeClass(player,THIEF);
player->CheckAndPerformAbility(player->GetAbility3(),testKeyboardInput);
Game::Update(0.f);
Assert::AreEqual("Thief.Ability 3.Duration"_F,player->GetBuffs(BuffType::ADRENALINE_RUSH)[0].duration,L"Adrenaline Rush buff duration is normal.");
Assert::AreEqual("Thief.Ability 3.Duration"_F,player->GetBuffs(BuffType::ADRENALINE_RUSH)[0].lifetime,L"Adrenaline Rush buff duration is normal.");
Assert::AreEqual(100,player->GetHealth(),L"Adrenaline Rush does not reduce health.");
Assert::AreEqual("Thief.Auto Attack.Cooldown"_F*"Thief.Ability 3.Attack Speed Increase"_F/100.f,player->GetAttackRecoveryRateReduction(),L"Adrenaline Rush boosts attack rate normally.");
Assert::AreEqual("Thief.Auto Attack.Range"_F,player->GetAttackRange(),L"Adrenaline Rush does not boost attack range.");
@ -678,7 +678,7 @@ namespace EnchantTests
Game::GiveAndEquipEnchantedRing("Adrenaline Stim");
player->CheckAndPerformAbility(player->GetAbility3(),testKeyboardInput);
Game::Update(0.f);
Assert::AreEqual("Adrenaline Stim"_ENC["NEW ADRENALINE RUSH DURATION"],player->GetBuffs(BuffType::ADRENALINE_RUSH)[0].duration,L"Adrenaline Stim enchant boosts the duration of Adrenaline Rush.");
Assert::AreEqual("Adrenaline Stim"_ENC["NEW ADRENALINE RUSH DURATION"],player->GetBuffs(BuffType::ADRENALINE_RUSH)[0].lifetime,L"Adrenaline Stim enchant boosts the duration of Adrenaline Rush.");
Assert::AreEqual(80,player->GetHealth(),L"Adrenaline Stim reduces health.");
Assert::AreEqual("Thief.Auto Attack.Cooldown"_F*"Thief.Ability 3.Attack Speed Increase"_F/100.f,player->GetAttackRecoveryRateReduction(),L"Adrenaline Stim still boosts attack rate normally.");
Assert::AreEqual("Thief.Auto Attack.Range"_F+"Thief.Auto Attack.Range"_F*"Adrenaline Stim"_ENC["ATTACK RANGE INCREASE PCT"]/100.f,player->GetAttackRange(),L"Adrenaline Stim boosts attack range.");
@ -695,7 +695,7 @@ namespace EnchantTests
testMonster.Hurt(1000,testMonster.OnUpperLevel(),testMonster.GetZ());
}
Assert::AreEqual(100,player->GetAttack(),L"Bloodlust's bonus attack should not be applying here.");
Assert::AreEqual("Thief.Ability 3.Duration"_F,player->GetBuffs(BuffType::ADRENALINE_RUSH)[0].duration,L"Bloodlust's duration increase should not be applying here.");
Assert::AreEqual("Thief.Ability 3.Duration"_F,player->GetBuffs(BuffType::ADRENALINE_RUSH)[0].lifetime,L"Bloodlust's duration increase should not be applying here.");
player->RestoreMana(100);
player->RemoveAllBuffs();
Thief::ability3.charges=1;
@ -707,7 +707,7 @@ namespace EnchantTests
testMonster.Hurt(1000,testMonster.OnUpperLevel(),testMonster.GetZ());
}
Assert::AreEqual(110,player->GetAttack(),L"Bloodlust's bonus attacks stack only to 10.");
Assert::AreEqual("Thief.Ability 3.Duration"_F+"Bloodlust"_ENC["BUFF TIMER INCREASE"]*30,player->GetBuffs(BuffType::ADRENALINE_RUSH)[0].duration,L"Bloodlust's duration increases per kill.");
Assert::AreEqual("Thief.Ability 3.Duration"_F+"Bloodlust"_ENC["BUFF TIMER INCREASE"]*30,player->GetBuffs(BuffType::ADRENALINE_RUSH)[0].lifetime,L"Bloodlust's duration increases per kill.");
}
TEST_METHOD(EvasiveMovementCheck){
Game::ChangeClass(player,THIEF);
@ -808,7 +808,7 @@ namespace EnchantTests
player->_ForceCastSpell(player->GetAbility1());
Game::Update("Trapper.Ability 1.Precast Time"_F);
Game::Update(0.f);
Assert::AreEqual(7.f,newMonster.GetBuffs(BuffType::TRAPPER_MARK)[0].duration,L"Mark duration is unaffected.");
Assert::AreEqual(7.f,newMonster.GetBuffs(BuffType::TRAPPER_MARK)[0].lifetime,L"Mark duration is unaffected.");
}
TEST_METHOD(LongLastingMarkEnchantCheck){
Game::ChangeClass(player,TRAPPER);
@ -818,7 +818,7 @@ namespace EnchantTests
player->_ForceCastSpell(player->GetAbility1());
Game::Update("Trapper.Ability 1.Precast Time"_F);
Game::Update(0.f); //Wait for mark lock-on
Assert::AreEqual(20.f,newMonster.GetBuffs(BuffType::TRAPPER_MARK)[0].duration,L"Mark duration is increased greatly.");
Assert::AreEqual(20.f,newMonster.GetBuffs(BuffType::TRAPPER_MARK)[0].lifetime,L"Mark duration is increased greatly.");
}
TEST_METHOD(TrapCollectorNoEnchantCheck){
Game::ChangeClass(player,TRAPPER);
@ -838,7 +838,7 @@ namespace EnchantTests
Game::Update(0.5f);
BearTrap trap{*(BearTrap*)BULLET_LIST.back().get()};
trap.MonsterHit(newMonster,1); //Simulate 1 mark stack on the monster to trigger the bleed.
Assert::AreEqual("Trapper.Ability 2.Marked Target Bleed"_f[1],newMonster.GetBuffs(BuffType::OVER_TIME)[0].duration,L"Bleed duration should be 10 seconds by default.");
Assert::AreEqual("Trapper.Ability 2.Marked Target Bleed"_f[1],newMonster.GetBuffs(BuffType::OVER_TIME)[0].lifetime,L"Bleed duration should be 10 seconds by default.");
}
TEST_METHOD(LingeringScentEnchantCheck){
Game::ChangeClass(player,TRAPPER);
@ -848,7 +848,7 @@ namespace EnchantTests
Game::Update(0.5f);
BearTrap trap{*(BearTrap*)BULLET_LIST.back().get()};
trap.MonsterHit(newMonster,1); //Simulate 1 mark stack on the monster to trigger the bleed.
Assert::AreEqual("Trapper.Ability 2.Marked Target Bleed"_f[1]+"Lingering Scent"_ENC["BLEED EXTRA DURATION"],newMonster.GetBuffs(BuffType::OVER_TIME)[0].duration,L"Bleed duration should be 10 seconds by default.");
Assert::AreEqual("Trapper.Ability 2.Marked Target Bleed"_f[1]+"Lingering Scent"_ENC["BLEED EXTRA DURATION"],newMonster.GetBuffs(BuffType::OVER_TIME)[0].lifetime,L"Bleed duration should be 10 seconds by default.");
}
TEST_METHOD(OpportunityShotNoEnchantCheck){
Game::ChangeClass(player,TRAPPER);
@ -1112,9 +1112,9 @@ namespace EnchantTests
Assert::IsTrue(!newMonster3.IsDead(),L"The other monsters should not be dead.");
Assert::IsTrue(!newMonster4.IsDead(),L"The other monsters should not be dead.");
Assert::AreEqual(size_t(1),newMonster3.GetBuffs(BuffType::CURSE_OF_PAIN).size(),L"With the enchant, Curse of Pain should have spread to Monster 3 and only 1 stack should have been applied.");
Assert::AreEqual("Witch.Ability 1.Curse Debuff"_f[2],newMonster3.GetBuffs(BuffType::CURSE_OF_PAIN)[0].duration,L"When Curse of Pain is reapplied to a monster that already has Curse of Pain, it refreshes the duration.");
Assert::AreEqual("Witch.Ability 1.Curse Debuff"_f[2],newMonster3.GetBuffs(BuffType::CURSE_OF_PAIN)[0].lifetime,L"When Curse of Pain is reapplied to a monster that already has Curse of Pain, it refreshes the duration.");
Assert::AreEqual(size_t(1),newMonster4.GetBuffs(BuffType::CURSE_OF_PAIN).size(),L"With the enchant, Curse of Pain should have spread to Monster 4 and only 1 stack should have been applied.");
Assert::AreEqual("Witch.Ability 1.Curse Debuff"_f[2],newMonster4.GetBuffs(BuffType::CURSE_OF_PAIN)[0].duration,L"When Curse of Pain is reapplied to a monster that already has Curse of Pain, it refreshes the duration.");
Assert::AreEqual("Witch.Ability 1.Curse Debuff"_f[2],newMonster4.GetBuffs(BuffType::CURSE_OF_PAIN)[0].lifetime,L"When Curse of Pain is reapplied to a monster that already has Curse of Pain, it refreshes the duration.");
}
TEST_METHOD(PoolingPoisonNoEnchantCheck){
Game::ChangeClass(player,WITCH);

View File

@ -135,6 +135,14 @@ namespace MonsterTests
Monster testMonster{{},MONSTER_DATA["TestName"]};
testMonster.Hurt(5,testMonster.OnUpperLevel(),testMonster.GetZ());
Assert::AreEqual(testMonster.GetHealth(),testMonster.GetMaxHealth()-5);
Assert::AreEqual(false,testMonster.HasIframes(),L"Monster should not have iframes.");
}
TEST_METHOD(ZeroDamageResultsInTrue)
{
Monster testMonster{{},MONSTER_DATA["TestName"]};
const bool result{testMonster.Hurt(0,testMonster.OnUpperLevel(),testMonster.GetZ())};
Assert::AreEqual(true,result,L"Even zero damage should return true.");
Assert::AreEqual(false,testMonster.HasIframes(),L"Monster should not have iframes.");
}
TEST_METHOD(IFrameShouldResultInNoDamage){
Monster testMonster{{},MONSTER_DATA["TestName"]};

View File

@ -111,9 +111,16 @@ namespace PlayerTests
TEST_METHOD(PlayerAlive){
Assert::IsTrue(player->IsAlive());
}
TEST_METHOD(PlayerTakesDamage){
TEST_METHOD(PlayerTakesDamageAndNoDamageFlagWorks){
player->Hurt(30,player->OnUpperLevel(),player->GetZ());
Assert::AreEqual(70,player->GetHealth());
Assert::AreEqual(size_t(1),DAMAGENUMBER_LIST.size(),L"Hurting the player should generate a damage number.");
Game::Update(0.1f); //Prevent stacking up damage all on one damage number.
player->Hurt(0,player->OnUpperLevel(),player->GetZ());
Assert::AreEqual(size_t(2),DAMAGENUMBER_LIST.size(),L"Hurting the player for 0 damage should still generate a damage number.");
Game::Update(0.1f); //Prevent stacking up damage all on one damage number.
player->Hurt(0,player->OnUpperLevel(),player->GetZ(),HurtFlag::NO_DAMAGE_NUMBER);
Assert::AreEqual(size_t(2),DAMAGENUMBER_LIST.size(),L"Flag should suppress a damage number being generated");
}
TEST_METHOD(PlayerBlockingTakesNoDamage){
player->SetState(State::BLOCK);

View File

@ -883,6 +883,7 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="FreezeGround.cpp" />
<ClCompile Include="GhostSaber.cpp">
<SubType>
</SubType>

View File

@ -1373,6 +1373,9 @@
<ClCompile Include="Error.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="FreezeGround.cpp">
<Filter>Source Files\Effects</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="cpp.hint" />

View File

@ -845,6 +845,11 @@ void AiL::UpdateBullets(float fElapsedTime){
std::erase_if(BULLET_LIST,[](std::unique_ptr<IBullet>&b){return b->IsDead();});
}
std::vector<Entity>AiL::GetTargetsInRange(vf2d pos,float radius,bool upperLevel,float z,const HurtType hurtTargets)const{
const HurtList&fullList{Hurt(pos,radius,0,upperLevel,z,hurtTargets,HurtFlag::NO_DAMAGE_NUMBER)};
return fullList|std::views::transform([](const HurtListItem&item)->Entity{return item.first;})|std::ranges::to<std::vector>();
}
const HurtList AiL::Hurt(vf2d pos,float radius,int damage,bool upperLevel,float z,const HurtType hurtTargets,HurtFlag::HurtFlag hurtFlags)const{
HurtList hitList;
const bool CheckForMonsterCollisions=hurtTargets&HurtType::MONSTER;
@ -1093,10 +1098,10 @@ void AiL::RenderWorld(float fElapsedTime){
const bool displayCoinSymbol{player->GetBuffs(BuffType::PIRATE_GHOST_CAPTAIN_CURSE_COIN).size()>0};
Pixel playerCol{WHITE};
if(attackBuffs.size()>0)playerCol={255,uint8_t(255*abs(sin(1.4f*attackBuffs[0].lifetime))),uint8_t(255*abs(sin(1.4f*attackBuffs[0].lifetime)))};
else if(adrenalineRushBuffs.size()>0)playerCol={uint8_t(255*abs(sin(6.f*adrenalineRushBuffs[0].lifetime))),255,uint8_t(255*abs(sin(6.f*adrenalineRushBuffs[0].lifetime)))};
else if(movespeedBuffs.size()>0)playerCol={uint8_t(255*abs(sin(2.f*movespeedBuffs[0].lifetime))),255,uint8_t(255*abs(sin(2.f*movespeedBuffs[0].lifetime)))};
else if(inkSlowdownDebuff.size()>0)playerCol={uint8_t(255*abs(sin(2.f*inkSlowdownDebuff[0].lifetime))),uint8_t(255*abs(sin(2.f*inkSlowdownDebuff[0].lifetime))),uint8_t(255*abs(sin(2.f*inkSlowdownDebuff[0].lifetime)))};
if(attackBuffs.size()>0)playerCol={255,uint8_t(255*abs(sin(1.4f*attackBuffs[0].totalTimeAlive))),uint8_t(255*abs(sin(1.4f*attackBuffs[0].totalTimeAlive)))};
else if(adrenalineRushBuffs.size()>0)playerCol={uint8_t(255*abs(sin(6.f*adrenalineRushBuffs[0].totalTimeAlive))),255,uint8_t(255*abs(sin(6.f*adrenalineRushBuffs[0].totalTimeAlive)))};
else if(movespeedBuffs.size()>0)playerCol={uint8_t(255*abs(sin(2.f*movespeedBuffs[0].totalTimeAlive))),255,uint8_t(255*abs(sin(2.f*movespeedBuffs[0].totalTimeAlive)))};
else if(inkSlowdownDebuff.size()>0)playerCol={uint8_t(255*abs(sin(2.f*inkSlowdownDebuff[0].totalTimeAlive))),uint8_t(255*abs(sin(2.f*inkSlowdownDebuff[0].totalTimeAlive))),uint8_t(255*abs(sin(2.f*inkSlowdownDebuff[0].totalTimeAlive)))};
else if(curseDebuff.size()>0)playerCol={uint8_t(128*abs(sin(2.f*GetRunTime()))+127),uint8_t(128*abs(sin(2.f*GetRunTime()))),uint8_t(128*abs(sin(2.f*GetRunTime()))+127)};
else if(hastenBuff.size()>0)playerCol={uint8_t(abs(sin(0.4f*GetRunTime()))+127),uint8_t(0),uint8_t(0)};

View File

@ -203,6 +203,7 @@ public:
//If back is true, places the effect in the background
Effect&AddEffect(std::unique_ptr<Effect>foreground,bool back=false);
const std::vector<Effect*>GetEffect(EffectType type);
std::vector<Entity>GetTargetsInRange(vf2d pos,float radius,bool upperLevel,float z,const HurtType hurtTargets)const;
const HurtList Hurt(vf2d pos,float radius,int damage,bool upperLevel,float z,const HurtType hurtTargets,HurtFlag::HurtFlag hurtFlags=HurtFlag::NONE)const;
const HurtList HurtMonsterType(vf2d pos,float radius,int damage,bool upperLevel,float z,const std::string_view monsterName,HurtFlag::HurtFlag hurtFlags=HurtFlag::NONE)const;
//NOTE: This function will also add any enemies that were hit into the hit list!

View File

@ -43,7 +43,7 @@ All rights reserved.
INCLUDE_game
Blizzard::Blizzard(const vf2d pos,const float radius, const float lifetime,const int damage,const float tickRate,const std::pair<MinScale,MaxScale>snowSizeRange,const float emitterFreq,const bool upperLevel,const FriendlyType friendly)
:damage(damage),tickRate(tickRate),tickTimer(tickRate),radius(radius),friendly(friendly), Effect(pos,lifetime,"aiming_target.png",upperLevel,(radius*2)*vf2d{1.f,1.f},0.5f,{},{199,247,239,128},0.f,0.f,true)
:damage(damage),tickRate(tickRate),tickTimer(tickRate),radius(radius),friendly(friendly), Effect(pos,lifetime,"aiming_target.png",upperLevel,(radius*2)*vf2d{1.f,1.f}/24.f,0.5f,{},{199,247,239,128},0.f,0.f,true)
,snow(std::make_unique<BlizzardSnowEmitter>(pos,"circle.png",radius,snowSizeRange,emitterFreq,lifetime,upperLevel)){
for(int i:std::ranges::iota_view(0,100))snow->Emit();
}
@ -51,11 +51,8 @@ bool Blizzard::Update(float fElapsedTime){
tickTimer-=fElapsedTime;
if(tickTimer<=0.f){
tickTimer+=tickRate;
game->Hurt(pos,radius,damage,OnUpperLevel(),z,friendly?HurtType::MONSTER:HurtType::PLAYER);
game->Hurt(pos,radius*0.9f,damage,OnUpperLevel(),z,friendly?HurtType::MONSTER:HurtType::PLAYER);
}
snow->Update(fElapsedTime);
return Effect::Update(fElapsedTime);
}
void Blizzard::Draw(const Pixel blendCol)const{
Effect::Draw(blendCol);
}

View File

@ -44,14 +44,14 @@ Buff::Buff(std::variant<Player*,std::weak_ptr<Monster>>attachedTarget,BuffType t
:Buff(attachedTarget,type,duration,intensity,[](std::weak_ptr<Monster>m,Buff&b){}){}
Buff::Buff(std::variant<Player*,std::weak_ptr<Monster>>attachedTarget,BuffType type,float duration,float intensity,PlayerBuffExpireCallbackFunction expireCallbackFunc)
:attachedTarget(attachedTarget),type(type),duration(duration),intensity(intensity),originalDuration(this->duration),playerBuffCallbackFunc(expireCallbackFunc){}
:attachedTarget(attachedTarget),type(type),lifetime(duration),intensity(intensity),originalDuration(this->lifetime),playerBuffCallbackFunc(expireCallbackFunc){}
Buff::Buff(std::variant<Player*,std::weak_ptr<Monster>>attachedTarget,BuffType type,float duration,float intensity,MonsterBuffExpireCallbackFunction expireCallbackFunc)
:attachedTarget(attachedTarget),type(type),duration(duration),intensity(intensity),originalDuration(this->duration),monsterBuffCallbackFunc(expireCallbackFunc){}
:attachedTarget(attachedTarget),type(type),lifetime(duration),intensity(intensity),originalDuration(this->lifetime),monsterBuffCallbackFunc(expireCallbackFunc){}
Buff::Buff(std::variant<Player*,std::weak_ptr<Monster>>attachedTarget,BuffType type,float duration,float intensity,std::set<ItemAttribute> attr)
:attachedTarget(attachedTarget),type(type),duration(duration),intensity(intensity),attr(attr),originalDuration(this->duration){}
:attachedTarget(attachedTarget),type(type),lifetime(duration),intensity(intensity),attr(attr),originalDuration(this->lifetime){}
Buff::Buff(std::variant<Player*,std::weak_ptr<Monster>>attachedTarget,BuffType type,float duration,float intensity,std::set<std::string> attr)
:attachedTarget(attachedTarget),type(type),duration(duration),intensity(intensity),originalDuration(this->duration){
:attachedTarget(attachedTarget),type(type),lifetime(duration),intensity(intensity),originalDuration(this->lifetime){
for(const std::string&s:attr){
this->attr.insert(ItemAttribute::attributes.at(s));
}
@ -115,13 +115,13 @@ Buff::Buff(std::variant<Player*,std::weak_ptr<Monster>>attachedTarget,BuffType t
}
Buff::Buff(std::variant<Player*,std::weak_ptr<Monster>>attachedTarget,BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks)
:attachedTarget(attachedTarget),type(type),restorationType(restorationType),duration(duration),intensity(intensity),nextTick(timeBetweenTicks),timeBetweenTicks(timeBetweenTicks),overTimeType(overTimeType),originalDuration(this->duration){}
:attachedTarget(attachedTarget),type(type),restorationType(restorationType),lifetime(duration),intensity(intensity),nextTick(timeBetweenTicks),timeBetweenTicks(timeBetweenTicks),overTimeType(overTimeType),originalDuration(this->lifetime){}
void Buff::Update(AiL*game,float fElapsedTime){
if(!waitOneTick){
duration-=fElapsedTime;
lifetime+=fElapsedTime;
if(enabled&&overTimeType.has_value()&&lifetime>=nextTick){
lifetime-=fElapsedTime;
totalTimeAlive+=fElapsedTime;
if(enabled&&overTimeType.has_value()&&totalTimeAlive>=nextTick){
BuffTick(game,fElapsedTime);
nextTick+=timeBetweenTicks;
if(restorationType==BuffRestorationType::ONE_OFF)enabled=false;

View File

@ -89,17 +89,20 @@ namespace BuffOverTimeType{
class AiL;
using BuffDuration=float;
using BuffIntensity=float;
struct Buff{
using PlayerBuffExpireCallbackFunction=std::function<void(Player*attachedTarget,Buff&b)>;
using MonsterBuffExpireCallbackFunction=std::function<void(std::weak_ptr<Monster>attachedTarget,Buff&b)>;
BuffType type;
BuffRestorationType restorationType{BuffRestorationType::ONE_OFF};
float duration=1;
float lifetime=1;
float timeBetweenTicks=1;
float intensity=1;
float nextTick=0;
float lifetime{};
float totalTimeAlive{};
std::set<ItemAttribute> attr;
std::variant<Player*,std::weak_ptr<Monster>>attachedTarget; //Who has this buff.
PlayerBuffExpireCallbackFunction playerBuffCallbackFunc=[](Player*p,Buff&b){};

View File

@ -111,7 +111,9 @@ void Effect::Draw(const Pixel blendCol)const{
vf2d shadowScale=vf2d{8*size.x/3.f,1}/std::max(1.f,GetZ()/24);
game->view.DrawDecal(pos-vf2d{3,3}*shadowScale/2+vf2d{0,6*size.x},GFX["circle.png"].Decal(),shadowScale,BLACK);
}
//game->view.DrawDecal(pos,GetFrame().GetSourceImage()->Decal());
game->view.DrawPartialRotatedDecal(pos-vf2d{0,GetZ()},GetFrame().GetSourceImage()->Decal(),rotation,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,size,blendCol);
//game->view.DrawRectDecal(pos,GetFrame().GetSourceRect().size);
}
Animate2D::Frame Effect::GetFrame()const{

View File

@ -113,8 +113,8 @@ struct Meteor:Effect{
Meteor(const vf2d pos,const float lifetime,const bool upperLevel,const MeteorSetting setting,const std::pair<MeteorDamage,PulsatingFireDamage>damage,const float fadeout=0.0f,const vf2d spd={},const Pixel col=WHITE,const float rotation=0,const float rotationSpd=0,const bool additiveBlending=false);
float startLifetime=0;
bool shakeField=false;
bool Update(float fElapsedTime)override;
void Draw(const Pixel blendCol)const override;
bool Update(float fElapsedTime)override; //NOTE: In most cases, call Effect::Update() in your overwritten function!
void Draw(const Pixel blendCol)const override; //NOTE: In most cases, call Effect::Update() in your overwritten function!
private:
int meteorImpactParticles{};
float randomColorTintR{};
@ -141,13 +141,13 @@ struct PulsatingFire:Effect{
float radius;
int damage;
const PulsatingFireSetting setting;
bool Update(float fElapsedTime)override;
void Draw(const Pixel blendCol)const override;
bool Update(float fElapsedTime)override; //NOTE: In most cases, call Effect::Update() in your overwritten function!
void Draw(const Pixel blendCol)const override; //NOTE: In most cases, call Effect::Update() in your overwritten function!
};
struct SwordSlash:Effect{
SwordSlash(float lifetime,std::string imgFile,float damageMult, float swordSweepAngle,vf2d size={1,1},float fadeout=0.0f,vf2d spd={},Pixel col=WHITE,float rotation=0,float rotationSpd=0,bool additiveBlending=false);
bool Update(float fElapsedTime)override;
bool Update(float fElapsedTime)override; //NOTE: In most cases, call Effect::Update() in your overwritten function!
private:
HitList hitList;
const float damageMult;
@ -158,21 +158,21 @@ private:
struct ForegroundEffect:Effect{
ForegroundEffect(vf2d pos,float lifetime,std::string imgFile,bool upperLevel,float size=1.0f,float fadeout=0.0f,vf2d spd={},Pixel col=WHITE,float rotation=0,float rotationSpd=0,bool additiveBlending=false);
ForegroundEffect(vf2d pos,float lifetime,std::string imgFile,bool upperLevel,vf2d size={1,1},float fadeout=0.0f,vf2d spd={},Pixel col=WHITE,float rotation=0,float rotationSpd=0,bool additiveBlending=false);
virtual void Draw(const Pixel blendCol)const override final;
virtual void Draw(const Pixel blendCol)const override final; //NOTE: In most cases, call Effect::Update() in your overwritten function!
};
struct SpellCircle:Effect{
SpellCircle(vf2d pos,float lifetime,std::string imgFile,std::string spellInsigniaFile,bool upperLevel,float size=1.0f,float fadeout=0.0f,vf2d spd={},Pixel col=WHITE,float rotation=0,float rotationSpd=0,bool additiveBlending=false,float insigniaSize=1.0f,float insigniaFadeout=0.0f,vf2d insigniaSpd={},Pixel insigniaCol=WHITE,float insigniaRotation=0,float insigniaRotationSpd=0,bool insigniaAdditiveBlending=false);
SpellCircle(vf2d pos,float lifetime,std::string imgFile,std::string spellInsigniaFile,bool upperLevel,vf2d size={1,1},float fadeout=0.0f,vf2d spd={},Pixel col=WHITE,float rotation=0,float rotationSpd=0,bool additiveBlending=false,vf2d insigniaSize={1,1},float insigniaFadeout=0.0f,vf2d insigniaSpd={},Pixel insigniaCol=WHITE,float insigniaRotation=0,float insigniaRotationSpd=0,bool insigniaAdditiveBlending=false);
Effect spellInsignia{vf2d{},0.f,"spell_insignia.png",false,{}};
virtual bool Update(float fElapsedTime)override final;
virtual void Draw(const Pixel blendCol)const override final;
virtual bool Update(float fElapsedTime)override final; //NOTE: In most cases, call Effect::Update() in your overwritten function!
virtual void Draw(const Pixel blendCol)const override final; //NOTE: In most cases, call Effect::Update() in your overwritten function!
};
struct RockLaunch:Effect{
//The lifetime is for how long the effect lasts after it's launched. You don't have to calculate extra lifetime to compensate for the delayTime, it is done for you.
RockLaunch(vf2d pos,float lifetime,std::string imgFile,float delayTime,float size,float fadeout,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending=false);
virtual bool Update(float fElapsedTime)override final;
virtual bool Update(float fElapsedTime)override final; //NOTE: In most cases, call Effect::Update() in your overwritten function!
private:
float delayTime;
vf2d futureSpd;
@ -181,7 +181,7 @@ private:
struct ShineEffect:Effect{
//An effect that starts invisible, becomes visible, then fades out again.
ShineEffect(vf2d pos,float fadeinTime,float fadeoutTime,std::string imgFile,float size,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending=false);
virtual bool Update(float fElapsedTime)override final;
virtual bool Update(float fElapsedTime)override final; //NOTE: In most cases, call Effect::Update() in your overwritten function!
private:
float fadeinTime;
const Pixel originalCol;
@ -190,8 +190,8 @@ private:
//Spawns and moves towards the player after some amount of time.
struct MonsterSoul:Effect{
MonsterSoul(vf2d pos,float fadeoutTime,float size,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending=false);
virtual bool Update(float fElapsedTime)override final;
virtual void Draw(const Pixel blendCol)const override final;
virtual bool Update(float fElapsedTime)override final; //NOTE: In most cases, call Effect::Update() in your overwritten function!
virtual void Draw(const Pixel blendCol)const override final; //NOTE: In most cases, call Effect::Update() in your overwritten function!
public:
enum Phase{
RISING,
@ -209,8 +209,8 @@ struct FadeInOutEffect:Effect{
FadeInOutEffect(vf2d pos,const std::string&img,float lifetime,float cycleSpd,bool onUpperLevel,float size,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending=false,float particleSpawnFreq=0.f,const std::function<Effect(const Effect&self)>&particleGenerator={});
//A version with oscillators for position and colors, for extra animation effects!
FadeInOutEffect(Oscillator<vf2d>pos,const std::string&img,float lifetime,bool onUpperLevel,Oscillator<vf2d>size,vf2d spd,Oscillator<Pixel>col,float rotation,float rotationSpd,bool additiveBlending=false,float particleSpawnFreq=0.f,const std::function<Effect(const Effect&self)>&particleGenerator={});
virtual bool Update(float fElapsedTime)override;
virtual void Draw(const Pixel blendCol)const override;
virtual bool Update(float fElapsedTime)override; //NOTE: In most cases, call Effect::Update() in your overwritten function!
virtual void Draw(const Pixel blendCol)const override; //NOTE: In most cases, call Effect::Update() in your overwritten function!
std::function<Effect(const Effect&self)>particleGenerator;
const float particleSpawnFreq;
Oscillator<vf2d>posOscillator;
@ -222,7 +222,7 @@ struct FadeInOutEffect:Effect{
struct LingeringEffect:FadeInOutEffect{
LingeringEffect(vf2d pos,const std::string&img,const std::string&soundEffect,float radius,int damage,float damageFreq,const HurtType friendly,float lifetime,float cycleSpd,bool onUpperLevel,float size,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending=false,float particleSpawnFreq=0.f,const std::function<Effect(const Effect&self)>&particleGenerator={});
virtual bool Update(float fElapsedTime)override final;
virtual bool Update(float fElapsedTime)override final; //NOTE: In most cases, call Effect::Update() in your overwritten function!
const int damage;
float damageTimer{};
const float originalDamageTimer{};
@ -235,7 +235,7 @@ struct Ink:Effect{
//NOTE: The way ink works is that every update frame, each ink stack will check for distance to player, and if the player collides with the radius of the ink it will add one to the ink debuff for the player.
// At the end of AdventuresInLestoria::HandleUserInput(), the ink stack counter is reset after handling user input for moving the player.
Ink(vf2d pos,float radius,float lifetime,float inkSlowdownDuration,float inkSlowdownPct,float fadeinTime,float fadeoutTime,vf2d size,Pixel col,bool onUpperLevel,float rotation);
virtual bool Update(float fElapsedTime)override final;
virtual bool Update(float fElapsedTime)override final; //NOTE: In most cases, call Effect::Update() in your overwritten function!
private:
const float radius;
const float inkSlowdownDuration;
@ -245,8 +245,8 @@ private:
struct FlipCoinEffect:Effect{
//cycleSpd is how long it takes to get from fully opaque to fully transparent, and back to fully opaque
FlipCoinEffect(Oscillator<vf2d>startEndPos,float coinArcRiseZAmt,float lifetime,const std::string&imgFile,bool upperLevel,float size=1.0f,float fadeout=0.0f,vf2d spd={},Pixel col=WHITE,float rotation=0,float rotationSpd=0,bool additiveBlending=false);
virtual bool Update(float fElapsedTime)override;
virtual void Draw(const Pixel blendCol)const override;
virtual bool Update(float fElapsedTime)override; //NOTE: In most cases, call Effect::Update() in your overwritten function!
virtual void Draw(const Pixel blendCol)const override; //NOTE: In most cases, call Effect::Update() in your overwritten function!
Oscillator<vf2d>startEndPos;
Oscillator<float>sizeFlipper;
Oscillator<float>zRiser;
@ -255,8 +255,8 @@ struct FlipCoinEffect:Effect{
struct CollectCoinEffect:Effect{
//cycleSpd is how long it takes to get from fully opaque to fully transparent, and back to fully opaque
CollectCoinEffect(const Entity attachedTarget,float coinArcRiseZAmt,float lifetime,const std::string&imgFile,bool upperLevel,float size=1.0f,float fadeout=0.0f,vf2d spd={},Pixel col=WHITE,float rotation=0,float rotationSpd=0,bool additiveBlending=false);
virtual bool Update(float fElapsedTime)override;
virtual void Draw(const Pixel blendCol)const override;
virtual bool Update(float fElapsedTime)override; //NOTE: In most cases, call Effect::Update() in your overwritten function!
virtual void Draw(const Pixel blendCol)const override; //NOTE: In most cases, call Effect::Update() in your overwritten function!
const Entity attachedTarget;
Oscillator<float>sizeFlipper;
Oscillator<float>zRiser;
@ -266,7 +266,6 @@ struct Blizzard:Effect{
public:
Blizzard(const vf2d pos,const float radius,const float lifetime,const int damage,const float tickRate,const std::pair<MinScale,MaxScale>snowSizeRange,const float emitterFreq,const bool upperLevel,const FriendlyType friendly);
bool Update(float fElapsedTime)override; //NOTE: In most cases, call Effect::Update() in your overwritten function!
void Draw(const Pixel blendCol)const override; //NOTE: In most cases, call Effect::Draw() in your overwritten function!
private:
const int damage;
const float tickRate;
@ -274,4 +273,27 @@ private:
const float radius;
const FriendlyType friendly;
const std::unique_ptr<BlizzardSnowEmitter>snow;
};
struct FreezeGround:Effect{
struct FreezeGroundSettings{
float radius;
float slowdownPerStack;
float slowdownFrequency;
float maxSlowdownStacks;
int stacksRequiredForDamage;
float attackMultiplierPerStack;
int stacksRemovedImmediatelyOnLeave;
float stackRemovalRate;
float lifetime;
};
public:
FreezeGround(const vf2d pos,const float lifetime,const int damage,const FreezeGroundSettings settings,const bool upperLevel,const FriendlyType friendly);
bool Update(float fElapsedTime)override; //NOTE: In most cases, call Effect::Update() in your overwritten function!
private:
const int damage;
float tickTimer{};
std::unordered_set<Entity>entitiesInside;
const FriendlyType friendly;
const FreezeGroundSettings settings;
};

View File

@ -39,22 +39,42 @@ All rights reserved.
#include "Entity.h"
#include "Player.h"
#define is(type) std::holds_alternative<type>(entity)
#define get(type) std::get<type>(entity)
#define is(type) std::holds_alternative<type>(this->entity)
#define get(type) std::get<type>(this->entity)
#define CallClassFunc(func) \
if(is(Player*))return get(Player*)->func; \
if(is(Monster*))return get(Monster*)->func; \
ERR("Entity is not a valid type! THIS SHOULD NOT BE HAPPENING!");
Entity::Entity(Player*player):entity(player){}
Entity::Entity(Monster*monster):entity(monster){}
Entity::Entity(const std::variant<Monster*,Player*>ent):entity(ent){}
const vf2d Entity::GetPos()const{
if(is(Player*))return get(Player*)->GetPos();
if(is(Monster*))return get(Monster*)->GetPos();
ERR("Entity is not a valid type! THIS SHOULD NOT BE HAPPENING!");
return {}; //NOTE: Unreachable
CallClassFunc(GetPos());
}
void Entity::SetIframeTime(const float iframeTime){
if(is(Player*))get(Player*)->ApplyIframes(iframeTime);
else if(is(Monster*))get(Monster*)->ApplyIframes(iframeTime);
else ERR("Entity is not a valid type! THIS SHOULD NOT BE HAPPENING!");
CallClassFunc(ApplyIframes(iframeTime));
}
Buff&Entity::GetOrAddBuff(BuffType buffType,std::pair<BuffDuration,BuffIntensity>newBuff){
CallClassFunc(GetOrAddBuff(buffType,newBuff));
}
const std::optional<Buff>Entity::GetBuff(BuffType buff)const{
CallClassFunc(GetBuff(buff));
}
const float Entity::GetDistanceFrom(vf2d target)const{
CallClassFunc(GetDistanceFrom(target));
}
bool Entity::Hurt(int damage,bool onUpperLevel,float z,HurtFlag::HurtFlag hurtFlags){
CallClassFunc(Hurt(damage,onUpperLevel,z,hurtFlags));
}
std::optional<Buff>Entity::EditBuff(BuffType buff){
CallClassFunc(EditBuff(buff));
}

View File

@ -39,18 +39,45 @@ All rights reserved.
#include "olcUTIL_Geometry2D.h"
#include <variant>
#include"Buff.h"
#include"HurtDamageInfo.h"
class Player;
class Monster;
//A helper class to connect multiple entity types based on their commonalities without using inheritance shenanigans.
class Entity{
friend struct std::hash<Entity>;
friend bool operator==(const Entity&lhs,const Entity&rhs);
public:
Entity(Player*player);
Entity(Monster*monster);
Entity(const std::variant<Monster*,Player*>ent);
const vf2d GetPos()const;
void SetIframeTime(const float iframeTime);
Buff&GetOrAddBuff(BuffType buffType,std::pair<BuffDuration,BuffIntensity>newBuff);
const std::optional<Buff>GetBuff(BuffType buff)const;
const float GetDistanceFrom(vf2d target)const;
bool Hurt(int damage,bool onUpperLevel,float z,HurtFlag::HurtFlag hurtFlags=HurtFlag::NONE);
std::optional<Buff>EditBuff(BuffType buff);
private:
const std::variant<Monster*,Player*>entity;
inline bool operator==(const Entity&rhs){return entity==rhs.entity;}
};
inline bool operator==(const Entity&lhs,const Entity&rhs){return lhs.entity==rhs.entity;}
template <>
struct std::hash<Entity>
{
std::size_t operator()(const Entity&entity)const{
if(std::holds_alternative<Player*>(entity.entity))return std::hash<Player*>()(std::get<Player*>(entity.entity)); \
if(std::holds_alternative<Monster*>(entity.entity))return std::hash<Monster*>()(std::get<Monster*>(entity.entity)); \
ERR("Entity is not a valid type! THIS SHOULD NOT BE HAPPENING!");
}
std::size_t operator()(Entity&entity)const{
if(std::holds_alternative<Player*>(entity.entity))return std::hash<Player*>()(std::get<Player*>(entity.entity)); \
if(std::holds_alternative<Monster*>(entity.entity))return std::hash<Monster*>()(std::get<Monster*>(entity.entity)); \
ERR("Entity is not a valid type! THIS SHOULD NOT BE HAPPENING!");
}
};

View File

@ -47,6 +47,7 @@ All rights reserved.
#ifndef __EMSCRIPTEN__
#include <stacktrace>
#endif
#include<utility>
inline std::ofstream debugLogger;
@ -71,7 +72,9 @@ inline std::ofstream debugLogger;
#define ERR(err) { \
std::stringstream errStream; \
errStream<<err; \
Error::log(errStream,std::source_location::current());}
Error::log(errStream,std::source_location::current()); \
std::unreachable(); \
}
#define LOG(msg) { \
std::stringstream debugStream; \
debugStream<<msg; \

View File

@ -0,0 +1,50 @@
#include"Effect.h"
#include"AdventuresInLestoria.h"
INCLUDE_GFX
INCLUDE_game
FreezeGround::FreezeGround(const vf2d pos,const float lifetime,const int damage,const FreezeGroundSettings settings,const bool upperLevel,const FriendlyType friendly)
:damage(damage),friendly(friendly),settings(settings),Effect(pos,lifetime,"freezeground.png",upperLevel,settings.radius/100.f/(GFX["freezeground.png"].Sprite()->Size().x/24.f)*2.35f,0.5f,{},{199,247,239,32},0.f,0.f,true)
{}
bool FreezeGround::Update(float fElapsedTime){
tickTimer+=fElapsedTime;
while(tickTimer>=settings.slowdownFrequency){
for(Entity&target:game->GetTargetsInRange(pos,settings.radius/100.f*24,OnUpperLevel(),GetZ(),friendly?HurtType::MONSTER:HurtType::PLAYER)){
Buff&slowdownBuff{target.GetOrAddBuff(BuffType::SLOWDOWN,{settings.slowdownFrequency,0.f})};
slowdownBuff.monsterBuffCallbackFunc=[settings=settings](std::weak_ptr<Monster>m,Buff&buff){
int stacks=round(buff.intensity/settings.slowdownPerStack*100.f);
if(stacks>1){
stacks--;
buff.intensity=settings.slowdownPerStack/100.f*stacks;
buff.lifetime+=1.f;
}
};
int stacks=round(slowdownBuff.intensity/settings.slowdownPerStack*100.f);
if(stacks<settings.maxSlowdownStacks)stacks++;
if(stacks>=settings.stacksRequiredForDamage)target.Hurt(settings.attackMultiplierPerStack/100.f*damage,OnUpperLevel(),GetZ());
slowdownBuff.intensity=settings.slowdownPerStack/100.f*stacks;
slowdownBuff.lifetime+=1.f;
entitiesInside.insert(std::move(target));
}
auto entitiesThatHaveLeft{entitiesInside|std::views::filter([&pos=pos,&radius=settings.radius](const Entity&entity){return entity.GetDistanceFrom(pos)>=radius;})|std::ranges::to<std::unordered_set>()};
std::for_each(entitiesThatHaveLeft.begin(),entitiesThatHaveLeft.end(),[&slowdownPerStack=settings.slowdownPerStack](const Entity&entity){
if(auto buffCheck{const_cast<Entity&>(entity).EditBuff(BuffType::SLOWDOWN)};buffCheck){
Buff&slowdownBuff{*buffCheck};
int stacks=round(slowdownBuff.intensity/slowdownPerStack*100.f);
if(stacks>1)stacks--;
else slowdownBuff.lifetime=0.f;
slowdownBuff.intensity=slowdownPerStack/100.f*stacks;
}
});
tickTimer-=settings.slowdownFrequency;
}
return Effect::Update(fElapsedTime);
}

View File

@ -46,7 +46,7 @@ bool Ink::Update(float fElapsedTime){
const float distToPlayer{util::distance(game->GetPlayer()->GetPos(),pos)};
if(distToPlayer<=radius){
Buff&currentInkSlowdown{game->GetPlayer()->GetOrAddBuff(BuffType::INK_SLOWDOWN,{inkSlowdownDuration,inkSlowdownAmount})};
currentInkSlowdown.duration=inkSlowdownDuration; //Reset the duration if needed.
currentInkSlowdown.lifetime=inkSlowdownDuration; //Reset the duration if needed.
}
return Effect::Update(fElapsedTime);
}

View File

@ -92,7 +92,7 @@ void LoadingScreen::Draw(){
scale=2.f;
}
const std::vector<Buff>attackBuffs=game->GetPlayer()->GetStatBuffs({"Attack","Attack %"});
Pixel blendCol=attackBuffs.size()>0?Pixel{255,uint8_t(255*abs(sin(1.4*attackBuffs[0].duration))),uint8_t(255*abs(sin(1.4*attackBuffs[0].duration)))}:WHITE;
Pixel blendCol=attackBuffs.size()>0?Pixel{255,uint8_t(255*abs(sin(1.4*attackBuffs[0].lifetime))),uint8_t(255*abs(sin(1.4*attackBuffs[0].lifetime)))}:WHITE;
if(showGhost){
blendCol=BLACK;
}

View File

@ -374,10 +374,10 @@ void Monster::Update(const float fElapsedTime){
if(IsAlive()){
std::for_each(buffList.begin(),buffList.end(),[&](Buff&b){b.Update(game,fElapsedTime);});
std::erase_if(buffList,[](Buff&b){
if(b.duration<=0.f){
if(b.lifetime<=0.f){
if(!std::holds_alternative<std::weak_ptr<Monster>>(b.attachedTarget))ERR(std::format("WARNING! Somehow removed buff of type {} is inside the player buff list but is not attached to the Player? THIS SHOULD NOT BE HAPPENING!",int(b.type)));
if(b.monsterBuffCallbackFunc)b.monsterBuffCallbackFunc(std::get<std::weak_ptr<Monster>>(b.attachedTarget),b);
return true;
return b.lifetime<=0.f;
}
return false;
});
@ -536,7 +536,7 @@ void Monster::Draw()const{
const auto HasBuff=[&](const BuffType buff){return GetBuffs(buff).size()>0;};
//The lerpCutAmount is how much to divide the initial color by, which is used as the lerp oscillation amount. 0.5 means half the color is always active, and the other half linearly oscillates. 0.1 would mean 90% of the color is normal and 10% of the color oscillates.
const auto GetBuffBlendCol=[&](const BuffType buff,const float oscillationTime_s,const Pixel blendCol,const float lerpCutAmount=0.5f){return Pixel{uint8_t(blendCol.r*(1-lerpCutAmount)+blendCol.r*lerpCutAmount*abs(sin(oscillationTime_s*PI*GetBuffs(buff)[0].lifetime))),uint8_t(blendCol.g*(1-lerpCutAmount)+blendCol.g*lerpCutAmount*abs(sin(oscillationTime_s*PI*GetBuffs(buff)[0].lifetime))),uint8_t(blendCol.b*(1-lerpCutAmount)+blendCol.b*lerpCutAmount*abs(sin(oscillationTime_s*PI*GetBuffs(buff)[0].lifetime)))};};
const auto GetBuffBlendCol=[&](const BuffType buff,const float oscillationTime_s,const Pixel blendCol,const float lerpCutAmount=0.5f){return Pixel{uint8_t(blendCol.r*(1-lerpCutAmount)+blendCol.r*lerpCutAmount*abs(sin(oscillationTime_s*PI*GetBuffs(buff)[0].totalTimeAlive))),uint8_t(blendCol.g*(1-lerpCutAmount)+blendCol.g*lerpCutAmount*abs(sin(oscillationTime_s*PI*GetBuffs(buff)[0].totalTimeAlive))),uint8_t(blendCol.b*(1-lerpCutAmount)+blendCol.b*lerpCutAmount*abs(sin(oscillationTime_s*PI*GetBuffs(buff)[0].totalTimeAlive)))};};
if(HasBuff(BuffType::BURNING_ARROW_BURN))blendCol=GetBuffBlendCol(BuffType::BURNING_ARROW_BURN,1.f,{255,160,0});
else if(HasBuff(BuffType::DAMAGE_AMPLIFICATION))blendCol=GetBuffBlendCol(BuffType::DAMAGE_AMPLIFICATION,1.4f,{255,0,0});
@ -544,7 +544,7 @@ void Monster::Draw()const{
else if(HasBuff(BuffType::COLOR_MOD))blendCol=GetBuffBlendCol(BuffType::COLOR_MOD,1.4f,PixelRaw(GetBuffs(BuffType::COLOR_MOD)[0].intensity),1.f);
else if(HasBuff(BuffType::SLOWDOWN))blendCol=GetBuffBlendCol(BuffType::SLOWDOWN,1.4f,{255,255,128},0.5f);
else if(HasBuff(BuffType::HASTEN))blendCol=GetBuffBlendCol(BuffType::HASTEN,0.4f,{128,0,0},1.f);
else if(glowPurpleBuff.has_value())blendCol=Pixel{uint8_t(255*abs(sin(1.4*glowPurpleBuff.value().duration))),uint8_t(255*abs(sin(1.4*glowPurpleBuff.value().duration))),uint8_t(128+127*abs(sin(1.4*glowPurpleBuff.value().duration)))};
else if(glowPurpleBuff.has_value())blendCol=Pixel{uint8_t(255*abs(sin(1.4*glowPurpleBuff.value().lifetime))),uint8_t(255*abs(sin(1.4*glowPurpleBuff.value().lifetime))),uint8_t(128+127*abs(sin(1.4*glowPurpleBuff.value().lifetime)))};
const vf2d hitTimerOffset=vf2d{sin(20*PI*lastHitTimer+randomFrameOffset),0.f}*2.f*GetSizeMult();
const vf2d zOffset=-vf2d{0,GetZ()}+vf2d{0,(MONSTER_DATA[GetName()].IsFloating()?floatOscillator.get():0.f)};
@ -610,7 +610,7 @@ void Monster::Draw()const{
float remainingStackDuration{};
for(const Buff&b:buffList){
if(b.type==BuffType::TRAPPER_MARK){
remainingStackDuration=b.duration;
remainingStackDuration=b.lifetime;
break;
}
}
@ -626,7 +626,7 @@ void Monster::Draw()const{
float remainingStackDuration{};
for(const Buff&b:buffList){
if(b.type==BuffType::SPECIAL_MARK){
remainingStackDuration=b.duration;
remainingStackDuration=b.lifetime;
specialMarkOriginalDuration=b.originalDuration;
break;
}
@ -831,7 +831,7 @@ bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag da
if(GetSpecialMarkStacks()>0){
ApplyMarkEffects();
const Buff specialMarkBuff{GetBuffs(BuffType::SPECIAL_MARK)[0]};
if(game->GetPlayer()->HasEnchant("Opportunity Shot")&&specialMarkBuff.originalDuration-specialMarkBuff.duration<="Opportunity Shot"_ENC["MARK TRIGGER TIME"])Trapper::ability1.cooldown-="Opportunity Shot"_ENC["MARK COOLDOWN REDUCE"];
if(game->GetPlayer()->HasEnchant("Opportunity Shot")&&specialMarkBuff.originalDuration-specialMarkBuff.lifetime<="Opportunity Shot"_ENC["MARK TRIGGER TIME"])Trapper::ability1.cooldown-="Opportunity Shot"_ENC["MARK COOLDOWN REDUCE"];
RemoveSpecialMarkStack();
}else if(GetMarkStacks()>0){
ApplyMarkEffects();
@ -839,7 +839,7 @@ bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag da
}
if(game->GetPlayer()->HasEnchant("Lethal Tempo")){
Buff&lethalTempoBuff{game->GetPlayer()->GetOrAddBuff(BuffType::LETHAL_TEMPO,{"Lethal Tempo"_ENC["STACK DURATION"],0.f})};
lethalTempoBuff.duration="Lethal Tempo"_ENC["STACK DURATION"];
lethalTempoBuff.lifetime="Lethal Tempo"_ENC["STACK DURATION"];
const int maxStackCount{int("Lethal Tempo"_ENC["MAX ATTACK SPEED INCREASE"]/"Lethal Tempo"_ENC["ATTACK SPEED INCREASE"])};
lethalTempoBuff.intensity=std::min(maxStackCount,int(lethalTempoBuff.intensity)+1);
}
@ -949,31 +949,39 @@ const bool Monster::OnUpperLevel()const{
return upperLevel;
}
void Monster::AddBuff(BuffType type,float duration,float intensity){
buffList.emplace_back(GetWeakPointer(),type,duration,intensity);
Buff&Monster::AddBuff(BuffType type,float duration,float intensity){
return buffList.emplace_back(GetWeakPointer(),type,duration,intensity);
}
void Monster::AddBuff(BuffType type,float duration,float intensity,std::set<ItemAttribute>attr){
Buff&Monster::AddBuff(BuffType type,float duration,float intensity,std::set<ItemAttribute>attr){
if(type==STAT_UP)std::for_each(attr.begin(),attr.end(),[](const ItemAttribute&attr){if(attr.ActualName()!="Health"&&attr.ActualName()!="Health %"&&attr.ActualName()!="Attack"&&attr.ActualName()!="Attack %")ERR(std::format("WARNING! Stat Up Attribute type {} is NOT IMPLEMENTED!",attr.ActualName()));});
buffList.emplace_back(GetWeakPointer(),type,duration,intensity,attr);
return buffList.emplace_back(GetWeakPointer(),type,duration,intensity,attr);
}
void Monster::AddBuff(BuffType type,float duration,float intensity,std::set<std::string>attr){
Buff&Monster::AddBuff(BuffType type,float duration,float intensity,std::set<std::string>attr){
if(type==STAT_UP)std::for_each(attr.begin(),attr.end(),[](const std::string&attr){if(attr!="Health"&&attr!="Health %"&&attr!="Attack"&&attr!="Attack %")ERR(std::format("WARNING! Stat Up Attribute type {} is NOT IMPLEMENTED!",attr));});
buffList.emplace_back(GetWeakPointer(),type,duration,intensity,attr);
return buffList.emplace_back(GetWeakPointer(),type,duration,intensity,attr);
}
void Monster::AddBuff(BuffType type,float duration,float intensity,Buff::MonsterBuffExpireCallbackFunction expireCallback){
buffList.emplace_back(GetWeakPointer(),type,duration,intensity,expireCallback);
Buff&Monster::AddBuff(BuffType type,float duration,float intensity,Buff::MonsterBuffExpireCallbackFunction expireCallback){
return buffList.emplace_back(GetWeakPointer(),type,duration,intensity,expireCallback);
}
void Monster::AddBuff(BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks){
Buff&Monster::AddBuff(BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks){
if(type==STAT_UP)ERR("WARNING! Incorrect usage of STAT_UP buff! It should not be an overtime buff! Use a different constructor!!!");
buffList.emplace_back(GetWeakPointer(),type,restorationType,overTimeType,duration,intensity,timeBetweenTicks);
return buffList.emplace_back(GetWeakPointer(),type,restorationType,overTimeType,duration,intensity,timeBetweenTicks);
}
void Monster::AddBuff(BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks,Buff::MonsterBuffExpireCallbackFunction expireCallback){
Buff&Monster::AddBuff(BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks,Buff::MonsterBuffExpireCallbackFunction expireCallback){
if(type==STAT_UP)ERR("WARNING! Incorrect usage of STAT_UP buff! It should not be an overtime buff! Use a different constructor!!!");
buffList.emplace_back(GetWeakPointer(),type,restorationType,overTimeType,duration,intensity,timeBetweenTicks,expireCallback);
return buffList.emplace_back(GetWeakPointer(),type,restorationType,overTimeType,duration,intensity,timeBetweenTicks,expireCallback);
}
Buff&Monster::AddBuff(BuffRestorationType type,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks){
return buffList.emplace_back(Buff{GetWeakPointer(),type,overTimeType,duration,intensity,timeBetweenTicks});
}
Buff&Monster::AddBuff(BuffRestorationType type,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks,Buff::MonsterBuffExpireCallbackFunction expireCallback){
return buffList.emplace_back(Buff{GetWeakPointer(),type,overTimeType,duration,intensity,timeBetweenTicks,expireCallback});
}
void Monster::RemoveBuff(BuffType type){
@ -1033,22 +1041,29 @@ Buff&Monster::EditBuff(BuffType buff,size_t buffInd){
return EditBuffs(buff)[buffInd];
}
std::optional<Buff>Monster::EditBuff(BuffType buff){
auto filteredResults{EditBuffs(buff)};
if(filteredResults.size()>0){return filteredResults.front();}
return {};
}
std::vector<std::reference_wrapper<Buff>>Monster::EditBuffs(BuffType buff){
std::vector<std::reference_wrapper<Buff>>filteredBuffs;
std::copy_if(buffList.begin(),buffList.end(),std::back_inserter(filteredBuffs),[&buff](const Buff&b){return b.type==buff;});
return filteredBuffs;
return buffList|std::views::filter([&buff](const Buff&b){return b.type==buff;})|std::ranges::to<std::vector<std::reference_wrapper<Buff>>>();
}
const std::vector<Buff>Monster::GetBuffs(BuffType buff)const{
std::vector<Buff>filteredBuffs;
std::copy_if(buffList.begin(),buffList.end(),std::back_inserter(filteredBuffs),[&buff](const Buff&b){return b.type==buff;});
return filteredBuffs;
return buffList|std::views::filter([&buff](const Buff&b){return b.type==buff;})|std::ranges::to<std::vector>();
}
const std::optional<Buff>Monster::GetBuff(BuffType buff)const{
auto filteredResults{buffList|std::views::filter([&buff](const Buff&b){return b.type==buff;})};
size_t numberOfElements{(filteredResults|std::ranges::to<std::vector>()).size()};
if(numberOfElements>0){return filteredResults.front();}
return {};
}
const std::vector<Buff>Monster::GetBuffs(std::vector<BuffType>buffs)const{
std::vector<Buff>filteredBuffs;
std::copy_if(buffList.begin(),buffList.end(),std::back_inserter(filteredBuffs),[&buffs](const Buff&b){return std::find(buffs.begin(),buffs.end(),b.type)!=buffs.end();});
return filteredBuffs;
return buffList|std::views::filter([&buffs](const Buff&b){return std::find(buffs.begin(),buffs.end(),b.type)!=buffs.end();})|std::ranges::to<std::vector>();
}
State::State Monster::GetState(){
@ -1185,7 +1200,7 @@ void Monster::OnDeath(){
if(game->GetPlayer()->HasEnchant("Reaper of Souls"))game->AddEffect(std::make_unique<MonsterSoul>(GetPos(),0.3f,GetSizeMult(),vf2d{},WHITE,0.f,0.f,false));
if(game->GetPlayer()->HasEnchant("Bloodlust")&&game->GetPlayer()->GetBuffs(BuffType::ADRENALINE_RUSH).size()>0){
Buff&adrenalineRushBuff{game->GetPlayer()->EditBuff(BuffType::ADRENALINE_RUSH,0)};
adrenalineRushBuff.duration+="Bloodlust"_ENC["BUFF TIMER INCREASE"];
adrenalineRushBuff.lifetime+="Bloodlust"_ENC["BUFF TIMER INCREASE"];
adrenalineRushBuff.intensity=std::min(int("Bloodlust"_ENC["MAX ATTACK BUFF STACKS"]),int(adrenalineRushBuff.intensity)+1);
}
if(game->GetPlayer()->HasEnchant("Spreading Pain")&&GetBuffs(BuffType::CURSE_OF_PAIN).size()>0){
@ -1197,7 +1212,7 @@ void Monster::OnDeath(){
for(std::shared_ptr<Monster>&m:MONSTER_LIST){
if(&*m==&*GetWeakPointer().lock()||m->InUndamageableState(OnUpperLevel(),GetZ()))continue;
if(m->GetBuffs(BuffType::CURSE_OF_PAIN).size()>0){
m->EditBuff(BuffType::CURSE_OF_PAIN,0U).duration=m->EditBuff(BuffType::CURSE_OF_PAIN,0U).originalDuration;
m->EditBuff(BuffType::CURSE_OF_PAIN,0U).lifetime=m->EditBuff(BuffType::CURSE_OF_PAIN,0U).originalDuration;
}else{
float distFromDeadMonster{geom2d::line<float>(GetPos(),m->GetPos()).length()};
if(distFromDeadMonster<"Spreading Pain"_ENC["SPREAD RANGE"]/100*24){
@ -1579,7 +1594,7 @@ void Monster::ApplyMark(float time,uint8_t stackCount){
if(b.type==BuffType::TRAPPER_MARK){
b.intensity+=stackCount;
b.duration=std::max(b.duration,markDuration);
b.lifetime=std::max(b.lifetime,markDuration);
break;
}
}
@ -1598,7 +1613,7 @@ void Monster::ApplySpecialMark(float time,uint8_t stackCount){
if(b.type==BuffType::SPECIAL_MARK){
b.intensity+=stackCount;
b.duration=std::max(b.duration,markDuration);
b.lifetime=std::max(b.lifetime,markDuration);
break;
}
}
@ -1643,14 +1658,6 @@ const bool Monster::InUndamageableState(const bool onUpperLevel,const float z)co
return Invulnerable()||!IsAlive()||onUpperLevel!=OnUpperLevel()||AttackAvoided(z)||IsNPC()||GetCollisionRadius()<=0.f;
}
void Monster::AddBuff(BuffRestorationType type,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks){
buffList.push_back(Buff{GetWeakPointer(),type,overTimeType,duration,intensity,timeBetweenTicks});
}
void Monster::AddBuff(BuffRestorationType type,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks,Buff::MonsterBuffExpireCallbackFunction expireCallback){
buffList.push_back(Buff{GetWeakPointer(),type,overTimeType,duration,intensity,timeBetweenTicks,expireCallback});
}
const bool Monster::CanMove()const{
return knockUpTimer==0.f&&IsAlive()&&stunTimer==0.f&&!currentCast;
}
@ -1802,4 +1809,9 @@ void Monster::PerformSpell(const CastInfo&castData){
void Monster::RunStrategy(const MonsterStrategy&strategy){
monsterStrategies.at(strategy).function(*this,game->GetElapsedTime(),monsterStrategies.at(strategy).name);
}
Buff&Monster::GetOrAddBuff(BuffType buffType,std::pair<BuffDuration,BuffIntensity>newBuff){
if(GetBuffs(buffType).size()>0)return EditBuff(buffType,0);
else return AddBuff(buffType,newBuff.first,newBuff.second);
}

View File

@ -247,17 +247,19 @@ public:
//Returns false if a path could not be found.
bool StartPathfinding(float pathingTime);
void PathAroundBehavior(float fElapsedTime);
void AddBuff(BuffType type,float duration,float intensity);
Buff&AddBuff(BuffType type,float duration,float intensity);
//NOTE: If adding a % increase stat, please use the percentage version! 100% = 1!!
void AddBuff(BuffType type,float duration,float intensity,std::set<ItemAttribute>attr);
Buff&AddBuff(BuffType type,float duration,float intensity,std::set<ItemAttribute>attr);
//NOTE: If adding a % increase stat, please use the percentage version! 100% = 1!!
void AddBuff(BuffType type,float duration,float intensity,std::set<std::string>attr);
void AddBuff(BuffType type,float duration,float intensity,Buff::MonsterBuffExpireCallbackFunction expireCallback);
void AddBuff(BuffRestorationType type,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks);
void AddBuff(BuffRestorationType type,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks,Buff::MonsterBuffExpireCallbackFunction expireCallback);
void AddBuff(BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks);
void AddBuff(BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks,Buff::MonsterBuffExpireCallbackFunction expireCallback);
const bool HasBuff(BuffType buff)const;
Buff&AddBuff(BuffType type,float duration,float intensity,std::set<std::string>attr);
Buff&AddBuff(BuffType type,float duration,float intensity,Buff::MonsterBuffExpireCallbackFunction expireCallback);
Buff&AddBuff(BuffRestorationType type,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks);
Buff&AddBuff(BuffRestorationType type,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks,Buff::MonsterBuffExpireCallbackFunction expireCallback);
Buff&AddBuff(BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks);
Buff&AddBuff(BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks,Buff::MonsterBuffExpireCallbackFunction expireCallback);
Buff&GetOrAddBuff(BuffType buff,std::pair<BuffDuration,BuffIntensity>newBuff);
const bool HasBuff(BuffType buff)const;
const std::optional<Buff>GetBuff(BuffType buff)const;
const std::vector<Buff>GetBuffs(BuffType buff)const;
const std::vector<Buff>GetBuffs(std::vector<BuffType>buffs)const;
//Removes all buffs of a given type.
@ -332,6 +334,7 @@ public:
void ApplyDot(float duration,int damage,float timeBetweenTicks,Buff::MonsterBuffExpireCallbackFunction expireCallbackFunc=[](std::weak_ptr<Monster>m,Buff&b){});
void ApplyDot(float duration,int damage,float timeBetweenTicks,BuffType identifierType,Buff::MonsterBuffExpireCallbackFunction expireCallbackFunc=[](std::weak_ptr<Monster>m,Buff&b){});
const float GetDamageAmplificationMult(const bool backstabOccurred)const;
std::optional<Buff>EditBuff(BuffType buff);
Buff&EditBuff(BuffType buff,size_t buffInd);
std::vector<std::reference_wrapper<Buff>>EditBuffs(BuffType buff);
const bool IsBackstabAttack()const;

View File

@ -17,6 +17,17 @@ std::unordered_map<std::string_view,std::function<void(SpellName,CastPos,CasterM
game->AddEffect(std::make_unique<Blizzard>(pos,ConfigPixels("Blizzard Radius"),ConfigFloat("Blizzard Duration"),m.GetAttack()*ConfigFloat("Blizzard Tick Damage Mult"),ConfigFloat("Blizzard Tick Rate"),std::pair<MinScale,MaxScale>{1.f,2.f},0.1f,m.OnUpperLevel(),NON_FRIENDLY));
}},
{"Freeze Ground",[](SpellName spellName,CastPos pos,CasterMonster&m,const StrategyName&strategy)->void{
std::cout<<"Casted Freeze Ground"<<std::endl;
const FreezeGround::FreezeGroundSettings settings{
.radius=ConfigFloat("Freeze Ground Radius"),
.slowdownPerStack=ConfigFloat("Freeze Ground Slowdown Per Stack"),
.slowdownFrequency=ConfigFloat("Freeze Ground Slowdown Frequency"),
.maxSlowdownStacks=ConfigFloat("Freeze Ground Max Slowdown Stacks"),
.stacksRequiredForDamage=ConfigInt("Freeze Ground Stacks Required For Damage"),
.attackMultiplierPerStack=ConfigFloat("Freeze Ground Attack Multiplier per Stack"),
.stacksRemovedImmediatelyOnLeave=ConfigInt("Freeze Ground Stacks Removed Immediately On Leave"),
.stackRemovalRate=ConfigFloat("Freeze Ground Stack Removal Rate"),
.lifetime=ConfigFloat("Freeze Ground Lifetime"),
};
game->AddEffect(std::make_unique<FreezeGround>(pos,ConfigFloat("Freeze Ground Lifetime"),m.GetAttack(),settings,m.OnUpperLevel(),NON_FRIENDLY),true);
}},
};

View File

@ -435,7 +435,7 @@ void Player::Update(float fElapsedTime){
}
std::for_each(buffList.begin(),buffList.end(),[&](Buff&b){b.Update(game,fElapsedTime);});
std::erase_if(buffList,[](Buff&b){
if(b.duration<=0.f){
if(b.lifetime<=0.f){
if(!std::holds_alternative<Player*>(b.attachedTarget))ERR(std::format("WARNING! Somehow removed buff of type {} is inside the player buff list but is not attached to the Player? THIS SHOULD NOT BE HAPPENING!",int(b.type)));
if(b.playerBuffCallbackFunc)b.playerBuffCallbackFunc(std::get<Player*>(b.attachedTarget),b);
return true;
@ -985,25 +985,25 @@ bool Player::Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag dama
hurtRumbleTime="Player.Hurt Rumble Time"_F;
Input::StartVibration();
Input::SetLightbar(PixelLerp(DARK_RED,GREEN,GetHealthRatio()));
if(IsDOT){
if(lastDotTimer>0){
dotNumberPtr.get()->AddDamage(int(mod_dmg));
if(!(hurtFlags&HurtFlag::NO_DAMAGE_NUMBER)){
if(IsDOT){
if(lastDotTimer>0){
dotNumberPtr.get()->AddDamage(int(mod_dmg));
}else{
dotNumberPtr=DamageNumber::AddDamageNumber(pos+vf2d{0,8.f},int(mod_dmg),NON_FRIENDLY,DamageNumberType::DOT);
}
lastDotTimer=0.05f;
}else{
dotNumberPtr=DamageNumber::AddDamageNumber(pos+vf2d{0,8.f},int(mod_dmg),NON_FRIENDLY,DamageNumberType::DOT);
if(lastHitTimer>0){
damageNumberPtr.get()->AddDamage(int(mod_dmg));
if(tookShieldDamage)damageNumberPtr->SetType(DamageNumberType::SHIELD_LOSS);
damageNumberPtr.get()->SetPauseTimer(0.4f);
}else{
damageNumberPtr=DamageNumber::AddDamageNumber(pos,int(mod_dmg),FRIENDLY);
if(tookShieldDamage)damageNumberPtr->SetType(DamageNumberType::SHIELD_LOSS);
}
lastHitTimer=0.05f;
}
lastDotTimer=0.05f;
}else{
if(lastHitTimer>0){
damageNumberPtr.get()->AddDamage(int(mod_dmg));
if(tookShieldDamage)damageNumberPtr->SetType(DamageNumberType::SHIELD_LOSS);
damageNumberPtr.get()->SetPauseTimer(0.4f);
}else{
damageNumberPtr=DamageNumber::AddDamageNumber(pos,int(mod_dmg),FRIENDLY);
if(tookShieldDamage)damageNumberPtr->SetType(DamageNumberType::SHIELD_LOSS);
DAMAGENUMBER_LIST.push_back(damageNumberPtr);
}
lastHitTimer=0.05f;
}
if(!lowHealthSoundPlayed&&lowHealthSoundPlayedTimer==0.f&&GetHealthRatio()<="Player.Health Warning Pct"_F/100.f){
@ -1175,10 +1175,15 @@ bool Player::OnUpperLevel(){
return upperLevel;
}
const std::optional<Buff>Player::GetBuff(BuffType buff)const{
auto filteredResults{buffList|std::views::filter([&buff](const Buff&b){return b.type==buff;})};
size_t numberOfElements{(filteredResults|std::ranges::to<std::vector>()).size()};
if(numberOfElements>0){return filteredResults.front();}
return {};
}
const std::vector<Buff>Player::GetBuffs(BuffType buff)const{
std::vector<Buff>filteredBuffs;
std::copy_if(buffList.begin(),buffList.end(),std::back_inserter(filteredBuffs),[&buff](const Buff&b){return b.type==buff;});
return filteredBuffs;
return buffList|std::views::filter([&buff](const Buff&b){return b.type==buff;})|std::ranges::to<std::vector<Buff>>();
}
void Player::RemoveBuff(BuffType buff){
@ -1200,10 +1205,14 @@ Buff&Player::EditBuff(BuffType buff,size_t buffInd){
return EditBuffs(buff)[buffInd];
}
std::optional<Buff>Player::EditBuff(BuffType buff){
auto filteredResults{EditBuffs(buff)};
if(filteredResults.size()>0){return filteredResults.front();}
return {};
}
std::vector<std::reference_wrapper<Buff>>Player::EditBuffs(BuffType buff){
std::vector<std::reference_wrapper<Buff>>filteredBuffs;
std::copy_if(buffList.begin(),buffList.end(),std::back_inserter(filteredBuffs),[&buff](const Buff&b){return b.type==buff;});
return filteredBuffs;
return buffList|std::views::filter([&buff](const Buff&b){return b.type==buff;})|std::ranges::to<std::vector<std::reference_wrapper<Buff>>>();
}
void Player::RemoveAllBuffs(BuffType buff){
@ -2343,4 +2352,8 @@ const float Player::GetHastePct()const{
mod_hastePct+=hastePct*b.intensity;
}
return mod_hastePct;
}
const float Player::GetDistanceFrom(vf2d target)const{
return geom2d::line<float>(GetPos(),target).length();
}

View File

@ -182,9 +182,10 @@ public:
const std::vector<Buff>GetBuffs(BuffType buff)const;
const std::vector<Buff>GetStatBuffs(const std::vector<std::string>&attr)const;
using BuffDuration=float;
using BuffIntensity=float;
const std::optional<Buff>GetBuff(BuffType buff)const;
Buff&GetOrAddBuff(BuffType buff,std::pair<BuffDuration,BuffIntensity>newBuff);//When you need a buff added regardless if it already exists. You'll either modify an existing one or create a new one.
std::optional<Buff>EditBuff(BuffType buff);
Buff&EditBuff(BuffType buff,size_t buffInd);
std::vector<std::reference_wrapper<Buff>>EditBuffs(BuffType buff);
//Removes the first buff found.
@ -336,6 +337,7 @@ public:
const bool CoveredInInk()const;
const vf2d&GetPreviousPos()const; //The position the player was at on the last frame, can be used for comparison purposes to predict where the player may be next.
const std::unordered_set<std::string>&GetEnchants()const; //Get all enchants currently affecting the player.
const float GetDistanceFrom(vf2d target)const;
private:
static std::vector<std::reference_wrapper<Ability>>ABILITY_LIST;
const int SHIELD_CAPACITY{32};

View File

@ -20,10 +20,11 @@ Cherry pick 6355054d6c8e76c6aa4b18760a293e3d1a020752 from master
Cherry pick 5914938b709927bb8a8557795d6d46fcbfcfb4ea from SkeletonFireMage
Cherry pick 66a10cee9f874f024633efdc84ebb4432752ab15 from SkeletonFireMage
Snow particles must fade away when Blizzard dies
Blizzard blue area circle should remain during the duration of the attack
Add comments to Effect child Update and Draw declarations
Prevent buff from expiring if the duration is adjusted again.
The Start button on the overworld map should progress to the item loadout screen.

View File

@ -39,7 +39,7 @@ All rights reserved.
#define VERSION_MAJOR 1
#define VERSION_MINOR 3
#define VERSION_PATCH 0
#define VERSION_BUILD 12821
#define VERSION_BUILD 12891
#define stringify(a) stringify_(a)
#define stringify_(a) #a

View File

@ -1446,5 +1446,15 @@ MonsterStrategy
Blizzard Duration = 5s
Blizzard Tick Damage Mult = 0.5x
Blizzard Tick Rate = 1s
Freeze Ground Radius = 500
Freeze Ground Slowdown Per Stack = 5%
Freeze Ground Slowdown Frequency = 1s
Freeze Ground Max Slowdown Stacks = 8
Freeze Ground Stacks Required For Damage = 4
Freeze Ground Attack Multiplier per Stack = 10%
Freeze Ground Stacks Removed Immediately On Leave = 1
Freeze Ground Stack Removal Rate = 1s
Freeze Ground Lifetime = 10s
}
}

View File

@ -2239,8 +2239,8 @@ Monsters
Abilities
{
#Ability Name = Mana cost, % chance to cast (every second), Cast Time (Instant = 0.0s), [Variable representing the cast radius]
Blizzard = 60mp, 60%, 1.5sec, MonsterStrategy.Skeleton Mage.Blizzard Radius
# Freeze Ground = 30mp, 20%, 0.0sec
# Blizzard = 60mp, 60%, 1.5sec, MonsterStrategy.Skeleton Mage.Blizzard Radius
Freeze Ground = 30mp, 50%, 0.0sec
}
Strategy = Skeleton Mage

View File

@ -149,6 +149,7 @@ Images
GFX_Cannonball = cannonball.png
GFX_GhostDagger = ghost_dagger.png
GFX_Coin = coin.png
GFX_FreezeGround = freezeground.png
GFX_Thief_Sheet = nico-thief.png
GFX_Trapper_Sheet = nico-trapper.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB