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.
1141 lines
39 KiB
1141 lines
39 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);
|
|
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()){
|
|
const bool BothAreSolid=IsSolid()&&m->IsSolid();
|
|
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()||BothAreSolid){
|
|
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){
|
|
return _Hurt(damage,onUpperLevel,z,TrueDamageFlag::NORMAL_DAMAGE);
|
|
}
|
|
bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag damageRule){
|
|
const bool TrueDamage=damageRule==TrueDamageFlag::IGNORE_DAMAGE_RULES;
|
|
if(!TrueDamage&&(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);
|
|
|
|
if(TrueDamage){
|
|
mod_dmg=damage; //True damage override, ignore all damage changes.
|
|
crit=false; //True damage disables critting.
|
|
}
|
|
|
|
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()&&isBoss){
|
|
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(GetSizeT(Attribute::LOOPING_SOUND_ID)!=std::numeric_limits<size_t>::max()){//Just make sure on death any looping sound effect has been discarded proper.
|
|
SoundEffect::StopLoopingSFX(GetSizeT(Attribute::LOOPING_SOUND_ID));
|
|
GetSizeT(Attribute::LOOPING_SOUND_ID)=std::numeric_limits<size_t>::max();
|
|
}
|
|
|
|
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(strategyDeathFunc)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){
|
|
if(IsSolid())return;
|
|
//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();
|
|
}
|
|
|
|
//Sets the strategy death function that runs when a monster dies.
|
|
// The function should return false to indicate the event is over. If the event should keep running, return true.
|
|
//Arguments are:
|
|
// GameEvent& - The death event itself.
|
|
// Monster& - The monster reference
|
|
// const std::string& - The strategy name.
|
|
void Monster::SetStrategyDeathFunction(std::function<bool(GameEvent&,Monster&,const std::string&)>func){
|
|
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();
|
|
}
|
|
|
|
void Monster::_DealTrueDamage(const uint32_t damageAmt){
|
|
_Hurt(damageAmt,OnUpperLevel(),GetZ(),TrueDamageFlag::IGNORE_DAMAGE_RULES);
|
|
} |