Implement rock toss and stone pillar spawning behaviors for stone elemental. Refactor HurtEnemies functions to instead hurt any target with targeting flags. Fix bug with player to monster collision not respecting new collision radii. Release Build 9318.

mac-build
sigonasr2 9 months ago
parent 9a9cbe765e
commit 1d5d4d8240
  1. 4
      Adventures in Lestoria/Adventures in Lestoria.vcxproj
  2. 98
      Adventures in Lestoria/AdventuresInLestoria.cpp
  3. 15
      Adventures in Lestoria/AdventuresInLestoria.h
  4. 6
      Adventures in Lestoria/Bomb.cpp
  5. 6
      Adventures in Lestoria/Bullet.cpp
  6. 2
      Adventures in Lestoria/Bullet.h
  7. 23
      Adventures in Lestoria/BulletTypes.h
  8. 4
      Adventures in Lestoria/Effect.h
  9. 4
      Adventures in Lestoria/FireBolt.cpp
  10. 114
      Adventures in Lestoria/LevitatingRock.cpp
  11. 2
      Adventures in Lestoria/Meteor.cpp
  12. 7
      Adventures in Lestoria/Monster.cpp
  13. 3
      Adventures in Lestoria/Monster.h
  14. 6
      Adventures in Lestoria/MonsterData.cpp
  15. 2
      Adventures in Lestoria/MonsterData.h
  16. 26
      Adventures in Lestoria/Player.cpp
  17. 4
      Adventures in Lestoria/Player.h
  18. 2
      Adventures in Lestoria/PulsatingFire.cpp
  19. 51
      Adventures in Lestoria/Stone_Elemental.cpp
  20. 2
      Adventures in Lestoria/SwordSlash.cpp
  21. 2
      Adventures in Lestoria/Version.h
  22. 11
      Adventures in Lestoria/assets/config/MonsterStrategies.txt
  23. 2
      Adventures in Lestoria/assets/config/gfx/gfx.txt
  24. BIN
      Adventures in Lestoria/assets/gamepack.pak
  25. BIN
      Adventures in Lestoria/assets/rock_outline.png
  26. BIN
      x64/Release/Adventures in Lestoria.exe

@ -771,6 +771,10 @@
<ClCompile Include="ItemLoadoutWindow.cpp" />
<ClCompile Include="Key.cpp" />
<ClCompile Include="LevelCompleteWindow.cpp" />
<ClCompile Include="LevitatingRock.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="LightningBolt.cpp" />
<ClCompile Include="LightningBoltEmitter.cpp" />
<ClCompile Include="LoadGameWindow.cpp">

