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