4d9167066d8a7c9e348b9c7788318f8276067d53
[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 void ctf_FakeTimeLimit(entity e, float t)
7 {
8         msg_entity = e;
9         WriteByte(MSG_ONE, 3); // svc_updatestat
10         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
11         if(t < 0)
12                 WriteCoord(MSG_ONE, autocvar_timelimit);
13         else
14                 WriteCoord(MSG_ONE, (t + 1) / 60);
15 }
16
17 void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for easy changing and quick editing later
18 {
19         if(autocvar_sv_eventlog)
20                 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
21 }
22
23 string ctf_CaptureRecord(entity flag, entity player)
24 {
25         float cap_time, cap_record, success;
26         string cap_message, refername;
27         
28         if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots)) 
29         {
30                 cap_record = ctf_captimerecord;
31                 cap_time = (time - flag.ctf_pickuptime);
32
33                 refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
34                 refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's"));
35
36                 if(!ctf_captimerecord) 
37                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; }
38                 else if(cap_time < cap_record) 
39                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; }
40                 else
41                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; }
42
43                 if(success) 
44                 {
45                         ctf_captimerecord = cap_time;
46                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
47                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
48                         write_recordmarker(player, (time - cap_time), cap_time); 
49                 } 
50         }
51         
52         return cap_message;
53 }
54
55 void ctf_FlagcarrierWaypoints(entity player)
56 {
57         WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
58         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
59         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
60         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
61 }
62
63
64 // =======================
65 // CaptureShield Functions 
66 // =======================
67
68 float ctf_CaptureShield_CheckStatus(entity p) 
69 {
70         float s, se;
71         entity e;
72         float players_worseeq, players_total;
73
74         if(ctf_captureshield_max_ratio <= 0)
75                 return FALSE;
76
77         s = PlayerScore_Add(p, SP_SCORE, 0);
78         if(s >= -ctf_captureshield_min_negscore)
79                 return FALSE;
80
81         players_total = players_worseeq = 0;
82         FOR_EACH_PLAYER(e)
83         {
84                 if(IsDifferentTeam(e, p))
85                         continue;
86                 se = PlayerScore_Add(e, SP_SCORE, 0);
87                 if(se <= s)
88                         ++players_worseeq;
89                 ++players_total;
90         }
91
92         // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
93         // use this rule here
94         
95         if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
96                 return FALSE;
97
98         return TRUE;
99 }
100
101 void ctf_CaptureShield_Update(entity player, float wanted_status)
102 {
103         float updated_status = ctf_CaptureShield_CheckStatus(player);
104         if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
105         {
106                 if(updated_status) // TODO csqc notifier for this // Samual: How?
107                         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);
108                 else
109                         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);
110                         
111                 player.ctf_captureshielded = updated_status;
112         }
113 }
114
115 float ctf_CaptureShield_Customize()
116 {
117         if(!other.ctf_captureshielded) { return FALSE; }
118         if(!IsDifferentTeam(self, other)) { return FALSE; }
119         
120         return TRUE;
121 }
122
123 void ctf_CaptureShield_Touch()
124 {
125         if(!other.ctf_captureshielded) { return; }
126         if(!IsDifferentTeam(self, other)) { return; }
127         
128         vector mymid = (self.absmin + self.absmax) * 0.5;
129         vector othermid = (other.absmin + other.absmax) * 0.5;
130
131         Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
132         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);
133 }
134
135 void ctf_CaptureShield_Spawn(entity flag)
136 {
137         entity shield = spawn();
138         
139         shield.enemy = self;
140         shield.team = self.team;
141         shield.touch = ctf_CaptureShield_Touch;
142         shield.customizeentityforclient = ctf_CaptureShield_Customize;
143         shield.classname = "ctf_captureshield";
144         shield.effects = EF_ADDITIVE;
145         shield.movetype = MOVETYPE_NOCLIP;
146         shield.solid = SOLID_TRIGGER;
147         shield.avelocity = '7 0 11';
148         shield.scale = 0.5;
149         
150         setorigin(shield, self.origin);
151         setmodel(shield, "models/ctf/shield.md3");
152         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
153 }
154
155
156 // ====================
157 // Drop/Pass/Throw Code
158 // ====================
159
160 void ctf_Handle_Drop(entity flag, entity player, float droptype)
161 {
162         // declarations
163         player = (player ? player : flag.pass_sender);
164
165         // main
166         flag.movetype = MOVETYPE_TOSS;
167         flag.takedamage = DAMAGE_YES;
168         flag.health = flag.max_flag_health;
169         flag.ctf_droptime = time;
170         flag.ctf_dropper = player;
171         flag.ctf_status = FLAG_DROPPED;
172         
173         // messages and sounds
174         Send_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
175         sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
176         ctf_EventLog("dropped", player.team, player);
177
178         // scoring
179         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_penalty_drop);    
180         PlayerScore_Add(player, SP_CTF_DROPS, 1);
181         
182         // waypoints
183         if(autocvar_g_ctf_flag_dropped_waypoint)
184                 WaypointSprite_Spawn("flagdropped", 0, 0, flag, FLAG_WAYPOINT_OFFSET, world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, TRUE, RADARICON_FLAG, WPCOLOR_DROPPEDFLAG(flag.team));
185
186         if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
187         {
188                 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
189                 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
190         }
191         
192         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
193         
194         if(droptype == DROP_PASS)
195         {
196                 flag.pass_sender = world;
197                 flag.pass_target = world;
198         }
199 }
200
201 void ctf_Handle_Retrieve(entity flag, entity player)
202 {
203         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
204         entity sender = flag.pass_sender;
205         
206         // transfer flag to player
207         flag.owner = player;
208         flag.owner.flagcarried = flag;
209         
210         // reset flag
211         setattachment(flag, player, "");
212         setorigin(flag, FLAG_CARRY_OFFSET);
213         flag.movetype = MOVETYPE_NONE;
214         flag.takedamage = DAMAGE_NO;
215         flag.solid = SOLID_NOT;
216         flag.ctf_carrier = player;
217         flag.ctf_status = FLAG_CARRY;
218
219         // messages and sounds
220         sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTN_NORM);
221         ctf_EventLog("recieve", flag.team, player);
222         
223         FOR_EACH_REALPLAYER(tmp_player)
224         {
225                 if(tmp_player == sender)
226                         centerprint(tmp_player, strcat("You passed the ", flag.netname, " to ", player.netname));
227                 else if(tmp_player == player)
228                         centerprint(tmp_player, strcat("You recieved the ", flag.netname, " from ", sender.netname));
229                 else if(!IsDifferentTeam(tmp_player, sender))
230                         centerprint(tmp_player, strcat(sender.netname, " passed the ", flag.netname, " to ", player.netname));
231         }
232         
233         // create new waypoint
234         ctf_FlagcarrierWaypoints(player);
235         
236         sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
237         player.throw_antispam = sender.throw_antispam;
238         
239         flag.pass_sender = world;
240         flag.pass_target = world;
241 }
242
243 void ctf_Handle_Throw(entity player, entity reciever, float droptype)
244 {
245         entity flag = player.flagcarried;
246         vector targ_origin;
247         
248         if(!flag) { return; }
249         if((droptype == DROP_PASS) && !reciever) { return; }
250         
251         if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
252         
253         // reset the flag
254         setattachment(flag, world, "");
255         setorigin(flag, player.origin + FLAG_DROP_OFFSET);
256         flag.owner.flagcarried = world;
257         flag.owner = world;
258         flag.solid = SOLID_TRIGGER;
259         flag.ctf_droptime = time;
260         
261         flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
262         
263         switch(droptype)
264         {
265                 case DROP_PASS:
266                 {
267                         WarpZone_RefSys_MakeSameRefSys(flag, player);
268                         targ_origin = WarpZone_RefSys_TransformOrigin(reciever, flag, (0.5 * (reciever.absmin + reciever.absmax)));
269                         flag.velocity = (normalize(targ_origin - player.origin) * autocvar_g_ctf_pass_velocity);
270                         break;
271                 }
272                 
273                 case DROP_THROW:
274                 {
275                         makevectors((player.v_angle_y * '0 1 0') + (player.v_angle_x * '0.5 0 0'));
276                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + (v_forward * autocvar_g_ctf_drop_velocity)), FALSE);
277                         break;
278                 }
279                 
280                 case DROP_RESET:
281                 {
282                         flag.velocity = '0 0 0'; // do nothing
283                         break;
284                 }
285                 
286                 default:
287                 case DROP_NORMAL:
288                 {
289                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + ('0 100 0' * crandom()) + ('100 0 0' * crandom())), FALSE);
290                         break;
291                 }
292         }
293         
294         switch(droptype)
295         {
296                 case DROP_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                         WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), targ_origin, player.origin);
308                         ctf_EventLog("pass", flag.team, player);
309                         break;
310                 }
311
312                 case DROP_RESET: 
313                 {
314                         // do nothing
315                         break;
316                 }
317                 
318                 default:
319                 case DROP_THROW:
320                 case DROP_NORMAL:
321                 {
322                         ctf_Handle_Drop(flag, player, droptype);
323                         break;
324                 }
325         }
326
327         // kill old waypointsprite
328         WaypointSprite_Ping(player.wps_flagcarrier);
329         WaypointSprite_Kill(player.wps_flagcarrier);
330         
331         if(player.wps_enemyflagcarrier)
332                 WaypointSprite_Kill(player.wps_enemyflagcarrier);
333         
334         // captureshield
335         ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
336 }
337
338
339 // ==============
340 // Event Handlers
341 // ==============
342
343 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
344 {
345         entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
346         entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
347         float old_time, new_time; 
348         
349         if not(player) { return; } // without someone to give the reward to, we can't possibly cap
350         
351         // messages and sounds
352         Send_KillNotification(player.netname, enemy_flag.netname, ctf_CaptureRecord(enemy_flag, player), INFO_CAPTUREFLAG, MSG_INFO);
353         sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
354         
355         switch(capturetype)
356         {
357                 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
358                 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
359                 default: break;
360         }
361         
362         // scoring
363         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
364         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
365
366         old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
367         new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
368         if(!old_time || new_time < old_time)
369                 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
370
371         // effects
372         if(autocvar_g_ctf_flag_capture_effects) 
373         {
374                 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
375                 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
376         }
377
378         // other
379         if(capturetype == CAPTURE_NORMAL)
380         {
381                 WaypointSprite_Kill(player.wps_flagcarrier);
382                 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
383         }
384         
385         // reset the flag
386         player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
387         ctf_RespawnFlag(enemy_flag);
388 }
389
390 void ctf_Handle_Return(entity flag, entity player)
391 {
392         // messages and sounds
393         //centerprint(player, strcat("You returned the ", flag.netname));
394         Send_KillNotification(player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO);
395         sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
396         ctf_EventLog("return", flag.team, player);
397
398         // scoring
399         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
400         PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
401
402         TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
403         
404         if(flag.ctf_dropper) 
405         {
406                 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
407                 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag 
408                 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
409         }
410         
411         // reset the flag
412         ctf_RespawnFlag(flag);
413 }
414
415 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
416 {
417         // declarations
418         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
419         string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
420         float pickup_dropped_score; // used to calculate dropped pickup score
421         
422         // attach the flag to the player
423         flag.owner = player;
424         player.flagcarried = flag;
425         setattachment(flag, player, "");
426         setorigin(flag, FLAG_CARRY_OFFSET);
427         
428         // flag setup
429         flag.movetype = MOVETYPE_NONE;
430         flag.takedamage = DAMAGE_NO;
431         flag.solid = SOLID_NOT;
432         flag.angles = '0 0 0';
433         flag.ctf_carrier = player;
434         flag.ctf_status = FLAG_CARRY;
435         
436         switch(pickuptype)
437         {
438                 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
439                 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
440                 default: break;
441         }
442
443         // messages and sounds
444         Send_KillNotification (player.netname, flag.netname, "", INFO_GOTFLAG, MSG_INFO);
445         sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
446         verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
447         
448         FOR_EACH_REALPLAYER(tmp_player)
449         {
450                 if(tmp_player == player)
451                         centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
452                 else if(!IsDifferentTeam(tmp_player, player))
453                         centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
454                 else if(!IsDifferentTeam(tmp_player, flag))
455                         centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
456         }
457                 
458         switch(pickuptype)
459         {
460                 case PICKUP_BASE: ctf_EventLog("steal", flag.team, player); break;
461                 case PICKUP_DROPPED: ctf_EventLog("pickup", flag.team, player); break;
462                 default: break;
463         }
464         
465         // scoring
466         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
467         switch(pickuptype)
468         {               
469                 case PICKUP_BASE:
470                 {
471                         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
472                         break;
473                 }
474                 
475                 case PICKUP_DROPPED:
476                 {
477                         pickup_dropped_score = (autocvar_g_ctf_flag_return_time ? bound(0, ((flag.ctf_droptime + autocvar_g_ctf_flag_return_time) - time) / autocvar_g_ctf_flag_return_time, 1) : 1);
478                         pickup_dropped_score = floor((autocvar_g_ctf_score_pickup_dropped_late * (1 - pickup_dropped_score) + autocvar_g_ctf_score_pickup_dropped_early * pickup_dropped_score) + 0.5);
479                         print("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
480                         PlayerTeamScore_AddScore(player, pickup_dropped_score);
481                         break;
482                 }
483                 
484                 default: break;
485         }
486         
487         // speedrunning
488         if(pickuptype == PICKUP_BASE)
489         {
490                 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
491                 if((player.speedrunning) && (ctf_captimerecord))
492                         ctf_FakeTimeLimit(player, time + ctf_captimerecord);
493         }
494                 
495         // effects
496         if(autocvar_g_ctf_flag_pickup_effects)
497                 pointparticles(particleeffectnum("smoke_ring"), 0.5 * (flag.absmin + flag.absmax), '0 0 0', 1);
498         
499         // waypoints 
500         if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
501         ctf_FlagcarrierWaypoints(player);
502         WaypointSprite_Ping(player.wps_flagcarrier);
503 }
504
505
506 // ===================
507 // Main Flag Functions
508 // ===================
509
510 void ctf_CheckFlagReturn(entity flag, float returntype)
511 {
512         if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
513         
514         if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
515         {
516                 switch(returntype)
517                 {
518                         case RETURN_DROPPED: bprint("The ", flag.netname, " was dropped in the base and returned itself\n"); break;
519                         case RETURN_DAMAGE: bprint("The ", flag.netname, " was destroyed and returned to base\n"); break;
520                         case RETURN_SPEEDRUN: bprint("The ", flag.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n"); break;
521                         case RETURN_NEEDKILL: bprint("The ", flag.netname, " fell somewhere it couldn't be reached and returned to base\n"); break;
522                         
523                         default:
524                         case RETURN_TIMEOUT:
525                                 { bprint("The ", flag.netname, " has returned to base\n"); break; }
526                 }
527                 sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
528                 ctf_EventLog("returned", flag.team, world);
529                 ctf_RespawnFlag(flag);
530         }
531 }
532
533 void ctf_CheckStalemate(void)
534 {
535         // declarations
536         float stale_red_flags, stale_blue_flags;
537         entity tmp_entity;
538
539         entity ctf_staleflaglist; // reset the list, we need to build the list each time this function runs
540
541         // build list of stale flags
542         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
543         {
544                 if(autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
545                 if(tmp_entity.ctf_status != FLAG_BASE)
546                 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
547                 {
548                         tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
549                         ctf_staleflaglist = tmp_entity;
550                         
551                         switch(tmp_entity.team)
552                         {
553                                 case COLOR_TEAM1: ++stale_red_flags; break;
554                                 case COLOR_TEAM2: ++stale_blue_flags; break;
555                         }
556                 }
557         }
558
559         if(stale_red_flags && stale_blue_flags)
560                 ctf_stalemate = TRUE;
561         else if(!stale_red_flags && !stale_blue_flags)
562                 ctf_stalemate = FALSE;
563         
564         // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
565         if(ctf_stalemate)
566         {
567                 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
568                 {
569                         if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
570                                 WaypointSprite_Spawn("enemyflagcarrier", 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, tmp_entity.team, tmp_entity.owner, wps_enemyflagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_ENEMYFC(tmp_entity.owner.team));
571                 }
572                 
573                 if not(wpforenemy_announced)
574                 {
575                         FOR_EACH_REALPLAYER(tmp_entity)
576                                 if(tmp_entity.flagcarried)
577                                         centerprint(tmp_entity, "Stalemate! Enemies can now see you on radar!");
578                                 else
579                                         centerprint(tmp_entity, "Stalemate! Flag carriers can now be seen by enemies on radar!");
580                         
581                         wpforenemy_announced = TRUE;
582                 }
583         }
584 }
585
586 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
587 {
588         if(ITEM_DAMAGE_NEEDKILL(deathtype))
589         {
590                 // automatically kill the flag and return it
591                 self.health = 0;
592                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
593                 return;
594         }
595         if(autocvar_g_ctf_flag_return_damage) 
596         {
597                 // reduce health and check if it should be returned
598                 self.health = self.health - damage;
599                 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
600                 return;
601         }
602 }
603
604 void ctf_FlagThink()
605 {
606         // declarations
607         entity tmp_entity;
608
609         self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
610
611         // captureshield
612         if(self == ctf_worldflaglist) // only for the first flag
613                 FOR_EACH_CLIENT(tmp_entity)
614                         ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
615
616         // sanity checks
617         if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
618                 dprint("wtf the flag got squashed?\n");
619                 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
620                 if(!trace_startsolid) // can we resize it without getting stuck?
621                         setsize(self, FLAG_MIN, FLAG_MAX); }
622                         
623         switch(self.ctf_status) // reset flag angles in case warpzones adjust it
624         {
625                 case FLAG_DROPPED:
626                 case FLAG_PASSING:
627                 {
628                         self.angles = '0 0 0';
629                         break;
630                 }
631                 
632                 default: break;
633         }
634
635         // main think method
636         switch(self.ctf_status)
637         {       
638                 case FLAG_BASE:
639                 {
640                         if(autocvar_g_ctf_dropped_capture_radius)
641                         {
642                                 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
643                                         if(tmp_entity.ctf_status == FLAG_DROPPED)
644                                                 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
645                                                         ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
646                         }
647                         return;
648                 }
649                 
650                 case FLAG_DROPPED:
651                 {
652                         if(autocvar_g_ctf_flag_dropped_floatinwater)
653                         {
654                                 vector midpoint = ((self.absmin + self.absmax) * 0.5);
655                                 if(pointcontents(midpoint) == CONTENT_WATER)
656                                 {
657                                         self.velocity = self.velocity * 0.5;
658                                         
659                                         if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
660                                                 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
661                                         else
662                                                 { self.movetype = MOVETYPE_FLY; }
663                                 }
664                                 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
665                         }
666                         if(autocvar_g_ctf_flag_return_dropped)
667                         {
668                                 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
669                                 {
670                                         self.health = 0;
671                                         ctf_CheckFlagReturn(self, RETURN_DROPPED);
672                                         return;
673                                 }
674                         }
675                         if(autocvar_g_ctf_flag_return_time)
676                         {
677                                 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
678                                 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
679                                 return;
680                         } 
681                         return;
682                 }
683                         
684                 case FLAG_CARRY:
685                 {
686                         if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord)) 
687                         {
688                                 self.health = 0;
689                                 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
690
691                                 tmp_entity = self;
692                                 self = self.owner;
693                                 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
694                                 ImpulseCommands();
695                                 self = tmp_entity;
696                         }
697                         if(autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
698                         {
699                                 if(time >= wpforenemy_nextthink)
700                                 {
701                                         ctf_CheckStalemate();
702                                         wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
703                                 }
704                         }
705                         return;
706                 }
707                 
708                 case FLAG_PASSING: // todo make work with warpzones
709                 {
710                         vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
711                         vector old_targ_origin = targ_origin;
712                         targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin);
713                         WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
714
715                         print(strcat("self: ", vtos(self.origin), ", old: ", vtos(old_targ_origin), " (", ftos(vlen(self.origin - old_targ_origin)), "qu)"), ", transformed: ", vtos(targ_origin), " (", ftos(vlen(self.origin - targ_origin)), "qu)", ".\n");
716                         
717                         if((self.pass_target.deadflag != DEAD_NO)
718                                 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
719                                 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
720                                 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
721                         {
722                                 ctf_Handle_Drop(self, world, DROP_PASS);
723                         }
724                         else // still a viable target, go for it
725                         {
726                                 vector desired_direction = normalize(targ_origin - self.origin);
727                                 vector current_direction = normalize(self.velocity);
728
729                                 self.velocity = (normalize(current_direction + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); 
730                         }
731                         return;
732                 }
733
734                 default: // this should never happen
735                 {
736                         dprint("ctf_FlagThink(): Flag exists with no status?\n");
737                         return;
738                 }
739         }
740 }
741
742 void ctf_FlagTouch()
743 {
744         if(gameover) { return; }
745         
746         entity toucher = other;
747         
748         // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
749         if(ITEM_TOUCH_NEEDKILL())
750         {
751                 self.health = 0;
752                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
753                 return;
754         }
755         
756         // special touch behaviors
757         if(toucher.vehicle_flags & VHF_ISVEHICLE)
758         {
759                 if(autocvar_g_ctf_allow_vehicle_touch)
760                         toucher = toucher.owner; // the player is actually the vehicle owner, not other
761                 else
762                         return; // do nothing
763         }
764         else if(toucher.classname != "player") // The flag just touched an object, most likely the world
765         {
766                 if(time > self.wait) // if we haven't in a while, play a sound/effect
767                 {
768                         pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
769                         sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
770                         self.wait = time + FLAG_TOUCHRATE;
771                 }
772                 return;
773         }
774         else if(toucher.deadflag != DEAD_NO) { return; }
775
776         switch(self.ctf_status) 
777         {       
778                 case FLAG_BASE:
779                 {
780                         if(!IsDifferentTeam(toucher, self) && (toucher.flagcarried) && IsDifferentTeam(toucher.flagcarried, self))
781                                 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
782                         else if(IsDifferentTeam(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time))
783                                 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
784                         break;
785                 }
786                 
787                 case FLAG_DROPPED:
788                 {
789                         if(!IsDifferentTeam(toucher, self))
790                                 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
791                         else if((!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
792                                 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
793                         break;
794                 }
795                         
796                 case FLAG_CARRY:
797                 {
798                         dprint("Someone touched a flag even though it was being carried?\n");
799                         break;
800                 }
801                 
802                 case FLAG_PASSING:
803                 {
804                         if((toucher.classname == "player") && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
805                         {
806                                 if(IsDifferentTeam(toucher, self.pass_sender))
807                                         ctf_Handle_Return(self, toucher);
808                                 else
809                                         ctf_Handle_Retrieve(self, toucher);
810                         }
811                         break;
812                 }
813         }
814 }
815
816 void ctf_RespawnFlag(entity flag)
817 {
818         // reset the player (if there is one)
819         if((flag.owner) && (flag.owner.flagcarried == flag))
820         {
821                 if(flag.owner.wps_enemyflagcarrier)
822                         WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
823                         
824                 WaypointSprite_Kill(flag.wps_flagcarrier);
825                 
826                 flag.owner.flagcarried = world;
827
828                 if(flag.speedrunning)
829                         ctf_FakeTimeLimit(flag.owner, -1);
830         }
831
832         if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
833                 { WaypointSprite_Kill(flag.wps_flagdropped); }
834
835         // reset the flag
836         setattachment(flag, world, "");
837         setorigin(flag, flag.ctf_spawnorigin);
838         
839         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
840         flag.takedamage = DAMAGE_NO;
841         flag.health = flag.max_flag_health;
842         flag.solid = SOLID_TRIGGER;
843         flag.velocity = '0 0 0';
844         flag.angles = flag.mangle;
845         flag.flags = FL_ITEM | FL_NOTARGET;
846         
847         flag.ctf_status = FLAG_BASE;
848         flag.owner = world;
849         flag.pass_sender = world;
850         flag.pass_target = world;
851         flag.ctf_carrier = world;
852         flag.ctf_dropper = world;
853         flag.ctf_pickuptime = 0;
854         flag.ctf_droptime = 0;
855
856         wpforenemy_announced = FALSE;
857 }
858
859 void ctf_Reset()
860 {
861         if(self.owner)
862                 if(self.owner.classname == "player")
863                         ctf_Handle_Throw(self.owner, world, DROP_RESET);
864                         
865         ctf_RespawnFlag(self);
866 }
867
868 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
869 {
870         // bot waypoints
871         waypoint_spawnforitem_force(self, self.origin);
872         self.nearestwaypointtimeout = 0; // activate waypointing again
873         self.bot_basewaypoint = self.nearestwaypoint;
874
875         // waypointsprites
876         WaypointSprite_SpawnFixed(((self.team == COLOR_TEAM1) ? "redbase" : "bluebase"), self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
877         WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
878
879         // captureshield setup
880         ctf_CaptureShield_Spawn(self);
881 }
882
883 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc 
884 {       
885         // declarations
886         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. 
887         self = flag; // for later usage with droptofloor()
888         
889         // main setup
890         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
891         ctf_worldflaglist = flag;
892
893         setattachment(flag, world, ""); 
894
895         flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
896         flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue)
897         flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
898         flag.classname = "item_flag_team";
899         flag.target = "###item###"; // wut?
900         flag.flags = FL_ITEM | FL_NOTARGET;
901         flag.solid = SOLID_TRIGGER;
902         flag.takedamage = DAMAGE_NO;
903         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;   
904         flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
905         flag.health = flag.max_flag_health;
906         flag.event_damage = ctf_FlagDamage;
907         flag.pushable = TRUE;
908         flag.teleportable = TELEPORT_NORMAL;
909         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
910         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
911         flag.velocity = '0 0 0';
912         flag.mangle = flag.angles;
913         flag.reset = ctf_Reset;
914         flag.touch = ctf_FlagTouch;
915         flag.think = ctf_FlagThink;
916         flag.nextthink = time + FLAG_THINKRATE;
917         flag.ctf_status = FLAG_BASE;
918         
919         if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
920         if(!flag.scale) { flag.scale = FLAG_SCALE; }
921         if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
922         if(!flag.toucheffect) { flag.toucheffect = ((teamnumber) ? "redflag_touch" : "blueflag_touch"); }
923         if(!flag.passeffect) { flag.passeffect = ((!teamnumber) ? "red_pass" : "blue_pass"); } // invert the team number of the flag to pass as enemy team color
924         
925         // sound 
926         if(!flag.snd_flag_taken) { flag.snd_flag_taken  = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
927         if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
928         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
929         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.
930         if(!flag.snd_flag_dropped) { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
931         if(!flag.snd_flag_touch) { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
932         if(!flag.snd_flag_pass) { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
933         
934         // precache
935         precache_sound(flag.snd_flag_taken);
936         precache_sound(flag.snd_flag_returned);
937         precache_sound(flag.snd_flag_capture);
938         precache_sound(flag.snd_flag_respawn);
939         precache_sound(flag.snd_flag_dropped);
940         precache_sound(flag.snd_flag_touch);
941         precache_sound(flag.snd_flag_pass);
942         precache_model(flag.model);
943         precache_model("models/ctf/shield.md3");
944         precache_model("models/ctf/shockwavetransring.md3");
945
946         // appearence
947         setmodel(flag, flag.model); // precision set below
948         setsize(flag, FLAG_MIN, FLAG_MAX);
949         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
950         
951         if(autocvar_g_ctf_flag_glowtrails)
952         {
953                 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
954                 flag.glow_size = 25;
955                 flag.glow_trail = 1;
956         }
957         
958         flag.effects |= EF_LOWPRECISION;
959         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
960         if(autocvar_g_ctf_dynamiclights)   { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
961         
962         // flag placement
963         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
964         {       
965                 flag.dropped_origin = flag.origin; 
966                 flag.noalign = TRUE;
967                 flag.movetype = MOVETYPE_NONE;
968         }
969         else // drop to floor, automatically find a platform and set that as spawn origin
970         { 
971                 flag.noalign = FALSE;
972                 self = flag;
973                 droptofloor();
974                 flag.movetype = MOVETYPE_TOSS; 
975         }       
976         
977         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
978 }
979
980
981 // ==============
982 // Hook Functions
983 // ==============
984
985 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
986 {
987         entity flag;
988         
989         // initially clear items so they can be set as necessary later.
990         self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST 
991                 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
992
993         // scan through all the flags and notify the client about them 
994         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
995         {
996                 switch(flag.ctf_status)
997                 {
998                         case FLAG_PASSING:
999                         case FLAG_CARRY:
1000                         {
1001                                 if((flag.owner == self) || (flag.pass_sender == self))
1002                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1003                                 else 
1004                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1005                                 break;
1006                         }
1007                         case FLAG_DROPPED:
1008                         {
1009                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1010                                 break;
1011                         }
1012                 }
1013         }
1014         
1015         // item for stopping players from capturing the flag too often
1016         if(self.ctf_captureshielded)
1017                 self.items |= IT_CTF_SHIELDED;
1018         
1019         // update the health of the flag carrier waypointsprite
1020         if(self.wps_flagcarrier) 
1021                 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1022         
1023         return 0;
1024 }
1025
1026 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1027 {
1028         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1029         {
1030                 if(frag_target == frag_attacker) // damage done to yourself
1031                 {
1032                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1033                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1034                 }
1035                 else // damage done to everyone else
1036                 {
1037                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1038                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1039                 }
1040         }
1041         else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1042         {
1043                 if(autocvar_g_ctf_flagcarrier_auto_helpme_when_damaged > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent)))
1044                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier); // TODO: only do this if there is a significant loss of health?
1045         }
1046         return 0;
1047 }
1048
1049 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1050 {
1051         if((frag_attacker != frag_target) && (frag_attacker.classname == "player") && (frag_target.flagcarried))
1052         {
1053                 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1054                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1055         }
1056                                 
1057         if(frag_target.flagcarried)
1058                 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1059                 
1060         return 0;
1061 }
1062
1063 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1064 {
1065         frag_score = 0;
1066         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1067 }
1068
1069 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1070 {
1071         if(self.flagcarried)
1072                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1073                 
1074         return 0;
1075 }
1076
1077 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1078 {
1079         if(self.flagcarried) 
1080         if(!autocvar_g_ctf_portalteleport)
1081                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1082
1083         return 0;
1084 }
1085
1086 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1087 {
1088         entity player = self;
1089
1090         if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1091         {
1092                 // pass the flag to a team mate
1093                 if(autocvar_g_ctf_pass)
1094                 {
1095                         entity head, closest_target;
1096                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1097                         
1098                         while(head) // find the closest acceptable target to pass to
1099                         {
1100                                 if(head.classname == "player" && head.deadflag == DEAD_NO)
1101                                 if(head != player && !IsDifferentTeam(head, player))
1102                                 if(!head.speedrunning && (!head.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1103                                 {
1104                                         if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried) 
1105                                         { 
1106                                                 if(clienttype(head) == CLIENTTYPE_BOT)
1107                                                 {
1108                                                         centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1109                                                         ctf_Handle_Throw(head, player, DROP_PASS);
1110                                                 }
1111                                                 else
1112                                                 {
1113                                                         centerprint(head, strcat(player.netname, " requests you to pass the ", head.flagcarried.netname)); 
1114                                                         centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1115                                                 }
1116                                                 player.throw_antispam = time + autocvar_g_ctf_pass_wait; 
1117                                                 return 0; 
1118                                         }
1119                                         else if(player.flagcarried)
1120                                         {
1121                                                 if(closest_target)
1122                                                 {
1123                                                         if(vlen(player.origin - WarpZone_UnTransformOrigin(head, head.origin)) < vlen(player.origin - WarpZone_UnTransformOrigin(closest_target, closest_target.origin)))
1124                                                                 { closest_target = head; }
1125                                                 }
1126                                                 else { closest_target = head; }
1127                                         }
1128                                 }
1129                                 head = head.chain;
1130                         }
1131                         
1132                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return 0; }
1133                 }
1134                 
1135                 // throw the flag in front of you
1136                 if(autocvar_g_ctf_drop && player.flagcarried)
1137                         { ctf_Handle_Throw(player, world, DROP_THROW); }
1138         }
1139                 
1140         return 0;
1141 }
1142
1143 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1144 {
1145         if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1146         {
1147                 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1148         } 
1149         else // create a normal help me waypointsprite
1150         {
1151                 WaypointSprite_Spawn("helpme", waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, FALSE, RADARICON_HELPME, '1 0.5 0');
1152                 WaypointSprite_Ping(self.wps_helpme);
1153         }
1154
1155         return 1;
1156 }
1157
1158 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1159 {
1160         if(vh_player.flagcarried)
1161         {
1162                 if(!autocvar_g_ctf_flagcarrier_allow_vehicle_carry)
1163                 {
1164                         ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1165                 }
1166                 else
1167                 {            
1168                         setattachment(vh_player.flagcarried, vh_vehicle, ""); 
1169                         setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1170                         vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1171                         //vh_player.flagcarried.angles = '0 0 0';       
1172                 }
1173         }
1174                 
1175         return 0;
1176 }
1177
1178 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1179 {
1180         if(vh_player.flagcarried)
1181         {
1182                 setattachment(vh_player.flagcarried, vh_player, ""); 
1183                 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
1184                 vh_player.flagcarried.scale = FLAG_SCALE;
1185                 vh_player.flagcarried.angles = '0 0 0'; 
1186         }
1187
1188         return 0;
1189 }
1190
1191 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
1192 {
1193         if(self.flagcarried)
1194         {
1195                 bprint("The ", self.flagcarried.netname, " was returned to base by its carrier\n");
1196                 ctf_RespawnFlag(self);
1197         }
1198         
1199         return 0;
1200 }
1201
1202 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
1203 {
1204         entity flag; // temporary entity for the search method
1205         
1206         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1207         {
1208                 switch(flag.ctf_status)
1209                 {
1210                         case FLAG_DROPPED:
1211                         case FLAG_PASSING:
1212                         {
1213                                 // lock the flag, game is over
1214                                 flag.movetype = MOVETYPE_NONE;
1215                                 flag.takedamage = DAMAGE_NO;
1216                                 flag.solid = SOLID_NOT;
1217                                 flag.nextthink = 0; // stop thinking
1218                                 
1219                                 print("stopping the ", flag.netname, " from moving.\n");
1220                                 break;
1221                         }
1222                         
1223                         default:
1224                         case FLAG_BASE:
1225                         case FLAG_CARRY:
1226                         {
1227                                 // do nothing for these flags
1228                                 break;
1229                         }
1230                 }
1231         }
1232         
1233         return 0;
1234 }
1235
1236
1237 // ==========
1238 // Spawnfuncs
1239 // ==========
1240
1241 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
1242 CTF Starting point for a player in team one (Red).
1243 Keys: "angle" viewing angle when spawning. */
1244 void spawnfunc_info_player_team1()
1245 {
1246         if(g_assault) { remove(self); return; }
1247         
1248         self.team = COLOR_TEAM1; // red
1249         spawnfunc_info_player_deathmatch();
1250 }
1251
1252
1253 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
1254 CTF Starting point for a player in team two (Blue).
1255 Keys: "angle" viewing angle when spawning. */
1256 void spawnfunc_info_player_team2()
1257 {
1258         if(g_assault) { remove(self); return; }
1259         
1260         self.team = COLOR_TEAM2; // blue
1261         spawnfunc_info_player_deathmatch();
1262 }
1263
1264 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
1265 CTF Starting point for a player in team three (Yellow).
1266 Keys: "angle" viewing angle when spawning. */
1267 void spawnfunc_info_player_team3()
1268 {
1269         if(g_assault) { remove(self); return; }
1270         
1271         self.team = COLOR_TEAM3; // yellow
1272         spawnfunc_info_player_deathmatch();
1273 }
1274
1275
1276 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
1277 CTF Starting point for a player in team four (Purple).
1278 Keys: "angle" viewing angle when spawning. */
1279 void spawnfunc_info_player_team4()
1280 {
1281         if(g_assault) { remove(self); return; }
1282         
1283         self.team = COLOR_TEAM4; // purple
1284         spawnfunc_info_player_deathmatch();
1285 }
1286
1287 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1288 CTF flag for team one (Red).
1289 Keys: 
1290 "angle" Angle the flag will point (minus 90 degrees)... 
1291 "model" model to use, note this needs red and blue as skins 0 and 1...
1292 "noise" sound played when flag is picked up...
1293 "noise1" sound played when flag is returned by a teammate...
1294 "noise2" sound played when flag is captured...
1295 "noise3" sound played when flag is lost in the field and respawns itself... 
1296 "noise4" sound played when flag is dropped by a player...
1297 "noise5" sound played when flag touches the ground... */
1298 void spawnfunc_item_flag_team1()
1299 {
1300         if(!g_ctf) { remove(self); return; }
1301
1302         ctf_FlagSetup(1, self); // 1 = red
1303 }
1304
1305 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1306 CTF flag for team two (Blue).
1307 Keys: 
1308 "angle" Angle the flag will point (minus 90 degrees)... 
1309 "model" model to use, note this needs red and blue as skins 0 and 1...
1310 "noise" sound played when flag is picked up...
1311 "noise1" sound played when flag is returned by a teammate...
1312 "noise2" sound played when flag is captured...
1313 "noise3" sound played when flag is lost in the field and respawns itself... 
1314 "noise4" sound played when flag is dropped by a player...
1315 "noise5" sound played when flag touches the ground... */
1316 void spawnfunc_item_flag_team2()
1317 {
1318         if(!g_ctf) { remove(self); return; }
1319
1320         ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
1321 }
1322
1323 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
1324 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
1325 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.
1326 Keys:
1327 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
1328 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
1329 void spawnfunc_ctf_team()
1330 {
1331         if(!g_ctf) { remove(self); return; }
1332         
1333         self.classname = "ctf_team";
1334         self.team = self.cnt + 1;
1335 }
1336
1337
1338 // ==============
1339 // Initialization
1340 // ==============
1341
1342 // code from here on is just to support maps that don't have flag and team entities
1343 void ctf_SpawnTeam (string teamname, float teamcolor)
1344 {
1345         entity oldself;
1346         oldself = self;
1347         self = spawn();
1348         self.classname = "ctf_team";
1349         self.netname = teamname;
1350         self.cnt = teamcolor;
1351
1352         spawnfunc_ctf_team();
1353
1354         self = oldself;
1355 }
1356
1357 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
1358 {
1359         // if no teams are found, spawn defaults
1360         if(find(world, classname, "ctf_team") == world)
1361         {
1362                 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
1363                 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
1364                 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
1365         }
1366         
1367         ScoreRules_ctf();
1368 }
1369
1370 void ctf_Initialize()
1371 {
1372         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
1373
1374         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
1375         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
1376         ctf_captureshield_force = autocvar_g_ctf_shield_force;
1377         
1378         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
1379 }
1380
1381
1382 MUTATOR_DEFINITION(gamemode_ctf)
1383 {
1384         MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
1385         MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
1386         MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
1387         MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
1388         MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
1389         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
1390         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
1391         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
1392         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
1393         MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
1394         MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
1395         MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
1396         MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
1397         
1398         MUTATOR_ONADD
1399         {
1400                 if(time > 1) // game loads at time 1
1401                         error("This is a game type and it cannot be added at runtime.");
1402                 g_ctf = 1;
1403                 ctf_Initialize();
1404         }
1405
1406         MUTATOR_ONREMOVE
1407         {
1408                 g_ctf = 0;
1409                 error("This is a game type and it cannot be removed at runtime.");
1410         }
1411
1412         return 0;
1413 }