]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/mutator/gamemode_invasion.qc
Mutators: combine headers
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / mutator / gamemode_invasion.qc
1 #ifndef GAMEMODE_INVASION_H
2 #define GAMEMODE_INVASION_H
3
4 float inv_numspawned;
5 float inv_maxspawned;
6 float inv_roundcnt;
7 float inv_maxrounds;
8 float inv_numkilled;
9 float inv_lastcheck;
10 float inv_maxcurrent;
11
12 float invasion_teams;
13 float inv_monsters_perteam[17];
14
15 float inv_monsterskill;
16
17 const float ST_INV_KILLS = 1;
18 #endif
19
20 #ifdef IMPLEMENTATION
21
22 #include "../../../common/monsters/spawn.qh"
23 #include "../../../common/monsters/sv_monsters.qh"
24
25 #include "../../teamplay.qh"
26
27 bool g_invasion;
28
29 float autocvar_g_invasion_round_timelimit;
30 int autocvar_g_invasion_teams;
31 bool autocvar_g_invasion_team_spawns;
32 float autocvar_g_invasion_spawnpoint_spawn_delay;
33 #define autocvar_g_invasion_point_limit cvar("g_invasion_point_limit")
34 float autocvar_g_invasion_warmup;
35 int autocvar_g_invasion_monster_count;
36 bool autocvar_g_invasion_zombies_only;
37 float autocvar_g_invasion_spawn_delay;
38
39 spawnfunc(invasion_spawnpoint)
40 {
41         if(!g_invasion) { remove(self); return; }
42
43         self.classname = "invasion_spawnpoint";
44
45         if(autocvar_g_invasion_zombies_only) // precache only if it hasn't been already
46         if(self.monsterid) {
47                 Monster mon = get_monsterinfo(self.monsterid);
48                 mon.mr_precache(mon);
49         }
50 }
51
52 float invasion_PickMonster(float supermonster_count)
53 {
54         if(autocvar_g_invasion_zombies_only)
55                 return MON_ZOMBIE.monsterid;
56
57         float i;
58         entity mon;
59
60         RandomSelection_Init();
61
62         for(i = MON_FIRST; i <= MON_LAST; ++i)
63         {
64                 mon = get_monsterinfo(i);
65                 if((mon.spawnflags & MONSTER_TYPE_FLY) || (mon.spawnflags & MONSTER_TYPE_SWIM) || ((mon.spawnflags & MON_FLAG_SUPERMONSTER) && supermonster_count >= 1))
66                         continue; // flying/swimming monsters not yet supported
67
68                 RandomSelection_Add(world, i, string_null, 1, 1);
69         }
70
71         return RandomSelection_chosen_float;
72 }
73
74 entity invasion_PickSpawn()
75 {
76         entity e;
77
78         RandomSelection_Init();
79
80         for(e = world;(e = find(e, classname, "invasion_spawnpoint")); )
81         {
82                 RandomSelection_Add(e, 0, string_null, 1, ((time >= e.spawnshieldtime) ? 0.2 : 1)); // give recently used spawnpoints a very low rating
83                 e.spawnshieldtime = time + autocvar_g_invasion_spawnpoint_spawn_delay;
84         }
85
86         return RandomSelection_chosen_ent;
87 }
88
89 void invasion_SpawnChosenMonster(float mon)
90 {
91         entity spawn_point, monster;
92
93         spawn_point = invasion_PickSpawn();
94
95         if(spawn_point == world)
96         {
97                 LOG_TRACE("Warning: couldn't find any invasion_spawnpoint spawnpoints, attempting to spawn monsters in random locations\n");
98                 entity e = spawn();
99                 setsize(e, (get_monsterinfo(mon)).mins, (get_monsterinfo(mon)).maxs);
100
101                 if(MoveToRandomMapLocation(e, DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_PLAYERCLIP, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_BODY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 10, 1024, 256))
102                         monster = spawnmonster("", mon, world, world, e.origin, false, false, 2);
103                 else return;
104
105                 e.think = SUB_Remove;
106                 e.nextthink = time + 0.1;
107         }
108         else
109                 monster = spawnmonster("", ((spawn_point.monsterid) ? spawn_point.monsterid : mon), spawn_point, spawn_point, spawn_point.origin, false, false, 2);
110
111         if(spawn_point) monster.target2 = spawn_point.target2;
112         monster.spawnshieldtime = time;
113         if(spawn_point && spawn_point.target_range) monster.target_range = spawn_point.target_range;
114
115         if(teamplay)
116         if(spawn_point && spawn_point.team && inv_monsters_perteam[spawn_point.team] > 0)
117                 monster.team = spawn_point.team;
118         else
119         {
120                 RandomSelection_Init();
121                 if(inv_monsters_perteam[NUM_TEAM_1] > 0) RandomSelection_Add(world, NUM_TEAM_1, string_null, 1, 1);
122                 if(inv_monsters_perteam[NUM_TEAM_2] > 0) RandomSelection_Add(world, NUM_TEAM_2, string_null, 1, 1);
123                 if(invasion_teams >= 3) if(inv_monsters_perteam[NUM_TEAM_3] > 0) { RandomSelection_Add(world, NUM_TEAM_3, string_null, 1, 1); }
124                 if(invasion_teams >= 4) if(inv_monsters_perteam[NUM_TEAM_4] > 0) { RandomSelection_Add(world, NUM_TEAM_4, string_null, 1, 1); }
125
126                 monster.team = RandomSelection_chosen_float;
127         }
128
129         if(teamplay)
130         {
131                 monster_setupcolors(monster);
132
133                 if(monster.sprite)
134                 {
135                         WaypointSprite_UpdateTeamRadar(monster.sprite, RADARICON_DANGER, ((monster.team) ? Team_ColorRGB(monster.team) : '1 0 0'));
136
137                         monster.sprite.team = 0;
138                         monster.sprite.SendFlags |= 1;
139                 }
140         }
141
142         monster.monster_attack = false; // it's the player's job to kill all the monsters
143
144         if(inv_roundcnt >= inv_maxrounds)
145                 monster.spawnflags |= MONSTERFLAG_MINIBOSS; // last round spawns minibosses
146 }
147
148 void invasion_SpawnMonsters(float supermonster_count)
149 {
150         float chosen_monster = invasion_PickMonster(supermonster_count);
151
152         invasion_SpawnChosenMonster(chosen_monster);
153 }
154
155 float Invasion_CheckWinner()
156 {
157         entity head;
158         if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
159         {
160                 FOR_EACH_MONSTER(head)
161                         Monster_Remove(head);
162
163                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_OVER);
164                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_OVER);
165                 round_handler_Init(5, autocvar_g_invasion_warmup, autocvar_g_invasion_round_timelimit);
166                 return 1;
167         }
168
169         float total_alive_monsters = 0, supermonster_count = 0, red_alive = 0, blue_alive = 0, yellow_alive = 0, pink_alive = 0;
170
171         FOR_EACH_MONSTER(head) if(head.health > 0)
172         {
173                 if((get_monsterinfo(head.monsterid)).spawnflags & MON_FLAG_SUPERMONSTER)
174                         ++supermonster_count;
175                 ++total_alive_monsters;
176
177                 if(teamplay)
178                 switch(head.team)
179                 {
180                         case NUM_TEAM_1: ++red_alive; break;
181                         case NUM_TEAM_2: ++blue_alive; break;
182                         case NUM_TEAM_3: ++yellow_alive; break;
183                         case NUM_TEAM_4: ++pink_alive; break;
184                 }
185         }
186
187         if((total_alive_monsters + inv_numkilled) < inv_maxspawned && inv_maxcurrent < inv_maxspawned)
188         {
189                 if(time >= inv_lastcheck)
190                 {
191                         invasion_SpawnMonsters(supermonster_count);
192                         inv_lastcheck = time + autocvar_g_invasion_spawn_delay;
193                 }
194
195                 return 0;
196         }
197
198         if(inv_numspawned < 1)
199                 return 0; // nothing has spawned yet
200
201         if(teamplay)
202         {
203                 if(((red_alive > 0) + (blue_alive > 0) + (yellow_alive > 0) + (pink_alive > 0)) > 1)
204                         return 0;
205         }
206         else if(inv_numkilled < inv_maxspawned)
207                 return 0;
208
209         entity winner = world;
210         float winning_score = 0, winner_team = 0;
211
212
213         if(teamplay)
214         {
215                 if(red_alive > 0) { winner_team = NUM_TEAM_1; }
216                 if(blue_alive > 0)
217                 if(winner_team) { winner_team = 0; }
218                 else { winner_team = NUM_TEAM_2; }
219                 if(yellow_alive > 0)
220                 if(winner_team) { winner_team = 0; }
221                 else { winner_team = NUM_TEAM_3; }
222                 if(pink_alive > 0)
223                 if(winner_team) { winner_team = 0; }
224                 else { winner_team = NUM_TEAM_4; }
225         }
226         else
227         FOR_EACH_PLAYER(head)
228         {
229                 float cs = PlayerScore_Add(head, SP_KILLS, 0);
230                 if(cs > winning_score)
231                 {
232                         winning_score = cs;
233                         winner = head;
234                 }
235         }
236
237         FOR_EACH_MONSTER(head)
238                 Monster_Remove(head);
239
240         if(teamplay)
241         {
242                 if(winner_team)
243                 {
244                         Send_Notification(NOTIF_ALL, world, MSG_CENTER, APP_TEAM_NUM_4(winner_team, CENTER_ROUND_TEAM_WIN_));
245                         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(winner_team, INFO_ROUND_TEAM_WIN_));
246                 }
247         }
248         else if(winner)
249         {
250                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_PLAYER_WIN, winner.netname);
251                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_PLAYER_WIN, winner.netname);
252         }
253
254         round_handler_Init(5, autocvar_g_invasion_warmup, autocvar_g_invasion_round_timelimit);
255
256         return 1;
257 }
258
259 float Invasion_CheckPlayers()
260 {
261         return true;
262 }
263
264 void Invasion_RoundStart()
265 {
266         entity e;
267         float numplayers = 0;
268         FOR_EACH_PLAYER(e)
269         {
270                 e.player_blocked = 0;
271                 ++numplayers;
272         }
273
274         if(inv_roundcnt < inv_maxrounds)
275                 inv_roundcnt += 1; // a limiter to stop crazy counts
276
277         inv_monsterskill = inv_roundcnt + max(1, numplayers * 0.3);
278
279         inv_maxcurrent = 0;
280         inv_numspawned = 0;
281         inv_numkilled = 0;
282
283         inv_maxspawned = rint(max(autocvar_g_invasion_monster_count, autocvar_g_invasion_monster_count * (inv_roundcnt * 0.5)));
284
285         if(teamplay)
286         {
287                 DistributeEvenly_Init(inv_maxspawned, invasion_teams);
288                 inv_monsters_perteam[NUM_TEAM_1] = DistributeEvenly_Get(1);
289                 inv_monsters_perteam[NUM_TEAM_2] = DistributeEvenly_Get(1);
290                 if(invasion_teams >= 3) inv_monsters_perteam[NUM_TEAM_3] = DistributeEvenly_Get(1);
291                 if(invasion_teams >= 4) inv_monsters_perteam[NUM_TEAM_4] = DistributeEvenly_Get(1);
292         }
293 }
294
295 MUTATOR_HOOKFUNCTION(inv, MonsterDies)
296 {SELFPARAM();
297         if(!(self.spawnflags & MONSTERFLAG_RESPAWNED))
298         {
299                 inv_numkilled += 1;
300                 inv_maxcurrent -= 1;
301                 if(teamplay) { inv_monsters_perteam[self.team] -= 1; }
302
303                 if(IS_PLAYER(frag_attacker))
304                 if(SAME_TEAM(frag_attacker, self)) // in non-teamplay modes, same team = same player, so this works
305                         PlayerScore_Add(frag_attacker, SP_KILLS, -1);
306                 else
307                 {
308                         PlayerScore_Add(frag_attacker, SP_KILLS, +1);
309                         if(teamplay)
310                                 TeamScore_AddToTeam(frag_attacker.team, ST_INV_KILLS, +1);
311                 }
312         }
313
314         return false;
315 }
316
317 MUTATOR_HOOKFUNCTION(inv, MonsterSpawn)
318 {SELFPARAM();
319         if(!(self.spawnflags & MONSTERFLAG_SPAWNED))
320                 return true;
321
322         if(!(self.spawnflags & MONSTERFLAG_RESPAWNED))
323         {
324                 inv_numspawned += 1;
325                 inv_maxcurrent += 1;
326         }
327
328         self.monster_skill = inv_monsterskill;
329
330         if((get_monsterinfo(self.monsterid)).spawnflags & MON_FLAG_SUPERMONSTER)
331                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_INVASION_SUPERMONSTER, self.monster_name);
332
333         self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_MONSTERCLIP;
334
335         return false;
336 }
337
338 MUTATOR_HOOKFUNCTION(inv, OnEntityPreSpawn)
339 {SELFPARAM();
340         if(startsWith(self.classname, "monster_"))
341         if(!(self.spawnflags & MONSTERFLAG_SPAWNED))
342                 return true;
343
344         return false;
345 }
346
347 MUTATOR_HOOKFUNCTION(inv, SV_StartFrame)
348 {
349         monsters_total = inv_maxspawned; // TODO: make sure numspawned never exceeds maxspawned
350         monsters_killed = inv_numkilled;
351
352         return false;
353 }
354
355 MUTATOR_HOOKFUNCTION(inv, PlayerRegen)
356 {
357         // no regeneration in invasion
358         return true;
359 }
360
361 MUTATOR_HOOKFUNCTION(inv, PlayerSpawn)
362 {SELFPARAM();
363         self.bot_attack = false;
364         return false;
365 }
366
367 MUTATOR_HOOKFUNCTION(inv, PlayerDamage_Calculate)
368 {
369         if(IS_PLAYER(frag_attacker) && IS_PLAYER(frag_target) && frag_attacker != frag_target)
370         {
371                 frag_damage = 0;
372                 frag_force = '0 0 0';
373         }
374
375         return false;
376 }
377
378 MUTATOR_HOOKFUNCTION(inv, SV_ParseClientCommand)
379 {SELFPARAM();
380         if(MUTATOR_RETURNVALUE) // command was already handled?
381                 return false;
382
383         if(cmd_name == "debuginvasion")
384         {
385                 sprint(self, strcat("inv_maxspawned = ", ftos(inv_maxspawned), "\n"));
386                 sprint(self, strcat("inv_numspawned = ", ftos(inv_numspawned), "\n"));
387                 sprint(self, strcat("inv_numkilled = ", ftos(inv_numkilled), "\n"));
388                 sprint(self, strcat("inv_roundcnt = ", ftos(inv_roundcnt), "\n"));
389                 sprint(self, strcat("monsters_total = ", ftos(monsters_total), "\n"));
390                 sprint(self, strcat("monsters_killed = ", ftos(monsters_killed), "\n"));
391                 sprint(self, strcat("inv_monsterskill = ", ftos(inv_monsterskill), "\n"));
392
393                 return true;
394         }
395
396         return false;
397 }
398
399 MUTATOR_HOOKFUNCTION(inv, BotShouldAttack)
400 {
401         if(!IS_MONSTER(checkentity))
402                 return true;
403
404         return false;
405 }
406
407 MUTATOR_HOOKFUNCTION(inv, SetStartItems)
408 {
409         start_health = 200;
410         start_armorvalue = 200;
411         return false;
412 }
413
414 MUTATOR_HOOKFUNCTION(inv, AccuracyTargetValid)
415 {
416         if(IS_MONSTER(frag_target))
417                 return MUT_ACCADD_INVALID;
418         return MUT_ACCADD_INDIFFERENT;
419 }
420
421 MUTATOR_HOOKFUNCTION(inv, AllowMobSpawning)
422 {
423         // monster spawning disabled during an invasion
424         return true;
425 }
426
427 MUTATOR_HOOKFUNCTION(inv, GetTeamCount, CBC_ORDER_EXCLUSIVE)
428 {
429         ret_float = invasion_teams;
430         return false;
431 }
432
433 MUTATOR_HOOKFUNCTION(inv, AllowMobButcher)
434 {
435         ret_string = "This command does not work during an invasion!";
436         return true;
437 }
438
439 void invasion_ScoreRules(float inv_teams)
440 {
441         if(inv_teams) { CheckAllowedTeams(world); }
442         ScoreRules_basics(inv_teams, 0, 0, false);
443         if(inv_teams) ScoreInfo_SetLabel_TeamScore(ST_INV_KILLS, "frags", SFL_SORT_PRIO_PRIMARY);
444         ScoreInfo_SetLabel_PlayerScore(SP_KILLS, "frags", ((inv_teams) ? SFL_SORT_PRIO_SECONDARY : SFL_SORT_PRIO_PRIMARY));
445         ScoreRules_basics_end();
446 }
447
448 void invasion_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
449 {
450         if(autocvar_g_invasion_teams)
451                 invasion_teams = bound(2, autocvar_g_invasion_teams, 4);
452         else
453                 invasion_teams = 0;
454
455         independent_players = 1; // to disable extra useless scores
456
457         invasion_ScoreRules(invasion_teams);
458
459         independent_players = 0;
460
461         round_handler_Spawn(Invasion_CheckPlayers, Invasion_CheckWinner, Invasion_RoundStart);
462         round_handler_Init(5, autocvar_g_invasion_warmup, autocvar_g_invasion_round_timelimit);
463
464         inv_roundcnt = 0;
465         inv_maxrounds = 15; // 15?
466 }
467
468 void invasion_Initialize()
469 {
470         if(autocvar_g_invasion_zombies_only) {
471                 Monster mon = MON_ZOMBIE;
472                 mon.mr_precache(mon);
473         } else
474         {
475                 float i;
476                 entity mon;
477                 for(i = MON_FIRST; i <= MON_LAST; ++i)
478                 {
479                         mon = get_monsterinfo(i);
480                         if((mon.spawnflags & MONSTER_TYPE_FLY) || (mon.spawnflags & MONSTER_TYPE_SWIM))
481                                 continue; // flying/swimming monsters not yet supported
482
483                         mon.mr_precache(mon);
484                 }
485         }
486
487         InitializeEntity(world, invasion_DelayedInit, INITPRIO_GAMETYPE);
488 }
489
490 REGISTER_MUTATOR(inv, IS_GAMETYPE(INVASION))
491 {
492         SetLimits(autocvar_g_invasion_point_limit, -1, -1, -1);
493         if(autocvar_g_invasion_teams >= 2)
494         {
495                 ActivateTeamplay();
496                 if(autocvar_g_invasion_team_spawns)
497                         have_team_spawns = -1; // request team spawns
498         }
499
500         MUTATOR_ONADD
501         {
502                 if(time > 1) // game loads at time 1
503                         error("This is a game type and it cannot be added at runtime.");
504                 g_invasion = true;
505                 invasion_Initialize();
506
507                 cvar_settemp("g_monsters", "1");
508         }
509
510         MUTATOR_ONROLLBACK_OR_REMOVE
511         {
512                 // we actually cannot roll back invasion_Initialize here
513                 // BUT: we don't need to! If this gets called, adding always
514                 // succeeds.
515         }
516
517         MUTATOR_ONREMOVE
518         {
519                 LOG_INFO("This is a game type and it cannot be removed at runtime.");
520                 return -1;
521         }
522
523         return 0;
524 }
525 #endif