1void() movetarget_f; 2void() t_movetarget; 3void() knight_walk1; 4void() knight_bow6; 5void() knight_bow1; 6void() FoundTarget; 7 8float MONSTER_WANDER = 32; // disable wandering around 9float MONSTER_APPEAR = 64; // spawn invisible, and appear when triggered 10 11.float ismonster; 12.float monsterawaitingteleport; // avoid awaking monsters in teleport rooms 13 14// when a monster becomes angry at a player, that monster will be used 15// as the sight target the next frame so that monsters near that one 16// will wake up even if they wouldn't have noticed the player 17// 18entity sight_entity; 19float sight_entity_time; 20 21/* 22 23.enemy 24Will be world if not currently angry at anyone. 25 26.movetarget 27The next path spot to walk toward. If .enemy, ignore .movetarget. 28When an enemy is killed, the monster will try to return to it's path. 29 30.huntt_ime 31Set to time + something when the player is in sight, but movement straight for 32him is blocked. This causes the monster to use wall following code for 33movement direction instead of sighting on the player. 34 35.ideal_yaw 36A yaw angle of the intended direction, which will be turned towards at up 37to 45 deg / state. If the enemy is in view and hunt_time is not active, 38this will be the exact line towards the enemy. 39 40.pausetime 41A monster will leave it's stand state and head towards it's .movetarget when 42time > .pausetime. 43 44walkmove(angle, speed) primitive is all or nothing 45*/ 46 47 48// 49// globals 50// 51//float current_yaw; 52 53float(float v) anglemod = 54{ 55 v = v - 360 * floor(v / 360); 56 return v; 57}; 58 59/* 60============================================================================== 61 62MOVETARGET CODE 63 64The angle of the movetarget effects standing and bowing direction, but has no effect on movement, which allways heads to the next target. 65 66targetname 67must be present. The name of this movetarget. 68 69target 70the next spot to move to. If not present, stop here for good. 71 72pausetime 73The number of seconds to spend standing or bowing for path_stand or path_bow 74 75============================================================================== 76*/ 77 78 79void() movetarget_f = 80{ 81 if (!self.targetname) 82 objerror ("monster_movetarget: no targetname"); 83 84 self.solid = SOLID_TRIGGER; 85 self.touch = t_movetarget; 86 setsize (self, '-8 -8 -8', '8 8 8'); 87}; 88 89/*QUAKED path_corner (0.5 0.3 0) (-8 -8 -8) (8 8 8) 90Monsters will continue walking towards the next target corner. 91*/ 92void() path_corner = 93{ 94 movetarget_f (); 95}; 96 97/* 98============= 99t_movetarget 100 101Something has bumped into a movetarget. If it is a monster 102moving towards it, change the next destination and continue. 103============== 104*/ 105void() t_movetarget = 106{ 107 local entity temp; 108 109 if (other.health < 1) 110 return; 111 if (other.movetarget != self) 112 return; 113 114 if (other.enemy) 115 return; // fighting, not following a path 116 117 temp = self; 118 self = other; 119 other = temp; 120 121 if (self.classname == "monster_ogre") 122 sound (self, CHAN_VOICE, "ogre/ogdrag.wav", 1, ATTN_IDLE);// play chainsaw drag sound 123 124//dprint ("t_movetarget\n"); 125 self.goalentity = self.movetarget = find (world, targetname, other.target); 126 self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin); 127 if (!self.movetarget) 128 { 129 self.pausetime = time + 999999; 130 self.th_stand (); 131 return; 132 } 133}; 134 135void() monster_wanderpaththink = 136{ 137 local vector v, v1; 138 local float b, c; 139 self.nextthink = time + random() * 10 + 1; 140 if (self.owner.health < 1) // dead, also handled in death code 141 { 142 self.owner.movetarget = world; 143 remove(self); 144 return; 145 } 146 b = -1; 147 c = 10; 148 while (c > 0) 149 { 150 c = c - 1; 151 v = randomvec(); 152 traceline(self.owner.origin, v * 1024 + self.owner.origin, FALSE, self); 153 v = trace_endpos - (normalize(v) * 16) - self.owner.origin; 154 if (vlen(v) > b) 155 { 156 b = vlen(v); 157 v1 = v; 158 } 159 } 160 setorigin(self, v1 + self.owner.origin); 161 self.owner.ideal_yaw = vectoyaw(self.origin - self.owner.origin); 162}; 163 164void() monster_wanderpathtouch = 165{ 166 if (other.health < 1) 167 return; 168 if (other.movetarget != self) 169 return; 170 171 if (other.enemy) 172 return; // fighting, not following a path 173 174 if (other.classname == "monster_ogre") 175 sound (other, CHAN_VOICE, "ogre/ogdrag.wav", 1, ATTN_IDLE);// play chainsaw drag sound 176 monster_wanderpaththink(); 177}; 178 179void() monster_spawnwanderpath = 180{ 181 newmis = spawn(); 182 newmis.classname = "monster_wanderpath"; 183 newmis.solid = SOLID_TRIGGER; 184 newmis.touch = monster_wanderpathtouch; 185 setsize (newmis, '-8 -8 -8', '8 8 8'); 186 newmis.think = monster_wanderpaththink; 187 newmis.nextthink = time + random() * 10 + 1; 188 newmis.owner = self; 189 self.goalentity = self.movetarget = newmis; 190}; 191 192void() monster_checkbossflag = 193{ 194 local float healthboost; 195 local float r; 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}; 226 227 228//============================================================================ 229 230/* 231============= 232range 233 234returns the range catagorization of an entity reletive to self 2350 melee range, will become hostile even if back is turned 2361 visibility and infront, or visibility and show hostile 2372 infront and show hostile 2383 only triggered by damage 239============= 240*/ 241float(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============= 256visible 257 258returns 1 if the entity is visible to self, even if not infront () 259============= 260*/ 261float (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============= 279infront 280 281returns 1 if the entity is in front (in sight) of self 282============= 283*/ 284float(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 294float(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=========== 310ChangeYaw 311 312Turns towards self.ideal_yaw at self.yaw_speed 313Sets the global variable current_yaw 314Called every 0.1 sec by monsters 315============ 316*/ 317/* 318 319void() 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 364void() 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 375void() 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 388void() 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; 409entity lastcheckplayer; 410entity havocbot_list; 411 412 413entity() 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=========== 462FindTarget 463 464Self is currently not attacking anything, so try to find a target 465 466Returns TRUE if an enemy was sighted 467 468When a player fires a missile, the point of impact becomes a fakeplayer so 469that monsters that see the impact will respond as if they had seen the 470player. 471 472To avoid spending too much time, only a single client (or fakeclient) is 473checked each frame. This means multi player games will have slightly 474slower noticing monsters. 475============ 476*/ 477.float findtarget; 478float() 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 (client.items & IT_INVISIBILITY) 519 return FALSE; 520 521 // on skill 5 the monsters usually ignore the player and remain ghostlike 522 if (skill >= 5) 523 if (self.classname != "monster_hellfish") 524 if (random() < 0.99) 525 return FALSE; 526 527 r = range(client); 528 if (r == RANGE_FAR) 529 return FALSE; 530 531 if (!visible (client)) 532 return FALSE; 533 534 if (r == RANGE_NEAR) 535 { 536 if (client.show_hostile < time && !infront (client)) 537 return FALSE; 538 } 539 else if (r == RANGE_MID) 540 { 541 // LordHavoc: was if ( /* client.show_hostile < time || */ !infront (client)) 542 if (client.show_hostile < time && !infront (client)) 543 return FALSE; 544 } 545 546 // 547 // got one 548 // 549 550 if (client.model == "") 551 return FALSE; 552 self.enemy = client; 553 if (self.enemy.classname != "player" && self.enemy.classname != "turretbase") 554 { 555 self.enemy = self.enemy.enemy; 556 if (self.enemy.classname != "player" && self.enemy.classname != "turretbase") 557 { 558 self.enemy = world; 559 return FALSE; 560 } 561 } 562 563 FoundTarget (); 564 565 return TRUE; 566}; 567 568 569//============================================================================= 570 571void(float dist) ai_forward = 572{ 573 walkmove (self.angles_y, dist); 574}; 575 576void(float dist) ai_back = 577{ 578 walkmove ( (self.angles_y+180), dist); 579}; 580 581 582void(float a) monster_setalpha; 583 584/* 585============= 586ai_pain 587 588stagger back a bit 589============= 590*/ 591void(float dist) ai_pain = 592{ 593 if (self.health < 1) 594 return; 595 ai_back (dist); 596}; 597 598/* 599============= 600ai_painforward 601 602stagger back a bit 603============= 604*/ 605void(float dist) ai_painforward = 606{ 607 if (self.health < 1) 608 return; 609 walkmove (self.ideal_yaw, dist); 610}; 611 612/* 613============= 614ai_walk 615 616The monster is walking it's beat 617============= 618*/ 619void(float dist) ai_walk = 620{ 621 if (self.health < 1) 622 return; 623 624 movedist = dist; 625 626 // check for noticing a player 627 if (self.oldenemy.takedamage) 628 if (self.oldenemy.health >= 1) 629 { 630 self.enemy = self.oldenemy; 631 self.oldenemy = world; 632 FoundTarget(); 633 monster_setalpha(0); 634 return; 635 } 636 if (self.enemy) 637 { 638 if (self.enemy.takedamage) 639 { 640 if (self.enemy.health >= 1) 641 { 642 FoundTarget(); 643 monster_setalpha(0); 644 return; 645 } 646 else 647 self.enemy = world; 648 } 649 else 650 self.enemy = world; 651 } 652 653 self.findtarget = TRUE; 654 655 movetogoal (dist); 656 monster_setalpha(0); 657}; 658 659 660/* 661============= 662ai_stand 663 664The monster is staying in one place for a while, with slight angle turns 665============= 666*/ 667void() ai_stand = 668{ 669 if (self.health < 1) 670 return; 671 if (self.enemy) 672 { 673 if (self.enemy.takedamage) 674 { 675 if (self.enemy.health >= 1) 676 { 677 FoundTarget(); 678 monster_setalpha(0); 679 return; 680 } 681 else 682 self.enemy = world; 683 } 684 else 685 self.enemy = world; 686 } 687 self.findtarget = TRUE; 688 689 if (time > self.pausetime) 690 { 691 self.th_walk (); 692 monster_setalpha(0); 693 return; 694 } 695 696// change angle slightly 697 698 monster_setalpha(0); 699}; 700 701/* 702============= 703ai_turn 704 705don't move, but turn towards ideal_yaw 706============= 707*/ 708void() ai_turn = 709{ 710 if (self.enemy) 711 { 712 if (self.enemy.takedamage) 713 { 714 if (self.enemy.health >= 1) 715 { 716 FoundTarget(); 717 monster_setalpha(0); 718 return; 719 } 720 else 721 self.enemy = world; 722 } 723 else 724 self.enemy = world; 725 } 726 self.findtarget = TRUE; 727 728 ChangeYaw (); 729 monster_setalpha(0); 730}; 731 732//============================================================================= 733 734/* 735============= 736ChooseTurn 737============= 738*/ 739void(vector destvec) ChooseTurn = 740{ 741 local vector dir, newdir; 742 743 dir = self.origin - destvec; 744 745 newdir_x = trace_plane_normal_y; 746 newdir_y = 0 - trace_plane_normal_x; 747 newdir_z = 0; 748 749 if (dir * newdir > 0) 750 { 751 dir_x = 0 - trace_plane_normal_y; 752 dir_y = trace_plane_normal_x; 753 } 754 else 755 { 756 dir_x = trace_plane_normal_y; 757 dir_y = 0 - trace_plane_normal_x; 758 } 759 760 dir_z = 0; 761 self.ideal_yaw = vectoyaw(dir); 762}; 763 764/* 765============ 766FacingIdeal 767 768============ 769*/ 770float() FacingIdeal = 771{ 772 local float delta; 773 774 delta = anglemod(self.angles_y - self.ideal_yaw); 775 if (delta > 45 && delta < 315) 776 return FALSE; 777 return TRUE; 778}; 779 780 781//============================================================================= 782 783.float() th_checkattack; 784 785 786 787/* 788============= 789ai_run 790 791The monster has an enemy it is trying to kill 792============= 793*/ 794void(float dist) ai_run = 795{ 796 local float ofs; 797 if (self.health < 1) 798 return; 799 movedist = dist; 800 // see if the enemy is dead 801 if (self.enemy.health < 1 || self.enemy.takedamage == DAMAGE_NO) 802 { 803 self.enemy = world; 804 // FIXME: look all around for other targets 805 if (self.oldenemy.health >= 1 && self.oldenemy.takedamage) 806 { 807 self.enemy = self.oldenemy; 808 self.oldenemy = world; 809 HuntTarget (); 810 } 811 else 812 { 813 if (self.movetarget) 814 self.th_walk (); 815 else 816 self.th_stand (); 817 return; 818 } 819 } 820 821 // wake up other monsters 822 self.show_hostile = time + 1; 823 824 // check knowledge of enemy 825 enemy_range = range(self.enemy); 826 827 self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin); 828 ChangeYaw (); 829 830 if (self.attack_state == AS_MELEE) 831 { 832 //dprint ("ai_run_melee\n"); 833 //Turn and close until within an angle to launch a melee attack 834 if (FacingIdeal()) 835 { 836 self.th_melee (); 837 self.attack_state = AS_STRAIGHT; 838 } 839 return; 840 } 841 else if (self.attack_state == AS_MISSILE) 842 { 843 //dprint ("ai_run_missile\n"); 844 //Turn in place until within an angle to launch a missile attack 845 if (FacingIdeal()) 846 if (self.th_missile ()) 847 self.attack_state = AS_STRAIGHT; 848 return; 849 } 850 851 if (self.th_checkattack()) 852 return; // beginning an attack 853 854 if (visible(self.enemy)) 855 self.search_time = time + 5; 856 else if (coop) 857 { 858 // look for other coop players 859 if (self.search_time < time) 860 self.findtarget = TRUE; 861 } 862 863 if (self.attack_state == AS_SLIDING) 864 { 865 //dprint ("ai_run_slide\n"); 866 //Strafe sideways, but stay at aproximately the same range 867 if (self.lefty) 868 ofs = 90; 869 else 870 ofs = -90; 871 872 if (walkmove (self.ideal_yaw + ofs, movedist)) 873 return; 874 875 self.lefty = !self.lefty; 876 877 walkmove (self.ideal_yaw - ofs, movedist); 878 } 879 880 // head straight in 881 movetogoal (dist); // done in C code... 882}; 883 884