Adjust collision radius default based on monster's sheet frame size. Use defined collision radius of a monster instead of 12*SizeMult as that is the actual radius for HurtMonsterType() damage calls (fixes large stone hitting pillars a little too wide in the chapter 2 boss fight). Refactored Bullet check systems to include damage flags: DOT, PLAYER_ABILITY, and NONE. Player abilities flags assigned to all auto attack and abilities that players can launch, in preparation for marked target proccing. Mark Target buff detection added, Mark buff added. Reorganized bullet hierarchy, turning the default bullet into an interface and making the normal Bullet class a base child class all Bullets derive from. Added HurtDamageInfo structure, which is passed onto bullets to modify flags before being applied to the Hurt function when bullets hit targets. Changed all storage containers holding Bullet classes to now hold IBullet classes. Release Build 10248.

removeExposedPackKey
sigonasr2 4 months ago
parent 2a50695f51
commit a9b59b5eba
  1. 16
      Adventures in Lestoria/Adventures in Lestoria.vcxproj
  2. 13
      Adventures in Lestoria/Adventures in Lestoria.vcxproj.filters
  3. 167
      Adventures in Lestoria/AdventuresInLestoria.cpp
  4. 14
      Adventures in Lestoria/AdventuresInLestoria.h
  5. 4
      Adventures in Lestoria/Arrow.cpp
  6. 2
      Adventures in Lestoria/Bomb.cpp
  7. 1
      Adventures in Lestoria/Buff.h
  8. 239
      Adventures in Lestoria/Bullet.cpp
  9. 93
      Adventures in Lestoria/Bullet.h
  10. 18
      Adventures in Lestoria/BulletTypes.h
  11. 4
      Adventures in Lestoria/ChargedArrow.cpp
  12. 2
      Adventures in Lestoria/DEFINES.h
  13. 2
      Adventures in Lestoria/DaggerSlash.cpp
  14. 2
      Adventures in Lestoria/DaggerStab.cpp
  15. 59
      Adventures in Lestoria/DamageNumber.cpp
  16. 12
      Adventures in Lestoria/DamageNumber.h
  17. 7
      Adventures in Lestoria/DeadlyDash.cpp
  18. 2
      Adventures in Lestoria/Debris.cpp
  19. 4
      Adventures in Lestoria/EnergyBolt.cpp
  20. 2
      Adventures in Lestoria/FallingStone.cpp
  21. 3
      Adventures in Lestoria/Feather.cpp
  22. 8
      Adventures in Lestoria/FireBolt.cpp
  23. 4
      Adventures in Lestoria/FrogTongue.cpp
  24. 59
      Adventures in Lestoria/HurtDamageInfo.h
  25. 282
      Adventures in Lestoria/IBullet.cpp
  26. 133
      Adventures in Lestoria/IBullet.h
  27. 2
      Adventures in Lestoria/LargeStone.cpp
  28. 2
      Adventures in Lestoria/LargeTornado.cpp
  29. 2
      Adventures in Lestoria/LevitatingRock.cpp
  30. 6
      Adventures in Lestoria/LightningBolt.cpp
  31. 2
      Adventures in Lestoria/Meteor.cpp
  32. 59
      Adventures in Lestoria/Monster.cpp
  33. 18
      Adventures in Lestoria/Monster.h
  34. 2
      Adventures in Lestoria/MonsterData.cpp
  35. 10
      Adventures in Lestoria/Pixel.cpp
  36. 4
      Adventures in Lestoria/Pixel.h
  37. 44
      Adventures in Lestoria/Player.cpp
  38. 7
      Adventures in Lestoria/Player.h
  39. 2
      Adventures in Lestoria/SwordSlash.cpp
  40. 2
      Adventures in Lestoria/Thief.cpp
  41. 2
      Adventures in Lestoria/Tornado.cpp
  42. 30
      Adventures in Lestoria/Trapper.cpp
  43. 2
      Adventures in Lestoria/Ursule.cpp
  44. 2
      Adventures in Lestoria/Version.h
  45. 2
      Adventures in Lestoria/Wisp.cpp
  46. 12
      Adventures in Lestoria/Zephy.cpp
  47. 4
      Adventures in Lestoria/assets/config/classes/Trapper.txt
  48. BIN
      x64/Release/Adventures in Lestoria.exe

@ -381,7 +381,15 @@
</SubType>
</ClInclude>
<ClInclude Include="Buff.h" />
<ClInclude Include="Bullet.h" />
<ClInclude Include="Bullet.h">
<SubType>
</SubType>
</ClInclude>
<ClInclude Include="HurtDamageInfo.h">
<SubType>
</SubType>
</ClInclude>
<ClInclude Include="IBullet.h" />
<ClInclude Include="BulletTypes.h" />
<ClInclude Include="CharacterAbilityPreviewComponent.h" />
<ClInclude Include="CharacterRotatingDisplay.h" />
@ -740,7 +748,11 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="Bullet.cpp" />
<ClCompile Include="Bullet.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="IBullet.cpp" />
<ClCompile Include="BuyItemWindow.cpp">
<SubType>
</SubType>

@ -138,7 +138,7 @@
<ClInclude Include="DEFINES.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Bullet.h">
<ClInclude Include="IBullet.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Class.h">
@ -663,6 +663,12 @@
<ClInclude Include="TEST_DEFINES.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Bullet.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HurtDamageInfo.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Player.cpp">
@ -686,7 +692,7 @@
<ClCompile Include="Effect.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Bullet.cpp">
<ClCompile Include="IBullet.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Ability.cpp">
@ -1130,6 +1136,9 @@
<ClCompile Include="DeadlyDash.cpp">
<Filter>Source Files\Bullet Types</Filter>
</ClCompile>
<ClCompile Include="Bullet.cpp">
<Filter>Source Files\Bullet Types</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="cpp.hint" />

