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