Added arrow simulation for bow goblins. Added perception levels which increase the accuracy of the shooting monster over time. Refactor bullet update code to be inside the bullet class itself. Release Build 9169.

mac-build
sigonasr2 7 months ago
parent aaaf10f6fc
commit 89bc17c0a2
  1. 65
      Adventures in Lestoria/AdventuresInLestoria.cpp
  2. 36
      Adventures in Lestoria/Arrow.cpp
  3. 78
      Adventures in Lestoria/Bullet.cpp
  4. 8
      Adventures in Lestoria/Bullet.h
  5. 8
      Adventures in Lestoria/BulletTypes.h
  6. 11
      Adventures in Lestoria/Goblin_Bow.cpp
  7. 3
      Adventures in Lestoria/MonsterAttribute.h
  8. 2
      Adventures in Lestoria/Player.cpp
  9. 4
      Adventures in Lestoria/Ranger.cpp
  10. 2
      Adventures in Lestoria/Version.h
  11. 8
      Adventures in Lestoria/assets/config/MonsterStrategies.txt
  12. BIN
      x64/Release/Adventures in Lestoria.exe

@ -732,70 +732,9 @@ 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();
b->UpdateFadeTime(fElapsedTime);
b->Update(fElapsedTime);
b->animation.UpdateState(b->internal_animState,fElapsedTime);
if(!b->deactivated){
float totalDistance=(b->vel*fElapsedTime).mag();
int iterations=int(std::max(1.f,(b->vel*fElapsedTime).mag()));
int totalIterations=iterations;
vf2d finalBulletPos=b->pos+b->vel*fElapsedTime;
b->distanceTraveled+=totalDistance/24.f*100.f;
const auto CollisionCheck=[&](){
if(b->friendly){
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
if(geom2d::overlaps(m->Hitbox(),geom2d::circle(b->pos,b->radius))){
if(b->hitList.find(&*m)==b->hitList.end()&&m->Hurt(b->damage,b->OnUpperLevel(),0)){
if(!b->hitsMultiple){
if(b->MonsterHit(*m)){
b->dead=true;
}
return false;
}
b->hitList.insert(&*m);
}
}
}
} else {
if(geom2d::overlaps(player->Hitbox(),geom2d::circle(b->pos,b->radius))){
if(player->Hurt(b->damage,b->OnUpperLevel(),0)){
if(b->PlayerHit(&*player)){
b->dead=true;
}
return false;
}
}
}
return true;
};
while(iterations>0){
iterations--;
b->pos+=(b->vel*fElapsedTime)/float(totalIterations);
if(!CollisionCheck()){
goto nextBullet;
}
}
b->pos=finalBulletPos;
if(!CollisionCheck()){
goto nextBullet;
}
}else{
b->pos+=b->vel*fElapsedTime;
}
if(/*World size in PIXELS!*/vi2d worldSize=GetCurrentMap().MapData.MapSize*GetCurrentMap().MapData.TileSize;b->pos.x+b->radius<-WINDOW_SIZE.x||b->pos.x-b->radius>worldSize.x+WINDOW_SIZE.x||b->pos.y+b->radius<-WINDOW_SIZE.y||b->pos.y-b->radius>worldSize.y+WINDOW_SIZE.y){
b->dead=true;
continue;
}
b->lifetime-=fElapsedTime;
if(b->lifetime<=0){
b->dead=true;
continue;
}
nextBullet:
while(false);
b->_Update(fElapsedTime);
}
std::erase_if(BULLET_LIST,[](std::unique_ptr<Bullet>&b){return b->dead;});
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;

