# 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 "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 "Unlock.h"
# ifndef __EMSCRIPTEN__
# include "steam/isteamuserstats.h"
# endif
INCLUDE_ANIMATION_DATA
INCLUDE_MONSTER_DATA
INCLUDE_MONSTER_LIST
INCLUDE_DAMAGENUMBER_LIST
INCLUDE_game
INCLUDE_BULLET_LIST
INCLUDE_DATA
INCLUDE_GFX
safemap < std : : string , std : : function < void ( Monster & , float , std : : string ) > > STRATEGY_DATA ;
std : : map < std : : string , Renderable * > MonsterData : : 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 ) ;
}
const vf2d & Monster : : GetPos ( ) const {
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 ( ) const {
return size ;
}
Animate2D : : Frame Monster : : GetFrame ( ) const {
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 : : rect < float > collisionRect = game - > GetTileCollision ( game - > GetCurrentLevel ( ) , newPos , upperLevel ) ;
if ( 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 : : rect < float > collision = { collisionRect . pos , collisionRect . size } ;
bool insideArenaBounds = true ;
# pragma region Calculate Arena Bounds check for Bosses
if ( isBoss ) {
const geom2d : : rect < int > arenaBounds = game - > GetZones ( ) . at ( " BossArena " ) [ 0 ] . zone ;
if ( ! geom2d : : contains ( arenaBounds , newPos ) ) {
insideArenaBounds = false ;
}
}
# pragma endregion
# pragma region lambdas
auto NoEnemyCollisionWithTile = [ & ] ( ) { return ( isBoss & & insideArenaBounds ) | | ! 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 < float > ( 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 : : rect < float > collisionRect = game - > GetTileCollision ( game - > GetCurrentLevel ( ) , newPos , upperLevel ) ;
if ( 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 : : rect < float > collision = { collisionRect . pos , collisionRect . size } ;
bool insideArenaBounds = true ;
# pragma region Calculate Arena Bounds check for Bosses
if ( isBoss ) {
const geom2d : : rect < int > arenaBounds = game - > GetZones ( ) . at ( " BossArena " ) [ 0 ] . zone ;
if ( ! geom2d : : contains ( arenaBounds , newPos ) ) {
insideArenaBounds = false ;
}
}
# pragma endregion
# pragma region lambdas
auto NoEnemyCollisionWithTile = [ & ] ( ) { return ( isBoss & & insideArenaBounds ) | | ! 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 < float > ( 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 ) ;
lastPathfindingCooldown = std : : max ( 0.f , lastPathfindingCooldown - 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 < Buff > : : 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 ( ) const {
return facingDirection ;
}
void Monster : : UpdateFacingDirection ( vf2d facingTargetPoint ) {
if ( facingTargetPoint . x > GetPos ( ) . x ) {
facingDirection = RIGHT ;
}
if ( facingTargetPoint . x < GetPos ( ) . x ) {
facingDirection = LEFT ;
}
}
void Monster : : Draw ( ) const {
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 ) ;
}
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 : : vector < Buff > shieldBuffs = 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 ; index < path . points . size ( ) ; index + = 0.01f ) {
Pixel col = DARK_GREY ;
if ( index < pathIndex ) {
col = VERY_DARK_GREY ;
}
if ( index > pathIndex + 1 ) {
col = GREY ;
}
game - > view . FillRectDecal ( path . GetSplinePoint ( index ) . pos , { 1 , 1 } , col ) ;
}
for ( size_t counter = 0 ; const Pathfinding : : sPoint2D & point : path . points ) {
Pixel col = CYAN ;
if ( counter < pathIndex ) {
col = RED ;
}
if ( counter > pathIndex + 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 : : array < vf2d , 4 > points = {
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 < float > ( GetPos ( ) , p - > GetPos ( ) ) . vector ( ) . norm ( ) ;
float knockbackStrength = 0.f ;
std : : vector < Buff > 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 < float > ( GetPos ( ) , m . GetPos ( ) ) . vector ( ) . norm ( ) ;
float knockbackStrength = 0.f ;
std : : vector < Buff > 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 ( ) {
const std : : map < std : : string , std : : vector < ZoneData > > & zoneData = game - > GetZones ( game - > GetCurrentLevel ( ) ) ;
for ( const ZoneData & upperLevelZone : zoneData . at ( " UpperZone " ) ) {
if ( geom2d : : overlaps ( upperLevelZone . zone , pos ) ) {
upperLevel = true ;
}
}
for ( const ZoneData & lowerLevelZone : zoneData . at ( " 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 ) < game - > 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 < DamageNumber > ( 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 < std : : pair < std : : string , vf2d > > 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 < std : : string , vf2d > & 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 ) ;
if ( lastPathfindingCooldown = = 0.f ) {
path = game - > pathfinder . Solve_WalkPath ( pos , target , 24 , OnUpperLevel ( ) ) ;
lastPathfindingCooldown = 0.25f ;
}
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 : : vector < Buff > Monster : : GetBuffs ( BuffType buff ) const {
std : : vector < Buff > filteredBuffs ;
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 < void ( Monster & , float , std : : string ) > & 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 : : function < void ( AiL * , Monster & , const std : : string & ) > func ) {
strategyDraw = func ;
}
void Monster : : SetStrategyDrawOverlayFunction ( std : : function < void ( AiL * , Monster & , const std : : string & ) > func ) {
strategyDrawOverlay = func ;
}
std : : map < ItemInfo * , uint16_t > Monster : : SpawnDrops ( ) {
std : : map < ItemInfo * , uint16_t > drops ;
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 ; i < dropQuantity ; i + + ) {
ItemDrop : : SpawnItem ( data . item , GetPos ( ) , OnUpperLevel ( ) ) ;
drops [ data . item ] + + ;
}
}
}
return drops ;
}
void Monster : : OnDeath ( ) {
animation . ChangeState ( internal_animState , GetDeathAnimationName ( ) ) ;
if ( isBoss ) {
game - > ReduceBossEncounterMobCount ( ) ;
if ( game - > BossEncounterMobCount ( ) = = 0 ) {
ZoneData exitRing { geom2d : : rect < int > { 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 : : rect < int > arenaBounds = game - > GetZones ( ) . at ( " BossArena " ) [ 0 ] . zone ;
geom2d : : rect < int > clampedArena { 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.
}
}
Unlock : : IncreaseKillCount ( ) ;
STEAMUSERSTATS (
for ( auto & [ key , size ] : DATA . GetProperty ( " Achievement.Kill Unlocks " ) ) {
//Monster-specific achievement unlocks.
datafile & unlock = DATA . GetProperty ( std : : format ( " Achievement.Kill Unlocks.{} " , key ) ) ;
if ( unlock . HasProperty ( " Monster Name " ) ) {
if ( unlock [ " Monster Name " ] . GetString ( ) ! = GetName ( ) ) continue ;
if ( unlock . HasProperty ( " Time Limit " ) & & isBoss ) {
if ( game - > GetEncounterDuration ( ) < = unlock [ " Time Limit " ] . GetReal ( ) ) {
SteamUserStats ( ) - > SetAchievement ( unlock [ " API Name " ] . GetString ( ) . c_str ( ) ) ;
SteamUserStats ( ) - > StoreStats ( ) ;
}
} else {
SteamUserStats ( ) - > SetAchievement ( unlock [ " API Name " ] . GetString ( ) . c_str ( ) ) ;
SteamUserStats ( ) - > StoreStats ( ) ;
}
}
}
)
if ( hasStrategyDeathFunction ) {
GameEvent : : AddEvent ( std : : make_unique < MonsterStrategyGameEvent > ( 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 : : circle < float > Monster : : 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 : : function < bool ( GameEvent & , Monster & , const std : : string & ) > func ) {
hasStrategyDeathFunction = true ;
strategyDeathFunc = func ;
}
const bool Monster : : IsNPC ( ) const {
return MONSTER_DATA [ name ] . IsNPC ( ) ;
}
const bool MonsterData : : IsNPC ( ) const {
return isNPC ;
}