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