5 float MONSTER_WANDER = 64; // disable wandering around
\r
6 float MONSTER_APPEAR = 128; // spawn invisible, and appear when triggered
\r
9 .float monsterawaitingteleport; // avoid awaking monsters in teleport rooms
\r
11 // when a monster becomes angry at a player, that monster will be used
\r
12 // as the sight target the next frame so that monsters near that one
\r
13 // will wake up even if they wouldn't have noticed the player
\r
15 entity sight_entity;
\r
16 float sight_entity_time;
\r
21 Will be world if not currently angry at anyone.
\r
24 The next path spot to walk toward. If .enemy, ignore .movetarget.
\r
25 When an enemy is killed, the monster will try to return to it's path.
\r
28 Set to time + something when the player is in sight, but movement straight for
\r
29 him is blocked. This causes the monster to use wall following code for
\r
30 movement direction instead of sighting on the player.
\r
33 A yaw angle of the intended direction, which will be turned towards at up
\r
34 to 45 deg / state. If the enemy is in view and hunt_time is not active,
\r
35 this will be the exact line towards the enemy.
\r
38 A monster will leave it's stand state and head towards it's .movetarget when
\r
41 walkmove(angle, speed) primitive is all or nothing
\r
48 //float current_yaw;
\r
50 float(float v) anglemod =
\r
52 v = v - 360 * floor(v / 360);
\r
57 ==============================================================================
\r
61 The angle of the movetarget effects standing and bowing direction, but has no effect on movement, which allways heads to the next target.
\r
64 must be present. The name of this movetarget.
\r
67 the next spot to move to. If not present, stop here for good.
\r
70 The number of seconds to spend standing or bowing for path_stand or path_bow
\r
72 ==============================================================================
\r
76 void() movetarget_f =
\r
78 if (!self.targetname)
\r
79 objerror ("monster_movetarget: no targetname");
\r
81 self.solid = SOLID_TRIGGER;
\r
82 self.touch = t_movetarget;
\r
83 setsize (self, '-8 -8 -8', '8 8 8');
\r
86 /*QUAKED path_corner (0.5 0.3 0) (-8 -8 -8) (8 8 8)
\r
87 Monsters will continue walking towards the next target corner.
\r
89 void() path_corner =
\r
98 Something has bumped into a movetarget. If it is a monster
\r
99 moving towards it, change the next destination and continue.
\r
102 void() t_movetarget =
\r
106 if (other.health < 1)
\r
108 if (other.movetarget != self)
\r
112 return; // fighting, not following a path
\r
118 if (self.classname == "monster_ogre")
\r
119 sound (self, CHAN_VOICE, "ogre/ogdrag.wav", 1, ATTN_IDLE);// play chainsaw drag sound
\r
121 //dprint ("t_movetarget\n");
\r
122 self.goalentity = self.movetarget = find (world, targetname, other.target);
\r
123 self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin);
\r
124 if (!self.movetarget)
\r
126 self.pausetime = time + 999999;
\r
132 void() monster_wanderpaththink =
\r
134 local vector v, v1;
\r
136 self.nextthink = time + random() * 10 + 1;
\r
137 if (self.owner.health < 1) // dead, also handled in death code
\r
139 self.owner.movetarget = world;
\r
149 traceline(self.owner.origin, v * 1024 + self.owner.origin, FALSE, self);
\r
150 v = trace_endpos - (normalize(v) * 16) - self.owner.origin;
\r
157 setorigin(self, v1 + self.owner.origin);
\r
158 self.owner.ideal_yaw = vectoyaw(self.origin - self.owner.origin);
\r
161 void() monster_wanderpathtouch =
\r
163 if (other.health < 1)
\r
165 if (other.movetarget != self)
\r
169 return; // fighting, not following a path
\r
171 if (other.classname == "monster_ogre")
\r
172 sound (other, CHAN_VOICE, "ogre/ogdrag.wav", 1, ATTN_IDLE);// play chainsaw drag sound
\r
173 monster_wanderpaththink();
\r
176 void() monster_spawnwanderpath =
\r
179 newmis.classname = "monster_wanderpath";
\r
180 newmis.solid = SOLID_TRIGGER;
\r
181 newmis.touch = monster_wanderpathtouch;
\r
182 setsize (newmis, '-8 -8 -8', '8 8 8');
\r
183 newmis.think = monster_wanderpaththink;
\r
184 newmis.nextthink = time + random() * 10 + 1;
\r
185 newmis.owner = self;
\r
186 self.goalentity = self.movetarget = newmis;
\r
189 void() monster_checkbossflag =
\r
192 local float healthboost;
\r
195 // monsterbosses cvar or spawnflag 64 causes a monster to be a miniboss
\r
196 if ((self.spawnflags & 64) || (random() * 100 < cvar("monsterbosspercent")))
\r
198 self.radsuit_finished = time + 1000000000;
\r
202 self.super_damage_finished = time + 1000000000;
\r
203 healthboost = 30 + self.health * 0.5;
\r
204 self.effects = self.effects | (EF_FULLBRIGHT | EF_BLUE);
\r
208 healthboost = 30 + self.health * bound(0.5, skill * 0.5, 1.5);
\r
209 self.effects = self.effects | (EF_FULLBRIGHT | EF_RED);
\r
210 self.healthregen = max(self.healthregen, min(skill * 10, 30));
\r
212 self.health = self.health + healthboost;
\r
213 self.max_health = self.health;
\r
214 self.bodyhealth = self.bodyhealth * 2 + healthboost;
\r
217 self.colormod_x = random();
\r
218 self.colormod_y = random();
\r
219 self.colormod_z = random();
\r
220 self.colormod = normalize(self.colormod);
\r
222 while (self.colormod_x > 0.6 && self.colormod_y > 0.6 && self.colormod_z > 0.6);
\r
228 //============================================================================
\r
234 returns the range catagorization of an entity reletive to self
\r
235 0 melee range, will become hostile even if back is turned
\r
236 1 visibility and infront, or visibility and show hostile
\r
237 2 infront and show hostile
\r
238 3 only triggered by damage
\r
241 float(entity targ) range =
\r
244 r = vlen ((self.origin + self.view_ofs) - (targ.origin + targ.view_ofs));
\r
246 return RANGE_MELEE;
\r
249 if (r < 2000) // increased from 1000 for DP
\r
258 returns 1 if the entity is visible to self, even if not infront ()
\r
261 float (entity targ) visible =
\r
263 if (vlen(targ.origin - self.origin) > 5000) // long traces are slow
\r
266 traceline ((self.origin + self.view_ofs), (targ.origin + targ.view_ofs), TRUE, self); // see through other monsters
\r
268 if (trace_inopen && trace_inwater)
\r
269 return FALSE; // sight line crossed contents
\r
271 if (trace_fraction == 1)
\r
281 returns 1 if the entity is in front (in sight) of self
\r
284 float(entity targ) infront =
\r
288 makevectors (self.angles);
\r
289 dot = normalize (targ.origin - self.origin) * v_forward;
\r
291 return (dot > 0.3);
\r
293 // returns 0 if not infront, or the dotproduct if infront
\r
294 float(vector dir, entity targ) infront2 =
\r
298 dir = normalize(dir);
\r
299 dot = normalize (targ.origin - self.origin) * dir;
\r
301 if (dot >= 0.3) return dot; // infront
\r
306 //============================================================================
\r
312 Turns towards self.ideal_yaw at self.yaw_speed
\r
313 Sets the global variable current_yaw
\r
314 Called every 0.1 sec by monsters
\r
321 local float ideal, move;
\r
323 //current_yaw = self.ideal_yaw;
\r
324 // mod down the current angle
\r
325 current_yaw = anglemod( self.angles_y );
\r
326 ideal = self.ideal_yaw;
\r
328 if (current_yaw == ideal)
\r
331 move = ideal - current_yaw;
\r
332 if (ideal > current_yaw)
\r
345 if (move > self.yaw_speed)
\r
346 move = self.yaw_speed;
\r
350 if (move < 0-self.yaw_speed )
\r
351 move = 0-self.yaw_speed;
\r
354 current_yaw = anglemod (current_yaw + move);
\r
356 self.angles_y = current_yaw;
\r
362 //============================================================================
\r
364 void() HuntTarget =
\r
366 self.goalentity = self.enemy;
\r
367 self.think = self.th_run;
\r
368 self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin);
\r
369 self.nextthink = time + 0.1;
\r
370 SUB_AttackFinished (1); // wait a while before first attack
\r
373 .void() th_sightsound;
\r
375 void() SightSound =
\r
377 if (self.health < 1)
\r
379 // skill 5 does not play sight sounds, instead you only hear the appear sound as they are about to attack
\r
381 if (self.classname != "monster_hellfish")
\r
384 if (self.th_sightsound)
\r
385 self.th_sightsound();
\r
388 void() FoundTarget =
\r
390 if (self.health < 1 || !self.th_run)
\r
392 if (self.enemy.health < 1 || !self.enemy.takedamage)
\r
394 if (self.enemy.classname == "player")
\r
396 // let other monsters see this monster for a while
\r
397 sight_entity = self;
\r
398 sight_entity_time = time + 0.1;
\r
401 self.show_hostile = time + 1; // wake up other monsters
\r
408 //float checkplayertime;
\r
409 entity lastcheckplayer;
\r
410 entity havocbot_list;
\r
413 entity() checkplayer =
\r
415 local entity check;
\r
416 local float worldcount;
\r
417 // we can just fallback on checkclient if there are no bots
\r
418 if (!havocbot_list)
\r
419 return checkclient();
\r
422 if (time < checkplayertime)
\r
424 traceline(self.origin + self.view_ofs, lastcheckplayer.origin + lastcheckplayer.view_ofs, TRUE, self);
\r
425 if (trace_fraction == 1)
\r
426 return lastcheckplayer;
\r
427 if (trace_ent == lastcheckplayer)
\r
428 return lastcheckplayer;
\r
430 checkplayertime = time + 0.1;
\r
433 check = lastcheckplayer;
\r
439 check = findfloat(check, havocattack, TRUE);
\r
440 if (check.classname == "player" || check.classname == "turretbase")
\r
442 traceline(self.origin + self.view_ofs, check.origin + check.view_ofs, TRUE, self);
\r
443 if (trace_fraction == 1)
\r
444 return lastcheckplayer = check;
\r
445 if (trace_ent == check)
\r
446 return lastcheckplayer = check;
\r
448 else if (check == world)
\r
450 worldcount = worldcount + 1;
\r
451 if (worldcount >= 2)
\r
452 return lastcheckplayer = check;
\r
455 while(check != lastcheckplayer && c < 100);
\r
464 Self is currently not attacking anything, so try to find a target
\r
466 Returns TRUE if an enemy was sighted
\r
468 When a player fires a missile, the point of impact becomes a fakeplayer so
\r
469 that monsters that see the impact will respond as if they had seen the
\r
472 To avoid spending too much time, only a single client (or fakeclient) is
\r
473 checked each frame. This means multi player games will have slightly
\r
474 slower noticing monsters.
\r
478 float() FindTarget =
\r
480 local entity client;
\r
483 if (self.health < 1)
\r
486 // if the first or second spawnflag bit is set, the monster will only
\r
487 // wake up on really seeing the player, not another monster getting angry
\r
489 if (self.spawnflags & 3)
\r
491 // don't wake up on seeing another monster getting angry
\r
492 client = checkclient ();
\r
494 return FALSE; // current check entity isn't in PVS
\r
498 if (sight_entity_time >= time)
\r
500 client = sight_entity;
\r
501 if (client.enemy == self.enemy)
\r
506 client = checkclient ();
\r
508 return FALSE; // current check entity isn't in PVS
\r
512 if (client == self.enemy)
\r
515 if (client.flags & FL_NOTARGET)
\r
519 if (client.items & IT_INVISIBILITY)
\r
523 // on skill 5 the monsters usually ignore the player and remain ghostlike
\r
525 if (self.classname != "monster_hellfish")
\r
526 if (random() < 0.99)
\r
530 if (r == RANGE_FAR)
\r
533 if (!visible (client))
\r
536 if (r == RANGE_NEAR)
\r
538 if (client.show_hostile < time && !infront (client))
\r
541 else if (r == RANGE_MID)
\r
543 // LordHavoc: was if ( /* client.show_hostile < time || */ !infront (client))
\r
544 if (client.show_hostile < time && !infront (client))
\r
552 if (client.model == "")
\r
554 self.enemy = client;
\r
555 if (self.enemy.classname != "player" && self.enemy.classname != "turretbase")
\r
557 self.enemy = self.enemy.enemy;
\r
558 if (self.enemy.classname != "player" && self.enemy.classname != "turretbase")
\r
560 self.enemy = world;
\r
571 //=============================================================================
\r
573 void(float dist) ai_forward =
\r
575 walkmove (self.angles_y, dist);
\r
578 void(float dist) ai_back =
\r
580 walkmove ( (self.angles_y+180), dist);
\r
584 void(float a) monster_setalpha;
\r
593 void(float dist) ai_pain =
\r
595 if (self.health < 1)
\r
607 void(float dist) ai_painforward =
\r
609 if (self.health < 1)
\r
611 walkmove (self.ideal_yaw, dist);
\r
618 The monster is walking it's beat
\r
621 void(float dist) ai_walk =
\r
623 if (self.health < 1)
\r
628 // check for noticing a player
\r
629 if (self.oldenemy.takedamage)
\r
630 if (self.oldenemy.health >= 1)
\r
632 self.enemy = self.oldenemy;
\r
633 self.oldenemy = world;
\r
635 monster_setalpha(0);
\r
640 if (self.enemy.takedamage)
\r
642 if (self.enemy.health >= 1)
\r
645 monster_setalpha(0);
\r
649 self.enemy = world;
\r
652 self.enemy = world;
\r
655 self.findtarget = TRUE;
\r
658 monster_setalpha(0);
\r
666 The monster is staying in one place for a while, with slight angle turns
\r
671 if (self.health < 1)
\r
675 if (self.enemy.takedamage)
\r
677 if (self.enemy.health >= 1)
\r
680 monster_setalpha(0);
\r
684 self.enemy = world;
\r
687 self.enemy = world;
\r
689 self.findtarget = TRUE;
\r
691 if (time > self.pausetime)
\r
694 monster_setalpha(0);
\r
698 // change angle slightly
\r
700 monster_setalpha(0);
\r
707 don't move, but turn towards ideal_yaw
\r
714 if (self.enemy.takedamage)
\r
716 if (self.enemy.health >= 1)
\r
719 monster_setalpha(0);
\r
723 self.enemy = world;
\r
726 self.enemy = world;
\r
728 self.findtarget = TRUE;
\r
731 monster_setalpha(0);
\r
734 //=============================================================================
\r
741 void(vector pDestvec) ChooseTurn =
\r
743 local vector dir, newdir;
\r
745 dir = self.origin - pDestvec;
\r
747 newdir_x = trace_plane_normal_y;
\r
748 newdir_y = 0 - trace_plane_normal_x;
\r
751 if (dir * newdir > 0)
\r
753 dir_x = 0 - trace_plane_normal_y;
\r
754 dir_y = trace_plane_normal_x;
\r
758 dir_x = trace_plane_normal_y;
\r
759 dir_y = 0 - trace_plane_normal_x;
\r
763 self.ideal_yaw = vectoyaw(dir);
\r
772 float() FacingIdeal =
\r
776 delta = anglemod(self.angles_y - self.ideal_yaw);
\r
777 if (delta > 45 && delta < 315)
\r
783 //=============================================================================
\r
785 .float() th_checkattack;
\r
793 The monster has an enemy it is trying to kill
\r
796 void(float dist) ai_run =
\r
799 if (self.health < 1)
\r
802 // see if the enemy is dead
\r
803 if (self.enemy.health < 1 || self.enemy.takedamage == DAMAGE_NO)
\r
805 self.enemy = world;
\r
806 // FIXME: look all around for other targets
\r
807 if (self.oldenemy.health >= 1 && self.oldenemy.takedamage)
\r
809 self.enemy = self.oldenemy;
\r
810 self.oldenemy = world;
\r
815 if (self.movetarget)
\r
823 // wake up other monsters
\r
824 self.show_hostile = time + 1;
\r
826 // check knowledge of enemy
\r
827 enemy_range = range(self.enemy);
\r
829 self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin);
\r
832 if (self.attack_state == AS_MELEE)
\r
834 //dprint ("ai_run_melee\n");
\r
835 //Turn and close until within an angle to launch a melee attack
\r
839 self.attack_state = AS_STRAIGHT;
\r
843 else if (self.attack_state == AS_MISSILE)
\r
845 //dprint ("ai_run_missile\n");
\r
846 //Turn in place until within an angle to launch a missile attack
\r
848 if (self.th_missile ())
\r
849 self.attack_state = AS_STRAIGHT;
\r
853 if (self.th_checkattack())
\r
854 return; // beginning an attack
\r
856 if (visible(self.enemy))
\r
857 self.search_time = time + 5;
\r
860 // look for other coop players
\r
861 if (self.search_time < time)
\r
862 self.findtarget = TRUE;
\r
865 if (self.attack_state == AS_SLIDING)
\r
867 //dprint ("ai_run_slide\n");
\r
868 //Strafe sideways, but stay at aproximately the same range
\r
874 if (walkmove (self.ideal_yaw + ofs, movedist))
\r
877 self.lefty = !self.lefty;
\r
879 walkmove (self.ideal_yaw - ofs, movedist);
\r
882 // head straight in
\r
883 movetogoal (dist); // done in C code...
\r