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, float monster)
198 td_debug("Warning: couldn't find any td_spawnpoint spawnpoints, no monsters will spawn!\n");
202 mon = spawnmonster("", monster, 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 float RandomMonster()
216 RandomSelection_Init();
220 for(i = MONSTER_FIRST + 1; i < MONSTER_LAST; ++i)
222 if(i == MONSTER_STINGRAY || i == MONSTER_WYVERN)
223 continue; // flying/swimming monsters not yet supported
225 RandomSelection_Add(world, i, "", 1, 1);
228 return RandomSelection_chosen_float;
231 void SpawnMonsters(float tm)
235 whichmon = RandomMonster();
237 TD_SpawnMonster(tm, whichmon);
240 entity PickGenerator(float tm)
244 RandomSelection_Init();
245 for(head = world;(head = findflags(head, flags, FL_GENERATOR)); )
247 RandomSelection_Add(head, 0, string_null, 1, 1);
249 return RandomSelection_chosen_ent;
252 float td_checkfuel(entity ent, string tur)
254 float turcost = cvar(strcat("g_td_turret_", tur, "_cost"));
256 if(ent.ammo_fuel < turcost)
258 Send_Notification(NOTIF_ONE, ent, MSG_MULTI, MULTI_TD_NOFUEL);
262 ent.ammo_fuel -= turcost;
267 void spawnturret(entity spawnedby, entity own, string turet, vector orig)
269 if not(IS_PLAYER(spawnedby)) { td_debug("Warning: A non-player entity tried to spawn a turret\n"); return; }
270 if not(td_checkfuel(spawnedby, turet)) { return; }
277 setorigin(self, orig);
278 self.spawnflags = TSL_NO_RESPAWN;
279 self.monster_attack = TRUE;
280 self.realowner = own;
281 self.playerid = own.playerid;
282 self.angles_y = spawnedby.v_angle_y;
283 spawnedby.turret_cnt += 1;
284 self.team = own.team;
288 case "plasma": spawnfunc_turret_plasma(); break;
289 case "mlrs": spawnfunc_turret_mlrs(); break;
290 case "walker": spawnfunc_turret_walker(); break;
291 case "flac": spawnfunc_turret_flac(); break;
292 case "towerbuff": spawnfunc_turret_fusionreactor(); break;
293 default: Send_Notification(NOTIF_ONE, spawnedby, MSG_INFO, INFO_TD_INVALID); remove(self); self = oldself; return;
296 Send_Notification(NOTIF_ONE, spawnedby, MSG_MULTI, MULTI_TD_SPAWN);
301 void buffturret(entity tur, float buff)
303 float refbuff = bound(0.01, buff * 0.05, 0.1);
305 tur.turret_buff += 1;
306 tur.max_health *= buff;
307 tur.tur_health = tur.max_health;
308 tur.health = tur.max_health;
309 tur.ammo_max *= buff;
310 tur.ammo_recharge *= buff;
311 tur.shot_dmg *= buff;
312 tur.shot_radius *= buff;
313 tur.shot_speed *= buff;
314 tur.shot_spread *= buff;
315 tur.shot_force *= buff;
318 tur.shot_refire += refbuff;
320 tur.shot_refire -= refbuff;
323 void spawn_td_fuel(float fuel_size)
325 if not(g_td) {remove(self); return; }
327 self.ammo_fuel = fuel_size * monster_skill;
328 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);
330 self.velocity = randomvec() * 175 + '0 0 325';
333 void td_generator_delayed()
335 generator_link(td_generator_setup);
337 self.SendFlags = GSF_SETUP;
341 #define TD_ALIVE_TEAMS() ((redalive > 0) + (bluealive > 0))
342 #define TD_ALIVE_TEAMS_OK() (TD_ALIVE_TEAMS() == 2)
347 allowed_to_spawn = TRUE;
349 ignore_turrets = TRUE;
351 FOR_EACH_PLAYER(head)
357 void TD_count_alive_monsters()
365 FOR_EACH_MONSTER(head)
367 if(head.health <= 0) continue;
373 case NUM_TEAM_1: ++redalive; break;
374 case NUM_TEAM_2: ++bluealive; break;
379 float TD_GetWinnerTeam()
381 float winner_team = 0;
383 winner_team = NUM_TEAM_1;
386 if(winner_team) return 0;
387 winner_team = NUM_TEAM_2;
391 return -1; // no monster left
394 float TD_CheckWinner()
398 if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
400 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_OVER);
401 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_OVER);
402 round_handler_Init(5, 10, 180);
403 FOR_EACH_MONSTER(head) if(head.health > 0)
405 WaypointSprite_Kill(head.sprite);
406 if(head.weaponentity) remove(head.weaponentity);
407 if(head.iceblock) remove(head.iceblock);
413 TD_count_alive_monsters();
415 max_perteam = max_monsters * 0.5;
417 if(time >= last_check)
418 if(total_killed < max_monsters)
420 if(redalive < max_perteam)
421 SpawnMonsters(NUM_TEAM_1);
422 if(bluealive < max_perteam)
423 SpawnMonsters(NUM_TEAM_2);
425 last_check = time + 0.5;
428 if(total_killed < max_monsters)
431 if(TD_ALIVE_TEAMS_OK())
434 float winner_team = TD_GetWinnerTeam();
437 Send_Notification(NOTIF_ALL, world, MSG_CENTER, APP_TEAM_NUM_4(winner_team, CENTER_ROUND_TEAM_WIN_));
438 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(winner_team, INFO_ROUND_TEAM_WIN_));
439 TeamScore_AddToTeam(winner_team, ST_SCORE, +1);
441 else if(winner_team == -1)
443 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_TIED);
444 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_TIED);
447 FOR_EACH_MONSTER(head) if(head.health > 0)
449 WaypointSprite_Kill(head.sprite);
450 if(head.weaponentity) remove(head.weaponentity);
451 if(head.iceblock) remove(head.iceblock);
455 round_handler_Init(5, 10, 180);
459 float TD_CheckTeams()
462 float readycount = 0, num_players = 0, ready_needed_factor, ready_needed_count;
464 FOR_EACH_REALPLAYER(head)
471 ready_needed_factor = bound(0.5, cvar("g_td_majority_factor"), 0.999);
472 ready_needed_count = floor(num_players * ready_needed_factor) + 1;
474 if(readycount >= ready_needed_count || time >= ready_timeout)
477 allowed_to_spawn = TRUE;
483 void spawnfunc_td_generator()
485 if not(g_td) { remove(self); return; }
488 td_debug("Generator without a team, removing it.\n");
493 precache_sound("onslaught/generator_underattack.wav");
494 precache_sound("onslaught/ons_hit1.wav");
495 precache_sound("onslaught/ons_hit2.wav");
496 precache_sound("weapons/rocket_impact.wav");
501 self.max_health = self.health;
502 self.classname = "td_generator";
503 self.flags = FL_GENERATOR;
505 setsize(self, GENERATOR_MIN, GENERATOR_MAX);
507 setorigin(self, self.origin + '0 0 20');
510 InitializeEntity(self, td_generator_delayed, INITPRIO_LAST);
513 void spawnfunc_td_waypoint()
515 if not(g_td) { remove(self); return; }
517 setsize(self, '-6 -6 -6', '6 6 6');
521 setorigin(self, self.origin + '0 0 20');
525 self.classname = "td_waypoint";
526 self.think = td_waypoint_think;
527 self.nextthink = time + 0.1;
530 void spawnfunc_td_controller()
532 if not(g_td) { remove(self); return; }
535 void spawnfunc_td_spawnpoint()
537 if not(g_td) { remove(self); return; }
539 self.classname = "td_spawnpoint";
541 self.effects = EF_STARDUST;
544 // initialization stuff
547 ScoreRules_basics(2, SFL_SORT_PRIO_SECONDARY, SFL_SORT_PRIO_SECONDARY, TRUE);
548 ScoreInfo_SetLabel_TeamScore(ST_TD_DESTROYS, "destroyed", SFL_SORT_PRIO_PRIMARY);
549 ScoreInfo_SetLabel_PlayerScore(SP_TD_DESTROYS,"destroyed", SFL_SORT_PRIO_PRIMARY);
550 ScoreRules_basics_end();
553 void td_SpawnController()
555 entity oldself = self;
557 self.classname = "td_controller";
558 spawnfunc_td_controller();
562 void td_DelayedInit()
564 if(find(world, classname, "td_controller") == world)
566 td_debug("No ""td_controller"" entity found on this map, creating it anyway.\n");
567 td_SpawnController();
575 InitializeEntity(world, td_DelayedInit, INITPRIO_GAMETYPE);
577 readyrestart_happened = TRUE; // disable normal ready command
579 ready_timeout = time + 30;
581 round_handler_Spawn(TD_CheckTeams, TD_CheckWinner, TD_RoundStart);
582 round_handler_Init(5, 10, 180);
586 MUTATOR_HOOKFUNCTION(td_TurretSpawn)
588 if(self.realowner == world)
591 if(self.turrcaps_flags & TFL_TURRCAPS_SUPPORT)
592 self.target_range = 500;
594 self.bot_attack = FALSE;
595 buffturret(self, 0.7);
600 MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
602 if(!self.team || !self.realowner)
604 td_debug(strcat("Removed monster ", self.netname, " with team ", ftos(self.team), "\n"));
605 WaypointSprite_Kill(self.sprite);
606 if(self.weaponentity) remove(self.weaponentity);
611 self.candrop = FALSE;
612 self.bot_attack = FALSE;
613 self.ammo_fuel = bound(20, 20 * self.level, 100);
614 self.target_range = 300;
615 self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_MONSTERCLIP;
620 MUTATOR_HOOKFUNCTION(td_MonsterDies)
625 if(IS_PLAYER(frag_attacker.realowner))
627 PlayerScore_Add(frag_attacker.realowner, SP_SCORE, 5);
628 PlayerScore_Add(frag_attacker.realowner, SP_KILLS, 1);
633 backuporigin = self.origin;
638 setorigin(self, backuporigin + '0 0 5');
639 spawn_td_fuel(oldself.ammo_fuel);
640 self.touch = M_Item_Touch;
646 SUB_SetFade(self, time + 5, 1);
653 MUTATOR_HOOKFUNCTION(td_MonsterThink)
655 if(time <= game_starttime && round_handler_IsActive())
658 if(IS_PLAYER(self.enemy))
664 if(monster_target.flags & FL_GENERATOR)
665 if(monster_target.health <= 0)
668 if not(self.enemy) // don't change targets while attacking
669 if(vlen(monster_target.origin - self.origin) <= tr)
671 if(monster_target.target2)
674 self.target2 = monster_target.target2;
676 self.target2 = monster_target.target;
679 self.target2 = monster_target.target;
681 monster_target = find(world, targetname, self.target2);
683 if(monster_target == world)
684 monster_target = PickGenerator(self.team);
686 if(monster_target == world)
687 return TRUE; // no generators or waypoints?!
690 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)));
692 if(!self.enemy && !monster_target)
693 return TRUE; // no enemy or target, must be wandering
695 monster_speed_run = (150 + random() * 4) * monster_skill;
696 monster_speed_walk = (100 + random() * 4) * monster_skill;
701 MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
705 for(e = world;(e = findflags(e, monster_attack, TRUE)); )
708 if(e.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
711 if(e.flags & FL_MONSTER)
712 continue; // don't attack other monsters?
714 if(monster_isvalidtarget(e, self))
721 MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
723 self.monster_attack = FALSE;
724 self.bot_attack = FALSE;
725 self.solid = SOLID_CORPSE;
729 self.ammo_fuel = self.newfuel;
736 MUTATOR_HOOKFUNCTION(td_Damage)
738 if(IS_PLAYER(frag_attacker))
739 if(frag_target.flags & FL_MONSTER || frag_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
742 if(IS_PLAYER(frag_attacker) || frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
743 if(IS_PLAYER(frag_target))
746 if(frag_attacker != frag_target)
747 frag_force = '0 0 0';
753 MUTATOR_HOOKFUNCTION(td_PlayerCommand)
755 if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
759 makevectors(self.v_angle);
761 org = self.origin + self.view_ofs + v_forward * 100;
763 tracebox(self.origin + self.view_ofs, '-16 -16 -16', '16 16 16', org, MOVE_NORMAL, self);
764 entity targ = trace_ent;
765 if(targ.owner.realowner == self)
768 if(cmd_name == "ready")
772 bprint(self.netname, "^2 is ready\n");
774 Nagger_ReadyCounted();
779 if(cmd_name == "turretspawn")
781 if(argv(1) == "list")
783 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_LIST, "mlrs walker plasma towerbuff flac");
786 if(!IS_PLAYER(self) || self.health <= 0)
788 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_CANTSPAWN);
793 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_DISABLED);
796 if(self.turret_cnt >= max_turrets)
798 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXTURRETS, max_turrets);
802 spawnturret(self, self, argv(1), trace_endpos);
806 if(cmd_name == "turretremove")
808 if((targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && (targ.playerid == self.playerid || targ.realowner == self))
810 self.turret_cnt -= 1;
811 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_REMOVE);
812 WaypointSprite_Kill(targ.sprite);
813 remove(targ.tur_head);
817 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_REMOVE);
824 MUTATOR_HOOKFUNCTION(td_ClientConnect)
832 for(t = world; (t = findflags(t, turrcaps_flags, TFL_TURRCAPS_ISTURRET)); )
833 if(t.playerid == self.playerid)
836 self.turret_cnt += 1; // nice try
842 MUTATOR_HOOKFUNCTION(td_DisableVehicles)
847 MUTATOR_HOOKFUNCTION(td_SetModname)
854 MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
856 if(time < game_starttime || (time <= game_starttime && round_handler_IsActive()) || gameover)
858 turret_target = world;
859 return FALSE; // battle hasn't started
862 if(turret_flags & TFL_TARGETSELECT_MISSILESONLY)
863 if(turret_target.flags & FL_PROJECTILE)
864 if(turret_target.owner.flags & FL_MONSTER)
865 return TRUE; // flac support
867 if not(turret_target.flags & FL_MONSTER)
868 turret_target = world;
870 if(!IsDifferentTeam(turret_target, turret))
871 turret_target = world;
876 MUTATOR_HOOKFUNCTION(td_TurretDies)
879 self.realowner.turret_cnt -= 1;
884 MUTATOR_HOOKFUNCTION(td_GetTeamCount)
891 MUTATOR_DEFINITION(gamemode_towerdefense)
893 MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
894 MUTATOR_HOOK(MonsterSpawn, td_MonsterSpawn, CBC_ORDER_ANY);
895 MUTATOR_HOOK(MonsterDies, td_MonsterDies, CBC_ORDER_ANY);
896 MUTATOR_HOOK(MonsterMove, td_MonsterThink, CBC_ORDER_ANY);
897 MUTATOR_HOOK(MonsterFindTarget, td_MonsterFindTarget, CBC_ORDER_ANY);
898 MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
899 MUTATOR_HOOK(PlayerDamage_Calculate, td_Damage, CBC_ORDER_ANY);
900 MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, CBC_ORDER_ANY);
901 MUTATOR_HOOK(ClientConnect, td_ClientConnect, CBC_ORDER_ANY);
902 MUTATOR_HOOK(VehicleSpawn, td_DisableVehicles, CBC_ORDER_ANY);
903 MUTATOR_HOOK(SetModname, td_SetModname, CBC_ORDER_ANY);
904 MUTATOR_HOOK(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
905 MUTATOR_HOOK(TurretDies, td_TurretDies, CBC_ORDER_ANY);
906 MUTATOR_HOOK(GetTeamCount, td_GetTeamCount, CBC_ORDER_ANY);
910 if(time > 1) // game loads at time 1
911 error("This is a game type and it cannot be added at runtime.");
912 cvar_settemp("g_monsters", "1");
913 cvar_settemp("g_turrets", "1");
917 MUTATOR_ONROLLBACK_OR_REMOVE
919 // we actually cannot roll back td_Initialize here
920 // BUT: we don't need to! If this gets called, adding always
926 error("This is a game type and it cannot be removed at runtime.");