//----------------------------------------------------------------------------- // // $Logfile:: /Quake 2 Engine/Sin/code/game/actor.cpp $ // $Revision:: 233 $ // $Author:: Markd $ // $Date:: 2/08/99 5:12p $ // // Copyright (C) 1998 by Ritual Entertainment, Inc. // All rights reserved. // // This source may not be distributed and/or modified without // expressly written permission by Ritual Entertainment, Inc. // // $Log:: /Quake 2 Engine/Sin/code/game/actor.cpp $ // // 233 2/08/99 5:12p Markd // fixed 2015 actor problem // // 232 1/29/99 6:14p Jimdose // added 2015 changes // // 231 1/27/99 10:02p Markd // Merged 2015 source into main code base // // 230 11/16/98 8:50p Markd // increased chat times // // 229 11/16/98 7:28p Markd // bullet proofed TargetEnemies so that dead bodies only spoke their secret // once and then cleared who they were upset at, much like a soul being // released from a dead body ;) // // 228 11/15/98 7:58p Jimdose // Actors now exclusively use flags for movement types // Made eyeposition max out at maxs.z // Swim monsters will no longer leave water // Air monsters will no longer enter water // // 227 11/15/98 2:58a Markd // scale pain threshold by skill a bit less than what it is right now // // 226 11/15/98 1:24a Jimdose // monsters don't play snd_sightenemy now unless DoAction( "sightenemy" ) // succeeds. // // 225 11/14/98 6:41p Jimdose // guys no longer drown in moving water after the water has moved // // 224 11/13/98 11:55p Markd // bodyimpact sound to goryimpact sound // // 223 11/13/98 11:00p Jimdose // Took EV_CONSOLE off of fov // // 222 11/13/98 10:02p Jimdose // bodies no longer float when they die in water // guys will allow themselves to fall when they are standing on other setients // // 221 11/13/98 4:04p Markd // increased min melee range // // 220 11/12/98 11:30p Jimdose // Increased melee kick // // 219 11/10/98 5:47p Jimdose // Made CanSee non-inline because it caused link problems // // 218 11/09/98 6:23p Markd // Added turnspeed event // // 217 11/08/98 7:06p Markd // Put in nochatter, made mutants not Like each other, added seenEnemy to // actorinfo // // 216 11/07/98 10:16p Jimdose // actors no longer get mad at scriptobjects that hurt them // // 215 11/07/98 3:34p Markd // Added nodeathfade command // // 214 11/06/98 8:25p Jimdose // made visiondistance adjustable from script // // 213 11/03/98 10:02p Aldie // Drop inventory when being tesselated // // 212 10/27/98 9:06p Markd // tweaked enemy aiming // // 211 10/27/98 7:52p Jimdose // added IfEnemyWithinEvent // made guys not lean if they're not moving very far // // 210 10/27/98 3:46p Markd // Changed skill level on actors // // 209 10/27/98 3:51a Jimdose // added kick to MeleeEvent // // 208 10/27/98 1:50a Markd // Made JumpTo take in account water level // // 207 10/26/98 9:53p Markd // Moved GotKill around, added gibfest sound // // 206 10/26/98 9:04p Markd // Changed jumpto behavior // // 205 10/26/98 8:22p Markd // Put CheckWater in Killed function // // 204 10/26/98 5:51p Jimdose // actor saves off spawn args before processing init commands // // 203 10/26/98 2:19p Markd // Added last_jump_time // // 202 10/26/98 1:20p Markd // made guys be BestTargetable after having an enemy already // // 201 10/26/98 12:13p Markd // fixed bug with blood preventing parent mode melee attacks // // 200 10/26/98 3:59a Jimdose // made Trymove not return STEPMOVE_STUCK when opening doors // // 199 10/26/98 1:53a Jimdose // TryMove was still using origin insteard of worldorigin, so the M_CheckBottom // call was always checking where the monster currently was, and not where he // was going to. // // 198 10/25/98 11:53p Aldie // Another dead check to avoid bodies that are in the air not going dead // // 197 10/25/98 4:43a Markd // Added animal event // // 196 10/25/98 4:28a Jimdose // Made opendoor a forced action // made CanShootFrom do extra tests to see if it should shoot damagable objects // // 195 10/25/98 1:52a Aldie // Changed gibanim functionality to be location based // // 194 10/24/98 3:10p Markd // Put in eyeoffset command // // 193 10/24/98 3:26a Markd // Put in more debug info for actors // // 192 10/24/98 12:42a Markd // changed origins to worldorigins where appropriate // // 191 10/23/98 6:46p Aldie // Turn off shadows for death and do deathgib anim on location // // 190 10/22/98 9:28p Aldie // Added ability for deathgib animation // // 189 10/22/98 3:05p Markd // fixed movespeed on actors // // 188 10/22/98 1:40a Markd // Added FL_STEALTH ability // // 187 10/21/98 11:15p Markd // Put in FL_CLOAK ability for targetting from enemies // // 186 10/21/98 6:43p Markd // made eyeposition the top of their bounding box // // 185 10/21/98 3:37p Markd // put in different enemy alignment and put in support for skins // // 184 10/21/98 2:13a Markd // Fixed jumping stuff // // 183 10/20/98 11:29p Markd // Made CanShootFrom aim at the chest rather than the centroid // // 182 10/20/98 8:05p Aldie // Added a DoGib function to ease readability // // 181 10/20/98 2:37a Markd // Added additional JumpTo methods // // 180 10/20/98 12:43a Markd // Put in scale for health, setsize, melee_range and melee_damage // // 179 10/19/98 11:53p Aldie // Moved the solid_trigger setting to the setup function of touchfields // // 178 10/19/98 11:49p Aldie // Fixed a bug where touch field was SOLID_BBOX. Now it is forced to // SOLID_TRIGGER. // // 177 10/19/98 9:55p Markd // Made it so "state" event does a ProcessScript // // 176 10/19/98 8:58p Aldie // Fix dead check for bodies in water // // 175 10/19/98 2:58a Markd // Fixed CanShootFrom and Jumping // // 174 10/19/98 2:41a Markd // made MakeEnemy not target the world // // 173 10/19/98 2:41a Aldie // Removed MOVETYPE_TOSS when actors die // // 172 10/19/98 12:04a Jimdose // made all code use fast checks for inheritance (no text lookups when // possible) // isSubclassOf no longer requires ::_classinfo() // // 171 10/18/98 6:43p Markd // If no melee attack, make weapon not droppable // // 170 10/18/98 12:34a Markd // Scaled shots_per_attack based on skill level // // 169 10/18/98 12:01a Markd // Fixed AttackEntity and IfCanShoot // // 168 10/17/98 11:00p Markd // Added IfCanShoot and not gettine mad at non-damageable things // // 167 10/17/98 9:41p Markd // Made InvestigateSound not set nextsoundtime unless the Action responds // // 166 10/17/98 7:34p Jimdose // deleted trig when actor deleted // // 165 10/17/98 4:59p Markd // Added attackmode event // // 164 10/17/98 3:31p Markd // Made firing more accurate over great distances // // 163 10/17/98 12:34a Markd // Initialized chattime to random( 20 ) // // 162 10/16/98 7:18p Markd // Added a bunch of new commands, and added interfaces to the editor // // 161 10/16/98 3:11a Aldie // Double the armor when actors die // // 160 10/16/98 1:41a Jimdose // Added goto command for actors. This helps the all too common mistake of // typing "local.self goto label" // // 159 10/15/98 9:32p Markd // Made inanimate objects not move, not bleed, and not gib // // 158 10/14/98 10:54p Jimdose // Made Hates check for a null entity // Added null check to investigate functions // // 157 10/14/98 9:00p Markd // Fixed JumpTo command, speed wise // // 156 10/14/98 5:20p Markd // Added jumpto event and jumpto function // // 155 10/14/98 2:17a Markd // Added in ifcanmeleeattack event // // 154 10/14/98 1:27a Markd // Added Has_weapon support // // 153 10/14/98 12:11a Markd // Put extra reach into melee event // // 152 10/13/98 11:03p Markd // Put scale in on all Anim_delta calls // // 151 10/13/98 7:34p Markd // made gib code scalable // // 150 10/13/98 12:26a Markd // Added support for flying creatures, also made it so that CanShootFrom fired // from the centroid, and not from the gun position // // 149 10/11/98 8:17p Markd // Working on nautic fixes // // 148 10/11/98 5:20p Markd // Put in default pain_thresholds // // 147 10/11/98 5:00p Markd // Fixed water drowning, changed ProcessCommands so that it doesn't overwrite // the the "thread" variable // // 146 10/11/98 12:32a Markd // Added Flight ability // // 145 10/10/98 9:12p Markd // Fixed Chatter using an alias for SoundTimeLength // // 144 10/10/98 7:59p Markd // Put in painthreshold and snd_pain_taunt // // 143 10/10/98 5:01p Markd // Changed trace masks to edict->clipmasks // // 142 10/10/98 3:27p Markd // Made EndBehavior always notify the actor script // // 141 10/10/98 1:24a Jimdose // Did some bulletproofing against possible NULL actorthread // Prethink kills actors with NULL threads // added SetVariable for setting thread variables safely // // 140 10/09/98 5:24p Markd // Fixed inanimate actor animations // // 139 10/09/98 4:59p Jimdose // Working on savegames // // 138 10/07/98 11:32a Markd // Made DamageThresholds targetable by actors // // 137 10/06/98 5:25p Markd // Made MakeEnemy and DoAction take the force parameter // // 136 10/06/98 12:14p Markd // Added ForceAction // // 135 10/05/98 10:33p Markd // Added canStrafe and noStrafe, also added MinimumAttackRange // // 134 10/05/98 2:57p Markd // Changed Chatter settings // // 133 10/04/98 10:36p Markd // Put in attack_finished // // 132 10/04/98 8:45p Aldie // Gib functions // // 131 10/04/98 2:49p Markd // Fixed swimming and forward speed stuff // // 130 10/03/98 7:15p Markd // got swimming characters working, in the process re-wrote a lot of the combat // stuff // // 129 10/01/98 8:00p Markd // Added melee capabilities // // 128 10/01/98 4:13p Jimdose // Fixed some problems with CanShootFrom that prevented guys from being able to // shoot the player when he's on ledges with railings // // 127 9/24/98 7:26p Markd // made inanimate objects not set off triggers // // 126 9/24/98 7:10p Jimdose // Added MyGunAngles // // 125 9/24/98 12:42a Jimdose // attempting to fix dead guys hanging in air // // 124 9/22/98 10:19p Jimdose // Initialized lastmove // Fixed InFov // Set fovdot in Fov event // // 123 9/22/98 5:19p Markd // Put in new consolidated gib function // // 122 9/22/98 5:10p Jimdose // Fixed inanimate objects // // 121 9/22/98 3:21p Markd // put in parentmode lockout for blood and gibs // // 120 9/22/98 6:27a Jimdose // Fixed bug when setting "attackmode" // // 119 9/22/98 5:37a Jimdose // Made attackmode only set once when thread is created // // 118 9/22/98 5:31a Jimdose // Added attackmode // // 117 9/22/98 1:58a Jimdose // Added lastmove and forwardspeed // Regrouped functions by functionality // Removed reaction table stuff // // 116 9/19/98 7:49p Jimdose // Fixed uninitialized pointer in ShowInfo and added more data // // 115 9/19/98 6:16p Jimdose // Added AttackPlayer // AttackEntity and AttackPlayer makes the actor attack the entity immediately // by forcing a call to SightEnemy // // 114 9/19/98 4:34p Jimdose // changed gi.dprintf's to gi.printf's in ShowInfo // // 113 9/18/98 10:55p Jimdose // Added inanimate actor type // started work on swimming monsters // // 112 9/16/98 8:38p Jimdose // Made InFOV only test the fov in 2 dimensions // // 111 9/14/98 5:26p Jimdose // Made EndBehavior set script variables // Added some checks for NULL when getting nearest nodes // SeenEnemy is now cleared when currentEnemy is killed // CanShootFrom no longer assumes ScriptSlaves are shootable, but does shoot // ScriptModel. Gotta find some solution that allows the level designers to // specify whats shootable by the AI. // // 110 9/08/98 9:48p Jimdose // Enabled multi-size bounding box path code // // 109 9/03/98 9:05p Jimdose // Guys will now shoot through scriptslaves and objects with the assumption // that he can destroy it // // 108 8/31/98 7:46p Jimdose // Spread out targeting a bit // // 107 8/31/98 5:42p Jimdose // Added checks for NULL pointer in MakeEnemy and AttackEnemy // // 106 8/29/98 9:39p Jimdose // Added call info to G_Trace // // 105 8/29/98 5:26p Markd // added specialfx, replaced misc with specialfx where appropriate // // 104 8/29/98 1:11p Markd // Made it so M_CheckBottom only checks if groundentity is not world // // 103 8/27/98 9:00p Jimdose // Made centroid into a variable // Made guys target enemies less often // MakeEnemy checks if ent is the world (when a target is non-valid, the ent // may be the world) // // 102 8/27/98 6:00p Aldie // Drop inventory items if the actors are gibbed. // // 101 8/27/98 4:49p Jimdose // Fixed bug with hidden actors being shown again on startup // Made actors not target hidden enemies, or target while hidden (for // cinematics) // // 100 8/27/98 4:27p Markd // detach children // // 99 8/26/98 11:13p Jimdose // Began strafe support // // 98 8/26/98 6:45p Jimdose // Made DefineStateEvent prepend the value in ai_actorscript // // 97 8/24/98 6:55p Jimdose // Added crouching pain and death animations // Added path // // 96 8/20/98 8:38p Jimdose // Actors now share enemy info between each other on sight // CanShoot checks the head as well as the centroid // InFOV now uses an eliptical cone for testing vision (better vertical // peripheral vision). // // 95 8/19/98 4:57p Jimdose // CanShootFrom now takes min and max weapon firing distances into account // // 94 8/19/98 3:59p Jimdose // Added IfNearEvent and CopyStateEvent // // 93 8/19/98 2:30p Jimdose // Can no longer alias "alert" to "idle" so I've added a hasalert variable // // 92 8/18/98 11:57p Aldie // Fixed init variable warning // // 91 8/18/98 11:08p Markd // Added new Alias System // // 90 8/18/98 9:59p Jimdose // Made actors only react to some sounds when they're caused by an enemy. // Hmmm, would be nice if they checked their "memory" to see if they expect // that kind of sound from there. // // 89 8/15/98 5:29p Jimdose // Fixed up the thread startup code. No longer need to wait .1 in the script // before sending commands. Actor script is no longer run twice (because of // the delayed model init commands. // Got rid of !init label in scripts. Now starts at beginning unless a label // is given. // CanShoot checks vision distance as an early exit. // // 88 8/14/98 11:34p Jimdose // Got rid of "runtoarea" state. Attack now just lets MakeEnemy call // sightenemy. Hmmm...probably should change to make sure that actor // immediatly attacks the specified entity. // don't set currentEnemy until sightenemy is responded to. Gets rid of a lot // of brain dead situations. // // 87 8/14/98 6:24p Jimdose // Got rid of decelleration for steering // // 86 8/13/98 7:30p Jimdose // actorThread is now persistant. Threads don't get killed unless actor dies // or is removed. // // 85 8/08/98 2:05p Markd // Added entity who got the event to ForwardBehaviorEvent // // 84 8/07/98 7:57p Markd // Added ForwardToBehavior event // // 83 8/07/98 6:01p Jimdose // Rewrite AimAndShoot // // 82 8/06/98 6:59p Jimdose // Added IfEnemyVisible // // 81 8/05/98 7:18p Jimdose // Added definestate, ignoresounds, respondtosounds, respondtoall // // 80 8/03/98 6:49p Jimdose // fixed thread bug // // 79 7/29/98 6:32p Jimdose // Added MonsterStart // // 78 7/29/98 6:27p Jimdose // Removed monster.h // // 77 7/29/98 2:30p Aldie // Changed health to a float // // 76 7/26/98 3:40p Jimdose // Fixed guys in water // guys no longer open locked doors // // 75 7/26/98 11:43a Jimdose // Added EnemyCanSeeMeFrom // // 74 7/26/98 6:44a Jimdose // Overrode the setsize event so that def files don't change the hardcoded size // (need to fix these damn paths one day). // // 73 7/26/98 3:48a Jimdose // Modified aim based on skill // // 72 7/25/98 4:39p Jimdose // Guys shoot at vehicles and shoot out windows // // 71 7/25/98 2:07a Jimdose // Fixed bug with trig being delete without being NULLed out when gibbing // Guys now attack any enemy // // 70 7/24/98 9:59p Aldie // Gib stuff // // 69 7/24/98 6:16p Aldie // Check for inventory items before removing dead guys // // 68 7/24/98 4:14p Aldie // Cancel useless checks // // 67 7/24/98 3:46p Aldie // Increased gibs // // 66 7/23/98 10:16p Jimdose // Removed ignoreenemies stuff // Fixed a bunch of anim screwups when actor is killed // fixed "big gib" bug // // 65 7/23/98 6:55p Aldie // Fun with gibs. // // 64 7/23/98 6:19p Jimdose // ignoreEnemies was being set when character was targeted // // 63 7/22/98 7:02p Aldie // Added ability to remove useless bodies. // // 62 7/22/98 2:32p Jimdose // fixed bug where guys wouldn't sight the enemy if the went into pain // // 61 7/21/98 9:34p Jimdose // Chatter now checks if the alias exists before playing the sound, thereby not // generating warnings of missing sounds. // // 60 7/19/98 9:20p Jimdose // Improved support for overriding the default script // // 59 7/16/98 6:21p Jimdose // Fixed crash when actorthread was null // // 58 7/14/98 11:50p Jimdose // Made PopState return whether a new state was popped off the stack // StateDoneEvent now kills the calling thread if no previous state exists on // the stack // DoAction creates a new thread if no actor thread exists // // 57 7/14/98 6:57p Aldie // Moved setsize before setmodel // // 56 7/13/98 4:59p Aldie // Added dead player bodies with gibbing // // 55 7/09/98 10:40p Aldie // Moved bodyparts to game // // 54 7/08/98 8:59p Jimdose // Moved opening door to script // // 53 7/08/98 1:00p Jimdose // added crouchsize and standsize // // 52 7/07/98 11:37p Jimdose // Replaced priority-based system with full script-based state system // // 51 7/06/98 1:06p Jimdose // working on ai // // 50 6/30/98 6:47p Aldie // Added Gib Event // // 49 6/30/98 6:00p Jimdose // Added more states and state detection code. // Added range detection // added health level detection // // 48 6/25/98 8:46p Markd // Removed rotating doors // // 47 6/25/98 8:29p Aldie // Updated killed and death // // 46 6/25/98 8:10p Jimdose // Disabled some prints // // 45 6/25/98 8:10p Jimdose // Using global actor script // // 44 6/24/98 4:26p Jimdose // Fixed bugs in TurnTo // // 43 6/23/98 5:59p Aldie // Fixed dropWeapon stuff. // // 42 6/20/98 7:47p Markd // Added location based pain and death support // // 41 6/19/98 9:27p Jimdose // Added GetGunOrientation // // 40 6/19/98 4:44p Jimdose // Added util functions InFOV, etc. // // 39 6/18/98 8:45p Jimdose // Removed string based SetBehavior and PushState functions // Added source info to events // // 38 6/18/98 2:00p Markd // rewrote tesselation code // // 37 6/17/98 10:00p Jimdose // Improved door opening code // Moved phyics move into class // // 36 6/17/98 3:15p Markd // fixed arg issue // // 35 6/17/98 3:03p Markd // Changed NumArgs back to previous behavior // // 34 6/17/98 1:19a Jimdose // Moved setOwner to Item. // Added EV_Item_Pickup // // 33 6/15/98 10:46p Jimdose // Made neutral allias become enemies with pain // shurnk bounding box temporarily // // 32 6/13/98 8:25p Jimdose // Added leaning into turns (alright already Tom!) // Added enemy visibility tests for FindCover // // 31 6/11/98 12:44a Jimdose // behaviors now get info from the script at startup // // 30 6/10/98 10:24p Jimdose // added priority based state system // // 29 6/09/98 4:18p Jimdose // worked on ai // // 28 6/04/98 10:48p Jimdose // Fixed a bunch of things that got broken just in time for E3. Paths and // scripting actually work now. // // 27 6/03/98 5:43p Jimdose // Fixed spelling of behavior. :) // // 26 6/03/98 4:36p Markd // Removed gun stuff from entity, it is now sent over as a separate object // // 25 6/02/98 7:46p Markd // Took out currentweapon assignments for client-side stuff // // 24 5/28/98 6:01p Markd // When actors die, they kill their dynamic light // // 23 5/28/98 2:11p Markd // Made enemies drop weapons and only fire if their weapons are ready // // 22 5/28/98 1:50p Markd // made actors drop weapons // // 21 5/27/98 4:56p Markd // Only gib when inflictor is rocket // // 20 5/27/98 12:04p Jimdose // fixed null pointer prob // // 19 5/27/98 11:59a Jimdose // Gibbing is harder // // 18 5/27/98 11:50a Jimdose // Added killtarget and target to killed // // 17 5/27/98 7:15a Jimdose // took out dprintf // // 16 5/27/98 7:12a Jimdose // ai ai ai // // 15 5/27/98 6:39a Jimdose // working on ai // // 14 5/27/98 5:27a Aldie // Added gibs // // 13 5/27/98 5:11a Jimdose // working on ai // // 12 5/25/98 6:46p Jimdose // Made animateframe, prethink and posthink into functions built into the base // entity class // // 11 5/25/98 5:32p Jimdose // Pathnodes are no longer a subclass of Entity. This was done to save on // edicts // // 10 5/25/98 1:06a Jimdose // Added chatter // // 9 5/24/98 8:46p Jimdose // Made a lot of functions more str-friendly. // Got rid of a lot of char * based strings // Cleaned up get spawn arg functions and sound functions // sound functions now use consistant syntax // // 8 5/24/98 1:02a Jimdose // Added monster hearing // // 7 5/23/98 6:27p Jimdose // Improved steering and opening doors // // 6 5/22/98 9:44p Jimdose // Disabled debug lines when checking to shoot // // 5 5/22/98 9:38p Jimdose // Commented out prints // // 4 5/22/98 9:36p Jimdose // AI Scripting works again (somewhat) // // 3 5/20/98 6:36p Jimdose // Rewrite // // 2 5/18/98 8:15p Jimdose // Created file // // DESCRIPTION: // Base class for character AI. // #include "g_local.h" #include "actor.h" #include "behavior.h" #include "scriptmaster.h" #include "doors.h" #include "gibs.h" #include "vehicle.h" #include "misc.h" #include "specialfx.h" #include "object.h" #include "scriptslave.h" #include "explosion.h" #include "misc.h" //#define DEBUG_PRINT CLASS_DECLARATION( Class, ActorState, NULL ); ResponseDef ActorState::Responses[] = { { NULL, NULL } }; cvar_t *ai_actorscript; // used below for a slight movement tweak // added as a global here to prevent constant re-allocation const Vector movetweak = "0 0 2"; #define TURN_SPEED 30 CLASS_DECLARATION( Sentient, Actor, "monster_generic" ); Event EV_Actor_CrouchSize( "crouchsize" ); Event EV_Actor_Fov( "fov" ); Event EV_Actor_VisionDistance( "visiondistance" ); Event EV_Actor_Start( "start" ); Event EV_Actor_Dead( "dead" ); Event EV_Actor_ClearState( "clearstate" ); Event EV_Actor_SetState( "state" ); Event EV_Actor_SetBehavior( "behavior" ); Event EV_Actor_Friend( "friend" ); // Allied to player Event EV_Actor_Civilian( "civilian" ); // Nuetral to all, runs from danger Event EV_Actor_Enemy( "enemy" ); // Enemy to player Event EV_Actor_Monster( "monster" ); // Enemy to player, civilians, and inanimate objects Event EV_Actor_Animal( "animal" ); // neutral to all Event EV_Actor_Inanimate( "inanimate" ); // Inanimate objects Event EV_Actor_Swim( "swim" ); // specifies actor as being able to swim Event EV_Actor_Fly( "fly" ); // specifies actor as being able to swim Event EV_Actor_NotLand( "noland" ); // specifies actor as not being able to walk on land Event EV_Actor_TargetEnemies( "targetenemies" ); // Scripting Event EV_Actor_Script( "script" ); Event EV_Actor_Thread( "thread" ); Event EV_Actor_IfEnemyVisible( "ifenemyvisible" ); Event EV_Actor_IfNear( "ifnear" ); Event EV_Actor_ForwardSpeed( "forwardspeed" ); Event EV_Actor_Idle( "idle" ); Event EV_Actor_LookAt( "lookat" ); Event EV_Actor_TurnTo( "turnto" ); Event EV_Actor_FinishedBehavior( "finishedbehavior" ); Event EV_Actor_NotifyBehavior( "notifybehavior" ); Event EV_Actor_WalkTo( "walkto" ); Event EV_Actor_JumpTo( "jumpto" ); Event EV_Actor_RunTo( "runto" ); Event EV_Actor_Anim( "anim" ); Event EV_Actor_Attack( "attack" ); Event EV_Actor_AttackPlayer( "attackplayer" ); Event EV_Actor_ReserveNode( "reservenode" ); Event EV_Actor_ReleaseNode( "releasenode" ); Event EV_Actor_IfCanHideAt( "ifcanhideat" ); Event EV_Actor_IfCanStrafeAttack( "ifcanstrafeattack" ); Event EV_Actor_IfCanMeleeAttack( "ifcanmeleeattack" ); Event EV_Actor_IfCanShoot( "ifcanshoot" ); Event EV_Actor_IfEnemyWithin( "ifenemywithin" ); Event EV_Actor_IgnoreSounds( "ignoresounds" ); Event EV_Actor_RespondToSounds( "respondtosounds" ); Event EV_Actor_IgnoreAll( "ignoreall" ); Event EV_Actor_RespondToAll( "respondtoall" ); Event EV_Actor_RespondTo( "respondto" ); Event EV_Actor_Ignore( "ignore" ); Event EV_Actor_DefineState( "definestate" ); Event EV_Actor_CopyState( "copystate" ); Event EV_Actor_Remove( "remove_useless" ); Event EV_Actor_ForwardToBehavior( "forwardcommand" ); Event EV_Actor_Aim( "aim" ); Event EV_Actor_MeleeRange( "meleerange" ); Event EV_Actor_MeleeDamage( "meleedamage" ); Event EV_Actor_Melee( "melee" ); Event EV_Actor_AttackFinished( "attack_finished" ); Event EV_Actor_CanStrafe( "canstrafe" ); Event EV_Actor_NoStrafe( "nostrafe" ); Event EV_Actor_PainThreshold( "painthreshold" ); Event EV_Actor_SetKillThread( "killthread" ); Event EV_Actor_AttackRange( "attackrange" ); Event EV_Actor_ShotsPerAttack( "shotsperattack" ); Event EV_Actor_ClearEnemy( "clearenemy" ); Event EV_Actor_AttackMode( "attackmode" ); Event EV_Actor_EyePositionOffset( "eyeoffset" ); Event EV_Actor_NoDeathFade( "nodeathfade" ); Event EV_Actor_NoChatter( "nochatter" ); Event EV_Actor_TurnSpeed( "turnspeed" ); ResponseDef StateInfo::Responses[] = { { NULL, NULL } }; CLASS_DECLARATION( Class, StateInfo, NULL ); StateInfo::StateInfo() { ignore = true; } ResponseDef Actor::Responses[] = { { &EV_Activate, ( Response )Actor::ActivateEvent }, { &EV_Use, ( Response )Actor::UseEvent }, { &EV_Actor_Start, ( Response )Actor::Start }, { &EV_Pain, ( Response )Actor::Pain }, { &EV_Killed, ( Response )Actor::Killed }, { &EV_Actor_Dead, ( Response )Actor::Dead }, { &EV_Actor_TargetEnemies, ( Response )Actor::TargetEnemies }, { &EV_Actor_SetBehavior, ( Response )Actor::SetBehaviorEvent }, { &EV_Actor_ForwardSpeed, ( Response )Actor::ForwardSpeedEvent }, { &EV_Actor_CrouchSize, ( Response )Actor::CrouchSize }, { &EV_Actor_Fov, ( Response )Actor::SetFov }, { &EV_Actor_VisionDistance, ( Response )Actor::SetVisionDistance }, { &EV_Actor_Friend, ( Response )Actor::FriendEvent }, { &EV_Actor_Civilian, ( Response )Actor::CivilianEvent }, { &EV_Actor_Enemy, ( Response )Actor::EnemyEvent }, { &EV_Actor_Monster, ( Response )Actor::MonsterEvent }, { &EV_Actor_Animal, ( Response )Actor::AnimalEvent }, { &EV_Actor_Inanimate, ( Response )Actor::InanimateEvent }, { &EV_Actor_Swim, ( Response )Actor::SwimEvent }, { &EV_Actor_Fly, ( Response )Actor::FlyEvent }, { &EV_Actor_NotLand, ( Response )Actor::NotLandEvent }, { &EV_Gib, ( Response )Actor::GibEvent }, // Scripting { &EV_Actor_DefineState, ( Response )Actor::DefineStateEvent }, { &EV_Actor_CopyState, ( Response )Actor::CopyStateEvent }, { &EV_Actor_IgnoreAll, ( Response )Actor::IgnoreAllEvent }, { &EV_Actor_RespondToAll, ( Response )Actor::RespondToAllEvent }, { &EV_Actor_RespondTo, ( Response )Actor::RespondToEvent }, { &EV_Actor_Ignore, ( Response )Actor::IgnoreEvent }, { &EV_Actor_IgnoreSounds, ( Response )Actor::IgnoreSoundsEvent }, { &EV_Actor_RespondToSounds, ( Response )Actor::RespondToSoundsEvent }, { &EV_Actor_ClearState, ( Response )Actor::ClearStateEvent }, { &EV_ScriptThread_End, ( Response )Actor::StateDoneEvent }, { &EV_Actor_SetState, ( Response )Actor::SetStateEvent }, { &EV_Actor_Script, ( Response )Actor::SetScript }, { &EV_Actor_Thread, ( Response )Actor::SetThread }, { &EV_Actor_IfEnemyVisible, ( Response )Actor::IfEnemyVisibleEvent }, { &EV_Actor_IfNear, ( Response )Actor::IfNearEvent }, { &EV_Actor_Idle, ( Response )Actor::IdleEvent }, { &EV_ProcessCommands, ( Response )Actor::StartMove }, { &EV_Actor_LookAt, ( Response )Actor::LookAt }, { &EV_Actor_TurnTo, ( Response )Actor::TurnToEvent }, { &EV_Actor_FinishedBehavior, ( Response )Actor::FinishedBehavior }, { &EV_Actor_NotifyBehavior, ( Response )Actor::NotifyBehavior }, { &EV_Actor_WalkTo, ( Response )Actor::WalkTo }, { &EV_Actor_JumpTo, ( Response )Actor::JumpToEvent }, { &EV_Actor_RunTo, ( Response )Actor::RunTo }, { &EV_Actor_Anim, ( Response )Actor::Anim }, { &EV_Actor_Attack, ( Response )Actor::AttackEntity }, { &EV_Actor_AttackPlayer, ( Response )Actor::AttackPlayer }, { &EV_Actor_Remove, ( Response )Actor::RemoveUselessBody }, { &EV_Actor_ForwardToBehavior,( Response )Actor::ForwardBehaviorEvent }, { &EV_Actor_ReserveNode, ( Response )Actor::ReserveNodeEvent }, { &EV_Actor_ReleaseNode, ( Response )Actor::ReleaseNodeEvent }, { &EV_Actor_IfCanHideAt, ( Response )Actor::IfCanHideAtEvent }, { &EV_Actor_IfCanStrafeAttack,( Response )Actor::IfCanStrafeAttackEvent }, { &EV_Actor_IfCanMeleeAttack, ( Response )Actor::IfCanMeleeAttackEvent }, { &EV_Actor_IfCanShoot, ( Response )Actor::IfCanShootEvent }, { &EV_Actor_IfEnemyWithin, ( Response )Actor::IfEnemyWithinEvent }, { &EV_HeardWeapon, ( Response )Actor::InvestigateWeaponSound }, { &EV_HeardMovement, ( Response )Actor::InvestigateMovementSound }, { &EV_HeardPain, ( Response )Actor::InvestigatePainSound }, { &EV_HeardDeath, ( Response )Actor::InvestigateDeathSound }, { &EV_HeardBreaking, ( Response )Actor::InvestigateBreakingSound }, { &EV_HeardDoor, ( Response )Actor::InvestigateDoorSound }, { &EV_HeardMutant, ( Response )Actor::InvestigateMutantSound }, { &EV_HeardVoice, ( Response )Actor::InvestigateVoiceSound }, { &EV_HeardMachine, ( Response )Actor::InvestigateMachineSound }, { &EV_HeardRadio, ( Response )Actor::InvestigateRadioSound }, { &EV_Actor_Aim, ( Response )Actor::SetAim }, { &EV_Actor_MeleeRange, ( Response )Actor::SetMeleeRange }, { &EV_Actor_MeleeDamage, ( Response )Actor::SetMeleeDamage }, { &EV_Actor_Melee, ( Response )Actor::MeleeEvent }, { &EV_Actor_AttackFinished, ( Response )Actor::AttackFinishedEvent }, { &EV_Actor_CanStrafe, ( Response )Actor::CanStrafeEvent }, { &EV_Actor_NoStrafe, ( Response )Actor::NoStrafeEvent }, { &EV_Actor_PainThreshold, ( Response )Actor::SetPainThresholdEvent }, { &EV_Actor_SetKillThread, ( Response )Actor::SetKillThreadEvent }, { &EV_Actor_AttackRange, ( Response )Actor::AttackRangeEvent }, { &EV_Actor_ShotsPerAttack, ( Response )Actor::ShotsPerAttackEvent }, { &EV_Actor_ClearEnemy, ( Response )Actor::ClearEnemyEvent }, { &EV_Actor_AttackMode, ( Response )Actor::AttackModeEvent }, { &EV_SetHealth, ( Response )Actor::SetHealth }, { &EV_Actor_EyePositionOffset,( Response )Actor::EyeOffset }, { &EV_Actor_NoDeathFade, ( Response )Actor::NoDeathFadeEvent }, { &EV_Actor_NoChatter, ( Response )Actor::NoChatterEvent }, { &EV_Actor_TurnSpeed, ( Response )Actor::TurnSpeedEvent }, { &EV_ScriptThread_Goto, ( Response )Actor::GotoEvent }, { NULL, NULL } }; //*********************************************************************************************** // // Initialization functions // //*********************************************************************************************** Actor::Actor() { const char *text; str script; const char * skinname; Event *ev; SpawnArgs args; // don't spawn monsters in deathmatch if ( deathmatch->value || nomonsters->value ) { PostEvent( EV_Remove, 0 ); return; } actortype = IS_ENEMY; setSolidType( SOLID_BBOX ); setMoveType( MOVETYPE_STEP ); health = G_GetFloatArg( "health", 100 ); max_health = health; takedamage = DAMAGE_AIM; mass = G_GetFloatArg( "mass", 200 ); deadflag = DEAD_NO; edict->clipmask = MASK_MONSTERSOLID; edict->svflags |= SVF_MONSTER; forwardspeed = 0; checkStrafe = true; // No longer used movement = AI_CANWALK; lastmove = STEPMOVE_OK; gunoffset = "0 0 44"; numonstack = 0; behavior = NULL; path = NULL; newanimnum = -1; newanim = ""; newanimevent = NULL; text = G_GetSpawnArg( "weapon" ); if ( text ) { giveWeapon( text ); } spawngroup = G_GetStringArg( "spawngroup" ); attackmode = G_GetIntArg( "attackmode", 0 ); attack_range = G_GetFloatArg( "attackrange", 8192 ); fov = 150; fovdot = cos( fov * 0.5 * M_PI / 180.0 ); vision_distance = G_GetFloatArg( "visiondistance", 1024 ); eyeoffset = "0 0 0"; eyeposition = "0 0 64"; hasalert = false; lastEnemy = NULL; enemyRange = RANGE_FAR; seenEnemy = false; nodeathfade = false; nochatter = false; turnspeed = 60; if ( !parentmode->value ) { flags |= FL_BLOOD; flags |= FL_DIE_GIBS; } // // don't talk all at once initially // chattime = G_Random( 20 ); nextsoundtime = 0; trig = NULL; deathgib = false; // set default crouchsize crouchsize_min = "-16 -16 0"; crouchsize_max = "16 16 32"; standsize_min = mins; standsize_max = maxs; // use a cvar to help with debugging ai_actorscript = gi.cvar( "ai_actorscript", "", 0 ); actorscript = G_GetStringArg( "script", "global/enemy.scr" ); actorstart = G_GetStringArg( "thread", "" ); kill_thread = G_GetStringArg( "killthread", "" ); // default melee characteristics melee_range = 100; melee_damage = 30; // default aim (normal) aim = G_GetFloatArg( "aim", 0 ); // default pain_threshold is 10 pain_threshold = G_GetFloatArg( "painthreshold", 10 * skill->value ); // default shots per attack is 5 + ( 2 * skill->level ) shots_per_attack = G_GetFloatArg( "shotsperattack", 3 + ( 2 * skill->value ) ); startpos = worldorigin; next_drown_time = 0; air_finished = level.time + 5; last_jump_time = 0; CheckWater(); setSize( "-16 -16 0", "16 16 76" ); //setModel( G_GetSpawnArg( "model", "grunt.def" ) ); showModel(); if ( !LoadingSavegame ) { // save off our spawn args args.SetArgs(); G_InitSpawnArguments(); // force the init commands to be processed so that we start the right actor script immediately CancelEventsOfType( EV_ProcessInitCommands ); ev = new Event( EV_ProcessInitCommands ); ev->AddInteger( edict->s.modelindex ); ProcessEvent( ev ); SetupThread(); if ( eyeposition.z > maxs.z ) { eyeposition.z = maxs.z; } // restore our args G_InitSpawnArguments(); args.RestoreArgs(); // wait until the script starts before thinking PostEvent( EV_Actor_Start, FRAMETIME ); } // // I put this here, so that the initcommands would already be processed // skinname = G_GetSpawnArg( "skin" ); if ( skinname && skinname[ 0 ] ) { int skinnum; skinnum = gi.Skin_NumForName( edict->s.modelindex, skinname ); if (skinnum >= 0) edict->s.skinnum = skinnum; } } Actor::~Actor() { int n; int i; if ( actorthread ) { actorthread->ProcessEvent( EV_ScriptThread_End ); actorthread = NULL; } // delete the old action/response list n = actionList.NumObjects(); for( i = n; i >= 1; i-- ) { delete actionList.ObjectAt( i ); } actionList.ClearObjectList(); if ( behavior ) { delete behavior; behavior = NULL; } if ( path ) { delete path; path = NULL; } if ( trig ) { delete trig; trig = NULL; } ClearStateStack(); } void Actor::Start ( Event *ev ) { MonsterStart *start; // This is only used for choosing delay times for targeting enemies static int actornum = 0; hasalert = ( gi.Anim_Random( edict->s.modelindex, "alert" ) != -1 ); start = MonsterStart::GetRandomSpot( spawngroup ); if ( start ) { setOrigin( start->worldorigin ); worldorigin.copyTo( edict->s.old_origin ); setAngles( start->worldangles ); if ( start->animname != "" ) { SetAnim( start->animname ); } } droptofloor( 16 ); flags |= FL_PRETHINK; // see if we have any melee attacks if ( HasAnim( "melee" ) ) { has_melee = true; } else { // // make sure we can't knock the weapon out of this characters hands // if ( currentWeapon ) { Event * ev; ev = new Event( EV_Weapon_NotDroppable ); currentWeapon->ProcessEvent( ev ); } has_melee = false; } // spread their targeting about a bit PostEvent( EV_Actor_TargetEnemies, ( actornum++ % 10 ) * FRAMETIME ); } //*********************************************************************************************** // // Vision functions // //*********************************************************************************************** range_t Actor::Range ( Entity *targ ) { float r; Vector delta; delta = centroid - targ->centroid; r = delta * delta; if ( r < 120 * 120 ) { return RANGE_MELEE; } if ( r < 500 * 500 ) { return RANGE_NEAR; } if ( r < 1000 * 1000 ) { return RANGE_MID; } return RANGE_FAR; } inline qboolean Actor::InFOV ( Vector pos ) { Vector delta; float dot; delta = pos - EyePosition(); if ( !delta.x && !delta.y ) { // special case for straight up and down return true; } // give better vertical vision delta.z = 0; delta.normalize(); dot = DotProduct( orientation[ 0 ], delta.vec3() ); return ( dot > fovdot ); } inline qboolean Actor::InFOV ( Entity *ent ) { return InFOV( ent->centroid ); } inline qboolean Actor::CanSeeFOV ( Entity *ent ) { return InFOV( ent ) && CanSeeFrom( worldorigin, ent ); } inline qboolean Actor::CanSeeFrom ( Vector pos, Entity *ent ) { trace_t trace; Vector p; p = ent->centroid; // Check if he's visible trace = G_Trace( pos + eyeposition, vec_zero, vec_zero, p, this, MASK_OPAQUE, "Actor::CanSeeFrom 1" ); if ( trace.fraction == 1.0 || trace.ent == ent->edict ) { return true; } // Check if his head is visible p.z = ent->absmax.z; trace = G_Trace( pos + eyeposition, vec_zero, vec_zero, p, this, MASK_OPAQUE, "Actor::CanSeeFrom 2" ); if ( trace.fraction == 1.0 || trace.ent == ent->edict ) { return true; } return false; } qboolean Actor::CanSee ( Entity *ent ) { return CanSeeFrom( worldorigin, ent ); } int Actor::EnemyCanSeeMeFrom ( Vector pos ) { Entity *ent; int i; int n; float rad; Vector d; Vector p1; Vector p2; int c; rad = max( size.x, size.y ) * 1.44 * 0.5; c = 0; n = enemyList.NumObjects(); for( i = 1; i <= n; i++ ) { ent = enemyList.ObjectAt( i ); if ( !ent || ent->deadflag || ( ent->flags & FL_NOTARGET ) ) { continue; } if ( WithinDistance( ent, vision_distance ) ) { // To check if we're visible, I create a plane that intersects the actor // and is perpendicular to the delta vector between the actor and his enemy. // I place four points on this plane that "frame" the actor and check if // the enemy can see any of those points. d = ent->centroid - pos; d.z = 0; d.normalize(); p1.x = -d.y; p1.y = d.x; p1 *= rad; p2 = p1; p1.z = mins.z; p2.z = maxs.z; if ( CanSeeFrom( pos + p1, ent ) ) { c++; } if ( CanSeeFrom( pos + p2, ent ) ) { c++; } p1.z = -p1.z; p2.z = -p2.z; if ( CanSeeFrom( pos - p1, ent ) ) { c++; } if ( CanSeeFrom( pos - p2, ent ) ) { c++; } } } return c; } qboolean Actor::CanSeeEnemyFrom ( Vector pos ) { Entity *ent; int i; int n; n = enemyList.NumObjects(); for( i = 1; i <= n; i++ ) { ent = enemyList.ObjectAt( i ); if ( !ent || ent->deadflag || ( ent->flags & FL_NOTARGET ) ) { continue; } if ( WithinDistance( ent, vision_distance ) && CanSeeFrom( pos, ent ) ) { return true; } } return false; } //*********************************************************************************************** // // Weapon functions // //*********************************************************************************************** qboolean Actor::WeaponReady ( void ) { if ( currentWeapon && currentWeapon->ReadyToFire() ) { return true; } else if ( !currentWeapon && has_melee ) { return true; } return false; } void Actor::Attack ( Event *ev ) { Vector delta; Vector ang; Vector ang2; if ( ( currentWeapon ) && currentWeapon->ReadyToFire() && currentWeapon->HasAmmo() ) { if ( currentEnemy ) { ang = angles; delta = currentEnemy->centroid - GunPosition(); ang2 = delta.toAngles(); ang2[ 0 ] = -ang2[ 0 ]; setAngles( ang2 ); currentWeapon->Fire(); setAngles( ang ); } else { currentWeapon->Fire(); } } } Vector Actor::GunPosition ( void ) { vec3_t trans[ 3 ]; vec3_t orient; int groupindex; int tri_num; Vector offset = vec_zero; Vector result; // get the gun position of the actor if ( !gi.GetBoneInfo( edict->s.modelindex, "gun", &groupindex, &tri_num, orient ) ) { // Gun doesn't have a barrel, just return the default return worldorigin + gunoffset; } gi.GetBoneTransform( edict->s.modelindex, groupindex, tri_num, orient, edict->s.anim, edict->s.frame, edict->s.scale, trans, offset.vec3() ); MatrixTransformVector( offset.vec3(), orientation, result.vec3() ); result += worldorigin; return result; } inline Vector Actor::MyGunAngles ( Vector muzzlepos, qboolean firing ) { Vector ang; Vector dir; if ( currentEnemy && firing ) { dir = currentEnemy->centroid - muzzlepos; dir.z += ( currentEnemy->absmax.z - currentEnemy->centroid.z ) * 0.75f; dir.normalize(); ang = dir.toAngles(); ang.x = -ang.x; } else { ang.x = -worldangles.x; ang.y = worldangles.y; ang.z = worldangles.z; } return ang; } #define MIN_AIM_DISTANCE 400 inline void Actor::GetGunOrientation ( Vector muzzlepos, Vector *forward, Vector *right, Vector *up ) { Vector ang; float accuracy; float spread; float invaim; float skl; float enemydistance; ang = MyGunAngles( muzzlepos, true ); if ( currentEnemy ) { enemydistance = ( currentEnemy->centroid - muzzlepos ).length(); if ( enemydistance < MIN_AIM_DISTANCE ) enemydistance = MIN_AIM_DISTANCE; } else { enemydistance = MIN_AIM_DISTANCE; } // 0 is maximum accuracy invaim = 1.0f - aim; if ( invaim < 0 ) invaim = 0; skl = min( skill->value, 3 ); if ( skl < 1 ) skl = 1; accuracy = 1 - ( min( skl, 3 ) * 0.18 ); accuracy *= invaim; spread = ( 8 * MIN_AIM_DISTANCE * accuracy ) / enemydistance; ang.x += G_CRandom( spread ); ang.y += G_CRandom( spread ); ang.AngleVectors( forward, right, up ); } qboolean Actor::CanShootFrom ( Vector pos, Entity *ent, qboolean usecurrentangles ) { int mask; Vector delta; Vector start; Vector end; float len; trace_t trace; Vehicle *v; Entity *t; Vector ang; if ( !currentWeapon || !WithinDistance( ent, vision_distance ) ) { if (!currentWeapon && !has_melee ) return false; } if ( usecurrentangles ) { Vector dir; start = pos + GunPosition() - worldorigin; // start = pos + centroid - worldorigin; end = ent->centroid; end.z += ( ent->absmax.z - ent->centroid.z ) * 0.75f; delta = end - start; ang = delta.toAngles(); ang.x = -ang.x; ang.y = angles.y; len = delta.length(); ang.AngleVectors( &dir, NULL, NULL ); dir *= len; end = start + dir; } else { start = pos + GunPosition() - worldorigin; end = ent->centroid; end.z += ( ent->absmax.z - ent->centroid.z ) * 0.75f; delta = end - start; len = delta.length(); } // check if we're too far away, or too close if ( currentWeapon ) { if ( ( len > attack_range ) || ( len > currentWeapon->GetMaxRange() ) || ( len < currentWeapon->GetMinRange() ) ) { return false; } mask = MASK_SHOT; } else { if ( ( len > attack_range ) || ( len > melee_range ) ) { return false; } mask = MASK_PROJECTILE; } // shoot past the guy we're shooting at end += delta * 4; #if 0 if ( usecurrentangles ) { G_DebugLine( start, end, 1, 0, 0, 1 ); } else { G_DebugLine( start, end, 1, 1, 0, 1 ); } #endif // Check if he's visible trace = G_Trace( start, vec_zero, vec_zero, end, this, mask, "Actor::CanShootFrom" ); if ( trace.startsolid ) { return false; } // If we hit the guy we wanted, then shoot if ( trace.ent == ent->edict ) { return true; } // if we hit a vehicle, check if the driver is someone we want to hit t = trace.ent->entity; if ( t && t->isSubclassOf( Vehicle ) ) { v = ( Vehicle * )t; if ( ( v->Driver() == ent ) || IsEnemy( v->Driver() ) ) { return true; } return false; } // If we hit someone else we don't like, then shoot if ( IsEnemy( t ) ) { return true; } // if we hit something breakable, check if shooting it will // let us shoot someone. if ( t->isSubclassOf( Shatter ) || t->isSubclassOf( Object ) || t->isSubclassOf( DamageThreshold ) || t->isSubclassOf( ScriptModel ) ) { trace = G_Trace( Vector( trace.endpos ), vec_zero, vec_zero, end, t, mask, "Actor::CanShootFrom 2" ); if ( trace.startsolid ) { return false; } // If we hit the guy we wanted, then shoot if ( trace.ent == ent->edict ) { return true; } // If we hit someone else we don't like, then shoot if ( IsEnemy( trace.ent->entity ) ) { return true; } // Forget it then return false; } return false; } qboolean Actor::CanShoot ( Entity *ent, qboolean usecurrentangles ) { return CanShootFrom( worldorigin, ent, usecurrentangles ); } float Actor::AttackRange ( void ) { if ( !currentWeapon && !has_melee ) { return 0; } if ( currentWeapon ) { return ( currentWeapon->GetMaxRange() ); } else { return ( melee_range ); } } float Actor::MinimumAttackRange ( void ) { float range; float maxrange; if ( !currentWeapon && !has_melee ) { return 100; } range = melee_range * 0.75f; maxrange = melee_range; if ( currentWeapon ) { range = currentWeapon->GetMinRange(); if ( !range && melee_range ) range = melee_range * 0.75f; maxrange = currentWeapon->GetMaxRange(); } if ( range > maxrange ) range = maxrange; return range; } qboolean Actor::HasWeapon ( void ) { return ( currentWeapon != NULL ); } //*********************************************************************************************** // // Actor type script commands // //*********************************************************************************************** void Actor::FriendEvent ( Event *ev ) { actortype = IS_FRIEND; } void Actor::CivilianEvent ( Event *ev ) { actortype = IS_CIVILIAN; } void Actor::EnemyEvent ( Event *ev ) { actortype = IS_ENEMY; } void Actor::InanimateEvent ( Event *ev ) { actortype = IS_INANIMATE; // // clear the monster flag so triggers are not triggered // edict->svflags &= ~SVF_MONSTER; // // don't make them move // setMoveType( MOVETYPE_NONE ); // // don't make it bleed // flags &= ~FL_BLOOD; // // don't make it gib // flags &= ~FL_DIE_GIBS; } void Actor::MonsterEvent ( Event *ev ) { actortype = IS_MONSTER; } void Actor::AnimalEvent ( Event *ev ) { actortype = IS_ANIMAL; } //*********************************************************************************************** // // Enemy management // //*********************************************************************************************** qboolean Actor::HasEnemies ( void ) { return ( enemyList.NumObjects() > 0 ); } qboolean Actor::IsEnemy ( Entity *ent ) { return enemyList.ObjectInList( EntityPtr( ent ) ) && seenEnemy; } void Actor::MakeEnemy ( Entity *ent, qboolean force ) { // don't get mad at things that can't be hurt or the world if ( ent && ( ent != world ) && ( ent != this ) && !( ent->flags & FL_NOTARGET ) && ( ent->takedamage != DAMAGE_NO ) ) { if ( !enemyList.ObjectInList( EntityPtr( ent ) ) ) { enemyList.AddObject( EntityPtr( ent ) ); } if ( !currentEnemy && !seenEnemy ) { currentEnemy = ent; if ( DoAction( "sightenemy", force ) ) { seenEnemy = true; Chatter( "snd_sightenemy", 5 ); } else { currentEnemy = NULL; } } } } void Actor::ClearEnemies ( void ) { currentEnemy = NULL; seenEnemy = false; enemyList.ClearObjectList(); } qboolean Actor::Likes ( Entity *ent ) { Actor *act; if ( ent->isClient() ) { return ( actortype == IS_FRIEND ); } else if ( actortype == IS_MONSTER ) { // monsters don't like anyone, but they don't particular hate everyone return false; } else if ( ent->isSubclassOf( Actor ) ) { act = ( Actor * )ent; return ( act->actortype == actortype ); } return false; } qboolean Actor::Hates ( Entity *ent ) { Actor *act; assert( ent ); if ( !ent ) { return false; } if ( ent->isClient() ) { if ( ent->flags & FL_SP_MUTANT ) { if ( actortype == IS_ENEMY ) return false; else return true; } else { return ( actortype != IS_CIVILIAN ) && ( actortype != IS_FRIEND ); } } else if ( ent->isSubclassOf( Actor ) && ( actortype != IS_INANIMATE ) ) { act = ( Actor * )ent; // if ( act->actortype == IS_INANIMATE ) // { // // heh... Mutants hate inanimate objects. :) // return ( actortype == IS_MONSTER ); // } if ( ( act->actortype <= IS_ENEMY ) && ( actortype <= IS_ENEMY ) ) { return false; } if ( ( act->actortype == IS_FRIEND ) && ( actortype <= IS_ENEMY ) ) { return true; } if ( ( act->actortype <= IS_ENEMY ) && ( actortype == IS_FRIEND ) ) { return true; } } return false; } //*********************************************************************************************** // // Targeting functions // //*********************************************************************************************** qboolean Actor::GetVisibleTargets ( void ) { Sentient *ent; Vector delta; int i; int n; targetList.ClearObjectList(); nearbyList.ClearObjectList(); n = SentientList.NumObjects(); for( i = 1; i <= n; i++ ) { ent = SentientList.ObjectAt( i ); //if ( ( ent == this ) || ent->deadflag || ( ent->flags & FL_NOTARGET ) || !Hates( ent ) ) if ( ( ent == this ) || ( ent->flags & FL_NOTARGET ) || ent->hidden() ) { continue; } if ( WithinDistance( ent, vision_distance ) && CanSeeFOV( ent ) ) { targetList.AddObject( EntityPtr( ent ) ); if ( WithinDistance( ent, 96 ) ) { nearbyList.AddObject( EntityPtr( ent ) ); } } } return ( targetList.NumObjects() > 0 ); } void Actor::TargetEnemies ( Event *ev ) { int i; int n; Entity *ent; Entity *newtarget; Actor *act; if ( actortype == IS_INANIMATE ) { // inanimate objects don't need to worry about this kind of thing return; } if ( enemyList.NumObjects() >= 1 ) { // don't target new enemies as much while we've already got an enemy PostEvent( EV_Actor_TargetEnemies, 5 ); } else { PostEvent( EV_Actor_TargetEnemies, 1 ); } if ( hidden() ) { // don't target while hidden (for cinematic characters) //FIXME // probably should have a start/stop function return; } if ( GetVisibleTargets() ) { n = targetList.NumObjects(); for( i = 1; i <= n; i++ ) { ent = targetList.ObjectAt( i ); if ( ent ) { if ( ent->flags & (FL_CLOAK|FL_STEALTH) ) continue; if ( !ent->deadflag && Hates( ent ) && !IsEnemy( ent ) ) { MakeEnemy( ent ); } else if ( ent->isSubclassOf( Actor ) && Likes( ent ) ) { act = ( Actor * )ent; if ( act->currentEnemy && Hates( act->currentEnemy ) && !IsEnemy( act->currentEnemy ) ) { MakeEnemy( act->currentEnemy ); if ( act->deadflag ) { // // we have passed on the post mortem message of our death, so let's clear it // act->ClearEnemies(); } } } } } } newtarget = BestTarget(); if ( newtarget && ( newtarget != currentEnemy ) ) { seenEnemy = false; currentEnemy = newtarget; if ( DoAction( "sightenemy" ) ) { seenEnemy = true; Chatter( "snd_sightenemy", 5 ); } else { currentEnemy = NULL; } } } Entity *Actor::BestTarget ( void ) { int i; int n; Entity *ent; Entity *bestent; float bestscore; float score; bestscore = 8192 * 8192 * 20; n = enemyList.NumObjects(); if ( n == 1 ) { // don't waste our time when we only have one enemy return enemyList.ObjectAt( 1 ); } bestent = NULL; for( i = 1; i <= n; i++ ) { ent = enemyList.ObjectAt( i ); if ( !ent || ent->deadflag ) { enemyList.RemoveObjectAt( i ); i--; n--; continue; } score = Range( ent ) + 1; if ( ent->health < WEAK_HEALTH ) { // Try to kill off really weak enemies score *= WEAK_WEIGHT; } if ( ent->health > health ) { score *= STRONGER_WEIGHT; } if ( CanSeeFOV( ent ) ) { // We're more interested in guys we can see score *= VISIBLE_WEIGHT; } if ( i == n ) { // Favor the latest enemy score *= NEWENEMY_WEIGHT; } if ( score < bestscore ) { bestscore = score; bestent = ent; } } return bestent; } Sentient *Actor::NearFriend ( void ) { int i; int num; Entity *ent; num = nearbyList.NumObjects(); for( i = 1; i < num; i++ ) { ent = nearbyList.ObjectAt( i ); if ( Likes( ent ) ) { return ( Sentient * )ent; } } return NULL; } qboolean Actor::CloseToEnemy ( Vector pos, float howclose ) { Entity *ent; int i; int n; n = enemyList.NumObjects(); for( i = 1; i <= n; i++ ) { ent = enemyList.ObjectAt( i ); if ( !ent || ent->deadflag || ( ent->flags & FL_NOTARGET ) ) { continue; } if ( WithinDistance( ent, howclose ) ) { return true; } } return false; } void Actor::EyeOffset ( Event *ev ) { eyeposition -= eyeoffset; eyeoffset = ev->GetVector( 1 ); eyeposition += eyeoffset; } //*********************************************************************************************** // // State control functions // //*********************************************************************************************** void Actor::EnableState ( str action ) { StateInfo *ptr; ptr = GetState( action ); if ( ptr ) { ptr->ignore = false; } } void Actor::DisableState ( str action ) { StateInfo *ptr; ptr = GetState( action ); if ( ptr ) { ptr->ignore = true; } } StateInfo *Actor::SetResponse ( str action, str response, qboolean ignore ) { StateInfo *ptr; ptr = GetState( action ); if ( !ptr ) { ptr = new StateInfo; actionList.AddObject( ptr ); ptr->action = action; } ptr->response = response; ptr->ignore = ignore; return ptr; } const char *Actor::GetResponse ( str action, qboolean force ) { StateInfo *ptr; ptr = GetState( action ); if ( ptr && ( force || !ptr->ignore ) ) { return ptr->response.c_str(); } return ""; } StateInfo *Actor::GetState ( str action ) { int i; int n; StateInfo *ptr; n = actionList.NumObjects(); for( i = 1; i <= n; i++ ) { ptr = actionList.ObjectAt( i ); if ( ptr->action == action ) { return ptr; } } return NULL; } //*********************************************************************************************** // // State stack management // //*********************************************************************************************** void Actor::ClearStateStack ( void ) { ActorState *state; int n; int i; while( !stateStack.Empty() ) { state = stateStack.Pop(); if ( state->animDoneEvent ) { delete state->animDoneEvent; } // delete the old action/response list n = state->actionList.NumObjects(); for( i = n; i >= 1; i-- ) { delete state->actionList.ObjectAt( i ); } state->actionList.ClearObjectList(); if ( state->behavior ) { delete state->behavior; } if ( state->path ) { delete state->path; } delete state; } numonstack = 0; } qboolean Actor::PopState ( void ) { ActorState *newstate; int n; int i; #ifdef DEBUG_PRINT gi.dprintf( "%d Pop:", numonstack ); #endif if ( !stateStack.Empty() ) { newstate = stateStack.Pop(); numonstack--; state = newstate->name; #ifdef DEBUG_PRINT gi.dprintf( "state '%s' anim '%s'", state.c_str(), newstate->anim.c_str() ); #endif if ( newstate->anim.length() ) { SetAnim( newstate->anim, newstate->animDoneEvent ); ChangeAnim(); } SetPath( newstate->path ); #ifdef DEBUG_PRINT if ( newstate->behavior ) { gi.dprintf( "%s", newstate->behavior->getClassname() ); } else { gi.dprintf( "NULL behavior" ); } gi.dprintf( "\n" ); #endif // NULL out our current thread so that EndBehavior doesn't // signal the thread that the behavior ended. thread = NULL; EndBehavior(); assert( !behavior ); // Set the thread after ending the old behavior, but before the new behavior if ( newstate->thread != -1 ) { thread = Director.GetThread( newstate->thread ); // Since PopState is called from the thread, we don't need to tell the thread to continue processing. // In fact, depending upon the state of the thread when we pushed it onto the stack, we may not want // the thread to continue processing (for example, if the last command was a waitFor). Just restoring the // position here will tell the thread to continue executing or wait for an event to occur. // If we ever call PopState from outside of a thread, we MUST check to see if the thread should // continue execution. //FIXME // This is probably an actorthread that was removed // ADDENDUM: actorThread is now only removed once. Thread should never be NULL. if ( thread ) { thread->Restore( &newstate->marker ); } } else { thread = NULL; } // delete the old action/response list n = actionList.NumObjects(); for( i = n; i >= 1; i-- ) { delete actionList.ObjectAt( i ); } actionList.ClearObjectList(); // Copy the new action/response list n = newstate->actionList.NumObjects(); for( i = 1; i <= n; i++ ) { actionList.AddObject( newstate->actionList.ObjectAt( i ) ); } assert( !behavior ); SetBehavior( newstate->behavior, NULL, thread ); delete newstate; } else { #ifdef DEBUG_PRINT gi.dprintf( "\n" ); #endif EndBehavior(); return false; } return true; } void Actor::PushState ( const char *newstate, ScriptThread *newthread, ThreadMarker *marker ) { ActorState *oldstate; int i; int n; oldstate = new ActorState; // push the old state #ifdef DEBUG_PRINT gi.dprintf( "%d : Pushing old state %s\n", numonstack, state.c_str() ); if ( behavior ) { gi.dprintf( "old behavior %s\n", behavior->getClassname() ); } else { gi.dprintf( "old behavior NULL\n" ); } gi.dprintf( "new state %s\n", newstate ); #endif oldstate->name = state; oldstate->anim = animname; oldstate->animDoneEvent = animDoneEvent; animDoneEvent = NULL; oldstate->path = path; oldstate->behavior = behavior; // newthread must always be the old thread assert( newthread ); oldstate->thread = newthread->ThreadNum(); assert( marker ); oldstate->marker = *marker; // Copy the action/response list n = actionList.NumObjects(); for( i = 1; i <= n; i++ ) { StateInfo *ptr; StateInfo *newobj; ptr = actionList.ObjectAt( i ); newobj = new StateInfo; newobj->action = ptr->action; newobj->response = ptr->response; newobj->ignore = ptr->ignore; oldstate->actionList.AddObject( newobj ); } numonstack++; stateStack.Push( oldstate ); // Any SetBehavior following this will delete this behavior, so null it out so that it doesn't happen if ( behavior ) { behavior->End( *this ); behavior = NULL; } state = newstate; thread = newthread; } //*********************************************************************************************** // // State control script commands // //*********************************************************************************************** void Actor::DefineStateEvent ( Event *ev ) { const char *action; str response; ScriptThread *thread; str script; int len; action = ev->GetString( 1 ); response = ev->GetString( 2 ); // check if we have a filename in the label if ( !strstr( response.c_str(), "::" ) ) { thread = ev->GetThread(); if ( thread ) { // add filename to the label so that if we jump to another script, our labels are still valid response = str( thread->Filename() ) + "::" + response; } } else { // prepend our debug directory name script = ai_actorscript->string; len = script.length(); // if we have a directory, make sure that it ends with a '/' or a '\' if ( ( len > 0 ) && ( script[ len - 1 ] != '/' ) && ( script[ len - 1 ] != '\\' ) ) { script += "/"; } response = script + response; } SetResponse( action, response ); } void Actor::CopyStateEvent ( Event *ev ) { str action1; str action2; str response; StateInfo *ptr; action1 = ev->GetString( 1 ); action2 = ev->GetString( 2 ); ptr = GetState( action2 ); if ( ptr ) { response = ptr->response; } SetResponse( action1, response ); } void Actor::IgnoreAllEvent ( Event *ev ) { int i; int n; n = actionList.NumObjects(); for( i = 1; i <= n; i++ ) { actionList.ObjectAt( i )->ignore = true; } } void Actor::IgnoreEvent ( Event *ev ) { int i; int n; n = ev->NumArgs(); for( i = 1; i <= n; i++ ) { DisableState( ev->GetString( i ) ); } } void Actor::RespondToAllEvent ( Event *ev ) { int i; int n; n = actionList.NumObjects(); for( i = 1; i <= n; i++ ) { actionList.ObjectAt( i )->ignore = false; } } void Actor::RespondToEvent ( Event *ev ) { int i; int n; n = ev->NumArgs(); for( i = 1; i <= n; i++ ) { EnableState( ev->GetString( i ) ); } } void Actor::ClearStateEvent ( Event *ev ) { ClearStateStack(); } void Actor::StateDoneEvent ( Event *ev ) { assert( actorthread == ev->GetThread() ); if ( !deadflag || ( actortype == IS_INANIMATE ) ) { // if we're not dead, pop off the previous state PopState(); } else { // if we're dead, kill off all the previous states ClearStateStack(); // kill off the thread actorthread->ProcessEvent( EV_ScriptThread_End ); actorthread = NULL; } } void Actor::SetStateEvent ( Event *ev ) { str response; str name; ThreadMarker marker; StateInfo *ptr; if ( ( deadflag == DEAD_DEAD ) && ( actortype != IS_INANIMATE ) ) { return; } name = ev->GetString( 1 ); // Don't check ignore flag ptr = GetState( name ); if ( ptr ) { response = ptr->response; } #ifdef DEBUG_PRINT gi.dprintf( "%d : %d : Action: %s - %s\n", actorthread->ThreadNum(), ev->GetThread()->ThreadNum(), name.c_str(), response.c_str() ); #endif // check the existance of the actor's thread to prevent a crash if ( !actorthread ) { return; } actorthread->Mark( &marker ); if ( response != "" && actorthread->Goto( response.c_str() ) ) { PushState( name.c_str(), actorthread, &marker ); SetAnim( "idle" ); animname = "idle"; SetVariable( "state", name.c_str() ); ProcessScript( actorthread ); } else { ev->Error( "Could not find label '%s'", response.c_str() ); actorthread->Goto( response.c_str() ); } } void Actor::CanStrafeEvent ( Event *ev ) { checkStrafe = true; } void Actor::NoStrafeEvent ( Event *ev ) { checkStrafe = false; } //*********************************************************************************************** // // Thread management // //*********************************************************************************************** void Actor::SetupThread ( void ) { str script; int len; Event *event; // we should never have a thread at this point assert( !actorthread ); // prepend our debug director name script = ai_actorscript->string; len = script.length(); // if we have a directory, make sure that it ends with a '/' or a '\' if ( ( len > 0 ) && ( script[ len - 1 ] != '/' ) && ( script[ len - 1 ] != '\\' ) ) { script += "/"; } script += actorscript; actorthread = Director.CreateThread( script.c_str(), MODEL_SCRIPT ); if ( actorthread ) { // setup thread variables SetVariable( "attackmode", attackmode ); if ( actorstart.length() ) { if ( !actorthread->Goto( actorstart.c_str() ) ) { gi.dprintf( "Label '%s' not found in %s.", actorstart.c_str(), script.c_str() ); } } ProcessScript( actorthread ); } assert( actorthread ); if ( !actorthread ) { // not having a script is bad gi.dprintf( "Unable to start actor script '%s'. Killing actor '%s'(%d).\n", script.c_str(), targetname.c_str(), entnum ); // just kill him event = new Event( EV_Killed ); event->AddEntity( this ); event->AddInteger( 0 ); event->AddEntity( this ); event->AddString( "all" ); ProcessEvent( event ); return; } } qboolean Actor::DoAction ( str name, qboolean force ) { str response; ThreadMarker marker; if ( !actorthread ) { return false; } if ( ( deadflag == DEAD_DEAD ) && ( actortype != IS_INANIMATE ) ) { return false; } response = GetResponse( name, force ); #ifdef DEBUG_PRINT gi.dprintf( "Action: %s - %s\n", name.c_str(), response.c_str() ); #endif actorthread->Mark( &marker ); if ( response != "" && actorthread->Goto( response.c_str() ) ) { PushState( name.c_str(), actorthread, &marker ); SetAnim( "idle" ); animname = "idle"; SetVariable( "state", name.c_str() ); ProcessScript( actorthread ); return true; } return false; } qboolean Actor::ForceAction ( str name ) { return DoAction( name, true ); } void Actor::ProcessScript ( ScriptThread *thread, Event *ev ) { thread->Vars()->SetVariable( "self", this ); thread->Vars()->SetVariable( "origin", worldorigin ); thread->Vars()->SetVariable( "yaw", worldangles.y ); thread->Vars()->SetVariable( "health", health ); thread->Vars()->SetVariable( "startpos", startpos ); thread->Vars()->SetVariable( "shots_per_attack", shots_per_attack ); // // see if the enemy lost his weapon // i.e. it was shot out of his hands // if ( currentWeapon ) { thread->Vars()->SetVariable( "has_weapon", 1 ); } else { thread->Vars()->SetVariable( "has_weapon", 0 ); } if ( currentEnemy ) { thread->Vars()->SetVariable( "enemy", currentEnemy ); } else { thread->Vars()->SetVariable( "enemy", "" ); } if ( ev ) { thread->ProcessEvent( ev ); } else { thread->ProcessEvent( EV_ScriptThread_Execute ); } } void Actor::StartMove ( Event *ev ) { if ( deadflag ) { return; } // thread = ev->GetThread(); } inline ScriptVariable *Actor::SetVariable ( const char *name, float value ) { if ( actorthread ) { return actorthread->Vars()->SetVariable( name, value ); } return NULL; } inline ScriptVariable *Actor::SetVariable ( const char *name, int value ) { if ( actorthread ) { return actorthread->Vars()->SetVariable( name, value ); } return NULL; } inline ScriptVariable *Actor::SetVariable ( const char *name, const char *text ) { if ( actorthread ) { return actorthread->Vars()->SetVariable( name, text ); } return NULL; } inline ScriptVariable *Actor::SetVariable ( const char *name, str &text ) { if ( actorthread ) { return actorthread->Vars()->SetVariable( name, text.c_str() ); } return NULL; } inline ScriptVariable *Actor::SetVariable ( const char *name, Entity *ent ) { if ( actorthread ) { return actorthread->Vars()->SetVariable( name, ent ); } return NULL; } inline ScriptVariable *Actor::SetVariable ( const char *name, Vector &vec ) { if ( actorthread ) { return actorthread->Vars()->SetVariable( name, vec ); } return NULL; } //*********************************************************************************************** // // Thread based script commands // //*********************************************************************************************** void Actor::SetScript ( Event *ev ) { str script; str label; int len; actorscript = ev->GetString( 1 ); // gotta start the thread again! if ( actorthread ) { // prepend our debug directory name script = ai_actorscript->string; len = script.length(); // if we have a directory, make sure that it ends with a '/' or a '\' if ( ( len > 0 ) && ( script[ len - 1 ] != '/' ) && ( script[ len - 1 ] != '\\' ) ) { script += "/"; } script += actorscript; if ( !actorthread->SetScript( script.c_str() ) ) { ev->Error( "Script '%s' not found", script.c_str() ); } else { // setup thread variables SetVariable( "attackmode", attackmode ); ProcessScript( actorthread ); } } } void Actor::SetThread ( Event *ev ) { ScriptThread *thread; if ( ( deadflag == DEAD_DEAD ) && ( actortype != IS_INANIMATE ) ) { return; } thread = ev->GetThread(); actorstart = ev->GetString( 1 ); // get the name of the script if ( thread ) { actorstart = str( thread->Filename() ) + "::" + actorstart; } // we may be getting this command from the .def file, so the thread may not have started yet if ( actorthread && actorstart.length() ) { // setup thread variables SetVariable( "attackmode", attackmode ); if ( actorthread->Goto( actorstart.c_str() ) ) { ProcessScript( actorthread ); } else { ev->Error( "Label '%s' not found. Couldn't change thread.", actorstart.c_str() ); } } } //*********************************************************************************************** // // Behavior management // //*********************************************************************************************** void Actor::EndBehavior ( void ) { ScriptThread *t; Event *event; if ( behavior ) { behavior->End( *this ); delete behavior; behavior = NULL; if ( thread ) { t = thread; thread = NULL; event = new Event( EV_MoveDone ); event->AddEntity( this ); #if 0 if ( t == actorthread ) { ProcessScript( actorthread, event ); } else { t->ProcessEvent( event ); } #else ProcessScript( actorthread, event ); if ( t != actorthread ) { event = new Event( EV_MoveDone ); event->AddEntity( this ); t->ProcessEvent( event ); } #endif } } } void Actor::SetBehaviorEvent ( Event *ev ) { ClassDef *cls; ScriptThread *thread; Event *e; str name; int i; int n; name = ev->GetString( 1 ); if ( !checkInheritance( &Behavior::ClassInfo, name.c_str() ) ) { ev->Error( "%s is not a valid behavior\n", name.c_str() ); return; } thread = ev->GetThread(); e = new Event( EV_Behavior_Args ); e->SetSource( EV_FROM_SCRIPT ); e->SetThread( thread ); e->SetLineNumber( ev->GetLineNumber() ); e->AddEntity( this ); n = ev->NumArgs(); for( i = 2; i <= n; i++ ) { e->AddToken( ev->GetToken( i ) ); } cls = getClass( name.c_str() ); SetBehavior( ( Behavior * )cls->newInstance(), e, thread ); } void Actor::SetBehavior ( Behavior *newbehavior, Event *startevent, ScriptThread *newthread ) { if ( ( deadflag ) && ( actortype != IS_INANIMATE ) ) { return; } // End any previous behavior, but don't call EV_MoveDone if we're using the same thread, // or we'll end THIS behavior if ( thread == newthread ) { thread = NULL; } EndBehavior(); behavior = newbehavior; if ( behavior ) { if ( actortype == IS_INANIMATE ) { // think while we have a behavior flags |= FL_PRETHINK; } #ifdef DEBUG_PRINT gi.dprintf( "SetBehavior %s\n", behavior->getClassname() ); #endif if ( startevent ) { behavior->ProcessEvent( startevent ); } currentBehavior = behavior->getClassname(); behavior->Begin( *this ); thread = newthread; } } void Actor::FinishedBehavior ( Event *ev ) { EndBehavior(); } void Actor::NotifyBehavior ( Event *ev ) { if ( behavior ) { behavior->ProcessEvent( EV_Behavior_AnimDone ); } } void Actor::ForwardBehaviorEvent ( Event *ev ) { ScriptThread *thread; Event *e; str name; int i; int n; name = ev->GetString( 1 ); thread = ev->GetThread(); e = new Event( name ); e->SetSource( EV_FROM_SCRIPT ); e->SetThread( thread ); e->SetLineNumber( ev->GetLineNumber() ); // always add who got this event e->AddEntity( this ); n = ev->NumArgs(); for( i = 2; i <= n; i++ ) { e->AddToken( ev->GetToken( i ) ); } if ( behavior ) behavior->ProcessEvent( e ); else warning( "ForwardBehaviorEvent", "no behavior defined" ); } //*********************************************************************************************** // // Path and node management // //*********************************************************************************************** void Actor::SetPath ( Path *newpath ) { if ( path && ( path != newpath ) ) { delete path; } path = newpath; } void Actor::ReserveNodeEvent ( Event *ev ) { PathNode *node; Vector pos; pos = ev->GetVector( 1 ); node = PathManager.NearestNode( pos, this ); if ( node && ( !node->entnum || ( node->entnum == entnum ) || ( node->occupiedTime < level.time ) ) ) { // Mark node as occupied for a short time node->occupiedTime = level.time + ev->GetFloat( 2 ); node->entnum = entnum; } } void Actor::ReleaseNodeEvent ( Event *ev ) { PathNode *node; Vector pos; pos = ev->GetVector( 1 ); node = PathManager.NearestNode( pos, this ); if ( node && ( node->entnum == entnum ) ) { node->occupiedTime = 0; node->entnum = 0; } } //*********************************************************************************************** // // Animation control functions // //*********************************************************************************************** void Actor::ChangeAnim ( void ) { float time; Vector totalmove; if ( newanimnum == -1 ) { return; } // If we're changing to the same anim, don't restart the animation if ( animating && newanimnum == edict->s.anim ) { if ( animDoneEvent ) { delete animDoneEvent; } animDoneEvent = newanimevent; } else { StopAnimating(); animname = newanim; time = gi.Anim_Time( edict->s.modelindex, newanimnum ); gi.Anim_Delta( edict->s.modelindex, newanimnum, totalmove.vec3() ); totalmove[ 1 ] = -totalmove[ 1 ]; totalmove *= edict->s.scale; totallen = totalmove.length(); // always have a valid move direction if ( totallen > 0.01 ) { movespeed = totallen / time; animdir = totalmove * ( 1 / totallen ); } else { if ( forwardspeed ) { movespeed = forwardspeed; } else { movespeed = 1; } animdir = Vector( 1, 0, 0 ); } MatrixTransformVector( animdir.vec3(), orientation, movedir.vec3() ); movevelocity = movedir * movespeed; NextAnim( newanimnum ); animDoneEvent = newanimevent; StartAnimating(); } // clear the new anim variables newanimnum = -1; newanim = ""; newanimevent = NULL; } qboolean Actor::SetAnim ( str anim, Event *ev ) { int num; // FIXME // HACK to make actors use alert when fighting instead of idle if ( hasalert && currentEnemy && ( anim == "idle" ) ) { anim = "alert"; } num = gi.Anim_Random( edict->s.modelindex, anim.c_str() ); if ( num != -1 ) { newanim = anim; newanimnum = num; newanimevent = ev; if ( actortype == IS_INANIMATE ) { // inanimate objects change anims immediately ChangeAnim(); } return true; } // kill the event if ( ev ) { delete ev; } return false; } qboolean Actor::SetAnim ( str anim, Event &ev ) { Event * event; event = new Event( ev ); return SetAnim( anim, event ); } void Actor::Anim ( Event *ev ) { Event *e; if ( ( deadflag ) && ( actortype != IS_INANIMATE ) ) { return; } e = new Event( EV_Behavior_Args ); e->SetSource( EV_FROM_SCRIPT ); e->SetThread( ev->GetThread() ); e->SetLineNumber( ev->GetLineNumber() ); e->AddEntity( this ); e->AddToken( ev->GetToken( 1 ) ); SetBehavior( new PlayAnim, e, ev->GetThread() ); } //*********************************************************************************************** // // Script commands // //*********************************************************************************************** void Actor::CrouchSize ( Event *ev ) { crouchsize_min = ev->GetVector( 1 ); crouchsize_max = ev->GetVector( 2 ); } void Actor::SetFov ( Event *ev ) { fov = ev->GetFloat( 1 ); fovdot = cos( fov * 0.5 * M_PI / 180.0 ); } void Actor::SetVisionDistance ( Event *ev ) { vision_distance = ev->GetFloat( 1 ); } void Actor::LookAt ( Event *ev ) { Entity *ent; TurnTo *turnTo; if ( ( deadflag ) && ( actortype != IS_INANIMATE ) ) { return; } ent = ev->GetEntity( 1 ); if ( ent && ent != world ) { turnTo = new TurnTo; turnTo->SetTarget( ent ); SetBehavior( turnTo, NULL, ev->GetThread() ); } } void Actor::TurnToEvent ( Event *ev ) { TurnTo *turnTo; if ( ( deadflag ) && ( actortype != IS_INANIMATE ) ) { return; } turnTo = new TurnTo; turnTo->SetDirection( ev->GetFloat( 1 ) ); SetBehavior( turnTo, NULL, ev->GetThread() ); } void Actor::IdleEvent ( Event *ev ) { Event *e; if ( ( deadflag ) && ( actortype != IS_INANIMATE ) ) { return; } e = new Event( EV_Behavior_Args ); e->SetSource( EV_FROM_SCRIPT ); e->SetThread( ev->GetThread() ); e->SetLineNumber( ev->GetLineNumber() ); e->AddEntity( this ); e->AddString( ev->GetToken( 1 ) ); SetBehavior( new Idle, e, ev->GetThread() ); } void Actor::WalkTo ( Event *ev ) { Event *e; int i; int n; if ( ( deadflag ) && ( actortype != IS_INANIMATE ) ) { return; } e = new Event( EV_Behavior_Args ); e->SetSource( EV_FROM_SCRIPT ); e->SetThread( ev->GetThread() ); e->SetLineNumber( ev->GetLineNumber() ); e->AddEntity( this ); n = ev->NumArgs(); e->AddString( "walk" ); for( i = 1; i <= n; i++ ) { e->AddToken( ev->GetToken( i ) ); } SetBehavior( new GotoPathNode, e, ev->GetThread() ); } void Actor::RunTo ( Event *ev ) { Event *e; int i; int n; if ( ( deadflag ) && ( actortype != IS_INANIMATE ) ) { return; } e = new Event( EV_Behavior_Args ); e->SetSource( EV_FROM_SCRIPT ); e->SetThread( ev->GetThread() ); e->SetLineNumber( ev->GetLineNumber() ); e->AddEntity( this ); n = ev->NumArgs(); e->AddString( "run" ); for( i = 1; i <= n; i++ ) { e->AddToken( ev->GetToken( i ) ); } SetBehavior( new GotoPathNode, e, ev->GetThread() ); } void Actor::AttackEntity ( Event *ev ) { Entity *ent; if ( ( deadflag ) && ( actortype != IS_INANIMATE ) ) { return; } ent = ev->GetEntity( 1 ); // don't get mad at things that can't be hurt or the world if ( ent && ( ent != world ) && ( ent->takedamage != DAMAGE_NO ) ) { ClearEnemies(); MakeEnemy( ent, true ); } } void Actor::AttackPlayer ( Event *ev ) { int i; edict_t *ent; if ( ( deadflag ) && ( actortype != IS_INANIMATE ) ) { return; } ClearEnemies(); // make enemies of all the players for( i = 0; i < maxclients->value; i++ ) { ent = &g_edicts[ i + 1 ]; if ( !ent->inuse || !ent->client || !ent->entity ) { continue; } MakeEnemy( ent->entity, true ); } currentEnemy = BestTarget(); if ( state != "sightenemy" ) { if ( ForceAction( "sightenemy" ) ) { seenEnemy = true; Chatter( "snd_sightenemy", 5 ); } } } void Actor::JumpToEvent ( Event *ev ) { Event *e; int i; int n; if ( ( deadflag ) && ( actortype != IS_INANIMATE ) ) { return; } e = new Event( EV_Behavior_Args ); e->SetSource( EV_FROM_SCRIPT ); e->SetThread( ev->GetThread() ); e->SetLineNumber( ev->GetLineNumber() ); e->AddEntity( this ); n = ev->NumArgs(); e->AddString( "jump" ); for( i = 1; i <= n; i++ ) { e->AddToken( ev->GetToken( i ) ); } SetBehavior( new Jump, e, ev->GetThread() ); } void Actor::GotoEvent ( Event *ev ) { // This command was added because it was a common mistake when writing actor scripts // to say "local.self goto label". Since we're such nice guys, we'll print a warning // and send it on the the calling thread. :) ScriptThread *thread; thread = ev->GetThread(); if ( thread ) { gi.dprintf( "Actor recieved 'goto' command. Passing to thread.\n" ); thread->ProcessEvent( ev ); } else { // should never happen, but say something anyways. gi.dprintf( "Actor recieved 'goto' command. Thread is NULL.\n" ); } } //*********************************************************************************************** // // Script conditionals // //*********************************************************************************************** void Actor::IfEnemyVisibleEvent ( Event *ev ) { ScriptThread *thread; thread = ev->GetThread(); assert( thread ); if ( !thread ) { return; } if ( CanSeeEnemyFrom( worldorigin ) ) { thread->ProcessCommandFromEvent( ev, 1 ); } } void Actor::IfNearEvent ( Event *ev ) { ScriptThread *thread; Entity *ent; Entity *bestent; float bestdist; float dist; str name; Vector delta; float distance; TargetList *tlist; int n; int i; thread = ev->GetThread(); assert( thread ); if ( !thread ) { return; } name = ev->GetString( 1 ); distance = ev->GetFloat( 2 ); if ( name[ 0 ] == '*' ) { ent = ev->GetEntity( 1 ); if ( WithinDistance( ent, distance ) ) { SetVariable( "other", ent ); thread->ProcessCommandFromEvent( ev, 3 ); } } else if ( name[ 0 ] == '$' ) { bestent = NULL; bestdist = distance * distance; tlist = world->GetTargetList( str( &name[ 1 ] ) ); n = tlist->list.NumObjects(); for( i = 1; i <= n; i++ ) { ent = tlist->list.ObjectAt( i ); delta = centroid - ent->centroid; dist = delta * delta; if ( dist <= bestdist ) { bestent = ent; bestdist = dist; } } if ( bestent ) { SetVariable( "other", bestent ); thread->ProcessCommandFromEvent( ev, 3 ); } } else { bestent = NULL; bestdist = distance * distance; ent = NULL; while( ent = findradius( ent, worldorigin.vec3(), distance ) ) { if ( ent->inheritsFrom( name.c_str() ) ) { delta = centroid - ent->centroid; dist = delta * delta; if ( dist <= bestdist ) { bestent = ent; bestdist = dist; } } } if ( bestent ) { SetVariable( "other", bestent ); thread->ProcessCommandFromEvent( ev, 3 ); } } } void Actor::IfCanHideAtEvent ( Event *ev ) { PathNode *node; Vector pos; ScriptThread *thread; thread = ev->GetThread(); assert( thread ); if ( !thread ) { return; } pos = ev->GetVector( 1 ); node = PathManager.NearestNode( pos, this ); if ( node && ( node->nodeflags & ( AI_DUCK | AI_COVER ) ) && !CanSeeEnemyFrom( pos ) ) { if ( !node->entnum || ( node->entnum == entnum ) || ( node->occupiedTime < level.time ) ) { thread->ProcessCommandFromEvent( ev, 2 ); } } } void Actor::IfCanStrafeAttackEvent ( Event *ev ) { ScriptThread *thread; int num; Vector delta; Vector left; Vector pos; if ( !checkStrafe ) return; if ( !currentEnemy ) { return; } thread = ev->GetThread(); assert( thread ); if ( !thread ) { return; } delta = currentEnemy->worldorigin - worldorigin; left.x = -delta.y; left.y = delta.x; left.normalize(); num = gi.Anim_Random( edict->s.modelindex, "step_left" ); if ( num != -1 ) { gi.Anim_Delta( edict->s.modelindex, num, delta.vec3() ); delta *= edict->s.scale; pos = worldorigin + left * delta.length(); if ( CanMoveTo( pos ) && CanShootFrom( pos, currentEnemy, false ) ) { thread->ProcessCommandFromEvent( ev, 1 ); return; } } num = gi.Anim_Random( edict->s.modelindex, "step_right" ); if ( num != -1 ) { gi.Anim_Delta( edict->s.modelindex, num, delta.vec3() ); delta *= edict->s.scale; pos = worldorigin - left * delta.length(); if ( CanMoveTo( pos ) && CanShootFrom( pos, currentEnemy, false ) ) { thread->ProcessCommandFromEvent( ev, 1 ); return; } } } void Actor::IfCanMeleeAttackEvent ( Event *ev ) { ScriptThread *thread; Vector delta; float r; if ( !currentEnemy || !has_melee ) { return; } thread = ev->GetThread(); assert( thread ); if ( !thread ) { return; } delta = centroid - currentEnemy->centroid; r = delta.length(); if ( r <= melee_range ) { thread->ProcessCommandFromEvent( ev, 1 ); } } void Actor::IfCanShootEvent ( Event *ev ) { ScriptThread *thread; if ( !currentEnemy ) { return; } thread = ev->GetThread(); assert( thread ); if ( !thread ) { return; } if ( CanShoot( currentEnemy, false ) ) { thread->ProcessCommandFromEvent( ev, 1 ); } } void Actor::IfEnemyWithinEvent ( Event *ev ) { ScriptThread *thread; if ( !currentEnemy ) { return; } thread = ev->GetThread(); assert( thread ); if ( !thread ) { return; } if ( WithinDistance( currentEnemy, ev->GetFloat( 1 ) ) ) { thread->ProcessCommandFromEvent( ev, 2 ); } } //*********************************************************************************************** // // Sound reaction functions // //*********************************************************************************************** void Actor::IgnoreSoundsEvent ( Event *ev ) { DisableState( "weaponsound" ); DisableState( "movementsound" ); DisableState( "painsound" ); DisableState( "deathsound" ); DisableState( "breakingsound" ); DisableState( "doorsound" ); DisableState( "mutantsound" ); DisableState( "voicesound" ); DisableState( "machinesound" ); DisableState( "radiosound" ); } void Actor::RespondToSoundsEvent ( Event *ev ) { EnableState( "weaponsound" ); EnableState( "movementsound" ); EnableState( "painsound" ); EnableState( "deathsound" ); EnableState( "breakingsound" ); EnableState( "doorsound" ); EnableState( "mutantsound" ); EnableState( "voicesound" ); EnableState( "machinesound" ); EnableState( "radiosound" ); } void Actor::InvestigateWeaponSound ( Event *ev ) { Vector location; Entity *other; if ( !currentEnemy && !deadflag && ( nextsoundtime < level.time ) ) { other = ev->GetEntity( 1 ); location = ev->GetVector( 2 ); SetVariable( "other", other ); SetVariable( "location", location ); if ( DoAction( "weaponsound" ) ) { nextsoundtime = level.time + 2; } } } void Actor::InvestigateMovementSound ( Event *ev ) { Vector location; Entity *other; other = ev->GetEntity( 1 ); if ( other && !currentEnemy && !deadflag && ( nextsoundtime < level.time ) && Hates( other ) ) { location = ev->GetVector( 2 ); SetVariable( "other", other ); SetVariable( "location", location ); if ( DoAction( "movementsound" ) ) nextsoundtime = level.time + 2; } } void Actor::InvestigatePainSound ( Event *ev ) { Vector location; Entity *other; if ( !currentEnemy && !deadflag && ( nextsoundtime < level.time ) ) { other = ev->GetEntity( 1 ); location = ev->GetVector( 2 ); SetVariable( "other", other ); SetVariable( "location", location ); if ( DoAction( "painsound" ) ) nextsoundtime = level.time + 2; } } void Actor::InvestigateDeathSound ( Event *ev ) { Vector location; Entity *other; if ( !currentEnemy && !deadflag && ( nextsoundtime < level.time ) ) { other = ev->GetEntity( 1 ); location = ev->GetVector( 2 ); SetVariable( "other", other ); SetVariable( "location", location ); if ( DoAction( "deathsound" ) ) nextsoundtime = level.time + 2; } } void Actor::InvestigateBreakingSound ( Event *ev ) { Vector location; Entity *other; if ( !currentEnemy && !deadflag && ( nextsoundtime < level.time ) ) { other = ev->GetEntity( 1 ); location = ev->GetVector( 2 ); SetVariable( "other", other ); SetVariable( "location", location ); if ( DoAction( "breakingsound" ) ) nextsoundtime = level.time + 2; } } void Actor::InvestigateDoorSound ( Event *ev ) { Vector location; Entity *other; other = ev->GetEntity( 1 ); if ( other && !currentEnemy && !deadflag && ( nextsoundtime < level.time ) && Hates( other ) ) { location = ev->GetVector( 2 ); SetVariable( "other", other ); SetVariable( "location", location ); if ( DoAction( "doorsound" ) ) nextsoundtime = level.time + 2; } } void Actor::InvestigateMutantSound ( Event *ev ) { Vector location; Entity *other; other = ev->GetEntity( 1 ); if ( other && !currentEnemy && !deadflag && ( nextsoundtime < level.time ) && Hates( other ) ) { location = ev->GetVector( 2 ); SetVariable( "other", other ); SetVariable( "location", location ); if ( DoAction( "mutantsound" ) ) nextsoundtime = level.time + 2; } } void Actor::InvestigateVoiceSound ( Event *ev ) { Vector location; Entity *other; other = ev->GetEntity( 1 ); if ( other && !currentEnemy && !deadflag && ( nextsoundtime < level.time ) && Hates( other ) ) { location = ev->GetVector( 2 ); SetVariable( "other", other ); SetVariable( "location", location ); if ( DoAction( "voicesound" ) ) nextsoundtime = level.time + 2; } } void Actor::InvestigateMachineSound ( Event *ev ) { Vector location; Entity *other; other = ev->GetEntity( 1 ); if ( other && !currentEnemy && !deadflag && ( nextsoundtime < level.time ) && Hates( other ) ) { location = ev->GetVector( 2 ); SetVariable( "other", other ); SetVariable( "location", location ); if ( DoAction( "machinesound" ) ) nextsoundtime = level.time + 2; } } void Actor::InvestigateRadioSound ( Event *ev ) { Vector location; Entity *other; other = ev->GetEntity( 1 ); if ( other && !currentEnemy && !deadflag && ( nextsoundtime < level.time ) && Hates( other ) ) { location = ev->GetVector( 2 ); SetVariable( "other", other ); SetVariable( "location", location ); if ( DoAction( "radiosound" ) ) nextsoundtime = level.time + 2; } } //*********************************************************************************************** // // Pain and death related functions // //*********************************************************************************************** void Actor::Pain ( Event *ev ) { float damage; Entity *ent; float oldhealth; float newhealth; #ifdef DEBUG_PRINT gi.dprintf( "Pain\n" ); #endif damage = ev->GetFloat( 1 ); ent = ev->GetEntity( 2 ); // if it's a Sentient and not liked, attack 'em. if ( ent && ent->isSubclassOf( Sentient ) && !Likes( ent ) ) { MakeEnemy( ent ); if ( ent != currentEnemy ) { currentEnemy = BestTarget(); } } if ( damage <= 0 ) { return; } oldhealth = ( health + damage ) / max_health; newhealth = health / max_health; SetVariable( "other", ev->GetEntity( 2 ) ); // If we pass more than one range, if ( ( oldhealth > 0.75 ) && ( newhealth <= 0.75 ) ) { DoAction( "health_ok" ); } if ( ( oldhealth > 0.5 ) && ( newhealth <= 0.5 ) ) { DoAction( "health_med" ); } if ( ( oldhealth > 0.25 ) && ( newhealth <= 0.25 ) ) { DoAction( "health_low" ); } if ( ( oldhealth > 0.1 ) && ( newhealth <= 0.1 ) ) { DoAction( "health_danger" ); } if ( damage <= pain_threshold ) { Chatter( "snd_pain_taunt", 5, true ); return; } if ( strncmp( animname.c_str(), "pain", 4 ) && strncmp( animname.c_str(), "crouch_pain", 11 ) ) { str aname; int index; // // determine pain animation // if ( !strncmp( animname.c_str(), "crouch", 6 ) ) { aname = "crouch_"; } aname += str("pain_") + str( ev->GetString( 3 ) ); index = gi.Anim_Random( edict->s.modelindex, aname.c_str() ); if ( ( index == -1 ) && !strncmp( animname.c_str(), "crouch", 6 ) ) { aname = "crouch_pain"; index = gi.Anim_Random( edict->s.modelindex, aname.c_str() ); } if ( index == -1 ) { aname = "pain"; } SetVariable( "painanim", aname.c_str() ); DoAction( "pain" ); } } void Actor::Dead ( Event *ev ) { Vector min, max; Event *event; StopAnimating(); if ( !groundentity && ( velocity != vec_zero ) ) { // wait until we hit the ground PostEvent( ev, FRAMETIME ); return; } // // drop anything that might be attached to us // if ( numchildren ) { Entity * child; int i; // // detach all our children // for ( i = 0; i < MAX_MODEL_CHILDREN; i++ ) { if ( children[ i ] ) { child = ( Entity * )G_GetEntity( children[ i ] ); child->ProcessEvent( EV_Detach ); } } } deadflag = DEAD_DEAD; maxs[0] *= 2.0f; maxs[1] *= 2.0f; maxs[2] *= 0.3f; setSize(mins,maxs); edict->svflags |= SVF_DEADMONSTER; setMoveType( MOVETYPE_NONE ); setOrigin( worldorigin ); if ( deathgib ) { // Put down a bloodsplat Vector start, end, dir, norm, ang; float scale; BloodSplat *splat; trace_t trace; dir = Vector( 0, 0, -1 ); start = centroid; end = start + 100 * dir; trace = G_Trace( start, vec_zero, vec_zero, end, this, MASK_SOLIDNONFENCE, "Actor::Dead" ); if ( !( HitSky( &trace ) || ( trace.ent->solid != SOLID_BSP ) || ( trace.ent->s.number != 0 ) ) ) { scale = G_Random( 1.2 ); if ( scale < 0.5 ) scale = 0.5; norm = trace.plane.normal; norm.x = -norm.x; norm.y = -norm.y; ang = norm.toAngles(); ang.z = G_Random( 360 ); end = trace.endpos + Vector( trace.plane.normal ) * 0.2; splat = new BloodSplat( end, ang, scale ); } } // If this body has nothing interesting, then remove it if ( !NumInventoryItems() ) { if ( nodeathfade ) return; PostEvent( EV_FadeOut, 10 ); return; } // Spawn a touch field min = worldorigin - "32 32 32"; max = worldorigin + "32 32 32"; trig = new TouchField; trig->Setup( this, EV_Touch, min, max, TRIGGER_PLAYERS ); assert( trig->edict->solid == SOLID_TRIGGER ); if ( !nodeathfade ) { // Check in the future for uselessness event = new Event ( EV_Sentient_UselessCheck ); PostEvent ( event, 1.0f + G_Random() ); // If this guy has no InventoryItems, then fade him in 1 minute if ( !HasInventoryOfType( "InventoryItem" ) ) { PostEvent( EV_Actor_Remove, 60.0f ); } } } void Actor::Killed ( Event *ev ) { const char *name; Entity *ent; int num; Entity *attacker; Entity *inflictor; Vector dir; Event *event; int i; str dname; int meansofdeath; CheckWater(); StopAnimating(); CancelPendingEvents(); // don't allow them to fly, think, or swim anymore flags &= ~( FL_PRETHINK | FL_SWIM | FL_FLY ); deadflag = DEAD_DYING; takedamage = DAMAGE_YES; groundentity = NULL; attacker = ev->GetEntity( 1 ); inflictor = ev->GetEntity( 3 ); meansofdeath = ev->GetInteger( 5 ); // Double all the armor DoubleArmor(); SetVariable( "other", ev->GetEntity( 1 ) ); if ( !DoAction( "killed" ) && actorthread ) { actorthread->ProcessEvent( EV_ScriptThread_End ); } // Turn off dlight and shadow edict->s.renderfx &= ~( RF_DLIGHT|RF_XFLIP ); // // kill the killtargets // name = KillTarget(); if ( name && strcmp( name, "" ) ) { num = 0; do { num = G_FindTarget( num, name ); if ( !num ) { break; } ent = G_GetEntity( num ); ent->PostEvent( EV_Remove, 0 ); } while ( 1 ); } // // fire targets // name = Target(); if ( name && strcmp( name, "" ) ) { num = 0; do { num = G_FindTarget( num, name ); if ( !num ) { break; } ent = G_GetEntity( num ); event = new Event( EV_Activate ); event->AddEntity( attacker ); ent->PostEvent( event, 0 ); } while ( 1 ); } // // see if we have a kill_thread // if ( kill_thread.length() > 1 ) { ScriptThread * thread; // // create the thread, but don't start it yet // thread = ExecuteThread( kill_thread, false ); if ( thread ) { ProcessScript( thread, NULL ); } else { warning( "Killed", "could not process kill_thread" ); } } if (flags & FL_DIE_EXPLODE) { CreateExplosion( worldorigin, 150*edict->s.scale, edict->s.scale * 2, true, this, this, this ); } if ( flags & FL_DIE_TESSELATE ) { float power; setSolidType( SOLID_NOT ); hideModel(); dir = worldorigin - attacker->worldorigin; if ( meansofdeath == MOD_ION ) { power = 1000; } else { power = ev->GetFloat( 2 ); } TesselateModel ( this, tess_min_size, tess_max_size, dir, power, 1.0f, tess_thickness, vec3_origin ); DropInventoryItems(); // Tesselated models require the model to be on the client for 1 more frame PostEvent( EV_Remove, FRAMETIME ); return; } if ( DoGib( meansofdeath, inflictor ) ) { deathgib = true; } if ( currentWeapon ) { DropWeapon( currentWeapon ); } animOverride = false; // // determine death animation // if ( !strncmp( animname.c_str(), "crouch", 6 ) ) { dname = "crouch_"; } if ( deathgib ) { str location; location = ev->GetString( 4 ); // Check for location first otherwise randomize if ( location == "torso_upper" ) dname += str( "gibdeath_upper" ); else if ( location == "torso_lower" ) dname += str( "gibdeath_lower" ); else if ( strstr( location.c_str(), "leg" ) ) dname += str( "gibdeath_lower" ); else if ( strstr( location.c_str(), "arm" ) ) dname += str( "gibdeath_upper" ); else if ( strstr( location.c_str(), "head" ) ) dname += str( "gibdeath_upper" ); else if ( G_Random() > 0.5 ) dname += str( "gibdeath_upper" ); else dname += str( "gibdeath_lower" ); } else { dname += str( "death_" ) + str( ev->GetString( 4 ) ); } i = gi.Anim_Random( edict->s.modelindex, dname.c_str() ); if ( ( i == -1 ) && !strncmp( animname.c_str(), "crouch", 6 ) ) { dname = "crouch_death"; i = gi.Anim_Random( edict->s.modelindex, dname.c_str() ); } if ( i == -1 ) { dname = "death"; } if ( ( i != -1 ) && ( !strncmp( dname.c_str(), "gibdeath", 7 ) ) ) { Event *ev1; ev1 = new Event( EV_Gib ); ev1->AddInteger( 1 ); ProcessEvent( ev1 ); } if ( attacker ) { str location; float damage; damage = ev->GetFloat( 2 ); location = ev->GetString( 4 ); event = new Event( EV_GotKill ); event->AddEntity( this ); event->AddInteger( damage ); event->AddEntity( inflictor ); event->AddString( location ); event->AddInteger( meansofdeath ); event->AddInteger( deathgib ); attacker->ProcessEvent( event ); } SetAnim( dname.c_str(), EV_Actor_Dead ); // Call changeanim immediatly since we're no longer calling prethink ChangeAnim(); // // moved this here so guys would not be solid right away // edict->svflags |= SVF_DEADMONSTER; edict->clipmask = MASK_DEADSOLID; if ( velocity.z < 10 ) { velocity.z += G_Random( 300 ); } angles.x = 0; angles.z = 0; setAngles( angles ); } void Actor::GibEvent ( Event *ev ) { qboolean hidemodel; hidemodel = !ev->GetInteger( 1 ); if ( sv_gibs->value && !parentmode->value ) { int numgibs; float gibsize; if ( hidemodel ) { takedamage = DAMAGE_NO; setSolidType( SOLID_NOT ); hideModel(); // If there is a touchfield, remove it. if ( trig ) { trig->PostEvent( EV_Remove, 0 ); trig = NULL; } } gibsize = size.length() / 140; numgibs = gibsize * 6; if ( numgibs > 4 ) numgibs = 4; CreateGibs( this, health, gibsize, numgibs ); } DropInventoryItems(); if ( hidemodel ) PostEvent( EV_Remove, 0 ); } void Actor::RemoveUselessBody ( Event *ev ) { if ( trig ) { trig->PostEvent( EV_Remove, 0 ); trig = NULL; } CancelEventsOfType( EV_Sentient_UselessCheck ); PostEvent( EV_FadeOut, 5); } void Actor::SetPainThresholdEvent ( Event *ev ) { pain_threshold = ( ev->GetFloat( 1 ) ) * skill->value * 0.66f; } void Actor::SetKillThreadEvent ( Event *ev ) { kill_thread = ev->GetString( 1 ); } void Actor::AttackRangeEvent ( Event *ev ) { attack_range = ev->GetFloat( 1 ); } void Actor::AttackModeEvent ( Event *ev ) { attackmode = ev->GetInteger( 1 ); SetVariable( "attackmode", attackmode ); } void Actor::ShotsPerAttackEvent ( Event *ev ) { float shots; shots = ev->GetFloat( 1 ); // // scale the shots based on skill // if ( skill->value < 1 ) shots_per_attack = shots * 0.4f; else if ( skill->value < 2 ) shots_per_attack = shots * 0.7f; else if ( skill->value < 3 ) shots_per_attack = shots; else shots_per_attack = shots * skill->value * 0.4f; } void Actor::ClearEnemyEvent ( Event *ev ) { ClearEnemies(); } void Actor::NoDeathFadeEvent ( Event *ev ) { nodeathfade = true; } void Actor::NoChatterEvent ( Event *ev ) { nochatter = true; } void Actor::TurnSpeedEvent ( Event *ev ) { turnspeed = ev->GetFloat( 1 ); } //*********************************************************************************************** // // Movement functions // //*********************************************************************************************** void Actor::ForwardSpeedEvent ( Event *ev ) { forwardspeed = ev->GetFloat( 1 ); } void Actor::SwimEvent ( Event *ev ) { // movement |= AI_CANSWIM; flags &= ~FL_FLY; flags |= FL_SWIM; } void Actor::FlyEvent ( Event *ev ) { // movement |= AI_CANFLY; flags &= ~FL_SWIM; flags |= FL_FLY; } void Actor::NotLandEvent ( Event *ev ) { //movement &= ~AI_CANWALK; flags &= FL_SWIM | FL_FLY; } inline qboolean Actor::CanMoveTo ( Vector pos ) { Vector min; trace_t trace; Vector start; Vector end; Vector s; s = Vector( 0, 0, STEPSIZE ); start = worldorigin + s; end = pos + s; trace = G_Trace( start, mins, maxs, end, this, edict->clipmask, "Actor::CanMoveTo" ); if ( trace.fraction == 1 ) { return true; } return false; } inline void Actor::CheckWater ( void ) { Vector sample[3]; int cont; // // get waterlevel and type // waterlevel = 0; watertype = 0; sample[ 0 ] = worldorigin; sample[ 2 ] = EyePosition(); sample[ 1 ] = ( sample[ 0 ] + sample[ 2 ] ) * 0.5f; cont = gi.pointcontents( sample[ 0 ].vec3() ); if (cont & MASK_WATER) { watertype = cont; waterlevel = 1; cont = gi.pointcontents( sample[ 2 ].vec3() ); if (cont & MASK_WATER) { waterlevel = 3; } else { cont = gi.pointcontents( sample[ 1 ].vec3() ); if (cont & MASK_WATER) { waterlevel = 2; } } } } #define MAX_PITCH 75 void Actor::Accelerate ( Vector steering ) { // activate this to limit turnrate #if 0 if ( steering.y > turnspeed ) steering.y = turnspeed; else if ( steering.y < -turnspeed ) steering.y = -turnspeed; #endif angles.y += steering.y; if ( frame_delta.x > 4 ) { // make him lean into the turn a bit angles.z = movespeed * ( 0.4f / 320.0f ) * steering.y; if ( ( flags & FL_FLY ) || ( ( flags & FL_SWIM ) && waterlevel > 0 ) ) { angles.z = bound( angles.z, -2, 2 ); } else { angles.z = bound( angles.z, -5, 5 ); } } else { angles.z = 0; } if ( ( flags & FL_FLY ) || ( ( flags & FL_SWIM ) && waterlevel > 0 ) ) { angles.x -= steering.x; //angles.x = bound( angles.x, -MAX_PITCH, MAX_PITCH ); } setAngles( angles ); } void Actor::CalcMove ( void ) { if ( total_delta != vec_zero ) { // movement deltas have inverted Y axis total_delta[ 1 ] = -total_delta[ 1 ]; MatrixTransformVector( total_delta.vec3(), orientation, move.vec3() ); total_delta = vec_zero; } else { move = vec_zero; } // force movement if forwardspeed is set if ( forwardspeed ) { if ( move == vec_zero ) { move = orientation[ 0 ]; } else { move.normalize(); } animdir = move; movedir = move; movespeed = forwardspeed; move *= movespeed * FRAMETIME; totallen = forwardspeed; movevelocity = movedir * movespeed; } } void Actor::setAngles ( Vector ang ) { Sentient::setAngles( ang ); MatrixTransformVector( animdir.vec3(), orientation, movedir.vec3() ); movevelocity = movedir * movespeed; } stepmoveresult_t Actor::WaterMove ( void ) { Vector oldorg; Vector neworg; trace_t trace; int oldwater; if ( ( totallen <= 0.01f ) || ( move == vec_zero ) ) { return STEPMOVE_OK; } // try the move oldorg = worldorigin; neworg = worldorigin + move; trace = G_Trace( oldorg, mins, maxs, neworg, this, edict->clipmask, "Actor::WaterMove 1" ); if ( trace.fraction == 0 ) { return STEPMOVE_STUCK; } oldwater = waterlevel; setOrigin( trace.endpos ); CheckWater(); // swim monsters don't exit water voluntarily if ( ( oldwater > 1 ) && ( waterlevel < 2 ) ) { waterlevel = oldwater; setOrigin( oldorg ); return STEPMOVE_STUCK; } return STEPMOVE_OK; } stepmoveresult_t Actor::AirMove ( void ) { Vector oldorg; Vector neworg; trace_t trace; int oldwater; if ( ( totallen <= 0.01f ) || ( move == vec_zero ) ) { return STEPMOVE_OK; } // try the move oldorg = worldorigin; neworg = worldorigin + move; trace = G_Trace( oldorg, mins, maxs, neworg, this, edict->clipmask, "Actor::AirMove 1" ); if ( trace.fraction == 0 ) { return STEPMOVE_BLOCKED_BY_WATER; } oldwater = waterlevel; setOrigin( trace.endpos ); CheckWater(); // fly monsters don't enter water voluntarily if ( !oldwater && waterlevel ) { waterlevel = oldwater; setOrigin( oldorg ); return STEPMOVE_STUCK; } return STEPMOVE_OK; } stepmoveresult_t Actor::TryMove ( void ) { Vector oldorg; Vector neworg; Vector end; Entity *ent; trace_t trace; Door *door; #if 0 vec3_t test; int contents; #endif if ( ( totallen <= 0.01f ) || ( move == vec_zero ) ) { return STEPMOVE_OK; } // try the move oldorg = worldorigin; neworg = worldorigin + move; // push down from a step height above the wished position neworg[ 2 ] += STEPSIZE; end = neworg; end[ 2 ] -= STEPSIZE * 4;//2; trace = G_Trace( neworg, mins, maxs, end, this, edict->clipmask, "Actor::TryMove 1" ); if ( trace.allsolid ) { ent = trace.ent->entity; if ( ent && ent->isSubclassOf( Door ) ) { if ( state == "opendoor" ) { return STEPMOVE_OK; } door = ( Door * )ent; if ( !door->locked && !door->isOpen() ) { SetVariable( "other", ent ); SetVariable( "dir", end - worldorigin ); ForceAction( "opendoor" ); return STEPMOVE_OK; } } return STEPMOVE_STUCK; } if ( trace.startsolid ) { neworg[ 2 ] -= STEPSIZE; #if 0 trace = G_Trace( neworg, mins, maxs, end, this, edict->clipmask, "Actor::TryMove 2" ); #else // 2015 - The following line altered to allow short actors to detect doors, // It doesn't seem to break anything else... if ( maxs[ 2 ] > ( STEPSIZE * 3 ) ) { trace = G_Trace( neworg, mins, maxs, end, this, edict->clipmask, "Actor::TryMove 2" ); } else { trace = G_Trace( neworg + movetweak, mins, maxs - movetweak, end, this, edict->clipmask, "Actor::TryMove 2" ); } #endif if ( trace.allsolid || trace.startsolid ) { ent = trace.ent->entity; if ( ent && ent->isSubclassOf( Door ) ) { if ( state == "opendoor" ) { return STEPMOVE_OK; } door = ( Door * )ent; if ( !door->locked && !door->isOpen() ) { SetVariable( "other", ent ); SetVariable( "dir", end - worldorigin ); ForceAction( "opendoor" ); return STEPMOVE_OK; } } return STEPMOVE_STUCK; } } #if 0 // don't go in to water if ( waterlevel == 0 ) { test[ 0 ] = trace.endpos[ 0 ]; test[ 1 ] = trace.endpos[ 1 ]; test[ 2 ] = trace.endpos[ 2 ] + mins[ 2 ] + 1; contents = gi.pointcontents( test ); if ( contents & MASK_WATER ) { return STEPMOVE_BLOCKED_BY_WATER; } } #endif if ( trace.fraction == 1 ) { // don't let guys get stuck standing on other guys // if monster had the ground pulled out, go ahead and fall if ( ( flags & FL_PARTIALGROUND ) || ( groundentity && groundentity->entity && ( groundentity->entity->isSubclassOf( Sentient ) ) ) ) { setOrigin( worldorigin + move ); groundentity = NULL; return STEPMOVE_OK; } // walked off an edge return STEPMOVE_BLOCKED_BY_FALL; } // check point traces down for dangling corners worldorigin = trace.endpos; if ( !M_CheckBottom( this ) ) { // don't let guys get stuck standing on other guys if ( ( flags & FL_PARTIALGROUND ) || ( groundentity && groundentity->entity && ( groundentity->entity->isSubclassOf( Sentient ) ) ) ) { // entity had floor mostly pulled out from underneath it // and is trying to correct setOrigin( worldorigin ); CheckWater(); return STEPMOVE_OK; } setOrigin( oldorg ); return STEPMOVE_BLOCKED_BY_FALL; } if ( flags & FL_PARTIALGROUND ) { flags &= ~FL_PARTIALGROUND; } groundentity = trace.ent; groundentity_linkcount = trace.ent->linkcount; groundplane = trace.plane; groundsurface = trace.surface; groundcontents = trace.contents; // the move is ok setOrigin( worldorigin ); CheckWater(); return STEPMOVE_OK; } void Actor::SetAim ( Event *ev ) { aim = ev->GetFloat( 1 ); } void Actor::SetMeleeRange ( Event *ev ) { melee_range = ev->GetFloat( 1 ); melee_range *= edict->s.scale; if ( melee_range < 70 ) melee_range = 70; } void Actor::SetMeleeDamage ( Event *ev ) { melee_damage = ev->GetFloat( 1 ); melee_damage *= edict->s.scale; } void Actor::MeleeEvent ( Event *ev ) { trace_t trace; Vector start; Vector end; float damage; float extra_reach; Vector org; Vector ang; Vector dir; float kick; if ( !has_melee ) { warning( "MeleeEvent","Melee being called without animation" ); return; } damage = G_Random( melee_damage*0.5f )+ G_Random( melee_damage*0.5f ); if ( ev->NumArgs() > 0 ) extra_reach = ev->GetFloat( 1 ); else extra_reach = 0; if ( ev->NumArgs() > 1 ) { kick = ev->GetFloat( 2 ); } else { kick = damage * 3; } // get the position of the attack start = GunPosition(); // get the attack_dir ang = MyGunAngles( start, true ); ang.AngleVectors( &dir, NULL, NULL ); end = start + dir * ( melee_range + extra_reach ); trace = G_FullTrace( start, vec_zero, vec_zero, end, 15, this, MASK_PROJECTILE, "Actor::Melee" ); dir = Vector(trace.endpos) - start; dir.normalize(); org = Vector(trace.endpos) - dir; if ( (trace.fraction < 1.0f) ) { if ( trace.ent->entity && trace.ent->entity->takedamage ) { if ( trace.ent->entity->flags & FL_BLOOD ) SpawnBlood( org, trace.plane.normal, damage ); RandomGlobalSound("impact_goryimpact"); if ( trace.intersect.valid ) { // We hit a valid group so send in location based damage trace.ent->entity->Damage( this, this, damage, trace.endpos, dir, trace.plane.normal, kick, 0, MOD_FISTS, trace.intersect.parentgroup, -1, trace.intersect.damage_multiplier ); } else { // take the ground out so that the kick works trace.ent->entity->groundentity = NULL; // We didn't hit any groups, so send in generic damage trace.ent->entity->Damage( this, this, damage, trace.endpos, dir, trace.plane.normal, kick, 0, MOD_FISTS, -1, -1, 1 ); } } } } void Actor::AttackFinishedEvent ( Event *ev ) { if ( currentWeapon ) { currentWeapon->PostEvent( EV_Weapon_FinishAttack, 0 ); } } float Actor::JumpTo ( Vector targ, float speed ) { float traveltime; float vertical_speed; Vector target; Vector dir; Vector xydir; CheckWater(); // // if we got a jump, go into that mode // traveltime = 0; if ( speed <= 0 ) { speed = movespeed * 1.5f; if ( speed < (400 * gravity) ) speed = (400 * gravity); } target = targ; dir = target - worldorigin; xydir = dir; xydir.z = 0; setAngles( xydir.toAngles() ); traveltime = xydir.length() / speed; // // we add 16 to allow for a little bit higher // if ( waterlevel > 2 ) { vertical_speed = ( ( dir.z + 16 ) / traveltime ) + ( 0.5f * gravity * 60 * traveltime ); } else { vertical_speed = ( ( dir.z + 16 ) / traveltime ) + ( 0.5f * gravity * sv_gravity->value * traveltime ); } xydir.normalize(); velocity = speed * xydir; velocity.z = vertical_speed; return traveltime; } float Actor::JumpTo ( PathNode *goal, float speed ) { if ( goal ) return JumpTo( goal->worldorigin, speed ); else return 0; } float Actor::JumpTo ( Entity *goal, float speed ) { if ( goal ) return JumpTo( goal->worldorigin, speed ); else return 0; } EXPORT_FROM_DLL void Actor::setSize ( Vector min, Vector max ) { min *= edict->s.scale; max *= edict->s.scale; Sentient::setSize( min, max ); } EXPORT_FROM_DLL void Actor::SetHealth ( Event *ev ) { health = ev->GetFloat( 1 ) * edict->s.scale; max_health = health; } //*********************************************************************************************** // // Debug functions // //*********************************************************************************************** void Actor::ShowInfo ( void ) { Entity *ent; int i; int n; StateInfo *ptr; gi.printf( "\nEntity # : %d\n", entnum ); gi.printf( "Class ID : %s\n", getClassID() ); gi.printf( "Classname : %s\n", getClassname() ); gi.printf( "Targetname : %s\n", TargetName() ); gi.printf( "Origin : ( %f, %f, %f )\n", worldorigin.x, worldorigin.y, worldorigin.z ); gi.printf( "Bounds : Mins( %.2f, %.2f, %.2f ) Maxs( %.2f, %.2f, %.2f )\n", mins.x, mins.y, mins.z, maxs.x, maxs.y, maxs.z ); gi.printf( "State : %s\n", state.c_str() ); if ( behavior ) { gi.printf( "Behavior : %s\n", behavior->getClassname() ); } else { gi.printf( "Behavior : NULL -- was '%s'\n", currentBehavior.c_str() ); } if ( actorthread ) { gi.printf( "Thread : %s(%d)\n", actorthread->Filename(), actorthread->CurrentLine() ); } else { gi.printf( "Thread : NULL\n" ); } gi.printf( "Actortype : %d\n", actortype ); gi.printf( "Attackmode : %d\n", attackmode ); gi.printf( "Model : %s\n", model.c_str() ); gi.printf( "Anim : %s\n", animname.c_str() ); gi.printf( "Movespeed : %.2f\n", movespeed ); gi.printf( "Health : %f\n", health ); gi.printf( "\nResponses:\n" ); n = actionList.NumObjects(); for( i = 1; i <= n; i++ ) { ptr = actionList.ObjectAt( i ); gi.printf( "%s : ", ptr->action.c_str() ); if ( ptr->ignore ) { gi.printf( "ignored - " ); } gi.printf( "%s\n", ptr->response.c_str() ); } n = enemyList.NumObjects(); if ( n ) { gi.printf( "\nEnemies:\n" ); for( i = 1; i <= n; i++ ) { ent = enemyList.ObjectAt( i ); if ( ent ) { if ( currentEnemy == ent ) { gi.printf( "*" ); } else { gi.printf( " " ); } gi.printf( "%d : '%s'", ent->entnum, ent->targetname.c_str() ); gi.printf( "\n" ); } } } gi.printf( "seenEnemy: %d\n", seenEnemy ); gi.printf( "actortype: %d\n", actortype ); gi.printf( "deadflag: %d\n", deadflag ); gi.printf( "\n" ); if ( behavior ) { gi.printf( "Behavior Info:\n" ); gi.printf( "Game time: %f\n", level.time ); behavior->ShowInfo( *this ); gi.printf( "\n" ); } } //*********************************************************************************************** // // General functions // //*********************************************************************************************** void Actor::Chatter ( const char *snd, float chance, float volume, int channel ) { str realname; if ( nochatter || chattime > level.time ) { return; } if ( G_Random( 10 ) > chance ) { chattime = level.time + 1 + G_Random( 2 ); return; } realname = GetRandomAlias( snd ); if ( realname.length() > 1 ) { float delay; delay = gi.SoundLength( realname.c_str() ); chattime = level.time + delay + 4 + G_Random( 5 ); sound( realname, volume, channel ); } else { // set it into the future, so we don't check it again right away chattime = level.time + 1; } } void Actor::ActivateEvent ( Event *ev ) { Entity *ent; if ( ( deadflag ) && ( actortype != IS_INANIMATE ) ) { return; } ent = ev->GetEntity( 1 ); SetVariable( "other", ent ); DoAction( "activate" ); } void Actor::UseEvent ( Event *ev ) { Entity *ent; if ( ( deadflag ) && ( actortype != IS_INANIMATE ) ) { return; } ent = ev->GetEntity( 1 ); SetVariable( "other", ent ); DoAction( "use" ); } void Actor::Prethink ( void ) { range_t range; Event *event; assert( actorthread ); if ( !actorthread ) { // not having a script is bad gi.dprintf( "Null actorthread. Killing actor '%s'(%d).\n", targetname.c_str(), entnum ); // just kill him event = new Event( EV_Killed ); event->AddEntity( this ); event->AddInteger( 0 ); event->AddEntity( this ); event->AddString( "all" ); ProcessEvent( event ); return; } if ( hidden() ) { // Don't think while hidden, you're just a cinematic. return; } if ( actortype == IS_INANIMATE ) { if ( behavior && !behavior->Evaluate( *this ) ) { // stop thinking flags &= ~FL_PRETHINK; EndBehavior(); } return; } if ( currentEnemy ) { if ( currentEnemy->deadflag ) { DoAction( "enemydead" ); currentEnemy = NULL; seenEnemy = false; } else { range = Range( currentEnemy ); if ( ( lastEnemy != currentEnemy ) || ( range != enemyRange ) ) { lastEnemy = currentEnemy; enemyRange = range; SetVariable( "other", currentEnemy ); switch( range ) { case RANGE_MELEE : DoAction( "range_melee" ); break; case RANGE_NEAR : DoAction( "range_near" ); break; case RANGE_MID : DoAction( "range_mid" ); break; case RANGE_FAR : DoAction( "range_far" ); break; } } } } else { lastEnemy = NULL; enemyRange = RANGE_FAR; } eyeposition[ 2 ] = maxs[ 2 ] + eyeoffset[ 2 ]; angles.z = 0; #ifdef DEBUG_PRINT gi.dprintf( "stack %d : %s : %s\n", numonstack, behavior ? behavior->getClassname() : "", animname.c_str() ); #endif if ( behavior && !behavior->Evaluate( *this ) ) { EndBehavior(); } if ( newanimnum != -1 ) { ChangeAnim(); } CalcMove(); lastmove = STEPMOVE_STUCK; if ( flags & FL_SWIM ) { lastmove = WaterMove(); } else if ( flags & FL_FLY ) { lastmove = AirMove(); } else { lastmove = TryMove(); } // // see if we should damage the actor because of waterlevel // if ( waterlevel == 3 && !( flags & FL_SWIM ) ) { // if out of air, start drowning if ( air_finished < level.time ) { // we may have been in a water brush when we spawned, so check our water level again to be sure CheckWater(); if ( waterlevel < 3 ) { // we're ok, so reset our air air_finished = level.time + 5; } else if ( next_drown_time < level.time && health > 0 ) { // drown! next_drown_time = level.time + 1; RandomGlobalSound( "snd_uwchoke", 1, CHAN_VOICE, ATTN_NORM ); ProcessEvent( EV_PainSound ); Damage( world, world, 15, worldorigin, vec_zero, vec_zero, 0, DAMAGE_NO_ARMOR, MOD_DROWN, -1, -1, 1.0f ); } } } else { air_finished = level.time + 5; } G_TouchTriggers( this ); if ( groundentity && ( groundentity->entity != world ) && !M_CheckBottom( this ) ) { // G_FixCheckBottom( this ); flags |= FL_PARTIALGROUND; } } /*****************************************************************************/ /*SINED info_monster_spawnspot (1 0 0) (-16 -16 0) (16 16 64) Potential spawn location for a monster. "spawngroup" - the name of the group of spawn spots that this one belongs to. Monsters use spawngroup to choose which spawn spots to spawn at. "angle" - the orientation for the monster to spawn in. "angles" - the full ( x, y, z ) orientation for the monster to spawn in. "anim" - the animation that the monster should use when spawning here. /*****************************************************************************/ CLASS_DECLARATION( PathNode, MonsterStart, "info_monster_spawnspot" ); ResponseDef MonsterStart::Responses[] = { { NULL, NULL } }; MonsterStart::MonsterStart() { spawngroup = G_GetSpawnArg( "spawngroup", "" ); leader = NULL; leader = GetGroupLeader( spawngroup ); if ( !leader ) { leader = this; groupchain = NULL; } else { groupchain = leader->groupchain; leader->groupchain = this; } } MonsterStart *MonsterStart::GetGroupLeader ( str& groupname ) { Entity *ent; MonsterStart *m; for( ent = world; ent; ent = G_NextEntity( ent ) ) { if ( ent->isSubclassOf( MonsterStart ) ) { m = ( MonsterStart * )ent; if ( ( m->spawngroup == groupname ) && m->leader ) { return m->leader; } } } return NULL; } MonsterStart *MonsterStart::GetRandomSpot ( str& groupname ) { MonsterStart *leader; MonsterStart *m; int num; int i; int j; leader = GetGroupLeader( groupname ); if ( !leader ) { return NULL; } num = 0; for( m = leader; m != NULL; m = m->groupchain ) { num++; } j = ( int )G_Random( num ); for( m = leader, i = 0; m != NULL; m = m->groupchain, i++ ) { if ( i == j ) { break; } } return m; }