mirror of
https://github.com/id-Software/DOOM-3-BFG.git
synced 2025-03-16 15:41:16 +00:00
2108 lines
42 KiB
Text
2108 lines
42 KiB
Text
/***********************************************************************
|
|
|
|
ai_monster_base.script
|
|
|
|
base class for all monsters. implements common behavior.
|
|
|
|
***********************************************************************/
|
|
|
|
//
|
|
// attack flags
|
|
//
|
|
#define ATTACK_DODGE_LEFT 1
|
|
#define ATTACK_DODGE_RIGHT 2
|
|
#define ATTACK_COMBAT_NODE 4
|
|
#define ATTACK_MELEE 8
|
|
#define ATTACK_LEAP 16
|
|
#define ATTACK_MISSILE 32
|
|
#define ATTACK_SPECIAL1 64
|
|
#define ATTACK_SPECIAL2 128
|
|
#define ATTACK_SPECIAL3 256
|
|
#define ATTACK_SPECIAL4 512
|
|
|
|
|
|
#define AI_NOT_ACTIVATED 0
|
|
#define AI_CHASING_ENEMY 1
|
|
#define AI_LOST 2
|
|
#define AI_PATH_FOLLOWING 3
|
|
#define AI_ATTACK_NODE 4
|
|
|
|
/***********************************************************************
|
|
|
|
base class for monsters
|
|
|
|
***********************************************************************/
|
|
|
|
object monster_base : ai {
|
|
// common state variables
|
|
float run_distance; // distance to trigger running when chasing enemy
|
|
float walk_turn; // max turn amount allowed when running
|
|
boolean run;
|
|
boolean ambush;
|
|
boolean ignoreEnemies; // used to disable enemy checks during attack_path
|
|
boolean stay_on_attackpath; // used to disable enemy checks during attack_path
|
|
boolean idle_sight_fov;
|
|
float customBlendOut;
|
|
entity current_path;
|
|
entity next_path;
|
|
boolean resurrect;
|
|
boolean ignore_lostcombat;
|
|
boolean blocked;
|
|
boolean ignore_sight;
|
|
|
|
// common animation functions
|
|
void Torso_CustomCycle();
|
|
void Torso_CustomAnim();
|
|
void Torso_Sight();
|
|
void Torso_Death();
|
|
void Legs_Idle();
|
|
void Legs_Death();
|
|
|
|
// attack checks
|
|
// these functions are meant to be implemented by subclasses.
|
|
// by default, they do nothing.
|
|
float check_attacks();
|
|
void do_attack( float attack_flags );
|
|
boolean checkTurretAttack(); // tests if monster can still do a turret attack from current location
|
|
|
|
// common functions
|
|
void init();
|
|
void state_Begin();
|
|
void monster_begin();
|
|
void archvile_minion();
|
|
boolean can_resurrect();
|
|
void monster_resurrect( entity enemy );
|
|
void wait_for_enemy();
|
|
void state_Killed();
|
|
void state_Dead();
|
|
void wake_on_enemy();
|
|
void wake_on_trigger();
|
|
void walk_on_trigger();
|
|
void wake_on_attackcone();
|
|
void sight_enemy();
|
|
void enemy_dead();
|
|
void state_Combat();
|
|
void state_LostCombat();
|
|
void state_Spawner();
|
|
void monster_base_lost_combat();
|
|
boolean check_blocked();
|
|
boolean combat_chase();
|
|
void combat_turret_node( entity node );
|
|
void combat_attack_cone( entity node );
|
|
void combat_ainode( entity node );
|
|
void combat_lost();
|
|
void combat_wander();
|
|
void playCustomCycle( string animname, float blendTime );
|
|
void playCustomAnim( string animname, float blendIn, float blendOut );
|
|
void checkNodeAnim( entity node, string prefix, string anim );
|
|
void trigger_wakeup_targets();
|
|
boolean checkForEnemy( float use_fov );
|
|
void state_FollowAlternatePath();
|
|
void follow_alternate_path1();
|
|
void follow_alternate_path2();
|
|
void follow_alternate_path3();
|
|
|
|
// path following
|
|
void idle_followPathEntities( entity pathnode );
|
|
void path_corner();
|
|
void path_anim();
|
|
void path_cycleanim();
|
|
void path_turn();
|
|
void path_wait();
|
|
void path_waitfortrigger();
|
|
void path_hide();
|
|
void path_show();
|
|
void path_attack();
|
|
};
|
|
|
|
/***********************************************************************
|
|
|
|
Torso animation control
|
|
|
|
***********************************************************************/
|
|
|
|
void monster_base::Torso_CustomCycle() {
|
|
eachFrame {
|
|
;
|
|
}
|
|
}
|
|
|
|
void monster_base::Torso_CustomAnim() {
|
|
while( !animDone( ANIMCHANNEL_TORSO, customBlendOut ) ) {
|
|
waitFrame();
|
|
}
|
|
|
|
finishAction( "customAnim" );
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", customBlendOut );
|
|
}
|
|
|
|
void monster_base::Torso_Sight() {
|
|
string animname;
|
|
float blendFrames;
|
|
|
|
if ( !getIntKey( "walk_on_sight" ) ) {
|
|
overrideAnim( ANIMCHANNEL_LEGS );
|
|
}
|
|
|
|
animname = self.getKey( "on_activate" );
|
|
if ( self.getKey( "on_activate_blend" ) ) {
|
|
blendFrames = self.getIntKey( "on_activate_blend" );
|
|
} else {
|
|
blendFrames = 4;
|
|
}
|
|
|
|
if ( animname != "" ) {
|
|
if ( hasAnim( ANIMCHANNEL_TORSO, animname ) ) {
|
|
playAnim( ANIMCHANNEL_TORSO, animname );
|
|
|
|
while( !animDone( ANIMCHANNEL_TORSO, blendFrames ) ) {
|
|
waitFrame();
|
|
}
|
|
}
|
|
}
|
|
|
|
finishAction( "sight" );
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", blendFrames );
|
|
}
|
|
|
|
void monster_base::Torso_Death() {
|
|
finishAction( "dead" );
|
|
}
|
|
|
|
void monster_base::Legs_Idle() {
|
|
// implemented by subclasses
|
|
}
|
|
|
|
void monster_base::Legs_Death() {
|
|
}
|
|
|
|
/***********************************************************************
|
|
|
|
AI
|
|
|
|
***********************************************************************/
|
|
|
|
/*
|
|
=====================
|
|
monster_base::init
|
|
=====================
|
|
*/
|
|
void monster_base::init() {
|
|
ignoreEnemies = false;
|
|
run_distance = 0;
|
|
walk_turn = 360;
|
|
ambush = getIntKey( "ambush" );
|
|
ignore_lostcombat = getIntKey( "ignore_lostcombat" );
|
|
stay_on_attackpath = getIntKey( "stay_on_attackpath" );
|
|
idle_sight_fov = true;
|
|
run = false;
|
|
|
|
//_D3XP: Allow AI to be invulnerable always
|
|
float nodamage;
|
|
nodamage = getIntKey("no_damage");
|
|
if(nodamage) {
|
|
ignoreDamage();
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::state_Begin
|
|
|
|
Initial state for monsters. Called after spawn and when monster is resurrected.
|
|
=====================
|
|
*/
|
|
void monster_base::state_Begin() {
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::do_attack
|
|
|
|
Performs an attack based on which bits are set on the attack_flags.
|
|
|
|
Implemented by subclasses
|
|
=====================
|
|
*/
|
|
void monster_base::do_attack( float attack_flags ) {
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::check_attacks
|
|
|
|
Returns flags stating which attacks can be performed this frame.
|
|
|
|
Implemented by subclasses
|
|
=====================
|
|
*/
|
|
float monster_base::check_attacks() {
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::playCustomCycle
|
|
=====================
|
|
*/
|
|
void monster_base::playCustomCycle( string animname, float blendTime ) {
|
|
animState( ANIMCHANNEL_TORSO, "Torso_CustomCycle", blendTime );
|
|
overrideAnim( ANIMCHANNEL_LEGS );
|
|
playCycle( ANIMCHANNEL_TORSO, animname );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::playCustomAnim
|
|
=====================
|
|
*/
|
|
void monster_base::playCustomAnim( string animname, float blendIn, float blendOut ) {
|
|
customBlendOut = blendOut;
|
|
animState( ANIMCHANNEL_TORSO, "Torso_CustomAnim", blendIn );
|
|
overrideAnim( ANIMCHANNEL_LEGS );
|
|
playAnim( ANIMCHANNEL_TORSO, animname );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::trigger_wakeup_targets
|
|
=====================
|
|
*/
|
|
void monster_base::trigger_wakeup_targets() {
|
|
string key;
|
|
string name;
|
|
entity ent;
|
|
|
|
key = getNextKey( "wakeup_target", "" );
|
|
while( key != "" ) {
|
|
name = getKey( key );
|
|
ent = sys.getEntity( name );
|
|
if ( !ent ) {
|
|
sys.warning( "Unknown wakeup_target '" + name + "' on entity '" + getName() + "'" );
|
|
} else {
|
|
sys.trigger( ent );
|
|
}
|
|
key = getNextKey( "wakeup_target", key );
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::checkForEnemy
|
|
=====================
|
|
*/
|
|
boolean monster_base::checkForEnemy( float use_fov ) {
|
|
entity enemy;
|
|
vector size;
|
|
float dist;
|
|
|
|
if ( sys.influenceActive() ) {
|
|
return false;
|
|
}
|
|
|
|
if ( AI_PAIN ) {
|
|
// get out of ambush mode when shot
|
|
ambush = false;
|
|
}
|
|
|
|
if ( ignoreEnemies ) {
|
|
// while we're following paths, we only respond to enemies on pain, or when close enough to them
|
|
if ( stay_on_attackpath ) {
|
|
// don't exit attack_path when close to enemy
|
|
return false;
|
|
}
|
|
|
|
enemy = getEnemy();
|
|
if ( !enemy ) {
|
|
enemy = findEnemy( false );
|
|
}
|
|
|
|
if ( !enemy ) {
|
|
return false;
|
|
}
|
|
|
|
size = getSize();
|
|
dist = ( size_x * 1.414 ) + 16; // diagonal distance plus 16 units
|
|
if ( enemyRange() > dist ) {
|
|
return false;
|
|
}
|
|
} else {
|
|
if ( getEnemy() ) {
|
|
// we were probably triggered (which sets our enemy)
|
|
return true;
|
|
}
|
|
|
|
if ( !ignore_sight ) {
|
|
enemy = findEnemy( use_fov );
|
|
}
|
|
|
|
if ( !enemy ) {
|
|
if ( ambush ) {
|
|
return false;
|
|
}
|
|
|
|
enemy = heardSound( true );
|
|
if ( !enemy ) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
ignoreEnemies = false;
|
|
|
|
// once we've woken up, get out of ambush mode
|
|
ambush = false;
|
|
|
|
// don't use the fov for sight anymore
|
|
idle_sight_fov = false;
|
|
|
|
setEnemy( enemy );
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::state_Spawner
|
|
=====================
|
|
*/
|
|
void monster_base::state_Spawner() {
|
|
entity ent;
|
|
float triggerCount;
|
|
float maxSpawn;
|
|
float i;
|
|
string name;
|
|
|
|
maxSpawn = getIntKey( "spawner" );
|
|
name = getName();
|
|
|
|
hide();
|
|
|
|
triggerCount = 0;
|
|
AI_ACTIVATED = false;
|
|
while( 1 ) {
|
|
if ( AI_ACTIVATED ) {
|
|
triggerCount++;
|
|
AI_ACTIVATED = false;
|
|
}
|
|
|
|
if ( triggerCount ) {
|
|
if ( canBecomeSolid() ) {
|
|
for( i = 0; i < maxSpawn; i++ ) {
|
|
ent = sys.getEntity( name + i );
|
|
if ( !ent ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( i < maxSpawn ) {
|
|
triggerCount--;
|
|
sys.copySpawnArgs( self );
|
|
sys.setSpawnArg( "spawner", "0" );
|
|
sys.setSpawnArg( "name", name + i );
|
|
if ( getKey( "spawn_target" ) != "" ) {
|
|
sys.setSpawnArg( "target", getKey( "spawn_target" ) );
|
|
}
|
|
ent = sys.spawn( getKey( "classname" ) );
|
|
sys.trigger( ent );
|
|
}
|
|
}
|
|
}
|
|
waitFrame();
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::monster_begin
|
|
=====================
|
|
*/
|
|
void monster_base::monster_begin() {
|
|
float teleportType;
|
|
string triggerAnim;
|
|
float waittime;
|
|
boolean start_active;
|
|
entity path;
|
|
float movetype;
|
|
entity enemy;
|
|
|
|
run = false;
|
|
ignore_sight = getIntKey( "no_sight" );
|
|
|
|
if ( !getIntKey( "ignore_flashlight" ) ) {
|
|
// allow waking up from the flashlight
|
|
wakeOnFlashlight( true );
|
|
}
|
|
|
|
start_active = false;
|
|
|
|
if ( resurrect ) {
|
|
teleportType = 4;
|
|
triggerAnim = "";
|
|
AI_ACTIVATED = true;
|
|
} else {
|
|
teleportType = getIntKey( "teleport" );
|
|
triggerAnim = getKey( "trigger_anim" );
|
|
}
|
|
|
|
if ( getIntKey( "spawner" ) ) {
|
|
setState( "state_Spawner" );
|
|
}
|
|
|
|
if ( triggerAnim != "" ) {
|
|
//
|
|
// hide until triggered and then play a special animation
|
|
//
|
|
checkAnim( ANIMCHANNEL_TORSO, triggerAnim );
|
|
hide();
|
|
waitUntil( AI_ACTIVATED );
|
|
waitUntil( canBecomeSolid() );
|
|
|
|
// don't go dormant during trigger_anim anims since they
|
|
// may end up floating in air during no gravity anims.
|
|
setNeverDormant( true );
|
|
|
|
show();
|
|
trigger_wakeup_targets();
|
|
playCustomAnim( triggerAnim, 0, 4 );
|
|
waitAction( "customAnim" );
|
|
setNeverDormant( getFloatKey( "neverdormant" ) );
|
|
locateEnemy();
|
|
start_active = true;
|
|
} else if ( teleportType > 0 ) {
|
|
//
|
|
// teleport in when triggered
|
|
//
|
|
hide();
|
|
waitUntil( AI_ACTIVATED );
|
|
waitUntil( canBecomeSolid() );
|
|
becomeSolid();
|
|
movetype = getMoveType();
|
|
setMoveType( MOVETYPE_STATIC );
|
|
|
|
// don't go dormant during teleport anims since they
|
|
// may end up floating in air during no gravity anims.
|
|
setNeverDormant( true );
|
|
|
|
trigger_wakeup_targets();
|
|
if ( teleportType == 1 ) {
|
|
startFx( getKey( "fx_teleport1" ) );
|
|
wait( 1.6 );
|
|
} else if ( teleportType == 2 ) {
|
|
startFx( getKey( "fx_teleport2" ) );
|
|
wait( 2.6 );
|
|
} else if ( teleportType == 3 ) {
|
|
startFx( getKey( "fx_teleport3" ) );
|
|
wait( 3.6 );
|
|
} else {
|
|
startFx( getKey( "fx_teleport" ) );
|
|
wait( 0.6 );
|
|
}
|
|
show();
|
|
playCustomAnim( "teleport", 0, 4 );
|
|
waitAction( "customAnim" );
|
|
setNeverDormant( getFloatKey( "neverdormant" ) );
|
|
locateEnemy();
|
|
setMoveType( movetype );
|
|
start_active = true;
|
|
} else if ( getIntKey( "hide" ) ) {
|
|
//
|
|
// hide until triggered
|
|
//
|
|
hide();
|
|
waitUntil( AI_ACTIVATED );
|
|
if ( ( getIntKey( "hide" ) == 1 ) || ambush ) {
|
|
AI_ACTIVATED = false;
|
|
clearEnemy();
|
|
}
|
|
waitUntil( canBecomeSolid() );
|
|
show();
|
|
}
|
|
|
|
waittime = getFloatKey( "wait" );
|
|
if ( waittime > 0 ) {
|
|
sys.wait( waittime );
|
|
}
|
|
|
|
enemy = getEntityKey( "enemy" );
|
|
if ( enemy ) {
|
|
setEnemy( enemy );
|
|
}
|
|
|
|
if ( !start_active ) {
|
|
if ( getIntKey( "wake_on_attackcone" ) ) {
|
|
wake_on_attackcone();
|
|
} else if ( getIntKey( "walk_on_trigger" ) ) {
|
|
walk_on_trigger();
|
|
} else if ( getIntKey( "trigger" ) ) {
|
|
wake_on_trigger();
|
|
} else {
|
|
wake_on_enemy();
|
|
}
|
|
} else if ( getIntKey( "attack_path" ) ) {
|
|
// follow a path and fight player at end
|
|
path = randomPath();
|
|
if ( path ) {
|
|
run = true;
|
|
ignoreEnemies = true;
|
|
idle_followPathEntities( path );
|
|
ignoreEnemies = false;
|
|
}
|
|
}
|
|
|
|
// allow him to see after he's woken up
|
|
ignore_sight = false;
|
|
|
|
// ignore the flashlight from now on
|
|
wakeOnFlashlight( false );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::archvile_minion
|
|
=====================
|
|
*/
|
|
void monster_base::archvile_minion() {
|
|
hide();
|
|
resurrect = true;
|
|
AI_DEAD = true;
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::can_resurrect
|
|
=====================
|
|
*/
|
|
boolean monster_base::can_resurrect() {
|
|
if ( !AI_DEAD ) {
|
|
return false;
|
|
}
|
|
|
|
if ( !isHidden() ) {
|
|
return false;
|
|
}
|
|
|
|
if ( !canBecomeSolid() ) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::state_Resurrect
|
|
=====================
|
|
*/
|
|
void monster_base::state_Resurrect() {
|
|
float health;
|
|
vector ang;
|
|
|
|
AI_DEAD = false;
|
|
|
|
stopMove();
|
|
hide();
|
|
stopRagdoll();
|
|
restorePosition();
|
|
|
|
waitUntil( canBecomeSolid() );
|
|
|
|
health = getFloatKey( "health" );
|
|
setHealth( health );
|
|
|
|
ang_y = getFloatKey( "angle" );
|
|
setAngles( ang );
|
|
turnTo( ang_y );
|
|
|
|
clearBurn();
|
|
allowDamage();
|
|
|
|
setState( "state_Begin" );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::monster_resurrect
|
|
=====================
|
|
*/
|
|
void monster_base::monster_resurrect( entity enemy ) {
|
|
// mark them as not dead so we don't get resurrected twice this frame
|
|
AI_DEAD = false;
|
|
if ( enemy ) {
|
|
setEnemy( enemy );
|
|
} else {
|
|
setEnemy( $player1 );
|
|
}
|
|
setState( "state_Resurrect" );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::wait_for_enemy
|
|
=====================
|
|
*/
|
|
void monster_base::wait_for_enemy() {
|
|
// prevent an infinite loop when in notarget
|
|
AI_PAIN = false;
|
|
|
|
stopMove();
|
|
|
|
while( !AI_PAIN && !getEnemy() ) {
|
|
if ( checkForEnemy( idle_sight_fov ) ) {
|
|
break;
|
|
}
|
|
waitFrame();
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::state_Killed
|
|
=====================
|
|
*/
|
|
void monster_base::state_Killed() {
|
|
stopMove();
|
|
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Death", 0 );
|
|
animState( ANIMCHANNEL_LEGS, "Legs_Death", 0 );
|
|
|
|
waitAction( "dead" );
|
|
setState( "state_Dead" );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::state_Dead
|
|
=====================
|
|
*/
|
|
void monster_base::state_Dead() {
|
|
|
|
float burnDelay = getFloatKey( "burnaway" );
|
|
if ( burnDelay != 0 ) {
|
|
preBurn();
|
|
sys.wait( burnDelay );
|
|
burn();
|
|
startSound( "snd_burn", SND_CHANNEL_BODY, false );
|
|
}
|
|
|
|
sys.wait( 3 );
|
|
|
|
if ( resurrect ) {
|
|
hide();
|
|
stopRagdoll();
|
|
restorePosition();
|
|
|
|
// wait until we're resurrected
|
|
waitUntil( 0 );
|
|
}
|
|
remove();
|
|
}
|
|
/*
|
|
=====================
|
|
monster_base::sight_enemy
|
|
=====================
|
|
*/
|
|
void monster_base::sight_enemy() {
|
|
string animname;
|
|
|
|
faceEnemy();
|
|
animname = self.getKey( "on_activate" );
|
|
if ( animname != "" ) {
|
|
// don't go dormant during on_activate anims since they
|
|
// may end up floating in air during no gravity anims.
|
|
setNeverDormant( true );
|
|
if ( getIntKey( "walk_on_sight" ) ) {
|
|
moveToEnemy();
|
|
}
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Sight", 4 );
|
|
waitAction( "sight" );
|
|
setNeverDormant( getFloatKey( "neverdormant" ) );
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::enemy_dead
|
|
=====================
|
|
*/
|
|
void monster_base::enemy_dead() {
|
|
AI_ENEMY_DEAD = false;
|
|
checkForEnemy( false );
|
|
if ( !getEnemy() ) {
|
|
waitFrame(); // avoid infinite loops
|
|
setState( "state_Idle" );
|
|
} else {
|
|
setState( "state_Combat" );
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::walk_on_trigger
|
|
=====================
|
|
*/
|
|
void monster_base::walk_on_trigger() {
|
|
string animname;
|
|
entity path;
|
|
|
|
allowMovement( false );
|
|
|
|
// sit in our idle anim till we're activated
|
|
animname = self.getKey( "anim" );
|
|
playCustomCycle( animname, 4 );
|
|
waitUntil( AI_ACTIVATED || AI_PAIN );
|
|
|
|
if ( AI_ACTIVATED ) {
|
|
clearEnemy();
|
|
AI_ACTIVATED = false;
|
|
}
|
|
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 );
|
|
allowMovement( true );
|
|
|
|
// follow a path
|
|
path = randomPath();
|
|
if ( path ) {
|
|
idle_followPathEntities( path );
|
|
}
|
|
|
|
if ( !getEnemy() && !AI_ACTIVATED && !AI_PAIN ) {
|
|
// sit in our idle anim till we're activated
|
|
allowMovement( false );
|
|
playCustomCycle( animname, 4 );
|
|
while( !AI_PAIN && !AI_ACTIVATED ) {
|
|
if ( checkForEnemy( true ) ) {
|
|
break;
|
|
}
|
|
waitFrame();
|
|
}
|
|
allowMovement( true );
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 );
|
|
}
|
|
|
|
trigger_wakeup_targets();
|
|
sight_enemy();
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::wake_on_trigger
|
|
=====================
|
|
*/
|
|
void monster_base::wake_on_trigger() {
|
|
string animname;
|
|
entity path;
|
|
|
|
if ( !getIntKey( "attack_path" ) ) {
|
|
path = randomPath();
|
|
if ( path ) {
|
|
idle_followPathEntities( path );
|
|
}
|
|
}
|
|
|
|
if ( !getEnemy() && !AI_ACTIVATED && !AI_PAIN ) {
|
|
// sit in our idle anim till we're activated
|
|
allowMovement( false );
|
|
|
|
animname = self.getKey( "anim" );
|
|
playCustomCycle( animname, 4 );
|
|
|
|
waitUntil( AI_ACTIVATED || AI_PAIN );
|
|
|
|
allowMovement( true );
|
|
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 );
|
|
}
|
|
|
|
trigger_wakeup_targets();
|
|
if ( getIntKey( "attack_path" ) ) {
|
|
// follow a path and fight player at end
|
|
path = randomPath();
|
|
if ( path ) {
|
|
ignoreEnemies = true;
|
|
run = true;
|
|
idle_followPathEntities( path );
|
|
ignoreEnemies = false;
|
|
}
|
|
} else {
|
|
sight_enemy();
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::wake_on_enemy
|
|
=====================
|
|
*/
|
|
void monster_base::wake_on_enemy() {
|
|
string animname;
|
|
entity path;
|
|
|
|
if ( !getIntKey( "attack_path" ) ) {
|
|
path = randomPath();
|
|
if ( path ) {
|
|
idle_followPathEntities( path );
|
|
}
|
|
}
|
|
|
|
if ( !getEnemy() && !AI_ACTIVATED && !AI_PAIN ) {
|
|
// sit in our idle anim till we're activated
|
|
allowMovement( false );
|
|
animname = self.getKey( "anim" );
|
|
playCustomCycle( animname, 4 );
|
|
|
|
while( !AI_PAIN && !AI_ACTIVATED ) {
|
|
if ( checkForEnemy( true ) ) {
|
|
break;
|
|
}
|
|
waitFrame();
|
|
}
|
|
}
|
|
|
|
allowMovement( true );
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 );
|
|
|
|
trigger_wakeup_targets();
|
|
|
|
if ( getIntKey( "attack_path" ) ) {
|
|
// follow a path and fight player at end
|
|
path = randomPath();
|
|
if ( path ) {
|
|
run = true;
|
|
ignoreEnemies = true;
|
|
idle_followPathEntities( path );
|
|
ignoreEnemies = false;
|
|
}
|
|
} else {
|
|
sight_enemy();
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::wake_on_attackcone
|
|
=====================
|
|
*/
|
|
void monster_base::wake_on_attackcone() {
|
|
string animname;
|
|
entity path;
|
|
entity enemy;
|
|
|
|
if ( !getIntKey( "attack_path" ) ) {
|
|
path = randomPath();
|
|
if ( path ) {
|
|
idle_followPathEntities( path );
|
|
run = path.getIntKey( "run" );
|
|
while( !AI_MOVE_DONE && !AI_ACTIVATED && !AI_PAIN ) {
|
|
enemy = findEnemyInCombatNodes();
|
|
if ( enemy ) {
|
|
setEnemy( enemy );
|
|
break;
|
|
}
|
|
waitFrame();
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( !getEnemy() && !AI_ACTIVATED && !AI_PAIN ) {
|
|
// sit in our idle anim till we're activated
|
|
allowMovement( false );
|
|
|
|
animname = self.getKey( "anim" );
|
|
playCustomCycle( animname, 4 );
|
|
|
|
while( !AI_ACTIVATED && !AI_PAIN ) {
|
|
enemy = findEnemyInCombatNodes();
|
|
if ( enemy ) {
|
|
setEnemy( enemy );
|
|
break;
|
|
}
|
|
waitFrame();
|
|
}
|
|
|
|
allowMovement( true );
|
|
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 );
|
|
}
|
|
|
|
trigger_wakeup_targets();
|
|
|
|
if ( getIntKey( "attack_path" ) ) {
|
|
// follow a path and fight player at end
|
|
path = randomPath();
|
|
if ( path ) {
|
|
run = true;
|
|
ignoreEnemies = true;
|
|
idle_followPathEntities( path );
|
|
ignoreEnemies = false;
|
|
}
|
|
} else {
|
|
sight_enemy();
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::idle_followPathEntities
|
|
=====================
|
|
*/
|
|
void monster_base::idle_followPathEntities( entity pathnode ) {
|
|
string nodeaction;
|
|
string triggername;
|
|
entity triggerent;
|
|
|
|
current_path = pathnode;
|
|
do {
|
|
next_path = current_path.randomPath();
|
|
nodeaction = current_path.getKey( "classname" );
|
|
if ( hasFunction( nodeaction ) ) {
|
|
|
|
//#ifdef _D3XP
|
|
// trigger an entity right when the path corner is accepted
|
|
string pretriggername;
|
|
entity pretriggerent;
|
|
pretriggername = current_path.getKey( "pretrigger" );
|
|
if ( pretriggername != "" ) {
|
|
pretriggerent = sys.getEntity( pretriggername );
|
|
if ( pretriggerent ) {
|
|
pretriggerent.activate( self );
|
|
}
|
|
}
|
|
//#endif
|
|
|
|
callFunction( nodeaction );
|
|
} else {
|
|
sys.warning( "'" + getName() + "' encountered an unsupported path entity '" + nodeaction + "' on entity '" + current_path.getName() + "'\n" );
|
|
return;
|
|
}
|
|
|
|
if ( checkForEnemy( true ) ) {
|
|
return;
|
|
}
|
|
|
|
// trigger any entities the path had targeted
|
|
triggername = current_path.getKey( "trigger" );
|
|
if ( triggername != "" ) {
|
|
triggerent = sys.getEntity( triggername );
|
|
if ( triggerent ) {
|
|
triggerent.activate( self );
|
|
}
|
|
}
|
|
|
|
current_path = next_path;
|
|
} while( !( !current_path ) );
|
|
}
|
|
|
|
/***********************************************************************
|
|
|
|
path functions
|
|
|
|
***********************************************************************/
|
|
|
|
/*
|
|
=====================
|
|
monster_base::path_corner
|
|
=====================
|
|
*/
|
|
void monster_base::path_corner() {
|
|
string customAnim;
|
|
|
|
if ( current_path.getKey( "run" ) != "" ) {
|
|
run = current_path.getIntKey( "run" );
|
|
}
|
|
customAnim = current_path.getKey( "anim" );
|
|
|
|
while( 1 ) {
|
|
if ( customAnim != "" ) {
|
|
playCustomCycle( customAnim, 4 );
|
|
}
|
|
moveToEntity( current_path );
|
|
waitFrame();
|
|
while( !AI_MOVE_DONE ) {
|
|
if ( sys.influenceActive() ) {
|
|
break;
|
|
}
|
|
if ( checkForEnemy( true ) ) {
|
|
break;
|
|
}
|
|
waitFrame();
|
|
}
|
|
|
|
if ( customAnim != "" ) {
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 );
|
|
}
|
|
|
|
if ( sys.influenceActive() ) {
|
|
stopMove();
|
|
while( sys.influenceActive() ) {
|
|
waitFrame();
|
|
}
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if ( AI_DEST_UNREACHABLE ) {
|
|
// Can't reach
|
|
sys.warning( "entity '" + getName() + "' couldn't reach path_corner '" + current_path.getName() + "'" );
|
|
waitFrame();
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::path_anim
|
|
=====================
|
|
*/
|
|
void monster_base::path_anim() {
|
|
string animname;
|
|
float ang;
|
|
float blend_in;
|
|
float blend_out;
|
|
|
|
animname = current_path.getKey( "anim" );
|
|
blend_in = current_path.getIntKey( "blend_in" );
|
|
blend_out = current_path.getIntKey( "blend_out" );
|
|
|
|
if ( current_path.getKey( "angle" ) != "" ) {
|
|
ang = current_path.getFloatKey( "angle" );
|
|
turnTo( ang );
|
|
while( !facingIdeal() ) {
|
|
if ( checkForEnemy( true ) ) {
|
|
return;
|
|
}
|
|
waitFrame();
|
|
}
|
|
}
|
|
|
|
playCustomAnim( animname, blend_in, blend_out );
|
|
while( !animDone( ANIMCHANNEL_TORSO, blend_out ) ) {
|
|
if ( checkForEnemy( true ) ) {
|
|
break;
|
|
}
|
|
waitFrame();
|
|
}
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", blend_out );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::path_cycleanim
|
|
=====================
|
|
*/
|
|
void monster_base::path_cycleanim() {
|
|
string animname;
|
|
vector ang;
|
|
float blend_in;
|
|
float blend_out;
|
|
float waittime;
|
|
|
|
animname = current_path.getKey( "anim" );
|
|
blend_in = current_path.getIntKey( "blend_in" );
|
|
blend_out = current_path.getIntKey( "blend_out" );
|
|
ang = current_path.getAngles();
|
|
turnTo( ang_y );
|
|
while( !facingIdeal() ) {
|
|
if ( checkForEnemy( true ) ) {
|
|
return;
|
|
}
|
|
waitFrame();
|
|
}
|
|
playCustomCycle( animname, blend_in );
|
|
|
|
waittime = current_path.getFloatKey( "wait" );
|
|
if ( waittime ) {
|
|
waittime += sys.getTime();
|
|
while( sys.getTime() < waittime ) {
|
|
if ( checkForEnemy( true ) ) {
|
|
return;
|
|
}
|
|
waitFrame();
|
|
}
|
|
} else {
|
|
AI_ACTIVATED = false;
|
|
while( !AI_ACTIVATED ) {
|
|
if ( checkForEnemy( true ) ) {
|
|
return;
|
|
}
|
|
waitFrame();
|
|
}
|
|
}
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", blend_out );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::path_turn
|
|
=====================
|
|
*/
|
|
void monster_base::path_turn() {
|
|
vector ang;
|
|
|
|
ang = current_path.getAngles();
|
|
turnTo( ang_y );
|
|
while( !facingIdeal() ) {
|
|
if ( checkForEnemy( true ) ) {
|
|
return;
|
|
}
|
|
waitFrame();
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::path_wait
|
|
=====================
|
|
*/
|
|
void monster_base::path_wait() {
|
|
float waittime;
|
|
|
|
waittime = current_path.getFloatKey( "wait" );
|
|
waittime += sys.getTime();
|
|
while( sys.getTime() < waittime ) {
|
|
if ( checkForEnemy( true ) ) {
|
|
return;
|
|
}
|
|
waitFrame();
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::path_waitfortrigger
|
|
=====================
|
|
*/
|
|
void monster_base::path_waitfortrigger() {
|
|
AI_ACTIVATED = false;
|
|
while( !AI_ACTIVATED ) {
|
|
if ( checkForEnemy( true ) ) {
|
|
return;
|
|
}
|
|
waitFrame();
|
|
}
|
|
AI_ACTIVATED = false;
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::path_hide
|
|
=====================
|
|
*/
|
|
void monster_base::path_hide() {
|
|
hide();
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::path_show
|
|
=====================
|
|
*/
|
|
void monster_base::path_show() {
|
|
waitUntil( canBecomeSolid() );
|
|
show();
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::path_attack
|
|
=====================
|
|
*/
|
|
void monster_base::path_attack() {
|
|
entity enemy;
|
|
float delta;
|
|
boolean do_run;
|
|
float range;
|
|
float attack_flags;
|
|
|
|
enemy = current_path.getEntityKey( "enemy" );
|
|
if ( !enemy ) {
|
|
return;
|
|
}
|
|
|
|
setEnemy( enemy );
|
|
locateEnemy();
|
|
|
|
AI_ACTIVATED = false;
|
|
while( !AI_ACTIVATED && !( !enemy ) ) {
|
|
stopMove();
|
|
while( sys.influenceActive() ) {
|
|
waitFrame();
|
|
}
|
|
if ( enemyRange() > run_distance ) {
|
|
do_run = true;
|
|
} else {
|
|
do_run = false;
|
|
}
|
|
|
|
// set our enemy again in case we were shot by the player
|
|
setEnemy( enemy );
|
|
if ( AI_ENEMY_DEAD ) {
|
|
break;
|
|
}
|
|
moveToEnemy();
|
|
if ( AI_MOVE_DONE ) {
|
|
locateEnemy();
|
|
moveToEnemy();
|
|
if ( AI_MOVE_DONE ) {
|
|
// prevent runaway loops if monster can't reach enemy
|
|
waitFrame();
|
|
}
|
|
}
|
|
while( !AI_ACTIVATED && !AI_MOVE_DONE && !AI_DEST_UNREACHABLE && !( !enemy ) ) {
|
|
// set our enemy again in case we were shot by the player
|
|
setEnemy( enemy );
|
|
if ( AI_ENEMY_DEAD ) {
|
|
break;
|
|
}
|
|
|
|
moveToEnemy();
|
|
|
|
if ( AI_ENEMY_IN_FOV ) {
|
|
lookAtEnemy( 1 );
|
|
}
|
|
|
|
if ( sys.influenceActive() ) {
|
|
break;
|
|
}
|
|
attack_flags = check_attacks();
|
|
if ( attack_flags ) {
|
|
do_attack( attack_flags );
|
|
}
|
|
|
|
range = enemyRange();
|
|
if ( range > run_distance ) {
|
|
do_run = true;
|
|
}
|
|
|
|
delta = getTurnDelta();
|
|
if ( ( delta > walk_turn ) || ( delta < -walk_turn ) ) {
|
|
run = false;
|
|
} else {
|
|
run = do_run;
|
|
}
|
|
|
|
waitFrame();
|
|
}
|
|
stopMove();
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
|
|
Combat
|
|
|
|
***********************************************************************/
|
|
|
|
/*
|
|
=====================
|
|
monster_base::state_Combat
|
|
=====================
|
|
*/
|
|
void monster_base::state_Combat() {
|
|
float attack_flags;
|
|
|
|
eachFrame {
|
|
faceEnemy();
|
|
if ( AI_ENEMY_IN_FOV ) {
|
|
lookAtEnemy( 1 );
|
|
}
|
|
|
|
if ( sys.influenceActive() ) {
|
|
waitFrame();
|
|
continue;
|
|
}
|
|
|
|
if ( AI_ENEMY_DEAD ) {
|
|
enemy_dead();
|
|
}
|
|
|
|
attack_flags = check_attacks();
|
|
if ( attack_flags ) {
|
|
do_attack( attack_flags );
|
|
continue;
|
|
}
|
|
|
|
if ( !combat_chase() ) {
|
|
locateEnemy();
|
|
if ( !combat_chase() ) {
|
|
combat_lost();
|
|
}
|
|
}
|
|
|
|
waitFrame();
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::check_blocked
|
|
|
|
returns true when an attack was called, since the move command may be different from when entering the function.
|
|
=====================
|
|
*/
|
|
boolean monster_base::check_blocked() {
|
|
entity obstacle;
|
|
float attack_flags;
|
|
monster_base monster;
|
|
float endTime;
|
|
boolean oldrun;
|
|
|
|
oldrun = run;
|
|
if ( AI_BLOCKED ) {
|
|
//sys.print( sys.getTime() + " : " + getName() + " is stuck in place\n" );
|
|
saveMove();
|
|
run = true;
|
|
wander();
|
|
endTime = sys.getTime() + 2;
|
|
while( sys.getTime() < endTime ) {
|
|
attack_flags = check_attacks();
|
|
if ( attack_flags ) {
|
|
restoreMove();
|
|
do_attack( attack_flags );
|
|
run = oldrun;
|
|
return true;
|
|
}
|
|
waitFrame();
|
|
}
|
|
restoreMove();
|
|
} else if ( moveStatus() == MOVE_STATUS_BLOCKED_BY_OBJECT ) {
|
|
float force = getFloatKey( "kick_force" );
|
|
if ( !force ) {
|
|
force = 60;
|
|
}
|
|
kickObstacles( getObstacle(), force );
|
|
} else if ( moveStatus() > MOVE_STATUS_BLOCKED_BY_OBJECT ) {
|
|
// just wait for the path to be clear
|
|
obstacle = getObstacle();
|
|
monster = obstacle;
|
|
if ( monster ) {
|
|
if ( monster.blocked ) {
|
|
run = oldrun;
|
|
return false;
|
|
}
|
|
}
|
|
blocked = true;
|
|
saveMove();
|
|
while( moveStatus() > MOVE_STATUS_BLOCKED_BY_OBJECT ) {
|
|
run = true;
|
|
wander();
|
|
endTime = sys.getTime() + 1;
|
|
while( sys.getTime() < endTime ) {
|
|
if ( AI_MOVE_DONE ) {
|
|
faceEnemy();
|
|
}
|
|
attack_flags = check_attacks();
|
|
if ( attack_flags ) {
|
|
blocked = false;
|
|
restoreMove();
|
|
do_attack( attack_flags );
|
|
run = oldrun;
|
|
return true;
|
|
}
|
|
waitFrame();
|
|
}
|
|
restoreMove();
|
|
}
|
|
blocked = false;
|
|
}
|
|
|
|
run = oldrun;
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::combat_chase
|
|
=====================
|
|
*/
|
|
boolean monster_base::combat_chase() {
|
|
float delta;
|
|
boolean do_run;
|
|
float range;
|
|
float attack_flags;
|
|
|
|
if ( !AI_ENEMY_VISIBLE || ( enemyRange() > run_distance ) ) {
|
|
do_run = true;
|
|
} else {
|
|
do_run = false;
|
|
}
|
|
|
|
moveToEnemy();
|
|
if ( AI_MOVE_DONE ) {
|
|
return false;
|
|
}
|
|
|
|
waitFrame();
|
|
if ( AI_MOVE_DONE ) {
|
|
attack_flags = check_attacks();
|
|
if ( attack_flags ) {
|
|
do_attack( attack_flags );
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
while( !AI_MOVE_DONE && !AI_DEST_UNREACHABLE ) {
|
|
if ( AI_ENEMY_DEAD ) {
|
|
enemy_dead();
|
|
}
|
|
|
|
if ( sys.influenceActive() ) {
|
|
return true;
|
|
}
|
|
|
|
if ( AI_ENEMY_IN_FOV ) {
|
|
lookAtEnemy( 1 );
|
|
}
|
|
|
|
attack_flags = check_attacks();
|
|
if ( attack_flags ) {
|
|
do_attack( attack_flags );
|
|
return true;
|
|
}
|
|
|
|
if ( check_blocked() ) {
|
|
return true;
|
|
}
|
|
|
|
range = enemyRange();
|
|
if ( !AI_ENEMY_VISIBLE || ( range > run_distance ) ) {
|
|
do_run = true;
|
|
}
|
|
|
|
delta = getTurnDelta();
|
|
if ( ( delta > walk_turn ) || ( delta < -walk_turn ) ) {
|
|
run = false;
|
|
} else {
|
|
run = do_run;
|
|
}
|
|
|
|
waitFrame();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::checkTurretAttack
|
|
|
|
tests if monster can still do a turret attack from current location
|
|
=====================
|
|
*/
|
|
boolean monster_base::checkTurretAttack() {
|
|
return canHitEnemy();
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::combat_turret_node
|
|
=====================
|
|
*/
|
|
void monster_base::combat_turret_node( entity node ) {
|
|
vector pos;
|
|
float min_wait;
|
|
float max_wait;
|
|
float num_shots;
|
|
float wait_end;
|
|
float current_time;
|
|
float count;
|
|
boolean exit;
|
|
boolean do_melee;
|
|
|
|
min_wait = node.getFloatKey( "min_wait" );
|
|
max_wait = node.getFloatKey( "max_wait" );
|
|
num_shots = node.getFloatKey( "num_shots" );
|
|
|
|
pos = node.getOrigin();
|
|
|
|
exit = false;
|
|
while( !AI_ENEMY_DEAD ) {
|
|
if ( !node ) {
|
|
// level designers sometimes remove the combat node, so check for it so the thread doesn't get killed
|
|
break;
|
|
}
|
|
// run to the combat node
|
|
allowMovement( true );
|
|
run = true;
|
|
|
|
moveToEntity( node );
|
|
while( !AI_MOVE_DONE ) {
|
|
if ( sys.influenceActive() ) {
|
|
stopMove();
|
|
while( sys.influenceActive() ) {
|
|
waitFrame();
|
|
}
|
|
moveToEntity( node );
|
|
}
|
|
|
|
if ( !node ) {
|
|
// level designers sometimes remove the combat node, so check for it so the thread doesn't get killed
|
|
break;
|
|
}
|
|
|
|
if ( AI_ENEMY_IN_FOV ) {
|
|
lookAtEnemy( 1 );
|
|
}
|
|
// exit out if we can't get there or we can do a melee attack
|
|
do_melee = testMeleeAttack();
|
|
if ( AI_DEST_UNREACHABLE || do_melee ) {
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 );
|
|
return;
|
|
}
|
|
waitFrame();
|
|
}
|
|
|
|
if ( !AI_ENEMY_VISIBLE ) {
|
|
waitFrame();
|
|
continue;
|
|
}
|
|
|
|
if ( !checkTurretAttack() ) {
|
|
waitFrame();
|
|
continue;
|
|
}
|
|
|
|
if ( !node ) {
|
|
// level designers sometimes remove the combat node, so check for it so the thread doesn't get killed
|
|
break;
|
|
}
|
|
|
|
faceEnemy();
|
|
allowMovement( false );
|
|
|
|
// make sure he's precisely at the node
|
|
slideTo( pos, 0.25 );
|
|
waitUntil( AI_MOVE_DONE );
|
|
faceEnemy();
|
|
|
|
// do our attack
|
|
allowMovement( false );
|
|
|
|
count = int( sys.random( num_shots ) ) + 1;
|
|
while( count > 0 ) {
|
|
if ( sys.influenceActive() ) {
|
|
break;
|
|
}
|
|
if ( !node ) {
|
|
// level designers sometimes remove the combat node, so check for it so the thread doesn't get killed
|
|
exit = true;
|
|
break;
|
|
}
|
|
animState( ANIMCHANNEL_TORSO, "Torso_TurretAttack", 4 );
|
|
while( inAnimState( ANIMCHANNEL_TORSO, "Torso_TurretAttack" ) ) {
|
|
if ( AI_ENEMY_IN_FOV ) {
|
|
lookAtEnemy( 1 );
|
|
}
|
|
waitFrame();
|
|
}
|
|
if ( testMeleeAttack() || AI_ENEMY_DEAD ) {
|
|
exit = true;
|
|
break;
|
|
}
|
|
count--;
|
|
}
|
|
|
|
if ( exit ) {
|
|
break;
|
|
}
|
|
|
|
faceEnemy();
|
|
current_time = sys.getTime();
|
|
wait_end = current_time + min_wait + sys.random( max_wait - min_wait );
|
|
while( sys.influenceActive() || ( sys.getTime() < wait_end ) ) {
|
|
if ( AI_ENEMY_IN_FOV ) {
|
|
lookAtEnemy( 1 );
|
|
}
|
|
// exit out if we can do a melee attack or enemy is dead
|
|
// level designers sometimes remove the combat node, so check for it so the thread doesn't get killed
|
|
if ( testMeleeAttack() || AI_ENEMY_DEAD || !node ) {
|
|
exit = true;
|
|
break;
|
|
}
|
|
waitFrame();
|
|
}
|
|
|
|
if ( exit ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( node ) {
|
|
// we're done with the node, so get rid of it so we don't use it again
|
|
node.remove();
|
|
}
|
|
|
|
allowMovement( true );
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::checkNodeAnim
|
|
=====================
|
|
*/
|
|
void monster_base::checkNodeAnim( entity node, string prefix, string anim ) {
|
|
if ( !hasAnim( ANIMCHANNEL_TORSO, prefix + "_" + anim ) ) {
|
|
sys.error( "AI node '" + node.getName() + "' specifies missing anim '" + prefix + "_" + anim + "' on monster '" + getName() + "'" );
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::combat_attack_cone
|
|
=====================
|
|
*/
|
|
void monster_base::combat_attack_cone( entity node ) {
|
|
vector ang;
|
|
vector pos;
|
|
float min_wait;
|
|
float max_wait;
|
|
float num_shots;
|
|
float wait_end;
|
|
float current_time;
|
|
float count;
|
|
boolean exit;
|
|
boolean attack;
|
|
boolean do_melee;
|
|
boolean dont_wait;
|
|
string prefix;
|
|
|
|
// run to the combat node
|
|
run = true;
|
|
moveToEntity( node );
|
|
while( !AI_MOVE_DONE ) {
|
|
if ( AI_ENEMY_IN_FOV ) {
|
|
lookAtEnemy( 1 );
|
|
}
|
|
// exit out if we can't get there or we can do a melee attack
|
|
do_melee = testMeleeAttack();
|
|
if ( AI_DEST_UNREACHABLE || do_melee || !enemyInCombatCone( node, false ) || AI_ENEMY_DEAD || !node ) {
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 );
|
|
return;
|
|
}
|
|
waitFrame();
|
|
}
|
|
|
|
if ( !node ) {
|
|
// level designers sometimes remove the combat node, so check for it so the thread doesn't get killed
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 );
|
|
return;
|
|
}
|
|
|
|
// set our anim prefix
|
|
prefix = node.getKey( "anim" );
|
|
if ( prefix == "" ) {
|
|
sys.error( "Missing 'anim' key on entity '" + node.getName() + "'" );
|
|
}
|
|
setAnimPrefix( prefix );
|
|
checkNodeAnim( node, prefix, "out" );
|
|
checkNodeAnim( node, prefix, "fire" );
|
|
checkNodeAnim( node, prefix, "in" );
|
|
checkNodeAnim( node, prefix, "wait" );
|
|
|
|
min_wait = node.getFloatKey( "min_wait" );
|
|
max_wait = node.getFloatKey( "max_wait" );
|
|
num_shots = node.getFloatKey( "num_shots" );
|
|
dont_wait = getIntKey( "wake_on_attackcone" );
|
|
if ( dont_wait ) {
|
|
setKey( "wake_on_attackcone", "0" );
|
|
}
|
|
|
|
// face the direction the node points
|
|
ang = node.getAngles();
|
|
turnTo( ang_y );
|
|
|
|
exit = false;
|
|
attack = false;
|
|
pos = node.getOrigin();
|
|
while( !( !node ) && enemyInCombatCone( node, true ) ) {
|
|
allowMovement( false );
|
|
|
|
// play the wait animation
|
|
playCustomCycle( "wait", 4 );
|
|
|
|
// make sure he's precisely at the node
|
|
slideTo( pos, 0.5 );
|
|
current_time = sys.getTime();
|
|
if ( dont_wait ) {
|
|
dont_wait = false;
|
|
wait_end = current_time + 0.5;
|
|
} else {
|
|
wait_end = current_time + min_wait + sys.random( max_wait - min_wait );
|
|
}
|
|
while( sys.influenceActive() || !AI_MOVE_DONE || !facingIdeal() || ( current_time < wait_end ) ) {
|
|
if ( AI_ENEMY_IN_FOV ) {
|
|
lookAtEnemy( 1 );
|
|
}
|
|
// exit out if we can do a melee attack or enemy has left the combat cone
|
|
if ( testMeleeAttack() ) {
|
|
exit = true;
|
|
break;
|
|
}
|
|
if ( !node || !enemyInCombatCone( node, false ) ) {
|
|
exit = true;
|
|
break;
|
|
}
|
|
waitFrame();
|
|
current_time = sys.getTime();
|
|
}
|
|
|
|
if ( exit ) {
|
|
break;
|
|
}
|
|
|
|
// do our attack
|
|
allowMovement( true );
|
|
playCustomAnim( "out", 4, 0 );
|
|
while( !animDone( ANIMCHANNEL_TORSO, 0 ) ) {
|
|
if ( AI_ENEMY_IN_FOV ) {
|
|
lookAtEnemy( 1 );
|
|
}
|
|
if ( testMeleeAttack() ) {
|
|
exit = true;
|
|
break;
|
|
}
|
|
if ( !node || !enemyInCombatCone( node, true ) ) {
|
|
exit = true;
|
|
break;
|
|
}
|
|
waitFrame();
|
|
}
|
|
|
|
if ( exit ) {
|
|
break;
|
|
}
|
|
|
|
AI_DAMAGE = false;
|
|
allowMovement( false );
|
|
count = int( sys.random( num_shots ) ) + 1;
|
|
while( !AI_DAMAGE && ( count > 0 ) ) {
|
|
if ( sys.influenceActive() ) {
|
|
break;
|
|
}
|
|
playCustomAnim( "fire", 0, 0 );
|
|
while( !animDone( ANIMCHANNEL_TORSO, 0 ) ) {
|
|
if ( sys.influenceActive() ) {
|
|
break;
|
|
}
|
|
if ( AI_ENEMY_IN_FOV ) {
|
|
lookAtEnemy( 1 );
|
|
}
|
|
if ( testMeleeAttack() ) {
|
|
exit = true;
|
|
break;
|
|
}
|
|
if ( !node || !enemyInCombatCone( node, true ) ) {
|
|
exit = true;
|
|
break;
|
|
}
|
|
waitFrame();
|
|
}
|
|
attack = true;
|
|
|
|
if ( testMeleeAttack() ) {
|
|
exit = true;
|
|
break;
|
|
}
|
|
if ( !node || !enemyInCombatCone( node, true ) ) {
|
|
exit = true;
|
|
break;
|
|
}
|
|
count--;
|
|
}
|
|
AI_DAMAGE = false;
|
|
|
|
if ( exit ) {
|
|
break;
|
|
}
|
|
|
|
allowMovement( true );
|
|
playCustomAnim( "in", 0, 4 );
|
|
while( !animDone( ANIMCHANNEL_TORSO, 4 ) ) {
|
|
if ( AI_ENEMY_IN_FOV ) {
|
|
lookAtEnemy( 1 );
|
|
}
|
|
if ( testMeleeAttack() ) {
|
|
exit = true;
|
|
break;
|
|
}
|
|
if ( !node || !enemyInCombatCone( node, true ) ) {
|
|
exit = true;
|
|
break;
|
|
}
|
|
waitFrame();
|
|
}
|
|
|
|
if ( exit ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !( !node ) && attack ) {
|
|
// mark the node as used in case it's single use
|
|
node.markUsed();
|
|
}
|
|
|
|
allowMovement( true );
|
|
setAnimPrefix( "" );
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 );
|
|
animState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::combat_ainode
|
|
=====================
|
|
*/
|
|
void monster_base::combat_ainode( entity node ) {
|
|
if ( node.getKey( "classname" ) == "ai_attackcone_turret" ) {
|
|
combat_turret_node( node );
|
|
} else {
|
|
combat_attack_cone( node );
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::combat_lost
|
|
=====================
|
|
*/
|
|
void monster_base::combat_lost() {
|
|
if ( !ignore_lostcombat ) {
|
|
setState( "state_LostCombat" );
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::state_LostCombat
|
|
=====================
|
|
*/
|
|
void monster_base::state_LostCombat() {
|
|
monster_base_lost_combat();
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::monster_base_lost_combat
|
|
=====================
|
|
*/
|
|
void monster_base::monster_base_lost_combat() {
|
|
entity node;
|
|
vector ang;
|
|
float dist;
|
|
float yaw;
|
|
float attack_flags;
|
|
entity possibleEnemy;
|
|
float allow_attack;
|
|
float lost_time;
|
|
|
|
lost_time = sys.getTime() + sys.getFrameTime();
|
|
allow_attack = sys.getTime() + 4;
|
|
|
|
node = getClosestHiddenTarget( "ai_lostcombat" );
|
|
if ( node ) {
|
|
dist = distanceTo( node );
|
|
if ( dist < 40 ) {
|
|
// fixes infinite loops when close to lost combat node
|
|
waitFrame();
|
|
} else {
|
|
run = true;
|
|
moveToEntity( node );
|
|
while( !AI_MOVE_DONE ) {
|
|
if ( sys.influenceActive() ) {
|
|
break;
|
|
}
|
|
possibleEnemy = heardSound( true );
|
|
if ( possibleEnemy ) {
|
|
if ( canReachEntity( possibleEnemy ) ) {
|
|
setEnemy( possibleEnemy );
|
|
break;
|
|
}
|
|
}
|
|
if ( canReachEnemy() ) {
|
|
if ( AI_ENEMY_IN_FOV || AI_PAIN ) {
|
|
break;
|
|
}
|
|
}
|
|
if ( check_blocked() ) {
|
|
break;
|
|
}
|
|
waitFrame();
|
|
|
|
// allow attacks when enemy is outside of fov
|
|
if ( sys.getTime() > allow_attack ) {
|
|
AI_ENEMY_IN_FOV = AI_ENEMY_VISIBLE;
|
|
}
|
|
attack_flags = check_attacks();
|
|
if ( attack_flags ) {
|
|
do_attack( attack_flags );
|
|
setState( "state_Combat" );
|
|
}
|
|
}
|
|
ang = node.getAngles();
|
|
turnTo( ang_y );
|
|
while( !AI_MOVE_DONE ) {
|
|
if ( canReachEnemy() ) {
|
|
if ( heardSound( true ) ) {
|
|
break;
|
|
}
|
|
if ( AI_ENEMY_IN_FOV || AI_PAIN ) {
|
|
break;
|
|
}
|
|
}
|
|
waitFrame();
|
|
}
|
|
}
|
|
} else {
|
|
run = true;
|
|
moveToCover();
|
|
if ( AI_DEST_UNREACHABLE ) {
|
|
combat_wander();
|
|
}
|
|
|
|
// if we're not already in cover
|
|
if ( !AI_MOVE_DONE ) {
|
|
while( !AI_MOVE_DONE ) {
|
|
if ( sys.influenceActive() ) {
|
|
break;
|
|
}
|
|
// allow attacks when enemy is outside of fov
|
|
if ( sys.getTime() > allow_attack ) {
|
|
AI_ENEMY_IN_FOV = AI_ENEMY_VISIBLE;
|
|
}
|
|
attack_flags = check_attacks();
|
|
if ( attack_flags ) {
|
|
do_attack( attack_flags );
|
|
setState( "state_Combat" );
|
|
}
|
|
|
|
possibleEnemy = heardSound( true );
|
|
if ( possibleEnemy ) {
|
|
if ( canReachEntity( possibleEnemy ) ) {
|
|
setEnemy( possibleEnemy );
|
|
break;
|
|
}
|
|
}
|
|
if ( canReachEnemy() ) {
|
|
if ( AI_ENEMY_IN_FOV || AI_PAIN ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( check_blocked() ) {
|
|
break;
|
|
}
|
|
waitFrame();
|
|
}
|
|
|
|
|
|
if ( !sys.influenceActive() ) {
|
|
if ( AI_ENEMY_VISIBLE ) {
|
|
faceEnemy();
|
|
} else if ( AI_MOVE_DONE ) {
|
|
// turn around to face the way we came
|
|
yaw = getCurrentYaw();
|
|
turnTo( yaw + 180 );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// wait at least 1 frame to avoid loops
|
|
waitFrame();
|
|
|
|
if ( lost_time >= sys.getTime() ) {
|
|
combat_wander();
|
|
}
|
|
|
|
if ( AI_ENEMY_VISIBLE || sys.influenceActive() ) {
|
|
setState( "state_Combat" );
|
|
} else {
|
|
clearEnemy();
|
|
setState( "state_Idle" );
|
|
}
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::combat_wander
|
|
=====================
|
|
*/
|
|
void monster_base::combat_wander() {
|
|
float endtime;
|
|
float mintime;
|
|
|
|
mintime = sys.getTime() + 0.2;
|
|
endtime = sys.getTime() + 3;
|
|
wander();
|
|
while( sys.getTime() < endtime ) {
|
|
if ( sys.influenceActive() ) {
|
|
break;
|
|
}
|
|
if ( check_attacks() ) {
|
|
break;
|
|
}
|
|
if ( sys.getTime() > mintime ) {
|
|
if ( canReachEnemy() ) {
|
|
if ( heardSound( true ) ) {
|
|
break;
|
|
}
|
|
if ( AI_ENEMY_IN_FOV || AI_PAIN ) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
waitFrame();
|
|
}
|
|
|
|
// wait at least 1 frame to avoid loops
|
|
waitFrame();
|
|
setState( "state_Combat" );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::state_FollowAlternatePath
|
|
=====================
|
|
*/
|
|
void monster_base::state_FollowAlternatePath() {
|
|
if ( inAnimState( ANIMCHANNEL_TORSO, "Torso_CustomCycle" ) || inAnimState( ANIMCHANNEL_TORSO, "Torso_CustomAnim" ) ) {
|
|
animState( ANIMCHANNEL_TORSO, "Torso_Idle", 8 );
|
|
}
|
|
|
|
ignoreEnemies = true;
|
|
idle_followPathEntities( current_path );
|
|
ignoreEnemies = false;
|
|
setState( "state_Idle" );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::follow_alternate_path1
|
|
=====================
|
|
*/
|
|
void monster_base::follow_alternate_path1() {
|
|
current_path = getEntityKey( "alt_path1" );
|
|
setState( "state_FollowAlternatePath" );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::follow_alternate_path2
|
|
=====================
|
|
*/
|
|
void monster_base::follow_alternate_path2() {
|
|
current_path = getEntityKey( "alt_path2" );
|
|
setState( "state_FollowAlternatePath" );
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
monster_base::follow_alternate_path3
|
|
=====================
|
|
*/
|
|
void monster_base::follow_alternate_path3() {
|
|
current_path = getEntityKey( "alt_path2" );
|
|
setState( "state_FollowAlternatePath" );
|
|
}
|