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