Backstabber enchant implemented. Fix audio transition fades suddenly not working. Release Build 10944.

mac-build
sigonasr2 6 months ago
parent a6b4caad02
commit 9ba98a032e
  1. 23
      Adventures in Lestoria Tests/EnchantTests.cpp
  2. 37
      Adventures in Lestoria/Audio.cpp
  3. 10
      Adventures in Lestoria/DamageNumber.cpp
  4. 1
      Adventures in Lestoria/DamageNumber.h
  5. 42
      Adventures in Lestoria/Monster.cpp
  6. 5
      Adventures in Lestoria/Monster.h
  7. 2
      Adventures in Lestoria/Version.h
  8. BIN
      x64/Release/Adventures in Lestoria.exe

@ -601,5 +601,28 @@ namespace EnchantTests
const float newMultishotCooldownTime{multishotCooldownTime-multishotCooldownTime*"Multi-Multishot"_ENC["COOLDOWN REDUCTION PCT"]/100.f}; 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()); 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_ptr<Item>nullRing{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.");
}
}; };
} }

@ -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. currentTimestamp=Engine().GetCursorMilliseconds(track.GetChannelIDs()[0])/1000.f; //Update to new timestamp now that it's been shifted over.
} }
Self().lastTimestamp=currentTimestamp; Self().lastTimestamp=currentTimestamp;
}else{ }
if(Self().fadeToTargetVolumeTime==0.f&&Self().playBGMWaitTime>0.f){ if(Self().fadeToTargetVolumeTime==0.f&&Self().playBGMWaitTime>0.f){
Self().playBGMWaitTime=std::max(Self().playBGMWaitTime-game->GetElapsedTime(),0.f); Self().playBGMWaitTime=std::max(Self().playBGMWaitTime-game->GetElapsedTime(),0.f);
if(Self().playBGMWaitTime==0.f&&Self().immediatelyLoadAudio){ if(Self().playBGMWaitTime==0.f&&Self().immediatelyLoadAudio){
while(!Self().BGMFullyLoaded()){ 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. 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. //Start playing the tracks and setup a callback to repeat at looped time.
Audio::BGM&track=Self().bgm[Self().GetTrackName()]; Audio::BGM&track=Self().bgm[Self().GetTrackName()];
for(int trackID:track.GetChannelIDs()){ for(int trackID:track.GetChannelIDs()){
Engine().Play(trackID,true); Engine().Play(trackID,true);
}
} }
} }
if(Self().fadeToTargetVolumeTime>0.f){ }
Self().fadeToTargetVolumeTime=std::max(0.f,Self().fadeToTargetVolumeTime-game->GetElapsedTime()); if(Self().fadeToTargetVolumeTime>0.f){
for(int counter=0;float&vol:Self().prevVolumes){ Self().fadeToTargetVolumeTime=std::max(0.f,Self().fadeToTargetVolumeTime-game->GetElapsedTime());
const BGM&currentBgm=Self().bgm[Self().currentBGM]; for(int counter=0;float&vol:Self().prevVolumes){
Engine().SetVolume(currentBgm.GetChannelIDs()[counter],util::lerp(Self().GetCalculatedBGMVolume(vol),Self().GetCalculatedBGMVolume(Self().targetVolumes[counter]),1-(Self().fadeToTargetVolumeTime/currentBgm.GetFadeTime()))); const BGM&currentBgm=Self().bgm[Self().currentBGM];
counter++; Engine().SetVolume(currentBgm.GetChannelIDs()[counter],util::lerp(Self().GetCalculatedBGMVolume(vol),Self().GetCalculatedBGMVolume(Self().targetVolumes[counter]),1-(Self().fadeToTargetVolumeTime/currentBgm.GetFadeTime())));
} counter++;
} }
} }
} }

@ -69,7 +69,7 @@ void DamageNumber::Draw(){
#define NumberScalesWithDamage true #define NumberScalesWithDamage true
#define NormalNumber false #define NormalNumber false
auto DrawDamageNumber=[&](const bool ScaleWithNumber,std::string_view text,std::pair<Pixel,Pixel>colorsEnemy,std::pair<Pixel,Pixel>colorsFriendly,vf2d scaling={1.f,1.f}){ auto DrawDamageNumber=[&](const bool ScaleWithNumber,std::string_view text,std::pair<Pixel,Pixel>colorWhenAppliedToMonster,std::pair<Pixel,Pixel>colorWhenAppliedToPlayer,vf2d scaling={1.f,1.f}){
vf2d textSize=game->GetTextSizeProp(text)*scaling; vf2d textSize=game->GetTextSizeProp(text)*scaling;
if(!friendly){ if(!friendly){
vf2d additionalScaling={1.f,1.f}; 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.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); 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{ }else{
vf2d drawPos=pos-textSize/2.f; vf2d drawPos=pos-textSize/2.f;
drawPos.x=std::clamp(drawPos.x,game->view.GetWorldTL().x,game->view.GetWorldBR().x-textSize.x); 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); 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); std::string text=std::to_string(damage);
DrawDamageNumber(NormalNumber,text,{0xE1BEE7,0x1F083A},{0xDCE775,0x37320A}); DrawDamageNumber(NormalNumber,text,{0xE1BEE7,0x1F083A},{0xDCE775,0x37320A});
}break; }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))); default:ERR(std::format("Damage Number Type {} is not implemented! THIS SHOULD NOT BE HAPPENING!",int(type)));
} }
} }

