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.
1507 lines
46 KiB
1507 lines
46 KiB
#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 "Monster.h"
|
|
#include "Player.h"
|
|
#include "AdventuresInLestoria.h"
|
|
#include "DamageNumber.h"
|
|
#include "Bullet.h"
|
|
#include "BulletTypes.h"
|
|
#include "DEFINES.h"
|
|
#include "safemap.h"
|
|
#include "util.h"
|
|
#include "Key.h"
|
|
#include "Menu.h"
|
|
#include "GameState.h"
|
|
#include "MenuComponent.h"
|
|
#include "config.h"
|
|
#include <string_view>
|
|
#include "SoundEffect.h"
|
|
#include "olcPGEX_Gamepad.h"
|
|
#include "ProgressBar.h"
|
|
#include "MenuLabel.h"
|
|
#include "GameSettings.h"
|
|
#include "Unlock.h"
|
|
#include "Tutorial.h"
|
|
#ifndef __EMSCRIPTEN__
|
|
#include "steam/isteamuserstats.h"
|
|
#endif
|
|
|
|
INCLUDE_MONSTER_DATA
|
|
INCLUDE_MONSTER_LIST
|
|
INCLUDE_ANIMATION_DATA
|
|
INCLUDE_SPAWNER_LIST
|
|
INCLUDE_BULLET_LIST
|
|
INCLUDE_DAMAGENUMBER_LIST
|
|
INCLUDE_game
|
|
INCLUDE_DATA
|
|
|
|
float Player::GROUND_SLAM_SPIN_TIME=0.6f;
|
|
const bool Player::INVERTED=true;
|
|
const bool Player::USE_WALK_DIR=true;
|
|
|
|
InputGroup Player::KEY_ABILITY1;
|
|
InputGroup Player::KEY_ABILITY2;
|
|
InputGroup Player::KEY_ABILITY3;
|
|
InputGroup Player::KEY_ABILITY4;
|
|
InputGroup Player::KEY_DEFENSIVE;
|
|
InputGroup Player::KEY_ITEM1;
|
|
InputGroup Player::KEY_ITEM2;
|
|
InputGroup Player::KEY_ITEM3;
|
|
|
|
std::vector<std::weak_ptr<MenuComponent>>Player::moneyListeners;
|
|
|
|
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;
|
|
SetBaseStat("Health",hp);
|
|
SetBaseStat("Mana",mana);
|
|
SetBaseStat("Defense",0);
|
|
SetBaseStat("Attack","Warrior.BaseAtk"_I);
|
|
SetBaseStat("Move Spd %",100);
|
|
SetBaseStat("CDR",0);
|
|
SetBaseStat("Crit Rate","Player.Crit Rate"_F);
|
|
SetBaseStat("Crit Dmg","Player.Crit Dmg"_F);
|
|
SetBaseStat("Health %",0);
|
|
SetBaseStat("HP6 Recovery %",0);
|
|
cooldownSoundInstance=Audio::Engine().LoadSound("spell_cast.ogg"_SFX);
|
|
}
|
|
|
|
void Player::ForceSetPos(vf2d pos){
|
|
this->pos=pos;
|
|
Moved();
|
|
}
|
|
|
|
bool Player::_SetX(float x,const bool playerInvoked){
|
|
vf2d newPos={x,pos.y};
|
|
vi2d tilePos=vi2d(newPos/float(game->GetCurrentMapData().tilewidth))*game->GetCurrentMapData().tilewidth;
|
|
geom2d::rect<float>collisionRect=game->GetTileCollision(game->GetCurrentLevel(),newPos,upperLevel);
|
|
#pragma region lambdas
|
|
auto NoTileCollisionExistsHere=[&](){return collisionRect==game->NO_COLLISION;};
|
|
#pragma endregion
|
|
if(NoTileCollisionExistsHere()){
|
|
pos.x=std::clamp(x,game->GetCurrentMapData().tilewidth/2.f*GetSizeMult(),float(game->GetCurrentMapData().width*game->GetCurrentMapData().tilewidth-game->GetCurrentMapData().tilewidth/2.f*GetSizeMult()));
|
|
Moved();
|
|
return true;
|
|
} else {
|
|
geom2d::rect<float>collision={collisionRect.pos,collisionRect.size};
|
|
#pragma region lambdas
|
|
auto NoPlayerCollisionWithTile=[&](){return !geom2d::overlaps(geom2d::circle<float>(newPos,4),collision);};
|
|
#pragma endregion
|
|
collision.pos+=tilePos;
|
|
if(NoPlayerCollisionWithTile()){
|
|
pos.x=std::clamp(x,game->GetCurrentMapData().tilewidth/2.f*GetSizeMult(),float(game->GetCurrentMapData().width*game->GetCurrentMapData().tilewidth-game->GetCurrentMapData().tilewidth/2.f*GetSizeMult()));
|
|
Moved();
|
|
return true;
|
|
}else
|
|
if(playerInvoked){ //If player invoked, we'll try the smart move system.
|
|
vf2d pushDir=geom2d::line<float>(collision.middle(),pos).vector().norm();
|
|
newPos={newPos.x,pos.y+pushDir.y*12};
|
|
if(NoPlayerCollisionWithTile()){
|
|
return _SetY(pos.y+pushDir.y*game->GetElapsedTime()*12,false);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
bool Player::_SetY(float y,const bool playerInvoked){
|
|
vf2d newPos={pos.x,y};
|
|
vi2d tilePos=vi2d(newPos/float(game->GetCurrentMapData().tilewidth))*game->GetCurrentMapData().tilewidth;
|
|
geom2d::rect<float>collisionRect=game->GetTileCollision(game->GetCurrentLevel(),newPos,upperLevel);
|
|
#pragma region lambdas
|
|
auto NoTileCollisionExistsHere=[&](){return collisionRect==game->NO_COLLISION;};
|
|
#pragma endregion
|
|
if(NoTileCollisionExistsHere()){
|
|
pos.y=std::clamp(y,game->GetCurrentMapData().tilewidth/2.f*GetSizeMult(),float(game->GetCurrentMapData().height*game->GetCurrentMapData().tilewidth-game->GetCurrentMapData().tilewidth/2.f*GetSizeMult()));
|
|
Moved();
|
|
return true;
|
|
} else {
|
|
geom2d::rect<float>collision={collisionRect.pos,collisionRect.size};
|
|
#pragma region lambdas
|
|
auto NoPlayerCollisionWithTile=[&](){return !geom2d::overlaps(geom2d::circle<float>(newPos,4),collision);};
|
|
#pragma endregion
|
|
collision.pos+=tilePos;
|
|
if(NoPlayerCollisionWithTile()){
|
|
pos.y=std::clamp(y,game->GetCurrentMapData().tilewidth/2.f*GetSizeMult(),float(game->GetCurrentMapData().height*game->GetCurrentMapData().tilewidth-game->GetCurrentMapData().tilewidth/2.f*GetSizeMult()));
|
|
Moved();
|
|
return true;
|
|
}else
|
|
if(playerInvoked){ //If player invoked, we'll try the smart move system.{
|
|
vf2d pushDir=geom2d::line<float>(collision.middle(),pos).vector().norm();
|
|
newPos={pos.x+pushDir.x*12,newPos.y};
|
|
if(NoPlayerCollisionWithTile()){
|
|
return _SetX(pos.x+pushDir.x*game->GetElapsedTime()*12,false);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Player::SetX(float x){
|
|
return _SetX(x);
|
|
}
|
|
|
|
bool Player::SetY(float y){
|
|
return _SetY(y);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
const int Player::GetHealth()const{
|
|
return hp;
|
|
}
|
|
|
|
const float&Player::GetMaxHealth()const{
|
|
return GetStat("Health");
|
|
}
|
|
|
|
const int Player::GetMana()const{
|
|
return mana;
|
|
}
|
|
|
|
const int Player::GetMaxMana()const{
|
|
return GetStat("Mana");
|
|
}
|
|
|
|
const int Player::GetAttack(){
|
|
float mod_atk=float(GetStat("Attack"));
|
|
mod_atk+=Get("Attack");
|
|
mod_atk+=Get("Attack %");
|
|
return int(mod_atk);
|
|
}
|
|
|
|
float Player::GetMoveSpdMult(){
|
|
float moveSpdPct=GetStat("Move Spd %")/100.f;
|
|
float mod_moveSpd=moveSpdPct;
|
|
mod_moveSpd+=ItemAttribute::Get("Move Spd %",this);
|
|
for(const Buff&b:GetBuffs(BuffType::SLOWDOWN)){
|
|
mod_moveSpd-=moveSpdPct*b.intensity;
|
|
}
|
|
for(const Buff&b:GetBuffs(BuffType::BLOCK_SLOWDOWN)){
|
|
mod_moveSpd-=moveSpdPct*b.intensity;
|
|
}
|
|
for(const Buff&b:GetBuffs(LOCKON_SPEEDBOOST)){
|
|
mod_moveSpd+=moveSpdPct*b.intensity;
|
|
}
|
|
for(const Buff&b:GetBuffs(SPEEDBOOST)){
|
|
mod_moveSpd+=moveSpdPct*b.intensity;
|
|
}
|
|
return mod_moveSpd;
|
|
}
|
|
|
|
float Player::GetSizeMult(){
|
|
return size;
|
|
}
|
|
|
|
float Player::GetAttackRangeMult(){
|
|
return attack_range*GetSizeMult();
|
|
}
|
|
|
|
float Player::GetSpinAngle(){
|
|
return spin_angle;
|
|
}
|
|
|
|
State::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(),
|
|
&item1=useItem1,
|
|
&item2=useItem2,
|
|
&item3=useItem3;
|
|
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);
|
|
lastPathfindingCooldown=std::max(0.f,lastPathfindingCooldown-fElapsedTime);
|
|
lowHealthSoundPlayedTimer=std::max(0.f,lowHealthSoundPlayedTimer-fElapsedTime);
|
|
if(hurtRumbleTime>0.f){
|
|
hurtRumbleTime=std::max(0.f,hurtRumbleTime-fElapsedTime);
|
|
if(hurtRumbleTime==0.f){
|
|
Input::StopVibration();
|
|
}
|
|
}
|
|
blockTimer=std::max(0.f,blockTimer-fElapsedTime);
|
|
lastCombatTime=lastCombatTime+fElapsedTime;
|
|
manaTickTimer-=fElapsedTime;
|
|
hpRecoveryTimer=std::max(0.f,hpRecoveryTimer-fElapsedTime);
|
|
hp6RecoveryTimer=std::max(0.f,hp6RecoveryTimer-fElapsedTime);
|
|
hp4RecoveryTimer=std::max(0.f,hp4RecoveryTimer-fElapsedTime);
|
|
|
|
PerformHPRecovery();
|
|
|
|
CheckEndZoneCollision();
|
|
|
|
if(castInfo.castTimer>0){
|
|
castInfo.castTimer-=fElapsedTime;
|
|
if(castInfo.castTimer<=0){
|
|
bool allowed=castPrepAbility->actionPerformedDuringCast;
|
|
SetState(State::NORMAL);
|
|
if(!allowed&&castPrepAbility->action(this,castInfo.castPos))allowed=true;
|
|
if(allowed){
|
|
castPrepAbility->cooldown=castPrepAbility->GetCooldownTime();
|
|
ConsumeMana(castPrepAbility->manaCost);
|
|
}
|
|
castInfo.castTimer=0;
|
|
}
|
|
}
|
|
|
|
if(state==State::CASTING){
|
|
if(!Audio::Engine().IsPlaying(cooldownSoundInstance)){
|
|
Audio::Engine().SetVolume(cooldownSoundInstance,Audio::GetCalculatedSFXVolume("Audio.Casting Sound Volume"_F/100.f));
|
|
Audio::Engine().Play(cooldownSoundInstance,true);
|
|
}
|
|
}else{
|
|
Audio::Engine().Stop(cooldownSoundInstance);
|
|
}
|
|
|
|
while(manaTickTimer<=0){
|
|
manaTickTimer+=0.2f;
|
|
RestoreMana(1,true);
|
|
}
|
|
for(Buff&b:buffList){
|
|
b.duration-=fElapsedTime;
|
|
if(b.nextTick>0&&b.duration<b.nextTick){
|
|
b.repeatAction(game,b.intensity);
|
|
b.nextTick-=b.timeBetweenTicks;
|
|
}
|
|
}
|
|
std::erase_if(buffList,[](Buff&b){return b.duration<=0;});
|
|
//Class-specific update events.
|
|
OnUpdate(fElapsedTime);
|
|
switch(state){
|
|
case State::SPIN:{
|
|
switch(facingDirection){
|
|
case UP:{
|
|
if(lastAnimationFlip==0){
|
|
lastAnimationFlip=0.03f;
|
|
facingDirection=DOWN;
|
|
animation.ChangeState(internal_animState,"WARRIOR_WALK_S");
|
|
}
|
|
}break;
|
|
case DOWN:{
|
|
if(lastAnimationFlip==0){
|
|
lastAnimationFlip=0.03f;
|
|
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=float("Warrior.Ability 2.SpinMaxHeight"_I)*sin(3.3f*(GROUND_SLAM_SPIN_TIME-spin_attack_timer)/GROUND_SLAM_SPIN_TIME);
|
|
spin_attack_timer=std::max(0.f,spin_attack_timer-fElapsedTime);
|
|
} else {
|
|
SetState(State::NORMAL);
|
|
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);
|
|
#pragma region Knockback effect
|
|
for(auto&[monsterPtr,wasHurt]:hitEnemies){
|
|
float knockbackDir=0;
|
|
float knockbackAmt=0;
|
|
if(geom2d::line<float>(GetPos(),monsterPtr->GetPos()).length()<=0.001f){
|
|
knockbackDir=util::random(2*PI);
|
|
knockbackAmt="Warrior.Ability 2.KnockbackAmt"_F;
|
|
}else{
|
|
knockbackDir=geom2d::line<float>(GetPos(),monsterPtr->GetPos()).vector().norm().polar().y;
|
|
knockbackAmt=std::clamp("Warrior.Ability 2.KnockbackAmt"_F-geom2d::line<float>(GetPos(),monsterPtr->GetPos()).length()*"Warrior.Ability 2.KnockbackReduction"_F,1.f,"Warrior.Ability 2.KnockbackAmt"_F);
|
|
}
|
|
knockbackAmt=std::max(1.f,knockbackAmt-"Warrior.Ability 2.KnockbackWeightFactor"_F*(monsterPtr->GetSizeMult()-1.f));
|
|
monsterPtr->Knockback(vf2d{knockbackAmt,knockbackDir}.cart());
|
|
}
|
|
#pragma endregion
|
|
game->AddEffect(std::make_unique<Effect>(GetPos(),"Warrior.Ability 2.EffectLifetime"_F,"ground-slam-attack-front.png",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.png",upperLevel,"Warrior.Ability 2.Range"_F/300*1.33f,"Warrior.Ability 2.EffectFadetime"_F));
|
|
SoundEffect::PlaySFX("Warrior Ground Slam",SoundEffect::CENTERED);
|
|
}
|
|
if(lastAnimationFlip>0){
|
|
lastAnimationFlip=std::max(0.f,lastAnimationFlip-fElapsedTime);
|
|
}
|
|
animation.UpdateState(internal_animState,fElapsedTime);
|
|
}break;
|
|
case State::BLOCK:{
|
|
if(blockTimer<=0){
|
|
SetState(State::NORMAL);
|
|
}
|
|
}break;
|
|
case State::SWING_SONIC_SWORD:{
|
|
if(ability3.GetCooldownTime()-ability3.cooldown>0.5){
|
|
SetState(State::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 State::TELEPORT:{
|
|
teleportAnimationTimer=std::max(0.f,teleportAnimationTimer-fElapsedTime);
|
|
if(teleportAnimationTimer<=0){
|
|
ForceSetPos(teleportTarget);
|
|
SetState(State::NORMAL);
|
|
}
|
|
animation.UpdateState(internal_animState,fElapsedTime);
|
|
}break;
|
|
default:{
|
|
//Update animations normally.
|
|
animation.UpdateState(internal_animState,fElapsedTime);
|
|
}
|
|
}
|
|
float cooldownMultiplier;
|
|
if(game->GetPlayer()->GetCooldownReductionPct()>=1.0f){
|
|
cooldownMultiplier=999999;
|
|
}else{
|
|
cooldownMultiplier=1/(1-game->GetPlayer()->GetCooldownReductionPct());
|
|
}
|
|
rightClickAbility.cooldown-=fElapsedTime*cooldownMultiplier;
|
|
ability.cooldown-=fElapsedTime*cooldownMultiplier;
|
|
ability2.cooldown-=fElapsedTime*cooldownMultiplier;
|
|
ability3.cooldown-=fElapsedTime*cooldownMultiplier;
|
|
ability4.cooldown-=fElapsedTime*cooldownMultiplier;
|
|
item1.cooldown-=fElapsedTime;
|
|
item2.cooldown-=fElapsedTime;
|
|
item3.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;
|
|
}
|
|
if(item1.cooldown<0){
|
|
item1.cooldown=0;
|
|
}
|
|
if(item2.cooldown<0){
|
|
item2.cooldown=0;
|
|
}
|
|
if(item3.cooldown<0){
|
|
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(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->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;
|
|
}
|
|
}
|
|
}
|
|
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(Menu::stack.empty()){
|
|
|
|
if(CanAct()&&attack_cooldown_timer==0&&AiL::KEY_ATTACK.Held()){
|
|
Tutorial::GetTask(TutorialTaskName::USE_ATTACK).I(A::ATTACK_COUNT)++;
|
|
AutoAttack();
|
|
}
|
|
|
|
auto AllowedToCast=[&](Ability&ability){return !ability.precastInfo.precastTargetingRequired&&GetState()!=State::ANIMATION_LOCK;};
|
|
auto HasEnoughOfItem=[&](Ability&ability){
|
|
if(!ability.itemAbility)return true;
|
|
if(&ability==&item1&&game->GetLoadoutItem(0).lock()->Amt()>0)return true;
|
|
if(&ability==&item2&&game->GetLoadoutItem(1).lock()->Amt()>0)return true;
|
|
if(&ability==&item3&&game->GetLoadoutItem(2).lock()->Amt()>0)return true;
|
|
return false;
|
|
};
|
|
//If pressed is set to false, uses held instead.
|
|
auto CheckAndPerformAbility=[&](Ability&ability,InputGroup key){
|
|
if(ability.name!="???"){
|
|
if(CanAct(ability)){
|
|
if(ability.cooldown==0&&GetMana()>=ability.manaCost){
|
|
if(key.Held()||key.Released()&&&ability==castPrepAbility&&GetState()==State::PREP_CAST){
|
|
#pragma region Tutorial Defensive/Use Ability Tasks
|
|
if(&ability==&rightClickAbility){
|
|
Tutorial::GetTask(TutorialTaskName::USE_DEFENSIVE).I(A::DEFENSIVE_COUNT)++;
|
|
}else{
|
|
Tutorial::GetTask(TutorialTaskName::USE_ABILITIES).I(A::ABILITY_COUNT)++;
|
|
}
|
|
#pragma endregion
|
|
if(AllowedToCast(ability)&&ability.action(this,{})){
|
|
bool allowed=ability.actionPerformedDuringCast;
|
|
ability.cooldown=ability.GetCooldownTime();
|
|
CancelCast();
|
|
ConsumeMana(ability.manaCost);
|
|
}else
|
|
if(ability.precastInfo.precastTargetingRequired&&GetState()==State::NORMAL
|
|
&&HasEnoughOfItem(ability)){
|
|
PrepareCast(ability);
|
|
}
|
|
}
|
|
} else
|
|
if(ability.cooldown==0&&GetMana()<ability.manaCost&&key.Pressed()){
|
|
notEnoughManaDisplay={ability.name,1.f};
|
|
}
|
|
}else
|
|
if(key.Released()||!key.Held())ability.waitForRelease=false;
|
|
}
|
|
};
|
|
CheckAndPerformAbility(rightClickAbility,Player::KEY_DEFENSIVE);
|
|
CheckAndPerformAbility(ability,Player::KEY_ABILITY1);
|
|
CheckAndPerformAbility(ability2,Player::KEY_ABILITY2);
|
|
CheckAndPerformAbility(ability3,Player::KEY_ABILITY3);
|
|
CheckAndPerformAbility(ability4,Player::KEY_ABILITY4);
|
|
CheckAndPerformAbility(item1,Player::KEY_ITEM1);
|
|
CheckAndPerformAbility(item2,Player::KEY_ITEM2);
|
|
CheckAndPerformAbility(item3,Player::KEY_ITEM3);
|
|
|
|
if(GetState()==State::PREP_CAST){
|
|
#define CheckAbilityKeyReleasedAndCastSpell(ability,releaseState) \
|
|
if(&ability==castPrepAbility&&releaseState){CastSpell(ability);}
|
|
|
|
CheckAbilityKeyReleasedAndCastSpell(rightClickAbility,Player::KEY_DEFENSIVE.Released())
|
|
else
|
|
CheckAbilityKeyReleasedAndCastSpell(ability,Player::KEY_ABILITY1.Released())
|
|
else
|
|
CheckAbilityKeyReleasedAndCastSpell(ability2,Player::KEY_ABILITY2.Released())
|
|
else
|
|
CheckAbilityKeyReleasedAndCastSpell(ability3,Player::KEY_ABILITY3.Released())
|
|
else
|
|
CheckAbilityKeyReleasedAndCastSpell(ability4,Player::KEY_ABILITY4.Released())
|
|
else
|
|
CheckAbilityKeyReleasedAndCastSpell(item1,Player::KEY_ITEM1.Released())
|
|
else
|
|
CheckAbilityKeyReleasedAndCastSpell(item2,Player::KEY_ITEM2.Released())
|
|
else
|
|
CheckAbilityKeyReleasedAndCastSpell(item3,Player::KEY_ITEM3.Released())
|
|
}
|
|
}
|
|
|
|
#pragma region Warrior
|
|
switch(GetState()){
|
|
case State::SWING_SWORD:{
|
|
SetSwordSwingTimer(GetSwordSwingTimer()-fElapsedTime);
|
|
if(GetSwordSwingTimer()<=0){
|
|
SetSwordSwingTimer(0);
|
|
SetState(State::NORMAL);
|
|
}
|
|
}break;
|
|
}
|
|
#pragma endregion
|
|
|
|
#pragma region Ranger
|
|
if(GetState()==State::SHOOT_ARROW){
|
|
if(rangerShootAnimationTimer>0.f){
|
|
rangerShootAnimationTimer=std::max(0.f,rangerShootAnimationTimer-fElapsedTime);
|
|
}else
|
|
if(attack_cooldown_timer<=ARROW_ATTACK_COOLDOWN-0.3){
|
|
SetState(State::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(),GetWorldAimingLocation());
|
|
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,12*"Ranger.Ability 1.ArrowRadius"_F/100,int(GetAttack()*"Ranger.Ability 1.DamageMult"_F),OnUpperLevel(),true)));
|
|
SetAnimationBasedOnTargetingDirection(angleToCursor);
|
|
rapidFireTimer=RAPID_FIRE_SHOOT_DELAY;
|
|
}else{
|
|
SetState(State::NORMAL);
|
|
}
|
|
}
|
|
}
|
|
#pragma endregion
|
|
|
|
#pragma region Handle knockup timers
|
|
if(knockUpTimer>0.f){
|
|
knockUpTimer=std::max(0.f,knockUpTimer-fElapsedTime);
|
|
if(knockUpTimer==0.f){
|
|
totalKnockupTime=0.f;
|
|
knockUpZAmt=0.f;
|
|
SetZ(0.f);
|
|
}else{
|
|
SetZ(util::lerp(0.f,1.f,-(pow((knockUpTimer-totalKnockupTime/2)/(totalKnockupTime/2),2))+1)*knockUpZAmt);
|
|
}
|
|
}
|
|
#pragma endregion
|
|
}
|
|
|
|
float Player::GetSwordSwingTimer(){
|
|
return swordSwingTimer;
|
|
}
|
|
|
|
void Player::SetSwordSwingTimer(float val){
|
|
swordSwingTimer=val;
|
|
}
|
|
|
|
void Player::SetState(State::State newState){
|
|
if(GetState()==State::BLOCK){
|
|
RemoveAllBuffs(BuffType::BLOCK_SLOWDOWN);
|
|
}
|
|
state=newState;
|
|
}
|
|
|
|
vf2d Player::GetVelocity(){
|
|
return vel;
|
|
}
|
|
|
|
bool Player::CanMove(){
|
|
return knockUpTimer==0.f&&state!=State::ANIMATION_LOCK&&(state!=State::CASTING||(castInfo.castTotalTime-castInfo.castTimer>0.2f));
|
|
}
|
|
|
|
bool Player::CanAct(){
|
|
Ability dummyAbility;
|
|
return CanAct(dummyAbility);
|
|
}
|
|
|
|
bool Player::CanAct(Ability&ability){
|
|
return knockUpTimer==0&&!ability.waitForRelease&&(ability.canCancelCast||state!=State::CASTING)&&state!=State::ANIMATION_LOCK&&
|
|
(GameState::STATE==GameState::states[States::GAME_RUN]
|
|
||GameState::STATE==GameState::states[States::GAME_HUB]&&!ability.itemAbility);
|
|
}
|
|
|
|
bool Player::HasIframes(){
|
|
return iframe_time>0;
|
|
}
|
|
|
|
bool Player::Hurt(int damage,bool onUpperLevel,float z){
|
|
if(hp<=0||HasIframes()||OnUpperLevel()!=onUpperLevel||abs(GetZ()-z)>1) return false;
|
|
float mod_dmg=float(damage);
|
|
if(GetState()==State::BLOCK){
|
|
mod_dmg=0;
|
|
SoundEffect::PlaySFX("Warrior Block Hit",SoundEffect::CENTERED);
|
|
}else{
|
|
float otherDmgTaken=1-GetDamageReductionFromBuffs();
|
|
float armorDmgTaken=1-GetDamageReductionFromArmor();
|
|
lastCombatTime=0;
|
|
|
|
float finalPctDmgTaken=armorDmgTaken*otherDmgTaken;
|
|
|
|
if(finalPctDmgTaken<=6._Pct){
|
|
LOG("WARNING! Damage Reduction has somehow ended up below 6%, which is over the cap!");
|
|
}
|
|
|
|
finalPctDmgTaken=std::max(6.25_Pct,finalPctDmgTaken);//Apply Damage Cap.
|
|
|
|
float minPctDmgReduction=0.05_Pct*GetStat("Defense");
|
|
float finalPctDmgReduction=1-finalPctDmgTaken;
|
|
|
|
float pctDmgReductionDiff=finalPctDmgReduction-minPctDmgReduction;
|
|
float dmgRoll=minPctDmgReduction+util::random(pctDmgReductionDiff);
|
|
|
|
mod_dmg*=1-dmgRoll;
|
|
|
|
mod_dmg=std::ceil(mod_dmg);
|
|
|
|
SoundEffect::PlaySFX("Player Hit",SoundEffect::CENTERED);
|
|
}
|
|
if(Menu::IsMenuOpen()&&mod_dmg>0)Menu::CloseAllMenus();
|
|
|
|
if(mod_dmg>0)game->ShowDamageVignetteOverlay();
|
|
|
|
hp=std::max(0,hp-int(mod_dmg));
|
|
|
|
if(hp==0&&GameState::STATE!=GameState::states[States::DEATH])GameState::ChangeState(States::DEATH);
|
|
|
|
hurtRumbleTime="Player.Hurt Rumble Time"_F;
|
|
Input::StartVibration();
|
|
Input::SetLightbar(PixelLerp(DARK_RED,GREEN,GetHealth()/GetMaxHealth()));
|
|
|
|
if(lastHitTimer>0){
|
|
damageNumberPtr.get()->damage+=int(mod_dmg);
|
|
damageNumberPtr.get()->pauseTime=0.4f;
|
|
damageNumberPtr.get()->RecalculateSize();
|
|
} else {
|
|
damageNumberPtr=std::make_shared<DamageNumber>(pos,int(mod_dmg),true);
|
|
DAMAGENUMBER_LIST.push_back(damageNumberPtr);
|
|
}
|
|
lastHitTimer=0.05f;
|
|
|
|
if(!lowHealthSoundPlayed&&lowHealthSoundPlayedTimer==0.f&&GetHealth()/GetMaxHealth()<="Player.Health Warning Pct"_F/100.f){
|
|
SoundEffect::PlaySFX("Health Warning",SoundEffect::CENTERED);
|
|
lowHealthSoundPlayed=true;
|
|
lowHealthSoundPlayedTimer="Player.Health Warning Cooldown"_F;
|
|
}
|
|
|
|
if(game->GetPlayer()->GetHealth()<game->GetPlayer()->GetMaxHealth()*0.5f&&!Tutorial::TaskIsComplete(TutorialTaskName::USE_RECOVERY_ITEMS)){
|
|
Tutorial::SetNextTask(TutorialTaskName::USE_RECOVERY_ITEMS);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Player::AddAnimation(std::string state){
|
|
animation.AddState(state,ANIMATION_DATA.at(state));
|
|
}
|
|
|
|
void Player::UpdateAnimation(std::string animState,int specificClass, const float frameMult){
|
|
if(specificClass==ANY||specificClass&GetClass()){
|
|
animation.ChangeState(internal_animState,animState,frameMult);
|
|
}
|
|
}
|
|
|
|
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::CancelCast(){
|
|
bool wasCasting=castInfo.castTimer>0;
|
|
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));
|
|
}
|
|
if(state==State::CASTING){
|
|
state=State::NORMAL;
|
|
castPrepAbility->waitForRelease=true;
|
|
}
|
|
}
|
|
|
|
void Player::Moved(){
|
|
if(state==State::CASTING){
|
|
state=State::NORMAL;
|
|
castPrepAbility->waitForRelease=true;
|
|
CancelCast();
|
|
}
|
|
for(MonsterSpawner&spawner:SPAWNER_LIST){
|
|
if(!spawner.SpawnTriggered()&&spawner.DoesUpperLevelSpawning()==OnUpperLevel()&&geom2d::contains(geom2d::rect<float>{spawner.GetPos(),spawner.GetRange()},pos)){
|
|
if(GameState::STATE==GameState::states[States::GAME_RUN]){
|
|
if(!Tutorial::TaskIsComplete(TutorialTaskName::USE_ATTACK)){
|
|
Tutorial::SetNextTask(TutorialTaskName::USE_ATTACK);
|
|
}else
|
|
if(!Tutorial::TaskIsComplete(TutorialTaskName::USE_ABILITIES)){
|
|
Tutorial::SetNextTask(TutorialTaskName::USE_ABILITIES);
|
|
}else
|
|
if(game->GetPlayer()->GetHealth()<game->GetPlayer()->GetMaxHealth()*0.8f&&!Tutorial::TaskIsComplete(TutorialTaskName::USE_DEFENSIVE)){
|
|
Tutorial::SetNextTask(TutorialTaskName::USE_DEFENSIVE);
|
|
}
|
|
}
|
|
spawner.SetTriggered(true);
|
|
}
|
|
}
|
|
const std::map<std::string,std::vector<ZoneData>>&zoneData=game->GetZones(game->GetCurrentLevel());
|
|
for(const ZoneData&upperLevelZone:zoneData.at("UpperZone")){
|
|
if(geom2d::overlaps(upperLevelZone.zone,pos)){
|
|
upperLevel=true;
|
|
}
|
|
}
|
|
for(const ZoneData&lowerLevelZone:zoneData.at("LowerZone")){
|
|
if(geom2d::overlaps(lowerLevelZone.zone,pos)){
|
|
upperLevel=false;
|
|
}
|
|
}
|
|
EnvironmentalAudio::UpdateEnvironmentalAudio();
|
|
|
|
if(!std::isfinite(pos.x)){
|
|
ERR(std::format("WARNING! Player X position is {}...Trying to recover. THIS SHOULD NOT BE HAPPENING!",pos.x));
|
|
ForceSetPos({float(game->GetCurrentMapData().playerSpawnLocation.x),pos.y});
|
|
}
|
|
if(!std::isfinite(pos.y)){
|
|
ERR(std::format("WARNING! Player Y position is {}...Trying to recover. THIS SHOULD NOT BE HAPPENING!",pos.y));
|
|
ForceSetPos({pos.x,float(game->GetCurrentMapData().playerSpawnLocation.y)});
|
|
}
|
|
|
|
game->minimap.UpdateChunk(GetPos()/"Minimap.Chunk Size"_I);
|
|
}
|
|
|
|
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, const float frameMult){
|
|
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,0,frameMult);
|
|
}
|
|
|
|
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});
|
|
}
|
|
void Player::AddBuff(BuffType type,float duration,float intensity,std::set<ItemAttribute>attr){
|
|
buffList.push_back(Buff{type,duration,intensity,attr});
|
|
}
|
|
void Player::AddBuff(BuffType type,float duration,float intensity,std::set<std::string>attr){
|
|
buffList.push_back(Buff{type,duration,intensity,attr});
|
|
}
|
|
void Player::AddBuff(BuffType type,float duration,float intensity,float timeBetweenTicks,std::function<void(AiL*,int)>repeatAction){
|
|
buffList.push_back(Buff{type,duration,intensity,timeBetweenTicks,repeatAction});
|
|
}
|
|
|
|
bool Player::OnUpperLevel(){
|
|
return upperLevel;
|
|
}
|
|
|
|
const std::vector<Buff>Player::GetBuffs(BuffType buff)const{
|
|
std::vector<Buff>filteredBuffs;
|
|
std::copy_if(buffList.begin(),buffList.end(),std::back_inserter(filteredBuffs),[buff](const 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=GetWorldAimingLocation();
|
|
float distance=float(sqrt(pow(GetX()-GetWorldAimingLocation().x,2)+pow(GetY()-GetWorldAimingLocation().y,2)));
|
|
if(distance>ability.precastInfo.range){//Clamp the distance.
|
|
vf2d pointToCursor = {GetWorldAimingLocation().x-GetX(),GetWorldAimingLocation().y-GetY()};
|
|
pointToCursor=pointToCursor.norm()*ability.precastInfo.range;
|
|
castPosition=GetPos()+pointToCursor;
|
|
}
|
|
castInfo={ability.name,ability.precastInfo.castTime,ability.precastInfo.castTime,castPosition};
|
|
if(ability.actionPerformedDuringCast){ability.action(this,castPosition);}
|
|
SetState(State::CASTING);
|
|
}
|
|
|
|
CastInfo&Player::GetCastInfo(){
|
|
return castInfo;
|
|
}
|
|
|
|
bool Player::CanPathfindTo(vf2d pos,vf2d targetPos,float range){
|
|
range*=(24/game->pathfinder.gridSpacing.x);
|
|
if(targetPos.x<0||targetPos.y<0||targetPos.x>game->GetCurrentMapData().width*game->GetCurrentMapData().tilewidth||targetPos.y>game->GetCurrentMapData().height*game->GetCurrentMapData().tileheight)return false;
|
|
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){
|
|
auto FacingEast=[&](){return targetDirection<=PI/4&&targetDirection>-PI/4;};
|
|
auto FacingWest=[&](){return targetDirection>=3*PI/4||targetDirection<-3*PI/4;};
|
|
auto FacingSouth=[&](){return targetDirection<=3*PI/4&&targetDirection>PI/4;};
|
|
auto FacingNorth=[&](){return targetDirection>=-3*PI/4&&targetDirection<-PI/4;};
|
|
|
|
switch(GetClass()){
|
|
case Class::WARRIOR:
|
|
case Class::THIEF:{
|
|
if(FacingNorth()){
|
|
UpdateAnimation("WARRIOR_SWINGSWORD_N");
|
|
}else
|
|
if(FacingSouth()){
|
|
UpdateAnimation("WARRIOR_SWINGSWORD_S");
|
|
}else
|
|
if(FacingWest()){
|
|
UpdateAnimation("WARRIOR_SWINGSWORD_W");
|
|
}else
|
|
if(FacingEast()){
|
|
UpdateAnimation("WARRIOR_SWINGSWORD_E");
|
|
}
|
|
}break;
|
|
case Class::RANGER:
|
|
case Class::TRAPPER:{
|
|
if(FacingNorth()){
|
|
UpdateAnimation("RANGER_SHOOT_N");
|
|
}else
|
|
if(FacingSouth()){
|
|
UpdateAnimation("RANGER_SHOOT_S");
|
|
}else
|
|
if(FacingWest()){
|
|
UpdateAnimation("RANGER_SHOOT_W");
|
|
}else
|
|
if(FacingEast()){
|
|
UpdateAnimation("RANGER_SHOOT_E");
|
|
}
|
|
}break;
|
|
}
|
|
}
|
|
|
|
void Player::SetIframes(float duration){
|
|
iframe_time=duration;
|
|
}
|
|
|
|
bool Player::Heal(int damage,bool suppressDamageNumber){
|
|
hp=std::clamp(hp+damage,0,int(GetStat("Health")));
|
|
if(!suppressDamageNumber&&damage>0){
|
|
DAMAGENUMBER_LIST.push_back(std::make_shared<DamageNumber>(GetPos(),damage,true,HEALTH_GAIN));
|
|
}
|
|
Input::SetLightbar(PixelLerp(DARK_RED,GREEN,GetHealth()/GetMaxHealth()));
|
|
return true;
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
void Player::ConsumeMana(int amt){
|
|
mana=std::clamp(mana-amt,0,GetMaxMana());
|
|
}
|
|
|
|
void Player::SetSizeMult(float size){
|
|
this->size=size;
|
|
}
|
|
|
|
void Player::ResetLastCombatTime(){
|
|
lastCombatTime=0;
|
|
}
|
|
|
|
bool Player::IsOutOfCombat(){
|
|
return lastCombatTime>="Player.Out of Combat Time"_F;
|
|
}
|
|
|
|
float Player::GetEndZoneStandTime(){
|
|
return endZoneStandTime;
|
|
}
|
|
|
|
void Player::CheckEndZoneCollision(){
|
|
auto HasZoneData=[&](){
|
|
return game->GetZones().count("EndZone");
|
|
};
|
|
|
|
if(IsOutOfCombat()&&HasZoneData()){
|
|
for(const ZoneData&zone:game->GetZones().at("EndZone")){
|
|
if(zone.isUpper==upperLevel&&geom2d::overlaps(GetPos(),zone.zone)){
|
|
endZoneStandTime+=game->GetElapsedTime();
|
|
if(endZoneStandTime>="Player.End Zone Wait Time"_F){
|
|
Component<MenuLabel>(LEVEL_COMPLETE,"Stage Complete Label")->SetLabel("Stage Completed");
|
|
game->SetCompletedStageFlag();
|
|
Component<MenuComponent>(LEVEL_COMPLETE,"Level Details Outline")->SetLabel("Complete Bonus\n +10% XP");
|
|
if(Unlock::IsUnlocked("STORY_1_1")){
|
|
Component<MenuComponent>(LEVEL_COMPLETE,"Next Button")->Enable();
|
|
}else{
|
|
Component<MenuComponent>(LEVEL_COMPLETE,"Next Button")->Disable();
|
|
}
|
|
GameState::ChangeState(States::LEVEL_COMPLETE);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
endZoneStandTime=0;
|
|
}
|
|
|
|
|
|
void Player::SetItem1UseFunc(Ability a){
|
|
useItem1=a;
|
|
};
|
|
void Player::SetItem2UseFunc(Ability a){
|
|
useItem2=a;
|
|
};
|
|
void Player::SetItem3UseFunc(Ability a){
|
|
useItem3=a;
|
|
};
|
|
|
|
const float&Player::GetStat(std::string_view a)const{
|
|
return GetStat(ItemAttribute::Get(a));
|
|
}
|
|
|
|
const float&Player::GetStat(ItemAttribute a)const{
|
|
return stats.GetStat(a);
|
|
}
|
|
|
|
const float&Player::GetBaseStat(std::string_view a)const{
|
|
return GetBaseStat(ItemAttribute::Get(a));
|
|
}
|
|
|
|
const float&Player::GetBaseStat(ItemAttribute a)const{
|
|
return stats.GetBaseStat(a);
|
|
}
|
|
|
|
|
|
void EntityStats::RecalculateEquipStats(){
|
|
baseStats.copyTo(equipStats);
|
|
for(int i=int(EquipSlot::HELMET);i<=int(EquipSlot::RING2);i<<=1){
|
|
EquipSlot slot=EquipSlot(i);
|
|
std::weak_ptr<Item>equip=Inventory::GetEquip(slot);
|
|
if(ISBLANK(equip))continue;
|
|
for(auto&[key,size]:ItemAttribute::attributes){
|
|
equipStats.A(key)+=equip.lock()->GetStats().A_Read(key);
|
|
equipStats.A(key)+=equip.lock()->RandomStats().A_Read(key);
|
|
}
|
|
}
|
|
|
|
const std::map<ItemSet,int>&setCounts=Inventory::GetEquippedItemSets();
|
|
|
|
for(const auto&[set,equipCount]:setCounts){
|
|
const Stats&setStats=set[equipCount];
|
|
for(auto&[key,size]:ItemAttribute::attributes){
|
|
equipStats.A(key)+=setStats.A_Read(key);
|
|
}
|
|
}
|
|
game->GetPlayer()->UpdateHealthAndMana();
|
|
for(std::weak_ptr<MenuComponent>component:Menu::equipStatListeners){
|
|
component.lock()->OnEquipStatsUpdate();
|
|
}
|
|
}
|
|
|
|
const float&EntityStats::GetStat(ItemAttribute stat)const{
|
|
return equipStats.A_Read(stat);
|
|
}
|
|
|
|
const float&EntityStats::GetStat(std::string_view stat)const{
|
|
return equipStats.A_Read(stat);
|
|
}
|
|
|
|
const float&EntityStats::GetBaseStat(ItemAttribute stat)const{
|
|
return baseStats.A_Read(stat);
|
|
}
|
|
|
|
const float&EntityStats::GetBaseStat(std::string_view stat)const{
|
|
return baseStats.A_Read(stat);
|
|
}
|
|
|
|
void EntityStats::SetBaseStat(ItemAttribute stat,float val){
|
|
baseStats.A(stat)=val;
|
|
RecalculateEquipStats();
|
|
}
|
|
void EntityStats::SetBaseStat(std::string_view stat,float val){
|
|
SetBaseStat(ItemAttribute::Get(stat),val);
|
|
}
|
|
void Player::SetBaseStat(std::string_view a,float val){
|
|
stats.SetBaseStat(ItemAttribute::Get(a),val);
|
|
}
|
|
void Player::SetBaseStat(ItemAttribute a,float val){
|
|
stats.SetBaseStat(a,val);
|
|
}
|
|
|
|
const std::string&ItemSet::GetSetName()const{
|
|
return name;
|
|
}
|
|
|
|
uint32_t Player::GetMoney()const{
|
|
return money;
|
|
}
|
|
void Player::SetMoney(uint32_t newMoney){
|
|
money=newMoney;
|
|
for(auto&component:moneyListeners){
|
|
component.lock()->OnPlayerMoneyUpdate(newMoney);
|
|
}
|
|
}
|
|
void Player::AddMoneyListener(std::weak_ptr<MenuComponent>component){
|
|
if(std::find_if(moneyListeners.begin(),moneyListeners.end(),[&](auto&ptr){return &*ptr.lock()==&*component.lock();})!=moneyListeners.end()){
|
|
ERR("WARNING! Component "<<component.lock()->GetName()<<" has already been added to the Chapter listener list! There should not be any duplicates!!")
|
|
}
|
|
moneyListeners.push_back(component);
|
|
}
|
|
|
|
const float Player::GetDamageReductionFromBuffs()const{
|
|
float dmgReduction=0;
|
|
for(const Buff&b:GetBuffs(BuffType::DAMAGE_REDUCTION)){
|
|
dmgReduction+=b.intensity;
|
|
}
|
|
dmgReduction+=GetDamageReductionPct();
|
|
return std::min(0.75f,dmgReduction);
|
|
};
|
|
|
|
const float Player::GetDamageReductionFromArmor()const{
|
|
float dmgReduction=0;
|
|
dmgReduction+=Stats::GetDamageReductionPct(GetStat("Defense"));
|
|
return std::min(0.75f,dmgReduction);
|
|
};
|
|
|
|
void Player::RecalculateEquipStats(){
|
|
stats.RecalculateEquipStats();
|
|
}
|
|
|
|
const std::vector<Buff>Player::GetStatBuffs(const std::vector<std::string>&attr)const{
|
|
std::vector<Buff>statUpBuffs;
|
|
std::copy_if(buffList.begin(),buffList.end(),std::back_inserter(statUpBuffs),[&](const Buff b){
|
|
return b.type==STAT_UP&&std::find_if(attr.begin(),attr.end(),[&](const std::string&attr){return b.attr.count(ItemAttribute::Get(attr));})!=attr.end();
|
|
});
|
|
return statUpBuffs;
|
|
}
|
|
|
|
const ItemAttributable&EntityStats::GetStats()const{
|
|
return equipStats;
|
|
}
|
|
|
|
const ItemAttributable&Player::GetStats()const{
|
|
return stats.GetStats();
|
|
};
|
|
|
|
const ItemAttributable&EntityStats::GetBaseStats()const{
|
|
return baseStats;
|
|
};
|
|
|
|
const ItemAttributable&Player::GetBaseStats()const{
|
|
return stats.GetBaseStats();
|
|
};
|
|
|
|
ItemAttribute&Player::Get(std::string_view attr){
|
|
return ItemAttribute::Get(attr,this);
|
|
}
|
|
|
|
const float Player::GetCooldownReductionPct()const{
|
|
float modCDRPct=0;
|
|
const std::vector<Buff>&buffs=GetStatBuffs({"CDR"});
|
|
for(const Buff&b:buffs){
|
|
modCDRPct+=b.intensity;
|
|
}
|
|
modCDRPct+=GetStat("CDR")/100;
|
|
return modCDRPct;
|
|
}
|
|
|
|
const float Player::GetCritRatePct()const{
|
|
float modCritRatePct=0;
|
|
modCritRatePct+=GetStat("Crit Rate")/100;
|
|
return modCritRatePct;
|
|
}
|
|
const float Player::GetCritDmgPct()const{
|
|
float modCritDmgPct=0;
|
|
modCritDmgPct+=GetStat("Crit Dmg")/100;
|
|
return modCritDmgPct;
|
|
}
|
|
|
|
const float Player::GetHPRecoveryPct()const{
|
|
float modHPRecoveryPct=0;
|
|
modHPRecoveryPct+=GetStat("HP Recovery %")/100;
|
|
return modHPRecoveryPct;
|
|
}
|
|
const float Player::GetHP6RecoveryPct()const{
|
|
float modHPRecoveryPct=0;
|
|
modHPRecoveryPct+=GetStat("HP6 Recovery %")/100;
|
|
return modHPRecoveryPct;
|
|
}
|
|
const float Player::GetHP4RecoveryPct()const{
|
|
float modHPRecoveryPct=0;
|
|
modHPRecoveryPct+=GetStat("HP4 Recovery %")/100;
|
|
return modHPRecoveryPct;
|
|
}
|
|
|
|
void Player::PerformHPRecovery(){
|
|
int hpRecoveryAmt=0;
|
|
if(hpRecoveryTimer==0){
|
|
if(float(GetHPRecoveryPct()*GetMaxHealth())>0.1_Pct){
|
|
hpRecoveryAmt+=std::ceil(GetHPRecoveryPct()*GetMaxHealth());
|
|
}
|
|
hpRecoveryTimer=1;
|
|
}
|
|
if(hp4RecoveryTimer==0){
|
|
if(float(GetHP4RecoveryPct()*GetMaxHealth())>0.1_Pct){
|
|
hpRecoveryAmt+=std::ceil(GetHP4RecoveryPct()*GetMaxHealth());
|
|
}
|
|
hp4RecoveryTimer=4;
|
|
}
|
|
if(hp6RecoveryTimer==0){
|
|
if(float(GetHP6RecoveryPct()*GetMaxHealth())>0.1_Pct){
|
|
hpRecoveryAmt+=std::ceil(GetHP6RecoveryPct()*GetMaxHealth());
|
|
}
|
|
hp6RecoveryTimer=6;
|
|
}
|
|
if(GetHealth()<GetMaxHealth()){
|
|
Heal(hpRecoveryAmt);
|
|
}
|
|
|
|
if(GetHealth()/GetMaxHealth()>"Player.Health Warning Pct"_F/100.f){
|
|
lowHealthSoundPlayed=false;
|
|
}
|
|
}
|
|
|
|
const float Player::GetDamageReductionPct()const{
|
|
float modDmgReductionPct=0;
|
|
modDmgReductionPct+=GetStat("Damage Reduction")/100;
|
|
return modDmgReductionPct;
|
|
}
|
|
|
|
void Player::AddXP(const uint32_t xpGain){
|
|
currentLevelXP+=xpGain;
|
|
totalXPEarned+=xpGain;
|
|
if(Level()<LevelCap()){
|
|
while(currentLevelXP>=NextLevelXPRequired()){
|
|
currentLevelXP-=NextLevelXPRequired();
|
|
SetLevel(Level()+1);
|
|
OnLevelUp();
|
|
}
|
|
}
|
|
Component<ProgressBar>(MenuType::CHARACTER_MENU,"XP Bar")->ResetProgressBar(game->GetPlayer()->CurrentXP(),game->GetPlayer()->NextLevelXPRequired());
|
|
}
|
|
|
|
void Player::SetXP(const uint32_t xp){
|
|
currentLevelXP=xp;
|
|
Component<ProgressBar>(MenuType::CHARACTER_MENU,"XP Bar")->ResetProgressBar(game->GetPlayer()->CurrentXP(),game->GetPlayer()->NextLevelXPRequired());
|
|
}
|
|
|
|
void Player::SetTotalXPEarned(const uint32_t totalXP){
|
|
totalXPEarned=totalXP;
|
|
}
|
|
|
|
void Player::OnLevelUp(){
|
|
stats.SetBaseStat("Health",GetBaseStat("Health")+hpGrowthRate);
|
|
stats.SetBaseStat("Attack",GetBaseStat("Attack")+atkGrowthRate);
|
|
Heal(GetBaseStat("Health"));
|
|
|
|
STEAMUSERSTATS(
|
|
for(auto&[key,size]:DATA.GetProperty("Achievement.Class Unlocks")){
|
|
datafile&unlock=DATA.GetProperty(std::format("Achievement.Class Unlocks.{}",key));
|
|
if(classutils::StringToClass(unlock["Class Requirement"].GetString())==GetClass()&&
|
|
Level()-1<unlock["Level Requirement"].GetInt()&&
|
|
Level()==unlock["Level Requirement"].GetInt()){
|
|
SteamUserStats()->SetAchievement(unlock["API Name"].GetString().c_str());
|
|
SteamUserStats()->StoreStats();
|
|
}
|
|
}
|
|
)
|
|
}
|
|
const uint8_t Player::LevelCap()const{
|
|
return levelCap;
|
|
}
|
|
const uint8_t Player::Level()const{
|
|
return level;
|
|
}
|
|
void Player::SetLevel(uint8_t newLevel){
|
|
level=newLevel;
|
|
Component<MenuLabel>(CHARACTER_MENU,"Level Class Display")->SetLabel(std::format("Lv{} {}",game->GetPlayer()->Level(),game->GetPlayer()->GetClassName()));
|
|
Component<MenuLabel>(LEVEL_COMPLETE,"Level Display")->SetLabel(std::format("Lv{}",game->GetPlayer()->Level()));
|
|
}
|
|
const uint32_t Player::CurrentXP()const{
|
|
return currentLevelXP;
|
|
}
|
|
const uint32_t Player::TotalXP()const{
|
|
return totalXPEarned;
|
|
}
|
|
|
|
const uint32_t Player::NextLevelXPRequired()const{
|
|
if(Level()<LevelCap()){
|
|
return DATA.GetProperty(std::format("PlayerXP.LEVEL[{}]",Level()+1)).GetInt();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
void Player::ResetAccumulatedXP(){
|
|
accumulatedXP=0;
|
|
}
|
|
void Player::AddAccumulatedXP(const uint32_t xpGain){
|
|
accumulatedXP+=xpGain;
|
|
}
|
|
|
|
const uint32_t Player::GetAccumulatedXP()const{
|
|
return accumulatedXP;
|
|
}
|
|
|
|
const float Player::GetAttackRecoveryRateReduction()const{
|
|
return GetStat("Attack Spd");
|
|
}
|
|
|
|
void EntityStats::Reset(){
|
|
equipStats.clear();
|
|
baseStats.clear();
|
|
}
|
|
|
|
geom2d::circle<float>Player::Hitbox(){
|
|
return {GetPos(),12*GetSizeMult()/2};
|
|
}
|
|
|
|
void Player::Knockup(float duration){
|
|
knockUpTimer+=duration;
|
|
totalKnockupTime+=duration;
|
|
knockUpZAmt+=32*pow(duration,2);
|
|
}
|
|
|
|
const vf2d Player::GetWorldAimingLocation(bool useWalkDir,bool invert){
|
|
return game->view.ScreenToWorld(GetAimingLocation(useWalkDir,invert));
|
|
}
|
|
|
|
const vf2d Player::GetAimingLocation(bool useWalkDir,bool invert){
|
|
if(UsingAutoAim()){
|
|
float xAxis=0.f,yAxis=0.f;
|
|
|
|
#pragma region Manual Aiming
|
|
STEAMINPUT(
|
|
if(fabs(game->KEY_SCROLLHORZ_R.Analog())>0.f){
|
|
xAxis=game->KEY_SCROLLHORZ_R.Analog();
|
|
}
|
|
if(fabs(game->KEY_SCROLLVERT_R.Analog())>0.f){
|
|
yAxis=game->KEY_SCROLLVERT_R.Analog();
|
|
}
|
|
)else{
|
|
for(GamePad*gamepad:GamePad::getGamepads()){
|
|
if(gamepad->stillConnected){
|
|
if(fabs(gamepad->getAxis(GPAxes::RX))>0.f){
|
|
xAxis=gamepad->getAxis(GPAxes::RX);
|
|
}
|
|
|
|
if(fabs(gamepad->getAxis(GPAxes::RY))>0.f){
|
|
yAxis=gamepad->getAxis(GPAxes::RY);
|
|
}
|
|
|
|
if(xAxis!=0.f||yAxis!=0.f)break; //Found a controller, so we're good to break.
|
|
}
|
|
}
|
|
}
|
|
#pragma endregion
|
|
if(xAxis!=0.f||yAxis!=0.f){
|
|
return {(game->ScreenWidth()*xAxis)/2+game->ScreenWidth()/2,(game->ScreenHeight()*yAxis)/2+game->ScreenHeight()/2};
|
|
}else{
|
|
|
|
if(useWalkDir&&movementVelocity!=vf2d{0,0}){
|
|
xAxis=aimingAngle.cart().x;
|
|
yAxis=aimingAngle.cart().y;
|
|
}
|
|
|
|
if(xAxis!=0.f||yAxis!=0.f){
|
|
return {(game->ScreenWidth()*xAxis)/2+game->ScreenWidth()/2,(game->ScreenHeight()*yAxis)/2+game->ScreenHeight()/2};
|
|
}else{
|
|
//Find the closest monster target.
|
|
vf2d closestPoint={std::numeric_limits<float>::max(),std::numeric_limits<float>::max()};
|
|
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
|
|
if(m->IsAlive()){
|
|
geom2d::line<float>aimingLine=geom2d::line<float>(GetPos(),m->GetPos());
|
|
float distToMonster=aimingLine.length();
|
|
float distToClosestPoint=geom2d::line<float>(GetPos(),closestPoint).length();
|
|
if(distToClosestPoint>distToMonster&&distToMonster<=operator""_Pixels("Player.Auto Aim Detection Distance"_F)){
|
|
closestPoint=m->GetPos();
|
|
}
|
|
}
|
|
}
|
|
if(closestPoint!=vf2d{std::numeric_limits<float>::max(),std::numeric_limits<float>::max()}){
|
|
geom2d::line<float>aimingLine=geom2d::line<float>(GetPos(),closestPoint);
|
|
vf2d aimingPoint=aimingLine.rpoint(invert?(-operator""_Pixels("Player.Aiming Cursor Max Distance"_F)):std::min(aimingLine.length()+24.f,float(operator""_Pixels("Player.Aiming Cursor Max Distance"_F))));
|
|
return game->GetScreenSize()/2+aimingPoint-GetPos();
|
|
}else
|
|
return game->GetScreenSize()/2+vf2d{float(operator""_Pixels("Player.Aiming Cursor Max Distance"_F)),aimingAngle.y}.cart();
|
|
}
|
|
}
|
|
}else{
|
|
return game->GetMousePos();
|
|
}
|
|
}
|
|
const bool Player::UsingAutoAim()const{
|
|
return (GameSettings::KeyboardAutoAimEnabled()&&game->LastMouseMovement()>=2.f)||Input::UsingGamepad();
|
|
}
|
|
|
|
void Player::UpdateHealthAndMana(){
|
|
//Perform a check to make sure stats are initialized before they can be used.
|
|
if(game->GameInitialized()){
|
|
hp=std::min(hp,int(GetStat("Health")));
|
|
mana=std::min(mana,GetMaxMana());
|
|
}
|
|
}
|
|
|
|
void Player::SetInvisible(const bool invisibleState){
|
|
invisibility=invisibleState;
|
|
}
|
|
|
|
|
|
const bool Player::IsInvisible()const{
|
|
return invisibility;
|
|
}
|
|
|
|
void Player::ResetVelocity(){
|
|
vel={};
|
|
}
|
|
|
|
|
|
const float Player::GetHealthGrowthRate()const{
|
|
return hpGrowthRate;
|
|
}
|
|
const float Player::GetAtkGrowthRate()const{
|
|
return atkGrowthRate;
|
|
}
|
|
|
|
const float Player::GetIframeTime()const{
|
|
return iframe_time;
|
|
} |