1 float redalive, bluealive, total_alive;
3 var float max_monsters = 20;
4 var float max_alive = 10;
8 var float max_turrets = 3;
10 .float newfuel; // hack to not give players fuel every time they spawn
19 void td_debug(string input)
21 switch(autocvar_g_td_debug)
23 case 1: dprint(input); break;
24 case 2: print(input); break;
28 void td_waypoint_link(float tm, vector from, vector to)
33 WarpZone_TrailParticles(world, particleeffectnum("waypoint_link_red"), from, to);
36 WarpZone_TrailParticles(world, particleeffectnum("waypoint_link_blue"), from, to);
41 void td_waypoint_think()
49 if(time >= self.last_trace)
53 e = find(world, targetname, self.target);
54 e2 = find(world, target, self.targetname);
55 e3 = find(world, targetname, self.target2);
57 if(e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
58 td_waypoint_link(self.team, self.origin, e.origin);
59 if(e2.classname == "td_spawnpoint")
60 td_waypoint_link(self.team, self.origin, e2.origin);
61 if(e3.classname == "td_waypoint" || e3.flags & FL_GENERATOR)
62 td_waypoint_link(self.team, self.origin, e3.origin);
64 self.last_trace = time + 0.5;
67 self.nextthink = time + 0.1;
70 void td_generator_die()
72 if(autocvar_sv_eventlog)
73 GameLogEcho(":gendestroyed");
75 Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_GENDESTROYED);
77 self.solid = SOLID_NOT;
78 self.takedamage = DAMAGE_NO;
79 self.event_damage = func_null;
82 WaypointSprite_Kill(self.sprite);
85 void td_generator_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
87 if(IS_PLAYER(attacker) || attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET || attacker.vehicle_flags & VHF_ISVEHICLE || self.takedamage == DAMAGE_NO)
92 if (time > self.pain_finished)
94 self.pain_finished = time + 10;
95 play2team(self.team, "onslaught/generator_underattack.wav");
99 spamsound(self, CH_TRIGGER, "onslaught/ons_hit1.wav", VOL_BASE, ATTN_NORM);
101 spamsound(self, CH_TRIGGER, "onslaught/ons_hit2.wav", VOL_BASE, ATTN_NORM);
104 FOR_EACH_REALPLAYER(head)
105 if(!IsDifferentTeam(head, self))
106 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_TD_GENDAMAGED);
108 self.health -= damage;
110 WaypointSprite_UpdateHealth(self.sprite, self.health);
114 FOR_EACH_REALPLAYER(head)
115 if(!IsDifferentTeam(head, attacker))
116 PlayerScore_Add(head, SP_TD_DESTROYS, 1);
118 TeamScore_AddToTeam(attacker.team, ST_TD_DESTROYS, 1);
122 self.SendFlags |= GSF_STATUS;
125 void td_generator_reset()
127 self.SendFlags |= GSF_SETUP;
130 void td_generator_setup()
132 self.think = func_null;
134 self.solid = SOLID_BBOX;
135 self.takedamage = DAMAGE_AIM;
136 self.event_damage = td_generator_damage;
137 self.movetype = MOVETYPE_NONE;
138 self.monster_attack = TRUE;
139 self.SendFlags = GSF_SETUP;
140 self.netname = "Generator";
141 self.reset = td_generator_reset;
143 WaypointSprite_SpawnFixed(self.netname, self.origin + '0 0 90', self, sprite, RADARICON_OBJECTIVE, Team_ColorRGB(self.team));
144 WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
145 WaypointSprite_UpdateHealth(self.sprite, self.health);
148 void AnnounceSpawn(string anounce)
151 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_TD_ANNOUNCE_SPAWN, anounce);
153 FOR_EACH_REALCLIENT(e) soundto(MSG_ONE, e, CHAN_AUTO, "kh/alarm.wav", VOL_BASE, ATTN_NONE);
156 entity PickSpawn (float tm)
159 RandomSelection_Init();
160 for(e = world;(e = find(e, classname, "td_spawnpoint")); )
162 RandomSelection_Add(e, 0, string_null, 1, 1);
164 return RandomSelection_chosen_ent;
167 void TD_SpawnMonster(float tm, string mnster)
175 td_debug("Warning: couldn't find any td_spawnpoint spawnpoints, no monsters will spawn!\n");
179 mon = spawnmonster(mnster, e, e, e.origin, FALSE, 2);
182 if(random() <= 0.5 && e.target)
183 mon.target2 = e.target;
185 mon.target2 = e.target2;
188 mon.target2 = e.target;
191 string monster_type2string(float mnster)
195 case MONSTER_ZOMBIE: return "zombie";
196 case MONSTER_BRUTE: return "brute";
197 case MONSTER_ANIMUS: return "animus";
198 case MONSTER_SHAMBLER: return "shambler";
199 case MONSTER_BRUISER: return "bruiser";
200 case MONSTER_WYVERN: return "wyvern";
201 case MONSTER_CERBERUS: return "cerberus";
202 case MONSTER_SLIME: return "slime";
203 case MONSTER_KNIGHT: return "knight";
204 case MONSTER_STINGRAY: return "stingray";
205 case MONSTER_MAGE: return "mage";
206 case MONSTER_SPIDER: return "spider";
211 float RandomMonster()
213 RandomSelection_Init();
217 for(i = MONSTER_FIRST + 1; i < MONSTER_LAST; ++i)
219 if(i == MONSTER_STINGRAY || i == MONSTER_WYVERN)
220 continue; // flying/swimming monsters not yet supported
222 RandomSelection_Add(world, i, "", 1, 1);
225 return RandomSelection_chosen_float;
228 void SpawnMonsters(float tm)
232 whichmon = RandomMonster();
234 TD_SpawnMonster(tm, monster_type2string(whichmon));
237 entity PickGenerator(float tm)
241 RandomSelection_Init();
242 for(head = world;(head = findflags(head, flags, FL_GENERATOR)); )
244 RandomSelection_Add(head, 0, string_null, 1, 1);
246 return RandomSelection_chosen_ent;
249 float td_checkfuel(entity ent, string tur)
251 float turcost = cvar(strcat("g_td_turret_", tur, "_cost"));
253 if(ent.ammo_fuel < turcost)
255 Send_Notification(NOTIF_ONE, ent, MSG_MULTI, MULTI_TD_NOFUEL);
259 ent.ammo_fuel -= turcost;
264 void spawnturret(entity spawnedby, entity own, string turet, vector orig)
266 if not(IS_PLAYER(spawnedby)) { dprint("Warning: A non-player entity tried to spawn a turret\n"); return; }
267 if not(td_checkfuel(spawnedby, turet)) { return; }
274 setorigin(self, orig);
275 self.spawnflags = TSL_NO_RESPAWN;
276 self.monster_attack = TRUE;
277 self.realowner = own;
278 self.playerid = own.playerid;
279 self.angles_y = spawnedby.v_angle_y;
280 spawnedby.turret_cnt += 1;
281 self.team = own.team;
285 case "plasma": spawnfunc_turret_plasma(); break;
286 case "mlrs": spawnfunc_turret_mlrs(); break;
287 case "walker": spawnfunc_turret_walker(); break;
288 case "flac": spawnfunc_turret_flac(); break;
289 case "towerbuff": spawnfunc_turret_fusionreactor(); break;
290 default: Send_Notification(NOTIF_ONE, spawnedby, MSG_INFO, INFO_TD_INVALID); remove(self); self = oldself; return;
293 Send_Notification(NOTIF_ONE, spawnedby, MSG_MULTI, MULTI_TD_SPAWN);
298 void spawn_td_fuel(float fuel_size)
300 if not(g_td) {remove(self); return; }
302 self.ammo_fuel = fuel_size * monster_skill;
303 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);
305 self.velocity = randomvec() * 175 + '0 0 325';
309 #define TD_ALIVE_TEAMS() ((redalive > 0) + (bluealive > 0))
310 #define TD_ALIVE_TEAMS_OK() (TD_ALIVE_TEAMS() == 2)
313 allowed_to_spawn = TRUE;
318 void TD_count_alive_monsters()
326 FOR_EACH_MONSTER(head)
328 if(head.health <= 0) continue;
334 case NUM_TEAM_1: ++redalive; break;
335 case NUM_TEAM_2: ++bluealive; break;
340 float TD_GetWinnerTeam()
342 float winner_team = 0;
344 winner_team = NUM_TEAM_1;
347 if(winner_team) return 0;
348 winner_team = NUM_TEAM_2;
352 return -1; // no monster left
355 float TD_CheckWinner()
357 if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
359 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_OVER);
360 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_OVER);
361 round_handler_Init(5, 1, 180);
365 TD_count_alive_monsters();
367 if(time >= last_check)
368 if(total_alive < max_alive)
369 if(total_killed < max_monsters)
371 SpawnMonsters(NUM_TEAM_1);
372 SpawnMonsters(NUM_TEAM_2);
374 last_check = time + 0.5;
378 return 0; // nothing has died, can't be a tie
380 if(TD_ALIVE_TEAMS_OK())
383 float winner_team = TD_GetWinnerTeam();
386 Send_Notification(NOTIF_ALL, world, MSG_CENTER, APP_TEAM_NUM_4(winner_team, CENTER_ROUND_TEAM_WIN_));
387 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(winner_team, INFO_ROUND_TEAM_WIN_));
388 TeamScore_AddToTeam(winner_team, ST_SCORE, +1);
390 else if(winner_team == -1)
392 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_TIED);
393 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_TIED);
396 round_handler_Init(5, 1, 180);
400 float TD_CheckTeams()
402 allowed_to_spawn = TRUE;
408 void spawnfunc_td_generator()
410 if not(g_td) { remove(self); return; }
413 td_debug("Generator without a team, removing it.\n");
418 precache_sound("onslaught/generator_underattack.wav");
419 precache_sound("onslaught/ons_hit1.wav");
420 precache_sound("onslaught/ons_hit2.wav");
421 precache_sound("weapons/rocket_impact.wav");
426 self.max_health = self.health;
427 self.classname = "td_generator";
428 self.flags = FL_GENERATOR;
430 setsize(self, GENERATOR_MIN, GENERATOR_MAX);
432 setorigin(self, self.origin + '0 0 20');
435 generator_link(td_generator_setup);
438 void spawnfunc_td_waypoint()
440 if not(g_td) { remove(self); return; }
443 td_debug("Tower Defense waypoint without a team, removing it.\n");
448 setsize(self, '-6 -6 -6', '6 6 6');
452 setorigin(self, self.origin + '0 0 20');
456 self.classname = "td_waypoint";
457 self.think = td_waypoint_think;
458 self.nextthink = time + 0.1;
461 void spawnfunc_td_controller()
463 if not(g_td) { remove(self); return; }
466 void spawnfunc_td_spawnpoint()
468 if not(g_td) { remove(self); return; }
470 self.classname = "td_spawnpoint";
472 self.effects = EF_STARDUST;
475 // initialization stuff
478 ScoreRules_basics(2, SFL_SORT_PRIO_SECONDARY, SFL_SORT_PRIO_SECONDARY, TRUE);
479 ScoreInfo_SetLabel_TeamScore(ST_TD_DESTROYS, "destroyed", SFL_SORT_PRIO_PRIMARY);
480 ScoreInfo_SetLabel_PlayerScore(SP_TD_DESTROYS,"destroyed", SFL_SORT_PRIO_PRIMARY);
481 ScoreRules_basics_end();
484 void td_SpawnController()
486 entity oldself = self;
488 self.classname = "td_controller";
489 spawnfunc_td_controller();
493 void td_DelayedInit()
495 if(find(world, classname, "td_controller") == world)
497 print("No ""td_controller"" entity found on this map, creating it anyway.\n");
498 td_SpawnController();
506 InitializeEntity(world, td_DelayedInit, INITPRIO_GAMETYPE);
508 round_handler_Spawn(TD_CheckTeams, TD_CheckWinner, TD_RoundStart);
509 round_handler_Init(5, 10, 180);
513 MUTATOR_HOOKFUNCTION(td_TurretSpawn)
515 if(self.realowner == world)
518 self.bot_attack = FALSE;
523 MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
525 if(!self.team || !self.realowner)
527 td_debug(strcat("Removed monster ", self.netname, " with team ", ftos(self.team), "\n"));
528 WaypointSprite_Kill(self.sprite);
529 if(self.weaponentity) remove(self.weaponentity);
534 WaypointSprite_Kill(self.sprite);
536 self.candrop = FALSE;
537 self.bot_attack = FALSE;
538 self.ammo_fuel = bound(20, 20 * self.level, 100);
539 self.target_range = 300;
540 self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_CORPSE | DPCONTENTS_MONSTERCLIP;
545 MUTATOR_HOOKFUNCTION(td_MonsterDies)
550 if(IS_PLAYER(frag_attacker.realowner))
552 PlayerScore_Add(frag_attacker.realowner, SP_SCORE, 5);
553 PlayerScore_Add(frag_attacker.realowner, SP_KILLS, 1);
558 backuporigin = self.origin;
563 setorigin(self, backuporigin + '0 0 5');
564 spawn_td_fuel(oldself.ammo_fuel);
565 self.touch = M_Item_Touch;
571 SUB_SetFade(self, time + 5, 1);
578 MUTATOR_HOOKFUNCTION(td_MonsterThink)
580 if(time <= game_starttime && round_handler_IsActive())
583 if(IS_PLAYER(self.enemy))
586 if not(self.enemy) // don't change targets while attacking
587 if(vlen(monster_target.origin - self.origin) <= 100)
589 if(monster_target.target2)
592 self.target2 = monster_target.target2;
594 self.target2 = monster_target.target;
597 self.target2 = monster_target.target;
599 monster_target = find(world, targetname, self.target2);
601 if(monster_target == world)
602 monster_target = PickGenerator(self.team);
604 if(monster_target == world)
605 return TRUE; // no generators or waypoints?!
608 td_debug(sprintf("Monster target: %s. Monster target2: %s. Monster target entity: %s.\n", self.target, self.target2, etos(monster_target)));
610 if(!self.enemy && !monster_target)
611 return TRUE; // no enemy or target, must be wandering
613 monster_speed_run = (150 + random() * 4) * monster_skill;
614 monster_speed_walk = (100 + random() * 4) * monster_skill;
619 MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
623 for(e = world;(e = findflags(e, monster_attack, TRUE)); )
626 if(e.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
629 if(e.flags & FL_MONSTER)
630 continue; // don't attack other monsters?
632 if(monster_isvalidtarget(e, self))
639 MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
641 self.monster_attack = FALSE;
642 self.bot_attack = FALSE;
646 self.ammo_fuel = self.newfuel;
653 MUTATOR_HOOKFUNCTION(td_Damage)
655 if(IS_PLAYER(frag_attacker))
656 if(frag_target.flags & FL_MONSTER)
659 if(IS_PLAYER(frag_target))
662 if(frag_attacker != frag_target)
663 frag_force = '0 0 0';
669 MUTATOR_HOOKFUNCTION(td_PlayerCommand)
671 if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
675 makevectors(self.v_angle);
677 org = self.origin + self.view_ofs + v_forward * 100;
679 tracebox(self.origin + self.view_ofs, '-16 -16 -16', '16 16 16', org, MOVE_NORMAL, self);
680 entity targ = trace_ent;
681 if(targ.owner.realowner == self)
684 if(cmd_name == "turretspawn")
686 if(argv(1) == "list")
688 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_LIST, "mlrs walker plasma towerbuff flac");
691 if(!IS_PLAYER(self) || self.health <= 0)
693 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_CANTSPAWN);
698 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_DISABLED);
701 if(self.turret_cnt >= max_turrets)
703 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXTURRETS, max_turrets);
707 spawnturret(self, self, argv(1), trace_endpos);
711 if(cmd_name == "turretremove")
713 if((targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && (targ.playerid == self.playerid || targ.realowner == self))
715 self.turret_cnt -= 1;
716 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_REMOVE);
717 WaypointSprite_Kill(targ.sprite);
718 remove(targ.tur_head);
722 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_REMOVE);
729 MUTATOR_HOOKFUNCTION(td_ClientConnect)
737 for(t = world; (t = findflags(t, turrcaps_flags, TFL_TURRCAPS_ISTURRET)); )
738 if(t.playerid == self.playerid)
741 self.turret_cnt += 1; // nice try
747 MUTATOR_HOOKFUNCTION(td_DisableVehicles)
752 MUTATOR_HOOKFUNCTION(td_SetModname)
759 MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
761 if(time < game_starttime || (time <= game_starttime && round_handler_IsActive()) || gameover)
763 turret_target = world;
764 return FALSE; // battle hasn't started
767 if(turret_flags & TFL_TARGETSELECT_MISSILESONLY)
768 if(turret_target.flags & FL_PROJECTILE)
769 if(turret_target.owner.flags & FL_MONSTER)
770 return TRUE; // flac support
772 if(turret.turrcaps_flags & TFL_TURRCAPS_SUPPORT && turret_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
774 if not(turret_target.flags & FL_MONSTER)
775 turret_target = world;
777 if(!IsDifferentTeam(turret_target, turret))
778 turret_target = world;
783 MUTATOR_HOOKFUNCTION(td_TurretDies)
786 self.realowner.turret_cnt -= 1;
791 MUTATOR_HOOKFUNCTION(td_GetTeamCount)
798 MUTATOR_DEFINITION(gamemode_towerdefense)
800 MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
801 MUTATOR_HOOK(MonsterSpawn, td_MonsterSpawn, CBC_ORDER_ANY);
802 MUTATOR_HOOK(MonsterDies, td_MonsterDies, CBC_ORDER_ANY);
803 MUTATOR_HOOK(MonsterMove, td_MonsterThink, CBC_ORDER_ANY);
804 MUTATOR_HOOK(MonsterFindTarget, td_MonsterFindTarget, CBC_ORDER_ANY);
805 MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
806 MUTATOR_HOOK(PlayerDamage_Calculate, td_Damage, CBC_ORDER_ANY);
807 MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, CBC_ORDER_ANY);
808 MUTATOR_HOOK(ClientConnect, td_ClientConnect, CBC_ORDER_ANY);
809 MUTATOR_HOOK(VehicleSpawn, td_DisableVehicles, CBC_ORDER_ANY);
810 MUTATOR_HOOK(SetModname, td_SetModname, CBC_ORDER_ANY);
811 MUTATOR_HOOK(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
812 MUTATOR_HOOK(TurretDies, td_TurretDies, CBC_ORDER_ANY);
813 MUTATOR_HOOK(GetTeamCount, td_GetTeamCount, CBC_ORDER_ANY);
817 if(time > 1) // game loads at time 1
818 error("This is a game type and it cannot be added at runtime.");
819 cvar_settemp("g_monsters", "1");
820 cvar_settemp("g_turrets", "1");
824 MUTATOR_ONROLLBACK_OR_REMOVE
826 // we actually cannot roll back td_Initialize here
827 // BUT: we don't need to! If this gets called, adding always
833 error("This is a game type and it cannot be removed at runtime.");