]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_ctf.qc
Don't allow players to throw the flag in intermission
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_ctf.qc
1 // ================================================================
2 //  Official capture the flag game mode coding, reworked by Samual
3 //  Last updated: September, 2012
4 // ================================================================
5
6 void ctf_FakeTimeLimit(entity e, float t)
7 {
8         msg_entity = e;
9         WriteByte(MSG_ONE, 3); // svc_updatestat
10         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
11         if(t < 0)
12                 WriteCoord(MSG_ONE, autocvar_timelimit);
13         else
14                 WriteCoord(MSG_ONE, (t + 1) / 60);
15 }
16
17 void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for easy changing and quick editing later
18 {
19         if(autocvar_sv_eventlog)
20                 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
21 }
22
23 string ctf_CaptureRecord(entity flag, entity player)
24 {
25         float cap_time, cap_record, success;
26         string cap_message, refername;
27         
28         if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots)) 
29         {
30                 cap_record = ctf_captimerecord;
31                 cap_time = (time - flag.ctf_pickuptime);
32
33                 refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
34                 refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's"));
35
36                 if(!ctf_captimerecord) 
37                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; }
38                 else if(cap_time < cap_record) 
39                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; }
40                 else
41                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; }
42
43                 if(success) 
44                 {
45                         ctf_captimerecord = cap_time;
46                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
47                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
48                         write_recordmarker(player, (time - cap_time), cap_time); 
49                 } 
50         }
51         
52         return cap_message;
53 }
54
55 void ctf_FlagcarrierWaypoints(entity player)
56 {
57         WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
58         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
59         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
60         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
61 }
62
63
64 // =======================
65 // CaptureShield Functions 
66 // =======================
67
68 float ctf_CaptureShield_CheckStatus(entity p) 
69 {
70         float s, se;
71         entity e;
72         float players_worseeq, players_total;
73
74         if(ctf_captureshield_max_ratio <= 0)
75                 return FALSE;
76
77         s = PlayerScore_Add(p, SP_SCORE, 0);
78         if(s >= -ctf_captureshield_min_negscore)
79                 return FALSE;
80
81         players_total = players_worseeq = 0;
82         FOR_EACH_PLAYER(e)
83         {
84                 if(IsDifferentTeam(e, p))
85                         continue;
86                 se = PlayerScore_Add(e, SP_SCORE, 0);
87                 if(se <= s)
88                         ++players_worseeq;
89                 ++players_total;
90         }
91
92         // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
93         // use this rule here
94         
95         if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
96                 return FALSE;
97
98         return TRUE;
99 }
100
101 void ctf_CaptureShield_Update(entity player, float wanted_status)
102 {
103         float updated_status = ctf_CaptureShield_CheckStatus(player);
104         if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
105         {
106                 if(updated_status) // TODO csqc notifier for this // Samual: How?
107                         Send_CSQC_Centerprint_Generic(player, CPID_CTF_CAPTURESHIELD, "^3You are now ^4shielded^3 from the flag\n^3for ^1too many unsuccessful attempts^3 to capture.\n\n^3Make some defensive scores before trying again.", 5, 0);
108                 else
109                         Send_CSQC_Centerprint_Generic(player, CPID_CTF_CAPTURESHIELD, "^3You are now free.\n\n^3Feel free to ^1try to capture^3 the flag again\n^3if you think you will succeed.", 5, 0);
110                         
111                 player.ctf_captureshielded = updated_status;
112         }
113 }
114
115 float ctf_CaptureShield_Customize()
116 {
117         if(!other.ctf_captureshielded) { return FALSE; }
118         if(!IsDifferentTeam(self, other)) { return FALSE; }
119         
120         return TRUE;
121 }
122
123 void ctf_CaptureShield_Touch()
124 {
125         if(!other.ctf_captureshielded) { return; }
126         if(!IsDifferentTeam(self, other)) { return; }
127         
128         vector mymid = (self.absmin + self.absmax) * 0.5;
129         vector othermid = (other.absmin + other.absmax) * 0.5;
130
131         Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
132         Send_CSQC_Centerprint_Generic(other, CPID_CTF_CAPTURESHIELD, "^3You are ^4shielded^3 from the flag\n^3for ^1too many unsuccessful attempts^3 to capture.\n\n^3Get some defensive scores before trying again.", 5, 0);
133 }
134
135 void ctf_CaptureShield_Spawn(entity flag)
136 {
137         entity shield = spawn();
138         
139         shield.enemy = self;
140         shield.team = self.team;
141         shield.touch = ctf_CaptureShield_Touch;
142         shield.customizeentityforclient = ctf_CaptureShield_Customize;
143         shield.classname = "ctf_captureshield";
144         shield.effects = EF_ADDITIVE;
145         shield.movetype = MOVETYPE_NOCLIP;
146         shield.solid = SOLID_TRIGGER;
147         shield.avelocity = '7 0 11';
148         shield.scale = 0.5;
149         
150         setorigin(shield, self.origin);
151         setmodel(shield, "models/ctf/shield.md3");
152         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
153 }
154
155
156 // ====================
157 // Drop/Pass/Throw Code
158 // ====================
159
160 void ctf_Handle_Drop(entity flag, entity player, float droptype)
161 {
162         // declarations
163         player = (player ? player : flag.pass_sender);
164
165         // main
166         flag.movetype = MOVETYPE_TOSS;
167         flag.takedamage = DAMAGE_YES;
168         flag.health = flag.max_flag_health;
169         flag.ctf_droptime = time;
170         flag.ctf_dropper = player;
171         flag.ctf_status = FLAG_DROPPED;
172         
173         // messages and sounds
174         Send_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
175         sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
176         ctf_EventLog("dropped", player.team, player);
177
178         // scoring
179         PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);   
180         PlayerScore_Add(player, SP_CTF_DROPS, 1);
181         
182         // waypoints
183         if(autocvar_g_ctf_flag_dropped_waypoint)
184                 WaypointSprite_Spawn("flagdropped", 0, 0, flag, FLAG_WAYPOINT_OFFSET, world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, TRUE, RADARICON_FLAG, WPCOLOR_DROPPEDFLAG(flag.team));
185
186         if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
187         {
188                 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
189                 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
190         }
191         
192         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
193         
194         if(droptype == DROP_PASS)
195         {
196                 flag.pass_sender = world;
197                 flag.pass_target = world;
198         }
199 }
200
201 void ctf_Handle_Retrieve(entity flag, entity player)
202 {
203         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
204         entity sender = flag.pass_sender;
205         
206         // transfer flag to player
207         flag.owner = player;
208         flag.owner.flagcarried = flag;
209         
210         // reset flag
211         setattachment(flag, player, "");
212         setorigin(flag, FLAG_CARRY_OFFSET);
213         flag.movetype = MOVETYPE_NONE;
214         flag.takedamage = DAMAGE_NO;
215         flag.solid = SOLID_NOT;
216         flag.ctf_status = FLAG_CARRY;
217
218         // messages and sounds
219         sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTN_NORM);
220         ctf_EventLog("receive", flag.team, player);
221         
222         FOR_EACH_REALPLAYER(tmp_player)
223         {
224                 if(tmp_player == sender)
225                         centerprint(tmp_player, strcat("You passed the ", flag.netname, " to ", player.netname));
226                 else if(tmp_player == player)
227                         centerprint(tmp_player, strcat("You received the ", flag.netname, " from ", sender.netname));
228                 else if(!IsDifferentTeam(tmp_player, sender))
229                         centerprint(tmp_player, strcat(sender.netname, " passed the ", flag.netname, " to ", player.netname));
230         }
231         
232         // create new waypoint
233         ctf_FlagcarrierWaypoints(player);
234         
235         sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
236         player.throw_antispam = sender.throw_antispam;
237
238         flag.pass_sender = world;
239         flag.pass_target = world;
240 }
241
242 void ctf_Handle_Throw(entity player, entity receiver, float droptype)
243 {
244         entity flag = player.flagcarried;
245         vector targ_origin, flag_velocity;
246         
247         if(!flag) { return; }
248         if((droptype == DROP_PASS) && !receiver) { return; }
249         
250         if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
251         
252         // reset the flag
253         setattachment(flag, world, "");
254         setorigin(flag, player.origin + FLAG_DROP_OFFSET);
255         flag.owner.flagcarried = world;
256         flag.owner = world;
257         flag.solid = SOLID_TRIGGER;
258         flag.ctf_dropper = player;
259         flag.ctf_droptime = time;
260         
261         flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
262         
263         switch(droptype)
264         {
265                 case DROP_PASS:
266                 {
267                         WarpZone_RefSys_Copy(flag, receiver);
268                         targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax)));
269                         flag.velocity = (normalize(targ_origin - player.origin) * autocvar_g_ctf_pass_velocity);
270                         break;
271                 }
272                 
273                 case DROP_THROW:
274                 {
275                         makevectors((player.v_angle_y * '0 1 0') + (player.v_angle_x * '0.5 0 0'));
276                         flag_velocity = ('0 0 200' + ((v_forward * autocvar_g_ctf_drop_velocity) * ((player.items & IT_STRENGTH) ? autocvar_g_ctf_drop_strengthmultiplier : 1)));
277                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, FALSE);
278                         break;
279                 }
280                 
281                 case DROP_RESET:
282                 {
283                         flag.velocity = '0 0 0'; // do nothing
284                         break;
285                 }
286                 
287                 default:
288                 case DROP_NORMAL:
289                 {
290                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, ('0 0 200' + ('0 100 0' * crandom()) + ('100 0 0' * crandom())), FALSE);
291                         break;
292                 }
293         }
294         
295         switch(droptype)
296         {
297                 case DROP_PASS:
298                 {
299                         // main
300                         flag.movetype = MOVETYPE_FLY;
301                         flag.takedamage = DAMAGE_NO;
302                         flag.pass_sender = player;
303                         flag.pass_target = receiver;
304                         flag.ctf_status = FLAG_PASSING;
305                         
306                         // other
307                         sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM);
308                         WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), targ_origin, player.origin);
309                         ctf_EventLog("pass", flag.team, player);
310                         break;
311                 }
312
313                 case DROP_RESET: 
314                 {
315                         // do nothing
316                         break;
317                 }
318                 
319                 default:
320                 case DROP_THROW:
321                 case DROP_NORMAL:
322                 {
323                         ctf_Handle_Drop(flag, player, droptype);
324                         break;
325                 }
326         }
327
328         // kill old waypointsprite
329         WaypointSprite_Ping(player.wps_flagcarrier);
330         WaypointSprite_Kill(player.wps_flagcarrier);
331         
332         if(player.wps_enemyflagcarrier)
333                 WaypointSprite_Kill(player.wps_enemyflagcarrier);
334         
335         // captureshield
336         ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
337 }
338
339
340 // ==============
341 // Event Handlers
342 // ==============
343
344 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
345 {
346         entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
347         entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
348         float old_time, new_time; 
349         
350         if not(player) { return; } // without someone to give the reward to, we can't possibly cap
351         
352         // messages and sounds
353         Send_KillNotification(player.netname, enemy_flag.netname, ctf_CaptureRecord(enemy_flag, player), INFO_CAPTUREFLAG, MSG_INFO);
354         sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
355         
356         switch(capturetype)
357         {
358                 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
359                 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
360                 default: break;
361         }
362         
363         // scoring
364         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
365         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
366
367         old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
368         new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
369         if(!old_time || new_time < old_time)
370                 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
371
372         // effects
373         if(autocvar_g_ctf_flag_capture_effects) 
374         {
375                 pointparticles(particleeffectnum((player.team == COLOR_TEAM1) ? "red_ground_quake" : "blue_ground_quake"), flag.origin, '0 0 0', 1);
376                 shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
377         }
378
379         // other
380         if(capturetype == CAPTURE_NORMAL)
381         {
382                 WaypointSprite_Kill(player.wps_flagcarrier);
383                 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
384                 
385                 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
386                         { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
387         }
388         
389         // reset the flag
390         player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
391         ctf_RespawnFlag(enemy_flag);
392 }
393
394 void ctf_Handle_Return(entity flag, entity player)
395 {
396         // messages and sounds
397         //centerprint(player, strcat("You returned the ", flag.netname));
398         Send_KillNotification(player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO);
399         sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
400         ctf_EventLog("return", flag.team, player);
401
402         // scoring
403         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
404         PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
405
406         TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
407         
408         if(flag.ctf_dropper) 
409         {
410                 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
411                 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag 
412                 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
413         }
414         
415         // reset the flag
416         ctf_RespawnFlag(flag);
417 }
418
419 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
420 {
421         // declarations
422         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
423         string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
424         float pickup_dropped_score; // used to calculate dropped pickup score
425         
426         // attach the flag to the player
427         flag.owner = player;
428         player.flagcarried = flag;
429         setattachment(flag, player, "");
430         setorigin(flag, FLAG_CARRY_OFFSET);
431         
432         // flag setup
433         flag.movetype = MOVETYPE_NONE;
434         flag.takedamage = DAMAGE_NO;
435         flag.solid = SOLID_NOT;
436         flag.angles = '0 0 0';
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_dropper = world;
855         flag.ctf_pickuptime = 0;
856         flag.ctf_droptime = 0;
857
858         wpforenemy_announced = FALSE;
859 }
860
861 void ctf_Reset()
862 {
863         if(self.owner)
864                 if(self.owner.classname == "player")
865                         ctf_Handle_Throw(self.owner, world, DROP_RESET);
866                         
867         ctf_RespawnFlag(self);
868 }
869
870 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
871 {
872         // bot waypoints
873         waypoint_spawnforitem_force(self, self.origin);
874         self.nearestwaypointtimeout = 0; // activate waypointing again
875         self.bot_basewaypoint = self.nearestwaypoint;
876
877         // waypointsprites
878         WaypointSprite_SpawnFixed(((self.team == COLOR_TEAM1) ? "redbase" : "bluebase"), self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
879         WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
880
881         // captureshield setup
882         ctf_CaptureShield_Spawn(self);
883 }
884
885 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc 
886 {       
887         // declarations
888         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. 
889         self = flag; // for later usage with droptofloor()
890         
891         // main setup
892         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
893         ctf_worldflaglist = flag;
894
895         setattachment(flag, world, ""); 
896
897         flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
898         flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue)
899         flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
900         flag.classname = "item_flag_team";
901         flag.target = "###item###"; // wut?
902         flag.flags = FL_ITEM | FL_NOTARGET;
903         flag.solid = SOLID_TRIGGER;
904         flag.takedamage = DAMAGE_NO;
905         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;   
906         flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
907         flag.health = flag.max_flag_health;
908         flag.event_damage = ctf_FlagDamage;
909         flag.pushable = TRUE;
910         flag.teleportable = TELEPORT_NORMAL;
911         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
912         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
913         flag.velocity = '0 0 0';
914         flag.mangle = flag.angles;
915         flag.reset = ctf_Reset;
916         flag.touch = ctf_FlagTouch;
917         flag.think = ctf_FlagThink;
918         flag.nextthink = time + FLAG_THINKRATE;
919         flag.ctf_status = FLAG_BASE;
920         
921         if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
922         if(!flag.scale) { flag.scale = FLAG_SCALE; }
923         if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
924         if(!flag.toucheffect) { flag.toucheffect = ((teamnumber) ? "redflag_touch" : "blueflag_touch"); }
925         if(!flag.passeffect) { flag.passeffect = ((!teamnumber) ? "red_pass" : "blue_pass"); } // invert the team number of the flag to pass as enemy team color
926         
927         // sound 
928         if(!flag.snd_flag_taken) { flag.snd_flag_taken  = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
929         if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
930         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
931         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.
932         if(!flag.snd_flag_dropped) { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
933         if(!flag.snd_flag_touch) { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
934         if(!flag.snd_flag_pass) { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
935         
936         // precache
937         precache_sound(flag.snd_flag_taken);
938         precache_sound(flag.snd_flag_returned);
939         precache_sound(flag.snd_flag_capture);
940         precache_sound(flag.snd_flag_respawn);
941         precache_sound(flag.snd_flag_dropped);
942         precache_sound(flag.snd_flag_touch);
943         precache_sound(flag.snd_flag_pass);
944         precache_model(flag.model);
945         precache_model("models/ctf/shield.md3");
946         precache_model("models/ctf/shockwavetransring.md3");
947
948         // appearence
949         setmodel(flag, flag.model); // precision set below
950         setsize(flag, FLAG_MIN, FLAG_MAX);
951         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
952         
953         if(autocvar_g_ctf_flag_glowtrails)
954         {
955                 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
956                 flag.glow_size = 25;
957                 flag.glow_trail = 1;
958         }
959         
960         flag.effects |= EF_LOWPRECISION;
961         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
962         if(autocvar_g_ctf_dynamiclights)   { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
963         
964         // flag placement
965         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
966         {       
967                 flag.dropped_origin = flag.origin; 
968                 flag.noalign = TRUE;
969                 flag.movetype = MOVETYPE_NONE;
970         }
971         else // drop to floor, automatically find a platform and set that as spawn origin
972         { 
973                 flag.noalign = FALSE;
974                 self = flag;
975                 droptofloor();
976                 flag.movetype = MOVETYPE_TOSS; 
977         }       
978         
979         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
980 }
981
982
983 // ==============
984 // Hook Functions
985 // ==============
986
987 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
988 {
989         if(gameover) { return 0; }
990         
991         entity flag;
992         
993         // initially clear items so they can be set as necessary later.
994         self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST 
995                 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
996
997         // scan through all the flags and notify the client about them 
998         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
999         {
1000                 switch(flag.ctf_status)
1001                 {
1002                         case FLAG_PASSING:
1003                         case FLAG_CARRY:
1004                         {
1005                                 if((flag.owner == self) || (flag.pass_sender == self))
1006                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1007                                 else 
1008                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1009                                 break;
1010                         }
1011                         case FLAG_DROPPED:
1012                         {
1013                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1014                                 break;
1015                         }
1016                 }
1017         }
1018         
1019         // item for stopping players from capturing the flag too often
1020         if(self.ctf_captureshielded)
1021                 self.items |= IT_CTF_SHIELDED;
1022         
1023         // update the health of the flag carrier waypointsprite
1024         if(self.wps_flagcarrier) 
1025                 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1026         
1027         return 0;
1028 }
1029
1030 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1031 {
1032         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1033         {
1034                 if(frag_target == frag_attacker) // damage done to yourself
1035                 {
1036                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1037                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1038                 }
1039                 else // damage done to everyone else
1040                 {
1041                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1042                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1043                 }
1044         }
1045         else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1046         {
1047                 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)))
1048                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier); // TODO: only do this if there is a significant loss of health?
1049         }
1050         return 0;
1051 }
1052
1053 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1054 {
1055         if((frag_attacker != frag_target) && (frag_attacker.classname == "player") && (frag_target.flagcarried))
1056         {
1057                 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1058                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1059         }
1060                                 
1061         if(frag_target.flagcarried)
1062                 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1063                 
1064         return 0;
1065 }
1066
1067 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1068 {
1069         frag_score = 0;
1070         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1071 }
1072
1073 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1074 {
1075         if(self.flagcarried)
1076                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1077                 
1078         return 0;
1079 }
1080
1081 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1082 {
1083         if(self.flagcarried) 
1084         if(!autocvar_g_ctf_portalteleport)
1085                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1086
1087         return 0;
1088 }
1089
1090 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1091 {
1092         if(gameover) { return 0; }
1093         
1094         entity player = self;
1095
1096         if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1097         {
1098                 // pass the flag to a team mate
1099                 if(autocvar_g_ctf_pass)
1100                 {
1101                         entity head, closest_target;
1102                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1103                         
1104                         while(head) // find the closest acceptable target to pass to
1105                         {
1106                                 if(head.classname == "player" && head.deadflag == DEAD_NO)
1107                                 if(head != player && !IsDifferentTeam(head, player))
1108                                 if(!head.speedrunning && (!head.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1109                                 {
1110                                         if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried) 
1111                                         { 
1112                                                 if(clienttype(head) == CLIENTTYPE_BOT)
1113                                                 {
1114                                                         centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1115                                                         ctf_Handle_Throw(head, player, DROP_PASS);
1116                                                 }
1117                                                 else
1118                                                 {
1119                                                         centerprint(head, strcat(player.netname, " requests you to pass the ", head.flagcarried.netname)); 
1120                                                         centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1121                                                 }
1122                                                 player.throw_antispam = time + autocvar_g_ctf_pass_wait; 
1123                                                 return 0; 
1124                                         }
1125                                         else if(player.flagcarried)
1126                                         {
1127                                                 if(closest_target)
1128                                                 {
1129                                                         if(vlen(player.origin - WarpZone_UnTransformOrigin(head, head.origin)) < vlen(player.origin - WarpZone_UnTransformOrigin(closest_target, closest_target.origin)))
1130                                                                 { closest_target = head; }
1131                                                 }
1132                                                 else { closest_target = head; }
1133                                         }
1134                                 }
1135                                 head = head.chain;
1136                         }
1137                         
1138                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return 0; }
1139                 }
1140                 
1141                 // throw the flag in front of you
1142                 if(autocvar_g_ctf_drop && player.flagcarried)
1143                         { ctf_Handle_Throw(player, world, DROP_THROW); }
1144         }
1145                 
1146         return 0;
1147 }
1148
1149 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1150 {
1151         if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1152         {
1153                 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1154         } 
1155         else // create a normal help me waypointsprite
1156         {
1157                 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');
1158                 WaypointSprite_Ping(self.wps_helpme);
1159         }
1160
1161         return 1;
1162 }
1163
1164 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1165 {
1166         if(vh_player.flagcarried)
1167         {
1168                 if(!autocvar_g_ctf_flagcarrier_allow_vehicle_carry)
1169                 {
1170                         ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1171                 }
1172                 else
1173                 {            
1174                         setattachment(vh_player.flagcarried, vh_vehicle, ""); 
1175                         setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1176                         vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1177                         //vh_player.flagcarried.angles = '0 0 0';       
1178                 }
1179         }
1180                 
1181         return 0;
1182 }
1183
1184 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1185 {
1186         if(vh_player.flagcarried)
1187         {
1188                 setattachment(vh_player.flagcarried, vh_player, ""); 
1189                 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
1190                 vh_player.flagcarried.scale = FLAG_SCALE;
1191                 vh_player.flagcarried.angles = '0 0 0'; 
1192         }
1193
1194         return 0;
1195 }
1196
1197 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
1198 {
1199         if(self.flagcarried)
1200         {
1201                 bprint("The ", self.flagcarried.netname, " was returned to base by its carrier\n");
1202                 ctf_RespawnFlag(self);
1203         }
1204         
1205         return 0;
1206 }
1207
1208 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
1209 {
1210         entity flag; // temporary entity for the search method
1211         
1212         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1213         {
1214                 switch(flag.ctf_status)
1215                 {
1216                         case FLAG_DROPPED:
1217                         case FLAG_PASSING:
1218                         {
1219                                 // lock the flag, game is over
1220                                 flag.movetype = MOVETYPE_NONE;
1221                                 flag.takedamage = DAMAGE_NO;
1222                                 flag.solid = SOLID_NOT;
1223                                 flag.nextthink = FALSE; // stop thinking
1224                                 
1225                                 print("stopping the ", flag.netname, " from moving.\n");
1226                                 break;
1227                         }
1228                         
1229                         default:
1230                         case FLAG_BASE:
1231                         case FLAG_CARRY:
1232                         {
1233                                 // do nothing for these flags
1234                                 break;
1235                         }
1236                 }
1237         }
1238         
1239         return 0;
1240 }
1241
1242
1243 // ==========
1244 // Spawnfuncs
1245 // ==========
1246
1247 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
1248 CTF Starting point for a player in team one (Red).
1249 Keys: "angle" viewing angle when spawning. */
1250 void spawnfunc_info_player_team1()
1251 {
1252         if(g_assault) { remove(self); return; }
1253         
1254         self.team = COLOR_TEAM1; // red
1255         spawnfunc_info_player_deathmatch();
1256 }
1257
1258
1259 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
1260 CTF Starting point for a player in team two (Blue).
1261 Keys: "angle" viewing angle when spawning. */
1262 void spawnfunc_info_player_team2()
1263 {
1264         if(g_assault) { remove(self); return; }
1265         
1266         self.team = COLOR_TEAM2; // blue
1267         spawnfunc_info_player_deathmatch();
1268 }
1269
1270 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
1271 CTF Starting point for a player in team three (Yellow).
1272 Keys: "angle" viewing angle when spawning. */
1273 void spawnfunc_info_player_team3()
1274 {
1275         if(g_assault) { remove(self); return; }
1276         
1277         self.team = COLOR_TEAM3; // yellow
1278         spawnfunc_info_player_deathmatch();
1279 }
1280
1281
1282 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
1283 CTF Starting point for a player in team four (Purple).
1284 Keys: "angle" viewing angle when spawning. */
1285 void spawnfunc_info_player_team4()
1286 {
1287         if(g_assault) { remove(self); return; }
1288         
1289         self.team = COLOR_TEAM4; // purple
1290         spawnfunc_info_player_deathmatch();
1291 }
1292
1293 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1294 CTF flag for team one (Red).
1295 Keys: 
1296 "angle" Angle the flag will point (minus 90 degrees)... 
1297 "model" model to use, note this needs red and blue as skins 0 and 1...
1298 "noise" sound played when flag is picked up...
1299 "noise1" sound played when flag is returned by a teammate...
1300 "noise2" sound played when flag is captured...
1301 "noise3" sound played when flag is lost in the field and respawns itself... 
1302 "noise4" sound played when flag is dropped by a player...
1303 "noise5" sound played when flag touches the ground... */
1304 void spawnfunc_item_flag_team1()
1305 {
1306         if(!g_ctf) { remove(self); return; }
1307
1308         ctf_FlagSetup(1, self); // 1 = red
1309 }
1310
1311 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1312 CTF flag for team two (Blue).
1313 Keys: 
1314 "angle" Angle the flag will point (minus 90 degrees)... 
1315 "model" model to use, note this needs red and blue as skins 0 and 1...
1316 "noise" sound played when flag is picked up...
1317 "noise1" sound played when flag is returned by a teammate...
1318 "noise2" sound played when flag is captured...
1319 "noise3" sound played when flag is lost in the field and respawns itself... 
1320 "noise4" sound played when flag is dropped by a player...
1321 "noise5" sound played when flag touches the ground... */
1322 void spawnfunc_item_flag_team2()
1323 {
1324         if(!g_ctf) { remove(self); return; }
1325
1326         ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
1327 }
1328
1329 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
1330 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
1331 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.
1332 Keys:
1333 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
1334 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
1335 void spawnfunc_ctf_team()
1336 {
1337         if(!g_ctf) { remove(self); return; }
1338         
1339         self.classname = "ctf_team";
1340         self.team = self.cnt + 1;
1341 }
1342
1343
1344 // ==============
1345 // Initialization
1346 // ==============
1347
1348 // code from here on is just to support maps that don't have flag and team entities
1349 void ctf_SpawnTeam (string teamname, float teamcolor)
1350 {
1351         entity oldself;
1352         oldself = self;
1353         self = spawn();
1354         self.classname = "ctf_team";
1355         self.netname = teamname;
1356         self.cnt = teamcolor;
1357
1358         spawnfunc_ctf_team();
1359
1360         self = oldself;
1361 }
1362
1363 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
1364 {
1365         // if no teams are found, spawn defaults
1366         if(find(world, classname, "ctf_team") == world)
1367         {
1368                 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
1369                 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
1370                 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
1371         }
1372         
1373         ScoreRules_ctf();
1374 }
1375
1376 void ctf_Initialize()
1377 {
1378         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
1379
1380         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
1381         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
1382         ctf_captureshield_force = autocvar_g_ctf_shield_force;
1383         
1384         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
1385 }
1386
1387
1388 MUTATOR_DEFINITION(gamemode_ctf)
1389 {
1390         MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
1391         MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
1392         MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
1393         MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
1394         MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
1395         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
1396         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
1397         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
1398         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
1399         MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
1400         MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
1401         MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
1402         MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
1403         
1404         MUTATOR_ONADD
1405         {
1406                 if(time > 1) // game loads at time 1
1407                         error("This is a game type and it cannot be added at runtime.");
1408                 g_ctf = 1;
1409                 ctf_Initialize();
1410         }
1411
1412         MUTATOR_ONREMOVE
1413         {
1414                 g_ctf = 0;
1415                 error("This is a game type and it cannot be removed at runtime.");
1416         }
1417
1418         return 0;
1419 }