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