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