#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),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; } int Monster::GetHealth(){ return hp; } 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::UpdateAnimation(std::string state){ animation.ChangeState(internal_animState,state); } 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()); } 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.pos==game->NO_COLLISION.pos&&collisionRect.size==game->NO_COLLISION.size){ 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 !geom2d::overlaps(geom2d::circle(newPos,game->GetCurrentMapData().tilewidth/2*GetSizeMult()),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.pos==game->NO_COLLISION.pos&&collisionRect.size==game->NO_COLLISION.size){ 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 !geom2d::overlaps(geom2d::circle(newPos,game->GetCurrentMapData().tilewidth/2*GetSizeMult()),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); } } 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::Draw(){ if(GetZ()>0){ vf2d shadowScale=vf2d{8*GetSizeMult()/3.f,1}/std::max(1.f,GetZ()/24); game->view.DrawDecal(GetPos()-vf2d{3,3}*shadowScale/2+vf2d{0,6*GetSizeMult()},GFX["circle.png"].Decal(),shadowScale,BLACK); } 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()),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); #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); game->view.DrawPartialRotatedDecal(GetPos()+vf2d{drawRatioX*GetFrame().GetSourceRect().size.x,GetZ()+(GetFrame().GetSourceRect().size.y-16)*GetSizeMult()},GetFrame().GetSourceImage()->Decal(),0,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,vf2d(GetSizeMult()*(GetFacingDirection()==RIGHT?-1:1)*multiplierX,GetSizeMult()*-1),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->SetDecalMode(DecalMode::NORMAL); } void Monster::Collision(Player*p){ if(MONSTER_DATA[name].GetCollisionDmg()>0&&lastHitPlayer==0.0f){ if(p->Hurt(MONSTER_DATA[name].GetCollisionDmg(),OnUpperLevel(),GetZ())){ lastHitPlayer=1.0f; } } 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(){ 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()); } } std::string Monster::GetDeathAnimationName(){ return MONSTER_DATA[name].GetDeathAnimation(); } bool Monster::Hurt(int damage,bool onUpperLevel,float z){ if(!IsAlive()||onUpperLevel!=OnUpperLevel()||HasIframes()||abs(GetZ()-z)>1) 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 for(Buff&b:GetBuffs(BuffType::DAMAGE_REDUCTION)){ mod_dmg-=damage*b.intensity; } 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; } 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}); } bool Monster::StartPathfinding(float pathingTime){ SetState(State::PATH_AROUND); path=game->pathfinder.Solve_WalkPath(pos,target,12,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()>2){ if(!SetPos(pos+moveTowardsLine.vector().norm()*100*fElapsedTime*GetMoveSpdMult())){ //We are stuck, so stop pathfinding. path.points.clear(); pathIndex=0; targetAcquireTimer=0; } if(moveTowardsLine.vector().x>0){ facingDirection=RIGHT; } else { facingDirection=LEFT; } }else{ if(pathIndex>=path.points.size()-1){ //We have reached the end of the path! pathIndex=0; targetAcquireTimer=0; }else{ pathIndex+=0.5f; } } } else { //We actually can't do anything so just quit. targetAcquireTimer=0; } } std::vectorMonster::GetBuffs(BuffType buff){ std::vectorfilteredBuffs; std::copy_if(buffList.begin(),buffList.end(),std::back_inserter(filteredBuffs),[buff](Buff&b){return b.type==buff;}); return filteredBuffs; } State::State Monster::GetState(){ return state; } void Monster::SetState(State::State newState){ state=newState; } bool Monster::HasIframes(){ return iframe_timer>0; } float Monster::GetZ(){ 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::OnDeath(){ animation.ChangeState(internal_animState,GetDeathAnimationName()); if(isBoss){ game->ReduceBossEncounterMobCount(); } 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;iGetPlayer()->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; }