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