Files
HL2Overcharged/game/server/overcharged/npc_basemortarsynth.cpp
2025-05-21 21:20:08 +03:00

1919 lines
55 KiB
C++

//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: Combine Alien Synth enemy
// 9 september 2021
//=============================================================================
#include "cbase.h"
#include "npcevent.h"
#include "ai_basenpc_physicsflyer.h"
#include "weapon_physcannon.h"
#include "hl2_player.h"
#include "npc_basemortarsynth.h"
#include "IEffects.h"
#include "explode.h"
#include "ai_route.h"
#include "gib.h" //added for dive bombmode explode gibs
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
ConVar g_debug_basemortarsynth("g_debug_basemortarsynth", "0", FCVAR_CHEAT);
BEGIN_DATADESC(CNPC_BaseMSynth)
DEFINE_EMBEDDED(m_KilledInfo),
DEFINE_SOUNDPATCH(m_pEngineSound),
DEFINE_FIELD(m_flFlyNoiseBase, FIELD_FLOAT),
DEFINE_FIELD(m_flEngineStallTime, FIELD_TIME),
DEFINE_FIELD(m_fNextFlySoundTime, FIELD_TIME),
DEFINE_FIELD(m_nFlyMode, FIELD_INTEGER),
DEFINE_FIELD(m_vecDiveBombDirection, FIELD_VECTOR),
DEFINE_FIELD(m_flDiveBombRollForce, FIELD_FLOAT),
// Physics Influence
DEFINE_FIELD(m_hPhysicsAttacker, FIELD_EHANDLE),
DEFINE_FIELD(m_flLastPhysicsInfluenceTime, FIELD_TIME),
DEFINE_FIELD(m_flGoalOverrideDistance, FIELD_FLOAT),
DEFINE_FIELD(m_flAttackNearDist, FIELD_FLOAT),
DEFINE_FIELD(m_flAttackFarDist, FIELD_FLOAT),
DEFINE_FIELD(m_flAttackRange, FIELD_FLOAT),
DEFINE_FIELD(m_nPoseTail, FIELD_INTEGER),
DEFINE_FIELD(m_nPoseDynamo, FIELD_INTEGER),
DEFINE_FIELD(m_nPoseFlare, FIELD_INTEGER),
DEFINE_FIELD(m_nPoseFaceVert, FIELD_INTEGER),
DEFINE_FIELD(m_nPoseFaceHoriz, FIELD_INTEGER),
// DEFINE_FIELD( m_bHasSpoken, FIELD_BOOLEAN ),
DEFINE_FIELD(m_pSmokeTrail, FIELD_CLASSPTR),
DEFINE_INPUTFUNC(FIELD_FLOAT, "SetDistanceOverride", InputSetDistanceOverride),
DEFINE_INPUTFUNC(FIELD_INTEGER, "SetFlightSpeed", InputSetFlightSpeed),
DEFINE_THINKFUNC(DiveBombSoundThink),
END_DATADESC()
ConVar sk_mortarsynth_bombmode_damage("sk_mortarsynth_bombmode_damage", "0");
//-----------------------------------------------------------------------------
// Think contexts
//-----------------------------------------------------------------------------
static const char *s_pDiveBombSoundThinkContextMsynth = "DiveBombSoundThinkContext";
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CNPC_BaseMSynth::CNPC_BaseMSynth()
{
#ifdef _DEBUG
m_vCurrentBanking.Init();
#endif
m_pEngineSound = NULL;
m_bHasSpoken = false;
m_flAttackNearDist = MSYNTH_ATTACK_NEAR_DIST;
m_flAttackFarDist = MSYNTH_ATTACK_FAR_DIST;
m_flAttackRange = MSYNTH_ATTACK_RANGE;
ReadDataFromFile(filesystem, this);
}
void CNPC_BaseMSynth::ReadDataFromFile(IFileSystem* filesystem, CBaseEntity *pCaller)
{
KeyValues *pkvProperties = new KeyValues("npc_mortarsynth");
//DevMsg("ReadDataFromFile: npc_mortarsynth.txt \n");
char ss[256];
Q_snprintf(ss, sizeof(ss), "scripts/npc/%s.txt", "npc_mortarsynth");
if (pkvProperties->LoadFromFile(filesystem, "scripts/npc/npc_mortarsynth.txt", "MOD"))
{
//DevMsg("LoadFromFile: npc_mortarsynth.txt \n");
if (pkvProperties)
{
//DevMsg("Loaded successfully\n");
KeyValues *pMain = pkvProperties->FindKey("Particles");
if (pMain)
{
if (strcmp("None", pMain->GetString("MSYNTH_PARTICLE_TRAIL", "None")) == 0)
Q_strncpy(MSYNTH_PARTICLE_TRAIL, "", sizeof(MSYNTH_PARTICLE_TRAIL));
else
{
Q_strncpy(MSYNTH_PARTICLE_TRAIL, pMain->GetString("MSYNTH_PARTICLE_TRAIL"), sizeof(MSYNTH_PARTICLE_TRAIL));
PrecacheParticleSystem(pMain->GetString("MSYNTH_PARTICLE_TRAIL", ""));
}
if (strcmp("None", pMain->GetString("MSYNTH_PARTICLE_GLOW", "None")) == 0)
Q_strncpy(MSYNTH_PARTICLE_GLOW, "", sizeof(MSYNTH_PARTICLE_GLOW));
else
{
Q_strncpy(MSYNTH_PARTICLE_GLOW, pMain->GetString("MSYNTH_PARTICLE_GLOW"), sizeof(MSYNTH_PARTICLE_GLOW));
PrecacheParticleSystem(pMain->GetString("MSYNTH_PARTICLE_GLOW", ""));
}
if (strcmp("None", pMain->GetString("MSYNTH_PARTICLE_EXPLOSION", "None")) == 0)
Q_strncpy(MSYNTH_PARTICLE_EXPLOSION, "", sizeof(MSYNTH_PARTICLE_EXPLOSION));
else
{
Q_strncpy(MSYNTH_PARTICLE_EXPLOSION, pMain->GetString("MSYNTH_PARTICLE_EXPLOSION"), sizeof(MSYNTH_PARTICLE_EXPLOSION));
PrecacheParticleSystem(pMain->GetString("MSYNTH_PARTICLE_EXPLOSION", ""));
}
//DevMsg("%s \n", MSYNTH_PARTICLE_TRAIL);
//DevMsg("%s \n", MSYNTH_PARTICLE_GLOW);
//DevMsg("%s \n", MSYNTH_PARTICLE_EXPLOSION);
}
}
}
pkvProperties->deleteThis();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::Spawn(void)
{
#ifdef _XBOX
// Always fade the corpse
AddSpawnFlags(SF_NPC_FADE_CORPSE);
AddEffects(EF_NOSHADOW);
#endif // _XBOX
SetHullType(HULL_TINY_CENTERED);
SetHullSizeNormal();
SetSolid(SOLID_BBOX);
AddSolidFlags(FSOLID_NOT_STANDABLE);
SetMoveType(MOVETYPE_VPHYSICS);
m_bloodColor = DONT_BLEED;
SetViewOffset(Vector(0, 0, 10)); // Position of the eyes relative to NPC's origin.
m_flFieldOfView = 0.2;
m_NPCState = NPC_STATE_NONE;
SetNavType(NAV_FLY);
AddFlag(FL_FLY);
// This entity cannot be dissolved by the combine balls,
// nor does it get killed by the mega physcannon.
AddEFlags(EFL_NO_DISSOLVE | EFL_NO_MEGAPHYSCANNON_RAGDOLL);
m_flGoalOverrideDistance = 0.0f;
m_nFlyMode = MSYNTH_FLY_PATROL;
AngleVectors(GetLocalAngles(), &m_vCurrentBanking);
m_fHeadYaw = 0;
m_pSmokeTrail = NULL;
SetCurrentVelocity(vec3_origin);
// Noise modifier
Vector bobAmount;
bobAmount.x = random->RandomFloat(-2.0f, 2.0f);
bobAmount.y = random->RandomFloat(-2.0f, 2.0f);
bobAmount.z = random->RandomFloat(2.0f, 4.0f);
if (random->RandomInt(0, 1))
{
bobAmount.z *= -1.0f;
}
SetNoiseMod(bobAmount);
// set flight speed
m_flSpeed = GetMaxSpeed();
// --------------------------------------------
CapabilitiesAdd(bits_CAP_MOVE_FLY | bits_CAP_SQUAD | bits_CAP_TURN_HEAD | bits_CAP_SKIP_NAV_GROUND_CHECK);
NPCInit();
m_flFlyNoiseBase = random->RandomFloat(0, M_PI);
m_flNextAttack = gpGlobals->curtime;
}
//-----------------------------------------------------------------------------
//
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::UpdateEfficiency(bool bInPVS)
{
SetEfficiency((GetSleepState() != AISS_AWAKE) ? AIE_DORMANT : AIE_NORMAL);
SetMoveEfficiency(AIME_NORMAL);
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
float CNPC_BaseMSynth::GetAutoAimRadius()
{
if (g_pGameRules->GetAutoAimMode() == AUTOAIM_ON_CONSOLE)
{
return 24.0f;
}
return 12.0f;
}
//-----------------------------------------------------------------------------
// Purpose: Called just before we are deleted.
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::UpdateOnRemove(void)
{
// Stop combat loops if I'm alive. If I'm dead, the die sound will already have stopped it.
if (IsAlive() && m_bHasSpoken)
{
SentenceStop();
}
BaseClass::UpdateOnRemove();
}
//-----------------------------------------------------------------------------
// Purpose: Gets the appropriate next schedule based on current condition
// bits.
//-----------------------------------------------------------------------------
int CNPC_BaseMSynth::SelectSchedule(void)
{
// ----------------------------------------------------
// If I'm dead, go into a dive bomb
// ----------------------------------------------------
if (m_iHealth <= 0)
{
m_flSpeed = MSYNTH_MAX_DIVE_BOMB_SPEED;
return SCHED_SCANNER_ATTACK_DIVEBOMB;
}
// -------------------------------
// If I'm in a script sequence
// -------------------------------
if (m_NPCState == NPC_STATE_SCRIPT)
return(BaseClass::SelectSchedule());
// -------------------------------
// Flinch
// -------------------------------
if (HasCondition(COND_LIGHT_DAMAGE) || HasCondition(COND_HEAVY_DAMAGE))
{
if (IsHeldByPhyscannon())
return SCHED_SMALL_FLINCH;
if (m_NPCState == NPC_STATE_IDLE)
return SCHED_SMALL_FLINCH;
if (m_NPCState == NPC_STATE_ALERT)
{
if (m_iHealth < (3 * m_iMaxHealth / 4))
return SCHED_TAKE_COVER_FROM_ORIGIN;
if (SelectWeightedSequence(ACT_SMALL_FLINCH) != -1)
return SCHED_SMALL_FLINCH;
}
else
{
if (random->RandomInt(0, 10) < 4)
return SCHED_SMALL_FLINCH;
}
}
// I'm being held by the physcannon... struggle!
if (IsHeldByPhyscannon())
return SCHED_SCANNER_HELD_BY_PHYSCANNON;
// ----------------------------------------------------------
// If I have an enemy
// ----------------------------------------------------------
if (GetEnemy() != NULL && GetEnemy()->IsAlive())
{
// Patrol if the enemy has vanished
if (HasCondition(COND_LOST_ENEMY))
return SCHED_SCANNER_PATROL;
// Chase via route if we're directly blocked
if (HasCondition(COND_SCANNER_FLY_BLOCKED))
return SCHED_SCANNER_CHASE_ENEMY;
// Attack if it's time
if (gpGlobals->curtime >= m_flNextAttack)
{
if (HasCondition(COND_CAN_MELEE_ATTACK1))
return SCHED_SCANNER_ATTACK;
}
// Otherwise fly in low for attack
return SCHED_SCANNER_ATTACK_HOVER;
}
// Default to patrolling around
return SCHED_SCANNER_PATROL;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::OnScheduleChange(void)
{
m_flSpeed = GetMaxSpeed();
BaseClass::OnScheduleChange();
}
//-----------------------------------------------------------------------------
// Purpose: For innate melee attack
//-----------------------------------------------------------------------------
int CNPC_BaseMSynth::MeleeAttack1Conditions(float flDot, float flDist)
{
if (GetEnemy() == NULL)
{
return COND_NONE;
}
// Check too far to attack with 2D distance
float vEnemyDist2D = (GetEnemy()->GetLocalOrigin() - GetLocalOrigin()).Length2D();
if (m_flNextAttack > gpGlobals->curtime)
{
return COND_NONE;
}
else if (vEnemyDist2D > m_flAttackRange)
{
return COND_TOO_FAR_TO_ATTACK;
}
else if (flDot < 0.7)
{
return COND_NOT_FACING_ATTACK;
}
return COND_CAN_MELEE_ATTACK1;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : eOldState -
// eNewState -
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::OnStateChange(NPC_STATE eOldState, NPC_STATE eNewState)
{
if ((eNewState == NPC_STATE_ALERT) || (eNewState == NPC_STATE_COMBAT))
{
SetPoseParameter(m_nPoseFlare, 1.0f);
}
else
{
SetPoseParameter(m_nPoseFlare, 0);
}
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : pTask -
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::StartTask(const Task_t *pTask)
{
switch (pTask->iTask)
{
case TASK_SCANNER_SET_FLY_PATROL:
{
// Fly in patrol mode and clear any
// remaining target entity
m_nFlyMode = MSYNTH_FLY_PATROL;
TaskComplete();
break;
}
case TASK_SCANNER_SET_FLY_CHASE:
{
m_nFlyMode = MSYNTH_FLY_CHASE;
TaskComplete();
break;
}
case TASK_SCANNER_SET_FLY_ATTACK:
{
m_nFlyMode = MSYNTH_FLY_ATTACK;
TaskComplete();
break;
}
case TASK_SCANNER_SET_FLY_DIVE:
{
// Pick a direction to divebomb.
if (GetEnemy() != NULL)
{
// Fly towards my enemy
Vector vEnemyPos = GetEnemyLKP();
m_vecDiveBombDirection = vEnemyPos - GetLocalOrigin();
}
else
{
// Pick a random forward and down direction.
Vector forward;
GetVectors(&forward, NULL, NULL);
m_vecDiveBombDirection = forward + Vector(random->RandomFloat(-10, 10), random->RandomFloat(-10, 10), random->RandomFloat(-20, -10));
}
VectorNormalize(m_vecDiveBombDirection);
// Calculate a roll force.
m_flDiveBombRollForce = random->RandomFloat(20.0, 420.0);
if (random->RandomInt(0, 1))
{
m_flDiveBombRollForce *= -1;
}
DiveBombSoundThink();
m_nFlyMode = MSYNTH_FLY_DIVE;
TaskComplete();
break;
}
default:
BaseClass::StartTask(pTask);
break;
}
}
//------------------------------------------------------------------------------
// Purpose: Override to split in two when attacked
//------------------------------------------------------------------------------
int CNPC_BaseMSynth::OnTakeDamage_Alive(const CTakeDamageInfo &info)
{
// Start smoking when we're nearly dead
if (m_iHealth < (m_iMaxHealth - (m_iMaxHealth / 4)))
{
StartSmokeTrail();
}
return (BaseClass::OnTakeDamage_Alive(info));
}
//------------------------------------------------------------------------------
// Purpose: Override to split in two when attacked
//------------------------------------------------------------------------------
int CNPC_BaseMSynth::OnTakeDamage_Dying(const CTakeDamageInfo &info)
{
// do the damage
m_iHealth -= info.GetDamage();
if (m_iHealth < -40)
{
Gib();
return 1;
}
return VPhysicsTakeDamage(info);
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::TraceAttack(const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator)
{
if (info.GetDamageType() & DMG_BULLET)
{
g_pEffects->Ricochet(ptr->endpos, ptr->plane.normal);
}
BaseClass::TraceAttack(info, vecDir, ptr, pAccumulator);
}
//-----------------------------------------------------------------------------
// Take damage from being thrown by a physcannon
//-----------------------------------------------------------------------------
#define SCANNER_SMASH_SPEED 250.0 // How fast a scanner must slam into something to take full damage
void CNPC_BaseMSynth::TakeDamageFromPhyscannon(CBasePlayer *pPlayer)
{
CTakeDamageInfo info;
info.SetDamageType(DMG_GENERIC);
info.SetInflictor(this);
info.SetAttacker(pPlayer);
info.SetDamagePosition(GetAbsOrigin());
info.SetDamageForce(Vector(1.0, 1.0, 1.0));
// Convert velocity into damage.
Vector vel;
VPhysicsGetObject()->GetVelocity(&vel, NULL);
float flSpeed = vel.Length();
float flFactor = flSpeed / SCANNER_SMASH_SPEED;
// Clamp. Don't inflict negative damage or massive damage!
flFactor = clamp(flFactor, 0.0f, 2.0f);
float flDamage = m_iMaxHealth * flFactor;
#if 0
Msg("Doing %f damage for %f speed!\n", flDamage, flSpeed);
#endif
info.SetDamage(flDamage);
TakeDamage(info);
}
//-----------------------------------------------------------------------------
// Take damage from physics impacts
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::TakeDamageFromPhysicsImpact(int index, gamevcollisionevent_t *pEvent)
{
CBaseEntity *pHitEntity = pEvent->pEntities[!index];
// NOTE: Augment the normal impact energy scale here.
float flDamageScale = PlayerHasMegaPhysCannon() ? 10.0f : 5.0f;
// Scale by the mapmaker's energyscale
flDamageScale *= m_impactEnergyScale;
int damageType = 0;
float damage = CalculateDefaultPhysicsDamage(index, pEvent, flDamageScale, true, damageType);
if (damage == 0)
return;
Vector damagePos;
pEvent->pInternalData->GetContactPoint(damagePos);
Vector damageForce = pEvent->postVelocity[index] * pEvent->pObjects[index]->GetMass();
if (damageForce == vec3_origin)
{
// This can happen if this entity is motion disabled, and can't move.
// Use the velocity of the entity that hit us instead.
damageForce = pEvent->postVelocity[!index] * pEvent->pObjects[!index]->GetMass();
}
// FIXME: this doesn't pass in who is responsible if some other entity "caused" this collision
PhysCallbackDamage(this, CTakeDamageInfo(pHitEntity, pHitEntity, damageForce, damagePos, damage, damageType), *pEvent, index);
}
//-----------------------------------------------------------------------------
// Is the scanner being held?
//-----------------------------------------------------------------------------
bool CNPC_BaseMSynth::IsHeldByPhyscannon()
{
return VPhysicsGetObject() && (VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD);
}
//------------------------------------------------------------------------------
// Physics impact
//------------------------------------------------------------------------------
#define SCANNER_SMASH_TIME 0.75 // How long after being thrown from a physcannon that a manhack is eligible to die from impact
void CNPC_BaseMSynth::VPhysicsCollision(int index, gamevcollisionevent_t *pEvent)
{
BaseClass::VPhysicsCollision(index, pEvent);
// Take no impact damage while being carried.
if (IsHeldByPhyscannon())
return;
CBasePlayer *pPlayer = HasPhysicsAttacker(SCANNER_SMASH_TIME);
if (pPlayer)
{
TakeDamageFromPhyscannon(pPlayer);
return;
}
// It also can take physics damage from things thrown by the player.
int otherIndex = !index;
CBaseEntity *pHitEntity = pEvent->pEntities[otherIndex];
if (pHitEntity)
{
if (pHitEntity->HasPhysicsAttacker(0.5f))
{
// It can take physics damage from things thrown by the player.
TakeDamageFromPhysicsImpact(index, pEvent);
}
else if (FClassnameIs(pHitEntity, "prop_combine_ball"))
{
// It also can take physics damage from a combine ball.
TakeDamageFromPhysicsImpact(index, pEvent);
}
}
}
//------------------------------------------------------------------------------
// Purpose:
//------------------------------------------------------------------------------
void CNPC_BaseMSynth::Gib(void)
{
if (IsMarkedForDeletion())
return;
CGib::SpawnSpecificGibs(this, 1, 500, 250, "models/gibs/Shield_Scanner_Gib1.mdl");
CGib::SpawnSpecificGibs(this, 1, 500, 250, "models/gibs/Shield_Scanner_Gib4.mdl");
CGib::SpawnSpecificGibs(this, 1, 500, 250, "models/gibs/Shield_Scanner_Gib5.mdl");
CGib::SpawnSpecificGibs(this, 1, 500, 250, "models/gibs/Shield_Scanner_Gib6.mdl");
CGib::SpawnSpecificGibs(this, 1, 500, 250, "models/gibs/scanner_gib04.mdl");
/*CGib::SpawnSpecificGibs(this, 1, 500, 250, "models/gibs/manhack_gib01.mdl");
CGib::SpawnSpecificGibs(this, 1, 500, 250, "models/gibs/manhack_gib02.mdl");
CGib::SpawnSpecificGibs(this, 1, 500, 250, "models/gibs/manhack_gib03.mdl");
CGib::SpawnSpecificGibs(this, 1, 500, 250, "models/gibs/manhack_gib04.mdl");*/
// Sparks
for (int i = 0; i < 4; i++)
{
Vector sparkPos = GetAbsOrigin();
sparkPos.x += random->RandomFloat(-12, 12);
sparkPos.y += random->RandomFloat(-12, 12);
sparkPos.z += random->RandomFloat(-12, 12);
g_pEffects->Sparks(sparkPos);
}
// Light
CBroadcastRecipientFilter filter;
te->DynamicLight(filter, 0.0, &WorldSpaceCenter(), 255, 180, 100, 0, 100, 0.1, 0);
// Cover the gib spawn
ExplosionCreate(WorldSpaceCenter(), GetAbsAngles(), this, 64, 64, false, true);
// Turn off any smoke trail
if (m_pSmokeTrail)
{
m_pSmokeTrail->m_ParticleLifetime = 0;
UTIL_Remove(m_pSmokeTrail);
m_pSmokeTrail = NULL;
}
// FIXME: This is because we couldn't save/load the CTakeDamageInfo.
// because it's midnight before the teamwide playtest. Real solution
// is to add a datadesc to CTakeDamageInfo
if (m_KilledInfo.GetInflictor())
{
BaseClass::Event_Killed(m_KilledInfo);
}
UTIL_Remove(this);
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : *pPhysGunUser -
// bPunting -
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::OnPhysGunPickup(CBasePlayer *pPhysGunUser, PhysGunPickup_t reason)
{
m_hPhysicsAttacker = pPhysGunUser;
m_flLastPhysicsInfluenceTime = gpGlobals->curtime;
if (reason == PUNTED_BY_CANNON)
{
// There's about to be a massive change in velocity.
// Think immediately to handle changes in m_vCurrentVelocity;
SetNextThink(gpGlobals->curtime + 0.01f);
m_flEngineStallTime = gpGlobals->curtime + 2.0f;
ScannerEmitSound("DiveBomb");
}
else
{
SetCondition(COND_SCANNER_GRABBED_BY_PHYSCANNON);
ClearCondition(COND_SCANNER_RELEASED_FROM_PHYSCANNON);
}
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : *pPhysGunUser -
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::OnPhysGunDrop(CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason)
{
m_hPhysicsAttacker = pPhysGunUser;
m_flLastPhysicsInfluenceTime = gpGlobals->curtime;
ClearCondition(COND_SCANNER_GRABBED_BY_PHYSCANNON);
SetCondition(COND_SCANNER_RELEASED_FROM_PHYSCANNON);
if (Reason == LAUNCHED_BY_CANNON)
{
m_flEngineStallTime = gpGlobals->curtime + 2.0f;
// There's about to be a massive change in velocity.
// Think immediately to handle changes in m_vCurrentVelocity;
SetNextThink(gpGlobals->curtime + 0.01f);
ScannerEmitSound("DiveBomb");
}
}
//------------------------------------------------------------------------------
// Do we have a physics attacker?
//------------------------------------------------------------------------------
CBasePlayer *CNPC_BaseMSynth::HasPhysicsAttacker(float dt)
{
// If the player is holding me now, or I've been recently thrown
// then return a pointer to that player
if (IsHeldByPhyscannon() || (gpGlobals->curtime - dt <= m_flLastPhysicsInfluenceTime))
{
return m_hPhysicsAttacker;
}
return NULL;
}
//------------------------------------------------------------------------------
// Purpose:
//------------------------------------------------------------------------------
void CNPC_BaseMSynth::StopLoopingSounds(void)
{
CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController();
controller.SoundDestroy(m_pEngineSound);
m_pEngineSound = NULL;
BaseClass::StopLoopingSounds();
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : pInflictor -
// pAttacker -
// flDamage -
// bitsDamageType -
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::Event_Killed(const CTakeDamageInfo &info)
{
// Copy off the takedamage info that killed me, since we're not going to call
// up into the base class's Event_Killed() until we gib. (gibbing is ultimate death)
m_KilledInfo = info;
// Interrupt whatever schedule I'm on
SetCondition(COND_SCHEDULE_DONE);
// If I have an enemy and I'm up high, do a dive bomb (unless dissolved)
if (GetEnemy() != NULL && (info.GetDamageType() & DMG_DISSOLVE) == false)
{
Vector vecDelta = GetLocalOrigin() - GetEnemy()->GetLocalOrigin();
if ((vecDelta.z > 120) && (vecDelta.Length() > 360))
{
// If I'm divebombing, don't take any more damage. It will make Event_Killed() be called again.
// This is especially bad if someone machineguns the divebombing scanner.
AttackDivebomb();
return;
}
}
Gib();
}
//------------------------------------------------------------------------------
// Purpose:
//------------------------------------------------------------------------------
void CNPC_BaseMSynth::AttackDivebomb(void)
{
ScannerEmitSound("DiveBomb");
m_takedamage = DAMAGE_NO;
StartSmokeTrail();
}
//------------------------------------------------------------------------------
// Purpose: Checks to see if we hit anything while dive bombing.
//------------------------------------------------------------------------------
void CNPC_BaseMSynth::AttackDivebombCollide(float flInterval)
{
//
// Trace forward to see if I hit anything
//
Vector checkPos = GetAbsOrigin() + (GetCurrentVelocity() * flInterval);
trace_t tr;
CBaseEntity* pHitEntity = NULL;
AI_TraceHull(GetAbsOrigin(), checkPos, GetHullMins(), GetHullMaxs(), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr);
if (tr.m_pEnt)
{
pHitEntity = tr.m_pEnt;
// Did I hit an entity that isn't another scanner?
if (pHitEntity && pHitEntity->Classify() != CLASS_SCANNER)
{
if (!pHitEntity->ClassMatches("item_battery"))
{
//if (!pHitEntity->IsWorld())
//{
//CTakeDamageInfo info(this, this, sk_mortarsynth_bombmode_damage.GetFloat(), DMG_CLUB);
//CalculateMeleeDamageForce(&info, (tr.endpos - tr.startpos), tr.endpos);
//pHitEntity->TakeDamage(info);
// BJ since it only take damage ON TOUCH player i changed to radius method
// BJ Not every time works, sometimes world hit rather than player so i moved under
//}
DevMsg("Msynth bombmode dmg \n");
RadiusDamage(CTakeDamageInfo(this, this, sk_mortarsynth_bombmode_damage.GetFloat(), DMG_BLAST), GetAbsOrigin(), 256, CLASS_COMBINE, NULL); // BJ Self Explode damage
Gib();
}
}
}
if (tr.fraction != 1.0)
{
// We've hit something so deflect our velocity based on the surface
// norm of what we've hit
if (flInterval > 0)
{
float moveLen = (1.0 - tr.fraction)*(GetAbsOrigin() - checkPos).Length();
Vector vBounceVel = moveLen*tr.plane.normal / flInterval;
// If I'm right over the ground don't push down
if (vBounceVel.z < 0)
{
float floorZ = GetFloorZ(GetAbsOrigin());
if (abs(GetAbsOrigin().z - floorZ) < 36)
{
vBounceVel.z = 0;
}
}
SetCurrentVelocity(GetCurrentVelocity() + vBounceVel);
}
CBaseCombatCharacter* pBCC = ToBaseCombatCharacter(pHitEntity);
if (pBCC)
{
// Spawn some extra blood where we hit
SpawnBlood(tr.endpos, g_vecAttackDir, pBCC->BloodColor(), sk_mortarsynth_bombmode_damage.GetFloat());
}
else
{
if (!(m_spawnflags & SF_NPC_GAG))
{
// <<TEMP>> need better sound here...
ScannerEmitSound("Shoot");
}
// For sparks we must trace a line in the direction of the surface norm
// that we hit.
checkPos = GetAbsOrigin() - (tr.plane.normal * 24);
AI_TraceLine(GetAbsOrigin(), checkPos, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr);
if (tr.fraction != 1.0)
{
g_pEffects->Sparks(tr.endpos);
CBroadcastRecipientFilter filter;
te->DynamicLight(filter, 0.0,
&GetAbsOrigin(), 255, 180, 100, 0, 50, 0.1, 0);
}
}
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::PlayFlySound(void)
{
if (IsMarkedForDeletion())
return;
CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController();
//Setup the sound if we're not already
if (m_pEngineSound == NULL)
{
// Create the sound
CPASAttenuationFilter filter(this);
m_pEngineSound = controller.SoundCreate(filter, entindex(), CHAN_STATIC, GetEngineSound(), ATTN_NORM);
Assert(m_pEngineSound);
// Start the engine sound
controller.Play(m_pEngineSound, 0.0f, 100.0f);
controller.SoundChangeVolume(m_pEngineSound, 1.0f, 2.0f);
}
float speed = GetCurrentVelocity().Length();
float flVolume = 0.25f + (0.75f*(speed / GetMaxSpeed()));
int iPitch = MIN(255, 80 + (20 * (speed / GetMaxSpeed())));
//Update our pitch and volume based on our speed
controller.SoundChangePitch(m_pEngineSound, iPitch, 0.1f);
controller.SoundChangeVolume(m_pEngineSound, flVolume, 0.1f);
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::ScannerEmitSound(const char *pszSoundName)
{
//CFmtStr snd;
//snd.sprintf("%s.%s", GetScannerSoundPrefix(), pszSoundName);
m_bHasSpoken = true;
//EmitSound(snd.Access());
EmitSound(pszSoundName);
}
//------------------------------------------------------------------------------
// Purpose:
//------------------------------------------------------------------------------
void CNPC_BaseMSynth::SpeakSentence(int sentenceType)
{
if (sentenceType == MSYNTH_SENTENCE_ATTENTION)
{
ScannerEmitSound(MSYNTH_SPEECH1_SOUND);
}
else if (sentenceType == MSYNTH_SENTENCE_HANDSUP)
{
ScannerEmitSound(MSYNTH_SPEECH2_SOUND);
}
else if (sentenceType == MSYNTH_SENTENCE_PROCEED)
{
ScannerEmitSound(MSYNTH_SPEECH3_SOUND);
}
else if (sentenceType == MSYNTH_SENTENCE_CURIOUS)
{
ScannerEmitSound(MSYNTH_SPEECH4_SOUND);
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::InputSetFlightSpeed(inputdata_t &inputdata)
{
//FIXME: Currently unsupported
/*
m_flFlightSpeed = inputdata.value.Int();
m_bFlightSpeedOverridden = (m_flFlightSpeed > 0);
*/
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::StartSmokeTrail(void)
{
if (m_pSmokeTrail != NULL)
return;
m_pSmokeTrail = SmokeTrail::CreateSmokeTrail();
if (m_pSmokeTrail)
{
m_pSmokeTrail->m_SpawnRate = 10;
m_pSmokeTrail->m_ParticleLifetime = 1;
m_pSmokeTrail->m_StartSize = 8;
m_pSmokeTrail->m_EndSize = 50;
m_pSmokeTrail->m_SpawnRadius = 10;
m_pSmokeTrail->m_MinSpeed = 15;
m_pSmokeTrail->m_MaxSpeed = 25;
m_pSmokeTrail->m_StartColor.Init(0.5f, 0.5f, 0.5f);
m_pSmokeTrail->m_EndColor.Init(0, 0, 0);
m_pSmokeTrail->SetLifetime(500.0f);
m_pSmokeTrail->FollowEntity(this);
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::BlendPhyscannonLaunchSpeed()
{
// Blend out desired velocity when launched by the physcannon
if (!VPhysicsGetObject())
return;
if (HasPhysicsAttacker(SCANNER_SMASH_TIME) && !IsHeldByPhyscannon())
{
Vector vecCurrentVelocity;
VPhysicsGetObject()->GetVelocity(&vecCurrentVelocity, NULL);
float flLerpFactor = (gpGlobals->curtime - m_flLastPhysicsInfluenceTime) / SCANNER_SMASH_TIME;
flLerpFactor = clamp(flLerpFactor, 0.0f, 1.0f);
flLerpFactor = SimpleSplineRemapVal(flLerpFactor, 0.0f, 1.0f, 0.0f, 1.0f);
flLerpFactor *= flLerpFactor;
VectorLerp(vecCurrentVelocity, m_vCurrentVelocity, flLerpFactor, m_vCurrentVelocity);
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::MoveExecute_Alive(float flInterval)
{
// Amount of noise to add to flying
float noiseScale = 3.0f;
// -------------------------------------------
// Avoid obstacles, unless I'm dive bombing
// -------------------------------------------
if (m_nFlyMode != MSYNTH_FLY_DIVE)
{
SetCurrentVelocity(GetCurrentVelocity() + VelocityToAvoidObstacles(flInterval));
}
// If I am dive bombing add more noise to my flying
else
{
AttackDivebombCollide(flInterval);
noiseScale *= 4;
}
IPhysicsObject *pPhysics = VPhysicsGetObject();
if (pPhysics && pPhysics->IsAsleep())
{
pPhysics->Wake();
}
// Add time-coherent noise to the current velocity so that it never looks bolted in place.
AddNoiseToVelocity(noiseScale);
AdjustScannerVelocity();
float maxSpeed = GetEnemy() ? (GetMaxSpeed() * 2.0f) : GetMaxSpeed();
if (m_nFlyMode == MSYNTH_FLY_DIVE)
{
maxSpeed = -1;
}
// Limit fall speed
LimitSpeed(maxSpeed);
// Blend out desired velocity when launched by the physcannon
BlendPhyscannonLaunchSpeed();
// Update what we're looking at
UpdateHead(flInterval);
// Control the tail based on our vertical travel
float tailPerc = clamp(GetCurrentVelocity().z, -150, 250);
tailPerc = SimpleSplineRemapVal(tailPerc, -150, 250, -25, 80);
SetPoseParameter(m_nPoseTail, tailPerc);
// Spin the dynamo based upon our speed
float flCurrentDynamo = GetPoseParameter(m_nPoseDynamo);
float speed = GetCurrentVelocity().Length();
float flDynamoSpeed = (maxSpeed > 0 ? speed / maxSpeed : 1.0) * 60;
flCurrentDynamo -= flDynamoSpeed;
if (flCurrentDynamo < -180.0)
{
flCurrentDynamo += 360.0;
}
SetPoseParameter(m_nPoseDynamo, flCurrentDynamo);
PlayFlySound();
}
//-----------------------------------------------------------------------------
// Purpose: Handles movement towards the last move target.
// Input : flInterval -
//-----------------------------------------------------------------------------
bool CNPC_BaseMSynth::OverridePathMove(CBaseEntity *pMoveTarget, float flInterval)
{
// Save our last patrolling direction
Vector lastPatrolDir = GetNavigator()->GetCurWaypointPos() - GetAbsOrigin();
// Continue on our path
if (ProgressFlyPath(flInterval, pMoveTarget, (MASK_NPCSOLID | CONTENTS_WATER), false, 64) == AINPP_COMPLETE)
{
if (IsCurSchedule(SCHED_SCANNER_PATROL))
{
m_vLastPatrolDir = lastPatrolDir;
VectorNormalize(m_vLastPatrolDir);
}
return true;
}
return false;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : flInterval -
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CNPC_BaseMSynth::OverrideMove(float flInterval)
{
// ----------------------------------------------
// If dive bombing
// ----------------------------------------------
if (m_nFlyMode == MSYNTH_FLY_DIVE)
{
MoveToDivebomb(flInterval);
}
else
{
Vector vMoveTargetPos(0, 0, 0);
CBaseEntity *pMoveTarget = NULL;
// The original line of code was, due to the accidental use of '|' instead of
// '&', always true. Replacing with 'true' to suppress the warning without changing
// the (long-standing) behavior.
if (true) //!GetNavigator()->IsGoalActive() || ( GetNavigator()->GetCurWaypointFlags() | bits_WP_TO_PATHCORNER ) )
{
// Select move target
if (GetTarget() != NULL)
{
pMoveTarget = GetTarget();
}
else if (GetEnemy() != NULL)
{
pMoveTarget = GetEnemy();
}
// Select move target position
if (GetEnemy() != NULL)
{
vMoveTargetPos = GetEnemy()->GetAbsOrigin();
}
}
else
{
vMoveTargetPos = GetNavigator()->GetCurWaypointPos();
}
ClearCondition(COND_SCANNER_FLY_CLEAR);
ClearCondition(COND_SCANNER_FLY_BLOCKED);
// See if we can fly there directly
if (pMoveTarget)
{
trace_t tr;
AI_TraceHull(GetAbsOrigin(), vMoveTargetPos, GetHullMins(), GetHullMaxs(), MASK_NPCSOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr);
float fTargetDist = (1.0f - tr.fraction)*(GetAbsOrigin() - vMoveTargetPos).Length();
if ((tr.m_pEnt == pMoveTarget) || (fTargetDist < 50))
{
if (g_debug_basemortarsynth.GetBool())
{
NDebugOverlay::Line(GetLocalOrigin(), vMoveTargetPos, 0, 255, 0, true, 0);
NDebugOverlay::Cross3D(tr.endpos, Vector(-5, -5, -5), Vector(5, 5, 5), 0, 255, 0, true, 0.1);
}
SetCondition(COND_SCANNER_FLY_CLEAR);
}
else
{
//HANDY DEBUG TOOL
if (g_debug_basemortarsynth.GetBool())
{
NDebugOverlay::Line(GetLocalOrigin(), vMoveTargetPos, 255, 0, 0, true, 0);
NDebugOverlay::Cross3D(tr.endpos, Vector(-5, -5, -5), Vector(5, 5, 5), 255, 0, 0, true, 0.1);
}
SetCondition(COND_SCANNER_FLY_BLOCKED);
}
}
// If I have a route, keep it updated and move toward target
if (GetNavigator()->IsGoalActive())
{
if (OverridePathMove(pMoveTarget, flInterval))
{
BlendPhyscannonLaunchSpeed();
return true;
}
}
// ----------------------------------------------
// If attacking
// ----------------------------------------------
else if (m_nFlyMode == MSYNTH_FLY_ATTACK)
{
MoveToAttack(flInterval);
}
// -----------------------------------------------------------------
// If I don't have a route, just decelerate
// -----------------------------------------------------------------
else if (!GetNavigator()->IsGoalActive())
{
float myDecay = 9.5;
Decelerate(flInterval, myDecay);
}
}
MoveExecute_Alive(flInterval);
return true;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : &goalPos -
// &startPos -
// idealRange -
// idealHeight -
// Output : Vector
//-----------------------------------------------------------------------------
Vector CNPC_BaseMSynth::IdealGoalForMovement(const Vector &goalPos, const Vector &startPos, float idealRange, float idealHeightDiff)
{
Vector vMoveDir;
if (GetGoalDirection(&vMoveDir) == false)
{
vMoveDir = (goalPos - startPos);
vMoveDir.z = 0;
VectorNormalize(vMoveDir);
}
// Move up from the position by the desired amount
Vector vIdealPos = goalPos + Vector(0, 0, idealHeightDiff) + (vMoveDir * -idealRange);
// Trace down and make sure we can fit here
trace_t tr;
AI_TraceHull(vIdealPos, vIdealPos - Vector(0, 0, MinGroundDist()), GetHullMins(), GetHullMaxs(), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr);
// Move up otherwise
if (tr.fraction < 1.0f)
{
vIdealPos.z += (MinGroundDist() * (1.0f - tr.fraction));
}
//FIXME: We also need to make sure that we fit here at all, and if not, chose a new spot
// Debug tools
if (g_debug_basemortarsynth.GetBool())
{
NDebugOverlay::Cross3D(goalPos, -Vector(8, 8, 8), Vector(8, 8, 8), 255, 255, 0, true, 0.1f);
NDebugOverlay::Cross3D(startPos, -Vector(8, 8, 8), Vector(8, 8, 8), 255, 0, 255, true, 0.1f);
NDebugOverlay::Cross3D(vIdealPos, -Vector(8, 8, 8), Vector(8, 8, 8), 255, 255, 255, true, 0.1f);
NDebugOverlay::Line(startPos, goalPos, 0, 255, 0, true, 0.1f);
NDebugOverlay::Cross3D(goalPos + (vMoveDir * -idealRange), -Vector(8, 8, 8), Vector(8, 8, 8), 255, 255, 255, true, 0.1f);
NDebugOverlay::Line(goalPos, goalPos + (vMoveDir * -idealRange), 255, 255, 0, true, 0.1f);
NDebugOverlay::Line(goalPos + (vMoveDir * -idealRange), vIdealPos, 255, 255, 0, true, 0.1f);
}
return vIdealPos;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : flInterval -
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::MoveToAttack(float flInterval)
{
if (GetEnemy() == NULL)
return;
if (flInterval <= 0)
return;
Vector vTargetPos = GetEnemyLKP();
//float flDesiredDist = m_flAttackNearDist + ( ( m_flAttackFarDist - m_flAttackNearDist ) / 2 );
Vector idealPos = IdealGoalForMovement(vTargetPos, GetAbsOrigin(), GetGoalDistance(), m_flAttackNearDist);
MoveToTarget(flInterval, idealPos);
//FIXME: Re-implement?
/*
// ---------------------------------------------------------
// Add evasion if I have taken damage recently
// ---------------------------------------------------------
if ((m_flLastDamageTime + SCANNER_EVADE_TIME) > gpGlobals->curtime)
{
vFlyDirection = vFlyDirection + VelocityToEvade(GetEnemyCombatCharacterPointer());
}
*/
}
//-----------------------------------------------------------------------------
// Purpose: Accelerates toward a given position.
// Input : flInterval - Time interval over which to move.
// vecMoveTarget - Position to move toward.
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::MoveToTarget(float flInterval, const Vector &vecMoveTarget)
{
// Don't move if stalling
if (m_flEngineStallTime > gpGlobals->curtime)
return;
// Look at our inspection target if we have one
if (GetEnemy() != NULL)
{
// Otherwise at our enemy
TurnHeadToTarget(flInterval, GetEnemy()->EyePosition());
}
else
{
// Otherwise face our motion direction
TurnHeadToTarget(flInterval, vecMoveTarget);
}
// -------------------------------------
// Move towards our target
// -------------------------------------
float myAccel;
float myZAccel = 400.0f;
float myDecay = 0.15f;
Vector vecCurrentDir;
// Get the relationship between my current velocity and the way I want to be going.
vecCurrentDir = GetCurrentVelocity();
VectorNormalize(vecCurrentDir);
Vector targetDir = vecMoveTarget - GetAbsOrigin();
float flDist = VectorNormalize(targetDir);
float flDot;
flDot = DotProduct(targetDir, vecCurrentDir);
if (flDot > 0.25)
{
// If my target is in front of me, my flight model is a bit more accurate.
myAccel = 250;
}
else
{
// Have a harder time correcting my course if I'm currently flying away from my target.
myAccel = 128;
}
if (myAccel > flDist / flInterval)
{
myAccel = flDist / flInterval;
}
if (myZAccel > flDist / flInterval)
{
myZAccel = flDist / flInterval;
}
MoveInDirection(flInterval, targetDir, myAccel, myZAccel, myDecay);
// calc relative banking targets
Vector forward, right, up;
GetVectors(&forward, &right, &up);
m_vCurrentBanking.x = targetDir.x;
m_vCurrentBanking.z = 120.0f * DotProduct(right, targetDir);
m_vCurrentBanking.y = 0;
float speedPerc = SimpleSplineRemapVal(GetCurrentVelocity().Length(), 0.0f, GetMaxSpeed(), 0.0f, 1.0f);
speedPerc = clamp(speedPerc, 0.0f, 1.0f);
m_vCurrentBanking *= speedPerc;
}
//-----------------------------------------------------------------------------
// Danger sounds.
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::DiveBombSoundThink()
{
Vector vecPosition, vecVelocity;
IPhysicsObject *pPhysicsObject = VPhysicsGetObject();
if (pPhysicsObject == NULL)
return;
pPhysicsObject->GetPosition(&vecPosition, NULL);
pPhysicsObject->GetVelocity(&vecVelocity, NULL);
CBasePlayer *pPlayer = AI_GetSinglePlayer();
if (pPlayer)
{
Vector vecDelta;
VectorSubtract(pPlayer->GetAbsOrigin(), vecPosition, vecDelta);
VectorNormalize(vecDelta);
if (DotProduct(vecDelta, vecVelocity) > 0.5f)
{
Vector vecEndPoint;
VectorMA(vecPosition, 2.0f * TICK_INTERVAL, vecVelocity, vecEndPoint);
float flDist = CalcDistanceToLineSegment(pPlayer->GetAbsOrigin(), vecPosition, vecEndPoint);
if (flDist < 200.0f)
{
ScannerEmitSound("DiveBombFlyby");
SetContextThink(&CNPC_BaseMSynth::DiveBombSoundThink, gpGlobals->curtime + 0.5f, s_pDiveBombSoundThinkContextMsynth);
return;
}
}
}
SetContextThink(&CNPC_BaseMSynth::DiveBombSoundThink, gpGlobals->curtime + 2.0f * TICK_INTERVAL, s_pDiveBombSoundThinkContextMsynth);
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : flInterval -
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::MoveToDivebomb(float flInterval)
{
float myAccel = 1600;
float myDecay = 0.05f; // decay current velocity to 10% in 1 second
// Fly towards my enemy
Vector vEnemyPos = GetEnemyLKP();
Vector vFlyDirection = vEnemyPos - GetLocalOrigin();
VectorNormalize(vFlyDirection);
// Set net velocity
MoveInDirection(flInterval, m_vecDiveBombDirection, myAccel, myAccel, myDecay);
// Spin out of control.
Vector forward;
VPhysicsGetObject()->LocalToWorldVector(&forward, Vector(1.0, 0.0, 0.0));
AngularImpulse torque = forward * m_flDiveBombRollForce;
VPhysicsGetObject()->ApplyTorqueCenter(torque);
// BUGBUG: why Y axis and not Z?
Vector up;
VPhysicsGetObject()->LocalToWorldVector(&up, Vector(0.0, 1.0, 0.0));
VPhysicsGetObject()->ApplyForceCenter(up * 2000);
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CNPC_BaseMSynth::IsEnemyPlayerInSuit()
{
if (GetEnemy() && GetEnemy()->IsPlayer())
{
CHL2_Player *pPlayer = NULL;
pPlayer = (CHL2_Player *)GetEnemy();
if (pPlayer && pPlayer->IsSuitEquipped())
{
return true;
}
}
return false;
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : float
//-----------------------------------------------------------------------------
float CNPC_BaseMSynth::GetGoalDistance(void)
{
if (m_flGoalOverrideDistance != 0.0f)
return m_flGoalOverrideDistance;
switch (m_nFlyMode)
{
case MSYNTH_FLY_ATTACK:
{
float goalDist = (m_flAttackNearDist + ((m_flAttackFarDist - m_flAttackNearDist) / 2));
if (IsEnemyPlayerInSuit())
{
goalDist *= 0.5;
}
return goalDist;
}
break;
}
return 128.0f;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : &vOut -
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CNPC_BaseMSynth::GetGoalDirection(Vector *vOut)
{
CBaseEntity *pTarget = GetTarget();
if (pTarget == NULL)
return false;
if (FClassnameIs(pTarget, "info_hint_air") || FClassnameIs(pTarget, "info_target"))
{
AngleVectors(pTarget->GetAbsAngles(), vOut);
return true;
}
return false;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
Vector CNPC_BaseMSynth::VelocityToEvade(CBaseCombatCharacter *pEnemy)
{
if (pEnemy)
{
// -----------------------------------------
// Keep out of enemy's shooting position
// -----------------------------------------
Vector vEnemyFacing = pEnemy->BodyDirection2D();
Vector vEnemyDir = pEnemy->EyePosition() - GetLocalOrigin();
VectorNormalize(vEnemyDir);
float fDotPr = DotProduct(vEnemyFacing, vEnemyDir);
if (fDotPr < -0.9)
{
Vector vDirUp(0, 0, 1);
Vector vDir;
CrossProduct(vEnemyFacing, vDirUp, vDir);
Vector crossProduct;
CrossProduct(vEnemyFacing, vEnemyDir, crossProduct);
if (crossProduct.y < 0)
{
vDir = vDir * -1;
}
return (vDir);
}
else if (fDotPr < -0.85)
{
Vector vDirUp(0, 0, 1);
Vector vDir;
CrossProduct(vEnemyFacing, vDirUp, vDir);
Vector crossProduct;
CrossProduct(vEnemyFacing, vEnemyDir, crossProduct);
if (random->RandomInt(0, 1))
{
vDir = vDir * -1;
}
return (vDir);
}
}
return vec3_origin;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
int CNPC_BaseMSynth::DrawDebugTextOverlays(void)
{
int nOffset = BaseClass::DrawDebugTextOverlays();
if (m_debugOverlays & OVERLAY_TEXT_BIT)
{
Vector vel;
GetVelocity(&vel, NULL);
char tempstr[512];
Q_snprintf(tempstr, sizeof(tempstr), "speed (max): %.2f (%.2f)", vel.Length(), m_flSpeed);
EntityText(nOffset, tempstr, 0);
nOffset++;
}
return nOffset;
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : float
//-----------------------------------------------------------------------------
float CNPC_BaseMSynth::GetHeadTurnRate(void)
{
if (GetEnemy())
return 800.0f;
return 350.0f;
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
inline CBaseEntity *CNPC_BaseMSynth::EntityToWatch(void)
{
return (GetTarget() != NULL) ? GetTarget() : GetEnemy(); // Okay if NULL
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : flInterval -
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::UpdateHead(float flInterval)
{
float yaw = GetPoseParameter(m_nPoseFaceHoriz);
float pitch = GetPoseParameter(m_nPoseFaceVert);
CBaseEntity *pTarget = EntityToWatch();
Vector vLookPos;
if (!HasCondition(COND_IN_PVS) || GetAttachment("eyes", vLookPos) == false)
{
vLookPos = EyePosition();
}
if (pTarget != NULL)
{
Vector lookDir = pTarget->EyePosition() - vLookPos;
VectorNormalize(lookDir);
if (DotProduct(lookDir, BodyDirection3D()) < 0.0f)
{
SetPoseParameter(m_nPoseFaceHoriz, UTIL_Approach(0, yaw, 10));
SetPoseParameter(m_nPoseFaceVert, UTIL_Approach(0, pitch, 10));
return;
}
float facingYaw = VecToYaw(BodyDirection3D());
float yawDiff = VecToYaw(lookDir);
yawDiff = UTIL_AngleDiff(yawDiff, facingYaw + yaw);
float facingPitch = UTIL_VecToPitch(BodyDirection3D());
float pitchDiff = UTIL_VecToPitch(lookDir);
pitchDiff = UTIL_AngleDiff(pitchDiff, facingPitch + pitch);
SetPoseParameter(m_nPoseFaceHoriz, UTIL_Approach(yaw + yawDiff, yaw, 50));
SetPoseParameter(m_nPoseFaceVert, UTIL_Approach(pitch + pitchDiff, pitch, 50));
}
else
{
SetPoseParameter(m_nPoseFaceHoriz, UTIL_Approach(0, yaw, 10));
SetPoseParameter(m_nPoseFaceVert, UTIL_Approach(0, pitch, 10));
}
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : &linear -
// &angular -
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::ClampMotorForces(Vector &linear, AngularImpulse &angular)
{
// limit reaction forces
if (m_nFlyMode != MSYNTH_FLY_DIVE)
{
linear.x = clamp(linear.x, -500, 500);
linear.y = clamp(linear.y, -500, 500);
linear.z = clamp(linear.z, -500, 500);
}
// If we're dive bombing, we need to drop faster than normal
if (m_nFlyMode != MSYNTH_FLY_DIVE)
{
// Add in weightlessness
linear.z += 800;
}
angular.z = clamp(angular.z, -GetHeadTurnRate(), GetHeadTurnRate());
if (m_nFlyMode == MSYNTH_FLY_DIVE)
{
// Disable pitch and roll motors while crashing.
angular.x = 0;
angular.y = 0;
}
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : &inputdata -
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::InputSetDistanceOverride(inputdata_t &inputdata)
{
m_flGoalOverrideDistance = inputdata.value.Float();
}
//-----------------------------------------------------------------------------
// Purpose: Emit sounds specific to the NPC's state.
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::AlertSound(void)
{
ScannerEmitSound(MSYNTH_ALERT_SOUND);
}
//------------------------------------------------------------------------------
// Purpose:
//------------------------------------------------------------------------------
void CNPC_BaseMSynth::DeathSound(const CTakeDamageInfo &info)
{
ScannerEmitSound(MSYNTH_DIE_SOUND);
}
//-----------------------------------------------------------------------------
// Purpose: Overridden so that scanners play battle sounds while fighting.
// Output : Returns TRUE on success, FALSE on failure.
//-----------------------------------------------------------------------------
bool CNPC_BaseMSynth::ShouldPlayIdleSound(void)
{
if (HasSpawnFlags(SF_NPC_GAG))
return false;
if (random->RandomInt(0, 25) != 0)
return false;
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Plays sounds while idle or in combat.
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::IdleSound(void)
{
if (m_NPCState == NPC_STATE_COMBAT)
{
// dvs: the combat sounds should be related to what is happening, rather than random
ScannerEmitSound(MSYNTH_IDLE2_SOUND);
}
else
{
ScannerEmitSound(MSYNTH_IDLE_SOUND);
}
}
//-----------------------------------------------------------------------------
// Purpose: Plays a sound when hurt.
//-----------------------------------------------------------------------------
void CNPC_BaseMSynth::PainSound(const CTakeDamageInfo &info)
{
ScannerEmitSound(MSYNTH_PAIN_SOUND);
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
float CNPC_BaseMSynth::GetMaxSpeed()
{
return MSYNTH_MAX_SPEED;
}
//-----------------------------------------------------------------------------
//
// Schedules
//
//-----------------------------------------------------------------------------
AI_BEGIN_CUSTOM_NPC(npc_basescanner, CNPC_BaseMSynth)
DECLARE_TASK(TASK_SCANNER_SET_FLY_PATROL)
DECLARE_TASK(TASK_SCANNER_SET_FLY_CHASE)
DECLARE_TASK(TASK_SCANNER_SET_FLY_ATTACK)
DECLARE_TASK(TASK_SCANNER_SET_FLY_DIVE)
DECLARE_CONDITION(COND_SCANNER_FLY_CLEAR)
DECLARE_CONDITION(COND_SCANNER_FLY_BLOCKED)
DECLARE_CONDITION(COND_SCANNER_RELEASED_FROM_PHYSCANNON)
DECLARE_CONDITION(COND_SCANNER_GRABBED_BY_PHYSCANNON)
//=========================================================
// > SCHED_SCANNER_PATROL
//=========================================================
DEFINE_SCHEDULE
(
SCHED_SCANNER_PATROL,
" Tasks"
" TASK_SCANNER_SET_FLY_PATROL 0"
" TASK_SET_TOLERANCE_DISTANCE 32"
" TASK_SET_ROUTE_SEARCH_TIME 5" // Spend 5 seconds trying to build a path if stuck
" TASK_GET_PATH_TO_RANDOM_NODE 2000"
" TASK_RUN_PATH 0"
" TASK_WAIT_FOR_MOVEMENT 0"
""
" Interrupts"
" COND_GIVE_WAY"
" COND_NEW_ENEMY"
" COND_SEE_ENEMY"
" COND_SEE_FEAR"
" COND_HEAR_COMBAT"
" COND_HEAR_DANGER"
" COND_HEAR_PLAYER"
" COND_LIGHT_DAMAGE"
" COND_HEAVY_DAMAGE"
" COND_PROVOKED"
" COND_SCANNER_GRABBED_BY_PHYSCANNON"
)
//=========================================================
// > SCHED_SCANNER_ATTACK
//
// This task does nothing. Translate it in your derived
// class to perform your attack.
//
//=========================================================
DEFINE_SCHEDULE
(
SCHED_SCANNER_ATTACK,
" Tasks"
" TASK_SCANNER_SET_FLY_ATTACK 0"
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
" TASK_WAIT 0.1"
""
" Interrupts"
" COND_TOO_FAR_TO_ATTACK"
" COND_SCANNER_FLY_BLOCKED"
" COND_NEW_ENEMY"
" COND_SCANNER_GRABBED_BY_PHYSCANNON"
)
//=========================================================
// > SCHED_SCANNER_ATTACK_HOVER
//=========================================================
DEFINE_SCHEDULE
(
SCHED_SCANNER_ATTACK_HOVER,
" Tasks"
" TASK_SCANNER_SET_FLY_ATTACK 0"
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
" TASK_WAIT 0.1"
""
" Interrupts"
" COND_TOO_FAR_TO_ATTACK"
" COND_SCANNER_FLY_BLOCKED"
" COND_NEW_ENEMY"
" COND_SCANNER_GRABBED_BY_PHYSCANNON"
)
//=========================================================
// > SCHED_SCANNER_ATTACK_DIVEBOMB
//
// Only done when scanner is dead
//=========================================================
DEFINE_SCHEDULE
(
SCHED_SCANNER_ATTACK_DIVEBOMB,
" Tasks"
" TASK_SCANNER_SET_FLY_DIVE 0"
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
" TASK_WAIT 10"
""
" Interrupts"
" COND_SCANNER_GRABBED_BY_PHYSCANNON"
)
//=========================================================
// > SCHED_SCANNER_CHASE_ENEMY
//
// Different interrupts than normal chase enemy.
//=========================================================
DEFINE_SCHEDULE
(
SCHED_SCANNER_CHASE_ENEMY,
" Tasks"
" TASK_SCANNER_SET_FLY_CHASE 0"
" TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_SCANNER_PATROL"
" TASK_SET_TOLERANCE_DISTANCE 120"
" TASK_GET_PATH_TO_ENEMY 0"
" TASK_RUN_PATH 0"
" TASK_WAIT_FOR_MOVEMENT 0"
""
""
" Interrupts"
" COND_SCANNER_FLY_CLEAR"
" COND_NEW_ENEMY"
" COND_ENEMY_DEAD"
" COND_LOST_ENEMY"
" COND_SCANNER_GRABBED_BY_PHYSCANNON"
)
//=========================================================
// > SCHED_SCANNER_CHASE_TARGET
//=========================================================
DEFINE_SCHEDULE
(
SCHED_SCANNER_CHASE_TARGET,
" Tasks"
" TASK_SCANNER_SET_FLY_CHASE 0"
" TASK_SET_TOLERANCE_DISTANCE 64"
" TASK_GET_PATH_TO_TARGET 0" //FIXME: This is wrong!
" TASK_RUN_PATH 0"
" TASK_WAIT_FOR_MOVEMENT 0"
""
" Interrupts"
" COND_SCANNER_FLY_CLEAR"
" COND_NEW_ENEMY"
" COND_SCANNER_GRABBED_BY_PHYSCANNON"
)
//=========================================================
// > SCHED_SCANNER_FOLLOW_HOVER
//=========================================================
DEFINE_SCHEDULE
(
SCHED_SCANNER_FOLLOW_HOVER,
" Tasks"
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
" TASK_WAIT 0.1"
""
" Interrupts"
" COND_SCANNER_FLY_BLOCKED"
" COND_SCANNER_GRABBED_BY_PHYSCANNON"
)
//=========================================================
// > SCHED_SCANNER_HELD_BY_PHYSCANNON
//=========================================================
DEFINE_SCHEDULE
(
SCHED_SCANNER_HELD_BY_PHYSCANNON,
" Tasks"
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
" TASK_WAIT 5.0"
""
" Interrupts"
" COND_LIGHT_DAMAGE"
" COND_HEAVY_DAMAGE"
" COND_SCANNER_RELEASED_FROM_PHYSCANNON"
)
AI_END_CUSTOM_NPC()