]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_towerdefense.qc
Also fix turret owner on connect
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_towerdefense.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(IS_PLAYER(attacker) || 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                 dprint("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 float td_checkfuel(entity ent, string tur)
238 {
239         float turcost = cvar(strcat("g_td_turret_", tur, "_cost"));
240         
241         if(ent.ammo_fuel < turcost)
242         {
243                 Send_Notification(NOTIF_ONE, ent, MSG_INFO, INFO_TD_NOFUEL);
244                 return FALSE;
245         }
246         
247         ent.ammo_fuel -= turcost;
248         
249         return TRUE;
250 }       
251
252 void spawnturret(entity spawnedby, entity own, string turet, vector orig)
253 {
254         if not(IS_PLAYER(spawnedby)) { dprint("Warning: A non-player entity tried to spawn a turret\n"); return; }
255                 
256         entity oldself;
257         
258         oldself = self;
259         self = spawn();
260         
261         setorigin(self, orig);
262         self.spawnflags = TSL_NO_RESPAWN;
263         self.monster_attack = TRUE;
264         self.realowner = own;
265         self.playerid = own.playerid;
266         self.angles_y = spawnedby.v_angle_y;
267         spawnedby.turret_cnt += 1;
268         self.colormap = spawnedby.colormap;
269         
270         switch(turet)
271         {
272                 case "plasma": if not(td_checkfuel(spawnedby, turet)) return; spawnfunc_turret_plasma(); break;
273                 case "mlrs": if not(td_checkfuel(spawnedby, turet)) return; spawnfunc_turret_mlrs(); break;
274                 case "walker": if not(td_checkfuel(spawnedby, turet)) return; spawnfunc_turret_walker(); break;
275                 case "flac": if not(td_checkfuel(spawnedby, turet)) return; spawnfunc_turret_flac(); break;
276                 case "towerbuff": if not(td_checkfuel(spawnedby, turet)) return; spawnfunc_turret_fusionreactor(); break;
277                 case "barricade": if not(td_checkfuel(spawnedby, turet)) return; spawn_barricade(); break;
278                 default: Send_Notification(NOTIF_ONE, spawnedby, MSG_INFO, INFO_TD_INVALID); remove(self); self = oldself; return;
279         }
280         
281         Send_Notification(NOTIF_ONE, spawnedby, MSG_INFO, INFO_TD_SPAWN);
282                 
283         self = oldself;
284 }
285
286 void buffturret (entity tur, float buff)
287 {
288         tur.turret_buff           += 1;
289         tur.max_health            *= buff;
290         tur.tur_health             = tur.max_health;
291         tur.health                         = tur.max_health;
292         tur.ammo_max              *= buff;
293         tur.ammo_recharge     *= buff;
294     tur.shot_dmg          *= buff;
295     tur.shot_refire       -= buff * 0.2;
296     tur.shot_radius       *= buff;
297     tur.shot_speed        *= buff;
298     tur.shot_spread       *= buff;
299     tur.shot_force        *= buff;
300 }
301
302 void AnnounceSpawn(string anounce)
303 {
304         Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_TD_ANNOUNCE_SPAWN, anounce);
305 }
306
307 entity PickSpawn (float strngth, float type)
308 {
309         entity e;
310         RandomSelection_Init();
311         for(e = world;(e = find(e, classname, "monster_swarm")); )
312         {
313                 if(flyspawns_count > 0 && type == SWARM_FLY && e.spawntype != SWARM_FLY) continue;
314                 if(waterspawns_count > 0 && type == SWARM_SWIM && e.spawntype != SWARM_SWIM) continue;
315                 
316                 RandomSelection_Add(e, 0, string_null, 1, 1);
317         }
318
319         return RandomSelection_chosen_ent;
320 }
321
322 void TD_SpawnMonster(string mnster, float strngth, float type)
323 {
324         entity e, mon;
325         
326         e = PickSpawn(strngth, type);
327         
328         if(e == world) // couldn't find anything for our class, so check for normal spawns
329                 e = PickSpawn(SWARM_NORMAL, SWARM_NORMAL);
330                 
331         if(e == world)
332         {
333                 dprint("Warning: couldn't find any monster_swarm spawnpoints, no monsters will spawn!\n");
334                 return;
335         }
336   
337         mon = spawnmonster(mnster, e, e, e.origin, FALSE, 0);
338         if(e.target2)
339         {
340                 if(random() > 0.5)
341                         mon.target = e.target2;
342                 else
343                         mon.target = e.target;
344         }
345         else
346                 mon.target = e.target;
347 }
348
349 float Monster_GetStrength(string mnster)
350 {
351         switch(mnster)
352         {
353                 case "knight":
354                 case "wizard":
355                 case "soldier":
356                 case "enforcer":
357                 case "zombie":
358                 case "tarbaby":
359                 case "dog":
360                 case "spider":
361                 case "fish":
362                         return SWARM_WEAK;
363                 case "ogre":
364                 case "shambler":
365                 case "shalrath":
366                 case "hellknight":
367                 case "demon":
368                         return SWARM_STRONG;
369                 default:
370                         return SWARM_NORMAL;
371         }
372 }
373
374 float Monster_GetType(string mnster)
375 {
376         switch(mnster)
377         {
378                 default:
379                 case "knight":
380                 case "soldier":
381                 case "enforcer":
382                 case "zombie":
383                 case "spider":
384                 case "tarbaby":
385                 case "dog":
386                 case "ogre":
387                 case "shambler":
388                 case "shalrath":
389                 case "hellknight":
390                 case "demon":
391                         return SWARM_NORMAL;
392                 case "wizard":
393                         return SWARM_FLY;
394                 case "fish":
395                         return SWARM_SWIM;
396         }
397 }
398
399 string RandomMonster()
400 {
401         RandomSelection_Init();
402         
403         if(n_demons) RandomSelection_Add(world, 0, "demon", 1, 1);
404         if(n_shalraths) RandomSelection_Add(world, 0, "vore", 1, 1);
405         if(n_soldiers) RandomSelection_Add(world, 0, "soldier", 1, 1);
406         if(n_hknights) RandomSelection_Add(world, 0, "hellknight", 1, 1);
407         if(n_enforcers) RandomSelection_Add(world, 0, "enforcer", 1, 1);
408         if(n_zombies) RandomSelection_Add(world, 0, "zombie", 1, 1);
409         if(n_spiders) RandomSelection_Add(world, 0, "spider", 1, 1);
410         if(n_ogres) RandomSelection_Add(world, 0, "ogre", 1, 1);
411         if(n_dogs) RandomSelection_Add(world, 0, "dog", 1, 1);
412         if(n_knights) RandomSelection_Add(world, 0, "knight", 1, 1);
413         if(n_shamblers) RandomSelection_Add(world, 0, "shambler", 0.2, 0.2);
414         if(n_tarbabies) RandomSelection_Add(world, 0, "spawn", 0.2, 0.2);
415         if(n_wizards && flyspawns_count) RandomSelection_Add(world, 0, "scrag", 1, 1);
416         if(n_fish && waterspawns_count) RandomSelection_Add(world, 0, "fish", 0.2, 0.2);
417         
418         return RandomSelection_chosen_string;
419 }
420
421 void combat_phase()
422 {
423         string whichmon;
424         float mstrength, montype;
425         
426         current_phase = PHASE_COMBAT;
427         
428         if(monster_count <= 0)
429         {
430                 wave_end(FALSE);
431                 return;
432         }
433         
434         self.think = combat_phase;
435         
436         whichmon = RandomMonster();
437         
438         mstrength = Monster_GetStrength(whichmon);
439         montype = Monster_GetType(whichmon);
440         
441         if(current_monsters <= max_current && whichmon != "")
442         {
443                 TD_SpawnMonster(whichmon, mstrength, montype);
444                 self.nextthink = time + spawn_delay;
445         }
446         else
447                 self.nextthink = time + 6;
448 }
449
450 void queue_monsters(float maxmonsters)
451 {
452         float mc = 11; // note: shambler + tarbaby = 1
453         
454         if(waterspawns_count > 0)
455                 mc += 1;
456         if(flyspawns_count > 0)
457                 mc += 1;
458                 
459         DistributeEvenly_Init(maxmonsters, mc);
460         n_demons        = DistributeEvenly_Get(1);
461         n_ogres         = DistributeEvenly_Get(1);
462         n_dogs          = DistributeEvenly_Get(1);
463         n_knights   = DistributeEvenly_Get(1);
464         n_shalraths = DistributeEvenly_Get(1);
465         n_soldiers  = DistributeEvenly_Get(1);
466         n_hknights  = DistributeEvenly_Get(1);
467         n_enforcers = DistributeEvenly_Get(1);
468         n_zombies   = DistributeEvenly_Get(1);
469         n_spiders   = DistributeEvenly_Get(1);
470         n_tarbabies = DistributeEvenly_Get(0.7);
471         n_shamblers = DistributeEvenly_Get(0.3);
472         if(flyspawns_count > 0)
473                 n_wizards = DistributeEvenly_Get(1);
474         if(waterspawns_count > 0)
475                 n_fish = DistributeEvenly_Get(1);
476 }
477
478 void combat_phase_begin()
479 {
480         monster_count = totalmonsters;
481         entity gen;
482         
483         Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_PHASE_COMBAT);
484         
485         if(autocvar_sv_eventlog)
486                 GameLogEcho(":combatphase");
487                 
488         self.think = combat_phase;
489         self.nextthink = time + 1;
490         
491         for(gen = world;(gen = find(gen, classname, "td_generator")); )
492                 gen.takedamage = DAMAGE_AIM;
493 }
494
495 float cphase_updates;
496 void combat_phase_announce() // TODO: clean up these fail nextthinks...
497 {
498         cphase_updates += 1;
499         
500         if(cphase_updates == 0)
501                 Announce("prepareforbattle");
502         else if(cphase_updates == 3)
503                 Announce("3");
504         else if(cphase_updates == 4)
505                 Announce("2");
506         else if(cphase_updates == 5)
507                 Announce("1");
508         else if(cphase_updates == 6)
509         {
510                 Announce("begin");
511                 combat_phase_begin();
512         }
513         
514         if(cphase_updates >= 6)
515                 return;
516
517         self.think = combat_phase_announce;
518         self.nextthink = time + 1;
519 }
520
521 void build_phase()
522 {
523         entity head;
524         float n_players = 0, gen_washealed = FALSE, player_washealed = FALSE;
525         
526         current_phase = PHASE_BUILD;
527         
528         for(head = world;(head = find(head, classname, "td_generator")); )
529         {
530                 if(head.health <= 5 && head.max_health > 10)
531                         Announce("lastsecond");
532                         
533                 if(head.health < head.max_health)
534                 {
535                         gen_washealed = TRUE;
536                         head.health = head.max_health;
537                         WaypointSprite_UpdateHealth(head.sprite, head.health);
538                 }
539                 head.takedamage = DAMAGE_NO;
540         }
541         
542         FOR_EACH_PLAYER(head)
543         {
544                 if(head.health < 100)
545                 {
546                         player_washealed = TRUE;
547                         break; // we found 1, so no need to check the others
548                 }
549         }
550                 
551         totalmonsters += autocvar_g_td_monster_count_increment * wave_count;
552         monster_skill += autocvar_g_td_monsters_skill_increment;
553         
554         if(wave_count < 1) wave_count = 1;
555         
556         Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_PHASE_BUILD, wave_count, totalmonsters, autocvar_g_td_buildphase_time);
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         dprint(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         queue_monsters(totalmonsters);
592         
593         cphase_updates = -1;
594         
595         if(autocvar_sv_eventlog)
596         GameLogEcho(strcat(":buildphase:", ftos(wave_count), ":", ftos(totalmonsters)));
597         
598         self.think = combat_phase_announce;
599         self.nextthink = time + build_time - 6;
600 }
601
602 void wave_end(float starting)
603 {
604         if not(starting)
605         {
606                 Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_VICTORY, ((wave_count >= max_waves) ? "Level" : "Wave"));
607                 
608                 if(autocvar_sv_eventlog)
609             GameLogEcho(strcat(":wave:", ftos(wave_count), ":victory"));
610         }
611         
612         if(wave_count >= max_waves)
613         {
614                 gensurvived = TRUE;
615                 return;
616         }
617         
618         if not(starting)
619                 wave_count += 1;
620                 
621         self.think = build_phase;
622         self.nextthink = time + 3;
623 }
624
625 void td_ScoreRules()
626 {
627         ScoreInfo_SetLabel_PlayerScore(SP_TD_SCORE,             "score",         SFL_SORT_PRIO_PRIMARY);
628         ScoreInfo_SetLabel_PlayerScore(SP_TD_KILLS,             "kills",         SFL_LOWER_IS_BETTER);
629         ScoreInfo_SetLabel_PlayerScore(SP_TD_TURKILLS,  "frags",         SFL_LOWER_IS_BETTER);
630         ScoreInfo_SetLabel_PlayerScore(SP_TD_DEATHS,    "deaths",    SFL_LOWER_IS_BETTER);
631         ScoreInfo_SetLabel_PlayerScore(SP_TD_SUICIDES,  "suicides",  SFL_LOWER_IS_BETTER | SFL_ALLOW_HIDE);
632         ScoreRules_basics_end();
633 }
634
635 void td_SpawnController()
636 {
637         entity oldself = self;
638         self = spawn();
639         self.classname = "td_controller";
640         spawnfunc_td_controller();
641         self = oldself;
642 }
643
644 void td_DelayedInit()
645 {
646         if(find(world, classname, "td_controller") == world)
647         {
648                 print("No ""td_controller"" entity found on this map, creating it anyway.\n");
649                 td_SpawnController();
650         }
651         
652         td_ScoreRules();
653 }
654
655 void td_Initialize()
656 {
657         InitializeEntity(world, td_DelayedInit, INITPRIO_GAMETYPE);
658 }
659
660 MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
661 {
662         if(turret_flags & TFL_TARGETSELECT_MISSILESONLY)
663     if(turret_target.flags & FL_PROJECTILE)
664         if(turret_target.owner.flags & FL_MONSTER)
665         return TRUE; // flac support
666                         
667         if(turret.turrcaps_flags & TFL_TURRCAPS_SUPPORT && turret_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
668                 return TRUE;
669         if not(turret_target.flags & FL_MONSTER)
670                 turret_target = world;
671                 
672         return FALSE;
673 }
674
675 MUTATOR_HOOKFUNCTION(td_PlayerThink)
676 {
677         self.stat_current_wave = wave_count;
678         self.stat_totalwaves = max_waves;
679         
680         return FALSE;
681 }
682
683 MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
684 {
685         self.bot_attack = FALSE;
686         
687         return FALSE;
688 }
689
690 MUTATOR_HOOKFUNCTION(td_PlayerDies)
691 {
692         if(frag_attacker.flags & FL_MONSTER)
693                 PlayerScore_Add(frag_target, SP_TD_DEATHS, 1);
694                 
695         if(frag_target == frag_attacker)
696                 PlayerScore_Add(frag_attacker, SP_TD_SUICIDES, 1);
697
698         return FALSE;
699 }
700
701 MUTATOR_HOOKFUNCTION(td_GiveFragsForKill)
702 {
703         frag_score = 0;
704                 
705         return TRUE; // no frags counted in td
706 }
707
708 MUTATOR_HOOKFUNCTION(td_PlayerDamage)
709 {
710         if(frag_attacker.realowner == frag_target)
711                 frag_damage = 0;
712                 
713         if(frag_target.flags & FL_MONSTER && time < frag_target.spawnshieldtime)
714                 frag_damage = 0;
715                 
716         if(frag_target.vehicle_flags & VHF_ISVEHICLE && !(frag_attacker.flags & FL_MONSTER))
717                 frag_damage = 0;
718                 
719         if(frag_attacker.vehicle_flags & VHF_ISVEHICLE && !(frag_target.flags & FL_MONSTER))
720                 frag_damage = 0;
721                 
722         if(!autocvar_g_td_pvp && frag_attacker != frag_target && IS_PLAYER(frag_target) && IS_PLAYER(frag_attacker))
723                 frag_damage = 0;
724                 
725         if(frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET && IS_PLAYER(frag_target))
726                 frag_damage = 0;
727                 
728         if((frag_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && !(frag_attacker.flags & FL_MONSTER || frag_attacker.turrcaps_flags & TFL_TURRCAPS_SUPPORT))
729                 frag_damage = 0;
730                 
731         return TRUE;
732 }
733
734 MUTATOR_HOOKFUNCTION(td_TurretDies)
735 {
736         if(self.realowner)
737                 self.realowner.turret_cnt -= 1;
738                         
739         return FALSE;
740 }
741
742 MUTATOR_HOOKFUNCTION(td_MonsterCheckBossFlag)
743 {
744         // No minibosses in tower defense
745         return TRUE;
746 }
747
748 MUTATOR_HOOKFUNCTION(td_MonsterMove)
749 {
750         entity player;
751         float n_players = 0;
752         FOR_EACH_PLAYER(player) { ++n_players; }
753         
754         if(n_players < 1) // no players online, so do nothing
755         {
756                 monster_target = world;
757                 monster_speed_run = monster_speed_walk = 0;
758                 return FALSE;
759         }
760         
761         if not(self.enemy) // don't change targets while attacking
762         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"))
763         {
764                 if(self.goalentity.target2)
765                 {
766                         if(random() > 0.5)
767                                 self.target = self.goalentity.target2;
768                         else
769                                 self.target = self.goalentity.target;
770                 }
771                 else
772                         self.target = self.goalentity.target;
773                                 
774                 self.goalentity = find(world, targetname, self.target);
775                 
776                 if(self.goalentity == world)
777                         self.goalentity = PickGenerator();
778         }
779         
780         monster_speed_run = m_speed_run * monster_skill;
781         monster_speed_walk = m_speed_walk * monster_skill;
782         
783         return FALSE;
784 }
785
786 MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
787 {
788         if(self.realowner == world) // nothing spawned it, so kill it
789         {
790                 WaypointSprite_Kill(self.sprite);
791                 remove(self);
792                 return TRUE;
793         }
794         
795         current_monsters += 1;
796         
797         self.spawnshieldtime = time + autocvar_g_td_monsters_spawnshield_time;
798         
799         self.drop_size = self.health * 0.05;
800         
801         if(self.drop_size < 1) self.drop_size = 1;
802         
803         self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_BODY;
804         
805         self.origin += '0 0 25'; // hopefully this fixes monsters falling through the floor
806         
807         switch(self.classname)
808         {
809                 case "monster_knight": n_knights -= 1; break;
810                 case "monster_dog": n_dogs -= 1; break;
811                 case "monster_ogre": n_ogres -= 1; break;
812                 case "monster_shambler": n_shamblers -= 1; AnnounceSpawn("Shambler"); break;
813                 case "monster_wizard": n_wizards -= 1; break;
814                 case "monster_shalrath": n_shalraths -= 1; break;
815                 case "monster_soldier": n_soldiers -= 1; break;
816                 case "monster_hellknight": n_hknights -= 1; break;
817                 case "monster_enforcer": n_enforcers -= 1; break;
818                 case "monster_demon": n_demons -= 1; break;
819                 case "monster_zombie": n_zombies -= 1; break;
820                 case "monster_spider": n_spiders -= 1; break;
821                 case "monster_tarbaby": n_tarbabies -= 1; break;
822         }
823         
824         return TRUE;
825 }
826
827 MUTATOR_HOOKFUNCTION(td_MonsterDies)
828 {
829         entity oldself;
830         vector backuporigin;
831
832         monster_count -= 1;
833         current_monsters -= 1;
834         monsters_killed += 1;
835         
836         if(IS_PLAYER(frag_attacker))
837         {
838                 PlayerScore_Add(frag_attacker, SP_TD_SCORE, autocvar_g_td_kill_points);
839                 PlayerScore_Add(frag_attacker, SP_TD_KILLS, 1);
840         }
841         else if(IS_PLAYER(frag_attacker.realowner))
842         {
843                 PlayerScore_Add(frag_attacker.realowner, SP_TD_SCORE, autocvar_g_td_turretkill_points);
844                 PlayerScore_Add(frag_attacker.realowner, SP_TD_TURKILLS, 1);
845         }
846
847         backuporigin = self.origin;
848         oldself = self;
849         self = spawn();
850         
851         self.gravity = 1;
852         setorigin(self, backuporigin + '0 0 5');
853         spawn_td_fuel(oldself.drop_size);
854         self.touch = M_Item_Touch;
855         if(self == world)
856         {
857                 self = oldself;
858                 return FALSE;
859         }
860         SUB_SetFade(self, time + 5, 1);
861         
862         self = oldself;
863
864         return FALSE;
865 }
866
867 MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
868 {
869         float n_players = 0;
870         entity player;
871         local entity e;
872         
873         FOR_EACH_PLAYER(player) { ++n_players; }
874         
875         if(n_players < 1) // no players online, so do nothing
876         {
877                 self.enemy = world;
878                 return TRUE;
879         }
880         
881         for(e = world;(e = findflags(e, monster_attack, TRUE)); ) 
882         {
883                 if(ignore_turrets)
884                 if(e.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
885                         continue;
886                 
887                 if(monster_isvalidtarget(e, self))
888                 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"))
889                 {
890                         self.enemy = e;
891                 }
892         }
893         
894         return TRUE;
895 }
896
897 MUTATOR_HOOKFUNCTION(td_SetStartItems)
898 {
899         start_ammo_fuel = 150; // to be nice...
900         
901         return FALSE;
902 }
903
904 MUTATOR_HOOKFUNCTION(td_TurretSpawn)
905 {
906         if(self.realowner == world)
907                 return TRUE; // wasn't spawned by a player
908                 
909         self.bot_attack = FALSE;
910         self.turret_buff = 1;
911         
912         return FALSE;
913 }
914
915 MUTATOR_HOOKFUNCTION(td_DisableVehicles)
916 {
917         // you shall not spawn!
918         return TRUE;
919 }
920
921 MUTATOR_HOOKFUNCTION(td_PlayerCommand)
922 {
923         if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
924         
925         makevectors(self.v_angle);
926         WarpZone_TraceLine(self.origin + self.view_ofs, self.origin + self.view_ofs + v_forward * 100, MOVE_HITMODEL, self);
927         
928         if(cmd_name == "turretspawn")
929         {
930                 if(argv(1) == "list")
931                 {
932                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_LIST, "mlrs walker plasma towerbuff flac barricade");
933                         return TRUE;
934                 }
935                 if(!IS_PLAYER(self) || self.health <= 0)
936                 { 
937                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_CANTSPAWN);
938                         return TRUE;
939                 }
940                 if(self.turret_cnt >= max_turrets)
941                 {
942                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_MAXTURRETS, max_turrets);
943                         return TRUE;
944                 }
945                 
946                 spawnturret(self, self, argv(1), trace_endpos);
947                 
948                 return TRUE;
949         }
950         if(cmd_name == "repairturret")
951         {
952                 if((trace_ent.playerid != self.playerid || trace_ent.realowner != self) || !(trace_ent.turrcaps_flags & TFL_TURRCAPS_ISTURRET))
953                 {
954                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_AIM_REPAIR);
955                         return TRUE;
956                 }
957                 if(self.ammo_fuel < autocvar_g_td_turret_repair_cost)   
958                 {
959                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_NOFUEL_REPAIR, autocvar_g_td_turret_repair_cost);
960                         return TRUE;
961                 }
962                 if(trace_ent.health >= trace_ent.max_health)
963                 {
964                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_MAXHEALTH);
965                         return TRUE;
966                 }
967                 
968                 self.ammo_fuel -= autocvar_g_td_turret_repair_cost;
969                 trace_ent.SendFlags |= TNSF_STATUS;
970                 trace_ent.health = bound(1, trace_ent.health + 100, trace_ent.max_health);
971                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_REPAIR);
972                 
973                 return TRUE;
974         }
975         if(cmd_name == "buffturret")
976         {
977                 if((trace_ent.playerid != self.playerid || trace_ent.realowner != self) || !(trace_ent.turrcaps_flags & TFL_TURRCAPS_ISTURRET))
978                 {
979                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_AIM_UPGRADE);
980                         return TRUE;
981                 }
982                 if(self.ammo_fuel < autocvar_g_td_turret_upgrade_cost)  
983                 {
984                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_NOFUEL_UPGRADE, autocvar_g_td_turret_upgrade_cost);
985                         return TRUE;
986                 }
987                 if(trace_ent.turret_buff >= 3)
988                 {
989                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_MAXPOWER);
990                         return TRUE;
991                 }
992                 
993                 self.ammo_fuel -= autocvar_g_td_turret_upgrade_cost;
994                 trace_ent.SendFlags |= TNSF_STATUS;
995                 buffturret(trace_ent, 1.2);
996                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_UPGRADE);
997                 
998                 return TRUE;
999         }
1000         if(cmd_name == "turretremove")
1001         {
1002                 if((trace_ent.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && (trace_ent.playerid == self.playerid || trace_ent.realowner == self))
1003                 {
1004                         self.turret_cnt -= 1;
1005                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_REMOVE);
1006                         WaypointSprite_Kill(trace_ent.sprite);
1007                         remove(trace_ent.tur_head);
1008                         remove(trace_ent);
1009                         return TRUE;
1010                 }
1011                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TD_AIM_REMOVE);
1012                 return TRUE;
1013         }
1014         
1015         return FALSE;
1016 }
1017
1018 MUTATOR_HOOKFUNCTION(td_ClientConnect)
1019 {
1020         entity t;
1021         
1022         self.turret_cnt = 0;
1023         
1024         for(t = world; (t = findflags(t, turrcaps_flags, TFL_TURRCAPS_ISTURRET)); )
1025         if(t.playerid == self.playerid)
1026         {
1027                 t.realowner = self;
1028                 self.turret_cnt += 1;
1029         }
1030
1031         return FALSE;
1032 }
1033
1034 MUTATOR_DEFINITION(gamemode_td)
1035 {
1036         MUTATOR_HOOK(MonsterSpawn, td_MonsterSpawn, CBC_ORDER_ANY);
1037         MUTATOR_HOOK(MonsterDies, td_MonsterDies, CBC_ORDER_ANY);
1038         MUTATOR_HOOK(MonsterMove, td_MonsterMove, CBC_ORDER_ANY);
1039         MUTATOR_HOOK(MonsterFindTarget, td_MonsterFindTarget, CBC_ORDER_ANY);
1040         MUTATOR_HOOK(MonsterCheckBossFlag, td_MonsterCheckBossFlag, CBC_ORDER_ANY);
1041         MUTATOR_HOOK(SetStartItems, td_SetStartItems, CBC_ORDER_ANY);
1042         MUTATOR_HOOK(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
1043         MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
1044         MUTATOR_HOOK(TurretDies, td_TurretDies, CBC_ORDER_ANY);
1045         MUTATOR_HOOK(GiveFragsForKill, td_GiveFragsForKill, CBC_ORDER_ANY);
1046         MUTATOR_HOOK(PlayerPreThink, td_PlayerThink, CBC_ORDER_ANY);
1047         MUTATOR_HOOK(PlayerDies, td_PlayerDies, CBC_ORDER_ANY);
1048         MUTATOR_HOOK(PlayerDamage_Calculate, td_PlayerDamage, CBC_ORDER_ANY);
1049         MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
1050         MUTATOR_HOOK(VehicleSpawn, td_DisableVehicles, CBC_ORDER_ANY);
1051         MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, CBC_ORDER_ANY);
1052         MUTATOR_HOOK(ClientConnect, td_ClientConnect, CBC_ORDER_ANY);
1053         
1054         MUTATOR_ONADD
1055         {
1056                 if(time > 1) // game loads at time 1
1057                         error("This is a game type and it cannot be added at runtime.");        
1058                 cvar_settemp("g_monsters", "1");
1059                 cvar_settemp("g_turrets", "1");
1060                 td_Initialize();
1061         }
1062         
1063         MUTATOR_ONROLLBACK_OR_REMOVE
1064         {
1065                 // we actually cannot roll back td_Initialize here
1066                 // BUT: we don't need to! If this gets called, adding always
1067                 // succeeds.
1068         }
1069
1070         MUTATOR_ONREMOVE
1071         {
1072                 error("This is a game type and it cannot be removed at runtime.");
1073                 return -1;
1074         }
1075
1076         return FALSE;
1077 }