1 void td_debug(string input)
3 switch(autocvar_g_td_debug)
5 case 1: dprint(input); break;
6 case 2: print(input); break;
10 void td_waypoint_link(float tm, vector from, vector to)
15 WarpZone_TrailParticles(world, particleeffectnum("waypoint_link_red"), from, to);
18 WarpZone_TrailParticles(world, particleeffectnum("waypoint_link_blue"), from, to);
21 WarpZone_TrailParticles(world, particleeffectnum("waypoint_link_yellow"), from, to);
24 WarpZone_TrailParticles(world, particleeffectnum("waypoint_link_pink"), from, to);
29 void td_waypoint_think()
40 e = find(world, target, self.targetname);
46 e = find(world, target2, self.targetname);
52 e = find(world, target3, self.targetname);
58 e = find(world, target4, self.targetname);
65 td_debug("Tower Defense waypoint without a team, removing it.\n");
70 if(time >= self.last_trace)
74 e = find(world, targetname, self.target);
75 if(e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
76 td_waypoint_link(self.team, self.origin, e.origin);
77 e = find(world, targetname, self.target2);
78 if(e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
79 td_waypoint_link(self.team, self.origin, e.origin);
80 e = find(world, targetname, self.target3);
81 if(e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
82 td_waypoint_link(self.team, self.origin, e.origin);
83 e = find(world, targetname, self.target4);
84 if(e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
85 td_waypoint_link(self.team, self.origin, e.origin);
87 e = find(world, target, self.targetname);
88 if(e.classname == "td_spawnpoint" || e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
89 td_waypoint_link(self.team, self.origin, e.origin);
90 e = find(world, target2, self.targetname);
91 if(e.classname == "td_spawnpoint" || e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
92 td_waypoint_link(self.team, self.origin, e.origin);
93 e = find(world, target3, self.targetname);
94 if(e.classname == "td_spawnpoint" || e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
95 td_waypoint_link(self.team, self.origin, e.origin);
96 e = find(world, target4, self.targetname);
97 if(e.classname == "td_spawnpoint" || e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
98 td_waypoint_link(self.team, self.origin, e.origin);
100 self.last_trace = time + 0.5;
103 self.nextthink = time + 0.1;
106 void td_generator_die()
108 if(autocvar_sv_eventlog)
109 GameLogEcho(":gendestroyed");
111 Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_GENDESTROYED);
113 self.solid = SOLID_NOT;
114 self.takedamage = DAMAGE_NO;
115 self.event_damage = func_null;
117 self.reset = func_null; // don't reset this generator
119 WaypointSprite_Kill(self.sprite);
122 void td_generator_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
124 if(IS_PLAYER(attacker) || attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET || attacker.vehicle_flags & VHF_ISVEHICLE || self.takedamage == DAMAGE_NO)
129 if (time > self.pain_finished)
131 self.pain_finished = time + 10;
132 play2team(self.team, "onslaught/generator_underattack.wav");
136 spamsound(self, CH_TRIGGER, "onslaught/ons_hit1.wav", VOL_BASE, ATTN_NORM);
138 spamsound(self, CH_TRIGGER, "onslaught/ons_hit2.wav", VOL_BASE, ATTN_NORM);
141 FOR_EACH_REALPLAYER(head)
142 if(!IsDifferentTeam(head, self))
143 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_TD_GENDAMAGED);
145 self.health -= damage;
147 WaypointSprite_UpdateHealth(self.sprite, self.health);
151 FOR_EACH_PLAYER(head)
152 if(!IsDifferentTeam(head, attacker))
153 PlayerScore_Add(head, SP_TD_DESTROYS, 1);
155 TeamScore_AddToTeam(attacker.team, ST_TD_DESTROYS, 1);
159 self.SendFlags |= GSF_STATUS;
162 void td_generator_setup()
164 self.think = func_null;
166 self.solid = SOLID_BBOX;
167 self.takedamage = DAMAGE_AIM;
168 self.event_damage = td_generator_damage;
169 self.movetype = MOVETYPE_NONE;
170 self.monster_attack = TRUE;
171 self.netname = "Generator";
172 self.reset = func_null;
174 WaypointSprite_SpawnFixed(self.netname, self.origin + '0 0 90', self, sprite, RADARICON_OBJECTIVE, Team_ColorRGB(self.team));
175 WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
176 WaypointSprite_UpdateHealth(self.sprite, self.health);
179 entity PickSpawn (float tm)
182 RandomSelection_Init();
183 for(e = world;(e = find(e, classname, "td_spawnpoint")); )
185 RandomSelection_Add(e, 0, string_null, 1, 1);
187 return RandomSelection_chosen_ent;
190 void TD_SpawnMonster(float tm, string mnster)
198 td_debug("Warning: couldn't find any td_spawnpoint spawnpoints, no monsters will spawn!\n");
202 mon = spawnmonster(mnster, e, e, e.origin, FALSE, 2);
205 if(random() <= 0.5 && e.target)
206 mon.target2 = e.target;
208 mon.target2 = e.target2;
211 mon.target2 = e.target;
214 string monster_type2string(float mnster)
218 case MONSTER_ZOMBIE: return "zombie";
219 case MONSTER_BRUTE: return "brute";
220 case MONSTER_ANIMUS: return "animus";
221 case MONSTER_SHAMBLER: return "shambler";
222 case MONSTER_BRUISER: return "bruiser";
223 case MONSTER_WYVERN: return "wyvern";
224 case MONSTER_CERBERUS: return "cerberus";
225 case MONSTER_SLIME: return "slime";
226 case MONSTER_KNIGHT: return "knight";
227 case MONSTER_STINGRAY: return "stingray";
228 case MONSTER_MAGE: return "mage";
229 case MONSTER_SPIDER: return "spider";
234 float RandomMonster()
236 RandomSelection_Init();
240 for(i = MONSTER_FIRST + 1; i < MONSTER_LAST; ++i)
242 if(i == MONSTER_STINGRAY || i == MONSTER_WYVERN)
243 continue; // flying/swimming monsters not yet supported
245 RandomSelection_Add(world, i, "", 1, 1);
248 return RandomSelection_chosen_float;
251 void SpawnMonsters(float tm)
255 whichmon = RandomMonster();
257 TD_SpawnMonster(tm, monster_type2string(whichmon));
260 entity PickGenerator(float tm)
264 RandomSelection_Init();
265 for(head = world;(head = findflags(head, flags, FL_GENERATOR)); )
267 RandomSelection_Add(head, 0, string_null, 1, 1);
269 return RandomSelection_chosen_ent;
272 float td_checkfuel(entity ent, string tur)
274 float turcost = cvar(strcat("g_td_turret_", tur, "_cost"));
276 if(ent.ammo_fuel < turcost)
278 Send_Notification(NOTIF_ONE, ent, MSG_MULTI, MULTI_TD_NOFUEL);
282 ent.ammo_fuel -= turcost;
287 void spawnturret(entity spawnedby, entity own, string turet, vector orig)
289 if not(IS_PLAYER(spawnedby)) { td_debug("Warning: A non-player entity tried to spawn a turret\n"); return; }
290 if not(td_checkfuel(spawnedby, turet)) { return; }
297 setorigin(self, orig);
298 self.spawnflags = TSL_NO_RESPAWN;
299 self.monster_attack = TRUE;
300 self.realowner = own;
301 self.playerid = own.playerid;
302 self.angles_y = spawnedby.v_angle_y;
303 spawnedby.turret_cnt += 1;
304 self.team = own.team;
308 case "plasma": spawnfunc_turret_plasma(); break;
309 case "mlrs": spawnfunc_turret_mlrs(); break;
310 case "walker": spawnfunc_turret_walker(); break;
311 case "flac": spawnfunc_turret_flac(); break;
312 case "towerbuff": spawnfunc_turret_fusionreactor(); break;
313 default: Send_Notification(NOTIF_ONE, spawnedby, MSG_INFO, INFO_TD_INVALID); remove(self); self = oldself; return;
316 Send_Notification(NOTIF_ONE, spawnedby, MSG_MULTI, MULTI_TD_SPAWN);
321 void buffturret(entity tur, float buff)
323 float refbuff = bound(0.01, buff * 0.05, 0.1);
325 tur.turret_buff += 1;
326 tur.max_health *= buff;
327 tur.tur_health = tur.max_health;
328 tur.health = tur.max_health;
329 tur.ammo_max *= buff;
330 tur.ammo_recharge *= buff;
331 tur.shot_dmg *= buff;
332 tur.shot_radius *= buff;
333 tur.shot_speed *= buff;
334 tur.shot_spread *= buff;
335 tur.shot_force *= buff;
338 tur.shot_refire += refbuff;
340 tur.shot_refire -= refbuff;
343 void spawn_td_fuel(float fuel_size)
345 if not(g_td) {remove(self); return; }
347 self.ammo_fuel = fuel_size * monster_skill;
348 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);
350 self.velocity = randomvec() * 175 + '0 0 325';
353 void td_generator_delayed()
355 generator_link(td_generator_setup);
357 self.SendFlags = GSF_SETUP;
361 #define TD_ALIVE_TEAMS() ((redalive > 0) + (bluealive > 0))
362 #define TD_ALIVE_TEAMS_OK() (TD_ALIVE_TEAMS() == 2)
367 allowed_to_spawn = TRUE;
369 ignore_turrets = TRUE;
371 FOR_EACH_PLAYER(head)
377 void TD_count_alive_monsters()
385 FOR_EACH_MONSTER(head)
387 if(head.health <= 0) continue;
393 case NUM_TEAM_1: ++redalive; break;
394 case NUM_TEAM_2: ++bluealive; break;
399 float TD_GetWinnerTeam()
401 float winner_team = 0;
403 winner_team = NUM_TEAM_1;
406 if(winner_team) return 0;
407 winner_team = NUM_TEAM_2;
411 return -1; // no monster left
414 float TD_CheckWinner()
418 if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
420 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_OVER);
421 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_OVER);
422 round_handler_Init(5, 10, 180);
423 FOR_EACH_MONSTER(head) if(head.health > 0)
425 WaypointSprite_Kill(head.sprite);
426 if(head.weaponentity) remove(head.weaponentity);
427 if(head.iceblock) remove(head.iceblock);
433 TD_count_alive_monsters();
435 max_perteam = max_monsters * 0.5;
437 if(time >= last_check)
438 if(total_killed < max_monsters)
440 if(redalive < max_perteam)
441 SpawnMonsters(NUM_TEAM_1);
442 if(bluealive < max_perteam)
443 SpawnMonsters(NUM_TEAM_2);
445 last_check = time + 0.5;
448 if(total_killed < max_monsters)
451 if(TD_ALIVE_TEAMS_OK())
454 float winner_team = TD_GetWinnerTeam();
457 Send_Notification(NOTIF_ALL, world, MSG_CENTER, APP_TEAM_NUM_4(winner_team, CENTER_ROUND_TEAM_WIN_));
458 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(winner_team, INFO_ROUND_TEAM_WIN_));
459 TeamScore_AddToTeam(winner_team, ST_SCORE, +1);
461 else if(winner_team == -1)
463 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_TIED);
464 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_TIED);
467 FOR_EACH_MONSTER(head) if(head.health > 0)
469 WaypointSprite_Kill(head.sprite);
470 if(head.weaponentity) remove(head.weaponentity);
471 if(head.iceblock) remove(head.iceblock);
475 round_handler_Init(5, 10, 180);
479 float TD_CheckTeams()
482 float readycount = 0, num_players = 0, ready_needed_factor, ready_needed_count;
484 FOR_EACH_REALPLAYER(head)
491 ready_needed_factor = bound(0.5, cvar("g_td_majority_factor"), 0.999);
492 ready_needed_count = floor(num_players * ready_needed_factor) + 1;
494 if(readycount >= ready_needed_count)
497 allowed_to_spawn = TRUE;
503 void spawnfunc_td_generator()
505 if not(g_td) { remove(self); return; }
508 td_debug("Generator without a team, removing it.\n");
513 precache_sound("onslaught/generator_underattack.wav");
514 precache_sound("onslaught/ons_hit1.wav");
515 precache_sound("onslaught/ons_hit2.wav");
516 precache_sound("weapons/rocket_impact.wav");
521 self.max_health = self.health;
522 self.classname = "td_generator";
523 self.flags = FL_GENERATOR;
525 setsize(self, GENERATOR_MIN, GENERATOR_MAX);
527 setorigin(self, self.origin + '0 0 20');
530 InitializeEntity(self, td_generator_delayed, INITPRIO_LAST);
533 void spawnfunc_td_waypoint()
535 if not(g_td) { remove(self); return; }
537 setsize(self, '-6 -6 -6', '6 6 6');
541 setorigin(self, self.origin + '0 0 20');
545 self.classname = "td_waypoint";
546 self.think = td_waypoint_think;
547 self.nextthink = time + 0.1;
550 void spawnfunc_td_controller()
552 if not(g_td) { remove(self); return; }
555 void spawnfunc_td_spawnpoint()
557 if not(g_td) { remove(self); return; }
559 self.classname = "td_spawnpoint";
561 self.effects = EF_STARDUST;
564 // initialization stuff
567 ScoreRules_basics(2, SFL_SORT_PRIO_SECONDARY, SFL_SORT_PRIO_SECONDARY, TRUE);
568 ScoreInfo_SetLabel_TeamScore(ST_TD_DESTROYS, "destroyed", SFL_SORT_PRIO_PRIMARY);
569 ScoreInfo_SetLabel_PlayerScore(SP_TD_DESTROYS,"destroyed", SFL_SORT_PRIO_PRIMARY);
570 ScoreRules_basics_end();
573 void td_SpawnController()
575 entity oldself = self;
577 self.classname = "td_controller";
578 spawnfunc_td_controller();
582 void td_DelayedInit()
584 if(find(world, classname, "td_controller") == world)
586 td_debug("No ""td_controller"" entity found on this map, creating it anyway.\n");
587 td_SpawnController();
595 InitializeEntity(world, td_DelayedInit, INITPRIO_GAMETYPE);
597 readyrestart_happened = TRUE; // disable normal ready command
599 round_handler_Spawn(TD_CheckTeams, TD_CheckWinner, TD_RoundStart);
600 round_handler_Init(5, 10, 180);
604 MUTATOR_HOOKFUNCTION(td_TurretSpawn)
606 if(self.realowner == world)
609 if(self.turrcaps_flags & TFL_TURRCAPS_SUPPORT)
610 self.target_range = 500;
612 self.bot_attack = FALSE;
613 buffturret(self, 0.7);
618 MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
620 if(!self.team || !self.realowner)
622 td_debug(strcat("Removed monster ", self.netname, " with team ", ftos(self.team), "\n"));
623 WaypointSprite_Kill(self.sprite);
624 if(self.weaponentity) remove(self.weaponentity);
629 self.candrop = FALSE;
630 self.bot_attack = FALSE;
631 self.ammo_fuel = bound(20, 20 * self.level, 100);
632 self.target_range = 300;
633 self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_MONSTERCLIP;
638 MUTATOR_HOOKFUNCTION(td_MonsterDies)
643 if(IS_PLAYER(frag_attacker.realowner))
645 PlayerScore_Add(frag_attacker.realowner, SP_SCORE, 5);
646 PlayerScore_Add(frag_attacker.realowner, SP_KILLS, 1);
651 backuporigin = self.origin;
656 setorigin(self, backuporigin + '0 0 5');
657 spawn_td_fuel(oldself.ammo_fuel);
658 self.touch = M_Item_Touch;
664 SUB_SetFade(self, time + 5, 1);
671 MUTATOR_HOOKFUNCTION(td_MonsterThink)
673 if(time <= game_starttime && round_handler_IsActive())
676 if(IS_PLAYER(self.enemy))
682 if(monster_target.flags & FL_GENERATOR)
683 if(monster_target.health <= 0)
686 if not(self.enemy) // don't change targets while attacking
687 if(vlen(monster_target.origin - self.origin) <= tr)
689 if(monster_target.target2)
692 self.target2 = monster_target.target2;
694 self.target2 = monster_target.target;
697 self.target2 = monster_target.target;
699 monster_target = find(world, targetname, self.target2);
701 if(monster_target == world)
702 monster_target = PickGenerator(self.team);
704 if(monster_target == world)
705 return TRUE; // no generators or waypoints?!
708 td_debug(sprintf("Monster name: %s. Monster target: %s. Monster target2: %s. Monster target entity: %s.\n", self.netname, self.target, self.target2, etos(monster_target)));
710 if(!self.enemy && !monster_target)
711 return TRUE; // no enemy or target, must be wandering
713 monster_speed_run = (150 + random() * 4) * monster_skill;
714 monster_speed_walk = (100 + random() * 4) * monster_skill;
719 MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
723 for(e = world;(e = findflags(e, monster_attack, TRUE)); )
726 if(e.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
729 if(e.flags & FL_MONSTER)
730 continue; // don't attack other monsters?
732 if(monster_isvalidtarget(e, self))
739 MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
741 self.monster_attack = FALSE;
742 self.bot_attack = FALSE;
743 self.solid = SOLID_CORPSE;
747 self.ammo_fuel = self.newfuel;
754 MUTATOR_HOOKFUNCTION(td_Damage)
756 if(IS_PLAYER(frag_attacker))
757 if(frag_target.flags & FL_MONSTER)
760 if(IS_PLAYER(frag_attacker) || frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
761 if(IS_PLAYER(frag_target))
764 if(frag_attacker != frag_target)
765 frag_force = '0 0 0';
771 MUTATOR_HOOKFUNCTION(td_PlayerCommand)
773 if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
777 makevectors(self.v_angle);
779 org = self.origin + self.view_ofs + v_forward * 100;
781 tracebox(self.origin + self.view_ofs, '-16 -16 -16', '16 16 16', org, MOVE_NORMAL, self);
782 entity targ = trace_ent;
783 if(targ.owner.realowner == self)
786 if(cmd_name == "ready")
790 bprint(self.netname, "^2 is ready\n");
792 Nagger_ReadyCounted();
797 if(cmd_name == "turretspawn")
799 if(argv(1) == "list")
801 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_LIST, "mlrs walker plasma towerbuff flac");
804 if(!IS_PLAYER(self) || self.health <= 0)
806 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_CANTSPAWN);
811 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_DISABLED);
814 if(self.turret_cnt >= max_turrets)
816 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXTURRETS, max_turrets);
820 spawnturret(self, self, argv(1), trace_endpos);
824 if(cmd_name == "turretremove")
826 if((targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && (targ.playerid == self.playerid || targ.realowner == self))
828 self.turret_cnt -= 1;
829 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_REMOVE);
830 WaypointSprite_Kill(targ.sprite);
831 remove(targ.tur_head);
835 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_REMOVE);
842 MUTATOR_HOOKFUNCTION(td_ClientConnect)
850 for(t = world; (t = findflags(t, turrcaps_flags, TFL_TURRCAPS_ISTURRET)); )
851 if(t.playerid == self.playerid)
854 self.turret_cnt += 1; // nice try
860 MUTATOR_HOOKFUNCTION(td_DisableVehicles)
865 MUTATOR_HOOKFUNCTION(td_SetModname)
872 MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
874 if(time < game_starttime || (time <= game_starttime && round_handler_IsActive()) || gameover)
876 turret_target = world;
877 return FALSE; // battle hasn't started
880 if(turret_flags & TFL_TARGETSELECT_MISSILESONLY)
881 if(turret_target.flags & FL_PROJECTILE)
882 if(turret_target.owner.flags & FL_MONSTER)
883 return TRUE; // flac support
885 if not(turret_target.flags & FL_MONSTER)
886 turret_target = world;
888 if(!IsDifferentTeam(turret_target, turret))
889 turret_target = world;
894 MUTATOR_HOOKFUNCTION(td_TurretDies)
897 self.realowner.turret_cnt -= 1;
902 MUTATOR_HOOKFUNCTION(td_GetTeamCount)
909 MUTATOR_DEFINITION(gamemode_towerdefense)
911 MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
912 MUTATOR_HOOK(MonsterSpawn, td_MonsterSpawn, CBC_ORDER_ANY);
913 MUTATOR_HOOK(MonsterDies, td_MonsterDies, CBC_ORDER_ANY);
914 MUTATOR_HOOK(MonsterMove, td_MonsterThink, CBC_ORDER_ANY);
915 MUTATOR_HOOK(MonsterFindTarget, td_MonsterFindTarget, CBC_ORDER_ANY);
916 MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
917 MUTATOR_HOOK(PlayerDamage_Calculate, td_Damage, CBC_ORDER_ANY);
918 MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, CBC_ORDER_ANY);
919 MUTATOR_HOOK(ClientConnect, td_ClientConnect, CBC_ORDER_ANY);
920 MUTATOR_HOOK(VehicleSpawn, td_DisableVehicles, CBC_ORDER_ANY);
921 MUTATOR_HOOK(SetModname, td_SetModname, CBC_ORDER_ANY);
922 MUTATOR_HOOK(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
923 MUTATOR_HOOK(TurretDies, td_TurretDies, CBC_ORDER_ANY);
924 MUTATOR_HOOK(GetTeamCount, td_GetTeamCount, CBC_ORDER_ANY);
928 if(time > 1) // game loads at time 1
929 error("This is a game type and it cannot be added at runtime.");
930 cvar_settemp("g_monsters", "1");
931 cvar_settemp("g_turrets", "1");
935 MUTATOR_ONROLLBACK_OR_REMOVE
937 // we actually cannot roll back td_Initialize here
938 // BUT: we don't need to! If this gets called, adding always
944 error("This is a game type and it cannot be removed at runtime.");