The open source repository for the action RPG game in development by Sig Productions titled 'Adventures in Lestoria'! https://forums.lestoria.net
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdventuresInLestoria/Crawler/Player.cpp

672 lines
18 KiB

#include "Monster.h"
#include "Player.h"
#include "Crawler.h"
#include "DamageNumber.h"
#include "Bullet.h"
#include "BulletTypes.h"
#include "DEFINES.h"
#include "safemap.h"
INCLUDE_MONSTER_DATA
INCLUDE_MONSTER_LIST
INCLUDE_ANIMATION_DATA
INCLUDE_SPAWNER_LIST
INCLUDE_BULLET_LIST
INCLUDE_DAMAGENUMBER_LIST
INCLUDE_game
float Player::GROUND_SLAM_SPIN_TIME=0.6f;
Player::Player()
:lastReleasedMovementKey(DOWN),facingDirection(DOWN),state(State::NORMAL){
Initialize();
}
Player::Player(Player*player)
:pos(player->GetPos()),vel(player->GetVelocity()),iframe_time(player->iframe_time),lastReleasedMovementKey(DOWN),
facingDirection(DOWN),state(State::NORMAL){
Initialize();
}
void Player::Initialize(){
Player::GROUND_SLAM_SPIN_TIME="Warrior.Ability 2.SpinTime"_F;
}
bool Player::SetX(float x){
vf2d newPos={x,pos.y};
vi2d tilePos=vi2d(newPos/24)*24;
geom2d::rect<int>collisionRect=game->GetTileCollision(game->GetCurrentLevel(),newPos,upperLevel);
#pragma region lambdas
auto NoTileCollisionExistsHere=[&](){return collisionRect.pos==game->NO_COLLISION.pos&&collisionRect.size==game->NO_COLLISION.size;};
#pragma endregion
if(NoTileCollisionExistsHere()){
pos.x=std::clamp(x,12.f*GetSizeMult(),float(game->WORLD_SIZE.x*24-12*GetSizeMult()));
Moved();
return true;
} else {
geom2d::rect<float>collision={collisionRect.pos,collisionRect.size};
#pragma region lambdas
auto NoPlayerCollisionWithTile=[&](){return !geom2d::overlaps(newPos,collision);};
#pragma endregion
collision.pos+=tilePos;
if(NoPlayerCollisionWithTile()){
pos.x=std::clamp(x,12.f*GetSizeMult(),float(game->WORLD_SIZE.x*24-12*GetSizeMult()));
Moved();
return true;
}
}
return false;
};
bool Player::SetY(float y){
vf2d newPos={pos.x,y};
vi2d tilePos=vi2d(newPos/24)*24;
geom2d::rect<int>collisionRect=game->GetTileCollision(game->GetCurrentLevel(),newPos,upperLevel);
#pragma region lambdas
auto NoTileCollisionExistsHere=[&](){return collisionRect.pos==game->NO_COLLISION.pos&&collisionRect.size==game->NO_COLLISION.size;};
#pragma endregion
if(NoTileCollisionExistsHere()){
pos.y=std::clamp(y,12.f*GetSizeMult(),float(game->WORLD_SIZE.y*24-12*GetSizeMult()));
Moved();
return true;
} else {
geom2d::rect<float>collision={collisionRect.pos,collisionRect.size};
#pragma region lambdas
auto NoPlayerCollisionWithTile=[&](){return !geom2d::overlaps(newPos,collision);};
#pragma endregion
collision.pos+=tilePos;
if(NoPlayerCollisionWithTile()){
pos.y=std::clamp(y,12.f*GetSizeMult(),float(game->WORLD_SIZE.y*24-12*GetSizeMult()));
Moved();
return true;
}
}
return false;
}
void Player::SetZ(float z){
this->z=z;
}
bool Player::SetPos(vf2d pos){
bool resultX=SetX(pos.x);
bool resultY=SetY(pos.y);
if(resultY&&!resultX){
resultX=SetX(pos.x);
}
return resultX|resultY;
}
vf2d&Player::GetPos(){
return pos;
}
float Player::GetX(){
return pos.x;
}
float Player::GetY(){
return pos.y;
}
float Player::GetZ(){
return z;
}
int Player::GetHealth(){
return hp;
}
int Player::GetMaxHealth(){
return maxhp;
}
int Player::GetMana(){
return mana;
}
int Player::GetMaxMana() {
return maxmana;
}
int Player::GetAttack(){
float mod_atk=atk;
for(Buff&b:GetBuffs(BuffType::ATTACK_UP)){
mod_atk+=atk*b.intensity;
}
return int(mod_atk);
}
float Player::GetMoveSpdMult(){
float mod_moveSpd=moveSpd;
for(Buff&b:GetBuffs(BuffType::SLOWDOWN)){
mod_moveSpd-=mod_moveSpd*b.intensity;
}
for(Buff&b:GetBuffs(BuffType::BLOCK_SLOWDOWN)){
mod_moveSpd-=mod_moveSpd*b.intensity;
}
return mod_moveSpd;
}
float Player::GetSizeMult(){
return size;
}
float Player::GetAttackRangeMult(){
return attack_range;
}
float Player::GetSpinAngle(){
return spin_angle;
}
State Player::GetState(){
return state;
}
void Player::Knockback(vf2d vel){
this->vel+=vel;
}
void Player::Update(float fElapsedTime){
Ability&rightClickAbility=GetRightClickAbility(),
&ability=GetAbility1(),
&ability2=GetAbility2(),
&ability3=GetAbility3(),
&ability4=GetAbility4();
attack_cooldown_timer=std::max(0.f,attack_cooldown_timer-fElapsedTime);
iframe_time=std::max(0.f,iframe_time-fElapsedTime);
notEnoughManaDisplay.second=std::max(0.f,notEnoughManaDisplay.second-fElapsedTime);
notificationDisplay.second=std::max(0.f,notificationDisplay.second-fElapsedTime);
lastHitTimer=std::max(0.f,lastHitTimer-fElapsedTime);
blockTimer=std::max(0.f,blockTimer-fElapsedTime);
manaTickTimer-=fElapsedTime;
if(castInfo.castTimer>0){
castInfo.castTimer-=fElapsedTime;
if(castInfo.castTimer<=0){
if(castPrepAbility->action(this,castInfo.castPos)){
castPrepAbility->cooldown=castPrepAbility->COOLDOWN_TIME;
mana-=castPrepAbility->manaCost;
}
castInfo.castTimer=0;
SetState(State::NORMAL);
}
}
while(manaTickTimer<=0){
manaTickTimer+=0.2;
mana=std::min(maxmana,mana+1);
}
for(std::vector<Buff>::iterator it=buffList.begin();it!=buffList.end();++it){
Buff&b=*it;
b.duration-=fElapsedTime;
if(b.duration<=0){
it=buffList.erase(it);
if(it==buffList.end())break;
}
}
//Class-specific update events.
OnUpdate(fElapsedTime);
switch(state){
case SPIN:{
switch(facingDirection){
case UP:{
if(lastAnimationFlip==0){
lastAnimationFlip=0.03;
facingDirection=DOWN;
animation.ChangeState(internal_animState,"WARRIOR_WALK_S");
}
}break;
case DOWN:{
if(lastAnimationFlip==0){
lastAnimationFlip=0.03;
facingDirection=UP;
animation.ChangeState(internal_animState,"WARRIOR_WALK_N");
}
}break;
}
if(facingDirection==RIGHT){
spin_angle+=spin_spd*fElapsedTime;
} else {
spin_angle-=spin_spd*fElapsedTime;
}
if(spin_attack_timer>0){
z="Warrior.Ability 2.SpinMaxHeight"_I*sin(3.3*(GROUND_SLAM_SPIN_TIME-spin_attack_timer)/GROUND_SLAM_SPIN_TIME);
spin_attack_timer=std::max(0.f,spin_attack_timer-fElapsedTime);
} else {
SetState(NORMAL);
spin_angle=0;
z=0;
float numb=4;
game->HurtEnemies(pos,"Warrior.Ability 2.Range"_F/100*12,GetAttack()*"Warrior.Ability 2.DamageMult"_F,OnUpperLevel());
game->AddEffect(std::make_unique<Effect>(GetPos(),"Warrior.Ability 2.EffectLifetime"_F,"GROUND_SLAM_ATTACK_FRONT",upperLevel,"Warrior.Ability 2.Range"_F/300*1.33f,"Warrior.Ability 2.EffectFadetime"_F),std::make_unique<Effect>(GetPos(),"Warrior.Ability 2.EffectLifetime"_F,"GROUND_SLAM_ATTACK_BACK",upperLevel,"Warrior.Ability 2.Range"_F/300*1.33f,"Warrior.Ability 2.EffectFadetime"_F));
}
if(lastAnimationFlip>0){
lastAnimationFlip=std::max(0.f,lastAnimationFlip-fElapsedTime);
}
animation.UpdateState(internal_animState,fElapsedTime);
}break;
case BLOCK:{
if(blockTimer<=0){
SetState(NORMAL);
}
}break;
case SWING_SONIC_SWORD:{
if(ability3.COOLDOWN_TIME-ability3.cooldown>0.5){
SetState(NORMAL);
switch(facingDirection){
case DOWN:{
UpdateAnimation("WARRIOR_IDLE_S");
}break;
case RIGHT:{
UpdateAnimation("WARRIOR_IDLE_E");
}break;
case LEFT:{
UpdateAnimation("WARRIOR_IDLE_W");
}break;
case UP:{
UpdateAnimation("WARRIOR_IDLE_N");
}break;
}
}
animation.UpdateState(internal_animState,fElapsedTime);
}break;
case TELEPORT:{
teleportAnimationTimer=std::max(0.f,teleportAnimationTimer-fElapsedTime);
if(teleportAnimationTimer<=0){
SetPos(teleportTarget);
SetState(NORMAL);
}
animation.UpdateState(internal_animState,fElapsedTime);
}break;
default:{
//Update animations normally.
animation.UpdateState(internal_animState,fElapsedTime);
}
}
rightClickAbility.cooldown-=fElapsedTime;
ability.cooldown-=fElapsedTime;
ability2.cooldown-=fElapsedTime;
ability3.cooldown-=fElapsedTime;
ability4.cooldown-=fElapsedTime;
if(rightClickAbility.cooldown<0){
rightClickAbility.cooldown=0;
}
if(ability.cooldown<0){
ability.cooldown=0;
}
if(ability2.cooldown<0){
ability2.cooldown=0;
}
if(ability3.cooldown<0){
ability3.cooldown=0;
}
if(ability4.cooldown<0){
ability4.cooldown=0;
}
for(Monster&m:MONSTER_LIST){
if(!HasIframes()&&OnUpperLevel()==m.OnUpperLevel()&&geom2d::overlaps(geom2d::circle(pos,12*size/2),geom2d::circle(m.GetPos(),12*m.GetSizeMult()/2))){
if(m.IsAlive()){
m.Collision(this);
}
geom2d::line line(pos,m.GetPos());
float dist = line.length();
m.SetPosition(line.rpoint(dist*1.1));
if(m.IsAlive()){
vel=line.vector().norm()*-128;
}
}
}
if(vel.x>0){
vel.x=std::max(0.f,vel.x-friction*fElapsedTime);
} else {
vel.x=std::min(0.f,vel.x+friction*fElapsedTime);
}
if(vel.y>0){
vel.y=std::max(0.f,vel.y-friction*fElapsedTime);
} else {
vel.y=std::min(0.f,vel.y+friction*fElapsedTime);
}
if(vel!=vf2d{0,0}){
float newX=pos.x+vel.x*fElapsedTime;
float newY=pos.y+vel.y*fElapsedTime;
SetX(newX);
SetY(newY);
}
if(CanAct()&&attack_cooldown_timer==0&&game->GetMouse(0).bHeld){
AutoAttack();
}
auto AllowedToCast=[&](Ability&ability){return !ability.precastInfo.precastTargetingRequired&&GetState()!=State::ANIMATION_LOCK;};
//If pressed is set to false, uses held instead.
auto CheckAndPerformAbility=[&](Ability&ability,HWButton key){
if(ability.name!="???"){
if(CanAct()){
if(ability.cooldown==0&&GetMana()>=ability.manaCost){
if(key.bHeld||key.bReleased&&&ability==castPrepAbility&&GetState()==State::PREP_CAST){
if(AllowedToCast(ability)&&ability.action(this,{})){
ability.cooldown=ability.COOLDOWN_TIME;
mana-=ability.manaCost;
}else
if(ability.precastInfo.precastTargetingRequired&&GetState()==State::NORMAL){
PrepareCast(ability);
}
}
} else
if(ability.cooldown==0&&GetMana()<ability.manaCost&&key.bPressed){
notEnoughManaDisplay={ability.name,1};
}
}
}
};
CheckAndPerformAbility(GetAbility1(),game->GetKey(Crawler::KEY_ABILITY1));
CheckAndPerformAbility(GetAbility2(),game->GetKey(Crawler::KEY_ABILITY2));
CheckAndPerformAbility(GetAbility3(),game->GetKey(Crawler::KEY_ABILITY3));
CheckAndPerformAbility(GetAbility4(),game->GetKey(Crawler::KEY_ABILITY4));
CheckAndPerformAbility(GetRightClickAbility(),game->GetMouse(1));
if(GetState()==State::PREP_CAST){
#define CheckAbilityKeyReleasedAndCastSpell(ability,input) \
if(&ability==castPrepAbility&&input.bReleased){CastSpell(ability);}
CheckAbilityKeyReleasedAndCastSpell(rightClickAbility,game->GetMouse(1))
else
CheckAbilityKeyReleasedAndCastSpell(ability,game->GetKey(Crawler::KEY_ABILITY1))
else
CheckAbilityKeyReleasedAndCastSpell(ability2,game->GetKey(Crawler::KEY_ABILITY2))
else
CheckAbilityKeyReleasedAndCastSpell(ability3,game->GetKey(Crawler::KEY_ABILITY3))
else
CheckAbilityKeyReleasedAndCastSpell(ability4,game->GetKey(Crawler::KEY_ABILITY4))
}
#pragma region Warrior
switch(GetState()){
case SWING_SWORD:{
switch(GetFacingDirection()){
case UP:{
UpdateAnimation("WARRIOR_SWINGSWORD_N");
}break;
case DOWN:{
UpdateAnimation("WARRIOR_SWINGSWORD_S");
}break;
case LEFT:{
UpdateAnimation("WARRIOR_SWINGSWORD_W");
}break;
case RIGHT:{
UpdateAnimation("WARRIOR_SWINGSWORD_E");
}break;
}
SetSwordSwingTimer(GetSwordSwingTimer()-fElapsedTime);
if(GetSwordSwingTimer()<=0){
SetSwordSwingTimer(0);
SetState(State::NORMAL);
}
}break;
}
#pragma endregion
#pragma region Ranger
if(GetState()==SHOOT_ARROW){
if(attack_cooldown_timer<=ARROW_ATTACK_COOLDOWN-0.3){
SetState(NORMAL);
}
}
if(retreatTimer>0){
SetZ(6*sin(PI/RETREAT_TIME*retreatTimer));
retreatTimer-=fElapsedTime;
if(retreatTimer<=0){
SetVelocity({});
SetZ(0);
SetState(State::NORMAL);
}
}
if(ghostRemoveTimer>0){
ghostRemoveTimer-=fElapsedTime;
if(ghostRemoveTimer<=0){
if(ghostPositions.size()>0){
ghostPositions.erase(ghostPositions.begin());
if(ghostPositions.size()>0){
ghostRemoveTimer=RETREAT_GHOST_FRAME_DELAY;
}
}
}
}
if(ghostFrameTimer>0){
ghostFrameTimer-=fElapsedTime;
if(ghostFrameTimer<=0&&GetState()==State::RETREAT){
ghostPositions.push_back(GetPos()+vf2d{0,-GetZ()});
ghostFrameTimer=RETREAT_GHOST_FRAME_DELAY;
}
}
if(rapidFireTimer>0){
rapidFireTimer-=fElapsedTime;
if(rapidFireTimer<=0){
if(remainingRapidFireShots>0){
remainingRapidFireShots--;
geom2d::line pointTowardsCursor(GetPos(),game->GetWorldMousePos());
vf2d extendedLine=pointTowardsCursor.upoint(1.1);
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,12*"Ranger.Ability 1.ArrowRadius"_F/100,GetAttack()*"Ranger.Ability 1.DamageMult"_F,OnUpperLevel(),true)));
SetAnimationBasedOnTargetingDirection(angleToCursor);
rapidFireTimer=RAPID_FIRE_SHOOT_DELAY;
}else{
SetState(State::NORMAL);
}
}
}
#pragma endregion
}
float Player::GetSwordSwingTimer(){
return swordSwingTimer;
}
void Player::SetSwordSwingTimer(float val){
swordSwingTimer=val;
}
void Player::SetState(State newState){
if(GetState()==State::BLOCK){
RemoveAllBuffs(BuffType::BLOCK_SLOWDOWN);
}
state=newState;
}
vf2d Player::GetVelocity(){
return vel;
}
bool Player::CanMove(){
return state!=State::CASTING&&state!=State::ANIMATION_LOCK;
}
bool Player::CanAct(){
return state!=State::CASTING&&state!=State::ANIMATION_LOCK;
}
bool Player::HasIframes(){
return iframe_time>0;
}
bool Player::Hurt(int damage,bool onUpperLevel){
if(hp<=0||HasIframes()||OnUpperLevel()!=onUpperLevel) return false;
if(GetState()==State::BLOCK)damage*=1-"Warrior.Right Click Ability.DamageReduction"_F;
float mod_dmg=damage;
for(Buff&b:GetBuffs(BuffType::DAMAGE_REDUCTION)){
mod_dmg-=damage*b.intensity;
}
hp=std::max(0,hp-int(mod_dmg));
if(lastHitTimer>0){
damageNumberPtr.get()->damage+=int(mod_dmg);
damageNumberPtr.get()->pauseTime=0.4;
} else {
DAMAGENUMBER_LIST.push_back(std::make_shared<DamageNumber>(pos,int(mod_dmg)));
std::shared_ptr<DamageNumber>numb=*(DAMAGENUMBER_LIST.end()-1);
damageNumberPtr=numb;
}
lastHitTimer=0.05;
return true;
}
void Player::AddAnimation(std::string state){
animation.AddState(state,ANIMATION_DATA[state]);
}
void Player::UpdateAnimation(std::string animState,int specificClass){
if(specificClass==ANY||specificClass&GetClass()){
animation.ChangeState(internal_animState,animState);
}
}
Animate2D::Frame Player::GetFrame(){
return animation.GetFrame(internal_animState);
}
void Player::SetLastReleasedMovementKey(Key k){
lastReleasedMovementKey=k;
}
Key Player::GetLastReleasedMovementKey(){
return lastReleasedMovementKey;
}
void Player::SetFacingDirection(Key direction){
facingDirection=direction;
}
Key Player::GetFacingDirection(){
return facingDirection;
}
void Player::Moved(){
for(MonsterSpawner&spawner:SPAWNER_LIST){
if(!spawner.SpawnTriggered()&&spawner.DoesUpperLevelSpawning()==OnUpperLevel()&&geom2d::contains(geom2d::rect<float>{spawner.GetPos(),spawner.GetRange()},pos)){
spawner.SetTriggered(true);
}
}
ZoneData&zoneData=game->GetZoneData(game->GetCurrentLevel());
for(geom2d::rect<int>&upperLevelZone:zoneData["UpperZone"]){
if(geom2d::overlaps(upperLevelZone,pos)){
upperLevel=true;
}
}
for(geom2d::rect<int>&lowerLevelZone:zoneData["LowerZone"]){
if(geom2d::overlaps(lowerLevelZone,pos)){
upperLevel=false;
}
}
}
void Player::Spin(float duration,float spinSpd){
SetState(State::SPIN);
spin_attack_timer=duration;
spin_spd=spinSpd;
spin_angle=0;
}
void Player::UpdateWalkingAnimation(Key direction){
std::string anim;
switch(direction){
case UP:anim=GetWalkNAnimation();break;
case RIGHT:anim=GetWalkEAnimation();break;
case DOWN:anim=GetWalkSAnimation();break;
case LEFT:anim=GetWalkWAnimation();break;
}
UpdateAnimation(anim);
}
void Player::UpdateIdleAnimation(Key direction){
std::string anim;
switch(direction){
case UP:anim=GetIdleNAnimation();break;
case RIGHT:anim=GetIdleEAnimation();break;
case DOWN:anim=GetIdleSAnimation();break;
case LEFT:anim=GetIdleWAnimation();break;
}
UpdateAnimation(anim);
}
void Player::AddBuff(BuffType type,float duration,float intensity){
buffList.push_back(Buff{type,duration,intensity});
}
bool Player::OnUpperLevel(){
return upperLevel;
}
std::vector<Buff>Player::GetBuffs(BuffType buff){
std::vector<Buff>filteredBuffs;
std::copy_if(buffList.begin(),buffList.end(),std::back_inserter(filteredBuffs),[buff](Buff&b){return b.type==buff;});
return filteredBuffs;
}
void Player::RemoveBuff(BuffType buff){
for(auto it=buffList.begin();it!=buffList.end();++it){
Buff&b=*it;
if(b.type==buff){
buffList.erase(it);
return;
}
}
}
void Player::RemoveAllBuffs(BuffType buff){
std::erase_if(buffList,[&](Buff&b){return b.type==buff;});
}
void Player::RemoveAllBuffs(){
buffList.clear();
}
void Player::CastSpell(Ability&ability){
vf2d castPosition=game->GetWorldMousePos();
float distance=sqrt(pow(GetX()-game->GetWorldMousePos().x,2)+pow(GetY()-game->GetWorldMousePos().y,2));
if(distance>ability.precastInfo.range){//Clamp the distance.
vf2d pointToCursor = {game->GetWorldMousePos().x-GetX(),game->GetWorldMousePos().y-GetY()};
pointToCursor=pointToCursor.norm()*ability.precastInfo.range;
castPosition=GetPos()+pointToCursor;
}
castInfo={ability.name,ability.precastInfo.castTime,ability.precastInfo.castTime,castPosition};
SetState(State::CASTING);
}
CastInfo&Player::GetCastInfo(){
return castInfo;
}
bool Player::CanPathfindTo(vf2d pos,vf2d targetPos,float range){
std::vector<vf2d>pathing=game->pathfinder.Solve_AStar(pos,targetPos,range,upperLevel);
return pathing.size()>0&&pathing.size()<range;//We'll say 7 tiles or less is close enough to 650 range. Have a little bit of wiggle room.
}
void Player::PrepareCast(Ability&ability){
castPrepAbility=&ability;
if(ability.precastInfo.range>0){
SetState(State::PREP_CAST);
}else{
CastSpell(ability);
}
}
void Player::SetVelocity(vf2d vel){
this->vel=vel;
}
void Player::SetAnimationBasedOnTargetingDirection(float targetDirection){
if(GetClass()==Class::RANGER){
if(targetDirection<=PI/4&&targetDirection>-PI/4){
UpdateAnimation("RANGER_SHOOT_E");
} else
if(targetDirection>=3*PI/4||targetDirection<-3*PI/4){
UpdateAnimation("RANGER_SHOOT_W");
} else
if(targetDirection<=3*PI/4&&targetDirection>PI/4){
UpdateAnimation("RANGER_SHOOT_S");
} else
if(targetDirection>=-3*PI/4&&targetDirection<-PI/4){
UpdateAnimation("RANGER_SHOOT_N");
}
}
}