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