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