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