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