]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_towerdefense.qc
Allow spawning monsters by ID & remove spawn list
[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)
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         round_handler_Spawn(TD_CheckTeams, TD_CheckWinner, TD_RoundStart);
580         round_handler_Init(5, 10, 180);
581 }
582
583 // mutator hooks
584 MUTATOR_HOOKFUNCTION(td_TurretSpawn)
585 {
586         if(self.realowner == world)
587                 return TRUE;
588                 
589         if(self.turrcaps_flags & TFL_TURRCAPS_SUPPORT)
590                 self.target_range = 500;
591                 
592         self.bot_attack = FALSE;
593         buffturret(self, 0.7);
594         
595         return FALSE;
596 }
597
598 MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
599 {
600         if(!self.team || !self.realowner)
601         {
602                 td_debug(strcat("Removed monster ", self.netname, " with team ", ftos(self.team), "\n"));
603                 WaypointSprite_Kill(self.sprite);
604                 if(self.weaponentity) remove(self.weaponentity);
605                 remove(self);
606                 return FALSE;
607         }
608         
609         self.candrop = FALSE;
610         self.bot_attack = FALSE;
611         self.ammo_fuel = bound(20, 20 * self.level, 100);
612         self.target_range = 300;
613         self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_MONSTERCLIP;
614         
615         return FALSE;
616 }
617
618 MUTATOR_HOOKFUNCTION(td_MonsterDies)
619 {
620         vector backuporigin;
621         entity oldself;
622         
623         if(IS_PLAYER(frag_attacker.realowner))
624         {
625                 PlayerScore_Add(frag_attacker.realowner, SP_SCORE, 5);
626                 PlayerScore_Add(frag_attacker.realowner, SP_KILLS, 1);
627         }
628         
629         total_killed++;
630         
631         backuporigin = self.origin;
632         oldself = self;
633         self = spawn();
634         
635         self.gravity = 1;
636         setorigin(self, backuporigin + '0 0 5');
637         spawn_td_fuel(oldself.ammo_fuel);
638         self.touch = M_Item_Touch;
639         if(self == world)
640         {
641                 self = oldself;
642                 return FALSE;
643         }
644         SUB_SetFade(self, time + 5, 1);
645         
646         self = oldself;
647         
648         return FALSE;
649 }
650
651 MUTATOR_HOOKFUNCTION(td_MonsterThink)
652 {
653         if(time <= game_starttime && round_handler_IsActive())
654                 return TRUE;
655                 
656         if(IS_PLAYER(self.enemy))
657                 self.enemy = world;
658                 
659         float tr = 100;
660         
661         if not(self.enemy)
662         if(monster_target.flags & FL_GENERATOR)
663         if(monster_target.health <= 0)
664                 tr = 250;
665
666         if not(self.enemy) // don't change targets while attacking
667         if(vlen(monster_target.origin - self.origin) <= tr)
668         {
669                 if(monster_target.target2)
670                 {
671                         if(random() > 0.5)
672                                 self.target2 = monster_target.target2;
673                         else
674                                 self.target2 = monster_target.target;
675                 }
676                 else
677                         self.target2 = monster_target.target;
678                                 
679                 monster_target = find(world, targetname, self.target2);
680                 
681                 if(monster_target == world)
682                         monster_target = PickGenerator(self.team);
683                         
684                 if(monster_target == world)
685                         return TRUE; // no generators or waypoints?!
686         }
687         
688         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)));
689         
690         if(!self.enemy && !monster_target)
691                 return TRUE; // no enemy or target, must be wandering
692         
693         monster_speed_run = (150 + random() * 4) * monster_skill;
694         monster_speed_walk = (100 + random() * 4) * monster_skill;
695         
696         return FALSE;
697 }
698
699 MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
700 {
701         entity e;
702         
703         for(e = world;(e = findflags(e, monster_attack, TRUE)); ) 
704         {
705                 if(ignore_turrets)
706                 if(e.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
707                         continue;
708                         
709                 if(e.flags & FL_MONSTER)
710                         continue; // don't attack other monsters?
711                 
712                 if(monster_isvalidtarget(e, self))
713                         self.enemy = e;
714         }
715         
716         return TRUE;
717 }
718
719 MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
720 {
721         self.monster_attack = FALSE;
722         self.bot_attack = FALSE;
723         self.solid = SOLID_CORPSE;
724         
725         if(self.newfuel)
726         {
727                 self.ammo_fuel = self.newfuel;
728                 self.newfuel = 0;
729         }
730         
731         return FALSE;
732 }
733
734 MUTATOR_HOOKFUNCTION(td_Damage)
735 {
736         if(IS_PLAYER(frag_attacker))
737         if(frag_target.flags & FL_MONSTER)
738                 frag_damage = 0;
739                 
740         if(IS_PLAYER(frag_attacker) || frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
741         if(IS_PLAYER(frag_target))
742         {
743                 frag_damage = 0;
744                 if(frag_attacker != frag_target)
745                         frag_force = '0 0 0';
746         }
747         
748         return FALSE;
749 }
750
751 MUTATOR_HOOKFUNCTION(td_PlayerCommand)
752 {
753         if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
754         
755         vector org;
756         
757         makevectors(self.v_angle);
758         
759         org = self.origin + self.view_ofs + v_forward * 100;
760         
761         tracebox(self.origin + self.view_ofs, '-16 -16 -16', '16 16 16', org, MOVE_NORMAL, self);
762         entity targ = trace_ent;
763         if(targ.owner.realowner == self)
764                 targ = targ.owner;
765                 
766         if(cmd_name == "ready")
767         if not(self.ready)
768         {
769                 self.ready = TRUE;
770                 bprint(self.netname, "^2 is ready\n");
771                 
772                 Nagger_ReadyCounted();
773                 
774                 return TRUE;
775         }
776         
777         if(cmd_name == "turretspawn")
778         {
779                 if(argv(1) == "list")
780                 {
781                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_LIST, "mlrs walker plasma towerbuff flac");
782                         return TRUE;
783                 }
784                 if(!IS_PLAYER(self) || self.health <= 0)
785                 { 
786                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_CANTSPAWN);
787                         return TRUE;
788                 }
789                 if(max_turrets <= 0)
790                 {
791                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_DISABLED);
792                         return TRUE;
793                 }
794                 if(self.turret_cnt >= max_turrets)
795                 {
796                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXTURRETS, max_turrets);
797                         return TRUE;
798                 }
799                 
800                 spawnturret(self, self, argv(1), trace_endpos);
801                 
802                 return TRUE;
803         }
804         if(cmd_name == "turretremove")
805         {
806                 if((targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && (targ.playerid == self.playerid || targ.realowner == self))
807                 {
808                         self.turret_cnt -= 1;
809                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_REMOVE);
810                         WaypointSprite_Kill(targ.sprite);
811                         remove(targ.tur_head);
812                         remove(targ);
813                         return TRUE;
814                 }
815                 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_REMOVE);
816                 return TRUE;
817         }
818         
819         return FALSE;
820 }
821
822 MUTATOR_HOOKFUNCTION(td_ClientConnect)
823 {
824         self.newfuel = 75;
825         
826         entity t;
827         
828         self.turret_cnt = 0;
829         
830         for(t = world; (t = findflags(t, turrcaps_flags, TFL_TURRCAPS_ISTURRET)); )
831         if(t.playerid == self.playerid)
832         {
833                 t.realowner = self;
834                 self.turret_cnt += 1; // nice try
835         }
836         
837         return FALSE;
838 }
839
840 MUTATOR_HOOKFUNCTION(td_DisableVehicles)
841 {
842         return TRUE;
843 }
844
845 MUTATOR_HOOKFUNCTION(td_SetModname)
846 {
847         g_cloaked = 1;
848         
849         return FALSE;
850 }
851
852 MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
853 {
854         if(time < game_starttime || (time <= game_starttime && round_handler_IsActive()) || gameover)
855         {
856                 turret_target = world;
857                 return FALSE; // battle hasn't started
858         }
859
860         if(turret_flags & TFL_TARGETSELECT_MISSILESONLY)
861     if(turret_target.flags & FL_PROJECTILE)
862         if(turret_target.owner.flags & FL_MONSTER)
863         return TRUE; // flac support
864         
865         if not(turret_target.flags & FL_MONSTER)
866                 turret_target = world;
867                 
868         if(!IsDifferentTeam(turret_target, turret))
869                 turret_target = world;
870                 
871         return FALSE;
872 }
873
874 MUTATOR_HOOKFUNCTION(td_TurretDies)
875 {
876         if(self.realowner)
877                 self.realowner.turret_cnt -= 1;
878                         
879         return FALSE;
880 }
881
882 MUTATOR_HOOKFUNCTION(td_GetTeamCount)
883 {
884         ret_float = 2;
885         
886         return FALSE;
887 }
888
889 MUTATOR_DEFINITION(gamemode_towerdefense)
890 {
891         MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
892         MUTATOR_HOOK(MonsterSpawn, td_MonsterSpawn, CBC_ORDER_ANY);
893         MUTATOR_HOOK(MonsterDies, td_MonsterDies, CBC_ORDER_ANY);
894         MUTATOR_HOOK(MonsterMove, td_MonsterThink, CBC_ORDER_ANY);
895         MUTATOR_HOOK(MonsterFindTarget, td_MonsterFindTarget, CBC_ORDER_ANY);
896         MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
897         MUTATOR_HOOK(PlayerDamage_Calculate, td_Damage, CBC_ORDER_ANY);
898         MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, CBC_ORDER_ANY);
899         MUTATOR_HOOK(ClientConnect, td_ClientConnect, CBC_ORDER_ANY);
900         MUTATOR_HOOK(VehicleSpawn, td_DisableVehicles, CBC_ORDER_ANY);
901         MUTATOR_HOOK(SetModname, td_SetModname, CBC_ORDER_ANY);
902         MUTATOR_HOOK(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
903         MUTATOR_HOOK(TurretDies, td_TurretDies, CBC_ORDER_ANY);
904         MUTATOR_HOOK(GetTeamCount, td_GetTeamCount, CBC_ORDER_ANY);
905
906         MUTATOR_ONADD
907         {
908                 if(time > 1) // game loads at time 1
909                         error("This is a game type and it cannot be added at runtime.");        
910                 cvar_settemp("g_monsters", "1");
911                 cvar_settemp("g_turrets", "1");
912                 td_Initialize();
913         }
914         
915         MUTATOR_ONROLLBACK_OR_REMOVE
916         {
917                 // we actually cannot roll back td_Initialize here
918                 // BUT: we don't need to! If this gets called, adding always
919                 // succeeds.
920         }
921
922         MUTATOR_ONREMOVE
923         {
924                 error("This is a game type and it cannot be removed at runtime.");
925                 return -1;
926         }
927
928         return FALSE;
929 }