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