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