@ -97,7 +97,7 @@ safemap<std::string,Animate2D::FrameSequence>ANIMATION_DATA;
std::vector<std::unique_ptr<Monster>>MONSTER_LIST;
std::unordered_map<MonsterSpawnerID,MonsterSpawner>SPAWNER_LIST;
std::vector<std::shared_ptr<DamageNumber>>DAMAGENUMBER_LIST;
std::vector<std::unique_ptr<Bullet>>BULLET_LIST;
std::vector<std::unique_ptr<IBullet>>BULLET_LIST;
std::optional<std::queue<MonsterSpawnerID>>SPAWNER_CONTROLLER;
safemap<std::string,Renderable>GFX;
utils::datafile DATA;
@ -722,13 +722,13 @@ void AiL::UpdateEffects(float fElapsedTime){
}
void AiL::UpdateBullets(float fElapsedTime){
for(auto it=BULLET_LIST.begin();it!=BULLET_LIST.end();++it){
Bullet*b=(*it).get();
IBullet*b=(*it).get();
b->_Update(fElapsedTime);
}
std::erase_if(BULLET_LIST,[](std::unique_ptr<Bullet>&b){return b->IsDead();});
std::erase_if(BULLET_LIST,[](std::unique_ptr<IBullet>&b){return b->IsDead();});
}
const HurtList AiL::Hurt(vf2d pos,float radius,int damage,bool upperLevel,float z,const HurtType hurtTargets)const{
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;
const bool CheckForPlayerCollisions=hurtTargets&HurtType::PLAYER;
@ -737,7 +737,7 @@ const HurtList AiL::Hurt(vf2d pos,float radius,int damage,bool upperLevel,float
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
const bool InRange=geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(m->GetPos(),12*m->GetSizeMult()));
if(InRange){
HurtReturnValue returnVal=m->Hurt(damage,upperLevel,z);
HurtReturnValue returnVal=m->Hurt(damage,upperLevel,z,hurtFlags);
hitList.push_back({&*m,returnVal});
}
}
@ -745,21 +745,21 @@ const HurtList AiL::Hurt(vf2d pos,float radius,int damage,bool upperLevel,float
if(CheckForPlayerCollisions){
const bool InRange=geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(GetPlayer()->GetPos(),12*GetPlayer()->GetSizeMult()));
if(InRange){
HurtReturnValue returnVal=GetPlayer()->Hurt(damage,upperLevel,z);
HurtReturnValue returnVal=GetPlayer()->Hurt(damage,upperLevel,z,hurtFlags);
hitList.push_back({GetPlayer(),returnVal});
}
}
return hitList;
}
const HurtList AiL::HurtMonsterType(vf2d pos,float radius,int damage,bool upperLevel,float z,const std::string_view monsterName)const{
const HurtList AiL::HurtMonsterType(vf2d pos,float radius,int damage,bool upperLevel,float z,const std::string_view monsterName,HurtFlag::HurtFlag hurtFlags)const{
if(!MONSTER_DATA.count(std::string(monsterName)))ERR(std::format("WARNING! Cannot check for monster type name {}! Does not exist!",monsterName));
HurtList hitList;
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
const bool InRange=geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(m->GetPos(),12*m->GetSizeMult()));
const bool InRange=geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(m->GetPos(),m->GetCollisionRadius()));
if(m->GetName()==monsterName&&InRange){
HurtReturnValue returnVal=m->Hurt(damage,upperLevel,z);
HurtReturnValue returnVal=m->Hurt(damage,upperLevel,z,hurtFlags);
hitList.push_back({&*m,returnVal});
}
}
@ -767,40 +767,39 @@ const HurtList AiL::HurtMonsterType(vf2d pos,float radius,int damage,bool upperL
return hitList;
}
void AiL::ProximityKnockback(const vf2d pos,const float radius,const float knockbackAmt,const HurtType knockbackTargets)const{
const bool CheckForMonsterCollisions=knockbackTargets&HurtType::MONSTER;
const bool CheckForPlayerCollisions=knockbackTargets&HurtType::PLAYER;
const HurtList AiL::HurtConeNotHit(vf2d pos,float radius,float angle,float sweepAngle,int damage,HitList&hitList,bool upperLevel,float z,const HurtType hurtTargets,HurtFlag::HurtFlag hurtFlags)const{
HurtList affectedList;
const bool CheckForMonsterCollisions=hurtTargets&HurtType::MONSTER;
const bool CheckForPlayerCollisions=hurtTargets&HurtType::PLAYER;
if(CheckForMonsterCollisions){
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
if(geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(m->GetPos(),12*m->GetSizeMult()))){
m->ProximityKnockback(pos,knockbackAmt);
if(!hitList.count(&*m)&&geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(m->GetPos(),12*m->GetSizeMult()))){
float angleToMonster=geom2d::line<float>{pos,m->GetPos()}.vector().polar().y;
float angleDiff=util::angle_difference(angleToMonster,angle);
if(abs(angleDiff)<=sweepAngle){
HurtReturnValue returnVal=m->Hurt(damage,upperLevel,z,hurtFlags);
affectedList.push_back({&*m,returnVal});
hitList.insert(&*m);
}
}
}
}
if(CheckForPlayerCollisions){
if(geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(GetPlayer()->GetPos(),12*GetPlayer()->GetSizeMult()))){
GetPlayer()->ProximityKnockback(pos,knockbackAmt);
if(!hitList.count(GetPlayer())&&geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(GetPlayer()->GetPos(),12*GetPlayer()->GetSizeMult()))){
float angleToPlayer=geom2d::line<float>{pos,GetPlayer()->GetPos()}.vector().polar().y;
float angleDiff=util::angle_difference(angleToPlayer,angle);
if(abs(angleDiff)<=sweepAngle){
HurtReturnValue returnVal=GetPlayer()->Hurt(damage,upperLevel,z,hurtFlags);
affectedList.push_back({GetPlayer(),returnVal});
hitList.insert(GetPlayer());
}
}
}
void AiL::ProximityKnockback(const vf2d pos,const float radius,const float knockbackAmt,const HurtList&knockbackTargets,const KnockbackCondition condition)const{
using enum KnockbackCondition;
for(auto&[target,wasHurt]:knockbackTargets){
if(condition==KNOCKBACK_HURT_TARGETS&&!wasHurt||
condition==KNOCKBACK_UNHURT_TARGETS&&wasHurt)continue;
if(std::holds_alternative<Player*>(target)){
Player*player{std::get<Player*>(target)};
player->ProximityKnockback(pos,knockbackAmt);
}else
if(std::holds_alternative<Monster*>(target)){
Monster*monster{std::get<Monster*>(target)};
monster->ProximityKnockback(pos,knockbackAmt);
}else ERR("WARNING! Target list was holding an unknown type??? THIS SHOULD NOT BE HAPPENING!")
}
return affectedList;
}
const HurtList AiL::HurtNotHit(vf2d pos,float radius,int damage,HitList&hitList,bool upperLevel,float z,const HurtType hurtTargets)const{
const HurtList AiL::HurtNotHit(vf2d pos,float radius,int damage,HitList&hitList,bool upperLevel,float z,const HurtType hurtTargets,HurtFlag::HurtFlag hurtFlags)const{
HurtList affectedList;
const bool CheckForMonsterCollisions=hurtTargets&HurtType::MONSTER;
const bool CheckForPlayerCollisions=hurtTargets&HurtType::PLAYER;
@ -808,7 +807,7 @@ const HurtList AiL::HurtNotHit(vf2d pos,float radius,int damage,HitList&hitList,
if(CheckForMonsterCollisions){
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
if(!hitList.count(&*m)&&geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(m->GetPos(),12*m->GetSizeMult()))){
HurtReturnValue returnVal=m->Hurt(damage,upperLevel,z);
HurtReturnValue returnVal=m->Hurt(damage,upperLevel,z,hurtFlags);
affectedList.push_back({&*m,returnVal});
hitList.insert(&*m);
}
@ -816,7 +815,7 @@ const HurtList AiL::HurtNotHit(vf2d pos,float radius,int damage,HitList&hitList,
}
if(CheckForPlayerCollisions){
if(!hitList.count(GetPlayer())&&geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(GetPlayer()->GetPos(),12*GetPlayer()->GetSizeMult()))){
HurtReturnValue returnVal=GetPlayer()->Hurt(damage,upperLevel,z);
HurtReturnValue returnVal=GetPlayer()->Hurt(damage,upperLevel,z,hurtFlags);
affectedList.push_back({GetPlayer(),returnVal});
hitList.insert(GetPlayer());
}
@ -824,36 +823,37 @@ const HurtList AiL::HurtNotHit(vf2d pos,float radius,int damage,HitList&hitList,
return affectedList;
}
const HurtList AiL::HurtConeNotHit(vf2d pos,float radius,float angle,float sweepAngle,int damage,HitList&hitList,bool upperLevel,float z,const HurtType hurtTargets)const{
HurtList affectedList;
const bool CheckForMonsterCollisions=hurtTargets&HurtType::MONSTER;
const bool CheckForPlayerCollisions=hurtTargets&HurtType::PLAYER;
void AiL::ProximityKnockback(const vf2d pos,const float radius,const float knockbackAmt,const HurtType knockbackTargets)const{
const bool CheckForMonsterCollisions=knockbackTargets&HurtType::MONSTER;
const bool CheckForPlayerCollisions=knockbackTargets&HurtType::PLAYER;
if(CheckForMonsterCollisions){
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
if(!hitList.count(&*m)&&geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(m->GetPos(),12*m->GetSizeMult()))){
float angleToMonster=geom2d::line<float>{pos,m->GetPos()}.vector().polar().y;
float angleDiff=util::angle_difference(angleToMonster,angle);
if(abs(angleDiff)<=sweepAngle){
HurtReturnValue returnVal=m->Hurt(damage,upperLevel,z);
affectedList.push_back({&*m,returnVal});
hitList.insert(&*m);
}
if(geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(m->GetPos(),12*m->GetSizeMult()))){
m->ProximityKnockback(pos,knockbackAmt);
}
}
}
if(CheckForPlayerCollisions){
if(!hitList.count(GetPlayer())&&geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(GetPlayer()->GetPos(),12*GetPlayer()->GetSizeMult()))){
float angleToPlayer=geom2d::line<float>{pos,GetPlayer()->GetPos()}.vector().polar().y;
float angleDiff=util::angle_difference(angleToPlayer,angle);
if(abs(angleDiff)<=sweepAngle){
HurtReturnValue returnVal=GetPlayer()->Hurt(damage,upperLevel,z);
affectedList.push_back({GetPlayer(),returnVal});
hitList.insert(GetPlayer());
if(geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(GetPlayer()->GetPos(),12*GetPlayer()->GetSizeMult()))){
GetPlayer()->ProximityKnockback(pos,knockbackAmt);
}
}
}
void AiL::ProximityKnockback(const vf2d pos,const float radius,const float knockbackAmt,const HurtList&knockbackTargets,const KnockbackCondition condition)const{
using enum KnockbackCondition;
for(auto&[target,wasHurt]:knockbackTargets){
if(condition==KNOCKBACK_HURT_TARGETS&&!wasHurt||
condition==KNOCKBACK_UNHURT_TARGETS&&wasHurt)continue;
if(std::holds_alternative<Player*>(target)){
Player*player{std::get<Player*>(target)};
player->ProximityKnockback(pos,knockbackAmt);
}else
if(std::holds_alternative<Monster*>(target)){
Monster*monster{std::get<Monster*>(target)};
monster->ProximityKnockback(pos,knockbackAmt);
}else ERR("WARNING! Target list was holding an unknown type??? THIS SHOULD NOT BE HAPPENING!")
}
return affectedList;
}
void AiL::PopulateRenderLists(){
@ -882,7 +882,7 @@ void AiL::PopulateRenderLists(){
std::sort(MONSTER_LIST.begin(),MONSTER_LIST.end(),[](std::unique_ptr<Monster>&m1,std::unique_ptr<Monster>&m2){return m1->GetPos().y<m2->GetPos().y;});
std::sort(ItemDrop::drops.begin(),ItemDrop::drops.end(),[](ItemDrop&id1,ItemDrop&id2){return id1.GetPos().y<id2.GetPos().y;});
std::sort(BULLET_LIST.begin(),BULLET_LIST.end(),[](std::unique_ptr<Bullet>&b1,std::unique_ptr<Bullet>&b2){return b1->pos.y<b2->pos.y;});
std::sort(BULLET_LIST.begin(),BULLET_LIST.end(),[](std::unique_ptr<IBullet>&b1,std::unique_ptr<IBullet>&b2){return b1->pos.y<b2->pos.y;});
std::sort(foregroundEffects.begin(),foregroundEffects.end(),[](std::unique_ptr<Effect>&e1,std::unique_ptr<Effect>&e2){return e1->pos.y<e2->pos.y;});
std::sort(backgroundEffects.begin(),backgroundEffects.end(),[](std::unique_ptr<Effect>&e1,std::unique_ptr<Effect>&e2){return e1->pos.y<e2->pos.y;});
@ -919,7 +919,7 @@ void AiL::PopulateRenderLists(){
}
}
for(auto it=BULLET_LIST.begin();it!=BULLET_LIST.end();++it){
Bullet*b=(*it).get();
IBullet*b=(*it).get();
if(b->OnUpperLevel()){
bulletsUpper.push_back(b);
}else{
@ -1487,7 +1487,7 @@ void AiL::RenderWorld(float fElapsedTime){
ItemDrop::drops[dropInd].Draw();
++dropsAfterLowerIt;
}
for(const Bullet*const b:bulletsLower){
for(const IBullet*const b:bulletsLower){
b->_Draw();
}
for(const Effect*const e:foregroundEffectsLower){
@ -1788,7 +1788,7 @@ void AiL::RenderWorld(float fElapsedTime){
ItemDrop::drops[dropInd].Draw();
++dropsAfterUpperIt;
}
for(const Bullet*const b:bulletsUpper){
for(const IBullet*const b:bulletsUpper){
b->_Draw();
}
for(const Effect*const e:foregroundEffectsUpper){
@ -1965,54 +1965,7 @@ void AiL::RenderHud(){
for(std::vector<std::shared_ptr<DamageNumber>>::iterator it=DAMAGENUMBER_LIST.begin();it!=DAMAGENUMBER_LIST.end();++it){
DamageNumber*dn=(*it).get();
#define NumberScalesWithDamage true
#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}){
vf2d textSize=GetTextSizeProp(text)*scaling;
if(!dn->friendly){
vf2d additionalScaling={1.f,1.f};
if(ScaleWithNumber)additionalScaling=dn->size;
textSize*=additionalScaling;
vf2d drawPos=dn->pos-textSize/2.f;
drawPos.x=std::clamp(drawPos.x,view.GetWorldTL().x,view.GetWorldBR().x-textSize.x);
drawPos.y=std::clamp(drawPos.y,view.GetWorldTL().y,view.GetWorldBR().y-textSize.y);
view.DrawShadowStringPropDecal(drawPos,text,colorsEnemy.first,colorsEnemy.second,scaling*additionalScaling);
}else{
vf2d drawPos=dn->pos-textSize/2.f;
drawPos.x=std::clamp(drawPos.x,view.GetWorldTL().x,view.GetWorldBR().x-textSize.x);
drawPos.y=std::clamp(drawPos.y,view.GetWorldTL().y,view.GetWorldBR().y-textSize.y);
view.DrawShadowStringPropDecal(drawPos,text,colorsFriendly.first,colorsFriendly.second,scaling);
}
};
switch(dn->type){
case HEALTH_LOSS:{
std::string text=std::to_string(dn->damage);
DrawDamageNumber(NumberScalesWithDamage,text,{DARK_RED,{255,160,160}},{RED,VERY_DARK_GREY});
}break;
case HEALTH_GAIN:{
std::string text="+"+std::to_string(dn->damage);
DrawDamageNumber(NormalNumber,text,{DARK_GREEN,BLACK},{GREEN,VERY_DARK_GREY});
}break;
case MANA_GAIN:{
std::string text="+"+std::to_string(dn->damage);
DrawDamageNumber(NormalNumber,text,{BLUE,VERY_DARK_GREY},{BLUE,VERY_DARK_GREY});
}break;
case INTERRUPT:{
std::string text="Interrupted!";
DrawDamageNumber(NormalNumber,text,{BLACK,VERY_DARK_GREY},{BLACK,VERY_DARK_GREY},{0.5f,1});
}break;
case CRIT:{
std::string text=std::to_string(dn->damage);
DrawDamageNumber(NumberScalesWithDamage,text,{YELLOW,DARK_YELLOW},{BLACK,{0,0,0,0}});
}break;
}
dn->Draw();
}
#ifdef _DEBUG

@ -171,7 +171,7 @@ private:
void InitializeClasses();
int DEBUG_PATHFINDING=0;
std::vector<Monster*>monstersBeforeLower,monstersAfterLower,monstersBeforeUpper,monstersAfterUpper;
std::vector<Bullet*>bulletsLower,bulletsUpper;
std::vector<IBullet*>bulletsLower,bulletsUpper;
std::vector<Effect*>backgroundEffectsLower,backgroundEffectsUpper,foregroundEffectsLower,foregroundEffectsUpper;
float reflectionUpdateTimer=0;
float reflectionStepTime=0;
@ -254,15 +254,16 @@ public:
void AddEffect(std::unique_ptr<Effect>foreground,std::unique_ptr<Effect>background);
//If back is true, places the effect in the background
void AddEffect(std::unique_ptr<Effect>foreground,bool back=false);
const HurtList Hurt(vf2d pos,float radius,int damage,bool upperLevel,float z,const HurtType hurtTargets)const;
void ProximityKnockback(const vf2d pos,const float radius,const float knockbackAmt,const HurtType knockbackTargets)const;
void ProximityKnockback(const vf2d pos,const float radius,const float knockbackAmt,const HurtList&knockbackTargets,const KnockbackCondition condition=KnockbackCondition::KNOCKBACK_HURT_TARGETS)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!
const HurtList HurtNotHit(vf2d pos,float radius,int damage,HitList&hitList,bool upperLevel,float z,const HurtType hurtTargets)const;
const HurtList HurtNotHit(vf2d pos,float radius,int damage,HitList&hitList,bool upperLevel,float z,const HurtType hurtTargets,HurtFlag::HurtFlag hurtFlags=HurtFlag::NONE)const;
// angle: The central angle where the arc will extend from.
// sweepAngle: The amount of radians to extend in both directions from the central angle.
// NOTE: This function will also add any enemies that were hit into the hit list!
const HurtList HurtConeNotHit(vf2d pos,float radius,float angle,float sweepAngle,int damage,HitList&hitList,bool upperLevel,float z,const HurtType hurtTargets)const;
const HurtList HurtConeNotHit(vf2d pos,float radius,float angle,float sweepAngle,int damage,HitList&hitList,bool upperLevel,float z,const HurtType hurtTargets,HurtFlag::HurtFlag hurtFlags=HurtFlag::NONE)const;
void ProximityKnockback(const vf2d pos,const float radius,const float knockbackAmt,const HurtType knockbackTargets)const;
void ProximityKnockback(const vf2d pos,const float radius,const float knockbackAmt,const HurtList&knockbackTargets,const KnockbackCondition condition=KnockbackCondition::KNOCKBACK_HURT_TARGETS)const;
vf2d GetWorldMousePos();
bool LeftHeld();
bool RightHeld();
@ -374,7 +375,6 @@ public:
Overlay&GetOverlay();
void SetWindSpeed(vf2d newWindSpd);
const vf2d&GetWindSpeed()const;
const HurtList HurtMonsterType(vf2d pos,float radius,int damage,bool upperLevel,float z,const std::string_view monsterName)const;
void InitializeGameConfigurations();
void InitializePlayer();
void SetWorldZoom(float newZoomScale);

@ -103,3 +103,7 @@ BulletDestroyState Arrow::MonsterHit(Monster& monster)
game->AddEffect(std::make_unique<Effect>(monster.GetPos(),0,"splash_effect.png",upperLevel,monster.GetSizeMult(),0.25));
return BulletDestroyState::KEEP_ALIVE;
}
void Arrow::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){
if(friendly)hurtFlags|=HurtFlag::PLAYER_ABILITY;
}

@ -105,3 +105,5 @@ void Bomb::Draw(const Pixel blendCol)const{
Bullet::Draw(blendCol);
game->view.DrawPartialRotatedDecal(pos-vf2d{0,GetZ()},bomb_animation.GetFrame(animation).GetSourceImage()->Decal(),rotates?atan2(vel.y,vel.x)-PI/2:0,bomb_animation.GetFrame(animation).GetSourceRect().size/2,bomb_animation.GetFrame(animation).GetSourceRect().pos,bomb_animation.GetFrame(animation).GetSourceRect().size,scale,fadeOutTime==0?col:Pixel{col.r,col.g,col.b,uint8_t(util::lerp(col.a,0,1-((fadeOutTime-GetFadeoutTimer())/fadeOutTime)))});
}
void Bomb::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){}

@ -53,6 +53,7 @@ enum BuffType{
COLLISION_KNOCKBACK_STRENGTH, //Causes an amount of knockback based on intensity when hit via collision with this buff
SELF_INFLICTED_SLOWDOWN, //Used for monsters and can't be applied by any player abilities.
ADRENALINE_RUSH,
TRAPPER_MARK,
};
class AiL;

@ -30,244 +30,17 @@ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "Bullet.h"
#include "AdventuresInLestoria.h"
#include "DEFINES.h"
#include "safemap.h"
#include "util.h"
#include "SoundEffect.h"
INCLUDE_ANIMATION_DATA
INCLUDE_game
INCLUDE_GFX
INCLUDE_MONSTER_LIST
INCLUDE_WINDOW_SIZE
#include "Bullet.h"
Bullet::Bullet(vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly,Pixel col,vf2d scale,float image_angle)
:pos(pos),vel(vel),radius(radius),damage(damage),col(col),friendly(friendly),upperLevel(upperLevel),scale(scale),image_angle(image_angle){};
:IBullet(pos,vel,radius,damage,upperLevel,friendly,col,scale,image_angle){}
//Initializes a bullet with an animation.
Bullet::Bullet(vf2d pos,vf2d vel,float radius,int damage,std::string animation,bool upperLevel,bool hitsMultiple,float lifetime,bool rotatesWithAngle,bool friendly,Pixel col,vf2d scale,float image_angle,std::string_view hitSound)
:pos(pos),vel(vel),radius(radius),damage(damage),col(col),animated(true),rotates(rotatesWithAngle),lifetime(lifetime),hitsMultiple(hitsMultiple),friendly(friendly),upperLevel(upperLevel),scale(scale),image_angle(image_angle),hitSound(std::string(hitSound)){
this->animation.AddState(animation,ANIMATION_DATA.at(animation));
this->animation.ChangeState(internal_animState,animation);
};
Animate2D::Frame Bullet::GetFrame()const{
return animation.GetFrame(internal_animState);
}
void Bullet::UpdateFadeTime(float fElapsedTime){
aliveTime+=fElapsedTime;
if(fadeInTime>0){
if(fadeInTimer<fadeInTime){
fadeInTimer=std::min(fadeInTime,fadeInTimer+fElapsedTime);
}
}
if(fadeOutTime>0){
if(fadeOutTimer==0){
lifetime=fadeOutTime;
}
fadeOutTimer+=fElapsedTime;
}
}
void Bullet::Update(float fElapsedTime){}
void Bullet::SimulateUpdate(const float fElapsedTime){
simulated=true;
_Update(fElapsedTime);
simulated=false;
}
void Bullet::_Update(const float fElapsedTime){
if(dead)return;
UpdateFadeTime(fElapsedTime);
Update(fElapsedTime);
animation.UpdateState(internal_animState,fElapsedTime);
const bool CollisionCheckRequired=IsActivated()&&fadeInTimer==fadeInTime&&radius!=0.f;
if(CollisionCheckRequired){
float totalDistance=(vel*fElapsedTime).mag();
int iterations=int(std::max(1.f,(vel*fElapsedTime).mag()));
int totalIterations=iterations;
vf2d finalBulletPos=pos+vel*fElapsedTime;
if(IsPlayerAutoAttackProjectile()){finalBulletPos+=(game->GetWindSpeed()*game->GetElapsedTime())/float(totalIterations);}
distanceTraveled+=totalDistance/24.f*100.f;
const auto CollisionCheck=[&](){
if(simulated)return true;
if(friendly){
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
if(geom2d::overlaps(m->BulletCollisionHitbox(),geom2d::circle(pos,radius))){
if(hitList.find(&*m)==hitList.end()&&m->Hurt(damage,OnUpperLevel(),z)){
if(!hitsMultiple){
if(_MonsterHit(*m)==BulletDestroyState::DESTROY){
dead=true;
}
return false;
}else _MonsterHit(*m);
hitList.insert(&*m);
}
}
}
} else {
if(geom2d::overlaps(game->GetPlayer()->Hitbox(),geom2d::circle(pos,radius))){
if(hitList.find(game->GetPlayer())==hitList.end()&&game->GetPlayer()->Hurt(damage,OnUpperLevel(),z)){
if(!hitsMultiple){
if(_PlayerHit(&*game->GetPlayer())==BulletDestroyState::DESTROY){
dead=true;
}
return false;
}else _PlayerHit(&*game->GetPlayer());
hitList.insert(game->GetPlayer());
}
}
}
return true;
};
while(iterations>0){
iterations--;
if(IsPlayerAutoAttackProjectile()){pos+=(game->GetWindSpeed()*game->GetElapsedTime())/float(totalIterations);}
pos+=(vel*fElapsedTime)/float(totalIterations);
if(!CollisionCheck()){
goto DeadBulletCheck;
}
}
pos=finalBulletPos;
if(!CollisionCheck()){
goto DeadBulletCheck;
}
}else{
if(IsPlayerAutoAttackProjectile()){pos+=game->GetWindSpeed()*game->GetElapsedTime();}
pos+=vel*fElapsedTime;
}
DeadBulletCheck:
if(/*World size in PIXELS!*/vi2d worldSize=game->GetCurrentMapData().MapSize*game->GetCurrentMapData().TileSize;pos.x+radius<-WINDOW_SIZE.x||pos.x-radius>worldSize.x+WINDOW_SIZE.x||pos.y+radius<-WINDOW_SIZE.y||pos.y-radius>worldSize.y+WINDOW_SIZE.y){
dead=true;
return;
}
lifetime-=fElapsedTime;
if(lifetime<=0){
dead=true;
return;
}
}
void Bullet::_Draw()const{
Pixel blendCol=col;
if(fadeInTime==0&&fadeOutTime==0)blendCol.a=col.a;
else if(fadeOutTime>0)blendCol.a=uint8_t(util::lerp(col.a,0,1-((fadeOutTime-fadeOutTimer)/fadeOutTime)));
else if(fadeInTime>0)blendCol.a=uint8_t(util::lerp(col.a,0,((fadeInTime-fadeInTimer)/fadeInTime)));
if(GetZ()>0){
vf2d shadowScale=vf2d{8*scale.x/3.f,1}/std::max(1.f,GetZ()/8);
game->view.DrawDecal(pos-vf2d{3,3}*shadowScale/2+vf2d{0,12*scale.y},GFX["circle.png"].Decal(),shadowScale,BLACK);
}
const bool NotOnTitleScreen=GameState::STATE!=GameState::states[States::MAIN_MENU];
if(NotOnTitleScreen
&&(game->GetPlayer()->HasIframes()||game->GetPlayer()->GetZ()>1)){
blendCol.a/=1.59f; //Comes from 255 divided by 160 which is roughly what we want the alpha to be when the bullet has full transparency.
}
Draw(blendCol);
}
void Bullet::Draw(const Pixel blendCol)const{
if(animated){
game->view.DrawPartialRotatedDecal(pos-vf2d{0,GetZ()}+drawOffsetY,GetFrame().GetSourceImage()->Decal(),rotates?atan2(vel.y,vel.x)-PI/2+image_angle:image_angle,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,scale,blendCol);
}else{
game->view.DrawRotatedDecal(pos-vf2d{0,GetZ()}+drawOffsetY,GFX["circle.png"].Decal(),image_angle,GFX["circle.png"].Sprite()->Size()/2.f,scale,blendCol);
game->view.DrawRotatedDecal(pos-vf2d{0,GetZ()}+drawOffsetY,GFX["circle_outline.png"].Decal(),image_angle,GFX["circle.png"].Sprite()->Size()/2.f,scale,Pixel{WHITE.r,WHITE.g,WHITE.b,blendCol.a});
}
}
BulletDestroyState Bullet::_PlayerHit(Player*player){
const BulletDestroyState destroyBullet=PlayerHit(player);
if(iframeTimerOnHit>0.f)player->ApplyIframes(iframeTimerOnHit);
if(hitSound)SoundEffect::PlaySFX(hitSound.value(),pos);
return destroyBullet;
}
BulletDestroyState Bullet::_MonsterHit(Monster&monster){
const BulletDestroyState destroyBullet=MonsterHit(monster);
if(iframeTimerOnHit>0.f)monster.ApplyIframes(iframeTimerOnHit);
if(hitSound)SoundEffect::PlaySFX(hitSound.value(),pos);
return destroyBullet;
}
BulletDestroyState Bullet::PlayerHit(Player*player){
if(!hitsMultiple)fadeOutTime=0.15f;
return BulletDestroyState::KEEP_ALIVE;
}
BulletDestroyState Bullet::MonsterHit(Monster&monster){
if(!hitsMultiple)fadeOutTime=0.15f;
return BulletDestroyState::KEEP_ALIVE;
}
bool Bullet::OnUpperLevel(){return upperLevel;}
const bool Bullet::IsDead()const{
return dead;
}
const float Bullet::GetZ()const{
return z;
}
Bullet&Bullet::SetIframeTimeOnHit(float iframeTimer){
iframeTimerOnHit=iframeTimer;
return *this;
}
Bullet&Bullet::SetFadeinTime(float fadeInTime){
const float durationDiff=fadeInTime-fadeInTimer;
this->fadeInTime=fadeInTime;
lifetime+=durationDiff;
return *this;
}
const float&Bullet::GetFadeoutTimer()const{
return fadeOutTimer;
}
Bullet&Bullet::SetIsPlayerAutoAttackProjectile(){
playerAutoAttackProjectile=true;
return *this;
}
const bool Bullet::IsPlayerAutoAttackProjectile()const{
return playerAutoAttackProjectile;
}
void Bullet::AddVelocity(vf2d vel){
this->vel+=vel*game->GetElapsedTime();
}
void Bullet::SetBulletType(const BulletType type){
this->type=type;
}
const BulletType Bullet::GetBulletType()const{
return type;
}
const bool Bullet::IsActivated()const{
return !IsDeactivated();
}
const bool Bullet::IsDeactivated()const{
return deactivated||fadeOutTime>0.f;
}
void Bullet::Deactivate(){
deactivated=true;
}
const double Bullet::GetTimeAlive()const{
return aliveTime;
}
const double Bullet::GetAliveTime()const{
return aliveTime;
}
:IBullet(pos,vel,radius,damage,animation,upperLevel,hitsMultiple,lifetime,rotatesWithAngle,friendly,col,scale,image_angle,hitSound){}
void Bullet::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){}

@ -30,103 +30,20 @@ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#pragma once
#include "Animation.h"
#include "olcUTIL_Animate2D.h"
#include "Monster.h"
#include "DEFINES.h"
using HitList=std::unordered_set<std::variant<Monster*,Player*>>;
enum class BulletType{
UNDEFINED,
FEATHER,
LARGE_TORNADO,
};
enum class BulletDestroyState{
KEEP_ALIVE,
DESTROY,
};
#include "IBullet.h"
struct Bullet{
friend class AiL;
vf2d vel;
vf2d pos;
float radius;
int damage;
Pixel col;
float lifetime=float(INFINITE);
bool hitsMultiple=false;
bool rotates=false;
bool animated=false;
float fadeOutTime=0.f; //Setting the fade out time causes the bullet's lifetime to be set to the fadeout time as well, as that's when the bullet's alpha will reach 0, so it dies.
bool friendly=false; //Whether or not it's a player bullet or enemy bullet.
bool upperLevel=false;
bool alwaysOnTop=false;
float z=0.f;
float image_angle=0.f;
protected:
float distanceTraveled=0.f;
vf2d scale={1,1};
float fadeInTime=0; //Setting the fade in time causes the bullet to be disabled and the bullet's alpha will fade in from zero to the actual alpha of the bullet. When the fade in timer reaches the fade in time automatically, the bullet will be enabled.
virtual void Update(float fElapsedTime);
void Deactivate();
private:
float fadeOutTimer=0;
float fadeInTimer=0;
void UpdateFadeTime(float fElapsedTime);
bool dead=false; //When marked as dead it wil be removed by the next frame.
bool simulated=false; //A simulated bullet cannot interact / damage things in the world. It's simply used for simulating the trajectory and potential path of the bullet
float iframeTimerOnHit{0.f};
bool playerAutoAttackProjectile=false; //Set to true for bullets that are auto attack projectiles to identify them.
void _Draw()const;
BulletType type{BulletType::UNDEFINED};
bool deactivated{false};
double aliveTime{};
float onContactFadeoutTime{}; //What fadeouttime will be set to when the bullet hits a monster.
std::optional<std::string>hitSound;
protected:
float drawOffsetY{};
BulletDestroyState _PlayerHit(Player*player); //Return true to destroy the bullet on hit, return false otherwise. THE BULLET HIT HAS ALREADY OCCURRED.
BulletDestroyState _MonsterHit(Monster&monster); //Return true to destroy the bullet on hit, return false otherwise. THE BULLET HIT HAS ALREADY OCCURRED.
const float&GetFadeoutTimer()const;
void SetBulletType(const BulletType type);
const double GetTimeAlive()const;
class Bullet:public IBullet{
public:
Animate2D::Animation<std::string>animation;
Animate2D::AnimationState internal_animState;
HitList hitList;
virtual ~Bullet()=default;
Bullet(vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle=0.f);
//Initializes a bullet with an animation.
Bullet(vf2d pos,vf2d vel,float radius,int damage,std::string animation,bool upperLevel,bool hitsMultiple=false,float lifetime=INFINITE,bool rotatesWithAngle=false,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle=0.f,std::string_view hitSound="");
public:
void SimulateUpdate(const float fElapsedTime);
void _Update(const float fElapsedTime);
//Used by special bullets to control custom despawning behavior! Return true when the bullet should be destroyed. Return false to handle it otherwise (like deactivating it instead). You become responsible for getting rid of the bullet.
//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
virtual BulletDestroyState PlayerHit(Player*player);
//Used by special bullets to control custom despawning behavior! Return true when the bullet should be destroyed. Return false to handle it otherwise (like deactivating it instead). You become responsible for getting rid of the bullet.
//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
virtual BulletDestroyState MonsterHit(Monster&monster);
Animate2D::Frame GetFrame()const;
virtual void Draw(const Pixel blendCol)const;
bool OnUpperLevel();
const bool IsDead()const;
const float GetZ()const;
Bullet&SetIframeTimeOnHit(float iframeTimer);
Bullet&SetFadeinTime(float fadeInTime);
Bullet&SetIsPlayerAutoAttackProjectile(); //Enables the playerAutoAttackProjectile flag.
const bool IsPlayerAutoAttackProjectile()const;
void AddVelocity(vf2d vel);
const BulletType GetBulletType()const;
const bool IsActivated()const;
const bool IsDeactivated()const;
const double GetAliveTime()const;
protected:
virtual void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};

@ -46,6 +46,7 @@ struct EnergyBolt:public Bullet{
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};
struct FireBolt:public Bullet{
@ -54,6 +55,7 @@ struct FireBolt:public Bullet{
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};
struct LightningBolt:public Bullet{
@ -62,6 +64,7 @@ struct LightningBolt:public Bullet{
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};
struct Arrow:public Bullet{
@ -77,6 +80,7 @@ struct Arrow:public Bullet{
const vf2d PointToBestTargetPath(const uint8_t perceptionLevel);
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};
struct ChargedArrow:public Bullet{
@ -85,6 +89,7 @@ struct ChargedArrow:public Bullet{
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};
struct FrogTongue:public Bullet{
@ -98,6 +103,7 @@ struct FrogTongue:public Bullet{
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void Draw(const Pixel blendCol)const override;
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};
struct Wisp:public Bullet{
@ -105,6 +111,7 @@ struct Wisp:public Bullet{
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};
enum class HorizontalFlip{
@ -133,6 +140,7 @@ struct DaggerStab:public Bullet{
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};
struct DaggerSlash:public Bullet{
@ -145,6 +153,7 @@ struct DaggerSlash:public Bullet{
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};
struct Bomb:public Bullet{
@ -161,6 +170,7 @@ struct Bomb:public Bullet{
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
void Draw(const Pixel blendCol)const override;
};
@ -186,6 +196,7 @@ struct LevitatingRock:public Bullet{
void Draw(const Pixel blendCol)const override;
void AssignMaster(LevitatingRock*masterRock);
const bool IsMaster()const;
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};
struct Tornado:public Bullet{
@ -200,6 +211,7 @@ struct Tornado:public Bullet{
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};
struct Debris:public Bullet{
@ -210,6 +222,7 @@ struct Debris:public Bullet{
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
void Draw(const Pixel blendCol)const override;
};
@ -222,10 +235,12 @@ struct LargeTornado:public Bullet{
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};
struct Feather:public Bullet{
Feather(vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool hitsMultiple=false,float lifetime=INFINITE,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle=0.f);
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
};
struct LargeStone:public Bullet{
@ -235,6 +250,7 @@ protected:
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
private:
const float gravity;
const float fixedTimeStep{1/30.f};
@ -254,6 +270,7 @@ struct FallingStone:public Bullet{
protected:
void Update(float fElapsedTime)override;
void Draw(const Pixel blendCol)const override;
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
private:
const vf2d targetPos;
const float zVel{};
@ -268,6 +285,7 @@ struct DeadlyDash:public Bullet{
DeadlyDash(vf2d startPos,vf2d endPos,float radius,float afterImagesLingeringTime,int damage,float knockbackAmt,bool upperLevel,bool friendly,float afterImagesSpreadDist,const std::string&animation,Pixel col);
protected:
void Draw(const Pixel blendCol)const override;
void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags);
private:
const vf2d startPos;
const vf2d endPos;

@ -68,3 +68,7 @@ BulletDestroyState ChargedArrow::MonsterHit(Monster& monster)
{
return BulletDestroyState::KEEP_ALIVE;
}
void ChargedArrow::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){
if(friendly)hurtFlags|=HurtFlag::PLAYER_ABILITY;
}

@ -50,7 +50,7 @@ using MonsterSpawnerID=int;
#define INCLUDE_DAMAGENUMBER_LIST extern std::vector<std::shared_ptr<DamageNumber>>DAMAGENUMBER_LIST;
#define INCLUDE_game extern AiL*game;
#define INCLUDE_MONSTER_DATA extern std::map<std::string,MonsterData>MONSTER_DATA;
#define INCLUDE_BULLET_LIST extern std::vector<std::unique_ptr<Bullet>>BULLET_LIST;
#define INCLUDE_BULLET_LIST extern std::vector<std::unique_ptr<IBullet>>BULLET_LIST;
#define INCLUDE_EMITTER_LIST extern std::vector<std::unique_ptr<Emitter>>EMITTER_LIST;
#define INCLUDE_DATA extern utils::datafile DATA;
#define INCLUDE_STRATEGY_DATA extern safemap<std::string,std::function<void(Monster&,float,std::string)>>STRATEGY_DATA;

@ -98,3 +98,5 @@ BulletDestroyState DaggerSlash::MonsterHit(Monster&monster){
Deactivate();
return BulletDestroyState::KEEP_ALIVE;
}
void DaggerSlash::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){}

@ -104,3 +104,5 @@ BulletDestroyState DaggerStab::MonsterHit(Monster&monster){
Deactivate();
return BulletDestroyState::KEEP_ALIVE;
}
void DaggerStab::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){}

@ -42,11 +42,13 @@ INCLUDE_game
const float DamageNumber::MOVE_UP_TIME=0.4f;
using enum DamageNumberType::DamageNumberType;
DamageNumber::DamageNumber()
:damage(0){
}
DamageNumber::DamageNumber(vf2d pos,int damage,bool friendly,DamageNumberType type):
DamageNumber::DamageNumber(vf2d pos,int damage,bool friendly,DamageNumberType::DamageNumberType type):
pos(pos),damage(damage),friendly(friendly),type(type),invertedDirection(type==INTERRUPT),riseSpd(20.f){
if(type==INTERRUPT||type==MANA_GAIN)riseSpd=40.f;
originalRiseSpd=riseSpd;
@ -64,3 +66,58 @@ void DamageNumber::RecalculateSize(){
size=vf2d{newSize,newSize};
}
}
void DamageNumber::Draw(){
#define NumberScalesWithDamage true
#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}){
vf2d textSize=game->GetTextSizeProp(text)*scaling;
if(!friendly){
vf2d additionalScaling={1.f,1.f};
if(ScaleWithNumber)additionalScaling=size;
textSize*=additionalScaling;
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,colorsEnemy.first,colorsEnemy.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);
}
};
switch(type){
case HEALTH_LOSS:{
std::string text=std::to_string(damage);
DrawDamageNumber(NumberScalesWithDamage,text,{DARK_RED,{255,160,160}},{RED,VERY_DARK_GREY});
}break;
case HEALTH_GAIN:{
std::string text="+"+std::to_string(damage);
DrawDamageNumber(NormalNumber,text,{DARK_GREEN,BLACK},{GREEN,VERY_DARK_GREY});
}break;
case MANA_GAIN:{
std::string text="+"+std::to_string(damage);
DrawDamageNumber(NormalNumber,text,{BLUE,VERY_DARK_GREY},{BLUE,VERY_DARK_GREY});
}break;
case INTERRUPT:{
std::string text="Interrupted!";
DrawDamageNumber(NormalNumber,text,{BLACK,VERY_DARK_GREY},{BLACK,VERY_DARK_GREY},{0.5f,1});
}break;
case CRIT:{
std::string text=std::to_string(damage);
DrawDamageNumber(NumberScalesWithDamage,text,{YELLOW,DARK_YELLOW},{BLACK,{0,0,0,0}});
}break;
case DOT:{
std::string text=std::to_string(damage);
DrawDamageNumber(NormalNumber,text,{0xE1BEE7,0x1F083A},{0xDCE775,0x37320A});
}break;
default:ERR(std::format("Damage Number Type {} is not implemented! THIS SHOULD NOT BE HAPPENING!",int(type)));
}
}

@ -38,13 +38,16 @@ All rights reserved.
#pragma once
#include "olcUTIL_Geometry2D.h"
enum DamageNumberType{
namespace DamageNumberType{
enum DamageNumberType{
HEALTH_LOSS,
HEALTH_GAIN,
MANA_GAIN,
INTERRUPT,
CRIT,
};
DOT,
};
}
struct DamageNumber{
vf2d pos;
@ -55,11 +58,12 @@ struct DamageNumber{
vf2d size{1.f,1.f};
bool friendly=false;
bool invertedDirection=false;
DamageNumberType type=HEALTH_LOSS;
DamageNumberType::DamageNumberType type=DamageNumberType::HEALTH_LOSS;
const static float MOVE_UP_TIME;
float originalRiseSpd=0.f;
DamageNumber();
//The friendly flag indicates if the number was for a friendly/player target or if it's for a monster target (set to false)
DamageNumber(vf2d pos,int damage,bool friendly=false,DamageNumberType type=HEALTH_LOSS);
DamageNumber(vf2d pos,int damage,bool friendly=false,DamageNumberType::DamageNumberType type=DamageNumberType::HEALTH_LOSS);
void RecalculateSize();
void Draw();
};

@ -53,7 +53,7 @@ DeadlyDash::DeadlyDash(vf2d startPos,vf2d endPos,float radius,float afterImagesL
const float offsetDist{afterImagesSpreadDist*(i+1)};
const vf2d afterImagePos{geom2d::line<float>{startPos,endPos}.rpoint(offsetDist)};
if(friendly){
const HurtList&hitList{game->HurtNotHit(afterImagePos,radius,damage,this->hitList,OnUpperLevel(),GetZ(),HurtType::MONSTER)};
const HurtList&hitList{game->HurtNotHit(afterImagePos,radius,damage,this->hitList,OnUpperLevel(),GetZ(),HurtType::MONSTER,HurtFlag::PLAYER_ABILITY)};
for(auto&[entityPtr,wasHit]:hitList){
if(wasHit){
Monster*monster{std::get<Monster*>(entityPtr)};
@ -91,3 +91,8 @@ void DeadlyDash::Draw(const Pixel blendCol)const{
game->SetDecalMode(DecalMode::NORMAL);
}
}
void DeadlyDash::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){
//NOTE: Despite it looking like we modify the damage here, the radius passed to Bullet is 0, so this shouldn't matter anyways, the damage happens in the check in Update()
if(friendly)hurtFlags|=HurtFlag::PLAYER_ABILITY;
}

@ -61,3 +61,5 @@ BulletDestroyState Debris::MonsterHit(Monster&monster){
void Debris::Draw(const Pixel blendCol)const{
game->view.DrawPartialRotatedDecal(pos-vf2d{0,GetZ()}+drawOffsetY,GetFrame().GetSourceImage()->Decal(),rotates?atan2(vel.y,vel.x)-PI/2+image_angle:image_angle,{12,12},vf2d{randomFrame*24.f,0.f},{24,24},scale,blendCol);
}
void Debris::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){}

@ -71,3 +71,7 @@ BulletDestroyState EnergyBolt::MonsterHit(Monster& monster)
game->AddEffect(std::make_unique<Effect>(monster.GetPos(),0,"splash_effect.png",upperLevel,monster.GetSizeMult(),"Wizard.Auto Attack.SplashEffectFadeoutTime"_F));
return BulletDestroyState::KEEP_ALIVE;
}
void EnergyBolt::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){
if(friendly)hurtFlags|=HurtFlag::PLAYER_ABILITY;
}

@ -88,3 +88,5 @@ void FallingStone::Draw(const Pixel blendCol)const{
}
Bullet::Draw(blendCol);
}
void FallingStone::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){}

@ -37,8 +37,11 @@ All rights reserved.
#pragma endregion
#include "BulletTypes.h"
#include "Player.h"
Feather::Feather(vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool hitsMultiple,float lifetime,bool friendly,Pixel col,vf2d scale,float image_angle)
:Bullet(pos,vel,radius,damage,"feather.png",upperLevel,hitsMultiple,lifetime,true,friendly,col){
SetBulletType(BulletType::FEATHER);
}
void Feather::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){}

@ -62,7 +62,7 @@ void FireBolt::Update(float fElapsedTime){
}
game->SetupWorldShake("Wizard.Ability 1.WorldShakeTime"_F);
if(friendly){
game->Hurt(pos,"Wizard.Ability 1.BulletHitExplosionRange"_F/100*12,int("Wizard.Ability 1.BulletHitExplosionDamageMult"_F*game->GetPlayer()->GetAttack()),OnUpperLevel(),0,HurtType::MONSTER);
game->Hurt(pos,"Wizard.Ability 1.BulletHitExplosionRange"_F/100*12,int("Wizard.Ability 1.BulletHitExplosionDamageMult"_F*game->GetPlayer()->GetAttack()),OnUpperLevel(),0,HurtType::MONSTER,HurtFlag::PLAYER_ABILITY);
}else{
if(geom2d::overlaps(geom2d::circle<float>{pos,"Wizard.Ability 1.BulletHitExplosionRange"_F/100*12},geom2d::circle<float>{game->GetPlayer()->GetPos(),12.f})){
game->GetPlayer()->Hurt(damage,OnUpperLevel(),0.f);
@ -94,9 +94,13 @@ BulletDestroyState FireBolt::MonsterHit(Monster& monster)
game->AddEffect(std::make_unique<Effect>(monster.GetPos(),"Wizard.Ability 1.BulletHitExplosionParticleLifetimeRange"_FRange,"circle.png",upperLevel,"Wizard.Ability 1.BulletHitExplosionParticleSizeRange"_FRange,"Wizard.Ability 1.BulletHitExplosionParticleFadeoutTimeRange"_FRange,vf2d{"Wizard.Ability 1.BulletHitExplosionParticleSpeedRange"_FRange,"Wizard.Ability 1.BulletHitExplosionParticleSpeedRange"_FRange},Pixel{uint8_t("Wizard.Ability 1.BulletHitExplosionParticleRedRange"_FRange),uint8_t("Wizard.Ability 1.BulletHitExplosionParticleGreenRange"_FRange),uint8_t("Wizard.Ability 1.BulletHitExplosionParticleBlueRange"_FRange),uint8_t("Wizard.Ability 1.BulletHitExplosionParticleAlphaRange"_FRange)}));
}
game->SetupWorldShake("Wizard.Ability 1.WorldShakeTime"_F);
game->Hurt(monster.GetPos(),"Wizard.Ability 1.BulletHitExplosionRange"_F/100*12,int("Wizard.Ability 1.BulletHitExplosionDamageMult"_F*game->GetPlayer()->GetAttack()),OnUpperLevel(),0,HurtType::MONSTER);
game->Hurt(monster.GetPos(),"Wizard.Ability 1.BulletHitExplosionRange"_F/100*12,int("Wizard.Ability 1.BulletHitExplosionDamageMult"_F*game->GetPlayer()->GetAttack()),OnUpperLevel(),0,HurtType::MONSTER,HurtFlag::PLAYER_ABILITY);
game->AddEffect(std::make_unique<Effect>(monster.GetPos(),0,"splash_effect.png",upperLevel,"Wizard.Ability 1.BulletHitExplosionRange"_F/100*2,"Wizard.Ability 1.BulletHitExplosionFadeoutTime"_F,vf2d{},"Wizard.Ability 1.BulletHitExplosionColor"_Pixel));
SoundEffect::PlaySFX("Wizard Fire Bolt Hit",pos);
return BulletDestroyState::KEEP_ALIVE;
}
void FireBolt::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){
if(friendly)hurtFlags|=HurtFlag::PLAYER_ABILITY;
}

@ -99,3 +99,7 @@ void FrogTongue::Draw(const Pixel blendCol)const{
game->view.DrawRotatedDecal(pos+drawVec,GFX["tongue.png"].Decal(),drawVec.polar().y,{0.f,1.f},{tongueLength,1.0f},col);
game->view.DrawRotatedDecal(tongueEndPos,GFX["tongue_end.png"].Decal(),drawVec.polar().y,{2.f,2.f},{1.f,1.f},col);
}
void FrogTongue::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){
if(friendly)hurtFlags|=HurtFlag::PLAYER_ABILITY;
}

@ -0,0 +1,59 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#pragma once
namespace HurtFlag{
enum HurtFlag{
NONE = 0b00,
PLAYER_ABILITY = 0b01, //Specifically use this flag for player auto attacks or Abilities only! (Identifies these attacks for the Trapper class)
DOT = 0b10, //Damage over time ability.
};
}
enum class TrueDamageFlag{
NORMAL_DAMAGE,
IGNORE_DAMAGE_RULES, //Deals true damage, ignoring established invulnerability/iframe rules. Will never miss and will not have its damage modified by any buffs/stats.
};
struct HurtDamageInfo{
int damage{};
bool onUpperLevel{false};
float z{};
HurtFlag::HurtFlag hurtFlags{HurtFlag::NONE};
TrueDamageFlag damageRule{TrueDamageFlag::NORMAL_DAMAGE};
};

@ -0,0 +1,282 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "Bullet.h"
#include "AdventuresInLestoria.h"
#include "DEFINES.h"
#include "safemap.h"
#include "util.h"
#include "SoundEffect.h"
INCLUDE_ANIMATION_DATA
INCLUDE_game
INCLUDE_GFX
INCLUDE_MONSTER_LIST
INCLUDE_WINDOW_SIZE
IBullet::IBullet(vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly,Pixel col,vf2d scale,float image_angle)
:pos(pos),vel(vel),radius(radius),damage(damage),col(col),friendly(friendly),upperLevel(upperLevel),scale(scale),image_angle(image_angle){};
IBullet::IBullet(vf2d pos,vf2d vel,float radius,int damage,std::string animation,bool upperLevel,bool hitsMultiple,float lifetime,bool rotatesWithAngle,bool friendly,Pixel col,vf2d scale,float image_angle,std::string_view hitSound)
:pos(pos),vel(vel),radius(radius),damage(damage),col(col),animated(true),rotates(rotatesWithAngle),lifetime(lifetime),hitsMultiple(hitsMultiple),friendly(friendly),upperLevel(upperLevel),scale(scale),image_angle(image_angle),hitSound(std::string(hitSound)){
this->animation.AddState(animation,ANIMATION_DATA.at(animation));
this->animation.ChangeState(internal_animState,animation);
};
Animate2D::Frame IBullet::GetFrame()const{
return animation.GetFrame(internal_animState);
}
void IBullet::UpdateFadeTime(float fElapsedTime){
aliveTime+=fElapsedTime;
if(fadeInTime>0){
if(fadeInTimer<fadeInTime){
fadeInTimer=std::min(fadeInTime,fadeInTimer+fElapsedTime);
}
}
if(fadeOutTime>0){
if(fadeOutTimer==0){
lifetime=fadeOutTime;
}
fadeOutTimer+=fElapsedTime;
}
}
void IBullet::Update(float fElapsedTime){}
void IBullet::SimulateUpdate(const float fElapsedTime){
simulated=true;
_Update(fElapsedTime);
simulated=false;
}
void IBullet::_Update(const float fElapsedTime){
if(dead)return;
UpdateFadeTime(fElapsedTime);
Update(fElapsedTime);
animation.UpdateState(internal_animState,fElapsedTime);
const bool CollisionCheckRequired=IsActivated()&&fadeInTimer==fadeInTime&&radius!=0.f;
if(CollisionCheckRequired){
float totalDistance=(vel*fElapsedTime).mag();
int iterations=int(std::max(1.f,(vel*fElapsedTime).mag()));
int totalIterations=iterations;
vf2d finalBulletPos=pos+vel*fElapsedTime;
if(IsPlayerAutoAttackProjectile()){finalBulletPos+=(game->GetWindSpeed()*game->GetElapsedTime())/float(totalIterations);}
distanceTraveled+=totalDistance/24.f*100.f;
const auto CollisionCheck=[&](){
if(simulated)return true;
if(friendly){
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
if(geom2d::overlaps(m->BulletCollisionHitbox(),geom2d::circle(pos,radius))){
if(hitList.find(&*m)==hitList.end()){
HurtDamageInfo damageData{damage,OnUpperLevel(),z,HurtFlag::NONE};
ModifyOutgoingDamageData(damageData.damage,damageData.onUpperLevel,damageData.z,damageData.hurtFlags);
if(m->Hurt(damageData)){
if(!hitsMultiple){
if(_MonsterHit(*m)==BulletDestroyState::DESTROY){
dead=true;
}
return false;
}else _MonsterHit(*m);
hitList.insert(&*m);
}
}
}
}
} else {
if(geom2d::overlaps(game->GetPlayer()->Hitbox(),geom2d::circle(pos,radius))){
if(hitList.find(game->GetPlayer())==hitList.end()){
HurtDamageInfo damageData{damage,OnUpperLevel(),z,HurtFlag::NONE};
//NOTE: OnHurt() will potentially change the Damage Data, passing it along to bullet children that might need to modify the flags!
ModifyOutgoingDamageData(damageData.damage,damageData.onUpperLevel,damageData.z,damageData.hurtFlags);
if(game->GetPlayer()->Hurt(damageData)){
if(!hitsMultiple){
if(_PlayerHit(&*game->GetPlayer())==BulletDestroyState::DESTROY){
dead=true;
}
return false;
}else _PlayerHit(&*game->GetPlayer());
hitList.insert(game->GetPlayer());
}
}
}
}
return true;
};
while(iterations>0){
iterations--;
if(IsPlayerAutoAttackProjectile()){pos+=(game->GetWindSpeed()*game->GetElapsedTime())/float(totalIterations);}
pos+=(vel*fElapsedTime)/float(totalIterations);
if(!CollisionCheck()){
goto DeadBulletCheck;
}
}
pos=finalBulletPos;
if(!CollisionCheck()){
goto DeadBulletCheck;
}
}else{
if(IsPlayerAutoAttackProjectile()){pos+=game->GetWindSpeed()*game->GetElapsedTime();}
pos+=vel*fElapsedTime;
}
DeadBulletCheck:
if(/*World size in PIXELS!*/vi2d worldSize=game->GetCurrentMapData().MapSize*game->GetCurrentMapData().TileSize;pos.x+radius<-WINDOW_SIZE.x||pos.x-radius>worldSize.x+WINDOW_SIZE.x||pos.y+radius<-WINDOW_SIZE.y||pos.y-radius>worldSize.y+WINDOW_SIZE.y){
dead=true;
return;
}
lifetime-=fElapsedTime;
if(lifetime<=0){
dead=true;
return;
}
}
void IBullet::_Draw()const{
Pixel blendCol=col;
if(fadeInTime==0&&fadeOutTime==0)blendCol.a=col.a;
else if(fadeOutTime>0)blendCol.a=uint8_t(util::lerp(col.a,0,1-((fadeOutTime-fadeOutTimer)/fadeOutTime)));
else if(fadeInTime>0)blendCol.a=uint8_t(util::lerp(col.a,0,((fadeInTime-fadeInTimer)/fadeInTime)));
if(GetZ()>0){
vf2d shadowScale=vf2d{8*scale.x/3.f,1}/std::max(1.f,GetZ()/8);
game->view.DrawDecal(pos-vf2d{3,3}*shadowScale/2+vf2d{0,12*scale.y},GFX["circle.png"].Decal(),shadowScale,BLACK);
}
const bool NotOnTitleScreen=GameState::STATE!=GameState::states[States::MAIN_MENU];
if(NotOnTitleScreen
&&(game->GetPlayer()->HasIframes()||game->GetPlayer()->GetZ()>1)){
blendCol.a/=1.59f; //Comes from 255 divided by 160 which is roughly what we want the alpha to be when the bullet has full transparency.
}
Draw(blendCol);
}
void IBullet::Draw(const Pixel blendCol)const{
if(animated){
game->view.DrawPartialRotatedDecal(pos-vf2d{0,GetZ()}+drawOffsetY,GetFrame().GetSourceImage()->Decal(),rotates?atan2(vel.y,vel.x)-PI/2+image_angle:image_angle,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,scale,blendCol);
}else{
game->view.DrawRotatedDecal(pos-vf2d{0,GetZ()}+drawOffsetY,GFX["circle.png"].Decal(),image_angle,GFX["circle.png"].Sprite()->Size()/2.f,scale,blendCol);
game->view.DrawRotatedDecal(pos-vf2d{0,GetZ()}+drawOffsetY,GFX["circle_outline.png"].Decal(),image_angle,GFX["circle.png"].Sprite()->Size()/2.f,scale,Pixel{WHITE.r,WHITE.g,WHITE.b,blendCol.a});
}
}
BulletDestroyState IBullet::_PlayerHit(Player*player){
const BulletDestroyState destroyBullet=PlayerHit(player);
if(iframeTimerOnHit>0.f)player->ApplyIframes(iframeTimerOnHit);
if(hitSound)SoundEffect::PlaySFX(hitSound.value(),pos);
return destroyBullet;
}
BulletDestroyState IBullet::_MonsterHit(Monster&monster){
const BulletDestroyState destroyBullet=MonsterHit(monster);
if(iframeTimerOnHit>0.f)monster.ApplyIframes(iframeTimerOnHit);
if(hitSound)SoundEffect::PlaySFX(hitSound.value(),pos);
return destroyBullet;
}
BulletDestroyState IBullet::PlayerHit(Player*player){
if(!hitsMultiple)fadeOutTime=0.15f;
return BulletDestroyState::KEEP_ALIVE;
}
BulletDestroyState IBullet::MonsterHit(Monster&monster){
if(!hitsMultiple)fadeOutTime=0.15f;
return BulletDestroyState::KEEP_ALIVE;
}
bool IBullet::OnUpperLevel(){return upperLevel;}
const bool IBullet::IsDead()const{
return dead;
}
const float IBullet::GetZ()const{
return z;
}
IBullet&IBullet::SetIframeTimeOnHit(float iframeTimer){
iframeTimerOnHit=iframeTimer;
return *this;
}
IBullet&IBullet::SetFadeinTime(float fadeInTime){
const float durationDiff=fadeInTime-fadeInTimer;
this->fadeInTime=fadeInTime;
lifetime+=durationDiff;
return *this;
}
const float&IBullet::GetFadeoutTimer()const{
return fadeOutTimer;
}
IBullet&IBullet::SetIsPlayerAutoAttackProjectile(){
playerAutoAttackProjectile=true;
return *this;
}
const bool IBullet::IsPlayerAutoAttackProjectile()const{
return playerAutoAttackProjectile;
}
void IBullet::AddVelocity(vf2d vel){
this->vel+=vel*game->GetElapsedTime();
}
void IBullet::SetBulletType(const BulletType type){
this->type=type;
}
const BulletType IBullet::GetBulletType()const{
return type;
}
const bool IBullet::IsActivated()const{
return !IsDeactivated();
}
const bool IBullet::IsDeactivated()const{
return deactivated||fadeOutTime>0.f;
}
void IBullet::Deactivate(){
deactivated=true;
}
const double IBullet::GetTimeAlive()const{
return aliveTime;
}
const double IBullet::GetAliveTime()const{
return aliveTime;
}

@ -0,0 +1,133 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#pragma once
#include "Animation.h"
#include "olcUTIL_Animate2D.h"
#include "Monster.h"
#include "DEFINES.h"
using HitList=std::unordered_set<std::variant<Monster*,Player*>>;
enum class BulletType{
UNDEFINED,
FEATHER,
LARGE_TORNADO,
};
enum class BulletDestroyState{
KEEP_ALIVE,
DESTROY,
};
struct IBullet{
friend class AiL;
vf2d vel;
vf2d pos;
float radius;
int damage;
Pixel col;
float lifetime=float(INFINITE);
bool hitsMultiple=false;
bool rotates=false;
bool animated=false;
float fadeOutTime=0.f; //Setting the fade out time causes the bullet's lifetime to be set to the fadeout time as well, as that's when the bullet's alpha will reach 0, so it dies.
bool friendly=false; //Whether or not it's a player bullet or enemy bullet.
bool upperLevel=false;
bool alwaysOnTop=false;
float z=0.f;
float image_angle=0.f;
protected:
float distanceTraveled=0.f;
vf2d scale={1,1};
float fadeInTime=0; //Setting the fade in time causes the bullet to be disabled and the bullet's alpha will fade in from zero to the actual alpha of the bullet. When the fade in timer reaches the fade in time automatically, the bullet will be enabled.
virtual void Update(float fElapsedTime);
void Deactivate();
private:
float fadeOutTimer=0;
float fadeInTimer=0;
void UpdateFadeTime(float fElapsedTime);
bool dead=false; //When marked as dead it wil be removed by the next frame.
bool simulated=false; //A simulated bullet cannot interact / damage things in the world. It's simply used for simulating the trajectory and potential path of the bullet
float iframeTimerOnHit{0.f};
bool playerAutoAttackProjectile=false; //Set to true for bullets that are auto attack projectiles to identify them.
void _Draw()const;
BulletType type{BulletType::UNDEFINED};
bool deactivated{false};
double aliveTime{};
float onContactFadeoutTime{}; //What fadeouttime will be set to when the bullet hits a monster.
std::optional<std::string>hitSound;
protected:
float drawOffsetY{};
BulletDestroyState _PlayerHit(Player*player); //Return true to destroy the bullet on hit, return false otherwise. THE BULLET HIT HAS ALREADY OCCURRED.
BulletDestroyState _MonsterHit(Monster&monster); //Return true to destroy the bullet on hit, return false otherwise. THE BULLET HIT HAS ALREADY OCCURRED.
const float&GetFadeoutTimer()const;
void SetBulletType(const BulletType type);
const double GetTimeAlive()const;
public:
Animate2D::Animation<std::string>animation;
Animate2D::AnimationState internal_animState;
HitList hitList;
virtual ~IBullet()=default;
IBullet(vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle=0.f);
//Initializes a bullet with an animation.
IBullet(vf2d pos,vf2d vel,float radius,int damage,std::string animation,bool upperLevel,bool hitsMultiple=false,float lifetime=INFINITE,bool rotatesWithAngle=false,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle=0.f,std::string_view hitSound="");
public:
void SimulateUpdate(const float fElapsedTime);
void _Update(const float fElapsedTime);
//Used by special bullets to control custom despawning behavior! Return true when the bullet should be destroyed. Return false to handle it otherwise (like deactivating it instead). You become responsible for getting rid of the bullet.
//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
virtual BulletDestroyState PlayerHit(Player*player);
//Used by special bullets to control custom despawning behavior! Return true when the bullet should be destroyed. Return false to handle it otherwise (like deactivating it instead). You become responsible for getting rid of the bullet.
//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
virtual BulletDestroyState MonsterHit(Monster&monster);
Animate2D::Frame GetFrame()const;
virtual void Draw(const Pixel blendCol)const;
bool OnUpperLevel();
const bool IsDead()const;
const float GetZ()const;
IBullet&SetIframeTimeOnHit(float iframeTimer);
IBullet&SetFadeinTime(float fadeInTime);
IBullet&SetIsPlayerAutoAttackProjectile(); //Enables the playerAutoAttackProjectile flag.
const bool IsPlayerAutoAttackProjectile()const;
void AddVelocity(vf2d vel);
const BulletType GetBulletType()const;
const bool IsActivated()const;
const bool IsDeactivated()const;
const double GetAliveTime()const;
virtual void ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags)=0;
};

@ -98,3 +98,5 @@ BulletDestroyState LargeStone::PlayerHit(Player*player){
BulletDestroyState LargeStone::MonsterHit(Monster&monster){
return BulletDestroyState::KEEP_ALIVE;
}
void LargeStone::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){}

@ -74,3 +74,5 @@ BulletDestroyState LargeTornado::MonsterHit(Monster&monster){
return BulletDestroyState::KEEP_ALIVE;
}
void LargeTornado::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){}

@ -113,3 +113,5 @@ void LevitatingRock::AssignMaster(LevitatingRock*masterRock){
const bool LevitatingRock::IsMaster()const{
return slaveRocks.size()>0;
}
void LevitatingRock::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){}

@ -96,7 +96,7 @@ BulletDestroyState LightningBolt::MonsterHit(Monster& monster)
geom2d::line<float>lineToTarget=geom2d::line<float>(monster.GetPos(),m->GetPos());
float dist=lineToTarget.length();
if(dist<="Wizard.Ability 2.LightningChainRadius"_F/100*24){
if(m->Hurt(int(game->GetPlayer()->GetAttack()*"Wizard.Ability 2.LightningChainDamageMult"_F),OnUpperLevel(),0)){
if(m->Hurt(int(game->GetPlayer()->GetAttack()*"Wizard.Ability 2.LightningChainDamageMult"_F),OnUpperLevel(),0,HurtFlag::PLAYER_ABILITY)){
EMITTER_LIST.push_back(std::make_unique<LightningBoltEmitter>(LightningBoltEmitter(monster.GetPos(),m->GetPos(),"Wizard.Ability 2.LightningChainFrequency"_F,"Wizard.Ability 2.LightningChainLifetime"_F,upperLevel)));
game->AddEffect(std::make_unique<Effect>(m->GetPos(),"Wizard.Ability 2.LightningChainSplashLifetime"_F,"lightning_splash_effect.png",upperLevel,monster.GetSizeMult(),"Wizard.Ability 2.LightningChainSplashFadeoutTime"_F,vf2d{},WHITE,"Wizard.Ability 2.LightningChainSplashRotationRange"_FRange));
targetsHit++;
@ -108,3 +108,7 @@ BulletDestroyState LightningBolt::MonsterHit(Monster& monster)
SoundEffect::PlaySFX("Wizard Lightning Bolt Hit",pos);
return BulletDestroyState::KEEP_ALIVE;
}
void LightningBolt::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){
if(friendly)hurtFlags|=HurtFlag::PLAYER_ABILITY;
}

@ -63,7 +63,7 @@ bool Meteor::Update(float fElapsedTime){
vf2d effectPos=vf2d{cos(randomAngle),sin(randomAngle)}*randomRange+meteorOffset;
game->AddEffect(std::make_unique<Effect>(effectPos,0,"circle.png",OnUpperLevel(),vf2d{util::random(2)+1,util::random(3)+1},util::random(3)+1,vf2d{util::random(10)-5,-util::random(20)-5},Pixel{255,uint8_t(randomColorTintG),uint8_t(randomColorTintB),uint8_t("Wizard.Ability 3.MeteorImpactParticleAlphaRange"_FRange)},0,0,true),effectPos.y<meteorOffset.y);
}
game->Hurt(pos,"Wizard.Ability 3.MeteorRadius"_F/100*24,int(game->GetPlayer()->GetAttack()*"Wizard.Ability 3.MeteorDamageMult"_F),OnUpperLevel(),0,HurtType::MONSTER);
game->Hurt(pos,"Wizard.Ability 3.MeteorRadius"_F/100*24,int(game->GetPlayer()->GetAttack()*"Wizard.Ability 3.MeteorDamageMult"_F),OnUpperLevel(),0,HurtType::MONSTER,HurtFlag::PLAYER_ABILITY);
game->AddEffect(std::make_unique<PulsatingFire>(pos,"Wizard.Ability 3.FireRingLifetime"_F,"fire_ring1.png",OnUpperLevel(),vf2d{"Wizard.Ability 3.MeteorRadius"_F/100*2,"Wizard.Ability 3.MeteorRadius"_F/100*2},"Wizard.Ability 3.FireRingFadeoutTime"_F),true);
SoundEffect::PlaySFX("Wizard Meteor",pos);
}

@ -601,11 +601,22 @@ const bool Monster::AttackAvoided(const float attackZ)const{
return HasIframes()||abs(GetZ()-attackZ)>1;
}
bool Monster::Hurt(int damage,bool onUpperLevel,float z){
return _Hurt(damage,onUpperLevel,z,TrueDamageFlag::NORMAL_DAMAGE);
bool Monster::Hurt(HurtDamageInfo damageData){
return _Hurt(damageData.damage,damageData.onUpperLevel,damageData.z,damageData.damageRule,damageData.hurtFlags);
}
bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag damageRule){
const bool TrueDamage=damageRule==TrueDamageFlag::IGNORE_DAMAGE_RULES;
bool Monster::Hurt(int damage,bool onUpperLevel,float z,HurtFlag::HurtFlag hurtFlags){
return _Hurt(damage,onUpperLevel,z,TrueDamageFlag::NORMAL_DAMAGE,hurtFlags);
}
bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag damageRule,HurtFlag::HurtFlag hurtFlags){
const bool TriggersMark{bool(hurtFlags&HurtFlag::PLAYER_ABILITY)};
const bool TrueDamage{damageRule==TrueDamageFlag::IGNORE_DAMAGE_RULES};
const bool IsDOT{bool(hurtFlags&HurtFlag::DOT)};
const bool NormalDamageCalculationRequired{!IsDOT&&!TrueDamage};
const bool PlayHitSoundEffect{!IsDOT};
if(!TrueDamage&&(Invulnerable()||!IsAlive()||onUpperLevel!=OnUpperLevel()||AttackAvoided(z)))return false;
if(game->InBossEncounter()){
game->StartBossEncounter();
@ -613,8 +624,9 @@ bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag da
game->GetPlayer()->ResetLastCombatTime();
float mod_dmg=float(damage);
bool crit{false};
if(NormalDamageCalculationRequired){
#pragma region Handle Crits
bool crit=false;
if(util::random(1)<game->GetPlayer()->GetCritRatePct()){
mod_dmg*=1+game->GetPlayer()->GetCritDmgPct();
crit=true;
@ -622,16 +634,24 @@ bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag da
#pragma endregion
mod_dmg-=mod_dmg*GetDamageReductionFromBuffs();
}
mod_dmg=std::ceil(mod_dmg);
if(TriggersMark&&GetMarkStacks()>0)Hurt(game->GetPlayer()->GetAttack()*"Trapper.Ability 1.Damage Increase Bonus"_F/100.f,OnUpperLevel(),GetZ(),HurtFlag::DOT);
if(TrueDamage){
mod_dmg=damage; //True damage override, ignore all damage changes.
crit=false; //True damage disables critting.
}
mod_dmg=std::ceil(mod_dmg);
hp=std::max(0,hp-int(mod_dmg));
if(IsDOT){
if(lastDotTimer>0){
dotNumberPtr.get()->damage+=int(mod_dmg);
dotNumberPtr.get()->RecalculateSize();
}else{
dotNumberPtr=std::make_shared<DamageNumber>(pos-vf2d{0,GetCollisionRadius()/2.f},int(mod_dmg),false,DamageNumberType::DOT);
DAMAGENUMBER_LIST.push_back(dotNumberPtr);
}
lastDotTimer=0.05f;
}else
if(lastHitTimer>0){
damageNumberPtr.get()->damage+=int(mod_dmg);
damageNumberPtr.get()->pauseTime=0.4f;
@ -642,7 +662,7 @@ bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag da
}
#pragma region Change Label to Crit
if(crit){
damageNumberPtr.get()->type=CRIT;
damageNumberPtr.get()->type=DamageNumberType::CRIT;
}
#pragma endregion
lastHitTimer=0.05f;
@ -650,12 +670,10 @@ bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag da
attackedByPlayer=true;
if(!IsAlive()){
OnDeath();
SoundEffect::PlaySFX(GetDeathSound(),GetPos());
}else{
hp=std::max(1,hp); //Make sure it stays alive if it's supposed to be alive...
if(monsterHurtSoundCooldown==0.f){
if(PlayHitSoundEffect&&monsterHurtSoundCooldown==0.f){
monsterHurtSoundCooldown=util::random(0.5f)+0.5f;
SoundEffect::PlaySFX(GetHurtSound(),GetPos());
}
@ -1150,8 +1168,8 @@ const bool Monster::IsSolid()const{
return Immovable();
}
void Monster::_DealTrueDamage(const uint32_t damageAmt){
_Hurt(damageAmt,OnUpperLevel(),GetZ(),TrueDamageFlag::IGNORE_DAMAGE_RULES);
void Monster::_DealTrueDamage(const uint32_t damageAmt,HurtFlag::HurtFlag hurtFlags){
_Hurt(damageAmt,OnUpperLevel(),GetZ(),TrueDamageFlag::IGNORE_DAMAGE_RULES,hurtFlags);
}
void Monster::Heal(const int healAmt){
@ -1177,3 +1195,12 @@ const float Monster::GetModdedStatBonuses(std::string_view stat)const{
const std::optional<geom2d::rect<float>>&Monster::GetRectangleCollision()const{
return MONSTER_DATA.at(GetName()).GetRectangleCollision();
}
const int Monster::GetMarkStacks()const{
const std::vector<Buff>&markBuffs{GetBuffs(BuffType::TRAPPER_MARK)};
int stackCount{};
for(const Buff&b:markBuffs){
stackCount+=b.intensity;
}
return stackCount;
}

@ -48,6 +48,7 @@ All rights reserved.
#include "TMXParser.h"
#include "MonsterData.h"
#include "Direction.h"
#include "HurtDamageInfo.h"
INCLUDE_ITEM_DATA
INCLUDE_MONSTER_DATA
@ -59,11 +60,6 @@ enum class Attribute;
class GameEvent;
enum class TrueDamageFlag{
NORMAL_DAMAGE,
IGNORE_DAMAGE_RULES, //Deals true damage, ignoring established invulnerability/iframe rules. Will never miss and will not have its damage modified by any buffs/stats.
};
namespace MonsterTests{
class MonsterTest;
};
@ -98,7 +94,10 @@ public:
bool Update(float fElapsedTime);
//Returns true when damage is actually dealt. Provide whether or not the attack is on the upper level or not. Monsters must be on the same level to get hit by it. (there is a death check and level check here.)
//If you need to hurt multiple enemies try AiL::HurtEnemies()
bool Hurt(int damage,bool onUpperLevel,float z);
bool Hurt(HurtDamageInfo damageData);
//Returns true when damage is actually dealt. Provide whether or not the attack is on the upper level or not. Monsters must be on the same level to get hit by it. (there is a death check and level check here.)
//If you need to hurt multiple enemies try AiL::HurtEnemies()
bool Hurt(int damage,bool onUpperLevel,float z,HurtFlag::HurtFlag hurtFlags=HurtFlag::NONE);
const bool IsAlive()const;
const vf2d&GetTargetPos()const;
Direction GetFacingDirection()const;
@ -195,11 +194,12 @@ public:
const bool HasArrowIndicator()const;
const bool ReachedTargetPos(const float maxDistanceFromTarget=4.f)const;
const float GetHealthRatio()const;
void _DealTrueDamage(const uint32_t damageAmt);
void _DealTrueDamage(const uint32_t damageAmt,HurtFlag::HurtFlag hurtFlags=HurtFlag::NONE);
void Heal(const int healAmt);
const float GetModdedStatBonuses(std::string_view stat)const;
//The collision rectangle is only used currently for determining the safe spots for the stone golem boss fight.
const std::optional<geom2d::rect<float>>&GetRectangleCollision()const;
const int GetMarkStacks()const; //Number of Trapper marks on this target.
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.
@ -240,8 +240,10 @@ private:
Pathfinding::sSpline path;
float pathIndex=0;
float lastHitTimer=0;
float lastDotTimer=0;
float spriteRot=0;
std::shared_ptr<DamageNumber>damageNumberPtr;
std::shared_ptr<DamageNumber>dotNumberPtr;
int phase=0;
bool diesNormally=true; //If set to false, the monster death is handled in a special way. Set it to true when it's time to die.
float targetSize=0;
@ -278,7 +280,7 @@ private:
float fadeTimer{0.f};
bool markedForDeletion{false}; //DO NOT MODIFY DIRECTLY. Use MarkForDeletion() if this monster needs to be marked. NOTE: Marking a monster for deletion does not trigger any death events. It just simply removes the monster from the field!!
float solidFadeTimer{0.f};
bool _Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag damageRule);
bool _Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag damageRule,HurtFlag::HurtFlag hurtFlags);
private:
struct STRATEGY{
static std::string ERR;

@ -188,7 +188,7 @@ void MonsterData::InitializeMonsterData(){
if(DATA["Monsters"][MonsterName].HasProperty("Invulnerable"))monster.invulnerable=DATA["Monsters"][MonsterName]["Invulnerable"].GetBool();
if(DATA["Monsters"][MonsterName].HasProperty("Lifetime"))monster.lifetime=DATA["Monsters"][MonsterName]["Lifetime"].GetReal();
monster.collisionRadius=8;
monster.collisionRadius=8*(std::min(DATA["Monsters"][MonsterName]["SheetFrameSize"].GetInt(0),DATA["Monsters"][MonsterName]["SheetFrameSize"].GetInt(1))/24.f);
if(DATA["Monsters"][MonsterName].HasProperty("Collision Radius"))monster.collisionRadius=DATA["Monsters"][MonsterName]["Collision Radius"].GetReal();
if(DATA["Monsters"][MonsterName].HasProperty("ShowBossIndicator"))monster.hasArrowIndicator=DATA["Monsters"][MonsterName]["ShowBossIndicator"].GetBool();

@ -63,8 +63,8 @@ namespace olc{
Pixel::Pixel(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha)
{ n = red | (green << 8) | (blue << 16) | (alpha << 24); } // Thanks jarekpelczar
Pixel::Pixel(uint32_t p)
{ n = p; }
Pixel::Pixel(uint32_t hex)
{ n=((hex&0xFF0000)>>16)|(hex&0x00FF00)|((hex&0x0000FF)<<16)|0xFF000000; }
bool Pixel::operator==(const Pixel& p) const
{ return n == p.n; }
@ -168,5 +168,11 @@ namespace olc{
Pixel PixelLerp(const olc::Pixel& p1, const olc::Pixel& p2, float t)
{ return (p2 * t) + p1 * (1.0f - t); }
Pixel PixelRaw(uint32_t n){
Pixel newPixel{};
newPixel.n=n;
return newPixel;
}
#endif
}

@ -72,7 +72,8 @@ namespace olc{
Pixel();
Pixel(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha = nDefaultAlpha);
Pixel(uint32_t p);
//Hex Implicit Constructor!! ALPHA IS ASSUMED TO BE 255! If you need access to modifying the raw value, use PixelRaw()
Pixel(uint32_t hex);
Pixel& operator = (const Pixel& v) = default;
bool operator ==(const Pixel& p) const;
bool operator !=(const Pixel& p) const;
@ -91,6 +92,7 @@ namespace olc{
Pixel PixelF(float red, float green, float blue, float alpha = 1.0f);
Pixel PixelLerp(const olc::Pixel& p1, const olc::Pixel& p2, float t);
Pixel PixelRaw(uint32_t n);
// O------------------------------------------------------------------------------O

@ -424,7 +424,7 @@ void Player::Update(float fElapsedTime){
spin_angle=0;
z=0;
float numb=4;
const HurtList&hitEnemies=game->Hurt(pos,"Warrior.Ability 2.Range"_F/100*12,int(GetAttack()*"Warrior.Ability 2.DamageMult"_F),OnUpperLevel(),0,HurtType::MONSTER);
const HurtList&hitEnemies=game->Hurt(pos,"Warrior.Ability 2.Range"_F/100*12,int(GetAttack()*"Warrior.Ability 2.DamageMult"_F),OnUpperLevel(),0,HurtType::MONSTER,HurtFlag::PLAYER_ABILITY);
#pragma region Knockback effect
for(auto&[targetPtr,wasHurt]:hitEnemies){
if(!std::holds_alternative<Monster*>(targetPtr))ERR("WARNING! A hurt request list was expecting only monster pointers, but got another type instead! THIS SHOULD NOT BE HAPPENING!");
@ -798,9 +798,24 @@ bool Player::HasIframes(){
return iframe_time>0;
}
bool Player::Hurt(int damage,bool onUpperLevel,float z){
if(!IsAlive()||HasIframes()||OnUpperLevel()!=onUpperLevel||abs(GetZ()-z)>1) return false;
bool Player::Hurt(HurtDamageInfo damageData){
return Hurt(damageData.damage,damageData.onUpperLevel,damageData.z,damageData.damageRule,damageData.hurtFlags);
}
bool Player::Hurt(int damage,bool onUpperLevel,float z,HurtFlag::HurtFlag hurtFlags){
return Hurt(damage,onUpperLevel,z,TrueDamageFlag::NORMAL_DAMAGE,hurtFlags);
}
bool Player::Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag damageRule,HurtFlag::HurtFlag hurtFlags){
const bool IsDOT{bool(hurtFlags&HurtFlag::DOT)};
const bool TrueDamage{damageRule==TrueDamageFlag::IGNORE_DAMAGE_RULES};
const bool NormalDamageCalculationRequired{!IsDOT&&!TrueDamage};
const bool PlayHitSoundEffect{!IsDOT};
if(!TrueDamage&&(!IsAlive()||HasIframes()||OnUpperLevel()!=onUpperLevel||abs(GetZ()-z)>1))return false;
float mod_dmg=float(damage);
if(NormalDamageCalculationRequired){
if(GetState()==State::BLOCK){
mod_dmg=0;
SoundEffect::PlaySFX("Warrior Block Hit",SoundEffect::CENTERED);
@ -824,11 +839,14 @@ bool Player::Hurt(int damage,bool onUpperLevel,float z){
float dmgRoll=minPctDmgReduction+util::random(pctDmgReductionDiff);
mod_dmg*=1-dmgRoll;
}
}
mod_dmg=std::ceil(mod_dmg);
SoundEffect::PlaySFX("Player Hit",SoundEffect::CENTERED);
}
if(PlayHitSoundEffect)SoundEffect::PlaySFX("Player Hit",SoundEffect::CENTERED);
if(Menu::IsMenuOpen()&&mod_dmg>0)Menu::CloseAllMenus();
if(mod_dmg>0)game->ShowDamageVignetteOverlay();
@ -841,6 +859,16 @@ bool Player::Hurt(int damage,bool onUpperLevel,float z){
Input::StartVibration();
Input::SetLightbar(PixelLerp(DARK_RED,GREEN,GetHealthRatio()));
if(IsDOT){
if(lastDotTimer>0){
dotNumberPtr.get()->damage+=int(mod_dmg);
dotNumberPtr.get()->RecalculateSize();
}else{
dotNumberPtr=std::make_shared<DamageNumber>(pos-vf2d{0,8.f},int(mod_dmg),false,DamageNumberType::DOT);
DAMAGENUMBER_LIST.push_back(dotNumberPtr);
}
lastDotTimer=0.05f;
}else
if(lastHitTimer>0){
damageNumberPtr.get()->damage+=int(mod_dmg);
damageNumberPtr.get()->pauseTime=0.4f;
@ -895,7 +923,7 @@ void Player::CancelCast(){
castInfo={"",0};
std::erase_if(buffList,[](Buff&b){return b.type==RESTORATION_DURING_CAST;}); //Remove all buffs that would be applied during a cast, as we got interrupted.
if(wasCasting){
DAMAGENUMBER_LIST.push_back(std::make_shared<DamageNumber>(GetPos(),0,true,INTERRUPT));
DAMAGENUMBER_LIST.push_back(std::make_shared<DamageNumber>(GetPos(),0,true,DamageNumberType::INTERRUPT));
}
if(state==State::CASTING){
state=State::NORMAL;
@ -1115,7 +1143,7 @@ void Player::_SetIframes(float duration){
bool Player::Heal(int damage,bool suppressDamageNumber){
hp=std::clamp(hp+damage,0,int(GetMaxHealth()));
if(!suppressDamageNumber&&damage>0){
DAMAGENUMBER_LIST.push_back(std::make_shared<DamageNumber>(GetPos(),damage,true,HEALTH_GAIN));
DAMAGENUMBER_LIST.push_back(std::make_shared<DamageNumber>(GetPos(),damage,true,DamageNumberType::HEALTH_GAIN));
}
Input::SetLightbar(PixelLerp(DARK_RED,GREEN,GetHealthRatio()));
return true;
@ -1124,7 +1152,7 @@ bool Player::Heal(int damage,bool suppressDamageNumber){
void Player::RestoreMana(int amt,bool suppressDamageNumber){
mana=std::clamp(mana+amt,0,GetMaxMana());
if(amt>0&&!suppressDamageNumber){
DAMAGENUMBER_LIST.push_back(std::make_shared<DamageNumber>(GetPos(),amt,true,MANA_GAIN));
DAMAGENUMBER_LIST.push_back(std::make_shared<DamageNumber>(GetPos(),amt,true,DamageNumberType::MANA_GAIN));
}
}

@ -194,7 +194,9 @@ public:
void RecalculateEquipStats();
bool Hurt(int damage,bool onUpperLevel,float z);
bool Hurt(HurtDamageInfo damageData);
bool Hurt(int damage,bool onUpperLevel,float z,HurtFlag::HurtFlag hurtFlags=HurtFlag::NONE);
bool Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag damageRule,HurtFlag::HurtFlag hurtFlags=HurtFlag::NONE);
//Return false if healing was not possible.
bool Heal(int damage,bool suppressDamageNumber=false);
//specificClass is a bitwise-combination of classes from the Class enum. It makes sure certain animations only play if you are a certain class.=
@ -324,7 +326,8 @@ private:
CastInfo castInfo={"",0};
vf2d movementVelocity={};//This tells us if the player is moving (mostly controlled by user input) since their velocity is not used for regular movement.
float lastHitTimer=0; //When this is greater than zero, if we get hit again it adds to our displayed combo number.
std::shared_ptr<DamageNumber>damageNumberPtr;
float lastDotTimer=0; //When this is greater than zero, if we get hit again it adds to our displayed combo number.
std::shared_ptr<DamageNumber>damageNumberPtr,dotNumberPtr;
void Initialize();
float iframe_time=0;
float lastCombatTime=0;

@ -46,7 +46,7 @@ SwordSlash::SwordSlash(float lifetime, std::string imgFile,float damageMult, flo
bool SwordSlash::Update(float fElapsedTime){
if(lifetime>0){
game->HurtConeNotHit(game->GetPlayer()->GetPos(),game->GetPlayer()->GetAttackRangeMult()*12.f,rotation,util::degToRad(swordSweepAngle),game->GetPlayer()->GetAttack()*damageMult,hitList,game->GetPlayer()->OnUpperLevel(),game->GetPlayer()->GetZ(),HurtType::MONSTER);
game->HurtConeNotHit(game->GetPlayer()->GetPos(),game->GetPlayer()->GetAttackRangeMult()*12.f,rotation,util::degToRad(swordSweepAngle),game->GetPlayer()->GetAttack()*damageMult,hitList,game->GetPlayer()->OnUpperLevel(),game->GetPlayer()->GetZ(),HurtType::MONSTER,HurtFlag::PLAYER_ABILITY);
}
pos=game->GetPlayer()->GetPos();

@ -156,7 +156,7 @@ void Thief::InitializeClassAbilities(){
#pragma region Thief Ability 3 (Adrenaline Rush)
Thief::ability3.action=
[](Player*p,vf2d pos={}){
SoundEffect::PlaySFX("Adrenaline Rush",p->GetPos());
SoundEffect::PlaySFX("Adrenaline Rush",SoundEffect::CENTERED);
p->AddBuff(BuffType::ADRENALINE_RUSH,"Thief.Ability 3.Duration"_F,0.f);
for(int i:std::ranges::iota_view(0,50)){
float size{util::random_range(0.4f,0.8f)};

@ -76,3 +76,5 @@ BulletDestroyState Tornado::MonsterHit(Monster&monster){
return BulletDestroyState::KEEP_ALIVE;
}
void Tornado::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){}

@ -41,6 +41,10 @@ All rights reserved.
#include "Effect.h"
#include "AdventuresInLestoria.h"
#include "config.h"
#include "SoundEffect.h"
#include "BulletTypes.h"
#include <ranges>
#include "util.h"
INCLUDE_MONSTER_LIST
INCLUDE_BULLET_LIST
@ -65,19 +69,35 @@ void Trapper::OnUpdate(float fElapsedTime){
}
bool Trapper::AutoAttack(){
return false;
geom2d::line pointTowardsCursor(GetPos(),GetWorldAimingLocation());
vf2d extendedLine=pointTowardsCursor.upoint(1.1f);
float angleToCursor=atan2(extendedLine.y-GetPos().y,extendedLine.x-GetPos().x);
attack_cooldown_timer=ARROW_ATTACK_COOLDOWN-GetAttackRecoveryRateReduction();
CreateBullet(Arrow)(GetPos(),extendedLine,vf2d{cos(angleToCursor)*"Ranger.Auto Attack.ArrowSpd"_F,float(sin(angleToCursor)*"Ranger.Auto Attack.ArrowSpd"_F-PI/8*"Ranger.Auto Attack.ArrowSpd"_F)}+movementVelocity/1.5f,"Ranger.Auto Attack.Radius"_F,int(GetAttack()*"Ranger.Auto Attack.DamageMult"_F),OnUpperLevel(),true)EndBullet;
BULLET_LIST.back()->SetIsPlayerAutoAttackProjectile();
SetState(State::SHOOT_ARROW);
SetAnimationBasedOnTargetingDirection("SHOOT",angleToCursor);
SoundEffect::PlaySFX("Ranger.Auto Attack.Sound"_S,SoundEffect::CENTERED);
return true;
}
void Trapper::InitializeClassAbilities(){
#pragma region Trapper Right-click Ability (???)
#pragma region Trapper Right-click Ability (Sprint)
Trapper::rightClickAbility.action=
[](Player*p,vf2d pos={}){
return false;
SoundEffect::PlaySFX("Adrenaline Rush",SoundEffect::CENTERED);
p->AddBuff(BuffType::SPEEDBOOST,"Trapper.Right Click Ability.Movement Speed Buff"_f[1],"Trapper.Right Click Ability.Movement Speed Buff"_f[0]/100.f);
for(int i:std::ranges::iota_view(0,50)){
float size{util::random_range(0.4f,0.8f)};
game->AddEffect(std::make_unique<Effect>(p->GetPos()+vf2d{8,util::random(2*PI)}.cart(),util::random_range(0.1f,0.4f),"circle.png",p->OnUpperLevel(),vf2d{size,size},0.3f,vf2d{util::random_range(-6.f,6.f),util::random_range(-8.f,-1.f)},PixelLerp(BLACK,RED,util::random(1))));
}
return true;
};
#pragma endregion
#pragma region Trapper Ability 1 (???)
#pragma region Trapper Ability 1 (Mark Target)
Trapper::ability1.action=
[](Player*p,vf2d pos={}){
return false;
return true;
};
#pragma endregion
#pragma region Trapper Ability 2 (???)

@ -62,7 +62,7 @@ void Monster::STRATEGY::URSULE(Monster&m,float fElapsedTime,std::string strategy
#pragma region Setup On Death Function
m.SetStrategyDeathFunction([&](GameEvent&event,Monster&m,const std::string&strategy){
if(!m.B(A::BULLETS_REMOVED)){
for(const std::unique_ptr<Bullet>&b:BULLET_LIST){
for(const std::unique_ptr<IBullet>&b:BULLET_LIST){
b->fadeOutTime=ConfigFloat("Phase 4.End Wisp Fadeout Time");
}
m.B(A::BULLETS_REMOVED)=true;

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

@ -60,3 +60,5 @@ BulletDestroyState Wisp::MonsterHit(Monster&monster){
fadeOutTime="MonsterStrategy.Ursule.Phase 2.Wisp Fadeout Time"_F;
return BulletDestroyState::KEEP_ALIVE;
}
void Wisp::ModifyOutgoingDamageData(int&damage,bool&onUpperLevel,float&z,HurtFlag::HurtFlag&hurtFlags){}

@ -96,7 +96,7 @@ void Monster::STRATEGY::ZEPHY(Monster&m,float fElapsedTime,std::string strategy)
game->SetWindSpeed({});
game->GetOverlay().Disable();
std::for_each(BULLET_LIST.begin(),BULLET_LIST.end(),[](const std::unique_ptr<Bullet>&bullet){
std::for_each(BULLET_LIST.begin(),BULLET_LIST.end(),[](const std::unique_ptr<IBullet>&bullet){
if(!bullet->friendly){ //Forces all bullets at the end of a fight for the boss to be completely nullified.
bullet->fadeOutTime=0.5f;
}
@ -182,8 +182,8 @@ void Monster::STRATEGY::ZEPHY(Monster&m,float fElapsedTime,std::string strategy)
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
m.F(A::SHOOT_TIMER)-=fElapsedTime;
if(m.F(A::SHOOT_TIMER)<=0.f){
CreateBullet(Bullet)(m.GetPos(),vf2d{0.f,ConfigFloat("Fly Across Attack.Attack Y Speed")},4,ConfigInt("Fly Across Attack.Poop Damage"),"birdpoop.png",m.OnUpperLevel(),false,INFINITY,false,false,WHITE,vf2d{1.f,1.25f})
.SetIframeTimeOnHit(0.25f)EndBullet;
CreateBullet(Bullet)(m.GetPos(),vf2d{0.f,ConfigFloat("Fly Across Attack.Attack Y Speed")},4,ConfigInt("Fly Across Attack.Poop Damage"),"birdpoop.png",m.OnUpperLevel(),false,INFINITY,false,false,WHITE,vf2d{1.f,1.25f})EndBullet;
BULLET_LIST.back()->SetIframeTimeOnHit(0.25f);
const int extraPoopBitsCount=util::random()%6;
for(int i=0;i<extraPoopBitsCount;i++){
const bool RightDirection=util::random()%2;
@ -191,8 +191,8 @@ void Monster::STRATEGY::ZEPHY(Monster&m,float fElapsedTime,std::string strategy)
if(RightDirection)xOffset=1.f;
else xOffset=-1.f;
xOffset*=9.f;
CreateBullet(Bullet)(m.GetPos()+vf2d{xOffset+util::random_range(-3.f,3.f),-util::random(10.f)-4.f},vf2d{0.f,ConfigFloat("Fly Across Attack.Attack Y Speed")},1,ConfigInt("Fly Across Attack.Poop Damage"),"birdpoop.png",m.OnUpperLevel(),false,INFINITY,false,false,WHITE,vf2d{util::random_range(0.2f,0.3f),util::random_range(0.2f,0.3f)},util::random(2*PI))
.SetIframeTimeOnHit(0.25f)EndBullet;
CreateBullet(Bullet)(m.GetPos()+vf2d{xOffset+util::random_range(-3.f,3.f),-util::random(10.f)-4.f},vf2d{0.f,ConfigFloat("Fly Across Attack.Attack Y Speed")},1,ConfigInt("Fly Across Attack.Poop Damage"),"birdpoop.png",m.OnUpperLevel(),false,INFINITY,false,false,WHITE,vf2d{util::random_range(0.2f,0.3f),util::random_range(0.2f,0.3f)},util::random(2*PI))EndBullet;
BULLET_LIST.back()->SetIframeTimeOnHit(0.25f);
}
m.F(A::SHOOT_TIMER)=ConfigFloat("Fly Across Attack.Attack Frequency");
}
@ -349,7 +349,7 @@ void Monster::STRATEGY::ZEPHY(Monster&m,float fElapsedTime,std::string strategy)
m.PerformAnimation("ATTACK");
if(game->BossEncounterMobCount()==1){
m.phase=IDLE;
std::for_each(BULLET_LIST.begin(),BULLET_LIST.end(),[](std::unique_ptr<Bullet>&bullet){
std::for_each(BULLET_LIST.begin(),BULLET_LIST.end(),[](std::unique_ptr<IBullet>&bullet){
if(bullet->GetBulletType()==BulletType::LARGE_TORNADO){
bullet->fadeOutTime=1.f;
}else if(bullet->GetBulletType()==BulletType::FEATHER){

@ -16,7 +16,7 @@ Trapper
{
DamageMult = 1x
Radius = 12
Cooldown = 0.6
Cooldown = 0.6s
# Whether or not this ability cancels casts.
CancelCast = 0
@ -31,7 +31,7 @@ Trapper
Short Name = SPRINT
Description = Gain 60% bonus movement speed for 3 seconds.
Icon = sprint.png
Cooldown = 7
Cooldown = 7s
Mana Cost = 0
# Whether or not this ability cancels casts.
CancelCast = 0

Loading…
Cancel
Save