mirror of
https://github.com/id-Software/quake2-rerelease-dll.git
synced 2025-02-25 04:30:45 +00:00
532 lines
14 KiB
C++
532 lines
14 KiB
C++
|
// Copyright (c) ZeniMax Media Inc.
|
||
|
// Licensed under the GNU General Public License 2.0.
|
||
|
|
||
|
#include "../g_local.h"
|
||
|
#include "../m_player.h"
|
||
|
#include "bot_utils.h"
|
||
|
|
||
|
constexpr int Team_Coop_Monster = 0;
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
Player_UpdateState
|
||
|
================
|
||
|
*/
|
||
|
void Player_UpdateState( edict_t * player ) {
|
||
|
const client_persistant_t & persistant = player->client->pers;
|
||
|
|
||
|
player->sv.ent_flags = SVFL_NONE;
|
||
|
if ( player->groundentity != nullptr || ( player->flags & FL_PARTIALGROUND ) != 0 ) {
|
||
|
player->sv.ent_flags |= SVFL_ONGROUND;
|
||
|
} else {
|
||
|
if ( player->client->ps.pmove.pm_flags & PMF_JUMP_HELD ) {
|
||
|
player->sv.ent_flags |= SVFL_IS_JUMPING;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( player->client->ps.pmove.pm_flags & PMF_ON_LADDER ) {
|
||
|
player->sv.ent_flags |= SVFL_ON_LADDER;
|
||
|
}
|
||
|
|
||
|
if ( ( player->client->ps.pmove.pm_flags & PMF_DUCKED ) != 0 ) {
|
||
|
player->sv.ent_flags |= SVFL_IS_CROUCHING;
|
||
|
}
|
||
|
|
||
|
if ( player->client->quad_time > level.time ) {
|
||
|
player->sv.ent_flags |= SVFL_HAS_DMG_BOOST;
|
||
|
} else if ( player->client->quadfire_time > level.time ) {
|
||
|
player->sv.ent_flags |= SVFL_HAS_DMG_BOOST;
|
||
|
} else if ( player->client->double_time > level.time ) {
|
||
|
player->sv.ent_flags |= SVFL_HAS_DMG_BOOST;
|
||
|
}
|
||
|
|
||
|
if ( player->client->invincible_time > level.time ) {
|
||
|
player->sv.ent_flags |= SVFL_HAS_PROTECTION;
|
||
|
}
|
||
|
|
||
|
if ( player->client->invisible_time > level.time ) {
|
||
|
player->sv.ent_flags |= SVFL_HAS_INVISIBILITY;
|
||
|
}
|
||
|
|
||
|
if ( ( player->client->ps.pmove.pm_flags & PMF_TIME_TELEPORT ) != 0 ) {
|
||
|
player->sv.ent_flags |= SVFL_HAS_TELEPORTED;
|
||
|
}
|
||
|
|
||
|
if ( player->takedamage ) {
|
||
|
player->sv.ent_flags |= SVFL_TAKES_DAMAGE;
|
||
|
}
|
||
|
|
||
|
if ( player->solid == SOLID_NOT ) {
|
||
|
player->sv.ent_flags |= SVFL_IS_HIDDEN;
|
||
|
}
|
||
|
|
||
|
if ( ( player->flags & FL_INWATER ) != 0 ) {
|
||
|
if ( player->waterlevel >= WATER_WAIST ) {
|
||
|
player->sv.ent_flags |= SVFL_IN_WATER;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( ( player->flags & FL_NOTARGET ) != 0 ) {
|
||
|
player->sv.ent_flags |= SVFL_NO_TARGET;
|
||
|
}
|
||
|
|
||
|
if ( ( player->flags & FL_GODMODE ) != 0 ) {
|
||
|
player->sv.ent_flags |= SVFL_GOD_MODE;
|
||
|
}
|
||
|
|
||
|
if ( player->movetype == MOVETYPE_NOCLIP ) {
|
||
|
player->sv.ent_flags |= SVFL_IS_NOCLIP;
|
||
|
}
|
||
|
|
||
|
if ( player->client->anim_end == FRAME_flip12 ) {
|
||
|
player->sv.ent_flags |= SVFL_IS_FLIPPING_OFF;
|
||
|
}
|
||
|
|
||
|
if ( player->client->anim_end == FRAME_salute11 ) {
|
||
|
player->sv.ent_flags |= SVFL_IS_SALUTING;
|
||
|
}
|
||
|
|
||
|
if ( player->client->anim_end == FRAME_taunt17 ) {
|
||
|
player->sv.ent_flags |= SVFL_IS_TAUNTING;
|
||
|
}
|
||
|
|
||
|
if ( player->client->anim_end == FRAME_wave11 ) {
|
||
|
player->sv.ent_flags |= SVFL_IS_WAVING;
|
||
|
}
|
||
|
|
||
|
if ( player->client->anim_end == FRAME_point12 ) {
|
||
|
player->sv.ent_flags |= SVFL_IS_POINTING;
|
||
|
}
|
||
|
|
||
|
if ( ( player->client->ps.pmove.pm_flags & PMF_DUCKED ) == 0 && player->client->anim_priority <= ANIM_WAVE ) {
|
||
|
player->sv.ent_flags |= SVFL_CAN_GESTURE;
|
||
|
}
|
||
|
|
||
|
if ( player->lastMOD.id == MOD_TELEFRAG || player->lastMOD.id == MOD_TELEFRAG_SPAWN ) {
|
||
|
player->sv.ent_flags |= SVFL_WAS_TELEFRAGGED;
|
||
|
}
|
||
|
|
||
|
if ( player->client->resp.spectator ) {
|
||
|
player->sv.ent_flags |= SVFL_IS_SPECTATOR;
|
||
|
}
|
||
|
|
||
|
player_skinnum_t pl_skinnum;
|
||
|
pl_skinnum.skinnum = player->s.skinnum;
|
||
|
player->sv.team = pl_skinnum.team_index;
|
||
|
|
||
|
player->sv.buttons = player->client->buttons;
|
||
|
|
||
|
const item_id_t armorType = ArmorIndex( player );
|
||
|
player->sv.armor_type = armorType;
|
||
|
player->sv.armor_value = persistant.inventory[ armorType ];
|
||
|
|
||
|
player->sv.health = ( player->deadflag != true ) ? player->health : -1;
|
||
|
player->sv.weapon = ( persistant.weapon != nullptr ) ? persistant.weapon->id : IT_NULL;
|
||
|
|
||
|
player->sv.last_attackertime = static_cast<int32_t>( player->client->last_attacker_time.milliseconds() );
|
||
|
player->sv.respawntime = static_cast<int32_t>( player->client->respawn_time.milliseconds() );
|
||
|
player->sv.waterlevel = player->waterlevel;
|
||
|
player->sv.viewheight = player->viewheight;
|
||
|
|
||
|
player->sv.viewangles = player->client->v_angle;
|
||
|
player->sv.viewforward = player->client->v_forward;
|
||
|
player->sv.velocity = player->velocity;
|
||
|
|
||
|
player->sv.ground_entity = player->groundentity;
|
||
|
player->sv.enemy = player->enemy;
|
||
|
|
||
|
static_assert( sizeof( persistant.inventory ) <= sizeof( player->sv.inventory ) );
|
||
|
memcpy( &player->sv.inventory, &persistant.inventory, sizeof( persistant.inventory ) );
|
||
|
|
||
|
if ( !player->sv.init ) {
|
||
|
player->sv.init = true;
|
||
|
player->sv.classname = player->classname;
|
||
|
player->sv.targetname = player->targetname;
|
||
|
player->sv.lobby_usernum = P_GetLobbyUserNum( player );
|
||
|
player->sv.starting_health = player->health;
|
||
|
player->sv.max_health = player->max_health;
|
||
|
|
||
|
// NOTE: entries are assumed to be ranked with the first armor assumed
|
||
|
// NOTE: to be the "best", and last the "worst". You don't need to add
|
||
|
// NOTE: entries for things like armor shards, only actual armors.
|
||
|
// NOTE: Check "Max_Armor_Types" to raise/lower the armor count.
|
||
|
armorInfo_t * armorInfo = player->sv.armor_info;
|
||
|
armorInfo[ 0 ].item_id = IT_ARMOR_BODY;
|
||
|
armorInfo[ 0 ].max_count = bodyarmor_info.max_count;
|
||
|
armorInfo[ 1 ].item_id = IT_ARMOR_COMBAT;
|
||
|
armorInfo[ 1 ].max_count = combatarmor_info.max_count;
|
||
|
armorInfo[ 2 ].item_id = IT_ARMOR_JACKET;
|
||
|
armorInfo[ 2 ].max_count = jacketarmor_info.max_count;
|
||
|
|
||
|
gi.Info_ValueForKey( player->client->pers.userinfo, "name", player->sv.netname, sizeof( player->sv.netname ) );
|
||
|
|
||
|
gi.Bot_RegisterEdict( player );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
Monster_UpdateState
|
||
|
================
|
||
|
*/
|
||
|
void Monster_UpdateState( edict_t * monster ) {
|
||
|
monster->sv.ent_flags = SVFL_NONE;
|
||
|
if ( monster->groundentity != nullptr ) {
|
||
|
monster->sv.ent_flags |= SVFL_ONGROUND;
|
||
|
}
|
||
|
|
||
|
if ( monster->takedamage ) {
|
||
|
monster->sv.ent_flags |= SVFL_TAKES_DAMAGE;
|
||
|
}
|
||
|
|
||
|
if ( monster->solid == SOLID_NOT || monster->movetype == MOVETYPE_NONE ) {
|
||
|
monster->sv.ent_flags |= SVFL_IS_HIDDEN;
|
||
|
}
|
||
|
|
||
|
if ( ( monster->flags & FL_INWATER ) != 0 ) {
|
||
|
monster->sv.ent_flags |= SVFL_IN_WATER;
|
||
|
}
|
||
|
|
||
|
if ( coop->integer ) {
|
||
|
monster->sv.team = Team_Coop_Monster;
|
||
|
} else {
|
||
|
monster->sv.team = Team_None; // TODO: CTF/TDM/etc...
|
||
|
}
|
||
|
|
||
|
monster->sv.health = ( monster->deadflag != true ) ? monster->health : -1;
|
||
|
monster->sv.waterlevel = monster->waterlevel;
|
||
|
monster->sv.enemy = monster->enemy;
|
||
|
monster->sv.ground_entity = monster->groundentity;
|
||
|
|
||
|
int32_t viewHeight = monster->viewheight;
|
||
|
if ( ( monster->monsterinfo.aiflags & AI_DUCKED ) != 0 ) {
|
||
|
viewHeight = int32_t( monster->maxs[ 2 ] - 4.0f );
|
||
|
}
|
||
|
monster->sv.viewheight = viewHeight;
|
||
|
|
||
|
monster->sv.viewangles = monster->s.angles;
|
||
|
|
||
|
AngleVectors( monster->s.angles, monster->sv.viewforward, nullptr, nullptr );
|
||
|
|
||
|
monster->sv.velocity = monster->velocity;
|
||
|
|
||
|
if ( !monster->sv.init ) {
|
||
|
monster->sv.init = true;
|
||
|
monster->sv.classname = monster->classname;
|
||
|
monster->sv.targetname = monster->targetname;
|
||
|
monster->sv.starting_health = monster->health;
|
||
|
monster->sv.max_health = monster->max_health;
|
||
|
|
||
|
gi.Bot_RegisterEdict( monster );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
Item_UpdateState
|
||
|
================
|
||
|
*/
|
||
|
void Item_UpdateState( edict_t * item ) {
|
||
|
item->sv.ent_flags = SVFL_IS_ITEM;
|
||
|
item->sv.respawntime = 0;
|
||
|
|
||
|
if ( item->team != nullptr ) {
|
||
|
item->sv.ent_flags |= SVFL_IN_TEAM;
|
||
|
} // some DM maps have items chained together in teams...
|
||
|
|
||
|
if ( item->solid == SOLID_NOT ) {
|
||
|
item->sv.ent_flags |= SVFL_IS_HIDDEN;
|
||
|
|
||
|
if ( item->nextthink.milliseconds() > 0 ) {
|
||
|
if ( ( item->svflags & SVF_RESPAWNING ) != 0 ) {
|
||
|
const gtime_t pendingRespawnTime = ( item->nextthink - level.time );
|
||
|
item->sv.respawntime = static_cast<int32_t>( pendingRespawnTime.milliseconds() );
|
||
|
} else {
|
||
|
// item will respawn at some unknown time in the future...
|
||
|
item->sv.respawntime = Item_UnknownRespawnTime;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// track who has picked us up so far...
|
||
|
item->sv.pickedup_list = item->item_picked_up_by;
|
||
|
|
||
|
const item_id_t itemID = item->item->id;
|
||
|
if ( itemID == IT_FLAG1 || itemID == IT_FLAG2 ) {
|
||
|
item->sv.ent_flags |= SVFL_IS_OBJECTIVE;
|
||
|
// TODO: figure out if the objective is dropped/carried/home...
|
||
|
}
|
||
|
|
||
|
// always need to update these for items, since random item spawning
|
||
|
// could change them at any time...
|
||
|
item->sv.classname = item->classname;
|
||
|
item->sv.item_id = item->item->id;
|
||
|
|
||
|
if ( !item->sv.init ) {
|
||
|
item->sv.init = true;
|
||
|
item->sv.targetname = item->targetname;
|
||
|
|
||
|
gi.Bot_RegisterEdict( item );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
Trap_UpdateState
|
||
|
================
|
||
|
*/
|
||
|
void Trap_UpdateState( edict_t * danger ) {
|
||
|
danger->sv.ent_flags = SVFL_TRAP_DANGER;
|
||
|
danger->sv.velocity = danger->velocity;
|
||
|
|
||
|
if ( danger->owner != nullptr && danger->owner->client != nullptr ) {
|
||
|
player_skinnum_t pl_skinnum;
|
||
|
pl_skinnum.skinnum = danger->owner->s.skinnum;
|
||
|
danger->sv.team = pl_skinnum.team_index;
|
||
|
}
|
||
|
|
||
|
if ( danger->groundentity != nullptr ) {
|
||
|
danger->sv.ent_flags |= SVFL_ONGROUND;
|
||
|
}
|
||
|
|
||
|
if ( ( danger->flags & FL_TRAP_LASER_FIELD ) == 0 ) {
|
||
|
danger->sv.ent_flags |= SVFL_ACTIVE; // non-lasers are always active
|
||
|
} else {
|
||
|
danger->sv.start_origin = danger->s.origin;
|
||
|
danger->sv.end_origin = danger->s.old_origin;
|
||
|
if ( ( danger->svflags & SVF_NOCLIENT ) == 0 ) {
|
||
|
if ( ( danger->s.renderfx & RF_BEAM ) ) {
|
||
|
danger->sv.ent_flags |= SVFL_ACTIVE; // lasers are active!!
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( !danger->sv.init ) {
|
||
|
danger->sv.init = true;
|
||
|
danger->sv.classname = danger->classname;
|
||
|
|
||
|
gi.Bot_RegisterEdict( danger );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
Edict_UpdateState
|
||
|
================
|
||
|
*/
|
||
|
void Edict_UpdateState( edict_t * edict ) {
|
||
|
edict->sv.ent_flags = SVFL_NONE;
|
||
|
edict->sv.health = edict->health;
|
||
|
|
||
|
if ( edict->takedamage ) {
|
||
|
edict->sv.ent_flags |= SVFL_TAKES_DAMAGE;
|
||
|
}
|
||
|
|
||
|
// plats, movers, and doors use this to determine move state.
|
||
|
const bool isDoor = ( ( edict->svflags & SVF_DOOR ) != 0 );
|
||
|
const bool isReversedDoor = ( isDoor && edict->spawnflags.has( SPAWNFLAG_DOOR_REVERSE ) );
|
||
|
|
||
|
// doors have their top/bottom states reversed from plats
|
||
|
// ( unless "reverse" spawnflag is set! )
|
||
|
if ( isDoor && !isReversedDoor ) {
|
||
|
if ( edict->moveinfo.state == STATE_TOP ) {
|
||
|
edict->sv.ent_flags |= SVFL_MOVESTATE_BOTTOM;
|
||
|
} else if ( edict->moveinfo.state == STATE_BOTTOM ) {
|
||
|
edict->sv.ent_flags |= SVFL_MOVESTATE_TOP;
|
||
|
}
|
||
|
} else {
|
||
|
if ( edict->moveinfo.state == STATE_TOP ) {
|
||
|
edict->sv.ent_flags |= SVFL_MOVESTATE_TOP;
|
||
|
} else if ( edict->moveinfo.state == STATE_BOTTOM ) {
|
||
|
edict->sv.ent_flags |= SVFL_MOVESTATE_BOTTOM;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( edict->moveinfo.state == STATE_UP || edict->moveinfo.state == STATE_DOWN ) {
|
||
|
edict->sv.ent_flags |= SVFL_MOVESTATE_MOVING;
|
||
|
}
|
||
|
|
||
|
edict->sv.start_origin = edict->moveinfo.start_origin;
|
||
|
edict->sv.end_origin = edict->moveinfo.end_origin;
|
||
|
|
||
|
if ( edict->svflags & SVF_DOOR ) {
|
||
|
if ( edict->flags & FL_LOCKED ) {
|
||
|
edict->sv.ent_flags |= SVFL_IS_LOCKED_DOOR;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( !edict->sv.init ) {
|
||
|
edict->sv.init = true;
|
||
|
edict->sv.classname = edict->classname;
|
||
|
edict->sv.targetname = edict->targetname;
|
||
|
edict->sv.spawnflags = edict->spawnflags.value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
Entity_UpdateState
|
||
|
================
|
||
|
*/
|
||
|
void Entity_UpdateState( edict_t * edict ) {
|
||
|
if ( edict->svflags & SVF_MONSTER ) {
|
||
|
Monster_UpdateState( edict );
|
||
|
} else if ( edict->flags & FL_TRAP || edict->flags & FL_TRAP_LASER_FIELD ) {
|
||
|
Trap_UpdateState( edict );
|
||
|
} else if ( edict->item != nullptr ) {
|
||
|
Item_UpdateState( edict );
|
||
|
} else if ( edict->client != nullptr ) {
|
||
|
Player_UpdateState( edict );
|
||
|
} else {
|
||
|
Edict_UpdateState( edict );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
USE( info_nav_lock_use ) ( edict_t * self, edict_t * other, edict_t * activator ) -> void {
|
||
|
edict_t * n = nullptr;
|
||
|
|
||
|
while ( ( n = G_FindByString<&edict_t::targetname>( n, self->target ) ) ) {
|
||
|
if ( !( n->svflags & SVF_DOOR ) ) {
|
||
|
gi.Com_PrintFmt( "{} tried targeting {}, a non-SVF_DOOR\n", *self, *n );
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
n->flags ^= FL_LOCKED;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*QUAKED info_nav_lock (1.0 1.0 0.0) (-16 -16 0) (16 16 32)
|
||
|
toggle locked state on linked entity
|
||
|
*/
|
||
|
void SP_info_nav_lock( edict_t * self ) {
|
||
|
if ( !self->targetname ) {
|
||
|
gi.Com_PrintFmt( "{} missing targetname\n", *self );
|
||
|
G_FreeEdict( self );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ( !self->target ) {
|
||
|
gi.Com_PrintFmt( "{} missing target\n", *self );
|
||
|
G_FreeEdict( self );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
self->svflags |= SVF_NOCLIENT;
|
||
|
self->use = info_nav_lock_use;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
FindLocalPlayer
|
||
|
================
|
||
|
*/
|
||
|
const edict_t * FindLocalPlayer() {
|
||
|
const edict_t * localPlayer = nullptr;
|
||
|
|
||
|
const edict_t * ent = &g_edicts[ 0 ];
|
||
|
for ( uint32_t i = 0; i < globals.num_edicts; i++, ent++ ) {
|
||
|
if ( !ent->inuse || !( ent->svflags & SVF_PLAYER ) ) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ( ent->health <= 0 ) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
localPlayer = ent;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return localPlayer;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
FindFirstBot
|
||
|
================
|
||
|
*/
|
||
|
const edict_t * FindFirstBot() {
|
||
|
const edict_t * firstBot = nullptr;
|
||
|
|
||
|
const edict_t * ent = &g_edicts[ 0 ];
|
||
|
for ( uint32_t i = 0; i < globals.num_edicts; i++, ent++ ) {
|
||
|
if ( !ent->inuse || !( ent->svflags & SVF_PLAYER ) ) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ( ent->health <= 0 ) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ( !( ent->svflags & SVF_BOT ) ) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
firstBot = ent;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return firstBot;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
FindFirstMonster
|
||
|
================
|
||
|
*/
|
||
|
const edict_t * FindFirstMonster() {
|
||
|
const edict_t * firstMonster = nullptr;
|
||
|
|
||
|
const edict_t * ent = &g_edicts[ 0 ];
|
||
|
for ( uint32_t i = 0; i < globals.num_edicts; i++, ent++ ) {
|
||
|
if ( !ent->inuse || !( ent->svflags & SVF_MONSTER ) ) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ( ent->health <= 0 ) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
firstMonster = ent;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return firstMonster;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
================
|
||
|
FindFirstMonster
|
||
|
|
||
|
"Actors" are either players or monsters - i.e. something alive and thinking.
|
||
|
================
|
||
|
*/
|
||
|
const edict_t * FindActorUnderCrosshair( const edict_t * player ) {
|
||
|
if ( player == nullptr || !player->inuse ) {
|
||
|
return nullptr;
|
||
|
}
|
||
|
|
||
|
vec3_t forward, right, up;
|
||
|
AngleVectors( player->client->v_angle, forward, right, up );
|
||
|
|
||
|
const vec3_t eye_position = ( player->s.origin + vec3_t{ 0.0f, 0.0f, (float)player->viewheight } );
|
||
|
const vec3_t end = ( eye_position + ( forward * 8192.0f ) );
|
||
|
const contents_t mask = ( MASK_PROJECTILE & ~CONTENTS_DEADMONSTER );
|
||
|
|
||
|
trace_t tr = gi.traceline( eye_position, end, player, mask );
|
||
|
|
||
|
const edict_t * traceEnt = tr.ent;
|
||
|
if ( traceEnt == nullptr || !tr.ent->inuse ) {
|
||
|
return nullptr;
|
||
|
}
|
||
|
|
||
|
if ( !( traceEnt->svflags & SVF_PLAYER ) && !( traceEnt->svflags & SVF_MONSTER ) ) {
|
||
|
return nullptr;
|
||
|
}
|
||
|
|
||
|
if ( traceEnt->health <= 0 ) {
|
||
|
return nullptr;
|
||
|
}
|
||
|
|
||
|
return traceEnt;
|
||
|
}
|