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