4 float td_moncount[MONSTER_LAST];
6 void spawnfunc_td_controller()
8 if not(g_td) { remove(self); return; }
10 if(autocvar_g_td_force_settings)
12 // TODO: find a better way to do this?
15 self.monstercount = 0;
23 self.ignoreturrets = 0;
26 self.netname = "Tower Defense controller entity";
27 self.classname = "td_controller";
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);
43 if(autocvar_g_td_monsters_skill_start)
44 monster_skill = autocvar_g_td_monsters_skill_start;
49 void td_generator_die()
51 if(autocvar_sv_eventlog)
52 GameLogEcho(":gendestroyed");
56 pointparticles(particleeffectnum("explosion_medium"), self.origin, '0 0 0', 1);
57 sound(self, CH_TRIGGER, "weapons/rocket_impact.wav", VOL_BASE, ATTN_NORM);
59 Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_GENDESTROYED);
61 self.solid = SOLID_NOT;
62 self.takedamage = DAMAGE_NO;
63 self.event_damage = func_null;
67 WaypointSprite_Kill(self.sprite);
70 void td_generator_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
72 if(IS_PLAYER(attacker) || attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET || attacker.vehicle_flags & VHF_ISVEHICLE || self.takedamage == DAMAGE_NO)
75 if (time > self.pain_finished)
77 self.pain_finished = time + 10;
78 play2all("onslaught/generator_underattack.wav");
82 spamsound(self, CH_TRIGGER, "onslaught/ons_hit1.wav", VOL_BASE, ATTN_NORM);
84 spamsound(self, CH_TRIGGER, "onslaught/ons_hit2.wav", VOL_BASE, ATTN_NORM);
86 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_TD_GENDAMAGED);
88 self.health -= damage;
90 WaypointSprite_UpdateHealth(self.sprite, self.health);
95 self.SendFlags |= GSF_STATUS;
98 void td_generator_setup()
100 self.think = func_null;
102 self.solid = SOLID_BBOX;
103 self.takedamage = DAMAGE_AIM;
104 self.event_damage = td_generator_damage;
106 self.movetype = MOVETYPE_NONE;
107 self.monster_attack = TRUE;
108 self.netname = "Generator";
109 self.SendFlags = GSF_SETUP;
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);
116 void spawnfunc_td_generator()
118 if not(g_td) { remove(self); return; }
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");
125 gendestroyed = FALSE;
128 self.health = autocvar_g_td_generator_health;
130 self.max_health = self.health;
132 self.classname = "td_generator";
133 self.flags = FL_GENERATOR;
136 setsize(self, GENERATOR_MIN, GENERATOR_MAX);
138 setorigin(self, self.origin + '0 0 20');
141 generator_link(td_generator_setup);
144 entity PickGenerator()
146 entity generator, head;
148 generator = findflags(world, flags, FL_GENERATOR);
151 RandomSelection_Init();
152 for(head = world;(head = findflags(head, flags, FL_GENERATOR)); )
154 RandomSelection_Add(head, 0, string_null, 1, 1);
156 generator = RandomSelection_chosen_ent;
161 void spawn_td_fuel(float fuel_size)
163 if not(g_td) {remove(self); return; }
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);
168 self.velocity = randomvec() * 175 + '0 0 325';
171 void spawnfunc_td_waypoint()
173 if not(g_td) { remove(self); return; }
175 self.classname = "td_waypoint";
178 void spawnfunc_monster_swarm()
180 if not(g_td) { remove(self); return; }
182 self.flags = SWARM_NORMAL; // marked as a spawnpoint
183 self.classname = "monster_swarm";
185 if(self.spawntype == SWARM_SWIM) waterspawns_count += 1;
186 if(self.spawntype == SWARM_FLY) flyspawns_count += 1;
188 WaypointSprite_SpawnFixed("Monsters", self.origin + '0 0 60', self, sprite, RADARICON_HERE, '0 0 1');
190 if(self.target == "")
191 dprint("Warning: monster_swarm entity without a set target\n");
194 void barricade_touch()
196 if not(other.flags & FL_MONSTER)
199 if(time < self.dmg_time)
202 Damage(other, self, self, autocvar_g_td_barricade_damage, DEATH_HURTTRIGGER, self.origin, '0 0 0');
204 self.dmg_time = time + 1;
209 self.takedamage = DAMAGE_NO;
210 self.event_damage = func_null;
212 WaypointSprite_Kill(self.sprite);
214 pointparticles(particleeffectnum("explosion_medium"), self.origin, '0 0 0', 1);
215 sound(self, CH_SHOTS, "weapons/rocket_impact.wav", VOL_BASE, ATTN_NORM);
218 self.realowner.turret_cnt -= 1;
220 self.think = SUB_Remove;
221 self.nextthink = time;
224 void barricade_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
226 if not(attacker.flags & FL_MONSTER) return;
228 self.health -= damage;
230 WaypointSprite_UpdateHealth(self.sprite, self.health);
236 void spawn_barricade()
239 self.max_health = self.health;
240 self.dmg_time = time;
241 self.touch = barricade_touch;
242 self.think = func_null;
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";
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);
254 precache_model("models/td/barricade.md3");
255 setmodel(self, "models/td/barricade.md3");
259 self.movetype = MOVETYPE_NONE;
262 float td_checkfuel(entity ent, string tur)
264 float turcost = cvar(strcat("g_td_turret_", tur, "_cost"));
266 if(ent.ammo_fuel < turcost)
268 Send_Notification(NOTIF_ONE, ent, MSG_MULTI, MULTI_TD_NOFUEL);
272 ent.ammo_fuel -= turcost;
277 void spawnturret(entity spawnedby, entity own, string turet, vector orig)
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; }
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';
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;
308 Send_Notification(NOTIF_ONE, spawnedby, MSG_MULTI, MULTI_TD_SPAWN);
313 void buffturret (entity tur, float buff)
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;
329 void AnnounceSpawn(string anounce)
332 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_TD_ANNOUNCE_SPAWN, anounce);
334 FOR_EACH_REALCLIENT(e) soundto(MSG_ONE, e, CHAN_AUTO, "kh/alarm.wav", VOL_BASE, ATTN_NONE);
337 entity PickSpawn (float strngth, float type)
340 RandomSelection_Init();
341 for(e = world;(e = find(e, classname, "monster_swarm")); )
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;
346 RandomSelection_Add(e, 0, string_null, 1, 1);
349 return RandomSelection_chosen_ent;
352 void TD_SpawnMonster(string mnster, float strngth, float type)
356 e = PickSpawn(strngth, type);
358 if(e == world) // couldn't find anything for our class, so check for normal spawns
359 e = PickSpawn(SWARM_NORMAL, SWARM_NORMAL);
363 dprint("Warning: couldn't find any monster_swarm spawnpoints, no monsters will spawn!\n");
367 mon = spawnmonster(mnster, e, e, e.origin, FALSE, 2);
370 if(random() <= 0.5 && e.target)
371 mon.target2 = e.target;
373 mon.target2 = e.target2;
376 mon.target2 = e.target;
379 float Monster_GetStrength(float mnster)
384 case MONSTER_BRUISER:
388 case MONSTER_CERBERUS:
390 case MONSTER_STINGRAY:
394 case MONSTER_SHAMBLER:
398 default: return SWARM_NORMAL;
402 string monster_type2string(float mnster)
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";
422 float Monster_GetType(float mnster)
427 case MONSTER_BRUISER:
431 case MONSTER_CERBERUS:
433 case MONSTER_SHAMBLER:
440 case MONSTER_STINGRAY:
445 float RandomMonster()
447 RandomSelection_Init();
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);
456 RandomSelection_Add(world, i, "", 1, 1);
458 return RandomSelection_chosen_float;
463 float mstrength, montype, whichmon;
465 current_phase = PHASE_COMBAT;
467 if(monster_count <= 0)
473 self.think = combat_phase;
475 whichmon = RandomMonster();
476 mstrength = Monster_GetStrength(whichmon);
477 montype = Monster_GetType(whichmon);
479 if(current_monsters <= max_current && whichmon)
481 TD_SpawnMonster(monster_type2string(whichmon), mstrength, montype);
482 self.nextthink = time + spawn_delay;
485 self.nextthink = time + 6;
488 void queue_monsters(float maxmonsters)
490 float mc = 9; // note: shambler + slime = 1
492 if(waterspawns_count > 0)
494 if(flyspawns_count > 0)
497 DistributeEvenly_Init(maxmonsters, mc);
501 for(i = MONSTER_FIRST + 1; i < MONSTER_LAST; ++i)
503 if(i == MONSTER_WYVERN)
504 if(flyspawns_count < 1)
507 if(i == MONSTER_STINGRAY)
508 if(waterspawns_count < 1)
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);
516 td_moncount[i] = DistributeEvenly_Get(1);
520 void combat_phase_begin()
522 monster_count = totalmonsters;
525 Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_PHASE_COMBAT);
527 if(autocvar_sv_eventlog)
528 GameLogEcho(":combatphase");
530 self.think = combat_phase;
531 self.nextthink = time + 1;
533 for(gen = world;(gen = findflags(gen, flags, FL_GENERATOR)); )
534 gen.takedamage = DAMAGE_AIM;
537 float cphase_updates;
538 void combat_phase_announce() // TODO: clean up these fail nextthinks...
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)
552 Send_Notification(NOTIF_ALL, world, MSG_ANNCE, ANNCE_BEGIN);
553 combat_phase_begin();
556 if(cphase_updates >= 6)
559 self.think = combat_phase_announce;
560 self.nextthink = time + 1;
566 float n_players = 0, gen_washealed = FALSE, mcount, mskill;
568 current_phase = PHASE_BUILD;
570 for(head = world;(head = findflags(head, flags, FL_GENERATOR)); )
572 if(head.health < head.max_health)
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;
580 head.takedamage = DAMAGE_NO;
583 FOR_EACH_PLAYER(head)
585 if(head.health < 100) head.health = 100;
586 if(gen_washealed) PlayerScore_Add(head, SP_TD_SCORE, -autocvar_g_td_generator_damaged_points);
591 mcount = autocvar_g_td_monster_count_increment * wave_count;
592 mskill = n_players * 0.02;
594 totalmonsters += mcount;
595 monster_skill += autocvar_g_td_monsters_skill_increment;
596 monster_skill += mskill;
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;
602 Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_PHASE_BUILD, wave_count, totalmonsters, build_time);
604 FOR_EACH_MONSTER(head)
609 dprint(strcat("Warning: Monster still alive during build phase! Monster name: ", head.netname, "\n"));
611 WaypointSprite_Kill(head.sprite);
615 monsters_total = totalmonsters;
618 queue_monsters(totalmonsters);
622 if(autocvar_sv_eventlog)
623 GameLogEcho(strcat(":buildphase:", ftos(wave_count), ":", ftos(totalmonsters)));
625 self.think = combat_phase_announce;
626 self.nextthink = time + build_time - 6;
629 void wave_end(float starting)
633 Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_VICTORY, ((wave_count >= max_waves) ? "Level" : "Wave"));
635 if(autocvar_sv_eventlog)
636 GameLogEcho(strcat(":wave:", ftos(wave_count), ":victory"));
639 if(wave_count >= max_waves)
648 self.think = build_phase;
649 self.nextthink = time + 3;
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();
662 void td_SpawnController()
664 entity oldself = self;
666 self.classname = "td_controller";
667 spawnfunc_td_controller();
671 void td_DelayedInit()
673 if(find(world, classname, "td_controller") == world)
675 print("No ""td_controller"" entity found on this map, creating it anyway.\n");
676 td_SpawnController();
684 InitializeEntity(world, td_DelayedInit, INITPRIO_GAMETYPE);
686 addstat(STAT_CURRENT_WAVE, AS_FLOAT, stat_current_wave);
687 addstat(STAT_TOTALWAVES, AS_FLOAT, stat_totalwaves);
690 MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
692 if(time < game_starttime || current_phase != PHASE_COMBAT || gameover)
694 turret_target = world;
695 return FALSE; // battle hasn't started
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
703 if(turret.turrcaps_flags & TFL_TURRCAPS_SUPPORT && turret_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
705 if not(turret_target.flags & FL_MONSTER)
706 turret_target = world;
711 MUTATOR_HOOKFUNCTION(td_PlayerThink)
713 self.stat_current_wave = wave_count;
714 self.stat_totalwaves = max_waves;
719 MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
721 self.bot_attack = FALSE;
723 Send_Notification(NOTIF_ONE, self, MSG_CENTER, CENTER_TD_PROTECT);
728 MUTATOR_HOOKFUNCTION(td_PlayerDies)
730 if(frag_attacker.flags & FL_MONSTER)
731 PlayerScore_Add(frag_target, SP_TD_DEATHS, 1);
733 if(frag_target == frag_attacker)
734 PlayerScore_Add(frag_attacker, SP_TD_SUICIDES, 1);
739 MUTATOR_HOOKFUNCTION(td_GiveFragsForKill)
743 return TRUE; // no frags counted in td
746 MUTATOR_HOOKFUNCTION(td_PlayerDamage)
748 if(frag_attacker.realowner == frag_target)
751 if(frag_target.flags & FL_MONSTER && time < frag_target.spawnshieldtime)
754 if(frag_target.vehicle_flags & VHF_ISVEHICLE && !(frag_attacker.flags & FL_MONSTER))
757 if(frag_attacker.vehicle_flags & VHF_ISVEHICLE && !(frag_target.flags & FL_MONSTER))
760 if(!autocvar_g_td_pvp && frag_attacker != frag_target && IS_PLAYER(frag_target) && IS_PLAYER(frag_attacker))
762 frag_attacker.typehitsound += 1;
766 if(frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET && IS_PLAYER(frag_target))
769 if((frag_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && !(frag_attacker.flags & FL_MONSTER || frag_attacker.turrcaps_flags & TFL_TURRCAPS_SUPPORT))
775 MUTATOR_HOOKFUNCTION(td_TurretDies)
778 self.realowner.turret_cnt -= 1;
783 MUTATOR_HOOKFUNCTION(td_MonsterCheckBossFlag)
785 // No minibosses in tower defense
789 MUTATOR_HOOKFUNCTION(td_MonsterMove)
794 FOR_EACH_PLAYER(head) { ++n_players; }
795 if(n_players < 1) return TRUE;
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"))
800 if(monster_target.target2)
803 self.target2 = monster_target.target2;
805 self.target2 = monster_target.target;
808 self.target2 = monster_target.target;
810 monster_target = find(world, targetname, self.target2);
812 if(monster_target == world)
813 monster_target = PickGenerator();
816 monster_speed_run = (m_speed_run + random() * 4) * monster_skill;
817 monster_speed_walk = (m_speed_walk + random() * 4) * monster_skill;
822 MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
824 if(self.realowner == world) // nothing spawned it, so kill it
826 WaypointSprite_Kill(self.sprite);
827 remove(self.weaponentity);
832 current_monsters += 1;
834 self.spawnshieldtime = time + autocvar_g_td_monsters_spawnshield_time;
836 self.drop_size = bound(5, self.health * 0.05, autocvar_g_pickup_fuel_max);
838 self.target_range = 600;
840 self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_CORPSE | DPCONTENTS_MONSTERCLIP;
842 td_moncount[self.monsterid] -= 1;
847 MUTATOR_HOOKFUNCTION(td_MonsterDies)
853 current_monsters -= 1;
854 monsters_killed += 1;
856 if(IS_PLAYER(frag_attacker))
858 PlayerScore_Add(frag_attacker, SP_TD_SCORE, autocvar_g_td_kill_points);
859 PlayerScore_Add(frag_attacker, SP_TD_KILLS, 1);
861 else if(IS_PLAYER(frag_attacker.realowner))
862 if(frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
864 PlayerScore_Add(frag_attacker.realowner, SP_TD_SCORE, autocvar_g_td_turretkill_points);
865 PlayerScore_Add(frag_attacker.realowner, SP_TD_TURKILLS, 1);
868 backuporigin = self.origin;
873 setorigin(self, backuporigin + '0 0 5');
874 spawn_td_fuel(oldself.drop_size);
875 self.touch = M_Item_Touch;
881 SUB_SetFade(self, time + autocvar_g_monsters_drop_time, 1);
888 MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
894 FOR_EACH_PLAYER(player) { ++n_players; }
896 if(n_players < 1) // no players online, so do nothing
902 for(e = world;(e = findflags(e, monster_attack, TRUE)); )
905 if(e.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
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))
918 MUTATOR_HOOKFUNCTION(td_SetStartItems)
920 start_ammo_fuel = 150; // to be nice...
925 MUTATOR_HOOKFUNCTION(td_TurretSpawn)
927 if(self.realowner == world)
928 return TRUE; // wasn't spawned by a player
930 self.bot_attack = FALSE;
931 buffturret(self, 0.5);
936 MUTATOR_HOOKFUNCTION(td_DisableVehicles)
938 // you shall not spawn!
942 MUTATOR_HOOKFUNCTION(td_PlayerCommand)
944 if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
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)
952 if(cmd_name == "turretspawn")
954 if(argv(1) == "list")
956 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_LIST, "mlrs walker plasma towerbuff flac barricade");
959 if(!IS_PLAYER(self) || self.health <= 0)
961 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_CANTSPAWN);
966 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_DISABLED);
969 if(self.turret_cnt >= max_turrets)
971 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXTURRETS, max_turrets);
975 spawnturret(self, self, argv(1), trace_endpos);
979 if(cmd_name == "repairturret")
981 if((targ.playerid != self.playerid || targ.realowner != self) || !(targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET))
983 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_REPAIR);
986 if(self.ammo_fuel < autocvar_g_td_turret_repair_cost)
988 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_NOFUEL_REPAIR, autocvar_g_td_turret_repair_cost);
991 if(targ.health >= targ.max_health)
993 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXHEALTH);
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);
1005 if(cmd_name == "buffturret")
1007 if((targ.playerid != self.playerid || targ.realowner != self) || !(targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET))
1009 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_UPGRADE);
1012 if(self.ammo_fuel < autocvar_g_td_turret_upgrade_cost)
1014 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_NOFUEL_UPGRADE, autocvar_g_td_turret_upgrade_cost);
1017 if(targ.turret_buff >= 5)
1019 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXPOWER);
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);
1031 if(cmd_name == "turretremove")
1033 if((targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && (targ.playerid == self.playerid || targ.realowner == self))
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);
1042 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_REMOVE);
1049 MUTATOR_HOOKFUNCTION(td_ClientConnect)
1053 self.turret_cnt = 0;
1055 for(t = world; (t = findflags(t, turrcaps_flags, TFL_TURRCAPS_ISTURRET)); )
1056 if(t.playerid == self.playerid)
1059 self.turret_cnt += 1;
1065 MUTATOR_DEFINITION(gamemode_td)
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);
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");
1094 MUTATOR_ONROLLBACK_OR_REMOVE
1096 // we actually cannot roll back td_Initialize here
1097 // BUT: we don't need to! If this gets called, adding always
1103 error("This is a game type and it cannot be removed at runtime.");