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