Implemented Spreading Pain enchant. Made applied buffs not immediately start ticking down on the same frame as they are applied since that causes some strange interactions with chained deaths. Release Build 11152.

pull/65/head
sigonasr2 3 months ago
parent dd38b8d58e
commit b0ee888b83
  1. 63
      Adventures in Lestoria Tests/EnchantTests.cpp
  2. 3
      Adventures in Lestoria Tests/ItemTests.cpp
  3. 3
      Adventures in Lestoria/Buff.cpp
  4. 2
      Adventures in Lestoria/Buff.h
  5. 39
      Adventures in Lestoria/Monster.cpp
  6. 1
      Adventures in Lestoria/Monster.h
  7. 6
      Adventures in Lestoria/Player.cpp
  8. 6
      Adventures in Lestoria/Player.h
  9. 2
      Adventures in Lestoria/Version.h
  10. 3
      Adventures in Lestoria/Witch.cpp
  11. 2
      Adventures in Lestoria/assets/config/items/ItemEnchants.txt
  12. BIN
      x64/Release/Adventures in Lestoria.exe

@ -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_ptr<Item>nullRing{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.");
}
};
}

@ -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.");

@ -112,6 +112,7 @@ Buff::Buff(std::variant<Player*,std::weak_ptr<Monster>>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){
if(!waitOneTick){
duration-=fElapsedTime;
lifetime+=fElapsedTime;
if(enabled&&overTimeType.has_value()&&lifetime>=nextTick){
@ -119,6 +120,8 @@ void Buff::Update(AiL*game,float fElapsedTime){
nextTick+=timeBetweenTicks;
if(restorationType==BuffRestorationType::ONE_OFF)enabled=false;
}
}
waitOneTick=false;
}
void Buff::BuffTick(AiL*game,float fElapsedTime){

@ -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::optional<BuffOverTimeType::BuffOverTimeType>overTimeType;
void BuffTick(AiL*game,float fElapsedTime);

@ -53,6 +53,7 @@ All rights reserved.
#endif
#include "GameSettings.h"
#include "ItemEnchant.h"
#include <ranges>
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<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;
}else{
float distFromDeadMonster{geom2d::line<float>(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_ptr<Monster>m,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<Effect>(geom2d::line<float>(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_ptr<Monster>Monster::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<Monster>&sharedMonsterPtr){
weakPtr=sharedMonsterPtr;

@ -214,6 +214,7 @@ public:
const bool CanMove()const;
const std::weak_ptr<Monster>GetWeakPointer()const;
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;
Buff&EditBuff(BuffType buff,size_t buffInd);
std::vector<std::reference_wrapper<Buff>>EditBuffs(BuffType buff);

@ -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
@ -2244,3 +2245,6 @@ void Player::UpdateAnimationStates(){
animation.UpdateState(internal_animState,game->GetElapsedTime());
animation.UpdateState(internal_catAnimState,game->GetElapsedTime());
}
void Player::SetTestScreenAimingLocation(vf2d forcedAimingLoc){
testAimingLoc=forcedAimingLoc;
}

@ -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<std::pair<PlayerTimerType,ShieldAmount>>shield;
bool catForm{false};
std::optional<vf2d>testAimingLoc{};
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;

@ -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

@ -139,10 +139,11 @@ void Witch::InitializeClassAbilities(){
[](Player*p,vf2d pos={}){
std::optional<std::weak_ptr<Monster>>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_ptr<Monster>m,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);

@ -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

Loading…
Cancel
Save