merge some changes from my autocvarizer branch that change nothing but make code...
[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 //#NO AUTOCVARS START
192 #if 0
193         local float healthboost;
194         local float r;
195
196         // monsterbosses cvar or spawnflag 64 causes a monster to be a miniboss
197         if ((self.spawnflags & 64) || (random() * 100 < cvar("monsterbosspercent")))
198         {
199                 self.radsuit_finished = time + 1000000000;
200                 r = random() * 4;
201                 if (r < 2)
202                 {
203                         self.super_damage_finished = time + 1000000000;
204                         healthboost = 30 + self.health * 0.5;
205                         self.effects = self.effects | (EF_FULLBRIGHT | EF_BLUE);
206                 }
207                 if (r >= 1)
208                 {
209                         healthboost = 30 + self.health * bound(0.5, skill * 0.5, 1.5);
210                         self.effects = self.effects | (EF_FULLBRIGHT | EF_RED);
211                         self.healthregen = max(self.healthregen, min(skill * 10, 30));
212                 }
213                 self.health = self.health + healthboost;
214                 self.max_health = self.health;
215                 self.bodyhealth = self.bodyhealth * 2 + healthboost;
216                 do
217                 {
218                         self.colormod_x = random();
219                         self.colormod_y = random();
220                         self.colormod_z = random();
221                         self.colormod =  normalize(self.colormod);
222                 }
223                 while (self.colormod_x > 0.6 && self.colormod_y > 0.6 && self.colormod_z > 0.6);
224         }
225 #endif
226 //#NO AUTOCVARS END
227 };
228
229
230 //============================================================================
231
232 /*
233 =============
234 range
235
236 returns the range catagorization of an entity reletive to self
237 0       melee range, will become hostile even if back is turned
238 1       visibility and infront, or visibility and show hostile
239 2       infront and show hostile
240 3       only triggered by damage
241 =============
242 */
243 float(entity targ) range =
244 {
245         local float r;
246         r = vlen ((self.origin + self.view_ofs) - (targ.origin + targ.view_ofs));
247         if (r < 120)
248                 return RANGE_MELEE;
249         if (r < 500)
250                 return RANGE_NEAR;
251         if (r < 2000) // increased from 1000 for DP
252                 return RANGE_MID;
253         return RANGE_FAR;
254 };
255
256 /*
257 =============
258 visible
259
260 returns 1 if the entity is visible to self, even if not infront ()
261 =============
262 */
263 float (entity targ) visible =
264 {
265         if (vlen(targ.origin - self.origin) > 5000) // long traces are slow
266                 return FALSE;
267
268         traceline ((self.origin + self.view_ofs), (targ.origin + targ.view_ofs), TRUE, self);   // see through other monsters
269
270         if (trace_inopen && trace_inwater)
271                 return FALSE;                   // sight line crossed contents
272
273         if (trace_fraction == 1)
274                 return TRUE;
275         return FALSE;
276 };
277
278
279 /*
280 =============
281 infront
282
283 returns 1 if the entity is in front (in sight) of self
284 =============
285 */
286 float(entity targ) infront =
287 {
288         local float dot;
289
290         makevectors (self.angles);
291         dot = normalize (targ.origin - self.origin) * v_forward;
292
293         return (dot > 0.3);
294 };
295 // returns 0 if not infront, or the dotproduct if infront
296 float(vector dir, entity targ) infront2 =
297 {
298         local float dot;
299
300         dir = normalize(dir);
301         dot = normalize (targ.origin - self.origin) * dir;
302
303         if (dot >= 0.3) return dot; // infront
304         return 0;
305 };
306
307
308 //============================================================================
309
310 /*
311 ===========
312 ChangeYaw
313
314 Turns towards self.ideal_yaw at self.yaw_speed
315 Sets the global variable current_yaw
316 Called every 0.1 sec by monsters
317 ============
318 */
319 /*
320
321 void() ChangeYaw =
322 {
323         local float ideal, move;
324
325 //current_yaw = self.ideal_yaw;
326 // mod down the current angle
327         current_yaw = anglemod( self.angles_y );
328         ideal = self.ideal_yaw;
329
330         if (current_yaw == ideal)
331                 return;
332
333         move = ideal - current_yaw;
334         if (ideal > current_yaw)
335         {
336                 if (move > 180)
337                         move = move - 360;
338         }
339         else
340         {
341                 if (move < -180)
342                         move = move + 360;
343         }
344
345         if (move > 0)
346         {
347                 if (move > self.yaw_speed)
348                         move = self.yaw_speed;
349         }
350         else
351         {
352                 if (move < 0-self.yaw_speed )
353                         move = 0-self.yaw_speed;
354         }
355
356         current_yaw = anglemod (current_yaw + move);
357
358         self.angles_y = current_yaw;
359 };
360
361 */
362
363
364 //============================================================================
365
366 void() HuntTarget =
367 {
368         self.goalentity = self.enemy;
369         self.think = self.th_run;
370         self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin);
371         self.nextthink = time + 0.1;
372         SUB_AttackFinished (1); // wait a while before first attack
373 };
374
375 .void() th_sightsound;
376
377 void() SightSound =
378 {
379         if (self.health < 1)
380                 return;
381         // skill 5 does not play sight sounds, instead you only hear the appear sound as they are about to attack
382         if (skill >= 5)
383         if (self.classname != "monster_hellfish")
384                 return;
385
386         if (self.th_sightsound)
387                 self.th_sightsound();
388 };
389
390 void() FoundTarget =
391 {
392         if (self.health < 1 || !self.th_run)
393                 return;
394         if (self.enemy.health < 1 || !self.enemy.takedamage)
395                 return;
396         if (self.enemy.classname == "player")
397         {
398                 // let other monsters see this monster for a while
399                 sight_entity = self;
400                 sight_entity_time = time + 0.1;
401         }
402
403         self.show_hostile = time + 1;           // wake up other monsters
404
405         SightSound ();
406         HuntTarget ();
407 };
408
409 /*
410 //float checkplayertime;
411 entity lastcheckplayer;
412 entity havocbot_list;
413
414
415 entity() checkplayer =
416 {
417         local entity check;
418         local float worldcount;
419         // we can just fallback on checkclient if there are no bots
420         if (!havocbot_list)
421                 return checkclient();
422 */
423         /*
424         if (time < checkplayertime)
425         {
426                 traceline(self.origin + self.view_ofs, lastcheckplayer.origin + lastcheckplayer.view_ofs, TRUE, self);
427                 if (trace_fraction == 1)
428                         return lastcheckplayer;
429                 if (trace_ent == lastcheckplayer)
430                         return lastcheckplayer;
431         }
432         checkplayertime = time + 0.1;
433         */
434 /*
435         check = lastcheckplayer;
436         worldcount = 0;
437         c = 0;
438         do
439         {
440                 c = c + 1;
441                 check = findfloat(check, havocattack, TRUE);
442                 if (check.classname == "player" || check.classname == "turretbase")
443                 {
444                         traceline(self.origin + self.view_ofs, check.origin + check.view_ofs, TRUE, self);
445                         if (trace_fraction == 1)
446                                 return lastcheckplayer = check;
447                         if (trace_ent == check)
448                                 return lastcheckplayer = check;
449                 }
450                 else if (check == world)
451                 {
452                         worldcount = worldcount + 1;
453                         if (worldcount >= 2)
454                                 return lastcheckplayer = check;
455                 }
456         }
457         while(check != lastcheckplayer && c < 100);
458         return world;
459 };
460 */
461
462 /*
463 ===========
464 FindTarget
465
466 Self is currently not attacking anything, so try to find a target
467
468 Returns TRUE if an enemy was sighted
469
470 When a player fires a missile, the point of impact becomes a fakeplayer so
471 that monsters that see the impact will respond as if they had seen the
472 player.
473
474 To avoid spending too much time, only a single client (or fakeclient) is
475 checked each frame.  This means multi player games will have slightly
476 slower noticing monsters.
477 ============
478 */
479 .float findtarget;
480 float() FindTarget =
481 {
482         local entity client;
483         local float r;
484
485         if (self.health < 1)
486                 return FALSE;
487
488         // if the first or second spawnflag bit is set, the monster will only
489         // wake up on really seeing the player, not another monster getting angry
490
491         if (self.spawnflags & 3)
492         {
493                 // don't wake up on seeing another monster getting angry
494                 client = checkclient ();
495                 if (!client)
496                         return FALSE;   // current check entity isn't in PVS
497         }
498         else
499         {
500                 if (sight_entity_time >= time)
501                 {
502                         client = sight_entity;
503                         if (client.enemy == self.enemy)
504                                 return TRUE;
505                 }
506                 else
507                 {
508                         client = checkclient ();
509                         if (!client)
510                                 return FALSE;   // current check entity isn't in PVS
511                 }
512         }
513
514         if (client == self.enemy)
515                 return FALSE;
516
517         if (client.flags & FL_NOTARGET)
518                 return FALSE;
519
520 #if 0
521         if (client.items & IT_INVISIBILITY)
522                 return FALSE;
523 #endif
524
525         // on skill 5 the monsters usually ignore the player and remain ghostlike
526         if (skill >= 5)
527         if (self.classname != "monster_hellfish")
528         if (random() < 0.99)
529                 return FALSE;
530
531         r = range(client);
532         if (r == RANGE_FAR)
533                 return FALSE;
534
535         if (!visible (client))
536                 return FALSE;
537
538         if (r == RANGE_NEAR)
539         {
540                 if (client.show_hostile < time && !infront (client))
541                         return FALSE;
542         }
543         else if (r == RANGE_MID)
544         {
545                 // LordHavoc: was if ( /* client.show_hostile < time || */ !infront (client))
546                 if (client.show_hostile < time && !infront (client))
547                         return FALSE;
548         }
549
550         //
551         // got one
552         //
553
554         if (client.model == "")
555                 return FALSE;
556         self.enemy = client;
557         if (self.enemy.classname != "player" && self.enemy.classname != "turretbase")
558         {
559                 self.enemy = self.enemy.enemy;
560                 if (self.enemy.classname != "player" && self.enemy.classname != "turretbase")
561                 {
562                         self.enemy = world;
563                         return FALSE;
564                 }
565         }
566
567         FoundTarget ();
568
569         return TRUE;
570 };
571
572
573 //=============================================================================
574
575 void(float dist) ai_forward =
576 {
577         walkmove (self.angles_y, dist);
578 };
579
580 void(float dist) ai_back =
581 {
582         walkmove ( (self.angles_y+180), dist);
583 };
584
585
586 void(float a) monster_setalpha;
587
588 /*
589 =============
590 ai_pain
591
592 stagger back a bit
593 =============
594 */
595 void(float dist) ai_pain =
596 {
597         if (self.health < 1)
598                 return;
599         ai_back (dist);
600 };
601
602 /*
603 =============
604 ai_painforward
605
606 stagger back a bit
607 =============
608 */
609 void(float dist) ai_painforward =
610 {
611         if (self.health < 1)
612                 return;
613         walkmove (self.ideal_yaw, dist);
614 };
615
616 /*
617 =============
618 ai_walk
619
620 The monster is walking it's beat
621 =============
622 */
623 void(float dist) ai_walk =
624 {
625         if (self.health < 1)
626                 return;
627
628         movedist = dist;
629
630         // check for noticing a player
631         if (self.oldenemy.takedamage)
632         if (self.oldenemy.health >= 1)
633         {
634                 self.enemy = self.oldenemy;
635                 self.oldenemy = world;
636                 FoundTarget();
637                 monster_setalpha(0);
638                 return;
639         }
640         if (self.enemy)
641         {
642                 if (self.enemy.takedamage)
643                 {
644                         if (self.enemy.health >= 1)
645                         {
646                                 FoundTarget();
647                                 monster_setalpha(0);
648                                 return;
649                         }
650                         else
651                                 self.enemy = world;
652                 }
653                 else
654                         self.enemy = world;
655         }
656
657         self.findtarget = TRUE;
658
659         movetogoal (dist);
660         monster_setalpha(0);
661 };
662
663
664 /*
665 =============
666 ai_stand
667
668 The monster is staying in one place for a while, with slight angle turns
669 =============
670 */
671 void() ai_stand =
672 {
673         if (self.health < 1)
674                 return;
675         if (self.enemy)
676         {
677                 if (self.enemy.takedamage)
678                 {
679                         if (self.enemy.health >= 1)
680                         {
681                                 FoundTarget();
682                                 monster_setalpha(0);
683                                 return;
684                         }
685                         else
686                                 self.enemy = world;
687                 }
688                 else
689                         self.enemy = world;
690         }
691         self.findtarget = TRUE;
692
693         if (time > self.pausetime)
694         {
695                 self.th_walk ();
696                 monster_setalpha(0);
697                 return;
698         }
699
700 // change angle slightly
701
702         monster_setalpha(0);
703 };
704
705 /*
706 =============
707 ai_turn
708
709 don't move, but turn towards ideal_yaw
710 =============
711 */
712 void() ai_turn =
713 {
714         if (self.enemy)
715         {
716                 if (self.enemy.takedamage)
717                 {
718                         if (self.enemy.health >= 1)
719                         {
720                                 FoundTarget();
721                                 monster_setalpha(0);
722                                 return;
723                         }
724                         else
725                                 self.enemy = world;
726                 }
727                 else
728                         self.enemy = world;
729         }
730         self.findtarget = TRUE;
731
732         ChangeYaw ();
733         monster_setalpha(0);
734 };
735
736 //=============================================================================
737
738 /*
739 =============
740 ChooseTurn
741 =============
742 */
743 void(vector pDestvec) ChooseTurn =
744 {
745         local vector dir, newdir;
746
747         dir = self.origin - pDestvec;
748
749         newdir_x = trace_plane_normal_y;
750         newdir_y = 0 - trace_plane_normal_x;
751         newdir_z = 0;
752
753         if (dir * newdir > 0)
754         {
755                 dir_x = 0 - trace_plane_normal_y;
756                 dir_y = trace_plane_normal_x;
757         }
758         else
759         {
760                 dir_x = trace_plane_normal_y;
761                 dir_y = 0 - trace_plane_normal_x;
762         }
763
764         dir_z = 0;
765         self.ideal_yaw = vectoyaw(dir);
766 };
767
768 /*
769 ============
770 FacingIdeal
771
772 ============
773 */
774 float() FacingIdeal =
775 {
776         local float delta;
777
778         delta = anglemod(self.angles_y - self.ideal_yaw);
779         if (delta > 45 && delta < 315)
780                 return FALSE;
781         return TRUE;
782 };
783
784
785 //=============================================================================
786
787 .float() th_checkattack;
788
789
790
791 /*
792 =============
793 ai_run
794
795 The monster has an enemy it is trying to kill
796 =============
797 */
798 void(float dist) ai_run =
799 {
800         local float ofs;
801         if (self.health < 1)
802                 return;
803         movedist = dist;
804         // see if the enemy is dead
805         if (self.enemy.health < 1 || self.enemy.takedamage == DAMAGE_NO)
806         {
807                 self.enemy = world;
808                 // FIXME: look all around for other targets
809                 if (self.oldenemy.health >= 1 && self.oldenemy.takedamage)
810                 {
811                         self.enemy = self.oldenemy;
812                         self.oldenemy = world;
813                         HuntTarget ();
814                 }
815                 else
816                 {
817                         if (self.movetarget)
818                                 self.th_walk ();
819                         else
820                                 self.th_stand ();
821                         return;
822                 }
823         }
824
825         // wake up other monsters
826         self.show_hostile = time + 1;
827
828         // check knowledge of enemy
829         enemy_range = range(self.enemy);
830
831         self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin);
832         ChangeYaw ();
833
834         if (self.attack_state == AS_MELEE)
835         {
836                 //dprint ("ai_run_melee\n");
837                 //Turn and close until within an angle to launch a melee attack
838                 if (FacingIdeal())
839                 {
840                         self.th_melee ();
841                         self.attack_state = AS_STRAIGHT;
842                 }
843                 return;
844         }
845         else if (self.attack_state == AS_MISSILE)
846         {
847                 //dprint ("ai_run_missile\n");
848                 //Turn in place until within an angle to launch a missile attack
849                 if (FacingIdeal())
850                 if (self.th_missile ())
851                         self.attack_state = AS_STRAIGHT;
852                 return;
853         }
854
855         if (self.th_checkattack())
856                 return;                                 // beginning an attack
857
858         if (visible(self.enemy))
859                 self.search_time = time + 5;
860         else if (coop)
861         {
862                 // look for other coop players
863                 if (self.search_time < time)
864                         self.findtarget = TRUE;
865         }
866
867         if (self.attack_state == AS_SLIDING)
868         {
869                 //dprint ("ai_run_slide\n");
870                 //Strafe sideways, but stay at aproximately the same range
871                 if (self.lefty)
872                         ofs = 90;
873                 else
874                         ofs = -90;
875
876                 if (walkmove (self.ideal_yaw + ofs, movedist))
877                         return;
878
879                 self.lefty = !self.lefty;
880
881                 walkmove (self.ideal_yaw - ofs, movedist);
882         }
883
884         // head straight in
885         movetogoal (dist);              // done in C code...
886 };
887