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