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