Merge branch 'master' into Mario/vaporizer_damage
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_domination.qc
1 #include "gamemode_domination.qh"
2 #include "../_all.qh"
3
4 #include "gamemode.qh"
5
6 void dom_EventLog(string mode, float team_before, entity actor) // use an alias for easy changing and quick editing later
7 {
8         if(autocvar_sv_eventlog)
9                 GameLogEcho(strcat(":dom:", mode, ":", ftos(team_before), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
10 }
11
12 void set_dom_state(entity e)
13 {
14         e.dom_total_pps = total_pps;
15         e.dom_pps_red = pps_red;
16         e.dom_pps_blue = pps_blue;
17         if(domination_teams >= 3)
18                 e.dom_pps_yellow = pps_yellow;
19         if(domination_teams >= 4)
20                 e.dom_pps_pink = pps_pink;
21 }
22
23 void dompoint_captured ()
24 {
25         entity head;
26         float old_delay, old_team, real_team;
27
28         // now that the delay has expired, switch to the latest team to lay claim to this point
29         head = self.owner;
30
31         real_team = self.cnt;
32         self.cnt = -1;
33
34         dom_EventLog("taken", self.team, self.dmg_inflictor);
35         self.dmg_inflictor = world;
36
37         self.goalentity = head;
38         self.model = head.mdl;
39         self.modelindex = head.dmg;
40         self.skin = head.skin;
41
42         float points, wait_time;
43         if (autocvar_g_domination_point_amt)
44                 points = autocvar_g_domination_point_amt;
45         else
46                 points = self.frags;
47         if (autocvar_g_domination_point_rate)
48                 wait_time = autocvar_g_domination_point_rate;
49         else
50                 wait_time = self.wait;
51
52         if(domination_roundbased)
53                 bprint(sprintf("^3%s^3%s\n", head.netname, self.message));
54         else
55                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_DOMINATION_CAPTURE_TIME, head.netname, self.message, points, wait_time);
56
57         if(self.enemy.playerid == self.enemy_playerid)
58                 PlayerScore_Add(self.enemy, SP_DOM_TAKES, 1);
59         else
60                 self.enemy = world;
61
62         if (head.noise != "")
63                 if(self.enemy)
64                         sound(self.enemy, CH_TRIGGER, head.noise, VOL_BASE, ATTEN_NORM);
65                 else
66                         sound(self, CH_TRIGGER, head.noise, VOL_BASE, ATTEN_NORM);
67         if (head.noise1 != "")
68                 play2all(head.noise1);
69
70         self.delay = time + wait_time;
71
72         // do trigger work
73         old_delay = self.delay;
74         old_team = self.team;
75         self.team = real_team;
76         self.delay = 0;
77         activator = self;
78         SUB_UseTargets ();
79         self.delay = old_delay;
80         self.team = old_team;
81
82         entity msg = WP_DomNeut;
83         switch(self.team)
84         {
85                 case NUM_TEAM_1: msg = WP_DomRed; break;
86                 case NUM_TEAM_2: msg = WP_DomBlue; break;
87                 case NUM_TEAM_3: msg = WP_DomYellow; break;
88                 case NUM_TEAM_4: msg = WP_DomPink; break;
89         }
90
91         WaypointSprite_UpdateSprites(self.sprite, msg, WP_Null, WP_Null);
92
93         total_pps = 0, pps_red = 0, pps_blue = 0, pps_yellow = 0, pps_pink = 0;
94         for(head = world; (head = find(head, classname, "dom_controlpoint")) != world; )
95         {
96                 if (autocvar_g_domination_point_amt)
97                         points = autocvar_g_domination_point_amt;
98                 else
99                         points = head.frags;
100                 if (autocvar_g_domination_point_rate)
101                         wait_time = autocvar_g_domination_point_rate;
102                 else
103                         wait_time = head.wait;
104                 switch(head.goalentity.team)
105                 {
106                         case NUM_TEAM_1:
107                                 pps_red += points/wait_time;
108                                 break;
109                         case NUM_TEAM_2:
110                                 pps_blue += points/wait_time;
111                                 break;
112                         case NUM_TEAM_3:
113                                 pps_yellow += points/wait_time;
114                                 break;
115                         case NUM_TEAM_4:
116                                 pps_pink += points/wait_time;
117                                 break;
118                 }
119                 total_pps += points/wait_time;
120         }
121
122         WaypointSprite_UpdateTeamRadar(self.sprite, RADARICON_DOMPOINT, colormapPaletteColor(self.goalentity.team - 1, 0));
123         WaypointSprite_Ping(self.sprite);
124
125         self.captime = time;
126
127         FOR_EACH_REALCLIENT(head)
128                 set_dom_state(head);
129 }
130
131 void AnimateDomPoint()
132 {
133         if(self.pain_finished > time)
134                 return;
135         self.pain_finished = time + self.t_width;
136         if(self.nextthink > self.pain_finished)
137                 self.nextthink = self.pain_finished;
138
139         self.frame = self.frame + 1;
140         if(self.frame > self.t_length)
141                 self.frame = 0;
142 }
143
144 void dompointthink()
145 {
146         float fragamt;
147
148         self.nextthink = time + 0.1;
149
150         //self.frame = self.frame + 1;
151         //if(self.frame > 119)
152         //      self.frame = 0;
153         AnimateDomPoint();
154
155         // give points
156
157         if (gameover || self.delay > time || time < game_starttime)     // game has ended, don't keep giving points
158                 return;
159
160         if(autocvar_g_domination_point_rate)
161                 self.delay = time + autocvar_g_domination_point_rate;
162         else
163                 self.delay = time + self.wait;
164
165         // give credit to the team
166         // NOTE: this defaults to 0
167         if (!domination_roundbased)
168         if (self.goalentity.netname != "")
169         {
170                 if(autocvar_g_domination_point_amt)
171                         fragamt = autocvar_g_domination_point_amt;
172                 else
173                         fragamt = self.frags;
174                 TeamScore_AddToTeam(self.goalentity.team, ST_SCORE, fragamt);
175                 TeamScore_AddToTeam(self.goalentity.team, ST_DOM_TICKS, fragamt);
176
177                 // give credit to the individual player, if he is still there
178                 if (self.enemy.playerid == self.enemy_playerid)
179                 {
180                         PlayerScore_Add(self.enemy, SP_SCORE, fragamt);
181                         PlayerScore_Add(self.enemy, SP_DOM_TICKS, fragamt);
182                 }
183                 else
184                         self.enemy = world;
185         }
186 }
187
188 void dompointtouch()
189 {
190         entity head;
191         if (!IS_PLAYER(other))
192                 return;
193         if (other.health < 1)
194                 return;
195
196         if(round_handler_IsActive() && !round_handler_IsRoundStarted())
197                 return;
198
199         if(time < self.captime + 0.3)
200                 return;
201
202         // only valid teams can claim it
203         head = find(world, classname, "dom_team");
204         while (head && head.team != other.team)
205                 head = find(head, classname, "dom_team");
206         if (!head || head.netname == "" || head == self.goalentity)
207                 return;
208
209         // delay capture
210
211         self.team = self.goalentity.team; // this stores the PREVIOUS team!
212
213         self.cnt = other.team;
214         self.owner = head; // team to switch to after the delay
215         self.dmg_inflictor = other;
216
217         // self.state = 1;
218         // self.delay = time + cvar("g_domination_point_capturetime");
219         //self.nextthink = time + cvar("g_domination_point_capturetime");
220         //self.think = dompoint_captured;
221
222         // go to neutral team in the mean time
223         head = find(world, classname, "dom_team");
224         while (head && head.netname != "")
225                 head = find(head, classname, "dom_team");
226         if(head == world)
227                 return;
228
229         WaypointSprite_UpdateSprites(self.sprite, WP_DomNeut, WP_Null, WP_Null);
230         WaypointSprite_UpdateTeamRadar(self.sprite, RADARICON_DOMPOINT, '0 1 1');
231         WaypointSprite_Ping(self.sprite);
232
233         self.goalentity = head;
234         self.model = head.mdl;
235         self.modelindex = head.dmg;
236         self.skin = head.skin;
237
238         self.enemy = other; // individual player scoring
239         self.enemy_playerid = other.playerid;
240         dompoint_captured();
241 }
242
243 void dom_controlpoint_setup()
244 {
245         entity head;
246         // find the spawnfunc_dom_team representing unclaimed points
247         head = find(world, classname, "dom_team");
248         while(head && head.netname != "")
249                 head = find(head, classname, "dom_team");
250         if (!head)
251                 objerror("no spawnfunc_dom_team with netname \"\" found\n");
252
253         // copy important properties from spawnfunc_dom_team entity
254         self.goalentity = head;
255         setmodel(self, head.mdl); // precision already set
256         self.skin = head.skin;
257
258         self.cnt = -1;
259
260         if(self.message == "")
261                 self.message = " has captured a control point";
262
263         if(self.frags <= 0)
264                 self.frags = 1;
265         if(self.wait <= 0)
266                 self.wait = 5;
267
268         float points, waittime;
269         if (autocvar_g_domination_point_amt)
270                 points = autocvar_g_domination_point_amt;
271         else
272                 points = self.frags;
273         if (autocvar_g_domination_point_rate)
274                 waittime = autocvar_g_domination_point_rate;
275         else
276                 waittime = self.wait;
277
278         total_pps += points/waittime;
279
280         if(!self.t_width)
281                 self.t_width = 0.02; // frame animation rate
282         if(!self.t_length)
283                 self.t_length = 239; // maximum frame
284
285         self.think = dompointthink;
286         self.nextthink = time;
287         self.touch = dompointtouch;
288         self.solid = SOLID_TRIGGER;
289         self.flags = FL_ITEM;
290         setsize(self, '-32 -32 -32', '32 32 32');
291         setorigin(self, self.origin + '0 0 20');
292         droptofloor();
293
294         waypoint_spawnforitem(self);
295         WaypointSprite_SpawnFixed(WP_DomNeut, self.origin + '0 0 32', self, sprite, RADARICON_DOMPOINT);
296 }
297
298 float total_controlpoints, redowned, blueowned, yellowowned, pinkowned;
299 void Domination_count_controlpoints()
300 {
301         entity e;
302         total_controlpoints = redowned = blueowned = yellowowned = pinkowned = 0;
303         for(e = world; (e = find(e, classname, "dom_controlpoint")) != world; )
304         {
305                 ++total_controlpoints;
306                 redowned += (e.goalentity.team == NUM_TEAM_1);
307                 blueowned += (e.goalentity.team == NUM_TEAM_2);
308                 yellowowned += (e.goalentity.team == NUM_TEAM_3);
309                 pinkowned += (e.goalentity.team == NUM_TEAM_4);
310         }
311 }
312
313 float Domination_GetWinnerTeam()
314 {
315         float winner_team = 0;
316         if(redowned == total_controlpoints)
317                 winner_team = NUM_TEAM_1;
318         if(blueowned == total_controlpoints)
319         {
320                 if(winner_team) return 0;
321                 winner_team = NUM_TEAM_2;
322         }
323         if(yellowowned == total_controlpoints)
324         {
325                 if(winner_team) return 0;
326                 winner_team = NUM_TEAM_3;
327         }
328         if(pinkowned == total_controlpoints)
329         {
330                 if(winner_team) return 0;
331                 winner_team = NUM_TEAM_4;
332         }
333         if(winner_team)
334                 return winner_team;
335         return -1; // no control points left?
336 }
337
338 #define DOM_OWNED_CONTROLPOINTS() ((redowned > 0) + (blueowned > 0) + (yellowowned > 0) + (pinkowned > 0))
339 #define DOM_OWNED_CONTROLPOINTS_OK() (DOM_OWNED_CONTROLPOINTS() < total_controlpoints)
340 float Domination_CheckWinner()
341 {
342         if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
343         {
344                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_OVER);
345                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_OVER);
346                 round_handler_Init(5, autocvar_g_domination_warmup, autocvar_g_domination_round_timelimit);
347                 return 1;
348         }
349
350         Domination_count_controlpoints();
351
352         float winner_team = Domination_GetWinnerTeam();
353
354         if(winner_team == -1)
355                 return 0;
356
357         if(winner_team > 0)
358         {
359                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, APP_TEAM_NUM_4(winner_team, CENTER_ROUND_TEAM_WIN_));
360                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(winner_team, INFO_ROUND_TEAM_WIN_));
361                 TeamScore_AddToTeam(winner_team, ST_DOM_CAPS, +1);
362         }
363         else if(winner_team == -1)
364         {
365                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_TIED);
366                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_TIED);
367         }
368
369         round_handler_Init(5, autocvar_g_domination_warmup, autocvar_g_domination_round_timelimit);
370
371         return 1;
372 }
373
374 float Domination_CheckPlayers()
375 {
376         return 1;
377 }
378
379 void Domination_RoundStart()
380 {
381         entity e;
382         FOR_EACH_PLAYER(e)
383                 e.player_blocked = 0;
384 }
385
386 //go to best items, or control points you don't own
387 void havocbot_role_dom()
388 {
389         if(self.deadflag != DEAD_NO)
390                 return;
391
392         if (self.bot_strategytime < time)
393         {
394                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
395                 navigation_goalrating_start();
396                 havocbot_goalrating_controlpoints(10000, self.origin, 15000);
397                 havocbot_goalrating_items(8000, self.origin, 8000);
398                 //havocbot_goalrating_enemyplayers(3000, self.origin, 2000);
399                 //havocbot_goalrating_waypoints(1, self.origin, 1000);
400                 navigation_goalrating_end();
401         }
402 }
403
404 MUTATOR_HOOKFUNCTION(dom_GetTeamCount)
405 {
406         ret_float = domination_teams;
407         return 0;
408 }
409
410 MUTATOR_HOOKFUNCTION(dom_ResetMap)
411 {
412         total_pps = 0, pps_red = 0, pps_blue = 0, pps_yellow = 0, pps_pink = 0;
413         FOR_EACH_PLAYER(self)
414         {
415                 PutClientInServer();
416                 self.player_blocked = 1;
417                 if(IS_REAL_CLIENT(self))
418                         set_dom_state(self);
419         }
420         return 1;
421 }
422
423 MUTATOR_HOOKFUNCTION(dom_PlayerSpawn)
424 {
425         if(domination_roundbased)
426         if(!round_handler_IsRoundStarted())
427                 self.player_blocked = 1;
428         else
429                 self.player_blocked = 0;
430         return false;
431 }
432
433 MUTATOR_HOOKFUNCTION(dom_ClientConnect)
434 {
435         set_dom_state(self);
436         return false;
437 }
438
439 MUTATOR_HOOKFUNCTION(dom_BotRoles)
440 {
441         self.havocbot_role = havocbot_role_dom;
442         return true;
443 }
444
445 /*QUAKED spawnfunc_dom_controlpoint (0 .5 .8) (-16 -16 -24) (16 16 32)
446 Control point for Domination gameplay.
447 */
448 void spawnfunc_dom_controlpoint()
449 {
450         if(!g_domination)
451         {
452                 remove(self);
453                 return;
454         }
455         self.think = dom_controlpoint_setup;
456         self.nextthink = time + 0.1;
457         self.reset = dom_controlpoint_setup;
458
459         if(!self.scale)
460                 self.scale = 0.6;
461
462         self.effects = self.effects | EF_LOWPRECISION;
463         if (autocvar_g_domination_point_fullbright)
464                 self.effects |= EF_FULLBRIGHT;
465 }
466
467 /*QUAKED spawnfunc_dom_team (0 .5 .8) (-32 -32 -24) (32 32 32)
468 Team declaration for Domination gameplay, this allows you to decide what team
469 names and control point models are used in your map.
470
471 Note: If you use spawnfunc_dom_team entities you must define at least 3 and only two
472 can have netname set!  The nameless team owns all control points at start.
473
474 Keys:
475 "netname"
476  Name of the team (for example Red Team, Blue Team, Green Team, Yellow Team, Life, Death, etc)
477 "cnt"
478  Scoreboard color of the team (for example 4 is red and 13 is blue)
479 "model"
480  Model to use for control points owned by this team (for example
481  "progs/b_g_key.mdl" is a gold keycard, and "progs/b_s_key.mdl" is a silver
482  keycard)
483 "skin"
484  Skin of the model to use (for team skins on a single model)
485 "noise"
486  Sound to play when this team captures a point.
487  (this is a localized sound, like a small alarm or other effect)
488 "noise1"
489  Narrator speech to play when this team captures a point.
490  (this is a global sound, like "Red team has captured a control point")
491 */
492
493 void spawnfunc_dom_team()
494 {
495         if(!g_domination || autocvar_g_domination_teams_override >= 2)
496         {
497                 remove(self);
498                 return;
499         }
500         precache_model(self.model);
501         if (self.noise != "")
502                 precache_sound(self.noise);
503         if (self.noise1 != "")
504                 precache_sound(self.noise1);
505         self.classname = "dom_team";
506         setmodel(self, self.model); // precision not needed
507         self.mdl = self.model;
508         self.dmg = self.modelindex;
509         self.model = "";
510         self.modelindex = 0;
511         // this would have to be changed if used in quakeworld
512         if(self.cnt)
513                 self.team = self.cnt + 1; // WHY are these different anyway?
514 }
515
516 // scoreboard setup
517 void ScoreRules_dom(float teams)
518 {
519         if(domination_roundbased)
520         {
521                 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
522                 ScoreInfo_SetLabel_TeamScore  (ST_DOM_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
523                 ScoreInfo_SetLabel_PlayerScore(SP_DOM_TAKES, "takes", 0);
524                 ScoreRules_basics_end();
525         }
526         else
527         {
528                 float sp_domticks, sp_score;
529                 sp_score = sp_domticks = 0;
530                 if(autocvar_g_domination_disable_frags)
531                         sp_domticks = SFL_SORT_PRIO_PRIMARY;
532                 else
533                         sp_score = SFL_SORT_PRIO_PRIMARY;
534                 ScoreRules_basics(teams, sp_score, sp_score, true);
535                 ScoreInfo_SetLabel_TeamScore  (ST_DOM_TICKS,    "ticks",     sp_domticks);
536                 ScoreInfo_SetLabel_PlayerScore(SP_DOM_TICKS,    "ticks",     sp_domticks);
537                 ScoreInfo_SetLabel_PlayerScore(SP_DOM_TAKES,    "takes",     0);
538                 ScoreRules_basics_end();
539         }
540 }
541
542 // code from here on is just to support maps that don't have control point and team entities
543 void dom_spawnteam (string teamname, float teamcolor, string pointmodel, float pointskin, string capsound, string capnarration, string capmessage)
544 {
545         entity oldself;
546         oldself = self;
547         self = spawn();
548         self.classname = "dom_team";
549         self.netname = teamname;
550         self.cnt = teamcolor;
551         self.model = pointmodel;
552         self.skin = pointskin;
553         self.noise = capsound;
554         self.noise1 = capnarration;
555         self.message = capmessage;
556
557         // this code is identical to spawnfunc_dom_team
558         setmodel(self, self.model); // precision not needed
559         self.mdl = self.model;
560         self.dmg = self.modelindex;
561         self.model = "";
562         self.modelindex = 0;
563         // this would have to be changed if used in quakeworld
564         self.team = self.cnt + 1;
565
566         //eprint(self);
567         self = oldself;
568 }
569
570 void dom_spawnpoint(vector org)
571 {
572         entity oldself;
573         oldself = self;
574         self = spawn();
575         self.classname = "dom_controlpoint";
576         self.think = spawnfunc_dom_controlpoint;
577         self.nextthink = time;
578         setorigin(self, org);
579         spawnfunc_dom_controlpoint();
580         self = oldself;
581 }
582
583 // spawn some default teams if the map is not set up for domination
584 void dom_spawnteams(float teams)
585 {
586         dom_spawnteam("Red", NUM_TEAM_1-1, "models/domination/dom_red.md3", 0, "domination/claim.wav", "", "Red team has captured a control point");
587         dom_spawnteam("Blue", NUM_TEAM_2-1, "models/domination/dom_blue.md3", 0, "domination/claim.wav", "", "Blue team has captured a control point");
588         if(teams >= 3)
589                 dom_spawnteam("Yellow", NUM_TEAM_3-1, "models/domination/dom_yellow.md3", 0, "domination/claim.wav", "", "Yellow team has captured a control point");
590         if(teams >= 4)
591                 dom_spawnteam("Pink", NUM_TEAM_4-1, "models/domination/dom_pink.md3", 0, "domination/claim.wav", "", "Pink team has captured a control point");
592         dom_spawnteam("", 0, "models/domination/dom_unclaimed.md3", 0, "", "", "");
593 }
594
595 void dom_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
596 {
597         // if no teams are found, spawn defaults
598         if(find(world, classname, "dom_team") == world || autocvar_g_domination_teams_override >= 2)
599         {
600                 LOG_INFO("No ""dom_team"" entities found on this map, creating them anyway.\n");
601                 domination_teams = bound(2, ((autocvar_g_domination_teams_override < 2) ? autocvar_g_domination_default_teams : autocvar_g_domination_teams_override), 4);
602                 dom_spawnteams(domination_teams);
603         }
604
605         CheckAllowedTeams(world);
606         domination_teams = ((c4>=0) ? 4 : (c3>=0) ? 3 : 2);
607
608         addstat(STAT_DOM_TOTAL_PPS, AS_FLOAT, dom_total_pps);
609         addstat(STAT_DOM_PPS_RED, AS_FLOAT, dom_pps_red);
610         addstat(STAT_DOM_PPS_BLUE, AS_FLOAT, dom_pps_blue);
611         if(domination_teams >= 3) addstat(STAT_DOM_PPS_YELLOW, AS_FLOAT, dom_pps_yellow);
612         if(domination_teams >= 4) addstat(STAT_DOM_PPS_PINK, AS_FLOAT, dom_pps_pink);
613
614         domination_roundbased = autocvar_g_domination_roundbased;
615
616         ScoreRules_dom(domination_teams);
617
618         if(domination_roundbased)
619         {
620                 round_handler_Spawn(Domination_CheckPlayers, Domination_CheckWinner, Domination_RoundStart);
621                 round_handler_Init(5, autocvar_g_domination_warmup, autocvar_g_domination_round_timelimit);
622         }
623 }
624
625 void dom_Initialize()
626 {
627         precache_model("models/domination/dom_red.md3");
628         precache_model("models/domination/dom_blue.md3");
629         precache_model("models/domination/dom_yellow.md3");
630         precache_model("models/domination/dom_pink.md3");
631         precache_model("models/domination/dom_unclaimed.md3");
632         precache_sound("domination/claim.wav");
633
634         InitializeEntity(world, dom_DelayedInit, INITPRIO_GAMETYPE);
635 }
636
637
638 MUTATOR_DEFINITION(gamemode_domination)
639 {
640         MUTATOR_HOOK(GetTeamCount, dom_GetTeamCount, CBC_ORDER_ANY);
641         MUTATOR_HOOK(reset_map_players, dom_ResetMap, CBC_ORDER_ANY);
642         MUTATOR_HOOK(PlayerSpawn, dom_PlayerSpawn, CBC_ORDER_ANY);
643         MUTATOR_HOOK(ClientConnect, dom_ClientConnect, CBC_ORDER_ANY);
644         MUTATOR_HOOK(HavocBot_ChooseRole, dom_BotRoles, CBC_ORDER_ANY);
645
646         MUTATOR_ONADD
647         {
648                 if(time > 1) // game loads at time 1
649                         error("This is a game type and it cannot be added at runtime.");
650                 dom_Initialize();
651         }
652
653         MUTATOR_ONREMOVE
654         {
655                 LOG_INFO("This is a game type and it cannot be removed at runtime.");
656                 return -1;
657         }
658
659         return 0;
660 }