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