#pragma region License /* License (OLC-3) ~~~~~~~~~~~~~~~ Copyright 2024 Joshua Sigona 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>STRATEGY_DATA; std::unordered_mapMonsterData::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::rectcollisionRect=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::rectcollision={collisionRect.pos,collisionRect.size}; bool insideArenaBounds=true; #pragma region Calculate Arena Bounds check for Bosses if(isBoss){ const geom2d::rectarenaBounds=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(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::rectcollisionRect=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::rectcollision={collisionRect.pos,collisionRect.size}; bool insideArenaBounds=true; #pragma region Calculate Arena Bounds check for Bosses if(isBoss){ const geom2d::rectarenaBounds=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(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::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&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 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)abs(diff.y)){ if(facingTargetPoint.x>GetPos().x){ facingDirection=Direction::EAST; } if(facingTargetPoint.xGetPos().y){ facingDirection=Direction::SOUTH; } if(facingTargetPoint.y0){ 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::vectorshieldBuffs=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(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;indexpathIndex+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(counterpathIndex+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::arraypoints={ 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>&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)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(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>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&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::vectorMonster::GetBuffs(BuffType buff)const{ std::vectorfilteredBuffs; 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&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::functionfunc){ strategyDraw=func; } void Monster::SetStrategyDrawOverlayFunction(std::functionfunc){ strategyDrawOverlay=func; } std::mapMonster::SpawnDrops(){ std::mapdrops; 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(&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{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::rectarenaBounds=game->GetZones().at("BossArena")[0].zone; geom2d::rectclampedArena{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(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::circleMonster::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(); } void Monster::SetStrategyDeathFunction(std::functionfunc){ 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::linelosLine=geom2d::line(GetPos(),targetPos); float losLineLength=losLine.length(); float losLineMarker=0.f; bool hasLoS=true; while(losLineMarkerGetTileCollision(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(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::optionalMonster::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;iSpawnMonster(monsterDeathPos+spawnLocOffset,MONSTER_DATA.at(monsterSpawnName),onUpperLevel).ApplyIframes(0.25f); } } void Monster::ProximityKnockback(const vf2d centerPoint,const float knockbackFactor){ geom2d::linelineToMonster(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::optionalMonster::GetLifetime()const{ return lifetime; } const std::optionalMonster::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(); }