]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_towerdefense.qc
Merge branch 'master' into Mario/monsters
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_towerdefense.qc
1 void td_debug(string input)
2 {
3         switch(autocvar_g_td_debug)
4         {
5                 case 1: dprint(input); break;
6                 case 2: print(input); break;
7         }
8 }
9
10 void td_waypoint_link(float tm, vector from, vector to)
11 {
12         switch(tm)
13         {
14                 case NUM_TEAM_1:
15                         WarpZone_TrailParticles(world, particleeffectnum("waypoint_link_red"), from, to);
16                         break;
17                 case NUM_TEAM_2:
18                         WarpZone_TrailParticles(world, particleeffectnum("waypoint_link_blue"), from, to);
19                         break;
20                 case NUM_TEAM_3:
21                         WarpZone_TrailParticles(world, particleeffectnum("waypoint_link_yellow"), from, to);
22                         break;
23                 case NUM_TEAM_4:
24                         WarpZone_TrailParticles(world, particleeffectnum("waypoint_link_pink"), from, to);
25                         break;
26         }
27 }
28
29 void td_waypoint_think()
30 {
31         entity e = world;
32         if(gameover)
33         {
34                 remove(self);
35                 return;
36         }
37         
38         if not(self.team)
39         {
40                 e = find(world, target, self.targetname);
41                 if(e)
42                         self.team = e.team;
43         }
44         if not(self.team)
45         {
46                 e = find(world, target2, self.targetname);
47                 if(e)
48                         self.team = e.team;
49         }
50         if not(self.team)
51         {
52                 e = find(world, target3, self.targetname);
53                 if(e)
54                         self.team = e.team;
55         }
56         if not(self.team)
57         {
58                 e = find(world, target4, self.targetname);
59                 if(e)
60                         self.team = e.team;
61         }
62         
63         if not(self.team)
64         {
65                 td_debug("Tower Defense waypoint without a team, removing it.\n");
66                 remove(self);
67                 return;
68         }
69         
70         if(time >= self.last_trace)
71         {
72                 entity e;
73                 
74                 e = find(world, targetname, self.target);
75                 if(e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
76                         td_waypoint_link(self.team, self.origin, e.origin);
77                 e = find(world, targetname, self.target2);
78                 if(e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
79                         td_waypoint_link(self.team, self.origin, e.origin);
80                 e = find(world, targetname, self.target3);
81                 if(e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
82                         td_waypoint_link(self.team, self.origin, e.origin);
83                 e = find(world, targetname, self.target4);
84                 if(e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
85                         td_waypoint_link(self.team, self.origin, e.origin);
86                         
87                 e = find(world, target, self.targetname);
88                 if(e.classname == "td_spawnpoint" || e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
89                         td_waypoint_link(self.team, self.origin, e.origin);
90                 e = find(world, target2, self.targetname);
91                 if(e.classname == "td_spawnpoint" || e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
92                         td_waypoint_link(self.team, self.origin, e.origin);
93                 e = find(world, target3, self.targetname);
94                 if(e.classname == "td_spawnpoint" || e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
95                         td_waypoint_link(self.team, self.origin, e.origin);
96                 e = find(world, target4, self.targetname);
97                 if(e.classname == "td_spawnpoint" || e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
98                         td_waypoint_link(self.team, self.origin, e.origin);
99                 
100                 self.last_trace = time + 0.5;
101         }
102         
103         self.nextthink = time + 0.1;
104 }
105
106 void td_generator_die() 
107 {
108         if(autocvar_sv_eventlog)
109                 GameLogEcho(":gendestroyed");
110         
111         Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_GENDESTROYED);
112         
113         self.solid                      = SOLID_NOT;
114         self.takedamage         = DAMAGE_NO;
115         self.event_damage   = func_null;
116         self.enemy                      = world;
117         self.reset                      = func_null; // don't reset this generator
118         
119         WaypointSprite_Kill(self.sprite);
120 }
121
122 void td_generator_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force) 
123 {
124         if(IS_PLAYER(attacker) || attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET || attacker.vehicle_flags & VHF_ISVEHICLE || self.takedamage == DAMAGE_NO)
125                 return;
126                 
127         entity head;
128                 
129         if (time > self.pain_finished)
130         {
131                 self.pain_finished = time + 10;
132                 play2team(self.team, "onslaught/generator_underattack.wav");
133         }
134         
135         if (random() < 0.5)
136                 spamsound(self, CH_TRIGGER, "onslaught/ons_hit1.wav", VOL_BASE, ATTN_NORM);
137         else
138                 spamsound(self, CH_TRIGGER, "onslaught/ons_hit2.wav", VOL_BASE, ATTN_NORM);
139         
140         
141         FOR_EACH_REALPLAYER(head)
142         if(!IsDifferentTeam(head, self))
143                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_TD_GENDAMAGED);
144         
145         self.health -= damage;
146         
147         WaypointSprite_UpdateHealth(self.sprite, self.health);
148                 
149         if(self.health <= 0)
150         {
151                 FOR_EACH_PLAYER(head)
152                 if(!IsDifferentTeam(head, attacker))
153                         PlayerScore_Add(head, SP_TD_DESTROYS, 1);
154                 
155                 TeamScore_AddToTeam(attacker.team, ST_TD_DESTROYS, 1);
156                 td_generator_die();
157         }
158                 
159         self.SendFlags |= GSF_STATUS;
160 }
161
162 void td_generator_setup()
163 {
164         self.think                      = func_null;
165         self.nextthink          = -1;
166         self.solid                      = SOLID_BBOX;
167         self.takedamage         = DAMAGE_AIM;
168         self.event_damage       = td_generator_damage;
169         self.movetype           = MOVETYPE_NONE;
170         self.monster_attack     = TRUE;
171         self.netname            = "Generator";
172         self.reset                      = func_null;
173         
174         WaypointSprite_SpawnFixed(self.netname, self.origin + '0 0 90', self, sprite, RADARICON_OBJECTIVE, Team_ColorRGB(self.team));   
175         WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
176         WaypointSprite_UpdateHealth(self.sprite, self.health);
177 }
178
179 entity PickSpawn (float tm)
180 {
181         entity e;
182         RandomSelection_Init();
183         for(e = world;(e = find(e, classname, "td_spawnpoint")); )
184         if(e.team == tm)
185                 RandomSelection_Add(e, 0, string_null, 1, 1);
186
187         return RandomSelection_chosen_ent;
188 }
189
190 void TD_SpawnMonster(float tm, float monster)
191 {
192         entity e, mon;
193         
194         e = PickSpawn(tm);
195                 
196         if(e == world)
197         {
198                 td_debug("Warning: couldn't find any td_spawnpoint spawnpoints, no monsters will spawn!\n");
199                 return;
200         }
201   
202         mon = spawnmonster("", monster, e, e, e.origin, FALSE, 2);
203         if(e.target2)
204         {
205                 if(random() <= 0.5 && e.target)
206                         mon.target2 = e.target;
207                 else
208                         mon.target2 = e.target2;
209         }
210         else
211                 mon.target2 = e.target;
212 }
213
214 float RandomMonster()
215 {
216         RandomSelection_Init();
217         
218         float i;
219         
220         for(i = MONSTER_FIRST + 1; i < MONSTER_LAST; ++i)
221         {
222                 if(i == MONSTER_STINGRAY || i == MONSTER_WYVERN)
223                         continue; // flying/swimming monsters not yet supported
224                 
225                 RandomSelection_Add(world, i, "", 1, 1);
226         }
227         
228         return RandomSelection_chosen_float;
229 }
230
231 void SpawnMonsters(float tm)
232 {
233         float whichmon;
234         
235         whichmon = RandomMonster();
236         
237         TD_SpawnMonster(tm, whichmon);
238 }
239
240 entity PickGenerator(float tm)
241 {
242         entity head;
243         
244         RandomSelection_Init();
245         for(head = world;(head = findflags(head, flags, FL_GENERATOR)); )
246         if(head.team != tm)
247                 RandomSelection_Add(head, 0, string_null, 1, 1);
248         
249         return RandomSelection_chosen_ent;
250 }
251
252 float td_checkfuel(entity ent, string tur)
253 {
254         float turcost = cvar(strcat("g_td_turret_", tur, "_cost"));
255         
256         if(ent.ammo_fuel < turcost)
257         {
258                 Send_Notification(NOTIF_ONE, ent, MSG_MULTI, MULTI_TD_NOFUEL);
259                 return FALSE;
260         }
261         
262         ent.ammo_fuel -= turcost;
263         
264         return TRUE;
265 }       
266
267 void spawnturret(entity spawnedby, entity own, string turet, vector orig)
268 {
269         if not(IS_PLAYER(spawnedby)) { td_debug("Warning: A non-player entity tried to spawn a turret\n"); return; }
270         if not(td_checkfuel(spawnedby, turet)) { return; }
271                 
272         entity oldself;
273         
274         oldself = self;
275         self = spawn();
276         
277         setorigin(self, orig);
278         self.spawnflags = TSL_NO_RESPAWN;
279         self.monster_attack = TRUE;
280         self.realowner = own;
281         self.playerid = own.playerid;
282         self.angles_y = spawnedby.v_angle_y;
283         spawnedby.turret_cnt += 1;
284         self.team = own.team;
285         
286         switch(turet)
287         {
288                 case "plasma": spawnfunc_turret_plasma(); break;
289                 case "mlrs": spawnfunc_turret_mlrs(); break;
290                 case "walker": spawnfunc_turret_walker(); break;
291                 case "flac": spawnfunc_turret_flac(); break;
292                 case "towerbuff": spawnfunc_turret_fusionreactor(); break;
293                 default: Send_Notification(NOTIF_ONE, spawnedby, MSG_INFO, INFO_TD_INVALID); remove(self); self = oldself; return;
294         }
295         
296         Send_Notification(NOTIF_ONE, spawnedby, MSG_MULTI, MULTI_TD_SPAWN);
297                 
298         self = oldself;
299 }
300
301 void buffturret(entity tur, float buff)
302 {
303         float refbuff = bound(0.01, buff * 0.05, 0.1);
304         
305         tur.turret_buff           += 1;
306         tur.max_health            *= buff;
307         tur.tur_health             = tur.max_health;
308         tur.health                         = tur.max_health;
309         tur.ammo_max              *= buff;
310         tur.ammo_recharge     *= buff;
311     tur.shot_dmg          *= buff;
312     tur.shot_radius       *= buff;
313     tur.shot_speed        *= buff;
314     tur.shot_spread       *= buff;
315     tur.shot_force        *= buff;
316         
317         if(buff < 1)
318                 tur.shot_refire += refbuff;
319         else
320                 tur.shot_refire -= refbuff;
321 }
322
323 void spawn_td_fuel(float fuel_size)
324 {
325         if not(g_td) {remove(self); return; }
326         
327         self.ammo_fuel = fuel_size * monster_skill;
328         StartItem("models/items/g_fuel.md3", "misc/itempickup.wav", g_pickup_respawntime_ammo, g_pickup_respawntimejitter_ammo, "Turret Fuel", IT_FUEL, 0, 0, commodity_pickupevalfunc, BOT_PICKUP_RATING_LOW);
329         
330         self.velocity = randomvec() * 175 + '0 0 325';
331 }
332
333 void td_generator_delayed()
334 {
335         generator_link(td_generator_setup);
336         
337         self.SendFlags = GSF_SETUP;
338 }
339
340 // round handling
341 #define TD_ALIVE_TEAMS() ((redalive > 0) + (bluealive > 0))
342 #define TD_ALIVE_TEAMS_OK() (TD_ALIVE_TEAMS() == 2)
343 void TD_RoundStart()
344 {
345         entity head;
346         
347         allowed_to_spawn = TRUE;
348         
349         ignore_turrets = TRUE;
350         
351         FOR_EACH_PLAYER(head)
352                 head.ready = FALSE;
353         
354         total_killed = 0;
355 }
356
357 void TD_count_alive_monsters()
358 {
359         entity head;
360         
361         total_alive = 0;
362         redalive = 0;
363         bluealive = 0;
364         
365         FOR_EACH_MONSTER(head)
366         {
367                 if(head.health <= 0) continue;
368                 
369                 ++total_alive;
370                 
371                 switch(head.team)
372                 {
373                         case NUM_TEAM_1: ++redalive; break;
374                         case NUM_TEAM_2: ++bluealive; break;
375                 }
376         }
377 }
378
379 float TD_GetWinnerTeam()
380 {
381         float winner_team = 0;
382         if(redalive >= 1)
383                 winner_team = NUM_TEAM_1;
384         if(bluealive >= 1)
385         {
386                 if(winner_team) return 0;
387                 winner_team = NUM_TEAM_2;
388         }
389         if(winner_team)
390                 return winner_team;
391         return -1; // no monster left
392 }
393
394 float TD_CheckWinner()
395 {
396         entity head = world;
397         
398         if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
399         {
400                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_OVER);
401                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_OVER);
402                 round_handler_Init(5, 10, 180);
403                 FOR_EACH_MONSTER(head) if(head.health > 0)
404                 {
405                         WaypointSprite_Kill(head.sprite);
406                         if(head.weaponentity) remove(head.weaponentity);
407                         if(head.iceblock) remove(head.iceblock);
408                         remove(head);
409                 }
410                 return 1;
411         }
412         
413         TD_count_alive_monsters();
414         
415         max_perteam = max_monsters * 0.5;
416         
417         if(time >= last_check)
418         if(total_killed < max_monsters)
419         {
420                 if(redalive < max_perteam)
421                         SpawnMonsters(NUM_TEAM_1);
422                 if(bluealive < max_perteam)
423                         SpawnMonsters(NUM_TEAM_2);
424                         
425                 last_check = time + 0.5;
426         }
427                 
428         if(total_killed < max_monsters)
429                 return 0;
430         
431         if(TD_ALIVE_TEAMS_OK())
432                 return 0;
433
434         float winner_team = TD_GetWinnerTeam();
435         if(winner_team > 0)
436         {
437                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, APP_TEAM_NUM_4(winner_team, CENTER_ROUND_TEAM_WIN_));
438                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(winner_team, INFO_ROUND_TEAM_WIN_));
439                 TeamScore_AddToTeam(winner_team, ST_SCORE, +1);
440         }
441         else if(winner_team == -1)
442         {
443                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_TIED);
444                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_TIED);
445         }
446         
447         FOR_EACH_MONSTER(head) if(head.health > 0)
448         {
449                 WaypointSprite_Kill(head.sprite);
450                 if(head.weaponentity) remove(head.weaponentity);
451                 if(head.iceblock) remove(head.iceblock);
452                 remove(head);
453         }
454
455         round_handler_Init(5, 10, 180);
456         return 1;
457 }
458
459 float TD_CheckTeams()
460 {
461         entity head;
462         float readycount = 0, num_players = 0, ready_needed_factor, ready_needed_count;
463         
464         FOR_EACH_REALPLAYER(head)
465         {
466                 ++num_players;
467                 if(head.ready)
468                         ++readycount;
469         }
470         
471         ready_needed_factor = bound(0.5, cvar("g_td_majority_factor"), 0.999);
472         ready_needed_count = floor(num_players * ready_needed_factor) + 1;
473         
474         if(readycount >= ready_needed_count || time >= ready_timeout)
475                 return TRUE;
476         
477         allowed_to_spawn = TRUE;
478         
479         return FALSE;
480 }
481
482 // spawnfuncs   
483 void spawnfunc_td_generator()
484 {
485         if not(g_td) { remove(self); return; }
486         if not(self.team)
487         {
488                 td_debug("Generator without a team, removing it.\n");
489                 remove(self);
490                 return;
491         }
492         
493         precache_sound("onslaught/generator_underattack.wav");
494         precache_sound("onslaught/ons_hit1.wav");
495         precache_sound("onslaught/ons_hit2.wav");
496         precache_sound("weapons/rocket_impact.wav");
497         
498         if not(self.health)
499                 self.health = 1000;
500                 
501         self.max_health = self.health;
502         self.classname = "td_generator";
503         self.flags = FL_GENERATOR;
504         
505         setsize(self, GENERATOR_MIN, GENERATOR_MAX);
506         
507         setorigin(self, self.origin + '0 0 20');
508         droptofloor();
509         
510         InitializeEntity(self, td_generator_delayed, INITPRIO_LAST);
511 }
512
513 void spawnfunc_td_waypoint()
514 {
515         if not(g_td) { remove(self); return; }
516         
517         setsize(self, '-6 -6 -6', '6 6 6');
518         
519         if not(self.noalign)
520         {
521                 setorigin(self, self.origin + '0 0 20');
522                 droptofloor();
523         }
524         
525         self.classname = "td_waypoint";
526         self.think = td_waypoint_think;
527         self.nextthink = time + 0.1;
528 }
529
530 void spawnfunc_td_controller()
531 {
532         if not(g_td) { remove(self); return; }
533 }
534
535 void spawnfunc_td_spawnpoint()
536 {
537         if not(g_td) { remove(self); return; }
538         
539         self.classname = "td_spawnpoint";
540         
541         self.effects = EF_STARDUST;
542 }
543
544 // initialization stuff
545 void td_ScoreRules()
546 {
547         ScoreRules_basics(2, SFL_SORT_PRIO_SECONDARY, SFL_SORT_PRIO_SECONDARY, TRUE);
548         ScoreInfo_SetLabel_TeamScore(ST_TD_DESTROYS,  "destroyed", SFL_SORT_PRIO_PRIMARY);
549         ScoreInfo_SetLabel_PlayerScore(SP_TD_DESTROYS,"destroyed", SFL_SORT_PRIO_PRIMARY);
550         ScoreRules_basics_end();
551 }
552
553 void td_SpawnController()
554 {
555         entity oldself = self;
556         self = spawn();
557         self.classname = "td_controller";
558         spawnfunc_td_controller();
559         self = oldself;
560 }
561
562 void td_DelayedInit()
563 {
564         if(find(world, classname, "td_controller") == world)
565         {
566                 td_debug("No ""td_controller"" entity found on this map, creating it anyway.\n");
567                 td_SpawnController();
568         }
569         
570         td_ScoreRules();
571 }
572
573 void td_Initialize()
574 {
575         InitializeEntity(world, td_DelayedInit, INITPRIO_GAMETYPE);
576         
577         readyrestart_happened = TRUE; // disable normal ready command
578         
579         ready_timeout = time + 30;
580         
581         round_handler_Spawn(TD_CheckTeams, TD_CheckWinner, TD_RoundStart);
582         round_handler_Init(5, 10, 180);
583 }
584
585 // mutator hooks
586 MUTATOR_HOOKFUNCTION(td_TurretSpawn)
587 {
588         if(self.realowner == world)
589                 return TRUE;
590                 
591         if(self.turrcaps_flags & TFL_TURRCAPS_SUPPORT)
592                 self.target_range = 500;
593                 
594         self.bot_attack = FALSE;
595         buffturret(self, 0.7);
596         
597         return FALSE;
598 }
599
600 MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
601 {
602         if(!self.team || !self.realowner)
603         {
604                 td_debug(strcat("Removed monster ", self.netname, " with team ", ftos(self.team), "\n"));
605                 WaypointSprite_Kill(self.sprite);
606                 if(self.weaponentity) remove(self.weaponentity);
607                 remove(self);
608                 return FALSE;
609         }
610         
611         self.candrop = FALSE;
612         self.bot_attack = FALSE;
613         self.ammo_fuel = bound(20, 20 * self.level, 100);
614         self.target_range = 300;
615         self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_MONSTERCLIP;
616         
617         return FALSE;
618 }
619
620 MUTATOR_HOOKFUNCTION(td_MonsterDies)
621 {
622         vector backuporigin;
623         entity oldself;
624         
625         if(IS_PLAYER(frag_attacker.realowner))
626         {
627                 PlayerScore_Add(frag_attacker.realowner, SP_SCORE, 5);
628                 PlayerScore_Add(frag_attacker.realowner, SP_KILLS, 1);
629         }
630         
631         total_killed++;
632         
633         backuporigin = self.origin;
634         oldself = self;
635         self = spawn();
636         
637         self.gravity = 1;
638         setorigin(self, backuporigin + '0 0 5');
639         spawn_td_fuel(oldself.ammo_fuel);
640         self.touch = M_Item_Touch;
641         if(self == world)
642         {
643                 self = oldself;
644                 return FALSE;
645         }
646         SUB_SetFade(self, time + 5, 1);
647         
648         self = oldself;
649         
650         return FALSE;
651 }
652
653 MUTATOR_HOOKFUNCTION(td_MonsterThink)
654 {
655         if(time <= game_starttime && round_handler_IsActive())
656                 return TRUE;
657                 
658         if(IS_PLAYER(self.enemy))
659                 self.enemy = world;
660                 
661         float tr = 100;
662         
663         if not(self.enemy)
664         if(monster_target.flags & FL_GENERATOR)
665         if(monster_target.health <= 0)
666                 tr = 250;
667
668         if not(self.enemy) // don't change targets while attacking
669         if(vlen(monster_target.origin - self.origin) <= tr)
670         {
671                 if(monster_target.target2)
672                 {
673                         if(random() > 0.5)
674                                 self.target2 = monster_target.target2;
675                         else
676                                 self.target2 = monster_target.target;
677                 }
678                 else
679                         self.target2 = monster_target.target;
680                                 
681                 monster_target = find(world, targetname, self.target2);
682                 
683                 if(monster_target == world)
684                         monster_target = PickGenerator(self.team);
685                         
686                 if(monster_target == world)
687                         return TRUE; // no generators or waypoints?!
688         }
689         
690         td_debug(sprintf("Monster name: %s. Monster target: %s. Monster target2: %s. Monster target entity: %s.\n", self.netname, self.target, self.target2, etos(monster_target)));
691         
692         if(!self.enemy && !monster_target)
693                 return TRUE; // no enemy or target, must be wandering
694         
695         monster_speed_run = (150 + random() * 4) * monster_skill;
696         monster_speed_walk = (100 + random() * 4) * monster_skill;
697         
698         return FALSE;
699 }
700
701 MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
702 {
703         entity e;
704         
705         for(e = world;(e = findflags(e, monster_attack, TRUE)); ) 
706         {
707                 if(ignore_turrets)
708                 if(e.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
709                         continue;
710                         
711                 if(e.flags & FL_MONSTER)
712                         continue; // don't attack other monsters?
713                 
714                 if(monster_isvalidtarget(e, self))
715                         self.enemy = e;
716         }
717         
718         return TRUE;
719 }
720
721 MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
722 {
723         self.monster_attack = FALSE;
724         self.bot_attack = FALSE;
725         self.solid = SOLID_CORPSE;
726         
727         if(self.newfuel)
728         {
729                 self.ammo_fuel = self.newfuel;
730                 self.newfuel = 0;
731         }
732         
733         return FALSE;
734 }
735
736 MUTATOR_HOOKFUNCTION(td_Damage)
737 {
738         if(IS_PLAYER(frag_attacker))
739         if(frag_target.flags & FL_MONSTER || frag_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
740                 frag_damage = 0;
741                 
742         if(IS_PLAYER(frag_attacker) || frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
743         if(IS_PLAYER(frag_target))
744         {
745                 frag_damage = 0;
746                 if(frag_attacker != frag_target)
747                         frag_force = '0 0 0';
748         }
749         
750         return FALSE;
751 }
752
753 MUTATOR_HOOKFUNCTION(td_PlayerCommand)
754 {
755         if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
756         
757         vector org;
758         
759         makevectors(self.v_angle);
760         
761         org = self.origin + self.view_ofs + v_forward * 100;
762         
763         tracebox(self.origin + self.view_ofs, '-16 -16 -16', '16 16 16', org, MOVE_NORMAL, self);
764         entity targ = trace_ent;
765         if(targ.owner.realowner == self)
766                 targ = targ.owner;
767                 
768         if(cmd_name == "ready")
769         if not(self.ready)
770         {
771                 self.ready = TRUE;
772                 bprint(self.netname, "^2 is ready\n");
773                 
774                 Nagger_ReadyCounted();
775                 
776                 return TRUE;
777         }
778         
779         if(cmd_name == "turretspawn")
780         {
781                 if(argv(1) == "list")
782                 {
783                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_LIST, "mlrs walker plasma towerbuff flac");
784                         return TRUE;
785                 }
786                 if(!IS_PLAYER(self) || self.health <= 0)
787                 { 
788                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_CANTSPAWN);
789                         return TRUE;
790                 }
791                 if(max_turrets <= 0)
792                 {
793                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_DISABLED);
794                         return TRUE;
795                 }
796                 if(self.turret_cnt >= max_turrets)
797                 {
798                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXTURRETS, max_turrets);
799                         return TRUE;
800                 }
801                 
802                 spawnturret(self, self, argv(1), trace_endpos);
803                 
804                 return TRUE;
805         }
806         if(cmd_name == "turretremove")
807         {
808                 if((targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && (targ.playerid == self.playerid || targ.realowner == self))
809                 {
810                         self.turret_cnt -= 1;
811                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_REMOVE);
812                         WaypointSprite_Kill(targ.sprite);
813                         remove(targ.tur_head);
814                         remove(targ);
815                         return TRUE;
816                 }
817                 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_REMOVE);
818                 return TRUE;
819         }
820         
821         return FALSE;
822 }
823
824 MUTATOR_HOOKFUNCTION(td_ClientConnect)
825 {
826         self.newfuel = 75;
827         
828         entity t;
829         
830         self.turret_cnt = 0;
831         
832         for(t = world; (t = findflags(t, turrcaps_flags, TFL_TURRCAPS_ISTURRET)); )
833         if(t.playerid == self.playerid)
834         {
835                 t.realowner = self;
836                 self.turret_cnt += 1; // nice try
837         }
838         
839         return FALSE;
840 }
841
842 MUTATOR_HOOKFUNCTION(td_DisableVehicles)
843 {
844         return TRUE;
845 }
846
847 MUTATOR_HOOKFUNCTION(td_SetModname)
848 {
849         g_cloaked = 1;
850         
851         return FALSE;
852 }
853
854 MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
855 {
856         if(time < game_starttime || (time <= game_starttime && round_handler_IsActive()) || gameover)
857         {
858                 turret_target = world;
859                 return FALSE; // battle hasn't started
860         }
861
862         if(turret_flags & TFL_TARGETSELECT_MISSILESONLY)
863     if(turret_target.flags & FL_PROJECTILE)
864         if(turret_target.owner.flags & FL_MONSTER)
865         return TRUE; // flac support
866         
867         if not(turret_target.flags & FL_MONSTER)
868                 turret_target = world;
869                 
870         if(!IsDifferentTeam(turret_target, turret))
871                 turret_target = world;
872                 
873         return FALSE;
874 }
875
876 MUTATOR_HOOKFUNCTION(td_TurretDies)
877 {
878         if(self.realowner)
879                 self.realowner.turret_cnt -= 1;
880                         
881         return FALSE;
882 }
883
884 MUTATOR_HOOKFUNCTION(td_GetTeamCount)
885 {
886         ret_float = 2;
887         
888         return FALSE;
889 }
890
891 MUTATOR_DEFINITION(gamemode_towerdefense)
892 {
893         MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
894         MUTATOR_HOOK(MonsterSpawn, td_MonsterSpawn, CBC_ORDER_ANY);
895         MUTATOR_HOOK(MonsterDies, td_MonsterDies, CBC_ORDER_ANY);
896         MUTATOR_HOOK(MonsterMove, td_MonsterThink, CBC_ORDER_ANY);
897         MUTATOR_HOOK(MonsterFindTarget, td_MonsterFindTarget, CBC_ORDER_ANY);
898         MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
899         MUTATOR_HOOK(PlayerDamage_Calculate, td_Damage, CBC_ORDER_ANY);
900         MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, CBC_ORDER_ANY);
901         MUTATOR_HOOK(ClientConnect, td_ClientConnect, CBC_ORDER_ANY);
902         MUTATOR_HOOK(VehicleSpawn, td_DisableVehicles, CBC_ORDER_ANY);
903         MUTATOR_HOOK(SetModname, td_SetModname, CBC_ORDER_ANY);
904         MUTATOR_HOOK(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
905         MUTATOR_HOOK(TurretDies, td_TurretDies, CBC_ORDER_ANY);
906         MUTATOR_HOOK(GetTeamCount, td_GetTeamCount, CBC_ORDER_ANY);
907
908         MUTATOR_ONADD
909         {
910                 if(time > 1) // game loads at time 1
911                         error("This is a game type and it cannot be added at runtime.");        
912                 cvar_settemp("g_monsters", "1");
913                 cvar_settemp("g_turrets", "1");
914                 td_Initialize();
915         }
916         
917         MUTATOR_ONROLLBACK_OR_REMOVE
918         {
919                 // we actually cannot roll back td_Initialize here
920                 // BUT: we don't need to! If this gets called, adding always
921                 // succeeds.
922         }
923
924         MUTATOR_ONREMOVE
925         {
926                 error("This is a game type and it cannot be removed at runtime.");
927                 return -1;
928         }
929
930         return FALSE;
931 }