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/Adventures in Lestoria/Monster.cpp

1118 lines
38 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 "GameSettings.h"
INCLUDE_ANIMATION_DATA
INCLUDE_MONSTER_DATA
INCLUDE_MONSTER_LIST
INCLUDE_DAMAGENUMBER_LIST
INCLUDE_game
INCLUDE_BULLET_LIST
INCLUDE_DATA
INCLUDE_GFX
INCLUDE_SPAWNER_LIST
INCLUDE_SPAWNER_CONTROLLER
safemap<std::string,std::function<void(Monster&,float,std::string)>>STRATEGY_DATA;
std::unordered_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(Direction::SOUTH),lifetime(GetTotalLifetime()){
for(const std::string&anim:data.GetAnimations()){
animation.AddState(anim,ANIMATION_DATA[std::format("{}_{}",name,anim)]);
}
PerformIdleAnimation();
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);
UpdateFacingDirection(game->GetPlayer()->GetPos());
}
const vf2d&Monster::GetPos()const{
return pos;
}
const int Monster::GetHealth()const{
return hp;
}
const int Monster::GetMaxHealth()const{
return stats.A_Read("Health");
}
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(SELF_INFLICTED_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(const Direction facingDir){
facingDirection=facingDir;
PerformJumpAnimation();
}
void Monster::PerformJumpAnimation(){
animation.ChangeState(internal_animState,MONSTER_DATA.at(name).GetJumpAnimation(facingDirection));
}
void Monster::PerformShootAnimation(const Direction facingDir){
facingDirection=facingDir;
PerformShootAnimation();
}
void Monster::PerformShootAnimation(){
animation.ChangeState(internal_animState,MONSTER_DATA.at(name).GetShootAnimation(facingDirection));
}
void Monster::PerformIdleAnimation(const Direction facingDir){
facingDirection=facingDir;
PerformIdleAnimation();
}
void Monster::PerformIdleAnimation(){
animation.ChangeState(internal_animState,MONSTER_DATA.at(name).GetIdleAnimation(facingDirection));
}
void Monster::PerformNPCDownAnimation(){
facingDirection=Direction::SOUTH;
PerformAnimation("DOWN");
}
void Monster::PerformNPCUpAnimation(){
facingDirection=Direction::NORTH;
PerformAnimation("UP");
}
void Monster::PerformNPCLeftAnimation(){
facingDirection=Direction::WEST;
PerformAnimation("LEFT");
}
void Monster::PerformNPCRightAnimation(){
facingDirection=Direction::EAST;
PerformAnimation("RIGHT");
}
void Monster::PerformAnimation(const std::string_view animationName){
if(HasFourWaySprites())animation.ChangeState(internal_animState,std::format("{}_{}",animationName,int(facingDirection)));
else animation.ChangeState(internal_animState,std::string(animationName));
}
//Performs an animation, optionally changes the facing direction of this monster.
void Monster::PerformAnimation(const std::string_view animationName,const Direction facingDir){
facingDirection=facingDir;
PerformAnimation(animationName);
}
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 IgnoresTerrainCollision()||(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 IgnoresTerrainCollision()||(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);
lastFacingDirectionChange+=fElapsedTime;
timeSpentAlive+=fElapsedTime;
if(IsSolid()){
if(GetPos().y>=game->GetPlayer()->GetPos().y)solidFadeTimer=std::min(TileGroup::FADE_TIME,solidFadeTimer+game->GetElapsedTime());
else solidFadeTimer=std::max(0.f,solidFadeTimer-game->GetElapsedTime());
}
if(HasArrowIndicator()&&IsAlive())game->SetBossIndicatorPos(GetPos());
#pragma region Handle Monster Lifetime and fade timer.
if(fadeTimer>0.f){
fadeTimer=std::max(0.f,fadeTimer-fElapsedTime);
if(fadeTimer==0.f)MarkForDeletion();
}else
if(GetLifetime().has_value()){
lifetime.value()=std::max(0.f,lifetime.value()-fElapsedTime);
if(lifetime.value()==0.f){
fadeTimer=1.f;
lifetime={}; //Nullify the lifetime value as the monster has expired. So we'll prevent a loop by doing this.
}
}
#pragma endregion
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){
const float monsterRadius{GetCollisionRadius()};
const float otherMonsterRadius{m->GetCollisionRadius()};
if(&*m==this)continue;
if(!m->HasIframes()&&OnUpperLevel()==m->OnUpperLevel()&&abs(m->GetZ()-GetZ())<=1&&geom2d::overlaps(geom2d::circle(pos,monsterRadius),geom2d::circle(m->GetPos(),otherMonsterRadius))){
m->Collision(*this);
if(IsSolid())continue; //Solid objects don't need any movement/knockback processing and can be ignored from here on out.
geom2d::line line(pos,m->GetPos());
float dist = line.length();
while(dist<=0.001){
line={pos+vf2d{util::random(0.2f)-0.1f,util::random(0.2f)-0.1f},m->GetPos()};
dist=line.length();
}
const float displacementDist=(otherMonsterRadius+monsterRadius)-dist;
if(m->IsAlive()){
if(!m->IsSolid()){
float knockbackStrength=1.f;
std::vector<Buff> knockbackBuffs=m->GetBuffs(COLLISION_KNOCKBACK_STRENGTH);
for(Buff&b:knockbackBuffs){
knockbackStrength+=b.intensity;
}
Knockback(line.vector().norm()*-128*knockbackStrength);
}else
if(!IgnoresTerrainCollision()){
SetPos(line.rpoint(-displacementDist));
}
}
}
}
}
if(GetState()==State::NORMAL){
UpdateFacingDirection(game->GetPlayer()->GetPos());
}
Monster::STRATEGY::RUN_STRATEGY(*this,fElapsedTime);
}
if(!IsAlive()){
deathTimer+=fElapsedTime;
if(deathTimer>3){
return false;
}
}
animation.UpdateState(internal_animState,randomFrameOffset+fElapsedTime);
if(HasMountedMonster())mounted_animation.value().UpdateState(internal_mounted_animState.value(),fElapsedTime);
randomFrameOffset=0;
attackedByPlayer=false;
return true;
}
Direction Monster::GetFacingDirection()const{
return facingDirection;
}
void Monster::UpdateFacingDirection(Direction newFacingDir){
if(HasFourWaySprites()){
facingDirection=newFacingDir;
}else{
if(newFacingDir==Direction::NORTH||newFacingDir==Direction::SOUTH)ERR(std::format("WARNING! Trying to set a facing direction of {} for Monster {}! Not possible because the monster does not have four-way facing sprites!",int(newFacingDir),GetDisplayName()));
facingDirection=newFacingDir;
}
}
void Monster::UpdateFacingDirection(vf2d facingTargetPoint){
if(Immovable())return;
float facingAngle=util::angleTo(GetPos(),facingTargetPoint);
vf2d diff=GetPos()-facingTargetPoint;
if(abs(facingAngle-prevFacingDirectionAngle)<PI/4.f||lastFacingDirectionChange<0.25f)return; //We don't want to change facing angle until a more drastic angle of change has occurred. About 1/4 circle should be acceptable.
prevFacingDirectionAngle=facingAngle;
lastFacingDirectionChange=0.f;
if(HasFourWaySprites()){
if(abs(diff.x)>abs(diff.y)){
if(facingTargetPoint.x>GetPos().x){
facingDirection=Direction::EAST;
}
if(facingTargetPoint.x<GetPos().x){
facingDirection=Direction::WEST;
}
}else{
if(facingTargetPoint.y>GetPos().y){
facingDirection=Direction::SOUTH;
}
if(facingTargetPoint.y<GetPos().y){
facingDirection=Direction::NORTH;
}
}
animation.ModifyDisplaySprite(internal_animState,std::format("{}_{}",animation.currentStateName.substr(0,animation.currentStateName.length()-2),int(facingDirection)));
if(HasMountedMonster())mounted_animation.value().ModifyDisplaySprite(internal_mounted_animState.value(),std::format("{}_{}",mounted_animation.value().currentStateName.substr(0,mounted_animation.value().currentStateName.length()-2),int(facingDirection)));
}else{
if(diff.x>0){
facingDirection=Direction::WEST;
}else{
facingDirection=Direction::EAST;
}
}
}
void Monster::Draw()const{
if(markedForDeletion)return;
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;
const vf2d hitTimerOffset=vf2d{sin(20*PI*lastHitTimer+randomFrameOffset),0.f}*2.f*GetSizeMult();
const vf2d zOffset=-vf2d{0,GetZ()};
const vf2d drawPos=GetPos()+zOffset+hitTimerOffset;
if(GetZ()>0){
vf2d shadowScale=vf2d{8*GetSizeMult()/3.f,1}/std::max(1.f,GetZ()/24);
game->view.DrawDecal(GetPos()+hitTimerOffset-vf2d{3,3}*shadowScale/2+vf2d{0,6*GetSizeMult()},GFX["circle.png"].Decal(),shadowScale,BLACK);
}
const bool NotOnTitleScreen=GameState::STATE!=GameState::states[States::MAIN_MENU];
uint8_t blendColAlpha=blendCol.a;
if(fadeTimer>0.f)blendColAlpha=uint8_t(util::lerp(0,blendCol.a,fadeTimer)); //Fade timer goes from 1 to 0 seconds.
else
if(NotOnTitleScreen
&&(game->GetPlayer()->HasIframes()||OnUpperLevel()!=game->GetPlayer()->OnUpperLevel()||abs(GetZ()-game->GetPlayer()->GetZ())>1))blendColAlpha=blendCol.a*0.62f;
else
if(IsSolid()&&solidFadeTimer>0.f)blendColAlpha=uint8_t(util::lerp(blendCol.a,255-TileGroup::FADE_AMT,solidFadeTimer/TileGroup::FADE_TIME));
blendCol.a=blendColAlpha;
const float finalSpriteRot=HasFourWaySprites()?0.f:spriteRot; //Prevent 4-way sprites from being rotated.
game->view.DrawPartialRotatedDecal(drawPos,GetFrame().GetSourceImage()->Decal(),finalSpriteRot,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,vf2d(GetSizeMult()*(!HasFourWaySprites()&&GetFacingDirection()==Direction::EAST?-1:1),GetSizeMult()),blendCol);
if(overlaySprite.length()!=0){
game->view.DrawPartialRotatedDecal(drawPos,GFX[overlaySprite].Decal(),finalSpriteRot,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,vf2d(GetSizeMult()*(!HasFourWaySprites()&&GetFacingDirection()==Direction::EAST?-1:1),GetSizeMult()),{blendCol.r,blendCol.g,blendCol.b,overlaySpriteTransparency});
}
if(HasMountedMonster())game->view.DrawPartialRotatedDecal(drawPos+mountedSprOffset,GetMountedFrame().value().GetSourceImage()->Decal(),finalSpriteRot,GetMountedFrame().value().GetSourceRect().size/2,GetMountedFrame().value().GetSourceRect().pos,GetMountedFrame().value().GetSourceRect().size,vf2d(GetSizeMult()*(!HasFourWaySprites()&&GetFacingDirection()==Direction::EAST?-1:1),GetSizeMult()),blendCol);
std::vector<Buff>shieldBuffs=GetBuffs(BARRIER_DAMAGE_REDUCTION);
if(shieldBuffs.size()>0){
game->view.DrawRotatedDecal(drawPos,GFX["block.png"].Decal(),0.f,GFX["block.png"].Sprite()->Size()/2,{GetSizeMult(),GetSizeMult()});
}
if(GameSettings::TerrainCollisionBoxesEnabled()&&IsSolid()&&solidFadeTimer>0.f){
float distToPlayer=geom2d::line<float>(game->GetPlayer()->GetPos(),GetPos()).length();
const float collisionRadiusFactor=GetCollisionRadius()/12.f;
if(distToPlayer<24*3*collisionRadiusFactor){
game->DrawPie(game->view.WorldToScreen(GetPos()),GetCollisionRadius(),0.f,{255,0,0,uint8_t(128*(blendColAlpha/255.f)/sqrt(distToPlayer*collisionRadiusFactor))});
game->SetDecalMode(DecalMode::WIREFRAME);
game->DrawPie(game->view.WorldToScreen(GetPos()),GetCollisionRadius(),0.f,{128,0,0,255});
game->SetDecalMode(DecalMode::NORMAL);
}
}
#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()==Direction::EAST){
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;
}
}
B(Attribute::COLLIDED_WITH_PLAYER)=true;
Collision();
}
void Monster::Collision(Monster&m){
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(GetFacingDirection());
}
const bool Monster::AttackAvoided(const float attackZ)const{
return HasIframes()||abs(GetZ()-attackZ)>1;
}
bool Monster::Hurt(int damage,bool onUpperLevel,float z){
if(Invulnerable()||!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;
attackedByPlayer=true;
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));
}
using A=Attribute;
GetInt(A::HITS_UNTIL_DEATH)=std::max(0,GetInt(A::HITS_UNTIL_DEATH)-1);
ApplyIframes(GetFloat(A::IFRAME_TIME_UPON_HIT));
return true;
}
const bool Monster::IsAlive()const{
return hp>0||!diesNormally;
}
const vf2d&Monster::GetTargetPos()const{
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.f*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.f*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.f*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[const_cast<ItemInfo*>(&data.item)]++;
}
}
}
return drops;
}
void Monster::OnDeath(){
animation.ChangeState(internal_animState,GetDeathAnimationName());
if(HasMountedMonster()){
for(DeathSpawnInfo&deathInfo:deathData){
deathInfo.Spawn(GetPos(),OnUpperLevel());
}
mounted_animation={};
internal_mounted_animState={};
}
if(isBoss){
game->ReduceBossEncounterMobCount();
if(game->BossEncounterMobCount()==0){
const bool exitRingShouldNotSpawn=SPAWNER_CONTROLLER.has_value()&&!SPAWNER_CONTROLLER.value().empty();
if(exitRingShouldNotSpawn){ //See if we have a spawn controller and if we do, spawn the monsters from it instead of spawning the boss ring first.
const int nextSpawnerId=SPAWNER_CONTROLLER.value().front();
SPAWNER_CONTROLLER.value().pop();
SPAWNER_LIST[nextSpawnerId].SetTriggered(true);
}else{
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::BulletCollisionHitbox(){
return {GetPos(),GetCollisionRadius()*2};
}
void Monster::Knockback(const vf2d&vel){
//A new angle will be applied, but will be constrained by whichever applied velocity is strongest (either the current velocity, or the new one). This prevents continuous uncapped velocities to knockbacks applied.
if(vel==vf2d{})return;
float maxVelThreshold;
if(this->vel==vf2d{})maxVelThreshold=vel.mag();
else maxVelThreshold=std::max(vel.mag(),this->vel.mag());
this->vel+=vel;
float newVelAngle=this->vel.polar().y;
this->vel=vf2d{maxVelThreshold,newVelAngle}.cart();
}
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=HasFourWaySprites()?GetFacingDirectionToTarget(targetPos):Direction::EAST;
spriteRot=dirToPlayer;
}else{
facingDirection=HasFourWaySprites()?GetFacingDirectionToTarget(targetPos):Direction::WEST;
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[std::format("{}_{}",name,animation.currentStateName)];
}
const Animate2D::FrameSequence&Monster::GetAnimation(const std::string_view animationName)const{
return ANIMATION_DATA[std::format("{}_{}_{}",name,animationName,int(GetFacingDirection()))];
}
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;
}
const float Monster::GetDistanceFrom(vf2d target)const{
return geom2d::line<float>(GetPos(),target).length();
}
const Direction Monster::GetFacingDirectionToTarget(vf2d target)const{
float targetDirection=util::angleTo(GetPos(),target);
if(targetDirection<=PI/4&&targetDirection>-PI/4)return Direction::EAST;
else if(targetDirection>=3*PI/4||targetDirection<-3*PI/4)return Direction::WEST;
else if(targetDirection<=3*PI/4&&targetDirection>PI/4)return Direction::SOUTH;
else if(targetDirection>=-3*PI/4&&targetDirection<-PI/4)return Direction::NORTH;
ERR(std::format("WARNING! Target direction {} did not result in a proper facing direction!! THIS SHOULD NOT BE HAPPENING!",targetDirection));
return Direction::NORTH;
}
const bool Monster::HasFourWaySprites()const{
return MONSTER_DATA.at(name).HasFourWaySprites();
}
const bool Monster::HasMountedMonster()const{
if(internal_mounted_animState.has_value()^mounted_animation.has_value())ERR("WARNING! The internal mounted animation state and the mounted animation variables are not matching! They should both either be on or both be off! THIS SHOULD NOT BE HAPPENING!");
return internal_mounted_animState.has_value()&&mounted_animation.has_value();
}
const std::optional<const Animate2D::Frame>Monster::GetMountedFrame()const{
if(!HasMountedMonster())return {};
else return mounted_animation.value().GetFrame(internal_mounted_animState.value());
}
DeathSpawnInfo::DeathSpawnInfo(const std::string_view monsterName,const uint8_t spawnAmt,const vf2d spawnOffset)
:monsterSpawnName(monsterName),spawnAmt(spawnAmt),spawnLocOffset(spawnOffset){
if(!MONSTER_DATA.count(std::string(monsterName)))ERR(std::format("WARNING! Monster {} specified in DeathSpawnInfo does not exist! Please provide a proper monster name.",monsterName));
}
void DeathSpawnInfo::Spawn(const vf2d monsterDeathPos,const bool onUpperLevel){
for(uint8_t i=0;i<spawnAmt;i++){
game->SpawnMonster(monsterDeathPos+spawnLocOffset,MONSTER_DATA.at(monsterSpawnName),onUpperLevel).ApplyIframes(0.25f);
}
}
void Monster::ProximityKnockback(const vf2d centerPoint,const float knockbackFactor){
geom2d::line<float>lineToMonster(centerPoint,GetPos());
float dist=lineToMonster.length();
if(dist<0.001f){
float randomDir=util::random(2*PI);
lineToMonster={centerPoint,centerPoint+vf2d{cos(randomDir),sin(randomDir)}*1};
}
Knockback(lineToMonster.vector().norm()*knockbackFactor);
}
const bool Monster::IgnoresTerrainCollision()const{
return MONSTER_DATA.at(GetName()).IgnoresTerrainCollision();
}
const float Monster::TimeSpentAlive()const{
return timeSpentAlive;
}
const bool Monster::Immovable()const{
return MONSTER_DATA.at(GetName()).Immovable();
}
const bool Monster::Invulnerable()const{
return MONSTER_DATA.at(GetName()).Invulnerable();
}
const std::optional<float>Monster::GetLifetime()const{
return lifetime;
}
const std::optional<float>Monster::GetTotalLifetime()const{
return MONSTER_DATA.at(GetName()).GetLifetime();
}
const float Monster::GetCollisionRadius()const{
return MONSTER_DATA.at(GetName()).GetCollisionRadius()*GetSizeMult();
}
void Monster::MarkForDeletion(){
markedForDeletion=true;
}
const bool Monster::IsDead()const{
return !IsAlive();
}
void Monster::ApplyIframes(const float iframeTime){
iframe_timer=std::max(iframe_timer,iframeTime);
}
void Monster::_SetIframes(const float iframeTime){
iframe_timer=iframeTime;
}
const std::string_view Monster::GetDisplayName()const{
return MONSTER_DATA.at(GetName()).GetDisplayName();
}
const bool Monster::HasArrowIndicator()const{
return MONSTER_DATA.at(GetName()).HasArrowIndicator();
}
const bool Monster::ReachedTargetPos(const float maxDistanceFromTarget)const{
return util::distance(GetPos(),GetTargetPos())<=maxDistanceFromTarget;
}
const float Monster::GetHealthRatio()const{
return GetHealth()/float(GetMaxHealth());
}
const bool Monster::IsSolid()const{
return Immovable();
}