mirror of
https://github.com/jmarshall23/DoomRTX.git
synced 2026-08-16 00:01:53 +02:00
838 lines
26 KiB
C++
838 lines
26 KiB
C++
#include "precompiled.h"
|
|
#pragma hdrstop
|
|
|
|
#include "prey_local.h"
|
|
|
|
const idEventDef MA_AntiProjectileAttack("<antiProjectileAttack>", "e");
|
|
const idEventDef MA_StartPreDeath("<startPreDeath>" );
|
|
const idEventDef MA_HandlePassageway("<handlePassageway>", NULL);
|
|
const idEventDef MA_EnterPassageway("<enterPassageway>", "e",NULL);
|
|
const idEventDef MA_ExitPassageway("<exitPassageway>", "e",NULL);
|
|
const idEventDef MA_UseThisPassageway("useThisPassageway", "ed",NULL);
|
|
const idEventDef MA_GibOnDeath("gibOnDeath", "d");
|
|
|
|
CLASS_DECLARATION( hhMonsterAI, hhHarvesterSimple )
|
|
EVENT( MA_OnProjectileLaunch, hhHarvesterSimple::Event_OnProjectileLaunch )
|
|
EVENT( MA_AntiProjectileAttack, hhHarvesterSimple::Event_AntiProjectileAttack )
|
|
EVENT( MA_StartPreDeath, hhHarvesterSimple::Event_StartPreDeath )
|
|
EVENT( MA_HandlePassageway, hhHarvesterSimple::Event_HandlePassageway )
|
|
EVENT( MA_EnterPassageway, hhHarvesterSimple::Event_EnterPassageway )
|
|
EVENT( MA_ExitPassageway, hhHarvesterSimple::Event_ExitPassageway )
|
|
EVENT( MA_UseThisPassageway, hhHarvesterSimple::Event_UseThisPassageway )
|
|
EVENT( EV_Broadcast_AppendFxToList, hhHarvesterSimple::Event_AppendFxToList )
|
|
EVENT( MA_GibOnDeath, hhHarvesterSimple::Event_GibOnDeath )
|
|
END_CLASS
|
|
|
|
#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build
|
|
//
|
|
// Spawn()
|
|
//
|
|
void hhHarvesterSimple::Spawn(void) {
|
|
allowPreDeath = true;
|
|
lastAntiProjectileAttack = 0;
|
|
lastPassagewayTime = 0;
|
|
passageCount = spawnArgs.GetInt("passage_count", "0"); // Used for passing to and from torso
|
|
bSmokes = spawnArgs.GetBool("smokes", "0");
|
|
bGibOnDeath = false;
|
|
}
|
|
|
|
//
|
|
// Event_OnProjectileLaunch()
|
|
//
|
|
void hhHarvesterSimple::Event_OnProjectileLaunch(hhProjectile *proj) {
|
|
|
|
// Can't launch again yet
|
|
float min = spawnArgs.GetFloat( "dda_delay_min" );
|
|
float max = spawnArgs.GetFloat( "dda_delay_max" );
|
|
float delay = min + (max - min) * (1.0f - gameLocal.GetDDAValue());
|
|
|
|
if(gameLocal.GetTime() - lastAntiProjectileAttack < delay)
|
|
return;
|
|
|
|
// The person who launched this projectile wasn't someone to worry about
|
|
if(proj->GetOwner() && !(ReactionTo(proj->GetOwner()) & (ATTACK_ON_SIGHT | ATTACK_ON_DAMAGE)))
|
|
return;
|
|
|
|
// TODO: more intelligent checks for if we should launch the anti-projectile chaff
|
|
idVec3 fw = viewAxis[0];
|
|
idVec3 projFw = proj->GetAxis()[0];
|
|
if(proj->GetOwner())
|
|
projFw = proj->GetOwner()->GetAxis()[0];
|
|
float dot = fw * projFw;
|
|
if(dot > -.7f)
|
|
return;
|
|
|
|
ProcessEvent(&MA_AntiProjectileAttack, proj);
|
|
}
|
|
|
|
|
|
//
|
|
// Event_AntiProjectileAttack()
|
|
//
|
|
void hhHarvesterSimple::Event_AntiProjectileAttack(hhProjectile *proj) {
|
|
|
|
if(!spawnArgs.GetBool("block_projectiles", "1")) {
|
|
return;
|
|
}
|
|
|
|
const idDict *projectileDef = gameLocal.FindEntityDefDict( spawnArgs.GetString("def_chaff") );
|
|
if ( !projectileDef ) {
|
|
gameLocal.Error( "Unknown def_chaff: %s\n", spawnArgs.GetString("def_chaff") );
|
|
}
|
|
hhProjectile* projectile = NULL;
|
|
|
|
//bjk: new parms
|
|
int numProj = spawnArgs.GetInt("shieldNumProj", "10");
|
|
float spread = DEG2RAD( spawnArgs.GetFloat("shieldSpread", "10") );
|
|
float yaw = DEG2RAD( spawnArgs.GetFloat("shieldYawSpread", "10") );
|
|
|
|
//StartSound("snd_fire_chaff", SND_CHANNEL_ANY, 0, true, NULL);
|
|
torsoAnim.PlayAnim( GetAnimator()->GetAnim( "antiprojectile_attack" ) );
|
|
|
|
idVec3 dir = proj->GetOrigin() - GetOrigin();
|
|
dir.Normalize();
|
|
dir = dir+idVec3(0.f, 0.f, -.3f); // bjk: bias towards ground
|
|
dir.Normalize();
|
|
idMat3 muzzleAxis = dir.ToMat3();
|
|
// todo: read this in from a bone
|
|
idVec3 launchPos = GetOrigin() + idVec3(0.0f, 0.0f, 64.0f);
|
|
|
|
for(int i=0; i<numProj; i++) {
|
|
projectile = hhProjectile::SpawnProjectile( projectileDef );
|
|
HH_ASSERT( projectile );
|
|
|
|
//bjk: uses random now
|
|
float ang = spread * hhMath::Sqrt( gameLocal.random.RandomFloat() );
|
|
float spin = hhMath::TWO_PI * gameLocal.random.RandomFloat();
|
|
dir = muzzleAxis[ 0 ] + muzzleAxis[ 2 ] * ( hhMath::Sin(ang) * hhMath::Sin(spin) ) - muzzleAxis[ 1 ] * ( hhMath::Sin(ang+yaw) * hhMath::Cos(spin) );
|
|
dir.Normalize();
|
|
|
|
projectile->Create(this, launchPos, dir);
|
|
projectile->Launch(launchPos, dir, idVec3(0.0f, 0.0f, 0.0f));
|
|
}
|
|
|
|
lastAntiProjectileAttack = gameLocal.GetTime();
|
|
}
|
|
|
|
bool hhHarvesterSimple::Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) {
|
|
int preDeathThresh = spawnArgs.GetInt("predeath_thresh", "-1");
|
|
if(allowPreDeath && !bGibOnDeath && preDeathThresh > 0 && health < preDeathThresh) {
|
|
allowPreDeath = false; // We've tried, cannot try anymore
|
|
float preDeathFreq = spawnArgs.GetFloat("predeath_freq", "1");
|
|
if(gameLocal.random.RandomFloat() < preDeathFreq) {
|
|
ProcessEvent(&MA_StartPreDeath);
|
|
}
|
|
}
|
|
|
|
if ( bSmokes ) {
|
|
if ( !fxSmoke[0].IsValid() && health <= AI_PASSAGEWAY_HEALTH ) {
|
|
SpawnSmoke();
|
|
} else if (fxSmoke[0].IsValid() && health > AI_PASSAGEWAY_HEALTH) {
|
|
ClearSmoke();
|
|
}
|
|
}
|
|
|
|
return( idAI::Pain(inflictor, attacker, damage, dir, location) );
|
|
}
|
|
|
|
void hhHarvesterSimple::Event_StartPreDeath(void) {
|
|
int i;
|
|
|
|
// No torso? Just kill myself .....
|
|
if(!spawnArgs.GetString("def_torso")) {
|
|
ProcessEvent(&AI_Kill);
|
|
return;
|
|
}
|
|
|
|
// Spawn death fx, if any
|
|
const char *fx = spawnArgs.GetString("fx_death");
|
|
if (fx && fx[0]) {
|
|
hhFxInfo fxInfo;
|
|
|
|
fxInfo.RemoveWhenDone(true);
|
|
BroadcastFxInfo(fx, GetOrigin(), GetAxis(), &fxInfo);
|
|
}
|
|
|
|
// do blood splats
|
|
float size = spawnArgs.GetFloat("decal_torso_pop_size","96");
|
|
|
|
// copy my target keys to the keys that will spawn the torso
|
|
idDict dict;
|
|
const idDict *torsoDict = gameLocal.FindEntityDefDict(spawnArgs.GetString("def_torso"));
|
|
if ( !torsoDict ) {
|
|
gameLocal.Error("Unknown def_torso: %s\n", spawnArgs.GetString("def_torso"));
|
|
}
|
|
dict.Copy(*torsoDict);
|
|
for(i=0;i<targets.Num();i++)
|
|
{
|
|
if(!targets[i].GetEntity()) {
|
|
continue;
|
|
}
|
|
|
|
idStr key("target");
|
|
if(i > 0) {
|
|
sprintf(key, "target%i", i);
|
|
}
|
|
|
|
dict.Set((const char*)key, (const char*)targets[i].GetEntity()->name);
|
|
}
|
|
|
|
// Passageway variables
|
|
dict.SetInt("passage_health", AI_PASSAGEWAY_HEALTH);
|
|
dict.SetInt("passage_count", passageCount);
|
|
dict.SetVector("origin", GetOrigin());
|
|
dict.SetMatrix("rotation", GetAxis());
|
|
|
|
// remove my target keys (so they don't get fired)
|
|
targets.Clear();
|
|
|
|
// Spawn the torso
|
|
idEntity *e;
|
|
hhMonsterAI *aiTorso;
|
|
if(!gameLocal.SpawnEntityDef(dict, &e))
|
|
gameLocal.Error("Failed to spawn ai torso.");
|
|
HH_ASSERT(e && e->IsType(hhMonsterAI::Type));
|
|
aiTorso = static_cast<hhMonsterAI*>(e);
|
|
HH_ASSERT(aiTorso != NULL);
|
|
|
|
// Throw gibs
|
|
idStr debrisDef = spawnArgs.GetString("def_gibDebrisSpawnerPreDeath");
|
|
if(debrisDef.Length() > 0) {
|
|
hhUtils::SpawnDebrisMass(debrisDef.c_str(), this, 1.0f);
|
|
}
|
|
|
|
health = 0;
|
|
|
|
// remove myself
|
|
Hide();
|
|
PostEventMS(&EV_Remove, 3000);
|
|
}
|
|
|
|
void hhHarvesterSimple::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) {
|
|
if ( attacker == this ) {
|
|
return;
|
|
}
|
|
const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName );
|
|
if ( !AI_DEAD ) {
|
|
if ( damageDef && damageDef->GetBool( "ice" ) && spawnArgs.GetBool( "can_freeze", "0" ) ) {
|
|
spawnArgs.Set( "bAlwaysGib", "0" );
|
|
allowPreDeath = false;
|
|
} else {
|
|
allowPreDeath = true;
|
|
}
|
|
if ( damageDef && damageDef->GetBool( "no_special_death" ) ) {
|
|
allowPreDeath = false;
|
|
}
|
|
}
|
|
hhMonsterAI::Damage( inflictor, attacker, dir, damageDefName, damageScale, location );
|
|
}
|
|
|
|
int hhHarvesterSimple::EvaluateReaction( const hhReaction *react ) {
|
|
// Let the script limit the distance of Climb reactions we consider
|
|
if ( react->desc->effect == hhReactionDesc::Effect_Climb && AI_CLIMB_RANGE >= 0.0f && ( react->causeEntity->GetOrigin() - GetOrigin()).LengthSqr() > AI_CLIMB_RANGE ) {
|
|
return 0;
|
|
}
|
|
|
|
// If we're supposed to use a specific passageway, only accept it and reject the others
|
|
if ( nextPassageway.IsValid() && react->desc->effect == hhReactionDesc::Effect_Passageway ) {
|
|
if (react->causeEntity.GetEntity() == nextPassageway.GetEntity()) {
|
|
return 100;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int rank = hhMonsterAI::EvaluateReaction( react );
|
|
if ( react->desc->effect == hhReactionDesc::Effect_Passageway &&
|
|
react->causeEntity.GetEntity() == lastPassageway.GetEntity() ) {
|
|
if ( lastPassagewayTime + 10000 > gameLocal.time ) {
|
|
if (rank > 10) {
|
|
// Discourage entering the same passageway we just left
|
|
return 10;
|
|
}
|
|
} else {
|
|
// Ignore previous passageway for 10 seconds
|
|
return 0;
|
|
}
|
|
}
|
|
return rank;
|
|
}
|
|
|
|
#define CheckPassageFreqInitial 6000
|
|
void hhHarvesterSimple::Event_EnterPassageway(hhAIPassageway *pn) {
|
|
CancelEvents( &MA_OnProjectileLaunch ); // Clear any anti-projectile attacks
|
|
CancelEvents( &MA_AntiProjectileAttack ); // Clear any anti-projectile attacks
|
|
if (nextPassageway.IsValid()) {
|
|
if (nextPassageway.GetEntity() != pn) {
|
|
gameLocal.Warning("hhAIPassageway entered was not the specified one!\n");
|
|
}
|
|
nextPassageway.Clear();
|
|
}
|
|
|
|
if(currPassageway != NULL) {
|
|
gameLocal.Error("%s tried to ENTER a passageway but already had a passageway!", (const char*)name);
|
|
}
|
|
|
|
HH_ASSERT(currPassageway == NULL);
|
|
Hide(); // Should already be hidden but just to make sure
|
|
|
|
// Only consider passageCount if damaged (ignores scripted passage sequences) or if it's the torso
|
|
if (health < spawnHealth || !idStr::Icmp(spawnArgs.GetString("classname"), "monster_harvester_torso") ) {
|
|
passageCount++;
|
|
if (passageCount > 1) {
|
|
//TODO make this affected by the DDA system
|
|
AI_PASSAGEWAY_HEALTH = AI_PASSAGEWAY_HEALTH * 0.75f; // Reduce passageway seeking health level to 3/4 of it's original value
|
|
}
|
|
}
|
|
|
|
GetAnimator()->ClearAllAnims(gameLocal.GetTime(), 0);
|
|
Event_AnimState(ANIMCHANNEL_LEGS, "Legs_Idle", (int)0);
|
|
Event_AnimState(ANIMCHANNEL_TORSO, "Torso_Idle", (int)0);
|
|
torsoAnim.UpdateState();
|
|
legsAnim.UpdateState();
|
|
|
|
StartSound("snd_inside_passage", SND_CHANNEL_VOICE, SSF_LOOPING, true, NULL);
|
|
|
|
currPassageway = pn;
|
|
if(GetPhysics())
|
|
GetPhysics()->SetContents(0);
|
|
|
|
// Does this passage way give us health?
|
|
int h = 0;
|
|
pn->spawnArgs.GetInt("give_health", "0", h);
|
|
health += h;
|
|
health = idMath::ClampInt(0, spawnArgs.GetInt("health"), health);
|
|
|
|
|
|
// Should we transform to another monster when entering?
|
|
idStr transTo;
|
|
if(spawnArgs.GetString("passage_transform_to", "", transTo)) {
|
|
const idDict *def = gameLocal.FindEntityDefDict( transTo );
|
|
if(!def) {
|
|
gameLocal.Error("Unknown def : %s", (const char*)transTo);
|
|
}
|
|
|
|
// copy my target keys to the keys of the transformed-to entity
|
|
idDict dict;
|
|
dict.Copy(*def);
|
|
for(int i=0;i<targets.Num();i++)
|
|
{
|
|
if(!targets[i].GetEntity())
|
|
continue;
|
|
|
|
idStr key("target");
|
|
if(i > 0)
|
|
sprintf(key, "target%i", i);
|
|
|
|
dict.Set((const char*)key, (const char*)targets[i].GetEntity()->name);
|
|
}
|
|
|
|
// Passageway variables
|
|
dict.SetInt("passage_health", AI_PASSAGEWAY_HEALTH);
|
|
dict.SetInt("passage_count", passageCount);
|
|
|
|
idEntity *e = NULL;
|
|
gameLocal.SpawnEntityDef(dict, &e);
|
|
HH_ASSERT( e->IsType( hhHarvesterSimple::Type ) );
|
|
hhHarvesterSimple *ai = static_cast<hhHarvesterSimple*>(e);
|
|
ai->SetOrigin(GetOrigin());
|
|
ai->SetAxis(GetAxis());
|
|
ai->Event_EnterPassageway(pn);
|
|
targets.Clear();
|
|
PostEventSec(&EV_Remove, 0.1f);
|
|
return;
|
|
}
|
|
|
|
|
|
PostEventMS(&MA_HandlePassageway, CheckPassageFreqInitial);
|
|
}
|
|
|
|
void hhHarvesterSimple::Event_ExitPassageway(hhAIPassageway *pn) {
|
|
if(currPassageway == NULL) {
|
|
gameLocal.Error("%s tried to EXIT a passageway but did NOT have a curr passageway!", (const char*)name);
|
|
}
|
|
|
|
HH_ASSERT(currPassageway != NULL);
|
|
|
|
// Store this passageway so we don't jump right back into it.
|
|
lastPassageway = idEntityPtr<idEntity> (pn);
|
|
lastPassagewayTime = gameLocal.time;
|
|
|
|
if(GetPhysics())
|
|
GetPhysics()->SetContents(CONTENTS_BODY);
|
|
currPassageway = NULL;
|
|
AI_ACTIVATED = true; // Sometimes this is NOT true?!?!
|
|
StopSound(SND_CHANNEL_VOICE, true);
|
|
|
|
// Teleport to new pos
|
|
idAngles angs = pn->GetAxis().ToAngles();
|
|
idVec3 pos = pn->GetExitPos();
|
|
Teleport( pos, angs, pn );
|
|
Show();
|
|
|
|
current_yaw = angs.yaw;
|
|
ideal_yaw = angs.yaw;
|
|
turnVel = 0.0f;
|
|
HH_ASSERT(FacingIdeal());
|
|
|
|
// Exit anim to play?
|
|
idStr exitAnim = pn->spawnArgs.GetString("exit_anim");
|
|
if(exitAnim.Length() && GetAnimator()->HasAnim(exitAnim)) {
|
|
GetAnimator()->ClearAllAnims(gameLocal.GetTime(), 0);
|
|
torsoAnim.UpdateState();
|
|
legsAnim.UpdateState();
|
|
torsoAnim.PlayAnim( GetAnimator()->GetAnim( exitAnim ) );
|
|
legsAnim.PlayAnim( GetAnimator()->GetAnim( exitAnim ) );
|
|
}
|
|
}
|
|
|
|
//
|
|
// Event_HandlePassageway()
|
|
//
|
|
#define MaxExits 32
|
|
#define CheckPassageFreq 3000
|
|
void hhHarvesterSimple::Event_HandlePassageway(void) {
|
|
|
|
if(currPassageway == NULL) {
|
|
gameLocal.Error("%s tried to handle a passageway but did NOT have a passageway!", (const char*)name);
|
|
}
|
|
|
|
HH_ASSERT(currPassageway != NULL);
|
|
hhAIPassageway *exitNode = NULL;
|
|
|
|
// CATCH 'went inside hole - never came out' bug
|
|
if(currPassageway.GetEntity()->lastEntered == this && gameLocal.GetTime() - currPassageway.GetEntity()->timeLastEntered > 30000) {
|
|
gameLocal.Warning("%s has been stuck in a passageway for >30 secs, killing it off.", (const char*)name);
|
|
fl.takedamage = true;
|
|
// Use hhMonsterAI::Damage because our Damage returns if we're the attacker
|
|
hhMonsterAI::Damage(this, this, vec3_origin, "damage_gib", 1.0f, INVALID_JOINT);
|
|
return;
|
|
}
|
|
|
|
// No other way out - gotta go out the way we came in
|
|
if(currPassageway->targets.Num() <= 0) {
|
|
exitNode = currPassageway.GetEntity();
|
|
}
|
|
// We can go out another way
|
|
else {
|
|
hhAIPassageway *possibleExits[MaxExits];
|
|
hhAIPassageway *tmp;
|
|
int count = 0;
|
|
|
|
bool isRandom;
|
|
if (gameLocal.GetTime() - currPassageway.GetEntity()->timeLastEntered > SEC2MS(10)) {
|
|
// Try to prevent us from being stuck after 10 seconds
|
|
isRandom = true;
|
|
} else {
|
|
// Choose possibleExits[] randomly if we have no enemy or based on chance. Otherwise choose based on line-of-sight to the player
|
|
isRandom = !enemy.IsValid() || gameLocal.random.RandomInt(100) > 50;
|
|
}
|
|
trace_t tr;
|
|
idVec3 toPos;
|
|
if (!isRandom) {
|
|
toPos = enemy->GetOrigin();
|
|
}
|
|
|
|
// We've tried once, now we can use our entrance as an exit
|
|
if(gameLocal.GetTime() - currPassageway.GetEntity()->timeLastEntered > CheckPassageFreqInitial + 1000) {
|
|
count = 1;
|
|
possibleExits[0] = currPassageway.GetEntity(); // We can always go out the same way we came in
|
|
}
|
|
for(int i=0;i<currPassageway->targets.Num();i++) {
|
|
if(currPassageway->targets[i] != NULL && currPassageway->targets[i].GetEntity()->IsType(hhAIPassageway::Type)) {
|
|
if (isRandom) {
|
|
possibleExits[count++] = static_cast<hhAIPassageway*>(currPassageway->targets[i].GetEntity());
|
|
} else {
|
|
tmp = static_cast<hhAIPassageway*>(currPassageway->targets[i].GetEntity());
|
|
// Choose based on visibility to our enemy
|
|
gameLocal.clip.TracePoint( tr, tmp->GetOrigin(), toPos, MASK_SHOT_BOUNDINGBOX, tmp );
|
|
if ( tr.fraction >= 1.0f || ( gameLocal.GetTraceEntity( tr ) == enemy.GetEntity() ) ) {
|
|
lastVisibleEnemyPos = toPos; // Can now see enemy
|
|
possibleExits[count++] = tmp;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We've got enough
|
|
if(count >= MaxExits)
|
|
break;
|
|
}
|
|
|
|
if(count == 0) {
|
|
exitNode = currPassageway.GetEntity();
|
|
} else if ( enemy.IsValid() && gameLocal.random.RandomInt(100) > 70 ) {
|
|
// Find the one closest to the last known enemy pos
|
|
if (!exitNode) {
|
|
float bestDistance = -1.0f, distSq;
|
|
for (int i = 0; i < count; i++) {
|
|
distSq = ( possibleExits[i]->GetOrigin() - lastVisibleEnemyPos ).LengthSqr();
|
|
if ( bestDistance == -1.0f || distSq < bestDistance ) {
|
|
exitNode = possibleExits[i];
|
|
bestDistance = distSq;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Pick a random one
|
|
int randExit = gameLocal.random.RandomInt(count); // TODO: Randomness isn't very random! Fix!
|
|
HH_ASSERT(randExit >= 0 && randExit < count);
|
|
exitNode = possibleExits[randExit];
|
|
}
|
|
}
|
|
|
|
if(!exitNode)
|
|
exitNode = currPassageway.GetEntity();
|
|
|
|
// The one we picked is disabled, so go out where we came in
|
|
// TODO: make this time based, try a few more times before going back out
|
|
if(!exitNode->IsPassagewayEnabled()) {
|
|
exitNode = currPassageway.GetEntity();
|
|
}
|
|
|
|
// Special case: The passageway is never meant to be exited from, use it's specified exit point instead
|
|
const char *altPassage = exitNode->spawnArgs.GetString("force_passageway", "");
|
|
if (altPassage && altPassage[0]) {
|
|
idEntity *node = gameLocal.FindEntity(altPassage);
|
|
if (node && node->IsType(hhAIPassageway::Type)) {
|
|
exitNode = reinterpret_cast<hhAIPassageway *> (node);
|
|
}
|
|
}
|
|
|
|
// Make sure no one is blocking where we want to go out
|
|
idBounds myBnds = GetPhysics()->GetBounds();
|
|
myBnds.Expand(12.0f);
|
|
myBnds.TranslateSelf(exitNode->GetExitPos());
|
|
idEntity *ents[MAX_GENTITIES];
|
|
int count = gameLocal.clip.EntitiesTouchingBounds(myBnds, -1, ents, MAX_GENTITIES);
|
|
for(int i=0;i<count;i++) {
|
|
// We'll have to try again later when the coast is clear
|
|
if(ents[i] && ents[i] != this && !ents[i]->IsHidden() && (ents[i]->IsType(idActor::Type) || ents[i]->IsType(hhMoveable::Type))) {
|
|
// Chunk moveables
|
|
if ( ents[i]->IsType(idMoveable::Type) ) {
|
|
ents[i]->Damage( this, this, vec3_origin, "damage_gib", 1.0f, INVALID_JOINT );
|
|
ents[i]->SquishedByDoor(this);
|
|
continue;
|
|
}
|
|
gameLocal.Warning("Passageway exit blocked.\n");
|
|
PostEventMS(&MA_HandlePassageway, CheckPassageFreq);
|
|
if(ai_debugBrain.GetInteger() != 0) {
|
|
gameRenderWorld->DebugBounds(colorYellow, myBnds, vec3_origin, 2000);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Else we can just exit now - its safe
|
|
exitNode->ProcessEvent(&EV_Activate, this);
|
|
}
|
|
|
|
#define LinkScriptVariable( name ) name.LinkTo(scriptObject, #name)
|
|
void hhHarvesterSimple::LinkScriptVariables() {
|
|
hhMonsterAI::LinkScriptVariables();
|
|
LinkScriptVariable(AI_CLIMB_RANGE);
|
|
LinkScriptVariable(AI_PASSAGEWAY_HEALTH);
|
|
}
|
|
|
|
void hhHarvesterSimple::Event_UseThisPassageway(hhAIPassageway *pn, bool force) {
|
|
nextPassageway.Assign(pn);
|
|
|
|
if (force) { // Snap to position
|
|
Event_FindReaction("passageway");
|
|
hhReaction *reaction = targetReaction.GetReaction();
|
|
if (!reaction) {
|
|
gameLocal.Warning("useThisPassageway could not find a valid reaction!\n");
|
|
} else {
|
|
idVec3 tp = GetTouchPos( reaction->causeEntity.GetEntity(), reaction->desc );
|
|
SetOrigin(tp);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
hhHarvesterSimple::Save
|
|
=====================
|
|
*/
|
|
void hhHarvesterSimple::Save( idSaveGame *savefile ) const {
|
|
savefile->WriteBool( allowPreDeath );
|
|
savefile->WriteInt( lastAntiProjectileAttack );
|
|
savefile->WriteInt( passageCount );
|
|
savefile->WriteBool( bGibOnDeath );
|
|
savefile->WriteInt( lastPassagewayTime );
|
|
|
|
lastPassageway.Save( savefile );
|
|
nextPassageway.Save( savefile );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
hhHarvesterSimple::Restore
|
|
=====================
|
|
*/
|
|
void hhHarvesterSimple::Restore( idRestoreGame *savefile ) {
|
|
savefile->ReadBool( allowPreDeath );
|
|
savefile->ReadInt( lastAntiProjectileAttack );
|
|
savefile->ReadInt( passageCount );
|
|
savefile->ReadBool( bGibOnDeath );
|
|
savefile->ReadInt( lastPassagewayTime );
|
|
|
|
lastPassageway.Restore( savefile );
|
|
nextPassageway.Restore( savefile );
|
|
|
|
bSmokes = spawnArgs.GetBool("smokes", "0");
|
|
}
|
|
|
|
void hhHarvesterSimple::SpawnSmoke(void) {
|
|
const char *bones[MAX_HARVESTER_LEGS];
|
|
bones[0] = spawnArgs.GetString( "bone_smoke_front_left" );
|
|
bones[1] = spawnArgs.GetString( "bone_smoke_front_right" );
|
|
bones[2] = spawnArgs.GetString( "bone_smoke_rear_left" );
|
|
bones[3] = spawnArgs.GetString( "bone_smoke_rear_right" );
|
|
|
|
const char *psystem = spawnArgs.GetString( "fx_smoke" );
|
|
if ( psystem && *psystem ) {
|
|
idVec3 bonePos;
|
|
idMat3 boneAxis;
|
|
|
|
for(int i = 0; i < MAX_HARVESTER_LEGS; i++) {
|
|
if( fxSmoke[i] != NULL ) {
|
|
gameLocal.Warning("Harvester already had smoke particles for leg %i.\n", i);
|
|
continue;
|
|
}
|
|
|
|
hhFxInfo fxInfo;
|
|
|
|
this->GetJointWorldTransform( bones[i], bonePos, boneAxis );
|
|
fxInfo.SetEntity( this );
|
|
fxInfo.SetBindBone( bones[i] );
|
|
fxInfo.Toggle();
|
|
fxInfo.RemoveWhenDone( false );
|
|
|
|
BroadcastFxInfo( psystem, bonePos, boneAxis, &fxInfo, &EV_Broadcast_AppendFxToList );
|
|
}
|
|
}
|
|
}
|
|
|
|
void hhHarvesterSimple::ClearSmoke(void) {
|
|
for(int i=0; i < MAX_HARVESTER_LEGS; i++) {
|
|
if(fxSmoke[i].IsValid()) {
|
|
fxSmoke[i]->Nozzle(false);
|
|
}
|
|
}
|
|
|
|
for(int i = 0; i < MAX_HARVESTER_LEGS; i++) {
|
|
SAFE_REMOVE(fxSmoke[MAX_HARVESTER_LEGS]);
|
|
}
|
|
}
|
|
|
|
void hhHarvesterSimple::Event_AppendFxToList(hhEntityFx *fx) {
|
|
for(int i = 0; i < MAX_HARVESTER_LEGS; ++i) {
|
|
if(fxSmoke[i] != NULL) {
|
|
continue;
|
|
}
|
|
|
|
fxSmoke[i] = fx;
|
|
return;
|
|
}
|
|
}
|
|
|
|
void hhHarvesterSimple::Event_GibOnDeath(const idList<idStr>* parmList) {
|
|
bGibOnDeath = (atoi((*parmList)[0].c_str()) != 0);
|
|
}
|
|
|
|
void hhHarvesterSimple::Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location) {
|
|
ClearSmoke();
|
|
hhMonsterAI::Killed(inflictor, attacker, damage, dir, location);
|
|
|
|
if (bGibOnDeath) {
|
|
Gib(vec3_origin, "damage_gib");
|
|
}
|
|
}
|
|
|
|
void hhHarvesterSimple::Event_Activate(idEntity *activator) {
|
|
if (fl.hidden) {
|
|
// If we're hidden, we're probably birthing out of passageway. Figure out which one.
|
|
hhAIPassageway *nearest = NULL;
|
|
float dist, nearDist = idMath::INFINITY;
|
|
hhAIPassageway *passage = reinterpret_cast<hhAIPassageway *> (gameLocal.FindEntityOfType( hhAIPassageway::Type, NULL ));
|
|
while (passage) {
|
|
dist = (passage->GetOrigin() - GetOrigin()).Length();
|
|
if (dist < nearDist) {
|
|
nearDist = dist;
|
|
nearest = passage;
|
|
}
|
|
passage = reinterpret_cast<hhAIPassageway *> (gameLocal.FindEntityOfType( hhAIPassageway::Type, passage ));
|
|
}
|
|
|
|
// If we found one near us and it's near enough, mark it as our last passageway
|
|
if (nearest && nearDist < 250.0f) {
|
|
lastPassageway = nearest;
|
|
lastPassagewayTime = gameLocal.time;
|
|
}
|
|
}
|
|
hhMonsterAI::Event_Activate(activator);
|
|
}
|
|
|
|
void hhHarvesterSimple::AnimMove( void ) {
|
|
idVec3 goalPos;
|
|
idVec3 delta;
|
|
idVec3 goalDelta;
|
|
float goalDist;
|
|
monsterMoveResult_t moveResult;
|
|
idVec3 newDest;
|
|
|
|
// Turn on physics to prevent flying harvesters
|
|
BecomeActive(TH_PHYSICS);
|
|
|
|
idVec3 oldorigin = physicsObj.GetOrigin();
|
|
#ifdef HUMANHEAD //jsh wallwalk
|
|
idMat3 oldaxis = GetGravViewAxis();
|
|
#else
|
|
idMat3 oldaxis = viewAxis;
|
|
#endif
|
|
|
|
AI_BLOCKED = false;
|
|
|
|
if ( move.moveCommand < NUM_NONMOVING_COMMANDS ){
|
|
move.lastMoveOrigin.Zero();
|
|
move.lastMoveTime = gameLocal.time;
|
|
}
|
|
|
|
move.obstacle = NULL;
|
|
if ( ( move.moveCommand == MOVE_FACE_ENEMY ) && enemy.GetEntity() ) {
|
|
TurnToward( lastVisibleEnemyPos );
|
|
goalPos = oldorigin;
|
|
} else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) {
|
|
TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() );
|
|
goalPos = oldorigin;
|
|
} else if ( GetMovePos( goalPos ) ) {
|
|
if ( move.moveCommand != MOVE_WANDER ) {
|
|
CheckObstacleAvoidance( goalPos, newDest );
|
|
TurnToward( newDest );
|
|
} else {
|
|
TurnToward( goalPos );
|
|
}
|
|
}
|
|
|
|
Turn();
|
|
|
|
if ( move.moveCommand == MOVE_SLIDE_TO_POSITION ) {
|
|
if ( gameLocal.time < move.startTime + move.duration ) {
|
|
goalPos = move.moveDest - move.moveDir * MS2SEC( move.startTime + move.duration - gameLocal.time );
|
|
delta = goalPos - oldorigin;
|
|
delta.z = 0.0f;
|
|
} else {
|
|
delta = move.moveDest - oldorigin;
|
|
delta.z = 0.0f;
|
|
StopMove( MOVE_STATUS_DONE );
|
|
}
|
|
} else if ( allowMove ) {
|
|
#ifdef HUMANHEAD //jsh wallwalk
|
|
GetMoveDelta( oldaxis, GetGravViewAxis(), delta );
|
|
#else
|
|
GetMoveDelta( oldaxis, viewAxis, delta );
|
|
#endif
|
|
} else {
|
|
delta.Zero();
|
|
}
|
|
|
|
if ( move.moveCommand == MOVE_TO_POSITION ) {
|
|
goalDelta = move.moveDest - oldorigin;
|
|
goalDist = goalDelta.LengthFast();
|
|
if ( goalDist < delta.LengthFast() ) {
|
|
delta = goalDelta;
|
|
}
|
|
}
|
|
|
|
#ifdef HUMANHEAD //shrink functionality
|
|
float scale = renderEntity.shaderParms[SHADERPARM_ANY_DEFORM_PARM1];
|
|
if ( scale > 0.0f && scale < 2.0f ) {
|
|
delta *= scale;
|
|
}
|
|
#endif
|
|
physicsObj.SetDelta( delta );
|
|
physicsObj.ForceDeltaMove( disableGravity );
|
|
|
|
RunPhysics();
|
|
|
|
if ( ai_debugMove.GetBool() ) {
|
|
// HUMANHEAD JRM - so we can see if grav is on or off
|
|
if(disableGravity) {
|
|
gameRenderWorld->DebugLine( colorRed, oldorigin, physicsObj.GetOrigin(), 5000 );
|
|
} else {
|
|
gameRenderWorld->DebugLine( colorCyan, oldorigin, physicsObj.GetOrigin(), 5000 );
|
|
}
|
|
}
|
|
|
|
moveResult = physicsObj.GetMoveResult();
|
|
if ( !af_push_moveables && attack.Length() && TestMelee() ) {
|
|
DirectDamage( attack, enemy.GetEntity() );
|
|
} else {
|
|
idEntity *blockEnt = physicsObj.GetSlideMoveEntity();
|
|
if ( blockEnt && blockEnt->IsType( hhHarvesterSimple::Type ) ) {
|
|
StopMove( MOVE_STATUS_BLOCKED_BY_MONSTER );
|
|
return;
|
|
}
|
|
if ( blockEnt && blockEnt->IsType( idMoveable::Type ) && blockEnt->GetPhysics()->IsPushable() ) {
|
|
KickObstacles( viewAxis[ 0 ], kickForce, blockEnt );
|
|
}
|
|
}
|
|
|
|
BlockedFailSafe();
|
|
|
|
AI_ONGROUND = physicsObj.OnGround();
|
|
|
|
idVec3 org = physicsObj.GetOrigin();
|
|
if ( oldorigin != org ) {
|
|
TouchTriggers();
|
|
}
|
|
|
|
if ( ai_debugMove.GetBool() ) {
|
|
gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), org, gameLocal.msec );
|
|
gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), move.moveDest, gameLocal.msec );
|
|
gameRenderWorld->DebugLine( colorYellow, org + EyeOffset(), org + EyeOffset() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true );
|
|
DrawRoute();
|
|
}
|
|
}
|
|
|
|
void hhHarvesterSimple::Event_CanBecomeSolid( void ) {
|
|
int i;
|
|
int num;
|
|
idEntity * hit;
|
|
idClipModel *cm;
|
|
idClipModel *clipModels[ MAX_GENTITIES ];
|
|
|
|
num = gameLocal.clip.ClipModelsTouchingBounds( physicsObj.GetAbsBounds(), MASK_MONSTERSOLID, clipModels, MAX_GENTITIES );
|
|
for ( i = 0; i < num; i++ ) {
|
|
cm = clipModels[ i ];
|
|
|
|
// don't check render entities
|
|
if ( cm->IsRenderModel() ) {
|
|
continue;
|
|
}
|
|
|
|
hit = cm->GetEntity();
|
|
if ( hit == this ) {
|
|
continue;
|
|
}
|
|
|
|
// Special case, crush any moveables blocking our start point
|
|
if ( hit->IsType( idMoveable::Type ) ) {
|
|
hit->Damage( this, this, vec3_origin, "damage_gib", 1.0f, INVALID_JOINT );
|
|
hit->SquishedByDoor(this);
|
|
}
|
|
|
|
if ( !hit->fl.takedamage ) {
|
|
continue;
|
|
}
|
|
|
|
if ( physicsObj.ClipContents( cm ) ) {
|
|
idThread::ReturnFloat( false );
|
|
return;
|
|
}
|
|
}
|
|
|
|
idThread::ReturnFloat( true );
|
|
}
|
|
|
|
#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build
|