]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_ctf.qc
New capture effects
[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(flag.capeffect), 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         if(!flag.capeffect) { flag.capeffect = ((teamnumber) ? "red_cap" : "blue_cap"); }
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 FALSE;
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 FALSE;
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 FALSE;
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 FALSE;
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 FALSE;
1087 }
1088
1089 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1090 {
1091         if(gameover) { return FALSE; }
1092         
1093         entity player = self;
1094
1095         if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1096         {
1097                 // pass the flag to a team mate
1098                 if(autocvar_g_ctf_pass)
1099                 {
1100                         entity head, closest_target;
1101                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1102                         
1103                         while(head) // find the closest acceptable target to pass to
1104                         {
1105                                 if(head.classname == "player" && head.deadflag == DEAD_NO)
1106                                 if(head != player && !IsDifferentTeam(head, player))
1107                                 if(!head.speedrunning && (!head.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1108                                 {
1109                                         if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried) 
1110                                         { 
1111                                                 if(clienttype(head) == CLIENTTYPE_BOT)
1112                                                 {
1113                                                         centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1114                                                         ctf_Handle_Throw(head, player, DROP_PASS);
1115                                                 }
1116                                                 else
1117                                                 {
1118                                                         centerprint(head, strcat(player.netname, " requests you to pass the ", head.flagcarried.netname)); 
1119                                                         centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1120                                                 }
1121                                                 player.throw_antispam = time + autocvar_g_ctf_pass_wait; 
1122                                                 return TRUE; 
1123                                         }
1124                                         else if(player.flagcarried)
1125                                         {
1126                                                 if(closest_target)
1127                                                 {
1128                                                         if(vlen(player.origin - WarpZone_UnTransformOrigin(head, head.origin)) < vlen(player.origin - WarpZone_UnTransformOrigin(closest_target, closest_target.origin)))
1129                                                                 { closest_target = head; }
1130                                                 }
1131                                                 else { closest_target = head; }
1132                                         }
1133                                 }
1134                                 head = head.chain;
1135                         }
1136                         
1137                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1138                 }
1139                 
1140                 // throw the flag in front of you
1141                 if(autocvar_g_ctf_drop && player.flagcarried)
1142                         { ctf_Handle_Throw(player, world, DROP_THROW); return TRUE; }
1143         }
1144                 
1145         return FALSE;
1146 }
1147
1148 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1149 {
1150         if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1151         {
1152                 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1153         } 
1154         else // create a normal help me waypointsprite
1155         {
1156                 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');
1157                 WaypointSprite_Ping(self.wps_helpme);
1158         }
1159
1160         return TRUE;
1161 }
1162
1163 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1164 {
1165         if(vh_player.flagcarried)
1166         {
1167                 if(!autocvar_g_ctf_flagcarrier_allow_vehicle_carry)
1168                 {
1169                         ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1170                 }
1171                 else
1172                 {            
1173                         setattachment(vh_player.flagcarried, vh_vehicle, ""); 
1174                         setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1175                         vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1176                         //vh_player.flagcarried.angles = '0 0 0';       
1177                 }
1178         }
1179                 
1180         return FALSE;
1181 }
1182
1183 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1184 {
1185         if(vh_player.flagcarried)
1186         {
1187                 setattachment(vh_player.flagcarried, vh_player, ""); 
1188                 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
1189                 vh_player.flagcarried.scale = FLAG_SCALE;
1190                 vh_player.flagcarried.angles = '0 0 0';
1191                 return TRUE;
1192         }
1193
1194         return FALSE;
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                 return TRUE;
1204         }
1205         
1206         return FALSE;
1207 }
1208
1209 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
1210 {
1211         entity flag; // temporary entity for the search method
1212         
1213         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1214         {
1215                 switch(flag.ctf_status)
1216                 {
1217                         case FLAG_DROPPED:
1218                         case FLAG_PASSING:
1219                         {
1220                                 // lock the flag, game is over
1221                                 flag.movetype = MOVETYPE_NONE;
1222                                 flag.takedamage = DAMAGE_NO;
1223                                 flag.solid = SOLID_NOT;
1224                                 flag.nextthink = FALSE; // stop thinking
1225                                 
1226                                 print("stopping the ", flag.netname, " from moving.\n");
1227                                 break;
1228                         }
1229                         
1230                         default:
1231                         case FLAG_BASE:
1232                         case FLAG_CARRY:
1233                         {
1234                                 // do nothing for these flags
1235                                 break;
1236                         }
1237                 }
1238         }
1239         
1240         return FALSE;
1241 }
1242
1243
1244 // ==========
1245 // Spawnfuncs
1246 // ==========
1247
1248 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
1249 CTF Starting point for a player in team one (Red).
1250 Keys: "angle" viewing angle when spawning. */
1251 void spawnfunc_info_player_team1()
1252 {
1253         if(g_assault) { remove(self); return; }
1254         
1255         self.team = COLOR_TEAM1; // red
1256         spawnfunc_info_player_deathmatch();
1257 }
1258
1259
1260 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
1261 CTF Starting point for a player in team two (Blue).
1262 Keys: "angle" viewing angle when spawning. */
1263 void spawnfunc_info_player_team2()
1264 {
1265         if(g_assault) { remove(self); return; }
1266         
1267         self.team = COLOR_TEAM2; // blue
1268         spawnfunc_info_player_deathmatch();
1269 }
1270
1271 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
1272 CTF Starting point for a player in team three (Yellow).
1273 Keys: "angle" viewing angle when spawning. */
1274 void spawnfunc_info_player_team3()
1275 {
1276         if(g_assault) { remove(self); return; }
1277         
1278         self.team = COLOR_TEAM3; // yellow
1279         spawnfunc_info_player_deathmatch();
1280 }
1281
1282
1283 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
1284 CTF Starting point for a player in team four (Purple).
1285 Keys: "angle" viewing angle when spawning. */
1286 void spawnfunc_info_player_team4()
1287 {
1288         if(g_assault) { remove(self); return; }
1289         
1290         self.team = COLOR_TEAM4; // purple
1291         spawnfunc_info_player_deathmatch();
1292 }
1293
1294 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1295 CTF flag for team one (Red).
1296 Keys: 
1297 "angle" Angle the flag will point (minus 90 degrees)... 
1298 "model" model to use, note this needs red and blue as skins 0 and 1...
1299 "noise" sound played when flag is picked up...
1300 "noise1" sound played when flag is returned by a teammate...
1301 "noise2" sound played when flag is captured...
1302 "noise3" sound played when flag is lost in the field and respawns itself... 
1303 "noise4" sound played when flag is dropped by a player...
1304 "noise5" sound played when flag touches the ground... */
1305 void spawnfunc_item_flag_team1()
1306 {
1307         if(!g_ctf) { remove(self); return; }
1308
1309         ctf_FlagSetup(1, self); // 1 = red
1310 }
1311
1312 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
1313 CTF flag for team two (Blue).
1314 Keys: 
1315 "angle" Angle the flag will point (minus 90 degrees)... 
1316 "model" model to use, note this needs red and blue as skins 0 and 1...
1317 "noise" sound played when flag is picked up...
1318 "noise1" sound played when flag is returned by a teammate...
1319 "noise2" sound played when flag is captured...
1320 "noise3" sound played when flag is lost in the field and respawns itself... 
1321 "noise4" sound played when flag is dropped by a player...
1322 "noise5" sound played when flag touches the ground... */
1323 void spawnfunc_item_flag_team2()
1324 {
1325         if(!g_ctf) { remove(self); return; }
1326
1327         ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
1328 }
1329
1330 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
1331 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
1332 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.
1333 Keys:
1334 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
1335 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
1336 void spawnfunc_ctf_team()
1337 {
1338         if(!g_ctf) { remove(self); return; }
1339         
1340         self.classname = "ctf_team";
1341         self.team = self.cnt + 1;
1342 }
1343
1344
1345 // ==============
1346 // Initialization
1347 // ==============
1348
1349 // code from here on is just to support maps that don't have flag and team entities
1350 void ctf_SpawnTeam (string teamname, float teamcolor)
1351 {
1352         entity oldself;
1353         oldself = self;
1354         self = spawn();
1355         self.classname = "ctf_team";
1356         self.netname = teamname;
1357         self.cnt = teamcolor;
1358
1359         spawnfunc_ctf_team();
1360
1361         self = oldself;
1362 }
1363
1364 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
1365 {
1366         // if no teams are found, spawn defaults
1367         if(find(world, classname, "ctf_team") == world)
1368         {
1369                 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
1370                 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
1371                 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
1372         }
1373         
1374         ScoreRules_ctf();
1375 }
1376
1377 void ctf_Initialize()
1378 {
1379         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
1380
1381         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
1382         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
1383         ctf_captureshield_force = autocvar_g_ctf_shield_force;
1384         
1385         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
1386 }
1387
1388
1389 MUTATOR_DEFINITION(gamemode_ctf)
1390 {
1391         MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
1392         MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
1393         MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
1394         MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
1395         MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
1396         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
1397         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
1398         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
1399         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
1400         MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
1401         MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
1402         MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
1403         MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
1404         
1405         MUTATOR_ONADD
1406         {
1407                 if(time > 1) // game loads at time 1
1408                         error("This is a game type and it cannot be added at runtime.");
1409                 g_ctf = 1;
1410                 ctf_Initialize();
1411         }
1412
1413         MUTATOR_ONREMOVE
1414         {
1415                 g_ctf = 0;
1416                 error("This is a game type and it cannot be removed at runtime.");
1417         }
1418
1419         return 0;
1420 }