1 float redalive, bluealive, total_alive;
3 var float max_monsters = 20;
4 var float max_alive = 10;
10 var float max_turrets = 10;
14 .float newfuel; // hack to not give players fuel every time they spawn
23 void td_debug(string input)
25 switch(autocvar_g_td_debug)
27 case 1: dprint(input); break;
28 case 2: print(input); break;
32 void td_waypoint_link(float tm, vector from, vector to)
37 WarpZone_TrailParticles(world, particleeffectnum("waypoint_link_red"), from, to);
40 WarpZone_TrailParticles(world, particleeffectnum("waypoint_link_blue"), from, to);
45 void td_waypoint_think()
53 if(time >= self.last_trace)
57 e = find(world, targetname, self.target);
58 e2 = find(world, target, self.targetname);
59 e3 = find(world, targetname, self.target2);
61 if(e.classname == "td_waypoint" || e.flags & FL_GENERATOR)
62 td_waypoint_link(self.team, self.origin, e.origin);
63 if(e2.classname == "td_spawnpoint")
64 td_waypoint_link(self.team, self.origin, e2.origin);
65 if(e3.classname == "td_waypoint" || e3.flags & FL_GENERATOR)
66 td_waypoint_link(self.team, self.origin, e3.origin);
68 self.last_trace = time + 0.5;
71 self.nextthink = time + 0.1;
74 void td_generator_die()
76 if(autocvar_sv_eventlog)
77 GameLogEcho(":gendestroyed");
79 Send_Notification(NOTIF_ALL, world, MSG_MULTI, MULTI_TD_GENDESTROYED);
81 self.solid = SOLID_NOT;
82 self.takedamage = DAMAGE_NO;
83 self.event_damage = func_null;
85 self.reset = func_null; // don't reset this generator
87 WaypointSprite_Kill(self.sprite);
90 void td_generator_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
92 if(IS_PLAYER(attacker) || attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET || attacker.vehicle_flags & VHF_ISVEHICLE || self.takedamage == DAMAGE_NO)
97 if (time > self.pain_finished)
99 self.pain_finished = time + 10;
100 play2team(self.team, "onslaught/generator_underattack.wav");
104 spamsound(self, CH_TRIGGER, "onslaught/ons_hit1.wav", VOL_BASE, ATTN_NORM);
106 spamsound(self, CH_TRIGGER, "onslaught/ons_hit2.wav", VOL_BASE, ATTN_NORM);
109 FOR_EACH_REALPLAYER(head)
110 if(!IsDifferentTeam(head, self))
111 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_TD_GENDAMAGED);
113 self.health -= damage;
115 WaypointSprite_UpdateHealth(self.sprite, self.health);
119 FOR_EACH_PLAYER(head)
120 if(!IsDifferentTeam(head, attacker))
121 PlayerScore_Add(head, SP_TD_DESTROYS, 1);
123 TeamScore_AddToTeam(attacker.team, ST_TD_DESTROYS, 1);
127 self.SendFlags |= GSF_STATUS;
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.netname = "Generator";
140 self.reset = func_null;
142 WaypointSprite_SpawnFixed(self.netname, self.origin + '0 0 90', self, sprite, RADARICON_OBJECTIVE, Team_ColorRGB(self.team));
143 WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
144 WaypointSprite_UpdateHealth(self.sprite, self.health);
147 entity PickSpawn (float tm)
150 RandomSelection_Init();
151 for(e = world;(e = find(e, classname, "td_spawnpoint")); )
153 RandomSelection_Add(e, 0, string_null, 1, 1);
155 return RandomSelection_chosen_ent;
158 void TD_SpawnMonster(float tm, string mnster)
166 td_debug("Warning: couldn't find any td_spawnpoint spawnpoints, no monsters will spawn!\n");
170 mon = spawnmonster(mnster, e, e, e.origin, FALSE, 2);
173 if(random() <= 0.5 && e.target)
174 mon.target2 = e.target;
176 mon.target2 = e.target2;
179 mon.target2 = e.target;
182 string monster_type2string(float mnster)
186 case MONSTER_ZOMBIE: return "zombie";
187 case MONSTER_BRUTE: return "brute";
188 case MONSTER_ANIMUS: return "animus";
189 case MONSTER_SHAMBLER: return "shambler";
190 case MONSTER_BRUISER: return "bruiser";
191 case MONSTER_WYVERN: return "wyvern";
192 case MONSTER_CERBERUS: return "cerberus";
193 case MONSTER_SLIME: return "slime";
194 case MONSTER_KNIGHT: return "knight";
195 case MONSTER_STINGRAY: return "stingray";
196 case MONSTER_MAGE: return "mage";
197 case MONSTER_SPIDER: return "spider";
202 float RandomMonster()
204 RandomSelection_Init();
208 for(i = MONSTER_FIRST + 1; i < MONSTER_LAST; ++i)
210 if(i == MONSTER_STINGRAY || i == MONSTER_WYVERN)
211 continue; // flying/swimming monsters not yet supported
213 RandomSelection_Add(world, i, "", 1, 1);
216 return RandomSelection_chosen_float;
219 void SpawnMonsters(float tm)
223 whichmon = RandomMonster();
225 TD_SpawnMonster(tm, monster_type2string(whichmon));
228 entity PickGenerator(float tm)
232 RandomSelection_Init();
233 for(head = world;(head = findflags(head, flags, FL_GENERATOR)); )
235 RandomSelection_Add(head, 0, string_null, 1, 1);
237 return RandomSelection_chosen_ent;
240 float td_checkfuel(entity ent, string tur)
242 float turcost = cvar(strcat("g_td_turret_", tur, "_cost"));
244 if(ent.ammo_fuel < turcost)
246 Send_Notification(NOTIF_ONE, ent, MSG_MULTI, MULTI_TD_NOFUEL);
250 ent.ammo_fuel -= turcost;
255 void spawnturret(entity spawnedby, entity own, string turet, vector orig)
257 if not(IS_PLAYER(spawnedby)) { td_debug("Warning: A non-player entity tried to spawn a turret\n"); return; }
258 if not(td_checkfuel(spawnedby, turet)) { return; }
265 setorigin(self, orig);
266 self.spawnflags = TSL_NO_RESPAWN;
267 self.monster_attack = TRUE;
268 self.realowner = own;
269 self.playerid = own.playerid;
270 self.angles_y = spawnedby.v_angle_y;
271 spawnedby.turret_cnt += 1;
272 self.team = own.team;
276 case "plasma": spawnfunc_turret_plasma(); break;
277 case "mlrs": spawnfunc_turret_mlrs(); break;
278 case "walker": spawnfunc_turret_walker(); break;
279 case "flac": spawnfunc_turret_flac(); break;
280 case "towerbuff": spawnfunc_turret_fusionreactor(); break;
281 default: Send_Notification(NOTIF_ONE, spawnedby, MSG_INFO, INFO_TD_INVALID); remove(self); self = oldself; return;
284 Send_Notification(NOTIF_ONE, spawnedby, MSG_MULTI, MULTI_TD_SPAWN);
289 void buffturret(entity tur, float buff)
291 float refbuff = bound(0.01, buff * 0.05, 0.1);
293 tur.turret_buff += 1;
294 tur.max_health *= buff;
295 tur.tur_health = tur.max_health;
296 tur.health = tur.max_health;
297 tur.ammo_max *= buff;
298 tur.ammo_recharge *= buff;
299 tur.shot_dmg *= buff;
300 tur.shot_radius *= buff;
301 tur.shot_speed *= buff;
302 tur.shot_spread *= buff;
303 tur.shot_force *= buff;
306 tur.shot_refire += refbuff;
308 tur.shot_refire -= refbuff;
311 void spawn_td_fuel(float fuel_size)
313 if not(g_td) {remove(self); return; }
315 self.ammo_fuel = fuel_size * monster_skill;
316 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);
318 self.velocity = randomvec() * 175 + '0 0 325';
321 void td_generator_delayed()
323 generator_link(td_generator_setup);
325 self.SendFlags = GSF_SETUP;
329 #define TD_ALIVE_TEAMS() ((redalive > 0) + (bluealive > 0))
330 #define TD_ALIVE_TEAMS_OK() (TD_ALIVE_TEAMS() == 2)
333 allowed_to_spawn = TRUE;
335 ignore_turrets = TRUE;
340 void TD_count_alive_monsters()
348 FOR_EACH_MONSTER(head)
350 if(head.health <= 0) continue;
356 case NUM_TEAM_1: ++redalive; break;
357 case NUM_TEAM_2: ++bluealive; break;
362 float TD_GetWinnerTeam()
364 float winner_team = 0;
366 winner_team = NUM_TEAM_1;
369 if(winner_team) return 0;
370 winner_team = NUM_TEAM_2;
374 return -1; // no monster left
377 float TD_CheckWinner()
381 if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
383 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_OVER);
384 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_OVER);
385 round_handler_Init(5, 10, 180);
389 TD_count_alive_monsters();
391 max_perteam = max_monsters * 0.5;
393 if(time >= last_check)
394 if(total_killed < max_monsters)
396 if(redalive < max_perteam)
397 SpawnMonsters(NUM_TEAM_1);
398 if(bluealive < max_perteam)
399 SpawnMonsters(NUM_TEAM_2);
401 last_check = time + 0.5;
404 if(total_killed < max_monsters)
407 if(TD_ALIVE_TEAMS_OK())
410 float winner_team = TD_GetWinnerTeam();
413 Send_Notification(NOTIF_ALL, world, MSG_CENTER, APP_TEAM_NUM_4(winner_team, CENTER_ROUND_TEAM_WIN_));
414 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(winner_team, INFO_ROUND_TEAM_WIN_));
415 TeamScore_AddToTeam(winner_team, ST_SCORE, +1);
417 else if(winner_team == -1)
419 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_TIED);
420 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_TIED);
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);
431 round_handler_Init(5, 10, 180);
435 float TD_CheckTeams()
437 allowed_to_spawn = TRUE;
443 void spawnfunc_td_generator()
445 if not(g_td) { remove(self); return; }
448 td_debug("Generator without a team, removing it.\n");
453 precache_sound("onslaught/generator_underattack.wav");
454 precache_sound("onslaught/ons_hit1.wav");
455 precache_sound("onslaught/ons_hit2.wav");
456 precache_sound("weapons/rocket_impact.wav");
461 self.max_health = self.health;
462 self.classname = "td_generator";
463 self.flags = FL_GENERATOR;
465 setsize(self, GENERATOR_MIN, GENERATOR_MAX);
467 setorigin(self, self.origin + '0 0 20');
470 InitializeEntity(self, td_generator_delayed, INITPRIO_LAST);
473 void spawnfunc_td_waypoint()
475 if not(g_td) { remove(self); return; }
478 td_debug("Tower Defense waypoint without a team, removing it.\n");
483 setsize(self, '-6 -6 -6', '6 6 6');
487 setorigin(self, self.origin + '0 0 20');
491 self.classname = "td_waypoint";
492 self.think = td_waypoint_think;
493 self.nextthink = time + 0.1;
496 void spawnfunc_td_controller()
498 if not(g_td) { remove(self); return; }
501 void spawnfunc_td_spawnpoint()
503 if not(g_td) { remove(self); return; }
505 self.classname = "td_spawnpoint";
507 self.effects = EF_STARDUST;
510 // initialization stuff
513 ScoreRules_basics(2, SFL_SORT_PRIO_SECONDARY, SFL_SORT_PRIO_SECONDARY, TRUE);
514 ScoreInfo_SetLabel_TeamScore(ST_TD_DESTROYS, "destroyed", SFL_SORT_PRIO_PRIMARY);
515 ScoreInfo_SetLabel_PlayerScore(SP_TD_DESTROYS,"destroyed", SFL_SORT_PRIO_PRIMARY);
516 ScoreRules_basics_end();
519 void td_SpawnController()
521 entity oldself = self;
523 self.classname = "td_controller";
524 spawnfunc_td_controller();
528 void td_DelayedInit()
530 if(find(world, classname, "td_controller") == world)
532 td_debug("No ""td_controller"" entity found on this map, creating it anyway.\n");
533 td_SpawnController();
541 InitializeEntity(world, td_DelayedInit, INITPRIO_GAMETYPE);
543 round_handler_Spawn(TD_CheckTeams, TD_CheckWinner, TD_RoundStart);
544 round_handler_Init(5, 10, 180);
548 MUTATOR_HOOKFUNCTION(td_TurretSpawn)
550 if(self.realowner == world)
553 self.bot_attack = FALSE;
554 buffturret(self, 0.7);
559 MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
561 if(!self.team || !self.realowner)
563 td_debug(strcat("Removed monster ", self.netname, " with team ", ftos(self.team), "\n"));
564 WaypointSprite_Kill(self.sprite);
565 if(self.weaponentity) remove(self.weaponentity);
570 self.candrop = FALSE;
571 self.bot_attack = FALSE;
572 self.ammo_fuel = bound(20, 20 * self.level, 100);
573 self.target_range = 300;
574 self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_MONSTERCLIP;
579 MUTATOR_HOOKFUNCTION(td_MonsterDies)
584 if(IS_PLAYER(frag_attacker.realowner))
586 PlayerScore_Add(frag_attacker.realowner, SP_SCORE, 5);
587 PlayerScore_Add(frag_attacker.realowner, SP_KILLS, 1);
592 backuporigin = self.origin;
597 setorigin(self, backuporigin + '0 0 5');
598 spawn_td_fuel(oldself.ammo_fuel);
599 self.touch = M_Item_Touch;
605 SUB_SetFade(self, time + 5, 1);
612 MUTATOR_HOOKFUNCTION(td_MonsterThink)
614 if(time <= game_starttime && round_handler_IsActive())
617 if(IS_PLAYER(self.enemy))
623 if(monster_target.flags & FL_GENERATOR)
624 if(monster_target.health <= 0)
627 if not(self.enemy) // don't change targets while attacking
628 if(vlen(monster_target.origin - self.origin) <= tr)
630 if(monster_target.target2)
633 self.target2 = monster_target.target2;
635 self.target2 = monster_target.target;
638 self.target2 = monster_target.target;
640 monster_target = find(world, targetname, self.target2);
642 if(monster_target == world)
643 monster_target = PickGenerator(self.team);
645 if(monster_target == world)
646 return TRUE; // no generators or waypoints?!
649 td_debug(sprintf("Monster target: %s. Monster target2: %s. Monster target entity: %s.\n", self.target, self.target2, etos(monster_target)));
651 if(!self.enemy && !monster_target)
652 return TRUE; // no enemy or target, must be wandering
654 monster_speed_run = (150 + random() * 4) * monster_skill;
655 monster_speed_walk = (100 + random() * 4) * monster_skill;
660 MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
664 for(e = world;(e = findflags(e, monster_attack, TRUE)); )
667 if(e.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
670 if(e.flags & FL_MONSTER)
671 continue; // don't attack other monsters?
673 if(monster_isvalidtarget(e, self))
680 MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
682 self.monster_attack = FALSE;
683 self.bot_attack = FALSE;
684 self.solid = SOLID_CORPSE;
688 self.ammo_fuel = self.newfuel;
695 MUTATOR_HOOKFUNCTION(td_Damage)
697 if(IS_PLAYER(frag_attacker))
698 if(frag_target.flags & FL_MONSTER)
701 if(IS_PLAYER(frag_target))
704 if(frag_attacker != frag_target)
705 frag_force = '0 0 0';
711 MUTATOR_HOOKFUNCTION(td_PlayerCommand)
713 if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
717 makevectors(self.v_angle);
719 org = self.origin + self.view_ofs + v_forward * 100;
721 tracebox(self.origin + self.view_ofs, '-16 -16 -16', '16 16 16', org, MOVE_NORMAL, self);
722 entity targ = trace_ent;
723 if(targ.owner.realowner == self)
726 if(cmd_name == "turretspawn")
728 if(argv(1) == "list")
730 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_LIST, "mlrs walker plasma towerbuff flac");
733 if(!IS_PLAYER(self) || self.health <= 0)
735 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_CANTSPAWN);
740 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_DISABLED);
743 if(self.turret_cnt >= max_turrets)
745 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXTURRETS, max_turrets);
749 spawnturret(self, self, argv(1), trace_endpos);
753 if(cmd_name == "turretremove")
755 if((targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && (targ.playerid == self.playerid || targ.realowner == self))
757 self.turret_cnt -= 1;
758 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_REMOVE);
759 WaypointSprite_Kill(targ.sprite);
760 remove(targ.tur_head);
764 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_REMOVE);
771 MUTATOR_HOOKFUNCTION(td_ClientConnect)
779 for(t = world; (t = findflags(t, turrcaps_flags, TFL_TURRCAPS_ISTURRET)); )
780 if(t.playerid == self.playerid)
783 self.turret_cnt += 1; // nice try
789 MUTATOR_HOOKFUNCTION(td_DisableVehicles)
794 MUTATOR_HOOKFUNCTION(td_SetModname)
801 MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
803 if(time < game_starttime || (time <= game_starttime && round_handler_IsActive()) || gameover)
805 turret_target = world;
806 return FALSE; // battle hasn't started
809 if(turret_flags & TFL_TARGETSELECT_MISSILESONLY)
810 if(turret_target.flags & FL_PROJECTILE)
811 if(turret_target.owner.flags & FL_MONSTER)
812 return TRUE; // flac support
814 if(turret.turrcaps_flags & TFL_TURRCAPS_SUPPORT && turret_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
816 if not(turret_target.flags & FL_MONSTER)
817 turret_target = world;
819 if(!IsDifferentTeam(turret_target, turret))
820 turret_target = world;
825 MUTATOR_HOOKFUNCTION(td_TurretDies)
828 self.realowner.turret_cnt -= 1;
833 MUTATOR_HOOKFUNCTION(td_GetTeamCount)
840 MUTATOR_DEFINITION(gamemode_towerdefense)
842 MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
843 MUTATOR_HOOK(MonsterSpawn, td_MonsterSpawn, CBC_ORDER_ANY);
844 MUTATOR_HOOK(MonsterDies, td_MonsterDies, CBC_ORDER_ANY);
845 MUTATOR_HOOK(MonsterMove, td_MonsterThink, CBC_ORDER_ANY);
846 MUTATOR_HOOK(MonsterFindTarget, td_MonsterFindTarget, CBC_ORDER_ANY);
847 MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
848 MUTATOR_HOOK(PlayerDamage_Calculate, td_Damage, CBC_ORDER_ANY);
849 MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, CBC_ORDER_ANY);
850 MUTATOR_HOOK(ClientConnect, td_ClientConnect, CBC_ORDER_ANY);
851 MUTATOR_HOOK(VehicleSpawn, td_DisableVehicles, CBC_ORDER_ANY);
852 MUTATOR_HOOK(SetModname, td_SetModname, CBC_ORDER_ANY);
853 MUTATOR_HOOK(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
854 MUTATOR_HOOK(TurretDies, td_TurretDies, CBC_ORDER_ANY);
855 MUTATOR_HOOK(GetTeamCount, td_GetTeamCount, CBC_ORDER_ANY);
859 if(time > 1) // game loads at time 1
860 error("This is a game type and it cannot be added at runtime.");
861 cvar_settemp("g_monsters", "1");
862 cvar_settemp("g_turrets", "1");
866 MUTATOR_ONROLLBACK_OR_REMOVE
868 // we actually cannot roll back td_Initialize here
869 // BUT: we don't need to! If this gets called, adding always
875 error("This is a game type and it cannot be removed at runtime.");