diff --git a/neo/doom3/game/ai/AI_base.cpp b/neo/doom3/game/ai/AI_base.cpp index 2da4f8ad..677d596b 100644 --- a/neo/doom3/game/ai/AI_base.cpp +++ b/neo/doom3/game/ai/AI_base.cpp @@ -179,10 +179,8 @@ float rvmAIObject::RandomTime( float seconds ) const { } bool rvmAIObject::InfluenceActive() const { - // Some branches keep the old DoomScript influence query only in script/event glue, - // and idPlayer has no public InfluenceActive() accessor. Keep this wrapper compile-safe; - // wire it to your branch's real influence flag here if one exists. - return false; + idPlayer* player = gameLocal.GetLocalPlayer(); + return player && player->GetInfluenceLevel(); } idPlayer* rvmAIObject::Player() const { diff --git a/neo/doom3/game/ai/AI_monster_base.cpp b/neo/doom3/game/ai/AI_monster_base.cpp index 643e4198..ec009bb8 100644 --- a/neo/doom3/game/ai/AI_monster_base.cpp +++ b/neo/doom3/game/ai/AI_monster_base.cpp @@ -22,6 +22,14 @@ rvmMonsterBase::rvmMonsterBase() { ignore_lostcombat = false; blocked = false; ignore_sight = false; + monsterBeginStartActive = false; + monsterBeginTeleportType = 0; + monsterBeginMoveType = MOVETYPE_ANIM; + spawnerTriggerCount = 0; + monsterBeginActionName.Clear(); + ClearParms( monsterBeginActionParms ); + pathActionName.Clear(); + ClearParms( pathActionParms ); } void rvmMonsterBase::Init( idAI* aiOwner ) { @@ -36,6 +44,14 @@ void rvmMonsterBase::Init( idAI* aiOwner ) { run = false; blocked = false; ignore_sight = false; + monsterBeginStartActive = false; + monsterBeginTeleportType = 0; + monsterBeginMoveType = MOVETYPE_ANIM; + spawnerTriggerCount = 0; + monsterBeginActionName.Clear(); + ClearParms( monsterBeginActionParms ); + pathActionName.Clear(); + ClearParms( pathActionParms ); } stateResult_t rvmMonsterBase::RunStateByName( const char* stateName, stateParms_t* parms ) { @@ -113,12 +129,14 @@ stateResult_t rvmMonsterBase::Torso_Sight( stateParms_t* parms ) { return SRESULT_WAIT; } FinishAction( "sight" ); + SetNeverDormant( GetFloatKey( "neverdormant" ) != 0.0f ); SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", GetIntKey( "on_activate_blend", "4" ) ); return SRESULT_DONE; } case STAGE_WAIT: if ( AnimDone( ANIMCHANNEL_TORSO, GetIntKey( "on_activate_blend", "4" ) ) ) { FinishAction( "sight" ); + SetNeverDormant( GetFloatKey( "neverdormant" ) != 0.0f ); SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", GetIntKey( "on_activate_blend", "4" ) ); return SRESULT_DONE; } @@ -158,12 +176,16 @@ stateResult_t rvmMonsterBase::state_Spawner( stateParms_t* parms ) { case STAGE_START: HideOwner(); ClearActivated(); + spawnerTriggerCount = 0; parms->stage = STAGE_LOOP; return SRESULT_WAIT; case STAGE_LOOP: - if ( Activated() && CanBecomeSolid() ) { + if ( Activated() ) { + spawnerTriggerCount++; ClearActivated(); - // Spawn-copy behavior is project-specific; keep this state non-blocking. + } + if ( spawnerTriggerCount > 0 && CanBecomeSolid() && SpawnFromSpawner() ) { + spawnerTriggerCount--; } return SRESULT_WAIT; } @@ -171,7 +193,18 @@ stateResult_t rvmMonsterBase::state_Spawner( stateParms_t* parms ) { } stateResult_t rvmMonsterBase::monster_begin( stateParms_t* parms ) { - enum { STAGE_START, STAGE_HIDDEN_WAIT_TRIGGER, STAGE_HIDDEN_WAIT_SOLID, STAGE_DELAY, STAGE_WAKE, STAGE_SIGHT }; + enum { + STAGE_START, + STAGE_WAIT_TRIGGER, + STAGE_WAIT_SOLID, + STAGE_TELEPORT_FX, + STAGE_TRIGGER_ANIM, + STAGE_DELAY, + STAGE_WAKE_SELECT, + STAGE_WAKE_ACTION, + STAGE_ATTACK_PATH, + STAGE_DONE + }; switch ( parms->stage ) { case STAGE_START: run = false; @@ -179,29 +212,104 @@ stateResult_t rvmMonsterBase::monster_begin( stateParms_t* parms ) { if ( !GetIntKey( "ignore_flashlight" ) ) { WakeOnFlashlight( true ); } - if ( GetIntKey( "hide" ) ) { + + monsterBeginStartActive = false; + monsterBeginMoveType = owner ? owner->GetMoveType() : MOVETYPE_ANIM; + monsterBeginTeleportType = resurrect ? 4 : GetIntKey( "teleport" ); + scratchString = resurrect ? "" : GetKey( "trigger_anim" ); + if ( resurrect && owner ) { + owner->AI_ACTIVATED = true; + } + + if ( GetIntKey( "spawner" ) ) { + SetState( "state_Spawner" ); + return SRESULT_DONE; + } + + if ( scratchString.Length() || monsterBeginTeleportType > 0 || GetIntKey( "hide" ) ) { HideOwner(); - ClearActivated(); - parms->stage = STAGE_HIDDEN_WAIT_TRIGGER; + if ( !resurrect ) { + ClearActivated(); + } + parms->stage = STAGE_WAIT_TRIGGER; return SRESULT_WAIT; } + + scratchEndTime = DelayTime( GetFloatKey( "wait" ) ); + parms->stage = STAGE_DELAY; + return SRESULT_WAIT; + + case STAGE_WAIT_TRIGGER: + if ( !Activated() ) { + return SRESULT_WAIT; + } + ClearActivated(); + parms->stage = STAGE_WAIT_SOLID; + return SRESULT_WAIT; + + case STAGE_WAIT_SOLID: + if ( !CanBecomeSolid() ) { + return SRESULT_WAIT; + } + if ( monsterBeginTeleportType > 0 ) { + BecomeSolid(); + monsterBeginMoveType = owner ? owner->GetMoveType() : MOVETYPE_ANIM; + SetMoveType( MOVETYPE_STATIC ); + SetNeverDormant( true ); + trigger_wakeup_targets(); + if ( monsterBeginTeleportType == 1 ) { + StartFx( GetKey( "fx_teleport1" ) ); + scratchEndTime = DelayTime( 1.6f ); + } else if ( monsterBeginTeleportType == 2 ) { + StartFx( GetKey( "fx_teleport2" ) ); + scratchEndTime = DelayTime( 2.6f ); + } else if ( monsterBeginTeleportType == 3 ) { + StartFx( GetKey( "fx_teleport3" ) ); + scratchEndTime = DelayTime( 3.6f ); + } else { + StartFx( GetKey( "fx_teleport" ) ); + scratchEndTime = DelayTime( 0.6f ); + } + parms->stage = STAGE_TELEPORT_FX; + return SRESULT_WAIT; + } + + ShowOwner(); + if ( scratchString.Length() ) { + SetNeverDormant( true ); + trigger_wakeup_targets(); + waitActionName = "customAnim"; + playCustomAnim( scratchString.c_str(), 0.0f, 4.0f ); + parms->stage = STAGE_TRIGGER_ANIM; + return SRESULT_WAIT; + } + if ( ( GetIntKey( "hide" ) == 1 ) || ambush ) { + ClearEnemy(); + } scratchEndTime = DelayTime( GetFloatKey( "wait" ) ); parms->stage = STAGE_DELAY; return SRESULT_WAIT; - case STAGE_HIDDEN_WAIT_TRIGGER: - if ( !Activated() && !Pain() ) { - return SRESULT_WAIT; - } - ClearActivated(); - parms->stage = STAGE_HIDDEN_WAIT_SOLID; - return SRESULT_WAIT; - - case STAGE_HIDDEN_WAIT_SOLID: - if ( !CanBecomeSolid() ) { + case STAGE_TELEPORT_FX: + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } ShowOwner(); + waitActionName = "customAnim"; + playCustomAnim( "teleport", 0.0f, 4.0f ); + parms->stage = STAGE_TRIGGER_ANIM; + return SRESULT_WAIT; + + case STAGE_TRIGGER_ANIM: + if ( !ActionDone( "customAnim" ) ) { + return SRESULT_WAIT; + } + SetNeverDormant( GetFloatKey( "neverdormant" ) != 0.0f ); + LocateEnemy(); + if ( monsterBeginTeleportType > 0 ) { + SetMoveType( monsterBeginMoveType ); + } + monsterBeginStartActive = true; scratchEndTime = DelayTime( GetFloatKey( "wait" ) ); parms->stage = STAGE_DELAY; return SRESULT_WAIT; @@ -213,24 +321,59 @@ stateResult_t rvmMonsterBase::monster_begin( stateParms_t* parms ) { if ( idEntity* enemyEnt = GetEntityKey( "enemy" ) ) { SetEnemy( enemyEnt ); } - parms->stage = STAGE_WAKE; - return SRESULT_WAIT; - - case STAGE_WAKE: - if ( GetEnemy() || Pain() || Activated() || checkForEnemy( idle_sight_fov ) ) { - trigger_wakeup_targets(); - ignore_sight = false; - WakeOnFlashlight( false ); - waitActionName = "sight"; - SetAnimState( ANIMCHANNEL_TORSO, "Torso_Sight", 4 ); - parms->stage = STAGE_SIGHT; - } - return SRESULT_WAIT; - - case STAGE_SIGHT: - if ( !ActionDone( "sight" ) ) { + if ( monsterBeginStartActive ) { + if ( GetIntKey( "attack_path" ) ) { + current_path = owner ? owner->RandomPath() : NULL; + if ( current_path.GetEntity() ) { + run = true; + ignoreEnemies = true; + monsterBeginActionName = "idle_followPathEntities"; + ClearParms( monsterBeginActionParms ); + parms->stage = STAGE_ATTACK_PATH; + return SRESULT_WAIT; + } + } + parms->stage = STAGE_DONE; return SRESULT_WAIT; } + + parms->stage = STAGE_WAKE_SELECT; + return SRESULT_WAIT; + + case STAGE_WAKE_SELECT: + if ( GetIntKey( "wake_on_attackcone" ) ) { + monsterBeginActionName = "wake_on_attackcone"; + } else if ( GetIntKey( "walk_on_trigger" ) ) { + monsterBeginActionName = "walk_on_trigger"; + } else if ( GetIntKey( "trigger" ) ) { + monsterBeginActionName = "wake_on_trigger"; + } else { + monsterBeginActionName = "wake_on_enemy"; + } + ClearParms( monsterBeginActionParms ); + parms->stage = STAGE_WAKE_ACTION; + return SRESULT_WAIT; + + case STAGE_WAKE_ACTION: + if ( RunStateByName( monsterBeginActionName.c_str(), &monsterBeginActionParms ) == SRESULT_WAIT ) { + return SRESULT_WAIT; + } + monsterBeginActionName.Clear(); + parms->stage = STAGE_DONE; + return SRESULT_WAIT; + + case STAGE_ATTACK_PATH: + if ( RunStateByName( monsterBeginActionName.c_str(), &monsterBeginActionParms ) == SRESULT_WAIT ) { + return SRESULT_WAIT; + } + monsterBeginActionName.Clear(); + ignoreEnemies = false; + parms->stage = STAGE_DONE; + return SRESULT_WAIT; + + case STAGE_DONE: + ignore_sight = false; + WakeOnFlashlight( false ); return SRESULT_DONE; } return SRESULT_ERROR; @@ -409,11 +552,19 @@ stateResult_t rvmMonsterBase::walk_on_trigger( stateParms_t* parms ) { AllowMovement( true ); SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); trigger_wakeup_targets(); - StartAction( "sight_enemy" ); + FaceEnemy(); + if ( GetKey( "on_activate" )[0] ) { + SetNeverDormant( true ); + if ( GetIntKey( "walk_on_sight" ) ) { + MoveToEnemy(); + } + waitActionName = "sight"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Sight", 4 ); + } parms->stage = STAGE_DONE; return SRESULT_WAIT; case STAGE_DONE: - return InAction() ? SRESULT_WAIT : SRESULT_DONE; + return !ActionDone( "sight" ) ? SRESULT_WAIT : SRESULT_DONE; } return SRESULT_ERROR; } @@ -433,11 +584,19 @@ stateResult_t rvmMonsterBase::wake_on_trigger( stateParms_t* parms ) { AllowMovement( true ); SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); trigger_wakeup_targets(); - StartAction( "sight_enemy" ); + FaceEnemy(); + if ( GetKey( "on_activate" )[0] ) { + SetNeverDormant( true ); + if ( GetIntKey( "walk_on_sight" ) ) { + MoveToEnemy(); + } + waitActionName = "sight"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Sight", 4 ); + } parms->stage = STAGE_DONE; return SRESULT_WAIT; case STAGE_DONE: - return InAction() ? SRESULT_WAIT : SRESULT_DONE; + return !ActionDone( "sight" ) ? SRESULT_WAIT : SRESULT_DONE; } return SRESULT_ERROR; } @@ -457,11 +616,19 @@ stateResult_t rvmMonsterBase::wake_on_enemy( stateParms_t* parms ) { AllowMovement( true ); SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); trigger_wakeup_targets(); - StartAction( "sight_enemy" ); + FaceEnemy(); + if ( GetKey( "on_activate" )[0] ) { + SetNeverDormant( true ); + if ( GetIntKey( "walk_on_sight" ) ) { + MoveToEnemy(); + } + waitActionName = "sight"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Sight", 4 ); + } parms->stage = STAGE_DONE; return SRESULT_WAIT; case STAGE_DONE: - return InAction() ? SRESULT_WAIT : SRESULT_DONE; + return !ActionDone( "sight" ) ? SRESULT_WAIT : SRESULT_DONE; } return SRESULT_ERROR; } @@ -485,12 +652,20 @@ stateResult_t rvmMonsterBase::wake_on_attackcone( stateParms_t* parms ) { AllowMovement( true ); SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); trigger_wakeup_targets(); - StartAction( "sight_enemy" ); + FaceEnemy(); + if ( GetKey( "on_activate" )[0] ) { + SetNeverDormant( true ); + if ( GetIntKey( "walk_on_sight" ) ) { + MoveToEnemy(); + } + waitActionName = "sight"; + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Sight", 4 ); + } parms->stage = STAGE_DONE; return SRESULT_WAIT; } case STAGE_DONE: - return InAction() ? SRESULT_WAIT : SRESULT_DONE; + return !ActionDone( "sight" ) ? SRESULT_WAIT : SRESULT_DONE; } return SRESULT_ERROR; } @@ -514,33 +689,35 @@ stateResult_t rvmMonsterBase::idle_followPathEntities( stateParms_t* parms ) { // Resolve common node class names into action states. scratchString = current_path.GetEntity()->GetEntityDefName(); if ( !scratchString.Icmp( "path_corner" ) ) { - StartAction( "path_corner" ); + pathActionName = "path_corner"; } else if ( !scratchString.Icmp( "path_anim" ) ) { - StartAction( "path_anim" ); + pathActionName = "path_anim"; } else if ( !scratchString.Icmp( "path_cycleanim" ) ) { - StartAction( "path_cycleanim" ); + pathActionName = "path_cycleanim"; } else if ( !scratchString.Icmp( "path_turn" ) ) { - StartAction( "path_turn" ); + pathActionName = "path_turn"; } else if ( !scratchString.Icmp( "path_wait" ) ) { - StartAction( "path_wait" ); + pathActionName = "path_wait"; } else if ( !scratchString.Icmp( "path_waitfortrigger" ) ) { - StartAction( "path_waitfortrigger" ); + pathActionName = "path_waitfortrigger"; } else if ( !scratchString.Icmp( "path_hide" ) ) { - StartAction( "path_hide" ); + pathActionName = "path_hide"; } else if ( !scratchString.Icmp( "path_show" ) ) { - StartAction( "path_show" ); + pathActionName = "path_show"; } else if ( !scratchString.Icmp( "path_attack" ) ) { - StartAction( "path_attack" ); + pathActionName = "path_attack"; } else { current_path = next_path; return SRESULT_WAIT; } + ClearParms( pathActionParms ); parms->stage = STAGE_NODE_ACTION; return SRESULT_WAIT; case STAGE_NODE_ACTION: - if ( InAction() ) { + if ( RunStateByName( pathActionName.c_str(), &pathActionParms ) == SRESULT_WAIT ) { return SRESULT_WAIT; } + pathActionName.Clear(); if ( checkForEnemy( true ) ) { return SRESULT_DONE; } @@ -1087,6 +1264,52 @@ void rvmMonsterBase::trigger_wakeup_targets() { } } +void rvmMonsterBase::TriggerEntity( idEntity* ent ) { + if ( !ent ) { + return; + } + const idEventDef* activateEvent = ent->FindEventDef( "activate", "e" ); + ent->Signal( SIG_TRIGGER ); + if ( activateEvent ) { + ent->ProcessEvent( activateEvent, gameLocal.GetLocalPlayer() ); + } + ent->TriggerGuis(); +} + +idEntity* rvmMonsterBase::SpawnFromSpawner() { + if ( !owner ) { + return NULL; + } + + const int maxSpawn = GetIntKey( "spawner" ); + const char* baseName = owner->GetName(); + int slot = 0; + for ( ; slot < maxSpawn; slot++ ) { + if ( !gameLocal.FindEntity( va( "%s%d", baseName, slot ) ) ) { + break; + } + } + if ( slot >= maxSpawn ) { + return NULL; + } + + idDict args = owner->spawnArgs; + args.SetInt( "spawner", 0 ); + args.Set( "name", va( "%s%d", baseName, slot ) ); + const char* spawnTarget = GetKey( "spawn_target" ); + if ( spawnTarget[0] ) { + args.Set( "target", spawnTarget ); + } + + idEntity* ent = NULL; + if ( gameLocal.SpawnEntityDef( args, &ent ) && ent ) { + TriggerEntity( ent ); + return ent; + } + + return NULL; +} + bool rvmMonsterBase::checkForEnemy( bool use_fov ) { if ( InfluenceActive() ) { return false; diff --git a/neo/doom3/game/ai/AI_monster_base.h b/neo/doom3/game/ai/AI_monster_base.h index 5aabced2..fdcfa916 100644 --- a/neo/doom3/game/ai/AI_monster_base.h +++ b/neo/doom3/game/ai/AI_monster_base.h @@ -27,6 +27,14 @@ protected: bool ignore_lostcombat; bool blocked; bool ignore_sight; + bool monsterBeginStartActive; + int monsterBeginTeleportType; + int monsterBeginMoveType; + int spawnerTriggerCount; + idStr monsterBeginActionName; + stateParms_t monsterBeginActionParms; + idStr pathActionName; + stateParms_t pathActionParms; // Animation state machines. virtual stateResult_t Torso_CustomCycle( stateParms_t* parms ); @@ -89,4 +97,6 @@ protected: virtual void follow_alternate_path1(); virtual void follow_alternate_path2(); virtual void follow_alternate_path3(); + virtual void TriggerEntity( idEntity* ent ); + virtual idEntity* SpawnFromSpawner(); }; diff --git a/neo/doom3/game/ai/AI_monster_boss_guardian.cpp b/neo/doom3/game/ai/AI_monster_boss_guardian.cpp index e5b29246..6977037a 100644 --- a/neo/doom3/game/ai/AI_monster_boss_guardian.cpp +++ b/neo/doom3/game/ai/AI_monster_boss_guardian.cpp @@ -200,7 +200,31 @@ stateResult_t rvmMonsterBossGuardian::combat_charge( stateParms_t* parms ) { nextAttack = DelayTime( GUARDIAN_RANGE_ATTACK_DELAY ); return SRESULT_DONE; } -stateResult_t rvmMonsterBossGuardian::pound_impact( stateParms_t* parms ) { RadiusDamageFromJoint( rightfoot ? "r_foot" : "l_foot", "damage_guardian_footstep" ); return SRESULT_DONE; } +stateResult_t rvmMonsterBossGuardian::pound_impact( stateParms_t* parms ) { + const char* jointName = rightfoot ? "rhand" : "lhand"; + idVec3 org = GetOrigin(); + idMat3 axis = mat3_identity; + if ( owner ) { + jointHandle_t joint = owner->GetAnimator()->GetJointHandle( jointName ); + if ( joint != INVALID_JOINT ) { + owner->GetJointWorldTransform( joint, gameLocal.time, org, axis ); + } + gameLocal.RadiusDamage( org, owner, owner, spawner.GetEntity(), spawner.GetEntity(), GetKey( "def_poundgrounddamage", "damage_guardianPoundGround" ), 1.0f ); + + const char* entname = GetKey( "def_poundground" ); + if ( entname[0] ) { + idEntity* explosion = NULL; + idDict args; + args.Set( "classname", entname ); + if ( gameLocal.SpawnEntityDef( args, &explosion ) && explosion ) { + explosion->SetOrigin( org - idVec3( 0.0f, 0.0f, 32.0f ) ); + explosion->SetShaderParm( SHADERPARM_TIMEOFFSET, -Time() ); + explosion->PostEventSec( &EV_Remove, 0.75f ); + } + } + } + return SRESULT_DONE; +} stateResult_t rvmMonsterBossGuardian::pound_attack( stateParms_t* parms ) { AttackMelee( "damage_guardian_melee" ); return SRESULT_DONE; } stateResult_t rvmMonsterBossGuardian::pound_attack_left( stateParms_t* parms ) { rightfoot = false; return pound_attack( parms ); } stateResult_t rvmMonsterBossGuardian::pound_attack_right( stateParms_t* parms ) { rightfoot = true; return pound_attack( parms ); } diff --git a/neo/doom3/game/ai/AI_monster_boss_vagary.cpp b/neo/doom3/game/ai/AI_monster_boss_vagary.cpp index cb11aded..885b8dc9 100644 --- a/neo/doom3/game/ai/AI_monster_boss_vagary.cpp +++ b/neo/doom3/game/ai/AI_monster_boss_vagary.cpp @@ -209,12 +209,12 @@ int rvmMonsterBossVagary::check_attacks() { if ( ( Time() > nextNoFOVAttack && EnemyVisible() ) || EnemyInFOV() ) { if ( !CanReachEnemy() || currentTime >= nextAttack ) { if ( CanHitEnemy() ) { - throwEntity = FindActorsInBounds( idVec3( -512, -512, -512 ), idVec3( 512, 512, 512 ) ); + throwEntity = ChooseObjectToThrow(); if ( throwEntity.GetEntity() ) { flags |= ATTACK_MISSILE; } } } } - return static_cast( flags ); + return flags; } bool rvmMonsterBossVagary::do_attack( int attack_flags ) { @@ -231,16 +231,93 @@ bool rvmMonsterBossVagary::do_attack( int attack_flags ) { stateResult_t rvmMonsterBossVagary::throw_object( stateParms_t* parms ) { idEntity* ent = throwEntity.GetEntity(); if ( !ent ) { return SRESULT_DONE; } - // Hook point for the project-specific vagary_ThrowObjectAtEnemy helper. - StartSoundKey( "snd_pickup", SND_CHANNEL_WEAPON, false ); - if ( GetEnemy() ) { - idVec3 dir = GetEnemyPos() - ent->GetPhysics()->GetOrigin(); - dir.Normalize(); - ent->GetPhysics()->SetLinearVelocity( dir * VAGARY_THROW_SPEED2 ); - } + ThrowObjectAtEnemy( ent, true ); return SRESULT_DONE; } +idEntity* rvmMonsterBossVagary::ChooseObjectToThrow() { + if ( !owner ) { + return NULL; + } + + idEntity* entityList[ MAX_GENTITIES ]; + idBounds checkBounds( VAGARY_THROW_MIN, VAGARY_THROW_MAX ); + checkBounds.TranslateSelf( GetOrigin() ); + const int numListedEntities = gameLocal.clip.EntitiesTouchingBounds( checkBounds, -1, entityList, MAX_GENTITIES ); + if ( numListedEntities <= 0 ) { + return NULL; + } + + idActor* enemyEnt = GetEnemy(); + const idVec3 enemyEyePos = enemyEnt ? owner->lastVisibleEnemyPos + owner->lastVisibleEnemyEyeOffset : GetEnemyPos(); + const idBounds& myBounds = owner->GetPhysics()->GetAbsBounds(); + const int startIndex = gameLocal.random.RandomInt( numListedEntities ); + + for ( int i = 0; i < numListedEntities; i++ ) { + int index = startIndex + i; + if ( index >= numListedEntities ) { + index -= numListedEntities; + } + + idEntity* ent = entityList[index]; + if ( !ent || !ent->IsType( idMoveable::Type ) || ent->fl.hidden ) { + continue; + } + + idPhysics* entPhys = ent->GetPhysics(); + const idVec3& entOrg = entPhys->GetOrigin(); + if ( ( entOrg - enemyEyePos ).LengthFast() < VAGARY_THROW_DIST ) { + continue; + } + + idBounds expandedBounds = myBounds.Expand( entPhys->GetBounds().GetRadius() ); + if ( expandedBounds.LineIntersection( entOrg, enemyEyePos ) ) { + continue; + } + + idVec3 vel; + if ( idAI::PredictTrajectory( entOrg + idVec3( 0.0f, 0.0f, VAGARY_THROW_OFFSET ), enemyEyePos, VAGARY_THROW_SPEED, entPhys->GetGravity(), + entPhys->GetClipModel(), entPhys->GetClipMask(), MAX_WORLD_SIZE, NULL, enemyEnt, ai_debugTrajectory.GetBool() ? 4000 : 0, vel ) ) { + return ent; + } + } + + return NULL; +} + +void rvmMonsterBossVagary::ThrowObjectAtEnemy( idEntity* ent, bool delayedThrow ) { + if ( !ent ) { + return; + } + + StartSoundKey( "snd_pickup", SND_CHANNEL_WEAPON, false ); + idPhysics* entPhys = ent->GetPhysics(); + entPhys->SetAngularVelocity( idVec3( 30.0f, 30.0f, 0.0f ) ); + + idVec3 throwVelocity = owner ? owner->GetPhysics()->GetAxis()[0] * VAGARY_THROW_SPEED2 : idVec3( VAGARY_THROW_SPEED2, 0.0f, 0.0f ); + idActor* enemyEnt = GetEnemy(); + if ( enemyEnt && owner ) { + idVec3 aimDir; + if ( idAI::PredictTrajectory( entPhys->GetOrigin(), owner->lastVisibleEnemyPos + owner->lastVisibleEnemyEyeOffset, VAGARY_THROW_SPEED2, + entPhys->GetGravity(), entPhys->GetClipModel(), entPhys->GetClipMask(), MAX_WORLD_SIZE, NULL, enemyEnt, + ai_debugTrajectory.GetBool() ? 4000 : 0, aimDir ) ) { + throwVelocity = aimDir * VAGARY_THROW_SPEED2; + } + } + + if ( delayedThrow ) { + const idVec3 hoverPos = entPhys->GetOrigin() + idVec3( 0.0f, 0.0f, VAGARY_THROW_OFFSET ); + entPhys->SetLinearVelocity( ( hoverPos - entPhys->GetOrigin() ) * 5.0f ); + ent->PostEventSec( &EV_SetLinearVelocity, 0.5f, throwVelocity ); + } else { + entPhys->SetLinearVelocity( throwVelocity ); + } + + if ( ent->IsType( idMoveable::Type ) ) { + static_cast( ent )->EnableDamage( true, 2.5f ); + } +} + stateResult_t rvmMonsterBossVagary::combat_range( stateParms_t* parms ) { enum { STAGE_FACE, STAGE_THROW, STAGE_COOLDOWN }; switch ( parms->stage ) { @@ -252,12 +329,20 @@ stateResult_t rvmMonsterBossVagary::combat_range( stateParms_t* parms ) { return SRESULT_WAIT; case STAGE_THROW: if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } - StartAction( "throw_object" ); + ThrowObjectAtEnemy( throwEntity.GetEntity(), true ); + scratchCount = RandomFloat( 4.0f ); + for ( int i = 1; i < idMath::Ftoi( scratchCount ); i++ ) { + idEntity* ent = ChooseObjectToThrow(); + if ( !ent ) { + break; + } + ThrowObjectAtEnemy( ent, true ); + } scratchEndTime = DelayTime( 1.0f ); parms->stage = STAGE_COOLDOWN; return SRESULT_WAIT; case STAGE_COOLDOWN: - if ( InAction() || Time() < scratchEndTime ) { return SRESULT_WAIT; } + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } LookAtEnemy( 1.0f ); nextAttack = DelayTime( VAGARY_ATTACK_RATE ); nextNoFOVAttack = Time() + VAGARY_NOFOVTIME; diff --git a/neo/doom3/game/ai/AI_monster_boss_vagary.h b/neo/doom3/game/ai/AI_monster_boss_vagary.h index 291a7c38..3fe1c2b9 100644 --- a/neo/doom3/game/ai/AI_monster_boss_vagary.h +++ b/neo/doom3/game/ai/AI_monster_boss_vagary.h @@ -22,6 +22,12 @@ #ifndef VAGARY_THROW_SPEED2 #define VAGARY_THROW_SPEED2 1500 #endif +#ifndef VAGARY_THROW_MIN +#define VAGARY_THROW_MIN idVec3( -512.0f, -512.0f, -512.0f ) +#endif +#ifndef VAGARY_THROW_MAX +#define VAGARY_THROW_MAX idVec3( 512.0f, 512.0f, 512.0f ) +#endif #ifndef VAGARY_THROW_DIST #define VAGARY_THROW_DIST 200 #endif @@ -105,5 +111,7 @@ virtual stateResult_t combat_dodge_right( stateParms_t* parms ); virtual int check_attacks() override; virtual bool do_attack( int attack_flags ) override; + virtual idEntity* ChooseObjectToThrow(); + virtual void ThrowObjectAtEnemy( idEntity* ent, bool delayedThrow ); }; diff --git a/neo/doom3/game/ai/AI_monster_demon_archvile.cpp b/neo/doom3/game/ai/AI_monster_demon_archvile.cpp index 76975c8f..8088d25b 100644 --- a/neo/doom3/game/ai/AI_monster_demon_archvile.cpp +++ b/neo/doom3/game/ai/AI_monster_demon_archvile.cpp @@ -6,6 +6,44 @@ #include "../Game_local.h" +static void PostEntityEventByName( idEntity* ent, const char* eventName, float delaySeconds ) { + if ( !ent ) { + return; + } + const idEventDef* eventDef = ent->FindEventDef( eventName, NULL ); + if ( !eventDef ) { + return; + } + if ( delaySeconds <= 0.0f ) { + ent->ProcessEvent( eventDef ); + } else { + ent->PostEventSec( eventDef, delaySeconds ); + } +} + +static idEntity* SpawnArchvileTimedTrigger( const char* classname, const idVec3& origin, const idAngles& angles, idAI* owner, float enableDelay, float activeTime, float removeDelay ) { + idEntity* ent = NULL; + idDict args; + args.Set( "classname", classname ); + args.Set( "cinematic_remove", "1" ); + if ( !gameLocal.SpawnEntityDef( args, &ent ) || !ent ) { + return NULL; + } + + ent->SetOrigin( origin ); + ent->SetAngles( angles ); + ent->SetShaderParm( SHADERPARM_TIMEOFFSET, -MS2SEC( gameLocal.time ) ); + ent->SetShaderParm( SHADERPARM_DIVERSITY, gameLocal.random.RandomFloat() ); + if ( owner ) { + ent->ProcessEvent( &EV_SetOwner, owner ); + } + PostEntityEventByName( ent, "disable", 0.0f ); + PostEntityEventByName( ent, "enable", enableDelay ); + PostEntityEventByName( ent, "disable", enableDelay + activeTime ); + ent->PostEventSec( &EV_Remove, removeDelay ); + return ent; +} + rvmMonsterDemonArchvile::rvmMonsterDemonArchvile() { nextDodge = 0.0f; @@ -412,47 +450,69 @@ stateResult_t rvmMonsterDemonArchvile::combat_dodge_right( stateParms_t* parms ) } stateResult_t rvmMonsterDemonArchvile::spawn_flame_wall( stateParms_t* parms ) { - // Hook point for the original script's repeated sys.spawn of flame-wall projectiles. - // The looping behavior is represented by flame_wall_loop() below, not a blocking wait. + idAngles angles = owner ? owner->GetPhysics()->GetAxis().ToAngles() : ang_zero; + angles.yaw = RandomFloat( 360.0f ); + SpawnArchvileTimedTrigger( "archvile_flamewall", flameWallOrigin, angles, owner, 0.2f, 0.5f, 2.7f ); return SRESULT_DONE; } stateResult_t rvmMonsterDemonArchvile::spawn_incerate( stateParms_t* parms ) { - // Hook point for the original script's incinerate projectile spawn. + idAngles angles = owner ? owner->GetPhysics()->GetAxis().ToAngles() : ang_zero; + angles.yaw = RandomFloat( 360.0f ); + SpawnArchvileTimedTrigger( "archvile_incinerate", incinerateOrigin, angles, owner, 0.4f, 0.4f, 1.4f ); return SRESULT_DONE; } stateResult_t rvmMonsterDemonArchvile::flame_wall_loop( stateParms_t* parms ) { - enum { STAGE_INIT, STAGE_SPAWN, STAGE_DELAY }; + enum { STAGE_INIT, STAGE_TRACE, STAGE_DELAY }; switch ( parms->stage ) { case STAGE_INIT: scratchCount = 0; - parms->stage = STAGE_SPAWN; + flameWallOrigin = GetOrigin(); + scratchEntity = NULL; + parms->stage = STAGE_TRACE; return SRESULT_WAIT; - case STAGE_SPAWN: - if ( scratchCount >= ARCH_FLAME_WALL_NUM ) { return SRESULT_DONE; } - flameWallOrigin = GetEnemyPos(); - StartAction( "spawn_flame_wall" ); + case STAGE_TRACE: { + if ( scratchCount >= ARCH_FLAME_WALL_NUM || !owner ) { return SRESULT_DONE; } + idAngles angles = owner->GetPhysics()->GetAxis().ToAngles(); + idVec3 dir = angles.ToForward() * ARCH_FLAME_WALL_SEPERATION; + idVec3 pos = flameWallOrigin; + const idBounds bounds( idVec3( -8.0f, -8.0f, 0.0f ), idVec3( 8.0f, 8.0f, 16.0f ) ); + trace_t trace; + + pos.z += 48.0f; + gameLocal.clip.TraceBounds( trace, pos, pos + dir, bounds, MASK_SOLID, owner ); + if ( trace.fraction < 1.0f ) { + return SRESULT_DONE; + } + pos += dir; + gameLocal.clip.TraceBounds( trace, pos, pos - idVec3( 0.0f, 0.0f, 96.0f ), bounds, MASK_SOLID, owner ); + if ( trace.fraction >= 1.0f ) { + return SRESULT_DONE; + } + pos.z -= 96.0f * trace.fraction; + flameWallOrigin = pos; + spawn_flame_wall( parms ); scratchCount++; - scratchEndTime = DelayTime( 0.05f ); + scratchEndTime = DelayTime( 0.075f ); parms->stage = STAGE_DELAY; return SRESULT_WAIT; + } case STAGE_DELAY: - if ( InAction() || Time() < scratchEndTime ) { return SRESULT_WAIT; } - parms->stage = STAGE_SPAWN; + if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } + parms->stage = STAGE_TRACE; return SRESULT_WAIT; } return SRESULT_ERROR; } stateResult_t rvmMonsterDemonArchvile::flame_wall( stateParms_t* parms ) { - if ( parms->stage == 0 ) { StartAction( "flame_wall_loop" ); parms->stage = 1; return SRESULT_WAIT; } - return InAction() ? SRESULT_WAIT : SRESULT_DONE; + return flame_wall_loop( parms ); } stateResult_t rvmMonsterDemonArchvile::incinerate( stateParms_t* parms ) { - incinerateOrigin = GetEnemyPos(); - StartAction( "spawn_incerate" ); + incinerateOrigin = PredictEnemyPos( 0.4f ); + spawn_incerate( parms ); return SRESULT_DONE; } diff --git a/neo/doom3/game/ai/AI_monster_demon_trite.cpp b/neo/doom3/game/ai/AI_monster_demon_trite.cpp index 445f8851..43f62df6 100644 --- a/neo/doom3/game/ai/AI_monster_demon_trite.cpp +++ b/neo/doom3/game/ai/AI_monster_demon_trite.cpp @@ -414,7 +414,20 @@ stateResult_t rvmMonsterDemonTrite::state_Explode( stateParms_t* parms ) { enum { STAGE_START, STAGE_REMOVE }; switch ( parms->stage ) { case STAGE_START: - RadiusDamageFromJoint( "", GetKey( "def_explode_damage", "damage_trite_explode" ) ); + if ( owner ) { + idEntity* explosion = NULL; + idDict args; + args.Set( "classname", "func_emitter" ); + args.Set( "model", GetKey( "model_explode" ) ); + if ( gameLocal.SpawnEntityDef( args, &explosion ) && explosion ) { + explosion->Show(); + explosion->SetOrigin( GetOrigin() ); + explosion->SetShaderParm( SHADERPARM_TIMEOFFSET, -Time() ); + explosion->SetShaderParm( SHADERPARM_DIVERSITY, RandomFloat( 1.0f ) ); + explosion->PostEventSec( &EV_Remove, 5.0f ); + } + gameLocal.RadiusDamage( GetOrigin(), owner, owner, owner, owner, GetKey( "def_explode_damage", "damage_trite_explode" ), 1.0f ); + } StartSoundKey( "snd_explode", SND_CHANNEL_BODY, false ); if ( owner ) { owner->ActivateTargets( owner ); } HideOwner(); diff --git a/neo/doom3/game/ai/AI_monster_demon_wraith.cpp b/neo/doom3/game/ai/AI_monster_demon_wraith.cpp index 86aff845..73ac9952 100644 --- a/neo/doom3/game/ai/AI_monster_demon_wraith.cpp +++ b/neo/doom3/game/ai/AI_monster_demon_wraith.cpp @@ -23,6 +23,14 @@ void rvmMonsterDemonWraith::Init( idAI* aiOwner ) { SetState( "state_Begin" ); } +void rvmMonsterDemonWraith::Destroy() { + if ( spawn_effect.GetEntity() ) { + spawn_effect.GetEntity()->PostEventMS( &EV_Remove, 0 ); + spawn_effect = NULL; + } + rvmMonsterBase::Destroy(); +} + stateResult_t rvmMonsterDemonWraith::RunStateByName( const char* stateName, stateParms_t* parms ) { AI_DISPATCH_STATE( "Torso_Idle", Torso_Idle ); AI_DISPATCH_STATE( "Torso_Pain", Torso_Pain ); @@ -249,8 +257,22 @@ stateResult_t rvmMonsterDemonWraith::combat_dodge_right( stateParms_t* parms ) { } stateResult_t rvmMonsterDemonWraith::spawn_fx( stateParms_t* parms ) { - // Project-specific effect spawning is intentionally isolated here. - // Bind your wraith_spawneffect entity to spawn_effect if desired. + if ( !owner ) { + return SRESULT_DONE; + } + if ( !spawn_effect.GetEntity() ) { + idDict args; + args.Set( "classname", "wraith_spawneffect" ); + idEntity* ent = NULL; + if ( gameLocal.SpawnEntityDef( args, &ent ) ) { + spawn_effect = ent; + } + } + if ( spawn_effect.GetEntity() ) { + spawn_effect.GetEntity()->SetOrigin( GetOrigin() ); + spawn_effect.GetEntity()->Show(); + spawn_effect.GetEntity()->SetShaderParm( SHADERPARM_TIMEOFFSET, -Time() ); + } return SRESULT_DONE; } @@ -270,13 +292,20 @@ stateResult_t rvmMonsterDemonWraith::combat_become_visible( stateParms_t* parms AllowMovement( true ); SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); StartSoundKey( "snd_fade_in", SND_CHANNEL_VOICE2, false ); - ShowOwner(); - scratchEndTime = DelayTime( 0.75f ); + spawn_fx( parms ); + scratchEndTime = DelayTime( 0.65f ); parms->stage = STAGE_FADE; return SRESULT_WAIT; case STAGE_FADE: if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } + ShowOwner(); + if ( owner ) { + owner->SetShaderParm( SHADERPARM_TIME_OF_DEATH, Time() ); + } ClearBurn(); + if ( spawn_effect.GetEntity() ) { + spawn_effect.GetEntity()->Hide(); + } invisible = false; nextInvisible = RandomDelay( WRAITH_INVISIBLE_DELAY_MIN, WRAITH_INVISIBLE_DELAY_MAX ); return SRESULT_DONE; @@ -292,6 +321,7 @@ stateResult_t rvmMonsterDemonWraith::combat_become_invisible( stateParms_t* parm if ( invisible ) { return SRESULT_DONE; } invisible = true; StartSoundKey( "snd_fade_out", SND_CHANNEL_VOICE2, false ); + spawn_fx( parms ); scratchEndTime = DelayTime( 0.817f ); parms->stage = STAGE_FADEOUT; return SRESULT_WAIT; @@ -299,7 +329,13 @@ stateResult_t rvmMonsterDemonWraith::combat_become_invisible( stateParms_t* parm if ( Time() < scratchEndTime ) { return SRESULT_WAIT; } PreBurn(); HideOwner(); + if ( spawn_effect.GetEntity() ) { + spawn_effect.GetEntity()->Hide(); + } if ( owner ) { owner->AllowHiddenMovement( 1 ); } + if ( owner ) { + owner->SetShaderParm( SHADERPARM_TIME_OF_DEATH, Time() - 100.0f ); + } nextInvisible = RandomDelay( WRAITH_VISIBLE_DELAY_MIN, WRAITH_VISIBLE_DELAY_MAX ); return SRESULT_DONE; } diff --git a/neo/doom3/game/ai/AI_monster_demon_wraith.h b/neo/doom3/game/ai/AI_monster_demon_wraith.h index c2e9f6ff..31cf5751 100644 --- a/neo/doom3/game/ai/AI_monster_demon_wraith.h +++ b/neo/doom3/game/ai/AI_monster_demon_wraith.h @@ -95,6 +95,7 @@ rvmMonsterDemonWraith(); virtual void Init( idAI* aiOwner ) override; + virtual void Destroy() override; virtual stateResult_t RunStateByName( const char* stateName, stateParms_t* parms ) override; protected: