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