#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 © 2023 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_ANIMATION_DATA INCLUDE_MONSTER_DATA INCLUDE_MONSTER_LIST INCLUDE_DAMAGENUMBER_LIST INCLUDE_game INCLUDE_BULLET_LIST INCLUDE_DATA INCLUDE_GFX safemap>STRATEGY_DATA; std::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(DOWN){ bool firstAnimation=true; for(std::string&anim:data.GetAnimations()){ animation.AddState(anim,ANIMATION_DATA[anim]); if(firstAnimation){ animation.ChangeState(internal_animState,anim); firstAnimation=false; } } stats.A("Health")=data.GetHealth(); stats.A("Attack")=data.GetAttack(); stats.A("Move Spd %")=data.GetMoveSpdMult(); randomFrameOffset=(util::random()%1000)/1000.f; monsterWalkSoundTimer=util::random(1.f); } vf2d&Monster::GetPos(){ return pos; } const int Monster::GetHealth()const{ return hp; } const int Monster::GetMaxHealth()const{ return stats.A_Read("Health"); } const float Monster::GetRemainingHPPct()const{ return float(GetHealth())/GetMaxHealth(); } int Monster::GetAttack(){ float mod_atk=float(stats.A("Attack")); mod_atk+=Get("Attack %"); mod_atk+=Get("Attack"); return int(mod_atk); } float Monster::GetMoveSpdMult(){ float moveSpdPct=stats.A("Move Spd %")/100.f; float mod_moveSpd=moveSpdPct; for(Buff&b:GetBuffs(SLOWDOWN)){ mod_moveSpd-=moveSpdPct*b.intensity; } for(Buff&b:GetBuffs(LOCKON_SPEEDBOOST)){ mod_moveSpd+=moveSpdPct*b.intensity; } for(Buff&b:GetBuffs(SPEEDBOOST)){ mod_moveSpd+=moveSpdPct*b.intensity; } return mod_moveSpd; } float Monster::GetSizeMult(){ return size; } Animate2D::Frame Monster::GetFrame(){ return animation.GetFrame(internal_animState); } void Monster::PerformJumpAnimation(){ animation.ChangeState(internal_animState,MONSTER_DATA[name].GetJumpAnimation()); } void Monster::PerformShootAnimation(){ animation.ChangeState(internal_animState,MONSTER_DATA[name].GetShootAnimation()); } void Monster::PerformIdleAnimation(){ animation.ChangeState(internal_animState,MONSTER_DATA[name].GetIdleAnimation()); } void Monster::PerformNPCDownAnimation(){ animation.ChangeState(internal_animState,MONSTER_DATA[name].GetIdleAnimation()); } void Monster::PerformNPCUpAnimation(){ animation.ChangeState(internal_animState,MONSTER_DATA[name].GetJumpAnimation()); } void Monster::PerformNPCLeftAnimation(){ animation.ChangeState(internal_animState,MONSTER_DATA[name].GetShootAnimation()); } void Monster::PerformNPCRightAnimation(){ animation.ChangeState(internal_animState,MONSTER_DATA[name].GetDeathAnimation()); } void Monster::PerformOtherAnimation(const uint8_t otherInd){ animation.ChangeState(internal_animState,MONSTER_DATA[name].GetAnimations()[4+otherInd]); } bool Monster::_SetX(float x,const bool monsterInvoked){ vf2d newPos={x,pos.y}; vi2d tilePos=vi2d(newPos/float(game->GetCurrentMapData().tilewidth))*game->GetCurrentMapData().tilewidth; geom2d::rectcollisionRect=game->GetTileCollision(game->GetCurrentLevel(),newPos,upperLevel); if(isBoss||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}; #pragma region lambdas auto NoEnemyCollisionWithTile=[&](){return 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(isBoss||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}; #pragma region lambdas auto NoEnemyCollisionWithTile=[&](){return 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); 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(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(Monster&m:MONSTER_LIST){ if(&m==this)continue; if(!m.HasIframes()&&OnUpperLevel()==m.OnUpperLevel()&&abs(m.GetZ()-GetZ())<=1&&geom2d::overlaps(geom2d::circle(pos,12*size/2),geom2d::circle(m.GetPos(),12*m.GetSizeMult()/2))){ m.Collision(*this); geom2d::line line(pos,m.GetPos()); float dist = line.length(); m.SetPos(line.rpoint(dist*1.1f)); if(m.IsAlive()){ vel=line.vector().norm()*-128; } } } if(!game->GetPlayer()->HasIframes()&&abs(game->GetPlayer()->GetZ()-GetZ())<=1&&game->GetPlayer()->OnUpperLevel()==OnUpperLevel()&&geom2d::overlaps(geom2d::circle(pos,12*size/2),geom2d::circle(game->GetPlayer()->GetPos(),12*game->GetPlayer()->GetSizeMult()/2))){ geom2d::line line(pos,game->GetPlayer()->GetPos()); float dist = line.length(); SetPos(line.rpoint(-0.1f)); vel=line.vector().norm()*-128; } } if(GetState()==State::NORMAL){ if(game->GetPlayer()->GetX()>pos.x){ facingDirection=RIGHT; } else { facingDirection=LEFT; } } Monster::STRATEGY::RUN_STRATEGY(*this,fElapsedTime); if(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); } if(vel!=vf2d{0,0}){ SetX(pos.x+vel.x*fElapsedTime); SetY(pos.y+vel.y*fElapsedTime); } } if(!IsAlive()){ deathTimer+=fElapsedTime; if(deathTimer>3){ return false; } } animation.UpdateState(internal_animState,randomFrameOffset+fElapsedTime); randomFrameOffset=0; return true; } Key Monster::GetFacingDirection(){ return facingDirection; } void Monster::UpdateFacingDirection(vf2d facingTargetPoint){ if(facingTargetPoint.x>GetPos().x){ facingDirection=RIGHT; } if(facingTargetPoint.x0){ vf2d shadowScale=vf2d{8*GetSizeMult()/3.f,1}/std::max(1.f,GetZ()/24); game->view.DrawDecal(GetPos()-vf2d{3,3}*shadowScale/2+vf2d{0,6*GetSizeMult()},GFX["circle.png"].Decal(),shadowScale,BLACK); } Pixel blendCol=GetBuffs(BuffType::SLOWDOWN).size()>0?Pixel{uint8_t(255*abs(sin(1.4*GetBuffs(BuffType::SLOWDOWN)[0].duration))),uint8_t(255*abs(sin(1.4*GetBuffs(BuffType::SLOWDOWN)[0].duration))),uint8_t(128+127*abs(sin(1.4*GetBuffs(BuffType::SLOWDOWN)[0].duration)))}:WHITE; game->view.DrawPartialRotatedDecal(GetPos()-vf2d{0,GetZ()},GetFrame().GetSourceImage()->Decal(),spriteRot,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,vf2d(GetSizeMult()*(GetFacingDirection()==RIGHT?-1:1),GetSizeMult()),blendCol); if(overlaySprite.length()!=0){ game->view.DrawPartialRotatedDecal(GetPos()-vf2d{0,GetZ()},GFX[overlaySprite].Decal(),spriteRot,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,vf2d(GetSizeMult()*(GetFacingDirection()==RIGHT?-1:1),GetSizeMult()),{blendCol.r,blendCol.g,blendCol.b,overlaySpriteTransparency}); } std::vectorshieldBuffs=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;indexpathIndex+1){ col=GREY; } game->view.FillRectDecal(path.GetSplinePoint(index).pos,{1,1},col); } for(size_t counter=0;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()==RIGHT){ points={ vf2d{defaultPos+spriteSize/2+vf2d{bottomExpansionAmount,0}}, //BR vf2d{defaultPos+vf2d{spriteSize.x/2,-spriteSize.y/2}}, //TR vf2d{defaultPos-spriteSize.x/2}, //TL vf2d{defaultPos+vf2d{-spriteSize.x/2,spriteSize.y}-vf2d{bottomExpansionAmount,0}}, //BL }; } game->view.DrawPartialWarpedDecal(GetFrame().GetSourceImage()->Decal(),points,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size); game->SetDecalMode(DecalMode::NORMAL); } void Monster::Collision(Player*p){ if(GetCollisionDamage()>0&&lastHitPlayer==0.0f){ if(p->Hurt(GetCollisionDamage(),OnUpperLevel(),GetZ())){ lastHitPlayer=1.0f; } } #pragma region Knockback due to buffs vf2d knockbackVecNorm=geom2d::line(GetPos(),p->GetPos()).vector().norm(); float knockbackStrength=0.f; std::vector 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(GetPos(),m.GetPos()).vector().norm(); float knockbackStrength=0.f; std::vector 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(){ std::map>&zoneData=game->GetZoneData(game->GetCurrentLevel()); for(ZoneData&upperLevelZone:zoneData["UpperZone"]){ if(geom2d::overlaps(upperLevelZone.zone,pos)){ upperLevel=true; } } for(ZoneData&lowerLevelZone:zoneData["LowerZone"]){ if(geom2d::overlaps(lowerLevelZone.zone,pos)){ upperLevel=false; } } monsterWalkSoundTimer+=game->GetElapsedTime(); if(monsterWalkSoundTimer>1.f){ monsterWalkSoundTimer-=1.f; SoundEffect::PlaySFX(GetWalkSound(),GetPos()); } if(!std::isfinite(pos.x)){ ERR(std::format("WARNING! Player X position is {}...Trying to recover. THIS SHOULD NOT BE HAPPENING!",pos.x)); pos.x=spawnPos.x; } if(!std::isfinite(pos.y)){ ERR(std::format("WARNING! Player Y position is {}...Trying to recover. THIS SHOULD NOT BE HAPPENING!",pos.y)); pos.y=spawnPos.y; } } std::string Monster::GetDeathAnimationName(){ return MONSTER_DATA[name].GetDeathAnimation(); } const bool Monster::AttackAvoided(const float attackZ)const{ return HasIframes()||abs(GetZ()-attackZ)>1; } bool Monster::Hurt(int damage,bool onUpperLevel,float z){ if(!IsAlive()||onUpperLevel!=OnUpperLevel()||AttackAvoided(z)) return false; if(game->InBossEncounter()){ game->StartBossEncounter(); } game->GetPlayer()->ResetLastCombatTime(); float mod_dmg=float(damage); #pragma region Handle Crits bool crit=false; if(util::random(1)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; 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>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; } bool Monster::OnUpperLevel(){ 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); path=game->pathfinder.Solve_WalkPath(pos,target,24,OnUpperLevel()); 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){ 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()){ SetPos(pos+moveTowardsLine.vector().norm()*100*fElapsedTime*GetMoveSpdMult()); /*if(!SetPos(pos+moveTowardsLine.vector().norm()*100*fElapsedTime*GetMoveSpdMult())){ //We are stuck, so stop pathfinding. path.points.clear(); pathIndex=0; targetAcquireTimer=0; }*/ UpdateFacingDirection(moveTowardsLine.end); }else{ if(pathIndex>=path.points.size()-1){ //We have reached the end of the path! pathIndex=0; targetAcquireTimer=0; }else{ while(moveTowardsLine.length()<100*fElapsedTime*GetMoveSpdMult()){ pathIndex+=0.1f; moveTowardsLine=geom2d::line(pos,path.GetSplinePoint(pathIndex).pos); if(pathIndex>=path.points.size()-1){ //We have reached the end of the path! pathIndex=0; targetAcquireTimer=0; break; } } } } } 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;iReduceBossEncounterMobCount(); if(game->BossEncounterMobCount()==0){ 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. } } 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::Hitbox(){ return {GetPos(),12*GetSizeMult()}; } void Monster::Knockback(const vf2d&vel){ this->vel+=vel; } void Monster::Knockup(float duration){ knockUpTimer+=duration; totalKnockupTime+=duration; knockUpZAmt+=32*pow(duration,2); } const std::string&Monster::GetName()const{ return name; } void Monster::RotateTowardsPos(const vf2d&targetPos){ float dirToPlayer=util::angleTo(GetPos(),targetPos); #pragma region Face towards lockon direction if(abs(dirToPlayer)<0.5f*PI){ //This sprite is supposed to be facing right (flipped) facingDirection=RIGHT; spriteRot=dirToPlayer; }else{ facingDirection=LEFT; if(dirToPlayer>0){ spriteRot=-PI+dirToPlayer; }else{ spriteRot=PI+dirToPlayer; } } #pragma endregion } const float Monster::GetDamageReductionFromBuffs()const{ float dmgReduction=0; for(const Buff&b:GetBuffs(BuffType::DAMAGE_REDUCTION)){ dmgReduction+=b.intensity; } for(const Buff&b:GetBuffs(BuffType::BARRIER_DAMAGE_REDUCTION)){ dmgReduction+=b.intensity; } return std::min(1.0f,dmgReduction); } const float Monster::GetCollisionDamage()const{ float collisionDmg=0.f; for(Buff&b:GetBuffs(FIXED_COLLISION_DMG)){ collisionDmg+=b.intensity; } if(collisionDmg>0)return collisionDmg; else return MONSTER_DATA[name].GetCollisionDmg(); } void Monster::SetStrategyDeathFunction(std::functionfunc){ hasStrategyDeathFunction=true; strategyDeathFunc=func; } const bool Monster::IsNPC()const{ return MONSTER_DATA[name].IsNPC(); } const bool MonsterData::IsNPC()const{ return isNPC; }