]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_assault.qc
239c3d3b4bf262da7ab8ba7d1cc2120930505f1c
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_assault.qc
1 #include "gamemode_assault.qh"
2 #include "../_all.qh"
3
4 #include "gamemode.qh"
5
6 .entity sprite;
7
8 // random functions
9 void assault_objective_use()
10 {SELFPARAM();
11         // activate objective
12         self.health = 100;
13         //print("^2Activated objective ", self.targetname, "=", etos(self), "\n");
14         //print("Activator is ", activator.classname, "\n");
15
16         for (entity e = world; (e = find(e, target, this.targetname)); )
17         {
18                 if (e.classname == "target_objective_decrease")
19                 {
20                         WITH(entity, self, e, target_objective_decrease_activate());
21                 }
22         }
23
24         setself(this);
25 }
26
27 vector target_objective_spawn_evalfunc(entity player, entity spot, vector current)
28 {SELFPARAM();
29         if(self.health < 0 || self.health >= ASSAULT_VALUE_INACTIVE)
30                 return '-1 0 0';
31         return current;
32 }
33
34 // reset this objective. Used when spawning an objective
35 // and when a new round starts
36 void assault_objective_reset()
37 {SELFPARAM();
38         self.health = ASSAULT_VALUE_INACTIVE;
39 }
40
41 // decrease the health of targeted objectives
42 void assault_objective_decrease_use()
43 {SELFPARAM();
44         if(activator.team != assault_attacker_team)
45         {
46                 // wrong team triggered decrease
47                 return;
48         }
49
50         if(other.assault_sprite)
51         {
52                 WaypointSprite_Disown(other.assault_sprite, waypointsprite_deadlifetime);
53                 if(other.classname == "func_assault_destructible")
54                         other.sprite = world;
55         }
56         else
57                 return; // already activated! cannot activate again!
58
59         if(self.enemy.health < ASSAULT_VALUE_INACTIVE)
60         {
61                 if(self.enemy.health - self.dmg > 0.5)
62                 {
63                         PlayerTeamScore_Add(activator, SP_SCORE, ST_SCORE, self.dmg);
64                         self.enemy.health = self.enemy.health - self.dmg;
65                 }
66                 else
67                 {
68                         PlayerTeamScore_Add(activator, SP_SCORE, ST_SCORE, self.enemy.health);
69                         PlayerTeamScore_Add(activator, SP_ASSAULT_OBJECTIVES, ST_ASSAULT_OBJECTIVES, 1);
70                         self.enemy.health = -1;
71
72                         entity oldactivator, head;
73
74                         setself(this.enemy);
75                         if(self.message)
76                         FOR_EACH_PLAYER(head)
77                                 centerprint(head, self.message);
78
79                         oldactivator = activator;
80                         activator = this;
81                         SUB_UseTargets();
82                         activator = oldactivator;
83                         setself(this);
84                 }
85         }
86 }
87
88 void assault_setenemytoobjective()
89 {SELFPARAM();
90         entity objective;
91         for(objective = world; (objective = find(objective, targetname, self.target)); )
92         {
93                 if(objective.classname == "target_objective")
94                 {
95                         if(self.enemy == world)
96                                 self.enemy = objective;
97                         else
98                                 objerror("more than one objective as target - fix the map!");
99                         break;
100                 }
101         }
102
103         if(self.enemy == world)
104                 objerror("no objective as target - fix the map!");
105 }
106
107 float assault_decreaser_sprite_visible(entity e)
108 {SELFPARAM();
109         entity decreaser;
110
111         decreaser = self.assault_decreaser;
112
113         if(decreaser.enemy.health >= ASSAULT_VALUE_INACTIVE)
114                 return false;
115
116         return true;
117 }
118
119 void target_objective_decrease_activate()
120 {SELFPARAM();
121         entity ent, spr;
122         self.owner = world;
123         for(ent = world; (ent = find(ent, target, self.targetname)); )
124         {
125                 if(ent.assault_sprite != world)
126                 {
127                         WaypointSprite_Disown(ent.assault_sprite, waypointsprite_deadlifetime);
128                         if(ent.classname == "func_assault_destructible")
129                                 ent.sprite = world;
130                 }
131
132                 spr = WaypointSprite_SpawnFixed(WP_Assault, 0.5 * (ent.absmin + ent.absmax), ent, assault_sprite, RADARICON_OBJECTIVE);
133                 spr.assault_decreaser = self;
134                 spr.waypointsprite_visible_for_player = assault_decreaser_sprite_visible;
135                 spr.classname = "sprite_waypoint";
136                 WaypointSprite_UpdateRule(spr, assault_attacker_team, SPRITERULE_TEAMPLAY);
137                 if(ent.classname == "func_assault_destructible")
138                 {
139                         WaypointSprite_UpdateSprites(spr, WP_AssaultDefend, WP_AssaultDestroy, WP_AssaultDestroy);
140                         WaypointSprite_UpdateMaxHealth(spr, ent.max_health);
141                         WaypointSprite_UpdateHealth(spr, ent.health);
142                         ent.sprite = spr;
143                 }
144                 else
145                         WaypointSprite_UpdateSprites(spr, WP_AssaultDefend, WP_AssaultPush, WP_AssaultPush);
146         }
147 }
148
149 void target_objective_decrease_findtarget()
150 {
151         assault_setenemytoobjective();
152 }
153
154 void target_assault_roundend_reset()
155 {SELFPARAM();
156         //print("round end reset\n");
157         self.cnt = self.cnt + 1; // up round counter
158         self.winning = 0; // up round
159 }
160
161 void target_assault_roundend_use()
162 {SELFPARAM();
163         self.winning = 1; // round has been won by attackers
164 }
165
166 void assault_roundstart_use()
167 {SELFPARAM();
168         activator = self;
169         SUB_UseTargets();
170
171         //(Re)spawn all turrets
172         for(entity ent = NULL; (ent = find(ent, classname, "turret_main")); ) {
173                 // Swap turret teams
174                 if(ent.team == NUM_TEAM_1)
175                         ent.team = NUM_TEAM_2;
176                 else
177                         ent.team = NUM_TEAM_1;
178
179                 // Dubbles as teamchange
180                 WITH(entity, self, ent, turret_respawn());
181         }
182 }
183
184 void assault_wall_think()
185 {SELFPARAM();
186         if(self.enemy.health < 0)
187         {
188                 self.model = "";
189                 self.solid = SOLID_NOT;
190         }
191         else
192         {
193                 self.model = self.mdl;
194                 self.solid = SOLID_BSP;
195         }
196
197         self.nextthink = time + 0.2;
198 }
199
200 // trigger new round
201 // reset objectives, toggle spawnpoints, reset triggers, ...
202 void vehicles_clearreturn(entity veh);
203 void vehicles_spawn();
204 void assault_new_round()
205 {SELFPARAM();
206         //bprint("ASSAULT: new round\n");
207
208         // Eject players from vehicles
209         entity e;
210     FOR_EACH_PLAYER(e)
211     {
212         if(e.vehicle)
213         {
214                 WITH(entity, self, e, vehicles_exit(VHEF_RELEASE));
215         }
216     }
217
218     for (entity e_ = findchainflags(vehicle_flags, VHF_ISVEHICLE); e_; e_ = e_.chain)
219     {
220         setself(e_);
221         vehicles_clearreturn(self);
222         vehicles_spawn();
223     }
224
225     setself(this);
226
227         // up round counter
228         self.winning = self.winning + 1;
229
230         // swap attacker/defender roles
231         if(assault_attacker_team == NUM_TEAM_1)
232                 assault_attacker_team = NUM_TEAM_2;
233         else
234                 assault_attacker_team = NUM_TEAM_1;
235
236         entity ent;
237         for(ent = world; (ent = nextent(ent)); )
238         {
239                 if(clienttype(ent) == CLIENTTYPE_NOTACLIENT)
240                 {
241                         if(ent.team_saved == NUM_TEAM_1)
242                                 ent.team_saved = NUM_TEAM_2;
243                         else if(ent.team_saved == NUM_TEAM_2)
244                                 ent.team_saved = NUM_TEAM_1;
245                 }
246         }
247
248         // reset the level with a countdown
249         cvar_set("timelimit", ftos(ceil(time - game_starttime) / 60));
250         ReadyRestart_force(); // sets game_starttime
251 }
252
253 // spawnfuncs
254 void spawnfunc_info_player_attacker()
255 {SELFPARAM();
256         if (!g_assault) { remove(self); return; }
257
258         self.team = NUM_TEAM_1; // red, gets swapped every round
259         spawnfunc_info_player_deathmatch();
260 }
261
262 void spawnfunc_info_player_defender()
263 {SELFPARAM();
264         if (!g_assault) { remove(self); return; }
265
266         self.team = NUM_TEAM_2; // blue, gets swapped every round
267         spawnfunc_info_player_deathmatch();
268 }
269
270 void spawnfunc_target_objective()
271 {SELFPARAM();
272         if (!g_assault) { remove(self); return; }
273
274         self.classname = "target_objective";
275         self.use = assault_objective_use;
276         assault_objective_reset();
277         self.reset = assault_objective_reset;
278         self.spawn_evalfunc = target_objective_spawn_evalfunc;
279 }
280
281 void spawnfunc_target_objective_decrease()
282 {SELFPARAM();
283         if (!g_assault) { remove(self); return; }
284
285         self.classname = "target_objective_decrease";
286
287         if(!self.dmg)
288                 self.dmg = 101;
289
290         self.use = assault_objective_decrease_use;
291         self.health = ASSAULT_VALUE_INACTIVE;
292         self.max_health = ASSAULT_VALUE_INACTIVE;
293         self.enemy = world;
294
295         InitializeEntity(self, target_objective_decrease_findtarget, INITPRIO_FINDTARGET);
296 }
297
298 // destructible walls that can be used to trigger target_objective_decrease
299 void spawnfunc_func_assault_destructible()
300 {SELFPARAM();
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();
312 }
313
314 void spawnfunc_func_assault_wall()
315 {SELFPARAM();
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 void spawnfunc_target_assault_roundend()
328 {SELFPARAM();
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 void spawnfunc_target_assault_roundstart()
339 {SELFPARAM();
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(assault_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(assault_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(assault_VehicleSpawn)
552 {SELFPARAM();
553         self.nextthink = time + 0.5;
554
555         return false;
556 }
557
558 MUTATOR_HOOKFUNCTION(assault_BotRoles)
559 {SELFPARAM();
560         havocbot_ast_reset_role(self);
561         return true;
562 }
563
564 MUTATOR_HOOKFUNCTION(assault_PlayHitsound)
565 {
566         return (frag_victim.classname == "func_assault_destructible");
567 }
568
569 // scoreboard setup
570 void assault_ScoreRules()
571 {
572         ScoreRules_basics(2, SFL_SORT_PRIO_SECONDARY, SFL_SORT_PRIO_SECONDARY, true);
573         ScoreInfo_SetLabel_TeamScore(  ST_ASSAULT_OBJECTIVES,    "objectives",      SFL_SORT_PRIO_PRIMARY);
574         ScoreInfo_SetLabel_PlayerScore(SP_ASSAULT_OBJECTIVES,    "objectives",      SFL_SORT_PRIO_PRIMARY);
575         ScoreRules_basics_end();
576 }
577
578 MUTATOR_DEFINITION(gamemode_assault)
579 {
580         MUTATOR_HOOK(PlayerSpawn, assault_PlayerSpawn, CBC_ORDER_ANY);
581         MUTATOR_HOOK(TurretSpawn, assault_TurretSpawn, CBC_ORDER_ANY);
582         MUTATOR_HOOK(VehicleSpawn, assault_VehicleSpawn, CBC_ORDER_ANY);
583         MUTATOR_HOOK(HavocBot_ChooseRole, assault_BotRoles, CBC_ORDER_ANY);
584         MUTATOR_HOOK(PlayHitsound, assault_PlayHitsound, CBC_ORDER_ANY);
585
586         MUTATOR_ONADD
587         {
588                 if(time > 1) // game loads at time 1
589                         error("This is a game type and it cannot be added at runtime.");
590                 assault_ScoreRules();
591         }
592
593         MUTATOR_ONROLLBACK_OR_REMOVE
594         {
595                 // we actually cannot roll back assault_Initialize here
596                 // BUT: we don't need to! If this gets called, adding always
597                 // succeeds.
598         }
599
600         MUTATOR_ONREMOVE
601         {
602                 LOG_INFO("This is a game type and it cannot be removed at runtime.");
603                 return -1;
604         }
605
606         return 0;
607 }