/* =========================================================================== Return to Castle Wolfenstein single player GPL Source Code Copyright (C) 1999-2010 id Software LLC, a ZeniMax Media company. This file is part of the Return to Castle Wolfenstein single player GPL Source Code (“RTCW SP Source Code”). RTCW SP Source Code is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. RTCW SP Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with RTCW SP Source Code. If not, see . In addition, the RTCW SP Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the RTCW SP Source Code. If not, please request a copy in writing from id Software at the address below. If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA. =========================================================================== */ //=========================================================================== // // Name: ai_cast_events.c // Function: Wolfenstein AI Character Events // Programmer: Ridah // Tab Size: 4 (real tabs) //=========================================================================== #include "../game/g_local.h" #include "../game/q_shared.h" #include "../game/botlib.h" //bot lib interface #include "../game/be_aas.h" #include "../game/be_ea.h" #include "../game/be_ai_gen.h" #include "../game/be_ai_goal.h" #include "../game/be_ai_move.h" #include "../botai/botai.h" //bot ai interface #include "ai_cast.h" /* Contains response functions for various events that require specific handling for Cast AI's. */ /* ============ AICast_Sight ============ */ void AICast_Sight( gentity_t *ent, gentity_t *other, int lastSight ) { cast_state_t *cs, *ocs; cs = AICast_GetCastState( ent->s.number ); ocs = AICast_GetCastState( other->s.number ); // // call the sightfunc for this cast, so we can play associated sounds, or do any character-specific things // if ( cs->sightfunc ) { // factor in the reaction time if ( AICast_EntityVisible( cs, other->s.number, qfalse ) ) { cs->sightfunc( ent, other, lastSight ); } } if ( other->aiName && other->health <= 0 ) { // they died since we last saw them if ( ocs->deathTime > lastSight ) { if ( !AICast_SameTeam( cs, other->s.number ) ) { AICast_ScriptEvent( cs, "enemysightcorpse", other->aiName ); } else if ( !( cs->castScriptStatus.scriptFlags & SFL_FRIENDLYSIGHTCORPSE_TRIGGERED ) ) { cs->castScriptStatus.scriptFlags |= SFL_FRIENDLYSIGHTCORPSE_TRIGGERED; AICast_ScriptEvent( cs, "friendlysightcorpse", "" ); } } // if this is the first time, call the sight script event } else if ( !lastSight && other->aiName ) { if ( !AICast_SameTeam( cs, other->s.number ) ) { // disabled.. triggered when entering combat mode //AICast_ScriptEvent( cs, "enemysight", other->aiName ); } else { AICast_ScriptEvent( cs, "sight", other->aiName ); } } } /* ============ AICast_Pain ============ */ void AICast_Pain( gentity_t *targ, gentity_t *attacker, int damage, vec3_t point ) { cast_state_t *cs; cs = AICast_GetCastState( targ->s.number ); // print debugging message if ( aicast_debug.integer == 2 && attacker->s.number == 0 ) { G_Printf( "hit %s %i\n", targ->aiName, targ->health ); } // if we are below alert mode, then go there immediately if ( cs->aiState < AISTATE_ALERT ) { AICast_StateChange( cs, AISTATE_ALERT ); } if ( cs->aiFlags & AIFL_NOPAIN ) { return; } // process the event (turn to face the attacking direction? go into hide/retreat state?) // need to weigh up the situation, but foremost, an inactive AI cast should always react in some way to being hurt cs->lastPain = level.time; // record the sighting (FIXME: silent weapons shouldn't do this, but the AI should react in some way) if ( attacker->client ) { AICast_UpdateVisibility( targ, attacker, qtrue, qtrue ); } // if either of us are neutral, then we are now enemies if ( targ->aiTeam == AITEAM_NEUTRAL || attacker->aiTeam == AITEAM_NEUTRAL ) { cs->vislist[attacker->s.number].flags |= AIVIS_ENEMY; } AICast_ScriptEvent( cs, "painenemy", attacker->aiName ); AICast_ScriptEvent( cs, "pain", va( "%d %d", targ->health, targ->health + damage ) ); if ( cs->aiFlags & AIFL_DENYACTION ) { // dont play any sounds return; } // // call the painfunc for this cast, so we can play associated sounds, or do any character-specific things // if ( cs->painfunc ) { cs->painfunc( targ, attacker, damage, point ); } } /* ============ AICast_Die ============ */ void AICast_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath ) { int contents; int killer; cast_state_t *cs; qboolean nogib = qtrue; char mapname[MAX_QPATH]; // print debugging message if ( aicast_debug.integer == 2 && attacker->s.number == 0 ) { G_Printf( "killed %s\n", self->aiName ); } cs = AICast_GetCastState( self->s.number ); if ( attacker ) { killer = attacker->s.number; } else { killer = ENTITYNUM_WORLD; } // record the sighting (FIXME: silent weapons shouldn't do this, but the AI should react in some way) if ( attacker->client ) { AICast_UpdateVisibility( self, attacker, qtrue, qtrue ); } if ( self->aiCharacter == AICHAR_HEINRICH || self->aiCharacter == AICHAR_HELGA || self->aiCharacter == AICHAR_SUPERSOLDIER || self->aiCharacter == AICHAR_PROTOSOLDIER ) { if ( self->health <= GIB_HEALTH ) { self->health = -1; } } // the zombie should show special effect instead of gibbing if ( self->aiCharacter == AICHAR_ZOMBIE && cs->secondDeadTime ) { if ( cs->secondDeadTime > 1 ) { // we are already totally dead self->health += damage; // don't drop below gib_health if we weren't already below it return; } /* if (!cs->rebirthTime) { self->health = -999; damage = 999; } else if ( self->health >= GIB_HEALTH ) { // while waiting for rebirth, we only "die" if we drop below gib health return; } */ // always gib self->health = -999; damage = 999; } // Zombies are very fragile against highly explosives if ( self->aiCharacter == AICHAR_ZOMBIE && damage > 20 && inflictor != attacker ) { self->health = -999; damage = 999; } // process the event if ( self->client->ps.pm_type == PM_DEAD ) { // already dead if ( self->health < GIB_HEALTH ) { if ( self->aiCharacter == AICHAR_ZOMBIE ) { // RF, changed this so Zombies always gib now GibEntity( self, killer ); nogib = qfalse; /* // Zombie has special exploding cloud effect if (attacker != inflictor || attacker->s.weapon == WP_VENOM) { GibEntity( self, killer ); nogib = qfalse; } else { // Zombie will decompose upon dying self->client->ps.eFlags |= EF_MONSTER_EFFECT2; self->s.effect2Time = level.time+200; self->health = -1; } */ self->takedamage = qfalse; self->r.contents = 0; cs->secondDeadTime = 2; cs->rebirthTime = 0; cs->revivingTime = 0; } else { body_die( self, inflictor, attacker, damage, meansOfDeath ); return; } } } else { // this is our first death, so set everything up if ( level.intermissiontime ) { return; } self->client->ps.pm_type = PM_DEAD; self->enemy = attacker; // drop a weapon? // if client is in a nodrop area, don't drop anything contents = trap_PointContents( self->r.currentOrigin, -1 ); if ( !( contents & CONTENTS_NODROP ) ) { TossClientItems( self ); } // make sure the client doesn't forget about this entity until it's set to "dead" frame // otherwise it might replay it's death animation if it goes out and into client view self->r.svFlags |= SVF_BROADCAST; self->takedamage = qtrue; // can still be gibbed self->s.weapon = WP_NONE; if ( cs->bs ) { cs->weaponNum = WP_NONE; } self->client->ps.weapon = WP_NONE; self->s.powerups = 0; self->r.contents = CONTENTS_CORPSE; self->s.angles[0] = 0; self->s.angles[1] = self->client->ps.viewangles[1]; self->s.angles[2] = 0; VectorCopy( self->s.angles, self->client->ps.viewangles ); self->s.loopSound = 0; self->r.maxs[2] = -8; self->client->ps.maxs[2] = self->r.maxs[2]; // remove powerups memset( self->client->ps.powerups, 0, sizeof( self->client->ps.powerups ) ); //cs->rebirthTime = 0; // never gib in a nodrop if ( self->health <= GIB_HEALTH ) { if ( self->aiCharacter == AICHAR_ZOMBIE ) { // RF, changed this so Zombies always gib now GibEntity( self, killer ); nogib = qfalse; } else if ( !( contents & CONTENTS_NODROP ) ) { body_die( self, inflictor, attacker, damage, meansOfDeath ); //GibEntity( self, killer ); nogib = qfalse; } } // if we are a zombie, and lying down during our first death, then we should just die if ( !( self->aiCharacter == AICHAR_ZOMBIE && cs->secondDeadTime && cs->rebirthTime ) ) { // set enemy weapon BG_UpdateConditionValue( self->s.number, ANIM_COND_ENEMY_WEAPON, 0, qfalse ); if ( attacker->client ) { BG_UpdateConditionValue( self->s.number, ANIM_COND_ENEMY_WEAPON, inflictor->s.weapon, qtrue ); } else { BG_UpdateConditionValue( self->s.number, ANIM_COND_ENEMY_WEAPON, 0, qfalse ); } // set enemy location BG_UpdateConditionValue( self->s.number, ANIM_COND_ENEMY_POSITION, 0, qfalse ); if ( infront( self, inflictor ) ) { BG_UpdateConditionValue( self->s.number, ANIM_COND_ENEMY_POSITION, POSITION_INFRONT, qtrue ); } else { BG_UpdateConditionValue( self->s.number, ANIM_COND_ENEMY_POSITION, POSITION_BEHIND, qtrue ); } if ( self->takedamage ) { // only play the anim if we haven't gibbed // play the animation BG_AnimScriptEvent( &self->client->ps, ANIM_ET_DEATH, qfalse, qtrue ); } // set gib delay if ( cs->aiCharacter == AICHAR_HEINRICH || cs->aiCharacter == AICHAR_HELGA ) { cs->lastLoadTime = level.time + self->client->ps.torsoTimer - 200; } // set this flag so no other anims override us self->client->ps.eFlags |= EF_DEAD; self->s.eFlags |= EF_DEAD; // make sure we dont move around while on the ground //self->flags |= FL_NO_HEADCHECK; } // if end map, sink into ground cs->deadSinkStartTime = 0; if ( cs->aiCharacter == AICHAR_WARZOMBIE ) { trap_Cvar_VariableStringBuffer( "mapname", mapname, sizeof( mapname ) ); if ( !Q_strncmp( mapname, "end", 3 ) ) { // !! FIXME: post beta2, make this a spawnflag! cs->deadSinkStartTime = level.time + 4000; } } } if ( nogib ) { // set for rebirth if ( self->aiCharacter == AICHAR_ZOMBIE ) { if ( !cs->secondDeadTime ) { cs->rebirthTime = level.time + 5000 + rand() % 2000; // RF, only set for gib at next death, if NoRevive is not set if ( !( self->spawnflags & 2 ) ) { cs->secondDeadTime = qtrue; } cs->revivingTime = 0; } else if ( cs->secondDeadTime > 1 ) { cs->rebirthTime = 0; cs->revivingTime = 0; cs->deathTime = level.time; } } else { // the body can still be gibbed self->die = body_die; } } trap_LinkEntity( self ); // kill, instanly, any streaming sound the character had going G_AddEvent( &g_entities[self->s.number], EV_STOPSTREAMINGSOUND, 0 ); // mark the time of death cs->deathTime = level.time; // dying ai's can trigger a target if ( !cs->rebirthTime ) { G_UseTargets( self, self ); // really dead now, so call the script AICast_ScriptEvent( cs, "death", attacker->aiName ? attacker->aiName : "" ); // call the deathfunc for this cast, so we can play associated sounds, or do any character-specific things if ( !( cs->aiFlags & AIFL_DENYACTION ) && cs->deathfunc ) { cs->deathfunc( self, attacker, damage, meansOfDeath ); //----(SA) added mod } } else { // really dead now, so call the script AICast_ScriptEvent( cs, "fakedeath", "" ); // call the deathfunc for this cast, so we can play associated sounds, or do any character-specific things if ( !( cs->aiFlags & AIFL_DENYACTION ) && cs->deathfunc ) { cs->deathfunc( self, attacker, damage, meansOfDeath ); //----(SA) added mod } } } /* =============== AICast_EndChase =============== */ void AICast_EndChase( cast_state_t *cs ) { // anything? } /* =============== AICast_AIDoor_Touch =============== */ void AICast_AIDoor_Touch( gentity_t *ent, gentity_t *aidoor_trigger, gentity_t *door ) { cast_state_t *cs, *ocs; gentity_t *trav; int i; trace_t tr; vec3_t mins, pos, dir; cs = AICast_GetCastState( ent->s.number ); if ( !cs->bs ) { return; } // does the aidoor have ai_marker's? if ( !aidoor_trigger->targetname ) { G_Printf( "trigger_aidoor has no ai_marker's at %s\n", vtos( ent->r.currentOrigin ) ); return; } // are we heading for an ai_marker? if ( cs->aifunc == AIFunc_DoorMarker ) { return; } // if they are moving away from the door, ignore them if ( VectorLength( cs->bs->velocity ) > 1 ) { VectorAdd( door->r.absmin, door->r.absmax, pos ); VectorScale( pos, 0.5, pos ); VectorSubtract( pos, cs->bs->origin, dir ); if ( DotProduct( cs->bs->velocity, dir ) < 0 ) { return; } } // TTimo: gcc: suggest () around assignment used as truth value for ( trav = NULL; ( trav = G_Find( trav, FOFS( target ), aidoor_trigger->targetname ) ); ) { // make sure the marker is vacant trap_Trace( &tr, trav->r.currentOrigin, ent->r.mins, ent->r.maxs, trav->r.currentOrigin, ent->s.number, ent->clipmask ); if ( tr.startsolid ) { continue; } // search all other AI's, to see if they are heading for this marker for ( i = 0, ocs = AICast_GetCastState( 0 ); i < aicast_maxclients; i++, ocs++ ) { if ( !ocs->bs ) { continue; } if ( ocs->aifunc != AIFunc_DoorMarker ) { continue; } if ( ocs->doorMarker != trav->s.number ) { continue; } // found a match break; } if ( i < aicast_maxclients ) { continue; } // make sure there is a clear path VectorCopy( ent->r.mins, mins ); mins[2] += 16; // step height trap_Trace( &tr, ent->r.currentOrigin, mins, ent->r.maxs, trav->r.currentOrigin, ent->s.number, ent->clipmask ); if ( tr.fraction < 1.0 ) { continue; } // the marker is vacant and available cs->doorMarkerTime = level.time; cs->doorMarkerNum = trav->s.number; cs->doorMarkerDoor = door->s.number; break; } } /* ============ AICast_ProcessActivate ============ */ void AICast_ProcessActivate( int entNum, int activatorNum ) { cast_state_t *cs; gentity_t *newent, *ent, *activator; gclient_t *client; cs = AICast_GetCastState( entNum ); client = &level.clients[entNum]; ent = &g_entities[entNum]; activator = &g_entities[activatorNum]; if ( cs->lastActivate > level.time - 1000 ) { return; } cs->lastActivate = level.time; if ( !AICast_SameTeam( cs, activatorNum ) ) { if ( ent->aiTeam == AITEAM_NEUTRAL ) { AICast_ScriptEvent( cs, "activate", g_entities[activatorNum].aiName ); } return; } // try running the activate event, if it denies us the request, then abort cs->aiFlags &= ~AIFL_DENYACTION; AICast_ScriptEvent( cs, "activate", g_entities[activatorNum].aiName ); if ( cs->aiFlags & AIFL_DENYACTION ) { return; } // if we are doing something else if ( cs->castScriptStatus.castScriptEventIndex >= 0 ) { if ( ent->eventTime != level.time ) { G_AddEvent( &g_entities[entNum], EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[cs->aiCharacter].soundScripts[ORDERSDENYSOUNDSCRIPT] ) ); } return; } // if we are already following them, stop following if ( cs->leaderNum == activatorNum ) { if ( ent->eventTime != level.time ) { G_AddEvent( &g_entities[entNum], EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[cs->aiCharacter].soundScripts[STAYSOUNDSCRIPT] ) ); } cs->leaderNum = -1; // create a goal at this position newent = G_Spawn(); newent->classname = "AI_wait_goal"; newent->r.ownerNum = entNum; G_SetOrigin( newent, cs->bs->origin ); AIFunc_ChaseGoalStart( cs, newent->s.number, 128, qtrue ); //AIFunc_IdleStart( cs ); } else { // start following int count, i; cast_state_t *tcs; // if they already have enough followers, deny for ( count = 0, i = 0, tcs = caststates; i < level.maxclients; i++, tcs++ ) { if ( tcs->bs && tcs != cs && tcs->entityNum != activatorNum && g_entities[tcs->entityNum].health > 0 && tcs->leaderNum == activatorNum ) { count++; } } if ( count >= 3 ) { if ( ent->eventTime != level.time ) { G_AddEvent( &g_entities[entNum], EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[cs->aiCharacter].soundScripts[ORDERSDENYSOUNDSCRIPT] ) ); } return; } if ( ent->eventTime != level.time ) { G_AddEvent( &g_entities[entNum], EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[cs->aiCharacter].soundScripts[FOLLOWSOUNDSCRIPT] ) ); } // if they have a wait goal, free it if ( cs->followEntity >= MAX_CLIENTS && g_entities[cs->followEntity].classname && !strcmp( g_entities[cs->followEntity].classname, "AI_wait_goal" ) ) { G_FreeEntity( &g_entities[cs->followEntity] ); } cs->followEntity = -1; cs->leaderNum = activatorNum; } } /* ================ AICast_RecordScriptSound ================ */ void AICast_RecordScriptSound( int client ) { cast_state_t *cs; cs = AICast_GetCastState( client ); cs->lastScriptSound = level.time; }