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