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