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