@ -44,13 +44,13 @@ All rights reserved.
INCLUDE_game
Arrow::Arrow(vf2d pos,vf2d targetPos,vf2d vel,float acc,float radius,int damage,bool upperLevel,bool friendly,Pixel col)
:finalDistance(geom2d::line(pos,targetPos).length()*1.2f),acc(acc),targetPos(targetPos),
Arrow::Arrow(vf2d pos,vf2d targetPos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly,Pixel col)
:finalDistance(geom2d::line(pos,targetPos).length()*1.2f),acc(PI/2*250),targetPos(targetPos),
Bullet(pos,vel,radius,damage,
"arrow.png",upperLevel,false,INFINITE,true,friendly,col){}
Arrow::Arrow(vf2d pos,vf2d targetPos,vf2d vel,const std::string_view gfx,float acc,float radius,int damage,bool upperLevel,bool friendly,Pixel col)
:finalDistance(geom2d::line(pos,targetPos).length()*1.2f),acc(acc),
Arrow::Arrow(vf2d pos,vf2d targetPos,vf2d vel,const std::string_view gfx,float radius,int damage,bool upperLevel,bool friendly,Pixel col)
:finalDistance(geom2d::line(pos,targetPos).length()*1.2f),acc(PI/2*250),targetPos(targetPos),
Bullet(pos,vel,radius,damage,std::string(gfx),upperLevel,false,INFINITE,true,friendly,col){}
void Arrow::Update(float fElapsedTime){
@ -63,8 +63,32 @@ void Arrow::Update(float fElapsedTime){
}
}
void Arrow::PointToBestTargetPath(const int perceptionLevel){
//TODO: Figure out arrow target and spawn an arrow.
void Arrow::PointToBestTargetPath(const uint8_t perceptionLevel){
if(perceptionLevel>90)ERR(std::format("WARNING! Perception level {} provided. Acceptable range is 0-90.",perceptionLevel));
Arrow copiedArrow{*this};
float closestDist=std::numeric_limits<float>::max();
vf2d closestVel{};
for(float angle=util::degToRad(-perceptionLevel);angle<=util::degToRad(perceptionLevel);angle+=util::degToRad(1.f)){
Arrow simulatedArrow{copiedArrow};
vf2d simulatedAimingDir=simulatedArrow.vel.polar();
simulatedAimingDir.y+=angle;
simulatedArrow.vel=simulatedAimingDir.cart();
vf2d originalSimulatedShootingAngle=simulatedArrow.vel;
while(!simulatedArrow.deactivated){
simulatedArrow.SimulateUpdate(1/30.f);
float distToPlayer=geom2d::line<float>(simulatedArrow.pos,game->GetPlayer()->GetPos()).length();
if(distToPlayer<closestDist){
closestDist=distToPlayer;
closestVel=originalSimulatedShootingAngle;
LOG(std::format("Angle {} is a better choice as the arrow gets to {} distance from the player.",util::radToDeg(closestVel.polar().y),closestDist));
}
}
}
if(closestVel==vf2d{0,0})ERR("WARNING! We didn't find a valid path of flight for the Arrow! THIS SHOULD NOT BE HAPPENING!");
vel=closestVel;
}
bool Arrow::PlayerHit(Player*player)

@ -43,6 +43,8 @@ All rights reserved.
INCLUDE_ANIMATION_DATA
INCLUDE_game
INCLUDE_GFX
INCLUDE_MONSTER_LIST
INCLUDE_WINDOW_SIZE
Bullet::Bullet(vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly,Pixel col,vf2d scale)
:pos(pos),vel(vel),radius(radius),damage(damage),col(col),friendly(friendly),upperLevel(upperLevel),scale(scale){};
@ -68,6 +70,76 @@ void Bullet::UpdateFadeTime(float fElapsedTime)
void Bullet::Update(float fElapsedTime){}
void Bullet::SimulateUpdate(const float fElapsedTime){
simulated=true;
_Update(fElapsedTime);
simulated=false;
}
void Bullet::_Update(const float fElapsedTime){
UpdateFadeTime(fElapsedTime);
Update(fElapsedTime);
animation.UpdateState(internal_animState,fElapsedTime);
if(!deactivated){
float totalDistance=(vel*fElapsedTime).mag();
int iterations=int(std::max(1.f,(vel*fElapsedTime).mag()));
int totalIterations=iterations;
vf2d finalBulletPos=pos+vel*fElapsedTime;
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->Hitbox(),geom2d::circle(pos,radius))){
if(hitList.find(&*m)==hitList.end()&&m->Hurt(damage,OnUpperLevel(),0)){
if(!hitsMultiple){
if(MonsterHit(*m)){
dead=true;
}
return false;
}
hitList.insert(&*m);
}
}
}
} else {
if(geom2d::overlaps(game->GetPlayer()->Hitbox(),geom2d::circle(pos,radius))){
if(game->GetPlayer()->Hurt(damage,OnUpperLevel(),0)){
if(PlayerHit(&*game->GetPlayer())){
dead=true;
}
return false;
}
}
}
return true;
};
while(iterations>0){
iterations--;
pos+=(vel*fElapsedTime)/float(totalIterations);
if(!CollisionCheck()){
return;
}
}
pos=finalBulletPos;
if(!CollisionCheck()){
return;
}
}else{
pos+=vel*fElapsedTime;
}
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{
auto lerp=[](uint8_t f1,uint8_t f2,float t){return uint8_t((float(f2)*t)+f1*(1-t));};
@ -81,4 +153,8 @@ void Bullet::Draw()const{
bool Bullet::PlayerHit(Player*player){return true;}
bool Bullet::MonsterHit(Monster&monster){return true;}
bool Bullet::OnUpperLevel(){return upperLevel;}
bool Bullet::OnUpperLevel(){return upperLevel;}
const bool Bullet::IsDead()const{
return dead;
}

@ -42,7 +42,6 @@ All rights reserved.
#include "DEFINES.h"
struct Bullet{
friend class AiL;
vf2d vel;
vf2d pos;
float radius;
@ -62,8 +61,10 @@ protected:
float distanceTraveled=0.f;
private:
void UpdateFadeTime(float fElapsedTime);
virtual void Update(float fElapsedTime);
vf2d scale={1,1};
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
public:
Animate2D::Animation<std::string>animation;
Animate2D::AnimationState internal_animState;
@ -73,7 +74,9 @@ public:
//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});
public:
virtual void Update(float fElapsedTime);
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.
virtual bool 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.
@ -81,4 +84,5 @@ public:
Animate2D::Frame GetFrame()const;
virtual void Draw()const;
bool OnUpperLevel();
const bool IsDead()const;
};

@ -68,10 +68,12 @@ struct Arrow:public Bullet{
float finalDistance=0;
float acc=PI/2*250;
vf2d targetPos;
Arrow(vf2d pos,vf2d targetPos,vf2d vel,float acc,float radius,int damage,bool upperLevel,bool friendly=false,Pixel col=WHITE);
Arrow(vf2d pos,vf2d targetPos,vf2d vel,const std::string_view gfx,float acc,float radius,int damage,bool upperLevel,bool friendly=false,Pixel col=WHITE);
Arrow(vf2d pos,vf2d targetPos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly=false,Pixel col=WHITE);
Arrow(vf2d pos,vf2d targetPos,vf2d vel,const std::string_view gfx,float radius,int damage,bool upperLevel,bool friendly=false,Pixel col=WHITE);
void Update(float fElapsedTime)override;
void PointToBestTargetPath(const int perceptionLevel);
// Change the arrow's heading by predicting a path somewhere in the future and aiming at the closest possible spot to its targetPos.
// The perception level can be a value from 0-90 indicating the sweep angle to check beyond the initial aiming angle.
void PointToBestTargetPath(const uint8_t perceptionLevel);
bool PlayerHit(Player*player)override;
bool MonsterHit(Monster&monster)override;
};

@ -59,6 +59,7 @@ using A=Attribute;
void Monster::STRATEGY::GOBLIN_BOW(Monster&m,float fElapsedTime,std::string strategy){
#pragma region Phase, Animation, and Helper function setup
enum PhaseName{
INITIALIZE_PERCEPTION,
MOVE,
WINDUP,
};
@ -67,6 +68,10 @@ void Monster::STRATEGY::GOBLIN_BOW(Monster&m,float fElapsedTime,std::string stra
m.F(A::ATTACK_COOLDOWN)+=fElapsedTime;
switch(m.phase){
case INITIALIZE_PERCEPTION:{
m.F(A::PERCEPTION_LEVEL)=ConfigFloat("Starting Perception Level");
m.phase=MOVE;
}break;
case MOVE:{
float distToPlayer=m.GetDistanceFrom(game->GetPlayer()->GetPos());
@ -107,9 +112,11 @@ void Monster::STRATEGY::GOBLIN_BOW(Monster&m,float fElapsedTime,std::string stra
if(m.F(A::SHOOT_TIMER)<=0){
geom2d::line pointTowardsPlayer(m.GetPos(),game->GetPlayer()->GetPos());
vf2d extendedLine=pointTowardsPlayer.upoint(1.1f);
CreateBullet(Arrow)(m.GetPos(),extendedLine,pointTowardsPlayer.vector().norm()*ConfigFloat("Arrow Spd"),"goblin_arrow.png",PI/2*ConfigFloat("Arrow Spd"),ConfigFloat("Arrow Hitbox Radius"),m.GetAttack(),m.OnUpperLevel())EndBullet;
CreateBullet(Arrow)(m.GetPos(),extendedLine,pointTowardsPlayer.vector().norm()*ConfigFloat("Arrow Spd"),"goblin_arrow.png",ConfigFloat("Arrow Hitbox Radius"),m.GetAttack(),m.OnUpperLevel())EndBullet;
Arrow&arrow=static_cast<Arrow&>(*BULLET_LIST.back());
arrow.PointToBestTargetPath(0);
arrow.PointToBestTargetPath(m.F(A::PERCEPTION_LEVEL));
m.F(A::PERCEPTION_LEVEL)=std::min(ConfigFloat("Maximum Perception Level"),m.F(A::PERCEPTION_LEVEL)+ConfigFloat("Perception Level Increase"));
m.phase=MOVE;
}
m.B(A::RANDOM_DIRECTION)=util::random()%2;

@ -110,5 +110,6 @@ enum class Attribute{
ATTACK_TYPE,
ATTACK_COOLDOWN,
RANDOM_DIRECTION,
RANDOM_RANGE
RANDOM_RANGE,
PERCEPTION_LEVEL,
};

@ -680,7 +680,7 @@ void Player::Update(float fElapsedTime){
vf2d extendedLine=pointTowardsCursor.upoint(1.1f);
float angleToCursor=atan2(extendedLine.y-GetPos().y,extendedLine.x-GetPos().x);
attack_cooldown_timer=ARROW_ATTACK_COOLDOWN;
BULLET_LIST.push_back(std::make_unique<Arrow>(Arrow(GetPos(),extendedLine,vf2d{cos(angleToCursor)*"Ranger.Ability 1.ArrowSpd"_F,float(sin(angleToCursor)*"Ranger.Ability 1.ArrowSpd"_F-PI/8*"Ranger.Ability 1.ArrowSpd"_F)}+movementVelocity/1.5f,PI/2*"Ranger.Auto Attack.ArrowSpd"_F,12*"Ranger.Ability 1.ArrowRadius"_F/100,int(GetAttack()*"Ranger.Ability 1.DamageMult"_F),OnUpperLevel(),true)));
BULLET_LIST.push_back(std::make_unique<Arrow>(Arrow(GetPos(),extendedLine,vf2d{cos(angleToCursor)*"Ranger.Ability 1.ArrowSpd"_F,float(sin(angleToCursor)*"Ranger.Ability 1.ArrowSpd"_F-PI/8*"Ranger.Ability 1.ArrowSpd"_F)}+movementVelocity/1.5f,12*"Ranger.Ability 1.ArrowRadius"_F/100,int(GetAttack()*"Ranger.Ability 1.DamageMult"_F),OnUpperLevel(),true)));
SetAnimationBasedOnTargetingDirection(angleToCursor);
rapidFireTimer=RAPID_FIRE_SHOOT_DELAY;
}else{

@ -71,7 +71,7 @@ bool Ranger::AutoAttack(){
vf2d extendedLine=pointTowardsCursor.upoint(1.1f);
float angleToCursor=atan2(extendedLine.y-GetPos().y,extendedLine.x-GetPos().x);
attack_cooldown_timer=ARROW_ATTACK_COOLDOWN-GetAttackRecoveryRateReduction();
BULLET_LIST.push_back(std::make_unique<Arrow>(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,PI/2*"Ranger.Auto Attack.ArrowSpd"_F,"Ranger.Auto Attack.Radius"_F,int(GetAttack()*"Ranger.Auto Attack.DamageMult"_F),OnUpperLevel(),true)));
BULLET_LIST.push_back(std::make_unique<Arrow>(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)));
SetState(State::SHOOT_ARROW);
SetAnimationBasedOnTargetingDirection(angleToCursor);
SoundEffect::PlaySFX("Ranger.Auto Attack.Sound"_S,SoundEffect::CENTERED);
@ -139,7 +139,7 @@ void Ranger::InitializeClassAbilities(){
const float newAngle=shootingAngle+leftAngle/2+i*increment;
geom2d::line pointTowardsCursor=geom2d::line(p->GetPos(),p->GetPos()+vf2d{cos(newAngle),sin(newAngle)}*shootingDist);
vf2d extendedLine=pointTowardsCursor.upoint(1.1f);
BULLET_LIST.push_back(std::make_unique<Arrow>(Arrow(p->GetPos(),extendedLine,vf2d{cos(newAngle)*"Ranger.Ability 3.ArrowSpd"_F,float(sin(newAngle)*"Ranger.Ability 3.ArrowSpd"_F-PI/8*"Ranger.Ability 3.ArrowSpd"_F)}+p->movementVelocity,PI/2*"Ranger.Auto Attack.ArrowSpd"_F,12*"Ranger.Ability 3.ArrowRadius"_F/100,int(p->GetAttack()*"Ranger.Ability 3.DamageMult"_F),p->OnUpperLevel(),true)));
BULLET_LIST.push_back(std::make_unique<Arrow>(Arrow(p->GetPos(),extendedLine,vf2d{cos(newAngle)*"Ranger.Ability 3.ArrowSpd"_F,float(sin(newAngle)*"Ranger.Ability 3.ArrowSpd"_F-PI/8*"Ranger.Ability 3.ArrowSpd"_F)}+p->movementVelocity,12*"Ranger.Ability 3.ArrowRadius"_F/100,int(p->GetAttack()*"Ranger.Ability 3.DamageMult"_F),p->OnUpperLevel(),true)));
}
p->rangerShootAnimationTimer=0.3f;
p->SetState(State::SHOOT_ARROW);

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

@ -595,7 +595,7 @@ MonsterStrategy
# How long it takes to prepare the attack once an attack is queued.
Attack Windup Time = 1.0s
Arrow Spd = 250
Arrow Spd = 350
Arrow Hitbox Radius = 8
@ -606,5 +606,11 @@ MonsterStrategy
# Does not move and shoots from anywhere in these ranges.
Stand Still and Shoot Range = 700,1000
# Anything outside the max "Stand Still and Shoot Range" will cause the monster to move towards the target instead.
# The perception level indicates how accurate the bow user's shots become over time. Perception can be between 0-90. A perception level of 90 should never miss. This doesn't necessarily mean lower numbers will miss, just that it doesn't auto-correct for error as much.
Starting Perception Level = 0
# Every shot taken, the bow user's perception level will increase by this amount.
Perception Level Increase = 2.5
Maximum Perception Level = 45
}
}
Loading…
Cancel
Save