]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/domination.qc
Merge branch 'master' into terencehill/physics_panel_updates
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / domination.qc
1
2 /*
3 Domination as a plugin for netquake mods
4 by LordHavoc (lordhavoc@ghdigital.com)
5
6 How to add domination points to a mod:
7 1. Add this line to progs.src above world.qc:
8 domination.qc
9 2. Comment out all lines in ClientObituary in client.qc that begin with targ.frags  or attacker.frags.
10 3. Add this above spawnfunc_worldspawn in world.qc:
11 void() dom_init;
12 4. Add this line to the end of spawnfunc_worldspawn in world.qc:
13 dom_init();
14
15 Note: The only teams who can use dom control points are identified by spawnfunc_dom_team entities (if none exist these default to red and blue and use only quake models/sounds).
16 */
17
18 #define DOMPOINTFRAGS frags
19
20 .float enemy_playerid;
21 .entity sprite;
22 .float captime;
23
24 // pps: points per second
25 .float dom_total_pps;
26 .float dom_pps_red;
27 .float dom_pps_blue;
28 .float dom_pps_yellow;
29 .float dom_pps_pink;
30 float total_pps;
31 float pps_red;
32 float pps_blue;
33 float pps_yellow;
34 float pps_pink;
35 void set_dom_state(entity e)
36 {
37         e.dom_total_pps = total_pps;
38         e.dom_pps_red = pps_red;
39         e.dom_pps_blue = pps_blue;
40         if(c3 >= 0)
41                 e.dom_pps_yellow = pps_yellow;
42         if(c4 >= 0)
43                 e.dom_pps_pink = pps_pink;
44 }
45
46 void() dom_controlpoint_setup;
47
48 void LogDom(string mode, float team_before, entity actor)
49 {
50         string s;
51         if(!autocvar_sv_eventlog)
52                 return;
53         s = strcat(":dom:", mode);
54         s = strcat(s, ":", ftos(team_before));
55         s = strcat(s, ":", ftos(actor.playerid));
56         GameLogEcho(s);
57 }
58
59 void() dom_spawnteams;
60
61 void dompoint_captured ()
62 {
63         local entity head;
64         local float old_delay, old_team, real_team;
65
66         // now that the delay has expired, switch to the latest team to lay claim to this point
67         head = self.owner;
68
69         real_team = self.cnt;
70         self.cnt = -1;
71
72         LogDom("taken", self.team, self.dmg_inflictor);
73         self.dmg_inflictor = world;
74
75         self.goalentity = head;
76         self.model = head.mdl;
77         self.modelindex = head.dmg;
78         self.skin = head.skin;
79
80         //bprint(head.message);
81         //bprint("\n");
82
83         //bprint(^3head.netname);
84         //bprint(head.netname);
85         //bprint(self.message);
86         //bprint("\n");
87
88         float points, wait_time;
89         if (autocvar_g_domination_point_amt)
90                 points = autocvar_g_domination_point_amt;
91         else
92                 points = self.frags;
93         if (autocvar_g_domination_point_rate)
94                 wait_time = autocvar_g_domination_point_rate;
95         else
96                 wait_time = self.wait;
97
98         bprint("^3", head.netname, "^3", self.message);
99         if (points != 1)
100                 bprint(" ^7(", ftos(points), " points every ", ftos(wait_time), " seconds)\n");
101         else
102                 bprint(" ^7(", ftos(points), " point every ", ftos(wait_time), " seconds)\n");
103
104         if(self.enemy.playerid == self.enemy_playerid)
105                 PlayerScore_Add(self.enemy, SP_DOM_TAKES, 1);
106         else
107                 self.enemy = world;
108
109         if (head.noise != "")
110                 if(self.enemy)
111                         sound(self.enemy, CH_TRIGGER, head.noise, VOL_BASE, ATTN_NORM);
112                 else
113                         sound(self, CH_TRIGGER, head.noise, VOL_BASE, ATTN_NORM);
114         if (head.noise1 != "")
115                 play2all(head.noise1);
116
117         //self.nextthink = time + autocvar_g_domination_point_rate;
118         //self.think = dompointthink;
119
120         self.delay = time + wait_time;
121
122         // do trigger work
123         old_delay = self.delay;
124         old_team = self.team;
125         self.team = real_team;
126         self.delay = 0;
127         activator = self;
128         SUB_UseTargets ();
129         self.delay = old_delay;
130         self.team = old_team;
131
132         switch(self.goalentity.team)
133         {
134                 case COLOR_TEAM1:
135                         WaypointSprite_UpdateSprites(self.sprite, "dom-red", "", "");
136                         break;
137                 case COLOR_TEAM2:
138                         WaypointSprite_UpdateSprites(self.sprite, "dom-blue", "", "");
139                         break;
140                 case COLOR_TEAM3:
141                         WaypointSprite_UpdateSprites(self.sprite, "dom-yellow", "", "");
142                         break;
143                 case COLOR_TEAM4:
144                         WaypointSprite_UpdateSprites(self.sprite, "dom-pink", "", "");
145         }
146
147         total_pps = 0, pps_red = 0, pps_blue = 0, pps_yellow = 0, pps_pink = 0;
148         for(head = world; (head = find(head, classname, "dom_controlpoint")) != world; )
149         {
150                 if (autocvar_g_domination_point_amt)
151                         points = autocvar_g_domination_point_amt;
152                 else
153                         points = head.frags;
154                 if (autocvar_g_domination_point_rate)
155                         wait_time = autocvar_g_domination_point_rate;
156                 else
157                         wait_time = head.wait;
158                 switch(head.goalentity.team)
159                 {
160                         case COLOR_TEAM1:
161                                 pps_red += points/wait_time;
162                                 break;
163                         case COLOR_TEAM2:
164                                 pps_blue += points/wait_time;
165                                 break;
166                         case COLOR_TEAM3:
167                                 pps_yellow += points/wait_time;
168                                 break;
169                         case COLOR_TEAM4:
170                                 pps_pink += points/wait_time;
171                 }
172                 total_pps += points/wait_time;
173         }
174
175         WaypointSprite_UpdateTeamRadar(self.sprite, RADARICON_DOMPOINT, colormapPaletteColor(self.goalentity.team - 1, 0));
176         WaypointSprite_Ping(self.sprite);
177
178         self.captime = time;
179
180         FOR_EACH_REALCLIENT(head)
181                 set_dom_state(head);
182 };
183
184 void AnimateDomPoint()
185 {
186         if(self.pain_finished > time)
187                 return;
188         self.pain_finished = time + self.t_width;
189         if(self.nextthink > self.pain_finished)
190                 self.nextthink = self.pain_finished;
191
192         self.frame = self.frame + 1;
193         if(self.frame > self.t_length)
194                 self.frame = 0;
195 }
196
197 void dompointthink()
198 {
199         local float fragamt;
200
201         self.nextthink = time + 0.1;
202
203         //self.frame = self.frame + 1;
204         //if(self.frame > 119)
205         //      self.frame = 0;
206         AnimateDomPoint();
207
208         // give points
209
210         if (gameover || self.delay > time || time < game_starttime)     // game has ended, don't keep giving points
211                 return;
212
213         if(autocvar_g_domination_point_rate)
214                 self.delay = time + autocvar_g_domination_point_rate;
215         else
216                 self.delay = time + self.wait;
217
218         // give credit to the team
219         // NOTE: this defaults to 0
220         if (self.goalentity.netname != "")
221         {
222                 if(autocvar_g_domination_point_amt)
223                         fragamt = autocvar_g_domination_point_amt;
224                 else
225                         fragamt = self.DOMPOINTFRAGS;
226                 TeamScore_AddToTeam(self.goalentity.team, ST_SCORE, fragamt);
227                 TeamScore_AddToTeam(self.goalentity.team, ST_DOM_TICKS, fragamt);
228
229                 // give credit to the individual player, if he is still there
230                 if (self.enemy.playerid == self.enemy_playerid)
231                 {
232                         PlayerScore_Add(self.enemy, SP_SCORE, fragamt);
233                         PlayerScore_Add(self.enemy, SP_DOM_TICKS, fragamt);
234                 }
235                 else
236                         self.enemy = world;
237         }
238 }
239
240 void dompointtouch()
241 {
242         local entity head;
243         if (other.classname != "player")
244                 return;
245         if (other.health < 1)
246                 return;
247
248         if(time < self.captime + 0.3)
249                 return;
250
251         // only valid teams can claim it
252         head = find(world, classname, "dom_team");
253         while (head && head.team != other.team)
254                 head = find(head, classname, "dom_team");
255         if (!head || head.netname == "" || head == self.goalentity)
256                 return;
257
258         // delay capture
259
260         self.team = self.goalentity.team; // this stores the PREVIOUS team!
261
262         self.cnt = other.team;
263         self.owner = head; // team to switch to after the delay
264         self.dmg_inflictor = other;
265
266         // self.state = 1;
267         // self.delay = time + cvar("g_domination_point_capturetime");
268         //self.nextthink = time + cvar("g_domination_point_capturetime");
269         //self.think = dompoint_captured;
270
271         // go to neutral team in the mean time
272         head = find(world, classname, "dom_team");
273         while (head && head.netname != "")
274                 head = find(head, classname, "dom_team");
275         if(head == world)
276                 return;
277
278         WaypointSprite_UpdateSprites(self.sprite, "dom-neut", "", "");
279         WaypointSprite_UpdateTeamRadar(self.sprite, RADARICON_DOMPOINT, '0 1 1');
280         WaypointSprite_Ping(self.sprite);
281
282         self.goalentity = head;
283         self.model = head.mdl;
284         self.modelindex = head.dmg;
285         self.skin = head.skin;
286
287         self.enemy = other; // individual player scoring
288         self.enemy_playerid = other.playerid;
289         dompoint_captured();
290 };
291
292 /*QUAKED spawnfunc_dom_team (0 .5 .8) (-32 -32 -24) (32 32 32)
293 Team declaration for Domination gameplay, this allows you to decide what team
294 names and control point models are used in your map.
295
296 Note: If you use spawnfunc_dom_team entities you must define at least 3 and only two
297 can have netname set!  The nameless team owns all control points at start.
298
299 Keys:
300 "netname"
301  Name of the team (for example Red Team, Blue Team, Green Team, Yellow Team, Life, Death, etc)
302 "cnt"
303  Scoreboard color of the team (for example 4 is red and 13 is blue)
304 "model"
305  Model to use for control points owned by this team (for example
306  "progs/b_g_key.mdl" is a gold keycard, and "progs/b_s_key.mdl" is a silver
307  keycard)
308 "skin"
309  Skin of the model to use (for team skins on a single model)
310 "noise"
311  Sound to play when this team captures a point.
312  (this is a localized sound, like a small alarm or other effect)
313 "noise1"
314  Narrator speech to play when this team captures a point.
315  (this is a global sound, like "Red team has captured a control point")
316 */
317
318 void spawnfunc_dom_team()
319 {
320         if(!g_domination || autocvar_g_domination_teams_override >= 2)
321         {
322                 remove(self);
323                 return;
324         }
325         precache_model(self.model);
326         if (self.noise != "")
327                 precache_sound(self.noise);
328         if (self.noise1 != "")
329                 precache_sound(self.noise1);
330         self.classname = "dom_team";
331         setmodel(self, self.model); // precision not needed
332         self.mdl = self.model;
333         self.dmg = self.modelindex;
334         self.model = "";
335         self.modelindex = 0;
336         // this would have to be changed if used in quakeworld
337         if(self.cnt)
338                 self.team = self.cnt + 1; // WHY are these different anyway?
339 };
340
341 void dom_controlpoint_setup()
342 {
343         local entity head;
344         // find the spawnfunc_dom_team representing unclaimed points
345         head = find(world, classname, "dom_team");
346         while(head && head.netname != "")
347                 head = find(head, classname, "dom_team");
348         if (!head)
349                 objerror("no spawnfunc_dom_team with netname \"\" found\n");
350
351         // copy important properties from spawnfunc_dom_team entity
352         self.goalentity = head;
353         setmodel(self, head.mdl); // precision already set
354         self.skin = head.skin;
355
356         self.cnt = -1;
357
358         if(!self.message)
359                 self.message = " has captured a control point";
360
361         if(self.DOMPOINTFRAGS <= 0)
362                 self.DOMPOINTFRAGS = 1;
363         if(self.wait <= 0)
364                 self.wait = 5;
365
366         float points, waittime;
367         if (autocvar_g_domination_point_amt)
368                 points = autocvar_g_domination_point_amt;
369         else
370                 points = self.frags;
371         if (autocvar_g_domination_point_rate)
372                 waittime = autocvar_g_domination_point_rate;
373         else
374                 waittime = self.wait;
375
376         total_pps += points/waittime;
377
378         if(!self.t_width)
379                 self.t_width = 0.02; // frame animation rate
380         if(!self.t_length)
381                 self.t_length = 239; // maximum frame
382
383         self.think = dompointthink;
384         self.nextthink = time;
385         self.touch = dompointtouch;
386         self.solid = SOLID_TRIGGER;
387         self.flags = FL_ITEM;
388         setsize(self, '-32 -32 -32', '32 32 32');
389         setorigin(self, self.origin + '0 0 20');
390         droptofloor();
391
392         waypoint_spawnforitem(self);
393         WaypointSprite_SpawnFixed("dom-neut", self.origin + '0 0 32', self, sprite, RADARICON_DOMPOINT, '0 1 1');
394 };
395
396
397
398 // player has joined game, get him on a team
399 // depreciated
400 /*void dom_player_join_team(entity pl)
401 {
402         entity head;
403         float c1, c2, c3, c4, totalteams, smallestteam, smallestteam_count, selectedteam;
404         float balance_teams, force_balance, balance_type;
405
406         balance_teams = autocvar_g_balance_teams;
407         balance_teams = autocvar_g_balance_teams_force;
408
409         c1 = c2 = c3 = c4 = -1;
410         totalteams = 0;
411
412         // first find out what teams are allowed
413         head = find(world, classname, "dom_team");
414         while(head)
415         {
416                 if(head.netname != "")
417                 {
418                         //if(head.team == pl.team)
419                         //      selected = head;
420                         if(head.team == COLOR_TEAM1)
421                         {
422                                         c1 = 0;
423                         }
424                         if(head.team == COLOR_TEAM2)
425                         {
426                                         c2 = 0;
427                         }
428                         if(head.team == COLOR_TEAM3)
429                         {
430                                         c3 = 0;
431                         }
432                         if(head.team == COLOR_TEAM4)
433                         {
434                                         c4 = 0;
435                         }
436                 }
437                 head = find(head, classname, "dom_team");
438         }
439
440         // make sure there are at least 2 teams to join
441         if(c1 >= 0)
442                 totalteams = totalteams + 1;
443         if(c2 >= 0)
444                 totalteams = totalteams + 1;
445         if(c3 >= 0)
446                 totalteams = totalteams + 1;
447         if(c4 >= 0)
448                 totalteams = totalteams + 1;
449
450         if(totalteams <= 1)
451                 error("dom_player_join_team: Too few teams available for domination\n");
452
453         // whichever teams that are available are set to 0 instead of -1
454
455         // if we don't care what team he ends up on, put him on whatever team he entered as.
456         // if he's not on a valid team, then put him on the smallest team
457         if(!balance_teams && !force_balance)
458         {
459                 if(     c1 >= 0 && pl.team == COLOR_TEAM1)
460                         selectedteam = pl.team;
461                 else if(c2 >= 0 && pl.team == COLOR_TEAM2)
462                         selectedteam = pl.team;
463                 else if(c3 >= 0 && pl.team == COLOR_TEAM3)
464                         selectedteam = pl.team;
465                 else if(c4 >= 0 && pl.team == COLOR_TEAM4)
466                         selectedteam = pl.team;
467                 else
468                         selectedteam = -1;
469                 if(selectedteam > 0)
470                 {
471                         SetPlayerColors(pl, selectedteam - 1);
472                         return;
473                 }
474                 // otherwise end up on the smallest team (handled below)
475         }
476
477         // now count how many players are on each team already
478
479         head = find(world, classname, "player");
480         while(head)
481         {
482                 //if(head.netname != "")
483                 {
484                         if(head.team == COLOR_TEAM1)
485                         {
486                                 if(c1 >= 0)
487                                         c1 = c1 + 1;
488                         }
489                         if(head.team == COLOR_TEAM2)
490                         {
491                                 if(c2 >= 0)
492                                         c2 = c2 + 1;
493                         }
494                         if(head.team == COLOR_TEAM3)
495                         {
496                                 if(c3 >= 0)
497                                         c3 = c3 + 1;
498                         }
499                         if(head.team == COLOR_TEAM4)
500                         {
501                                 if(c4 >= 0)
502                                         c4 = c4 + 1;
503                         }
504                 }
505                 head = find(head, classname, "player");
506         }
507
508         // c1...c4 now have counts of each team
509         // figure out which is smallest, giving priority to the team the player is already on as a tie-breaker
510
511         smallestteam = 0;
512         smallestteam_count = 999;
513
514         // 2 gives priority to what team you're already on, 1 goes in order
515         balance_type = 1;
516
517         if(balance_type == 1)
518         {
519                 if(c1 >= 0 && c1 < smallestteam_count)
520                 {
521                         smallestteam = 1;
522                         smallestteam_count = c1;
523                 }
524                 if(c2 >= 0 && c2 < smallestteam_count)
525                 {
526                         smallestteam = 2;
527                         smallestteam_count = c2;
528                 }
529                 if(c3 >= 0 && c3 < smallestteam_count)
530                 {
531                         smallestteam = 3;
532                         smallestteam_count = c3;
533                 }
534                 if(c4 >= 0 && c4 < smallestteam_count)
535                 {
536                         smallestteam = 4;
537                         smallestteam_count = c4;
538                 }
539         }
540         else
541         {
542                 if(c1 >= 0 && (c1 < smallestteam_count ||
543                                         (c1 == smallestteam_count && self.team == COLOR_TEAM1) ) )
544                 {
545                         smallestteam = 1;
546                         smallestteam_count = c1;
547                 }
548                 if(c2 >= 0 && c2 < (c2 < smallestteam_count ||
549                                         (c2 == smallestteam_count && self.team == COLOR_TEAM2) ) )
550                 {
551                         smallestteam = 2;
552                         smallestteam_count = c2;
553                 }
554                 if(c3 >= 0 && c3 < (c3 < smallestteam_count ||
555                                         (c3 == smallestteam_count && self.team == COLOR_TEAM3) ) )
556                 {
557                         smallestteam = 3;
558                         smallestteam_count = c3;
559                 }
560                 if(c4 >= 0 && c4 < (c4 < smallestteam_count ||
561                                         (c4 == smallestteam_count && self.team == COLOR_TEAM4) ) )
562                 {
563                         smallestteam = 4;
564                         smallestteam_count = c4;
565                 }
566         }
567
568         if(smallestteam == 1)
569         {
570                 selectedteam = COLOR_TEAM1 - 1;
571         }
572         if(smallestteam == 2)
573         {
574                 selectedteam = COLOR_TEAM2 - 1;
575         }
576         if(smallestteam == 3)
577         {
578                 selectedteam = COLOR_TEAM3 - 1;
579         }
580         if(smallestteam == 4)
581         {
582                 selectedteam = COLOR_TEAM4 - 1;
583         }
584
585         SetPlayerColors(pl, selectedteam);
586 }
587 */
588 /*QUAKED spawnfunc_dom_controlpoint (0 .5 .8) (-16 -16 -24) (16 16 32)
589 Control point for Domination gameplay.
590 */
591 void spawnfunc_dom_controlpoint()
592 {
593         if(!g_domination)
594         {
595                 remove(self);
596                 return;
597         }
598         self.think = dom_controlpoint_setup;
599         self.nextthink = time + 0.1;
600         self.reset = dom_controlpoint_setup;
601
602         if(!self.scale)
603                 self.scale = 0.6;
604
605         //if(!self.glow_size)
606         //      self.glow_size = cvar("g_domination_point_glow");
607         self.effects = self.effects | EF_LOWPRECISION;
608         if (autocvar_g_domination_point_fullbright)
609                 self.effects |= EF_FULLBRIGHT;
610 };
611
612 // code from here on is just to support maps that don't have control point and team entities
613 void dom_spawnteam (string teamname, float teamcolor, string pointmodel, float pointskin, string capsound, string capnarration, string capmessage)
614 {
615         local entity oldself;
616         oldself = self;
617         self = spawn();
618         self.classname = "dom_team";
619         self.netname = teamname;
620         self.cnt = teamcolor;
621         self.model = pointmodel;
622         self.skin = pointskin;
623         self.noise = capsound;
624         self.noise1 = capnarration;
625         self.message = capmessage;
626
627         // this code is identical to spawnfunc_dom_team
628         setmodel(self, self.model); // precision not needed
629         self.mdl = self.model;
630         self.dmg = self.modelindex;
631         self.model = "";
632         self.modelindex = 0;
633         // this would have to be changed if used in quakeworld
634         self.team = self.cnt + 1;
635
636         //eprint(self);
637         self = oldself;
638 };
639
640 void dom_spawnpoint(vector org)
641 {
642         local entity oldself;
643         oldself = self;
644         self = spawn();
645         self.classname = "dom_controlpoint";
646         self.think = spawnfunc_dom_controlpoint;
647         self.nextthink = time;
648         setorigin(self, org);
649         spawnfunc_dom_controlpoint();
650         self = oldself;
651 };
652
653 // spawn some default teams if the map is not set up for domination
654 void dom_spawnteams()
655 {
656         float numteams;
657         if(autocvar_g_domination_teams_override < 2)
658                 numteams = autocvar_g_domination_default_teams;
659         else
660                 numteams = autocvar_g_domination_teams_override;
661         // LordHavoc: edit this if you want to change defaults
662         dom_spawnteam("Red", COLOR_TEAM1-1, "models/domination/dom_red.md3", 0, "domination/claim.wav", "", "Red team has captured a control point");
663         dom_spawnteam("Blue", COLOR_TEAM2-1, "models/domination/dom_blue.md3", 0, "domination/claim.wav", "", "Blue team has captured a control point");
664         if(numteams > 2)
665                 dom_spawnteam("Yellow", COLOR_TEAM3-1, "models/domination/dom_yellow.md3", 0, "domination/claim.wav", "", "Yellow team has captured a control point");
666         if(numteams > 3)
667                 dom_spawnteam("Pink", COLOR_TEAM4-1, "models/domination/dom_pink.md3", 0, "domination/claim.wav", "", "Pink team has captured a control point");
668         dom_spawnteam("", 0, "models/domination/dom_unclaimed.md3", 0, "", "", "");
669 };
670
671 void dom_delayedinit()
672 {
673         local entity head;
674
675         // if no teams are found, spawn defaults, if custom teams are set, use them
676         if (find(world, classname, "dom_team") == world || autocvar_g_domination_teams_override >= 2)
677                 dom_spawnteams();
678         // if no control points are found, spawn defaults
679         if (find(world, classname, "dom_controlpoint") == world)
680         {
681                 // TODO in a few months (maybe 2011/08): change this into error() and remove this very poor dom point selection
682                 backtrace("This map contains no dom_controlpoint entities. A very poor dom point placement will be chosen. Please fix the map.");
683
684                 // if no supported map was found, make every deathmatch spawn a point
685                 head = find(world, classname, "info_player_deathmatch");
686                 while (head)
687                 {
688                         dom_spawnpoint(head.origin);
689                         head = find(head, classname, "info_player_deathmatch");
690                 }
691         }
692
693         ScoreRules_dom();
694 };
695
696 void dom_init()
697 {
698         // we have to precache default models/sounds even if they might not be
699         // used because spawnfunc_worldspawn is executed before any other entities are read,
700         // so we don't even know yet if this map is set up for domination...
701         precache_model("models/domination/dom_red.md3");
702         precache_model("models/domination/dom_blue.md3");
703         precache_model("models/domination/dom_yellow.md3");
704         precache_model("models/domination/dom_pink.md3");
705         precache_model("models/domination/dom_unclaimed.md3");
706         precache_sound("domination/claim.wav");
707         InitializeEntity(world, dom_delayedinit, INITPRIO_GAMETYPE);
708
709         addstat(STAT_DOM_TOTAL_PPS, AS_FLOAT, dom_total_pps);
710         addstat(STAT_DOM_PPS_RED, AS_FLOAT, dom_pps_red);
711         addstat(STAT_DOM_PPS_BLUE, AS_FLOAT, dom_pps_blue);
712         if(c3 >= 0) addstat(STAT_DOM_PPS_YELLOW, AS_FLOAT, dom_pps_yellow);
713         if(c4 >= 0) addstat(STAT_DOM_PPS_PINK, AS_FLOAT, dom_pps_pink);
714 };
715