diff --git a/Adventures in Lestoria Tests/EnchantTests.cpp b/Adventures in Lestoria Tests/EnchantTests.cpp index 8b09d13c..2b84d4fd 100644 --- a/Adventures in Lestoria Tests/EnchantTests.cpp +++ b/Adventures in Lestoria Tests/EnchantTests.cpp @@ -1023,6 +1023,7 @@ namespace EnchantTests nullRing.lock()->EnchantItem("Sword Enchantment"); player->CheckAndPerformAbility(player->GetAbility3(),testKeyboardInput); Assert::AreEqual(300.f,player->GetAttackRange(),L"Attack range of Warrior doubled."); + game->OnUserUpdate(0.f); //Wait an extra tick for the buff to begin going down. game->SetElapsedTime(8.f); game->OnUserUpdate(8.f); Assert::AreEqual(150.f,player->GetAttackRange(),L"Attack range of Warrior is normal again."); @@ -1241,5 +1242,67 @@ namespace EnchantTests player->CheckAndPerformAbility(player->GetRightClickAbility(),testKeyboardInput); Assert::AreEqual("Witch.Right Click Ability.Cooldown"_F,player->GetRightClickAbility().cooldown,L"Cooldown time should be 8s when already in cat form."); } + TEST_METHOD(SpreadingPainNoEnchantCheck){ + game->ChangePlayerClass(WITCH); + player=game->GetPlayer(); + MonsterData testMonsterData{"TestName2","Test Monster2",1,10,5,{MonsterDropData{"Health Potion",100.f,1,1}},200.f}; + MONSTER_DATA["TestName2"]=testMonsterData; + Monster&newMonster{game->SpawnMonster({30.f,0.f},MONSTER_DATA["TestName2"])}; + Monster&newMonster2{game->SpawnMonster({30.f,0.f},MONSTER_DATA["TestName2"])}; + Monster&newMonster3{game->SpawnMonster({30.f,0.f},MONSTER_DATA["TestName2"])}; + Monster&newMonster4{game->SpawnMonster({30.f,0.f},MONSTER_DATA["TestName2"])}; + game->OnUserUpdate(0.f); + player->SetTestScreenAimingLocation(game->GetScreenSize()/2+vf2d{30.f,0.f}); + player->PrepareCast(player->GetAbility1()); + player->CastSpell(player->GetAbility1()); + game->OnUserUpdate(0.f); + Assert::IsTrue(newMonster.GetBuffs(BuffType::CURSE_OF_PAIN).size()>0,L"The first monster should have been targeted with Curse of Pain."); + game->SetElapsedTime(3.f); + game->OnUserUpdate(3.f); + Assert::IsTrue(newMonster.IsDead(),L"The first monster has died to Curse of Pain's tick."); + Assert::IsTrue(newMonster2.GetBuffs(BuffType::CURSE_OF_PAIN).size()==0,L"Without the enchant, Curse of Pain should not have spread to Monster 2."); + Assert::IsTrue(newMonster3.GetBuffs(BuffType::CURSE_OF_PAIN).size()==0,L"Without the enchant, Curse of Pain should not have spread to Monster 3."); + Assert::IsTrue(newMonster4.GetBuffs(BuffType::CURSE_OF_PAIN).size()==0,L"Without the enchant, Curse of Pain should not have spread to Monster 4."); + } + TEST_METHOD(SpreadingPainEnchantCheck){ + game->ChangePlayerClass(WITCH); + player=game->GetPlayer(); + std::weak_ptrnullRing{Inventory::AddItem("Null Ring"s)}; + Inventory::EquipItem(nullRing,EquipSlot::RING1); + nullRing.lock()->EnchantItem("Spreading Pain"); + MonsterData testMonsterData{"TestName2","Test Monster2",1,10,5,{MonsterDropData{"Health Potion",100.f,1,1}},200.f}; + MONSTER_DATA["TestName2"]=testMonsterData; + Monster&newMonster5{game->SpawnMonster({30.f,0.f},MONSTER_DATA["TestName2"])}; + Monster&newMonster{game->SpawnMonster({30.f,0.f},MONSTER_DATA["TestName2"])}; + Monster&newMonster2{game->SpawnMonster({30.f,0.f},MONSTER_DATA["TestName2"])}; + Monster&newMonster3{game->SpawnMonster({30.f,0.f},MONSTER_DATA["TestName2"])}; + Monster&newMonster4{game->SpawnMonster({30.f,0.f},MONSTER_DATA["TestName2"])}; + game->OnUserUpdate(0.f); + newMonster5.Hurt(1,newMonster5.OnUpperLevel(),newMonster5.GetZ()); + Assert::AreEqual(size_t(0),newMonster.GetBuffs(BuffType::CURSE_OF_PAIN).size(),L"If a target without Curse of Pain dies, it should not spread onto other targets."); + Assert::AreEqual(size_t(0),newMonster2.GetBuffs(BuffType::CURSE_OF_PAIN).size(),L"If a target without Curse of Pain dies, it should not spread onto other targets."); + Assert::AreEqual(size_t(0),newMonster3.GetBuffs(BuffType::CURSE_OF_PAIN).size(),L"If a target without Curse of Pain dies, it should not spread onto other targets."); + Assert::AreEqual(size_t(0),newMonster4.GetBuffs(BuffType::CURSE_OF_PAIN).size(),L"If a target without Curse of Pain dies, it should not spread onto other targets."); + player->SetTestScreenAimingLocation(game->GetScreenSize()/2+vf2d{30.f,0.f}); + player->PrepareCast(player->GetAbility1()); + player->CastSpell(player->GetAbility1()); + game->OnUserUpdate(0.f); + Assert::AreEqual(size_t(1),newMonster.GetBuffs(BuffType::CURSE_OF_PAIN).size(),L"The first monster should have been targeted with Curse of Pain."); + game->SetElapsedTime(3.f); + game->OnUserUpdate(3.f); + Assert::IsTrue(newMonster.IsDead(),L"The first monster has died to Curse of Pain's tick."); + Assert::IsTrue(!newMonster2.IsDead(),L"The other monsters should not be dead."); + 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),newMonster2.GetBuffs(BuffType::CURSE_OF_PAIN).size(),L"With the enchant, Curse of Pain should have spread to Monster 2."); + newMonster2.Hurt(1,newMonster2.OnUpperLevel(),newMonster2.GetZ()); + Assert::IsTrue(newMonster2.IsDead(),L"Monster 2 should be dead."); + 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(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."); + } }; } diff --git a/Adventures in Lestoria Tests/ItemTests.cpp b/Adventures in Lestoria Tests/ItemTests.cpp index 165f87ef..b82bb3fd 100644 --- a/Adventures in Lestoria Tests/ItemTests.cpp +++ b/Adventures in Lestoria Tests/ItemTests.cpp @@ -152,6 +152,7 @@ namespace ItemTests game->SetLoadoutItem(0,"Flat Recovery Potion"); testKey->bHeld=true; //Simulate key being pressed. player->CheckAndPerformAbility(player->GetItem1(),testKeyboardInput); + game->OnUserUpdate(0.f); //Wait an extra tick for the buff to begin going down. game->SetElapsedTime(0.05f); game->OnUserUpdate(0.05f);//Wait some time as the item applies a buff that heals us. We're also going to gain one mana during this tick. Assert::AreEqual(75,player->GetHealth(),L"Player Health is 75 after using Flat Recovery Potion."); @@ -164,6 +165,7 @@ namespace ItemTests game->SetLoadoutItem(1,"Pct Recovery Potion"); testKey->bHeld=true; //Simulate key being pressed. player->CheckAndPerformAbility(player->GetItem2(),testKeyboardInput); + game->OnUserUpdate(0.f); //Wait an extra tick for the buff to begin going down. game->SetElapsedTime(0.05f); game->OnUserUpdate(0.05f);//Wait some time as the item applies a buff that heals us. Assert::AreEqual(75,player->GetHealth(),L"Player Health is 75 after using Pct Recovery Potion."); @@ -175,6 +177,7 @@ namespace ItemTests game->SetLoadoutItem(2,"Bandages"); testKey->bHeld=true; //Simulate key being pressed. player->CheckAndPerformAbility(player->GetItem3(),testKeyboardInput); + game->OnUserUpdate(0.f); //Wait an extra tick for the buff to begin going down. game->SetElapsedTime(0.05f); game->OnUserUpdate(0.05f);//Wait some time as the item applies a buff that heals us. Assert::AreEqual(30,player->GetHealth(),L"Player is immediately healed for 5 health points on Bandages use."); diff --git a/Adventures in Lestoria/Buff.cpp b/Adventures in Lestoria/Buff.cpp index 4b7395e2..dbacab9f 100644 --- a/Adventures in Lestoria/Buff.cpp +++ b/Adventures in Lestoria/Buff.cpp @@ -112,13 +112,16 @@ Buff::Buff(std::variant>attachedTarget,BuffType t :attachedTarget(attachedTarget),type(type),restorationType(restorationType),duration(duration),intensity(intensity),nextTick(timeBetweenTicks),timeBetweenTicks(timeBetweenTicks),overTimeType(overTimeType),originalDuration(this->duration){} void Buff::Update(AiL*game,float fElapsedTime){ - duration-=fElapsedTime; - lifetime+=fElapsedTime; - if(enabled&&overTimeType.has_value()&&lifetime>=nextTick){ - BuffTick(game,fElapsedTime); - nextTick+=timeBetweenTicks; - if(restorationType==BuffRestorationType::ONE_OFF)enabled=false; + if(!waitOneTick){ + duration-=fElapsedTime; + lifetime+=fElapsedTime; + if(enabled&&overTimeType.has_value()&&lifetime>=nextTick){ + BuffTick(game,fElapsedTime); + nextTick+=timeBetweenTicks; + if(restorationType==BuffRestorationType::ONE_OFF)enabled=false; + } } + waitOneTick=false; } void Buff::BuffTick(AiL*game,float fElapsedTime){ diff --git a/Adventures in Lestoria/Buff.h b/Adventures in Lestoria/Buff.h index c596ed8e..d0a0aeb7 100644 --- a/Adventures in Lestoria/Buff.h +++ b/Adventures in Lestoria/Buff.h @@ -62,6 +62,7 @@ enum BuffType{ LETHAL_TEMPO, BURNING_ARROW_BURN, SWORD_ENCHANTMENT, + CURSE_OF_PAIN, }; enum class BuffRestorationType{ ONE_OFF, //This is used as a hack fix for the RestoreDuringCast Item script since they require us to restore 1 tick immediately. Over time buffs do not apply a tick immediately. @@ -110,6 +111,7 @@ struct Buff{ void Update(AiL*game,float fElapsedTime); private: + bool waitOneTick{true}; bool enabled{true}; //This is only turned off because the ONE_OFF effect. See BuffType::ONE_OFF for more details. std::optionaloverTimeType; void BuffTick(AiL*game,float fElapsedTime); diff --git a/Adventures in Lestoria/Monster.cpp b/Adventures in Lestoria/Monster.cpp index e32617b8..82466695 100644 --- a/Adventures in Lestoria/Monster.cpp +++ b/Adventures in Lestoria/Monster.cpp @@ -53,6 +53,7 @@ All rights reserved. #endif #include "GameSettings.h" #include "ItemEnchant.h" +#include INCLUDE_ANIMATION_DATA INCLUDE_MONSTER_DATA @@ -1064,6 +1065,41 @@ void Monster::OnDeath(){ adrenalineRushBuff.duration+="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){ + //NOTE: If we have to change/modify Curse of Pain, we must also modify it in Witch.cpp (Witch::ability1.action define) + #pragma region Applies Curse of Pain to nearby targets + const float buffTimeBetweenTicks{"Witch.Ability 1.Curse Debuff"_f[1]}; + const float buffDamageMult{"Witch.Ability 1.Curse Debuff"_f[0]}; + const float buffDuration{"Witch.Ability 1.Curse Debuff"_f[2]}; + for(std::shared_ptr&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; + }else{ + float distFromDeadMonster{geom2d::line(GetPos(),m->GetPos()).length()}; + if(distFromDeadMonster<"Spreading Pain"_ENC["SPREAD RANGE"]/100*24){ + m->ApplyDot(buffDuration,game->GetPlayer()->GetAttack()*buffDamageMult,buffTimeBetweenTicks,BuffType::CURSE_OF_PAIN, + [](std::weak_ptrm,Buff&b){ + expireCallbackFunc: + if(!m.expired())m.lock()->Hurt(game->GetPlayer()->GetAttack()*"Witch.Ability 1.Final Tick Damage"_F,m.lock()->OnUpperLevel(),m.lock()->GetZ(),HurtFlag::DOT); + } + ); + m->AddBuff(BuffType::GLOW_PURPLE,buffDuration,1.f); + } + } + DrawLineToTarget: + const vf2d targetPos{m->GetPos()}; + for(int i:std::ranges::iota_view(0,int(util::distance(GetPos(),targetPos)/8))){ + float drawDist{i*8.f}; + float fadeInTime{i*0.05f}; + float fadeOutTime{0.5f+i*0.05f}; + float effectSize{util::random(0.2f)}; + game->AddEffect(std::make_unique(geom2d::line(GetPos(),targetPos).rpoint(drawDist),0.f,"mark_trail.png",OnUpperLevel(),fadeInTime,fadeOutTime,vf2d{effectSize,effectSize},vf2d{},Pixel{100,0,155,uint8_t(util::random_range(0,120))},0.f,0.f),true); + } + SoundEffect::PlaySFX("Curse of Pain",m->GetPos()); + } + #pragma endregion + } SpawnDrops(); @@ -1484,6 +1520,9 @@ const std::weak_ptrMonster::GetWeakPointer()const{ void Monster::ApplyDot(float duration,int damage,float timeBetweenTicks,Buff::MonsterBuffExpireCallbackFunction expireCallbackFunc){ AddBuff(BuffRestorationType::OVER_TIME,BuffOverTimeType::HP_DAMAGE_OVER_TIME,duration,damage,timeBetweenTicks,expireCallbackFunc); } +void Monster::ApplyDot(float duration,int damage,float timeBetweenTicks,BuffType identifierType,Buff::MonsterBuffExpireCallbackFunction expireCallbackFunc){ + AddBuff(identifierType,BuffRestorationType::OVER_TIME,BuffOverTimeType::HP_DAMAGE_OVER_TIME,duration,damage,timeBetweenTicks,expireCallbackFunc); +} void Monster::SetWeakPointer(std::shared_ptr&sharedMonsterPtr){ weakPtr=sharedMonsterPtr; diff --git a/Adventures in Lestoria/Monster.h b/Adventures in Lestoria/Monster.h index 8a4f1678..14144bb0 100644 --- a/Adventures in Lestoria/Monster.h +++ b/Adventures in Lestoria/Monster.h @@ -214,6 +214,7 @@ public: const bool CanMove()const; const std::weak_ptrGetWeakPointer()const; void ApplyDot(float duration,int damage,float timeBetweenTicks,Buff::MonsterBuffExpireCallbackFunction expireCallbackFunc=[](std::weak_ptrm,Buff&b){}); + void ApplyDot(float duration,int damage,float timeBetweenTicks,BuffType identifierType,Buff::MonsterBuffExpireCallbackFunction expireCallbackFunc=[](std::weak_ptrm,Buff&b){}); const float GetDamageAmplificationMult(const bool backstabOccurred)const; Buff&EditBuff(BuffType buff,size_t buffInd); std::vector>EditBuffs(BuffType buff); diff --git a/Adventures in Lestoria/Player.cpp b/Adventures in Lestoria/Player.cpp index 54861a08..571a96b0 100644 --- a/Adventures in Lestoria/Player.cpp +++ b/Adventures in Lestoria/Player.cpp @@ -1766,7 +1766,8 @@ const vf2d Player::GetWorldAimingLocation(bool useWalkDir,bool invert){ } const vf2d Player::GetAimingLocation(bool useWalkDir,bool invert){ - if(UsingAutoAim()){ + if(testAimingLoc)return testAimingLoc.value(); + else if(UsingAutoAim()){ float xAxis=0.f,yAxis=0.f; #pragma region Manual Aiming @@ -2243,4 +2244,7 @@ const bool Player::CatFormActive()const{ void Player::UpdateAnimationStates(){ animation.UpdateState(internal_animState,game->GetElapsedTime()); animation.UpdateState(internal_catAnimState,game->GetElapsedTime()); +} +void Player::SetTestScreenAimingLocation(vf2d forcedAimingLoc){ + testAimingLoc=forcedAimingLoc; } \ No newline at end of file diff --git a/Adventures in Lestoria/Player.h b/Adventures in Lestoria/Player.h index ad6782a7..75bca237 100644 --- a/Adventures in Lestoria/Player.h +++ b/Adventures in Lestoria/Player.h @@ -332,6 +332,9 @@ public: void DeactivateCatForm(); const bool CatFormActive()const; void UpdateAnimationStates(); + void CastSpell(Ability&ability); //NOTE: This usually is not called unless we are running tests! CastSpell is meant to be used when we have chosen a pre-casting target position and have released the associated key to cast the spell. + void PrepareCast(Ability&ability); //NOTE: This usually is not called unless we are running tests! PrepareCast is meant to be used before we use CastSpell in unit tests. + void SetTestScreenAimingLocation(vf2d forcedAimingLoc); private: const int SHIELD_CAPACITY{32}; int hp="Warrior.BaseHealth"_I; @@ -423,6 +426,7 @@ private: void OnBuffAdd(Buff&newBuff); std::vector>shield; bool catForm{false}; + std::optionaltestAimingLoc{}; protected: const float ATTACK_COOLDOWN="Warrior.Auto Attack.Cooldown"_F; const float MAGIC_ATTACK_COOLDOWN="Wizard.Auto Attack.Cooldown"_F; @@ -440,9 +444,7 @@ protected: vf2d vel={0,0}; Key facingDirection=DOWN; float swordSwingTimer=0; - void CastSpell(Ability&ability); Ability*castPrepAbility; - void PrepareCast(Ability&ability); vf2d precastLocation={}; void SetVelocity(vf2d vel); const float RETREAT_DISTANCE=24*"Ranger.Right Click Ability.RetreatDistance"_F/100; diff --git a/Adventures in Lestoria/Version.h b/Adventures in Lestoria/Version.h index cda3a935..780cfe0b 100644 --- a/Adventures in Lestoria/Version.h +++ b/Adventures in Lestoria/Version.h @@ -39,7 +39,7 @@ All rights reserved. #define VERSION_MAJOR 1 #define VERSION_MINOR 2 #define VERSION_PATCH 5 -#define VERSION_BUILD 11132 +#define VERSION_BUILD 11152 #define stringify(a) stringify_(a) #define stringify_(a) #a diff --git a/Adventures in Lestoria/Witch.cpp b/Adventures in Lestoria/Witch.cpp index 0d4e4c15..22100d41 100644 --- a/Adventures in Lestoria/Witch.cpp +++ b/Adventures in Lestoria/Witch.cpp @@ -139,10 +139,11 @@ void Witch::InitializeClassAbilities(){ [](Player*p,vf2d pos={}){ std::optional>curseTarget{Monster::GetNearestMonster(pos,"Witch.Ability 1.Casting Range"_F/100.f*24,p->OnUpperLevel(),p->GetZ())}; if(curseTarget.has_value()&&!curseTarget.value().expired()){ + //NOTE: If we have to change/modify Curse of Pain, we must also modify it in Monster::OnDeath (Monster.cpp) const float buffTimeBetweenTicks{"Witch.Ability 1.Curse Debuff"_f[1]}; const float buffDamageMult{"Witch.Ability 1.Curse Debuff"_f[0]}; const float buffDuration{"Witch.Ability 1.Curse Debuff"_f[2]}; - curseTarget.value().lock()->ApplyDot(buffDuration,p->GetAttack()*buffDamageMult,buffTimeBetweenTicks, + curseTarget.value().lock()->ApplyDot(buffDuration,p->GetAttack()*buffDamageMult,buffTimeBetweenTicks,BuffType::CURSE_OF_PAIN, [](std::weak_ptrm,Buff&b){ expireCallbackFunc: if(!m.expired())m.lock()->Hurt(game->GetPlayer()->GetAttack()*"Witch.Ability 1.Final Tick Damage"_F,m.lock()->OnUpperLevel(),m.lock()->GetZ(),HurtFlag::DOT); diff --git a/Adventures in Lestoria/assets/config/items/ItemEnchants.txt b/Adventures in Lestoria/assets/config/items/ItemEnchants.txt index b4edbff9..029c6825 100644 --- a/Adventures in Lestoria/assets/config/items/ItemEnchants.txt +++ b/Adventures in Lestoria/assets/config/items/ItemEnchants.txt @@ -411,7 +411,7 @@ Item Enchants } Spreading Pain { - Description = "If Curse of Pain kills a target, it can spread to nearby targets." + Description = "If a target with Curse of Pain dies, it can spread to nearby targets." Affects = Ability 1 SPREAD RANGE = 250 diff --git a/x64/Release/Adventures in Lestoria.exe b/x64/Release/Adventures in Lestoria.exe index cd9ca20d..f7f0174b 100644 Binary files a/x64/Release/Adventures in Lestoria.exe and b/x64/Release/Adventures in Lestoria.exe differ