/* =========================================================================== 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_funcs.c // Function: Wolfenstein AI Character Decision Making // 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" // TTimo: unused //static vec3_t forward, right, up; //================================================================================= // // Helga, the first boss // //================================================================================= #define HELGA_SPIRIT_BUILDUP_TIME 8000 // last for this long #define HELGA_SPIRIT_FADEOUT_TIME 1000 #define HELGA_SPIRIT_DLIGHT_RADIUS_MAX 384 #define HELGA_SPIRIT_FIRE_INTERVAL 1000 extern int lastZombieSpiritAttack; char *AIFunc_Helga_SpiritAttack( cast_state_t *cs ) { bot_state_t *bs; gentity_t *ent; // cs->aiFlags |= AIFL_SPECIAL_FUNC; ent = &g_entities[cs->entityNum]; bs = cs->bs; // make sure we're still playing the right anim if ( ( ent->client->ps.torsoAnim & ~ANIM_TOGGLEBIT ) - BG_AnimationIndexForString( "attack1", cs->entityNum ) ) { return AIFunc_DefaultStart( cs ); } // if ( cs->enemyNum < 0 ) { ent->client->ps.torsoTimer = 0; ent->client->ps.legsTimer = 0; return AIFunc_DefaultStart( cs ); } // // if we can't see them anymore, abort immediately if ( cs->vislist[cs->enemyNum].real_visible_timestamp != cs->vislist[cs->enemyNum].real_update_timestamp ) { ent->client->ps.torsoTimer = 0; ent->client->ps.legsTimer = 0; return AIFunc_DefaultStart( cs ); } // we are firing this weapon, so record it cs->weaponFireTimes[WP_MONSTER_ATTACK2] = level.time; // // once an attack has started, only abort once the player leaves our view, or time runs out if ( cs->thinkFuncChangeTime < level.time - HELGA_SPIRIT_BUILDUP_TIME ) { // if enough time has elapsed, finish this attack if ( level.time > cs->thinkFuncChangeTime + HELGA_SPIRIT_BUILDUP_TIME + HELGA_SPIRIT_FADEOUT_TIME ) { ent->client->ps.torsoTimer = 0; ent->client->ps.legsTimer = 0; return AIFunc_DefaultStart( cs ); } } else { // set timers ent->client->ps.torsoTimer = 1000; ent->client->ps.legsTimer = 1000; // draw the client-side effect ent->client->ps.eFlags |= EF_MONSTER_EFFECT; // inform the client of our enemies position VectorCopy( g_entities[cs->enemyNum].client->ps.origin, ent->s.origin2 ); ent->s.origin2[2] += g_entities[cs->enemyNum].client->ps.viewheight; } // // return NULL; } char *AIFunc_Helga_SpiritAttack_Start( cast_state_t *cs ) { gentity_t *ent; // ent = &g_entities[cs->entityNum]; ent->s.otherEntityNum2 = cs->enemyNum; ent->s.effect1Time = level.time; cs->aiFlags |= AIFL_SPECIAL_FUNC; // // dont turn cs->ideal_viewangles[YAW] = cs->viewangles[YAW]; // play an anim BG_UpdateConditionValue( cs->entityNum, ANIM_COND_WEAPON, WP_MONSTER_ATTACK2, qtrue ); BG_AnimScriptEvent( &ent->client->ps, ANIM_ET_FIREWEAPON, qfalse, qtrue ); // cs->aifunc = AIFunc_Helga_SpiritAttack; return "AIFunc_Helga_SpiritAttack"; } //================================================================================= // // Standing melee attacks // //================================================================================= #define NUM_HELGA_ANIMS 3 #define MAX_HELGA_IMPACTS 3 int helgaHitTimes[NUM_HELGA_ANIMS][MAX_HELGA_IMPACTS] = { // up to three hits per attack {ANIMLENGTH( 16,20 ),-1}, {ANIMLENGTH( 11,20 ),ANIMLENGTH( 19,20 ),-1}, {ANIMLENGTH( 10,20 ),ANIMLENGTH( 17,20 ),ANIMLENGTH( 26,20 )}, }; int helgaHitDamage[NUM_HELGA_ANIMS] = { 20, 14, 12 }; /* ================ AIFunc_Helga_Melee ================ */ char *AIFunc_Helga_Melee( cast_state_t *cs ) { gentity_t *ent = &g_entities[cs->entityNum]; gentity_t *enemy; cast_state_t *ecs; int hitDelay = -1, anim; trace_t tr; float enemyDist; aicast_predictmove_t move; vec3_t vec; cs->aiFlags |= AIFL_SPECIAL_FUNC; if ( !ent->client->ps.torsoTimer || !ent->client->ps.legsTimer ) { cs->aiFlags &= ~AIFL_SPECIAL_FUNC; return AIFunc_DefaultStart( cs ); } if ( cs->enemyNum < 0 ) { ent->client->ps.legsTimer = 0; // allow legs us to move ent->client->ps.torsoTimer = 0; // allow legs us to move cs->aiFlags &= ~AIFL_SPECIAL_FUNC; return AIFunc_DefaultStart( cs ); } ecs = AICast_GetCastState( cs->enemyNum ); enemy = &g_entities[cs->enemyNum]; anim = ( ent->client->ps.torsoAnim & ~ANIM_TOGGLEBIT ) - BG_AnimationIndexForString( "attack3", cs->entityNum ); if ( anim < 0 || anim >= NUM_HELGA_ANIMS ) { // animation interupted cs->aiFlags &= ~AIFL_SPECIAL_FUNC; return AIFunc_DefaultStart( cs ); //G_Error( "AIFunc_HelgaZombieMelee: helgaBoss using invalid or unknown attack anim" ); } if ( cs->animHitCount < MAX_HELGA_IMPACTS && helgaHitTimes[anim][cs->animHitCount] >= 0 ) { // face them VectorCopy( cs->bs->origin, vec ); vec[2] += ent->client->ps.viewheight; VectorSubtract( enemy->client->ps.origin, vec, vec ); VectorNormalize( vec ); vectoangles( vec, cs->ideal_viewangles ); cs->ideal_viewangles[PITCH] = AngleNormalize180( cs->ideal_viewangles[PITCH] ); // get hitDelay if ( !cs->animHitCount ) { hitDelay = helgaHitTimes[anim][cs->animHitCount]; } else { hitDelay = helgaHitTimes[anim][cs->animHitCount] - helgaHitTimes[anim][cs->animHitCount - 1]; } // check for inflicting damage if ( level.time - cs->weaponFireTimes[cs->weaponNum] > hitDelay ) { // do melee damage enemyDist = VectorDistance( enemy->r.currentOrigin, ent->r.currentOrigin ); enemyDist -= g_entities[cs->enemyNum].r.maxs[0]; enemyDist -= ent->r.maxs[0]; if ( enemyDist < 10 + AICast_WeaponRange( cs, cs->weaponNum ) ) { trap_Trace( &tr, ent->r.currentOrigin, NULL, NULL, enemy->r.currentOrigin, ent->s.number, MASK_SHOT ); if ( tr.entityNum == cs->enemyNum ) { G_Damage( &g_entities[tr.entityNum], ent, ent, vec3_origin, tr.endpos, helgaHitDamage[anim], 0, MOD_GAUNTLET ); G_AddEvent( enemy, EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[ent->aiCharacter].soundScripts[STAYSOUNDSCRIPT] ) ); } } cs->weaponFireTimes[cs->weaponNum] = level.time; cs->animHitCount++; } } // if they are outside range, move forward AICast_PredictMovement( ecs, 2, 0.3, &move, &g_entities[cs->enemyNum].client->pers.cmd, -1 ); VectorSubtract( move.endpos, cs->bs->origin, vec ); vec[2] = 0; enemyDist = VectorLength( vec ); enemyDist -= g_entities[cs->enemyNum].r.maxs[0]; enemyDist -= ent->r.maxs[0]; if ( enemyDist > 8 ) { // we can get closer //if (!ent->client->ps.legsTimer) { // cs->castScriptStatus.scriptNoMoveTime = 0; trap_EA_MoveForward( cs->entityNum ); //} //ent->client->ps.legsTimer = 0; // allow legs us to move } return NULL; } /* ================ AIFunc_Helga_MeleeStart ================ */ char *AIFunc_Helga_MeleeStart( cast_state_t *cs ) { gentity_t *ent; ent = &g_entities[cs->entityNum]; ent->s.effect1Time = level.time; cs->ideal_viewangles[YAW] = cs->viewangles[YAW]; cs->weaponFireTimes[cs->weaponNum] = level.time; cs->animHitCount = 0; cs->aiFlags |= AIFL_SPECIAL_FUNC; // face them AICast_AimAtEnemy( cs ); // play an anim BG_UpdateConditionValue( cs->entityNum, ANIM_COND_WEAPON, cs->weaponNum, qtrue ); BG_AnimScriptEvent( &ent->client->ps, ANIM_ET_FIREWEAPON, qfalse, qtrue ); // play a sound G_AddEvent( ent, EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[ent->aiCharacter].soundScripts[ATTACKSOUNDSCRIPT] ) ); cs->aifunc = AIFunc_Helga_Melee; cs->aifunc( cs ); // think once now, to prevent a delay return "AIFunc_Helga_Melee"; } //=================================================================== /* ============== AIFunc_FlameZombie_Portal ============== */ char *AIFunc_FlameZombie_Portal( cast_state_t *cs ) { gentity_t *ent = &g_entities[cs->entityNum]; // if ( cs->thinkFuncChangeTime < level.time - PORTAL_ZOMBIE_SPAWNTIME ) { // HACK, make them aware of the player AICast_UpdateVisibility( &g_entities[cs->entityNum], AICast_FindEntityForName( "player" ), qfalse, qtrue ); ent->s.time2 = 0; // turn spawning effect off return AIFunc_DefaultStart( cs ); } // return NULL; } /* ============== AIFunc_FlameZombie_PortalStart ============== */ char *AIFunc_FlameZombie_PortalStart( cast_state_t *cs ) { gentity_t *ent = &g_entities[cs->entityNum]; // ent->s.time2 = level.time + 200; // hijacking this for portal spawning effect // // play a special animation ent->client->ps.torsoAnim = ( ( ent->client->ps.torsoAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) | BOTH_EXTRA1; ent->client->ps.legsAnim = ( ( ent->client->ps.legsAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) | BOTH_EXTRA1; ent->client->ps.torsoTimer = PORTAL_ZOMBIE_SPAWNTIME - 200; ent->client->ps.legsTimer = PORTAL_ZOMBIE_SPAWNTIME - 200; // cs->thinkFuncChangeTime = level.time; // cs->aifunc = AIFunc_FlameZombie_Portal; return "AIFunc_FlameZombie_Portal"; } //================================================================================= // // Heinrich, the LAST boss // //================================================================================= // // Special Sound Precache // typedef enum { HEINRICH_SWORDIMPACT, HEINRICH_SWORDLUNGE_START, HEINRICH_SWORDKNOCKBACK_START, HEINRICH_SWORDKNOCKBACK_WEAPON, HEINRICH_SWORDSIDESLASH_START, HEINRICH_SWORDSIDESLASH_WEAPON, HEINRICH_EARTHQUAKE_START, HEINRICH_RAISEDEAD_START, HEINRICH_TAUNT_GOODHEALTH, HEINRICH_TAUNT_LOWHEALTH, MAX_HEINRICH_SOUNDS } heinrichSounds_t; char *heinrichSounds[MAX_HEINRICH_SOUNDS] = { "heinrichSwordImpact", "heinrichSwordLungeStart", "heinrichSwordKnockbackStart", "heinrichSwordKnockbackWeapon", "heinrichSwordSideSlashStart", "heinrichSwordSideSlashWeapon", "heinrichSwordEarthquakeStart", "heinrichRaiseWarriorStart", "heinrichTauntGoodHealth", "heinrichTauntLowHealth", }; int heinrichSoundIndex[MAX_HEINRICH_SOUNDS]; void AICast_Heinrich_SoundPrecache( void ) { int i; for ( i = 0; i < MAX_HEINRICH_SOUNDS; i++ ) { heinrichSoundIndex[i] = G_SoundIndex( heinrichSounds[i] ); } } void AICast_Heinrich_Taunt( cast_state_t *cs ) { gentity_t *ent = &g_entities[cs->entityNum]; static int lastTaunt; // sound if ( ent->health > cs->attributes[STARTING_HEALTH] * 0.25 ) { if ( lastTaunt > level.time || lastTaunt < level.time - 20000 ) { G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_TAUNT_GOODHEALTH] ); lastTaunt = level.time; } } else { if ( lastTaunt > level.time || lastTaunt < level.time - 40000 ) { G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_TAUNT_LOWHEALTH] ); lastTaunt = level.time; } } } #define HEINRICH_LUNGE_DELAY ANIMLENGTH( 15,20 ) #define HEINRICH_LUNGE_RANGE 170 #define HEINRICH_LUNGE_DAMAGE ( 50 + rand() % 20 ) char *AIFunc_Heinrich_SwordLunge( cast_state_t *cs ) { gentity_t *ent = &g_entities[cs->entityNum]; trace_t *tr; vec3_t left; float enemyDist; aicast_predictmove_t move; vec3_t vec; cast_state_t *ecs; cs->aiFlags |= AIFL_SPECIAL_FUNC; if ( cs->enemyNum < 0 ) { if ( ent->client->ps.torsoTimer ) { return NULL; } return AIFunc_DefaultStart( cs ); } ecs = AICast_GetCastState( cs->enemyNum ); if ( ent->client->ps.torsoTimer < 500 ) { if ( !ent->client->ps.legsTimer ) { trap_EA_MoveForward( cs->entityNum ); } ent->client->ps.legsTimer = 0; ent->client->ps.torsoTimer = 0; cs->castScriptStatus.scriptNoMoveTime = 0; AICast_Heinrich_Taunt( cs ); return AIFunc_BattleChaseStart( cs ); } // time for the melee? if ( cs->enemyNum >= 0 && !( cs->aiFlags & AIFL_MISCFLAG1 ) ) { // face them AICast_AimAtEnemy( cs ); // keep checking for impact status tr = CheckMeleeAttack( ent, HEINRICH_LUNGE_RANGE, qfalse ); /* // do we need to move? if (!(tr && (tr->entityNum == cs->enemyNum))) { ent->client->ps.legsTimer = 0; cs->castScriptStatus.scriptNoMoveTime = 0; trap_EA_MoveForward( cs->entityNum ); } */ // ready for damage? if ( cs->thinkFuncChangeTime < level.time - HEINRICH_LUNGE_DELAY ) { cs->aiFlags |= AIFL_MISCFLAG1; // do melee damage if ( tr && ( tr->entityNum == cs->enemyNum ) ) { G_Damage( &g_entities[tr->entityNum], ent, ent, left, tr->endpos, HEINRICH_LUNGE_DAMAGE, 0, MOD_GAUNTLET ); // sound G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_SWORDIMPACT] ); } } } // if they are outside range, move forward AICast_PredictMovement( ecs, 2, 0.3, &move, &g_entities[cs->enemyNum].client->pers.cmd, -1 ); VectorSubtract( move.endpos, cs->bs->origin, vec ); vec[2] = 0; enemyDist = VectorLength( vec ); enemyDist -= g_entities[cs->enemyNum].r.maxs[0]; enemyDist -= ent->r.maxs[0]; if ( enemyDist > 30 ) { // we can get closer if ( ent->client->ps.legsTimer ) { cs->castScriptStatus.scriptNoMoveTime = level.time + 100; ent->client->ps.legsTimer = 0; // allow legs to move us } if ( cs->castScriptStatus.scriptNoMoveTime < level.time ) { trap_EA_MoveForward( cs->entityNum ); } } return NULL; } char *AIFunc_Heinrich_SwordLungeStart( cast_state_t *cs ) { gentity_t *ent = &g_entities[cs->entityNum]; // gentity_t *enemy = &g_entities[cs->enemyNum]; cs->aiFlags |= AIFL_SPECIAL_FUNC; // sound G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_SWORDLUNGE_START] ); // face them AICast_AimAtEnemy( cs ); // clear flags cs->aiFlags &= ~( AIFL_MISCFLAG1 | AIFL_MISCFLAG2 ); // play the anim BG_PlayAnimName( &ent->client->ps, "attack9", ANIM_BP_BOTH, qtrue, qfalse, qtrue ); // start the func cs->aifunc = AIFunc_Heinrich_SwordLunge; return "AIFunc_Heinrich_SwordLunge"; } #define HEINRICH_KNOCKBACK_DELAY ANIMLENGTH( 26,20 ) #define HEINRICH_KNOCKBACK_RANGE 150 #define HEINRICH_KNOCKBACK_DAMAGE ( 60 + rand() % 20 ) char *AIFunc_Heinrich_SwordKnockback( cast_state_t *cs ) { gentity_t *ent = &g_entities[cs->entityNum]; trace_t *tr; vec3_t right, left; // float enemyDist; // aicast_predictmove_t move; // vec3_t vec; cast_state_t *ecs; cs->aiFlags |= AIFL_SPECIAL_FUNC; if ( cs->enemyNum < 0 ) { if ( ent->client->ps.torsoTimer ) { return NULL; } return AIFunc_DefaultStart( cs ); } ecs = AICast_GetCastState( cs->enemyNum ); if ( ent->client->ps.torsoTimer < 500 ) { if ( !ent->client->ps.legsTimer ) { trap_EA_MoveForward( cs->entityNum ); } ent->client->ps.legsTimer = 0; ent->client->ps.torsoTimer = 0; cs->castScriptStatus.scriptNoMoveTime = 0; AICast_Heinrich_Taunt( cs ); return AIFunc_BattleChaseStart( cs ); } // time for the melee? if ( cs->enemyNum >= 0 && !( cs->aiFlags & AIFL_MISCFLAG1 ) ) { // face them AICast_AimAtEnemy( cs ); // keep checking for impact status tr = CheckMeleeAttack( ent, HEINRICH_KNOCKBACK_RANGE, qfalse ); /* // do we need to move? if (!(tr && (tr->entityNum == cs->enemyNum))) { ent->client->ps.legsTimer = 0; cs->castScriptStatus.scriptNoMoveTime = 0; trap_EA_MoveForward( cs->entityNum ); } */ // ready for damage? if ( cs->thinkFuncChangeTime < level.time - HEINRICH_KNOCKBACK_DELAY ) { cs->aiFlags |= AIFL_MISCFLAG1; // do melee damage if ( tr && ( tr->entityNum == cs->enemyNum ) ) { AngleVectors( cs->viewangles, NULL, right, NULL ); VectorNegate( right, left ); G_Damage( &g_entities[tr->entityNum], ent, ent, left, tr->endpos, HEINRICH_KNOCKBACK_DAMAGE, 0, MOD_GAUNTLET ); // sound G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_SWORDIMPACT] ); // throw them in direction of impact if ( ( ent->client->ps.torsoAnim & ~ANIM_TOGGLEBIT ) == BG_AnimationIndexForString( "attack2", cs->entityNum ) ) { // right right[2] = 0.5; VectorMA( g_entities[cs->enemyNum].client->ps.velocity, 400, right, g_entities[cs->enemyNum].client->ps.velocity ); } else { // left left[2] = 0.5; VectorMA( g_entities[cs->enemyNum].client->ps.velocity, 400, left, g_entities[cs->enemyNum].client->ps.velocity ); } } } } /* DISABLED FOR SWORDKNOCKBACK..looks bad // if they are outside range, move forward AICast_PredictMovement( ecs, 2, 0.3, &move, &g_entities[cs->enemyNum].client->pers.cmd, -1 ); VectorSubtract( move.endpos, cs->bs->origin, vec ); vec[2] = 0; enemyDist = VectorLength( vec ); enemyDist -= g_entities[cs->enemyNum].r.maxs[0]; enemyDist -= ent->r.maxs[0]; if (enemyDist > 30) { // we can get closer if (ent->client->ps.legsTimer) { cs->castScriptStatus.scriptNoMoveTime = level.time + 100; ent->client->ps.legsTimer = 0; // allow legs to move us } if (cs->castScriptStatus.scriptNoMoveTime < level.time) { trap_EA_MoveForward(cs->entityNum); } } */ return NULL; } char *AIFunc_Heinrich_SwordKnockbackStart( cast_state_t *cs ) { gentity_t *ent = &g_entities[cs->entityNum]; // gentity_t *enemy = &g_entities[cs->enemyNum]; cs->aiFlags |= AIFL_SPECIAL_FUNC; // sound G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_SWORDKNOCKBACK_START] ); // weapon sound G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_SWORDKNOCKBACK_WEAPON] ); // face them AICast_AimAtEnemy( cs ); // clear flags cs->aiFlags &= ~( AIFL_MISCFLAG1 | AIFL_MISCFLAG2 ); // play the anim if ( rand() % 2 ) { BG_PlayAnimName( &ent->client->ps, "attack2", ANIM_BP_BOTH, qtrue, qfalse, qtrue ); } else { BG_PlayAnimName( &ent->client->ps, "attack3", ANIM_BP_BOTH, qtrue, qfalse, qtrue ); } // start the func cs->aifunc = AIFunc_Heinrich_SwordKnockback; return "AIFunc_Heinrich_SwordKnockback"; } #define HEINRICH_SLASH_DELAY ANIMLENGTH( 17,25 ) #define HEINRICH_SLASH_RANGE 140 #define HEINRICH_SLASH_DAMAGE ( 30 + rand() % 15 ) char *AIFunc_Heinrich_SwordSideSlash( cast_state_t *cs ) { gentity_t *ent = &g_entities[cs->entityNum]; trace_t *tr; vec3_t right, left; float enemyDist; aicast_predictmove_t move; vec3_t vec; cast_state_t *ecs; cs->aiFlags |= AIFL_SPECIAL_FUNC; if ( cs->enemyNum < 0 ) { if ( ent->client->ps.torsoTimer ) { return NULL; } return AIFunc_DefaultStart( cs ); } ecs = AICast_GetCastState( cs->enemyNum ); if ( ent->client->ps.torsoTimer < 500 ) { if ( !ent->client->ps.legsTimer ) { trap_EA_MoveForward( cs->entityNum ); } ent->client->ps.legsTimer = 0; ent->client->ps.torsoTimer = 0; cs->castScriptStatus.scriptNoMoveTime = 0; AICast_Heinrich_Taunt( cs ); return AIFunc_BattleChaseStart( cs ); } // time for the melee? if ( cs->enemyNum >= 0 && !( cs->aiFlags & AIFL_MISCFLAG1 ) ) { // face them AICast_AimAtEnemy( cs ); // keep checking for impact status tr = CheckMeleeAttack( ent, HEINRICH_SLASH_RANGE, qfalse ); // ready for damage? if ( cs->thinkFuncChangeTime < level.time - HEINRICH_SLASH_DELAY ) { cs->aiFlags |= AIFL_MISCFLAG1; // do melee damage if ( tr && ( tr->entityNum == cs->enemyNum ) ) { AngleVectors( cs->viewangles, NULL, right, NULL ); VectorNegate( right, left ); G_Damage( &g_entities[tr->entityNum], ent, ent, left, tr->endpos, HEINRICH_SLASH_DAMAGE, 0, MOD_GAUNTLET ); // sound G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_SWORDIMPACT] ); // throw them in direction of impact left[2] = 0.5; VectorMA( g_entities[cs->enemyNum].client->ps.velocity, 400, left, g_entities[cs->enemyNum].client->ps.velocity ); } } } // if they are outside range, move forward AICast_PredictMovement( ecs, 2, 0.3, &move, &g_entities[cs->enemyNum].client->pers.cmd, -1 ); VectorSubtract( move.endpos, cs->bs->origin, vec ); vec[2] = 0; enemyDist = VectorLength( vec ); enemyDist -= g_entities[cs->enemyNum].r.maxs[0]; enemyDist -= ent->r.maxs[0]; if ( enemyDist > 30 ) { // we can get closer if ( ent->client->ps.legsTimer ) { cs->castScriptStatus.scriptNoMoveTime = level.time + 100; ent->client->ps.legsTimer = 0; // allow legs to move us } if ( cs->castScriptStatus.scriptNoMoveTime < level.time ) { trap_EA_MoveForward( cs->entityNum ); } } return NULL; } char *AIFunc_Heinrich_SwordSideSlashStart( cast_state_t *cs ) { gentity_t *ent = &g_entities[cs->entityNum]; cs->aiFlags |= AIFL_SPECIAL_FUNC; // sound G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_SWORDSIDESLASH_START] ); // weapon sound G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_SWORDSIDESLASH_WEAPON] ); // face them AICast_AimAtEnemy( cs ); // clear flags cs->aiFlags &= ~( AIFL_MISCFLAG1 | AIFL_MISCFLAG2 ); // play the anim BG_PlayAnimName( &ent->client->ps, "attack8", ANIM_BP_BOTH, qtrue, qfalse, qtrue ); // start the func cs->aifunc = AIFunc_Heinrich_SwordSideSlash; return "AIFunc_Heinrich_SwordSideSlash"; } #define HEINRICH_STOMP_DELAY 900 #define HEINRICH_STOMP_RANGE 1024.0 #define HEINRICH_STOMP_VELOCITY_Z 420 #define HEINRICH_STOMP_DAMAGE 35 char *AIFunc_Heinrich_Earthquake( cast_state_t *cs ) { gentity_t *ent = &g_entities[cs->entityNum]; gentity_t *enemy; cast_state_t *ecs; vec3_t enemyVec; float enemyDist, scale; trace_t *tr; cs->aiFlags |= AIFL_SPECIAL_FUNC; if ( cs->enemyNum < 0 ) { if ( !ent->client->ps.torsoTimer ) { return AIFunc_DefaultStart( cs ); } return NULL; } enemy = &g_entities[cs->enemyNum]; ecs = AICast_GetCastState( cs->enemyNum ); VectorMA( enemy->r.currentOrigin, HEINRICH_STOMP_DELAY, enemy->client->ps.velocity, enemyVec ); enemyDist = VectorDistance( ent->r.currentOrigin, enemyVec ); if ( ent->client->ps.torsoTimer < 500 ) { int rnd; aicast_predictmove_t move; vec3_t vec; AICast_PredictMovement( ecs, 2, 0.5, &move, &g_entities[cs->enemyNum].client->pers.cmd, -1 ); VectorSubtract( move.endpos, cs->bs->origin, vec ); vec[2] = 0; enemyDist = VectorLength( vec ); enemyDist -= g_entities[cs->enemyNum].r.maxs[0]; enemyDist -= ent->r.maxs[0]; // if ( enemyDist < 140 ) { // combo attack rnd = rand() % 3; switch ( rnd ) { case 0: return AIFunc_Heinrich_SwordSideSlashStart( cs ); case 1: return AIFunc_Heinrich_SwordKnockbackStart( cs ); case 2: return AIFunc_Heinrich_SwordLungeStart( cs ); } } else { // back to roaming ent->client->ps.legsTimer = 0; ent->client->ps.torsoTimer = 0; cs->castScriptStatus.scriptNoMoveTime = 0; AICast_Heinrich_Taunt( cs ); return AIFunc_DefaultStart( cs ); } } // time for the thump? if ( !( cs->aiFlags & AIFL_MISCFLAG1 ) ) { // face them AICast_AimAtEnemy( cs ); // ready for damage? if ( cs->thinkFuncChangeTime < level.time - HEINRICH_STOMP_DELAY ) { cs->aiFlags |= AIFL_MISCFLAG1; // play the stomp sound G_AddEvent( ent, EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[ent->aiCharacter].soundScripts[ORDERSDENYSOUNDSCRIPT] ) ); // check for striking the player tr = CheckMeleeAttack( ent, 70, qfalse ); // do melee damage if ( tr && ( tr->entityNum == cs->enemyNum ) ) { G_Damage( &g_entities[tr->entityNum], ent, ent, vec3_origin, tr->endpos, HEINRICH_STOMP_DAMAGE, 0, MOD_GAUNTLET ); } // call the debris trigger AICast_ScriptEvent( cs, "trigger", "quake" ); } } enemyDist = Distance( enemy->s.pos.trBase, ent->s.pos.trBase ); // do the earthquake effects if ( cs->thinkFuncChangeTime < level.time - HEINRICH_STOMP_DELAY ) { // throw the player into the air, if they are on the ground if ( ( enemy->s.groundEntityNum != ENTITYNUM_NONE ) && enemyDist < HEINRICH_STOMP_RANGE ) { scale = 0.5 + 0.5 * ( (float)ent->client->ps.torsoTimer / 1000.0 ); if ( scale > 1.0 ) { scale = 1.0; } VectorSubtract( ent->s.pos.trBase, enemy->s.pos.trBase, enemyVec ); VectorScale( enemyVec, 2.0 * ( 0.6 + 0.5 * random() ) * scale * ( 0.6 + 0.6 * ( 1.0 - ( enemyDist / HEINRICH_STOMP_RANGE ) ) ), enemyVec ); enemyVec[2] = scale * HEINRICH_STOMP_VELOCITY_Z * ( 1.0 - 0.5 * ( enemyDist / HEINRICH_STOMP_RANGE ) ); // bounce the player using this velocity VectorAdd( enemy->client->ps.velocity, enemyVec, enemy->client->ps.velocity ); } } return NULL; } char *AIFunc_Heinrich_MeleeStart( cast_state_t *cs ) { gentity_t *ent = &g_entities[cs->entityNum]; gentity_t *enemy = &g_entities[cs->enemyNum]; int rnd; static int lastStomp; if ( cs->enemyNum < 0 ) { return NULL; } // record weapon fire cs->weaponFireTimes[cs->weaponNum] = level.time; // face them AICast_AimAtEnemy( cs ); // clear flags cs->aiFlags &= ~( AIFL_MISCFLAG1 | AIFL_MISCFLAG2 ); // decide which attack to use if ( VectorDistance( ent->r.currentOrigin, enemy->r.currentOrigin ) < 60 ) { rnd = 0; // sword slash up close } else if ( VectorDistance( ent->r.currentOrigin, enemy->r.currentOrigin ) >= HEINRICH_SLASH_RANGE ) { rnd = 1; // too far away, stomp } else { // pick at random rnd = rand() % 2; } // switch ( rnd ) { case 0: { int rnd = rand() % 3; switch ( rnd ) { case 0: return AIFunc_Heinrich_SwordSideSlashStart( cs ); case 1: return AIFunc_Heinrich_SwordKnockbackStart( cs ); case 2: return AIFunc_Heinrich_SwordLungeStart( cs ); } } case 1: // dont do stomp too often if ( lastStomp > level.time - 12000 ) { // plenty of time to let debris disappear return NULL; } lastStomp = level.time; cs->aiFlags |= AIFL_SPECIAL_FUNC; // sound G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_EARTHQUAKE_START] ); // play the anim BG_PlayAnimName( &ent->client->ps, "attack7", ANIM_BP_BOTH, qtrue, qfalse, qtrue ); // start the func cs->aifunc = AIFunc_Heinrich_Earthquake; return "AIFunc_Heinrich_Earthquake"; } // shutup compiler return NULL; } #define HEINRICH_RAISEDEAD_DELAY 1200 #define HEINRICH_RAISEDEAD_COUNT 3 int lastRaise; char *AIFunc_Heinrich_RaiseDead( cast_state_t *cs ) { int i; gentity_t *ent = &g_entities[cs->entityNum]; gentity_t *enemy = &g_entities[cs->enemyNum]; gentity_t *trav, *closest; float closestDist, dist; // cs->aiFlags |= AIFL_SPECIAL_FUNC; if ( cs->enemyNum < 0 ) { if ( !ent->client->ps.torsoTimer ) { return AIFunc_DefaultStart( cs ); } return NULL; } // // record weapon fire cs->weaponFireTimes[cs->weaponNum] = level.time; // if ( !ent->client->ps.torsoTimer ) { return AIFunc_DefaultStart( cs ); } if ( ent->count2 && lastRaise < level.time - HEINRICH_RAISEDEAD_DELAY ) { lastRaise = level.time; // summons the closest warrior closest = NULL; closestDist = 0; // shutup the compiler for ( i = 0, trav = g_entities; i < level.maxclients; i++, trav++ ) { if ( !trav->inuse ) { continue; } if ( !trav->aiInactive ) { continue; } if ( trav->aiCharacter != AICHAR_WARZOMBIE ) { continue; } dist = VectorDistance( trav->s.pos.trBase, enemy->r.currentOrigin ); if ( !closest || dist < closestDist ) { closest = trav; closestDist = dist; } } // if ( closest ) { closest->AIScript_AlertEntity( closest ); // make them aware of the player AICast_UpdateVisibility( closest, enemy, qtrue, qtrue ); // reduce the count ent->count2--; } } // return NULL; } char *AIFunc_Heinrich_RaiseDeadStart( cast_state_t *cs ) { int i, cnt, free; gentity_t *ent = &g_entities[cs->entityNum]; // gentity_t *enemy = &g_entities[cs->enemyNum]; gentity_t *trav, *spirits; float circleDist; // // count the number of active warriors cnt = 0; free = 0; for ( i = 0, trav = g_entities; i < level.maxclients; i++, trav++ ) { if ( !trav->inuse ) { continue; } if ( trav->aiCharacter != AICHAR_WARZOMBIE ) { continue; } if ( trav->aiInactive ) { free++; continue; } if ( trav->health <= 0 ) { continue; } cnt++; } // if ( cnt < HEINRICH_RAISEDEAD_COUNT && free ) { // need a new one cs->aiFlags &= ~AIFL_MISCFLAG1; ent->count2 = HEINRICH_RAISEDEAD_COUNT - cnt; lastRaise = level.time; cs->aiFlags |= AIFL_SPECIAL_FUNC; // start the animation BG_PlayAnimName( &ent->client->ps, "attack4", ANIM_BP_BOTH, qtrue, qfalse, qtrue ); // play the sound G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_RAISEDEAD_START] ); // start the func cs->aifunc = AIFunc_Heinrich_RaiseDead; return "AIFunc_Heinrich_RaiseDead"; } // enable all the spirit spawners trav = NULL; // TTimo: gcc: suggest () around assignment used as truth value while ( ( trav = G_Find( trav, FOFS( classname ), "func_bats" ) ) ) { if ( !trav->active && trav->spawnflags & 4 ) { trav->active = 1; // let them release spirits now } } // is the player outside the circle? trav = NULL; // TTimo: gcc: suggest () around assignment used as truth value while ( ( trav = G_Find( trav, FOFS( classname ), "func_bats" ) ) ) { if ( trav->spawnflags & 4 ) { spirits = trav; circleDist = trav->radius; trav = G_Find( NULL, FOFS( targetname ), trav->target ); if ( trav ) { if ( VectorDistance( g_entities[0].s.pos.trBase, trav->s.origin ) > circleDist ) { cs->aiFlags &= ~AIFL_MISCFLAG1; ent->count2 = 0; cs->aiFlags |= AIFL_SPECIAL_FUNC; // start the animation BG_PlayAnimName( &ent->client->ps, "attack4", ANIM_BP_BOTH, qtrue, qfalse, qtrue ); // play the sound G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_RAISEDEAD_START] ); // start the func cs->aifunc = AIFunc_Heinrich_RaiseDead; return "AIFunc_Heinrich_RaiseDead"; } } break; } } // return NULL; } char *AIFunc_Heinrich_SpawnSpiritsStart( cast_state_t *cs ) { gentity_t *ent = &g_entities[cs->entityNum]; gentity_t *trav, *spirits; float circleDist; // // enable all the spirit spawners trav = NULL; // TTimo: gcc: suggest () around assignment used as truth value while ( ( trav = G_Find( trav, FOFS( classname ), "func_bats" ) ) ) { if ( !trav->active && trav->spawnflags & 4 ) { trav->active = 1; // let them release spirits now } } // is the player outside the circle? trav = NULL; // TTimo: gcc: suggest () around assignment used as truth value while ( ( trav = G_Find( trav, FOFS( classname ), "func_bats" ) ) ) { if ( trav->spawnflags & 4 ) { spirits = trav; circleDist = trav->radius; trav = G_Find( NULL, FOFS( targetname ), trav->target ); if ( trav ) { if ( VectorDistance( g_entities[0].s.pos.trBase, trav->s.origin ) > circleDist ) { cs->aiFlags &= ~AIFL_MISCFLAG1; ent->count2 = 0; cs->aiFlags |= AIFL_SPECIAL_FUNC; // start the animation BG_PlayAnimName( &ent->client->ps, "attack4", ANIM_BP_BOTH, qtrue, qfalse, qtrue ); // play the sound G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_RAISEDEAD_START] ); // start the func cs->aifunc = AIFunc_Heinrich_RaiseDead; // just do raise dead, without raising any warriors return "AIFunc_Heinrich_RaiseDead"; } } break; } } // return NULL; }