]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/assault/sv_assault.qc
Merge branch 'master' into bones_was_here/q3compat
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / gamemodes / gamemode / assault / sv_assault.qc
1 #include "sv_assault.qh"
2
3 #include <common/mapobjects/func/breakable.qh>
4 #include <server/spawnpoints.qh>
5
6 .entity sprite;
7 #define AS_ROUND_DELAY 5
8
9 IntrusiveList g_assault_destructibles;
10 IntrusiveList g_assault_objectivedecreasers;
11 IntrusiveList g_assault_objectives;
12 STATIC_INIT(g_assault)
13 {
14         g_assault_destructibles = IL_NEW();
15         g_assault_objectivedecreasers = IL_NEW();
16         g_assault_objectives = IL_NEW();
17 }
18
19 // random functions
20 void assault_objective_use(entity this, entity actor, entity trigger)
21 {
22         // activate objective
23         SetResourceExplicit(this, RES_HEALTH, 100);
24         //print("^2Activated objective ", this.targetname, "=", etos(this), "\n");
25         //print("Activator is ", actor.classname, "\n");
26
27         IL_EACH(g_assault_objectivedecreasers, it.target == this.targetname,
28         {
29                 target_objective_decrease_activate(it);
30         });
31 }
32
33 vector target_objective_spawn_evalfunc(entity this, entity player, entity spot, vector current)
34 {
35         float hlth = GetResource(this, RES_HEALTH);
36         if (hlth < 0 || hlth >= ASSAULT_VALUE_INACTIVE)
37                 return '-1 0 0';
38         return current;
39 }
40
41 // reset this objective. Used when spawning an objective
42 // and when a new round starts
43 void assault_objective_reset(entity this)
44 {
45         SetResourceExplicit(this, RES_HEALTH, ASSAULT_VALUE_INACTIVE);
46 }
47
48 // decrease the health of targeted objectives
49 void assault_objective_decrease_use(entity this, entity actor, entity trigger)
50 {
51         if(actor.team != assault_attacker_team)
52         {
53                 // wrong team triggered decrease
54                 return;
55         }
56
57         if(trigger.assault_sprite)
58         {
59                 WaypointSprite_Disown(trigger.assault_sprite, waypointsprite_deadlifetime);
60                 if(trigger.classname == "func_assault_destructible")
61                         trigger.sprite = NULL; // TODO: just unsetting it?!
62         }
63         else
64                 return; // already activated! cannot activate again!
65
66         float hlth = GetResource(this.enemy, RES_HEALTH);
67         if (hlth < ASSAULT_VALUE_INACTIVE)
68         {
69                 if (hlth - this.dmg > 0.5)
70                 {
71                         GameRules_scoring_add_team(actor, SCORE, this.dmg);
72                         TakeResource(this.enemy, RES_HEALTH, this.dmg);
73                 }
74                 else
75                 {
76                         GameRules_scoring_add_team(actor, SCORE, hlth);
77                         GameRules_scoring_add_team(actor, ASSAULT_OBJECTIVES, 1);
78                         SetResourceExplicit(this.enemy, RES_HEALTH, -1);
79
80                         if(this.enemy.message)
81                                 FOREACH_CLIENT(IS_PLAYER(it), { centerprint(it, this.enemy.message); });
82
83                         SUB_UseTargets(this.enemy, this, trigger);
84                 }
85         }
86 }
87
88 void assault_setenemytoobjective(entity this)
89 {
90         IL_EACH(g_assault_objectives, it.targetname == this.target,
91         {
92                 if(this.enemy == NULL)
93                         this.enemy = it;
94                 else
95                         objerror(this, "more than one objective as target - fix the map!");
96                 break;
97         });
98
99         if(this.enemy == NULL)
100                 objerror(this, "no objective as target - fix the map!");
101 }
102
103 bool assault_decreaser_sprite_visible(entity this, entity player, entity view)
104 {
105         if(GetResource(this.assault_decreaser.enemy, RES_HEALTH) >= ASSAULT_VALUE_INACTIVE)
106                 return false;
107
108         return true;
109 }
110
111 void target_objective_decrease_activate(entity this)
112 {
113         entity spr;
114         this.owner = NULL;
115         FOREACH_ENTITY_STRING(target, this.targetname,
116         {
117                 if(it.assault_sprite != NULL)
118                 {
119                         WaypointSprite_Disown(it.assault_sprite, waypointsprite_deadlifetime);
120                         if(it.classname == "func_assault_destructible")
121                                 it.sprite = NULL; // TODO: just unsetting it?!
122                 }
123
124                 spr = WaypointSprite_SpawnFixed(WP_AssaultDefend, 0.5 * (it.absmin + it.absmax), it, assault_sprite, RADARICON_OBJECTIVE);
125                 spr.assault_decreaser = this;
126                 spr.waypointsprite_visible_for_player = assault_decreaser_sprite_visible;
127                 spr.classname = "sprite_waypoint";
128                 WaypointSprite_UpdateRule(spr, assault_attacker_team, SPRITERULE_TEAMPLAY);
129                 if(it.classname == "func_assault_destructible")
130                 {
131                         WaypointSprite_UpdateSprites(spr, WP_AssaultDefend, WP_AssaultDestroy, WP_AssaultDestroy);
132                         WaypointSprite_UpdateMaxHealth(spr, it.max_health);
133                         WaypointSprite_UpdateHealth(spr, GetResource(it, RES_HEALTH));
134                         it.sprite = spr;
135                 }
136                 else
137                         WaypointSprite_UpdateSprites(spr, WP_AssaultDefend, WP_AssaultPush, WP_AssaultPush);
138         });
139 }
140
141 void target_objective_decrease_findtarget(entity this)
142 {
143         assault_setenemytoobjective(this);
144 }
145
146 void target_assault_roundend_reset(entity this)
147 {
148         //print("round end reset\n");
149         ++this.cnt; // up round counter
150         this.winning = false; // up round
151 }
152
153 void target_assault_roundend_use(entity this, entity actor, entity trigger)
154 {
155         this.winning = 1; // round has been won by attackers
156 }
157
158 void assault_roundstart_use(entity this, entity actor, entity trigger)
159 {
160         SUB_UseTargets(this, this, trigger);
161
162         //(Re)spawn all turrets
163         IL_EACH(g_turrets, true,
164         {
165                 // Swap turret teams
166                 if(it.team == NUM_TEAM_1)
167                         it.team = NUM_TEAM_2;
168                 else
169                         it.team = NUM_TEAM_1;
170
171                 // Doubles as teamchange
172                 turret_respawn(it);
173         });
174 }
175 void assault_roundstart_use_this(entity this)
176 {
177         assault_roundstart_use(this, NULL, NULL);
178 }
179
180 void assault_wall_think(entity this)
181 {
182         if(GetResource(this.enemy, RES_HEALTH) < 0)
183         {
184                 this.model = "";
185                 this.solid = SOLID_NOT;
186         }
187         else
188         {
189                 this.model = this.mdl;
190                 this.solid = SOLID_BSP;
191         }
192
193         this.nextthink = time + 0.2;
194 }
195
196 // trigger new round
197 // reset objectives, toggle spawnpoints, reset triggers, ...
198 void assault_new_round(entity this)
199 {
200         //bprint("ASSAULT: new round\n");
201
202         // up round counter
203         this.winning = this.winning + 1;
204
205         // swap attacker/defender roles
206         if(assault_attacker_team == NUM_TEAM_1)
207                 assault_attacker_team = NUM_TEAM_2;
208         else
209                 assault_attacker_team = NUM_TEAM_1;
210
211         IL_EACH(g_saved_team, !IS_CLIENT(it),
212         {
213                 if(it.team_saved == NUM_TEAM_1)
214                         it.team_saved = NUM_TEAM_2;
215                 else if(it.team_saved == NUM_TEAM_2)
216                         it.team_saved = NUM_TEAM_1;
217         });
218
219         // reset the level with a countdown
220         cvar_set("timelimit", ftos(ceil(time - AS_ROUND_DELAY - game_starttime) / 60));
221         ReadyRestart_force(); // sets game_starttime
222 }
223
224 entity as_round;
225 .entity ent_winning;
226 void as_round_think()
227 {
228         game_stopped = false;
229         assault_new_round(as_round.ent_winning);
230         delete(as_round);
231         as_round = NULL;
232 }
233
234 // Assault winning condition: If the attackers triggered a round end (by fulfilling all objectives)
235 // they win. Otherwise the defending team wins once the timelimit passes.
236 int WinningCondition_Assault()
237 {
238         if(as_round)
239                 return WINNING_NO;
240
241         WinningConditionHelper(NULL); // set worldstatus
242
243         int status = WINNING_NO;
244         // as the timelimit has not yet passed just assume the defending team will win
245         if(assault_attacker_team == NUM_TEAM_1)
246         {
247                 SetWinners(team, NUM_TEAM_2);
248         }
249         else
250         {
251                 SetWinners(team, NUM_TEAM_1);
252         }
253
254         entity ent;
255         ent = find(NULL, classname, "target_assault_roundend");
256         if(ent)
257         {
258                 if(ent.winning) // round end has been triggered by attacking team
259                 {
260                         bprint("Assault: round completed.\n");
261                         SetWinners(team, assault_attacker_team);
262
263                         TeamScore_AddToTeam(assault_attacker_team, ST_ASSAULT_OBJECTIVES, 666 - TeamScore_AddToTeam(assault_attacker_team, ST_ASSAULT_OBJECTIVES, 0));
264
265                         if(ent.cnt == 1 || autocvar_g_campaign) // this was the second round
266                         {
267                                 status = WINNING_YES;
268                         }
269                         else
270                         {
271                                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ASSAULT_OBJ_DESTROYED, ceil(time - game_starttime));
272                                 as_round = new(as_round);
273                                 as_round.think = as_round_think;
274                                 as_round.ent_winning = ent;
275                                 as_round.nextthink = time + AS_ROUND_DELAY;
276                                 game_stopped = true;
277
278                                 // make sure timelimit isn't hit while the game is blocked
279                                 if(autocvar_timelimit > 0)
280                                 if(time + AS_ROUND_DELAY >= game_starttime + autocvar_timelimit * 60)
281                                         cvar_set("timelimit", ftos(autocvar_timelimit + AS_ROUND_DELAY / 60));
282                         }
283                 }
284         }
285
286         return status;
287 }
288
289 // spawnfuncs
290 spawnfunc(info_player_attacker)
291 {
292         if (!g_assault) { delete(this); return; }
293
294         this.team = NUM_TEAM_1; // red, gets swapped every round
295         spawnfunc_info_player_deathmatch(this);
296 }
297
298 spawnfunc(info_player_defender)
299 {
300         if (!g_assault) { delete(this); return; }
301
302         this.team = NUM_TEAM_2; // blue, gets swapped every round
303         spawnfunc_info_player_deathmatch(this);
304 }
305
306 spawnfunc(target_objective)
307 {
308         if (!g_assault) { delete(this); return; }
309
310         this.classname = "target_objective";
311         IL_PUSH(g_assault_objectives, this);
312         this.use = assault_objective_use;
313         this.reset = assault_objective_reset;
314         this.reset(this);
315         this.spawn_evalfunc = target_objective_spawn_evalfunc;
316 }
317
318 spawnfunc(target_objective_decrease)
319 {
320         if (!g_assault) { delete(this); return; }
321
322         this.classname = "target_objective_decrease";
323         IL_PUSH(g_assault_objectivedecreasers, this);
324
325         if(!this.dmg)
326                 this.dmg = 101;
327
328         this.use = assault_objective_decrease_use;
329         SetResourceExplicit(this, RES_HEALTH, ASSAULT_VALUE_INACTIVE);
330         this.max_health = ASSAULT_VALUE_INACTIVE;
331         this.enemy = NULL;
332
333         InitializeEntity(this, target_objective_decrease_findtarget, INITPRIO_FINDTARGET);
334 }
335
336 // destructible walls that can be used to trigger target_objective_decrease
337 bool destructible_heal(entity targ, entity inflictor, float amount, float limit)
338 {
339         float true_limit = ((limit != RES_LIMIT_NONE) ? limit : targ.max_health);
340         float hlth = GetResource(targ, RES_HEALTH);
341         if (hlth <= 0 || hlth >= true_limit)
342                 return false;
343
344         GiveResourceWithLimit(targ, RES_HEALTH, amount, true_limit);
345         if(targ.sprite)
346         {
347                 WaypointSprite_UpdateHealth(targ.sprite, GetResource(targ, RES_HEALTH));
348         }
349         func_breakable_colormod(targ);
350         return true;
351 }
352
353 spawnfunc(func_assault_destructible)
354 {
355         if (!g_assault) { delete(this); return; }
356
357         this.spawnflags = 3;
358         this.classname = "func_assault_destructible";
359         this.event_heal = destructible_heal;
360         IL_PUSH(g_assault_destructibles, this);
361
362         if(assault_attacker_team == NUM_TEAM_1)
363                 this.team = NUM_TEAM_2;
364         else
365                 this.team = NUM_TEAM_1;
366
367         spawnfunc_func_breakable(this);
368 }
369
370 spawnfunc(func_assault_wall)
371 {
372         if (!g_assault) { delete(this); return; }
373
374         this.classname = "func_assault_wall";
375         this.mdl = this.model;
376         _setmodel(this, this.mdl);
377         this.solid = SOLID_BSP;
378         setthink(this, assault_wall_think);
379         this.nextthink = time;
380         InitializeEntity(this, assault_setenemytoobjective, INITPRIO_FINDTARGET);
381 }
382
383 spawnfunc(target_assault_roundend)
384 {
385         if (!g_assault) { delete(this); return; }
386
387         this.winning = 0; // round not yet won by attackers
388         this.classname = "target_assault_roundend";
389         this.use = target_assault_roundend_use;
390         this.cnt = 0; // first round
391         this.reset = target_assault_roundend_reset;
392 }
393
394 spawnfunc(target_assault_roundstart)
395 {
396         if (!g_assault) { delete(this); return; }
397
398         assault_attacker_team = NUM_TEAM_1;
399         this.classname = "target_assault_roundstart";
400         this.use = assault_roundstart_use;
401         this.reset2 = assault_roundstart_use_this;
402         InitializeEntity(this, assault_roundstart_use_this, INITPRIO_FINDTARGET);
403 }
404
405 // legacy bot code
406 void havocbot_goalrating_ast_targets(entity this, float ratingscale)
407 {
408         IL_EACH(g_assault_destructibles, it.bot_attack,
409         {
410                 if (it.target == "")
411                         continue;
412
413                 bool found = false;
414                 entity destr = it;
415                 IL_EACH(g_assault_objectivedecreasers, it.targetname == destr.target,
416                 {
417                         float hlth = GetResource(it.enemy, RES_HEALTH);
418                         if (hlth > 0 && hlth < ASSAULT_VALUE_INACTIVE)
419                         {
420                                 found = true;
421                                 break;
422                         }
423                 });
424
425                 if(!found)
426                         continue;
427
428                 vector p = 0.5 * (it.absmin + it.absmax);
429
430                 // Find and rate waypoints around it
431                 found = false;
432                 entity best = NULL;
433                 float bestvalue = FLOAT_MAX;
434                 entity des = it;
435                 for (float radius = 500; radius <= 1500 && !found; radius += 500)
436                 {
437                         FOREACH_ENTITY_RADIUS(p, radius, it.classname == "waypoint" && !(it.wpflags & WAYPOINTFLAG_GENERATED),
438                         {
439                                 if(checkpvs(it.origin, des))
440                                 {
441                                         found = true;
442                                         if(it.cnt < bestvalue)
443                                         {
444                                                 best = it;
445                                                 bestvalue = it.cnt;
446                                         }
447                                 }
448                         });
449                 }
450
451                 if(best)
452                 {
453                 ///     dprint("waypoints around target were found\n");
454                 //      te_lightning2(NULL, '0 0 0', best.origin);
455                 //      te_knightspike(best.origin);
456
457                         navigation_routerating(this, best, ratingscale, 4000);
458                         best.cnt += 1;
459
460                         this.havocbot_attack_time = 0;
461
462                         if(checkpvs(this.origin + this.view_ofs, it))
463                         if(checkpvs(this.origin + this.view_ofs, best))
464                         {
465                         //      dprint("increasing attack time for this target\n");
466                                 this.havocbot_attack_time = time + 2;
467                         }
468                 }
469         });
470 }
471
472 void havocbot_role_ast_offense(entity this)
473 {
474         if(IS_DEAD(this))
475         {
476                 this.havocbot_attack_time = 0;
477                 havocbot_ast_reset_role(this);
478                 return;
479         }
480
481         // Set the role timeout if necessary
482         if (!this.havocbot_role_timeout)
483                 this.havocbot_role_timeout = time + 120;
484
485         if (time > this.havocbot_role_timeout)
486         {
487                 havocbot_ast_reset_role(this);
488                 return;
489         }
490
491         if(this.havocbot_attack_time>time)
492                 return;
493
494         if (navigation_goalrating_timeout(this))
495         {
496                 // role: offense
497                 navigation_goalrating_start(this);
498                 havocbot_goalrating_enemyplayers(this, 10000, this.origin, 650);
499                 havocbot_goalrating_ast_targets(this, 20000);
500                 havocbot_goalrating_items(this, 30000, this.origin, 10000);
501                 navigation_goalrating_end(this);
502
503                 navigation_goalrating_timeout_set(this);
504         }
505 }
506
507 void havocbot_role_ast_defense(entity this)
508 {
509         if(IS_DEAD(this))
510         {
511                 this.havocbot_attack_time = 0;
512                 havocbot_ast_reset_role(this);
513                 return;
514         }
515
516         // Set the role timeout if necessary
517         if (!this.havocbot_role_timeout)
518                 this.havocbot_role_timeout = time + 120;
519
520         if (time > this.havocbot_role_timeout)
521         {
522                 havocbot_ast_reset_role(this);
523                 return;
524         }
525
526         if(this.havocbot_attack_time>time)
527                 return;
528
529         if (navigation_goalrating_timeout(this))
530         {
531                 // role: defense
532                 navigation_goalrating_start(this);
533                 havocbot_goalrating_enemyplayers(this, 10000, this.origin, 3000);
534                 havocbot_goalrating_ast_targets(this, 20000);
535                 havocbot_goalrating_items(this, 30000, this.origin, 10000);
536                 navigation_goalrating_end(this);
537
538                 navigation_goalrating_timeout_set(this);
539         }
540 }
541
542 void havocbot_role_ast_setrole(entity this, float role)
543 {
544         switch(role)
545         {
546                 case HAVOCBOT_AST_ROLE_DEFENSE:
547                         this.havocbot_role = havocbot_role_ast_defense;
548                         this.havocbot_role_timeout = 0;
549                         break;
550                 case HAVOCBOT_AST_ROLE_OFFENSE:
551                         this.havocbot_role = havocbot_role_ast_offense;
552                         this.havocbot_role_timeout = 0;
553                         break;
554         }
555 }
556
557 void havocbot_ast_reset_role(entity this)
558 {
559         if(IS_DEAD(this))
560                 return;
561
562         if(this.team == assault_attacker_team)
563                 havocbot_role_ast_setrole(this, HAVOCBOT_AST_ROLE_OFFENSE);
564         else
565                 havocbot_role_ast_setrole(this, HAVOCBOT_AST_ROLE_DEFENSE);
566 }
567
568 // mutator hooks
569 MUTATOR_HOOKFUNCTION(as, PlayerSpawn)
570 {
571         entity player = M_ARGV(0, entity);
572
573         if(player.team == assault_attacker_team)
574                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_ASSAULT_ATTACKING);
575         else
576                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_ASSAULT_DEFENDING);
577 }
578
579 MUTATOR_HOOKFUNCTION(as, TurretSpawn)
580 {
581         entity turret = M_ARGV(0, entity);
582
583         if(!turret.team || turret.team == FLOAT_MAX)
584                 turret.team = assault_attacker_team; // this gets reversed when match starts (assault_roundstart_use)
585 }
586
587 MUTATOR_HOOKFUNCTION(as, VehicleInit)
588 {
589         entity veh = M_ARGV(0, entity);
590
591         veh.nextthink = time + 0.5;
592 }
593
594 MUTATOR_HOOKFUNCTION(as, HavocBot_ChooseRole)
595 {
596         entity bot = M_ARGV(0, entity);
597
598         havocbot_ast_reset_role(bot);
599         return true;
600 }
601
602 MUTATOR_HOOKFUNCTION(as, PlayHitsound)
603 {
604         entity frag_victim = M_ARGV(0, entity);
605
606         return (frag_victim.classname == "func_assault_destructible");
607 }
608
609 MUTATOR_HOOKFUNCTION(as, TeamBalance_CheckAllowedTeams)
610 {
611         // assault always has 2 teams
612         M_ARGV(0, float) = BIT(0) | BIT(1);
613         return true;
614 }
615
616 MUTATOR_HOOKFUNCTION(as, CheckRules_World)
617 {
618         M_ARGV(0, float) = WinningCondition_Assault();
619         return true;
620 }
621
622 MUTATOR_HOOKFUNCTION(as, ReadLevelCvars)
623 {
624         // incompatible
625         warmup_stage = 0;
626         sv_ready_restart_after_countdown = 0;
627 }
628
629 MUTATOR_HOOKFUNCTION(as, OnEntityPreSpawn)
630 {
631         entity ent = M_ARGV(0, entity);
632
633         switch(ent.classname)
634         {
635                 case "info_player_team1":
636                 case "info_player_team2":
637                 case "info_player_team3":
638                 case "info_player_team4":
639                         return true;
640         }
641 }
642
643 MUTATOR_HOOKFUNCTION(as, ReadyRestart_Deny)
644 {
645         // readyrestart not supported (yet)
646         return true;
647 }