#include "Monster.h" #include "MonsterStrategyHelpers.h" #include "DEFINES.h" #include "Crawler.h" #include "util.h" #include "safemap.h" #include "Effect.h" #include "FallingDebris.h" #include "MonsterAttribute.h" INCLUDE_game INCLUDE_BULLET_LIST INCLUDE_ANIMATION_DATA INCLUDE_MONSTER_NAME_DATA typedef Attribute A; void Monster::STRATEGY::SLIMEKING(Monster&m,float fElapsedTime,int strategyNumber){ float bulletSpd=ConfigFloat("BulletSpd")/100*24; m.F(A::SHOOT_TIMER)=std::max(0.f,m.F(A::SHOOT_TIMER)-fElapsedTime); m.F(A::SHOOT_RING_TIMER)=std::max(0.f,m.F(A::SHOOT_RING_TIMER)-fElapsedTime); m.F(A::SHOOT_RING_DELAY)=std::max(0.f,m.F(A::SHOOT_RING_DELAY)-fElapsedTime); m.F(A::JUMP_LANDING_TIMER)=std::max(0.f,m.F(A::JUMP_LANDING_TIMER)-fElapsedTime); m.F(A::CASTING_TIMER)=std::max(0.f,m.F(A::CASTING_TIMER)-fElapsedTime); m.F(A::RUN_AWAY_TIMER)=std::max(0.f,m.F(A::RUN_AWAY_TIMER)-fElapsedTime); const auto ShootBulletRing=[&](float angleOffset){ int bulletCount=ConfigInt("Phase1.RingBulletCount"); for(int i=0;i(m.GetPos(),vf2d{cos(angle),sin(angle)}*bulletSpd,6,ConfigInt("ProjectileDamage"),m.OnUpperLevel(),false,YELLOW,vf2d{6,6})); } }; const auto Landed=[&ShootBulletRing,&m](int currentPhase){ if(currentPhase==1){ ShootBulletRing(m.F(A::SHOOT_RING_OFFSET)); } }; const auto TransitionPhase=[&](int newPhase){ const int MAX_ATTEMPTS=100; //Maximum number of tries to find a valid location. Player*player=game->GetPlayer(); m.I(A::PATTERN_REPEAT_COUNT)=0; const auto PositionInRangeOfPlayer=[&player](vf2d&pos,float radius){ return geom2d::line(player->GetPos(),pos).length()<=player->GetSizeMult()*12*2+radius; }; const auto SpawnMonsterFromConfig=[&](int phase){ std::string spawnMonster=ConfigStringArr("Phase"+std::to_string(phase)+".MonsterSpawnOnChange",0); int spawnCount=ConfigIntArr("Phase"+std::to_string(phase)+".MonsterSpawnOnChange",1); for(int i=0;iGetSizeMult()*12);attempts++){ randomAngle=util::random(2*PI); spawnPos=m.pos+vf2d{cos(randomAngle),sin(randomAngle)}*m.GetSizeMult()*12; } game->SpawnMonster(spawnPos,MONSTER_NAME_DATA[spawnMonster]); } }; switch(newPhase){ case 2:{ SpawnMonsterFromConfig(2); }break; case 3:{ SpawnMonsterFromConfig(3); }break; case 4:{ SpawnMonsterFromConfig(4); }break; case 5:{ }break; } }; const auto StartJumpTowardsPlayer=[&](float jumpDuration,float recoveryTime,float jumpMoveSpd){ m.F(A::JUMP_ORIGINAL_LANDING_TIMER)=m.F(A::JUMP_LANDING_TIMER)=jumpDuration; m.B(A::JUMP_TOWARDS_PLAYER)=true; m.F(A::RECOVERY_TIME)=recoveryTime; m.F(A::JUMP_MOVE_SPD)=jumpMoveSpd; m.SetState(State::JUMP); }; const auto StartJump=[&](float jumpDuration,vf2d targetPos,float recoveryTime,float jumpMoveSpd){ m.F(A::JUMP_ORIGINAL_LANDING_TIMER)=m.F(A::JUMP_LANDING_TIMER)=jumpDuration; m.V(A::JUMP_TARGET_POS)=targetPos; m.B(A::JUMP_TOWARDS_PLAYER)=false; m.F(A::RECOVERY_TIME)=recoveryTime; m.F(A::JUMP_MOVE_SPD)=jumpMoveSpd; m.SetState(State::JUMP); }; const auto Recovered=[&](){ switch(m.phase){ case 2:{ switch(m.I(A::JUMP_COUNT)){ case 1: case 2:{ //After the 1st and 2nd jumps we still have another jump to accomplish. m.I(A::JUMP_COUNT)++; float jumpTime=ConfigFloatArr("Phase2.Jump["+std::to_string(m.I(A::JUMP_COUNT))+"]",0); float jumpSpd=ConfigFloatArr("Phase2.Jump["+std::to_string(m.I(A::JUMP_COUNT))+"]",1); StartJumpTowardsPlayer(jumpTime,0.2,jumpSpd); }break; default:{ m.PerformIdleAnimation(); m.SetState(State::NORMAL); } } }break; } }; if(m.F(A::RUN_AWAY_TIMER)>0){ Monster::STRATEGY::RUN_AWAY(m,fElapsedTime,4); /*HACK ALERT!! This is kind of a hack. If the Run Away script changes the 4 would be inaccurate, but the run away script doesn't use this value so it's probably fine.*/ return; } if(m.GetState()==State::RECOVERY){ m.F(A::RECOVERY_TIME)=std::max(0.f,m.F(A::RECOVERY_TIME)-fElapsedTime); if(m.F(A::RECOVERY_TIME)==0){ m.SetState(State::NORMAL); Recovered(); } return; } if(m.GetState()==State::JUMP){ float jumpLandingTimerRatio=m.F(A::JUMP_LANDING_TIMER)/m.F(A::JUMP_ORIGINAL_LANDING_TIMER); vf2d jumpTargetPos=m.V(A::JUMP_TARGET_POS); if(m.B(A::JUMP_TOWARDS_PLAYER)){ jumpTargetPos=game->GetPlayer()->GetPos(); } if(m.GetPos().x>jumpTargetPos.x){ m.SetX(std::max(jumpTargetPos.x,m.GetPos().x-m.F(A::JUMP_MOVE_SPD)*game->GetElapsedTime())); } else if(m.GetPos().xGetElapsedTime())); } if(m.GetPos().y>jumpTargetPos.y){ m.SetY(std::max(jumpTargetPos.y,m.GetPos().y-m.F(A::JUMP_MOVE_SPD)*game->GetElapsedTime())); } else if(m.GetPos().yGetElapsedTime())); } if(m.F(A::JUMP_LANDING_TIMER)>=m.F(A::JUMP_ORIGINAL_LANDING_TIMER)/2){ m.SetZ(util::lerp(0,ConfigInt("JumpHeight"),1-jumpLandingTimerRatio)); }else{ m.SetZ(util::lerp(0,ConfigInt("JumpHeight"),jumpLandingTimerRatio*2)); } if(m.F(A::JUMP_LANDING_TIMER)==0){ m.state=State::RECOVERY; game->SetupWorldShake(0.6); geom2d::linelineToPlayer(m.GetPos(),game->GetPlayer()->GetPos()); float dist=lineToPlayer.length(); for(int i=0;i<200;i++){ float randomDir=util::random(2*PI); game->AddEffect(std::make_unique(m.GetPos()+vf2d{cos(randomDir),sin(randomDir)}*m.GetSizeMult()*8,util::random(1),"circle.png",m.OnUpperLevel(),vf2d{1,1},0.5,vf2d{cos(randomDir)*util::random(5),sin(randomDir)*-util::random(15)-5}*30,BLACK),true); } if(dist<12*m.GetSizeMult()){ game->GetPlayer()->Hurt(ConfigInt("JumpAttackDamage"),m.OnUpperLevel(),m.GetZ()); if(dist<0.001){ float randomDir=util::random(2*PI); lineToPlayer={m.GetPos(),m.GetPos()+vf2d{cos(randomDir),sin(randomDir)}*1}; } game->GetPlayer()->Knockback(lineToPlayer.vector().norm()*ConfigInt("JumpKnockbackFactor")); if(m.phase!=2){ //In phase 2, the player can get slammed multiple times. No iframes for messing up. game->GetPlayer()->SetIframes(1); } } m.SetZ(0); Landed(m.phase); m.SetStrategyDrawFunction([](Crawler*game){}); } else if(m.F(A::JUMP_LANDING_TIMER)<=ConfigFloat("JumpWarningIndicatorTime")){ m.SetStrategyDrawFunction([&](Crawler*game){ Decal*dec=ANIMATION_DATA["range_indicator.png"].GetFrame(game->GetElapsedTime()).GetSourceImage()->Decal(); game->view.DrawRotatedDecal(m.GetPos(),dec,0,dec->sprite->Size()/2,vf2d{m.GetSizeMult(),m.GetSizeMult()},RED); }); } return; } if(m.GetState()==State::CASTING){ m.UpdateAnimation("monsters/Slime King - Cast.png"); if(m.F(A::CASTING_TIMER)==0){ m.SetState(State::NORMAL); m.I(A::JUMP_COUNT)++; float jumpTime=ConfigFloatArr("Phase2.Jump["+std::to_string(m.I(A::JUMP_COUNT))+"]",0); float jumpSpd=ConfigFloatArr("Phase2.Jump["+std::to_string(m.I(A::JUMP_COUNT))+"]",1); StartJumpTowardsPlayer(jumpTime,0.2,jumpSpd); } return; } switch(m.phase){ case 0:{ m.size=ConfigInt("Phase1.Size")/100; m.diesNormally=false; m.F(A::IFRAME_TIME_UPON_HIT)=0; m.iframe_timer=ConfigFloat("Phase5.IframeTimePerHit"); m.phase=ConfigInt("StartPhase"); }break; case 1:{ if(m.hp<=m.maxhp*ConfigFloat("Phase2.Change")/100){ m.phase=2; m.SetSize(ConfigFloat("Phase2.Size")/100,false); TransitionPhase(m.phase); return; } if(m.F(A::SHOOT_RING_TIMER)==0){ if(m.I(A::PATTERN_REPEAT_COUNT)>=ConfigInt("Phase1.JumpAfter")){ StartJumpTowardsPlayer(ConfigFloat("Phase1.AirborneTime"),ConfigFloat("Phase1.LandingRecoveryTime"),ConfigFloat("JumpMoveSpd")); m.I(A::PATTERN_REPEAT_COUNT)=0; return; } m.I(A::SHOOT_RING_COUNTER)=ConfigInt("Phase1.ShootRingCount")-1; m.F(A::SHOOT_RING_DELAY)=ConfigFloat("Phase1.ShootRingDelay"); ShootBulletRing(m.F(A::SHOOT_RING_OFFSET)); m.F(A::SHOOT_RING_TIMER)=ConfigFloat("Phase1.ShootRepeatTime"); m.B(A::SHOOT_RING_RIGHT)=bool(rand()%2); m.I(A::PATTERN_REPEAT_COUNT)++; } if(m.I(A::SHOOT_RING_COUNTER)>0){ if(m.F(A::SHOOT_RING_DELAY)==0){ m.I(A::SHOOT_RING_COUNTER)--; m.F(A::SHOOT_RING_DELAY)=ConfigFloat("Phase1.ShootRingDelay"); if(m.B(A::SHOOT_RING_RIGHT)){ m.F(A::SHOOT_RING_OFFSET)+=util::degToRad(ConfigFloat("Phase1.RingOffset")); }else{ m.F(A::SHOOT_RING_OFFSET)-=util::degToRad(ConfigFloat("Phase1.RingOffset")); } ShootBulletRing(m.F(A::SHOOT_RING_OFFSET)); } } }break; case 2:{ if(m.hp<=m.maxhp*ConfigFloat("Phase3.Change")/100){ m.phase=3; m.SetSize(ConfigFloat("Phase3.Size")/100,false); TransitionPhase(m.phase); return; } if(m.F(A::SHOOT_TIMER)==0){ m.F(A::SHOOT_TIMER)=ConfigInt("Phase2.ShootRate"); m.I(A::PATTERN_REPEAT_COUNT)++; int bulletCount=ConfigInt("Phase2.ShootProjectileCount"); for(int i=0;iGetPlayer()->GetPos()); float angle=(i-(bulletCount/2))*util::degToRad(ConfigFloat("Phase2.ShootAngleSpread"))+initialAngle; BULLET_LIST.emplace_back(std::make_unique(m.GetPos(),vf2d{cos(angle),sin(angle)}*bulletSpd,6,ConfigInt("ProjectileDamage"),m.OnUpperLevel(),false,YELLOW,vf2d{6,6})); } } if(m.I(A::PATTERN_REPEAT_COUNT)>ConfigInt("Phase2.ShootCount")){ m.I(A::PATTERN_REPEAT_COUNT)=0; m.I(A::JUMP_COUNT)=0; m.F(A::CASTING_TIMER)=5; m.SetState(State::CASTING); } }break; case 3:{ if(m.hp<=m.maxhp*ConfigFloat("Phase4.Change")/100){ m.phase=4; m.SetSize(ConfigFloat("Phase4.Size")/100,false); m.AddBuff(BuffType::SLOWDOWN,99999,ConfigFloat("Phase4.MoveSpdModifier")/100); TransitionPhase(m.phase); return; } if(m.I(A::PATTERN_REPEAT_COUNT)==0){ StartJumpTowardsPlayer(ConfigFloat("Phase3.JumpDelayTime"),ConfigFloat("Phase3.JumpRecoveryTime"),ConfigFloat("Phase3.JumpMoveSpd")); m.I(A::PATTERN_REPEAT_COUNT)++; }else if(m.I(A::PATTERN_REPEAT_COUNT)<4&&m.F(A::SHOOT_TIMER)==0){ m.F(A::SHOOT_TIMER)=ConfigFloat("Phase3.ShootRate"); m.I(A::PATTERN_REPEAT_COUNT)++; int bulletCount=ConfigInt("Phase3.ShootProjectileCount"); for(int i=0;iGetPlayer()->GetPos()); float angle=(i-(bulletCount/2))*util::degToRad(ConfigFloat("Phase3.ShootAngleSpread"))+initialAngle; BULLET_LIST.emplace_back(std::make_unique(m.GetPos(),vf2d{cos(angle),sin(angle)}*bulletSpd,6,ConfigInt("ProjectileDamage"),m.OnUpperLevel(),false,YELLOW,vf2d{6,6})); } }else if(m.I(A::PATTERN_REPEAT_COUNT)>=4){ m.F(A::RECOVERY_TIME)=ConfigFloat("Phase3.PhaseRecoveryTime"); m.SetState(State::RECOVERY); m.I(A::PATTERN_REPEAT_COUNT)=0; } }break; case 4:{ if(m.hp<=1){ //HP can't reach 0 when the dies normally flag is on. m.phase=5; m.F(A::IFRAME_TIME_UPON_HIT)=1; m.I(A::HITS_UNTIL_DEATH)=int(m.GetSizeMult()*100/ConfigFloat("Phase5.SizeLossPerHit"))-1; TransitionPhase(m.phase); return; } if(m.I(A::PHASE_REPEAT_COUNT)>=5){ m.I(A::PHASE_REPEAT_COUNT)=0; float jumpAngle=util::angleTo(m.GetPos(),game->GetCurrentMap().MapSize*game->GetCurrentMap().tilewidth/2); //We jump towards the center to keep the player from constantly dealing with a stuck boss. float jumpDistance=ConfigFloat("Phase4.JumpDistance")/100*game->GetCurrentMap().tilewidth; float jumpSpd=jumpDistance/ConfigFloat("Phase4.JumpDuration"); StartJump(ConfigFloat("Phase4.JumpDuration"),m.GetPos()+vf2d{cos(jumpAngle)*jumpDistance,sin(jumpAngle)*jumpDistance},0,jumpSpd); }else if(m.I(A::PATTERN_REPEAT_COUNT)<5&&m.F(A::SHOOT_TIMER)==0){ m.I(A::PATTERN_REPEAT_COUNT)++; m.F(A::SHOOT_TIMER)=ConfigFloat("Phase4.ShootRate"); float bulletAngle=util::angleTo(m.GetPos(),game->GetPlayer()->GetPos()); float spreadAngle=util::degToRad(ConfigFloat("Phase4.RandomOffsetAngle")); bulletAngle+=util::random(spreadAngle*2)-spreadAngle; BULLET_LIST.emplace_back(std::make_unique(m.GetPos(),vf2d{cos(bulletAngle),sin(bulletAngle)}*bulletSpd,6,ConfigInt("ProjectileDamage"),m.OnUpperLevel(),false,YELLOW,vf2d{6,6})); }else if(m.I(A::PATTERN_REPEAT_COUNT)==5){ m.I(A::PATTERN_REPEAT_COUNT)++; m.F(A::RUN_AWAY_TIMER)=ConfigFloat("Phase4.RunAwayTime"); }else if(m.I(A::PATTERN_REPEAT_COUNT)==6&&m.F(A::RUN_AWAY_TIMER)==0){ m.F(A::RECOVERY_TIME)=ConfigFloat("Phase4.WaitTime"); m.SetState(State::RECOVERY); m.I(A::PATTERN_REPEAT_COUNT)=0; m.I(A::PHASE_REPEAT_COUNT)++; } }break; case 5:{ float targetSize=ConfigFloat("Phase5.SizeLossPerHit")/100*m.I(A::HITS_UNTIL_DEATH); Monster::STRATEGY::RUN_AWAY(m,fElapsedTime,4); if(targetSize>0){ m.SetSize(targetSize,false); }else{ m.diesNormally=true; } }break; } }