]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_towerdefense.qc
970373a84fbe4cad8d8fc0b9d6e0a2e30b71a03f
[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
687 MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
688 {
689         if(time < game_starttime || current_phase != PHASE_COMBAT || gameover)
690         {
691                 turret_target = world;
692                 return FALSE; // battle hasn't started
693         }
694
695         if(turret_flags & TFL_TARGETSELECT_MISSILESONLY)
696     if(turret_target.flags & FL_PROJECTILE)
697         if(turret_target.owner.flags & FL_MONSTER)
698         return TRUE; // flac support
699                         
700         if(turret.turrcaps_flags & TFL_TURRCAPS_SUPPORT && turret_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
701                 return TRUE;
702         if not(turret_target.flags & FL_MONSTER)
703                 turret_target = world;
704                 
705         return FALSE;
706 }
707
708 MUTATOR_HOOKFUNCTION(td_PlayerThink)
709 {
710         self.stat_current_wave = wave_count;
711         self.stat_totalwaves = max_waves;
712         
713         return FALSE;
714 }
715
716 MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
717 {
718         self.bot_attack = FALSE;
719         
720         Send_Notification(NOTIF_ONE, self, MSG_CENTER, CENTER_TD_PROTECT);
721         
722         return FALSE;
723 }
724
725 MUTATOR_HOOKFUNCTION(td_PlayerDies)
726 {
727         if(frag_attacker.flags & FL_MONSTER)
728                 PlayerScore_Add(frag_target, SP_TD_DEATHS, 1);
729                 
730         if(frag_target == frag_attacker)
731                 PlayerScore_Add(frag_attacker, SP_TD_SUICIDES, 1);
732
733         return FALSE;
734 }
735
736 MUTATOR_HOOKFUNCTION(td_GiveFragsForKill)
737 {
738         frag_score = 0;
739                 
740         return TRUE; // no frags counted in td
741 }
742
743 MUTATOR_HOOKFUNCTION(td_PlayerDamage)
744 {
745         if(frag_attacker.realowner == frag_target)
746                 frag_damage = 0;
747                 
748         if(frag_target.flags & FL_MONSTER && time < frag_target.spawnshieldtime)
749                 frag_damage = 0;
750                 
751         if(frag_target.vehicle_flags & VHF_ISVEHICLE && !(frag_attacker.flags & FL_MONSTER))
752                 frag_damage = 0;
753                 
754         if(frag_attacker.vehicle_flags & VHF_ISVEHICLE && !(frag_target.flags & FL_MONSTER))
755                 frag_damage = 0;
756                 
757         if(!autocvar_g_td_pvp && frag_attacker != frag_target && IS_PLAYER(frag_target) && IS_PLAYER(frag_attacker))
758         {
759                 frag_attacker.typehitsound += 1;
760                 frag_damage = 0;
761         }
762                 
763         if(frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET && IS_PLAYER(frag_target))
764                 frag_damage = 0;
765                 
766         if((frag_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && !(frag_attacker.flags & FL_MONSTER || frag_attacker.turrcaps_flags & TFL_TURRCAPS_SUPPORT))
767                 frag_damage = 0;
768                 
769         return TRUE;
770 }
771
772 MUTATOR_HOOKFUNCTION(td_TurretDies)
773 {
774         if(self.realowner)
775                 self.realowner.turret_cnt -= 1;
776                         
777         return FALSE;
778 }
779
780 MUTATOR_HOOKFUNCTION(td_MonsterCheckBossFlag)
781 {
782         // No minibosses in tower defense
783         return TRUE;
784 }
785
786 MUTATOR_HOOKFUNCTION(td_MonsterMove)
787 {
788         entity head;
789         float n_players = 0;
790         
791         FOR_EACH_PLAYER(head) { ++n_players; }
792         if(n_players < 1) return TRUE;
793
794         if not(self.enemy) // don't change targets while attacking
795         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"))
796         {
797                 if(monster_target.target2)
798                 {
799                         if(random() > 0.5)
800                                 self.target2 = monster_target.target2;
801                         else
802                                 self.target2 = monster_target.target;
803                 }
804                 else
805                         self.target2 = monster_target.target;
806                                 
807                 monster_target = find(world, targetname, self.target2);
808                 
809                 if(monster_target == world)
810                         monster_target = PickGenerator();
811         }
812         
813         monster_speed_run = (m_speed_run + random() * 4) * monster_skill;
814         monster_speed_walk = (m_speed_walk + random() * 4) * monster_skill;
815         
816         return FALSE;
817 }
818
819 MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
820 {
821         if(self.realowner == world) // nothing spawned it, so kill it
822         {
823                 WaypointSprite_Kill(self.sprite);
824                 remove(self.weaponentity);
825                 remove(self);
826                 return TRUE;
827         }
828         
829         current_monsters += 1;
830         
831         self.spawnshieldtime = time + autocvar_g_td_monsters_spawnshield_time;
832         
833         self.drop_size = bound(5, self.health * 0.05, autocvar_g_pickup_fuel_max);
834         
835         self.target_range = 600;
836         
837         self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_CORPSE | DPCONTENTS_MONSTERCLIP;
838         
839         td_moncount[self.monsterid] -= 1;
840         
841         return TRUE;
842 }
843
844 MUTATOR_HOOKFUNCTION(td_MonsterDies)
845 {
846         entity oldself;
847         vector backuporigin;
848
849         monster_count -= 1;
850         current_monsters -= 1;
851         monsters_killed += 1;
852         
853         if(IS_PLAYER(frag_attacker))
854         {
855                 PlayerScore_Add(frag_attacker, SP_TD_SCORE, autocvar_g_td_kill_points);
856                 PlayerScore_Add(frag_attacker, SP_TD_KILLS, 1);
857         }
858         else if(IS_PLAYER(frag_attacker.realowner))
859         if(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 + autocvar_g_monsters_drop_time, 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.flags & FL_GENERATOR)) || (vlen(trace_endpos - self.origin) < 500 && e.flags & FL_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_SetModname)
923 {
924         // TODO: find out why td_Initialize doesn't work for TD stats...
925         addstat(STAT_CURRENT_WAVE, AS_FLOAT, stat_current_wave);
926         addstat(STAT_TOTALWAVES, AS_FLOAT, stat_totalwaves);
927                 
928         return FALSE;
929 }
930
931 MUTATOR_HOOKFUNCTION(td_TurretSpawn)
932 {
933         if(self.realowner == world)
934                 return TRUE; // wasn't spawned by a player
935                 
936         self.bot_attack = FALSE;
937         buffturret(self, 0.5);
938         
939         return FALSE;
940 }
941
942 MUTATOR_HOOKFUNCTION(td_DisableVehicles)
943 {
944         // you shall not spawn!
945         return TRUE;
946 }
947
948 MUTATOR_HOOKFUNCTION(td_PlayerCommand)
949 {
950         if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
951         
952         makevectors(self.v_angle);
953         WarpZone_TraceLine(self.origin, self.origin + v_forward * 100, MOVE_HITMODEL, self);
954         entity targ = trace_ent;
955         if(targ.owner.realowner == self)
956                 targ = targ.owner;
957         
958         if(cmd_name == "turretspawn")
959         {
960                 if(argv(1) == "list")
961                 {
962                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_LIST, "mlrs walker plasma towerbuff flac barricade");
963                         return TRUE;
964                 }
965                 if(!IS_PLAYER(self) || self.health <= 0)
966                 { 
967                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_CANTSPAWN);
968                         return TRUE;
969                 }
970                 if(max_turrets <= 0)
971                 {
972                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_DISABLED);
973                         return TRUE;
974                 }
975                 if(self.turret_cnt >= max_turrets)
976                 {
977                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXTURRETS, max_turrets);
978                         return TRUE;
979                 }
980                 
981                 spawnturret(self, self, argv(1), trace_endpos);
982                 
983                 return TRUE;
984         }
985         if(cmd_name == "repairturret")
986         {
987                 if((targ.playerid != self.playerid || targ.realowner != self) || !(targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET))
988                 {
989                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_REPAIR);
990                         return TRUE;
991                 }
992                 if(self.ammo_fuel < autocvar_g_td_turret_repair_cost)   
993                 {
994                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_NOFUEL_REPAIR, autocvar_g_td_turret_repair_cost);
995                         return TRUE;
996                 }
997                 if(targ.health >= targ.max_health)
998                 {
999                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXHEALTH);
1000                         return TRUE;
1001                 }
1002                 
1003                 self.ammo_fuel -= autocvar_g_td_turret_repair_cost;
1004                 targ.SendFlags |= TNSF_STATUS;
1005                 targ.health = bound(1, targ.health + 100, targ.max_health);
1006                 WaypointSprite_UpdateHealth(targ.sprite, targ.health);
1007                 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_REPAIR);
1008                 
1009                 return TRUE;
1010         }
1011         if(cmd_name == "buffturret")
1012         {
1013                 if((targ.playerid != self.playerid || targ.realowner != self) || !(targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET))
1014                 {
1015                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_UPGRADE);
1016                         return TRUE;
1017                 }
1018                 if(self.ammo_fuel < autocvar_g_td_turret_upgrade_cost)  
1019                 {
1020                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_NOFUEL_UPGRADE, autocvar_g_td_turret_upgrade_cost);
1021                         return TRUE;
1022                 }
1023                 if(targ.turret_buff >= 5)
1024                 {
1025                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXPOWER);
1026                         return TRUE;
1027                 }
1028                 
1029                 self.ammo_fuel -= autocvar_g_td_turret_upgrade_cost;
1030                 targ.SendFlags |= TNSF_STATUS;
1031                 buffturret(targ, 1.2);
1032                 WaypointSprite_UpdateHealth(targ.sprite, targ.health);
1033                 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_UPGRADE);
1034                 
1035                 return TRUE;
1036         }
1037         if(cmd_name == "turretremove")
1038         {
1039                 if((targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && (targ.playerid == self.playerid || targ.realowner == self))
1040                 {
1041                         self.turret_cnt -= 1;
1042                         Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_REMOVE);
1043                         WaypointSprite_Kill(targ.sprite);
1044                         remove(targ.tur_head);
1045                         remove(targ);
1046                         return TRUE;
1047                 }
1048                 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_REMOVE);
1049                 return TRUE;
1050         }
1051         
1052         return FALSE;
1053 }
1054
1055 MUTATOR_HOOKFUNCTION(td_ClientConnect)
1056 {
1057         entity t;
1058         
1059         self.turret_cnt = 0;
1060         
1061         for(t = world; (t = findflags(t, turrcaps_flags, TFL_TURRCAPS_ISTURRET)); )
1062         if(t.playerid == self.playerid)
1063         {
1064                 t.realowner = self;
1065                 self.turret_cnt += 1;
1066         }
1067
1068         return FALSE;
1069 }
1070
1071 MUTATOR_DEFINITION(gamemode_td)
1072 {
1073         MUTATOR_HOOK(MonsterSpawn, td_MonsterSpawn, CBC_ORDER_ANY);
1074         MUTATOR_HOOK(MonsterDies, td_MonsterDies, CBC_ORDER_ANY);
1075         MUTATOR_HOOK(MonsterMove, td_MonsterMove, CBC_ORDER_ANY);
1076         MUTATOR_HOOK(MonsterFindTarget, td_MonsterFindTarget, CBC_ORDER_ANY);
1077         MUTATOR_HOOK(MonsterCheckBossFlag, td_MonsterCheckBossFlag, CBC_ORDER_ANY);
1078         MUTATOR_HOOK(SetStartItems, td_SetStartItems, CBC_ORDER_ANY);
1079         MUTATOR_HOOK(SetModname, td_SetModname, CBC_ORDER_ANY);
1080         MUTATOR_HOOK(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
1081         MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
1082         MUTATOR_HOOK(TurretDies, td_TurretDies, CBC_ORDER_ANY);
1083         MUTATOR_HOOK(GiveFragsForKill, td_GiveFragsForKill, CBC_ORDER_ANY);
1084         MUTATOR_HOOK(PlayerPreThink, td_PlayerThink, CBC_ORDER_ANY);
1085         MUTATOR_HOOK(PlayerDies, td_PlayerDies, CBC_ORDER_ANY);
1086         MUTATOR_HOOK(PlayerDamage_Calculate, td_PlayerDamage, CBC_ORDER_ANY);
1087         MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
1088         MUTATOR_HOOK(VehicleSpawn, td_DisableVehicles, CBC_ORDER_ANY);
1089         MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, CBC_ORDER_ANY);
1090         MUTATOR_HOOK(ClientConnect, td_ClientConnect, CBC_ORDER_ANY);
1091         
1092         MUTATOR_ONADD
1093         {
1094                 if(time > 1) // game loads at time 1
1095                         error("This is a game type and it cannot be added at runtime.");        
1096                 cvar_settemp("g_monsters", "1");
1097                 cvar_settemp("g_turrets", "1");
1098                 td_Initialize();
1099         }
1100         
1101         MUTATOR_ONROLLBACK_OR_REMOVE
1102         {
1103                 // we actually cannot roll back td_Initialize here
1104                 // BUT: we don't need to! If this gets called, adding always
1105                 // succeeds.
1106         }
1107
1108         MUTATOR_ONREMOVE
1109         {
1110                 error("This is a game type and it cannot be removed at runtime.");
1111                 return -1;
1112         }
1113
1114         return FALSE;
1115 }