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_setup()
127 self.think = func_null;
129 self.solid = SOLID_BBOX;
130 self.takedamage = DAMAGE_AIM;
131 self.event_damage = td_generator_damage;
132 self.movetype = MOVETYPE_NONE;
133 self.monster_attack = TRUE;
134 self.SendFlags = GSF_SETUP;
135 self.netname = "Generator";
136 self.SendFlags |= GSF_STATUS;
138 WaypointSprite_SpawnFixed(self.netname, self.origin + '0 0 90', self, sprite, RADARICON_OBJECTIVE, Team_ColorRGB(self.team));
139 WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
140 WaypointSprite_UpdateHealth(self.sprite, self.health);
143 void AnnounceSpawn(string anounce)
146 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_TD_ANNOUNCE_SPAWN, anounce);
148 FOR_EACH_REALCLIENT(e) soundto(MSG_ONE, e, CHAN_AUTO, "kh/alarm.wav", VOL_BASE, ATTN_NONE);
151 entity PickSpawn (float tm)
154 RandomSelection_Init();
155 for(e = world;(e = find(e, classname, "td_spawnpoint")); )
157 RandomSelection_Add(e, 0, string_null, 1, 1);
159 return RandomSelection_chosen_ent;
162 void TD_SpawnMonster(float tm, string mnster)
170 td_debug("Warning: couldn't find any td_spawnpoint spawnpoints, no monsters will spawn!\n");
174 mon = spawnmonster(mnster, e, e, e.origin, FALSE, 2);
177 if(random() <= 0.5 && e.target)
178 mon.target2 = e.target;
180 mon.target2 = e.target2;
183 mon.target2 = e.target;
186 string monster_type2string(float mnster)
190 case MONSTER_ZOMBIE: return "zombie";
191 case MONSTER_BRUTE: return "brute";
192 case MONSTER_ANIMUS: return "animus";
193 case MONSTER_SHAMBLER: return "shambler";
194 case MONSTER_BRUISER: return "bruiser";
195 case MONSTER_WYVERN: return "wyvern";
196 case MONSTER_CERBERUS: return "cerberus";
197 case MONSTER_SLIME: return "slime";
198 case MONSTER_KNIGHT: return "knight";
199 case MONSTER_STINGRAY: return "stingray";
200 case MONSTER_MAGE: return "mage";
201 case MONSTER_SPIDER: return "spider";
206 float RandomMonster()
208 RandomSelection_Init();
212 for(i = MONSTER_FIRST + 1; i < MONSTER_LAST; ++i)
214 if(i == MONSTER_STINGRAY || i == MONSTER_WYVERN)
215 continue; // flying/swimming monsters not yet supported
217 RandomSelection_Add(world, i, "", 1, 1);
220 return RandomSelection_chosen_float;
223 void SpawnMonsters(float tm)
227 whichmon = RandomMonster();
229 TD_SpawnMonster(tm, monster_type2string(whichmon));
232 entity PickGenerator(float tm)
236 RandomSelection_Init();
237 for(head = world;(head = findflags(head, flags, FL_GENERATOR)); )
239 RandomSelection_Add(head, 0, string_null, 1, 1);
241 return RandomSelection_chosen_ent;
244 float td_checkfuel(entity ent, string tur)
246 float turcost = cvar(strcat("g_td_turret_", tur, "_cost"));
248 if(ent.ammo_fuel < turcost)
250 Send_Notification(NOTIF_ONE, ent, MSG_MULTI, MULTI_TD_NOFUEL);
254 ent.ammo_fuel -= turcost;
259 void spawnturret(entity spawnedby, entity own, string turet, vector orig)
261 if not(IS_PLAYER(spawnedby)) { dprint("Warning: A non-player entity tried to spawn a turret\n"); return; }
262 if not(td_checkfuel(spawnedby, turet)) { return; }
269 setorigin(self, orig);
270 self.spawnflags = TSL_NO_RESPAWN;
271 self.monster_attack = TRUE;
272 self.realowner = own;
273 self.playerid = own.playerid;
274 self.angles_y = spawnedby.v_angle_y;
275 spawnedby.turret_cnt += 1;
276 self.team = own.team;
280 case "plasma": spawnfunc_turret_plasma(); break;
281 case "mlrs": spawnfunc_turret_mlrs(); break;
282 case "walker": spawnfunc_turret_walker(); break;
283 case "flac": spawnfunc_turret_flac(); break;
284 case "towerbuff": spawnfunc_turret_fusionreactor(); break;
285 default: Send_Notification(NOTIF_ONE, spawnedby, MSG_INFO, INFO_TD_INVALID); remove(self); self = oldself; return;
288 Send_Notification(NOTIF_ONE, spawnedby, MSG_MULTI, MULTI_TD_SPAWN);
293 void spawn_td_fuel(float fuel_size)
295 if not(g_td) {remove(self); return; }
297 self.ammo_fuel = fuel_size * monster_skill;
298 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);
300 self.velocity = randomvec() * 175 + '0 0 325';
304 #define TD_ALIVE_TEAMS() ((redalive > 0) + (bluealive > 0))
305 #define TD_ALIVE_TEAMS_OK() (TD_ALIVE_TEAMS() == 2)
308 allowed_to_spawn = TRUE;
311 void TD_count_alive_monsters()
317 FOR_EACH_MONSTER(head)
319 if(head.health <= 0) continue;
333 float TD_GetWinnerTeam()
335 float winner_team = 0;
337 winner_team = NUM_TEAM_1;
340 if(winner_team) return 0;
341 winner_team = NUM_TEAM_2;
345 return -1; // no monster left
348 float TD_CheckWinner()
350 if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
352 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_OVER);
353 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_OVER);
354 round_handler_Init(5, 1, 180);
358 TD_count_alive_monsters();
360 if(total_alive < max_alive && time >= last_check && total_killed < max_monsters)
362 SpawnMonsters(NUM_TEAM_1);
363 SpawnMonsters(NUM_TEAM_2);
365 last_check = time + 0.5;
369 return 0; // nothing has died, can't be a tie
371 if(TD_ALIVE_TEAMS() > 1)
374 float winner_team = TD_GetWinnerTeam();
377 Send_Notification(NOTIF_ALL, world, MSG_CENTER, APP_TEAM_NUM_4(winner_team, CENTER_ROUND_TEAM_WIN_));
378 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(winner_team, INFO_ROUND_TEAM_WIN_));
379 TeamScore_AddToTeam(winner_team, ST_SCORE, +1);
381 else if(winner_team == -1)
383 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_TIED);
384 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_TIED);
387 round_handler_Init(5, 1, 180);
391 float TD_CheckTeams()
393 allowed_to_spawn = TRUE;
399 void spawnfunc_td_generator()
401 if not(g_td) { remove(self); return; }
404 td_debug("Generator without a team, removing it.\n");
409 precache_sound("onslaught/generator_underattack.wav");
410 precache_sound("onslaught/ons_hit1.wav");
411 precache_sound("onslaught/ons_hit2.wav");
412 precache_sound("weapons/rocket_impact.wav");
417 self.max_health = self.health;
418 self.classname = "td_generator";
419 self.flags = FL_GENERATOR;
421 setsize(self, GENERATOR_MIN, GENERATOR_MAX);
423 setorigin(self, self.origin + '0 0 20');
426 generator_link(td_generator_setup);
429 void spawnfunc_td_waypoint()
431 if not(g_td) { remove(self); return; }
434 td_debug("Tower Defense waypoint without a team, removing it.\n");
439 setsize(self, '-6 -6 -6', '6 6 6');
443 setorigin(self, self.origin + '0 0 20');
447 self.classname = "td_waypoint";
448 self.think = td_waypoint_think;
449 self.nextthink = time + 0.1;
452 void spawnfunc_td_controller()
454 if not(g_td) { remove(self); return; }
457 void spawnfunc_td_spawnpoint()
459 if not(g_td) { remove(self); return; }
461 self.classname = "td_spawnpoint";
463 self.effects = EF_STARDUST;
466 // initialization stuff
469 ScoreRules_basics(2, SFL_SORT_PRIO_SECONDARY, SFL_SORT_PRIO_SECONDARY, TRUE);
470 ScoreInfo_SetLabel_TeamScore(ST_TD_DESTROYS, "destroyed", SFL_SORT_PRIO_PRIMARY);
471 ScoreInfo_SetLabel_PlayerScore(SP_TD_DESTROYS,"destroyed", SFL_SORT_PRIO_PRIMARY);
472 ScoreRules_basics_end();
475 void td_SpawnController()
477 entity oldself = self;
479 self.classname = "td_controller";
480 spawnfunc_td_controller();
484 void td_DelayedInit()
486 if(find(world, classname, "td_controller") == world)
488 print("No ""td_controller"" entity found on this map, creating it anyway.\n");
489 td_SpawnController();
497 InitializeEntity(world, td_DelayedInit, INITPRIO_GAMETYPE);
499 round_handler_Spawn(TD_CheckTeams, TD_CheckWinner, TD_RoundStart);
500 round_handler_Init(5, 10, 180);
504 MUTATOR_HOOKFUNCTION(td_TurretSpawn)
506 if(self.realowner == world)
509 self.bot_attack = FALSE;
514 MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
516 if(!self.team || !self.realowner)
518 td_debug(strcat("Removed monster ", self.netname, " with team ", ftos(self.team), "\n"));
519 WaypointSprite_Kill(self.sprite);
520 if(self.weaponentity) remove(self.weaponentity);
525 self.candrop = FALSE;
526 self.bot_attack = FALSE;
527 self.ammo_fuel = bound(20, 20 * self.level, 100);
528 self.target_range = 300;
529 self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_CORPSE | DPCONTENTS_MONSTERCLIP;
534 MUTATOR_HOOKFUNCTION(td_MonsterDies)
541 backuporigin = self.origin;
546 setorigin(self, backuporigin + '0 0 5');
547 spawn_td_fuel(oldself.ammo_fuel);
548 self.touch = M_Item_Touch;
554 SUB_SetFade(self, time + 5, 1);
561 MUTATOR_HOOKFUNCTION(td_MonsterThink)
563 if(time <= game_starttime && round_handler_IsActive())
566 if(IS_PLAYER(self.enemy))
569 if not(self.enemy) // don't change targets while attacking
570 if(vlen(monster_target.origin - self.origin) <= 100)
572 if(monster_target.target2)
575 self.target2 = monster_target.target2;
577 self.target2 = monster_target.target;
580 self.target2 = monster_target.target;
582 monster_target = find(world, targetname, self.target2);
584 if(monster_target == world)
585 monster_target = PickGenerator(self.team);
587 if(monster_target == world)
588 return TRUE; // no generators or waypoints?!
591 td_debug(sprintf("Monster target: %s. Monster target2: %s. Monster target entity: %s.\n", self.target, self.target2, etos(monster_target)));
593 monster_speed_run = (150 + random() * 4) * monster_skill;
594 monster_speed_walk = (100 + random() * 4) * monster_skill;
599 MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
603 for(e = world;(e = findflags(e, monster_attack, TRUE)); )
606 if(e.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
609 if(e.flags & FL_MONSTER)
610 continue; // don't attack other monsters?
612 if(monster_isvalidtarget(e, self))
619 MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
621 self.monster_attack = FALSE;
622 self.bot_attack = FALSE;
626 self.ammo_fuel = self.newfuel;
633 MUTATOR_HOOKFUNCTION(td_Damage)
635 if(IS_PLAYER(frag_attacker))
636 if(frag_target.flags & FL_MONSTER)
639 if(IS_PLAYER(frag_target))
642 if(frag_attacker != frag_target)
643 frag_force = '0 0 0';
649 MUTATOR_HOOKFUNCTION(td_PlayerCommand)
651 if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
655 makevectors(self.v_angle);
657 org = self.origin + self.view_ofs + v_forward * 100;
659 tracebox(self.origin + self.view_ofs, '-16 -16 -16', '16 16 16', org, MOVE_NORMAL, self);
660 entity targ = trace_ent;
661 if(targ.owner.realowner == self)
664 if(cmd_name == "turretspawn")
666 if(argv(1) == "list")
668 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_LIST, "mlrs walker plasma towerbuff flac");
671 if(!IS_PLAYER(self) || self.health <= 0)
673 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_CANTSPAWN);
678 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_DISABLED);
681 if(self.turret_cnt >= max_turrets)
683 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_MAXTURRETS, max_turrets);
687 spawnturret(self, self, argv(1), trace_endpos);
691 if(cmd_name == "turretremove")
693 if((targ.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && (targ.playerid == self.playerid || targ.realowner == self))
695 self.turret_cnt -= 1;
696 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_REMOVE);
697 WaypointSprite_Kill(targ.sprite);
698 remove(targ.tur_head);
702 Send_Notification(NOTIF_ONE, self, MSG_MULTI, MULTI_TD_AIM_REMOVE);
709 MUTATOR_HOOKFUNCTION(td_ClientConnect)
717 for(t = world; (t = findflags(t, turrcaps_flags, TFL_TURRCAPS_ISTURRET)); )
718 if(t.playerid == self.playerid)
721 self.turret_cnt += 1; // nice try
727 MUTATOR_HOOKFUNCTION(td_DisableVehicles)
732 MUTATOR_HOOKFUNCTION(td_SetModname)
739 MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
741 if(time < game_starttime || (time <= game_starttime && round_handler_IsActive()) || gameover)
743 turret_target = world;
744 return FALSE; // battle hasn't started
747 if(turret_flags & TFL_TARGETSELECT_MISSILESONLY)
748 if(turret_target.flags & FL_PROJECTILE)
749 if(turret_target.owner.flags & FL_MONSTER)
750 return TRUE; // flac support
752 if(turret.turrcaps_flags & TFL_TURRCAPS_SUPPORT && turret_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
754 if not(turret_target.flags & FL_MONSTER)
755 turret_target = world;
757 if(!IsDifferentTeam(turret_target, turret))
758 turret_target = world;
763 MUTATOR_HOOKFUNCTION(td_TurretDies)
766 self.realowner.turret_cnt -= 1;
771 MUTATOR_HOOKFUNCTION(td_GetTeamCount)
778 MUTATOR_DEFINITION(gamemode_towerdefense)
780 MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
781 MUTATOR_HOOK(MonsterSpawn, td_MonsterSpawn, CBC_ORDER_ANY);
782 MUTATOR_HOOK(MonsterDies, td_MonsterDies, CBC_ORDER_ANY);
783 MUTATOR_HOOK(MonsterMove, td_MonsterThink, CBC_ORDER_ANY);
784 MUTATOR_HOOK(MonsterFindTarget, td_MonsterFindTarget, CBC_ORDER_ANY);
785 MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
786 MUTATOR_HOOK(PlayerDamage_Calculate, td_Damage, CBC_ORDER_ANY);
787 MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, CBC_ORDER_ANY);
788 MUTATOR_HOOK(ClientConnect, td_ClientConnect, CBC_ORDER_ANY);
789 MUTATOR_HOOK(VehicleSpawn, td_DisableVehicles, CBC_ORDER_ANY);
790 MUTATOR_HOOK(SetModname, td_SetModname, CBC_ORDER_ANY);
791 MUTATOR_HOOK(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
792 MUTATOR_HOOK(TurretDies, td_TurretDies, CBC_ORDER_ANY);
793 MUTATOR_HOOK(GetTeamCount, td_GetTeamCount, CBC_ORDER_ANY);
797 if(time > 1) // game loads at time 1
798 error("This is a game type and it cannot be added at runtime.");
799 cvar_settemp("g_monsters", "1");
800 cvar_settemp("g_turrets", "1");
804 MUTATOR_ONROLLBACK_OR_REMOVE
806 // we actually cannot roll back td_Initialize here
807 // BUT: we don't need to! If this gets called, adding always
813 error("This is a game type and it cannot be removed at runtime.");