//========= 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)) { // <> 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()