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