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