]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_ctf.qc
Fix record keeping for flag capture times
[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 30th, 2012
4 // ================================================================
5
6 float ctf_ReadScore(string parameter) // make this obsolete
7 {
8         //if(g_ctf_win_mode != 2)
9                 return cvar(strcat("g_ctf_personal", parameter));
10         //else
11         //      return cvar(strcat("g_ctf_flag", parameter));
12 }
13
14 void ctf_FakeTimeLimit(entity e, float t)
15 {
16         msg_entity = e;
17         WriteByte(MSG_ONE, 3); // svc_updatestat
18         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
19         if(t < 0)
20                 WriteCoord(MSG_ONE, autocvar_timelimit);
21         else
22                 WriteCoord(MSG_ONE, (t + 1) / 60);
23 }
24
25 void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for easy changing and quick editing later
26 {
27         if(autocvar_sv_eventlog)
28                 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
29 }
30
31 string ctf_CaptureRecord(entity flag, entity player)
32 {
33         float cap_time, cap_record, success;
34         string cap_message, refername;
35         
36         if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots)) 
37         {
38                 cap_record = ctf_captimerecord;
39                 cap_time = (time - flag.ctf_pickuptime);
40
41                 refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
42                 refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's"));
43
44                 if(!ctf_captimerecord) 
45                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; }
46                 else if(cap_time < cap_record) 
47                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; }
48                 else
49                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; }
50
51                 if(success) 
52                 {
53                         ctf_captimerecord = cap_time;
54                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
55                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
56                         write_recordmarker(player, (time - cap_time), cap_time); 
57                 } 
58         }
59         
60         return cap_message;
61 }
62
63 void ctf_AnnounceStolenFlag(entity flag, entity player)
64 {
65         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
66         string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
67         
68         verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
69         
70         FOR_EACH_PLAYER(tmp_player)
71                 if(tmp_player == player)
72                         centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
73                 else if(tmp_player.team == player.team)
74                         centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
75                 else if(tmp_player.team == flag.team)
76                         centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
77 }
78
79
80 // =======================
81 // CaptureShield Functions 
82 // =======================
83
84 float ctf_CaptureShield_CheckStatus(entity p) 
85 {
86         float s, se;
87         entity e;
88         float players_worseeq, players_total;
89
90         if(ctf_captureshield_max_ratio <= 0)
91                 return FALSE;
92
93         s = PlayerScore_Add(p, SP_SCORE, 0);
94         if(s >= -ctf_captureshield_min_negscore)
95                 return FALSE;
96
97         players_total = players_worseeq = 0;
98         FOR_EACH_PLAYER(e)
99         {
100                 if(e.team != p.team)
101                         continue;
102                 se = PlayerScore_Add(e, SP_SCORE, 0);
103                 if(se <= s)
104                         ++players_worseeq;
105                 ++players_total;
106         }
107
108         // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
109         // use this rule here
110         
111         if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
112                 return FALSE;
113
114         return TRUE;
115 }
116
117 void ctf_CaptureShield_Update(entity player, float wanted_status)
118 {
119         float updated_status = ctf_CaptureShield_CheckStatus(player);
120         if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
121         {
122                 if(updated_status) // TODO csqc notifier for this // Samual: How?
123                         Send_CSQC_Centerprint_Generic(player, CPID_CTF_CAPTURESHIELD, "^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.", 5, 0);
124                 else
125                         Send_CSQC_Centerprint_Generic(player, CPID_CTF_CAPTURESHIELD, "^3You are now free.\n\n^3Feel free to ^1try to capture^3 the flag again\n^3if you think you will succeed.", 5, 0);
126                         
127                 player.ctf_captureshielded = updated_status;
128         }
129 }
130
131 float ctf_CaptureShield_Customize()
132 {
133         if not(other.ctf_captureshielded) { return FALSE; }
134         if(self.team == other.team) { return FALSE; }
135         
136         return TRUE;
137 }
138
139 void ctf_CaptureShield_Touch()
140 {
141         if not(other.ctf_captureshielded) { return; }
142         if(self.team == other.team) { return; }
143         
144         vector mymid = (self.absmin + self.absmax) * 0.5;
145         vector othermid = (other.absmin + other.absmax) * 0.5;
146
147         Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
148         Send_CSQC_Centerprint_Generic(other, CPID_CTF_CAPTURESHIELD, "^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.", 5, 0);
149 }
150
151 void ctf_CaptureShield_Spawn(entity flag)
152 {
153         entity shield = spawn();
154         
155         shield.enemy = self;
156         shield.team = self.team;
157         shield.touch = ctf_CaptureShield_Touch;
158         shield.customizeentityforclient = ctf_CaptureShield_Customize;
159         shield.classname = "ctf_captureshield";
160         shield.effects = EF_ADDITIVE;
161         shield.movetype = MOVETYPE_NOCLIP;
162         shield.solid = SOLID_TRIGGER;
163         shield.avelocity = '7 0 11';
164         shield.scale = 0.5;
165         
166         setorigin(shield, self.origin);
167         setmodel(shield, "models/ctf/shield.md3");
168         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
169 }
170
171 // ====================
172 // Drop/Pass/Throw Code
173 // ====================
174
175 void ctf_Handle_Failed_Pass(entity flag)
176 {
177         print("ctf_Handle_Failed_Pass(entity flag) called.\n");
178         entity sender = flag.pass_sender;
179
180         flag.movetype = MOVETYPE_TOSS;
181         flag.takedamage = DAMAGE_YES;
182         flag.health = flag.max_flag_health;
183         flag.ctf_droptime = time;
184         flag.ctf_dropper = sender;
185         flag.ctf_status = FLAG_DROPPED;
186         
187         // messages and sounds
188         Send_KillNotification(sender.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
189         sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
190         ctf_EventLog("dropped", sender.team, sender);
191
192         // scoring
193         PlayerTeamScore_AddScore(sender, -ctf_ReadScore("penalty_drop"));       
194         PlayerScore_Add(sender, SP_CTF_DROPS, 1);
195         
196         // waypoints
197         if(autocvar_g_ctf_flag_dropped_waypoint)
198                 WaypointSprite_Spawn("flagdropped", 0, 0, flag, '0 0 64', world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : sender.team), flag, wps_flagdropped, FALSE, RADARICON_FLAG, '0 0.5 0' + ((flag.team == COLOR_TEAM1) ? '0.75 0 0' : '0 0 0.75'));
199
200         if(autocvar_g_ctf_flag_returntime || (autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health))
201         {
202                 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
203                 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
204         }
205         
206         sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
207         
208         flag.pass_sender = world;
209         flag.pass_target = world;
210 }
211
212 void ctf_Handle_Retrieve(entity flag, entity player)
213 {
214         print("ctf_Handle_Retrieve(entity flag, entity player) called.\n");
215         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
216         entity sender = flag.pass_sender;
217         
218         // transfer flag to player
219         flag.ctf_carrier = player;
220         flag.owner = player;
221         flag.owner.flagcarried = flag;
222         
223         // reset flag
224         setattachment(flag, player, "");
225         setorigin(flag, FLAG_CARRY_OFFSET);
226         flag.movetype = MOVETYPE_NONE;
227         flag.takedamage = DAMAGE_NO;
228         flag.solid = SOLID_NOT;
229         flag.ctf_carrier = player;
230         flag.ctf_status = FLAG_CARRY;
231
232         // messages and sounds
233         sound(player, CH_TRIGGER, "keepaway/respawn.wav", VOL_BASE, ATTN_NORM);
234         ctf_EventLog("recieve", flag.team, player);
235         FOR_EACH_PLAYER(tmp_player)
236                 if(tmp_player == sender)
237                         centerprint(tmp_player, strcat("You passed the ", flag.netname, " to ", player.netname));
238                 else if(tmp_player == player)
239                         centerprint(tmp_player, strcat("You recieved the ", flag.netname, " from ", sender.netname));
240                 else if(tmp_player.team == sender.team)
241                         centerprint(tmp_player, strcat(sender.netname, " passed the ", flag.netname, " to ", player.netname));
242         
243         // create new waypoint
244         WaypointSprite_Spawn("flagcarrier", 0, 0, player, '0 0 64', world, player.team, player, wps_flagcarrier, FALSE, RADARICON_FLAG, '1 1 0');
245         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
246         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
247         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
248         
249         sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
250         player.throw_antispam = sender.throw_antispam;
251         
252         flag.pass_sender = world;
253         flag.pass_target = world;
254 }
255
256 void ctf_Handle_Throw(entity player, entity reciever, float droptype)
257 {
258         entity flag = player.flagcarried;
259         
260         if(!flag) { return; }
261         if((droptype == DROPTYPE_PASS) && !reciever) { return; }
262         
263         //if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
264         
265         // reset the flag
266         setattachment(flag, world, "");
267         setorigin(flag, player.origin + FLAG_DROP_OFFSET);
268         flag.owner.flagcarried = world;
269         flag.owner = world;
270         flag.solid = SOLID_TRIGGER;
271         flag.ctf_droptime = time;
272         
273         switch(droptype)
274         {
275                 case DROPTYPE_PASS:
276                 {
277                         vector targ_origin = (0.5 * (reciever.absmin + reciever.absmax));
278                         flag.velocity = (normalize(targ_origin - player.origin) * autocvar_g_ctf_throw_velocity);
279                         break;
280                 }
281                 
282                 case DROPTYPE_THROW:
283                 {
284                         makevectors((player.v_angle_y * '0 1 0') + (player.v_angle_x * '0.5 0 0'));
285                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + (v_forward * autocvar_g_ctf_throw_velocity)), FALSE);
286                         break;
287                 }
288                 
289                 default:
290                 case DROPTYPE_DROP:
291                 {
292                         flag.velocity = ('0 0 200' + ('0 100 0' * crandom()) + ('100 0 0' * crandom()));
293                         break;
294                 }
295         }
296         
297         switch(droptype)
298         {
299                 case DROPTYPE_PASS:
300                 {
301                         // main
302                         flag.movetype = MOVETYPE_FLY;
303                         flag.takedamage = DAMAGE_NO;
304                         flag.pass_sender = player;
305                         flag.pass_target = reciever;
306                         flag.ctf_status = FLAG_PASSING;
307                         
308                         // other
309                         sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM);
310                         ctf_EventLog("pass", flag.team, player);
311                         te_lightning2(world, reciever.origin, player.origin);
312                         break;
313                 }
314                 
315                 default:
316                 case DROPTYPE_THROW:
317                 case DROPTYPE_DROP:
318                 {
319                         // main
320                         flag.movetype = MOVETYPE_TOSS;
321                         flag.takedamage = DAMAGE_YES;
322                         flag.health = flag.max_flag_health;
323                         flag.ctf_dropper = player;
324                         flag.ctf_status = FLAG_DROPPED;
325                         
326                         // messages and sounds
327                         Send_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
328                         sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
329                         ctf_EventLog("dropped", player.team, player);
330
331                         // scoring
332                         PlayerTeamScore_AddScore(player, -ctf_ReadScore("penalty_drop"));       
333                         PlayerScore_Add(player, SP_CTF_DROPS, 1);
334                         
335                         // waypoints
336                         if(autocvar_g_ctf_flag_dropped_waypoint)
337                                 WaypointSprite_Spawn("flagdropped", 0, 0, flag, '0 0 64', world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, FALSE, RADARICON_FLAG, '0 0.5 0' + ((flag.team == COLOR_TEAM1) ? '0.75 0 0' : '0 0 0.75')); // (COLOR_TEAM1 + COLOR_TEAM2 - flag.team)
338
339                         if(autocvar_g_ctf_flag_returntime || (autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health))
340                         {
341                                 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
342                                 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
343                         }
344                         break;
345                 }
346         }
347
348         // kill old waypointsprite
349         WaypointSprite_Ping(player.wps_flagcarrier);
350         WaypointSprite_Kill(player.wps_flagcarrier);
351         
352         // captureshield
353         //ctf_CaptureShield_Update(player, 0); // shield only
354 }
355
356
357 // ==============
358 // Event Handlers
359 // ==============
360
361 void ctf_Handle_Dropped_Capture(entity flag, entity enemy_flag)
362 {
363         // declarations
364         string cap_message;
365         entity player = enemy_flag.ctf_dropper;
366         
367         if not(player) { return; } // without someone to give the reward to, we can't possibly cap
368         
369         // messages and sounds
370         Send_KillNotification(player.netname, enemy_flag.netname, ctf_CaptureRecord(enemy_flag, player), INFO_CAPTUREFLAG, MSG_INFO);
371         sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
372         ctf_EventLog("droppedcapture", enemy_flag.team, player);
373         
374         // scoring
375         PlayerTeamScore_AddScore(player, ctf_ReadScore("score_capture"));
376         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
377
378         // effects
379         if(autocvar_g_ctf_flag_capture_effects) 
380         {
381                 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
382                 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
383         }
384         
385         // reset the flag
386         ctf_RespawnFlag(enemy_flag);
387 }
388
389 void ctf_Handle_Capture(entity flag, entity player)
390 {
391         // messages and sounds
392         Send_KillNotification(player.netname, player.flagcarried.netname, ctf_CaptureRecord(player.flagcarried, player), INFO_CAPTUREFLAG, MSG_INFO);
393         sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
394         ctf_EventLog("capture", player.flagcarried.team, player);
395         
396         // scoring
397         PlayerTeamScore_AddScore(player, ctf_ReadScore("score_capture"));
398         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
399
400         // effects
401         if(autocvar_g_ctf_flag_capture_effects) 
402         {
403                 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
404                 //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
405         }
406
407         // waypointsprites
408         WaypointSprite_Kill(player.wps_flagcarrier);
409
410         // reset the flag
411         if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
412         
413         ctf_RespawnFlag(player.flagcarried);
414 }
415
416 void ctf_Handle_Return(entity flag, entity player)
417 {
418         // messages and sounds
419         //centerprint(player, strcat("You returned ", flag.netname));
420         Send_KillNotification (player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO);
421         sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
422         ctf_EventLog("return", flag.team, player);
423
424         // scoring
425         PlayerTeamScore_AddScore(player, ctf_ReadScore("score_return")); // reward for return
426         PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
427
428         TeamScore_AddToTeam(((flag.team == COLOR_TEAM1) ? COLOR_TEAM2 : COLOR_TEAM1), ST_SCORE, -ctf_ReadScore("penalty_returned")); // punish the team who was last carrying it
429         FOR_EACH_PLAYER(player) if(player == flag.ctf_dropper) // punish the player who dropped the flag
430         {
431                 PlayerScore_Add(player, SP_SCORE, -ctf_ReadScore("penalty_returned"));
432                 ctf_CaptureShield_Update(player, 0); // shield only
433         }
434         
435         // reset the flag
436         ctf_RespawnFlag(flag);
437 }
438
439 void ctf_Handle_Pickup_Base(entity flag, entity player)
440 {
441         // attach the flag to the player
442         flag.owner = player;
443         player.flagcarried = flag;
444         setattachment(flag, player, "");
445         setorigin(flag, FLAG_CARRY_OFFSET);
446         
447         // set up the flag
448         flag.movetype = MOVETYPE_NONE;
449         flag.takedamage = DAMAGE_NO;
450         flag.solid = SOLID_NOT;
451         flag.angles = '0 0 0';
452         flag.ctf_pickuptime = time; // used for timing runs
453         flag.ctf_carrier = player;
454         flag.ctf_status = FLAG_CARRY;
455         
456         // messages and sounds
457         Send_KillNotification (player.netname, flag.netname, "", INFO_GOTFLAG, MSG_INFO);
458         sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
459         ctf_EventLog("steal", flag.team, player);
460         ctf_AnnounceStolenFlag(flag, player);
461         
462         // scoring
463         PlayerTeamScore_AddScore(player, ctf_ReadScore("score_pickup_base"));
464         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
465         
466         // speedrunning
467         flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
468         if((player.speedrunning) && (ctf_captimerecord))
469                 ctf_FakeTimeLimit(player, time + ctf_captimerecord);
470                 
471         // effects
472         if (autocvar_g_ctf_flag_pickup_effects)
473         {
474                 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1);
475         }
476         
477         // waypoints 
478         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)
479         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
480         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
481         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
482         WaypointSprite_Ping(player.wps_flagcarrier);
483 }
484  
485 void ctf_Handle_Pickup_Dropped(entity flag, entity player)
486 {
487         // declarations
488         float returnscore = (autocvar_g_ctf_flag_returntime ? bound(0, ((flag.ctf_droptime + autocvar_g_ctf_flag_returntime) - time) / autocvar_g_ctf_flag_returntime, 1) : 1);
489
490         // attach the flag to the player
491         flag.owner = player;
492         player.flagcarried = flag;
493         setattachment(flag, player, "");
494         setorigin(flag, FLAG_CARRY_OFFSET);
495         
496         // set up the flag
497         flag.movetype = MOVETYPE_NONE;
498         flag.takedamage = DAMAGE_NO;
499         flag.health = flag.max_flag_health;
500         flag.solid = SOLID_NOT;
501         flag.angles = '0 0 0';
502         //flag.ctf_pickuptime = time; // don't update pickuptime since this isn't a real steal. 
503         flag.ctf_carrier = player;
504         flag.ctf_status = FLAG_CARRY;
505
506         // messages and sounds
507         Send_KillNotification(player.netname, flag.netname, "", INFO_PICKUPFLAG, MSG_INFO);
508         sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
509         ctf_EventLog("pickup", flag.team, player);
510         ctf_AnnounceStolenFlag(flag, player);
511                         
512         // scoring
513         returnscore = floor((ctf_ReadScore("score_pickup_dropped_late") * (1-returnscore) + ctf_ReadScore("score_pickup_dropped_early") * returnscore) + 0.5);
514         print("score is ", ftos(returnscore), "\n");
515         PlayerTeamScore_AddScore(player, returnscore);
516         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
517
518         // effects
519         if(autocvar_g_ctf_flag_pickup_effects) // field pickup effect
520         {
521                 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1); 
522         }
523
524         // waypoints
525         WaypointSprite_Kill(flag.wps_flagdropped);
526         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)
527         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
528         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
529         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, '1 1 0');
530         WaypointSprite_Ping(player.wps_flagcarrier);
531 }
532
533
534 // ===================
535 // Main Flag Functions
536 // ===================
537
538 void ctf_CheckFlagReturn(entity flag)
539 {
540         if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
541         
542         if((flag.health <= 0) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_returntime))
543         {
544                 bprint("The ", flag.netname, " has returned to base\n");
545                 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
546                 ctf_EventLog("returned", flag.team, world);
547                 ctf_RespawnFlag(flag);
548         }
549 }
550
551 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
552 {
553         if(ITEM_DAMAGE_NEEDKILL(deathtype))
554         {
555                 // automatically kill the flag and return it
556                 self.health = 0;
557                 ctf_CheckFlagReturn(self);
558         }
559         
560         if(autocvar_g_ctf_flag_take_damage) 
561         {
562                 self.health = self.health - damage;
563                 ctf_CheckFlagReturn(self);
564         }
565 }
566
567 void ctf_FlagThink()
568 {
569         // declarations
570         entity tmp_entity;
571
572         self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
573
574         // captureshield
575         if(self == ctf_worldflaglist) // only for the first flag
576                 FOR_EACH_CLIENT(tmp_entity)
577                         ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
578
579         // sanity checks
580         if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
581                 dprint("wtf the flag got squashed?\n");
582                 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
583                 if(!trace_startsolid) // can we resize it without getting stuck?
584                         setsize(self, FLAG_MIN, FLAG_MAX); }
585
586         // main think method
587         switch(self.ctf_status)
588         {       
589                 case FLAG_BASE:
590                 {
591                         if(autocvar_g_ctf_dropped_capture_radius)
592                         {
593                                 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
594                                         if(tmp_entity.ctf_status == FLAG_DROPPED)
595                                                 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
596                                                         ctf_Handle_Dropped_Capture(self, tmp_entity);
597                         }
598                         return;
599                 }
600                 
601                 case FLAG_DROPPED:
602                 {
603                         if(autocvar_g_ctf_flag_returntime)
604                         {
605                                 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_returntime) * FLAG_THINKRATE);
606                                 ctf_CheckFlagReturn(self);
607                         } 
608                         return;
609                 }
610                         
611                 case FLAG_CARRY:
612                 {
613                         if((self.owner) && (self.speedrunning) && (ctf_captimerecord) && (time >= self.ctf_pickuptime + ctf_captimerecord)) 
614                         {
615                                 bprint("The ", self.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n");
616                                 sound(self, CH_TRIGGER, self.snd_flag_respawn, VOL_BASE, ATTN_NONE);
617                                 ctf_EventLog("returned", self.team, world);
618                                 ctf_RespawnFlag(tmp_entity);
619
620                                 tmp_entity = self;
621                                 self = self.owner;
622                                 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
623                                 ImpulseCommands();
624                                 self = tmp_entity;
625                         }
626                         return;
627                 }
628                 
629                 case FLAG_PASSING:
630                 {                       
631                         vector targ_origin = (0.5 * (self.pass_target.absmin + self.pass_target.absmax));
632                         
633                         traceline(self.origin, targ_origin, MOVE_NOMONSTERS, self);
634                         
635                         if((self.pass_target.deadflag != DEAD_NO)
636                                 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
637                                 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
638                                 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
639                         {
640                                 ctf_Handle_Failed_Pass(self);
641                         }
642                         else // still a viable target, go for it
643                         {
644                                 vector desired_direction = normalize(targ_origin - self.origin);
645                                 vector current_direction = normalize(self.velocity);
646                                 
647                                 self.velocity = (normalize(current_direction + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_throw_velocity); 
648                         }
649                         return;
650                 }
651
652                 default: // this should never happen
653                 {
654                         dprint("ctf_FlagThink(): Flag exists with no status?\n");
655                         return;
656                 }
657         }
658 }
659
660 void ctf_FlagTouch()
661 {
662         if(gameover) { return; }
663         if(!self) { return; }
664         if(other.deadflag != DEAD_NO) { return; }
665         if(ITEM_TOUCH_NEEDKILL())
666         {
667                 // automatically kill the flag and return it
668                 self.health = 0;
669                 ctf_CheckFlagReturn(self);
670                 return;
671         }
672         if(other.classname != "player") // The flag just touched an object, most likely the world
673         {
674                 if(time > self.wait) // if we haven't in a while, play a sound/effect
675                 {
676                         pointparticles(particleeffectnum("kaball_sparks"), self.origin, '0 0 0', 1);
677                         sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
678                 }
679                 return;
680         }
681
682         switch(self.ctf_status) 
683         {       
684                 case FLAG_BASE:
685                 {
686                         if((other.team == self.team) && (other.flagcarried) && (other.flagcarried.team != self.team))
687                                 ctf_Handle_Capture(self, other); // other just captured the enemies flag to his base
688                         else if((other.team != self.team) && (!other.flagcarried) && (!other.ctf_captureshielded))
689                                 ctf_Handle_Pickup_Base(self, other); // other just stole the enemies flag
690                         break;
691                 }
692                 
693                 case FLAG_DROPPED:
694                 {
695                         if(other.team == self.team)
696                                 ctf_Handle_Return(self, other); // other just returned his own flag
697                         else if((!other.flagcarried) && ((other != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
698                                 ctf_Handle_Pickup_Dropped(self, other); // other just picked up a dropped enemy flag
699                         break;
700                 }
701                         
702                 case FLAG_CARRY:
703                 {
704                         dprint("Someone touched a flag even though it was being carried?\n");
705                         break;
706                 }
707                 
708                 case FLAG_PASSING:
709                 {
710                         if((other.classname == "player") && (other.deadflag == DEAD_NO) && (other != self.pass_sender))
711                         {
712                                 if(IsDifferentTeam(other, self.pass_sender))
713                                         ctf_Handle_Return(self, other);
714                                 else
715                                         ctf_Handle_Retrieve(self, other);
716                         }
717                         break;
718                 }
719                 
720                 default: // this should never happen
721                 {
722                         dprint("Touch: Flag exists with no status?\n");
723                         break;
724                 }
725         }
726         
727         self.wait = time + FLAG_TOUCHRATE;
728 }
729
730 void ctf_RespawnFlag(entity flag)
731 {
732         // reset the player (if there is one)
733         if((flag.owner) && (flag.owner.flagcarried == flag))
734         {
735                 WaypointSprite_Kill(flag.wps_flagcarrier);
736                 flag.owner.flagcarried = world;
737
738                 if(flag.speedrunning)
739                         ctf_FakeTimeLimit(flag.owner, -1);
740         }
741
742         if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
743                 { WaypointSprite_Kill(flag.wps_flagdropped); }
744
745         // reset the flag
746         setattachment(flag, world, "");
747         setorigin(flag, flag.ctf_spawnorigin);
748         
749         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
750         flag.takedamage = DAMAGE_NO;
751         flag.health = flag.max_flag_health;
752         flag.solid = SOLID_TRIGGER;
753         flag.velocity = '0 0 0';
754         flag.angles = flag.mangle;
755         flag.flags = FL_ITEM | FL_NOTARGET;
756         
757         flag.ctf_status = FLAG_BASE;
758         flag.owner = world;
759         flag.pass_sender = world;
760         flag.pass_target = world;
761         flag.ctf_carrier = world;
762         flag.ctf_dropper = world;
763         flag.ctf_pickuptime = 0;
764         flag.ctf_droptime = 0;
765 }
766
767 void ctf_Reset()
768 {
769         if(self.owner)
770                 if(self.owner.classname == "player")
771                         ctf_Handle_Throw(self.owner, world, DROPTYPE_DROP);
772                         
773         ctf_RespawnFlag(self);
774 }
775
776 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
777 {
778         // declarations
779         float teamnumber = ((self.team == COLOR_TEAM1) ? TRUE : FALSE); // if we were originally 1, this will become 0. If we were originally 0, this will become 1. 
780
781         // bot waypoints
782         waypoint_spawnforitem_force(self, self.origin);
783         self.nearestwaypointtimeout = 0; // activate waypointing again
784         self.bot_basewaypoint = self.nearestwaypoint;
785
786         // waypointsprites
787         WaypointSprite_SpawnFixed(((teamnumber) ? "redbase" : "bluebase"), self.origin + '0 0 64', self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
788         WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2) - 1, FALSE));
789
790         // captureshield setup
791         ctf_CaptureShield_Spawn(self);
792 }
793
794 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc 
795 {       
796         // declarations
797         teamnumber = fabs(teamnumber - bound(0, autocvar_g_ctf_reverse, 1)); // if we were originally 1, this will become 0. If we were originally 0, this will become 1. 
798         self = flag; // for later usage with droptofloor()
799         
800         // main setup
801         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
802         ctf_worldflaglist = flag;
803
804         setattachment(flag, world, ""); 
805
806         flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
807         flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue)
808         flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
809         flag.classname = "item_flag_team";
810         flag.target = "###item###"; // wut?
811         flag.flags = FL_ITEM | FL_NOTARGET;
812         flag.solid = SOLID_TRIGGER;
813         flag.takedamage = DAMAGE_NO;
814         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;   
815         flag.max_flag_health = ((autocvar_g_ctf_flag_take_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
816         flag.health = flag.max_flag_health;
817         flag.event_damage = ctf_FlagDamage;
818         flag.pushable = TRUE;
819         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
820         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
821         flag.velocity = '0 0 0';
822         flag.mangle = flag.angles;
823         flag.reset = ctf_Reset;
824         flag.touch = ctf_FlagTouch;
825         flag.think = ctf_FlagThink;
826         flag.nextthink = time + FLAG_THINKRATE;
827         flag.ctf_status = FLAG_BASE;
828         
829         if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
830         if(!flag.scale) { flag.scale = FLAG_SCALE; }
831         if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
832         
833         // sound 
834         if(!flag.snd_flag_taken) { flag.snd_flag_taken  = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
835         if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
836         if(!flag.snd_flag_capture) { flag.snd_flag_capture = ((teamnumber) ? "ctf/red_capture.wav" : "ctf/blue_capture.wav"); } // blue team scores by capturing the red flag
837         if(!flag.snd_flag_respawn) { flag.snd_flag_respawn = "ctf/flag_respawn.wav"; } // if there is ever a team-based sound for this, update the code to match.
838         if(!flag.snd_flag_dropped) { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
839         if(!flag.snd_flag_touch) { flag.snd_flag_touch = "keepaway/touch.wav"; } // again has no team-based sound // FIXME
840         
841         // precache
842         precache_sound(flag.snd_flag_taken);
843         precache_sound(flag.snd_flag_returned);
844         precache_sound(flag.snd_flag_capture);
845         precache_sound(flag.snd_flag_respawn);
846         precache_sound(flag.snd_flag_dropped);
847         precache_sound(flag.snd_flag_touch);
848         precache_model(flag.model);
849         precache_model("models/ctf/shield.md3");
850         precache_model("models/ctf/shockwavetransring.md3");
851
852         // appearence
853         setmodel(flag, flag.model); // precision set below
854         setsize(flag, FLAG_MIN, FLAG_MAX);
855         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
856         
857         if(autocvar_g_ctf_flag_glowtrails)
858         {
859                 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
860                 flag.glow_size = 25;
861                 flag.glow_trail = 1;
862         }
863         
864         flag.effects |= EF_LOWPRECISION;
865         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
866         if(autocvar_g_ctf_dynamiclights)   { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
867         
868         // flag placement
869         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
870         {       
871                 flag.dropped_origin = flag.origin; 
872                 flag.noalign = TRUE;
873                 flag.movetype = MOVETYPE_NONE;
874         }
875         else // drop to floor, automatically find a platform and set that as spawn origin
876         { 
877                 flag.noalign = FALSE;
878                 self = flag;
879                 droptofloor();
880                 flag.movetype = MOVETYPE_TOSS; 
881         }       
882         
883         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
884 }
885
886
887 // ==============
888 // Hook Functions
889 // ==============
890
891 MUTATOR_HOOKFUNCTION(ctf_HookedDrop)
892 {
893         if(self.flagcarried) { ctf_Handle_Throw(self, world, DROPTYPE_DROP); }
894         return 0;
895 }
896
897 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
898 {
899         entity flag;
900         
901         // initially clear items so they can be set as necessary later.
902         self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST 
903                 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
904
905         // scan through all the flags and notify the client about them 
906         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
907         {
908                 if(flag.ctf_status == FLAG_CARRY)
909                         if(flag.owner == self)
910                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
911                         else 
912                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
913                 else if(flag.ctf_status == FLAG_DROPPED) 
914                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
915         }
916         
917         // item for stopping players from capturing the flag too often
918         if(self.ctf_captureshielded)
919                 self.items |= IT_CTF_SHIELDED;
920         
921         // update the health of the flag carrier waypointsprite
922         if(self.wps_flagcarrier) 
923                 WaypointSprite_UpdateHealth(self.wps_flagcarrier, self.health);
924         
925         return 0;
926 }
927
928 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
929 {
930         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
931         {
932                 if(frag_target == frag_attacker) // damage done to yourself
933                 {
934                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
935                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
936                 }
937                 else // damage done everyone else
938                 {
939                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
940                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
941                 }
942         }
943         return 0;
944 }
945
946 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
947 {
948         frag_score = 0;
949         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
950 }
951
952 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
953 {
954         entity player = self;
955
956         if(time > player.throw_antispam)
957         {
958                 // pass the flag to a team mate
959                 if(autocvar_g_ctf_allow_pass)
960                 {
961                         entity head, closest_target;
962                         head = findradius(player.origin, autocvar_g_ctf_pass_radius);
963                         
964                         while(head) // find the closest acceptable target to pass to
965                         {
966                                 if(head.classname == "player" && head.deadflag == DEAD_NO)
967                                 if(head != player && !IsDifferentTeam(head, player))
968                                 if(!player.speedrunning && !head.speedrunning)
969                                 {
970                                         traceline(player.origin, head.origin, MOVE_NOMONSTERS, player);
971                                         if not((trace_fraction < 1) && (trace_ent != head))
972                                         {
973                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried) 
974                                                 { 
975                                                         if(clienttype(head) == CLIENTTYPE_BOT)
976                                                         {
977                                                                 centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
978                                                                 ctf_Handle_Throw(head, player, DROPTYPE_PASS);
979                                                         }
980                                                         else
981                                                         {
982                                                                 centerprint(head, strcat(player.netname, " requests you to pass the ", head.flagcarried.netname)); 
983                                                                 centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
984                                                         }
985                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait; 
986                                                         return 0; 
987                                                 }
988                                                 else if(player.flagcarried)
989                                                 {
990                                                         if(closest_target) { if(vlen(player.origin - head.origin) < vlen(player.origin - closest_target.origin)) { closest_target = head; } }
991                                                         else { closest_target = head; }
992                                                 }
993                                         }
994                                 }
995                                 head = head.chain;
996                         }
997                         
998                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROPTYPE_PASS); return 0; }
999                 }
1000                 
1001                 // throw the flag in front of you
1002                 if(autocvar_g_ctf_allow_drop && player.flagcarried && !player.speedrunning)
1003                         { ctf_Handle_Throw(player, world, DROPTYPE_THROW); }
1004         }
1005                 
1006         return 0;
1007 }
1008
1009 // ==========
1010 // Spawnfuncs
1011 // ==========
1012
1013 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
1014 CTF Starting point for a player in team one (Red).
1015 Keys: "angle" viewing angle when spawning. */
1016 void spawnfunc_info_player_team1()
1017 {
1018         if(g_assault) { remove(self); return; }
1019         
1020         self.team = COLOR_TEAM1; // red
1021         spawnfunc_info_player_deathmatch();
1022 }
1023
1024
1025 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
1026 CTF Starting point for a player in team two (Blue).
1027 Keys: "angle" viewing angle when spawning. */
1028 void spawnfunc_info_player_team2()
1029 {
1030         if(g_assault) { remove(self); return; }
1031         
1032         self.team = COLOR_TEAM2; // blue
1033         spawnfunc_info_player_deathmatch();
1034 }
1035
1036 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
1037 CTF Starting point for a player in team three (Yellow).
1038 Keys: "angle" viewing angle when spawning. */
1039 void spawnfunc_info_player_team3()
1040 {
1041         if(g_assault) { remove(self); return; }
1042         
1043         self.team = COLOR_TEAM3; // yellow
1044         spawnfunc_info_player_deathmatch();
1045 }
1046
1047
1048 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
1049 CTF Starting point for a player in team four (Purple).
1050 Keys: "angle" viewing angle when spawning. */
1051 void spawnfunc_info_player_team4()
1052 {
1053         if(g_assault) { remove(self); return; }
1054         
1055         self.team = COLOR_TEAM4; // purple
1056         spawnfunc_info_player_deathmatch();
1057 }
1058
1059 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1060 CTF flag for team one (Red).
1061 Keys: 
1062 "angle" Angle the flag will point (minus 90 degrees)... 
1063 "model" model to use, note this needs red and blue as skins 0 and 1...
1064 "noise" sound played when flag is picked up...
1065 "noise1" sound played when flag is returned by a teammate...
1066 "noise2" sound played when flag is captured...
1067 "noise3" sound played when flag is lost in the field and respawns itself... 
1068 "noise4" sound played when flag is dropped by a player...
1069 "noise5" sound played when flag touches the ground... */
1070 void spawnfunc_item_flag_team1()
1071 {
1072         if(!g_ctf) { remove(self); return; }
1073
1074         ctf_FlagSetup(1, self); // 1 = red
1075 }
1076
1077 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1078 CTF flag for team two (Blue).
1079 Keys: 
1080 "angle" Angle the flag will point (minus 90 degrees)... 
1081 "model" model to use, note this needs red and blue as skins 0 and 1...
1082 "noise" sound played when flag is picked up...
1083 "noise1" sound played when flag is returned by a teammate...
1084 "noise2" sound played when flag is captured...
1085 "noise3" sound played when flag is lost in the field and respawns itself... 
1086 "noise4" sound played when flag is dropped by a player...
1087 "noise5" sound played when flag touches the ground... */
1088 void spawnfunc_item_flag_team2()
1089 {
1090         if(!g_ctf) { remove(self); return; }
1091
1092         ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
1093 }
1094
1095 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
1096 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
1097 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.
1098 Keys:
1099 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
1100 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
1101 void spawnfunc_ctf_team()
1102 {
1103         if(!g_ctf) { remove(self); return; }
1104         
1105         self.classname = "ctf_team";
1106         self.team = self.cnt + 1;
1107 }
1108
1109
1110 // ==============
1111 // Initialization
1112 // ==============
1113
1114 // code from here on is just to support maps that don't have flag and team entities
1115 void ctf_SpawnTeam (string teamname, float teamcolor)
1116 {
1117         entity oldself;
1118         oldself = self;
1119         self = spawn();
1120         self.classname = "ctf_team";
1121         self.netname = teamname;
1122         self.cnt = teamcolor;
1123
1124         spawnfunc_ctf_team();
1125
1126         self = oldself;
1127 }
1128
1129 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
1130 {
1131         // if no teams are found, spawn defaults
1132         if(find(world, classname, "ctf_team") == world)
1133         {
1134                 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
1135                 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
1136                 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
1137         }
1138         
1139         ScoreRules_ctf();
1140 }
1141
1142 void ctf_Initialize()
1143 {
1144         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
1145
1146         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
1147         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
1148         ctf_captureshield_force = autocvar_g_ctf_shield_force;
1149         
1150         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
1151 }
1152
1153
1154 MUTATOR_DEFINITION(gamemode_ctf)
1155 {
1156         MUTATOR_HOOK(MakePlayerObserver, ctf_HookedDrop, CBC_ORDER_ANY);
1157         MUTATOR_HOOK(ClientDisconnect, ctf_HookedDrop, CBC_ORDER_ANY);
1158         MUTATOR_HOOK(PlayerDies, ctf_HookedDrop, CBC_ORDER_ANY);
1159         MUTATOR_HOOK(PortalTeleport, ctf_HookedDrop, CBC_ORDER_ANY);
1160         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
1161         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
1162         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
1163         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
1164
1165         MUTATOR_ONADD
1166         {
1167                 if(time > 1) // game loads at time 1
1168                         error("This is a game type and it cannot be added at runtime.");
1169                 g_ctf = 1;
1170                 ctf_Initialize();
1171         }
1172
1173         MUTATOR_ONREMOVE
1174         {
1175                 g_ctf = 0;
1176                 error("This is a game type and it cannot be removed at runtime.");
1177         }
1178
1179         return 0;
1180 }