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.
359 lines
16 KiB
359 lines
16 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 "AdventuresInLestoria.h"
|
|
#include "DEFINES.h"
|
|
#include "Monster.h"
|
|
#include "MonsterStrategyHelpers.h"
|
|
#include "BulletTypes.h"
|
|
#include "util.h"
|
|
|
|
INCLUDE_game
|
|
INCLUDE_MONSTER_DATA
|
|
INCLUDE_WINDOW_SIZE
|
|
INCLUDE_GFX
|
|
INCLUDE_BULLET_LIST
|
|
|
|
using A=Attribute;
|
|
|
|
void Monster::STRATEGY::ZEPHY(Monster&m,float fElapsedTime,std::string strategy){
|
|
enum Phase{
|
|
INITIALIZE,
|
|
IDLE,
|
|
ATTACK_RESET,
|
|
FLY_ACROSS_PREPARE,
|
|
FLY_ACROSS,
|
|
TORNADO_ATTACK_PREPARE,
|
|
TORNADO_ATTACK,
|
|
WIND_ATTACK_FLY,
|
|
WIND_ATTACK_LAND,
|
|
WIND_ATTACK,
|
|
HALFHEALTH_PREPARE_PHASE,
|
|
HALFHEALTH_PHASE,
|
|
};
|
|
|
|
enum class AttackChoice{
|
|
RIGHT,
|
|
LEFT
|
|
};
|
|
|
|
if(m.phase!=HALFHEALTH_PHASE)m.F(A::SPAWNER_TIMER)-=fElapsedTime;
|
|
if(m.F(A::SPAWNER_TIMER)<=0.f){
|
|
const float randomDir=util::random(2*PI);
|
|
game->SpawnMonster(m.GetPos()+vf2d{ConfigFloat("Basic Hawk Spawn Radius"),randomDir}.cart(),MONSTER_DATA.at("Hawk_NOXP"),m.OnUpperLevel(),true);
|
|
m.F(A::SPAWNER_TIMER)=ConfigFloat("Basic Hawk Spawn Time");
|
|
}
|
|
|
|
#pragma region Flying Hover Effect
|
|
if(m.F(A::TARGET_FLYING_HEIGHT)==0.f)m.SetZ(std::max(m.F(A::FLYING_HEIGHT),0.f));
|
|
else m.SetZ(std::max(m.F(A::FLYING_HEIGHT)+ConfigFloat("Flight Oscillation Amount")*sin((PI*m.TimeSpentAlive())/1.5f),0.f));
|
|
#pragma endregion
|
|
if(m.F(A::FLYING_HEIGHT)<m.F(A::TARGET_FLYING_HEIGHT))m.F(A::FLYING_HEIGHT)=std::min(m.F(A::TARGET_FLYING_HEIGHT),m.F(A::FLYING_HEIGHT)+ConfigPixels("Fly Rise/Fall Speed")*fElapsedTime);
|
|
else if(m.F(A::FLYING_HEIGHT)>-ConfigFloat("Flight Oscillation Amount"))m.F(A::FLYING_HEIGHT)=std::max(-ConfigFloat("Flight Oscillation Amount"),m.F(A::FLYING_HEIGHT)-ConfigPixels("Fly Rise/Fall Speed")*fElapsedTime);
|
|
|
|
switch(m.phase){
|
|
case INITIALIZE:{
|
|
m.F(A::SPAWNER_TIMER)=ConfigFloat("Basic Hawk Spawn Time");
|
|
m.phase=IDLE;
|
|
game->SetOverlay(ConfigString("Wind Attack.Wind Overlay Sprite"),ConfigPixel("Wind Attack.Wind Overlay Color"));
|
|
game->GetOverlay().Disable();
|
|
|
|
m.SetStrategyDeathFunction([&](GameEvent&ev,Monster&m,const std::string&strategy){
|
|
game->SetWindSpeed({});
|
|
game->GetOverlay().Disable();
|
|
|
|
std::for_each(BULLET_LIST.begin(),BULLET_LIST.end(),[](const std::unique_ptr<Bullet>&bullet){
|
|
if(!bullet->friendly){ //Forces all bullets at the end of a fight for the boss to be completely nullified.
|
|
bullet->fadeOutTime=0.5f;
|
|
}
|
|
});
|
|
return true;
|
|
});
|
|
|
|
m.I(A::PREVIOUS_PHASE)=-1;
|
|
}break;
|
|
case IDLE:{
|
|
#pragma region Mid Phase Check
|
|
if(m.GetHealthRatio()<=ConfigFloat("Mid Phase Health Transition %")/100.f&&!m.B(A::PHASE)){
|
|
m.B(A::PHASE)=true;
|
|
m.phase=HALFHEALTH_PREPARE_PHASE;
|
|
m.F(A::TARGET_FLYING_HEIGHT)=50.f;
|
|
m.target=ConfigVec("Mid Phase.Pillar Position");
|
|
for(int i=0;i<ConfigInt("Mid Phase.Basic Hawk Spawn Count");i++){
|
|
const vf2d xRange=ConfigVec("Mid Phase.Adds Spawn X Range");
|
|
const vf2d yRange=ConfigVec("Mid Phase.Adds Spawn Y Range");
|
|
|
|
const vf2d spawnPos={util::random_range(xRange.x,xRange.y),util:: random_range(yRange.x,yRange.y)};
|
|
|
|
game->SpawnMonster(spawnPos,MONSTER_DATA.at("Hawk_NOXP"),m.OnUpperLevel(),true);
|
|
}
|
|
for(int i=0;i<ConfigInt("Mid Phase.Major Hawk Spawn Count");i++){
|
|
const vf2d xRange=ConfigVec("Mid Phase.Adds Spawn X Range");
|
|
const vf2d yRange=ConfigVec("Mid Phase.Adds Spawn Y Range");
|
|
|
|
const vf2d spawnPos={util::random_range(xRange.x,xRange.y),util:: random_range(yRange.x,yRange.y)};
|
|
|
|
game->SpawnMonster(spawnPos,MONSTER_DATA.at("Major Hawk"),m.OnUpperLevel(),true);
|
|
}
|
|
break; //An early break to not perform an attack.
|
|
}
|
|
#pragma endregion
|
|
|
|
|
|
int randomAttackChoice;
|
|
do randomAttackChoice=util::random()%3;
|
|
while(randomAttackChoice==m.I(A::PREVIOUS_PHASE));
|
|
|
|
m.I(A::PREVIOUS_PHASE)=randomAttackChoice;
|
|
|
|
switch(randomAttackChoice){
|
|
case 0:{
|
|
m.I(A::ATTACK_CHOICE)=util::random()%2;
|
|
|
|
const bool RightDirectionChosen=m.I(A::ATTACK_CHOICE)==int(AttackChoice::RIGHT);
|
|
|
|
if(RightDirectionChosen)m.target=ConfigVec("Fly Across Attack.Right Edge Start Pos");
|
|
else m.target=ConfigVec("Fly Across Attack.Left Edge Start Pos");
|
|
m.phase=FLY_ACROSS_PREPARE;
|
|
}break;
|
|
case 1:{
|
|
m.phase=TORNADO_ATTACK_PREPARE;
|
|
m.target=ConfigVec("Tornado Attack.Landing Area");
|
|
}break;
|
|
case 2:{
|
|
m.phase=WIND_ATTACK_FLY;
|
|
m.F(A::TARGET_FLYING_HEIGHT)=ConfigPixels("Wind Attack.Fly Up Height");
|
|
const bool LeftLandingSite=m.I(A::ATTACK_CHOICE)=util::random()%2;
|
|
if(LeftLandingSite)m.target=ConfigVec("Wind Attack.Left Landing Site");
|
|
else m.target=ConfigVec("Wind Attack.Right Landing Site");
|
|
}break;
|
|
}
|
|
}break;
|
|
case FLY_ACROSS_PREPARE:{
|
|
m.targetAcquireTimer=20.f;
|
|
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
|
|
if(m.ReachedTargetPos()){
|
|
const bool RightDirectionChosen=m.I(A::ATTACK_CHOICE)==int(AttackChoice::RIGHT);
|
|
|
|
//We're choosing the opposite side of the field to direct the boss towards for this attack.
|
|
if(RightDirectionChosen)m.target=ConfigVec("Fly Across Attack.Left Edge Start Pos");
|
|
else m.target=ConfigVec("Fly Across Attack.Right Edge Start Pos");
|
|
m.phase=FLY_ACROSS;
|
|
|
|
m.AddBuff(BuffType::SPEEDBOOST,INFINITY,ConfigFloat("Fly Across Attack.Move Speed Multiplier")-1.f);
|
|
}
|
|
}break;
|
|
case FLY_ACROSS:{
|
|
m.targetAcquireTimer=20.f;
|
|
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
|
|
m.F(A::SHOOT_TIMER)-=fElapsedTime;
|
|
if(m.F(A::SHOOT_TIMER)<=0.f){
|
|
CreateBullet(Bullet)(m.GetPos(),vf2d{0.f,ConfigFloat("Fly Across Attack.Attack Y Speed")},4,ConfigInt("Fly Across Attack.Poop Damage"),"birdpoop.png",m.OnUpperLevel(),false,INFINITY,false,false,WHITE,vf2d{1.f,1.25f})
|
|
.SetIframeTimeOnHit(0.25f)EndBullet;
|
|
const int extraPoopBitsCount=util::random()%6;
|
|
for(int i=0;i<extraPoopBitsCount;i++){
|
|
const bool RightDirection=util::random()%2;
|
|
float xOffset{0.f};
|
|
if(RightDirection)xOffset=1.f;
|
|
else xOffset=-1.f;
|
|
xOffset*=9.f;
|
|
CreateBullet(Bullet)(m.GetPos()+vf2d{xOffset+util::random_range(-3.f,3.f),-util::random(10.f)-4.f},vf2d{0.f,ConfigFloat("Fly Across Attack.Attack Y Speed")},1,ConfigInt("Fly Across Attack.Poop Damage"),"birdpoop.png",m.OnUpperLevel(),false,INFINITY,false,false,WHITE,vf2d{util::random_range(0.2f,0.3f),util::random_range(0.2f,0.3f)},util::random(2*PI))
|
|
.SetIframeTimeOnHit(0.25f)EndBullet;
|
|
}
|
|
m.F(A::SHOOT_TIMER)=ConfigFloat("Fly Across Attack.Attack Frequency");
|
|
}
|
|
if(m.ReachedTargetPos()){
|
|
m.phase=IDLE;
|
|
m.RemoveBuff(BuffType::SPEEDBOOST);
|
|
m.targetAcquireTimer=0.f;
|
|
}
|
|
}break;
|
|
case TORNADO_ATTACK_PREPARE:{
|
|
m.targetAcquireTimer=20.f;
|
|
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
|
|
if(m.ReachedTargetPos()){
|
|
m.phase=TORNADO_ATTACK;
|
|
m.PerformAnimation("ATTACK",Direction::SOUTH);
|
|
m.targetAcquireTimer=0.f;
|
|
m.F(A::CASTING_TIMER)=ConfigFloat("Tornado Attack.Attack Duration");
|
|
|
|
const int tornadoRingCount=Config("Tornado Attack.Tornados").GetKeys().size();
|
|
for(int tornadoRingId=1;tornadoRingId<=tornadoRingCount;tornadoRingId++){
|
|
|
|
//From strategy documentation:
|
|
//# For each Ring: Distance from Boss in Units, # of tornados, Rotation Speed (degrees/sec).
|
|
const float tornadoDistance=ConfigPixelsArr(std::format("Tornado Attack.Tornados.Ring {}",tornadoRingId),0);
|
|
const int tornadoCount=ConfigIntArr(std::format("Tornado Attack.Tornados.Ring {}",tornadoRingId),1);
|
|
const float tornadoRotSpd=util::degToRad(ConfigFloatArr(std::format("Tornado Attack.Tornados.Ring {}",tornadoRingId),2)); //It's in degrees, let's convert it to radians now.
|
|
|
|
const float randomRotDir=util::random(2*PI);
|
|
|
|
for(int tornadoCounter=0;tornadoCounter<tornadoCount;tornadoCounter++){
|
|
const float startAngle=randomRotDir+tornadoCounter*((2*PI)/tornadoCount);
|
|
|
|
CreateBullet(Tornado)(m.GetPos(),tornadoDistance,startAngle,tornadoRotSpd,m.GetAttack(),ConfigFloat("Tornado Attack.Knockup Duration"),ConfigFloat("Tornado Attack.Knockback Amount"),ConfigFloat("Tornado Attack.Attack Duration"),m.OnUpperLevel(),false,WHITE)EndBullet;
|
|
BULLET_LIST.back()->SetFadeinTime(ConfigFloat("Tornado Attack.Tornado Fade-In Time"));
|
|
}
|
|
}
|
|
}
|
|
}break;
|
|
case TORNADO_ATTACK:{
|
|
m.F(A::CASTING_TIMER)-=fElapsedTime;
|
|
if(m.F(A::CASTING_TIMER)<=0.f)m.phase=IDLE;
|
|
}break;
|
|
case WIND_ATTACK_FLY:{
|
|
m.targetAcquireTimer=20.f;
|
|
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
|
|
if(m.ReachedTargetPos()){
|
|
m.phase=WIND_ATTACK_LAND;
|
|
m.F(A::TARGET_FLYING_HEIGHT)=0.f;
|
|
}
|
|
}break;
|
|
case WIND_ATTACK_LAND:{
|
|
if(m.GetZ()==0.f){
|
|
m.phase=WIND_ATTACK;
|
|
game->GetOverlay().Enable();
|
|
m.F(A::CASTING_TIMER)=ConfigFloat("Wind Attack.Wind Duration");
|
|
m.F(A::WIND_STRENGTH)=ConfigFloat("Wind Attack.Wind Starting Strength")/100.f;
|
|
m.F(A::WIND_PHASE_TIMER)=ConfigFloat("Wind Attack.Wind Increase Phase Wait Time");
|
|
m.F(A::SHOOT_TIMER)=ConfigFloat("Wind Attack.Wind Projectile Spawn Rate");
|
|
}
|
|
}break;
|
|
case WIND_ATTACK:{
|
|
m.F(A::CASTING_TIMER)-=fElapsedTime;
|
|
m.F(A::WIND_PHASE_TIMER)-=fElapsedTime;
|
|
m.F(A::SHOOT_TIMER)-=fElapsedTime;
|
|
|
|
const bool OnLeftLandingSite=m.I(A::ATTACK_CHOICE);
|
|
|
|
if(OnLeftLandingSite)m.PerformAnimation("ATTACK",Direction::EAST);
|
|
else m.PerformAnimation("ATTACK",Direction::WEST);
|
|
|
|
#pragma region Wind Streak Effect
|
|
m.F(A::ENVIRONMENT_TIMER)-=fElapsedTime;
|
|
//Assuming facing right / on left landing site for initial values.
|
|
if(m.F(A::ENVIRONMENT_TIMER)<=0.f){
|
|
const bool spawnOnTopOfScreen=util::random()%2;
|
|
float windStreakYPos=util::random(WINDOW_SIZE.y/4.f);
|
|
if(!spawnOnTopOfScreen)windStreakYPos=WINDOW_SIZE.y-windStreakYPos;
|
|
vf2d randomScreenOffset={util::random(WINDOW_SIZE.x/2.f),windStreakYPos};
|
|
|
|
vf2d randomSpeed=vf2d{util::random_range(ConfigFloatArr("Wind Attack.Wind Streak X Speed Range",0),ConfigFloatArr("Wind Attack.Wind Streak X Speed Range",1)),util::random_range(ConfigFloatArr("Wind Attack.Wind Streak Y Speed Range",0),ConfigFloatArr("Wind Attack.Wind Streak Y Speed Range",1))};
|
|
float randomLifetime=util::random_range(ConfigFloatArr("Wind Attack.Wind Streak Lifetime Range",0),ConfigFloatArr("Wind Attack.Wind Streak Lifetime Range",1));
|
|
|
|
if(!OnLeftLandingSite){
|
|
randomScreenOffset.x+=WINDOW_SIZE.x/2.f;
|
|
randomSpeed*=-1.f;
|
|
}
|
|
|
|
const uint8_t randomColAmt=util::random()%256;
|
|
const uint8_t randomAlpha=util::random()%256;
|
|
const std::string windImageChoice=util::random()%2?"wind1.png":"wind2.png";
|
|
|
|
const bool positiveWindRotation=util::random()%2;
|
|
float windRotationSpd=util::random(util::degToRad(15.f));
|
|
if(!positiveWindRotation)windRotationSpd*=-1;
|
|
|
|
game->AddEffect(std::make_unique<ForegroundEffect>(randomScreenOffset,randomLifetime,windImageChoice,m.OnUpperLevel(),vf2d{1.f,1.f}*(util::random_range(0.25f,1.f)),1.f,randomSpeed,Pixel{randomColAmt,randomColAmt,randomColAmt,randomAlpha},util::random(2*PI),windRotationSpd,true));
|
|
|
|
m.F(A::ENVIRONMENT_TIMER)=ConfigFloat("Wind Attack.Wind Streak Spawn Rate");
|
|
}
|
|
#pragma endregion
|
|
|
|
if(m.F(A::WIND_STRENGTH)<ConfigFloat("Wind Attack.Wind Max Strength")/100.f&&m.F(A::WIND_PHASE_TIMER)<=0.f){
|
|
m.F(A::WIND_STRENGTH)=std::min(ConfigFloat("Wind Attack.Wind Max Strength")/100.f,m.F(A::WIND_STRENGTH)+ConfigFloat("Wind Attack.Wind Strength Increase")/100.f);
|
|
m.F(A::WIND_PHASE_TIMER)=ConfigFloat("Wind Attack.Wind Increase Phase Wait Time");
|
|
}
|
|
|
|
#pragma region Wind
|
|
const bool LeftLandingSite=m.I(A::ATTACK_CHOICE);
|
|
|
|
vf2d windSpd={m.F(A::WIND_STRENGTH)*"Player.MoveSpd"_F,0.f}; //Assume we landed left and causing a wind attack to the right.
|
|
if(!LeftLandingSite)windSpd*=-1;
|
|
game->SetWindSpeed(windSpd);
|
|
#pragma endregion
|
|
|
|
if(m.F(A::CASTING_TIMER)<=0.f){
|
|
m.phase=IDLE;
|
|
game->GetOverlay().Disable();
|
|
game->SetWindSpeed({});
|
|
}
|
|
if(m.F(A::SHOOT_TIMER)<=0.f){
|
|
vf2d debrisSpawningOffset={-200.f,util::random(WINDOW_SIZE.y)-WINDOW_SIZE.y/2.f}; //Assume left landing site.
|
|
if(!LeftLandingSite)debrisSpawningOffset*=-1.f;
|
|
|
|
vf2d debrisSpd={util::random_range(ConfigFloatArr("Wind Attack.Wind Projectile X Speed Range",0),ConfigFloatArr("Wind Attack.Wind Projectile X Speed Range",1)),util::random_range(ConfigFloatArr("Wind Attack.Wind Projectile Y Speed Range",0),ConfigFloatArr("Wind Attack.Wind Projectile Y Speed Range",1))};
|
|
if(!LeftLandingSite)debrisSpd*=-1.f;
|
|
|
|
const float sizeMultiplier=util::random_range(0.75f,1.25f);
|
|
|
|
CreateBullet(Debris)(m.GetPos()+debrisSpawningOffset,debrisSpd,ConfigInt("Wind Attack.Debris Damage"),ConfigFloat("Wind Attack.Debris Radius")*sizeMultiplier,ConfigFloat("Wind Attack.Debris Knockback Multiplier"),util::random_range(util::degToRad(-180.f),util::degToRad(180.f)),INFINITE,m.OnUpperLevel(),false,WHITE,vf2d{1.f,1.f}*sizeMultiplier)EndBullet;
|
|
|
|
m.F(A::SHOOT_TIMER)=ConfigFloat("Wind Attack.Wind Projectile Spawn Rate");
|
|
}
|
|
}break;
|
|
case HALFHEALTH_PREPARE_PHASE:{
|
|
m.targetAcquireTimer=20.f;
|
|
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
|
|
if(m.ReachedTargetPos()){
|
|
m.F(A::TARGET_FLYING_HEIGHT)=0.f;
|
|
|
|
const bool HasLandedOnGround=m.GetZ()==0.f;
|
|
|
|
if(HasLandedOnGround){
|
|
m.phase=HALFHEALTH_PHASE;
|
|
CreateBullet(LargeTornado)(ConfigVec("Mid Phase.Large Tornado Position"),ConfigPixels("Mid Phase.Large Tornado Suction"),ConfigFloat("Mid Phase.Large Tornado Knockup Duration"),ConfigFloat("Mid Phase.Large Tornado Knockback Amount"),ConfigInt("Mid Phase.Large Tornado Damage"),ConfigFloat("Mid Phase.Large Tornado Radius"),INFINITY,m.OnUpperLevel())EndBullet;
|
|
BULLET_LIST.back()->SetFadeinTime(1.0f);
|
|
}
|
|
}
|
|
}break;
|
|
case HALFHEALTH_PHASE:{
|
|
m.ApplyIframes(1.f);
|
|
m.UpdateFacingDirection(Direction::SOUTH);
|
|
m.PerformAnimation("ATTACK");
|
|
if(game->BossEncounterMobCount()==1){
|
|
m.phase=IDLE;
|
|
std::for_each(BULLET_LIST.begin(),BULLET_LIST.end(),[](std::unique_ptr<Bullet>&bullet){
|
|
if(bullet->GetBulletType()==BulletType::LARGE_TORNADO){
|
|
bullet->fadeOutTime=1.f;
|
|
}
|
|
});
|
|
game->SetWindSpeed({});
|
|
}
|
|
}break;
|
|
}
|
|
} |