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