23330cdfc22a8f24ddf65ee3344655538a44db26
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / monsters / ai.qc
1 void() movetarget_f;
2 void() t_movetarget;
3 void() FoundTarget;
4
5 float MONSTER_WANDER = 64; // disable wandering around
6 float MONSTER_APPEAR = 128; // spawn invisible, and appear when triggered
7
8 .float ismonster;
9 .float monsterawaitingteleport; // avoid awaking monsters in teleport rooms
10
11 // when a monster becomes angry at a player, that monster will be used
12 // as the sight target the next frame so that monsters near that one
13 // will wake up even if they wouldn't have noticed the player
14 //
15 entity sight_entity;
16 float sight_entity_time;
17
18 /*
19
20 .enemy
21 Will be world if not currently angry at anyone.
22
23 .movetarget
24 The next path spot to walk toward.  If .enemy, ignore .movetarget.
25 When an enemy is killed, the monster will try to return to it's path.
26
27 .huntt_ime
28 Set to time + something when the player is in sight, but movement straight for
29 him is blocked.  This causes the monster to use wall following code for
30 movement direction instead of sighting on the player.
31
32 .ideal_yaw
33 A yaw angle of the intended direction, which will be turned towards at up
34 to 45 deg / state.  If the enemy is in view and hunt_time is not active,
35 this will be the exact line towards the enemy.
36
37 .pausetime
38 A monster will leave it's stand state and head towards it's .movetarget when
39 time > .pausetime.
40
41 walkmove(angle, speed) primitive is all or nothing
42 */
43
44
45 //
46 // globals
47 //
48 //float current_yaw;
49
50 float(float v) anglemod =
51 {
52         v = v - 360 * floor(v / 360);
53         return v;
54 };
55
56 /*
57 ==============================================================================
58
59 MOVETARGET CODE
60
61 The angle of the movetarget effects standing and bowing direction, but has no effect on movement, which allways heads to the next target.
62
63 targetname
64 must be present.  The name of this movetarget.
65
66 target
67 the next spot to move to.  If not present, stop here for good.
68
69 pausetime
70 The number of seconds to spend standing or bowing for path_stand or path_bow
71
72 ==============================================================================
73 */
74
75
76 void() movetarget_f =
77 {
78         if (!self.targetname)
79                 objerror ("monster_movetarget: no targetname");
80
81         self.solid = SOLID_TRIGGER;
82         self.touch = t_movetarget;
83         setsize (self, '-8 -8 -8', '8 8 8');
84 };
85
86 /*QUAKED path_corner (0.5 0.3 0) (-8 -8 -8) (8 8 8)
87 Monsters will continue walking towards the next target corner.
88 */
89 void() path_corner =
90 {
91         movetarget_f ();
92 };
93
94 /*
95 =============
96 t_movetarget
97
98 Something has bumped into a movetarget.  If it is a monster
99 moving towards it, change the next destination and continue.
100 ==============
101 */
102 void() t_movetarget =
103 {
104         local entity temp;
105
106         if (other.health < 1)
107                 return;
108         if (other.movetarget != self)
109                 return;
110
111         if (other.enemy)
112                 return;         // fighting, not following a path
113
114         temp = self;
115         self = other;
116         other = temp;
117
118         if (self.classname == "monster_ogre")
119                 sound (self, CHAN_VOICE, "ogre/ogdrag.wav", 1, ATTN_IDLE);// play chainsaw drag sound
120
121 //dprint ("t_movetarget\n");
122         self.goalentity = self.movetarget = find (world, targetname, other.target);
123         self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin);
124         if (!self.movetarget)
125         {
126                 self.pausetime = time + 999999;
127                 self.th_stand ();
128                 return;
129         }
130 };
131
132 void() monster_wanderpaththink =
133 {
134         local vector v, v1;
135         local float b, c;
136         self.nextthink = time + random() * 10 + 1;
137         if (self.owner.health < 1) // dead, also handled in death code
138         {
139                 self.owner.movetarget = world;
140                 remove(self);
141                 return;
142         }
143         b = -1;
144         c = 10;
145         while (c > 0)
146         {
147                 c = c - 1;
148                 v = randomvec();
149                 traceline(self.owner.origin, v * 1024 + self.owner.origin, FALSE, self);
150                 v = trace_endpos - (normalize(v) * 16) - self.owner.origin;
151                 if (vlen(v) > b)
152                 {
153                         b = vlen(v);
154                         v1 = v;
155                 }
156         }
157         setorigin(self, v1 + self.owner.origin);
158         self.owner.ideal_yaw = vectoyaw(self.origin - self.owner.origin);
159 };
160
161 void() monster_wanderpathtouch =
162 {
163         if (other.health < 1)
164                 return;
165         if (other.movetarget != self)
166                 return;
167
168         if (other.enemy)
169                 return;         // fighting, not following a path
170
171         if (other.classname == "monster_ogre")
172                 sound (other, CHAN_VOICE, "ogre/ogdrag.wav", 1, ATTN_IDLE);// play chainsaw drag sound
173         monster_wanderpaththink();
174 };
175
176 void() monster_spawnwanderpath =
177 {
178         newmis = spawn();
179         newmis.classname = "monster_wanderpath";
180         newmis.solid = SOLID_TRIGGER;
181         newmis.touch = monster_wanderpathtouch;
182         setsize (newmis, '-8 -8 -8', '8 8 8');
183         newmis.think = monster_wanderpaththink;
184         newmis.nextthink = time + random() * 10 + 1;
185         newmis.owner = self;
186         self.goalentity = self.movetarget = newmis;
187 };
188
189 void() monster_checkbossflag =
190 {
191 #if 0
192         local float healthboost;
193         local float r;
194
195         // monsterbosses cvar or spawnflag 64 causes a monster to be a miniboss
196         if ((self.spawnflags & 64) || (random() * 100 < cvar("monsterbosspercent")))
197         {
198                 self.radsuit_finished = time + 1000000000;
199                 r = random() * 4;
200                 if (r < 2)
201                 {
202                         self.super_damage_finished = time + 1000000000;
203                         healthboost = 30 + self.health * 0.5;
204                         self.effects = self.effects | (EF_FULLBRIGHT | EF_BLUE);
205                 }
206                 if (r >= 1)
207                 {
208                         healthboost = 30 + self.health * bound(0.5, skill * 0.5, 1.5);
209                         self.effects = self.effects | (EF_FULLBRIGHT | EF_RED);
210                         self.healthregen = max(self.healthregen, min(skill * 10, 30));
211                 }
212                 self.health = self.health + healthboost;
213                 self.max_health = self.health;
214                 self.bodyhealth = self.bodyhealth * 2 + healthboost;
215                 do
216                 {
217                         self.colormod_x = random();
218                         self.colormod_y = random();
219                         self.colormod_z = random();
220                         self.colormod =  normalize(self.colormod);
221                 }
222                 while (self.colormod_x > 0.6 && self.colormod_y > 0.6 && self.colormod_z > 0.6);
223         }
224 #endif
225 };
226
227
228 //============================================================================
229
230 /*
231 =============
232 range
233
234 returns the range catagorization of an entity reletive to self
235 0       melee range, will become hostile even if back is turned
236 1       visibility and infront, or visibility and show hostile
237 2       infront and show hostile
238 3       only triggered by damage
239 =============
240 */
241 float(entity targ) range =
242 {
243         local float r;
244         r = vlen ((self.origin + self.view_ofs) - (targ.origin + targ.view_ofs));
245         if (r < 120)
246                 return RANGE_MELEE;
247         if (r < 500)
248                 return RANGE_NEAR;
249         if (r < 2000) // increased from 1000 for DP
250                 return RANGE_MID;
251         return RANGE_FAR;
252 };
253
254 /*
255 =============
256 visible
257
258 returns 1 if the entity is visible to self, even if not infront ()
259 =============
260 */
261 float (entity targ) visible =
262 {
263         if (vlen(targ.origin - self.origin) > 5000) // long traces are slow
264                 return FALSE;
265
266         traceline ((self.origin + self.view_ofs), (targ.origin + targ.view_ofs), TRUE, self);   // see through other monsters
267
268         if (trace_inopen && trace_inwater)
269                 return FALSE;                   // sight line crossed contents
270
271         if (trace_fraction == 1)
272                 return TRUE;
273         return FALSE;
274 };
275
276
277 /*
278 =============
279 infront
280
281 returns 1 if the entity is in front (in sight) of self
282 =============
283 */
284 float(entity targ) infront =
285 {
286         local float dot;
287
288         makevectors (self.angles);
289         dot = normalize (targ.origin - self.origin) * v_forward;
290
291         return (dot > 0.3);
292 };
293 // returns 0 if not infront, or the dotproduct if infront
294 float(vector dir, entity targ) infront2 =
295 {
296         local float dot;
297
298         dir = normalize(dir);
299         dot = normalize (targ.origin - self.origin) * dir;
300
301         if (dot >= 0.3) return dot; // infront
302         return 0;
303 };
304
305
306 //============================================================================
307
308 /*
309 ===========
310 ChangeYaw
311
312 Turns towards self.ideal_yaw at self.yaw_speed
313 Sets the global variable current_yaw
314 Called every 0.1 sec by monsters
315 ============
316 */
317 /*
318
319 void() ChangeYaw =
320 {
321         local float ideal, move;
322
323 //current_yaw = self.ideal_yaw;
324 // mod down the current angle
325         current_yaw = anglemod( self.angles_y );
326         ideal = self.ideal_yaw;
327
328         if (current_yaw == ideal)
329                 return;
330
331         move = ideal - current_yaw;
332         if (ideal > current_yaw)
333         {
334                 if (move > 180)
335                         move = move - 360;
336         }
337         else
338         {
339                 if (move < -180)
340                         move = move + 360;
341         }
342
343         if (move > 0)
344         {
345                 if (move > self.yaw_speed)
346                         move = self.yaw_speed;
347         }
348         else
349         {
350                 if (move < 0-self.yaw_speed )
351                         move = 0-self.yaw_speed;
352         }
353
354         current_yaw = anglemod (current_yaw + move);
355
356         self.angles_y = current_yaw;
357 };
358
359 */
360
361
362 //============================================================================
363
364 void() HuntTarget =
365 {
366         self.goalentity = self.enemy;
367         self.think = self.th_run;
368         self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin);
369         self.nextthink = time + 0.1;
370         SUB_AttackFinished (1); // wait a while before first attack
371 };
372
373 .void() th_sightsound;
374
375 void() SightSound =
376 {
377         if (self.health < 1)
378                 return;
379         // skill 5 does not play sight sounds, instead you only hear the appear sound as they are about to attack
380         if (skill >= 5)
381         if (self.classname != "monster_hellfish")
382                 return;
383
384         if (self.th_sightsound)
385                 self.th_sightsound();
386 };
387
388 void() FoundTarget =
389 {
390         if (self.health < 1 || !self.th_run)
391                 return;
392         if (self.enemy.health < 1 || !self.enemy.takedamage)
393                 return;
394         if (self.enemy.classname == "player")
395         {
396                 // let other monsters see this monster for a while
397                 sight_entity = self;
398                 sight_entity_time = time + 0.1;
399         }
400
401         self.show_hostile = time + 1;           // wake up other monsters
402
403         SightSound ();
404         HuntTarget ();
405 };
406
407 /*
408 //float checkplayertime;
409 entity lastcheckplayer;
410 entity havocbot_list;
411
412
413 entity() checkplayer =
414 {
415         local entity check;
416         local float worldcount;
417         // we can just fallback on checkclient if there are no bots
418         if (!havocbot_list)
419                 return checkclient();
420 */
421         /*
422         if (time < checkplayertime)
423         {
424                 traceline(self.origin + self.view_ofs, lastcheckplayer.origin + lastcheckplayer.view_ofs, TRUE, self);
425                 if (trace_fraction == 1)
426                         return lastcheckplayer;
427                 if (trace_ent == lastcheckplayer)
428                         return lastcheckplayer;
429         }
430         checkplayertime = time + 0.1;
431         */
432 /*
433         check = lastcheckplayer;
434         worldcount = 0;
435         c = 0;
436         do
437         {
438                 c = c + 1;
439                 check = findfloat(check, havocattack, TRUE);
440                 if (check.classname == "player" || check.classname == "turretbase")
441                 {
442                         traceline(self.origin + self.view_ofs, check.origin + check.view_ofs, TRUE, self);
443                         if (trace_fraction == 1)
444                                 return lastcheckplayer = check;
445                         if (trace_ent == check)
446                                 return lastcheckplayer = check;
447                 }
448                 else if (check == world)
449                 {
450                         worldcount = worldcount + 1;
451                         if (worldcount >= 2)
452                                 return lastcheckplayer = check;
453                 }
454         }
455         while(check != lastcheckplayer && c < 100);
456         return world;
457 };
458 */
459
460 /*
461 ===========
462 FindTarget
463
464 Self is currently not attacking anything, so try to find a target
465
466 Returns TRUE if an enemy was sighted
467
468 When a player fires a missile, the point of impact becomes a fakeplayer so
469 that monsters that see the impact will respond as if they had seen the
470 player.
471
472 To avoid spending too much time, only a single client (or fakeclient) is
473 checked each frame.  This means multi player games will have slightly
474 slower noticing monsters.
475 ============
476 */
477 .float findtarget;
478 float() FindTarget =
479 {
480         local entity client;
481         local float r;
482
483         if (self.health < 1)
484                 return FALSE;
485
486         // if the first or second spawnflag bit is set, the monster will only
487         // wake up on really seeing the player, not another monster getting angry
488
489         if (self.spawnflags & 3)
490         {
491                 // don't wake up on seeing another monster getting angry
492                 client = checkclient ();
493                 if (!client)
494                         return FALSE;   // current check entity isn't in PVS
495         }
496         else
497         {
498                 if (sight_entity_time >= time)
499                 {
500                         client = sight_entity;
501                         if (client.enemy == self.enemy)
502                                 return TRUE;
503                 }
504                 else
505                 {
506                         client = checkclient ();
507                         if (!client)
508                                 return FALSE;   // current check entity isn't in PVS
509                 }
510         }
511
512         if (client == self.enemy)
513                 return FALSE;
514
515         if (client.flags & FL_NOTARGET)
516                 return FALSE;
517
518 #if 0
519         if (client.items & IT_INVISIBILITY)
520                 return FALSE;
521 #endif
522
523         // on skill 5 the monsters usually ignore the player and remain ghostlike
524         if (skill >= 5)
525         if (self.classname != "monster_hellfish")
526         if (random() < 0.99)
527                 return FALSE;
528
529         r = range(client);
530         if (r == RANGE_FAR)
531                 return FALSE;
532
533         if (!visible (client))
534                 return FALSE;
535
536         if (r == RANGE_NEAR)
537         {
538                 if (client.show_hostile < time && !infront (client))
539                         return FALSE;
540         }
541         else if (r == RANGE_MID)
542         {
543                 // LordHavoc: was if ( /* client.show_hostile < time || */ !infront (client))
544                 if (client.show_hostile < time && !infront (client))
545                         return FALSE;
546         }
547
548         //
549         // got one
550         //
551
552         if (client.model == "")
553                 return FALSE;
554         self.enemy = client;
555         if (self.enemy.classname != "player" && self.enemy.classname != "turretbase")
556         {
557                 self.enemy = self.enemy.enemy;
558                 if (self.enemy.classname != "player" && self.enemy.classname != "turretbase")
559                 {
560                         self.enemy = world;
561                         return FALSE;
562                 }
563         }
564
565         FoundTarget ();
566
567         return TRUE;
568 };
569
570
571 //=============================================================================
572
573 void(float dist) ai_forward =
574 {
575         walkmove (self.angles_y, dist);
576 };
577
578 void(float dist) ai_back =
579 {
580         walkmove ( (self.angles_y+180), dist);
581 };
582
583
584 void(float a) monster_setalpha;
585
586 /*
587 =============
588 ai_pain
589
590 stagger back a bit
591 =============
592 */
593 void(float dist) ai_pain =
594 {
595         if (self.health < 1)
596                 return;
597         ai_back (dist);
598 };
599
600 /*
601 =============
602 ai_painforward
603
604 stagger back a bit
605 =============
606 */
607 void(float dist) ai_painforward =
608 {
609         if (self.health < 1)
610                 return;
611         walkmove (self.ideal_yaw, dist);
612 };
613
614 /*
615 =============
616 ai_walk
617
618 The monster is walking it's beat
619 =============
620 */
621 void(float dist) ai_walk =
622 {
623         if (self.health < 1)
624                 return;
625
626         movedist = dist;
627
628         // check for noticing a player
629         if (self.oldenemy.takedamage)
630         if (self.oldenemy.health >= 1)
631         {
632                 self.enemy = self.oldenemy;
633                 self.oldenemy = world;
634                 FoundTarget();
635                 monster_setalpha(0);
636                 return;
637         }
638         if (self.enemy)
639         {
640                 if (self.enemy.takedamage)
641                 {
642                         if (self.enemy.health >= 1)
643                         {
644                                 FoundTarget();
645                                 monster_setalpha(0);
646                                 return;
647                         }
648                         else
649                                 self.enemy = world;
650                 }
651                 else
652                         self.enemy = world;
653         }
654
655         self.findtarget = TRUE;
656
657         movetogoal (dist);
658         monster_setalpha(0);
659 };
660
661
662 /*
663 =============
664 ai_stand
665
666 The monster is staying in one place for a while, with slight angle turns
667 =============
668 */
669 void() ai_stand =
670 {
671         if (self.health < 1)
672                 return;
673         if (self.enemy)
674         {
675                 if (self.enemy.takedamage)
676                 {
677                         if (self.enemy.health >= 1)
678                         {
679                                 FoundTarget();
680                                 monster_setalpha(0);
681                                 return;
682                         }
683                         else
684                                 self.enemy = world;
685                 }
686                 else
687                         self.enemy = world;
688         }
689         self.findtarget = TRUE;
690
691         if (time > self.pausetime)
692         {
693                 self.th_walk ();
694                 monster_setalpha(0);
695                 return;
696         }
697
698 // change angle slightly
699
700         monster_setalpha(0);
701 };
702
703 /*
704 =============
705 ai_turn
706
707 don't move, but turn towards ideal_yaw
708 =============
709 */
710 void() ai_turn =
711 {
712         if (self.enemy)
713         {
714                 if (self.enemy.takedamage)
715                 {
716                         if (self.enemy.health >= 1)
717                         {
718                                 FoundTarget();
719                                 monster_setalpha(0);
720                                 return;
721                         }
722                         else
723                                 self.enemy = world;
724                 }
725                 else
726                         self.enemy = world;
727         }
728         self.findtarget = TRUE;
729
730         ChangeYaw ();
731         monster_setalpha(0);
732 };
733
734 //=============================================================================
735
736 /*
737 =============
738 ChooseTurn
739 =============
740 */
741 void(vector pDestvec) ChooseTurn =
742 {
743         local vector dir, newdir;
744
745         dir = self.origin - pDestvec;
746
747         newdir_x = trace_plane_normal_y;
748         newdir_y = 0 - trace_plane_normal_x;
749         newdir_z = 0;
750
751         if (dir * newdir > 0)
752         {
753                 dir_x = 0 - trace_plane_normal_y;
754                 dir_y = trace_plane_normal_x;
755         }
756         else
757         {
758                 dir_x = trace_plane_normal_y;
759                 dir_y = 0 - trace_plane_normal_x;
760         }
761
762         dir_z = 0;
763         self.ideal_yaw = vectoyaw(dir);
764 };
765
766 /*
767 ============
768 FacingIdeal
769
770 ============
771 */
772 float() FacingIdeal =
773 {
774         local float delta;
775
776         delta = anglemod(self.angles_y - self.ideal_yaw);
777         if (delta > 45 && delta < 315)
778                 return FALSE;
779         return TRUE;
780 };
781
782
783 //=============================================================================
784
785 .float() th_checkattack;
786
787
788
789 /*
790 =============
791 ai_run
792
793 The monster has an enemy it is trying to kill
794 =============
795 */
796 void(float dist) ai_run =
797 {
798         local float ofs;
799         if (self.health < 1)
800                 return;
801         movedist = dist;
802         // see if the enemy is dead
803         if (self.enemy.health < 1 || self.enemy.takedamage == DAMAGE_NO)
804         {
805                 self.enemy = world;
806                 // FIXME: look all around for other targets
807                 if (self.oldenemy.health >= 1 && self.oldenemy.takedamage)
808                 {
809                         self.enemy = self.oldenemy;
810                         self.oldenemy = world;
811                         HuntTarget ();
812                 }
813                 else
814                 {
815                         if (self.movetarget)
816                                 self.th_walk ();
817                         else
818                                 self.th_stand ();
819                         return;
820                 }
821         }
822
823         // wake up other monsters
824         self.show_hostile = time + 1;
825
826         // check knowledge of enemy
827         enemy_range = range(self.enemy);
828
829         self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin);
830         ChangeYaw ();
831
832         if (self.attack_state == AS_MELEE)
833         {
834                 //dprint ("ai_run_melee\n");
835                 //Turn and close until within an angle to launch a melee attack
836                 if (FacingIdeal())
837                 {
838                         self.th_melee ();
839                         self.attack_state = AS_STRAIGHT;
840                 }
841                 return;
842         }
843         else if (self.attack_state == AS_MISSILE)
844         {
845                 //dprint ("ai_run_missile\n");
846                 //Turn in place until within an angle to launch a missile attack
847                 if (FacingIdeal())
848                 if (self.th_missile ())
849                         self.attack_state = AS_STRAIGHT;
850                 return;
851         }
852
853         if (self.th_checkattack())
854                 return;                                 // beginning an attack
855
856         if (visible(self.enemy))
857                 self.search_time = time + 5;
858         else if (coop)
859         {
860                 // look for other coop players
861                 if (self.search_time < time)
862                         self.findtarget = TRUE;
863         }
864
865         if (self.attack_state == AS_SLIDING)
866         {
867                 //dprint ("ai_run_slide\n");
868                 //Strafe sideways, but stay at aproximately the same range
869                 if (self.lefty)
870                         ofs = 90;
871                 else
872                         ofs = -90;
873
874                 if (walkmove (self.ideal_yaw + ofs, movedist))
875                         return;
876
877                 self.lefty = !self.lefty;
878
879                 walkmove (self.ideal_yaw - ofs, movedist);
880         }
881
882         // head straight in
883         movetogoal (dist);              // done in C code...
884 };
885