]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_towerdefense.qc
Fusion Reactor now heals monsters instead of turrets, idea by hutty
[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, string mnster)
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(mnster, 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 string monster_type2string(float mnster)
215 {
216         switch(mnster)
217         {
218                 case MONSTER_ZOMBIE: return "zombie";
219                 case MONSTER_BRUTE: return "brute";
220                 case MONSTER_ANIMUS: return "animus";
221                 case MONSTER_SHAMBLER: return "shambler";
222                 case MONSTER_BRUISER: return "bruiser";
223                 case MONSTER_WYVERN: return "wyvern";
224                 case MONSTER_CERBERUS: return "cerberus";
225                 case MONSTER_SLIME: return "slime";
226                 case MONSTER_KNIGHT: return "knight";
227                 case MONSTER_STINGRAY: return "stingray";
228                 case MONSTER_MAGE: return "mage";
229                 case MONSTER_SPIDER: return "spider";
230                 default: return "";
231         }
232 }
233
234 float RandomMonster()
235 {
236         RandomSelection_Init();
237         
238         float i;
239         
240         for(i = MONSTER_FIRST + 1; i < MONSTER_LAST; ++i)
241         {
242                 if(i == MONSTER_STINGRAY || i == MONSTER_WYVERN)
243                         continue; // flying/swimming monsters not yet supported
244                 
245                 RandomSelection_Add(world, i, "", 1, 1);
246         }
247         
248         return RandomSelection_chosen_float;
249 }
250
251 void SpawnMonsters(float tm)
252 {
253         float whichmon;
254         
255         whichmon = RandomMonster();
256         
257         TD_SpawnMonster(tm, monster_type2string(whichmon));
258 }
259
260 entity PickGenerator(float tm)
261 {
262         entity head;
263         
264         RandomSelection_Init();
265         for(head = world;(head = findflags(head, flags, FL_GENERATOR)); )
266         if(head.team != tm)
267                 RandomSelection_Add(head, 0, string_null, 1, 1);
268         
269         return RandomSelection_chosen_ent;
270 }
271
272 float td_checkfuel(entity ent, string tur)
273 {
274         float turcost = cvar(strcat("g_td_turret_", tur, "_cost"));
275         
276         if(ent.ammo_fuel < turcost)
277         {
278                 Send_Notification(NOTIF_ONE, ent, MSG_MULTI, MULTI_TD_NOFUEL);
279                 return FALSE;
280         }
281         
282         ent.ammo_fuel -= turcost;
283         
284         return TRUE;
285 }       
286
287 void spawnturret(entity spawnedby, entity own, string turet, vector orig)
288 {
289         if not(IS_PLAYER(spawnedby)) { td_debug("Warning: A non-player entity tried to spawn a turret\n"); return; }
290         if not(td_checkfuel(spawnedby, turet)) { return; }
291                 
292         entity oldself;
293         
294         oldself = self;
295         self = spawn();
296         
297         setorigin(self, orig);
298         self.spawnflags = TSL_NO_RESPAWN;
299         self.monster_attack = TRUE;
300         self.realowner = own;
301         self.playerid = own.playerid;
302         self.angles_y = spawnedby.v_angle_y;
303         spawnedby.turret_cnt += 1;
304         self.team = own.team;
305         
306         switch(turet)
307         {
308                 case "plasma": spawnfunc_turret_plasma(); break;
309                 case "mlrs": spawnfunc_turret_mlrs(); break;
310                 case "walker": spawnfunc_turret_walker(); break;
311                 case "flac": spawnfunc_turret_flac(); break;
312                 case "towerbuff": spawnfunc_turret_fusionreactor(); break;
313                 default: Send_Notification(NOTIF_ONE, spawnedby, MSG_INFO, INFO_TD_INVALID); remove(self); self = oldself; return;
314         }
315         
316         Send_Notification(NOTIF_ONE, spawnedby, MSG_MULTI, MULTI_TD_SPAWN);
317                 
318         self = oldself;
319 }
320
321 void buffturret(entity tur, float buff)
322 {
323         float refbuff = bound(0.01, buff * 0.05, 0.1);
324         
325         tur.turret_buff           += 1;
326         tur.max_health            *= buff;
327         tur.tur_health             = tur.max_health;
328         tur.health                         = tur.max_health;
329         tur.ammo_max              *= buff;
330         tur.ammo_recharge     *= buff;
331     tur.shot_dmg          *= buff;
332     tur.shot_radius       *= buff;
333     tur.shot_speed        *= buff;
334     tur.shot_spread       *= buff;
335     tur.shot_force        *= buff;
336         
337         if(buff < 1)
338                 tur.shot_refire += refbuff;
339         else
340                 tur.shot_refire -= refbuff;
341 }
342
343 void spawn_td_fuel(float fuel_size)
344 {
345         if not(g_td) {remove(self); return; }
346         
347         self.ammo_fuel = fuel_size * monster_skill;
348         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);
349         
350         self.velocity = randomvec() * 175 + '0 0 325';
351 }
352
353 void td_generator_delayed()
354 {
355         generator_link(td_generator_setup);
356         
357         self.SendFlags = GSF_SETUP;
358 }
359
360 // round handling
361 #define TD_ALIVE_TEAMS() ((redalive > 0) + (bluealive > 0))
362 #define TD_ALIVE_TEAMS_OK() (TD_ALIVE_TEAMS() == 2)
363 void TD_RoundStart()
364 {
365         entity head;
366         
367         allowed_to_spawn = TRUE;
368         
369         ignore_turrets = TRUE;
370         
371         FOR_EACH_PLAYER(head)
372                 head.ready = FALSE;
373         
374         total_killed = 0;
375 }
376
377 void TD_count_alive_monsters()
378 {
379         entity head;
380         
381         total_alive = 0;
382         redalive = 0;
383         bluealive = 0;
384         
385         FOR_EACH_MONSTER(head)
386         {
387                 if(head.health <= 0) continue;
388                 
389                 ++total_alive;
390                 
391                 switch(head.team)
392                 {
393                         case NUM_TEAM_1: ++redalive; break;
394                         case NUM_TEAM_2: ++bluealive; break;
395                 }
396         }
397 }
398
399 float TD_GetWinnerTeam()
400 {
401         float winner_team = 0;
402         if(redalive >= 1)
403                 winner_team = NUM_TEAM_1;
404         if(bluealive >= 1)
405         {
406                 if(winner_team) return 0;
407                 winner_team = NUM_TEAM_2;
408         }
409         if(winner_team)
410                 return winner_team;
411         return -1; // no monster left
412 }
413
414 float TD_CheckWinner()
415 {
416         entity head = world;
417         
418         if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
419         {
420                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_OVER);
421                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_OVER);
422                 round_handler_Init(5, 10, 180);
423                 FOR_EACH_MONSTER(head) if(head.health > 0)
424                 {
425                         WaypointSprite_Kill(head.sprite);
426                         if(head.weaponentity) remove(head.weaponentity);
427                         if(head.iceblock) remove(head.iceblock);
428                         remove(head);
429                 }
430                 return 1;
431         }
432         
433         TD_count_alive_monsters();
434         
435         max_perteam = max_monsters * 0.5;
436         
437         if(time >= last_check)
438         if(total_killed < max_monsters)
439         {
440                 if(redalive < max_perteam)
441                         SpawnMonsters(NUM_TEAM_1);
442                 if(bluealive < max_perteam)
443                         SpawnMonsters(NUM_TEAM_2);
444                         
445                 last_check = time + 0.5;
446         }
447                 
448         if(total_killed < max_monsters)
449                 return 0;
450         
451         if(TD_ALIVE_TEAMS_OK())
452                 return 0;
453
454         float winner_team = TD_GetWinnerTeam();
455         if(winner_team > 0)
456         {
457                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, APP_TEAM_NUM_4(winner_team, CENTER_ROUND_TEAM_WIN_));
458                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(winner_team, INFO_ROUND_TEAM_WIN_));
459                 TeamScore_AddToTeam(winner_team, ST_SCORE, +1);
460         }
461         else if(winner_team == -1)
462         {
463                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_TIED);
464                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_TIED);
465         }
466         
467         FOR_EACH_MONSTER(head) if(head.health > 0)
468         {
469                 WaypointSprite_Kill(head.sprite);
470                 if(head.weaponentity) remove(head.weaponentity);
471                 if(head.iceblock) remove(head.iceblock);
472                 remove(head);
473         }
474
475         round_handler_Init(5, 10, 180);
476         return 1;
477 }
478
479 float TD_CheckTeams()
480 {
481         entity head;
482         float readycount = 0, num_players = 0, ready_needed_factor, ready_needed_count;
483         
484         FOR_EACH_REALPLAYER(head)
485         {
486                 ++num_players;
487                 if(head.ready)
488                         ++readycount;
489         }
490         
491         ready_needed_factor = bound(0.5, cvar("g_td_majority_factor"), 0.999);
492         ready_needed_count = floor(num_players * ready_needed_factor) + 1;
493         
494         if(readycount >= ready_needed_count)
495                 return TRUE;
496         
497         allowed_to_spawn = TRUE;
498         
499         return FALSE;
500 }
501
502 // spawnfuncs   
503 void spawnfunc_td_generator()
504 {
505         if not(g_td) { remove(self); return; }
506         if not(self.team)
507         {
508                 td_debug("Generator without a team, removing it.\n");
509                 remove(self);
510                 return;
511         }
512         
513         precache_sound("onslaught/generator_underattack.wav");
514         precache_sound("onslaught/ons_hit1.wav");
515         precache_sound("onslaught/ons_hit2.wav");
516         precache_sound("weapons/rocket_impact.wav");
517         
518         if not(self.health)
519                 self.health = 1000;
520                 
521         self.max_health = self.health;
522         self.classname = "td_generator";
523         self.flags = FL_GENERATOR;
524         
525         setsize(self, GENERATOR_MIN, GENERATOR_MAX);
526         
527         setorigin(self, self.origin + '0 0 20');
528         droptofloor();
529         
530         InitializeEntity(self, td_generator_delayed, INITPRIO_LAST);
531 }
532
533 void spawnfunc_td_waypoint()
534 {
535         if not(g_td) { remove(self); return; }
536         
537         setsize(self, '-6 -6 -6', '6 6 6');
538         
539         if not(self.noalign)
540         {
541                 setorigin(self, self.origin + '0 0 20');
542                 droptofloor();
543         }
544         
545         self.classname = "td_waypoint";
546         self.think = td_waypoint_think;
547         self.nextthink = time + 0.1;
548 }
549
550 void spawnfunc_td_controller()
551 {
552         if not(g_td) { remove(self); return; }
553 }
554
555 void spawnfunc_td_spawnpoint()
556 {
557         if not(g_td) { remove(self); return; }
558         
559         self.classname = "td_spawnpoint";
560         
561         self.effects = EF_STARDUST;
562 }
563
564 // initialization stuff
565 void td_ScoreRules()
566 {
567         ScoreRules_basics(2, SFL_SORT_PRIO_SECONDARY, SFL_SORT_PRIO_SECONDARY, TRUE);
568         ScoreInfo_SetLabel_TeamScore(ST_TD_DESTROYS,  "destroyed", SFL_SORT_PRIO_PRIMARY);
569         ScoreInfo_SetLabel_PlayerScore(SP_TD_DESTROYS,"destroyed", SFL_SORT_PRIO_PRIMARY);
570         ScoreRules_basics_end();
571 }
572
573 void td_SpawnController()
574 {
575         entity oldself = self;
576         self = spawn();
577         self.classname = "td_controller";
578         spawnfunc_td_controller();
579         self = oldself;
580 }
581
582 void td_DelayedInit()
583 {
584         if(find(world, classname, "td_controller") == world)
585         {
586                 td_debug("No ""td_controller"" entity found on this map, creating it anyway.\n");
587                 td_SpawnController();
588         }
589         
590         td_ScoreRules();
591 }
592
593 void td_Initialize()
594 {
595         InitializeEntity(world, td_DelayedInit, INITPRIO_GAMETYPE);
596         
597         readyrestart_happened = TRUE; // disable normal ready command
598         
599         round_handler_Spawn(TD_CheckTeams, TD_CheckWinner, TD_RoundStart);
600         round_handler_Init(5, 10, 180);
601 }
602
603 // mutator hooks
604 MUTATOR_HOOKFUNCTION(td_TurretSpawn)
605 {
606         if(self.realowner == world)
607                 return TRUE;
608                 
609         if(self.turrcaps_flags & TFL_TURRCAPS_SUPPORT)
610                 self.target_range = 500;
611                 
612         self.bot_attack = FALSE;
613         buffturret(self, 0.7);
614         
615         return FALSE;
616 }
617
618 MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
619 {
620         if(!self.team || !self.realowner)
621         {
622                 td_debug(strcat("Removed monster ", self.netname, " with team ", ftos(self.team), "\n"));
623                 WaypointSprite_Kill(self.sprite);
624                 if(self.weaponentity) remove(self.weaponentity);
625                 remove(self);
626                 return FALSE;
627         }
628         
629         self.candrop = FALSE;
630         self.bot_attack = FALSE;
631         self.ammo_fuel = bound(20, 20 * self.level, 100);
632         self.target_range = 300;
633         self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_MONSTERCLIP;
634         
635         return FALSE;
636 }
637
638 MUTATOR_HOOKFUNCTION(td_MonsterDies)
639 {
640         vector backuporigin;
641         entity oldself;
642         
643         if(IS_PLAYER(frag_attacker.realowner))
644         {
645                 PlayerScore_Add(frag_attacker.realowner, SP_SCORE, 5);
646                 PlayerScore_Add(frag_attacker.realowner, SP_KILLS, 1);
647         }
648         
649         total_killed++;
650         
651         backuporigin = self.origin;
652         oldself = self;
653         self = spawn();
654         
655         self.gravity = 1;
656         setorigin(self, backuporigin + '0 0 5');
657         spawn_td_fuel(oldself.ammo_fuel);
658         self.touch = M_Item_Touch;
659         if(self == world)
660         {
661                 self = oldself;
662                 return FALSE;
663         }
664         SUB_SetFade(self, time + 5, 1);
665         
666         self = oldself;
667         
668         return FALSE;
669 }
670
671 MUTATOR_HOOKFUNCTION(td_MonsterThink)
672 {
673         if(time <= game_starttime && round_handler_IsActive())
674                 return TRUE;
675                 
676         if(IS_PLAYER(self.enemy))
677                 self.enemy = world;
678                 
679         float tr = 100;
680         
681         if not(self.enemy)
682         if(monster_target.flags & FL_GENERATOR)
683         if(monster_target.health <= 0)
684                 tr = 250;
685
686         if not(self.enemy) // don't change targets while attacking
687         if(vlen(monster_target.origin - self.origin) <= tr)
688         {
689                 if(monster_target.target2)
690                 {
691                         if(random() > 0.5)
692                                 self.target2 = monster_target.target2;
693                         else
694                                 self.target2 = monster_target.target;
695                 }
696                 else
697                         self.target2 = monster_target.target;
698                                 
699                 monster_target = find(world, targetname, self.target2);
700                 
701                 if(monster_target == world)
702                         monster_target = PickGenerator(self.team);
703                         
704                 if(monster_target == world)
705                         return TRUE; // no generators or waypoints?!
706         }
707         
708         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)));
709         
710         if(!self.enemy && !monster_target)
711                 return TRUE; // no enemy or target, must be wandering
712         
713         monster_speed_run = (150 + random() * 4) * monster_skill;
714         monster_speed_walk = (100 + random() * 4) * monster_skill;
715         
716         return FALSE;
717 }
718
719 MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
720 {
721         entity e;
722         
723         for(e = world;(e = findflags(e, monster_attack, TRUE)); ) 
724         {
725                 if(ignore_turrets)
726                 if(e.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
727                         continue;
728                         
729                 if(e.flags & FL_MONSTER)
730                         continue; // don't attack other monsters?
731                 
732                 if(monster_isvalidtarget(e, self))
733                         self.enemy = e;
734         }
735         
736         return TRUE;
737 }
738
739 MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
740 {
741         self.monster_attack = FALSE;
742         self.bot_attack = FALSE;
743         self.solid = SOLID_CORPSE;
744         
745         if(self.newfuel)
746         {
747                 self.ammo_fuel = self.newfuel;
748                 self.newfuel = 0;
749         }
750         
751         return FALSE;
752 }
753
754 MUTATOR_HOOKFUNCTION(td_Damage)
755 {
756         if(IS_PLAYER(frag_attacker))
757         if(frag_target.flags & FL_MONSTER)
758                 frag_damage = 0;
759                 
760         if(IS_PLAYER(frag_attacker) || frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
761         if(IS_PLAYER(frag_target))
762         {
763                 frag_damage = 0;
764                 if(frag_attacker != frag_target)
765                         frag_force = '0 0 0';
766         }
767         
768         return FALSE;
769 }
770
771 MUTATOR_HOOKFUNCTION(td_PlayerCommand)
772 {
773         if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
774         
775         vector org;
776         
777         makevectors(self.v_angle);
778         
779         org = self.origin + self.view_ofs + v_forward * 100;
780         
781         tracebox(self.origin + self.view_ofs, '-16 -16 -16', '16 16 16', org, MOVE_NORMAL, self);
782         entity targ = trace_ent;
783         if(targ.owner.realowner == self)
784                 targ = targ.owner;
785                 
786         if(cmd_name == "ready")
787         if not(self.ready)
788         {
789                 self.ready = TRUE;
790                 bprint(self.netname, "^2 is ready\n");
791                 
792                 Nagger_ReadyCounted();
793                 
794                 return TRUE;
795         }
796         
797         if(cmd_name == "turretspawn")
798         {
799                 if(argv(1) == "list")
800                 {
801                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_LIST, "mlrs walker plasma towerbuff flac");
802                         return TRUE;
803                 }
804                 if(!IS_PLAYER(self) || self.health <= 0)
805                 { 
806                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_CANTSPAWN);
807                         return TRUE;
808                 }
809                 if(max_turrets <= 0)
810                 {
811                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_DISABLED);
812                         return TRUE;
813                 }
814                 if(self.turret_cnt >= max_turrets)
815                 {
816                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXTURRETS, max_turrets);
817                         return TRUE;
818                 }
819                 
820                 spawnturret(self, self, argv(1), trace_endpos);
821                 
822                 return TRUE;
823         }
824         if(cmd_name == "turretremove")
825         {
826                 if((targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && (targ.playerid == self.playerid || targ.realowner == self))
827                 {
828                         self.turret_cnt -= 1;
829                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_REMOVE);
830                         WaypointSprite_Kill(targ.sprite);
831                         remove(targ.tur_head);
832                         remove(targ);
833                         return TRUE;
834                 }
835                 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_REMOVE);
836                 return TRUE;
837         }
838         
839         return FALSE;
840 }
841
842 MUTATOR_HOOKFUNCTION(td_ClientConnect)
843 {
844         self.newfuel = 75;
845         
846         entity t;
847         
848         self.turret_cnt = 0;
849         
850         for(t = world; (t = findflags(t, turrcaps_flags, TFL_TURRCAPS_ISTURRET)); )
851         if(t.playerid == self.playerid)
852         {
853                 t.realowner = self;
854                 self.turret_cnt += 1; // nice try
855         }
856         
857         return FALSE;
858 }
859
860 MUTATOR_HOOKFUNCTION(td_DisableVehicles)
861 {
862         return TRUE;
863 }
864
865 MUTATOR_HOOKFUNCTION(td_SetModname)
866 {
867         g_cloaked = 1;
868         
869         return FALSE;
870 }
871
872 MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
873 {
874         if(time < game_starttime || (time <= game_starttime && round_handler_IsActive()) || gameover)
875         {
876                 turret_target = world;
877                 return FALSE; // battle hasn't started
878         }
879
880         if(turret_flags & TFL_TARGETSELECT_MISSILESONLY)
881     if(turret_target.flags & FL_PROJECTILE)
882         if(turret_target.owner.flags & FL_MONSTER)
883         return TRUE; // flac support
884         
885         if not(turret_target.flags & FL_MONSTER)
886                 turret_target = world;
887                 
888         if(!IsDifferentTeam(turret_target, turret))
889                 turret_target = world;
890                 
891         return FALSE;
892 }
893
894 MUTATOR_HOOKFUNCTION(td_TurretDies)
895 {
896         if(self.realowner)
897                 self.realowner.turret_cnt -= 1;
898                         
899         return FALSE;
900 }
901
902 MUTATOR_HOOKFUNCTION(td_GetTeamCount)
903 {
904         ret_float = 2;
905         
906         return FALSE;
907 }
908
909 MUTATOR_DEFINITION(gamemode_towerdefense)
910 {
911         MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
912         MUTATOR_HOOK(MonsterSpawn, td_MonsterSpawn, CBC_ORDER_ANY);
913         MUTATOR_HOOK(MonsterDies, td_MonsterDies, CBC_ORDER_ANY);
914         MUTATOR_HOOK(MonsterMove, td_MonsterThink, CBC_ORDER_ANY);
915         MUTATOR_HOOK(MonsterFindTarget, td_MonsterFindTarget, CBC_ORDER_ANY);
916         MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
917         MUTATOR_HOOK(PlayerDamage_Calculate, td_Damage, CBC_ORDER_ANY);
918         MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, CBC_ORDER_ANY);
919         MUTATOR_HOOK(ClientConnect, td_ClientConnect, CBC_ORDER_ANY);
920         MUTATOR_HOOK(VehicleSpawn, td_DisableVehicles, CBC_ORDER_ANY);
921         MUTATOR_HOOK(SetModname, td_SetModname, CBC_ORDER_ANY);
922         MUTATOR_HOOK(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
923         MUTATOR_HOOK(TurretDies, td_TurretDies, CBC_ORDER_ANY);
924         MUTATOR_HOOK(GetTeamCount, td_GetTeamCount, CBC_ORDER_ANY);
925
926         MUTATOR_ONADD
927         {
928                 if(time > 1) // game loads at time 1
929                         error("This is a game type and it cannot be added at runtime.");        
930                 cvar_settemp("g_monsters", "1");
931                 cvar_settemp("g_turrets", "1");
932                 td_Initialize();
933         }
934         
935         MUTATOR_ONROLLBACK_OR_REMOVE
936         {
937                 // we actually cannot roll back td_Initialize here
938                 // BUT: we don't need to! If this gets called, adding always
939                 // succeeds.
940         }
941
942         MUTATOR_ONREMOVE
943         {
944                 error("This is a game type and it cannot be removed at runtime.");
945                 return -1;
946         }
947
948         return FALSE;
949 }