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