Files
DoomRTX/neo/prey/Prey/game_monster_ai.cpp
T
2026-05-05 15:01:01 -07:00

2159 lines
66 KiB
C++

#include "precompiled.h"
#pragma hdrstop
#include "prey_local.h"
//-----------------------------------------------------
// hhNoClipEnt
//-----------------------------------------------------
CLASS_DECLARATION(idEntity, hhNoClipEnt)
END_CLASS
void hhNoClipEnt::Spawn( void ) {
if(GetPhysics())
GetPhysics()->SetContents(0);
}
//-----------------------------------------------------
// hhAINode
//-----------------------------------------------------
CLASS_DECLARATION( idEntity, hhAINode )
END_CLASS
hhAINode::hhAINode( void ) {
user.Clear();
}
void hhAINode::Save( idSaveGame *savefile ) const {
user.Save( savefile );
}
void hhAINode::Restore( idRestoreGame *savefile ) {
user.Restore( savefile );
}
//-----------------------------------------------------
// hhMonsterAI
//-----------------------------------------------------
idList<idAI*> hhMonsterAI::allSimpleMonsters;
hhMonsterAI::hhMonsterAI() {
lookOffset = ang_zero;
shootTarget = NULL;
bBossBar = false;
spawnThinkFlags = 0;
lastContactTime = gameLocal.time;
nextSpeechTime = gameLocal.time;
frozen = false;
soundOnModel = false;
}
hhMonsterAI::~hhMonsterAI() {
allSimpleMonsters.Remove(this);
if ( bBossBar && spawnArgs.GetBool( "remove_bar_on_dissolve" ) ) {
idPlayer *player = gameLocal.GetLocalPlayer();
if ( player && player->hud ) {
bBossBar = false;
player->hud->HandleNamedEvent("HideProgressBar");
player->hud->StateChanged(gameLocal.time);
}
}
}
void hhMonsterAI::Spawn() {
AI_HAS_RANGE_ATTACK = spawnArgs.GetBool("has_range_attack", "0");
AI_HAS_MELEE_ATTACK = spawnArgs.GetBool("has_melee_attack", "0");
targetReaction.reactionIndex = -1;
targetReaction.entity = NULL;
allSimpleMonsters.AddUnique(this);
bBindAxis = spawnArgs.GetBool( "bind_axis" );
bCanFall = spawnArgs.GetBool( "can_fall", "0" );
bSeeThroughPortals = spawnArgs.GetBool( "can_see_portals", "0" );
bBindOrient = spawnArgs.GetBool( "bind_orient", "0" );
hearingRange = spawnArgs.GetInt( "hearing_range", "1024" );
bCanWallwalk = spawnArgs.GetBool( "can_wallwalk", "0" );
fallDelay = spawnArgs.GetInt( "fall_delay" );
bOverrideKilledByGravityZones = spawnArgs.GetBool( "overrideKilledByGravityZones" );
bNeverTarget = spawnArgs.GetBool( "never_target", "0" );
bNoCombat = spawnArgs.GetBool( "no_combat", "0" );
const char *temp;
if ( spawnArgs.GetString( "fx_custom_blood", "", &temp ) ) {
bCustomBlood = true;
} else {
bCustomBlood = false;
}
nextSpiritProxyCheck = 0;
nextTurnUpdate = 0;
frozen = false;
//handle initial rotation and sticking on wallwalk
if ( !IsHidden() ) {
PostEventSec( &MA_InitialWallwalk, 0.1f );
PostEventMS(&MA_EnemyOnSpawn, 10);
}
PostEventMS(&EV_PostSpawn, 0);
if ( spawnArgs.GetInt( "wander_radius" ) ) {
spawnOrigin = GetOrigin();
}
// CJR: Clear the DDA values used for tracking damage inflicted on the player
totalDDADamage = 0;
}
void hhMonsterAI::Event_PostSpawn() {
CreateHealthTriggers();
//TODO: Move to character/girlfriend code when it exists
// Spawn earrings for girlfriend
idEntity *ent;
const char *defName = spawnArgs.GetString("def_earring", NULL);
if (defName && defName[0] && head.IsValid()) {
const char *boneNameL = spawnArgs.GetString("earringboneL");
const char *boneNameR = spawnArgs.GetString("earringboneR");
idDict args;
args.Clear();
args.Set( "origin", GetPhysics()->GetOrigin().ToString() );
ent = gameLocal.SpawnObject(defName, &args);
if (ent) {
ent->MoveToJoint(head.GetEntity(), boneNameL);
ent->BindToJoint(head.GetEntity(), boneNameL, false);
}
ent = gameLocal.SpawnObject(defName, &args);
if (ent) {
ent->MoveToJoint(head.GetEntity(), boneNameR);
ent->BindToJoint(head.GetEntity(), boneNameR, false);
}
}
}
bool hhMonsterAI::HasPathTo(const idVec3 &destPt) {
if(!GetAAS()) // || !allowAAS) //TODO: support the allowAAS functionality
return FALSE;
idVec3 fromPos = GetPhysics()->GetOrigin();
int currAreaNum = PointReachableAreaNum(fromPos);
int toAreaNum = PointReachableAreaNum(destPt);
// Invalid #'s ?
if(toAreaNum == 0 || currAreaNum == 0)
return FALSE;
aasPath_t path;
return PathToGoal(path, currAreaNum, fromPos, toAreaNum, destPt);
}
/*
==============================
hhMonsterAI::LinkScriptVariables(void)
==============================
*/
#define LinkScriptVariable( name ) name.LinkTo( scriptObject, #name )
void hhMonsterAI::LinkScriptVariables(void) {
idAI::LinkScriptVariables();
LinkScriptVariable( AI_HAS_RANGE_ATTACK ); // required for reaction system
LinkScriptVariable( AI_HAS_MELEE_ATTACK ); // required for reaction system
LinkScriptVariable( AI_USING_REACTION ); // TRUE if monster is currently using reaction
LinkScriptVariable( AI_REACTION_FAILED ); // TRUE if the last reaction attempt failed (path blocked, exclusive problem, etc)
LinkScriptVariable( AI_REACTION_ANIM );
LinkScriptVariable( AI_BACKWARD );
LinkScriptVariable( AI_STRAFE_LEFT );
LinkScriptVariable( AI_STRAFE_RIGHT );
LinkScriptVariable( AI_UPWARD );
LinkScriptVariable( AI_DOWNWARD );
LinkScriptVariable( AI_SHUTTLE_DOCKED );
LinkScriptVariable( AI_VEHICLE_ATTACK );
LinkScriptVariable( AI_VEHICLE_ALT_ATTACK );
LinkScriptVariable( AI_WALLWALK );
LinkScriptVariable( AI_FALLING );
LinkScriptVariable( AI_PATHING );
LinkScriptVariable( AI_TURN_DIR );
LinkScriptVariable( AI_FLY_NO_SEEK );
LinkScriptVariable( AI_FOLLOWING_PATH );
}
void hhMonsterAI::Think( void ) {
PROFILE_SCOPE("AI", PROFMASK_NORMAL|PROFMASK_AI);
if (ai_skipThink.GetBool()) { //HUMANHEAD rww
return;
}
idVec3 oldOrigin = physicsObj.GetOrigin();
idVec3 oldVelocity = physicsObj.GetLinearVelocity();
if ( thinkFlags & TH_THINK ) {
// Update vehicle guns
if(AI_VEHICLE && InVehicle()) {
usercmd_t cmds;
const signed char speed = 64; // In range [0..127]
memset( &cmds, 0, sizeof(usercmd_t) );
cmds.buttons |= (AI_VEHICLE_ATTACK) ? BUTTON_ATTACK : 0;
cmds.buttons |= (AI_VEHICLE_ALT_ATTACK) ? BUTTON_ATTACK_ALT : 0;
if(AI_FORWARD) {
cmds.forwardmove = speed;
} else if(AI_BACKWARD) {
cmds.forwardmove = -speed;
}
if(AI_STRAFE_RIGHT) {
cmds.rightmove = speed;
} else if(AI_STRAFE_LEFT) {
cmds.rightmove = -speed;
}
if(AI_UPWARD) {
cmds.upmove = speed;
} else if(AI_DOWNWARD) {
cmds.upmove = -speed;
}
GetVehicleInterfaceLocal()->BufferPilotCmds( &cmds, NULL );
AI_SHUTTLE_DOCKED = GetVehicleInterfaceLocal()->IsVehicleDocked();
}
// clear out the enemy when he dies or is hidden
idActor *enemyEnt = enemy.GetEntity();
if ( enemyEnt ) {
if ( enemyEnt->IsType( hhSpiritProxy::Type ) ) {
idPlayer *player = gameLocal.GetLocalPlayer();
if ( player && player->AI_DEAD ) {
EnemyDead();
}
} else if ( enemyEnt->health <= 0 ) {
EnemyDead();
}
}
//HUMANHEAD: aob - vehicle updates our viewAxis
if( !InVehicle() ) {
current_yaw += deltaViewAngles.yaw;
ideal_yaw = idMath::AngleNormalize180( ideal_yaw + deltaViewAngles.yaw );
deltaViewAngles.Zero();
viewAxis = idAngles( 0, current_yaw, 0 ).ToMat3();
// Determine turn dir
if (gameLocal.time > nextTurnUpdate) {
AI_TURN_DIR = GetTurnDir();
nextTurnUpdate = gameLocal.time + 250;
}
}
//HUMANHEAD END
if ( num_cinematics ) {
if ( !IsHidden() && torsoAnim.AnimDone( 0 ) ) {
PlayCinematic();
}
RunPhysics();
} else if ( !allowHiddenMovement && IsHidden() ) {
// hidden monsters
UpdateAIScript();
// HUMANHEAD pdm: Vehicle support
} else if ( InVehicle() ) {
UpdateEnemyPosition();
UpdateAIScript();
FlyMove();
// HUMANHEAD END
} else {
// clear the ik before we do anything else so the skeleton doesn't get updated twice
walkIK.ClearJointMods();
// HUMANHEAD NLA
physicsObj.ResetNumTouchEnt(0);
// HUMANHEAD END
switch( move.moveType ) {
case MOVETYPE_DEAD :
// dead monsters
UpdateAIScript();
DeadMove();
break;
case MOVETYPE_FLY :
// flying monsters
UpdateEnemyPosition();
UpdateAIScript();
FlyMove();
PlayChatter();
CheckBlink();
break;
case MOVETYPE_STATIC :
// static monsters
UpdateEnemyPosition();
UpdateAIScript();
StaticMove();
PlayChatter();
CheckBlink();
break;
case MOVETYPE_ANIM :
// animation based movement
UpdateEnemyPosition();
UpdateAIScript();
AnimMove();
PlayChatter();
CheckBlink();
break;
case MOVETYPE_SLIDE :
// velocity based movement
UpdateEnemyPosition();
UpdateAIScript();
SlideMove();
PlayChatter();
CheckBlink();
break;
}
// HUMANHEAD NLA
ClientImpacts();
// HUMANHEAD END
}
// clear pain flag so that we recieve any damage between now and the next time we run the script
AI_PAIN = false;
AI_SPECIAL_DAMAGE = 0;
AI_PUSHED = false;
} else if ( thinkFlags & TH_PHYSICS ) {
RunPhysics();
}
// HUMANHEAD jrm - need to call ticker function per aaron
if (thinkFlags & TH_TICKER) {
Ticker();
}
if ( af_push_moveables ) {
PushWithAF();
}
if ( fl.hidden && allowHiddenMovement ) {
// UpdateAnimation won't call frame commands when hidden, so call them here when we allow hidden movement
animator.ServiceAnims( gameLocal.previousTime, gameLocal.time );
}
UpdateMuzzleFlash();
UpdateAnimation();
UpdateParticles();
UpdateWounds();
Present();
UpdateDamageEffects();
LinkCombat();
CrashLand( oldOrigin, oldVelocity );
if(health > 0) {
idStr tmp;
if(ai_showNoAAS.GetBool() && spawnArgs.GetString("use_aas", "", tmp) && spawnArgs.GetBool("noaas_warning","1")) {
if(!aas) {
gameRenderWorld->DrawText("?", this->GetEyePosition() + idVec3(0.0f, 0.0f, 12.0f), 0.75f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3());
}
}
}
if ( !AI_DEAD && bCanFall && !IsHidden() && !fl.isTractored ) {
if ( physicsObj.HasContacts() || physicsObj.GetLinearVelocity().LengthSqr() < 4 ) {
lastContactTime = gameLocal.time;
if ( AI_FALLING ) {
AI_FALLING = false;
bCanFall = false;
SetState( GetScriptFunction( "state_Idle" ) );
SetWaitState( "" );
}
} else if ( !AI_FALLING && !af.IsActive() && physicsObj.GetLinearVelocity().LengthSqr() > 0 ) {
if ( gameLocal.time - lastContactTime > fallDelay ) {
AI_FALLING = true;
SetState( GetScriptFunction( "state_Nothing" ) );
Event_AnimState(ANIMCHANNEL_TORSO, "Torso_Fall", 4);
Event_AnimState(ANIMCHANNEL_LEGS, "Legs_Fall", 4);
}
}
}
//update wallwalk
if ( bCanWallwalk && !fl.isTractored ) {
trace_t TraceInfo;
gameLocal.clip.TracePoint(TraceInfo, GetOrigin(), GetOrigin() + this->GetPhysics()->GetGravityNormal() * 50, GetPhysics()->GetClipMask(), this);
if( TraceInfo.fraction < 1.0f && gameLocal.GetMatterType(TraceInfo, NULL) == SURFTYPE_WALLWALK ) {
SetGravity( -TraceInfo.c.normal * DEFAULT_GRAVITY );
AI_WALLWALK = true;
} else if ( AI_WALLWALK ) {
SetGravity( idVec3(0,0,-DEFAULT_GRAVITY) );
AI_WALLWALK = false;
}
} else if ( AI_WALLWALK ) {
SetGravity( idVec3(0,0,-DEFAULT_GRAVITY) );
AI_WALLWALK = false;
}
if ( bBossBar ) {
idPlayer *player = gameLocal.GetLocalPlayer();
if ( player && player->hud ) {
player->hud->SetStateFloat( "progress", idMath::ClampFloat( 0.0f, 1.0f, float(health) / (float)spawnHealth ) );
player->hud->StateChanged(gameLocal.time);
player->hud->Redraw( gameLocal.realClientTime );
}
}
if( ai_debugBrain.GetInteger() > 0 && !IsHidden() ) {
if ( enemy.IsValid() && enemy->GetHealth() > 0 ) {
gameRenderWorld->DebugArrow( colorWhite, GetOrigin(), enemy->GetOrigin(), 10 );
float dist = ( GetPhysics()->GetOrigin() - enemy->GetPhysics()->GetOrigin() ).LengthFast();
gameRenderWorld->DrawText( va("%i", int(dist)), this->GetEyePosition() + idVec3(0.0f, 0.0f, 60.0f), 0.75f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3());
}
if ( state ) {
if ( physicsObj.GetClipMask() == 0 ) {
gameRenderWorld->DrawText(state->Name(), GetEyePosition() + idVec3(0.0f, 0.0f, 40.0f), 0.75f, colorRed, gameLocal.GetLocalPlayer()->viewAngles.ToMat3());
} else {
gameRenderWorld->DrawText(state->Name(), GetEyePosition() + idVec3(0.0f, 0.0f, 40.0f), 0.75f, colorGreen, gameLocal.GetLocalPlayer()->viewAngles.ToMat3());
}
}
gameRenderWorld->DrawText(torsoAnim.state, this->GetEyePosition() + idVec3(0.0f, 0.0f, 20.0f), 0.75f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3());
gameRenderWorld->DrawText(legsAnim.state, this->GetEyePosition() + idVec3(0.0f, 0.0f, 0.0f), 0.75f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3());
if ( !walkIK.IsActivated() ) {
gameRenderWorld->DrawText("ik disabled", GetEyePosition() + idVec3(0.0f, 0.0f, -20.0f), 0.75f, colorRed, gameLocal.GetLocalPlayer()->viewAngles.ToMat3());
}
}
}
void hhMonsterAI::ApplyImpulse( idEntity *ent, int id, const idVec3 &point, const idVec3 &force ) {
//we don't want monsters pushed around by the player, projectiles, or splashDamange, so early out if they are.
if( !af.IsActive() ) {
if ( ent && (ent->IsType( hhProjectile::Type ) || ent->IsType( idWorldspawn::Type ) || ent->IsType( idPlayer::Type )) ) {
return;
}
}
af.GetPhysics()->UpdateTime( gameLocal.GetTime() );
BecomeActive(TH_THINK);
idAI::ApplyImpulse( ent, id, point, force );
}
//
// hhMonsterAI::CheckValidReaction
// Helper function for using reactions.
//
bool hhMonsterAI::CheckValidReaction() {
hhReaction *reaction = targetReaction.GetReaction();
if( !reaction || !reaction->causeEntity.IsValid() ) {
return false;
}
return true;
}
//
// hhMonsterAI::FinishReaction
// Called once a reaction has been used/failed. If it failed, set the failed flag and exit.
// Otherwise, set any 'finish_key' we have.
//
void hhMonsterAI::FinishReaction( bool bFailed ) {
AI_USING_REACTION = false;
if( bFailed ) {
AI_REACTION_FAILED = true;
}
else {
hhReaction *reaction = targetReaction.GetReaction();
if( reaction && reaction->desc->finish_key.Length() ) {
spawnArgs.Set( reaction->desc->finish_key.c_str(), reaction->desc->finish_val.c_str() );
}
}
//monster is finished with this reaction. let others use it
if ( targetReaction.entity.IsValid() ) {
targetReaction.entity->spawnArgs.Set( "react_inuse", "0" );
}
}
void hhMonsterAI::Killed(idEntity *inflictor, idEntity *attacker,
int damage, const idVec3 &dir, int location )
{
if ( AI_DEAD ) {
AI_DAMAGE = true;
return;
}
if ( bBossBar && spawnArgs.GetBool( "remove_bar_on_death" ) ) {
idPlayer *player = gameLocal.GetLocalPlayer();
if ( player && player->hud ) {
bBossBar = false;
player->hud->HandleNamedEvent("HideProgressBar");
player->hud->StateChanged(gameLocal.time);
}
}
HandleNoGore();
idAI::Killed(inflictor, attacker, damage, dir, location);
fl.noPortal = 0; // CJR: Set so that killed monsters never collide with portals
SendDamageToDDA(); // CJR DDA: Send any accumulated damage to the dda system
// General non-item dropping (for monsters, souls, etc.)
const idKeyValue *kv = NULL;
kv = spawnArgs.MatchPrefix( "def_drops", NULL );
while ( kv ) {
idStr drops = kv->GetValue();
idDict args;
idStr last5 = kv->GetKey().Right(5);
if ( drops.Length() && idStr::Icmp( last5, "Joint" ) != 0) {
args.Set( "classname", drops );
// HUMANHEAD pdm: specify monster so souls can call back to remove body when picked up
args.Set("monsterSpawnedBy", name.c_str());
idVec3 origin;
idMat3 axis;
idStr jointKey = kv->GetKey() + idStr("Joint");
idStr jointName = spawnArgs.GetString( jointKey );
idStr joint2JointKey = kv->GetKey() + idStr("Joint2Joint");
idStr j2jName = spawnArgs.GetString( joint2JointKey );
idEntity *newEnt = NULL;
gameLocal.SpawnEntityDef( args, &newEnt );
HH_ASSERT(newEnt != NULL);
// Spin to correct heading
if(newEnt->IsType(hhMonsterAI::Type)) {
hhMonsterAI *newAI = static_cast<hhMonsterAI*>(newEnt);
newAI->current_yaw = current_yaw;
newAI->ideal_yaw = ideal_yaw;
}
if(jointName.Length()) {
jointHandle_t joint = GetAnimator()->GetJointHandle( jointName );
if (!GetAnimator()->GetJointTransform( joint, gameLocal.time, origin, axis ) ) {
gameLocal.Printf( "%s refers to invalid joint '%s' on entity '%s'\n", (const char*)jointKey.c_str(), (const char*)jointName, (const char*)name );
origin = renderEntity.origin;
axis = renderEntity.axis;
}
axis *= renderEntity.axis;
origin = renderEntity.origin + origin * renderEntity.axis;
newEnt->SetAxis(axis);
newEnt->SetOrigin(origin);
}
else {
newEnt->SetAxis(viewAxis);
newEnt->SetOrigin(GetOrigin());
}
}
kv = spawnArgs.MatchPrefix( "def_drops", kv );
}
}
void hhMonsterAI::Event_Remove( void ) {
if( InVehicle() ) {
GetVehicleInterface()->GetVehicle()->EjectPilot();
}
idAI::Event_Remove();
}
void hhMonsterAI::EnterVehicle( hhVehicle* vehicle ) {
if (!vehicle->WillAcceptPilot(this)) {
return;
}
spawnArgs.Set( "use_aas", spawnArgs.GetString( "aas_shuttle", "aasDroid" ) );
SetAAS();
idAI::EnterVehicle( vehicle );
PostEventSec( &MA_SetVehicleState, 0.1f );
}
bool hhMonsterAI::TestMelee( void ) const {
trace_t trace;
idActor *enemyEnt = enemy.GetEntity();
if ( !enemyEnt || !melee_range ) {
return false;
}
//FIXME: make work with gravity vector
idVec3 org = physicsObj.GetOrigin();
const idBounds &myBounds = physicsObj.GetBounds();
idBounds bounds;
idBounds meleeBounds( spawnArgs.GetVector( "melee_boundmin", "0 0 0" ), spawnArgs.GetVector( "melee_boundmax", "0 0 0" ) );
if ( meleeBounds != bounds_zero ) {
//check custom rotated meleebound
idBox meleeBox( meleeBounds, org, renderEntity.axis );
idBounds enemyBounds = enemyEnt->GetPhysics()->GetBounds();
enemyBounds.TranslateSelf( enemyEnt->GetPhysics()->GetOrigin() );
idBox enemyBox( enemyBounds );
if ( ai_debugMove.GetBool() ) {
gameRenderWorld->DebugBox( colorYellow, meleeBox );
}
if ( !enemyBox.IntersectsBox( meleeBox ) ) {
return false;
}
} else {
// expand the bounds out by our melee range
bounds[0][0] = -melee_range;
bounds[0][1] = -melee_range;
bounds[0][2] = myBounds[0][2] - 4.0f;
bounds[1][0] = melee_range;
bounds[1][1] = melee_range;
bounds[1][2] = myBounds[1][2] + 4.0f;
bounds.TranslateSelf( org );
idVec3 enemyOrg = enemyEnt->GetPhysics()->GetOrigin();
idBounds enemyBounds = enemyEnt->GetPhysics()->GetBounds();
enemyBounds.TranslateSelf( enemyOrg );
if ( ai_debugMove.GetBool() ) {
gameRenderWorld->DebugBounds( colorYellow, bounds, vec3_zero, gameLocal.msec );
}
if ( !bounds.IntersectsBounds( enemyBounds ) ) {
return false;
}
}
idVec3 start = GetEyePosition();
idVec3 end = enemyEnt->GetEyePosition();
gameLocal.clip.TracePoint( trace, start, end, MASK_SHOT_BOUNDINGBOX, this );
if ( ( trace.fraction == 1.0f ) || ( gameLocal.GetTraceEntity( trace ) == enemyEnt ) ) {
return true;
}
return false;
}
bool hhMonsterAI::MoveToPosition( const idVec3 &pos, bool enemyBlocks ) {
if(AI_VEHICLE && InVehicle()) {
GetVehicleInterfaceLocal()->ThrustTowards( pos, 1.0f );
return true;
} else {
return idAI::MoveToPosition( pos, enemyBlocks );
}
}
bool hhMonsterAI::TurnToward( const idVec3 &pos ) {
if (AI_VEHICLE && InVehicle()) {
GetVehicleInterfaceLocal()->OrientTowards( pos, 0.5 );
return true;
} else {
return idAI::TurnToward( pos );
}
}
bool hhMonsterAI::CanSee( idEntity *ent, bool useFov ) {
trace_t tr;
idVec3 eye;
idVec3 toPos;
if ( ent->IsHidden() ) {
return false;
}
if ( ent->IsType( idActor::Type ) ) {
idActor *act = static_cast<idActor*>(ent);
// If this actor is in a vehicle, look at the vehicle, not the actor
if(act->InVehicle()) {
ent = act->GetVehicleInterface()->GetVehicle();
}
}
if ( ent->IsType( idActor::Type ) ) {
toPos = ( ( idActor * )ent )->GetEyePosition();
} else {
toPos = ent->GetPhysics()->GetOrigin();
}
if ( useFov && !CheckFOV( toPos ) ) {
return false;
}
eye = GetEyePosition();
if ( InVehicle() ) {
gameLocal.clip.TracePoint( tr, eye, toPos, MASK_SHOT_BOUNDINGBOX, GetVehicleInterface()->GetVehicle() ); // HUMANHEAD JRM
if ( tr.fraction >= 1.0f || ( gameLocal.GetTraceEntity( tr ) == ent ) ) {
return true;
}
} else {
gameLocal.clip.TracePoint( tr, eye, toPos, MASK_SHOT_BOUNDINGBOX, this ); // HUMANHEAD JRM
if ( tr.fraction >= 1.0f || ( gameLocal.GetTraceEntity( tr ) == ent ) ) {
return true;
} else if ( bSeeThroughPortals && aas ) {
#if 0 // jmarshall - AI portals
shootTarget = NULL;
int myArea = gameRenderWorld->PointInArea( GetOrigin() );
int numPortals = gameRenderWorld->NumGamePortalsInArea( myArea );
if ( numPortals > 0 ) {
int enemyArea = gameRenderWorld->PointInArea( ent->GetOrigin() );
for ( int i=0;i<numPortals;i++ ) {
//if portal's destination area is the same as monster's enemy's area
if ( gameRenderWorld->GetSoundPortal( myArea, i ).areas[0] == enemyArea ) {
//find the portal and set it as this monster's shoottarget
idEntity *spawnedEnt = NULL;
for( spawnedEnt = gameLocal.spawnedEntities.Next(); spawnedEnt != NULL; spawnedEnt = spawnedEnt->spawnNode.Next() ) {
if ( !spawnedEnt->IsType( hhPortal::Type ) ) {
continue;
}
if ( gameRenderWorld->PointInArea( spawnedEnt->GetOrigin() ) == myArea) {
shootTarget = spawnedEnt;
return true;
}
}
}
}
}
#endif
}
}
return false;
}
idPlayer* hhMonsterAI::GetClosestPlayer(void) {
idEntity *closestEnt = NULL;
float closestDist = idMath::INFINITY;
for(int i=0;i<gameLocal.numClients;i++) {
idEntity *ent = gameLocal.entities[i];
if(ent) {
HH_ASSERT(ent->IsType(idPlayer::Type));
float l = (ent->GetOrigin()-GetOrigin()).Length();
if(l < closestDist || !closestEnt) {
closestDist = l;
closestEnt = ent;
}
}
}
return static_cast<idPlayer*>(closestEnt);
}
void hhMonsterAI::Show() {
if ( spawnThinkFlags != 0 ) {
int temp = spawnThinkFlags;
spawnThinkFlags = 0;
BecomeActive( temp );
}
idAI::Show();
PostEventMS(&MA_EnemyOnSpawn, 10);
Event_InitialWallwalk();
}
bool hhMonsterAI::GetFacePosAngle( const idVec3 &pos, float &delta ) {
float diff;
float angle1;
float angle2;
idVec3 sourceOrigin;
idVec3 targetOrigin;
sourceOrigin = GetPhysics()->GetOrigin();
targetOrigin = pos;//target->GetPhysics()->GetOrigin();
angle1 = DEG2RAD( GetGravViewAxis()[0].ToYaw() ); // VIEWAXIS_TO_GETGRAVVIEWAXIS
angle2 = hhUtils::PointToAngle( targetOrigin.x - sourceOrigin.x, targetOrigin.y - sourceOrigin.y );
if(angle2 > angle1) {
diff = angle2 - angle1;
if( diff > DEG2RAD(180.0f) ) {
delta = DEG2RAD(359.9f) - diff;
return false;
}
else {
delta = diff;
return true;
}
}
else {
diff = angle1 - angle2;
if( diff > DEG2RAD(180.0f) ) {
delta = DEG2RAD(359.9f) - diff;
return true;
}
else {
delta = diff;
return false;
}
}
}
void hhMonsterAI::Distracted( idActor *newEnemy ) {
SetEnemy( newEnemy );
}
void hhMonsterAI::SetEnemy( idActor *newEnemy ) {
idAI::SetEnemy( newEnemy );
}
idVec3 hhMonsterAI::GetTouchPos(idEntity *ent, const hhReactionDesc *desc ) {
assert(ent != NULL);
assert(desc != NULL);
idVec3 pos = ent->GetOrigin();
idVec3 offset = desc->touchOffsets.GetVector("all", "0 0 0");
if ( offset == vec3_zero ) {
// Each monster def type has its own offset etc.
// touchoffset_monster_hunter
offset = desc->touchOffsets.GetVector(GetEntityDefName(), "0 0 0");
}
idStr touchDirType = desc->touchDir;
// When touching this ent, move to it as if it were 'cover'
if( touchDirType == idStr("cover") && enemy.IsValid() ) {
idVec3 goalPos = ent->GetOrigin() + offset.x * (ent->GetOrigin() - GetEnemy()->GetOrigin()).ToNormal();
goalPos.z += offset.z;
return goalPos;
}
// Move directly toward this ent from our pos
else if(touchDirType == idStr("direct")) {
idVec3 dir = GetOrigin() - pos;
dir.Normalize();
offset *= dir.ToMat3();
return pos + offset;
}
else if(touchDirType == idStr("object")) {
ent->GetFloorPos( 64.f, pos );
idVec3 dir = GetOrigin() - pos;
dir.Normalize();
return pos + (dir * offset.x);
}
else {
offset *= ent->GetAxis();
return pos + offset;
}
}
bool hhMonsterAI::GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ) {
if ( af.IsActive() ) {
af.GetPhysicsToVisualTransform( origin, axis );
return true;
}
origin = modelOffset;
if ( GetBindMaster() && bindJoint != INVALID_JOINT ) {
idMat3 masterAxis;
idVec3 masterOrigin;
GetMasterPosition( masterOrigin, masterAxis );
axis = physicsObj.localAxis * masterAxis;
origin = masterOrigin + physicsObj.GetLocalOrigin() * masterAxis;
} else if ( ( InVehicle() || bBindOrient ) && GetBindMaster() ) {
axis = GetBindMaster()->GetAxis();
} else {
axis = viewAxis;
}
return true;
}
void hhMonsterAI::UpdateModelTransform( void ) {
idVec3 origin;
idMat3 axis;
if ( GetPhysicsToVisualTransform( origin, axis ) ) {
if ( bBindAxis && GetBindMaster() ) {
renderEntity.axis = GetBindMaster() ->GetAxis();
} else {
renderEntity.axis = axis * GetPhysics()->GetAxis();
}
if ( GetBindMaster() && bindJoint != INVALID_JOINT ) {
if ( head.IsValid() && head->GetPhysics() ) {
head->GetPhysics()->Evaluate(gameLocal.time-gameLocal.previousTime, gameLocal.time);
}
renderEntity.origin = origin;
} else {
if ( GetBindMaster() && head.IsValid() && head->GetPhysics() ) {
head->GetPhysics()->Evaluate(gameLocal.time-gameLocal.previousTime, gameLocal.time);
}
renderEntity.origin = GetPhysics()->GetOrigin() + origin * renderEntity.axis;
}
} else {
renderEntity.axis = GetPhysics()->GetAxis();
renderEntity.origin = GetPhysics()->GetOrigin();
}
}
void hhMonsterAI::UpdateFromPhysics( bool moveBack ) {
// set master delta angles for actors
if ( GetBindMaster() ) {
if( !InVehicle() ) {
idAngles delta = GetDeltaViewAngles();
if ( moveBack ) {
delta.yaw -= physicsObj.GetMasterDeltaYaw();
} else {
delta.yaw += physicsObj.GetMasterDeltaYaw();
}
SetDeltaViewAngles( delta );
} else {
SetAxis( GetBindMaster()->GetAxis() );
}
}
if ( UpdateAnimationControllers() ) {
BecomeActive( TH_ANIMATE );
}
UpdateVisuals();
}
void hhMonsterAI::CreateHealthTriggers() {
const char keyPrefix[] = "health_percent_trigger_";
int keyPrefixLen = strlen(keyPrefix);
const idKeyValue *kv = spawnArgs.MatchPrefix(keyPrefix, NULL);
healthTriggers.Clear();
while(kv) {
hhMonsterHealthTrigger t;
idStr k = kv->GetKey();
idStr perct = k.Right(k.Length() - strlen(keyPrefix));
float p = float(atoi(perct.c_str())) * 0.01f;
t.healthThresh = int(float(health) * p);
t.triggerEnt = gameLocal.FindEntity(kv->GetValue().c_str());
if(!t.triggerEnt.GetEntity()) {
gameLocal.Warning("%s specified %s key with entity \"%s\", but that entity does not exist!", name.c_str(), kv->GetKey().c_str(), kv->GetValue().c_str());
}
healthTriggers.Append(t);
kv = spawnArgs.MatchPrefix(keyPrefix, kv);
}
}
void hhMonsterAI::UpdateHealthTriggers(int oldHealth, int currHealth) {
// Trigger them
for(int i=0;i<healthTriggers.Num();i++) {
// For now, only trigger them ONCE, if scripters want something else, we can change later
if(healthTriggers[i].triggerCount <= 0 && currHealth < healthTriggers[i].healthThresh) {
if(healthTriggers[i].triggerEnt.GetEntity()) {
healthTriggers[i].triggerEnt.GetEntity()->ProcessEvent(&EV_Activate, this);
healthTriggers[i].triggerCount++;
}
}
}
}
//
// Damage()
//
void hhMonsterAI::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) {
// Return if we don't take damage
if ( !fl.takedamage ) {
return;
}
if ( spawnArgs.GetBool( "noPlayerDamage", "0" ) ) {
if ( attacker && attacker->IsType( idPlayer::Type ) ) {
return;
}
}
int oldHealth = health;
idAI::Damage(inflictor, attacker, dir, damageDefName, damageScale, location);
UpdateHealthTriggers(oldHealth, health);
if ( bCustomBlood && inflictor ) {
hhFxInfo fxInfo;
fxInfo.RemoveWhenDone( true );
BroadcastFxInfoPrefixed( "fx_custom_blood", inflictor->GetOrigin(), mat3_identity, &fxInfo );
}
const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName );
if ( AI_DEAD && damageDef ) {
if ( damageDef->GetBool( "ice" ) && spawnArgs.GetBool( "can_freeze", "0" ) ) {
SetSkinByName( spawnArgs.GetString( "skin_freeze" ) );
if( af.IsLoaded() ) {
af.GetPhysics()->SetSelfCollision(false);
af.GetPhysics()->SetContactFrictionScale(0);
af.GetPhysics()->SetTimeScaleRamp(0,2);
for(int i=0; i < af.GetPhysics()->GetNumConstraints(); i++) {
idAFConstraint* constraint = af.GetPhysics()->GetConstraint(i);
switch( constraint->GetType() ) {
case CONSTRAINT_BALLANDSOCKETJOINT:
static_cast<idAFConstraint_BallAndSocketJoint*>(constraint)->SetFriction(200.0f);
static_cast<idAFConstraint_BallAndSocketJoint*>(constraint)->SetNoLimit();
break;
case CONSTRAINT_UNIVERSALJOINT:
static_cast<idAFConstraint_UniversalJoint*>(constraint)->SetFriction(200.0f);
static_cast<idAFConstraint_UniversalJoint*>(constraint)->SetNoLimit();
break;
case CONSTRAINT_HINGE:
static_cast<idAFConstraint_Hinge*>(constraint)->SetFriction(200.0f);
static_cast<idAFConstraint_Hinge*>(constraint)->SetNoLimit();
break;
}
}
}
spawnArgs.Set("fx_deatheffect", spawnArgs.GetString( "fx_ice" ));
spawnArgs.Set("produces_splats", "0");
if( modelDefHandle > 0 )
gameRenderWorld->RemoveDecals( modelDefHandle );
//SetDeformation(DEFORMTYPE_DEATHEFFECT, gameLocal.time + 5000, 12000);
SetShaderParm(SHADERPARM_TIME_OF_DEATH, MS2SEC(gameLocal.time+100000));
CancelEvents( &EV_Dispose );
PostEventSec( &EV_Dispose, 5 );
}
if ( damageDef->GetBool( "burn" ) && !spawnArgs.GetBool( "no_burn" ) ) {
SetSkinByName( spawnArgs.GetString( "skin_burn" ) );
spawnArgs.Set("fx_deatheffect", spawnArgs.GetString( "fx_burn" ));
if( modelDefHandle > 0 )
gameRenderWorld->RemoveDecals( modelDefHandle );
CancelEvents( &EV_Dispose );
PostEventSec( &EV_Dispose, 0 );
}
if ( damageDef->GetBool( "acid" ) && spawnArgs.GetBool( "acidburn" ) ) {
SetSkinByName( spawnArgs.GetString( "skin_acidburn" ) );
SetDeformation(DEFORMTYPE_DEATHEFFECT, gameLocal.time + 2500, 8000); // starttime, duration
PostEventSec( &EV_StartSound, 1.5f, "snd_acid", SND_CHANNEL_ANY, 1 );
spawnArgs.Set("fx_deatheffect", spawnArgs.GetString( "fx_acid" ));
spawnArgs.Set("mtr_splat1", spawnArgs.GetString( "mtr_acidsplat" ));
spawnArgs.Delete("mtr_splat2");
spawnArgs.Delete("mtr_splat3");
spawnArgs.Delete("mtr_splat4");
spawnArgs.Set("keepDecals", "1");
CancelEvents( &EV_Dispose );
PostEventSec( &EV_Dispose, 1.5 );
}
}
}
bool hhMonsterAI::NearEnoughTouchPos( idEntity* ent, const idVec3& targetPos, idBounds& bounds ) {
bounds.TranslateSelf( targetPos );
idBounds bnds( idVec3(-16.f, -16.f, -8.f), idVec3(16.f, 16.f, 64.f) );
bnds.TranslateSelf( physicsObj.GetOrigin() );
//uncomment these to show debug lines
// gameRenderWorld->DebugBounds( colorOrange, bounds );
// gameRenderWorld->DebugArrow( colorRed, targetPos, targetPos + idVec3(0, 0, 10), 5 );
// gameRenderWorld->DebugBounds( colorRed, bnds );
if( bnds.IntersectsBounds( bounds ) ) {
return true;
}
return false;
}
bool hhMonsterAI::GetTouchPosBound( const hhReactionDesc *desc, idBounds & bnds ) {
idStr minName, maxName;
idVec3 min, max;
minName = va("min_%s", GetEntityDefName());
maxName = va("max_%s", GetEntityDefName());
if( !desc->touchOffsets.GetVector(minName.c_str(), "0 0 0", min) || !desc->touchOffsets.GetVector(maxName.c_str(), "0 0 0", max) ) {
return false;
}
min -= idVec3( 16.f, 16.f, 8.f );
max += idVec3( 16.f, 16.f, 64.f );
bnds.Clear();
bnds[ 0 ] = min;
bnds[ 1 ] = max;
return true;
}
int hhMonsterAI::EvaluateReaction( const hhReaction *react ) {
// Volume/Distance
// If a volume in specified, then use that as our spatial check
float distSq = ( react->causeEntity->GetOrigin() - GetOrigin() ).LengthSqr();
if( react->desc->listenerVolumes.Num() > 0 ) {
int count = 0;
int i;
for( i = 0; i < react->desc->listenerVolumes.Num(); i++ ) {
assert( react->desc->listenerVolumes[ i ]->GetPhysics() != NULL );
assert( GetPhysics() != NULL );
if( react->desc->listenerVolumes[ i ]->GetPhysics()->GetAbsBounds().IntersectsBounds( GetPhysics()->GetAbsBounds() ) ) {
//MDC-TODO: Draw bounds debugging info...
count++; //one is good enough to continue - no need to check the rest
break;
}
}
// If we are in ZERO of the listener volumes, then we can bail because this reaction doesn't apply to us
if( count == 0 ) {
return 0;
}
}
if ( react->desc->effectVolumes.Num() > 0 && GetEnemy() ) {
int count = 0;
for( int i = 0; i < react->desc->effectVolumes.Num(); i++ ) {
assert( react->desc->effectVolumes[ i ]->GetPhysics() != NULL );
assert( GetPhysics() != NULL );
if( react->desc->effectVolumes[ i ]->GetPhysics()->GetAbsBounds().IntersectsBounds( GetEnemy()->GetPhysics()->GetAbsBounds() ) ) {
count++; //one is good enough to return false - no need to check the rest
break;
}
}
// If enemy is in ZERO of the effect volumes, then we can bail because this reaction doesn't apply to us
if( count == 0 ) {
return 0;
}
}
if( react->desc->listenerRadius > 0.f || react->desc->listenerMinRadius > 0.f ) {
// TOO FAR
if( react->desc->listenerRadius > 0.f && distSq > react->desc->listenerRadius * react->desc->listenerRadius ) {
return 0;
}
// TOO CLOSE
if( react->desc->listenerMinRadius > 0.f && distSq < react->desc->listenerMinRadius * react->desc->listenerMinRadius ) {
return 0;
}
}
// Path-finding
if( react->desc->CauseRequiresPathfinding() ) {
if( !HasPathTo( GetTouchPos(react->causeEntity.GetEntity(), react->desc ) ) ) {
//MDC-TODO: draw path debugging
return 0;
}
else {
//MDC-TODO: draw path debugging
}
}
// Can-See
if( react->desc->flags & hhReactionDesc::flagReq_CanSee ) {
if( react->causeEntity.IsValid() ) {
if( !CanSee(react->causeEntity.GetEntity(), TRUE) ) {
return 0;
}
}
}
return 100;
}
int hhMonsterAI::ReactionTo( const idEntity *ent ) {
if ( bNoCombat ) {
return ATTACK_IGNORE;
}
const idActor *actor = static_cast<const idActor *>( ent );
if( actor && actor->IsType(hhDeathProxy::Type) ) {
return ATTACK_IGNORE;
}
//only attack spiritwalking players if they hurt me
if ( ent->IsType( hhPlayer::Type ) ) {
const hhPlayer *player = static_cast<const hhPlayer*>( ent );
if ( nextSpiritProxyCheck == 0 && player && player->IsSpiritWalking() ) {
nextSpiritProxyCheck = gameLocal.time + SEC2MS(2);
return ATTACK_ON_DAMAGE;
}
}
if ( ent->IsType( hhSpiritProxy::Type ) ) {
if ( gameLocal.time > nextSpiritProxyCheck ) {
nextSpiritProxyCheck = 0;
//attack spiritproxy on sight if we have no enemy or if its closer than our current enemy
if ( enemy.IsValid() && enemy->IsType( hhPlayer::Type ) ) {
float distToEnemy = (enemy->GetOrigin() - GetOrigin()).LengthSqr();
float distToProxy = (ent->GetOrigin() - GetOrigin()).LengthSqr();
if ( distToProxy < distToEnemy ) {
return ATTACK_ON_SIGHT;
} else {
//HUMANHEAD jsh PCF 4/29/06 fixed logic error with spiritproxy checking
return ATTACK_IGNORE;
}
} else {
return ATTACK_ON_SIGHT;
}
} else {
return ATTACK_IGNORE;
}
}
if ( ent->IsType( hhMonsterAI::Type ) ) {
const hhMonsterAI *entAI = static_cast<const hhMonsterAI *>( ent );
if ( entAI && entAI->bNeverTarget ) {
return ATTACK_IGNORE;
}
}
return idAI::ReactionTo( ent );
}
bool hhMonsterAI::UpdateAnimationControllers( void ) {
idVec3 local;
idVec3 focusPos;
idVec3 left;
idVec3 dir;
idVec3 orientationJointPos;
idVec3 localDir;
idAngles newLookAng;
idAngles diff;
idMat3 mat;
idMat3 axis;
idMat3 orientationJointAxis;
idAFAttachment *headEnt = head.GetEntity();
idVec3 eyepos;
idVec3 pos;
int i;
idAngles jointAng;
float orientationJointYaw;
if ( AI_DEAD ) {
return idActor::UpdateAnimationControllers();
}
if ( orientationJoint == INVALID_JOINT ) {
orientationJointAxis = viewAxis;
orientationJointPos = physicsObj.GetOrigin();
orientationJointYaw = current_yaw;
} else {
GetJointWorldTransform( orientationJoint, gameLocal.time, orientationJointPos, orientationJointAxis );
orientationJointYaw = orientationJointAxis[ 2 ].ToYaw();
orientationJointAxis = idAngles( 0.0f, orientationJointYaw, 0.0f ).ToMat3();
}
if ( ai_debugBrain.GetBool() ) {
gameRenderWorld->DebugArrow( colorCyan, orientationJointPos, orientationJointPos + orientationJointAxis[0] * 64.0, 10, 1 );
}
if ( focusJoint != INVALID_JOINT ) {
if ( headEnt ) {
headEnt->GetJointWorldTransform( focusJoint, gameLocal.time, eyepos, axis );
} else {
// JRMMERGE_GRAVAXIS - What about GetGravAxis() are we still using/needing that?
GetJointWorldTransform( focusJoint, gameLocal.time, eyepos, axis );
}
eyeOffset.z = eyepos.z - physicsObj.GetOrigin().z;
} else {
eyepos = GetEyePosition();
}
if ( headEnt ) {
CopyJointsFromBodyToHead();
}
// Update the IK after we've gotten all the joint positions we need, but before we set any joint positions.
// Getting the joint positions causes the joints to be updated. The IK gets joint positions itself (which
// are already up to date because of getting the joints in this function) and then sets their positions, which
// forces the heirarchy to be updated again next time we get a joint or present the model. If IK is enabled,
// or if we have a seperate head, we end up transforming the joints twice per frame. Characters with no
// head entity and no ik will only transform their joints once. Set g_debuganim to the current entity number
// in order to see how many times an entity transforms the joints per frame.
idActor::UpdateAnimationControllers();
idEntity *focusEnt = focusEntity.GetEntity();
//HUMANHEAD jsh allow eyefocus independent from allowJointMod
if ( ( !allowJointMod && !allowEyeFocus ) || ( gameLocal.time >= focusTime && focusTime != -1 ) || GetPhysics()->GetGravityNormal() != idVec3( 0,0,-1) ) {
focusPos = GetEyePosition() + orientationJointAxis[ 0 ] * 512.0f;
} else if ( focusEnt == NULL ) {
// keep looking at last position until focusTime is up
focusPos = currentFocusPos;
} else if ( focusEnt == enemy.GetEntity() ) {
focusPos = lastVisibleEnemyPos + lastVisibleEnemyEyeOffset - eyeVerticalOffset * enemy.GetEntity()->GetPhysics()->GetGravityNormal();
} else if ( focusEnt->IsType( idActor::Type ) ) {
focusPos = static_cast<idActor *>( focusEnt )->GetEyePosition() - eyeVerticalOffset * focusEnt->GetPhysics()->GetGravityNormal();
} else {
focusPos = focusEnt->GetPhysics()->GetOrigin();
}
currentFocusPos = currentFocusPos + ( focusPos - currentFocusPos ) * eyeFocusRate;
// determine yaw from origin instead of from focus joint since joint may be offset, which can cause us to bounce between two angles
dir = focusPos - orientationJointPos;
newLookAng.yaw = idMath::AngleNormalize180( dir.ToYaw() - orientationJointYaw );
newLookAng.roll = 0.0f;
newLookAng.pitch = 0.0f;
newLookAng += lookOffset;
#if 0
gameRenderWorld->DebugLine( colorRed, orientationJointPos, focusPos, gameLocal.msec );
gameRenderWorld->DebugLine( colorYellow, orientationJointPos, orientationJointPos + orientationJointAxis[ 0 ] * 32.0f, gameLocal.msec );
gameRenderWorld->DebugLine( colorGreen, orientationJointPos, orientationJointPos + newLookAng.ToForward() * 48.0f, gameLocal.msec );
#endif
//JRMMERGE_GRAVAXIS: This changed to much to merge, see if you can get your monsters on planets changes back in here. I'll leave both versions
#if OLD_CODE
GetGravViewAxis().ProjectVector( dir, localDir ); // HUMANHEAD JRM: VIEWAXIS_TO_GETGRAVVIEWAXIS
lookAng.yaw = idMath::AngleNormalize180( localDir.ToYaw() );
lookAng.pitch = -idMath::AngleNormalize180( localDir.ToPitch() );
lookAng.roll = 0.0f;
#else
// determine pitch from joint position
dir = focusPos - eyepos;
if ( ai_debugBrain.GetBool() ) {
gameRenderWorld->DebugArrow( colorYellow, eyepos, eyepos + dir, 10, 1 );
}
dir.NormalizeFast();
orientationJointAxis.ProjectVector( dir, localDir );
newLookAng.pitch = -idMath::AngleNormalize180( localDir.ToPitch() ) + lookOffset.pitch;
newLookAng.roll = 0.0f;
#endif
diff = newLookAng - lookAng;
if ( eyeAng != diff ) {
eyeAng = diff;
eyeAng.Clamp( eyeMin, eyeMax );
idAngles angDelta = diff - eyeAng;
if ( !angDelta.Compare( ang_zero, 0.1f ) ) {
alignHeadTime = gameLocal.time;
} else {
alignHeadTime = gameLocal.time + ( 0.5f + 0.5f * gameLocal.random.RandomFloat() ) * focusAlignTime;
}
}
if ( idMath::Fabs( newLookAng.yaw ) < 0.1f ) {
alignHeadTime = gameLocal.time;
}
if ( ( gameLocal.time >= alignHeadTime ) || ( gameLocal.time < forceAlignHeadTime ) ) {
alignHeadTime = gameLocal.time + ( 0.5f + 0.5f * gameLocal.random.RandomFloat() ) * focusAlignTime;
destLookAng = newLookAng;
destLookAng.Clamp( lookMin, lookMax );
}
diff = destLookAng - lookAng;
if ( ( lookMin.pitch == -180.0f ) && ( lookMax.pitch == 180.0f ) ) {
if ( ( diff.pitch > 180.0f ) || ( diff.pitch <= -180.0f ) ) {
diff.pitch = 360.0f - diff.pitch;
}
}
if ( ( lookMin.yaw == -180.0f ) && ( lookMax.yaw == 180.0f ) ) {
if ( diff.yaw > 180.0f ) {
diff.yaw -= 360.0f;
} else if ( diff.yaw <= -180.0f ) {
diff.yaw += 360.0f;
}
}
lookAng = lookAng + diff * headFocusRate;
lookAng.Normalize180();
jointAng.roll = 0.0f;
if ( allowJointMod ) {
for( i = 0; i < lookJoints.Num(); i++ ) {
jointAng.pitch = 0;
jointAng.yaw = lookAng.yaw * lookJointAngles[ i ].yaw;
animator.SetJointAxis( lookJoints[ i ], JOINTMOD_WORLD, jointAng.ToMat3() );
idMat3 pitchRot;
hhMath::BuildRotationMatrix( DEG2RAD(-lookAng.pitch * lookJointAngles[ i ].pitch), 0, pitchRot );
animator.GetJointLocalTransform( lookJoints[ i ], gameLocal.time, pos, mat );
animator.SetJointAxis( lookJoints[ i ], JOINTMOD_LOCAL_OVERRIDE, pitchRot * mat );
}
}
if ( move.moveType == MOVETYPE_FLY ) {
// lean into turns
AdjustFlyingAngles();
}
if ( headEnt ) {
idAnimator *headAnimator = headEnt->GetAnimator();
// HUMANHEAD pdm: Added support for look joints in head entities
if ( allowJointMod ) {
for( i = 0; i < headLookJoints.Num(); i++ ) {
jointAng.pitch = lookAng.pitch * headLookJointAngles[ i ].pitch;
jointAng.yaw = lookAng.yaw * headLookJointAngles[ i ].yaw;
headAnimator->SetJointAxis( headLookJoints[ i ], JOINTMOD_WORLD, jointAng.ToMat3() );
}
}
// HUMANHEAD END
if ( allowEyeFocus ) {
idMat3 eyeAxis = ( lookAng + eyeAng ).ToMat3(); idMat3 headTranspose = headEnt->GetPhysics()->GetAxis().Transpose();
axis = eyeAxis * orientationJointAxis;
left = axis[ 1 ] * eyeHorizontalOffset;
eyepos -= headEnt->GetPhysics()->GetOrigin();
headAnimator->SetJointPos( leftEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos + ( axis[ 0 ] * 64.0f + left ) * headTranspose );
headAnimator->SetJointPos( rightEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos + ( axis[ 0 ] * 64.0f - left ) * headTranspose );
//if ( ai_debugMove.GetBool() ) {
// gameRenderWorld->DebugLine( colorRed, orientationJointPos, eyepos + ( axis[ 0 ] * 64.0f + left ) * headTranspose, gameLocal.msec );
//}
} else {
headEnt->BecomeActive( TH_ANIMATE );
headAnimator->ClearJoint( leftEyeJoint );
headAnimator->ClearJoint( rightEyeJoint );
}
} else {
if ( allowEyeFocus ) {
idMat3 eyeAxis = ( lookAng + eyeAng ).ToMat3();
axis = eyeAxis * orientationJointAxis;
left = axis[ 1 ] * eyeHorizontalOffset;
eyepos += axis[ 0 ] * 64.0f - physicsObj.GetOrigin();
animator.SetJointPos( leftEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos + left );
animator.SetJointPos( rightEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos - left );
} else {
animator.ClearJoint( leftEyeJoint );
animator.ClearJoint( rightEyeJoint );
}
}
//HUMANHEAD pdm jawflap
hhAnimator *theAnimator;
if (head.IsValid()) {
theAnimator = head->GetAnimator();
}
else {
theAnimator = GetAnimator();
}
JawFlap(theAnimator);
//END HUMANHEAD
return true;
}
//overridden to allow attack nodes farther away than our current enemydistance
void hhMonsterAI::Event_GetCombatNode( void ) {
int i;
float dist;
idEntity *targetEnt;
idCombatNode *node;
float bestDist;
idCombatNode *bestNode;
idActor *enemyEnt = enemy.GetEntity();
if ( !targets.Num() ) {
// no combat nodes
idThread::ReturnEntity( NULL );
return;
}
if ( !enemyEnt || !EnemyPositionValid() ) {
// don't return a combat node if we don't have an enemy or
// if we can see he's not in the last place we saw him
idThread::ReturnEntity( NULL );
return;
}
// find the closest attack node that can see our enemy and is closer than our enemy
bestNode = NULL;
const idVec3 &myPos = physicsObj.GetOrigin();
bestDist = 9999999.0f ;
for( i = 0; i < targets.Num(); i++ ) {
targetEnt = targets[ i ].GetEntity();
if ( !targetEnt || !targetEnt->IsType( idCombatNode::Type ) ) {
continue;
}
node = static_cast<idCombatNode *>( targetEnt );
if ( !node->IsDisabled() && node->EntityInView( enemyEnt, lastVisibleEnemyPos ) ) {
idVec3 org = node->GetPhysics()->GetOrigin();
dist = ( myPos - org ).LengthSqr();
if ( dist < bestDist ) {
bestNode = node;
bestDist = dist;
}
}
}
idThread::ReturnEntity( bestNode );
}
void hhMonsterAI::FlyTurn( void ) { //overridden for vehicle code changes
if ( move.moveCommand == MOVE_FACE_ENEMY ) {
TurnToward( lastVisibleEnemyPos );
} else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) {
TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() );
} else if ( move.speed > 0.0f ) {
const idVec3 &vel = physicsObj.GetLinearVelocity();
if ( vel.ToVec2().LengthSqr() > 0.1f ) {
if ( InVehicle() ) {
if ( move.goalEntity.IsValid() ) {
TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() );
} else {
TurnToward( GetVehicleInterface()->GetVehicle()->GetPhysics()->GetLinearVelocity().ToYaw() );
}
} else {
if ( vel.ToVec2().LengthSqr() > 0.1f ) {
TurnToward( vel.ToYaw() );
}
}
}
}
Turn();
}
//overridden to allow custom hearingRange
void hhMonsterAI::UpdateEnemyPosition( void ) {
idActor *enemyEnt = enemy.GetEntity();
int enemyAreaNum;
int areaNum;
aasPath_t path;
predictedPath_t predictedPath;
idVec3 enemyPos;
bool onGround;
if ( !enemyEnt ) {
return;
}
const idVec3 &org = physicsObj.GetOrigin();
if ( move.moveType == MOVETYPE_FLY ) {
enemyPos = enemyEnt->GetPhysics()->GetOrigin();
onGround = true;
} else {
onGround = enemyEnt->GetFloorPos( 64.0f, enemyPos );
if ( enemyEnt->OnLadder() ) {
onGround = false;
}
}
if ( onGround ) {
// when we don't have an AAS, we can't tell if an enemy is reachable or not,
// so just assume that he is.
if ( !aas ) {
enemyAreaNum = 0;
lastReachableEnemyPos = enemyPos;
} else {
enemyAreaNum = PointReachableAreaNum( enemyPos, 1.0f );
if ( enemyAreaNum ) {
areaNum = PointReachableAreaNum( org );
if ( PathToGoal( path, areaNum, org, enemyAreaNum, enemyPos ) ) {
lastReachableEnemyPos = enemyPos;
}
}
}
}
AI_ENEMY_IN_FOV = false;
AI_ENEMY_VISIBLE = false;
if ( CanSee( enemyEnt, false ) ) {
AI_ENEMY_VISIBLE = true;
if ( CheckFOV( enemyEnt->GetPhysics()->GetOrigin() ) ) {
AI_ENEMY_IN_FOV = true;
}
SetEnemyPosition();
} else {
// check if we heard any sounds in the last frame
if ( enemyEnt == gameLocal.GetAlertEntity() ) {
float dist = ( enemyEnt->GetPhysics()->GetOrigin() - org ).LengthSqr();
//allow the sound's own radius to override hearingRange, if it is set
if ( gameLocal.lastAIAlertRadius ) {
if ( dist < Square( gameLocal.lastAIAlertRadius ) ) {
SetEnemyPosition();
}
} else if ( dist < Square( hearingRange ) ) {
SetEnemyPosition();
}
}
}
if ( ai_debugMove.GetBool() ) {
gameRenderWorld->DebugBounds( colorLtGrey, enemyEnt->GetPhysics()->GetBounds(), lastReachableEnemyPos, gameLocal.msec );
gameRenderWorld->DebugBounds( colorWhite, enemyEnt->GetPhysics()->GetBounds(), lastVisibleReachableEnemyPos, gameLocal.msec );
}
}
void hhMonsterAI::BecameBound(hhBindController *b) {
SetState( GetScriptFunction( "state_Nothing" ) );
}
void hhMonsterAI::BecameUnbound(hhBindController *b) {
if ( health > 0 ) {
SetState( GetScriptFunction( "state_Idle" ) );
}
}
void hhMonsterAI::CrashLand( const idVec3 &oldOrigin, const idVec3 &oldVelocity ) {
const trace_t& trace = physicsObj.GetGroundTrace();
if ( af.IsActive() || (!physicsObj.HasGroundContacts() || trace.fraction == 1.0f) && !IsBound() ) {
return;
}
//aob - only check when we land on the ground
//If we get here we can assume we currently have ground contacts
if( physicsObj.HadGroundContacts() ) {
return;
}
// if the monster wasn't going down
if ( ( oldVelocity * -physicsObj.GetGravityNormal() ) >= 0.0f ) {
return;
}
idVec3 deltaVelocity = DetermineDeltaCollisionVelocity( oldVelocity, trace );
float delta = (IsBound()) ? deltaVelocity.Length() : deltaVelocity * physicsObj.GetGravityNormal();
if ( delta < spawnArgs.GetFloat( "fatal_fall_velocity", "900" ) ) {
return; // Early out
}
if( trace.fraction == 1.0f ) {
return;
}
Damage( NULL, NULL, oldVelocity.ToNormal(), "damage_monsterfall", 1, INVALID_JOINT );
}
hhEntityFx* hhMonsterAI::SpawnFxLocal( const char *fxName, const idVec3 &origin, const idMat3& axis, const hhFxInfo* const fxInfo, bool forceClient ) {
//overridden to use GetJointWorldTransform to set location if binding to a bone
idDict fxArgs;
hhEntityFx * fx = NULL;
if ( g_skipFX.GetBool() ) {
return NULL;
}
if( !fxName || !fxName[0] ) {
return NULL;
}
// Spawn an fx
fxArgs.Set( "fx", fxName );
fxArgs.SetBool( "start", fxInfo ? fxInfo->StartIsSet() : true );
fxArgs.SetVector( "origin", origin );
fxArgs.SetMatrix( "rotation", axis );
//HUMANHEAD: aob
if( fxInfo ) {
fxArgs.SetBool( "removeWhenDone", fxInfo->RemoveWhenDone() );
fxArgs.SetBool( "onlyVisibleInSpirit", fxInfo->OnlyVisibleInSpirit() ); // CJR
fxArgs.SetBool( "onlyInvisibleInSpirit", fxInfo->OnlyInvisibleInSpirit() ); // tmj
fxArgs.SetBool( "toggle", fxInfo->Toggle() );
}
//HUMANHEAD END
//HUMANHEAD rww - use forceClient
if (forceClient) {
//this can happen on the "server" in the case of listen servers as well
fx = (hhEntityFx *)gameLocal.SpawnClientObject( "func_fx", &fxArgs );
}
else {
assert(!gameLocal.isClient);
fx = (hhEntityFx *)gameLocal.SpawnObject( "func_fx", &fxArgs );
}
if( fxInfo ) {
fx->SetFxInfo( *fxInfo );
}
if( fxInfo && fxInfo->EntityIsSet() ) {
fx->fl.noRemoveWhenUnbound = fxInfo->NoRemoveWhenUnbound();
if( fxInfo->BindBoneIsSet() ) {
idVec3 bonePos;
idMat3 boneAxis;
GetJointWorldTransform( fxInfo->GetBindBone(), bonePos, boneAxis );
fx->SetOrigin( bonePos );
fx->BindToJoint( fxInfo->GetEntity(), fxInfo->GetBindBone(), true );
} else if( fx && fx->Joint() && *fx->Joint() ) {
fx->MoveToJoint( fxInfo->GetEntity(), fx->Joint() );
fx->BindToJoint( fxInfo->GetEntity(), fx->Joint(), true );
} else {
fx->Bind( fxInfo->GetEntity(), true );
}
}
fx->Show();
return fx;
}
bool hhMonsterAI::AttackMelee( const char *meleeDefName ) {
const idDict *meleeDef;
idActor *enemyEnt = enemy.GetEntity();
const char *p;
const idSoundShader *shader;
meleeDef = gameLocal.FindEntityDefDict( meleeDefName, false );
if ( !meleeDef ) {
gameLocal.Error( "Unknown melee '%s'", meleeDefName );
}
if ( !enemyEnt ) {
p = meleeDef->GetString( "snd_miss" );
if ( p && *p ) {
shader = declManager->FindSound( p );
StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL );
}
return false;
}
// make sure the trace can actually hit the enemy
if ( !TestMeleeDef(meleeDefName) ) {
// missed
p = meleeDef->GetString( "snd_miss" );
if ( p && *p ) {
shader = declManager->FindSound( p );
StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL );
}
return false;
}
//
// do the damage
//
p = meleeDef->GetString( "snd_hit" );
if ( p && *p ) {
shader = declManager->FindSound( p );
StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL );
}
idVec3 kickDir;
meleeDef->GetVector( "kickDir", "0 0 0", kickDir );
idVec3 globalKickDir;
globalKickDir = ( viewAxis * physicsObj.GetGravityAxis() ) * kickDir;
if ( spawnArgs.GetBool( "smart_knockback", "0" ) ) {
//do some traces to determine which dir to kick enemy
idAngles ang = viewAxis.ToAngles();
idVec3 forward, right, up;
ang.ToVectors( &forward, &right, &up );
trace_t trace;
gameLocal.clip.TraceBounds( trace, enemyEnt->GetOrigin(), enemyEnt->GetOrigin() + -forward * 400, enemyEnt->GetPhysics()->GetBounds(), MASK_SOLID, this );
if ( trace.fraction != 1.0f ) {
//there's room to kick the player back
globalKickDir = -forward;
if ( ai_debugMove.GetBool() ) {
gameRenderWorld->DebugArrow( colorRed, enemyEnt->GetOrigin(), enemyEnt->GetOrigin() + globalKickDir * 500, 10, 10000 );
}
} else {
gameLocal.clip.TraceBounds( trace, enemyEnt->GetOrigin(), enemyEnt->GetOrigin() + right * 400, enemyEnt->GetPhysics()->GetBounds(), MASK_SOLID, this );
if ( trace.fraction != 1.0f ) {
globalKickDir = right;
if ( ai_debugMove.GetBool() ) {
gameRenderWorld->DebugArrow( colorRed, enemyEnt->GetOrigin(), enemyEnt->GetOrigin() + globalKickDir * 500, 10, 10000 );
}
} else {
gameLocal.clip.TraceBounds( trace, enemyEnt->GetOrigin(), enemyEnt->GetOrigin() + right * 400, enemyEnt->GetPhysics()->GetBounds(), MASK_SOLID, this );
if ( trace.fraction != 1.0f ) {
globalKickDir = -right;
if ( ai_debugMove.GetBool() ) {
gameRenderWorld->DebugArrow( colorRed, enemyEnt->GetOrigin(), enemyEnt->GetOrigin() + globalKickDir * 500, 10, 10000 );
}
}
}
}
}
enemyEnt->Damage( this, this, globalKickDir, meleeDefName, 1.0f, INVALID_JOINT );
//swap skin if we have successfully hit
idStr hitSkin = spawnArgs.GetString( "skin_melee_hit", "" );
if ( hitSkin.Length() ) {
SetSkinByName( hitSkin.c_str() );
}
lastAttackTime = gameLocal.time;
return true;
}
bool hhMonsterAI::TestMeleeDef( const char *meleeDefName ) const {
//tests using "melee_boundmin" and "melee_boundmax" in the damage def
trace_t trace;
idActor *enemyEnt = enemy.GetEntity();
const idDict *meleeDef;
meleeDef = gameLocal.FindEntityDefDict( meleeDefName, false );
if ( !meleeDef ) {
gameLocal.Error( "Unknown melee '%s'", meleeDefName );
}
if ( !enemyEnt || !melee_range ) {
return false;
}
//FIXME: make work with gravity vector
idVec3 org = physicsObj.GetOrigin();
const idBounds &myBounds = physicsObj.GetBounds();
idBounds bounds;
idBounds meleeBounds( meleeDef->GetVector( "melee_boundmin" ), meleeDef->GetVector( "melee_boundmax" ) );
if ( meleeBounds != bounds_zero ) {
//check custom rotated meleebound
idBox meleeBox( meleeBounds, org, renderEntity.axis );
idBounds enemyBounds = enemyEnt->GetPhysics()->GetBounds();
enemyBounds.TranslateSelf( enemyEnt->GetPhysics()->GetOrigin() );
idBox enemyBox( enemyBounds );
if ( ai_debugMove.GetBool() ) {
gameRenderWorld->DebugBox( colorYellow, meleeBox, 5000 );
}
if ( !enemyBox.IntersectsBox( meleeBox ) ) {
return false;
}
} else {
// expand the bounds out by our melee range
bounds[0][0] = -melee_range;
bounds[0][1] = -melee_range;
bounds[0][2] = myBounds[0][2] - 4.0f;
bounds[1][0] = melee_range;
bounds[1][1] = melee_range;
bounds[1][2] = myBounds[1][2] + 4.0f;
bounds.TranslateSelf( org );
idVec3 enemyOrg = enemyEnt->GetPhysics()->GetOrigin();
idBounds enemyBounds = enemyEnt->GetPhysics()->GetBounds();
enemyBounds.TranslateSelf( enemyOrg );
if ( ai_debugMove.GetBool() ) {
gameRenderWorld->DebugBounds( colorYellow, bounds, vec3_zero, gameLocal.msec );
}
if ( !bounds.IntersectsBounds( enemyBounds ) ) {
return false;
}
}
idVec3 start = GetEyePosition();
idVec3 end = enemyEnt->GetEyePosition();
gameLocal.clip.TracePoint( trace, start, end, MASK_SHOT_BOUNDINGBOX, this );
if ( ( trace.fraction == 1.0f ) || ( gameLocal.GetTraceEntity( trace ) == enemyEnt ) ) {
return true;
}
return false;
}
void hhMonsterAI::PrintDebug() {
if ( enemy.IsValid() ) {
int dist = int(( GetPhysics()->GetOrigin() - enemy->GetPhysics()->GetOrigin() ).LengthFast());
gameLocal.Printf( " Enemy: %s\n", enemy->GetName() );
gameLocal.Printf( " Distance to Enemy: %d\n", dist );
gameLocal.Printf( " Enemy Area: %i\n", PointReachableAreaNum(enemy->GetOrigin()) );
gameLocal.Printf( " Enemy Reachable: %s\n", AI_ENEMY_REACHABLE ? "yes" : "no" );
gameLocal.Printf( " Enemy Visible: %s\n", AI_ENEMY_VISIBLE ? "yes" : "no" );
} else {
gameLocal.Printf( " Enemy: None\n" );
}
gameLocal.Printf( " Current Area: %i\n", PointReachableAreaNum(GetOrigin()) );
if ( state ) {
gameLocal.Printf( " State: %s\n", state->Name() );
}
gameLocal.Printf( " Health: %i/%i\n", GetHealth(), spawnArgs.GetInt( "health" ) );
gameLocal.Printf( " Torso State: %s\n", torsoAnim.state.c_str() );
gameLocal.Printf( " Legs State: %s\n", legsAnim.state.c_str() );
gameLocal.Printf( " IK: %s\n", walkIK.IsActivated() ? "enable" : "disabled" );
gameLocal.Printf( " Turnrate: %f\n", turnRate );
}
bool hhMonsterAI::NewWanderDir( const idVec3 &dest ) {
if ( spawnArgs.GetInt( "wander_radius" ) ) {
//pick a new dest based on radius from starting origin
idVec3 offset = hhUtils::RandomVector() * float(spawnArgs.GetInt( "wander_radius" ));
offset.z = 0.0f;
const idVec3 newDest = spawnOrigin + offset;
return idAI::NewWanderDir( newDest );
} else {
return idAI::NewWanderDir( dest );
}
}
/*
=====================
hhMonsterAI::Save
=====================
*/
void hhMonsterAI::Save( idSaveGame *savefile ) const {
savefile->WriteInt( targetReaction.reactionIndex );
targetReaction.entity.Save( savefile);
shootTarget.Save( savefile );
currPassageway.Save( savefile );
savefile->WriteBool( bCanFall );
int i, num = healthTriggers.Num();
savefile->WriteInt( num );
for (i = 0; i < num; i++) {
savefile->WriteInt( healthTriggers[i].healthThresh );
healthTriggers[i].triggerEnt.Save( savefile );
savefile->WriteInt( healthTriggers[i].triggerCount );
}
savefile->WriteInt( hearingRange );
savefile->WriteInt( lastContactTime );
savefile->WriteBool( bSeeThroughPortals );
savefile->WriteBool( bBossBar );
savefile->WriteInt( nextSpeechTime );
savefile->WriteAngles( lookOffset );
savefile->WriteVec3( spawnOrigin );
savefile->WriteBool( bBindOrient );
savefile->WriteInt( numDDADamageSamples );
savefile->WriteFloat( totalDDADamage );
savefile->WriteInt( spawnThinkFlags );
savefile->WriteBool( bCanWallwalk );
savefile->WriteBool( bOverrideKilledByGravityZones );
savefile->WriteInt( fallDelay );
savefile->WriteBool( soundOnModel );
savefile->WriteBool( bBindAxis );
savefile->WriteBool( bCustomBlood );
savefile->WriteBool( bNoCombat );
savefile->WriteBool( bNeverTarget );
savefile->WriteInt( nextSpiritProxyCheck );
};
/*
=====================
hhMonsterAI::Restore
=====================
*/
void hhMonsterAI::Restore( idRestoreGame *savefile ) {
savefile->ReadInt( targetReaction.reactionIndex );
targetReaction.entity.Restore( savefile);
shootTarget.Restore( savefile );
currPassageway.Restore( savefile );
savefile->ReadBool( bCanFall );
int i, num;
savefile->ReadInt( num );
healthTriggers.SetNum( num );
for (i = 0; i < num; i++) {
savefile->ReadInt( healthTriggers[i].healthThresh );
healthTriggers[i].triggerEnt.Restore( savefile );
savefile->ReadInt( healthTriggers[i].triggerCount );
}
savefile->ReadInt( hearingRange );
savefile->ReadInt( lastContactTime );
savefile->ReadBool( bSeeThroughPortals );
savefile->ReadBool( bBossBar );
savefile->ReadInt( nextSpeechTime );
savefile->ReadAngles( lookOffset );
savefile->ReadVec3( spawnOrigin );
savefile->ReadBool( bBindOrient );
savefile->ReadInt( numDDADamageSamples );
savefile->ReadFloat( totalDDADamage );
savefile->ReadInt( spawnThinkFlags );
savefile->ReadBool( bCanWallwalk );
savefile->ReadBool( bOverrideKilledByGravityZones );
savefile->ReadInt( fallDelay );
savefile->ReadBool( soundOnModel );
savefile->ReadBool( bBindAxis );
savefile->ReadBool( bCustomBlood );
savefile->ReadBool( bNoCombat );
savefile->ReadBool( bNeverTarget );
savefile->ReadInt( nextSpiritProxyCheck );
allSimpleMonsters.AddUnique(this);
nextTurnUpdate = 0;
if ( AI_VEHICLE ) {
if ( GetVehicleInterface() ) {
ResetClipModel();
hhVehicle *vehicle = GetVehicleInterface()->GetVehicle();
spawnArgs.Set( "use_aas", spawnArgs.GetString( "aas_shuttle", "aasDroid" ) );
SetAAS();
SetState( GetScriptFunction( "state_VehicleCombat" ) );
}
}
};
/*
=====================
hhMonsterAI::GetTurnDir
Returns -1 if left, 0 if facing ideal, and 1 if turning right
=====================
*/
int hhMonsterAI::GetTurnDir( void ) {
float diff;
if ( !turnRate ) {
return 0;
}
diff = idMath::AngleNormalize180( current_yaw - ideal_yaw );
if ( idMath::Fabs( diff ) < 0.01f ) {
// force it to be exact
current_yaw = ideal_yaw;
return 0;
}
return (diff < 0 ? -1 : 1);
}
//=============================================================================
//
// hhMonsterAI::SendDamageToDDA()
//
// Informs the player of the amount of damage inflicted upon it until this
// creature was killed.
//=============================================================================
void hhMonsterAI::SendDamageToDDA() {
if ( gameLocal.isMultiplayer ) {
return;
}
int ddaIndex = spawnArgs.GetInt( "ddaIndex", "0" );
if ( gameLocal.GetDDA() && ddaIndex >= 0 ) { // Only include certain creatures in the DDA calculation (exclude such things as crawlers and NPCs)
gameLocal.GetDDA()->DDA_AddDamage( ddaIndex, totalDDADamage );
gameLocal.GetDDA()->DDA_AddSurvivalHealth( ddaIndex, gameLocal.GetLocalPlayer()->GetHealth() );
}
totalDDADamage = 0;
}
void hhMonsterAI::FlyMove( void ) {
idVec3 goalPos;
idVec3 oldorigin;
idVec3 newDest;
AI_BLOCKED = false;
if ( ( move.moveCommand != MOVE_NONE ) && ReachedPos( move.moveDest, move.moveCommand ) ) {
StopMove( MOVE_STATUS_DONE );
}
if ( move.moveCommand != MOVE_TO_POSITION_DIRECT ) {
idVec3 vel = physicsObj.GetLinearVelocity();
if ( GetMovePos( goalPos ) ) {
CheckObstacleAvoidance( goalPos, newDest );
goalPos = newDest;
}
if ( !AI_FLY_NO_SEEK && move.speed ) {
FlySeekGoal( vel, goalPos );
}
// add in bobbing
AddFlyBob( vel );
if ( enemy.GetEntity() && ( move.moveCommand != MOVE_TO_POSITION ) ) {
AdjustFlyHeight( vel, goalPos );
}
AdjustFlySpeed( vel );
physicsObj.SetLinearVelocity( vel );
}
// turn
FlyTurn();
// run the physics for this frame
oldorigin = physicsObj.GetOrigin();
physicsObj.UseFlyMove( true );
physicsObj.UseVelocityMove( false );
physicsObj.SetDelta( vec3_zero );
physicsObj.ForceDeltaMove( disableGravity );
RunPhysics();
monsterMoveResult_t moveResult = physicsObj.GetMoveResult();
if ( !af_push_moveables && attack.Length() && TestMelee() ) {
DirectDamage( attack, enemy.GetEntity() );
} else {
idEntity *blockEnt = physicsObj.GetSlideMoveEntity();
if ( blockEnt && blockEnt->IsType( idMoveable::Type ) && blockEnt->GetPhysics()->IsPushable() ) {
KickObstacles( viewAxis[ 0 ], kickForce, blockEnt );
} else if ( moveResult == MM_BLOCKED ) {
move.blockTime = gameLocal.time + 500;
AI_BLOCKED = true;
}
}
idVec3 org = physicsObj.GetOrigin();
if ( oldorigin != org ) {
TouchTriggers();
}
if ( ai_debugMove.GetBool() ) {
gameRenderWorld->DebugLine( colorCyan, oldorigin, physicsObj.GetOrigin(), 4000 );
gameRenderWorld->DebugBounds( colorOrange, physicsObj.GetBounds(), org, gameLocal.msec );
gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), move.moveDest, gameLocal.msec );
gameRenderWorld->DebugLine( colorRed, org, org + physicsObj.GetLinearVelocity(), gameLocal.msec, true );
gameRenderWorld->DebugLine( colorBlue, org, goalPos, gameLocal.msec, true );
gameRenderWorld->DebugLine( colorYellow, org + EyeOffset(), org + EyeOffset() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true );
DrawRoute();
}
}
void hhMonsterAI::Activate( idEntity *activator ) {
if ( spawnThinkFlags != 0 ) {
int temp = spawnThinkFlags;
spawnThinkFlags = 0;
BecomeActive( temp );
}
idAI::Activate( activator );
}
void hhMonsterAI::Hide() {
if ( spawnThinkFlags == 0 && ai_hideSkipThink.GetBool() ) {
if ( ( spawnArgs.GetBool( "hide" ) || spawnArgs.GetBool( "portal" ) ) ) {
spawnThinkFlags = thinkFlags;
thinkFlags = 0;
}
}
idAI::Hide();
}
void hhMonsterAI::HideNoDormant() {
idAI::Hide();
}
void hhMonsterAI::BecomeActive( int flags ) {
if ( spawnThinkFlags != 0 ) {
spawnThinkFlags |= flags;
return;
}
idAI::BecomeActive( flags );
}
void hhMonsterAI::HandleNoGore(void) {
if (GERMAN_VERSION || g_nogore.GetBool()) {
fl.takedamage = false;
fl.canBeTractored = false;
GetPhysics()->SetContents(0);
float time = spawnArgs.GetFloat("nogore_dispose_time", "1");
if (time > 0.0f) {
//PostEventSec(&EV_Dispose, time);
}
}
}
bool hhMonsterAI::GetPhysicsToSoundTransform( idVec3 &origin, idMat3 &axis ) {
if ( soundOnModel ) {
idVec3 boneOrigin;
idMat3 boneAxis;
jointHandle_t bone = animator.GetJointHandle( spawnArgs.GetString( "sound_joint", "b1" ) );
if ( bone != INVALID_JOINT && animator.GetJointTransform( bone, gameLocal.time, boneOrigin, boneAxis ) ) {
origin = boneOrigin;
axis = boneAxis * viewAxis;
return true;
}
}
return idAI::GetPhysicsToSoundTransform( origin, axis );
}
void hhMonsterAI::AddLocalMatterWound( jointHandle_t jointNum, const idVec3 &localOrigin, const idVec3 &localNormal, const idVec3 &localDir, int damageDefIndex, const idMaterial *collisionMaterial ) {
if ( bCustomBlood ) {
return;
}
const idDeclEntityDef *def = static_cast<const idDeclEntityDef *>( declManager->DeclByIndex( DECL_ENTITYDEF, damageDefIndex ) );
if ( def == NULL ) {
return;
}
surfTypes_t matterType = gameLocal.GetMatterType( this, collisionMaterial, "idEntity::AddLocalMatterWound" );
GetWoundManager()->AddWounds( def, matterType, jointNum, localOrigin, localNormal, localDir );
if ( head.IsValid() ) {
head->AddLocalMatterWound( jointNum, localOrigin, localNormal, localDir, damageDefIndex, collisionMaterial );
}
//TODO: Grab head projection code from below and put into ApplyImpactMark()
}