// Gets called each time the mind is thinking void IdleSleepState::Think(idAI* owner) { Memory& memory = owner->GetMemory(); if (_startSleeping && owner->GetMoveType() == MOVETYPE_ANIM) { if (owner->ReachedPos(memory.idlePosition, MOVE_TO_POSITION) && owner->GetCurrentYaw() == memory.idleYaw) { owner->AI_LAY_DOWN_LEFT = owner->spawnArgs.GetBool("lay_down_left", "1"); owner->LayDown(); } } else if (owner->GetMoveType() == MOVETYPE_GET_UP_FROM_LYING) { owner->GetMind()->SwitchState(owner->backboneStates[ERelaxed]); owner->commSubsystem->ClearTasks(); return; } UpdateAlertLevel(); // Ensure we are in the correct alert level if (!CheckAlertLevel(owner)) return; }
// Gets called each time the mind is thinking void AgitatedSearchingStateLanternBot::Think(idAI* owner) { UpdateAlertLevel(); // Ensure we are in the correct alert level if (!CheckAlertLevel(owner)) { // owner->GetMind()->EndState(); // grayman #3182 - already done in CheckAlertLevel() return; } // Move (if alert position has changed) MoveTowardAlertPos(owner); if (owner->GetMoveStatus() == MOVE_STATUS_DONE) { // Look at alert position, now that we've finished moving owner->TurnToward(_curAlertPos); // Let the alertness decrease from now on CalculateAlertDecreaseRate(owner); } else { // Moving along, ensure that we're not dropping out of this state as long as we're moving _alertLevelDecreaseRate = 0; return; } }
void IdleSleepState::Init(idAI* owner) { // Init base class first State::Init(owner); DM_LOG(LC_AI, LT_INFO)LOGSTRING("IdleSleepState initialised.\r"); assert(owner); // Memory shortcut Memory& memory = owner->GetMemory(); memory.alertClass = EAlertNone; memory.alertType = EAlertTypeNone; if (owner->HasSeenEvidence() && !owner->spawnArgs.GetBool("disable_alert_idle", "0")) { owner->GetMind()->SwitchState(STATE_ALERT_IDLE); return; } if (owner->GetMoveType() != MOVETYPE_SLEEP && (!owner->ReachedPos(memory.idlePosition, MOVE_TO_POSITION) || owner->GetCurrentYaw() != memory.idleYaw)) { // we need to get to the bed first before starting to sleep, back to idle state owner->GetMind()->SwitchState(owner->backboneStates[ERelaxed]); return; } // Ensure we are in the correct alert level if (!CheckAlertLevel(owner)) return; _startSleeping = owner->spawnArgs.GetBool("sleeping", "0"); _startSitting = owner->spawnArgs.GetBool("sitting", "0"); _alertLevelDecreaseRate = 0.01f; // owner->SheathWeapon(); owner->actionSubsystem->ClearTasks(); owner->senseSubsystem->ClearTasks(); owner->commSubsystem->ClearTasks(); InitialiseMovement(owner); int idleBarkIntervalMin = SEC2MS(owner->spawnArgs.GetInt("sleep_bark_interval_min", "10")); int idleBarkIntervalMax = SEC2MS(owner->spawnArgs.GetInt("sleep_bark_interval_max", "30")); owner->commSubsystem->AddCommTask( CommunicationTaskPtr(new RepeatedBarkTask("snd_sleeping", idleBarkIntervalMin, idleBarkIntervalMax)) ); // Let the AI update their weapons (make them nonsolid) owner->UpdateAttachmentContents(false); }
void AgitatedSearchingStateLanternBot::Init(idAI* owner) { // Init base class first State::Init(owner); DM_LOG(LC_AI, LT_INFO)LOGSTRING("AgitatedSearchingStateLanternBot initialised.\r"); assert(owner); // Ensure we are in the correct alert level if (!CheckAlertLevel(owner)) { return; } owner->movementSubsystem->ClearTasks(); owner->senseSubsystem->ClearTasks(); owner->actionSubsystem->ClearTasks(); owner->commSubsystem->ClearTasks(); // grayman #3182 owner->searchSubsystem->ClearTasks(); // grayman #3857 owner->StopMove(MOVE_STATUS_DONE); // Start with an invalid position _curAlertPos = idVec3(idMath::INFINITY, idMath::INFINITY, idMath::INFINITY); // Move to a position where we can light up the alert position from MoveTowardAlertPos(owner); owner->GetMemory().currentSearchEventID = owner->LogSuspiciousEvent( E_EventTypeMisc, owner->GetPhysics()->GetOrigin(), NULL, true ); // grayman #3857 // This will hold the message to be delivered with the inaudible bark CommMessagePtr message(new CommMessage( CommMessage::RequestForHelp_CommType, // grayman #3857 - asking for a response //CommMessage::DetectedEnemy_CommType, // grayman #3857 - this does nothing when no entity (parameter 4) is provided owner, NULL,// from this AI to anyone NULL, _curAlertPos, owner->GetMemory().currentSearchEventID // grayman #3857 (was '0') )); // The communication system plays starting bark owner->commSubsystem->AddCommTask( CommunicationTaskPtr(new RepeatedBarkTask("snd_spotted_noise", 3000, 4000, message)) ); // Add the script task blowing the alarm whistle owner->actionSubsystem->PushTask(TaskPtr(new ScriptTask("startAlarmWhistle"))); }
// Gets called each time the mind is thinking void ObservantState::Think(idAI* owner) { UpdateAlertLevel(); // Ensure we are in the correct alert level if (!CheckAlertLevel(owner)) { return; } // grayman #3520 - look at alert spots if (owner->m_lookAtAlertSpot) { owner->m_lookAtAlertSpot = false; if ( owner->GetMoveType() != MOVETYPE_SLEEP ) // grayman #3487 - not if asleep { idVec3 alertSpot = owner->m_lookAtPos; if ( alertSpot.x != idMath::INFINITY ) { if (owner->CheckFOV(alertSpot)) { owner->Event_LookAtPosition(alertSpot,((float)owner->AI_AlertLevel)/10.0f); } } } owner->m_lookAtPos = idVec3(idMath::INFINITY,idMath::INFINITY,idMath::INFINITY); } // grayman #2866 - zero alert decrease rate if handling a door or elevator if ((owner->m_HandlingDoor) || (owner->m_HandlingElevator)) { _alertLevelDecreaseRate = 0; } else { _alertLevelDecreaseRate = owner->GetMemory().savedAlertLevelDecreaseRate; } if (owner->GetMoveType() != MOVETYPE_SLEEP) { // Let the AI check its senses owner->PerformVisualScan(); } }
void SearchingState::Init( idAI *owner ) { // Init base class first State::Init( owner ); DM_LOG( LC_AI, LT_INFO )LOGSTRING( "SearchingState initialised.\r" ); assert( owner ); // Ensure we are in the correct alert level if( !CheckAlertLevel( owner ) ) { return; } if( owner->GetMoveType() == MOVETYPE_SIT || owner->GetMoveType() == MOVETYPE_SLEEP ) { owner->GetUp(); } // Shortcut reference Memory &memory = owner->GetMemory(); float alertTime = owner->atime3 + owner->atime3_fuzzyness * ( gameLocal.random.RandomFloat() - 0.5 ); _alertLevelDecreaseRate = ( owner->thresh_4 - owner->thresh_3 ) / alertTime; if( owner->AlertIndexIncreased() || memory.mandatory ) { // grayman #3331 // Setup a new hiding spot search StartNewHidingSpotSearch( owner ); } if( owner->AlertIndexIncreased() ) { // grayman #3423 - when the alert level is ascending, kill the repeated bark task owner->commSubsystem->ClearTasks(); // Play bark if alert level is ascending // grayman #3496 - enough time passed since last alert bark? if( gameLocal.time >= memory.lastTimeAlertBark + MIN_TIME_BETWEEN_ALERT_BARKS ) { idStr bark; if( ( memory.alertedDueToCommunication == false ) && ( ( memory.alertType == EAlertTypeSuspicious ) || ( memory.alertType == EAlertTypeEnemy ) ) ) { bool friendsNear = ( ( MS2SEC( gameLocal.time - memory.lastTimeFriendlyAISeen ) ) <= MAX_FRIEND_SIGHTING_SECONDS_FOR_ACCOMPANIED_ALERT_BARK ); if( ( memory.alertClass == EAlertVisual_1 ) || ( memory.alertClass == EAlertVisual_2 ) || // grayman #2603, #3424 // (memory.alertClass == EAlertVisual_3) ) || // grayman #3472 - no longer needed ( memory.alertClass == EAlertVisual_4 ) ) { // grayman #3498 if( friendsNear ) { bark = "snd_alert3sc"; } else { bark = "snd_alert3s"; } } else if( memory.alertClass == EAlertAudio ) { if( friendsNear ) { bark = "snd_alert3hc"; } else { bark = "snd_alert3h"; } } else if( friendsNear ) { bark = "snd_alert3c"; } else { bark = "snd_alert3"; } // Allocate a SingleBarkTask, set the sound and enqueue it owner->commSubsystem->AddCommTask( CommunicationTaskPtr( new SingleBarkTask( bark ) ) ); memory.lastTimeAlertBark = gameLocal.time; // grayman #3496 if( cv_ai_debug_transition_barks.GetBool() ) { gameLocal.Printf( "%d: %s rises to Searching state, barks '%s'\n", gameLocal.time, owner->GetName(), bark.c_str() ); } } } else { if( cv_ai_debug_transition_barks.GetBool() ) { gameLocal.Printf( "%d: %s rises to Searching state, can't bark 'snd_alert3{s/sc/h/hc/c}' yet\n", gameLocal.time, owner->GetName() ); } } } else if( memory.alertType == EAlertTypeEnemy ) { // reduce the alert type, so we can react to other alert types (such as a dead person) memory.alertType = EAlertTypeSuspicious; } // grayman #3472 - When ascending, set up a repeated bark if( owner->AlertIndexIncreased() ) { owner->commSubsystem->AddSilence( 5000 + gameLocal.random.RandomInt( 3000 ) ); // grayman #3424 // This will hold the message to be delivered with the bark CommMessagePtr message( new CommMessage( CommMessage::DetectedEnemy_CommType, owner, NULL,// from this AI to anyone NULL, idVec3( idMath::INFINITY, idMath::INFINITY, idMath::INFINITY ), 0 ) ); int minTime = SEC2MS( owner->spawnArgs.GetFloat( "searchbark_delay_min", "10" ) ); int maxTime = SEC2MS( owner->spawnArgs.GetFloat( "searchbark_delay_max", "15" ) ); owner->commSubsystem->AddCommTask( CommunicationTaskPtr( new RepeatedBarkTask( "snd_state3", minTime, maxTime, message ) ) ); } else { // descending // Allow repeated barks from Agitated Searching to continue. } if( !owner->HasSeenEvidence() ) { owner->SheathWeapon(); owner->UpdateAttachmentContents( false ); } else { // Let the AI update their weapons (make them solid) owner->UpdateAttachmentContents( true ); } }
// Gets called each time the mind is thinking void SearchingState::Think( idAI *owner ) { UpdateAlertLevel(); // Ensure we are in the correct alert level if( !CheckAlertLevel( owner ) ) { return; } // grayman #3063 - move up so it gets done each time, // regardless of what state the hiding spot search is in. // Let the AI check its senses owner->PerformVisualScan(); if( owner->GetMoveType() == MOVETYPE_SIT || owner->GetMoveType() == MOVETYPE_SLEEP || owner->GetMoveType() == MOVETYPE_SIT_DOWN || owner->GetMoveType() == MOVETYPE_LAY_DOWN ) { owner->GetUp(); return; } Memory &memory = owner->GetMemory(); owner->MarkEventAsSearched( memory.currentSearchEventID ); // grayman #3424 // grayman #3520 - look at alert spots if( owner->m_lookAtAlertSpot ) { owner->m_lookAtAlertSpot = false; idVec3 alertSpot = owner->m_lookAtPos; if( alertSpot.x != idMath::INFINITY ) { // grayman #3438 if( !owner->CheckFOV( alertSpot ) ) { // Search spot is not within FOV, turn towards the position owner->TurnToward( alertSpot ); owner->Event_LookAtPosition( alertSpot, 2.0f ); } else { owner->Event_LookAtPosition( alertSpot, 2.0f ); } } owner->m_lookAtPos = idVec3( idMath::INFINITY, idMath::INFINITY, idMath::INFINITY ); } // grayman #3200 - if asked to restart the hiding spot search, don't continue with the current hiding spot search if( memory.restartSearchForHidingSpots ) { // We should restart the search (probably due to a new incoming stimulus) // Setup a new hiding spot search StartNewHidingSpotSearch( owner ); } else if( !memory.hidingSpotSearchDone ) { // Do we have an ongoing hiding spot search? // Let the hiding spot search do its task PerformHidingSpotSearch( owner ); // Let the AI check its senses // owner->PerformVisualScan(); // grayman #3063 - moved to front /* // angua: commented this out, problems with getting up from sitting idStr waitState(owner->WaitState()); if (waitState.IsEmpty()) { // Waitstate is not matching, this means that the animation // can be started. owner->SetAnimState(ANIMCHANNEL_TORSO, "Torso_LookAround", 5); //owner->SetAnimState(ANIMCHANNEL_LEGS, "Legs_LookAround", 5); // Set the waitstate, this gets cleared by // the script function when the animation is done. owner->SetWaitState("look_around"); } */ } // Is a hiding spot search in progress? else if( !memory.hidingSpotInvestigationInProgress ) { // Spot search and investigation done, what next? // Have run out of hiding spots? if( memory.noMoreHidingSpots ) { if( gameLocal.time >= memory.nextTime2GenRandomSpot ) { memory.nextTime2GenRandomSpot = gameLocal.time + DELAY_RANDOM_SPOT_GEN * ( 1 + ( gameLocal.random.RandomFloat() - 0.5 ) / 3 ); // grayman #2422 // Generate a random search point, but make sure it's inside an AAS area // and that it's also inside the search volume. idVec3 p; // random point int areaNum; // p's area number idVec3 searchSize = owner->m_searchLimits.GetSize(); idVec3 searchCenter = owner->m_searchLimits.GetCenter(); //gameRenderWorld->DebugBox(colorWhite, idBox(owner->m_searchLimits), MS2SEC(memory.nextTime2GenRandomSpot - gameLocal.time)); bool validPoint = false; for( int i = 0 ; i < 6 ; i++ ) { p = searchCenter; p.x += gameLocal.random.RandomFloat() * ( searchSize.x ) - searchSize.x / 2; p.y += gameLocal.random.RandomFloat() * ( searchSize.y ) - searchSize.y / 2; p.z += gameLocal.random.RandomFloat() * ( searchSize.z ) - searchSize.z / 2; //p.z += gameLocal.random.RandomFloat()*(searchSize.z/2) - searchSize.z/4; areaNum = owner->PointReachableAreaNum( p ); if( areaNum == 0 ) { //gameRenderWorld->DebugArrow(colorRed, owner->GetEyePosition(), p, 1, MS2SEC(memory.nextTime2GenRandomSpot - gameLocal.time)); continue; } owner->GetAAS()->PushPointIntoAreaNum( areaNum, p ); // if this point is outside this area, it will be moved to one of the area's edges if( !owner->m_searchLimits.ContainsPoint( p ) ) { //gameRenderWorld->DebugArrow(colorPink, owner->GetEyePosition(), p, 1, MS2SEC(memory.nextTime2GenRandomSpot - gameLocal.time)); continue; } //gameRenderWorld->DebugArrow(colorGreen, owner->GetEyePosition(), p, 1, MS2SEC(memory.nextTime2GenRandomSpot - gameLocal.time)); validPoint = true; break; } if( validPoint ) { // grayman #2422 - the point chosen memory.currentSearchSpot = p; // Choose to investigate spots closely on a random basis // grayman #2801 - and only if you weren't hit by a projectile memory.investigateStimulusLocationClosely = ( ( gameLocal.random.RandomFloat() < 0.3f ) && ( memory.alertType != EAlertTypeHitByProjectile ) ); owner->actionSubsystem->PushTask( TaskPtr( InvestigateSpotTask::CreateInstance() ) ); //gameRenderWorld->DebugArrow(colorGreen, owner->GetEyePosition(), memory.currentSearchSpot, 1, 500); // Set the flag to TRUE, so that the sensory scan can be performed memory.hidingSpotInvestigationInProgress = true; } if( !validPoint ) { // no valid random point found // Stop moving, the algorithm will choose another spot the next round owner->StopMove( MOVE_STATUS_DONE ); memory.StopReacting(); // grayman #3559 // grayman #2422 - at least turn toward and look at the last invalid point some of the time // grayman #3492 - do it every time //if ( gameLocal.random.RandomFloat() < 0.5 ) //{ p.z += 60; // look up a bit, to simulate searching for the player's head if( !owner->CheckFOV( p ) ) { owner->TurnToward( p ); } owner->Event_LookAtPosition( p, MS2SEC( memory.nextTime2GenRandomSpot - gameLocal.time + 100 ) ); //gameRenderWorld->DebugArrow(colorPink, owner->GetEyePosition(), p, 1, MS2SEC(memory.nextTime2GenRandomSpot - gameLocal.time + 100)); //} } } } // We should have more hiding spots, try to get the next one else if( !ChooseNextHidingSpotToSearch( owner ) ) { // No more hiding spots to search DM_LOG( LC_AI, LT_INFO )LOGSTRING( "No more hiding spots!\r" ); // Stop moving, the algorithm will choose another spot the next round owner->StopMove( MOVE_STATUS_DONE ); memory.StopReacting(); // grayman #3559 } else { // ChooseNextHidingSpot returned TRUE, so we have memory.currentSearchSpot set //gameRenderWorld->DebugArrow(colorBlue, owner->GetEyePosition(), memory.currentSearchSpot, 1, 2000); // Delegate the spot investigation to a new task, this will take the correct action. owner->actionSubsystem->PushTask( InvestigateSpotTask::CreateInstance() ); // Prevent falling into the same hole twice memory.hidingSpotInvestigationInProgress = true; } } /* grayman #3200 - moved up else if (memory.restartSearchForHidingSpots) { // We should restart the search (probably due to a new incoming stimulus) // Setup a new hiding spot search StartNewHidingSpotSearch(owner); } else // grayman #3063 - moved to front { // Move to Hiding spot is ongoing, do additional sensory tasks here // Let the AI check its senses owner->PerformVisualScan(); } */ }
// Gets called each time the mind is thinking void CombatState::Think(idAI* owner) { // Do we have an expiry date? if (_endTime > 0) { if (gameLocal.time >= _endTime) { owner->GetMind()->EndState(); } return; } // Ensure we are in the correct alert level if (!CheckAlertLevel(owner)) { // owner->GetMind()->EndState(); // grayman #3182 - already done in CheckAlertLevel() return; } // grayman #3331 - make sure you're still fighting the same enemy. // grayman #3355 - fight the closest enemy idActor* enemy = _enemy.GetEntity(); idActor* newEnemy = owner->GetEnemy(); if ( enemy ) { if ( newEnemy && ( newEnemy != enemy ) ) { idVec3 ownerOrigin = owner->GetPhysics()->GetOrigin(); float dist2EnemySqr = ( enemy->GetPhysics()->GetOrigin() - ownerOrigin ).LengthSqr(); float dist2NewEnemySqr = ( newEnemy->GetPhysics()->GetOrigin() - ownerOrigin ).LengthSqr(); if ( dist2NewEnemySqr < dist2EnemySqr ) { owner->GetMind()->EndState(); return; // state has ended } } } else { enemy = newEnemy; } if (!CheckEnemyStatus(enemy, owner)) { owner->GetMind()->EndState(); return; // state has ended } // grayman #3520 - don't look toward new alerts if ( owner->m_lookAtAlertSpot ) { owner->m_lookAtAlertSpot = false; owner->m_lookAtPos = idVec3(idMath::INFINITY,idMath::INFINITY,idMath::INFINITY); } // angua: look at enemy owner->Event_LookAtPosition(enemy->GetEyePosition(), gameLocal.msec); Memory& memory = owner->GetMemory(); idVec3 vec2Enemy = enemy->GetPhysics()->GetOrigin() - owner->GetPhysics()->GetOrigin(); float dist2Enemy = vec2Enemy.LengthFast(); // grayman #3331 - need to take vertical separation into account. It's possible to have the origins // close enough to be w/in the melee zone, but still be unable to hit the enemy. bool inMeleeRange = ( dist2Enemy <= ( 3 * owner->GetMeleeRange() ) ); ECombatType newCombatType; if ( inMeleeRange && !_meleePossible ) // grayman #3355 - can't fight up close { owner->fleeingEvent = false; // grayman #3356 owner->emitFleeBarks = false; // grayman #3474 owner->GetMind()->SwitchState(STATE_FLEE); return; } if ( !inMeleeRange && _rangedPossible ) { newCombatType = COMBAT_RANGED; } else { newCombatType = COMBAT_MELEE; } // Check for situation where you're in the melee zone, yet you're unable to hit // the enemy. This can happen if the enemy is above or below you and you can't // reach them. switch(_combatSubState) { case EStateReaction: { if ( gameLocal.time < _reactionEndTime ) { return; // stay in this state until the reaction time expires } // Check to see if the enemy is still visible. // grayman #2816 - Visibility doesn't matter if you're in combat because // you bumped into your enemy. idEntity* tactEnt = owner->GetTactEnt(); if ( ( tactEnt == NULL ) || !tactEnt->IsType(idActor::Type) || ( tactEnt != enemy ) || !owner->AI_TACTALERT ) { if ( !owner->CanSee(enemy, true) ) { owner->ClearEnemy(); owner->SetAlertLevel(owner->thresh_5 - 0.1); // reset alert level just under Combat owner->GetMind()->EndState(); return; } } owner->m_ignorePlayer = false; // grayman #3063 - clear flag that prevents mission statistics on player sightings // The AI has processed his reaction, and needs to move into combat, or flee. _criticalHealth = owner->spawnArgs.GetInt("health_critical", "0"); // greebo: Check for weapons and flee if ... if ( ( !_meleePossible && !_rangedPossible ) || // ... I'm unarmed ( owner->spawnArgs.GetBool("is_civilian", "0")) || // ... I'm a civilian, and don't fight ( owner->health < _criticalHealth ) ) // grayman #3140 ... I'm very damaged and can't afford to engage in combat { owner->fleeingEvent = false; // grayman #3356 owner->emitFleeBarks = true; // grayman #3474 owner->GetMind()->SwitchState(STATE_FLEE); return; } memory.stopRelight = true; // grayman #2603 - abort a relight in progress memory.stopExaminingRope = true; // grayman #2872 - stop examining a rope memory.stopReactingToHit = true; // grayman #2816 _combatSubState = EStateDoOnce; break; } case EStateDoOnce: { // Check for sitting or sleeping moveType_t moveType = owner->GetMoveType(); if ( moveType == MOVETYPE_SIT || moveType == MOVETYPE_SLEEP || moveType == MOVETYPE_SIT_DOWN || moveType == MOVETYPE_LAY_DOWN ) { owner->GetUp(); // okay if called multiple times return; } // grayman #3009 - check for getting up from sitting or sleeping if ( moveType == MOVETYPE_GET_UP || moveType == MOVETYPE_GET_UP_FROM_LYING ) { return; } // not sitting or sleeping at this point // Stop what you're doing owner->StopMove(MOVE_STATUS_DONE); owner->movementSubsystem->ClearTasks(); owner->senseSubsystem->ClearTasks(); owner->actionSubsystem->ClearTasks(); // Bark // This will hold the message to be delivered with the bark, if appropriate CommMessagePtr message; // Only alert the bystanders if we didn't receive the alert by message ourselves if (!memory.alertedDueToCommunication) { message = CommMessagePtr(new CommMessage( CommMessage::DetectedEnemy_CommType, owner, NULL, // from this AI to anyone enemy, memory.lastEnemyPos, 0 )); } // grayman #3496 - This is an alert bark, but it's not subject to the delay // between alert barks because the previous bark will end before we get here. // It's also the culmination of a journey up from Idle State, and if the AI is // going to go after an enemy, this bark needs to be heard. // We'll still reset the last alert bark time, though. // The communication system plays starting bark // grayman #3343 - accommodate different barks for human and non-human enemies idPlayer* player(NULL); if (enemy->IsType(idPlayer::Type)) { player = static_cast<idPlayer*>(enemy); } idStr bark = ""; if (player && player->m_bShoulderingBody) { bark = "snd_spotted_player_with_body"; } else if ((MS2SEC(gameLocal.time - memory.lastTimeFriendlyAISeen)) <= MAX_FRIEND_SIGHTING_SECONDS_FOR_ACCOMPANIED_ALERT_BARK) { idStr enemyAiUse = enemy->spawnArgs.GetString("AIUse"); if ( ( enemyAiUse == AIUSE_MONSTER ) || ( enemyAiUse == AIUSE_UNDEAD ) ) { bark = "snd_to_combat_company_monster"; } else { bark = "snd_to_combat_company"; } } else { idStr enemyAiUse = enemy->spawnArgs.GetString("AIUse"); if ( ( enemyAiUse == AIUSE_MONSTER ) || ( enemyAiUse == AIUSE_UNDEAD ) ) { bark = "snd_to_combat_monster"; } else { bark = "snd_to_combat"; } } owner->commSubsystem->AddCommTask(CommunicationTaskPtr(new SingleBarkTask(bark, message))); owner->GetMemory().lastTimeAlertBark = gameLocal.time; // grayman #3496 if (cv_ai_debug_transition_barks.GetBool()) { gameLocal.Printf("%d: %s starts combat, barks '%s'\n",gameLocal.time,owner->GetName(),bark.c_str()); } _justDrewWeapon = false; _combatSubState = EStateCheckWeaponState; break; } case EStateCheckWeaponState: // check which combat type we should use { // Can you continue with your current combat type, and not have to switch weapons? // Check for case where melee combat has stalled. You're in the melee zone, but you're // unable to hit the enemy. Perhaps he's higher or lower than you and you can't reach him. if ( !owner->AI_FORWARD && // not moving ( _combatType == COMBAT_MELEE ) && // in melee combat _rangedPossible && // ranged combat is possible !owner->TestMelee() ) // I can't hit the enemy { float orgZ = owner->GetPhysics()->GetOrigin().z; float height = owner->GetPhysics()->GetBounds().GetSize().z; float enemyOrgZ = enemy->GetPhysics()->GetOrigin().z; float enemyHeight = enemy->GetPhysics()->GetBounds().GetSize().z; if ( ( (orgZ + height + owner->melee_range_vert) < enemyOrgZ ) || // enemy too high ( (enemyOrgZ + enemyHeight) < orgZ ) ) // enemy too low { newCombatType = COMBAT_RANGED; } } if ( newCombatType == _combatType ) { // yes - no need to run weapon-switching animations _combatSubState = EStateCombatAndChecks; return; } // Do you need to switch a melee or ranged weapon? You might already have one // drawn, or you might have none drawn, or you might have to change weapons, // or you might be using unarmed attacks, and you don't need a drawn weapon. // Check for unarmed combat. if ( _unarmedMelee && ( newCombatType == COMBAT_MELEE ) ) { // unarmed combat doesn't need attached weapons _combatType = COMBAT_NONE; // clear ranged combat tasks and start melee combat tasks _combatSubState = EStateCombatAndChecks; return; } if ( _unarmedRanged && ( newCombatType == COMBAT_RANGED ) ) { // unarmed combat doesn't need attached weapons _combatType = COMBAT_NONE; // clear melee combat tasks and start ranged combat tasks _combatSubState = EStateCombatAndChecks; return; } // Do you have a drawn weapon? if ( owner->GetAttackFlag(COMBAT_MELEE) && ( newCombatType == COMBAT_MELEE ) ) { // melee weapon is already drawn _combatSubState = EStateCombatAndChecks; return; } if ( owner->GetAttackFlag(COMBAT_RANGED) && ( newCombatType == COMBAT_RANGED ) ) { // ranged weapon is already drawn _combatSubState = EStateCombatAndChecks; return; } // At this point, we know we need to draw a weapon that's not already drawn. // See if you need to sheathe a drawn weapon. if ( ( ( newCombatType == COMBAT_RANGED ) && owner->GetAttackFlag(COMBAT_MELEE) ) || ( ( newCombatType == COMBAT_MELEE ) && owner->GetAttackFlag(COMBAT_RANGED) ) ) { // switch from one type of weapon to another owner->movementSubsystem->ClearTasks(); owner->actionSubsystem->ClearTasks(); _combatType = COMBAT_NONE; // sheathe current weapon so you can draw the other weapon owner->SheathWeapon(); _waitEndTime = gameLocal.time + 2000; // safety net _combatSubState = EStateSheathingWeapon; return; } // No need to sheathe a weapon _combatSubState = EStateDrawWeapon; break; } case EStateSheathingWeapon: { // if you're sheathing a weapon, stay in this state until it's done, or until the timer expires // grayman #3355 - check wait state if ( ( gameLocal.time < _waitEndTime ) && ( idStr(owner->WaitState()) == "sheath") ) { return; } _combatSubState = EStateDrawWeapon; break; } case EStateDrawWeapon: { // grayman #3331 - if you don't already have the correct weapon drawn, // draw a ranged weapon if you're far from the enemy, and you have a // ranged weapon, otherwise draw your melee weapon bool drawingWeapon = false; if ( !inMeleeRange ) { // beyond melee range if ( !owner->GetAttackFlag(COMBAT_RANGED) && _rangedPossible ) { owner->DrawWeapon(COMBAT_RANGED); drawingWeapon = true; } else // no ranged weapon { owner->DrawWeapon(COMBAT_MELEE); drawingWeapon = true; } } else // in melee range { if ( _meleePossible && !owner->GetAttackFlag(COMBAT_MELEE) ) { owner->DrawWeapon(COMBAT_MELEE); drawingWeapon = true; } } // grayman #3331 - if this is the first weapon draw, to make sure the weapon is drawn // before starting combat, delay some before starting to chase the enemy. // The farther away the enemy is, the better the chance that you'll start chasing before your // weapon is drawn. If he's close, this gives you time to completely draw your weapon before // engaging him. The interesting distance is how far you have to travel to get w/in melee range. if ( _needInitialDrawDelay ) // True if this is the first time through, and you don't already have a raised weapon { int delay = 0; if ( drawingWeapon ) { delay = (int)(2064.0f - 20.0f*(dist2Enemy - owner->GetMeleeRange())); if ( delay < 0 ) { delay = gameLocal.random.RandomInt(2064); } _waitEndTime = gameLocal.time + delay; _combatSubState = EStateDrawingWeapon; // grayman #3563 - safety net when drawing a weapon _drawEndTime = gameLocal.time + MAX_DRAW_DURATION; } else { _combatSubState = EStateCombatAndChecks; } _needInitialDrawDelay = false; // No need to do this again } else { if ( drawingWeapon ) { _waitEndTime = gameLocal.time; _combatSubState = EStateDrawingWeapon; // grayman #3563 - safety net when drawing a weapon _drawEndTime = gameLocal.time + MAX_DRAW_DURATION; } else { _combatSubState = EStateCombatAndChecks; } } break; } case EStateDrawingWeapon: { // grayman #3355 - check wait state if ( idStr(owner->WaitState()) == "draw" ) { if ( gameLocal.time < _drawEndTime ) // grayman #3563 - check safety net { return; // wait until weapon is drawn } } if ( gameLocal.time < _waitEndTime ) { return; // wait until timer expires } // Weapon is now drawn _justDrewWeapon = true; _combatSubState = EStateCombatAndChecks; break; } case EStateCombatAndChecks: { // Need to check if a weapon that was just drawn is correct for the zone you're now in, in case // you started drawing the correct weapon for one zone, and while it was drawing, you switched // to the other zone. if ( _justDrewWeapon ) { if ( newCombatType == COMBAT_RANGED ) { // beyond melee range if ( !owner->GetAttackFlag(COMBAT_RANGED) && _rangedPossible ) { // wrong weapon raised - go back and get the correct one _justDrewWeapon = false; _combatSubState = EStateCheckWeaponState; return; } } else // in melee combat { if ( !owner->GetAttackFlag(COMBAT_MELEE) && _meleePossible ) { // wrong weapon raised - go back and get the correct one _justDrewWeapon = false; _combatSubState = EStateCheckWeaponState; return; } } } _justDrewWeapon = false; if ( _combatType == COMBAT_NONE ) // Either combat hasn't been initially set up, or you're switching weapons { if ( newCombatType == COMBAT_RANGED ) { // Set up ranged combat owner->actionSubsystem->PushTask(RangedCombatTask::CreateInstance()); owner->movementSubsystem->PushTask(ChaseEnemyRangedTask::CreateInstance()); _combatType = COMBAT_RANGED; } else { // Set up melee combat ChaseEnemyTaskPtr chaseEnemy = ChaseEnemyTask::CreateInstance(); chaseEnemy->SetEnemy(enemy); owner->movementSubsystem->PushTask(chaseEnemy); owner->actionSubsystem->PushTask(MeleeCombatTask::CreateInstance()); _combatType = COMBAT_MELEE; } // Let the AI update their weapons (make them nonsolid) owner->UpdateAttachmentContents(false); } // Check the distance to the enemy, the subsystem tasks need it. memory.canHitEnemy = owner->CanHitEntity(enemy, _combatType); // grayman #3331 - willBeAbleToHitEnemy is only relevant if canHitEnemy is FALSE if ( owner->m_bMeleePredictProximity && !memory.canHitEnemy ) { memory.willBeAbleToHitEnemy = owner->WillBeAbleToHitEntity(enemy, _combatType); } // Check whether the enemy can hit us in the near future memory.canBeHitByEnemy = owner->CanBeHitByEntity(enemy, _combatType); if ( !owner->AI_ENEMY_VISIBLE && ( ( ( _combatType == COMBAT_MELEE ) && !memory.canHitEnemy ) || ( _combatType == COMBAT_RANGED) ) ) { // The enemy is not visible, let's keep track of him for a small amount of time if (gameLocal.time - memory.lastTimeEnemySeen < MAX_BLIND_CHASE_TIME) { // Cheat a bit and take the last reachable position as "visible & reachable" owner->lastVisibleReachableEnemyPos = owner->lastReachableEnemyPos; } else if (owner->ReachedPos(owner->lastVisibleReachableEnemyPos, MOVE_TO_POSITION) || ( ( gameLocal.time - memory.lastTimeEnemySeen ) > 2 * MAX_BLIND_CHASE_TIME) ) { // BLIND_CHASE_TIME has expired, we have lost the enemy! owner->GetMind()->SwitchState(STATE_LOST_TRACK_OF_ENEMY); return; } } // Flee if you're damaged and the current melee action is finished if ( ( owner->health < _criticalHealth ) && ( owner->m_MeleeStatus.m_ActionState == MELEEACTION_READY ) ) { DM_LOG(LC_AI, LT_INFO)LOGSTRING("I'm badly hurt, I'm afraid, and am fleeing!\r"); owner->fleeingEvent = false; // grayman #3182 owner->emitFleeBarks = true; // grayman #3474 owner->GetMind()->SwitchState(STATE_FLEE); return; } _combatSubState = EStateCheckWeaponState; break; } default: break; } }
void CombatState::Init(idAI* owner) { // Init base class first State::Init(owner); // Set end time to something invalid _endTime = -1; DM_LOG(LC_AI, LT_INFO)LOGSTRING("CombatState initialised.\r"); assert(owner); // Ensure we are in the correct alert level if (!CheckAlertLevel(owner)) { return; } if (!owner->GetMind()->PerformCombatCheck()) { return; } if ( ( owner->GetMoveType() == MOVETYPE_SIT ) || ( owner->GetMoveType() == MOVETYPE_SLEEP) ) { owner->GetUp(); } // grayman #3075 - if we're kneeling, doing close inspection of // a spot, stop the animation. Otherwise, the kneeling animation gets // restarted a few moments later. idStr torsoString = "Torso_KneelDown"; idStr legsString = "Legs_KneelDown"; bool torsoKneelingAnim = (torsoString.Cmp(owner->GetAnimState(ANIMCHANNEL_TORSO)) == 0); bool legsKneelingAnim = (legsString.Cmp(owner->GetAnimState(ANIMCHANNEL_LEGS)) == 0); if ( torsoKneelingAnim || legsKneelingAnim ) { // Reset anims owner->StopAnim(ANIMCHANNEL_TORSO, 0); owner->StopAnim(ANIMCHANNEL_LEGS, 0); } // grayman #3472 - kill the repeated bark task owner->commSubsystem->ClearTasks(); // grayman #3182 // grayman #3496 // Enough time passed since last alert bark? if ( gameLocal.time >= owner->GetMemory().lastTimeAlertBark + MIN_TIME_BETWEEN_ALERT_BARKS ) { // grayman #3496 - But only bark if you haven't recently spent time in Agitated Search. if ( !owner->GetMemory().agitatedSearched ) { // The communication system plays reaction bark CommMessagePtr message; owner->commSubsystem->AddCommTask(CommunicationTaskPtr(new SingleBarkTask("snd_alert5", message))); owner->GetMemory().lastTimeAlertBark = gameLocal.time; // grayman #3496 if (cv_ai_debug_transition_barks.GetBool()) { gameLocal.Printf("%d: %s enters Combat State, barks surprised reaction 'snd_alert5'\n",gameLocal.time,owner->GetName()); } } else { if (cv_ai_debug_transition_barks.GetBool()) { gameLocal.Printf("%d: %s enters Combat State after spending time in Agitated Searching, so won't bark 'snd_alert5'\n",gameLocal.time,owner->GetName()); } } } else { if (cv_ai_debug_transition_barks.GetBool()) { gameLocal.Printf("%d: %s enters Combat State, can't bark 'snd_alert5' yet\n",gameLocal.time,owner->GetName()); } } // All remaining init code is moved into Think() and done in the EStateReaction substate, // because the things it does need to occur after the initial reaction delay. // grayman #3063 // Add a delay before you process the remainder of Init(). // The length of the delay depends on the distance to the enemy. // We have an enemy, store the enemy entity locally _enemy = owner->GetEnemy(); idActor* enemy = _enemy.GetEntity(); // grayman #3331 - clear combat state _combatType = COMBAT_NONE; // get melee possibilities _meleePossible = ( owner->GetNumMeleeWeapons() > 0 ); _rangedPossible = ( owner->GetNumRangedWeapons() > 0 ); /* grayman #3492 - should wait until after the reaction time before fleeing // grayman #3355 - flee if you have no weapons if (!_meleePossible && !_rangedPossible) { owner->fleeingEvent = false; // grayman #3356 owner->emitFleeBarks = true; // grayman #3474 owner->GetMind()->SwitchState(STATE_FLEE); return; } */ // grayman #3331 - save combat possibilities _unarmedMelee = owner->spawnArgs.GetBool("unarmed_melee","0"); _unarmedRanged = owner->spawnArgs.GetBool("unarmed_ranged","0"); _armedMelee = _meleePossible && !_unarmedMelee; _armedRanged = _rangedPossible && !_unarmedRanged; // grayman #3331 - do we need an initial delay at weapon drawing? _needInitialDrawDelay = !( owner->GetAttackFlag(COMBAT_MELEE) || owner->GetAttackFlag(COMBAT_RANGED) ); // not if we have a weapon raised idVec3 vec2Enemy = enemy->GetPhysics()->GetOrigin() - owner->GetPhysics()->GetOrigin(); //vec2Enemy.z = 0; // ignore vertical component float dist2Enemy = vec2Enemy.LengthFast(); int reactionTime = REACTION_TIME_MIN + (dist2Enemy*(REACTION_TIME_MAX - REACTION_TIME_MIN))/(cv_ai_sight_combat_cutoff.GetFloat()/s_DOOM_TO_METERS); if ( reactionTime > REACTION_TIME_MAX ) { reactionTime = REACTION_TIME_MAX; } // grayman #3331 - add a bit of variability so multiple AI spotting the enemy in the same frame aren't in sync reactionTime += gameLocal.random.RandomInt(REACTION_TIME_MAX/2); _combatSubState = EStateReaction; _reactionEndTime = gameLocal.time + reactionTime; }
void AgitatedSearchingState::Init(idAI* owner) { // Init base class first (note: we're not calling SearchingState::Init() on purpose here) State::Init(owner); DM_LOG(LC_AI, LT_INFO)LOGSTRING("AgitatedSearchingState initialised.\r"); assert(owner); // Shortcut reference Memory& memory = owner->GetMemory(); memory.leaveAlertState = false; // Ensure we are in the correct alert level if ( !CheckAlertLevel(owner) ) { return; } // grayman #3496 - note that we spent time in Agitated Search memory.agitatedSearched = true; CalculateAlertDecreaseRate(owner); if (owner->GetMoveType() == MOVETYPE_SIT || owner->GetMoveType() == MOVETYPE_SLEEP) { owner->GetUp(); } // Set up a new hiding spot search if not already assigned to one if (owner->m_searchID <= 0) { if (!StartNewHidingSpotSearch(owner)) // grayman #3857 - AI gets his assignment { // grayman - this section can't run because it causes // the stealth score to rise dramatically during player sightings //owner->SetAlertLevel(owner->thresh_3 - 0.1); // failed to create a search, so drop down to Suspicious mode //owner->GetMind()->EndState(); //return; } } // kill the repeated and single bark tasks owner->commSubsystem->ClearTasks(); // grayman #3182 memory.repeatedBarkState = ERBS_NULL; // grayman #3857 if (owner->AlertIndexIncreased()) { // grayman #3496 - enough time passed since last alert bark? // grayman #3857 - enough time passed since last visual stim bark? if ( ( gameLocal.time >= memory.lastTimeAlertBark + MIN_TIME_BETWEEN_ALERT_BARKS ) && ( gameLocal.time >= memory.lastTimeVisualStimBark + MIN_TIME_BETWEEN_ALERT_BARKS ) ) { idStr soundName = ""; if ( ( memory.alertedDueToCommunication == false ) && ( ( memory.alertType == EAlertTypeSuspicious ) || ( memory.alertType == EAlertTypeEnemy ) || ( memory.alertType == EAlertTypeFailedKO ) ) ) { if (owner->HasSeenEvidence()) { soundName = "snd_alert4"; } else { soundName = "snd_alert4NoEvidence"; } CommMessagePtr message = CommMessagePtr(new CommMessage( CommMessage::DetectedSomethingSuspicious_CommType, owner, NULL, // from this AI to anyone NULL, memory.alertPos, memory.currentSearchEventID // grayman #3438 )); owner->commSubsystem->AddCommTask(CommunicationTaskPtr(new SingleBarkTask(soundName,message))); memory.lastTimeAlertBark = gameLocal.time; // grayman #3496 if (cv_ai_debug_transition_barks.GetBool()) { gameLocal.Printf("%d: %s rises to Agitated Searching state, barks '%s'\n",gameLocal.time,owner->GetName(),soundName.c_str()); } } else if ( memory.respondingToSomethingSuspiciousMsg ) // grayman #3857 { soundName = "snd_helpSearch"; // Allocate a SingleBarkTask, set the sound and enqueue it owner->commSubsystem->AddCommTask(CommunicationTaskPtr(new SingleBarkTask(soundName))); memory.lastTimeAlertBark = gameLocal.time; // grayman #3496 if (cv_ai_debug_transition_barks.GetBool()) { gameLocal.Printf("%d: %s rises to Searching state, barks '%s'\n",gameLocal.time,owner->GetName(),soundName.c_str()); } } } else { if (cv_ai_debug_transition_barks.GetBool()) { gameLocal.Printf("%d: %s rises to Agitated Searching state, can't bark 'snd_alert4{NoEvidence}' yet\n",gameLocal.time,owner->GetName()); } } } owner->commSubsystem->AddSilence(5000 + gameLocal.random.RandomInt(3000)); // grayman #3424 SetRepeatedBark(owner); // grayman #3857 DrawWeapon(owner); // grayman #3507 // Let the AI update their weapons (make them solid) owner->UpdateAttachmentContents(true); // grayman #3857 - no idle search anims in this state owner->actionSubsystem->ClearTasks(); }
void ObservantState::Init(idAI* owner) { // Init base class first State::Init(owner); DM_LOG(LC_AI, LT_INFO)LOGSTRING("ObservantState initialised.\r"); assert(owner); // Ensure we are in the correct alert level if (!CheckAlertLevel(owner)) { return; } // Shortcut reference Memory& memory = owner->GetMemory(); float alertTime = owner->atime1 + owner->atime1_fuzzyness * (gameLocal.random.RandomFloat() - 0.5); _alertLevelDecreaseRate = (owner->thresh_2 - owner->thresh_1) / alertTime; // grayman #2866 - zero alert decrease rate if handling a door or elevator memory.savedAlertLevelDecreaseRate = _alertLevelDecreaseRate; // save for restoration later if ((owner->m_HandlingDoor) || (owner->m_HandlingElevator)) { _alertLevelDecreaseRate = 0; } // Stop playing idle animation owner->actionSubsystem->ClearTasks(); // grayman #3438 - kill the repeated bark task owner->commSubsystem->ClearTasks(); owner->searchSubsystem->ClearTasks(); // grayman #3857 memory.currentSearchEventID = -1; // grayman #3472 - Play bark if alert level is ascending // grayman #3487 - But not if asleep if (owner->GetMoveType() != MOVETYPE_SLEEP) { if (owner->AlertIndexIncreased()) { // grayman #3496 - Enough time passed since last alert bark? // grayman #3857 - Enough time passed since last visual stim bark? if ( ( gameLocal.time >= memory.lastTimeAlertBark + MIN_TIME_BETWEEN_ALERT_BARKS ) && ( gameLocal.time >= memory.lastTimeVisualStimBark + MIN_TIME_BETWEEN_ALERT_BARKS ) ) { if ( !memory.alertedDueToCommunication && ( memory.alertClass != EAlertVisual_4 ) ) // grayman #2920, grayman #3498 { // barking idStr bark; if ((memory.alertClass == EAlertVisual_1) || (memory.alertClass == EAlertVisual_2) ) //(memory.alertClass == EAlertVisual_3) ) // grayman #2603, #3424, grayman #3472 - no longer needed { bark = "snd_alert1s"; } else if (memory.alertClass == EAlertAudio) { bark = "snd_alert1h"; } else { bark = "snd_alert1"; } owner->commSubsystem->AddCommTask(CommunicationTaskPtr(new SingleBarkTask(bark))); memory.lastTimeAlertBark = gameLocal.time; // grayman #3496 if (cv_ai_debug_transition_barks.GetBool()) { gameLocal.Printf("%d: %s rises to Observant state, barks '%s'\n",gameLocal.time,owner->GetName(),bark.c_str()); } } else { memory.alertedDueToCommunication = false; // reset } } else { if (cv_ai_debug_transition_barks.GetBool()) { gameLocal.Printf("%d: %s rises to Observant state, can't bark 'snd_alert1s' yet\n",gameLocal.time,owner->GetName()); } } } } // Let the AI update their weapons (make them nonsolid) owner->UpdateAttachmentContents(false); }
// Gets called each time the mind is thinking void FleeDoneState::Think(idAI* owner) { if ( owner->AI_DEAD || owner->AI_KNOCKEDOUT ) // grayman #3317 - quit if KO'ed or dead { WrapUp(owner); owner->GetMind()->EndState(); return; } UpdateAlertLevel(); // Ensure we are in the correct alert level if (!CheckAlertLevel(owner)) { // terminate FleeDoneState when time is over WrapUp(owner); owner->GetMind()->EndState(); return; } // Let the AI check its senses owner->PerformVisualScan(); if (owner->AI_ALERTED) { // terminate FleeDoneState when the AI is alerted WrapUp(owner); owner->GetMind()->EndState(); return; // grayman #3474 } if ( !owner->emitFleeBarks ) // grayman #3474 { // not crying for help, time to end this state WrapUp(owner); owner->GetMind()->EndState(); return; } // Shortcut reference Memory& memory = owner->GetMemory(); if (!_searchForFriendDone) { idActor* friendlyAI = owner->FindFriendlyAI(-1); if ( friendlyAI != NULL) { // We found a friend, cry for help to him DM_LOG(LC_AI, LT_INFO)LOGSTRING("%s found friendly AI %s, crying for help\r", owner->name.c_str(),friendlyAI->name.c_str()); _searchForFriendDone = true; owner->movementSubsystem->ClearTasks(); owner->SetTurnRate(_oldTurnRate); owner->TurnToward(friendlyAI->GetPhysics()->GetOrigin()); //float distanceToFriend = (friendlyAI->GetPhysics()->GetOrigin() - owner->GetPhysics()->GetOrigin()).LengthFast(); // Cry for help // Create a new help message CommMessagePtr message(new CommMessage( CommMessage::RequestForHelp_CommType, owner, friendlyAI, NULL, memory.alertPos, memory.currentSearchEventID // grayman #3857 (was '0') )); CommunicationTaskPtr barkTask(new SingleBarkTask("snd_flee", message)); if (cv_ai_debug_transition_barks.GetBool()) { gameLocal.Printf("%d: %s flees, barks 'snd_flee'\n",gameLocal.time,owner->GetName()); } owner->commSubsystem->AddCommTask(barkTask); memory.lastTimeVisualStimBark = gameLocal.time; } else if (gameLocal.time >= _turnEndTime) { // We didn't find a friend, stop looking for them after some time _searchForFriendDone = true; owner->movementSubsystem->ClearTasks(); owner->SetTurnRate(_oldTurnRate); // Play the cowering animation owner->SetAnimState(ANIMCHANNEL_TORSO, "Torso_Cower", 4); owner->SetAnimState(ANIMCHANNEL_LEGS, "Legs_Cower", 4); } } }