@ -46,6 +46,7 @@ namespace DamageNumberType{
INTERRUPT, INTERRUPT,
CRIT, CRIT,
DOT, DOT,
BACKSTAB,
}; };
} }

@ -69,7 +69,7 @@ safemap<std::string,std::function<void(Monster&,float,std::string)>>STRATEGY_DAT
std::unordered_map<std::string,Renderable*>MonsterData::imgs; std::unordered_map<std::string,Renderable*>MonsterData::imgs;
Monster::Monster(vf2d pos,MonsterData data,bool upperLevel,bool bossMob): 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()){ for(const std::string&anim:data.GetAnimations()){
animation.AddState(anim,ANIMATION_DATA[std::format("{}_{}",name,anim)]); 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); 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); 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<DamageNumber>(pos,int(mod_dmg)); damageNumberPtr=std::make_shared<DamageNumber>(pos,int(mod_dmg));
DAMAGENUMBER_LIST.push_back(damageNumberPtr); DAMAGENUMBER_LIST.push_back(damageNumberPtr);
} }
#pragma region Change Label to Crit #pragma region Change Label based on bonus conditions (Crit/Backstab)
if(crit){ if(crit){
damageNumberPtr.get()->type=DamageNumberType::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 #pragma endregion
lastHitTimer=0.05f; lastHitTimer=0.05f;
} }
@ -1378,11 +1385,38 @@ void Monster::SetWeakPointer(std::shared_ptr<Monster>&sharedMonsterPtr){
weakPtr=sharedMonsterPtr; weakPtr=sharedMonsterPtr;
} }
const float Monster::GetDamageAmplificationMult()const{ const float Monster::GetDamageAmplificationMult(const bool backstabOccurred)const{
float damageAmpMult{1.f}; float damageAmpMult{1.f};
const std::vector<Buff>&buffList{GetBuffs(BuffType::DAMAGE_AMPLIFICATION)}; const std::vector<Buff>&buffList{GetBuffs(BuffType::DAMAGE_AMPLIFICATION)};
for(const Buff&buff:buffList){ for(const Buff&buff:buffList){
damageAmpMult+=buff.intensity; damageAmpMult+=buff.intensity;
} }
if(backstabOccurred)damageAmpMult+="Backstabber"_ENC["BACKSTAB BONUS DMG"]/100.f;
return damageAmpMult; 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().y<GetPos().y;
}break;
case EAST:{
return game->GetPlayer()->GetPos().x<GetPos().x;
}break;
case WEST:{
return game->GetPlayer()->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;
}

@ -210,9 +210,10 @@ public:
const bool CanMove()const; const bool CanMove()const;
const std::weak_ptr<Monster>GetWeakPointer()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,Buff::MonsterBuffExpireCallbackFunction expireCallbackFunc=[](std::weak_ptr<Monster>m,Buff&b){});
const float GetDamageAmplificationMult()const; const float GetDamageAmplificationMult(const bool backstabOccurred)const;
Buff&EditBuff(BuffType buff,size_t buffInd); Buff&EditBuff(BuffType buff,size_t buffInd);
std::vector<std::reference_wrapper<Buff>>EditBuffs(BuffType buff); std::vector<std::reference_wrapper<Buff>>EditBuffs(BuffType buff);
const bool IsBackstabAttack()const;
private: private:
//NOTE: Marking a monster for deletion does not trigger any death events. It just simply removes the monster from the field!! //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. // 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 queueShotTimer=0;
float z=0; float z=0;
float iframe_timer=0; float iframe_timer=0;
Direction facingDirection=Direction::WEST; Direction facingDirection;
std::string strategy; std::string strategy;
State::State state=State::NORMAL; State::State state=State::NORMAL;
std::string overlaySprite=""; std::string overlaySprite="";

@ -39,7 +39,7 @@ All rights reserved.
#define VERSION_MAJOR 1 #define VERSION_MAJOR 1
#define VERSION_MINOR 2 #define VERSION_MINOR 2
#define VERSION_PATCH 3 #define VERSION_PATCH 3
#define VERSION_BUILD 10936 #define VERSION_BUILD 10944
#define stringify(a) stringify_(a) #define stringify(a) stringify_(a)
#define stringify_(a) #a #define stringify_(a) #a

Loading…
Cancel
Save