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