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