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