#include "precompiled.h" #pragma hdrstop #include "prey_local.h" const idEventDef EV_StartBurst("startBurst"); const idEventDef EV_EndBurst("endBurst"); const idEventDef EV_StartStaticBeam("startStaticBeam"); const idEventDef EV_EndStaticBeam("endStaticBeam"); const idEventDef EV_StartChargeShot("startChargeShot"); const idEventDef EV_EndChargeShot("endChargeShot"); const idEventDef EV_StartPathing("startPathing"); const idEventDef EV_EndPathing("endPathing"); const idEventDef EV_EndZipBeams(""); const idEventDef EV_HealerReset(""); CLASS_DECLARATION(hhMonsterAI, hhDroid) EVENT(EV_StartStaticBeam, hhDroid::Event_StartStaticBeam) EVENT(EV_EndStaticBeam, hhDroid::Event_EndStaticBeam) EVENT(EV_StartBurst, hhDroid::Event_StartBurst) EVENT(EV_EndBurst, hhDroid::Event_EndBurst) EVENT(EV_StartChargeShot, hhDroid::Event_StartChargeShot) EVENT(EV_EndChargeShot, hhDroid::Event_EndChargeShot) EVENT(EV_StartPathing, hhDroid::Event_StartPathing) EVENT(EV_EndPathing, hhDroid::Event_EndPathing) EVENT(EV_EndZipBeams, hhDroid::Event_EndZipBeams) EVENT(EV_HealerReset, hhDroid::Event_HealerReset) END_CLASS #ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build hhDroid::hhDroid() { burstLength = 0.0; burstSpread = 0.0; burstDuration = 0.0; staticDuration = 0.0; staticRange = 0.0; numBurstBeams = 0; beamOffset = vec3_zero; chargeShotSize = 0.0; old_fly_bob_strength = 0.0; spinAngle = 0.0f; } hhDroid::~hhDroid() { for ( int i = 0; i < numBurstBeams; i ++ ) { SAFE_REMOVE( beamBurstList[i] ); } } void hhDroid::Show( void ) { hhMonsterAI::Show(); PostEventSec(&EV_StartStaticBeam, gameLocal.random.RandomFloat() * spawnArgs.GetFloat( "staticFreq", "2.5" ) ); } void hhDroid::Spawn(void) { Event_SetMoveType(MOVETYPE_FLY); lives = spawnArgs.GetInt( "lives", "3" ); numBurstBeams = spawnArgs.GetInt( "numBurstBeams", "5" ); burstLength = spawnArgs.GetFloat( "burstLength", "250" ); staticDuration = spawnArgs.GetFloat( "staticDuration", "2.0" ); burstDuration = spawnArgs.GetFloat( "burstDuration", "2.0" ); beamOffset = spawnArgs.GetVector( "beamOffset", "0 0 0" ); burstSpread = spawnArgs.GetFloat( "burstSpread", "1.0" ); enemyRange = spawnArgs.GetFloat( "enemy_range", "3000" ); flyDampening = spawnArgs.GetFloat( "fly_dampening", "0.01" ); bHealer = spawnArgs.GetBool( "healer" ); beamZip = hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamZip" ) ); if( beamZip.IsValid() ) { beamZip->Activate( false ); beamZip->SetOrigin( GetPhysics()->GetOrigin() + beamOffset ); beamZip->Bind( this, false ); } for ( int i = 0; i < numBurstBeams; i ++ ) { if ( gameLocal.random.RandomFloat() > 0.5f ) { beamBurstList.Append( hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamBurst1" ) ) ); } else { beamBurstList.Append( hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamBurst2" ) ) ); } if( beamBurstList[i].IsValid() ) { beamBurstList[i]->Activate( false ); beamBurstList[i]->SetOrigin( GetPhysics()->GetOrigin() + beamOffset ); beamBurstList[i]->Bind( this, false ); } } PostEventSec(&EV_StartStaticBeam, gameLocal.random.RandomFloat() * spawnArgs.GetFloat( "staticFreq", "2.5" ) ); } bool hhDroid::Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { bool bPain = hhMonsterAI::Pain( inflictor, attacker, damage, dir, location ); if ( !bPain ) { return bPain; } //activate some beams for damage fx for ( int i = 0; i < beamBurstList.Num(); i ++ ) { if ( beamBurstList[i].IsValid() ) { beamBurstList[i]->Activate( true ); beamBurstList[i]->SetTargetLocation( beamBurstList[i]->GetOrigin() + burstLength * hhUtils::RandomSpreadDir( dir.ToMat3(), burstSpread ) ); PostEventSec(&EV_EndBurst, staticDuration); } } return bPain; } void hhDroid::Think() { PROFILE_SCOPE("AI", PROFMASK_NORMAL|PROFMASK_AI); if (ai_skipThink.GetBool()) { return; } hhMonsterAI::Think(); //update burst beams idMat3 dir; if ( enemy.IsValid() ) { dir = (enemy->GetPhysics()->GetOrigin() - GetPhysics()->GetOrigin()).ToMat3(); } else { dir = GetPhysics()->GetAxis(); } for ( int i = 0; i < beamBurstList.Num(); i ++ ) { if ( beamBurstList[i].IsValid() && beamBurstList[i]->IsActivated() ) { beamBurstList[i]->SetTargetLocation( beamBurstList[i]->GetOrigin() + burstLength * hhUtils::RandomSpreadDir( hhUtils::RandomVector().ToMat3(), burstSpread ) ); } } if ( staticPoint.IsValid() && beamBurstList[0].IsValid() ) { beamBurstList[0]->SetTargetLocation( staticPoint->GetPhysics()->GetOrigin() ); } if ( chargeShot.IsValid() ) { chargeShot->SetShaderParm( 9, 1 ); chargeShot->SetShaderParm( 10, chargeShotSize ); chargeShotSize += spawnArgs.GetFloat( "chargeSizeDelta" ); chargeShot->GetPhysics()->DisableClip(); } } void hhDroid::Event_StartChargeShot(void) { //spawn chargeshot projectile const idDict *projectileDef = gameLocal.FindEntityDefDict( spawnArgs.GetString("def_chargeshot") ); if ( projectileDef ) { chargeShot = hhProjectile::SpawnProjectile( projectileDef ); if ( chargeShot.IsValid() ) { chargeShotSize = spawnArgs.GetFloat( "chargeSizeStart", "1.0" ); idVec3 launchStart = GetPhysics()->GetOrigin() + spawnArgs.GetVector( "chargeOffset", "15 0 0" ) * GetRenderEntity()->axis; chargeShot->Create(this, launchStart, GetRenderEntity()->axis); chargeShot->Launch(chargeShot->GetPhysics()->GetOrigin(), chargeShot->GetPhysics()->GetAxis(), vec3_zero ); chargeShot->Bind(this, true); } } } void hhDroid::Event_EndChargeShot(void) { //launch projectile if ( chargeShot.IsValid() ) { chargeShot->Unbind(); chargeShot->StartTracking(); chargeShot.Clear(); } } void hhDroid::Event_StartBurst(void) { gameLocal.RadiusDamage( GetPhysics()->GetOrigin(), this, this, this, this, spawnArgs.GetString("def_burstdamage") ); //burst effect idMat3 dir; if ( enemy.IsValid() ) { dir = (enemy->GetPhysics()->GetOrigin() - GetPhysics()->GetOrigin()).ToMat3(); } else { dir = GetPhysics()->GetAxis(); } for ( int i = 0; i < beamBurstList.Num(); i++ ) { if ( beamBurstList[i].IsValid() ) { beamBurstList[i]->Activate( true ); beamBurstList[i]->SetTargetLocation( beamBurstList[i]->GetOrigin() + burstLength * hhUtils::RandomSpreadDir( hhUtils::RandomVector().ToMat3(), burstSpread ) ); } } PostEventSec(&EV_EndBurst, burstDuration); } void hhDroid::Event_EndBurst(void) { for ( int i = 0; i < beamBurstList.Num(); i ++ ) { if ( beamBurstList[i].IsValid() ) { beamBurstList[i]->Activate( false ); } } } void hhDroid::Event_EndStaticBeam(void) { if ( beamBurstList[0].IsValid() ) { beamBurstList[0]->Activate( false ); } if ( staticPoint.IsValid() ) { staticPoint.Clear(); } //wait random time until next static beam if ( !IsHidden() && !AI_DEAD ) { PostEventSec(&EV_StartStaticBeam, gameLocal.random.RandomFloat() * spawnArgs.GetFloat( "staticFreq", "2.5" ) ); } } void hhDroid::Event_StartStaticBeam(void) { idVec3 targetPoint; idEntity *entityList[ MAX_GENTITIES ]; if ( IsHidden() || AI_DEAD ) { return; } //find nearest staticPoint float radius = spawnArgs.GetFloat( "staticRange" ); int listedEntities = gameLocal.EntitiesWithinRadius( GetPhysics()->GetOrigin(), radius, entityList, MAX_GENTITIES ); for( int i = 0; i < listedEntities; i++ ) { if ( entityList[ i ] && entityList[ i ]->spawnArgs.GetInt( "droidBeam", "0" ) ) { staticPoint = entityList[ i ]; break; } } //activate one beam if ( staticPoint.IsValid() && beamBurstList[0].IsValid() ) { StartSound( "snd_staticBeam", SND_CHANNEL_BODY, 0, true, NULL ); beamBurstList[0]->Activate( true ); beamBurstList[0]->SetTargetLocation( beamBurstList[0]->GetOrigin() + burstLength * hhUtils::RandomVector() ); staticPoint->ActivateTargets( this ); PostEventSec(&EV_EndStaticBeam, staticDuration); } } void hhDroid::FlyTurn( void ) { if ( AI_PATHING ) { hhMonsterAI::FlyTurn(); return; } if ( AI_ENEMY_VISIBLE || move.moveCommand == MOVE_FACE_ENEMY ) { TurnToward( lastVisibleEnemyPos ); } else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) { TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() ); } else if ( move.speed > 0.0f ) { const idVec3 &vel = physicsObj.GetLinearVelocity(); if ( vel.ToVec2().LengthSqr() > 0.1f ) { TurnToward( vel.ToYaw() ); } } Turn(); } void hhDroid::Event_StartPathing(void) { AI_PATHING = true; old_fly_bob_strength = fly_bob_strength; fly_bob_strength = 0.0; ignore_obstacles = true; } void hhDroid::Event_EndPathing(void) { AI_PATHING = false; fly_bob_strength = old_fly_bob_strength; ignore_obstacles = false; } void hhDroid::FlyMove( void ) { idVec3 goalPos; idVec3 oldorigin; idVec3 newDest; AI_BLOCKED = false; if ( ( move.moveCommand != MOVE_NONE ) && ReachedPos( move.moveDest, move.moveCommand ) ) { if ( AI_PATHING ) { physicsObj.SetLinearVelocity( idVec3(0,0,0) ); } StopMove( MOVE_STATUS_DONE ); } if ( move.moveCommand != MOVE_TO_POSITION_DIRECT ) { idVec3 vel = physicsObj.GetLinearVelocity(); if ( GetMovePos( goalPos ) ) { move.obstacle = NULL; } if ( move.speed ) { FlySeekGoal( vel, goalPos ); } // add in bobbing AddFlyBob( vel ); if ( enemy.GetEntity() && ( move.moveCommand != MOVE_TO_POSITION ) ) { float dist = ( GetPhysics()->GetOrigin() - enemy->GetPhysics()->GetOrigin() ).LengthFast(); if ( dist < enemyRange && enemyRange > 0 ) { AdjustFlyHeight( vel, goalPos ); } } AdjustFlySpeed( vel ); physicsObj.SetLinearVelocity( vel ); } // turn FlyTurn(); // run the physics for this frame oldorigin = physicsObj.GetOrigin(); physicsObj.UseFlyMove( true ); physicsObj.UseVelocityMove( false ); physicsObj.SetDelta( vec3_zero ); physicsObj.ForceDeltaMove( disableGravity ); RunPhysics(); monsterMoveResult_t moveResult = physicsObj.GetMoveResult(); if ( !af_push_moveables && attack.Length() && TestMelee() ) { DirectDamage( attack, enemy.GetEntity() ); } else { idEntity *blockEnt = physicsObj.GetSlideMoveEntity(); if ( blockEnt && blockEnt->IsType( idMoveable::Type ) && blockEnt->GetPhysics()->IsPushable() ) { KickObstacles( viewAxis[ 0 ], kickForce, blockEnt ); } else if ( moveResult == MM_BLOCKED ) { move.blockTime = gameLocal.time + 500; AI_BLOCKED = true; } } idVec3 org = physicsObj.GetOrigin(); if ( oldorigin != org ) { TouchTriggers(); } if ( ai_debugMove.GetBool() ) { gameRenderWorld->DebugLine( colorCyan, oldorigin, physicsObj.GetOrigin(), 4000 ); gameRenderWorld->DebugBounds( colorOrange, physicsObj.GetBounds(), org, gameLocal.msec ); gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), move.moveDest, gameLocal.msec ); gameRenderWorld->DebugLine( colorRed, org, org + physicsObj.GetLinearVelocity(), gameLocal.msec, true ); gameRenderWorld->DebugLine( colorBlue, org, goalPos, gameLocal.msec, true ); gameRenderWorld->DebugLine( colorYellow, org + EyeOffset(), org + EyeOffset() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true ); DrawRoute(); } } idProjectile *hhDroid::LaunchProjectile( const char *jointname, idEntity *target, bool clampToAttackCone, const idDict* desiredProjectileDef ) { //jsh overridden to allow per-projectile accuracy idVec3 muzzle; idVec3 dir; idVec3 start; trace_t tr; idBounds projBounds; float distance; const idClipModel *projClip; float attack_accuracy; float attack_cone; float projectile_spread; float diff; float angle; float spin; idAngles ang; int num_projectiles; int i; idMat3 axis; idVec3 tmp; idProjectile *lastProjectile; //HUMANHEAD mdc - added to support multiple projectiles if( desiredProjectileDef ) { //try to set our projectile to the desiredProjectile int projIndex = FindProjectileInfo( desiredProjectileDef ); if( projIndex >= 0 ) { SetCurrentProjectile( projIndex ); } } //HUMANHEAD END if ( !projectileDef ) { gameLocal.Warning( "%s (%s) doesn't have a projectile specified", name.c_str(), GetEntityDefName() ); return NULL; } if ( projectileDef->GetFloat( "attack_accuracy" ) ) { attack_accuracy = projectileDef->GetFloat( "attack_accuracy", "7" ); } else { attack_accuracy = spawnArgs.GetFloat( "attack_accuracy", "7" ); } attack_cone = spawnArgs.GetFloat( "attack_cone", "70" ); if ( projectileDef->GetFloat( "projectile_spread" ) ) { projectile_spread = projectileDef->GetFloat( "projectile_spread", "0" ); } else { projectile_spread = spawnArgs.GetFloat( "projectile_spread", "0" ); } if ( projectileDef->GetFloat( "num_projectiles" ) ) { num_projectiles = projectileDef->GetFloat( "num_projectiles", "1" ); } else { num_projectiles = spawnArgs.GetFloat( "num_projectiles", "1" ); } GetMuzzle( jointname, muzzle, axis ); if ( !projectile.GetEntity() ) { CreateProjectile( muzzle, axis[ 0 ] ); } lastProjectile = projectile.GetEntity(); if ( target != NULL ) { tmp = target->GetPhysics()->GetAbsBounds().GetCenter() - muzzle; tmp.Normalize(); axis = tmp.ToMat3(); } else { axis = viewAxis; } // rotate it because the cone points up by default tmp = axis[2]; axis[2] = axis[0]; axis[0] = -tmp; // make sure the projectile starts inside the monster bounding box const idBounds &ownerBounds = physicsObj.GetAbsBounds(); projClip = lastProjectile->GetPhysics()->GetClipModel(); projBounds = projClip->GetBounds().Rotate( axis ); // check if the owner bounds is bigger than the projectile bounds if ( ( ( ownerBounds[1][0] - ownerBounds[0][0] ) > ( projBounds[1][0] - projBounds[0][0] ) ) && ( ( ownerBounds[1][1] - ownerBounds[0][1] ) > ( projBounds[1][1] - projBounds[0][1] ) ) && ( ( ownerBounds[1][2] - ownerBounds[0][2] ) > ( projBounds[1][2] - projBounds[0][2] ) ) ) { if ( (ownerBounds - projBounds).RayIntersection( muzzle, viewAxis[ 0 ], distance ) ) { start = muzzle + distance * viewAxis[ 0 ]; } else { start = ownerBounds.GetCenter(); } } else { // projectile bounds bigger than the owner bounds, so just start it from the center start = ownerBounds.GetCenter(); } gameLocal.clip.Translation( tr, start, muzzle, projClip, axis, MASK_SHOT_RENDERMODEL, this ); muzzle = tr.endpos; // set aiming direction GetAimDir( muzzle, target, this, dir ); ang = dir.ToAngles(); // adjust his aim so it's not perfect. uses sine based movement so the tracers appear less random in their spread. float t = MS2SEC( gameLocal.time + entityNumber * 497 ); ang.pitch += idMath::Sin16( t * 5.1 ) * attack_accuracy; ang.yaw += idMath::Sin16( t * 6.7 ) * attack_accuracy; if ( !AI_WALLWALK && clampToAttackCone ) { // clamp the attack direction to be within monster's attack cone so he doesn't do // things like throw the missile backwards if you're behind him diff = idMath::AngleDelta( ang.yaw, current_yaw ); if ( diff > attack_cone ) { ang.yaw = current_yaw + attack_cone; } else if ( diff < -attack_cone ) { ang.yaw = current_yaw - attack_cone; } } axis = ang.ToMat3(); float spreadRad = DEG2RAD( projectile_spread ); for( i = 0; i < num_projectiles; i++ ) { // spread the projectiles out angle = idMath::Sin( spreadRad * gameLocal.random.RandomFloat() ); spin = (float)DEG2RAD( 360.0f ) * gameLocal.random.RandomFloat(); dir = axis[ 0 ] + axis[ 2 ] * ( angle * idMath::Sin( spin ) ) - axis[ 1 ] * ( angle * idMath::Cos( spin ) ); dir.Normalize(); // launch the projectile if ( !projectile.GetEntity() ) { CreateProjectile( muzzle, dir ); } lastProjectile = projectile.GetEntity(); lastProjectile->Launch( muzzle, dir, vec3_origin ); projectile = NULL; } TriggerWeaponEffects( muzzle, axis ); lastAttackTime = gameLocal.time; //HUMANHEAD mdc - added to support multiple projectiles projectile = NULL; SetCurrentProjectile( projectileDefaultDefIndex ); //set back to our default projectile to be on the safe side //HUMANHEAD END return lastProjectile; } void hhDroid::Event_FlyZip() { idVec3 old_origin = GetOrigin(); hhMonsterAI::Event_FlyZip(); if ( old_origin != GetOrigin() ) { if ( beamZip.IsValid() ) { beamZip->SetTargetLocation( old_origin ); } if ( beamZip.IsValid() ) { beamZip->Activate( true ); } PostEventSec( &EV_EndZipBeams, spawnArgs.GetFloat( "zip_beam_duration", "1.0" ) ); } } void hhDroid::Event_EndZipBeams() { if ( beamZip.IsValid() ) { beamZip->Activate( false ); } } void hhDroid::Event_HealerReset() { fl.takedamage = true; } void hhDroid::Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { if ( bHealer ) { lives--; if ( lives <= 0 ) { hhMonsterAI::Killed( inflictor, attacker, damage, dir, location ); } else { const char *defName = spawnArgs.GetString( "fx_pain" ); if (defName && defName[0]) { hhFxInfo fxInfo; fxInfo.RemoveWhenDone( true ); idEntityFx *painFx = SpawnFxLocal( defName, GetOrigin(), GetAxis(), &fxInfo, gameLocal.isClient ); if ( painFx ) { painFx->Bind( this, true ); } } health = spawnArgs.GetInt( "health" ); fl.takedamage = false; PostEventSec( &EV_HealerReset, spawnArgs.GetFloat( "reset_delay", "0.8" ) ); } return; } if ( AI_DEAD ) { AI_DAMAGE = true; return; } hhMonsterAI::Killed( inflictor, attacker, damage, dir, location ); for ( int i = 0; i < beamBurstList.Num(); i ++ ) { if ( beamBurstList[i].IsValid() ) { beamBurstList[i]->Activate( true ); beamBurstList[i]->SetTargetLocation( beamBurstList[i]->GetOrigin() + burstLength * hhUtils::RandomSpreadDir( dir.ToMat3(), burstSpread ) ); PostEventSec(&EV_EndBurst, staticDuration); } } } void hhDroid::AdjustFlySpeed( idVec3 &vel ) { float speed; // apply dampening vel -= vel * flyDampening * MS2SEC( gameLocal.msec ); // gradually speed up/slow down to desired speed speed = vel.Normalize(); speed += ( move.speed - speed ) * MS2SEC( gameLocal.msec ); if ( speed < 0.0f ) { speed = 0.0f; } else if ( move.speed && ( speed > move.speed ) ) { speed = move.speed; } vel *= speed; } void hhDroid::UpdateModelTransform( void ) { idVec3 origin; idMat3 axis; if ( GetPhysicsToVisualTransform( origin, axis ) ) { renderEntity.axis = axis * GetPhysics()->GetAxis(); renderEntity.origin = GetPhysics()->GetOrigin() + origin * renderEntity.axis; if ( bHealer ) { renderEntity.axis = idAngles( 0, 0, spinAngle ).ToMat3() * renderEntity.axis; spinAngle += 10; if ( spinAngle > 360.0f ) { spinAngle = 0.0f; } } } else { renderEntity.axis = GetPhysics()->GetAxis(); renderEntity.origin = GetPhysics()->GetOrigin(); } } /* ===================== hhDroid::Save ===================== */ void hhDroid::Save( idSaveGame *savefile ) const { int num = beamBurstList.Num(); savefile->WriteInt( num ); for ( int i = 0; i < num; i++ ) { beamBurstList[i].Save( savefile ); } staticPoint.Save( savefile ); chargeShot.Save( savefile ); beamZip.Save( savefile ); savefile->WriteVec3( savedGravity ); savefile->WriteFloat( chargeShotSize ); savefile->WriteFloat( burstLength ); savefile->WriteFloat( burstSpread ); savefile->WriteFloat( burstDuration ); savefile->WriteFloat( staticDuration ); savefile->WriteFloat( staticRange ); savefile->WriteInt( numBurstBeams ); savefile->WriteVec3( beamOffset ); savefile->WriteFloat( old_fly_bob_strength ); savefile->WriteFloat( enemyRange ); savefile->WriteFloat( flyDampening ); savefile->WriteFloat( spinAngle ); savefile->WriteBool( bHealer ); savefile->WriteInt( lives ); } /* ===================== hhDroid::Restore ===================== */ void hhDroid::Restore( idRestoreGame *savefile ) { int num; savefile->ReadInt( num ); beamBurstList.SetNum( num ); for ( int i = 0; i < num; i++ ) { beamBurstList[i].Restore( savefile ); } staticPoint.Restore( savefile ); chargeShot.Restore( savefile ); beamZip.Restore( savefile ); savefile->ReadVec3( savedGravity ); savefile->ReadFloat( chargeShotSize ); savefile->ReadFloat( burstLength ); savefile->ReadFloat( burstSpread ); savefile->ReadFloat( burstDuration ); savefile->ReadFloat( staticDuration ); savefile->ReadFloat( staticRange ); savefile->ReadInt( numBurstBeams ); savefile->ReadVec3( beamOffset ); savefile->ReadFloat( old_fly_bob_strength ); savefile->ReadFloat( enemyRange ); savefile->ReadFloat( flyDampening ); savefile->ReadFloat( spinAngle ); savefile->ReadBool( bHealer ); savefile->ReadInt( lives ); } #endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build