]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_ctf.qc
Merge remote branch 'origin/master' into samual/mutator_ctf
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_ctf.qc
1 // ================================================================
2 //  Official capture the flag game mode coding, reworked by Samual
3 //  Last updated: March 28th, 2011
4 // ================================================================
5
6 // Flag constants 
7 #define FLAG_MIN (PL_MIN + '0 0 -13')
8 #define FLAG_MAX (PL_MAX + '0 0 -13')
9 #define FLAG_CARRY_POS '-15 0 7'
10
11 .entity bot_basewaypoint; // flag waypointsprite
12 .entity wps_flagbase; 
13 .entity wps_flagcarrier;
14 .entity wps_flagdropped;
15
16 entity ctf_worldflaglist; // CTF flags in the map
17 .entity ctf_worldflagnext;
18
19 .vector ctf_spawnorigin; // stored vector for where the flag is placed on the map itself. 
20
21 float ctf_captimerecord; // record time for capturing the flag
22 .float ctf_pickuptime;
23 .float ctf_pickupid;
24 .float ctf_dropperid; // don't allow spam of dropping the flag
25 .float ctf_droptime;
26 .float ctf_status; // status of the flag (FLAG_BASE, FLAG_DROPPED, FLAG_CARRY declared globally)
27
28
29 .float next_take_time; // Delay between when the person can pick up a flag // is this obsolete from the stuff above?
30
31 // CaptureShield: If the player is too bad to be allowed to capture, shield them from taking the flag.
32 .float ctf_captureshielded; // set to 1 if the player is too bad to be allowed to capture
33 float ctf_captureshield_min_negscore; // punish at -20 points
34 float ctf_captureshield_max_ratio; // punish at most 30% of each team
35 float ctf_captureshield_force; // push force of the shield
36
37 // after game mode is finished, these will be changed to use #define with other entities so as to not create many more.
38
39 // ==================
40 // Misc CTF functions
41 // ==================
42
43 float ctf_ReadScore(string parameter) // make this obsolete
44 {
45         if(g_ctf_win_mode != 2)
46                 return cvar(strcat("g_ctf_personal", parameter));
47         else
48                 return cvar(strcat("g_ctf_flag", parameter));
49 }
50
51 void ctf_FakeTimeLimit(entity e, float t)
52 {
53         msg_entity = e;
54         WriteByte(MSG_ONE, 3); // svc_updatestat
55         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
56         if(t < 0)
57                 WriteCoord(MSG_ONE, autocvar_timelimit);
58         else
59                 WriteCoord(MSG_ONE, (t + 1) / 60);
60 }
61
62 void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for easy changing and quick editing later
63 {
64         if(autocvar_sv_eventlog)
65                 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
66 }
67
68
69 // =======================
70 // CaptureShield Functions 
71 // =======================
72
73 float ctf_CaptureShield_CheckStatus(entity p) 
74 {
75         float s, se;
76         entity e;
77         float players_worseeq, players_total;
78
79         if(ctf_captureshield_max_ratio <= 0)
80                 return FALSE;
81
82         s = PlayerScore_Add(p, SP_SCORE, 0);
83         if(s >= -ctf_captureshield_min_negscore)
84                 return FALSE;
85
86         players_total = players_worseeq = 0;
87         FOR_EACH_PLAYER(e)
88         {
89                 if(e.team != p.team)
90                         continue;
91                 se = PlayerScore_Add(e, SP_SCORE, 0);
92                 if(se <= s)
93                         ++players_worseeq;
94                 ++players_total;
95         }
96
97         // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
98         // use this rule here
99         
100         if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
101                 return FALSE;
102
103         return TRUE;
104 }
105
106 void ctf_CaptureShield_Update(entity player, float wanted_status)
107 {
108         float updated_status = ctf_CaptureShield_CheckStatus(player);
109         if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
110         {
111                 if(updated_status) // TODO csqc notifier for this // Samual: How?
112                         centerprint_atprio(player, CENTERPRIO_SHIELDING, "^3You are now ^4shielded^3 from the flag\n^3for ^1too many unsuccessful attempts^3 to capture.\n\n^3Make some defensive scores before trying again.");
113                 else
114                         centerprint_atprio(player, CENTERPRIO_SHIELDING, "^3You are now free.\n\n^3Feel free to ^1try to capture^3 the flag again\n^3if you think you will succeed.");
115                         
116                 player.ctf_captureshielded = updated_status;
117         }
118 }
119
120 float ctf_CaptureShield_Customize()
121 {
122         if not(other.ctf_captureshielded)
123                 return FALSE;
124         if(self.team == other.team)
125                 return FALSE;
126         return TRUE;
127 }
128
129 void ctf_CaptureShield_Touch()
130 {
131         if not(other.ctf_captureshielded)
132                 return;
133         if(self.team == other.team)
134                 return;
135         vector mymid;
136         vector othermid;
137         mymid = (self.absmin + self.absmax) * 0.5;
138         othermid = (other.absmin + other.absmax) * 0.5;
139         Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
140         centerprint_atprio(other, CENTERPRIO_SHIELDING, "^3You are ^4shielded^3 from the flag\n^3for ^1too many unsuccessful attempts^3 to capture.\n\n^3Get some defensive scores before trying again.");
141 }
142
143 void ctf_CaptureShield_Spawn(entity flag)
144 {
145         entity e;
146         e = spawn();
147         e.enemy = self;
148         e.team = self.team;
149         e.touch = ctf_CaptureShield_Touch;
150         e.customizeentityforclient = ctf_CaptureShield_Customize;
151         e.classname = "ctf_captureshield";
152         e.effects = EF_ADDITIVE;
153         e.movetype = MOVETYPE_NOCLIP;
154         e.solid = SOLID_TRIGGER;
155         e.avelocity = '7 0 11';
156         setorigin(e, self.origin);
157         setmodel(e, "models/ctf/shield.md3");
158         e.scale = 0.5;
159         setsize(e, e.scale * e.mins, e.scale * e.maxs);
160 }
161
162
163 // ==============
164 // Event Handlers
165 // ==============
166
167 void ctf_Handle_Drop(entity player)
168 {
169         entity flag = player.flagcarried;
170
171         if(!flag) { return; }
172         if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
173         
174         // reset the flag
175         setattachment(flag, world, "");
176         setorigin(flag, player.origin - '0 0 24' + '0 0 37');
177         flag.owner.flagcarried = world;
178         flag.owner = world;
179         flag.movetype = MOVETYPE_TOSS;
180         flag.solid = SOLID_TRIGGER;
181         flag.takedamage = DAMAGE_YES;
182         flag.velocity = ('0 0 200' + ('0 100 0' * crandom()) + ('100 0 0' * crandom()));
183         flag.pain_finished = time + autocvar_g_ctf_flag_returntime; // replace this later
184         
185         flag.ctf_droptime = time;
186         flag.ctf_dropperid = player.playerid;
187         flag.ctf_status = FLAG_DROPPED;
188
189         // messages and sounds
190         Send_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
191         sound(flag, CH_TRIGGER, flag.noise4, VOL_BASE, ATTN_NONE);
192         ctf_EventLog("dropped", player.team, player);
193         
194         // scoring
195         PlayerTeamScore_AddScore(player, -ctf_ReadScore("penalty_drop"));       
196         PlayerScore_Add(player, SP_CTF_DROPS, 1);
197
198         // waypoints
199         WaypointSprite_Spawn("flagdropped", 0, 0, flag, '0 0 64', world, player.team, flag, wps_flagdropped, FALSE, RADARICON_FLAG, '0 1 1'); // (COLOR_TEAM1 + COLOR_TEAM2 - flag.team)
200         WaypointSprite_Ping(player.wps_flagcarrier);
201         WaypointSprite_Kill(player.wps_flagcarrier);
202
203         // captureshield
204         ctf_CaptureShield_Update(player, 0); // shield only
205
206         // check if the flag will fall off the map
207         trace_startsolid = FALSE;
208         tracebox(flag.origin, flag.mins, flag.maxs, flag.origin, TRUE, flag);
209         if(trace_startsolid)
210                 dprint("FLAG FALLTHROUGH will happen SOON\n");
211 }
212
213 void ctf_Handle_Capture(entity flag, entity player)
214 {
215         // declarations
216         float cap_time, cap_record, success;
217         string cap_message, refername;
218
219         // records
220         if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots)) {
221                 cap_record = ctf_captimerecord;
222                 cap_time = (time - player.flagcarried.ctf_pickuptime);
223
224                 refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
225                 refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's"));
226
227                 if(!ctf_captimerecord) 
228                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; }
229                 else if(cap_time < cap_record) 
230                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; }
231                 else
232                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; }
233
234                 if(success) {
235                         ctf_captimerecord = cap_time;
236                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
237                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
238                         write_recordmarker(player, (time - cap_time), cap_time); } }
239         
240         // messages and sounds
241         Send_KillNotification(player.netname, player.flagcarried.netname, cap_message, INFO_CAPTUREFLAG, MSG_INFO);
242         sound(player, CH_TRIGGER, flag.noise2, VOL_BASE, ATTN_NONE); // "ctf/*_capture.wav"
243         ctf_EventLog("capture", player.flagcarried.team, player);
244         
245         // scoring
246         PlayerTeamScore_AddScore(player, ctf_ReadScore("score_capture"));
247         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
248
249         // effects
250         if (autocvar_g_ctf_flag_capture_effects) 
251         {
252                 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
253                 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
254         }
255
256         // waypointsprites
257         WaypointSprite_Kill(player.wps_flagcarrier);
258
259         // reset the flag
260         if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
261         
262         ctf_RespawnFlag(player.flagcarried);
263 }
264
265 void ctf_Handle_Return(entity flag, entity player)
266 {
267         // messages and sounds
268         Send_KillNotification (player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO);
269         sound(player, CH_TRIGGER, flag.noise1, VOL_BASE, ATTN_NONE);
270         ctf_EventLog("return", flag.team, player);
271
272         // scoring
273         PlayerTeamScore_AddScore(player, ctf_ReadScore(strcat("score_return", ((player.playerid == flag.playerid) ? "_by_killer" : "")))); // reward for return
274         PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
275
276         TeamScore_AddToTeam(((flag.team == COLOR_TEAM1) ? COLOR_TEAM2 : COLOR_TEAM1), ST_SCORE, -ctf_ReadScore("penalty_returned")); // punish the team who was last carrying it
277         FOR_EACH_PLAYER(player) if(player.playerid == flag.ctf_dropperid) // punish the player who dropped the flag
278         {
279                 PlayerScore_Add(player, SP_SCORE, -ctf_ReadScore("penalty_returned"));
280                 ctf_CaptureShield_Update(player, 0); // shield only
281         }
282
283         // waypointsprites 
284         WaypointSprite_Kill(flag.wps_flagdropped);
285         
286         // reset the flag
287         ctf_RespawnFlag(flag);
288 }
289
290 void ctf_Handle_Pickup_Base(entity flag, entity player)
291 {
292         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
293         string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
294
295         // attach the flag to the player
296         flag.owner = player;
297         player.flagcarried = flag;
298         setattachment(flag, player, "");
299         setorigin(flag, FLAG_CARRY_POS);
300         
301         // set up the flag
302         flag.movetype = MOVETYPE_NONE;
303         flag.takedamage = DAMAGE_NO;
304         flag.solid = SOLID_NOT;
305         flag.angles = '0 0 0';
306         flag.ctf_pickuptime = time; // used for timing runs
307         flag.ctf_pickupid = player.playerid;
308         flag.ctf_status = FLAG_CARRY;
309         
310         // messages and sounds
311         Send_KillNotification (player.netname, flag.netname, "", INFO_GOTFLAG, MSG_INFO);
312         sound(player, CH_TRIGGER, flag.noise, VOL_BASE, ATTN_NONE);
313         ctf_EventLog("steal", flag.team, player);
314         verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat("(", player.netname, ")") : ""); // replace TRUE with an autocvar for it.
315         FOR_EACH_PLAYER(tmp_player)
316                 if(tmp_player.team == flag.team)
317                         centerprint(tmp_player, strcat("The enemy ", verbosename, "got your flag! Retrieve it!"));
318                 else if((tmp_player.team == player.team) && (tmp_player != player))
319                         centerprint(tmp_player, strcat("Your team mate ", verbosename, "got the flag! Protect them!"));
320         
321         // scoring
322         PlayerTeamScore_AddScore(player, ctf_ReadScore("score_pickup_base"));
323         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
324         
325         // speedrunning
326         flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
327         if((player.speedrunning) && (ctf_captimerecord))
328                 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
329                 
330         // effects
331         if (autocvar_g_ctf_flag_pickup_effects)
332         {
333                 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1);
334         }
335         
336         // waypoints 
337         WaypointSprite_Spawn("flagcarrier", 0, 0, player, '0 0 64', world, player.team, player, wps_flagcarrier, FALSE, RADARICON_FLAG, '1 1 0'); // (COLOR_TEAM1 + COLOR_TEAM2 - flag.team)
338         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
339         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
340         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
341         WaypointSprite_Ping(player.wps_flagcarrier);
342 }
343  
344 void ctf_Handle_Pickup_Dropped(entity flag, entity player) // make sure this works
345 {
346         // declarations
347         float returnscore = bound(0, (flag.pain_finished - time) / autocvar_g_ctf_flag_returntime, 1); // can this be division by zero?
348         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
349         string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
350
351         // attach the flag to the player
352         flag.owner = player;
353         player.flagcarried = flag;
354         setattachment(flag, player, "");
355         setorigin(flag, FLAG_CARRY_POS);
356         
357         // set up the flag
358         flag.movetype = MOVETYPE_NONE;
359         flag.takedamage = DAMAGE_NO;
360         flag.solid = SOLID_NOT;
361         flag.angles = '0 0 0';
362         //flag.ctf_pickuptime = time; // don't update pickuptime since this isn't a real steal. 
363         flag.ctf_pickupid = player.playerid;
364         flag.ctf_status = FLAG_CARRY;
365
366         // messages and sounds
367         Send_KillNotification (player.netname, flag.netname, "", INFO_PICKUPFLAG, MSG_INFO);
368         sound (player, CH_TRIGGER, flag.noise, VOL_BASE, ATTN_NONE);
369         ctf_EventLog("pickup", flag.team, player);
370         verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat("(", player.netname, ")") : "");
371         FOR_EACH_PLAYER(tmp_player)
372                 if(tmp_player.team == flag.team)
373                         centerprint(tmp_player, strcat("The enemy ", verbosename, "got your flag! Retrieve it!"));
374                 else if((tmp_player.team == player.team) && (tmp_player != player))
375                         centerprint(tmp_player, strcat("Your team mate ", verbosename, "got the flag! Protect them!"));
376
377         // scoring
378         returnscore = floor((ctf_ReadScore("score_pickup_dropped_late") * (1-returnscore) + ctf_ReadScore("score_pickup_dropped_early") * returnscore) + 0.5);
379         print("score is ", ftos(returnscore), "\n");
380         PlayerTeamScore_AddScore(player, returnscore);
381         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
382
383         // effects
384         if (autocvar_g_ctf_flag_pickup_effects) // field pickup effect
385         {
386                 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1); 
387         }
388
389         // waypoints
390         WaypointSprite_Kill(flag.wps_flagdropped);
391         WaypointSprite_Spawn("flagcarrier", 0, 0, player, '0 0 64', world, player.team, player, wps_flagcarrier, FALSE, RADARICON_FLAG, '1 1 0'); // (COLOR_TEAM1 + COLOR_TEAM2 - flag.team)
392         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
393         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
394         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
395         WaypointSprite_Ping(player.wps_flagcarrier);
396 }
397
398
399 // ===================
400 // Main Flag Functions
401 // ===================
402
403 void ctf_FlagThink()
404 {
405         // declarations
406         entity tmp_entity;
407
408         self.nextthink = time + 0.1; // only 10 fps, more is unnecessary.
409
410         // captureshield
411         if(self == ctf_worldflaglist) // only for the first flag
412                 FOR_EACH_CLIENT(tmp_entity)
413                         ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
414
415         // sanity checks
416         if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
417                 dprint("wtf the flag got squished?\n");
418                 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
419                 if(!trace_startsolid) // can we resize it without getting stuck?
420                         setsize(self, FLAG_MIN, FLAG_MAX); }
421
422         if(self.owner.classname != "player" || (self.owner.deadflag) || (self.owner.flagcarried != self)) {
423                 dprint("CANNOT HAPPEN - player dead and STILL had a flag!\n");
424                 ctf_Handle_Drop(self.owner);
425                 return; }
426
427         // main think method
428         switch(self.ctf_status) 
429         {       
430                 case FLAG_BASE: // nothing to do here
431                         return;
432                 
433                 case FLAG_DROPPED:
434                         // flag fallthrough? FIXME remove this if bug is really fixed now
435                         if(self.origin_z < -131072)
436                         {
437                                 dprint("FLAG FALLTHROUGH just happened\n");
438                                 self.pain_finished = 0;
439                         }
440                         setattachment(self, world, "");
441                         if(time > self.pain_finished)
442                         {
443                                 bprint("The ", self.netname, " has returned to base\n");
444                                 sound (self, CH_TRIGGER, self.noise3, VOL_BASE, ATTN_NONE);
445                                 ctf_EventLog("returned", self.team, world);
446                                 ctf_RespawnFlag(self);
447                         }
448                         return;
449                                 
450                 case FLAG_CARRY:
451                         if((self.owner) && (self.speedrunning) && (ctf_captimerecord) && (time >= self.ctf_pickuptime + ctf_captimerecord)) 
452                         {
453                                 bprint("The ", self.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n");
454                                 sound (self, CH_TRIGGER, self.noise3, VOL_BASE, ATTN_NONE);
455
456                                 self.owner.impulse = 141; // returning!
457
458                                 tmp_entity = self;
459                                 self = self.owner;
460                                 ctf_RespawnFlag(tmp_entity);
461                                 ImpulseCommands();
462                                 self = tmp_entity;
463                         }
464                         return;
465
466                 default: // this should never happen
467                         dprint("Think: Flag exists with no status?\n");
468                         return;
469         }
470 }
471
472 void ctf_FlagTouch()
473 {
474         if(gameover) { return; }
475         if(!self) { return; }
476         if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_NOIMPACT)
477         { // The flag fell off the map, respawn it since players can't get to it
478                 //ctf_RespawnFlag(self);
479                 return;
480         }
481         if(other.deadflag != DEAD_NO) { return; }
482         if(other.classname != "player") 
483         {  // The flag just touched an object, most likely the world
484                 pointparticles(particleeffectnum("kaball_sparks"), self.origin, '0 0 0', 1);
485                 sound(self, CH_TRIGGER, "keepaway/touch.wav", VOL_BASE, ATTN_NORM);
486                 return; 
487         }
488         else if(self.wait > time) { return; }
489
490         switch(self.ctf_status) 
491         {       
492                 case FLAG_BASE:
493                         if((other.team == self.team) && (other.flagcarried) && (other.flagcarried.team != self.team))
494                                 ctf_Handle_Capture(self, other); // other just captured the enemies flag to his base
495                         else if((other.team != self.team) && (!other.flagcarried) && (!other.ctf_captureshielded))
496                                 ctf_Handle_Pickup_Base(self, other); // other just stole the enemies flag
497                         break;
498                 
499                 case FLAG_DROPPED:
500                         if(other.team == self.team)
501                                 ctf_Handle_Return(self, other); // other just returned his own flag
502                         else if((!other.flagcarried) && ((other.playerid != self.ctf_dropperid) || (time > self.ctf_droptime + autocvar_g_balance_ctf_delay_collect)))
503                                 ctf_Handle_Pickup_Dropped(self, other); // other just picked up a dropped enemy flag
504                         break;
505                                 
506                 case FLAG_CARRY:
507                         dprint("Someone touched a flag even though it was being carried?\n");
508                         break;
509
510                 default: // this should never happen
511                         dprint("Touch: Flag exists with no status?\n");
512                         break;
513         }
514 }
515
516 void ctf_RespawnFlag(entity flag)
517 {
518         // reset the player (if there is one)
519         if((flag.owner) && (flag.owner.flagcarried == flag))
520         {
521                 WaypointSprite_Kill(flag.wps_flagcarrier);
522                 flag.owner.flagcarried = world;
523
524                 if(flag.speedrunning)
525                         ctf_FakeTimeLimit(flag.owner, -1);
526         }
527
528         // reset the flag
529         setattachment(flag, world, "");
530         setorigin(flag, flag.ctf_spawnorigin); // replace with flag.ctf_spawnorigin
531         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
532         flag.takedamage = DAMAGE_NO;
533         flag.solid = SOLID_TRIGGER;
534         flag.velocity = '0 0 0';
535         flag.angles = flag.mangle;
536         flag.ctf_status = FLAG_BASE;
537         flag.flags = FL_ITEM | FL_NOTARGET;
538         flag.owner = world;
539 }
540
541 void ctf_Reset()
542 {
543         if(self.owner)
544                 if(self.owner.classname == "player")
545                         ctf_Handle_Drop(self.owner);
546                         
547         ctf_RespawnFlag(self);
548 }
549
550 void ctf_SetupFlag(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc 
551 {
552         // declarations
553         teamnumber = fabs(teamnumber - bound(0, g_ctf_reverse, 1)); // if we were originally 1, this will become 0. If we were originally 0, this will become 1. 
554         
555         // main setup
556         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist // todo: find out if this can be simplified
557         ctf_worldflaglist = flag;
558
559         setattachment(flag, world, ""); 
560
561         flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
562         flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue)
563         flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
564         flag.classname = "item_flag_team";
565         flag.target = "###item###"; // wut?
566         flag.flags = FL_ITEM | FL_NOTARGET;
567         flag.solid = SOLID_TRIGGER;
568         flag.velocity = '0 0 0';
569         flag.ctf_status = FLAG_BASE;
570         flag.ctf_spawnorigin = flag.origin; 
571         flag.mangle = flag.angles;
572         flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
573         
574         if(flag.spawnflags & 1) // I don't understand what all this is about.
575         {       
576                 flag.noalign = TRUE;
577                 flag.movetype = MOVETYPE_NONE;
578                 print("This map was loaded with flags using MOVETYPE_NONE\n");
579         }
580         else 
581         { 
582                 flag.noalign = FALSE;
583                 flag.movetype = MOVETYPE_TOSS; 
584                 print("This map was loaded with flags using MOVETYPE_TOSS\n");
585         }       
586         
587         flag.reset = ctf_Reset;
588         flag.touch = ctf_FlagTouch;
589
590         // appearence
591         if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
592         setmodel (flag, flag.model); // precision set below
593         setsize(flag, FLAG_MIN, FLAG_MAX);
594         setorigin(flag, flag.origin);
595         if(!flag.scale) { flag.scale = 0.6; }
596         
597         flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin);
598         
599         if(autocvar_g_ctf_flag_glowtrails)
600         {
601                 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
602                 flag.glow_size = 25;
603                 flag.glow_trail = 1;
604         }
605         
606         flag.effects |= EF_LOWPRECISION;
607         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
608         if(autocvar_g_ctf_dynamiclights)   { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
609         
610         // sound 
611         if(!flag.noise)  { flag.noise  = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
612         if(!flag.noise1) { flag.noise1 = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
613         if(!flag.noise2) { flag.noise2 = ((teamnumber) ? "ctf/red_capture.wav" : "ctf/blue_capture.wav"); } // blue team scores by capturing the red flag
614         if(!flag.noise3) { flag.noise3 = "ctf/flag_respawn.wav"; } // if there is ever a team-based sound for this, update the code to match.
615         if(!flag.noise4) { flag.noise4 = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
616         
617         // precache
618         precache_sound(flag.noise);
619         precache_sound(flag.noise1);
620         precache_sound(flag.noise2);
621         precache_sound(flag.noise3);
622         precache_sound(flag.noise4);
623         precache_model(flag.model);
624         precache_model("models/ctf/shield.md3");
625         precache_model("models/ctf/shockwavetransring.md3");
626
627         // bot waypoints
628         waypoint_spawnforitem_force(flag, flag.origin);
629         flag.nearestwaypointtimeout = 0; // activate waypointing again
630         flag.bot_basewaypoint = flag.nearestwaypoint;
631
632         // waypointsprites
633         WaypointSprite_SpawnFixed(((teamnumber) ? "redbase" : "bluebase"), flag.origin + '0 0 64', flag, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
634         WaypointSprite_UpdateTeamRadar(flag.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
635
636         // captureshield setup
637         ctf_CaptureShield_Spawn(flag);
638 }
639
640
641 // ==============
642 // Hook Functions
643 // ==============
644
645 // g_ctf_ignore_frags
646
647 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
648 {
649         if(self.flagcarried) { ctf_Handle_Drop(self); }
650         return 0;
651 }
652
653 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
654 {
655         entity flag;
656         
657         // initially clear items so they can be set as necessary later.
658         self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST 
659                 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
660
661         // item for stopping players from capturing the flag too often
662         if(self.ctf_captureshielded)
663                 self.items |= IT_CTF_SHIELDED;
664
665         // scan through all the flags and notify the client about them 
666         for (flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
667         {
668                 if(flag.ctf_status == FLAG_CARRY)
669                         if(flag.owner == self)
670                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
671                         else 
672                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
673                 else if(flag.ctf_status == FLAG_DROPPED) 
674                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
675         }
676         
677         return 0;
678 }
679
680 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
681 {       /*
682         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
683         {
684                 if(frag_target == frag_attacker) // damage done to yourself
685                 {
686                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
687                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
688                 }
689                 else // damage done to noncarriers
690                 {
691                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
692                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
693                 }
694         }*/
695         return 0;
696 }
697
698 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
699 {
700         frag_score = 0; // no frags counted in keepaway
701         return (g_ctf_ignore_frags); // you deceptive little bugger ;3 This needs to be true in order for this function to even count. 
702 }
703
704 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
705 {
706         if(autocvar_g_ctf_allow_drop)
707                 ctf_Handle_Drop(self);
708                 
709         return 0;
710 }
711
712 // ==========
713 // Spawnfuncs
714 // ==========
715
716 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
717 CTF Starting point for a player in team one (Red).
718 Keys: "angle" viewing angle when spawning. */
719 void spawnfunc_info_player_team1()
720 {
721         if(g_assault) { remove(self); return; }
722         
723         self.team = COLOR_TEAM1; // red
724         spawnfunc_info_player_deathmatch();
725 }
726
727
728 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
729 CTF Starting point for a player in team two (Blue).
730 Keys: "angle" viewing angle when spawning. */
731 void spawnfunc_info_player_team2()
732 {
733         if(g_assault) { remove(self); return; }
734         
735         self.team = COLOR_TEAM2; // blue
736         spawnfunc_info_player_deathmatch();
737 }
738
739 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
740 CTF Starting point for a player in team three (Yellow).
741 Keys: "angle" viewing angle when spawning. */
742 void spawnfunc_info_player_team3()
743 {
744         if(g_assault) { remove(self); return; }
745         
746         self.team = COLOR_TEAM3; // yellow
747         spawnfunc_info_player_deathmatch();
748 }
749
750
751 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
752 CTF Starting point for a player in team four (Purple).
753 Keys: "angle" viewing angle when spawning. */
754 void spawnfunc_info_player_team4()
755 {
756         if(g_assault) { remove(self); return; }
757         
758         self.team = COLOR_TEAM4; // purple
759         spawnfunc_info_player_deathmatch();
760 }
761
762 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
763 CTF flag for team one (Red). Multiple flags are allowed.
764 Keys: 
765 "angle" Angle the flag will point (minus 90 degrees)... 
766 "model" model to use, note this needs red and blue as skins 0 and 1 (default models/ctf/flag.md3)...
767 "noise" sound played when flag is picked up (default ctf/take.wav)...
768 "noise1" sound played when flag is returned by a teammate (default ctf/return.wav)...
769 "noise2" sound played when flag is captured (default ctf/redcapture.wav)...
770 "noise3" sound played when flag is lost in the field and respawns itself (default ctf/respawn.wav)... */
771 void spawnfunc_item_flag_team1()
772 {
773         if(!g_ctf) { remove(self); return; }
774
775         ctf_SetupFlag(1, self); // 1 = red
776 }
777
778 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
779 CTF flag for team two (Blue). Multiple flags are allowed.
780 Keys: 
781 "angle" Angle the flag will point (minus 90 degrees)... 
782 "model" model to use, note this needs red and blue as skins 0 and 1 (default models/ctf/flag.md3)...
783 "noise" sound played when flag is picked up (default ctf/take.wav)...
784 "noise1" sound played when flag is returned by a teammate (default ctf/return.wav)...
785 "noise2" sound played when flag is captured (default ctf/redcapture.wav)...
786 "noise3" sound played when flag is lost in the field and respawns itself (default ctf/respawn.wav)... */
787 void spawnfunc_item_flag_team2()
788 {
789         if(!g_ctf) { remove(self); return; }
790
791         ctf_SetupFlag(0, self); // the 0 is misleading, but -- 0 = blue.
792 }
793
794 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
795 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
796 Note: If you use spawnfunc_ctf_team entities you must define at least 2!  However, unlike domination, you don't need to make a blank one too.
797 Keys:
798 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
799 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
800 void spawnfunc_ctf_team()
801 {
802         if(!g_ctf) { remove(self); return; }
803         
804         self.classname = "ctf_team";
805         self.team = self.cnt + 1;
806 }
807
808
809 // ==============
810 // Initialization
811 // ==============
812
813 // code from here on is just to support maps that don't have flag and team entities
814 void ctf_SpawnTeam (string teamname, float teamcolor)
815 {
816         entity oldself;
817         oldself = self;
818         self = spawn();
819         self.classname = "ctf_team";
820         self.netname = teamname;
821         self.cnt = teamcolor;
822
823         spawnfunc_ctf_team();
824
825         self = oldself;
826 }
827
828 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
829 {
830         // if no teams are found, spawn defaults
831         if(find(world, classname, "ctf_team") == world)
832         {
833                 print("NO TEAMS FOUND FOR CTF! creating them anyway.\n");
834                 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
835                 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
836         }
837         
838         ScoreRules_ctf();
839 }
840
841 void ctf_Initialize()
842 {
843         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
844
845         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
846         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
847         ctf_captureshield_force = autocvar_g_ctf_shield_force;
848
849         g_ctf_win_mode = cvar("g_ctf_win_mode");
850         
851         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
852 }
853
854
855 MUTATOR_DEFINITION(gamemode_ctf)
856 {
857         MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
858         MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
859         MUTATOR_HOOK(PlayerDies, ctf_RemovePlayer, CBC_ORDER_ANY);
860         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
861         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
862         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
863         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
864         //MUTATOR_HOOK(PlayerPowerups, ctf_PlayerPowerups, CBC_ORDER_ANY);
865
866         MUTATOR_ONADD
867         {
868                 if(time > 1) // game loads at time 1
869                         error("This is a game type and it cannot be added at runtime.");
870                 g_ctf = 1;
871                 ctf_Initialize();
872         }
873
874         MUTATOR_ONREMOVE
875         {
876                 g_ctf = 0;
877                 error("This is a game type and it cannot be removed at runtime.");
878         }
879
880         return 0;
881 }