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.
869 lines
28 KiB
869 lines
28 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 "DamageNumber.h"
|
|
#include "AdventuresInLestoria.h"
|
|
#include "Bullet.h"
|
|
#include "BulletTypes.h"
|
|
#include "DEFINES.h"
|
|
#include "safemap.h"
|
|
#include "MonsterStrategyHelpers.h"
|
|
#include "util.h"
|
|
#include "MonsterAttribute.h"
|
|
#include "ItemDrop.h"
|
|
#include "SoundEffect.h"
|
|
#include "Unlock.h"
|
|
#ifndef __EMSCRIPTEN__
|
|
#include "steam/isteamuserstats.h"
|
|
#endif
|
|
|
|
INCLUDE_ANIMATION_DATA
|
|
INCLUDE_MONSTER_DATA
|
|
INCLUDE_MONSTER_LIST
|
|
INCLUDE_DAMAGENUMBER_LIST
|
|
INCLUDE_game
|
|
INCLUDE_BULLET_LIST
|
|
INCLUDE_DATA
|
|
INCLUDE_GFX
|
|
|
|
safemap<std::string,std::function<void(Monster&,float,std::string)>>STRATEGY_DATA;
|
|
std::map<std::string,Renderable*>MonsterData::imgs;
|
|
|
|
Monster::Monster(vf2d pos,MonsterData data,bool upperLevel,bool bossMob):
|
|
pos(pos),spawnPos(pos),hp(data.GetHealth()),size(data.GetSizeMult()),targetSize(data.GetSizeMult()),strategy(data.GetAIStrategy()),name(data.GetDisplayName()),upperLevel(upperLevel),isBoss(bossMob),facingDirection(DOWN){
|
|
bool firstAnimation=true;
|
|
for(std::string&anim:data.GetAnimations()){
|
|
animation.AddState(anim,ANIMATION_DATA[anim]);
|
|
if(firstAnimation){
|
|
animation.ChangeState(internal_animState,anim);
|
|
firstAnimation=false;
|
|
}
|
|
}
|
|
stats.A("Health")=data.GetHealth();
|
|
stats.A("Attack")=data.GetAttack();
|
|
stats.A("Move Spd %")=data.GetMoveSpdMult();
|
|
randomFrameOffset=(util::random()%1000)/1000.f;
|
|
monsterWalkSoundTimer=util::random(1.f);
|
|
}
|
|
const vf2d&Monster::GetPos()const{
|
|
return pos;
|
|
}
|
|
const int Monster::GetHealth()const{
|
|
return hp;
|
|
}
|
|
|
|
const int Monster::GetMaxHealth()const{
|
|
return stats.A_Read("Health");
|
|
|
|
}
|
|
const float Monster::GetRemainingHPPct()const{
|
|
return float(GetHealth())/GetMaxHealth();
|
|
}
|
|
int Monster::GetAttack(){
|
|
float mod_atk=float(stats.A("Attack"));
|
|
mod_atk+=Get("Attack %");
|
|
mod_atk+=Get("Attack");
|
|
return int(mod_atk);
|
|
}
|
|
float Monster::GetMoveSpdMult(){
|
|
float moveSpdPct=stats.A("Move Spd %")/100.f;
|
|
|
|
float mod_moveSpd=moveSpdPct;
|
|
for(Buff&b:GetBuffs(SLOWDOWN)){
|
|
mod_moveSpd-=moveSpdPct*b.intensity;
|
|
}
|
|
for(Buff&b:GetBuffs(LOCKON_SPEEDBOOST)){
|
|
mod_moveSpd+=moveSpdPct*b.intensity;
|
|
}
|
|
for(Buff&b:GetBuffs(SPEEDBOOST)){
|
|
mod_moveSpd+=moveSpdPct*b.intensity;
|
|
}
|
|
return mod_moveSpd;
|
|
}
|
|
float Monster::GetSizeMult()const{
|
|
return size;
|
|
}
|
|
Animate2D::Frame Monster::GetFrame()const{
|
|
return animation.GetFrame(internal_animState);
|
|
}
|
|
void Monster::PerformJumpAnimation(){
|
|
animation.ChangeState(internal_animState,MONSTER_DATA[name].GetJumpAnimation());
|
|
}
|
|
void Monster::PerformShootAnimation(){
|
|
animation.ChangeState(internal_animState,MONSTER_DATA[name].GetShootAnimation());
|
|
}
|
|
void Monster::PerformIdleAnimation(){
|
|
animation.ChangeState(internal_animState,MONSTER_DATA[name].GetIdleAnimation());
|
|
}
|
|
void Monster::PerformNPCDownAnimation(){
|
|
animation.ChangeState(internal_animState,MONSTER_DATA[name].GetIdleAnimation());
|
|
}
|
|
void Monster::PerformNPCUpAnimation(){
|
|
animation.ChangeState(internal_animState,MONSTER_DATA[name].GetJumpAnimation());
|
|
}
|
|
void Monster::PerformNPCLeftAnimation(){
|
|
animation.ChangeState(internal_animState,MONSTER_DATA[name].GetShootAnimation());
|
|
}
|
|
void Monster::PerformNPCRightAnimation(){
|
|
animation.ChangeState(internal_animState,MONSTER_DATA[name].GetDeathAnimation());
|
|
}
|
|
void Monster::PerformOtherAnimation(const uint8_t otherInd){
|
|
animation.ChangeState(internal_animState,MONSTER_DATA[name].GetAnimations()[4+otherInd]);
|
|
}
|
|
bool Monster::_SetX(float x,const bool monsterInvoked){
|
|
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);
|
|
if(collisionRect==game->NO_COLLISION){
|
|
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};
|
|
bool insideArenaBounds=true;
|
|
#pragma region Calculate Arena Bounds check for Bosses
|
|
if(isBoss){
|
|
const geom2d::rect<int>arenaBounds=game->GetZones().at("BossArena")[0].zone;
|
|
if(!geom2d::contains(arenaBounds,newPos)){
|
|
insideArenaBounds=false;
|
|
}
|
|
}
|
|
#pragma endregion
|
|
#pragma region lambdas
|
|
auto NoEnemyCollisionWithTile=[&](){return (isBoss&&insideArenaBounds)||(!isBoss&&!geom2d::overlaps(newPos,collision));};
|
|
#pragma endregion
|
|
collision.pos+=tilePos;
|
|
if(NoEnemyCollisionWithTile()){
|
|
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(monsterInvoked){ //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(NoEnemyCollisionWithTile()){
|
|
return _SetY(pos.y+pushDir.y*game->GetElapsedTime()*12,false);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Monster::_SetY(float y,const bool monsterInvoked){
|
|
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);
|
|
if(collisionRect==game->NO_COLLISION){
|
|
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};
|
|
bool insideArenaBounds=true;
|
|
#pragma region Calculate Arena Bounds check for Bosses
|
|
if(isBoss){
|
|
const geom2d::rect<int>arenaBounds=game->GetZones().at("BossArena")[0].zone;
|
|
if(!geom2d::contains(arenaBounds,newPos)){
|
|
insideArenaBounds=false;
|
|
}
|
|
}
|
|
#pragma endregion
|
|
#pragma region lambdas
|
|
auto NoEnemyCollisionWithTile=[&](){return (isBoss&&insideArenaBounds)||(!isBoss&&!geom2d::overlaps(newPos,collision));};
|
|
#pragma endregion
|
|
collision.pos+=tilePos;
|
|
if(NoEnemyCollisionWithTile()){
|
|
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(monsterInvoked){ //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(NoEnemyCollisionWithTile()){
|
|
return _SetX(pos.x+pushDir.x*game->GetElapsedTime()*12,false);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Monster::SetX(float x){
|
|
return _SetX(x);
|
|
}
|
|
|
|
bool Monster::SetY(float y){
|
|
return _SetY(y);
|
|
}
|
|
|
|
bool Monster::Update(float fElapsedTime){
|
|
lastHitTimer=std::max(0.f,lastHitTimer-fElapsedTime);
|
|
iframe_timer=std::max(0.f,iframe_timer-fElapsedTime);
|
|
monsterHurtSoundCooldown=std::max(0.f,monsterHurtSoundCooldown-fElapsedTime);
|
|
lastHitPlayer=std::max(0.f,lastHitPlayer-fElapsedTime);
|
|
lastPathfindingCooldown=std::max(0.f,lastPathfindingCooldown-fElapsedTime);
|
|
|
|
if(size!=targetSize){
|
|
if(size>targetSize){
|
|
size=std::max(targetSize,size-AiL::SIZE_CHANGE_SPEED*fElapsedTime);
|
|
}else{
|
|
size=std::min(targetSize,size+AiL::SIZE_CHANGE_SPEED*fElapsedTime);
|
|
}
|
|
}
|
|
|
|
#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
|
|
|
|
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);
|
|
}
|
|
bumpedIntoTerrain=false;
|
|
if(vel!=vf2d{0,0}){
|
|
bumpedIntoTerrain|=SetX(pos.x+vel.x*fElapsedTime);
|
|
bumpedIntoTerrain|=SetY(pos.y+vel.y*fElapsedTime);
|
|
}
|
|
|
|
if(IsAlive()){
|
|
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;
|
|
}
|
|
}
|
|
if(!HasIframes()){
|
|
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
|
|
if(&*m==this)continue;
|
|
if(!m->HasIframes()&&OnUpperLevel()==m->OnUpperLevel()&&abs(m->GetZ()-GetZ())<=1&&geom2d::overlaps(geom2d::circle(pos,12*size/2),geom2d::circle(m->GetPos(),12*m->GetSizeMult()/2))){
|
|
m->Collision(*this);
|
|
geom2d::line line(pos,m->GetPos());
|
|
float dist = line.length();
|
|
m->SetPos(line.rpoint(dist*1.1f));
|
|
if(m->IsAlive()){
|
|
vel=line.vector().norm()*-128;
|
|
}
|
|
}
|
|
}
|
|
if(!game->GetPlayer()->HasIframes()&&abs(game->GetPlayer()->GetZ()-GetZ())<=1&&game->GetPlayer()->OnUpperLevel()==OnUpperLevel()&&geom2d::overlaps(geom2d::circle(pos,12*size/2),geom2d::circle(game->GetPlayer()->GetPos(),12*game->GetPlayer()->GetSizeMult()/2))){
|
|
geom2d::line line(pos,game->GetPlayer()->GetPos());
|
|
float dist = line.length();
|
|
SetPos(line.rpoint(-0.1f));
|
|
vel=line.vector().norm()*-128;
|
|
}
|
|
}
|
|
if(GetState()==State::NORMAL){
|
|
if(game->GetPlayer()->GetX()>pos.x){
|
|
facingDirection=RIGHT;
|
|
} else {
|
|
facingDirection=LEFT;
|
|
}
|
|
}
|
|
Monster::STRATEGY::RUN_STRATEGY(*this,fElapsedTime);
|
|
}
|
|
if(!IsAlive()){
|
|
deathTimer+=fElapsedTime;
|
|
if(deathTimer>3){
|
|
return false;
|
|
}
|
|
}
|
|
animation.UpdateState(internal_animState,randomFrameOffset+fElapsedTime);
|
|
randomFrameOffset=0;
|
|
attackedByPlayer=false;
|
|
return true;
|
|
}
|
|
Key Monster::GetFacingDirection()const{
|
|
return facingDirection;
|
|
}
|
|
|
|
void Monster::UpdateFacingDirection(vf2d facingTargetPoint){
|
|
if(facingTargetPoint.x>GetPos().x){
|
|
facingDirection=RIGHT;
|
|
}
|
|
if(facingTargetPoint.x<GetPos().x){
|
|
facingDirection=LEFT;
|
|
}
|
|
}
|
|
void Monster::Draw()const{
|
|
if(GetZ()>0){
|
|
vf2d shadowScale=vf2d{8*GetSizeMult()/3.f,1}/std::max(1.f,GetZ()/24);
|
|
game->view.DrawDecal(GetPos()-vf2d{3,3}*shadowScale/2+vf2d{0,6*GetSizeMult()},GFX["circle.png"].Decal(),shadowScale,BLACK);
|
|
}
|
|
Pixel blendCol=GetBuffs(BuffType::SLOWDOWN).size()>0?Pixel{uint8_t(255*abs(sin(1.4*GetBuffs(BuffType::SLOWDOWN)[0].duration))),uint8_t(255*abs(sin(1.4*GetBuffs(BuffType::SLOWDOWN)[0].duration))),uint8_t(128+127*abs(sin(1.4*GetBuffs(BuffType::SLOWDOWN)[0].duration)))}:WHITE;
|
|
|
|
game->view.DrawPartialRotatedDecal(GetPos()-vf2d{0,GetZ()},GetFrame().GetSourceImage()->Decal(),spriteRot,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,vf2d(GetSizeMult()*(GetFacingDirection()==RIGHT?-1:1),GetSizeMult()),blendCol);
|
|
if(overlaySprite.length()!=0){
|
|
game->view.DrawPartialRotatedDecal(GetPos()-vf2d{0,GetZ()},GFX[overlaySprite].Decal(),spriteRot,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,vf2d(GetSizeMult()*(GetFacingDirection()==RIGHT?-1:1),GetSizeMult()),{blendCol.r,blendCol.g,blendCol.b,overlaySpriteTransparency});
|
|
}
|
|
|
|
std::vector<Buff>shieldBuffs=GetBuffs(BARRIER_DAMAGE_REDUCTION);
|
|
if(shieldBuffs.size()>0){
|
|
game->view.DrawRotatedDecal(GetPos()-vf2d{0,GetZ()},GFX["block.png"].Decal(),0.f,GFX["block.png"].Sprite()->Size()/2,{GetSizeMult(),GetSizeMult()});
|
|
}
|
|
|
|
#pragma region Debug Pathfinding
|
|
#ifdef _DEBUG
|
|
if("debug_pathfinding"_I){
|
|
for(float index=0.f;index<path.points.size();index+=0.01f){
|
|
Pixel col=DARK_GREY;
|
|
if(index<pathIndex){
|
|
col=VERY_DARK_GREY;
|
|
}
|
|
if(index>pathIndex+1){
|
|
col=GREY;
|
|
}
|
|
game->view.FillRectDecal(path.GetSplinePoint(index).pos,{1,1},col);
|
|
}
|
|
|
|
for(size_t counter=0;const Pathfinding::sPoint2D&point:path.points){
|
|
Pixel col=CYAN;
|
|
if(counter<pathIndex){
|
|
col=RED;
|
|
}
|
|
if(counter>pathIndex+1){
|
|
col=YELLOW;
|
|
}
|
|
game->view.FillRectDecal(point.pos,{3,3},col);
|
|
counter++;
|
|
}
|
|
}
|
|
#endif
|
|
#pragma endregion
|
|
}
|
|
void Monster::DrawReflection(float drawRatioX,float multiplierX){
|
|
game->SetDecalMode(DecalMode::ADDITIVE);
|
|
|
|
vf2d defaultPos=GetPos()+vf2d{drawRatioX*GetFrame().GetSourceRect().size.x,GetZ()+(GetFrame().GetSourceRect().size.y-16)*GetSizeMult()};
|
|
vf2d spriteSize=GetFrame().GetSourceRect().size/1.5f*GetSizeMult();
|
|
|
|
float bottomExpansionAmount=abs(util::radToDeg(spriteRot))/10;
|
|
|
|
//BL is in TR, BR is in TL, TR is in BL and TL is in BR.
|
|
std::array<vf2d,4>points={
|
|
vf2d{defaultPos+vf2d{-spriteSize.x/2,spriteSize.y}-vf2d{bottomExpansionAmount,0}}, //BL
|
|
vf2d{defaultPos-spriteSize.x/2}, //TL
|
|
vf2d{defaultPos+vf2d{spriteSize.x/2,-spriteSize.y/2}}, //TR
|
|
vf2d{defaultPos+spriteSize/2+vf2d{bottomExpansionAmount,0}}, //BR
|
|
};
|
|
if(GetFacingDirection()==RIGHT){
|
|
points={
|
|
vf2d{defaultPos+spriteSize/2+vf2d{bottomExpansionAmount,0}}, //BR
|
|
vf2d{defaultPos+vf2d{spriteSize.x/2,-spriteSize.y/2}}, //TR
|
|
vf2d{defaultPos-spriteSize.x/2}, //TL
|
|
vf2d{defaultPos+vf2d{-spriteSize.x/2,spriteSize.y}-vf2d{bottomExpansionAmount,0}}, //BL
|
|
};
|
|
}
|
|
|
|
game->view.DrawPartialWarpedDecal(GetFrame().GetSourceImage()->Decal(),points,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size);
|
|
game->SetDecalMode(DecalMode::NORMAL);
|
|
}
|
|
void Monster::Collision(Player*p){
|
|
if(GetCollisionDamage()>0&&lastHitPlayer==0.0f){
|
|
if(p->Hurt(GetCollisionDamage(),OnUpperLevel(),GetZ())){
|
|
lastHitPlayer=1.0f;
|
|
}
|
|
}
|
|
|
|
#pragma region Knockback due to buffs
|
|
vf2d knockbackVecNorm=geom2d::line<float>(GetPos(),p->GetPos()).vector().norm();
|
|
float knockbackStrength=0.f;
|
|
std::vector<Buff> knockbackBuffs=GetBuffs(COLLISION_KNOCKBACK_STRENGTH);
|
|
for(Buff&b:knockbackBuffs){
|
|
knockbackStrength+=b.intensity;
|
|
}
|
|
p->Knockback(knockbackVecNorm*knockbackStrength);
|
|
#pragma endregion
|
|
|
|
|
|
B(Attribute::COLLIDED_WITH_PLAYER)=true;
|
|
|
|
Collision();
|
|
}
|
|
void Monster::Collision(Monster&m){
|
|
|
|
#pragma region Knockback due to buffs
|
|
vf2d knockbackVecNorm=geom2d::line<float>(GetPos(),m.GetPos()).vector().norm();
|
|
float knockbackStrength=0.f;
|
|
std::vector<Buff> knockbackBuffs=GetBuffs(COLLISION_KNOCKBACK_STRENGTH);
|
|
for(Buff&b:knockbackBuffs){
|
|
knockbackStrength+=b.intensity;
|
|
}
|
|
m.Knockback(knockbackVecNorm*knockbackStrength);
|
|
#pragma endregion
|
|
|
|
Collision();
|
|
}
|
|
void Monster::Collision(){
|
|
if(strategy=="Run Towards"&&GetState()==State::MOVE_TOWARDS&&util::random(float(Monster::STRATEGY::_GetInt(*this,"BumpStopChance",strategy)))<1){//The run towards strategy causes state to return to normal upon a collision.
|
|
SetState(State::NORMAL);
|
|
targetAcquireTimer=0;
|
|
}
|
|
}
|
|
void Monster::SetVelocity(vf2d vel){
|
|
this->vel=vel;
|
|
}
|
|
bool Monster::SetPos(vf2d pos){
|
|
bool resultX=SetX(pos.x);
|
|
bool resultY=SetY(pos.y);
|
|
if(resultY&&!resultX){
|
|
resultX=SetX(pos.x);
|
|
}
|
|
return resultX||resultY;
|
|
}
|
|
void Monster::Moved(){
|
|
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;
|
|
}
|
|
}
|
|
monsterWalkSoundTimer+=game->GetElapsedTime();
|
|
if(monsterWalkSoundTimer>1.f){
|
|
monsterWalkSoundTimer-=1.f;
|
|
SoundEffect::PlaySFX(GetWalkSound(),GetPos());
|
|
}
|
|
|
|
if(!std::isfinite(pos.x)){
|
|
ERR(std::format("WARNING! Player X position is {}...Trying to recover. THIS SHOULD NOT BE HAPPENING!",pos.x));
|
|
pos.x=spawnPos.x;
|
|
}
|
|
if(!std::isfinite(pos.y)){
|
|
ERR(std::format("WARNING! Player Y position is {}...Trying to recover. THIS SHOULD NOT BE HAPPENING!",pos.y));
|
|
pos.y=spawnPos.y;
|
|
}
|
|
}
|
|
std::string Monster::GetDeathAnimationName(){
|
|
return MONSTER_DATA[name].GetDeathAnimation();
|
|
}
|
|
const bool Monster::AttackAvoided(const float attackZ)const{
|
|
return HasIframes()||abs(GetZ()-attackZ)>1;
|
|
}
|
|
|
|
bool Monster::Hurt(int damage,bool onUpperLevel,float z){
|
|
if(!IsAlive()||onUpperLevel!=OnUpperLevel()||AttackAvoided(z)) return false;
|
|
if(game->InBossEncounter()){
|
|
game->StartBossEncounter();
|
|
}
|
|
game->GetPlayer()->ResetLastCombatTime();
|
|
float mod_dmg=float(damage);
|
|
|
|
#pragma region Handle Crits
|
|
bool crit=false;
|
|
if(util::random(1)<game->GetPlayer()->GetCritRatePct()){
|
|
mod_dmg*=1+game->GetPlayer()->GetCritDmgPct();
|
|
crit=true;
|
|
}
|
|
#pragma endregion
|
|
|
|
mod_dmg-=mod_dmg*GetDamageReductionFromBuffs();
|
|
|
|
mod_dmg=std::ceil(mod_dmg);
|
|
hp=std::max(0,hp-int(mod_dmg));
|
|
|
|
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));
|
|
DAMAGENUMBER_LIST.push_back(damageNumberPtr);
|
|
}
|
|
#pragma region Change Label to Crit
|
|
if(crit){
|
|
damageNumberPtr.get()->type=CRIT;
|
|
}
|
|
#pragma endregion
|
|
lastHitTimer=0.05f;
|
|
if(!IsAlive()){
|
|
OnDeath();
|
|
|
|
SoundEffect::PlaySFX(GetDeathSound(),GetPos());
|
|
}else{
|
|
hp=std::max(1,hp); //Make sure it stays alive if it's supposed to be alive...
|
|
|
|
if(monsterHurtSoundCooldown==0.f){
|
|
monsterHurtSoundCooldown=util::random(0.5f)+0.5f;
|
|
SoundEffect::PlaySFX(GetHurtSound(),GetPos());
|
|
}
|
|
}
|
|
if(game->InBossEncounter()){
|
|
game->BossDamageDealt(int(mod_dmg));
|
|
}
|
|
GetInt(Attribute::HITS_UNTIL_DEATH)=std::max(0,GetInt(Attribute::HITS_UNTIL_DEATH)-1);
|
|
iframe_timer=GetFloat(Attribute::IFRAME_TIME_UPON_HIT);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Monster::IsAlive(){
|
|
return hp>0||!diesNormally;
|
|
}
|
|
vf2d&Monster::GetTargetPos(){
|
|
return target;
|
|
}
|
|
|
|
MonsterSpawner::MonsterSpawner(){}
|
|
MonsterSpawner::MonsterSpawner(vf2d pos,vf2d range,std::vector<std::pair<std::string,vf2d>>monsters,bool upperLevel,std::string bossNameDisplay)
|
|
:pos(pos),range(range),monsters(monsters),upperLevel(upperLevel),bossNameDisplay(bossNameDisplay){
|
|
}
|
|
bool MonsterSpawner::SpawnTriggered(){
|
|
return triggered;
|
|
}
|
|
vf2d MonsterSpawner::GetRange(){
|
|
return range;
|
|
}
|
|
vf2d MonsterSpawner::GetPos(){
|
|
return pos;
|
|
}
|
|
|
|
void MonsterSpawner::SetTriggered(bool trigger,bool spawnMonsters){
|
|
triggered=trigger;
|
|
if(spawnMonsters){
|
|
for(std::pair<std::string,vf2d>&monsterInfo:monsters){
|
|
game->SpawnMonster(pos+monsterInfo.second,MONSTER_DATA[monsterInfo.first],DoesUpperLevelSpawning(),bossNameDisplay!="");
|
|
}
|
|
if(bossNameDisplay!=""){
|
|
game->SetBossNameDisplay(bossNameDisplay);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool MonsterSpawner::DoesUpperLevelSpawning(){
|
|
return upperLevel;
|
|
}
|
|
|
|
const bool Monster::OnUpperLevel()const{
|
|
return upperLevel;
|
|
}
|
|
|
|
void Monster::AddBuff(BuffType type,float duration,float intensity){
|
|
buffList.push_back(Buff{type,duration,intensity});
|
|
}
|
|
|
|
void Monster::RemoveBuff(BuffType type){
|
|
std::erase_if(buffList,[&](const Buff&buff){return buff.type==type;});
|
|
}
|
|
|
|
bool Monster::StartPathfinding(float pathingTime){
|
|
SetState(State::PATH_AROUND);
|
|
if(lastPathfindingCooldown==0.f){
|
|
path=game->pathfinder.Solve_WalkPath(pos,target,24,OnUpperLevel());
|
|
lastPathfindingCooldown=0.25f;
|
|
}
|
|
if(path.points.size()>0){
|
|
pathIndex=0.f;
|
|
//We gives this mob 5 seconds to figure out a path to the target.
|
|
targetAcquireTimer=pathingTime;
|
|
}
|
|
return path.points.size()>0;
|
|
}
|
|
|
|
void Monster::PathAroundBehavior(float fElapsedTime){
|
|
canMove=false;
|
|
if(path.points.size()>0){
|
|
//Move towards the new path.
|
|
geom2d::line moveTowardsLine=geom2d::line(pos,path.GetSplinePoint(pathIndex).pos);
|
|
if(moveTowardsLine.length()>100*fElapsedTime*GetMoveSpdMult()){
|
|
canMove=SetPos(pos+moveTowardsLine.vector().norm()*100*fElapsedTime*GetMoveSpdMult());
|
|
UpdateFacingDirection(moveTowardsLine.end);
|
|
}else{
|
|
if(pathIndex>=path.points.size()-1){
|
|
//We have reached the end of the path!
|
|
pathIndex=0;
|
|
targetAcquireTimer=0;
|
|
}else{
|
|
while(moveTowardsLine.length()<100*fElapsedTime*GetMoveSpdMult()){
|
|
pathIndex+=0.1f;
|
|
moveTowardsLine=geom2d::line(pos,path.GetSplinePoint(pathIndex).pos);
|
|
if(pathIndex>=path.points.size()-1){
|
|
//We have reached the end of the path!
|
|
pathIndex=0;
|
|
targetAcquireTimer=0;
|
|
break;
|
|
}
|
|
}
|
|
//Try to move to the new determined location.
|
|
canMove=SetPos(pos+moveTowardsLine.vector().norm()*100*fElapsedTime*GetMoveSpdMult());
|
|
UpdateFacingDirection(moveTowardsLine.end);
|
|
}
|
|
}
|
|
} else {
|
|
//We actually can't do anything so just quit.
|
|
targetAcquireTimer=0;
|
|
}
|
|
}
|
|
|
|
std::vector<Buff>Monster::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;
|
|
}
|
|
|
|
State::State Monster::GetState(){
|
|
return state;
|
|
}
|
|
|
|
void Monster::SetState(State::State newState){
|
|
state=newState;
|
|
}
|
|
|
|
const bool Monster::HasIframes()const{
|
|
return iframe_timer>0;
|
|
}
|
|
|
|
const float Monster::GetZ()const{
|
|
return z;
|
|
}
|
|
|
|
const std::function<void(Monster&,float,std::string)>&Monster::GetStrategy()const{
|
|
return STRATEGY_DATA[strategy];
|
|
}
|
|
|
|
void Monster::SetSize(float newSize,bool immediate){
|
|
if(immediate){
|
|
size=targetSize=newSize;
|
|
}else{
|
|
targetSize=newSize;
|
|
}
|
|
}
|
|
|
|
void Monster::SetZ(float z){
|
|
this->z=z;
|
|
}
|
|
|
|
void Monster::SetStrategyDrawFunction(std::function<void(AiL*,Monster&,const std::string&)>func){
|
|
strategyDraw=func;
|
|
}
|
|
|
|
void Monster::SetStrategyDrawOverlayFunction(std::function<void(AiL*,Monster&,const std::string&)>func){
|
|
strategyDrawOverlay=func;
|
|
}
|
|
|
|
std::map<ItemInfo*,uint16_t>Monster::SpawnDrops(){
|
|
std::map<ItemInfo*,uint16_t>drops;
|
|
for(MonsterDropData data:MONSTER_DATA.at(name).GetDropData()){
|
|
if(util::random(100)<=data.dropChance){
|
|
//This isn't necessarily fair odds for each quantity dropped.
|
|
int dropQuantity=int(data.minQty+std::round(util::random(float(data.maxQty-data.minQty))));
|
|
for(int i=0;i<dropQuantity;i++){
|
|
ItemDrop::SpawnItem(&data.item,GetPos(),OnUpperLevel());
|
|
drops.at(const_cast<ItemInfo*>(&data.item))++;
|
|
}
|
|
}
|
|
}
|
|
return drops;
|
|
}
|
|
|
|
void Monster::OnDeath(){
|
|
animation.ChangeState(internal_animState,GetDeathAnimationName());
|
|
if(isBoss){
|
|
game->ReduceBossEncounterMobCount();
|
|
if(game->BossEncounterMobCount()==0){
|
|
ZoneData exitRing{geom2d::rect<int>{vi2d{GetPos()-vf2d{"boss_spawn_ring_radius"_F,"boss_spawn_ring_radius"_F}},vi2d{"boss_spawn_ring_radius"_I*2,"boss_spawn_ring_radius"_I*2}},OnUpperLevel()};
|
|
|
|
const geom2d::rect<int>arenaBounds=game->GetZones().at("BossArena")[0].zone;
|
|
geom2d::rect<int>clampedArena{vi2d(arenaBounds.pos+"boss_spawn_ring_radius"_I),vi2d(arenaBounds.size-"boss_spawn_ring_radius"_I*2)};
|
|
|
|
exitRing.zone.pos.x=std::clamp(exitRing.zone.pos.x,clampedArena.pos.x-"boss_spawn_ring_radius"_I,clampedArena.pos.x-"boss_spawn_ring_radius"_I+clampedArena.size.x);
|
|
exitRing.zone.pos.y=std::clamp(exitRing.zone.pos.y,clampedArena.pos.y-"boss_spawn_ring_radius"_I,clampedArena.pos.y-"boss_spawn_ring_radius"_I+clampedArena.size.y);
|
|
|
|
game->AddZone("EndZone",exitRing); //Create a 144x144 ring around the dead boss.
|
|
}
|
|
}
|
|
|
|
Unlock::IncreaseKillCount();
|
|
|
|
STEAMUSERSTATS(
|
|
for(auto&[key,size]:DATA.GetProperty("Achievement.Kill Unlocks")){
|
|
//Monster-specific achievement unlocks.
|
|
datafile&unlock=DATA.GetProperty(std::format("Achievement.Kill Unlocks.{}",key));
|
|
if(unlock.HasProperty("Monster Name")){
|
|
if(unlock["Monster Name"].GetString()!=GetName())continue;
|
|
if(unlock.HasProperty("Time Limit")&&isBoss){
|
|
if(game->GetEncounterDuration()<=unlock["Time Limit"].GetReal()){
|
|
SteamUserStats()->SetAchievement(unlock["API Name"].GetString().c_str());
|
|
SteamUserStats()->StoreStats();
|
|
}
|
|
}else{
|
|
SteamUserStats()->SetAchievement(unlock["API Name"].GetString().c_str());
|
|
SteamUserStats()->StoreStats();
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
if(hasStrategyDeathFunction){
|
|
GameEvent::AddEvent(std::make_unique<MonsterStrategyGameEvent>(strategyDeathFunc,*this,MONSTER_DATA[name].GetAIStrategy()));
|
|
}
|
|
|
|
SpawnDrops();
|
|
|
|
game->GetPlayer()->AddAccumulatedXP(MONSTER_DATA.at(name).GetXP());
|
|
}
|
|
|
|
const ItemAttributable&Monster::GetStats()const{
|
|
return stats;
|
|
}
|
|
|
|
ItemAttribute&Monster::Get(std::string_view attr){
|
|
return ItemAttribute::Get(attr,this);
|
|
}
|
|
|
|
const uint32_t MonsterData::GetXP()const{
|
|
return xp;
|
|
}
|
|
|
|
const EventName&Monster::GetHurtSound(){
|
|
return MONSTER_DATA[name].GetHurtSound();
|
|
}
|
|
const EventName&Monster::GetDeathSound(){
|
|
return MONSTER_DATA[name].GetDeathSound();
|
|
}
|
|
const EventName&Monster::GetWalkSound(){
|
|
return MONSTER_DATA[name].GetWalkSound();
|
|
}
|
|
|
|
geom2d::circle<float>Monster::Hitbox(){
|
|
return {GetPos(),12*GetSizeMult()};
|
|
}
|
|
|
|
void Monster::Knockback(const vf2d&vel){
|
|
this->vel+=vel;
|
|
}
|
|
|
|
void Monster::Knockup(float duration){
|
|
knockUpTimer+=duration;
|
|
totalKnockupTime+=duration;
|
|
knockUpZAmt+=32*pow(duration,2);
|
|
}
|
|
|
|
const std::string&Monster::GetName()const{
|
|
return name;
|
|
}
|
|
|
|
void Monster::RotateTowardsPos(const vf2d&targetPos){
|
|
float dirToPlayer=util::angleTo(GetPos(),targetPos);
|
|
#pragma region Face towards lockon direction
|
|
if(abs(dirToPlayer)<0.5f*PI){ //This sprite is supposed to be facing right (flipped)
|
|
facingDirection=RIGHT;
|
|
spriteRot=dirToPlayer;
|
|
}else{
|
|
facingDirection=LEFT;
|
|
if(dirToPlayer>0){
|
|
spriteRot=-PI+dirToPlayer;
|
|
}else{
|
|
spriteRot=PI+dirToPlayer;
|
|
}
|
|
}
|
|
#pragma endregion
|
|
}
|
|
|
|
const float Monster::GetDamageReductionFromBuffs()const{
|
|
float dmgReduction=0;
|
|
for(const Buff&b:GetBuffs(BuffType::DAMAGE_REDUCTION)){
|
|
dmgReduction+=b.intensity;
|
|
}
|
|
for(const Buff&b:GetBuffs(BuffType::BARRIER_DAMAGE_REDUCTION)){
|
|
dmgReduction+=b.intensity;
|
|
}
|
|
return std::min(1.0f,dmgReduction);
|
|
}
|
|
|
|
const float Monster::GetCollisionDamage()const{
|
|
float collisionDmg=0.f;
|
|
for(Buff&b:GetBuffs(FIXED_COLLISION_DMG)){
|
|
collisionDmg+=b.intensity;
|
|
}
|
|
if(collisionDmg>0)return collisionDmg;
|
|
else return MONSTER_DATA[name].GetCollisionDmg();
|
|
}
|
|
|
|
void Monster::SetStrategyDeathFunction(std::function<bool(GameEvent&,Monster&,const std::string&)>func){
|
|
hasStrategyDeathFunction=true;
|
|
strategyDeathFunc=func;
|
|
}
|
|
|
|
const bool Monster::IsNPC()const{
|
|
return MONSTER_DATA[name].IsNPC();
|
|
}
|
|
|
|
const bool MonsterData::IsNPC()const{
|
|
return isNPC;
|
|
}
|
|
|
|
const Animate2D::FrameSequence&Monster::GetCurrentAnimation()const{
|
|
return ANIMATION_DATA[animation.currentStateName];
|
|
}
|
|
|
|
const bool Monster::HasLineOfSight(vf2d targetPos)const{
|
|
geom2d::line<float>losLine=geom2d::line<float>(GetPos(),targetPos);
|
|
float losLineLength=losLine.length();
|
|
float losLineMarker=0.f;
|
|
bool hasLoS=true;
|
|
while(losLineMarker<losLineLength){
|
|
vf2d checkPos=losLine.rpoint(losLineMarker);
|
|
if(game->GetTileCollision(game->GetCurrentMapName(),checkPos,OnUpperLevel())!=game->NO_COLLISION){
|
|
hasLoS=false;
|
|
break;
|
|
}
|
|
losLineMarker+=game->GetCurrentMapData().TileSize.x/2.f;
|
|
}
|
|
return hasLoS;
|
|
} |