]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_towerdefense.qc
Remove marine
[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.movetype       = MOVETYPE_NONE;
105         self.monster_attack = TRUE;
106         self.netname            = "Generator";
107         self.SendFlags          = GSF_SETUP;
108         
109         WaypointSprite_SpawnFixed(self.netname, self.origin + '0 0 90', self, sprite, RADARICON_OBJECTIVE, '1 0.5 0');  
110         WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
111         WaypointSprite_UpdateHealth(self.sprite, self.health);
112 }
113
114 void spawnfunc_td_generator() 
115 {
116         if not(g_td) { remove(self); return; }
117         
118         precache_sound("onslaught/generator_underattack.wav");
119         precache_sound("onslaught/ons_hit1.wav");
120         precache_sound("onslaught/ons_hit2.wav");
121         precache_sound("weapons/rocket_impact.wav");
122         
123         gendestroyed = FALSE;
124         
125         if not(self.health)
126                 self.health = autocvar_g_td_generator_health;
127                 
128         self.max_health = self.health;
129         
130         self.classname = "td_generator";
131         self.flags = FL_GENERATOR;
132         td_gencount += 1;
133         
134         setsize(self, GENERATOR_MIN, GENERATOR_MAX);
135         
136         setorigin(self, self.origin + '0 0 20');
137         droptofloor();
138         
139         generator_link(td_generator_setup);
140 }
141
142 entity PickGenerator()
143 {
144         entity generator, head;
145         if(td_gencount == 1)
146                 generator = findflags(world, flags, FL_GENERATOR);
147         else
148         {
149                 RandomSelection_Init();
150                 for(head = world;(head = findflags(head, flags, FL_GENERATOR)); )
151                 {
152                         RandomSelection_Add(head, 0, string_null, 1, 1);
153                 }
154                 generator = RandomSelection_chosen_ent; 
155         }
156         return generator;
157 }
158
159 void spawn_td_fuel(float fuel_size)
160 {
161         if not(g_td) {remove(self); return; }
162         
163         self.ammo_fuel = fuel_size * monster_skill;
164         StartItem("models/items/g_fuel.md3", "misc/itempickup.wav", g_pickup_respawntime_ammo, g_pickup_respawntimejitter_ammo, "Turret Fuel", IT_FUEL, 0, 0, commodity_pickupevalfunc, BOT_PICKUP_RATING_LOW);
165         
166         self.velocity = randomvec() * 175 + '0 0 325';
167 }
168
169 void spawnfunc_td_waypoint() 
170 {
171         if not(g_td) { remove(self); return; }
172         
173         self.classname = "td_waypoint";
174 }
175
176 void spawnfunc_monster_swarm()
177 {
178         if not(g_td) { remove(self); return; }
179         
180         self.flags = SWARM_NORMAL; // marked as a spawnpoint
181         self.classname = "monster_swarm";
182         
183         if(self.spawntype == SWARM_SWIM) waterspawns_count += 1;
184         if(self.spawntype == SWARM_FLY) flyspawns_count += 1;
185         
186         WaypointSprite_SpawnFixed("Monsters", self.origin + '0 0 60', self, sprite, RADARICON_HERE, '0 0 1');
187         
188         if(self.target == "")
189                 dprint("Warning: monster_swarm entity without a set target\n");
190 }
191
192 void barricade_touch()
193 {
194         if not(other.flags & FL_MONSTER)
195                 return;
196                 
197         if(time < self.dmg_time)
198                 return;
199                 
200         Damage(other, self, self, autocvar_g_td_barricade_damage, DEATH_HURTTRIGGER, self.origin, '0 0 0');
201         
202         self.dmg_time = time + 1;
203 }
204
205 void barricade_die()
206 {
207         self.takedamage = DAMAGE_NO;
208         self.event_damage = func_null;
209         
210         WaypointSprite_Kill(self.sprite);
211         
212         pointparticles(particleeffectnum("explosion_medium"), self.origin, '0 0 0', 1);
213         sound(self, CH_SHOTS, "weapons/rocket_impact.wav", VOL_BASE, ATTN_NORM);
214         
215         if(self.realowner)
216                 self.realowner.turret_cnt -= 1;
217                 
218         self.think = SUB_Remove;
219         self.nextthink = time;
220 }
221
222 void barricade_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
223 {
224         if not(attacker.flags & FL_MONSTER) return;
225         
226         self.health -= damage;
227         
228         WaypointSprite_UpdateHealth(self.sprite, self.health);
229         
230         if(self.health < 1)
231                 barricade_die();
232 }
233
234 void spawn_barricade()
235 {
236         self.health = 2000;
237         self.max_health = self.health;
238         self.dmg_time = time;
239         self.touch = barricade_touch;
240         self.think = func_null;
241         self.nextthink = -1;
242         self.takedamage = DAMAGE_AIM;
243         self.turrcaps_flags = TFL_TURRCAPS_ISTURRET; // for turretremove commands etc.
244         self.solid = SOLID_CORPSE; // hax
245         self.event_damage = barricade_damage;
246         self.netname = "Barricade";
247         
248         WaypointSprite_Spawn(self.netname, 0, 1200, self, '0 0 110', world, 0, self, sprite, FALSE, RADARICON_DOMPOINT, '1 1 0');       
249         WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
250         WaypointSprite_UpdateHealth(self.sprite, self.health);
251         
252         precache_model("models/td/barricade.md3");
253         setmodel(self, "models/td/barricade.md3");
254         
255         droptofloor();
256         
257         self.movetype = MOVETYPE_NONE;
258 }
259
260 float td_checkfuel(entity ent, string tur)
261 {
262         float turcost = cvar(strcat("g_td_turret_", tur, "_cost"));
263         
264         if(ent.ammo_fuel < turcost)
265         {
266                 Send_Notification(NOTIF_ONE, ent, MSG_MULTI, MULTI_TD_NOFUEL);
267                 return FALSE;
268         }
269         
270         ent.ammo_fuel -= turcost;
271         
272         return TRUE;
273 }       
274
275 void spawnturret(entity spawnedby, entity own, string turet, vector orig)
276 {
277         if not(IS_PLAYER(spawnedby)) { dprint("Warning: A non-player entity tried to spawn a turret\n"); return; }
278         if not(td_checkfuel(spawnedby, turet)) { return; }
279                 
280         entity oldself;
281         
282         oldself = self;
283         self = spawn();
284         
285         setorigin(self, orig);
286         self.spawnflags = TSL_NO_RESPAWN;
287         self.monster_attack = TRUE;
288         self.realowner = own;
289         self.playerid = own.playerid;
290         self.angles_y = spawnedby.v_angle_y;
291         spawnedby.turret_cnt += 1;
292         self.colormap = spawnedby.colormap;
293         self.colormod = '1 1 1';
294         
295         switch(turet)
296         {
297                 case "plasma": spawnfunc_turret_plasma(); break;
298                 case "mlrs": spawnfunc_turret_mlrs(); break;
299                 case "walker": spawnfunc_turret_walker(); break;
300                 case "flac": spawnfunc_turret_flac(); break;
301                 case "towerbuff": spawnfunc_turret_fusionreactor(); break;
302                 case "barricade": spawn_barricade(); break;
303                 default: Send_Notification(NOTIF_ONE, spawnedby, MSG_INFO, INFO_TD_INVALID); remove(self); self = oldself; return;
304         }
305         
306         Send_Notification(NOTIF_ONE, spawnedby, MSG_MULTI, MULTI_TD_SPAWN);
307                 
308         self = oldself;
309 }
310
311 void buffturret (entity tur, float buff)
312 {
313         tur.turret_buff           += 1;
314         tur.max_health            *= buff;
315         tur.tur_health             = tur.max_health;
316         tur.health                         = tur.max_health;
317         tur.ammo_max              *= buff;
318         tur.ammo_recharge     *= buff;
319     tur.shot_dmg          *= buff;
320     tur.shot_refire       -= buff * 0.2;
321     tur.shot_radius       *= buff;
322     tur.shot_speed        *= buff;
323     tur.shot_spread       *= buff;
324     tur.shot_force        *= buff;
325 }
326
327 void AnnounceSpawn(string anounce)
328 {
329         entity e;
330         Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_TD_ANNOUNCE_SPAWN, anounce);
331         
332         FOR_EACH_REALCLIENT(e) soundto(MSG_ONE, e, CHAN_AUTO, "kh/alarm.wav", VOL_BASE, ATTN_NONE);
333 }
334
335 entity PickSpawn (float strngth, float type)
336 {
337         entity e;
338         RandomSelection_Init();
339         for(e = world;(e = find(e, classname, "monster_swarm")); )
340         {
341                 if(flyspawns_count > 0 && type == SWARM_FLY && e.spawntype != SWARM_FLY) continue;
342                 if(waterspawns_count > 0 && type == SWARM_SWIM && e.spawntype != SWARM_SWIM) continue;
343                 
344                 RandomSelection_Add(e, 0, string_null, 1, 1);
345         }
346
347         return RandomSelection_chosen_ent;
348 }
349
350 void TD_SpawnMonster(string mnster, float strngth, float type)
351 {
352         entity e, mon;
353         
354         e = PickSpawn(strngth, type);
355         
356         if(e == world) // couldn't find anything for our class, so check for normal spawns
357                 e = PickSpawn(SWARM_NORMAL, SWARM_NORMAL);
358                 
359         if(e == world)
360         {
361                 dprint("Warning: couldn't find any monster_swarm spawnpoints, no monsters will spawn!\n");
362                 return;
363         }
364   
365         mon = spawnmonster(mnster, e, e, e.origin, FALSE, 2);
366         if(e.target2)
367         {
368                 if(random() <= 0.5 && e.target)
369                         mon.target2 = e.target;
370                 else
371                         mon.target2 = e.target2;
372         }
373         else
374                 mon.target2 = e.target;
375 }
376
377 float Monster_GetStrength(float mnster)
378 {
379         switch(mnster)
380         {
381                 default:
382                 case MONSTER_BRUISER:
383                 case MONSTER_ZOMBIE:
384                 case MONSTER_SPIDER:
385                 case MONSTER_SLIME:
386                 case MONSTER_CERBERUS:
387                 case MONSTER_WYVERN:
388                 case MONSTER_FISH:
389                         return SWARM_WEAK;
390                 case MONSTER_KNIGHT:
391                 case MONSTER_OGRE:
392                 case MONSTER_SHAMBLER:
393                 case MONSTER_MAGE:
394                 case MONSTER_ANIMUS:
395                         return SWARM_STRONG;
396                 default: return SWARM_NORMAL;
397         }
398 }
399
400 string monster_type2string(float mnster)
401 {
402         switch(mnster)
403         {
404                 case MONSTER_ZOMBIE: return "zombie";
405                 case MONSTER_OGRE: return "ogre";
406                 case MONSTER_ANIMUS: return "animus";
407                 case MONSTER_SHAMBLER: return "shambler";
408                 case MONSTER_BRUISER: return "bruiser";
409                 case MONSTER_WYVERN: return "wyvern";
410                 case MONSTER_CERBERUS: return "cerberus";
411                 case MONSTER_SLIME: return "slime";
412                 case MONSTER_KNIGHT: return "knight";
413                 case MONSTER_FISH: return "fish";
414                 case MONSTER_MAGE: return "mage";
415                 case MONSTER_SPIDER: return "spider";
416                 default: return "";
417         }
418 }
419
420 float Monster_GetType(float mnster)
421 {
422         switch(mnster)
423         {
424                 default:
425                 case MONSTER_BRUISER:
426                 case MONSTER_ZOMBIE:
427                 case MONSTER_SPIDER:
428                 case MONSTER_SLIME:
429                 case MONSTER_CERBERUS:
430                 case MONSTER_OGRE:
431                 case MONSTER_SHAMBLER:
432                 case MONSTER_MAGE:
433                 case MONSTER_KNIGHT:
434                 case MONSTER_ANIMUS:
435                         return SWARM_NORMAL;
436                 case MONSTER_WYVERN:
437                         return SWARM_FLY;
438                 case MONSTER_FISH:
439                         return SWARM_SWIM;
440         }
441 }
442
443 float RandomMonster()
444 {
445         RandomSelection_Init();
446         
447         if(n_animuses) RandomSelection_Add(world, MONSTER_ANIMUS, "", 1, 1);
448         if(n_mages) RandomSelection_Add(world, MONSTER_MAGE, "", 1, 1);
449         if(n_knights) RandomSelection_Add(world, MONSTER_KNIGHT, "", 1, 1);
450         if(n_zombies) RandomSelection_Add(world, MONSTER_ZOMBIE, "", 1, 1);
451         if(n_spiders) RandomSelection_Add(world, MONSTER_SPIDER, "", 1, 1);
452         if(n_ogres) RandomSelection_Add(world, MONSTER_OGRE, "", 1, 1);
453         if(n_cerberuses) RandomSelection_Add(world, MONSTER_CERBERUS, "", 1, 1);
454         if(n_bruisers) RandomSelection_Add(world, MONSTER_BRUISER, "", 1, 1);
455         if(n_shamblers) RandomSelection_Add(world, MONSTER_SHAMBLER, "", 0.2, 0.2);
456         if(n_slimes) RandomSelection_Add(world, MONSTER_SLIME, "", 0.2, 0.2);
457         if(n_wyverns && flyspawns_count) RandomSelection_Add(world, MONSTER_WYVERN, "", 1, 1);
458         if(n_fish && waterspawns_count) RandomSelection_Add(world, MONSTER_FISH, "", 0.2, 0.2);
459         
460         return RandomSelection_chosen_float;
461 }
462
463 void combat_phase()
464 {
465         float mstrength, montype, whichmon;
466         
467         current_phase = PHASE_COMBAT;
468         
469         if(monster_count <= 0)
470         {
471                 wave_end(FALSE);
472                 return;
473         }
474         
475         self.think = combat_phase;
476         
477         whichmon = RandomMonster();
478         
479         mstrength = Monster_GetStrength(whichmon);
480         montype = Monster_GetType(whichmon);
481         
482         if(current_monsters <= max_current && whichmon)
483         {
484                 TD_SpawnMonster(monster_type2string(whichmon), mstrength, montype);
485                 self.nextthink = time + spawn_delay;
486         }
487         else
488                 self.nextthink = time + 6;
489 }
490
491 void queue_monsters(float maxmonsters)
492 {
493         float mc = 9; // note: shambler + slime = 1
494         
495         if(waterspawns_count > 0)
496                 mc += 1;
497         if(flyspawns_count > 0)
498                 mc += 1;
499                 
500         DistributeEvenly_Init(maxmonsters, mc);
501         n_animuses              = DistributeEvenly_Get(1);
502         n_ogres                 = DistributeEvenly_Get(1);
503         n_cerberuses    = DistributeEvenly_Get(1);
504         n_bruisers      = DistributeEvenly_Get(1);
505         n_mages                 = DistributeEvenly_Get(1);
506         n_knights       = DistributeEvenly_Get(1);
507         n_zombies       = DistributeEvenly_Get(1);
508         n_spiders       = DistributeEvenly_Get(1);
509         n_slimes                = DistributeEvenly_Get(0.7);
510         n_shamblers     = DistributeEvenly_Get(0.3);
511         if(flyspawns_count > 0)
512                 n_wyverns = DistributeEvenly_Get(1);
513         if(waterspawns_count > 0)
514                 n_fish = DistributeEvenly_Get(1);
515 }
516
517 void combat_phase_begin()
518 {
519         monster_count = totalmonsters;
520         entity gen;
521         
522         Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_PHASE_COMBAT);
523         
524         if(autocvar_sv_eventlog)
525                 GameLogEcho(":combatphase");
526                 
527         self.think = combat_phase;
528         self.nextthink = time + 1;
529         
530         for(gen = world;(gen = findflags(gen, flags, FL_GENERATOR)); )
531                 gen.takedamage = DAMAGE_AIM;
532 }
533
534 float cphase_updates;
535 void combat_phase_announce() // TODO: clean up these fail nextthinks...
536 {
537         cphase_updates += 1;
538         
539         if(cphase_updates == 0)
540                 Send_Notification(NOTIF_ALL, world, MSG_ANNCE, ANNCE_PREPARE);
541         else if(cphase_updates == 3)
542                 Send_Notification(NOTIF_ALL, world, MSG_ANNCE, ANNCE_NUM_3);
543         else if(cphase_updates == 4)
544                 Send_Notification(NOTIF_ALL, world, MSG_ANNCE, ANNCE_NUM_2);
545         else if(cphase_updates == 5)
546                 Send_Notification(NOTIF_ALL, world, MSG_ANNCE, ANNCE_NUM_1);
547         else if(cphase_updates == 6)
548         {
549                 Send_Notification(NOTIF_ALL, world, MSG_ANNCE, ANNCE_BEGIN);
550                 combat_phase_begin();
551         }
552         
553         if(cphase_updates >= 6)
554                 return;
555
556         self.think = combat_phase_announce;
557         self.nextthink = time + 1;
558 }
559
560 void build_phase()
561 {
562         entity head;
563         float n_players = 0, gen_washealed = FALSE, mcount, mskill;
564         
565         current_phase = PHASE_BUILD;
566         
567         for(head = world;(head = findflags(head, flags, FL_GENERATOR)); )
568         {
569                 if(head.health < head.max_health)
570                 {
571                         gen_washealed = TRUE;
572                         pointparticles(particleeffectnum("healing_fx"), head.origin, '0 0 0', 1);
573                         head.health = head.max_health;
574                         WaypointSprite_UpdateHealth(head.sprite, head.health);
575                         head.SendFlags |= GSF_STATUS;
576                 }
577                 head.takedamage = DAMAGE_NO;
578         }
579         
580         FOR_EACH_PLAYER(head)
581         {
582                 if(head.health < 100) head.health = 100;
583                 if(gen_washealed) PlayerScore_Add(head, SP_TD_SCORE, -autocvar_g_td_generator_damaged_points);
584                         
585         n_players += 1;
586         }
587         
588         mcount = autocvar_g_td_monster_count_increment * wave_count;
589         mskill = n_players * 0.02;
590                 
591         totalmonsters += mcount;
592         monster_skill += autocvar_g_td_monsters_skill_increment;
593         monster_skill += mskill;
594         
595         if(monster_skill < 1) monster_skill = 1;
596         if(totalmonsters < 1) totalmonsters = ((autocvar_g_td_monster_count > 0) ? autocvar_g_td_monster_count : 10);
597         if(wave_count < 1) wave_count = 1;
598         
599         Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_PHASE_BUILD, wave_count, totalmonsters, build_time);
600     
601     FOR_EACH_MONSTER(head)
602     {
603                 if(head.health <= 0)
604                         continue;
605                         
606         dprint(strcat("Warning: Monster still alive during build phase! Monster name: ", head.netname, "\n"));
607                 
608                 WaypointSprite_Kill(head.sprite);
609         remove(head);
610     }
611         
612         monsters_total = totalmonsters;
613         monsters_killed = 0;
614                 
615         queue_monsters(totalmonsters);
616         
617         cphase_updates = -1;
618         
619         if(autocvar_sv_eventlog)
620         GameLogEcho(strcat(":buildphase:", ftos(wave_count), ":", ftos(totalmonsters)));
621         
622         self.think = combat_phase_announce;
623         self.nextthink = time + build_time - 6;
624 }
625
626 void wave_end(float starting)
627 {
628         if not(starting)
629         {
630                 Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_VICTORY, ((wave_count >= max_waves) ? "Level" : "Wave"));
631                 
632                 if(autocvar_sv_eventlog)
633             GameLogEcho(strcat(":wave:", ftos(wave_count), ":victory"));
634         }
635         
636         if(wave_count >= max_waves)
637         {
638                 gensurvived = TRUE;
639                 return;
640         }
641         
642         if not(starting)
643                 wave_count += 1;
644                 
645         self.think = build_phase;
646         self.nextthink = time + 3;
647 }
648
649 void td_ScoreRules()
650 {
651         ScoreInfo_SetLabel_PlayerScore(SP_TD_SCORE,             "score",         SFL_SORT_PRIO_PRIMARY);
652         ScoreInfo_SetLabel_PlayerScore(SP_TD_KILLS,             "kills",         SFL_LOWER_IS_BETTER);
653         ScoreInfo_SetLabel_PlayerScore(SP_TD_TURKILLS,  "frags",         SFL_LOWER_IS_BETTER);
654         ScoreInfo_SetLabel_PlayerScore(SP_TD_DEATHS,    "deaths",    SFL_LOWER_IS_BETTER);
655         ScoreInfo_SetLabel_PlayerScore(SP_TD_SUICIDES,  "suicides",  SFL_LOWER_IS_BETTER | SFL_ALLOW_HIDE);
656         ScoreRules_basics_end();
657 }
658
659 void td_SpawnController()
660 {
661         entity oldself = self;
662         self = spawn();
663         self.classname = "td_controller";
664         spawnfunc_td_controller();
665         self = oldself;
666 }
667
668 void td_DelayedInit()
669 {
670         if(find(world, classname, "td_controller") == world)
671         {
672                 print("No ""td_controller"" entity found on this map, creating it anyway.\n");
673                 td_SpawnController();
674         }
675         
676         td_ScoreRules();
677 }
678
679 void td_Initialize()
680 {
681         InitializeEntity(world, td_DelayedInit, INITPRIO_GAMETYPE);
682 }
683
684 MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
685 {
686         if(time < game_starttime || current_phase != PHASE_COMBAT || gameover)
687         {
688                 turret_target = world;
689                 return FALSE; // battle hasn't started
690         }
691
692         if(turret_flags & TFL_TARGETSELECT_MISSILESONLY)
693     if(turret_target.flags & FL_PROJECTILE)
694         if(turret_target.owner.flags & FL_MONSTER)
695         return TRUE; // flac support
696                         
697         if(turret.turrcaps_flags & TFL_TURRCAPS_SUPPORT && turret_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
698                 return TRUE;
699         if not(turret_target.flags & FL_MONSTER)
700                 turret_target = world;
701                 
702         return FALSE;
703 }
704
705 MUTATOR_HOOKFUNCTION(td_PlayerThink)
706 {
707         self.stat_current_wave = wave_count;
708         self.stat_totalwaves = max_waves;
709         
710         return FALSE;
711 }
712
713 MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
714 {
715         self.bot_attack = FALSE;
716         
717         return FALSE;
718 }
719
720 MUTATOR_HOOKFUNCTION(td_PlayerDies)
721 {
722         if(frag_attacker.flags & FL_MONSTER)
723                 PlayerScore_Add(frag_target, SP_TD_DEATHS, 1);
724                 
725         if(frag_target == frag_attacker)
726                 PlayerScore_Add(frag_attacker, SP_TD_SUICIDES, 1);
727
728         return FALSE;
729 }
730
731 MUTATOR_HOOKFUNCTION(td_GiveFragsForKill)
732 {
733         frag_score = 0;
734                 
735         return TRUE; // no frags counted in td
736 }
737
738 MUTATOR_HOOKFUNCTION(td_PlayerDamage)
739 {
740         if(frag_attacker.realowner == frag_target)
741                 frag_damage = 0;
742                 
743         if(frag_target.flags & FL_MONSTER && time < frag_target.spawnshieldtime)
744                 frag_damage = 0;
745                 
746         if(frag_target.vehicle_flags & VHF_ISVEHICLE && !(frag_attacker.flags & FL_MONSTER))
747                 frag_damage = 0;
748                 
749         if(frag_attacker.vehicle_flags & VHF_ISVEHICLE && !(frag_target.flags & FL_MONSTER))
750                 frag_damage = 0;
751                 
752         if(!autocvar_g_td_pvp && frag_attacker != frag_target && IS_PLAYER(frag_target) && IS_PLAYER(frag_attacker))
753         {
754                 frag_attacker.typehitsound += 1;
755                 frag_damage = 0;
756         }
757                 
758         if(frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET && IS_PLAYER(frag_target))
759                 frag_damage = 0;
760                 
761         if((frag_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && !(frag_attacker.flags & FL_MONSTER || frag_attacker.turrcaps_flags & TFL_TURRCAPS_SUPPORT))
762                 frag_damage = 0;
763                 
764         return TRUE;
765 }
766
767 MUTATOR_HOOKFUNCTION(td_TurretDies)
768 {
769         if(self.realowner)
770                 self.realowner.turret_cnt -= 1;
771                         
772         return FALSE;
773 }
774
775 MUTATOR_HOOKFUNCTION(td_MonsterCheckBossFlag)
776 {
777         // No minibosses in tower defense
778         return TRUE;
779 }
780
781 MUTATOR_HOOKFUNCTION(td_MonsterMove)
782 {
783         entity head;
784         float n_players = 0;
785         
786         FOR_EACH_PLAYER(head) { ++n_players; }
787         if(n_players < 1) return TRUE;
788
789         if not(self.enemy) // don't change targets while attacking
790         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"))
791         {
792                 if(monster_target.target2)
793                 {
794                         if(random() > 0.5)
795                                 self.target2 = monster_target.target2;
796                         else
797                                 self.target2 = monster_target.target;
798                 }
799                 else
800                         self.target2 = monster_target.target;
801                                 
802                 monster_target = find(world, targetname, self.target2);
803                 
804                 if(monster_target == world)
805                         monster_target = PickGenerator();
806         }
807         
808         monster_speed_run = (m_speed_run + random() * 4) * monster_skill;
809         monster_speed_walk = (m_speed_walk + random() * 4) * monster_skill;
810         
811         return FALSE;
812 }
813
814 MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
815 {
816         if(self.realowner == world) // nothing spawned it, so kill it
817         {
818                 WaypointSprite_Kill(self.sprite);
819                 remove(self.weaponentity);
820                 remove(self);
821                 return TRUE;
822         }
823         
824         current_monsters += 1;
825         
826         self.spawnshieldtime = time + autocvar_g_td_monsters_spawnshield_time;
827         
828         self.drop_size = bound(5, self.health * 0.05, autocvar_g_pickup_fuel_max);
829         
830         self.target_range = 600;
831         
832         self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_CORPSE | DPCONTENTS_MONSTERCLIP;
833         
834         switch(self.monsterid)
835         {
836                 case MONSTER_ZOMBIE: n_zombies -= 1; break;
837                 case MONSTER_OGRE: n_ogres -= 1; break;
838                 case MONSTER_ANIMUS: n_animuses -= 1; break;
839                 case MONSTER_SHAMBLER: n_shamblers -= 1; break;
840                 case MONSTER_BRUISER: n_bruisers -= 1; break;
841                 case MONSTER_WYVERN: n_wyverns -= 1; break;
842                 case MONSTER_CERBERUS: n_cerberuses -= 1; break;
843                 case MONSTER_SLIME: n_slimes -= 1; break;
844                 case MONSTER_KNIGHT: n_knights -= 1; break;
845                 case MONSTER_FISH: n_fish -= 1; break;
846                 case MONSTER_MAGE: n_mages -= 1; break;
847                 case MONSTER_SPIDER: n_spiders -= 1; break;
848         }
849         
850         return TRUE;
851 }
852
853 MUTATOR_HOOKFUNCTION(td_MonsterDies)
854 {
855         entity oldself;
856         vector backuporigin;
857
858         monster_count -= 1;
859         current_monsters -= 1;
860         monsters_killed += 1;
861         
862         if(IS_PLAYER(frag_attacker))
863         {
864                 PlayerScore_Add(frag_attacker, SP_TD_SCORE, autocvar_g_td_kill_points);
865                 PlayerScore_Add(frag_attacker, SP_TD_KILLS, 1);
866         }
867         else if(IS_PLAYER(frag_attacker.realowner))
868         if(frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
869         {
870                 PlayerScore_Add(frag_attacker.realowner, SP_TD_SCORE, autocvar_g_td_turretkill_points);
871                 PlayerScore_Add(frag_attacker.realowner, SP_TD_TURKILLS, 1);
872         }
873
874         backuporigin = self.origin;
875         oldself = self;
876         self = spawn();
877         
878         self.gravity = 1;
879         setorigin(self, backuporigin + '0 0 5');
880         spawn_td_fuel(oldself.drop_size);
881         self.touch = M_Item_Touch;
882         if(self == world)
883         {
884                 self = oldself;
885                 return FALSE;
886         }
887         SUB_SetFade(self, time + autocvar_g_monsters_drop_time, 1);
888         
889         self = oldself;
890
891         return FALSE;
892 }
893
894 MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
895 {
896         float n_players = 0;
897         entity player;
898         local entity e;
899         
900         FOR_EACH_PLAYER(player) { ++n_players; }
901         
902         if(n_players < 1) // no players online, so do nothing
903         {
904                 self.enemy = world;
905                 return TRUE;
906         }
907         
908         for(e = world;(e = findflags(e, monster_attack, TRUE)); ) 
909         {
910                 if(ignore_turrets)
911                 if(e.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
912                         continue;
913                 
914                 if(monster_isvalidtarget(e, self))
915                 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))
916                 {
917                         self.enemy = e;
918                 }
919         }
920         
921         return TRUE;
922 }
923
924 MUTATOR_HOOKFUNCTION(td_SetStartItems)
925 {
926         start_ammo_fuel = 150; // to be nice...
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(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
1080         MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
1081         MUTATOR_HOOK(TurretDies, td_TurretDies, CBC_ORDER_ANY);
1082         MUTATOR_HOOK(GiveFragsForKill, td_GiveFragsForKill, CBC_ORDER_ANY);
1083         MUTATOR_HOOK(PlayerPreThink, td_PlayerThink, CBC_ORDER_ANY);
1084         MUTATOR_HOOK(PlayerDies, td_PlayerDies, CBC_ORDER_ANY);
1085         MUTATOR_HOOK(PlayerDamage_Calculate, td_PlayerDamage, CBC_ORDER_ANY);
1086         MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
1087         MUTATOR_HOOK(VehicleSpawn, td_DisableVehicles, CBC_ORDER_ANY);
1088         MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, CBC_ORDER_ANY);
1089         MUTATOR_HOOK(ClientConnect, td_ClientConnect, CBC_ORDER_ANY);
1090         
1091         MUTATOR_ONADD
1092         {
1093                 if(time > 1) // game loads at time 1
1094                         error("This is a game type and it cannot be added at runtime.");        
1095                 cvar_settemp("g_monsters", "1");
1096                 cvar_settemp("g_turrets", "1");
1097                 td_Initialize();
1098         }
1099         
1100         MUTATOR_ONROLLBACK_OR_REMOVE
1101         {
1102                 // we actually cannot roll back td_Initialize here
1103                 // BUT: we don't need to! If this gets called, adding always
1104                 // succeeds.
1105         }
1106
1107         MUTATOR_ONREMOVE
1108         {
1109                 error("This is a game type and it cannot be removed at runtime.");
1110                 return -1;
1111         }
1112
1113         return FALSE;
1114 }