diff --git a/Adventures in Lestoria Tests/EnchantTests.cpp b/Adventures in Lestoria Tests/EnchantTests.cpp index 92d43ab2..de788711 100644 --- a/Adventures in Lestoria Tests/EnchantTests.cpp +++ b/Adventures in Lestoria Tests/EnchantTests.cpp @@ -601,5 +601,28 @@ namespace EnchantTests const float newMultishotCooldownTime{multishotCooldownTime-multishotCooldownTime*"Multi-Multishot"_ENC["COOLDOWN REDUCTION PCT"]/100.f}; Assert::AreEqual(newMultishotCooldownTime,player->GetAbility3().GetCooldownTime(),util::wformat("Player starts with {} seconds of cooldown on Multishot.",newMultishotCooldownTime).c_str()); } + TEST_METHOD(BackstabberCheck){ + MonsterData testMonsterData{"TestName","Test Monster",30,10,5,{MonsterDropData{"Health Potion",100.f,1,1}},200.f}; + MONSTER_DATA["TestName"]=testMonsterData; + Monster testMonster{{},MONSTER_DATA["TestName"]}; + testMonster.Hurt(10,testMonster.OnUpperLevel(),testMonster.GetZ()); + Assert::AreEqual(20,testMonster.GetHealth(),L"Monster should have taken 10 hitpoints of damage."); + testMonster.Heal(10); + game->GetPlayer()->ForceSetPos({4,0}); + testMonster.Hurt(10,testMonster.OnUpperLevel(),testMonster.GetZ(),HurtFlag::PLAYER_ABILITY); + Assert::AreEqual(20,testMonster.GetHealth(),L"Monster should have taken 10 hitpoints of damage even if the attack was a valid backstab (no enchantment)."); + std::weak_ptrnullRing{Inventory::AddItem("Null Ring"s)}; + Inventory::EquipItem(nullRing,EquipSlot::RING1); + nullRing.lock()->EnchantItem("Backstabber"); + game->GetPlayer()->ForceSetPos({0,0}); + testMonster.Hurt(10,testMonster.OnUpperLevel(),testMonster.GetZ()); + Assert::AreEqual(10,testMonster.GetHealth(),L"Monster should have taken 10 hitpoints of damage still since it wasn't flagged as a player ability."); + testMonster.Heal(30); + testMonster.Hurt(10,testMonster.OnUpperLevel(),testMonster.GetZ(),HurtFlag::PLAYER_ABILITY); + Assert::AreEqual(20,testMonster.GetHealth(),L"Monster should have taken 10 hitpoints of damage with the backstab bonus because the player is not behind the target."); + game->GetPlayer()->ForceSetPos({4,0}); + testMonster.Hurt(10,testMonster.OnUpperLevel(),testMonster.GetZ(),HurtFlag::PLAYER_ABILITY); + Assert::AreEqual(6,testMonster.GetHealth(),L"Monster should have taken 14 hitpoints of damage with the backstab bonus successfully applied."); + } }; } \ No newline at end of file diff --git a/Adventures in Lestoria/Audio.cpp b/Adventures in Lestoria/Audio.cpp index 7084a4be..46d9005e 100644 --- a/Adventures in Lestoria/Audio.cpp +++ b/Adventures in Lestoria/Audio.cpp @@ -337,28 +337,27 @@ void Audio::Update(){ currentTimestamp=Engine().GetCursorMilliseconds(track.GetChannelIDs()[0])/1000.f; //Update to new timestamp now that it's been shifted over. } Self().lastTimestamp=currentTimestamp; - }else{ - if(Self().fadeToTargetVolumeTime==0.f&&Self().playBGMWaitTime>0.f){ - Self().playBGMWaitTime=std::max(Self().playBGMWaitTime-game->GetElapsedTime(),0.f); - if(Self().playBGMWaitTime==0.f&&Self().immediatelyLoadAudio){ - while(!Self().BGMFullyLoaded()){ - UpdateLoop(); //We immediately load the file. In a loading screen setting we would defer UpdateLoop() such that we have extra time to update the screen, UpdateLoop() is divided into many parts of the music loading process. - } + } + if(Self().fadeToTargetVolumeTime==0.f&&Self().playBGMWaitTime>0.f){ + Self().playBGMWaitTime=std::max(Self().playBGMWaitTime-game->GetElapsedTime(),0.f); + if(Self().playBGMWaitTime==0.f&&Self().immediatelyLoadAudio){ + while(!Self().BGMFullyLoaded()){ + UpdateLoop(); //We immediately load the file. In a loading screen setting we would defer UpdateLoop() such that we have extra time to update the screen, UpdateLoop() is divided into many parts of the music loading process. + } - //Start playing the tracks and setup a callback to repeat at looped time. - Audio::BGM&track=Self().bgm[Self().GetTrackName()]; - for(int trackID:track.GetChannelIDs()){ - Engine().Play(trackID,true); - } + //Start playing the tracks and setup a callback to repeat at looped time. + Audio::BGM&track=Self().bgm[Self().GetTrackName()]; + for(int trackID:track.GetChannelIDs()){ + Engine().Play(trackID,true); } } - if(Self().fadeToTargetVolumeTime>0.f){ - Self().fadeToTargetVolumeTime=std::max(0.f,Self().fadeToTargetVolumeTime-game->GetElapsedTime()); - for(int counter=0;float&vol:Self().prevVolumes){ - const BGM¤tBgm=Self().bgm[Self().currentBGM]; - Engine().SetVolume(currentBgm.GetChannelIDs()[counter],util::lerp(Self().GetCalculatedBGMVolume(vol),Self().GetCalculatedBGMVolume(Self().targetVolumes[counter]),1-(Self().fadeToTargetVolumeTime/currentBgm.GetFadeTime()))); - counter++; - } + } + if(Self().fadeToTargetVolumeTime>0.f){ + Self().fadeToTargetVolumeTime=std::max(0.f,Self().fadeToTargetVolumeTime-game->GetElapsedTime()); + for(int counter=0;float&vol:Self().prevVolumes){ + const BGM¤tBgm=Self().bgm[Self().currentBGM]; + Engine().SetVolume(currentBgm.GetChannelIDs()[counter],util::lerp(Self().GetCalculatedBGMVolume(vol),Self().GetCalculatedBGMVolume(Self().targetVolumes[counter]),1-(Self().fadeToTargetVolumeTime/currentBgm.GetFadeTime()))); + counter++; } } } diff --git a/Adventures in Lestoria/DamageNumber.cpp b/Adventures in Lestoria/DamageNumber.cpp index 0eb25b61..e6a38ed7 100644 --- a/Adventures in Lestoria/DamageNumber.cpp +++ b/Adventures in Lestoria/DamageNumber.cpp @@ -69,7 +69,7 @@ void DamageNumber::Draw(){ #define NumberScalesWithDamage true #define NormalNumber false - auto DrawDamageNumber=[&](const bool ScaleWithNumber,std::string_view text,std::paircolorsEnemy,std::paircolorsFriendly,vf2d scaling={1.f,1.f}){ + auto DrawDamageNumber=[&](const bool ScaleWithNumber,std::string_view text,std::paircolorWhenAppliedToMonster,std::paircolorWhenAppliedToPlayer,vf2d scaling={1.f,1.f}){ vf2d textSize=game->GetTextSizeProp(text)*scaling; if(!friendly){ vf2d additionalScaling={1.f,1.f}; @@ -81,13 +81,13 @@ void DamageNumber::Draw(){ drawPos.x=std::clamp(drawPos.x,game->view.GetWorldTL().x,game->view.GetWorldBR().x-textSize.x); drawPos.y=std::clamp(drawPos.y,game->view.GetWorldTL().y,game->view.GetWorldBR().y-textSize.y); - game->view.DrawShadowStringPropDecal(drawPos,text,colorsEnemy.first,colorsEnemy.second,scaling*additionalScaling); + game->view.DrawShadowStringPropDecal(drawPos,text,colorWhenAppliedToMonster.first,colorWhenAppliedToMonster.second,scaling*additionalScaling); }else{ vf2d drawPos=pos-textSize/2.f; drawPos.x=std::clamp(drawPos.x,game->view.GetWorldTL().x,game->view.GetWorldBR().x-textSize.x); drawPos.y=std::clamp(drawPos.y,game->view.GetWorldTL().y,game->view.GetWorldBR().y-textSize.y); - game->view.DrawShadowStringPropDecal(drawPos,text,colorsFriendly.first,colorsFriendly.second,scaling); + game->view.DrawShadowStringPropDecal(drawPos,text,colorWhenAppliedToPlayer.first,colorWhenAppliedToPlayer.second,scaling); } }; @@ -116,6 +116,10 @@ void DamageNumber::Draw(){ std::string text=std::to_string(damage); DrawDamageNumber(NormalNumber,text,{0xE1BEE7,0x1F083A},{0xDCE775,0x37320A}); }break; + case BACKSTAB:{ + std::string text=std::to_string(damage); + DrawDamageNumber(NumberScalesWithDamage,text,{0x888093,0x150035},{BLACK,{0,0,0,0}}); + }break; default:ERR(std::format("Damage Number Type {} is not implemented! THIS SHOULD NOT BE HAPPENING!",int(type))); } } \ No newline at end of file diff --git a/Adventures in Lestoria/DamageNumber.h b/Adventures in Lestoria/DamageNumber.h index 8ddda37d..d546a49e 100644 --- a/Adventures in Lestoria/DamageNumber.h +++ b/Adventures in Lestoria/DamageNumber.h @@ -46,6 +46,7 @@ namespace DamageNumberType{ INTERRUPT, CRIT, DOT, + BACKSTAB, }; } diff --git a/Adventures in Lestoria/Monster.cpp b/Adventures in Lestoria/Monster.cpp index 59b97e09..b7bec99a 100644 --- a/Adventures in Lestoria/Monster.cpp +++ b/Adventures in Lestoria/Monster.cpp @@ -69,7 +69,7 @@ safemap>STRATEGY_DAT std::unordered_mapMonsterData::imgs; Monster::Monster(vf2d pos,MonsterData data,bool upperLevel,bool bossMob): - pos(pos),spawnPos(pos),hp(data.GetHealth()),size(data.GetSizeMult()),targetSize(data.GetSizeMult()),strategy(data.GetAIStrategy()),name(data.GetInternalName()),upperLevel(upperLevel),isBoss(bossMob),facingDirection(Direction::SOUTH),lifetime(GetTotalLifetime()){ + pos(pos),spawnPos(pos),hp(data.GetHealth()),size(data.GetSizeMult()),targetSize(data.GetSizeMult()),strategy(data.GetAIStrategy()),name(data.GetInternalName()),upperLevel(upperLevel),isBoss(bossMob),facingDirection(Direction::WEST),lifetime(GetTotalLifetime()){ for(const std::string&anim:data.GetAnimations()){ animation.AddState(anim,ANIMATION_DATA[std::format("{}_{}",name,anim)]); } @@ -702,7 +702,9 @@ bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag da if(game->GetPlayer()->HasEnchant("Quickdraw")&&util::random(1.f)<="Quickdraw"_ENC["RESET CHANCE"]/100.f)game->GetPlayer()->ReduceAutoAttackTimer(INFINITE); } - mod_dmg*=GetDamageAmplificationMult(); + const bool backstabOccurred{game->GetPlayer()->HasEnchant("Backstabber")&&IsBackstabAttack()}; + + mod_dmg*=GetDamageAmplificationMult(backstabOccurred); mod_dmg=std::ceil(mod_dmg); @@ -727,10 +729,15 @@ bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag da damageNumberPtr=std::make_shared(pos,int(mod_dmg)); DAMAGENUMBER_LIST.push_back(damageNumberPtr); } - #pragma region Change Label to Crit + #pragma region Change Label based on bonus conditions (Crit/Backstab) if(crit){ damageNumberPtr.get()->type=DamageNumberType::CRIT; } + if(damageNumberPtr.get()->type==DamageNumberType::CRIT)goto doneChangingDamageNumberColors; //Crit number priority. Other colors should not apply if the crit has applied. + if(backstabOccurred){ + damageNumberPtr.get()->type=DamageNumberType::BACKSTAB; + } + doneChangingDamageNumberColors: #pragma endregion lastHitTimer=0.05f; } @@ -1378,11 +1385,38 @@ void Monster::SetWeakPointer(std::shared_ptr&sharedMonsterPtr){ weakPtr=sharedMonsterPtr; } -const float Monster::GetDamageAmplificationMult()const{ +const float Monster::GetDamageAmplificationMult(const bool backstabOccurred)const{ float damageAmpMult{1.f}; const std::vector&buffList{GetBuffs(BuffType::DAMAGE_AMPLIFICATION)}; for(const Buff&buff:buffList){ damageAmpMult+=buff.intensity; } + if(backstabOccurred)damageAmpMult+="Backstabber"_ENC["BACKSTAB BONUS DMG"]/100.f; return damageAmpMult; +} + +const bool Monster::IsBackstabAttack()const{ + using enum Direction; + switch(GetFacingDirection()){ + case NORTH:{ + if(!HasFourWaySprites())ERR(std::format("WARNING! Facing direction of a one-way facing monster was detected to be facing NORTH (Monster {} at Pos {}). THIS SHOULD NOT BE HAPPENING!",GetDisplayName(),GetPos().str())) + return game->GetPlayer()->GetPos().y>GetPos().y; + }break; + case SOUTH:{ + if(!HasFourWaySprites())ERR(std::format("WARNING! Facing direction of a one-way facing monster was detected to be facing SOUTH (Monster {} at Pos {}). THIS SHOULD NOT BE HAPPENING!",GetDisplayName(),GetPos().str())) + return game->GetPlayer()->GetPos().yGetPlayer()->GetPos().xGetPlayer()->GetPos().x>GetPos().x; + }break; + default:{ + ERR(std::format("WARNING! Facing direction of a monster was detected to be facing {} (Monster {} at Pos {}). This is not a normal facing direction and THIS SHOULD NOT BE HAPPENING!",int(GetFacingDirection()),GetDisplayName(),GetPos().str())); + return false; + } + } + ERR("WARNING! An unhandled case was detected while trying to determine if an attack was a backstab attack! THIS SHOULD NOT BE HAPPENING!"); + return false; } \ No newline at end of file diff --git a/Adventures in Lestoria/Monster.h b/Adventures in Lestoria/Monster.h index ce27e158..4fecbb79 100644 --- a/Adventures in Lestoria/Monster.h +++ b/Adventures in Lestoria/Monster.h @@ -210,9 +210,10 @@ 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){}); - const float GetDamageAmplificationMult()const; + const float GetDamageAmplificationMult(const bool backstabOccurred)const; Buff&EditBuff(BuffType buff,size_t buffInd); std::vector>EditBuffs(BuffType buff); + const bool IsBackstabAttack()const; private: //NOTE: Marking a monster for deletion does not trigger any death events. It just simply removes the monster from the field!! // The way this works is that monsters marked for deletion will cause the monster update loop to detect there's at least one or more monsters that must be deleted and will call erase_if on the list at the end of the iteration loop. @@ -231,7 +232,7 @@ private: float queueShotTimer=0; float z=0; float iframe_timer=0; - Direction facingDirection=Direction::WEST; + Direction facingDirection; std::string strategy; State::State state=State::NORMAL; std::string overlaySprite=""; diff --git a/Adventures in Lestoria/Version.h b/Adventures in Lestoria/Version.h index af96362a..40769f13 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 3 -#define VERSION_BUILD 10936 +#define VERSION_BUILD 10944 #define stringify(a) stringify_(a) #define stringify_(a) #a diff --git a/x64/Release/Adventures in Lestoria.exe b/x64/Release/Adventures in Lestoria.exe index a41a8f91..21b0fcb0 100644 Binary files a/x64/Release/Adventures in Lestoria.exe and b/x64/Release/Adventures in Lestoria.exe differ