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