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.
994 lines
34 KiB
994 lines
34 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(Direction::SOUTH){
|
|
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");
|
|
|
|
}
|
|
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(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;
|
|
|
|
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){
|
|
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(vf2d facingTargetPoint){
|
|
float facingAngle=util::angleTo(GetPos(),facingTargetPoint);
|
|
vf2d diff=GetPos()-facingTargetPoint;
|
|
|
|
if(abs(facingAngle-prevFacingDirectionAngle)<(7*PI)/16.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(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()*(!HasFourWaySprites()&&GetFacingDirection()==Direction::EAST?-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()*(!HasFourWaySprites()&&GetFacingDirection()==Direction::WEST?-1:1),GetSizeMult()),{blendCol.r,blendCol.g,blendCol.b,overlaySpriteTransparency});
|
|
}
|
|
if(HasMountedMonster())game->view.DrawPartialRotatedDecal(GetPos()-vf2d{0,GetZ()}+mountedSprOffset,GetMountedFrame().value().GetSourceImage()->Decal(),spriteRot,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(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()==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;
|
|
}
|
|
}
|
|
|
|
#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(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(!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));
|
|
}
|
|
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.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){
|
|
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){
|
|
//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.
|
|
float 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 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).iframe_timer=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();
|
|
} |