# 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 "GameSettings.h"
# include "ItemEnchant.h"
# include <ranges>
INCLUDE_ANIMATION_DATA
INCLUDE_MONSTER_DATA
INCLUDE_MONSTER_LIST
INCLUDE_DAMAGENUMBER_LIST
INCLUDE_game
INCLUDE_BULLET_LIST
INCLUDE_DATA
INCLUDE_GFX
INCLUDE_SPAWNER_LIST
INCLUDE_SPAWNER_CONTROLLER
safemap < std : : string , std : : function < void ( Monster & , float , std : : string ) > > STRATEGY_DATA ;
std : : unordered_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 . GetInternalName ( ) ) , upperLevel ( upperLevel ) , isBoss ( bossMob ) , facingDirection ( Direction : : WEST ) , lifetime ( GetTotalLifetime ( ) ) {
for ( const std : : string & anim : data . GetAnimations ( ) ) {
animation . AddState ( anim , ANIMATION_DATA [ std : : format ( " {}_{} " , name , anim ) ] ) ;
}
PerformIdleAnimation ( ) ;
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 ) ;
UpdateFacingDirection ( game - > GetPlayer ( ) - > GetPos ( ) ) ;
}
const vf2d & Monster : : GetPos ( ) const {
return pos ;
}
const int Monster : : GetHealth ( ) const {
return hp ;
}
const int Monster : : GetMaxHealth ( ) const {
return int ( GetModdedStatBonuses ( " Health " ) ) ;
}
int Monster : : GetAttack ( ) {
return int ( GetModdedStatBonuses ( " Attack " ) ) ;
}
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 ( SELF_INFLICTED_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 ( const Direction facingDir ) {
facingDirection = facingDir ;
PerformJumpAnimation ( ) ;
}
void Monster : : PerformJumpAnimation ( ) {
if ( ! animation . HasState ( MONSTER_DATA . at ( name ) . GetJumpAnimation ( facingDirection ) ) ) return ;
animation . ChangeState ( internal_animState , MONSTER_DATA . at ( name ) . GetJumpAnimation ( facingDirection ) ) ;
}
void Monster : : PerformShootAnimation ( const Direction facingDir ) {
facingDirection = facingDir ;
PerformShootAnimation ( ) ;
}
void Monster : : PerformShootAnimation ( ) {
if ( ! animation . HasState ( MONSTER_DATA . at ( name ) . GetShootAnimation ( facingDirection ) ) ) return ;
animation . ChangeState ( internal_animState , MONSTER_DATA . at ( name ) . GetShootAnimation ( facingDirection ) ) ;
}
void Monster : : PerformIdleAnimation ( const Direction facingDir ) {
facingDirection = facingDir ;
PerformIdleAnimation ( ) ;
}
void Monster : : PerformIdleAnimation ( ) {
if ( ! animation . HasState ( MONSTER_DATA . at ( name ) . GetIdleAnimation ( facingDirection ) ) ) return ;
animation . ChangeState ( internal_animState , MONSTER_DATA . at ( name ) . GetIdleAnimation ( facingDirection ) ) ;
}
void Monster : : PerformNPCDownAnimation ( ) {
facingDirection = Direction : : SOUTH ;
PerformAnimation ( " DOWN " ) ;
}
void Monster : : PerformNPCUpAnimation ( ) {
facingDirection = Direction : : NORTH ;
PerformAnimation ( " UP " ) ;
}
void Monster : : PerformNPCLeftAnimation ( ) {
facingDirection = Direction : : WEST ;
PerformAnimation ( " LEFT " ) ;
}
void Monster : : PerformNPCRightAnimation ( ) {
facingDirection = Direction : : EAST ;
PerformAnimation ( " RIGHT " ) ;
}
void Monster : : PerformAnimation ( const std : : string_view animationName ) {
if ( HasFourWaySprites ( ) ) animation . ChangeState ( internal_animState , std : : format ( " {}_{} " , animationName , int ( facingDirection ) ) ) ;
else animation . ChangeState ( internal_animState , std : : string ( animationName ) ) ;
}
void Monster : : PerformAnimation ( const std : : string_view animationName , const Direction facingDir ) {
facingDirection = facingDir ;
PerformAnimation ( animationName ) ;
}
void Monster : : PerformAnimation ( const std : : string_view animationName , const vf2d targetFacingDir ) {
facingDirection = GetFacingDirectionToTarget ( targetFacingDir ) ;
PerformAnimation ( animationName ) ;
}
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 IgnoresTerrainCollision ( ) | | ( isBoss & & insideArenaBounds ) | | ( ! isBoss & & ! 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 { vf2d { 1.f , util : : random ( 2 * PI ) } . cart ( ) } ;
if ( collision . middle ( ) ! = pos ) 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 IgnoresTerrainCollision ( ) | | ( isBoss & & insideArenaBounds ) | | ( ! isBoss & & ! 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 { vf2d { 1.f , util : : random ( 2 * PI ) } . cart ( ) } ;
if ( collision . middle ( ) ! = pos ) 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 ) ;
lastDotTimer = std : : max ( 0.f , lastDotTimer - fElapsedTime ) ;
iframe_timer = std : : max ( 0.f , iframe_timer - fElapsedTime ) ;
stunTimer = std : : max ( 0.f , stunTimer - fElapsedTime ) ;
monsterHurtSoundCooldown = std : : max ( 0.f , monsterHurtSoundCooldown - fElapsedTime ) ;
lastHitPlayer = std : : max ( 0.f , lastHitPlayer - fElapsedTime ) ;
lastPathfindingCooldown = std : : max ( 0.f , lastPathfindingCooldown - fElapsedTime ) ;
markApplicationTimer = std : : max ( 0.f , markApplicationTimer - fElapsedTime ) ;
specialMarkApplicationTimer = std : : max ( 0.f , specialMarkApplicationTimer - fElapsedTime ) ;
lastFacingDirectionChange + = fElapsedTime ;
timeSpentAlive + = fElapsedTime ;
if ( IsSolid ( ) & & FadeoutWhenStandingBehind ( ) ) {
if ( GetPos ( ) . y > = game - > GetPlayer ( ) - > GetPos ( ) . y ) solidFadeTimer = std : : min ( TileGroup : : FADE_TIME , solidFadeTimer + game - > GetElapsedTime ( ) ) ;
else solidFadeTimer = std : : max ( 0.f , solidFadeTimer - game - > GetElapsedTime ( ) ) ;
}
if ( HasArrowIndicator ( ) & & IsAlive ( ) ) game - > SetBossIndicatorPos ( GetPos ( ) ) ;
# pragma region Handle Monster Lifetime and fade timer.
if ( fadeTimer > 0.f ) {
fadeTimer = std : : max ( 0.f , fadeTimer - fElapsedTime ) ;
if ( fadeTimer = = 0.f ) MarkForDeletion ( ) ;
} else
if ( GetLifetime ( ) . has_value ( ) ) {
lifetime . value ( ) = std : : max ( 0.f , lifetime . value ( ) - fElapsedTime ) ;
if ( lifetime . value ( ) = = 0.f ) {
fadeTimer = 1.f ;
lifetime = { } ; //Nullify the lifetime value as the monster has expired. So we'll prevent a loop by doing this.
}
}
# pragma endregion
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 ( 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 ( addedVel . x > 0 ) {
addedVel . x = std : : max ( 0.f , addedVel . x - friction * fElapsedTime ) ;
} else {
addedVel . x = std : : min ( 0.f , addedVel . x + friction * fElapsedTime ) ;
}
if ( addedVel . y > 0 ) {
addedVel . y = std : : max ( 0.f , addedVel . y - friction * fElapsedTime ) ;
} else {
addedVel . y = std : : min ( 0.f , addedVel . y + friction * fElapsedTime ) ;
}
bumpedIntoTerrain = false ;
if ( vel ! = vf2d { 0 , 0 } | | addedVel ! = vf2d { 0 , 0 } ) {
bumpedIntoTerrain | = SetX ( pos . x + vf2d { vel + addedVel } . x * fElapsedTime ) ;
bumpedIntoTerrain | = SetY ( pos . y + vf2d { vel + addedVel } . y * fElapsedTime ) ;
}
if ( IsAlive ( ) ) {
std : : for_each ( buffList . begin ( ) , buffList . end ( ) , [ & ] ( Buff & b ) { b . Update ( game , fElapsedTime ) ; } ) ;
std : : erase_if ( buffList , [ ] ( Buff & b ) {
if ( b . duration < = 0.f ) {
if ( ! std : : holds_alternative < std : : weak_ptr < Monster > > ( b . attachedTarget ) ) ERR ( std : : format ( " WARNING! Somehow removed buff of type {} is inside the player buff list but is not attached to the Player? THIS SHOULD NOT BE HAPPENING! " , int ( b . type ) ) ) ;
if ( b . monsterBuffCallbackFunc ) b . monsterBuffCallbackFunc ( std : : get < std : : weak_ptr < Monster > > ( b . attachedTarget ) , b ) ;
return true ;
}
return false ;
} ) ;
if ( ! HasIframes ( ) ) {
for ( std : : shared_ptr < Monster > & m : MONSTER_LIST ) {
const float monsterRadius { GetCollisionRadius ( ) } ;
const float otherMonsterRadius { m - > GetCollisionRadius ( ) } ;
if ( & * m = = this ) continue ;
if ( ! m - > HasIframes ( ) & & OnUpperLevel ( ) = = m - > OnUpperLevel ( ) & & abs ( m - > GetZ ( ) - GetZ ( ) ) < = 1 & & geom2d : : overlaps ( geom2d : : circle ( pos , monsterRadius ) , geom2d : : circle ( m - > GetPos ( ) , otherMonsterRadius ) ) ) {
m - > Collision ( * this ) ;
geom2d : : line line ( pos , m - > GetPos ( ) ) ;
float dist = line . length ( ) ;
while ( dist < = 0.001 ) {
line = { pos + vf2d { util : : random ( 0.2f ) - 0.1f , util : : random ( 0.2f ) - 0.1f } , m - > GetPos ( ) } ;
dist = line . length ( ) ;
}
const float displacementDist = ( otherMonsterRadius + monsterRadius ) - dist ;
if ( m - > IsAlive ( ) ) {
const bool BothAreSolid = IsSolid ( ) & & m - > IsSolid ( ) ;
if ( ! m - > IsSolid ( ) ) {
float knockbackStrength = 1.f ;
std : : vector < Buff > knockbackBuffs = m - > GetBuffs ( COLLISION_KNOCKBACK_STRENGTH ) ;
for ( Buff & b : knockbackBuffs ) {
knockbackStrength + = b . intensity ;
}
Knockback ( line . vector ( ) . norm ( ) * - 128 * knockbackStrength ) ;
} else
if ( ! IgnoresTerrainCollision ( ) | | BothAreSolid ) {
SetPos ( line . rpoint ( - displacementDist ) ) ;
}
}
}
}
}
if ( GetState ( ) = = State : : NORMAL & & FaceTarget ( ) ) {
UpdateFacingDirection ( game - > GetPlayer ( ) - > GetPos ( ) ) ;
}
if ( ! game - > TestingModeEnabled ( ) & & CanMove ( ) ) Monster : : STRATEGY : : RUN_STRATEGY ( * this , fElapsedTime ) ;
}
if ( ! IsAlive ( ) ) {
deathTimer + = fElapsedTime ;
if ( deathTimer > 3 ) {
return false ;
}
}
animation . UpdateState ( internal_animState , randomFrameOffset + fElapsedTime ) ;
if ( HasMountedMonster ( ) ) mounted_animation . value ( ) . UpdateState ( internal_mounted_animState . value ( ) , fElapsedTime ) ;
randomFrameOffset = 0 ;
attackedByPlayer = false ;
return true ;
}
Direction Monster : : GetFacingDirection ( ) const {
return facingDirection ;
}
void Monster : : UpdateFacingDirection ( Direction newFacingDir ) {
if ( HasFourWaySprites ( ) ) {
facingDirection = newFacingDir ;
} else {
if ( newFacingDir = = Direction : : NORTH | | newFacingDir = = Direction : : SOUTH ) ERR ( std : : format ( " WARNING! Trying to set a facing direction of {} for Monster {}! Not possible because the monster does not have four-way facing sprites! " , int ( newFacingDir ) , GetDisplayName ( ) ) ) ;
facingDirection = newFacingDir ;
}
}
void Monster : : UpdateFacingDirection ( vf2d facingTargetPoint ) {
if ( Immovable ( ) ) return ;
float facingAngle = util : : angleTo ( GetPos ( ) , facingTargetPoint ) ;
vf2d diff = GetPos ( ) - facingTargetPoint ;
if ( abs ( facingAngle - prevFacingDirectionAngle ) < PI / 4.f | | lastFacingDirectionChange < 0.25f ) return ; //We don't want to change facing angle until a more drastic angle of change has occurred. About 1/4 circle should be acceptable.
prevFacingDirectionAngle = facingAngle ;
lastFacingDirectionChange = 0.f ;
if ( HasFourWaySprites ( ) ) {
if ( abs ( diff . x ) > abs ( diff . y ) ) {
if ( facingTargetPoint . x > GetPos ( ) . x ) {
facingDirection = Direction : : EAST ;
}
if ( facingTargetPoint . x < GetPos ( ) . x ) {
facingDirection = Direction : : WEST ;
}
} else {
if ( facingTargetPoint . y > GetPos ( ) . y ) {
facingDirection = Direction : : SOUTH ;
}
if ( facingTargetPoint . y < GetPos ( ) . y ) {
facingDirection = Direction : : NORTH ;
}
}
animation . ModifyDisplaySprite ( internal_animState , std : : format ( " {}_{} " , animation . currentStateName . substr ( 0 , animation . currentStateName . length ( ) - 2 ) , int ( facingDirection ) ) ) ;
if ( HasMountedMonster ( ) ) mounted_animation . value ( ) . ModifyDisplaySprite ( internal_mounted_animState . value ( ) , std : : format ( " {}_{} " , mounted_animation . value ( ) . currentStateName . substr ( 0 , mounted_animation . value ( ) . currentStateName . length ( ) - 2 ) , int ( facingDirection ) ) ) ;
} else {
if ( diff . x > 0 ) {
facingDirection = Direction : : WEST ;
} else {
facingDirection = Direction : : EAST ;
}
}
}
void Monster : : Draw ( ) const {
if ( markedForDeletion ) return ;
Pixel blendCol { WHITE } ;
std : : optional < std : : reference_wrapper < Buff > > glowPurpleBuff ;
if ( GetBuffs ( BuffType : : GLOW_PURPLE ) . size ( ) > 0 ) glowPurpleBuff = GetBuffs ( BuffType : : GLOW_PURPLE ) [ 0 ] ;
const auto HasBuff = [ & ] ( const BuffType buff ) { return GetBuffs ( buff ) . size ( ) > 0 ; } ;
//The lerpCutAmount is how much to divide the initial color by, which is used as the lerp oscillation amount. 0.5 means half the color is always active, and the other half linearly oscillates. 0.1 would mean 90% of the color is normal and 10% of the color oscillates.
const auto GetBuffBlendCol = [ & ] ( const BuffType buff , const float oscillationTime_s , const Pixel blendCol , const float lerpCutAmount = 0.5f ) { return Pixel { uint8_t ( blendCol . r * ( 1 - lerpCutAmount ) + blendCol . r * lerpCutAmount * abs ( sin ( oscillationTime_s * PI * GetBuffs ( buff ) [ 0 ] . duration ) ) ) , uint8_t ( blendCol . g * ( 1 - lerpCutAmount ) + blendCol . g * lerpCutAmount * abs ( sin ( oscillationTime_s * PI * GetBuffs ( buff ) [ 0 ] . duration ) ) ) , uint8_t ( blendCol . b * ( 1 - lerpCutAmount ) + blendCol . b * lerpCutAmount * abs ( sin ( oscillationTime_s * PI * GetBuffs ( buff ) [ 0 ] . duration ) ) ) } ; } ;
if ( HasBuff ( BuffType : : BURNING_ARROW_BURN ) ) blendCol = GetBuffBlendCol ( BuffType : : BURNING_ARROW_BURN , 1.f , { 255 , 160 , 0 } ) ;
else if ( HasBuff ( BuffType : : DAMAGE_AMPLIFICATION ) ) blendCol = GetBuffBlendCol ( BuffType : : DAMAGE_AMPLIFICATION , 1.4f , { 255 , 0 , 0 } ) ;
else if ( HasBuff ( BuffType : : CURSE_OF_DEATH ) ) blendCol = GetBuffBlendCol ( BuffType : : CURSE_OF_DEATH , 1.4f , { 255 , 0 , 0 } ) ;
else if ( HasBuff ( BuffType : : COLOR_MOD ) ) blendCol = GetBuffBlendCol ( BuffType : : COLOR_MOD , 1.4f , PixelRaw ( GetBuffs ( BuffType : : COLOR_MOD ) [ 0 ] . intensity ) , 1.f ) ;
else if ( HasBuff ( BuffType : : SLOWDOWN ) ) blendCol = GetBuffBlendCol ( BuffType : : SLOWDOWN , 1.4f , { 255 , 255 , 128 } , 0.5f ) ;
else if ( glowPurpleBuff . has_value ( ) ) blendCol = Pixel { uint8_t ( 255 * abs ( sin ( 1.4 * glowPurpleBuff . value ( ) . get ( ) . duration ) ) ) , uint8_t ( 255 * abs ( sin ( 1.4 * glowPurpleBuff . value ( ) . get ( ) . duration ) ) ) , uint8_t ( 128 + 127 * abs ( sin ( 1.4 * glowPurpleBuff . value ( ) . get ( ) . duration ) ) ) } ;
const vf2d hitTimerOffset = vf2d { sin ( 20 * PI * lastHitTimer + randomFrameOffset ) , 0.f } * 2.f * GetSizeMult ( ) ;
const vf2d zOffset = - vf2d { 0 , GetZ ( ) } ;
const vf2d drawPos = GetPos ( ) + zOffset + hitTimerOffset ;
if ( GetZ ( ) > 0 ) {
vf2d shadowScale = vf2d { 8 * GetSizeMult ( ) / 3.f , 1 } / std : : max ( 1.f , GetZ ( ) / 24 ) ;
game - > view . DrawDecal ( GetPos ( ) + hitTimerOffset - vf2d { 3 , 3 } * shadowScale / 2 + vf2d { 0 , 6 * GetSizeMult ( ) } , GFX [ " circle.png " ] . Decal ( ) , shadowScale , BLACK ) ;
}
const bool NotOnTitleScreen = GameState : : STATE ! = GameState : : states [ States : : MAIN_MENU ] ;
uint8_t blendColAlpha = blendCol . a ;
if ( fadeTimer > 0.f ) blendColAlpha = uint8_t ( util : : lerp ( 0 , blendCol . a , fadeTimer ) ) ; //Fade timer goes from 1 to 0 seconds.
else if ( NotOnTitleScreen
& & ( game - > GetPlayer ( ) - > HasIframes ( ) | | OnUpperLevel ( ) ! = game - > GetPlayer ( ) - > OnUpperLevel ( ) | | abs ( GetZ ( ) - game - > GetPlayer ( ) - > GetZ ( ) ) > 1 ) ) blendColAlpha = blendCol . a * 0.62f ;
else if ( IsSolid ( ) & & solidFadeTimer > 0.f ) blendColAlpha = uint8_t ( util : : lerp ( blendCol . a , 255 - TileGroup : : FADE_AMT , solidFadeTimer / TileGroup : : FADE_TIME ) ) ;
blendCol . a = blendColAlpha ;
const float finalSpriteRot = HasFourWaySprites ( ) ? 0.f : spriteRot ; //Prevent 4-way sprites from being rotated.
vf2d imageScale { vf2d ( GetSizeMult ( ) * ( ! HasFourWaySprites ( ) & & GetFacingDirection ( ) = = Direction : : EAST ? - 1 : 1 ) , GetSizeMult ( ) ) } ;
if ( GetRemainingStunDuration ( ) > 0.f ) imageScale * = abs ( sin ( 3 * PI * game - > GetRunTime ( ) ) ) * 0.15f + 0.85f ;
const vf2d glowPurpleImageScale { imageScale * 1.1f } ;
const auto DrawBaseMonster = [ & ] ( vf2d scale = { 1.f , 1.f } , Pixel col = WHITE ) {
game - > view . DrawPartialRotatedDecal ( drawPos , GetFrame ( ) . GetSourceImage ( ) - > Decal ( ) , finalSpriteRot , GetFrame ( ) . GetSourceRect ( ) . size / 2 , GetFrame ( ) . GetSourceRect ( ) . pos , GetFrame ( ) . GetSourceRect ( ) . size , scale , col ) ;
} ;
const auto DrawOverlayMonster = [ & ] ( vf2d scale = { 1.f , 1.f } , Pixel col = WHITE ) {
game - > view . DrawPartialRotatedDecal ( drawPos , GFX [ overlaySprite ] . Decal ( ) , finalSpriteRot , GetFrame ( ) . GetSourceRect ( ) . size / 2 , GetFrame ( ) . GetSourceRect ( ) . pos , GetFrame ( ) . GetSourceRect ( ) . size , scale , col ) ;
} ;
const auto DrawMountedMonster = [ & ] ( vf2d scale = { 1.f , 1.f } , Pixel col = WHITE ) {
game - > view . DrawPartialRotatedDecal ( drawPos + mountedSprOffset , GetMountedFrame ( ) . value ( ) . GetSourceImage ( ) - > Decal ( ) , finalSpriteRot , GetMountedFrame ( ) . value ( ) . GetSourceRect ( ) . size / 2 , GetMountedFrame ( ) . value ( ) . GetSourceRect ( ) . pos , GetMountedFrame ( ) . value ( ) . GetSourceRect ( ) . size , scale , col ) ;
} ;
if ( glowPurpleBuff . has_value ( ) ) DrawBaseMonster ( glowPurpleImageScale , { 43 , 0 , 66 , blendCol . a } ) ;
DrawBaseMonster ( imageScale , blendCol ) ;
if ( overlaySprite . length ( ) ! = 0 ) {
if ( glowPurpleBuff . has_value ( ) ) DrawOverlayMonster ( imageScale , { 43 , 0 , 66 , overlaySpriteTransparency } ) ;
DrawOverlayMonster ( imageScale , { blendCol . r , blendCol . g , blendCol . b , overlaySpriteTransparency } ) ;
}
if ( HasMountedMonster ( ) ) {
if ( glowPurpleBuff . has_value ( ) ) DrawMountedMonster ( imageScale , { 43 , 0 , 66 , blendCol . a } ) ;
DrawMountedMonster ( imageScale , blendCol ) ;
}
std : : vector < Buff > shieldBuffs = GetBuffs ( BARRIER_DAMAGE_REDUCTION ) ;
if ( shieldBuffs . size ( ) > 0 ) {
game - > view . DrawRotatedDecal ( drawPos , GFX [ " block.png " ] . Decal ( ) , 0.f , GFX [ " block.png " ] . Sprite ( ) - > Size ( ) / 2 , { GetSizeMult ( ) , GetSizeMult ( ) } , blendCol ) ;
}
# pragma region Render Trapper Marked Targets
const uint8_t markStackCount { GetMarkStacks ( ) } ;
if ( markStackCount > 0 ) {
float markRotation { - util : : lerp ( 0.f , 10.f , markApplicationTimer / 0.5f ) * sin ( PI * markApplicationTimer ) } ;
vf2d markScale { vf2d { } . lerp ( vf2d { GetSizeMult ( ) , GetSizeMult ( ) } , ( 0.5f - markApplicationTimer ) / 0.5f ) } ;
const Animate2D : : Frame & markImg { ANIMATION_DATA [ " target.png " ] . GetFrame ( game - > GetRunTime ( ) ) } ;
Pixel markCol { markStackCount > 1 ? WHITE : RED } ;
const std : : vector < Buff > & buffList { GetBuffs ( BuffType : : TRAPPER_MARK ) } ;
float remainingStackDuration { } ;
for ( const Buff & b : buffList ) {
if ( b . type = = BuffType : : TRAPPER_MARK ) {
remainingStackDuration = b . duration ;
break ;
}
}
if ( remainingStackDuration < 1.f ) markCol . a * = remainingStackDuration ;
game - > view . DrawPartialRotatedDecal ( drawPos , markImg . GetSourceImage ( ) - > Decal ( ) , markRotation , markImg . GetSourceRect ( ) . size / 2.f , markImg . GetSourceRect ( ) . pos , markImg . GetSourceRect ( ) . size , markScale , markCol ) ;
}
# pragma endregion
# pragma region Render Trapper Special Marked Targets
const uint8_t specialMarkStackCount { GetSpecialMarkStacks ( ) } ;
float specialMarkOriginalDuration { } ;
if ( specialMarkStackCount > 0 ) {
const std : : vector < Buff > & buffList { GetBuffs ( BuffType : : SPECIAL_MARK ) } ;
float remainingStackDuration { } ;
for ( const Buff & b : buffList ) {
if ( b . type = = BuffType : : SPECIAL_MARK ) {
remainingStackDuration = b . duration ;
specialMarkOriginalDuration = b . originalDuration ;
break ;
}
}
const bool OpportunityShotActive { game - > GetPlayer ( ) - > HasEnchant ( " Opportunity Shot " ) & & specialMarkOriginalDuration - remainingStackDuration < = " Opportunity Shot " _ENC [ " MARK TRIGGER TIME " ] } ;
float markRotation { - util : : lerp ( 0.f , 10.f , specialMarkApplicationTimer / 0.5f ) * sin ( PI * specialMarkApplicationTimer ) } ;
vf2d markScale { vf2d { } . lerp ( vf2d { GetSizeMult ( ) , GetSizeMult ( ) } , ( 0.5f - specialMarkApplicationTimer ) / 0.5f ) } ;
Animate2D : : Frame markImg = { ANIMATION_DATA [ " target.png " ] . GetFrame ( game - > GetRunTime ( ) ) } ;
if ( OpportunityShotActive ) markImg = { ANIMATION_DATA [ " special_target.png " ] . GetFrame ( game - > GetRunTime ( ) ) } ;
Pixel markCol { WHITE } ;
if ( remainingStackDuration < 1.f ) markCol . a * = remainingStackDuration ;
game - > view . DrawPartialRotatedDecal ( drawPos , markImg . GetSourceImage ( ) - > Decal ( ) , markRotation , markImg . GetSourceRect ( ) . size / 2.f , markImg . GetSourceRect ( ) . pos , markImg . GetSourceRect ( ) . size , markScale , markCol ) ;
}
# pragma endregion
if ( GameSettings : : TerrainCollisionBoxesEnabled ( ) & & IsSolid ( ) & & solidFadeTimer > 0.f ) {
float distToPlayer = geom2d : : line < float > ( game - > GetPlayer ( ) - > GetPos ( ) , GetPos ( ) ) . length ( ) ;
const float collisionRadiusFactor = GetCollisionRadius ( ) / 12.f ;
if ( distToPlayer < 24 * 3 * collisionRadiusFactor ) {
game - > DrawPie ( game - > view . WorldToScreen ( GetPos ( ) ) , GetCollisionRadius ( ) , 0.f , { 255 , 0 , 0 , uint8_t ( 128 * ( blendColAlpha / 255.f ) / sqrt ( distToPlayer * collisionRadiusFactor ) ) } ) ;
game - > SetDecalMode ( DecalMode : : WIREFRAME ) ;
game - > DrawPie ( game - > view . WorldToScreen ( GetPos ( ) ) , GetCollisionRadius ( ) , 0.f , { 128 , 0 , 0 , 255 } ) ;
game - > SetDecalMode ( DecalMode : : NORMAL ) ;
}
}
# 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 ( ) = = Direction : : EAST ) {
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 ;
}
}
B ( Attribute : : COLLIDED_WITH_PLAYER ) = true ;
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 ( ) {
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 ( GetFacingDirection ( ) ) ;
}
const bool Monster : : AttackAvoided ( const float attackZ ) const {
return HasIframes ( ) | | abs ( GetZ ( ) - attackZ ) > 1 ;
}
bool Monster : : Hurt ( HurtDamageInfo damageData ) {
return _Hurt ( damageData . damage , damageData . onUpperLevel , damageData . z , damageData . damageRule , damageData . hurtFlags ) ;
}
bool Monster : : Hurt ( int damage , bool onUpperLevel , float z , HurtFlag : : HurtFlag hurtFlags ) {
return _Hurt ( damage , onUpperLevel , z , TrueDamageFlag : : NORMAL_DAMAGE , hurtFlags ) ;
}
bool Monster : : _Hurt ( int damage , bool onUpperLevel , float z , const TrueDamageFlag damageRule , HurtFlag : : HurtFlag hurtFlags ) {
if ( IsDead ( ) ) return false ;
const bool HitByPlayerAbility { bool ( hurtFlags & HurtFlag : : PLAYER_ABILITY ) } ;
const bool TrueDamage { damageRule = = TrueDamageFlag : : IGNORE_DAMAGE_RULES } ;
const bool IsDOT { bool ( hurtFlags & HurtFlag : : DOT ) } ;
const bool NormalDamageCalculationRequired { ! IsDOT & & ! TrueDamage } ;
const bool PlayHitSoundEffect { ! IsDOT } ;
if ( ! TrueDamage & & ! IsDOT & & InUndamageableState ( onUpperLevel , z ) ) return false ;
if ( game - > InBossEncounter ( ) ) {
game - > StartBossEncounter ( ) ;
}
game - > GetPlayer ( ) - > ResetLastCombatTime ( ) ;
float mod_dmg = float ( damage ) ;
bool crit { false } ;
if ( NormalDamageCalculationRequired ) {
# pragma region Handle Crits
if ( util : : random ( 1 ) < game - > GetPlayer ( ) - > GetCritRatePct ( ) ) {
mod_dmg * = 1 + game - > GetPlayer ( ) - > GetCritDmgPct ( ) ;
crit = true ;
}
# pragma endregion
mod_dmg - = mod_dmg * GetDamageReductionFromBuffs ( ) ;
}
if ( HitByPlayerAbility ) {
auto ApplyMarkEffects = [ & ] ( ) {
Hurt ( game - > GetPlayer ( ) - > GetAttack ( ) * " Trapper.Ability 1.Damage Increase Bonus " _F / 100.f , OnUpperLevel ( ) , GetZ ( ) , HurtFlag : : DOT ) ;
} ;
if ( GetSpecialMarkStacks ( ) > 0 ) {
ApplyMarkEffects ( ) ;
const Buff specialMarkBuff { GetBuffs ( BuffType : : SPECIAL_MARK ) [ 0 ] } ;
if ( game - > GetPlayer ( ) - > HasEnchant ( " Opportunity Shot " ) & & specialMarkBuff . originalDuration - specialMarkBuff . duration < = " Opportunity Shot " _ENC [ " MARK TRIGGER TIME " ] ) Trapper : : ability1 . cooldown - = " Opportunity Shot " _ENC [ " MARK COOLDOWN REDUCE " ] ;
RemoveSpecialMarkStack ( ) ;
} else
if ( GetMarkStacks ( ) > 0 ) {
ApplyMarkEffects ( ) ;
RemoveMarkStack ( ) ;
}
if ( game - > GetPlayer ( ) - > HasEnchant ( " Lethal Tempo " ) ) {
Buff & lethalTempoBuff { game - > GetPlayer ( ) - > GetOrAddBuff ( BuffType : : LETHAL_TEMPO , { " Lethal Tempo " _ENC [ " STACK DURATION " ] , 0.f } ) } ;
lethalTempoBuff . duration = " Lethal Tempo " _ENC [ " STACK DURATION " ] ;
const int maxStackCount { int ( " Lethal Tempo " _ENC [ " MAX ATTACK SPEED INCREASE " ] / " Lethal Tempo " _ENC [ " ATTACK SPEED INCREASE " ] ) } ;
lethalTempoBuff . intensity = std : : min ( maxStackCount , int ( lethalTempoBuff . intensity ) + 1 ) ;
}
if ( game - > GetPlayer ( ) - > HasEnchant ( " Quickdraw " ) & & util : : random ( 1.f ) < = " Quickdraw " _ENC [ " RESET CHANCE " ] / 100.f ) game - > GetPlayer ( ) - > ReduceAutoAttackTimer ( INFINITE ) ;
}
const bool backstabOccurred { game - > GetPlayer ( ) - > HasEnchant ( " Backstabber " ) & & IsBackstabAttack ( ) } ;
mod_dmg * = GetDamageAmplificationMult ( backstabOccurred ) ;
mod_dmg = std : : ceil ( mod_dmg ) ;
hp = std : : max ( 0 , hp - int ( mod_dmg ) ) ;
if ( GetBuffs ( BuffType : : CURSE_OF_DEATH ) . size ( ) > 0 ) accumulatedCurseOfDeathDamage + = mod_dmg ;
if ( IsDOT ) {
if ( lastDotTimer > 0 ) {
dotNumberPtr . get ( ) - > AddDamage ( int ( mod_dmg ) ) ;
} else {
dotNumberPtr = std : : make_shared < DamageNumber > ( pos + vf2d { 0 , 8.f } , int ( mod_dmg ) , false , DamageNumberType : : DOT ) ;
DAMAGENUMBER_LIST . push_back ( dotNumberPtr ) ;
}
lastDotTimer = 0.05f ;
} else {
if ( lastHitTimer > 0 ) {
damageNumberPtr . get ( ) - > AddDamage ( int ( mod_dmg ) ) ;
damageNumberPtr . get ( ) - > SetPauseTimer ( 0.4f ) ;
} else {
damageNumberPtr = std : : make_shared < DamageNumber > ( pos , int ( mod_dmg ) ) ;
DAMAGENUMBER_LIST . push_back ( damageNumberPtr ) ;
}
# pragma region Change Label based on bonus conditions (Crit / Backstab)
if ( crit ) {
damageNumberPtr . get ( ) - > SetType ( DamageNumberType : : CRIT ) ;
}
if ( damageNumberPtr . get ( ) - > GetType ( ) = = DamageNumberType : : CRIT ) goto doneChangingDamageNumberColors ; //Crit number priority. Other colors should not apply if the crit has applied.
if ( backstabOccurred ) {
damageNumberPtr . get ( ) - > SetType ( DamageNumberType : : BACKSTAB ) ;
}
doneChangingDamageNumberColors :
# pragma endregion
lastHitTimer = 0.05f ;
}
attackedByPlayer = true ;
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 ( PlayHitSoundEffect & & monsterHurtSoundCooldown = = 0.f ) {
monsterHurtSoundCooldown = util : : random ( 0.5f ) + 0.5f ;
SoundEffect : : PlaySFX ( GetHurtSound ( ) , GetPos ( ) ) ;
}
}
if ( game - > InBossEncounter ( ) & & isBoss ) {
game - > BossDamageDealt ( int ( mod_dmg ) ) ;
}
using A = Attribute ;
GetInt ( A : : HITS_UNTIL_DEATH ) = std : : max ( 0 , GetInt ( A : : HITS_UNTIL_DEATH ) - 1 ) ;
ApplyIframes ( GetFloat ( A : : IFRAME_TIME_UPON_HIT ) ) ;
return true ;
}
const bool Monster : : IsAlive ( ) const {
return hp > 0 | | ! diesNormally ;
}
const vf2d & Monster : : GetTargetPos ( ) const {
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 ;
}
const bool Monster : : OnUpperLevel ( ) const {
return upperLevel ;
}
void Monster : : AddBuff ( BuffType type , float duration , float intensity ) {
buffList . emplace_back ( GetWeakPointer ( ) , type , duration , intensity ) ;
}
void Monster : : AddBuff ( BuffType type , float duration , float intensity , std : : set < ItemAttribute > attr ) {
if ( type = = STAT_UP ) std : : for_each ( attr . begin ( ) , attr . end ( ) , [ ] ( const ItemAttribute & attr ) { if ( attr . ActualName ( ) ! = " Health " & & attr . ActualName ( ) ! = " Health % " & & attr . ActualName ( ) ! = " Attack " & & attr . ActualName ( ) ! = " Attack % " ) ERR ( std : : format ( " WARNING! Stat Up Attribute type {} is NOT IMPLEMENTED! " , attr . ActualName ( ) ) ) ; } ) ;
buffList . emplace_back ( GetWeakPointer ( ) , type , duration , intensity , attr ) ;
}
void Monster : : AddBuff ( BuffType type , float duration , float intensity , std : : set < std : : string > attr ) {
if ( type = = STAT_UP ) std : : for_each ( attr . begin ( ) , attr . end ( ) , [ ] ( const std : : string & attr ) { if ( attr ! = " Health " & & attr ! = " Health % " & & attr ! = " Attack " & & attr ! = " Attack % " ) ERR ( std : : format ( " WARNING! Stat Up Attribute type {} is NOT IMPLEMENTED! " , attr ) ) ; } ) ;
buffList . emplace_back ( GetWeakPointer ( ) , type , duration , intensity , attr ) ;
}
void Monster : : AddBuff ( BuffType type , float duration , float intensity , Buff : : MonsterBuffExpireCallbackFunction expireCallback ) {
buffList . emplace_back ( GetWeakPointer ( ) , type , duration , intensity , expireCallback ) ;
}
void Monster : : AddBuff ( BuffType type , BuffRestorationType restorationType , BuffOverTimeType : : BuffOverTimeType overTimeType , float duration , float intensity , float timeBetweenTicks ) {
if ( type = = STAT_UP ) ERR ( " WARNING! Incorrect usage of STAT_UP buff! It should not be an overtime buff! Use a different constructor!!! " ) ;
buffList . emplace_back ( GetWeakPointer ( ) , type , restorationType , overTimeType , duration , intensity , timeBetweenTicks ) ;
}
void Monster : : AddBuff ( BuffType type , BuffRestorationType restorationType , BuffOverTimeType : : BuffOverTimeType overTimeType , float duration , float intensity , float timeBetweenTicks , Buff : : MonsterBuffExpireCallbackFunction expireCallback ) {
if ( type = = STAT_UP ) ERR ( " WARNING! Incorrect usage of STAT_UP buff! It should not be an overtime buff! Use a different constructor!!! " ) ;
buffList . emplace_back ( GetWeakPointer ( ) , type , restorationType , overTimeType , duration , intensity , timeBetweenTicks , expireCallback ) ;
}
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 ) {
canMove = false ;
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 ( ) ) {
canMove = SetPos ( pos + moveTowardsLine . vector ( ) . norm ( ) * 100.f * fElapsedTime * GetMoveSpdMult ( ) ) ;
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.f * 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 ;
}
}
//Try to move to the new determined location.
canMove = SetPos ( pos + moveTowardsLine . vector ( ) . norm ( ) * 100.f * fElapsedTime * GetMoveSpdMult ( ) ) ;
UpdateFacingDirection ( moveTowardsLine . end ) ;
}
}
} else {
//We actually can't do anything so just quit.
targetAcquireTimer = 0 ;
}
}
Buff & Monster : : EditBuff ( BuffType buff , size_t buffInd ) {
return EditBuffs ( buff ) [ buffInd ] ;
}
std : : vector < std : : reference_wrapper < Buff > > Monster : : EditBuffs ( BuffType buff ) {
std : : vector < std : : reference_wrapper < Buff > > filteredBuffs ;
std : : copy_if ( buffList . begin ( ) , buffList . end ( ) , std : : back_inserter ( filteredBuffs ) , [ & buff ] ( const Buff & b ) { return b . type = = buff ; } ) ;
return filteredBuffs ;
}
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 ;
}
std : : vector < Buff > Monster : : GetBuffs ( std : : vector < BuffType > buffs ) const {
std : : vector < Buff > filteredBuffs ;
std : : copy_if ( buffList . begin ( ) , buffList . end ( ) , std : : back_inserter ( filteredBuffs ) , [ & buffs ] ( const Buff & b ) { return std : : find ( buffs . begin ( ) , buffs . end ( ) , b . type ) ! = buffs . end ( ) ; } ) ;
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 [ const_cast < ItemInfo * > ( & data . item ) ] + + ;
}
}
}
return drops ;
}
void Monster : : OnDeath ( ) {
if ( animation . HasState ( MONSTER_DATA . at ( name ) . GetDefaultDeathAnimation ( ) ) ) animation . ChangeState ( internal_animState , GetDeathAnimationName ( ) ) ;
if ( GetSizeT ( Attribute : : LOOPING_SOUND_ID ) ! = std : : numeric_limits < size_t > : : max ( ) ) { //Just make sure on death any looping sound effect has been discarded proper.
SoundEffect : : StopLoopingSFX ( GetSizeT ( Attribute : : LOOPING_SOUND_ID ) ) ;
GetSizeT ( Attribute : : LOOPING_SOUND_ID ) = std : : numeric_limits < size_t > : : max ( ) ;
}
if ( GetSizeT ( Attribute : : RESPAWN_LOOPING_SOUND_ID ) ! = std : : numeric_limits < size_t > : : max ( ) ) { //Just make sure on death any looping sound effect has been discarded proper.
SoundEffect : : StopLoopingSFX ( GetSizeT ( Attribute : : RESPAWN_LOOPING_SOUND_ID ) ) ;
GetSizeT ( Attribute : : RESPAWN_LOOPING_SOUND_ID ) = std : : numeric_limits < size_t > : : max ( ) ;
}
if ( HasMountedMonster ( ) ) {
for ( DeathSpawnInfo & deathInfo : deathData ) {
deathInfo . Spawn ( GetPos ( ) , OnUpperLevel ( ) ) ;
}
mounted_animation = { } ;
internal_mounted_animState = { } ;
}
if ( isBoss ) {
game - > ReduceBossEncounterMobCount ( ) ;
if ( game - > BossEncounterMobCount ( ) = = 0 ) {
const bool exitRingShouldNotSpawn = SPAWNER_CONTROLLER . has_value ( ) & & ! SPAWNER_CONTROLLER . value ( ) . empty ( ) ;
if ( exitRingShouldNotSpawn ) { //See if we have a spawn controller and if we do, spawn the monsters from it instead of spawning the boss ring first.
const int nextSpawnerId = SPAWNER_CONTROLLER . value ( ) . front ( ) ;
SPAWNER_CONTROLLER . value ( ) . pop ( ) ;
SPAWNER_LIST [ nextSpawnerId ] . SetTriggered ( true ) ;
} else {
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.
Audio : : SetAudioEvent ( " BossFanfare " ) ;
game - > GetPlayer ( ) - > AddTimer ( PlayerTimerType : : FANFARE_WAIT_TIMER , Timer { " Fanfare Wait Timer " , 1.f , [ ] ( ) {
SoundEffect : : PlaySFX ( " Fanfare " , SoundEffect : : CENTERED ) ;
game - > GetPlayer ( ) - > AddTimer ( PlayerTimerType : : FANFARE_WAIT_TIMER , Timer { " Fanfare Wait Timer " , 5.f , [ ] ( ) {
Audio : : SetAudioEvent ( " BossDefeated " ) ;
} } ) ;
} } ) ;
}
}
}
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 ( strategyDeathFunc ) GameEvent : : AddEvent ( std : : make_unique < MonsterStrategyGameEvent > ( strategyDeathFunc , * this , MONSTER_DATA [ name ] . GetAIStrategy ( ) ) ) ;
if ( game - > GetPlayer ( ) - > HasEnchant ( " Reaper of Souls " ) ) game - > AddEffect ( std : : make_unique < MonsterSoul > ( GetPos ( ) , 0.3f , GetSizeMult ( ) , vf2d { } , WHITE , 0.f , 0.f , false ) ) ;
if ( game - > GetPlayer ( ) - > HasEnchant ( " Bloodlust " ) & & game - > GetPlayer ( ) - > GetBuffs ( BuffType : : ADRENALINE_RUSH ) . size ( ) > 0 ) {
Buff & adrenalineRushBuff { game - > GetPlayer ( ) - > EditBuff ( BuffType : : ADRENALINE_RUSH , 0 ) } ;
adrenalineRushBuff . duration + = " Bloodlust " _ENC [ " BUFF TIMER INCREASE " ] ;
adrenalineRushBuff . intensity = std : : min ( int ( " Bloodlust " _ENC [ " MAX ATTACK BUFF STACKS " ] ) , int ( adrenalineRushBuff . intensity ) + 1 ) ;
}
if ( game - > GetPlayer ( ) - > HasEnchant ( " Spreading Pain " ) & & GetBuffs ( BuffType : : CURSE_OF_PAIN ) . size ( ) > 0 ) {
//NOTE: If we have to change/modify Curse of Pain, we must also modify it in Witch.cpp (Witch::ability1.action define)
# pragma region Applies Curse of Pain to nearby targets
const float buffTimeBetweenTicks { " Witch.Ability 1.Curse Debuff " _f [ 1 ] } ;
const float buffDamageMult { " Witch.Ability 1.Curse Debuff " _f [ 0 ] } ;
const float buffDuration { " Witch.Ability 1.Curse Debuff " _f [ 2 ] } ;
for ( std : : shared_ptr < Monster > & m : MONSTER_LIST ) {
if ( & * m = = & * GetWeakPointer ( ) . lock ( ) | | m - > InUndamageableState ( OnUpperLevel ( ) , GetZ ( ) ) ) continue ;
if ( m - > GetBuffs ( BuffType : : CURSE_OF_PAIN ) . size ( ) > 0 ) {
m - > EditBuff ( BuffType : : CURSE_OF_PAIN , 0U ) . duration = m - > EditBuff ( BuffType : : CURSE_OF_PAIN , 0U ) . originalDuration ;
} else {
float distFromDeadMonster { geom2d : : line < float > ( GetPos ( ) , m - > GetPos ( ) ) . length ( ) } ;
if ( distFromDeadMonster < " Spreading Pain " _ENC [ " SPREAD RANGE " ] / 100 * 24 ) {
m - > ApplyDot ( buffDuration , game - > GetPlayer ( ) - > GetAttack ( ) * buffDamageMult , buffTimeBetweenTicks , BuffType : : CURSE_OF_PAIN ,
[ ] ( std : : weak_ptr < Monster > m , Buff & b ) {
expireCallbackFunc :
if ( ! m . expired ( ) ) m . lock ( ) - > Hurt ( game - > GetPlayer ( ) - > GetAttack ( ) * " Witch.Ability 1.Final Tick Damage " _F , m . lock ( ) - > OnUpperLevel ( ) , m . lock ( ) - > GetZ ( ) , HurtFlag : : DOT ) ;
}
) ;
m - > AddBuff ( BuffType : : GLOW_PURPLE , buffDuration , 1.f ) ;
}
}
DrawLineToTarget :
const vf2d targetPos { m - > GetPos ( ) } ;
for ( int i : std : : ranges : : iota_view ( 0 , int ( util : : distance ( GetPos ( ) , targetPos ) / 8 ) ) ) {
float drawDist { i * 8.f } ;
float fadeInTime { i * 0.05f } ;
float fadeOutTime { 0.5f + i * 0.05f } ;
float effectSize { util : : random ( 0.2f ) } ;
game - > AddEffect ( std : : make_unique < Effect > ( geom2d : : line < float > ( GetPos ( ) , targetPos ) . rpoint ( drawDist ) , 0.f , " mark_trail.png " , OnUpperLevel ( ) , fadeInTime , fadeOutTime , vf2d { effectSize , effectSize } , vf2d { } , Pixel { 100 , 0 , 155 , uint8_t ( util : : random_range ( 0 , 120 ) ) } , 0.f , 0.f ) , true ) ;
}
SoundEffect : : PlaySFX ( " Curse of Pain " , m - > GetPos ( ) ) ;
}
# pragma endregion
}
if ( game - > GetPlayer ( ) - > HasEnchant ( " Curse of Doom " ) & & accumulatedCurseOfDeathDamage > 0 ) {
# pragma region Go Boom
float radius { " Curse of Doom " _ENC [ " EXPLODE RANGE " ] / 100.f * 24 } ;
const HurtList list { game - > Hurt ( pos , radius , accumulatedCurseOfDeathDamage , OnUpperLevel ( ) , GetZ ( ) , HurtType : : MONSTER , HurtFlag : : PLAYER_ABILITY ) } ;
for ( const auto & [ targetPtr , wasHit ] : list ) {
if ( wasHit ) {
std : : get < Monster * > ( targetPtr ) - > ProximityKnockback ( pos , " Curse of Doom " _ENC [ " EXPLODE KNOCKBACK AMOUNT " ] ) ;
std : : get < Monster * > ( targetPtr ) - > Knockup ( " Curse of Doom " _ENC [ " EXPLODE KNOCKUP AMOUNT " ] ) ;
}
}
float targetScale { radius / 24 } ;
float startingScale { GetCollisionRadius ( ) / 24 } ;
std : : unique_ptr < Effect > explodeEffect { std : : make_unique < Effect > ( pos , ANIMATION_DATA [ " explosionframes.png " ] . GetTotalAnimationDuration ( ) - 0.2f , " explosionframes.png " , OnUpperLevel ( ) , startingScale , 0.2f , vf2d { 0 , - 6.f } , WHITE , util : : random ( 2 * PI ) , 0.f ) } ;
explodeEffect - > scaleSpd = { ( targetScale - startingScale ) / ANIMATION_DATA [ " explosionframes.png " ] . GetTotalAnimationDuration ( ) , ( targetScale - startingScale ) / ANIMATION_DATA [ " explosionframes.png " ] . GetTotalAnimationDuration ( ) } ;
game - > AddEffect ( std : : move ( explodeEffect ) ) ;
SoundEffect : : PlaySFX ( " Explosion " , pos ) ;
# pragma endregion
}
SpawnDrops ( ) ;
game - > GetPlayer ( ) - > AddAccumulatedXP ( MONSTER_DATA . at ( name ) . GetXP ( ) ) ;
RemoveBuff ( BuffType : : TRAPPER_MARK ) ;
}
const ItemAttributable & Monster : : GetStats ( ) const {
return stats ;
}
const ItemAttribute & Monster : : GetBonusStat ( std : : string_view attr ) const {
return ItemAttribute : : Get ( attr , const_cast < Monster * > ( 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 : : BulletCollisionHitbox ( ) {
return { GetPos ( ) , GetCollisionRadius ( ) * 2 } ;
}
void Monster : : Knockback ( const vf2d & vel ) {
if ( IsSolid ( ) ) return ;
//A new angle will be applied, but will be constrained by whichever applied velocity is strongest (either the current velocity, or the new one). This prevents continuous uncapped velocities to knockbacks applied.
if ( vel = = vf2d { } ) return ;
float maxVelThreshold ;
if ( this - > vel = = vf2d { } ) maxVelThreshold = vel . mag ( ) ;
else maxVelThreshold = std : : max ( vel . mag ( ) , this - > vel . mag ( ) ) ;
this - > vel + = vel ;
float newVelAngle = this - > vel . polar ( ) . y ;
this - > vel = vf2d { maxVelThreshold , newVelAngle } . cart ( ) ;
}
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 = HasFourWaySprites ( ) ? GetFacingDirectionToTarget ( targetPos ) : Direction : : EAST ;
spriteRot = dirToPlayer ;
} else {
facingDirection = HasFourWaySprites ( ) ? GetFacingDirectionToTarget ( targetPos ) : Direction : : WEST ;
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 ( ) ;
}
//Sets the strategy death function that runs when a monster dies.
// The function should return false to indicate the event is over. If the event should keep running, return true.
//Arguments are:
// GameEvent& - The death event itself.
// Monster& - The monster reference
// const std::string& - The strategy name.
void Monster : : SetStrategyDeathFunction ( std : : function < bool ( GameEvent & , Monster & , const std : : string & ) > func ) {
strategyDeathFunc = func ;
}
const bool Monster : : IsNPC ( ) const {
return MONSTER_DATA [ name ] . IsNPC ( ) ;
}
const bool MonsterData : : IsNPC ( ) const {
return isNPC ;
}
const Animate2D : : FrameSequence & Monster : : GetCurrentAnimation ( ) const {
return ANIMATION_DATA [ std : : format ( " {}_{} " , name , animation . currentStateName ) ] ;
}
const Animate2D : : FrameSequence & Monster : : GetAnimation ( const std : : string_view animationName ) const {
return ANIMATION_DATA [ std : : format ( " {}_{}_{} " , name , animationName , int ( GetFacingDirection ( ) ) ) ] ;
}
const bool Monster : : HasLineOfSight ( vf2d targetPos ) const {
geom2d : : line < float > losLine = geom2d : : line < float > ( GetPos ( ) , targetPos ) ;
float losLineLength = losLine . length ( ) ;
float losLineMarker = 0.f ;
bool hasLoS = true ;
while ( losLineMarker < losLineLength ) {
vf2d checkPos = losLine . rpoint ( losLineMarker ) ;
if ( game - > GetTileCollision ( game - > GetCurrentMapName ( ) , checkPos , OnUpperLevel ( ) ) ! = game - > NO_COLLISION ) {
hasLoS = false ;
break ;
}
losLineMarker + = game - > GetCurrentMapData ( ) . TileSize . x / 2.f ;
}
return hasLoS ;
}
const float Monster : : GetDistanceFrom ( vf2d target ) const {
return geom2d : : line < float > ( GetPos ( ) , target ) . length ( ) ;
}
const Direction Monster : : GetFacingDirectionToTarget ( vf2d target ) const {
float targetDirection = util : : angleTo ( GetPos ( ) , target ) ;
if ( targetDirection < = PI / 4 & & targetDirection > - PI / 4 ) return Direction : : EAST ;
else if ( targetDirection > = 3 * PI / 4 | | targetDirection < - 3 * PI / 4 ) return Direction : : WEST ;
else if ( targetDirection < = 3 * PI / 4 & & targetDirection > PI / 4 ) return Direction : : SOUTH ;
else if ( targetDirection > = - 3 * PI / 4 & & targetDirection < - PI / 4 ) return Direction : : NORTH ;
ERR ( std : : format ( " WARNING! Target direction {} did not result in a proper facing direction!! THIS SHOULD NOT BE HAPPENING! " , targetDirection ) ) ;
return Direction : : NORTH ;
}
const bool Monster : : HasFourWaySprites ( ) const {
return MONSTER_DATA . at ( name ) . HasFourWaySprites ( ) ;
}
const bool Monster : : HasMountedMonster ( ) const {
if ( internal_mounted_animState . has_value ( ) ^ mounted_animation . has_value ( ) ) ERR ( " WARNING! The internal mounted animation state and the mounted animation variables are not matching! They should both either be on or both be off! THIS SHOULD NOT BE HAPPENING! " ) ;
return internal_mounted_animState . has_value ( ) & & mounted_animation . has_value ( ) ;
}
const std : : optional < const Animate2D : : Frame > Monster : : GetMountedFrame ( ) const {
if ( ! HasMountedMonster ( ) ) return { } ;
else return mounted_animation . value ( ) . GetFrame ( internal_mounted_animState . value ( ) ) ;
}
DeathSpawnInfo : : DeathSpawnInfo ( const std : : string_view monsterName , const uint8_t spawnAmt , const vf2d spawnOffset )
: monsterSpawnName ( monsterName ) , spawnAmt ( spawnAmt ) , spawnLocOffset ( spawnOffset ) {
if ( ! MONSTER_DATA . count ( std : : string ( monsterName ) ) ) ERR ( std : : format ( " WARNING! Monster {} specified in DeathSpawnInfo does not exist! Please provide a proper monster name. " , monsterName ) ) ;
}
void DeathSpawnInfo : : Spawn ( const vf2d monsterDeathPos , const bool onUpperLevel ) {
for ( uint8_t i = 0 ; i < spawnAmt ; i + + ) {
game - > SpawnMonster ( monsterDeathPos + spawnLocOffset , MONSTER_DATA . at ( monsterSpawnName ) , onUpperLevel ) . ApplyIframes ( 0.25f ) ;
}
}
void Monster : : ProximityKnockback ( const vf2d centerPoint , const float knockbackFactor ) {
geom2d : : line < float > lineToMonster ( centerPoint , GetPos ( ) ) ;
float dist = lineToMonster . length ( ) ;
if ( dist < 0.001f ) {
float randomDir = util : : random ( 2 * PI ) ;
lineToMonster = { centerPoint , centerPoint + vf2d { cos ( randomDir ) , sin ( randomDir ) } * 1 } ;
}
Knockback ( lineToMonster . vector ( ) . norm ( ) * knockbackFactor ) ;
}
const bool Monster : : IgnoresTerrainCollision ( ) const {
return MONSTER_DATA . at ( GetName ( ) ) . IgnoresTerrainCollision ( ) ;
}
const float Monster : : TimeSpentAlive ( ) const {
return timeSpentAlive ;
}
const bool Monster : : Immovable ( ) const {
return MONSTER_DATA . at ( GetName ( ) ) . Immovable ( ) ;
}
const bool Monster : : Invulnerable ( ) const {
return MONSTER_DATA . at ( GetName ( ) ) . Invulnerable ( ) ;
}
const std : : optional < float > Monster : : GetLifetime ( ) const {
return lifetime ;
}
const std : : optional < float > Monster : : GetTotalLifetime ( ) const {
return MONSTER_DATA . at ( GetName ( ) ) . GetLifetime ( ) ;
}
const float Monster : : GetCollisionRadius ( ) const {
return MONSTER_DATA . at ( GetName ( ) ) . GetCollisionRadius ( ) * GetSizeMult ( ) ;
}
void Monster : : MarkForDeletion ( ) {
markedForDeletion = true ;
}
const bool Monster : : IsDead ( ) const {
return ! IsAlive ( ) ;
}
void Monster : : ApplyIframes ( const float iframeTime ) {
iframe_timer = std : : max ( iframe_timer , iframeTime ) ;
}
void Monster : : _SetIframes ( const float iframeTime ) {
iframe_timer = iframeTime ;
}
const std : : string_view Monster : : GetDisplayName ( ) const {
return MONSTER_DATA . at ( GetName ( ) ) . GetDisplayName ( ) ;
}
const bool Monster : : HasArrowIndicator ( ) const {
return MONSTER_DATA . at ( GetName ( ) ) . HasArrowIndicator ( ) ;
}
const bool Monster : : ReachedTargetPos ( const float maxDistanceFromTarget ) const {
return util : : distance ( GetPos ( ) , GetTargetPos ( ) ) < = maxDistanceFromTarget ;
}
const float Monster : : GetHealthRatio ( ) const {
return GetHealth ( ) / float ( GetMaxHealth ( ) ) ;
}
const bool Monster : : IsSolid ( ) const {
return Immovable ( ) ;
}
const bool Monster : : _DealTrueDamage ( const uint32_t damageAmt , HurtFlag : : HurtFlag hurtFlags ) {
return _Hurt ( damageAmt , OnUpperLevel ( ) , GetZ ( ) , TrueDamageFlag : : IGNORE_DAMAGE_RULES , hurtFlags ) ;
}
void Monster : : Heal ( const int healAmt ) {
hp = std : : clamp ( hp + healAmt , 0 , int ( GetMaxHealth ( ) ) ) ;
}
const float Monster : : GetModdedStatBonuses ( std : : string_view stat ) const {
if ( ItemAttribute : : Get ( stat ) . DisplayAsPercent ( ) ) ERR ( std : : format ( " WARNING! Stat {} was provided. A percentage-based stat should not be supplied here! GetModdedStatBonuses() is supposed to calculate using a BASE stat and includes the percentage modifier already! Please fix this! " , stat ) )
float flatBonuses { stats . A_Read ( stat ) + GetBonusStat ( stat ) } ;
float pctBonusSum { } ;
for ( const Buff & buff : GetBuffs ( BuffType : : STAT_UP ) ) {
for ( const ItemAttribute & attr : buff . attr ) {
if ( attr . Modifies ( ) = = stat ) {
if ( attr . DisplayAsPercent ( ) ) {
pctBonusSum + = buff . intensity * 100.f ;
}
}
}
}
return flatBonuses + flatBonuses * pctBonusSum / 100.f ;
}
const std : : optional < geom2d : : rect < float > > & Monster : : GetRectangleCollision ( ) const {
return MONSTER_DATA . at ( GetName ( ) ) . GetRectangleCollision ( ) ;
}
const uint8_t Monster : : GetMarkStacks ( ) const {
const std : : vector < Buff > & markBuffs { GetBuffs ( BuffType : : TRAPPER_MARK ) } ;
int stackCount { } ;
for ( const Buff & b : markBuffs ) {
stackCount + = b . intensity ;
}
return stackCount ;
}
const uint8_t Monster : : GetSpecialMarkStacks ( ) const {
const std : : vector < Buff > & markBuffs { GetBuffs ( BuffType : : SPECIAL_MARK ) } ;
int stackCount { } ;
for ( const Buff & b : markBuffs ) {
stackCount + = b . intensity ;
}
return stackCount ;
}
void Monster : : RemoveMarkStack ( ) {
if ( ! IsAlive ( ) ) return ;
if ( GetMarkStacks ( ) < = 0 ) ERR ( " WARNING! Tried to remove a mark stack, but no stacks exist. THIS SHOULD NOT BE HAPPENING! " ) ;
bool removeMarkDebuff { false } ;
for ( Buff & b : buffList ) {
if ( b . type = = BuffType : : TRAPPER_MARK & & b . intensity > 0 ) {
b . intensity - - ;
if ( b . intensity = = 0 ) removeMarkDebuff = true ;
break ;
}
}
if ( removeMarkDebuff ) RemoveBuff ( BuffType : : TRAPPER_MARK ) ;
}
void Monster : : RemoveSpecialMarkStack ( ) {
if ( ! IsAlive ( ) ) return ;
if ( GetSpecialMarkStacks ( ) < = 0 ) ERR ( " WARNING! Tried to remove a mark stack, but no stacks exist. THIS SHOULD NOT BE HAPPENING! " ) ;
bool removeMarkDebuff { false } ;
for ( Buff & b : buffList ) {
if ( b . type = = BuffType : : SPECIAL_MARK & & b . intensity > 0 ) {
b . intensity - - ;
if ( b . intensity = = 0 ) removeMarkDebuff = true ;
break ;
}
}
if ( removeMarkDebuff ) RemoveBuff ( BuffType : : SPECIAL_MARK ) ;
}
void Monster : : TriggerMark ( ) {
Hurt ( 0 , OnUpperLevel ( ) , GetZ ( ) , HurtFlag : : PLAYER_ABILITY ) ;
}
void Monster : : LongLastingMarkEffect ( float & out_markDuration ) {
if ( game - > GetPlayer ( ) - > HasEnchant ( " Long-Lasting Mark " ) ) out_markDuration = " Long-Lasting Mark " _ENC [ " DURATION " ] ;
}
void Monster : : ApplyMark ( float time , uint8_t stackCount ) {
float markDuration { time } ;
LongLastingMarkEffect ( markDuration ) ;
if ( GetMarkStacks ( ) > 0 ) {
for ( Buff & b : buffList ) {
if ( b . type = = BuffType : : TRAPPER_MARK ) {
b . intensity + = stackCount ;
b . duration = std : : max ( b . duration , markDuration ) ;
break ;
}
}
} else {
game - > AddToMarkedTargetList ( { GetWeakPointer ( ) , stackCount , markDuration } ) ;
}
markApplicationTimer = 0.5f ;
}
void Monster : : ApplySpecialMark ( float time , uint8_t stackCount ) {
float markDuration { time } ;
LongLastingMarkEffect ( markDuration ) ;
if ( GetSpecialMarkStacks ( ) > 0 ) {
for ( Buff & b : buffList ) {
if ( b . type = = BuffType : : SPECIAL_MARK ) {
b . intensity + = stackCount ;
b . duration = std : : max ( b . duration , markDuration ) ;
break ;
}
}
} else {
game - > AddToSpecialMarkedTargetList ( { GetWeakPointer ( ) , stackCount , markDuration } ) ;
}
specialMarkApplicationTimer = 0.5f ;
}
std : : optional < std : : weak_ptr < Monster > > Monster : : GetNearestMonster ( const vf2d point , const float maxDistance , const bool onUpperLevel , const float z , const std : : optional < std : : weak_ptr < Monster > > excludedTarget ) {
std : : optional < std : : weak_ptr < Monster > > closestMonster ;
std : : optional < std : : weak_ptr < Monster > > closestGenericMonster ;
for ( std : : shared_ptr < Monster > & m : MONSTER_LIST ) {
if ( excludedTarget & & ! excludedTarget . value ( ) . expired ( ) & & & * excludedTarget . value ( ) . lock ( ) = = & * m ) continue ;
geom2d : : line < float > aimingLine = geom2d : : line < float > ( point , m - > GetPos ( ) ) ;
float distToMonster = aimingLine . length ( ) ;
float distToClosestPoint , distToClosestGenericPoint ;
if ( closestMonster . has_value ( ) ) distToClosestPoint = geom2d : : line < float > ( point , closestMonster . value ( ) . lock ( ) - > GetPos ( ) ) . length ( ) ;
else distToClosestPoint = std : : numeric_limits < float > : : max ( ) ;
if ( closestGenericMonster . has_value ( ) ) distToClosestGenericPoint = geom2d : : line < float > ( point , closestGenericMonster . value ( ) . lock ( ) - > GetPos ( ) ) . length ( ) ;
else distToClosestGenericPoint = std : : numeric_limits < float > : : max ( ) ;
if ( ! m - > InUndamageableState ( onUpperLevel , z ) ) {
if ( distToClosestPoint > distToMonster & & distToMonster < = maxDistance ) {
closestMonster = m ;
}
}
if ( m - > IsAlive ( ) & & distToClosestGenericPoint > distToMonster & & distToMonster < = maxDistance ) {
closestGenericMonster = m ;
}
}
if ( closestMonster . has_value ( ) ) {
return closestMonster ;
} else
if ( closestGenericMonster . has_value ( ) ) {
return closestGenericMonster ;
}
return { } ;
}
const bool Monster : : InUndamageableState ( const bool onUpperLevel , const float z ) const {
return Invulnerable ( ) | | ! IsAlive ( ) | | onUpperLevel ! = OnUpperLevel ( ) | | AttackAvoided ( z ) | | IsNPC ( ) ;
}
void Monster : : AddBuff ( BuffRestorationType type , BuffOverTimeType : : BuffOverTimeType overTimeType , float duration , float intensity , float timeBetweenTicks ) {
buffList . push_back ( Buff { GetWeakPointer ( ) , type , overTimeType , duration , intensity , timeBetweenTicks } ) ;
}
void Monster : : AddBuff ( BuffRestorationType type , BuffOverTimeType : : BuffOverTimeType overTimeType , float duration , float intensity , float timeBetweenTicks , Buff : : MonsterBuffExpireCallbackFunction expireCallback ) {
buffList . push_back ( Buff { GetWeakPointer ( ) , type , overTimeType , duration , intensity , timeBetweenTicks , expireCallback } ) ;
}
const bool Monster : : CanMove ( ) const {
return knockUpTimer = = 0.f & & IsAlive ( ) & & stunTimer = = 0.f ;
}
const std : : weak_ptr < Monster > Monster : : GetWeakPointer ( ) const {
return weakPtr ;
}
void Monster : : ApplyDot ( float duration , int damage , float timeBetweenTicks , Buff : : MonsterBuffExpireCallbackFunction expireCallbackFunc ) {
AddBuff ( BuffRestorationType : : OVER_TIME , BuffOverTimeType : : HP_DAMAGE_OVER_TIME , duration , damage , timeBetweenTicks , expireCallbackFunc ) ;
}
void Monster : : ApplyDot ( float duration , int damage , float timeBetweenTicks , BuffType identifierType , Buff : : MonsterBuffExpireCallbackFunction expireCallbackFunc ) {
AddBuff ( identifierType , BuffRestorationType : : OVER_TIME , BuffOverTimeType : : HP_DAMAGE_OVER_TIME , duration , damage , timeBetweenTicks , expireCallbackFunc ) ;
}
void Monster : : SetWeakPointer ( std : : shared_ptr < Monster > & sharedMonsterPtr ) {
weakPtr = sharedMonsterPtr ;
}
const float Monster : : GetDamageAmplificationMult ( const bool backstabOccurred ) const {
float damageAmpMult { 1.f } ;
const std : : vector < Buff > & buffList { GetBuffs ( { BuffType : : DAMAGE_AMPLIFICATION , BuffType : : CURSE_OF_DEATH } ) } ;
for ( const Buff & buff : buffList ) {
damageAmpMult + = buff . intensity ;
}
if ( backstabOccurred ) damageAmpMult + = " Backstabber " _ENC [ " BACKSTAB BONUS DMG " ] / 100.f ;
return damageAmpMult ;
}
const bool Monster : : IsBackstabAttack ( ) const {
using enum Direction ;
switch ( GetFacingDirection ( ) ) {
case NORTH : {
if ( ! HasFourWaySprites ( ) ) ERR ( std : : format ( " WARNING! Facing direction of a one-way facing monster was detected to be facing NORTH (Monster {} at Pos {}). THIS SHOULD NOT BE HAPPENING! " , GetDisplayName ( ) , GetPos ( ) . str ( ) ) )
return game - > GetPlayer ( ) - > GetPos ( ) . y > GetPos ( ) . y ;
} break ;
case SOUTH : {
if ( ! HasFourWaySprites ( ) ) ERR ( std : : format ( " WARNING! Facing direction of a one-way facing monster was detected to be facing SOUTH (Monster {} at Pos {}). THIS SHOULD NOT BE HAPPENING! " , GetDisplayName ( ) , GetPos ( ) . str ( ) ) )
return game - > GetPlayer ( ) - > GetPos ( ) . y < GetPos ( ) . y ;
} break ;
case EAST : {
return game - > GetPlayer ( ) - > GetPos ( ) . x < GetPos ( ) . x ;
} break ;
case WEST : {
return game - > GetPlayer ( ) - > GetPos ( ) . x > GetPos ( ) . x ;
} break ;
default : {
ERR ( std : : format ( " WARNING! Facing direction of a monster was detected to be facing {} (Monster {} at Pos {}). This is not a normal facing direction and THIS SHOULD NOT BE HAPPENING! " , int ( GetFacingDirection ( ) ) , GetDisplayName ( ) , GetPos ( ) . str ( ) ) ) ;
return false ;
}
}
ERR ( " WARNING! An unhandled case was detected while trying to determine if an attack was a backstab attack! THIS SHOULD NOT BE HAPPENING! " ) ;
return false ;
}
void Monster : : Stun ( const float stunDuration ) {
stunTimer = std : : max ( stunTimer , stunDuration ) ;
}
const float & Monster : : GetRemainingStunDuration ( ) const {
return stunTimer ;
}
const bool Monster : : FadeoutWhenStandingBehind ( ) const {
return MONSTER_DATA . at ( name ) . FadeoutWhenStandingBehind ( ) ;
}
const bool Monster : : FaceTarget ( ) const {
return MONSTER_DATA . at ( name ) . FaceTarget ( ) ;
}
void Monster : : ResetCurseOfDeathDamage ( ) {
accumulatedCurseOfDeathDamage = 0 ;
}
void Monster : : AddAddedVelocity ( vf2d vel ) {
this - > addedVel + = vel ;
}
void Monster : : MoveForward ( const vf2d & moveForwardVec , const float fElapsedTime ) {
if ( moveForwardVec . mag ( ) = = 0.f ) ERR ( " WARNING! Passed a zero length vector into Move Forward! THIS IS NOT ALLOWED! " ) ;
SetPos ( pos + moveForwardVec . norm ( ) * 100.f * fElapsedTime * GetMoveSpdMult ( ) ) ;
}