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