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