]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_td.qc
b5baabaf9b7242d1a5df601b612ffb11619d68e0
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_td.qc
1 // Tower Defense
2 // Gamemode by Mario
3  
4 void spawnfunc_td_controller()
5 {
6         if not(g_td) { remove(self); return; }
7         
8         if(autocvar_g_td_force_settings)
9         {
10                 // TODO: find a better way to do this?
11                 self.dontend = FALSE;
12                 self.maxwaves = 0;
13                 self.monstercount = 0;
14                 self.startwave = 0;
15                 self.maxturrets = 0;
16                 self.buildtime = 0;
17                 self.mspeed_walk = 0;
18                 self.mspeed_run = 0;
19                 self.spawndelay = 0;
20                 self.maxcurrent = 0;
21                 self.ignoreturrets = 0;
22         }
23                 
24         self.netname = "Tower Defense controller entity";
25         self.classname = "td_controller";
26                 
27         gensurvived = FALSE;
28         td_dont_end = ((self.dontend) ? self.dontend : autocvar_g_td_generator_dontend);                
29         max_waves = ((self.maxwaves) ? self.maxwaves : autocvar_g_td_max_waves);        
30         totalmonsters = ((self.monstercount) ? self.monstercount : autocvar_g_td_monster_count);
31         wave_count = ((self.startwave) ? self.startwave : autocvar_g_td_start_wave);
32         max_turrets = ((self.maxturrets) ? self.maxturrets : autocvar_g_td_turret_max);
33         build_time = ((self.buildtime) ? self.buildtime : autocvar_g_td_buildphase_time);
34         m_speed_walk = ((self.mspeed_walk) ? self.mspeed_walk : autocvar_g_td_monsters_speed_walk);
35         m_speed_run = ((self.mspeed_run) ? self.mspeed_run : autocvar_g_td_monsters_speed_run);
36         spawn_delay = ((self.spawndelay) ? self.spawndelay : autocvar_g_td_monsters_spawn_delay);
37         max_current = ((self.maxcurrent) ? self.maxcurrent : autocvar_g_td_current_monsters);
38         ignore_turrets = ((self.ignoreturrets) ? self.ignoreturrets : autocvar_g_td_monsters_ignore_turrets);
39         
40         if(autocvar_g_td_monsters_skill_start)
41                 monster_skill = autocvar_g_td_monsters_skill_start;
42                 
43         wave_end(TRUE);
44 }
45
46 void td_generator_die() 
47 {
48         if(autocvar_sv_eventlog)
49                 GameLogEcho(":gendestroyed");
50                 
51         gendestroyed = TRUE;
52         
53         Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_GENDESTROYED);
54         
55         setmodel(self, "models/onslaught/generator_dead.md3");
56         self.solid                      = SOLID_NOT;
57         self.takedamage         = DAMAGE_NO;
58         self.event_damage   = func_null;
59         self.enemy                      = world;
60         td_gencount                     -= 1;
61                 
62         pointparticles(particleeffectnum("explosion_medium"), self.origin, '0 0 0', 1);
63         
64         WaypointSprite_Kill(self.sprite);
65 }
66
67 void td_generator_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force) 
68 {
69         if(attacker.classname == STR_PLAYER || attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET || attacker.vehicle_flags & VHF_ISVEHICLE)
70                 return;
71         
72         Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_TD_GENDAMAGED);
73         
74         self.health -= damage;
75         
76         WaypointSprite_UpdateHealth(self.sprite, self.health);
77                 
78         if(self.health <= 0) 
79                 td_generator_die();
80 }
81
82 void spawnfunc_td_generator() 
83 {
84         if not(g_td) { remove(self); return; }
85         
86         gendestroyed = FALSE;
87         
88         if not(self.health)
89                 self.health = autocvar_g_td_generator_health;
90
91         // precache generator model
92         precache_model("models/onslaught/generator.md3");
93         precache_model("models/onslaught/generator_dead.md3");   
94         
95         self.model                  = "models/onslaught/generator.md3";
96         setmodel(self, self.model);
97         self.classname      = "td_generator";
98         self.solid                  = SOLID_BBOX;
99         self.takedamage     = DAMAGE_AIM;
100         self.event_damage   = td_generator_damage;
101         self.enemy                  = world;
102         self.nextthink      = -1;
103         self.think                  = func_null;
104         self.max_health     = self.health;
105         self.movetype       = MOVETYPE_NONE;
106         self.monster_attack = TRUE;
107         td_gencount                += 1;
108         self.netname            = "Generator";
109         
110         setsize(self, GENERATOR_MIN, GENERATOR_MAX);
111         
112         droptofloor();
113         
114         WaypointSprite_SpawnFixed(self.netname, self.origin + '0 0 60', self, sprite, RADARICON_OBJECTIVE, '1 0.5 0');  
115         WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
116         WaypointSprite_UpdateHealth(self.sprite, self.health);
117 }
118
119 entity PickGenerator()
120 {
121         entity generator, head;
122         if(td_gencount == 1)
123                 generator = find(world, classname, "td_generator");
124         else
125         {
126                 RandomSelection_Init();
127                 for(head = world;(head = find(head, classname, "td_generator")); )
128                 {
129                         RandomSelection_Add(head, 0, string_null, 1, 1);
130                 }
131                 generator = RandomSelection_chosen_ent; 
132         }
133         return generator;
134 }
135
136 void spawn_td_fuel(float fuel_size)
137 {
138         if not(g_td) {remove(self); return; }
139         
140         self.ammo_fuel = fuel_size * monster_skill;
141         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);
142         
143         self.velocity = randomvec() * 175 + '0 0 325';
144 }
145
146 void spawnfunc_td_waypoint() 
147 {
148         if not(g_td) { remove(self); return; }
149         
150         self.classname = "td_waypoint";
151 }
152
153 void spawnfunc_monster_swarm()
154 {
155         if not(g_td) { remove(self); return; }
156         
157         self.flags = SWARM_NORMAL; // marked as a spawnpoint
158         self.classname = "monster_swarm";
159         
160         if(self.spawntype == SWARM_SWIM) waterspawns_count += 1;
161         if(self.spawntype == SWARM_FLY) flyspawns_count += 1;
162         
163         WaypointSprite_SpawnFixed("Monsters", self.origin + '0 0 60', self, sprite, RADARICON_HERE, '0 0 1');
164         
165         if(self.target == "")
166                 print("Warning: monster_swarm entity without a set target\n");
167 }
168
169 void barricade_touch()
170 {
171         if not(other.flags & FL_MONSTER)
172                 return;
173                 
174         if(time < self.dmg_time)
175                 return;
176                 
177         Damage(other, self, self, autocvar_g_td_barricade_damage, DEATH_HURTTRIGGER, self.origin, '0 0 0');
178         
179         self.dmg_time = time + 1;
180 }
181
182 void barricade_die()
183 {
184         self.takedamage = DAMAGE_NO;
185         self.event_damage = func_null;
186         
187         WaypointSprite_Kill(self.sprite);
188         
189         pointparticles(particleeffectnum("explosion_medium"), self.origin, '0 0 0', 1);
190         sound(self, CH_SHOTS, "weapons/rocket_impact.wav", VOL_BASE, ATTN_NORM);
191         
192         if(self.realowner)
193                 self.realowner.turret_cnt -= 1;
194                 
195         self.think = SUB_Remove;
196         self.nextthink = time;
197 }
198
199 void barricade_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
200 {
201         if not(attacker.flags & FL_MONSTER) return;
202         
203         self.health -= damage;
204         
205         WaypointSprite_UpdateHealth(self.sprite, self.health);
206         
207         if(self.health < 1)
208                 barricade_die();
209 }
210
211 void spawn_barricade()
212 {
213         self.health = 2000;
214         self.max_health = self.health;
215         self.dmg_time = time;
216         self.touch = barricade_touch;
217         self.think = func_null;
218         self.nextthink = -1;
219         self.takedamage = DAMAGE_AIM;
220         self.turrcaps_flags = TFL_TURRCAPS_ISTURRET; // for turretremove commands etc.
221         self.solid = SOLID_CORPSE; // hax
222         self.event_damage = barricade_damage;
223         self.netname = "Barricade";
224         
225         WaypointSprite_Spawn(self.netname, 0, 1200, self, '0 0 110', world, 0, self, sprite, FALSE, RADARICON_DOMPOINT, '1 1 0');       
226         WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
227         WaypointSprite_UpdateHealth(self.sprite, self.health);
228         
229         precache_model("models/td/barricade.md3");
230         setmodel(self, "models/td/barricade.md3");
231         
232         droptofloor();
233         
234         self.movetype = MOVETYPE_NONE;
235 }
236
237 void spawnturret(entity spawnedby, entity own, string turet, vector orig)
238 {
239         if(spawnedby.classname != STR_PLAYER)
240         {
241                 print("Warning: A non-player entity tried to spawn a turret\n");
242                 return;
243         }
244                 
245         entity oldself;
246         
247         oldself = self;
248         self = spawn();
249         
250         setorigin(self, orig);
251         self.spawnflags = TSL_NO_RESPAWN;
252         self.monster_attack = TRUE;
253         self.realowner = own;
254         self.angles_y = spawnedby.v_angle_y;
255         spawnedby.turret_cnt += 1;
256         self.colormap = spawnedby.colormap;
257         
258         switch(turet)
259         {
260                 default:
261                 case "plasma": spawnfunc_turret_plasma(); break;
262                 case "mlrs": spawnfunc_turret_mlrs(); break;
263                 case "phaser": spawnfunc_turret_phaser(); break;
264                 case "hellion": spawnfunc_turret_hellion(); break;
265                 case "walker": spawnfunc_turret_walker(); break;
266                 case "flac": spawnfunc_turret_flac(); break;
267                 case "tesla": spawnfunc_turret_tesla(); break;
268                 case "fusionreactor": spawnfunc_turret_fusionreactor(); break;
269                 case "barricade": spawn_barricade(); break;
270         }
271                 
272         self = oldself;
273 }
274
275 void buffturret (entity tur, float buff)
276 {
277         tur.turret_buff           += 1;
278         tur.max_health            *= buff;
279         tur.tur_health             = tur.max_health;
280         tur.health                         = tur.max_health;
281         tur.ammo_max              *= buff;
282         tur.ammo_recharge     *= buff;
283     tur.shot_dmg          *= buff;
284     tur.shot_refire       -= buff * 0.2;
285     tur.shot_radius       *= buff;
286     tur.shot_speed        *= buff;
287     tur.shot_spread       *= buff;
288     tur.shot_force        *= buff;
289 }
290
291 void AnnounceSpawn(string anounce)
292 {
293         Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_TD_ANNOUNCE_SPAWN, anounce);
294 }
295
296 entity PickSpawn (float strngth, float type)
297 {
298         entity e;
299         RandomSelection_Init();
300         for(e = world;(e = find(e, classname, "monster_swarm")); )
301         {
302                 if(flyspawns_count > 0 && type == SWARM_FLY && e.spawntype != SWARM_FLY) continue;
303                 if(waterspawns_count > 0 && type == SWARM_SWIM && e.spawntype != SWARM_SWIM) continue;
304                 
305                 RandomSelection_Add(e, 0, string_null, 1, 1);
306         }
307
308         return RandomSelection_chosen_ent;
309 }
310
311 void TD_SpawnMonster(string mnster, float strngth, float type)
312 {
313         entity e, mon;
314         
315         e = PickSpawn(strngth, type);
316         
317         if(e == world) // couldn't find anything for our class, so check for normal spawns
318                 e = PickSpawn(SWARM_NORMAL, SWARM_NORMAL);
319                 
320         if(e == world)
321         {
322                 print("Warning: couldn't find any monster_swarm spawnpoints, no monsters will spawn!\n");
323                 return;
324         }
325   
326         mon = spawnmonster(mnster, e, e, e.origin, FALSE, 0);
327         if(e.target2)
328         {
329                 if(random() > 0.5)
330                         mon.target = e.target2;
331                 else
332                         mon.target = e.target;
333         }
334         else
335                 mon.target = e.target;
336 }
337
338 float Monster_GetStrength(string mnster)
339 {
340         switch(mnster)
341         {
342                 case "knight":
343                 case "wizard":
344                 case "soldier":
345                 case "enforcer":
346                 case "zombie":
347                 case "tarbaby":
348                 case "dog":
349                 case "spider":
350                 case "fish":
351                         return SWARM_WEAK;
352                 case "ogre":
353                 case "shambler":
354                 case "shalrath":
355                 case "hellknight":
356                 case "demon":
357                         return SWARM_STRONG;
358                 default:
359                         return SWARM_NORMAL;
360         }
361 }
362
363 float Monster_GetType(string mnster)
364 {
365         switch(mnster)
366         {
367                 default:
368                 case "knight":
369                 case "soldier":
370                 case "enforcer":
371                 case "zombie":
372                 case "spider":
373                 case "tarbaby":
374                 case "dog":
375                 case "ogre":
376                 case "shambler":
377                 case "shalrath":
378                 case "hellknight":
379                 case "demon":
380                         return SWARM_NORMAL;
381                 case "wizard":
382                         return SWARM_FLY;
383                 case "fish":
384                         return SWARM_SWIM;
385         }
386 }
387
388 string RandomMonster()
389 {
390         RandomSelection_Init();
391         
392         if(n_demons) RandomSelection_Add(world, 0, "demon", 1, 1);
393         if(n_shalraths) RandomSelection_Add(world, 0, "vore", 1, 1);
394         if(n_soldiers) RandomSelection_Add(world, 0, "soldier", 1, 1);
395         if(n_hknights) RandomSelection_Add(world, 0, "hellknight", 1, 1);
396         if(n_enforcers) RandomSelection_Add(world, 0, "enforcer", 1, 1);
397         if(n_zombies) RandomSelection_Add(world, 0, "zombie", 1, 1);
398         if(n_spiders) RandomSelection_Add(world, 0, "spider", 1, 1);
399         if(n_ogres) RandomSelection_Add(world, 0, "ogre", 1, 1);
400         if(n_dogs) RandomSelection_Add(world, 0, "dog", 1, 1);
401         if(n_knights) RandomSelection_Add(world, 0, "knight", 1, 1);
402         if(n_shamblers) RandomSelection_Add(world, 0, "shambler", 0.2, 0.2);
403         if(n_tarbabies) RandomSelection_Add(world, 0, "spawn", 0.2, 0.2);
404         if(n_wizards && flyspawns_count) RandomSelection_Add(world, 0, "scrag", 1, 1);
405         if(n_fish && waterspawns_count) RandomSelection_Add(world, 0, "fish", 0.2, 0.2);
406         
407         return RandomSelection_chosen_string;
408 }
409
410 void combat_phase()
411 {
412         string whichmon;
413         float mstrength, montype;
414         
415         current_phase = PHASE_COMBAT;
416         
417         if(monster_count <= 0)
418         {
419                 wave_end(FALSE);
420                 return;
421         }
422         
423         self.think = combat_phase;
424         
425         whichmon = RandomMonster();
426         
427         mstrength = Monster_GetStrength(whichmon);
428         montype = Monster_GetType(whichmon);
429         
430         if(current_monsters <= max_current && whichmon != "")
431         {
432                 TD_SpawnMonster(whichmon, mstrength, montype);
433                 self.nextthink = time + spawn_delay;
434         }
435         else
436                 self.nextthink = time + 6;
437 }
438
439 void queue_monsters(float maxmonsters)
440 {
441         float mc = 11; // note: shambler + tarbaby = 1
442         
443         if(waterspawns_count > 0)
444                 mc += 1;
445         if(flyspawns_count > 0)
446                 mc += 1;
447                 
448         DistributeEvenly_Init(maxmonsters, mc);
449         n_demons        = DistributeEvenly_Get(1);
450         n_ogres         = DistributeEvenly_Get(1);
451         n_dogs          = DistributeEvenly_Get(1);
452         n_knights   = DistributeEvenly_Get(1);
453         n_shalraths = DistributeEvenly_Get(1);
454         n_soldiers  = DistributeEvenly_Get(1);
455         n_hknights  = DistributeEvenly_Get(1);
456         n_enforcers = DistributeEvenly_Get(1);
457         n_zombies   = DistributeEvenly_Get(1);
458         n_spiders   = DistributeEvenly_Get(1);
459         n_tarbabies = DistributeEvenly_Get(0.7);
460         n_shamblers = DistributeEvenly_Get(0.3);
461         if(flyspawns_count > 0)
462                 n_wizards = DistributeEvenly_Get(1);
463         if(waterspawns_count > 0)
464                 n_fish = DistributeEvenly_Get(1);
465 }
466
467 void combat_phase_begin()
468 {
469         monster_count = totalmonsters;
470         entity gen;
471         
472         print("^1Combat phase!\n");
473         
474         Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_TD_PHASE_COMBAT);
475         
476         if(autocvar_sv_eventlog)
477                 GameLogEcho(":combatphase");
478                 
479         self.think = combat_phase;
480         self.nextthink = time + 1;
481         
482         for(gen = world;(gen = find(gen, classname, "td_generator")); )
483                 gen.takedamage = DAMAGE_AIM;
484 }
485
486 float cphase_updates;
487 void combat_phase_announce() // TODO: clean up these fail nextthinks...
488 {
489         cphase_updates += 1;
490         
491         if(cphase_updates == 0)
492                 Announce("prepareforbattle");
493         else if(cphase_updates == 3)
494                 Announce("3");
495         else if(cphase_updates == 4)
496                 Announce("2");
497         else if(cphase_updates == 5)
498                 Announce("1");
499         else if(cphase_updates == 6)
500         {
501                 Announce("begin");
502                 combat_phase_begin();
503         }
504         
505         if(cphase_updates >= 6)
506                 return;
507
508         self.think = combat_phase_announce;
509         self.nextthink = time + 1;
510 }
511
512 void build_phase()
513 {
514         entity head;
515         float n_players = 0, gen_washealed = FALSE, player_washealed = FALSE;
516     string buildmsg, healmsg, countmsg, startmsg, genhealmsg, mainmsg;
517         
518         current_phase = PHASE_BUILD;
519         
520         for(head = world;(head = find(head, classname, "td_generator")); )
521         {
522                 if(head.health <= 5 && head.max_health > 10)
523                         Announce("lastsecond");
524                         
525                 if(head.health < head.max_health)
526                 {
527                         gen_washealed = TRUE;
528                         head.health = head.max_health;
529                         WaypointSprite_UpdateHealth(head.sprite, head.health);
530                 }
531                 head.takedamage = DAMAGE_NO;
532         }
533         
534         FOR_EACH_PLAYER(head)
535         {
536                 if(head.health < 100)
537                 {
538                         player_washealed = TRUE;
539                         break; // we found 1, so no need to check the others
540                 }
541         }
542                 
543         totalmonsters += autocvar_g_td_monster_count_increment * wave_count;
544         monster_skill += autocvar_g_td_monsters_skill_increment;
545         
546         if(wave_count < 1) wave_count = 1;
547
548         // TODO: make this play better with the notification system?
549         genhealmsg = (gen_washealed) ? ((td_gencount == 1) ? " and generator " : " and generators ") : "";
550         buildmsg = sprintf("%s build phase... ", (wave_count == max_waves) ? "^1Final wave^3" : strcat("Wave ", ftos(wave_count)));
551         healmsg = (player_washealed) ? strcat("All players ", genhealmsg, "healed. ") : "";     
552     countmsg = strcat("Next monsters: ", ftos(totalmonsters), ". ");
553     startmsg = strcat("Wave starts in ", ftos(autocvar_g_td_buildphase_time), " seconds");
554         mainmsg = strcat(buildmsg, healmsg, countmsg, startmsg);
555         
556         Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_TD_PHASE_BUILD, mainmsg);
557         
558         FOR_EACH_PLAYER(head) 
559     {
560                 if(head.health < 100) head.health = 100;
561                         
562                 if(gen_washealed) PlayerScore_Add(head, SP_TD_SCORE, -autocvar_g_td_generator_damaged_points);
563                         
564         n_players += 1;
565     }
566     
567     FOR_EACH_MONSTER(head)
568     {
569                 if(head.health <= 0)
570                         continue;
571                         
572         print(strcat("Warning: Monster still alive during build phase! Monster name: ", head.netname, "\n"));
573                 
574                 WaypointSprite_Kill(head.sprite);
575         remove(head);
576     }
577         
578         if(n_players >= 2)
579         {
580                 totalmonsters += n_players;
581                 monster_skill += n_players * 0.05;
582         }
583         
584         if(monster_skill < 1) monster_skill = 1;
585                 
586         if(totalmonsters < 1) totalmonsters = ((autocvar_g_td_monster_count > 0) ? autocvar_g_td_monster_count : 10);
587         
588         monsters_total = totalmonsters;
589         monsters_killed = 0;
590         
591         print(strcat(buildmsg, healmsg, countmsg, startmsg, "\n"));
592                 
593         queue_monsters(totalmonsters);
594         
595         cphase_updates = -1;
596         
597         if(autocvar_sv_eventlog)
598         GameLogEcho(strcat(":buildphase:", ftos(wave_count), ":", ftos(totalmonsters)));
599         
600         self.think = combat_phase_announce;
601         self.nextthink = time + build_time - 6;
602 }
603
604 void wave_end(float starting)
605 {
606         if not(starting)
607                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_TD_VICTORY, ((wave_count >= max_waves) ? "Level" : "Wave"));
608         
609         if not(starting)
610         {
611                 print((wave_count >= max_waves) ? "^2Level victory!\n" : "^2Wave victory!\n");
612                 if(autocvar_sv_eventlog)
613             GameLogEcho(strcat(":wave:", ftos(wave_count), ":victory"));
614         }
615         
616         if(wave_count >= max_waves)
617         {
618                 gensurvived = TRUE;
619                 return;
620         }
621         
622         if not(starting)
623                 wave_count += 1;
624                 
625         self.think = build_phase;
626         self.nextthink = time + 3;
627 }
628
629 void td_ScoreRules()
630 {
631         ScoreInfo_SetLabel_PlayerScore(SP_TD_SCORE,             "score",         SFL_SORT_PRIO_PRIMARY);
632         ScoreInfo_SetLabel_PlayerScore(SP_TD_KILLS,             "kills",         SFL_LOWER_IS_BETTER);
633         ScoreInfo_SetLabel_PlayerScore(SP_TD_TURKILLS,  "frags",         SFL_LOWER_IS_BETTER);
634         ScoreInfo_SetLabel_PlayerScore(SP_TD_DEATHS,    "deaths",    SFL_LOWER_IS_BETTER);
635         ScoreInfo_SetLabel_PlayerScore(SP_TD_SUICIDES,  "suicides",  SFL_LOWER_IS_BETTER | SFL_ALLOW_HIDE);
636         ScoreRules_basics_end();
637 }
638
639 void td_SpawnController()
640 {
641         entity oldself = self;
642         self = spawn();
643         self.classname = "td_controller";
644         spawnfunc_td_controller();
645         self = oldself;
646 }
647
648 void td_DelayedInit()
649 {
650         if(find(world, classname, "td_controller") == world)
651         {
652                 print("No ""td_controller"" entity found on this map, creating it anyway.\n");
653                 td_SpawnController();
654         }
655         
656         td_ScoreRules();
657 }
658
659 void td_Init()
660 {
661         InitializeEntity(world, td_DelayedInit, INITPRIO_GAMETYPE);
662 }
663
664 MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
665 {
666         if(turret_flags & TFL_TARGETSELECT_MISSILESONLY)
667     if(turret_target.flags & FL_PROJECTILE)
668         if(turret_target.owner.flags & FL_MONSTER)
669         return TRUE; // flac support
670                         
671         if(turret.turrcaps_flags & TFL_TURRCAPS_SUPPORT && turret_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
672                 return TRUE;
673         if not(turret_target.flags & FL_MONSTER)
674                 turret_target = world;
675                 
676         return FALSE;
677 }
678
679 MUTATOR_HOOKFUNCTION(td_PlayerThink)
680 {
681         self.stat_current_wave = wave_count;
682         self.stat_totalwaves = max_waves;
683         
684         return FALSE;
685 }
686
687 MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
688 {
689         self.bot_attack = FALSE;
690         
691         return FALSE;
692 }
693
694 MUTATOR_HOOKFUNCTION(td_PlayerDies)
695 {
696         if(frag_attacker.flags & FL_MONSTER)
697                 PlayerScore_Add(frag_target, SP_TD_DEATHS, 1);
698                 
699         if(frag_target == frag_attacker)
700                 PlayerScore_Add(frag_attacker, SP_TD_SUICIDES, 1);
701
702         return FALSE;
703 }
704
705 MUTATOR_HOOKFUNCTION(td_GiveFragsForKill)
706 {
707         frag_score = 0;
708                 
709         return TRUE; // no frags counted in td
710 }
711
712 MUTATOR_HOOKFUNCTION(td_PlayerDamage)
713 {
714         if(frag_attacker.realowner == frag_target)
715                 frag_damage = 0;
716                 
717         if(frag_target.flags & FL_MONSTER && time < frag_target.spawnshieldtime)
718                 frag_damage = 0;
719                 
720         if(frag_target.vehicle_flags & VHF_ISVEHICLE && !(frag_attacker.flags & FL_MONSTER))
721                 frag_damage = 0;
722                 
723         if(frag_attacker.vehicle_flags & VHF_ISVEHICLE && !(frag_target.flags & FL_MONSTER))
724                 frag_damage = 0;
725                 
726         if(!autocvar_g_td_pvp && frag_attacker != frag_target && frag_target.classname == STR_PLAYER && frag_attacker.classname == STR_PLAYER)
727                 frag_damage = 0;
728                 
729         if(frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET && frag_target.classname == STR_PLAYER)
730                 frag_damage = 0;
731                 
732         if((frag_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && !(frag_attacker.flags & FL_MONSTER || frag_attacker.turrcaps_flags & TFL_TURRCAPS_SUPPORT))
733                 frag_damage = 0;
734                 
735         return TRUE;
736 }
737
738 MUTATOR_HOOKFUNCTION(td_TurretDies)
739 {
740         if(self.realowner)
741                 self.realowner.turret_cnt -= 1;
742                         
743         return FALSE;
744 }
745
746 MUTATOR_HOOKFUNCTION(td_MonsterCheckBossFlag)
747 {
748         // No minibosses in tower defense
749         return TRUE;
750 }
751
752 MUTATOR_HOOKFUNCTION(td_MonsterMove)
753 {
754         entity player;
755         float n_players = 0;
756         FOR_EACH_PLAYER(player) { ++n_players; }
757         
758         if(n_players < 1) // no players online, so do nothing
759         {
760                 monster_target = world;
761                 monster_speed_run = monster_speed_walk = 0;
762                 return FALSE;
763         }
764         
765         if not(self.enemy) // don't change targets while attacking
766         if((vlen(self.goalentity.origin - self.origin) <= 100 && self.goalentity.classname == "td_waypoint") || (vlen(self.goalentity.origin - self.origin) <= 200 && self.flags & FL_FLY && self.goalentity.classname == "td_waypoint"))
767         {
768                 if(self.goalentity.target2)
769                 {
770                         if(random() > 0.5)
771                                 self.target = self.goalentity.target2;
772                         else
773                                 self.target = self.goalentity.target;
774                 }
775                 else
776                         self.target = self.goalentity.target;
777                                 
778                 self.goalentity = find(world, targetname, self.target);
779                 
780                 if(self.goalentity == world)
781                         self.goalentity = PickGenerator();
782         }
783         
784         monster_speed_run = m_speed_run * monster_skill;
785         monster_speed_walk = m_speed_walk * monster_skill;
786         
787         return FALSE;
788 }
789
790 MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
791 {
792         if(self.realowner == world) // nothing spawned it, so kill it
793         {
794                 WaypointSprite_Kill(self.sprite);
795                 remove(self);
796                 return TRUE;
797         }
798         
799         current_monsters += 1;
800         
801         self.spawnshieldtime = time + autocvar_g_td_monsters_spawnshield_time;
802         
803         self.drop_size = self.health * 0.05;
804         
805         if(self.drop_size < 1) self.drop_size = 1;
806         
807         self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_BODY;
808         
809         self.origin += '0 0 25'; // hopefully this fixes monsters falling through the floor
810         
811         switch(self.classname)
812         {
813                 case "monster_knight": n_knights -= 1; break;
814                 case "monster_dog": n_dogs -= 1; break;
815                 case "monster_ogre": n_ogres -= 1; break;
816                 case "monster_shambler": n_shamblers -= 1; AnnounceSpawn("Shambler"); break;
817                 case "monster_wizard": n_wizards -= 1; break;
818                 case "monster_shalrath": n_shalraths -= 1; break;
819                 case "monster_soldier": n_soldiers -= 1; break;
820                 case "monster_hellknight": n_hknights -= 1; break;
821                 case "monster_enforcer": n_enforcers -= 1; break;
822                 case "monster_demon": n_demons -= 1; break;
823                 case "monster_zombie": n_zombies -= 1; break;
824                 case "monster_spider": n_spiders -= 1; break;
825                 case "monster_tarbaby": n_tarbabies -= 1; break;
826         }
827         
828         return TRUE;
829 }
830
831 MUTATOR_HOOKFUNCTION(td_MonsterDies)
832 {
833         entity oldself;
834         vector backuporigin;
835
836         monster_count -= 1;
837         current_monsters -= 1;
838         monsters_killed += 1;
839         
840         if(frag_attacker.classname == STR_PLAYER)
841         {
842                 PlayerScore_Add(frag_attacker, SP_TD_SCORE, autocvar_g_td_kill_points);
843                 PlayerScore_Add(frag_attacker, SP_TD_KILLS, 1);
844         }
845         else if(frag_attacker.realowner.classname == STR_PLAYER)
846         {
847                 PlayerScore_Add(frag_attacker.realowner, SP_TD_SCORE, autocvar_g_td_turretkill_points);
848                 PlayerScore_Add(frag_attacker.realowner, SP_TD_TURKILLS, 1);
849         }
850
851         backuporigin = self.origin;
852         oldself = self;
853         self = spawn();
854         
855         self.gravity = 1;
856         setorigin(self, backuporigin + '0 0 5');
857         spawn_td_fuel(oldself.drop_size);
858         self.touch = M_Item_Touch;
859         if(self == world)
860         {
861                 self = oldself;
862                 return FALSE;
863         }
864         SUB_SetFade(self, time + 5, 1);
865         
866         self = oldself;
867
868         return FALSE;
869 }
870
871 MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
872 {
873         float n_players = 0;
874         entity player;
875         local entity e;
876         
877         FOR_EACH_PLAYER(player) { ++n_players; }
878         
879         if(n_players < 1) // no players online, so do nothing
880         {
881                 self.enemy = world;
882                 return TRUE;
883         }
884         
885         for(e = world;(e = findflags(e, monster_attack, TRUE)); ) 
886         {
887                 if(ignore_turrets)
888                 if(e.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
889                         continue;
890                 
891                 if(monster_isvalidtarget(e, self, FALSE))
892                 if((vlen(trace_endpos - self.origin) < 200 && e.turrcaps_flags & TFL_TURRCAPS_ISTURRET) || (vlen(trace_endpos - self.origin) < 200 && e.classname != "td_generator") || (vlen(trace_endpos - self.origin) < 500 && e.classname == "td_generator"))
893                 {
894                         self.enemy = e;
895                 }
896         }
897         
898         return TRUE;
899 }
900
901 MUTATOR_HOOKFUNCTION(td_SetStartItems)
902 {
903         start_ammo_fuel = 150; // to be nice...
904         
905         return FALSE;
906 }
907
908 MUTATOR_HOOKFUNCTION(td_TurretSpawn)
909 {
910         if(self.realowner == world)
911                 return TRUE; // wasn't spawned by a player
912                 
913         self.bot_attack = FALSE;
914         self.turret_buff = 1;
915         
916         return FALSE;
917 }
918
919 MUTATOR_HOOKFUNCTION(td_DisableVehicles)
920 {
921         // you shall not spawn!
922         return TRUE;
923 }
924
925 MUTATOR_HOOKFUNCTION(td_PlayerCommand)
926 {
927         if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
928         
929         makevectors(self.v_angle);
930         WarpZone_TraceLine(self.origin + self.view_ofs, self.origin + self.view_ofs + v_forward * 100, MOVE_NORMAL, self);
931         
932         if(cmd_name == "turretspawn")
933         {
934                 if(argv(1) == "list")
935                 {
936                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_LIST, "mlrs walker plasma towerbuff flac barricade");
937                         return TRUE;
938                 }
939                 if(self.classname != STR_PLAYER || self.health <= 0)
940                 { 
941                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_CANTSPAWN);
942                         return TRUE;
943                 }
944                 if(self.turret_cnt >= max_turrets)
945                 {
946                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_MAXTURRETS, max_turrets);
947                         return TRUE;
948                 }
949                 switch(argv(1))
950                 {
951                         case "plasma":
952                         {
953                 if(self.ammo_fuel < autocvar_g_td_turret_plasma_cost) break;
954                                 self.ammo_fuel -= autocvar_g_td_turret_plasma_cost;
955                                 spawnturret(self, self, "plasma", trace_endpos);
956                                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_SPAWN);
957                                 return TRUE;
958                         }
959                         case "mlrs":
960                         {
961                 if(self.ammo_fuel < autocvar_g_td_turret_mlrs_cost) break;
962                                 self.ammo_fuel -= autocvar_g_td_turret_mlrs_cost;
963                                 spawnturret(self, self, "mlrs", trace_endpos);
964                                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_SPAWN);
965                                 return TRUE;
966                         }
967                         case "flac":
968                         {
969                 if(self.ammo_fuel < autocvar_g_td_turret_flac_cost) break;
970                                 self.ammo_fuel -= autocvar_g_td_turret_flac_cost;
971                                 spawnturret(self, self, "flac", trace_endpos);
972                                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_SPAWN);
973                                 return TRUE;
974                         }
975                         case "walker":
976                         {
977                 if(self.ammo_fuel < autocvar_g_td_turret_walker_cost) break;
978                                 self.ammo_fuel -= autocvar_g_td_turret_walker_cost;
979                                 spawnturret(self, self, "walker", trace_endpos);
980                                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_SPAWN);
981                                 return TRUE;
982                         }
983                         case "towerbuff":
984                         {
985                 if(self.ammo_fuel < autocvar_g_td_tower_buff_cost) break;
986                                 self.ammo_fuel -= autocvar_g_td_tower_buff_cost;
987                                 spawnturret(self, self, "fusionreactor", trace_endpos);
988                                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_SPAWN);
989                                 return TRUE;
990                         }
991                         case "barricade":
992                         {
993                                 if(self.ammo_fuel < autocvar_g_td_barricade_cost) break;
994                                 self.ammo_fuel -= autocvar_g_td_barricade_cost;
995                                 spawnturret(self, self, "barricade", trace_endpos);
996                                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_SPAWN);
997                                 return TRUE;
998                         }
999                         default:
1000                         {
1001                                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_INVALID);
1002                                 return TRUE;
1003                         }
1004                 }
1005                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_NOFUEL);
1006                 return TRUE;
1007         }
1008         if(cmd_name == "repairturret")
1009         {
1010                 if(trace_ent.realowner != self || !(trace_ent.turrcaps_flags & TFL_TURRCAPS_ISTURRET))
1011                 {
1012                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_AIM_REPAIR);
1013                         return TRUE;
1014                 }
1015                 if(self.ammo_fuel < autocvar_g_td_turret_repair_cost)   
1016                 {
1017                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_NOFUEL_REPAIR, autocvar_g_td_turret_repair_cost);
1018                         return TRUE;
1019                 }
1020                 if(trace_ent.health >= trace_ent.max_health)
1021                 {
1022                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_MAXHEALTH);
1023                         return TRUE;
1024                 }
1025                 
1026                 self.ammo_fuel -= autocvar_g_td_turret_repair_cost;
1027                 trace_ent.SendFlags |= TNSF_STATUS;
1028                 trace_ent.health = bound(1, trace_ent.health + 100, trace_ent.max_health);
1029                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_REPAIR);
1030                 
1031                 return TRUE;
1032         }
1033         if(cmd_name == "buffturret")
1034         {
1035                 if(trace_ent.realowner != self || !(trace_ent.turrcaps_flags & TFL_TURRCAPS_ISTURRET))
1036                 {
1037                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_AIM_UPGRADE);
1038                         return TRUE;
1039                 }
1040                 if(self.ammo_fuel < autocvar_g_td_turret_upgrade_cost)  
1041                 {
1042                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_NOFUEL_UPGRADE, autocvar_g_td_turret_upgrade_cost);
1043                         return TRUE;
1044                 }
1045                 if(trace_ent.turret_buff >= 3)
1046                 {
1047                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_MAXPOWER);
1048                         return TRUE;
1049                 }
1050                 
1051                 self.ammo_fuel -= autocvar_g_td_turret_upgrade_cost;
1052                 trace_ent.SendFlags |= TNSF_STATUS;
1053                 buffturret(trace_ent, 1.2);
1054                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_UPGRADE);
1055                 
1056                 return TRUE;
1057         }
1058         if(cmd_name == "turretremove")
1059         {
1060                 if((trace_ent.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && trace_ent.realowner == self)
1061                 {
1062                         self.turret_cnt -= 1;
1063                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_REMOVE);
1064                         WaypointSprite_Kill(trace_ent.sprite);
1065                         remove(trace_ent.tur_head);
1066                         remove(trace_ent);
1067                         return TRUE;
1068                 }
1069                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_AIM_REMOVE);
1070                 return TRUE;
1071         }
1072         
1073         return FALSE;
1074 }
1075
1076 MUTATOR_DEFINITION(gamemode_td)
1077 {
1078         MUTATOR_HOOK(MonsterSpawn, td_MonsterSpawn, CBC_ORDER_ANY);
1079         MUTATOR_HOOK(MonsterDies, td_MonsterDies, CBC_ORDER_ANY);
1080         MUTATOR_HOOK(MonsterMove, td_MonsterMove, CBC_ORDER_ANY);
1081         MUTATOR_HOOK(MonsterFindTarget, td_MonsterFindTarget, CBC_ORDER_ANY);
1082         MUTATOR_HOOK(MonsterCheckBossFlag, td_MonsterCheckBossFlag, CBC_ORDER_ANY);
1083         MUTATOR_HOOK(SetStartItems, td_SetStartItems, CBC_ORDER_ANY);
1084         MUTATOR_HOOK(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
1085         MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
1086         MUTATOR_HOOK(TurretDies, td_TurretDies, CBC_ORDER_ANY);
1087         MUTATOR_HOOK(GiveFragsForKill, td_GiveFragsForKill, CBC_ORDER_ANY);
1088         MUTATOR_HOOK(PlayerPreThink, td_PlayerThink, CBC_ORDER_ANY);
1089         MUTATOR_HOOK(PlayerDies, td_PlayerDies, CBC_ORDER_ANY);
1090         MUTATOR_HOOK(PlayerDamage_Calculate, td_PlayerDamage, CBC_ORDER_ANY);
1091         MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
1092         MUTATOR_HOOK(VehicleSpawn, td_DisableVehicles, CBC_ORDER_ANY);
1093         MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, CBC_ORDER_ANY);
1094         
1095         MUTATOR_ONADD
1096         {
1097                 if(time > 1) // game loads at time 1
1098                         error("This is a game type and it cannot be added at runtime.");        
1099                 cvar_settemp("g_monsters", "1");
1100                 cvar_settemp("g_turrets", "1");
1101                 td_Init();
1102         }
1103
1104         MUTATOR_ONREMOVE
1105         {
1106                 error("This is a game type and it cannot be removed at runtime.");
1107         }
1108
1109         return FALSE;
1110 }