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