# 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 <EFBFBD> 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 ,
FLY_ACROSS_PREPARE ,
FLY_ACROSS ,
TORNADO_ATTACK_PREPARE ,
TORNADO_ATTACK ,
WIND_ATTACK_FLY ,
WIND_ATTACK_LAND ,
WIND_ATTACK ,
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 ( ) ) ;
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 ( ) ;
} break ;
case IDLE : {
const int randomAttackChoice = 2 ;
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 " ) ;
}
} break ;
case WIND_ATTACK : {
m . F ( A : : CASTING_TIMER ) - = fElapsedTime ;
m . F ( A : : WIND_PHASE_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 - > GetPlayer ( ) - > AddVelocity ( windSpd ) ;
m . F ( A : : CASTING_TIMER ) = ConfigFloat ( " Wind Attack.Wind Duration " ) ;
std : : for_each ( BULLET_LIST . begin ( ) , BULLET_LIST . end ( ) , [ & ] ( const std : : unique_ptr < Bullet > & bullet ) { if ( bullet - > IsPlayerAutoAttackProjectile ( ) ) { bullet - > AddVelocity ( windSpd ) ; } } ) ;
# pragma endregion
if ( m . F ( A : : CASTING_TIMER ) < = 0.f ) {
m . phase = IDLE ;
game - > GetOverlay ( ) . Disable ( ) ;
}
} break ;
case HALFHEALTH_PHASE : {
} break ;
}
}