@ -736,39 +736,79 @@ void AiL::UpdateBullets(float fElapsedTime){
}
std::erase_if(BULLET_LIST,[](std::unique_ptr<Bullet>&b){return b->IsDead();});
}
const MonsterHurtList AiL::HurtEnemies(vf2d pos,float radius,int damage,bool upperLevel,float z)const{
MonsterHurtList hitList;
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
if(geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(m->GetPos(),12*m->GetSizeMult()))){
HurtReturnValue returnVal=m->Hurt(damage,upperLevel,z);
hitList.push_back({&*m,returnVal});
const HurtList AiL::Hurt(vf2d pos,float radius,int damage,bool upperLevel,float z,const HurtType hurtTargets)const{
HurtList hitList;
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()))){
HurtReturnValue returnVal=m->Hurt(damage,upperLevel,z);
hitList.push_back({&*m,returnVal});
}
}
}
if(CheckForPlayerCollisions){
if(geom2d::overlaps(geom2d::circle(pos,radius),geom2d::circle(GetPlayer()->GetPos(),12*GetPlayer()->GetSizeMult()))){
HurtReturnValue returnVal=GetPlayer()->Hurt(damage,upperLevel,z);
hitList.push_back({GetPlayer(),returnVal});
}
}
return hitList;
}
const MonsterHurtList AiL::HurtEnemiesNotHit(vf2d pos,float radius,int damage,HitList&hitList,bool upperLevel,float z){
MonsterHurtList affectedList;
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);
affectedList.push_back({&*m,returnVal});
hitList.insert(&*m);
const HurtList AiL::HurtNotHit(vf2d pos,float radius,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;
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);
affectedList.push_back({&*m,returnVal});
hitList.insert(&*m);
}
}
}
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);
affectedList.push_back({GetPlayer(),returnVal});
hitList.insert(GetPlayer());
}
}
return affectedList;
}
const MonsterHurtList AiL::HurtEnemiesConeNotHit(vf2d pos,float radius,float angle,float sweepAngle,int damage,HitList&hitList,bool upperLevel,float z){
MonsterHurtList affectedList;
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);
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;
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(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=m->Hurt(damage,upperLevel,z);
affectedList.push_back({&*m,returnVal});
hitList.insert(&*m);
HurtReturnValue returnVal=GetPlayer()->Hurt(damage,upperLevel,z);
affectedList.push_back({GetPlayer(),returnVal});
hitList.insert(GetPlayer());
}
}
}
@ -1339,12 +1379,12 @@ void AiL::RenderWorld(float fElapsedTime){
m->strategyDraw(this,*m,MONSTER_DATA[m->GetName()].GetAIStrategy());
m->Draw();
}
for(const Bullet*const b:bulletsLower){
b->Draw();
}
for(const Effect*const e:backgroundEffectsLower){
e->Draw();
}
for(const Bullet*const b:bulletsLower){
b->Draw();
}
for(const int dropInd:dropsBeforeLower){
ItemDrop::drops[dropInd].Draw();
}
@ -1587,12 +1627,12 @@ void AiL::RenderWorld(float fElapsedTime){
m->strategyDraw(this,*m,MONSTER_DATA[m->GetName()].GetAIStrategy());
m->Draw();
}
for(const Bullet*const b:bulletsUpper){
b->Draw();
}
for(const Effect*const e:backgroundEffectsUpper){
e->Draw();
}
for(const Bullet*const b:bulletsUpper){
b->Draw();
}
for(const int dropInd:dropsBeforeUpper){
ItemDrop::drops[dropInd].Draw();
}
@ -1695,7 +1735,7 @@ void AiL::RenderWorld(float fElapsedTime){
#endif
}
Player*AiL::GetPlayer(){
Player*const AiL::GetPlayer()const{
return player.get();
}

@ -66,7 +66,12 @@ class SteamStatsReceivedHandler;
#define EndBullet ));
using HurtReturnValue=bool;
using MonsterHurtList=std::vector<std::pair<Monster*,HurtReturnValue>>;
using HurtList=std::vector<std::pair<std::variant<Monster*,Player*>,HurtReturnValue>>;
enum class HurtType{
PLAYER=0b01,
MONSTER=0b10,
};
class AiL : public olc::PixelGameEngine
{
@ -227,13 +232,13 @@ 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 MonsterHurtList HurtEnemies(vf2d pos,float radius,int damage,bool upperLevel,float z)const;
const HurtList Hurt(vf2d pos,float radius,int damage,bool upperLevel,float z,const HurtType hurtTargets)const;
//NOTE: This function will also add any enemies that were hit into the hit list!
const MonsterHurtList HurtEnemiesNotHit(vf2d pos,float radius,int damage,HitList&hitList,bool upperLevel,float z);
const HurtList HurtNotHit(vf2d pos,float radius,int damage,HitList&hitList,bool upperLevel,float z,const HurtType hurtTargets)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 MonsterHurtList HurtEnemiesConeNotHit(vf2d pos,float radius,float angle,float sweepAngle,int damage,HitList&hitList,bool upperLevel,float z);
const HurtList HurtConeNotHit(vf2d pos,float radius,float angle,float sweepAngle,int damage,HitList&hitList,bool upperLevel,float z,const HurtType hurtTargets)const;
vf2d GetWorldMousePos();
bool LeftHeld();
bool RightHeld();
@ -247,7 +252,7 @@ public:
bool RightReleased();
bool UpReleased();
bool DownReleased();
Player*GetPlayer();
Player*const GetPlayer()const;
void SetupWorldShake(float duration);
//tileID is the tile number from the tilesets.
bool IsForegroundTile(TilesheetData sheet,int tileID);

@ -71,8 +71,10 @@ void Bomb::Update(float fElapsedTime){
SoundEffect::PlaySFX("Bomb Explode",pos);
float distToPlayer=geom2d::line<float>(pos,game->GetPlayer()->GetPos()).length();
if(friendly){
const MonsterHurtList hurtEnemies=game->HurtEnemies(pos,radius,damage,OnUpperLevel(),z);
for(auto&[monsterPtr,wasHit]:hurtEnemies){
const HurtList hurtEnemies=game->Hurt(pos,radius,damage,OnUpperLevel(),z,HurtType::MONSTER);
for(auto&[targetPtr,wasHit]:hurtEnemies){
if(!std::holds_alternative<Monster*>(targetPtr))ERR("WARNING! Hurt enemies list returned a non-monster pointer!? THIS SHOULD NOT BE HAPPENING!");
Monster*monsterPtr=std::get<Monster*>(targetPtr);
if(wasHit)monsterPtr->ProximityKnockback(pos,bombKnockbackFactor);
}
if(distToPlayer<=radius){

@ -159,10 +159,10 @@ void Bullet::Draw()const{
}
if(animated){
game->view.DrawPartialRotatedDecal(pos-vf2d{0,GetZ()},GetFrame().GetSourceImage()->Decal(),rotates?atan2(vel.y,vel.x)-PI/2:0,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,scale,blendCol);
game->view.DrawPartialRotatedDecal(pos-vf2d{0,GetZ()}+drawOffsetY,GetFrame().GetSourceImage()->Decal(),rotates?atan2(vel.y,vel.x)-PI/2:0,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,scale,blendCol);
}else{
game->view.DrawDecal(pos-vf2d{0,GetZ()}-GFX["circle.png"].Sprite()->Size()*scale/2,GFX["circle.png"].Decal(),scale,blendCol);
game->view.DrawDecal(pos-vf2d{0,GetZ()}-GFX["circle.png"].Sprite()->Size()*scale/2,GFX["circle_outline.png"].Decal(),scale,Pixel{WHITE.r,WHITE.g,WHITE.b,blendCol.a});
game->view.DrawDecal(pos-vf2d{0,GetZ()}+drawOffsetY-GFX["circle.png"].Sprite()->Size()*scale/2,GFX["circle.png"].Decal(),scale,blendCol);
game->view.DrawDecal(pos-vf2d{0,GetZ()}+drawOffsetY-GFX["circle.png"].Sprite()->Size()*scale/2,GFX["circle_outline.png"].Decal(),scale,Pixel{WHITE.r,WHITE.g,WHITE.b,blendCol.a});
}
}

@ -66,6 +66,8 @@ private:
virtual void Update(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
protected:
float drawOffsetY{};
public:
Animate2D::Animation<std::string>animation;
Animate2D::AnimationState internal_animState;

@ -161,4 +161,27 @@ struct Bomb:public Bullet{
bool PlayerHit(Player*player)override;
bool MonsterHit(Monster&monster)override;
void Draw()const override;
};
struct LevitatingRock:public Bullet{
const Monster&attachedTarget;
const vf2d&attackingTarget;
float facingRotOffset;
float dist;
float lockOnTime;
float initialWaitTime;
float flyingSpd;
float fadeInTime;
const float fadeInOriginalTime;
std::optional<vf2d>targetVel;
std::vector<LevitatingRock*>slaveRocks;
//The rock will rotate around the attachedTarget towards the attackingTarget. The facingRotOffset determines the angle relative to the direction pointed towards this rock will be positioned at. Once the wait time expires, the rock is fired at target speed in that given direction.
//The lock on time is how long the rocks will follow the player. The wait time is how long to wait until firing.
LevitatingRock(const Monster&attachedTarget,const vf2d&attackingTarget,const float fadeInTime,const float facingRotOffset,const float distance,const float lockOnTime,const float waitTime,const float targetSpd,const float radius,const int damage,const bool upperLevel,const bool friendly=false,const Pixel col=WHITE,const vf2d scale={1,1});
void Update(float fElapsedTime)override;
bool PlayerHit(Player*player)override;
bool MonsterHit(Monster&monster)override;
void Draw()const override;
void AssignMaster(LevitatingRock*masterRock);
const bool IsMaster()const;
};

@ -38,8 +38,10 @@ All rights reserved.
#pragma once
#include "Animation.h"
#include <unordered_set>
#include <variant>
class Monster;
using HitList=std::unordered_set<Monster*>;
class Player;
using HitList=std::unordered_set<std::variant<Monster*,Player*>>;
struct Effect{
friend class AiL;

@ -63,7 +63,7 @@ void FireBolt::Update(float fElapsedTime){
}
game->SetupWorldShake("Wizard.Ability 1.WorldShakeTime"_F);
if(friendly){
game->HurtEnemies(pos,"Wizard.Ability 1.BulletHitExplosionRange"_F/100*12,int("Wizard.Ability 1.BulletHitExplosionDamageMult"_F*game->GetPlayer()->GetAttack()),OnUpperLevel(),0);
game->Hurt(pos,"Wizard.Ability 1.BulletHitExplosionRange"_F/100*12,int("Wizard.Ability 1.BulletHitExplosionDamageMult"_F*game->GetPlayer()->GetAttack()),OnUpperLevel(),0,HurtType::MONSTER);
}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);
@ -97,7 +97,7 @@ bool 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->HurtEnemies(monster.GetPos(),"Wizard.Ability 1.BulletHitExplosionRange"_F/100*12,int("Wizard.Ability 1.BulletHitExplosionDamageMult"_F*game->GetPlayer()->GetAttack()),OnUpperLevel(),0);
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->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);

@ -0,0 +1,114 @@
#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 "BulletTypes.h"
#include "AdventuresInLestoria.h"
#include "util.h"
INCLUDE_game
INCLUDE_GFX
LevitatingRock::LevitatingRock(const Monster&attachedTarget,const vf2d&attackingTarget,const float fadeInTime,const float facingRotOffset,const float distance,const float lockOnTime,const float waitTime,const float targetSpd,const float radius,const int damage,const bool upperLevel,const bool friendly,const Pixel col,const vf2d scale)
:attachedTarget(attachedTarget),attackingTarget(attackingTarget),fadeInTime(fadeInTime),fadeInOriginalTime(fadeInTime),facingRotOffset(facingRotOffset),dist(distance),lockOnTime(lockOnTime),initialWaitTime(waitTime),flyingSpd(targetSpd),
Bullet(attachedTarget.GetPos(),{},radius,damage,"rock.png",upperLevel,false,INFINITE,false,friendly,col){}
void LevitatingRock::Update(float fElapsedTime){
if(attachedTarget.IsDead()){
if(fadeOutTime==0.f){
fadeOutTime=1.5f;
col.r/=4;
col.g/=4;
col.b/=4;
}
return;
}
lockOnTime-=fElapsedTime;
initialWaitTime-=fElapsedTime;
fadeInTime=std::max(0.f,fadeInTime-fElapsedTime);
col.a=uint8_t(util::lerp(255.f,0.f,fadeInTime/fadeInOriginalTime));
if(lockOnTime>0.f){
vf2d normalizedVec=util::pointTo(attachedTarget.GetPos(),attackingTarget).polar();
normalizedVec.x=dist;
normalizedVec.y+=facingRotOffset;
pos=attachedTarget.GetPos()+normalizedVec.cart();
}else
if(!targetVel.has_value()&&IsMaster()){
targetVel=util::pointTo(attachedTarget.GetPos(),pos)*flyingSpd;
for(auto&rock:slaveRocks){
rock->targetVel=targetVel;
}
}
const bool RocksHaveLaunched=initialWaitTime<=0.f;
if(!RocksHaveLaunched){
deactivated=true;
drawOffsetY=cos(PI*game->GetRuntime())*3.f;
}
else if(targetVel.has_value()){
vel=targetVel.value();
deactivated=false;
}else ERR(std::format("WARNING! Levitating Rock does not have a target velocity! It did not get assigned somehow! Is Master: {}. THIS SHOULD NOT BE HAPPENING!",IsMaster()))
}
bool LevitatingRock::PlayerHit(Player*player){
if(initialWaitTime>0.f)return false;
deactivated=true;
fadeOutTime=0.5f;
return true;
}
bool LevitatingRock::MonsterHit(Monster&monster){
if(initialWaitTime>0.f)return false;
deactivated=true;
fadeOutTime=0.5f;
}
void LevitatingRock::Draw()const{
Bullet::Draw();
game->view.DrawRotatedDecal(pos-vf2d{0,GetZ()}+drawOffsetY,GFX["rock_outline.png"].Decal(),rotates?atan2(vel.y,vel.x)-PI/2:0,GFX["rock_outline.png"].Sprite()->Size()/2,scale,col);
}
void LevitatingRock::AssignMaster(LevitatingRock*masterRock){
masterRock->slaveRocks.push_back(this);
}
const bool LevitatingRock::IsMaster()const{
return slaveRocks.size()>0;
}

@ -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->HurtEnemies(pos,"Wizard.Ability 3.MeteorRadius"_F/100*24,int(game->GetPlayer()->GetAttack()*"Wizard.Ability 3.MeteorDamageMult"_F),OnUpperLevel(),0);
game->Hurt(pos,"Wizard.Ability 3.MeteorRadius"_F/100*24,int(game->GetPlayer()->GetAttack()*"Wizard.Ability 3.MeteorDamageMult"_F),OnUpperLevel(),0,HurtType::MONSTER);
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);
}

@ -372,6 +372,7 @@ Direction Monster::GetFacingDirection()const{
}
void Monster::UpdateFacingDirection(vf2d facingTargetPoint){
if(Immovable())return;
float facingAngle=util::angleTo(GetPos(),facingTargetPoint);
vf2d diff=GetPos()-facingTargetPoint;
@ -636,7 +637,7 @@ bool Monster::Hurt(int damage,bool onUpperLevel,float z){
return true;
}
bool Monster::IsAlive(){
const bool Monster::IsAlive()const{
return hp>0||!diesNormally;
}
vf2d&Monster::GetTargetPos(){
@ -1042,4 +1043,8 @@ const float Monster::GetCollisionRadius()const{
void Monster::MarkForDeletion(){
markedForDeletion=true;
}
const bool Monster::IsDead()const{
return !IsAlive();
}

@ -90,7 +90,7 @@ public:
//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 IsAlive();
const bool IsAlive()const;
vf2d&GetTargetPos();
Direction GetFacingDirection()const;
//Will make the monster face in the correct direction relative to a given target point to look at.
@ -171,6 +171,7 @@ public:
const std::optional<float>GetTotalLifetime()const;
//If an object has a collision radius, returns it.
const float GetCollisionRadius()const;
const bool IsDead()const;
private:
//NOTE: Marking a monster for deletion does not trigger any death events. It just simply removes the monster from the field!!
// The way this works is that monsters marked for deletion will cause the monster update loop to detect there's at least one or more monsters that must be deleted and will call erase_if on the list at the end of the iteration loop.

@ -171,7 +171,7 @@ void MonsterData::InitializeMonsterData(){
if(DATA["Monsters"][MonsterName].HasProperty("Lifetime"))monster.lifetime=DATA["Monsters"][MonsterName]["Lifetime"].GetReal();
monster.collisionRadius=12*monster.GetSizeMult()/2.f;
if(DATA["Monsters"][MonsterName].HasProperty("Collision Radius"))monster.collisionRadius=DATA["Monsters"][MonsterName]["Collision Radius"].GetBool();
if(DATA["Monsters"][MonsterName].HasProperty("Collision Radius"))monster.collisionRadius=DATA["Monsters"][MonsterName]["Collision Radius"].GetReal();
if(hasFourWaySpriteSheet)monster.SetUsesFourWaySprites();
@ -331,7 +331,7 @@ int MonsterData::GetAttack(){
float MonsterData::GetMoveSpdMult(){
return moveSpd;
}
float MonsterData::GetSizeMult(){
float MonsterData::GetSizeMult()const{
return size;
}
int MonsterData::GetCollisionDmg(){
@ -421,5 +421,5 @@ const std::optional<float>MonsterData::GetLifetime()const{
return lifetime;
}
const float MonsterData::GetCollisionRadius()const{
return collisionRadius;
return collisionRadius*GetSizeMult();
}

@ -66,7 +66,7 @@ public:
int GetAttack();
const uint32_t GetXP()const;
float GetMoveSpdMult();
float GetSizeMult();
float GetSizeMult()const;
const std::string&GetAIStrategy()const;
int GetCollisionDmg();
const std::string GetDefaultIdleAnimation()const;

@ -230,8 +230,8 @@ bool Player::SetPos(vf2d pos){
return resultX||resultY;
}
vf2d&Player::GetPos(){
return pos;
vf2d&Player::GetPos()const{
return const_cast<vf2d&>(pos);
}
float Player::GetX(){
@ -288,7 +288,7 @@ float Player::GetMoveSpdMult(){
return mod_moveSpd;
}
float Player::GetSizeMult(){
float Player::GetSizeMult()const{
return size;
}
@ -413,9 +413,11 @@ void Player::Update(float fElapsedTime){
spin_angle=0;
z=0;
float numb=4;
const MonsterHurtList&hitEnemies=game->HurtEnemies(pos,"Warrior.Ability 2.Range"_F/100*12,int(GetAttack()*"Warrior.Ability 2.DamageMult"_F),OnUpperLevel(),0);
const HurtList&hitEnemies=game->Hurt(pos,"Warrior.Ability 2.Range"_F/100*12,int(GetAttack()*"Warrior.Ability 2.DamageMult"_F),OnUpperLevel(),0,HurtType::MONSTER);
#pragma region Knockback effect
for(auto&[monsterPtr,wasHurt]:hitEnemies){
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!");
Monster*monsterPtr=std::get<Monster*>(targetPtr);
float knockbackDir=0;
float knockbackAmt=0;
if(geom2d::line<float>(GetPos(),monsterPtr->GetPos()).length()<=0.001f){
@ -514,19 +516,21 @@ void Player::Update(float fElapsedTime){
item3.cooldown=0;
}
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
if(!HasIframes()&&abs(m->GetZ()-GetZ())<=1&&OnUpperLevel()==m->OnUpperLevel()&&geom2d::overlaps(geom2d::circle(pos,12*size/2),geom2d::circle(m->GetPos(),12*m->GetSizeMult()/2))){
if(!HasIframes()&&abs(m->GetZ()-GetZ())<=1&&OnUpperLevel()==m->OnUpperLevel()&&geom2d::overlaps(geom2d::circle(pos,12*size/2),geom2d::circle(m->GetPos(),m->GetCollisionRadius()))){
if(m->IsAlive()){
m->Collision(this);
}
geom2d::line line(pos,m->GetPos());
float dist = line.length();
if(dist<=0.001){
m->SetPos(m->GetPos()+vf2d{util::random(2)-1,util::random(2)-1});
}else{
m->SetPos(line.rpoint(dist*1.1f));
if(!m->Immovable()){
if(dist<=0.001){
m->SetPos(m->GetPos()+vf2d{util::random(2)-1,util::random(2)-1});
}else{
m->SetPos(line.rpoint(dist*1.1f));
}
}
if(m->IsAlive()&&!m->IsNPC()){ //Don't set the knockback if this monster is actually an NPC. Let's just push them around.
vel+=line.vector().norm()*-128;
Knockback(line.vector().norm()*-128.f);
}
}
}

@ -98,7 +98,7 @@ public:
//one. It's hackish but it means we can reduce the amount of extra boilerplate when class changing...I don't know how to feel about this.
Player(Player*player);
static float GROUND_SLAM_SPIN_TIME;
vf2d&GetPos();
vf2d&GetPos()const;
float GetX();
float GetY();
float GetZ();
@ -116,7 +116,7 @@ public:
const float GetDamageReductionFromBuffs()const;
const float GetDamageReductionFromArmor()const;
float GetMoveSpdMult();
float GetSizeMult();
float GetSizeMult()const;
const float GetCooldownReductionPct()const;
const float GetCritRatePct()const;
const float GetCritDmgPct()const;

@ -69,7 +69,7 @@ bool PulsatingFire::Update(float fElapsedTime){
}
if(lastDamageTimer<=0){
lastDamageTimer="Wizard.Ability 3.FireRingDamageFreq"_F-0.01f;
game->HurtEnemies(pos,"Wizard.Ability 3.MeteorRadius"_F/100*24,int(game->GetPlayer()->GetAttack()*"Wizard.Ability 3.FireRingDamageMult"_F),OnUpperLevel(),0);
game->Hurt(pos,"Wizard.Ability 3.MeteorRadius"_F/100*24,int(game->GetPlayer()->GetAttack()*"Wizard.Ability 3.FireRingDamageMult"_F),OnUpperLevel(),0,HurtType::MONSTER);
SoundEffect::PlaySFX("Wizard Meteor Flames",pos);
}
return Effect::Update(fElapsedTime);

@ -43,6 +43,7 @@ All rights reserved.
#include "util.h"
INCLUDE_game
INCLUDE_BULLET_LIST
using A=Attribute;
@ -62,9 +63,7 @@ void Monster::STRATEGY::STONE_ELEMENTAL(Monster&m,float fElapsedTime,std::string
enum PhaseName{
WAITING,
STONE_PILLAR_CAST,
STONE_PILLAR_ATTACK,
SHOOT_STONE_CAST,
SHOOT_STONE_ATTACK,
DIVE_UNDERGROUND_DIG,
DIVE_UNDERGROUND_SURFACE,
};
@ -73,23 +72,36 @@ void Monster::STRATEGY::STONE_ELEMENTAL(Monster&m,float fElapsedTime,std::string
case WAITING:{
m.F(A::ATTACK_COOLDOWN)+=fElapsedTime;
//float distToPlayer=util::distance(m.GetPos(),game->GetPlayer()->GetPos());
//if(distToPlayer>=ConfigPixels("Auto Dive Range"))m.phase=DIVE_UNDERGROUND_DIG;
if(m.F(A::ATTACK_COOLDOWN)>=ConfigFloat("Attack Wait Time")){
switch(util::random()%1){
int randomAttackChoice=util::random()%2;
float distToPlayer=util::distance(m.GetPos(),game->GetPlayer()->GetPos());
//if(distToPlayer>=ConfigPixels("Auto Dive Range"))randomAttackChoice=2; //Force dig attack if too far away.
switch(randomAttackChoice){
case 0:{
m.PerformAnimation("STONE PILLAR CAST");
m.phase=STONE_PILLAR_CAST;
m.F(A::CASTING_TIMER)=ConfigFloat("Stone Pillar Cast Time");
m.V(A::LOCKON_POS)=game->GetPlayer()->GetPos();
game->AddEffect(std::make_unique<Effect>(m.V(A::LOCKON_POS),ConfigFloat("Stone Pillar Cast Time"),"range_indicator.png",m.OnUpperLevel(),vf2d{1.f,1.f}*MONSTER_DATA.at("Stone Pillar").GetSizeMult(),0.3f,vf2d{},ConfigPixel("Stone Pillar Spell Circle Color"),util::random(2*PI),util::degToRad(ConfigFloat("Stone Pillar Spell Circle Rotation Spd"))),true);
game->AddEffect(std::make_unique<Effect>(m.V(A::LOCKON_POS),ConfigFloat("Stone Pillar Cast Time"),"spell_insignia.png",m.OnUpperLevel(),vf2d{1.f,1.f}*MONSTER_DATA.at("Stone Pillar").GetSizeMult()*0.75f,0.3f,vf2d{},ConfigPixel("Stone Pillar Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Stone Pillar Spell Insignia Rotation Spd"))),true);
game->AddEffect(std::make_unique<Effect>(m.V(A::LOCKON_POS),ConfigFloat("Stone Pillar Cast Time"),"range_indicator.png",m.OnUpperLevel(),vf2d{1.f,1.f}*(MONSTER_DATA.at("Stone Pillar").GetCollisionRadius()/12.f),0.3f,vf2d{},ConfigPixel("Stone Pillar Spell Circle Color"),util::random(2*PI),util::degToRad(ConfigFloat("Stone Pillar Spell Circle Rotation Spd"))),true);
game->AddEffect(std::make_unique<Effect>(m.V(A::LOCKON_POS),ConfigFloat("Stone Pillar Cast Time"),"spell_insignia.png",m.OnUpperLevel(),vf2d{1.f,1.f}*(MONSTER_DATA.at("Stone Pillar").GetCollisionRadius()/12.f)*0.75f,0.3f,vf2d{},ConfigPixel("Stone Pillar Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Stone Pillar Spell Insignia Rotation Spd"))),true);
}break;
case 1:{
m.PerformAnimation("ROCK TOSS CAST");
m.phase=SHOOT_STONE_CAST;
m.F(A::CASTING_TIMER)=ConfigFloat("Rock Toss Track Time")+ConfigFloat("Rock Toss Wait Time");
CreateBullet(LevitatingRock)(m,game->GetPlayer()->GetPos(),1.f,0.f,ConfigPixels("Rock Toss Max Spawn Distance"),ConfigFloat("Rock Toss Track Time"),ConfigFloat("Rock Toss Wait Time"),ConfigFloat("Rock Toss Bullet Speed"),ConfigFloat("Rock Radius"),std::max(1,ConfigInt("Rock Toss Damage")/5),m.OnUpperLevel(),false,WHITE,vf2d{1,1})EndBullet;
const int masterRockInd=BULLET_LIST.size()-1;
CreateBullet(LevitatingRock)(m,game->GetPlayer()->GetPos(),1.f,util::degToRad(-20.f),ConfigPixels("Rock Toss Max Spawn Distance")*0.75f,ConfigFloat("Rock Toss Track Time"),ConfigFloat("Rock Toss Wait Time"),ConfigFloat("Rock Toss Bullet Speed"),ConfigFloat("Rock Radius")*0.6f,std::max(1,ConfigInt("Rock Toss Damage")/5),m.OnUpperLevel(),false,WHITE,vf2d{0.6f,0.6f})EndBullet;
CreateBullet(LevitatingRock)(m,game->GetPlayer()->GetPos(),1.f,util::degToRad(-40.f),ConfigPixels("Rock Toss Max Spawn Distance")*0.5f,ConfigFloat("Rock Toss Track Time"),ConfigFloat("Rock Toss Wait Time"),ConfigFloat("Rock Toss Bullet Speed"),ConfigFloat("Rock Radius")*0.6f,std::max(1,ConfigInt("Rock Toss Damage")/5),m.OnUpperLevel(),false,WHITE,vf2d{0.6f,0.6f})EndBullet;
CreateBullet(LevitatingRock)(m,game->GetPlayer()->GetPos(),1.f,util::degToRad(20.f),ConfigPixels("Rock Toss Max Spawn Distance")*0.75f,ConfigFloat("Rock Toss Track Time"),ConfigFloat("Rock Toss Wait Time"),ConfigFloat("Rock Toss Bullet Speed"),ConfigFloat("Rock Radius")*0.6f,std::max(1,ConfigInt("Rock Toss Damage")/5),m.OnUpperLevel(),false,WHITE,vf2d{0.6f,0.6f})EndBullet;
CreateBullet(LevitatingRock)(m,game->GetPlayer()->GetPos(),1.f,util::degToRad(40.f),ConfigPixels("Rock Toss Max Spawn Distance")*0.5f,ConfigFloat("Rock Toss Track Time"),ConfigFloat("Rock Toss Wait Time"),ConfigFloat("Rock Toss Bullet Speed"),ConfigFloat("Rock Radius")*0.6f,std::max(1,ConfigInt("Rock Toss Damage")/5),m.OnUpperLevel(),false,WHITE,vf2d{0.6f,0.6f})EndBullet;
auto bulletListEndIter{BULLET_LIST.end()};
for(int i=0;i<4;i++){ //This is going to assign the last four stones we created as slaves to the first rock so that their directions can all be changed together.
bulletListEndIter--;
((LevitatingRock*)(*bulletListEndIter).get())->AssignMaster((LevitatingRock*)BULLET_LIST[masterRockInd].get());
}
}break;
case 2:{
m.PerformAnimation("BURROW UNDEGROUND");
@ -102,13 +114,26 @@ void Monster::STRATEGY::STONE_ELEMENTAL(Monster&m,float fElapsedTime,std::string
case STONE_PILLAR_CAST:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0.f){
m.phase=STONE_PILLAR_ATTACK;
m.phase=WAITING;
game->SpawnMonster(m.V(A::LOCKON_POS),MONSTER_DATA.at("Stone Pillar"),m.OnUpperLevel());
m.PerformIdleAnimation();
m.F(A::ATTACK_COOLDOWN)=0.f;
game->Hurt(m.V(A::LOCKON_POS),MONSTER_DATA.at("Stone Pillar").GetCollisionRadius(),m.GetAttack(),m.OnUpperLevel(),0.f,HurtType::PLAYER);
}
if(geom2d::overlaps(geom2d::circle<float>{m.V(A::LOCKON_POS),MONSTER_DATA.at("Stone Pillar").GetCollisionRadius()},geom2d::circle<float>{m.GetPos(),m.GetCollisionRadius()})){
geom2d::line<float>stonePillarCastLine{m.V(A::LOCKON_POS),m.GetPos()};
const vf2d targetWalkPos=stonePillarCastLine.rpoint(stonePillarCastLine.length()+48.f);
m.target=targetWalkPos;
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
m.PerformAnimation("ROCK TOSS CAST");
}
}break;
case SHOOT_STONE_CAST:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0.f){
m.phase=SHOOT_STONE_ATTACK;
m.phase=WAITING;
m.PerformIdleAnimation();
m.F(A::ATTACK_COOLDOWN)=0.f;
}
}break;
case DIVE_UNDERGROUND_DIG:{
@ -116,12 +141,6 @@ void Monster::STRATEGY::STONE_ELEMENTAL(Monster&m,float fElapsedTime,std::string
if(m.F(A::CASTING_TIMER)<=0.f){
m.phase=DIVE_UNDERGROUND_SURFACE;
}
}break;
case STONE_PILLAR_ATTACK:{
}break;
case SHOOT_STONE_ATTACK:{
}break;
case DIVE_UNDERGROUND_SURFACE:{

@ -46,7 +46,7 @@ SwordSlash::SwordSlash(float lifetime, std::string imgFile, vf2d size, float fad
bool SwordSlash::Update(float fElapsedTime){
if(lifetime>0){
game->HurtEnemiesConeNotHit(game->GetPlayer()->GetPos(),game->GetPlayer()->GetAttackRangeMult()*12.f,rotation,util::degToRad("Warrior.Auto Attack.SwordSlashSweepAngle"_F),game->GetPlayer()->GetAttack(),hitList,game->GetPlayer()->OnUpperLevel(),game->GetPlayer()->GetZ());
game->HurtConeNotHit(game->GetPlayer()->GetPos(),game->GetPlayer()->GetAttackRangeMult()*12.f,rotation,util::degToRad("Warrior.Auto Attack.SwordSlashSweepAngle"_F),game->GetPlayer()->GetAttack(),hitList,game->GetPlayer()->OnUpperLevel(),game->GetPlayer()->GetZ(),HurtType::MONSTER);
}
pos=game->GetPlayer()->GetPos();

@ -39,7 +39,7 @@ All rights reserved.
#define VERSION_MAJOR 1
#define VERSION_MINOR 2
#define VERSION_PATCH 0
#define VERSION_BUILD 9283
#define VERSION_BUILD 9318
#define stringify(a) stringify_(a)
#define stringify_(a) #a

@ -737,9 +737,16 @@ MonsterStrategy
Stone Pillar Spell Insignia Rotation Spd = 50
Rock Toss Track Time = 1s
Rock Toss Wait Time = 1s
Rock Toss Wait Time = 2s
Rock Toss Bullet Speed = 900
Rock Toss Bullet Speed = 400
Rock Radius = 4
Rock Toss Damage = 52
# Distance in units.
Rock Toss Max Spawn Distance = 100
Burrow Wait Time = 1s

@ -94,6 +94,8 @@ Images
GFX_GoblinBombFuse = goblin_bomb_fuse.png
GFX_BombBoom = bomb_boom.png
GFX_SpellInsignia = spell_insignia.png
GFX_Rock = rock.png
GFX_RockOutline = rock_outline.png
# Ability Icons
GFX_Warrior_BattleCry_Icon = Ability Icons/battlecry.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Loading…
Cancel
Save