]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/invasion/sv_invasion.qc
Merge branch 'master' into Mario/monsters
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / gamemodes / gamemode / invasion / sv_invasion.qc
1 #include "sv_invasion.qh"
2
3 #include <common/mapobjects/triggers.qh>
4 #include <common/monsters/sv_spawn.qh>
5 #include <common/monsters/sv_spawner.qh>
6 #include <common/monsters/sv_monsters.qh>
7 #include <common/mutators/mutator/status_effects/_mod.qh>
8
9 #include <server/bot/api.qh>
10 #include <server/world.qh>
11 #include <server/teamplay.qh>
12
13 float autocvar_g_invasion_round_timelimit;
14 float autocvar_g_invasion_spawnpoint_spawn_delay;
15 float autocvar_g_invasion_warmup;
16 int autocvar_g_invasion_monster_count;
17 bool autocvar_g_invasion_zombies_only;
18 float autocvar_g_invasion_spawn_delay;
19
20 bool victent_present;
21 .bool inv_endreached;
22
23 bool inv_warning_shown; // spammy
24
25 void target_invasion_roundend_use(entity this, entity actor, entity trigger)
26 {
27         if(!IS_PLAYER(actor)) { return; }
28
29         actor.inv_endreached = true;
30
31         int plnum = 0;
32         int realplnum = 0;
33         // let's not count bots
34         FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
35                 ++realplnum;
36                 if(it.inv_endreached)
37                         ++plnum;
38         });
39         if(plnum < ceil(realplnum * min(1, this.count))) // 70% of players
40                 return;
41
42         this.winning = true;
43 }
44
45 spawnfunc(target_invasion_roundend)
46 {
47         if(!g_invasion) { delete(this); return; }
48
49         victent_present = true; // a victory entity is present, we don't need to rely on monster count TODO: merge this with the intrusive list (can check empty)
50
51         if(!this.count) { this.count = 0.7; } // require at least 70% of the players to reach the end before triggering victory
52
53         this.use = target_invasion_roundend_use;
54
55         IL_PUSH(g_invasion_roundends, this);
56 }
57
58 spawnfunc(invasion_wave)
59 {
60         if(!g_invasion) { delete(this); return; }
61
62         IL_PUSH(g_invasion_waves, this);
63 }
64
65 spawnfunc(invasion_spawnpoint)
66 {
67         if(!g_invasion) { delete(this); return; }
68
69         IL_PUSH(g_invasion_spawns, this);
70 }
71
72 void ClearWinners();
73
74 // Invasion stage mode winning condition: If the attackers triggered a round end (by fulfilling all objectives)
75 // they win.
76 int WinningCondition_Invasion()
77 {
78         WinningConditionHelper(NULL); // set worldstatus
79
80         int status = WINNING_NO;
81
82         if(autocvar_g_invasion_type == INV_TYPE_STAGE)
83         {
84                 SetWinners(inv_endreached, true);
85
86                 int found = 0;
87                 IL_EACH(g_invasion_roundends, true,
88                 {
89                         ++found;
90                         if(it.winning)
91                         {
92                                 bprint("Invasion: round completed.\n");
93                                 // winners already set
94
95                                 status = WINNING_YES;
96                                 break;
97                         }
98                 });
99
100                 if(!found)
101                         status = WINNING_YES; // just end it? TODO: should warn mapper!
102         }
103         else if(autocvar_g_invasion_type == INV_TYPE_HUNT)
104         {
105                 ClearWinners();
106
107                 int found = 0; // NOTE: this ends the round if no monsters are placed
108                 IL_EACH(g_monsters, !(it.spawnflags & MONSTERFLAG_RESPAWNED),
109                 {
110                         ++found;
111                 });
112
113                 if(found <= 0)
114                 {
115                         FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
116                         {
117                                 it.winning = true;
118                         });
119                         status = WINNING_YES;
120                 }
121         }
122
123         return status;
124 }
125
126 Monster invasion_PickMonster(int supermonster_count)
127 {
128         RandomSelection_Init();
129
130         FOREACH(Monsters, it != MON_Null,
131         {
132                 if((it.spawnflags & MON_FLAG_HIDDEN) || (it.spawnflags & MONSTER_TYPE_PASSIVE) || (it.spawnflags & MONSTER_TYPE_FLY) || (it.spawnflags & MONSTER_TYPE_SWIM)
133                         || (it.spawnflags & MONSTER_SIZE_QUAKE) || ((it.spawnflags & MON_FLAG_SUPERMONSTER) && supermonster_count >= 1))
134                         continue;
135                 if(autocvar_g_invasion_zombies_only && !(it.spawnflags & MONSTER_TYPE_UNDEAD))
136                         continue;
137         RandomSelection_AddEnt(it, 1, 1);
138         });
139
140         return RandomSelection_chosen_ent;
141 }
142
143 entity invasion_PickSpawn()
144 {
145         RandomSelection_Init();
146
147         IL_EACH(g_invasion_spawns, true,
148         {
149                 RandomSelection_AddEnt(it, 1, ((time < it.spawnshieldtime) ? 0.2 : 1)); // give recently used spawnpoints a very low rating
150                 it.spawnshieldtime = time + autocvar_g_invasion_spawnpoint_spawn_delay;
151         });
152
153         return RandomSelection_chosen_ent;
154 }
155
156 entity invasion_GetWaveEntity(int wavenum)
157 {
158         IL_EACH(g_invasion_waves, it.cnt == wavenum,
159         {
160                 return it; // found one
161         });
162
163         // if no specific one is found, find the last existing wave ent
164         entity best = NULL;
165         IL_EACH(g_invasion_waves, it.cnt <= wavenum,
166         {
167                 if(!best || it.cnt > best.cnt)
168                         best = it;
169         });
170
171         return best;
172 }
173
174 void invasion_SpawnChosenMonster(Monster mon)
175 {
176         entity monster;
177         entity spawn_point = invasion_PickSpawn();
178         entity wave_ent = invasion_GetWaveEntity(inv_roundcnt);
179
180         string tospawn = "";
181         if(wave_ent && wave_ent.spawnmob && wave_ent.spawnmob != "")
182         {
183                 RandomSelection_Init();
184                 FOREACH_WORD(wave_ent.spawnmob, true,
185                 {
186                         RandomSelection_AddString(it, 1, 1);
187                 });
188
189                 tospawn = RandomSelection_chosen_string;
190         }
191
192         if(spawn_point == NULL)
193         {
194                 if(!inv_warning_shown)
195                 {
196                         inv_warning_shown = true;
197                         LOG_TRACE("Warning: couldn't find any invasion_spawnpoint spawnpoints, attempting to spawn monsters in random locations");
198                 }
199                 entity e = spawn();
200                 setsize(e, mon.m_mins, mon.m_maxs);
201
202                 if(MoveToRandomMapLocation(e, DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_PLAYERCLIP, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_BODY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 10, 1024, 256))
203                 {
204                         monster = spawnmonster(e, tospawn, mon, NULL, NULL, e.origin, false, false, 2);
205                         monster.angles_x = monster.angles_z = 0;
206                 }
207                 else
208                 {
209                         delete(e);
210                         return;
211                 }
212         }
213         else // if spawnmob field falls through (unset), fallback to mon (relying on spawnmonster for that behaviour)
214                 monster = spawnmonster(spawn(), ((spawn_point.spawnmob && spawn_point.spawnmob != "") ? spawn_point.spawnmob : tospawn), mon, spawn_point, spawn_point, spawn_point.origin, false, false, 2);
215
216         if(!monster)
217                 return;
218
219         StatusEffects_remove(STATUSEFFECT_SpawnShield, monster, STATUSEFFECT_REMOVE_NORMAL);
220
221         if(spawn_point)
222         {
223                 if(spawn_point.target_range)
224                         monster.target_range = spawn_point.target_range;
225                 monster.target2 = spawn_point.target2;
226         }
227
228         if(monster.monster_attack)
229                 IL_REMOVE(g_monster_targets, monster);
230         monster.monster_attack = false; // it's the player's job to kill all the monsters
231
232         if(inv_roundcnt >= inv_maxrounds)
233                 monster.spawnflags |= MONSTERFLAG_MINIBOSS; // last round spawns minibosses
234 }
235
236 void invasion_SpawnMonsters(int supermonster_count)
237 {
238         Monster chosen_monster = invasion_PickMonster(supermonster_count);
239
240         invasion_SpawnChosenMonster(chosen_monster);
241 }
242
243 bool Invasion_CheckWinner()
244 {
245         if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
246         {
247                 IL_EACH(g_monsters, true,
248                 {
249                         Monster_Remove(it);
250                 });
251                 IL_CLEAR(g_monsters);
252
253                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_OVER);
254                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_OVER);
255                 round_handler_Init(5, autocvar_g_invasion_warmup, autocvar_g_invasion_round_timelimit);
256                 return 1;
257         }
258
259         float total_alive_monsters = 0, supermonster_count = 0;
260
261         IL_EACH(g_monsters, GetResource(it, RES_HEALTH) > 0,
262         {
263                 if(it.monsterdef.spawnflags & MON_FLAG_SUPERMONSTER)
264                         ++supermonster_count;
265                 ++total_alive_monsters;
266         });
267
268         if((total_alive_monsters + inv_numkilled) < inv_maxspawned && inv_maxcurrent < inv_maxspawned)
269         {
270                 if(time >= inv_lastcheck)
271                 {
272                         invasion_SpawnMonsters(supermonster_count);
273                         inv_lastcheck = time + autocvar_g_invasion_spawn_delay;
274                 }
275
276                 return 0;
277         }
278
279         if(inv_numspawned < 1)
280                 return 0; // nothing has spawned yet
281
282         if(inv_numkilled < inv_maxspawned)
283                 return 0;
284
285         entity winner = NULL;
286         float winning_score = 0;
287
288         FOREACH_CLIENT(IS_PLAYER(it), {
289                 float cs = GameRules_scoring_add(it, KILLS, 0);
290                 if(cs > winning_score)
291                 {
292                         winning_score = cs;
293                         winner = it;
294                 }
295         });
296
297         IL_EACH(g_monsters, true,
298         {
299                 Monster_Remove(it);
300         });
301         IL_CLEAR(g_monsters);
302
303         if(winner)
304         {
305                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_PLAYER_WIN, winner.netname);
306                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_PLAYER_WIN, winner.netname);
307         }
308
309         round_handler_Init(5, autocvar_g_invasion_warmup, autocvar_g_invasion_round_timelimit);
310
311         return 1;
312 }
313
314 bool Invasion_CheckPlayers()
315 {
316         return true;
317 }
318
319 void Invasion_RoundStart()
320 {
321         int numplayers = 0;
322         FOREACH_CLIENT(IS_PLAYER(it), {
323                 it.player_blocked = false;
324                 ++numplayers;
325         });
326
327         if(inv_roundcnt < inv_maxrounds)
328                 inv_roundcnt += 1; // a limiter to stop crazy counts
329
330         inv_monsterskill = inv_roundcnt + max(1, numplayers * 0.3);
331
332         inv_maxcurrent = 0;
333         inv_numspawned = 0;
334         inv_numkilled = 0;
335
336         inv_maxspawned = rint(max(autocvar_g_invasion_monster_count, autocvar_g_invasion_monster_count * (inv_roundcnt * 0.5)));
337 }
338
339 MUTATOR_HOOKFUNCTION(inv, MonsterDies)
340 {
341         entity frag_target = M_ARGV(0, entity);
342         entity frag_attacker = M_ARGV(1, entity);
343
344         if(!(frag_target.spawnflags & MONSTERFLAG_RESPAWNED))
345         {
346                 if(autocvar_g_invasion_type == INV_TYPE_ROUND)
347                 {
348                         inv_numkilled += 1;
349                         inv_maxcurrent -= 1;
350                 }
351
352                 if(IS_PLAYER(frag_attacker))
353                 {
354                         if(SAME_TEAM(frag_attacker, frag_target))
355                                 GameRules_scoring_add(frag_attacker, KILLS, -1);
356                         else
357                                 GameRules_scoring_add(frag_attacker, KILLS, +1);
358                 }
359         }
360 }
361
362 MUTATOR_HOOKFUNCTION(inv, MonsterSpawn)
363 {
364         entity mon = M_ARGV(0, entity);
365         mon.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_MONSTERCLIP;
366
367         if(autocvar_g_invasion_type == INV_TYPE_HUNT)
368                 return false; // allowed
369
370         if(!(mon.spawnflags & MONSTERFLAG_SPAWNED))
371                 return true;
372
373         if(!(mon.spawnflags & MONSTERFLAG_RESPAWNED))
374         {
375                 inv_numspawned += 1;
376                 inv_maxcurrent += 1;
377         }
378
379         mon.monster_skill = inv_monsterskill;
380
381         if(mon.monsterdef.spawnflags & MON_FLAG_SUPERMONSTER)
382                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_INVASION_SUPERMONSTER, mon.monster_name);
383 }
384
385 MUTATOR_HOOKFUNCTION(inv, SV_StartFrame)
386 {
387         if(autocvar_g_invasion_type != INV_TYPE_ROUND)
388                 return; // uses map spawned monsters
389
390         monsters_total = inv_maxspawned; // TODO: make sure numspawned never exceeds maxspawned
391         monsters_killed = inv_numkilled;
392 }
393
394 MUTATOR_HOOKFUNCTION(inv, PlayerRegen)
395 {
396         // no regeneration in invasion, regardless of the game type
397         return true;
398 }
399
400 MUTATOR_HOOKFUNCTION(inv, PlayerSpawn)
401 {
402         entity player = M_ARGV(0, entity);
403
404         if(player.bot_attack)
405                 IL_REMOVE(g_bot_targets, player);
406         player.bot_attack = false;
407 }
408
409 MUTATOR_HOOKFUNCTION(inv, Damage_Calculate)
410 {
411         entity frag_attacker = M_ARGV(1, entity);
412         entity frag_target = M_ARGV(2, entity);
413         float frag_damage = M_ARGV(4, float);
414         vector frag_force = M_ARGV(6, vector);
415
416         if(IS_PLAYER(frag_attacker) && IS_PLAYER(frag_target) && frag_attacker != frag_target)
417         {
418                 frag_damage = 0;
419                 frag_force = '0 0 0';
420
421                 M_ARGV(4, float) = frag_damage;
422                 M_ARGV(6, vector) = frag_force;
423         }
424 }
425
426 MUTATOR_HOOKFUNCTION(inv, BotShouldAttack)
427 {
428         entity targ = M_ARGV(1, entity);
429
430         if(!IS_MONSTER(targ))
431                 return true;
432 }
433
434 MUTATOR_HOOKFUNCTION(inv, SetStartItems)
435 {
436         if(autocvar_g_invasion_type == INV_TYPE_ROUND)
437         {
438                 start_health = 200;
439                 start_armorvalue = 200;
440         }
441 }
442
443 MUTATOR_HOOKFUNCTION(inv, AccuracyTargetValid)
444 {
445         entity frag_target = M_ARGV(1, entity);
446
447         if(IS_MONSTER(frag_target))
448                 return MUT_ACCADD_INVALID;
449         return MUT_ACCADD_INDIFFERENT;
450 }
451
452 MUTATOR_HOOKFUNCTION(inv, AllowMobSpawning)
453 {
454         // monster spawning disabled during an invasion
455         M_ARGV(1, string) = "You cannot spawn monsters during an invasion!";
456         return true;
457 }
458
459 MUTATOR_HOOKFUNCTION(inv, CheckRules_World)
460 {
461         if(autocvar_g_invasion_type == INV_TYPE_ROUND)
462                 return false;
463
464         M_ARGV(0, float) = WinningCondition_Invasion();
465         return true;
466 }
467
468 MUTATOR_HOOKFUNCTION(inv, AllowMobButcher)
469 {
470         M_ARGV(0, string) = "This command does not work during an invasion!";
471         return true;
472 }
473
474 void invasion_ScoreRules()
475 {
476         GameRules_score_enabled(false);
477         GameRules_scoring(0, 0, 0, {
478             field(SP_KILLS, "frags", SFL_SORT_PRIO_PRIMARY);
479         });
480 }
481
482 void invasion_DelayedInit(entity this)
483 {
484         if(autocvar_g_invasion_type == INV_TYPE_HUNT || autocvar_g_invasion_type == INV_TYPE_STAGE)
485                 cvar_set("fraglimit", "0");
486
487         independent_players = 1; // to disable extra useless scores
488
489         invasion_ScoreRules();
490
491         independent_players = 0;
492
493         if(autocvar_g_invasion_type == INV_TYPE_ROUND)
494         {
495                 round_handler_Spawn(Invasion_CheckPlayers, Invasion_CheckWinner, Invasion_RoundStart);
496                 round_handler_Init(5, autocvar_g_invasion_warmup, autocvar_g_invasion_round_timelimit);
497
498                 inv_roundcnt = 0;
499                 inv_maxrounds = 15; // 15?
500         }
501 }
502
503 void invasion_Initialize()
504 {
505         InitializeEntity(NULL, invasion_DelayedInit, INITPRIO_GAMETYPE);
506 }