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