]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_ctf.qc
Merge branch 'master' into terencehill/ca_arena_mutators
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_ctf.qc
1 // ================================================================
2 //  Official capture the flag game mode coding, reworked by Samual
3 //  Last updated: September, 2012
4 // ================================================================
5
6 void ctf_FakeTimeLimit(entity e, float t)
7 {
8         msg_entity = e;
9         WriteByte(MSG_ONE, 3); // svc_updatestat
10         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
11         if(t < 0)
12                 WriteCoord(MSG_ONE, autocvar_timelimit);
13         else
14                 WriteCoord(MSG_ONE, (t + 1) / 60);
15 }
16
17 void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for easy changing and quick editing later
18 {
19         if(autocvar_sv_eventlog)
20                 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
21 }
22
23 string ctf_CaptureRecord(entity flag, entity player)
24 {
25         float cap_time, cap_record, success;
26         string cap_message = string_null, refername;
27         
28         if((autocvar_g_ctf_captimerecord_always) || (player_count - currentbots)) 
29         {
30                 cap_record = ctf_captimerecord;
31                 cap_time = (time - flag.ctf_pickuptime);
32
33                 refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
34                 refername = ((refername == player.netname) ? "their" : strcat(refername, "^7's"));
35
36                 if(!ctf_captimerecord) 
37                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds"); success = TRUE; }
38                 else if(cap_time < cap_record) 
39                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, breaking ", refername, " previous record of ", ftos_decimals(cap_record, 2), " seconds"); success = TRUE; }
40                 else
41                         { cap_message = strcat(" in ", ftos_decimals(cap_time, 2), " seconds, failing to break ", refername, " record of ", ftos_decimals(cap_record, 2), " seconds"); success = FALSE; }
42
43                 if(success) 
44                 {
45                         ctf_captimerecord = cap_time;
46                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
47                         db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
48                         write_recordmarker(player, (time - cap_time), cap_time); 
49                 } 
50         }
51         
52         return cap_message;
53 }
54
55 void ctf_FlagcarrierWaypoints(entity player)
56 {
57         WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
58         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
59         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
60         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
61 }
62
63 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
64 {
65         float current_distance = vlen((('1 0 0' * to_x) + ('0 1 0' * to_y)) - (('1 0 0' * from_x) + ('0 1 0' * from_y))); // for the sake of this check, exclude Z axis
66         float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
67         float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
68         //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
69
70         vector targpos;
71         if(current_height) // make sure we can actually do this arcing path
72         {
73                 targpos = (to + ('0 0 1' * current_height));
74                 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
75                 if(trace_fraction < 1)
76                 {
77                         //print("normal arc line failed, trying to find new pos...");
78                         WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
79                         targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
80                         WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
81                         if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
82                         /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
83                 }
84         }
85         else { targpos = to; }
86
87         //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
88
89         vector desired_direction = normalize(targpos - from);
90         if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
91         else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
92 }
93
94 float ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
95 {
96         if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
97         {
98                 // directional tracing only
99                 float spreadlimit;
100                 makevectors(passer_angle);
101
102                 // find the closest point on the enemy to the center of the attack
103                 float ang; // angle between shotdir and h
104                 float h; // hypotenuse, which is the distance between attacker to head
105                 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
106                 
107                 h = vlen(head_center - passer_center);
108                 ang = acos(dotproduct(normalize(head_center - passer_center), v_forward));
109                 a = h * cos(ang);
110
111                 vector nearest_on_line = (passer_center + a * v_forward);
112                 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
113
114                 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
115                 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
116
117                 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
118                         { return TRUE; }
119                 else
120                         { return FALSE; }
121         }
122         else { return TRUE; }
123 }
124
125
126 // =======================
127 // CaptureShield Functions 
128 // =======================
129
130 float ctf_CaptureShield_CheckStatus(entity p) 
131 {
132         float s, se;
133         entity e;
134         float players_worseeq, players_total;
135
136         if(ctf_captureshield_max_ratio <= 0)
137                 return FALSE;
138
139         s = PlayerScore_Add(p, SP_SCORE, 0);
140         if(s >= -ctf_captureshield_min_negscore)
141                 return FALSE;
142
143         players_total = players_worseeq = 0;
144         FOR_EACH_PLAYER(e)
145         {
146                 if(IsDifferentTeam(e, p))
147                         continue;
148                 se = PlayerScore_Add(e, SP_SCORE, 0);
149                 if(se <= s)
150                         ++players_worseeq;
151                 ++players_total;
152         }
153
154         // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
155         // use this rule here
156         
157         if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
158                 return FALSE;
159
160         return TRUE;
161 }
162
163 void ctf_CaptureShield_Update(entity player, float wanted_status)
164 {
165         float updated_status = ctf_CaptureShield_CheckStatus(player);
166         if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
167         {
168                 if(updated_status) // TODO csqc notifier for this // Samual: How?
169                         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);
170                 else
171                         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);
172                         
173                 player.ctf_captureshielded = updated_status;
174         }
175 }
176
177 float ctf_CaptureShield_Customize()
178 {
179         if(!other.ctf_captureshielded) { return FALSE; }
180         if(!IsDifferentTeam(self, other)) { return FALSE; }
181         
182         return TRUE;
183 }
184
185 void ctf_CaptureShield_Touch()
186 {
187         if(!other.ctf_captureshielded) { return; }
188         if(!IsDifferentTeam(self, other)) { return; }
189         
190         vector mymid = (self.absmin + self.absmax) * 0.5;
191         vector othermid = (other.absmin + other.absmax) * 0.5;
192
193         Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
194         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);
195 }
196
197 void ctf_CaptureShield_Spawn(entity flag)
198 {
199         entity shield = spawn();
200         
201         shield.enemy = self;
202         shield.team = self.team;
203         shield.touch = ctf_CaptureShield_Touch;
204         shield.customizeentityforclient = ctf_CaptureShield_Customize;
205         shield.classname = "ctf_captureshield";
206         shield.effects = EF_ADDITIVE;
207         shield.movetype = MOVETYPE_NOCLIP;
208         shield.solid = SOLID_TRIGGER;
209         shield.avelocity = '7 0 11';
210         shield.scale = 0.5;
211         
212         setorigin(shield, self.origin);
213         setmodel(shield, "models/ctf/shield.md3");
214         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
215 }
216
217
218 // ====================
219 // Drop/Pass/Throw Code
220 // ====================
221
222 void ctf_Handle_Drop(entity flag, entity player, float droptype)
223 {
224         // declarations
225         player = (player ? player : flag.pass_sender);
226
227         // main
228         flag.movetype = MOVETYPE_TOSS;
229         flag.takedamage = DAMAGE_YES;
230         flag.angles = '0 0 0';
231         flag.health = flag.max_flag_health;
232         flag.ctf_droptime = time;
233         flag.ctf_dropper = player;
234         flag.ctf_status = FLAG_DROPPED;
235         
236         // messages and sounds
237         Send_KillNotification(player.netname, flag.netname, "", INFO_LOSTFLAG, MSG_INFO);
238         sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTN_NONE);
239         ctf_EventLog("dropped", player.team, player);
240
241         // scoring
242         PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);   
243         PlayerScore_Add(player, SP_CTF_DROPS, 1);
244         
245         // waypoints
246         if(autocvar_g_ctf_flag_dropped_waypoint)
247                 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));
248
249         if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
250         {
251                 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
252                 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
253         }
254         
255         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
256         
257         if(droptype == DROP_PASS)
258         {
259                 flag.pass_distance = 0;
260                 flag.pass_sender = world;
261                 flag.pass_target = world;
262         }
263 }
264
265 void ctf_Handle_Retrieve(entity flag, entity player)
266 {
267         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
268         entity sender = flag.pass_sender;
269         
270         // transfer flag to player
271         flag.owner = player;
272         flag.owner.flagcarried = flag;
273         
274         // reset flag
275         setattachment(flag, player, "");
276         setorigin(flag, FLAG_CARRY_OFFSET);
277         flag.movetype = MOVETYPE_NONE;
278         flag.takedamage = DAMAGE_NO;
279         flag.solid = SOLID_NOT;
280         flag.angles = '0 0 0';
281         flag.ctf_status = FLAG_CARRY;
282
283         // messages and sounds
284         sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTN_NORM);
285         ctf_EventLog("receive", flag.team, player);
286         
287         FOR_EACH_REALPLAYER(tmp_player)
288         {
289                 if(tmp_player == sender)
290                         centerprint(tmp_player, strcat("You passed the ", flag.netname, " to ", player.netname));
291                 else if(tmp_player == player)
292                         centerprint(tmp_player, strcat("You received the ", flag.netname, " from ", sender.netname));
293                 else if(!IsDifferentTeam(tmp_player, sender))
294                         centerprint(tmp_player, strcat(sender.netname, " passed the ", flag.netname, " to ", player.netname));
295         }
296         
297         // create new waypoint
298         ctf_FlagcarrierWaypoints(player);
299         
300         sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
301         player.throw_antispam = sender.throw_antispam;
302
303         flag.pass_distance = 0;
304         flag.pass_sender = world;
305         flag.pass_target = world;
306 }
307
308 void ctf_Handle_Throw(entity player, entity receiver, float droptype)
309 {
310         entity flag = player.flagcarried;
311         vector targ_origin, flag_velocity;
312         
313         if(!flag) { return; }
314         if((droptype == DROP_PASS) && !receiver) { return; }
315         
316         if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
317         
318         // reset the flag
319         setattachment(flag, world, "");
320         setorigin(flag, player.origin + FLAG_DROP_OFFSET);
321         flag.owner.flagcarried = world;
322         flag.owner = world;
323         flag.solid = SOLID_TRIGGER;
324         flag.ctf_dropper = player;
325         flag.ctf_droptime = time;
326         
327         flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
328         
329         switch(droptype)
330         {
331                 case DROP_PASS:
332                 {
333                         // warpzone support:
334                         // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
335                         // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
336                         WarpZone_RefSys_Copy(flag, receiver);
337                         WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
338                         targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
339
340                         flag.pass_distance = vlen((('1 0 0' * targ_origin_x) + ('0 1 0' * targ_origin_y)) - (('1 0 0' *  player.origin_x) + ('0 1 0' *  player.origin_y))); // for the sake of this check, exclude Z axis
341                         ctf_CalculatePassVelocity(flag, targ_origin, player.origin, FALSE);
342
343                         // main
344                         flag.movetype = MOVETYPE_FLY;
345                         flag.takedamage = DAMAGE_NO;
346                         flag.pass_sender = player;
347                         flag.pass_target = receiver;
348                         flag.ctf_status = FLAG_PASSING;
349                         
350                         // other
351                         sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTN_NORM);
352                         WarpZone_TrailParticles(world, particleeffectnum(flag.passeffect), player.origin, targ_origin);
353                         ctf_EventLog("pass", flag.team, player);
354                         break;
355                 }
356                 
357                 case DROP_THROW:
358                 {
359                         makevectors((player.v_angle_y * '0 1 0') + (bound(autocvar_g_ctf_throw_angle_min, player.v_angle_x, autocvar_g_ctf_throw_angle_max) * '1 0 0'));
360                                 
361                         flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & IT_STRENGTH) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
362                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, FALSE);
363                         ctf_Handle_Drop(flag, player, droptype);
364                         break;
365                 }
366                 
367                 case DROP_RESET:
368                 {
369                         flag.velocity = '0 0 0'; // do nothing
370                         break;
371                 }
372                 
373                 default:
374                 case DROP_NORMAL:
375                 {
376                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, (('0 0 1' * autocvar_g_ctf_drop_velocity_up) + ((('0 1 0' * crandom()) + ('1 0 0' * crandom())) * autocvar_g_ctf_drop_velocity_side)), FALSE);
377                         ctf_Handle_Drop(flag, player, droptype);
378                         break;
379                 }
380         }
381
382         // kill old waypointsprite
383         WaypointSprite_Ping(player.wps_flagcarrier);
384         WaypointSprite_Kill(player.wps_flagcarrier);
385         
386         if(player.wps_enemyflagcarrier)
387                 WaypointSprite_Kill(player.wps_enemyflagcarrier);
388         
389         // captureshield
390         ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
391 }
392
393
394 // ==============
395 // Event Handlers
396 // ==============
397
398 void ctf_Handle_Capture(entity flag, entity toucher, float capturetype)
399 {
400         entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
401         entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
402         float old_time, new_time; 
403         
404         if not(player) { return; } // without someone to give the reward to, we can't possibly cap
405         
406         // messages and sounds
407         Send_KillNotification(player.netname, enemy_flag.netname, ctf_CaptureRecord(enemy_flag, player), INFO_CAPTUREFLAG, MSG_INFO);
408         sound(player, CH_TRIGGER, flag.snd_flag_capture, VOL_BASE, ATTN_NONE);
409         
410         switch(capturetype)
411         {
412                 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
413                 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
414                 default: break;
415         }
416         
417         // scoring
418         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
419         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
420
421         old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
422         new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
423         if(!old_time || new_time < old_time)
424                 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
425
426         // effects
427         pointparticles(particleeffectnum(flag.capeffect), flag.origin, '0 0 0', 1);
428         //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
429
430         // other
431         if(capturetype == CAPTURE_NORMAL)
432         {
433                 WaypointSprite_Kill(player.wps_flagcarrier);
434                 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
435                 
436                 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
437                         { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
438         }
439         
440         // reset the flag
441         player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
442         ctf_RespawnFlag(enemy_flag);
443 }
444
445 void ctf_Handle_Return(entity flag, entity player)
446 {
447         // messages and sounds
448         //centerprint(player, strcat("You returned the ", flag.netname));
449         Send_KillNotification(player.netname, flag.netname, "", INFO_RETURNFLAG, MSG_INFO);
450         sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTN_NONE);
451         ctf_EventLog("return", flag.team, player);
452
453         // scoring
454         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
455         PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
456
457         TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
458         
459         if(flag.ctf_dropper) 
460         {
461                 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
462                 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag 
463                 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
464         }
465         
466         // reset the flag
467         ctf_RespawnFlag(flag);
468 }
469
470 void ctf_Handle_Pickup(entity flag, entity player, float pickuptype)
471 {
472         // declarations
473         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
474         string verbosename; // holds the name of the player OR no name at all for printing in the centerprints
475         float pickup_dropped_score; // used to calculate dropped pickup score
476         
477         // attach the flag to the player
478         flag.owner = player;
479         player.flagcarried = flag;
480         setattachment(flag, player, "");
481         setorigin(flag, FLAG_CARRY_OFFSET);
482         
483         // flag setup
484         flag.movetype = MOVETYPE_NONE;
485         flag.takedamage = DAMAGE_NO;
486         flag.solid = SOLID_NOT;
487         flag.angles = '0 0 0';
488         flag.ctf_status = FLAG_CARRY;
489         
490         switch(pickuptype)
491         {
492                 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
493                 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
494                 default: break;
495         }
496
497         // messages and sounds
498         Send_KillNotification (player.netname, flag.netname, "", INFO_GOTFLAG, MSG_INFO);
499         sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTN_NONE);
500         verbosename = ((autocvar_g_ctf_flag_pickup_verbosename) ? strcat(Team_ColorCode(player.team), "(^7", player.netname, Team_ColorCode(player.team), ") ") : "");
501         
502         FOR_EACH_REALPLAYER(tmp_player)
503         {
504                 if(tmp_player == player)
505                 {
506                         centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
507                         //if(ctf_stalemate) { centerprint(tmp_player, "Stalemate! Enemies can see you on radar!"); }
508                 }
509                 //else if(!IsDifferentTeam(tmp_player, player))
510                 //      centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
511                 else if(!IsDifferentTeam(tmp_player, flag))
512                         centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
513         }
514         
515         // scoring
516         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
517         switch(pickuptype)
518         {               
519                 case PICKUP_BASE:
520                 {
521                         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
522                         ctf_EventLog("steal", flag.team, player);
523                         break;
524                 }
525                 
526                 case PICKUP_DROPPED:
527                 {
528                         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);
529                         pickup_dropped_score = floor((autocvar_g_ctf_score_pickup_dropped_late * (1 - pickup_dropped_score) + autocvar_g_ctf_score_pickup_dropped_early * pickup_dropped_score) + 0.5);
530                         dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
531                         PlayerTeamScore_AddScore(player, pickup_dropped_score);
532                         ctf_EventLog("pickup", flag.team, player);
533                         break;
534                 }
535                 
536                 default: break;
537         }
538         
539         // speedrunning
540         if(pickuptype == PICKUP_BASE)
541         {
542                 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
543                 if((player.speedrunning) && (ctf_captimerecord))
544                         ctf_FakeTimeLimit(player, time + ctf_captimerecord);
545         }
546                 
547         // effects
548         pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
549         
550         // waypoints 
551         if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
552         ctf_FlagcarrierWaypoints(player);
553         WaypointSprite_Ping(player.wps_flagcarrier);
554 }
555
556
557 // ===================
558 // Main Flag Functions
559 // ===================
560
561 void ctf_CheckFlagReturn(entity flag, float returntype)
562 {
563         if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
564         {
565                 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
566                 
567                 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
568                 {
569                         switch(returntype)
570                         {
571                                 case RETURN_DROPPED: bprint("The ", flag.netname, " was dropped in the base and returned itself\n"); break;
572                                 case RETURN_DAMAGE: bprint("The ", flag.netname, " was destroyed and returned to base\n"); break;
573                                 case RETURN_SPEEDRUN: bprint("The ", flag.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n"); break;
574                                 case RETURN_NEEDKILL: bprint("The ", flag.netname, " fell somewhere it couldn't be reached and returned to base\n"); break;
575                                 
576                                 default:
577                                 case RETURN_TIMEOUT:
578                                         { bprint("The ", flag.netname, " has returned to base\n"); break; }
579                         }
580                         sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
581                         ctf_EventLog("returned", flag.team, world);
582                         ctf_RespawnFlag(flag);
583                 }
584         }
585 }
586
587 void ctf_CheckStalemate(void)
588 {
589         // declarations
590         float stale_red_flags = 0, stale_blue_flags = 0;
591         entity tmp_entity;
592
593         entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
594
595         // build list of stale flags
596         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
597         {
598                 if(autocvar_g_ctf_stalemate)
599                 if(tmp_entity.ctf_status != FLAG_BASE)
600                 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time)
601                 {
602                         tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
603                         ctf_staleflaglist = tmp_entity;
604                         
605                         switch(tmp_entity.team)
606                         {
607                                 case COLOR_TEAM1: ++stale_red_flags; break;
608                                 case COLOR_TEAM2: ++stale_blue_flags; break;
609                         }
610                 }
611         }
612
613         if(stale_red_flags && stale_blue_flags)
614                 ctf_stalemate = TRUE;
615         else if((!stale_red_flags && !stale_blue_flags) && autocvar_g_ctf_stalemate_endcondition == 2)
616                 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
617         else if((!stale_red_flags || !stale_blue_flags) && autocvar_g_ctf_stalemate_endcondition == 1)
618                 { ctf_stalemate = FALSE; wpforenemy_announced = FALSE; }
619                 
620         // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
621         if(ctf_stalemate)
622         {
623                 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
624                 {
625                         if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
626                                 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));
627                 }
628                 
629                 if not(wpforenemy_announced)
630                 {
631                         FOR_EACH_REALPLAYER(tmp_entity)
632                                 if(tmp_entity.flagcarried)
633                                         centerprint(tmp_entity, "Stalemate! Enemies can now see you on radar!");
634                                 else
635                                         centerprint(tmp_entity, "Stalemate! Flag carriers can now be seen by enemies on radar!");
636                         
637                         wpforenemy_announced = TRUE;
638                 }
639         }
640 }
641
642 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
643 {
644         if(ITEM_DAMAGE_NEEDKILL(deathtype))
645         {
646                 // automatically kill the flag and return it
647                 self.health = 0;
648                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
649                 return;
650         }
651         if(autocvar_g_ctf_flag_return_damage) 
652         {
653                 // reduce health and check if it should be returned
654                 self.health = self.health - damage;
655                 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
656                 return;
657         }
658 }
659
660 void ctf_FlagThink()
661 {
662         // declarations
663         entity tmp_entity;
664
665         self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
666
667         // captureshield
668         if(self == ctf_worldflaglist) // only for the first flag
669                 FOR_EACH_CLIENT(tmp_entity)
670                         ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
671
672         // sanity checks
673         if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
674                 dprint("wtf the flag got squashed?\n");
675                 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
676                 if(!trace_startsolid) // can we resize it without getting stuck?
677                         setsize(self, FLAG_MIN, FLAG_MAX); }
678                         
679         switch(self.ctf_status) // reset flag angles in case warpzones adjust it
680         {
681                 case FLAG_DROPPED:
682                 {
683                         self.angles = '0 0 0';
684                         break;
685                 }
686                 
687                 default: break;
688         }
689
690         // main think method
691         switch(self.ctf_status)
692         {       
693                 case FLAG_BASE:
694                 {
695                         if(autocvar_g_ctf_dropped_capture_radius)
696                         {
697                                 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
698                                         if(tmp_entity.ctf_status == FLAG_DROPPED)
699                                         if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
700                                         if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
701                                                 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
702                         }
703                         return;
704                 }
705                 
706                 case FLAG_DROPPED:
707                 {
708                         if(autocvar_g_ctf_flag_dropped_floatinwater)
709                         {
710                                 vector midpoint = ((self.absmin + self.absmax) * 0.5);
711                                 if(pointcontents(midpoint) == CONTENT_WATER)
712                                 {
713                                         self.velocity = self.velocity * 0.5;
714                                         
715                                         if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
716                                                 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
717                                         else
718                                                 { self.movetype = MOVETYPE_FLY; }
719                                 }
720                                 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
721                         }
722                         if(autocvar_g_ctf_flag_return_dropped)
723                         {
724                                 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
725                                 {
726                                         self.health = 0;
727                                         ctf_CheckFlagReturn(self, RETURN_DROPPED);
728                                         return;
729                                 }
730                         }
731                         if(autocvar_g_ctf_flag_return_time)
732                         {
733                                 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
734                                 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
735                                 return;
736                         } 
737                         return;
738                 }
739                         
740                 case FLAG_CARRY:
741                 {
742                         if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord)) 
743                         {
744                                 self.health = 0;
745                                 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
746
747                                 tmp_entity = self;
748                                 self = self.owner;
749                                 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
750                                 ImpulseCommands();
751                                 self = tmp_entity;
752                         }
753                         if(autocvar_g_ctf_stalemate)
754                         {
755                                 if(time >= wpforenemy_nextthink)
756                                 {
757                                         ctf_CheckStalemate();
758                                         wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
759                                 }
760                         }
761                         return;
762                 }
763                 
764                 case FLAG_PASSING:
765                 {
766                         vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
767                         targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
768                         WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
769                         
770                         if((self.pass_target == world)
771                                 || (self.pass_target.deadflag != DEAD_NO)
772                                 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
773                                 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
774                                 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
775                         {
776                                 // give up, pass failed
777                                 ctf_Handle_Drop(self, world, DROP_PASS);
778                         }
779                         else
780                         {
781                                 // still a viable target, go for it
782                                 ctf_CalculatePassVelocity(self, targ_origin, self.origin, TRUE);
783                         }
784                         return;
785                 }
786
787                 default: // this should never happen
788                 {
789                         dprint("ctf_FlagThink(): Flag exists with no status?\n");
790                         return;
791                 }
792         }
793 }
794
795 void ctf_FlagTouch()
796 {
797         if(gameover) { return; }
798         
799         entity toucher = other;
800         
801         // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
802         if(ITEM_TOUCH_NEEDKILL())
803         {
804                 self.health = 0;
805                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
806                 return;
807         }
808         
809         // special touch behaviors
810         if(toucher.vehicle_flags & VHF_ISVEHICLE)
811         {
812                 if(autocvar_g_ctf_allow_vehicle_touch)
813                         toucher = toucher.owner; // the player is actually the vehicle owner, not other
814                 else
815                         return; // do nothing
816         }
817         else if(toucher.classname != "player") // The flag just touched an object, most likely the world
818         {
819                 if(time > self.wait) // if we haven't in a while, play a sound/effect
820                 {
821                         pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
822                         sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
823                         self.wait = time + FLAG_TOUCHRATE;
824                 }
825                 return;
826         }
827         else if(toucher.deadflag != DEAD_NO) { return; }
828
829         switch(self.ctf_status) 
830         {       
831                 case FLAG_BASE:
832                 {
833                         if(!IsDifferentTeam(toucher, self) && (toucher.flagcarried) && IsDifferentTeam(toucher.flagcarried, self))
834                                 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
835                         else if(IsDifferentTeam(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time))
836                                 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
837                         break;
838                 }
839                 
840                 case FLAG_DROPPED:
841                 {
842                         if(!IsDifferentTeam(toucher, self))
843                                 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
844                         else if((!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
845                                 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
846                         break;
847                 }
848                         
849                 case FLAG_CARRY:
850                 {
851                         dprint("Someone touched a flag even though it was being carried?\n");
852                         break;
853                 }
854                 
855                 case FLAG_PASSING:
856                 {
857                         if((toucher.classname == "player") && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
858                         {
859                                 if(IsDifferentTeam(toucher, self.pass_sender))
860                                         ctf_Handle_Return(self, toucher);
861                                 else
862                                         ctf_Handle_Retrieve(self, toucher);
863                         }
864                         break;
865                 }
866         }
867 }
868
869 .float last_respawn;
870 void ctf_RespawnFlag(entity flag)
871 {
872         // check for flag respawn being called twice in a row
873         if(flag.last_respawn > time - 0.5)
874                 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
875
876         flag.last_respawn = time;
877         
878         // reset the player (if there is one)
879         if((flag.owner) && (flag.owner.flagcarried == flag))
880         {
881                 if(flag.owner.wps_enemyflagcarrier)
882                         WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
883                         
884                 WaypointSprite_Kill(flag.wps_flagcarrier);
885                 
886                 flag.owner.flagcarried = world;
887
888                 if(flag.speedrunning)
889                         ctf_FakeTimeLimit(flag.owner, -1);
890         }
891
892         if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
893                 { WaypointSprite_Kill(flag.wps_flagdropped); }
894
895         // reset the flag
896         setattachment(flag, world, "");
897         setorigin(flag, flag.ctf_spawnorigin);
898         
899         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
900         flag.takedamage = DAMAGE_NO;
901         flag.health = flag.max_flag_health;
902         flag.solid = SOLID_TRIGGER;
903         flag.velocity = '0 0 0';
904         flag.angles = flag.mangle;
905         flag.flags = FL_ITEM | FL_NOTARGET;
906         
907         flag.ctf_status = FLAG_BASE;
908         flag.owner = world;
909         flag.pass_distance = 0;
910         flag.pass_sender = world;
911         flag.pass_target = world;
912         flag.ctf_dropper = world;
913         flag.ctf_pickuptime = 0;
914         flag.ctf_droptime = 0;
915 }
916
917 void ctf_Reset()
918 {
919         if(self.owner)
920                 if(self.owner.classname == "player")
921                         ctf_Handle_Throw(self.owner, world, DROP_RESET);
922                         
923         ctf_RespawnFlag(self);
924 }
925
926 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
927 {
928         // bot waypoints
929         waypoint_spawnforitem_force(self, self.origin);
930         self.nearestwaypointtimeout = 0; // activate waypointing again
931         self.bot_basewaypoint = self.nearestwaypoint;
932
933         // waypointsprites
934         WaypointSprite_SpawnFixed(((self.team == COLOR_TEAM1) ? "redbase" : "bluebase"), self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
935         WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
936
937         // captureshield setup
938         ctf_CaptureShield_Spawn(self);
939 }
940
941 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc 
942 {       
943         // declarations
944         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. 
945         self = flag; // for later usage with droptofloor()
946         
947         // main setup
948         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
949         ctf_worldflaglist = flag;
950
951         setattachment(flag, world, ""); 
952
953         flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
954         flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue)
955         flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
956         flag.classname = "item_flag_team";
957         flag.target = "###item###"; // wut?
958         flag.flags = FL_ITEM | FL_NOTARGET;
959         flag.solid = SOLID_TRIGGER;
960         flag.takedamage = DAMAGE_NO;
961         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;   
962         flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
963         flag.health = flag.max_flag_health;
964         flag.event_damage = ctf_FlagDamage;
965         flag.pushable = TRUE;
966         flag.teleportable = TELEPORT_NORMAL;
967         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
968         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
969         flag.velocity = '0 0 0';
970         flag.mangle = flag.angles;
971         flag.reset = ctf_Reset;
972         flag.touch = ctf_FlagTouch;
973         flag.think = ctf_FlagThink;
974         flag.nextthink = time + FLAG_THINKRATE;
975         flag.ctf_status = FLAG_BASE;
976         
977         if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
978         if(!flag.scale) { flag.scale = FLAG_SCALE; }
979         if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
980         if(!flag.toucheffect) { flag.toucheffect = ((teamnumber) ? "redflag_touch" : "blueflag_touch"); }
981         if(!flag.passeffect) { flag.passeffect = ((teamnumber) ? "red_pass" : "blue_pass"); }
982         if(!flag.capeffect) { flag.capeffect = ((teamnumber) ? "red_cap" : "blue_cap"); }
983         
984         // sound 
985         if(!flag.snd_flag_taken) { flag.snd_flag_taken  = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
986         if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
987         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
988         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.
989         if(!flag.snd_flag_dropped) { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
990         if(!flag.snd_flag_touch) { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
991         if(!flag.snd_flag_pass) { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
992         
993         // precache
994         precache_sound(flag.snd_flag_taken);
995         precache_sound(flag.snd_flag_returned);
996         precache_sound(flag.snd_flag_capture);
997         precache_sound(flag.snd_flag_respawn);
998         precache_sound(flag.snd_flag_dropped);
999         precache_sound(flag.snd_flag_touch);
1000         precache_sound(flag.snd_flag_pass);
1001         precache_model(flag.model);
1002         precache_model("models/ctf/shield.md3");
1003         precache_model("models/ctf/shockwavetransring.md3");
1004
1005         // appearence
1006         setmodel(flag, flag.model); // precision set below
1007         setsize(flag, FLAG_MIN, FLAG_MAX);
1008         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1009         
1010         if(autocvar_g_ctf_flag_glowtrails)
1011         {
1012                 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
1013                 flag.glow_size = 25;
1014                 flag.glow_trail = 1;
1015         }
1016         
1017         flag.effects |= EF_LOWPRECISION;
1018         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1019         if(autocvar_g_ctf_dynamiclights)   { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
1020         
1021         // flag placement
1022         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1023         {       
1024                 flag.dropped_origin = flag.origin; 
1025                 flag.noalign = TRUE;
1026                 flag.movetype = MOVETYPE_NONE;
1027         }
1028         else // drop to floor, automatically find a platform and set that as spawn origin
1029         { 
1030                 flag.noalign = FALSE;
1031                 self = flag;
1032                 droptofloor();
1033                 flag.movetype = MOVETYPE_TOSS; 
1034         }       
1035         
1036         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1037 }
1038
1039
1040 // ================
1041 // Bot player logic
1042 // ================
1043
1044 // NOTE: LEGACY CODE, needs to be re-written!
1045
1046 void havocbot_calculate_middlepoint()
1047 {
1048         entity f;
1049         vector s = '0 0 0';
1050         vector fo = '0 0 0';
1051         float n = 0;
1052
1053         f = ctf_worldflaglist;
1054         while (f)
1055         {
1056                 fo = f.origin;
1057                 s = s + fo;
1058                 f = f.ctf_worldflagnext;
1059         }
1060         if(!n)
1061                 return;
1062         havocbot_ctf_middlepoint = s * (1.0 / n);
1063         havocbot_ctf_middlepoint_radius  = vlen(fo - havocbot_ctf_middlepoint);
1064 }
1065
1066
1067 entity havocbot_ctf_find_flag(entity bot)
1068 {
1069         entity f;
1070         f = ctf_worldflaglist;
1071         while (f)
1072         {
1073                 if (bot.team == f.team)
1074                         return f;
1075                 f = f.ctf_worldflagnext;
1076         }
1077         return world;
1078 }
1079
1080 entity havocbot_ctf_find_enemy_flag(entity bot)
1081 {
1082         entity f;
1083         f = ctf_worldflaglist;
1084         while (f)
1085         {
1086                 if (bot.team != f.team)
1087                         return f;
1088                 f = f.ctf_worldflagnext;
1089         }
1090         return world;
1091 }
1092
1093 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1094 {
1095         if not(teamplay)
1096                 return 0;
1097
1098         float c = 0;
1099         entity head;
1100
1101         FOR_EACH_PLAYER(head)
1102         {
1103                 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1104                         continue;
1105
1106                 if(vlen(head.origin - org) < tc_radius)
1107                         ++c;
1108         }
1109
1110         return c;
1111 }
1112
1113 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1114 {
1115         entity head;
1116         head = ctf_worldflaglist;
1117         while (head)
1118         {
1119                 if (self.team == head.team)
1120                         break;
1121                 head = head.ctf_worldflagnext;
1122         }
1123         if (head)
1124                 navigation_routerating(head, ratingscale, 10000);
1125 }
1126
1127 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1128 {
1129         entity head;
1130         head = ctf_worldflaglist;
1131         while (head)
1132         {
1133                 if (self.team == head.team)
1134                         break;
1135                 head = head.ctf_worldflagnext;
1136         }
1137         if not(head)
1138                 return;
1139
1140         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1141 }
1142
1143 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1144 {
1145         entity head;
1146         head = ctf_worldflaglist;
1147         while (head)
1148         {
1149                 if (self.team != head.team)
1150                         break;
1151                 head = head.ctf_worldflagnext;
1152         }
1153         if (head)
1154                 navigation_routerating(head, ratingscale, 10000);
1155 }
1156
1157 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1158 {
1159         if not(bot_waypoints_for_items)
1160         {
1161                 havocbot_goalrating_ctf_enemyflag(ratingscale);
1162                 return;
1163         }
1164
1165         entity head;
1166
1167         head = havocbot_ctf_find_enemy_flag(self);
1168
1169         if not(head)
1170                 return;
1171
1172         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1173 }
1174
1175 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1176 {
1177         entity mf;
1178
1179         mf = havocbot_ctf_find_flag(self);
1180
1181         if(mf.ctf_status == FLAG_BASE)
1182                 return;
1183
1184         if(mf.tag_entity)
1185                 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1186 }
1187
1188 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1189 {
1190         entity head;
1191         head = ctf_worldflaglist;
1192         while (head)
1193         {
1194                 // flag is out in the field
1195                 if(head.ctf_status != FLAG_BASE)
1196                 if(head.tag_entity==world)      // dropped
1197                 {
1198                         if(df_radius)
1199                         {
1200                                 if(vlen(org-head.origin)<df_radius)
1201                                         navigation_routerating(head, ratingscale, 10000);
1202                         }
1203                         else
1204                                 navigation_routerating(head, ratingscale, 10000);
1205                 }
1206
1207                 head = head.ctf_worldflagnext;
1208         }
1209 }
1210
1211 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1212 {
1213         entity head;
1214         float t;
1215         head = findchainfloat(bot_pickup, TRUE);
1216         while (head)
1217         {
1218                 // gather health and armor only
1219                 if (head.solid)
1220                 if (head.health || head.armorvalue)
1221                 if (vlen(head.origin - org) < sradius)
1222                 {
1223                         // get the value of the item
1224                         t = head.bot_pickupevalfunc(self, head) * 0.0001;
1225                         if (t > 0)
1226                                 navigation_routerating(head, t * ratingscale, 500);
1227                 }
1228                 head = head.chain;
1229         }
1230 }
1231
1232 void havocbot_ctf_reset_role(entity bot)
1233 {
1234         float cdefense, cmiddle, coffense;
1235         entity mf, ef, head;
1236         float c;
1237
1238         if(bot.deadflag != DEAD_NO)
1239                 return;
1240
1241         if(vlen(havocbot_ctf_middlepoint)==0)
1242                 havocbot_calculate_middlepoint();
1243
1244         // Check ctf flags
1245         if (bot.flagcarried)
1246         {
1247                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1248                 return;
1249         }
1250
1251         mf = havocbot_ctf_find_flag(bot);
1252         ef = havocbot_ctf_find_enemy_flag(bot);
1253
1254         // Retrieve stolen flag
1255         if(mf.ctf_status!=FLAG_BASE)
1256         {
1257                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1258                 return;
1259         }
1260
1261         // If enemy flag is taken go to the middle to intercept pursuers
1262         if(ef.ctf_status!=FLAG_BASE)
1263         {
1264                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1265                 return;
1266         }
1267
1268         // if there is only me on the team switch to offense
1269         c = 0;
1270         FOR_EACH_PLAYER(head)
1271         if(head.team==bot.team)
1272                 ++c;
1273
1274         if(c==1)
1275         {
1276                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1277                 return;
1278         }
1279
1280         // Evaluate best position to take
1281         // Count mates on middle position
1282         cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1283
1284         // Count mates on defense position
1285         cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1286
1287         // Count mates on offense position
1288         coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1289
1290         if(cdefense<=coffense)
1291                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1292         else if(coffense<=cmiddle)
1293                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1294         else
1295                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1296 }
1297
1298 void havocbot_role_ctf_carrier()
1299 {
1300         if(self.deadflag != DEAD_NO)
1301         {
1302                 havocbot_ctf_reset_role(self);
1303                 return;
1304         }
1305
1306         if (self.flagcarried == world)
1307         {
1308                 havocbot_ctf_reset_role(self);
1309                 return;
1310         }
1311
1312         if (self.bot_strategytime < time)
1313         {
1314                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1315
1316                 navigation_goalrating_start();
1317                 havocbot_goalrating_ctf_ourbase(50000);
1318
1319                 if(self.health<100)
1320                         havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1321
1322                 navigation_goalrating_end();
1323
1324                 if (self.navigation_hasgoals)
1325                         self.havocbot_cantfindflag = time + 10;
1326                 else if (time > self.havocbot_cantfindflag)
1327                 {
1328                         // Can't navigate to my own base, suicide!
1329                         // TODO: drop it and wander around
1330                         Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1331                         return;
1332                 }
1333         }
1334 }
1335
1336 void havocbot_role_ctf_escort()
1337 {
1338         entity mf, ef;
1339
1340         if(self.deadflag != DEAD_NO)
1341         {
1342                 havocbot_ctf_reset_role(self);
1343                 return;
1344         }
1345
1346         if (self.flagcarried)
1347         {
1348                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1349                 return;
1350         }
1351
1352         // If enemy flag is back on the base switch to previous role
1353         ef = havocbot_ctf_find_enemy_flag(self);
1354         if(ef.ctf_status==FLAG_BASE)
1355         {
1356                 self.havocbot_role = self.havocbot_previous_role;
1357                 self.havocbot_role_timeout = 0;
1358                 return;
1359         }
1360
1361         // If the flag carrier reached the base switch to defense
1362         mf = havocbot_ctf_find_flag(self);
1363         if(mf.ctf_status!=FLAG_BASE)
1364         if(vlen(ef.origin - mf.dropped_origin) < 300)
1365         {
1366                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1367                 return;
1368         }
1369
1370         // Set the role timeout if necessary
1371         if (!self.havocbot_role_timeout)
1372         {
1373                 self.havocbot_role_timeout = time + random() * 30 + 60;
1374         }
1375
1376         // If nothing happened just switch to previous role
1377         if (time > self.havocbot_role_timeout)
1378         {
1379                 self.havocbot_role = self.havocbot_previous_role;
1380                 self.havocbot_role_timeout = 0;
1381                 return;
1382         }
1383
1384         // Chase the flag carrier
1385         if (self.bot_strategytime < time)
1386         {
1387                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1388                 navigation_goalrating_start();
1389                 havocbot_goalrating_ctf_enemyflag(30000);
1390                 havocbot_goalrating_ctf_ourstolenflag(40000);
1391                 havocbot_goalrating_items(10000, self.origin, 10000);
1392                 navigation_goalrating_end();
1393         }
1394 }
1395
1396 void havocbot_role_ctf_offense()
1397 {
1398         entity mf, ef;
1399         vector pos;
1400
1401         if(self.deadflag != DEAD_NO)
1402         {
1403                 havocbot_ctf_reset_role(self);
1404                 return;
1405         }
1406
1407         if (self.flagcarried)
1408         {
1409                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1410                 return;
1411         }
1412
1413         // Check flags
1414         mf = havocbot_ctf_find_flag(self);
1415         ef = havocbot_ctf_find_enemy_flag(self);
1416
1417         // Own flag stolen
1418         if(mf.ctf_status!=FLAG_BASE)
1419         {
1420                 if(mf.tag_entity)
1421                         pos = mf.tag_entity.origin;
1422                 else
1423                         pos = mf.origin;
1424
1425                 // Try to get it if closer than the enemy base
1426                 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1427                 {
1428                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1429                         return;
1430                 }
1431         }
1432
1433         // Escort flag carrier
1434         if(ef.ctf_status!=FLAG_BASE)
1435         {
1436                 if(ef.tag_entity)
1437                         pos = ef.tag_entity.origin;
1438                 else
1439                         pos = ef.origin;
1440
1441                 if(vlen(pos-mf.dropped_origin)>700)
1442                 {
1443                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1444                         return;
1445                 }
1446         }
1447
1448         // About to fail, switch to middlefield
1449         if(self.health<50)
1450         {
1451                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1452                 return;
1453         }
1454
1455         // Set the role timeout if necessary
1456         if (!self.havocbot_role_timeout)
1457                 self.havocbot_role_timeout = time + 120;
1458
1459         if (time > self.havocbot_role_timeout)
1460         {
1461                 havocbot_ctf_reset_role(self);
1462                 return;
1463         }
1464
1465         if (self.bot_strategytime < time)
1466         {
1467                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1468                 navigation_goalrating_start();
1469                 havocbot_goalrating_ctf_ourstolenflag(50000);
1470                 havocbot_goalrating_ctf_enemybase(20000);
1471                 havocbot_goalrating_items(5000, self.origin, 1000);
1472                 havocbot_goalrating_items(1000, self.origin, 10000);
1473                 navigation_goalrating_end();
1474         }
1475 }
1476
1477 // Retriever (temporary role):
1478 void havocbot_role_ctf_retriever()
1479 {
1480         entity mf;
1481
1482         if(self.deadflag != DEAD_NO)
1483         {
1484                 havocbot_ctf_reset_role(self);
1485                 return;
1486         }
1487
1488         if (self.flagcarried)
1489         {
1490                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1491                 return;
1492         }
1493
1494         // If flag is back on the base switch to previous role
1495         mf = havocbot_ctf_find_flag(self);
1496         if(mf.ctf_status==FLAG_BASE)
1497         {
1498                 havocbot_ctf_reset_role(self);
1499                 return;
1500         }
1501
1502         if (!self.havocbot_role_timeout)
1503                 self.havocbot_role_timeout = time + 20;
1504
1505         if (time > self.havocbot_role_timeout)
1506         {
1507                 havocbot_ctf_reset_role(self);
1508                 return;
1509         }
1510
1511         if (self.bot_strategytime < time)
1512         {
1513                 float rt_radius;
1514                 rt_radius = 10000;
1515
1516                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1517                 navigation_goalrating_start();
1518                 havocbot_goalrating_ctf_ourstolenflag(50000);
1519                 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1520                 havocbot_goalrating_ctf_enemybase(30000);
1521                 havocbot_goalrating_items(500, self.origin, rt_radius);
1522                 navigation_goalrating_end();
1523         }
1524 }
1525
1526 void havocbot_role_ctf_middle()
1527 {
1528         entity mf;
1529
1530         if(self.deadflag != DEAD_NO)
1531         {
1532                 havocbot_ctf_reset_role(self);
1533                 return;
1534         }
1535
1536         if (self.flagcarried)
1537         {
1538                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1539                 return;
1540         }
1541
1542         mf = havocbot_ctf_find_flag(self);
1543         if(mf.ctf_status!=FLAG_BASE)
1544         {
1545                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1546                 return;
1547         }
1548
1549         if (!self.havocbot_role_timeout)
1550                 self.havocbot_role_timeout = time + 10;
1551
1552         if (time > self.havocbot_role_timeout)
1553         {
1554                 havocbot_ctf_reset_role(self);
1555                 return;
1556         }
1557
1558         if (self.bot_strategytime < time)
1559         {
1560                 vector org;
1561
1562                 org = havocbot_ctf_middlepoint;
1563                 org_z = self.origin_z;
1564
1565                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1566                 navigation_goalrating_start();
1567                 havocbot_goalrating_ctf_ourstolenflag(50000);
1568                 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1569                 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1570                 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1571                 havocbot_goalrating_items(2500, self.origin, 10000);
1572                 havocbot_goalrating_ctf_enemybase(2500);
1573                 navigation_goalrating_end();
1574         }
1575 }
1576
1577 void havocbot_role_ctf_defense()
1578 {
1579         entity mf;
1580
1581         if(self.deadflag != DEAD_NO)
1582         {
1583                 havocbot_ctf_reset_role(self);
1584                 return;
1585         }
1586
1587         if (self.flagcarried)
1588         {
1589                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1590                 return;
1591         }
1592
1593         // If own flag was captured
1594         mf = havocbot_ctf_find_flag(self);
1595         if(mf.ctf_status!=FLAG_BASE)
1596         {
1597                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1598                 return;
1599         }
1600
1601         if (!self.havocbot_role_timeout)
1602                 self.havocbot_role_timeout = time + 30;
1603
1604         if (time > self.havocbot_role_timeout)
1605         {
1606                 havocbot_ctf_reset_role(self);
1607                 return;
1608         }
1609         if (self.bot_strategytime < time)
1610         {
1611                 float mp_radius;
1612                 vector org;
1613
1614                 org = mf.dropped_origin;
1615                 mp_radius = havocbot_ctf_middlepoint_radius;
1616
1617                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1618                 navigation_goalrating_start();
1619
1620                 // if enemies are closer to our base, go there
1621                 entity head, closestplayer = world;
1622                 float distance, bestdistance = 10000;
1623                 FOR_EACH_PLAYER(head)
1624                 {
1625                         if(head.deadflag!=DEAD_NO)
1626                                 continue;
1627
1628                         distance = vlen(org - head.origin);
1629                         if(distance<bestdistance)
1630                         {
1631                                 closestplayer = head;
1632                                 bestdistance = distance;
1633                         }
1634                 }
1635
1636                 if(closestplayer)
1637                 if(closestplayer.team!=self.team)
1638                 if(vlen(org - self.origin)>1000)
1639                 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1640                         havocbot_goalrating_ctf_ourbase(30000);
1641
1642                 havocbot_goalrating_ctf_ourstolenflag(20000);
1643                 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1644                 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1645                 havocbot_goalrating_items(10000, org, mp_radius);
1646                 havocbot_goalrating_items(5000, self.origin, 10000);
1647                 navigation_goalrating_end();
1648         }
1649 }
1650
1651 void havocbot_role_ctf_setrole(entity bot, float role)
1652 {
1653         dprint(strcat(bot.netname," switched to "));
1654         switch(role)
1655         {
1656                 case HAVOCBOT_CTF_ROLE_CARRIER:
1657                         dprint("carrier");
1658                         bot.havocbot_role = havocbot_role_ctf_carrier;
1659                         bot.havocbot_role_timeout = 0;
1660                         bot.havocbot_cantfindflag = time + 10;
1661                         bot.bot_strategytime = 0;
1662                         break;
1663                 case HAVOCBOT_CTF_ROLE_DEFENSE:
1664                         dprint("defense");
1665                         bot.havocbot_role = havocbot_role_ctf_defense;
1666                         bot.havocbot_role_timeout = 0;
1667                         break;
1668                 case HAVOCBOT_CTF_ROLE_MIDDLE:
1669                         dprint("middle");
1670                         bot.havocbot_role = havocbot_role_ctf_middle;
1671                         bot.havocbot_role_timeout = 0;
1672                         break;
1673                 case HAVOCBOT_CTF_ROLE_OFFENSE:
1674                         dprint("offense");
1675                         bot.havocbot_role = havocbot_role_ctf_offense;
1676                         bot.havocbot_role_timeout = 0;
1677                         break;
1678                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1679                         dprint("retriever");
1680                         bot.havocbot_previous_role = bot.havocbot_role;
1681                         bot.havocbot_role = havocbot_role_ctf_retriever;
1682                         bot.havocbot_role_timeout = time + 10;
1683                         bot.bot_strategytime = 0;
1684                         break;
1685                 case HAVOCBOT_CTF_ROLE_ESCORT:
1686                         dprint("escort");
1687                         bot.havocbot_previous_role = bot.havocbot_role;
1688                         bot.havocbot_role = havocbot_role_ctf_escort;
1689                         bot.havocbot_role_timeout = time + 30;
1690                         bot.bot_strategytime = 0;
1691                         break;
1692         }
1693         dprint("\n");
1694 }
1695
1696
1697 // ==============
1698 // Hook Functions
1699 // ==============
1700
1701 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1702 {
1703         entity flag;
1704         
1705         // initially clear items so they can be set as necessary later.
1706         self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST 
1707                 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
1708
1709         // scan through all the flags and notify the client about them 
1710         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1711         {
1712                 switch(flag.ctf_status)
1713                 {
1714                         case FLAG_PASSING:
1715                         case FLAG_CARRY:
1716                         {
1717                                 if((flag.owner == self) || (flag.pass_sender == self))
1718                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1719                                 else 
1720                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1721                                 break;
1722                         }
1723                         case FLAG_DROPPED:
1724                         {
1725                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1726                                 break;
1727                         }
1728                 }
1729         }
1730         
1731         // item for stopping players from capturing the flag too often
1732         if(self.ctf_captureshielded)
1733                 self.items |= IT_CTF_SHIELDED;
1734         
1735         // update the health of the flag carrier waypointsprite
1736         if(self.wps_flagcarrier) 
1737                 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1738         
1739         return FALSE;
1740 }
1741
1742 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1743 {
1744         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1745         {
1746                 if(frag_target == frag_attacker) // damage done to yourself
1747                 {
1748                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1749                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1750                 }
1751                 else // damage done to everyone else
1752                 {
1753                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1754                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1755                 }
1756         }
1757         else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1758         {
1759                 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent)))
1760                 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1761                 {
1762                         frag_target.wps_helpme_time = time;
1763                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1764                 }
1765         }
1766         return FALSE;
1767 }
1768
1769 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1770 {
1771         if((frag_attacker != frag_target) && (frag_attacker.classname == "player") && (frag_target.flagcarried))
1772         {
1773                 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1774                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1775         }
1776                                 
1777         if(frag_target.flagcarried)
1778                 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1779                 
1780         return FALSE;
1781 }
1782
1783 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1784 {
1785         frag_score = 0;
1786         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1787 }
1788
1789 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1790 {
1791         entity flag; // temporary entity for the search method
1792         
1793         if(self.flagcarried)
1794                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1795         
1796         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1797         {
1798                 if(flag.pass_sender == self) { flag.pass_sender = world; }
1799                 if(flag.pass_target == self) { flag.pass_target = world; }
1800                 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1801         }
1802                 
1803         return FALSE;
1804 }
1805
1806 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1807 {
1808         if(self.flagcarried) 
1809         if(!autocvar_g_ctf_portalteleport)
1810                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1811
1812         return FALSE;
1813 }
1814
1815 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1816 {
1817         if(MUTATOR_RETURNVALUE || gameover) { return FALSE; }
1818         
1819         entity player = self;
1820
1821         if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1822         {
1823                 // pass the flag to a team mate
1824                 if(autocvar_g_ctf_pass)
1825                 {
1826                         entity head, closest_target = world;
1827                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1828                         
1829                         while(head) // find the closest acceptable target to pass to
1830                         {
1831                                 if(head.classname == "player" && head.deadflag == DEAD_NO)
1832                                 if(head != player && !IsDifferentTeam(head, player))
1833                                 if(!head.speedrunning && !head.vehicle)
1834                                 {
1835                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc) 
1836                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
1837                                         vector passer_center = CENTER_OR_VIEWOFS(player);
1838                                         
1839                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1840                                         {
1841                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried) 
1842                                                 { 
1843                                                         if(clienttype(head) == CLIENTTYPE_BOT)
1844                                                         {
1845                                                                 centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1846                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
1847                                                         }
1848                                                         else
1849                                                         {
1850                                                                 centerprint(head, strcat(player.netname, " requests you to pass the ", head.flagcarried.netname)); 
1851                                                                 centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1852                                                         }
1853                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait; 
1854                                                         return TRUE; 
1855                                                 }
1856                                                 else if(player.flagcarried)
1857                                                 {
1858                                                         if(closest_target)
1859                                                         {
1860                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
1861                                                                 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1862                                                                         { closest_target = head; }
1863                                                         }
1864                                                         else { closest_target = head; }
1865                                                 }
1866                                         }
1867                                 }
1868                                 head = head.chain;
1869                         }
1870                         
1871                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1872                 }
1873                 
1874                 // throw the flag in front of you
1875                 if(autocvar_g_ctf_throw && player.flagcarried)
1876                 {
1877                         if(player.throw_count == -1)
1878                         {
1879                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
1880                                 {
1881                                         player.throw_prevtime = time;
1882                                         player.throw_count = 1;
1883                                         ctf_Handle_Throw(player, world, DROP_THROW);
1884                                         return TRUE;
1885                                 }
1886                                 else
1887                                 {
1888                                         centerprint(player, strcat("Too many flag throws, throwing disabled for ", ftos(rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time)), " seconds."));
1889                                         return FALSE;
1890                                 }
1891                         }
1892                         else
1893                         {
1894                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
1895                                 else { player.throw_count += 1; }
1896                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
1897                                         
1898                                 player.throw_prevtime = time;
1899                                 ctf_Handle_Throw(player, world, DROP_THROW);
1900                                 return TRUE;
1901                         }
1902                 }
1903         }
1904                 
1905         return FALSE;
1906 }
1907
1908 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1909 {
1910         if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1911         {
1912                 self.wps_helpme_time = time;
1913                 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1914         } 
1915         else // create a normal help me waypointsprite
1916         {
1917                 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');
1918                 WaypointSprite_Ping(self.wps_helpme);
1919         }
1920
1921         return TRUE;
1922 }
1923
1924 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1925 {
1926         if(vh_player.flagcarried)
1927         {
1928                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
1929                 {
1930                         ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1931                 }
1932                 else
1933                 {            
1934                         setattachment(vh_player.flagcarried, vh_vehicle, ""); 
1935                         setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1936                         vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1937                         //vh_player.flagcarried.angles = '0 0 0';       
1938                 }
1939                 return TRUE;
1940         }
1941                 
1942         return FALSE;
1943 }
1944
1945 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1946 {
1947         if(vh_player.flagcarried)
1948         {
1949                 setattachment(vh_player.flagcarried, vh_player, ""); 
1950                 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
1951                 vh_player.flagcarried.scale = FLAG_SCALE;
1952                 vh_player.flagcarried.angles = '0 0 0';
1953                 return TRUE;
1954         }
1955
1956         return FALSE;
1957 }
1958
1959 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
1960 {
1961         if(self.flagcarried)
1962         {
1963                 bprint("The ", self.flagcarried.netname, " was returned to base by its carrier\n");
1964                 ctf_RespawnFlag(self);
1965                 return TRUE;
1966         }
1967         
1968         return FALSE;
1969 }
1970
1971 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
1972 {
1973         entity flag; // temporary entity for the search method
1974         
1975         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1976         {
1977                 switch(flag.ctf_status)
1978                 {
1979                         case FLAG_DROPPED:
1980                         case FLAG_PASSING:
1981                         {
1982                                 // lock the flag, game is over
1983                                 flag.movetype = MOVETYPE_NONE;
1984                                 flag.takedamage = DAMAGE_NO;
1985                                 flag.solid = SOLID_NOT;
1986                                 flag.nextthink = FALSE; // stop thinking
1987                                 
1988                                 //dprint("stopping the ", flag.netname, " from moving.\n");
1989                                 break;
1990                         }
1991                         
1992                         default:
1993                         case FLAG_BASE:
1994                         case FLAG_CARRY:
1995                         {
1996                                 // do nothing for these flags
1997                                 break;
1998                         }
1999                 }
2000         }
2001         
2002         return FALSE;
2003 }
2004
2005 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2006 {
2007         havocbot_ctf_reset_role(self);
2008         return TRUE;
2009 }
2010
2011
2012 // ==========
2013 // Spawnfuncs
2014 // ==========
2015
2016 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2017 CTF Starting point for a player in team one (Red).
2018 Keys: "angle" viewing angle when spawning. */
2019 void spawnfunc_info_player_team1()
2020 {
2021         if(g_assault) { remove(self); return; }
2022         
2023         self.team = COLOR_TEAM1; // red
2024         spawnfunc_info_player_deathmatch();
2025 }
2026
2027
2028 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2029 CTF Starting point for a player in team two (Blue).
2030 Keys: "angle" viewing angle when spawning. */
2031 void spawnfunc_info_player_team2()
2032 {
2033         if(g_assault) { remove(self); return; }
2034         
2035         self.team = COLOR_TEAM2; // blue
2036         spawnfunc_info_player_deathmatch();
2037 }
2038
2039 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2040 CTF Starting point for a player in team three (Yellow).
2041 Keys: "angle" viewing angle when spawning. */
2042 void spawnfunc_info_player_team3()
2043 {
2044         if(g_assault) { remove(self); return; }
2045         
2046         self.team = COLOR_TEAM3; // yellow
2047         spawnfunc_info_player_deathmatch();
2048 }
2049
2050
2051 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2052 CTF Starting point for a player in team four (Purple).
2053 Keys: "angle" viewing angle when spawning. */
2054 void spawnfunc_info_player_team4()
2055 {
2056         if(g_assault) { remove(self); return; }
2057         
2058         self.team = COLOR_TEAM4; // purple
2059         spawnfunc_info_player_deathmatch();
2060 }
2061
2062 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2063 CTF flag for team one (Red).
2064 Keys: 
2065 "angle" Angle the flag will point (minus 90 degrees)... 
2066 "model" model to use, note this needs red and blue as skins 0 and 1...
2067 "noise" sound played when flag is picked up...
2068 "noise1" sound played when flag is returned by a teammate...
2069 "noise2" sound played when flag is captured...
2070 "noise3" sound played when flag is lost in the field and respawns itself... 
2071 "noise4" sound played when flag is dropped by a player...
2072 "noise5" sound played when flag touches the ground... */
2073 void spawnfunc_item_flag_team1()
2074 {
2075         if(!g_ctf) { remove(self); return; }
2076
2077         ctf_FlagSetup(1, self); // 1 = red
2078 }
2079
2080 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2081 CTF flag for team two (Blue).
2082 Keys: 
2083 "angle" Angle the flag will point (minus 90 degrees)... 
2084 "model" model to use, note this needs red and blue as skins 0 and 1...
2085 "noise" sound played when flag is picked up...
2086 "noise1" sound played when flag is returned by a teammate...
2087 "noise2" sound played when flag is captured...
2088 "noise3" sound played when flag is lost in the field and respawns itself... 
2089 "noise4" sound played when flag is dropped by a player...
2090 "noise5" sound played when flag touches the ground... */
2091 void spawnfunc_item_flag_team2()
2092 {
2093         if(!g_ctf) { remove(self); return; }
2094
2095         ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
2096 }
2097
2098 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2099 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2100 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.
2101 Keys:
2102 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2103 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2104 void spawnfunc_ctf_team()
2105 {
2106         if(!g_ctf) { remove(self); return; }
2107         
2108         self.classname = "ctf_team";
2109         self.team = self.cnt + 1;
2110 }
2111
2112 // compatibility for quake maps
2113 void spawnfunc_team_CTF_redflag()    { spawnfunc_item_flag_team1();    }
2114 void spawnfunc_team_CTF_blueflag()   { spawnfunc_item_flag_team2();    }
2115 void spawnfunc_team_CTF_redplayer()  { spawnfunc_info_player_team1();  }
2116 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2();  }
2117 void spawnfunc_team_CTF_redspawn()   { spawnfunc_info_player_team1();  }
2118 void spawnfunc_team_CTF_bluespawn()  { spawnfunc_info_player_team2();  }
2119
2120
2121 // ==============
2122 // Initialization
2123 // ==============
2124
2125 // scoreboard setup
2126 void ctf_ScoreRules()
2127 {
2128         ScoreRules_basics(2, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
2129         ScoreInfo_SetLabel_TeamScore  (ST_CTF_CAPS,     "caps",      SFL_SORT_PRIO_PRIMARY);
2130         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS,     "caps",      SFL_SORT_PRIO_SECONDARY);
2131         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME,  "captime",   SFL_LOWER_IS_BETTER | SFL_TIME);
2132         ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS,  "pickups",   0);
2133         ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS,  "fckills",   0);
2134         ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS,  "returns",   0);
2135         ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS,    "drops",     SFL_LOWER_IS_BETTER);
2136         ScoreRules_basics_end();
2137 }
2138
2139 // code from here on is just to support maps that don't have flag and team entities
2140 void ctf_SpawnTeam (string teamname, float teamcolor)
2141 {
2142         entity oldself;
2143         oldself = self;
2144         self = spawn();
2145         self.classname = "ctf_team";
2146         self.netname = teamname;
2147         self.cnt = teamcolor;
2148
2149         spawnfunc_ctf_team();
2150
2151         self = oldself;
2152 }
2153
2154 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2155 {
2156         // if no teams are found, spawn defaults
2157         if(find(world, classname, "ctf_team") == world)
2158         {
2159                 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2160                 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
2161                 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
2162         }
2163         
2164         ctf_ScoreRules();
2165 }
2166
2167 void ctf_Initialize()
2168 {
2169         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2170
2171         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2172         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2173         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2174         
2175         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2176 }
2177
2178
2179 MUTATOR_DEFINITION(gamemode_ctf)
2180 {
2181         MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2182         MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2183         MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2184         MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2185         MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2186         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2187         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2188         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2189         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2190         MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2191         MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2192         MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2193         MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2194         MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY);
2195         
2196         MUTATOR_ONADD
2197         {
2198                 if(time > 1) // game loads at time 1
2199                         error("This is a game type and it cannot be added at runtime.");
2200                 ctf_Initialize();
2201         }
2202
2203         MUTATOR_ONROLLBACK_OR_REMOVE
2204         {
2205                 // we actually cannot roll back ctf_Initialize here
2206                 // BUT: we don't need to! If this gets called, adding always
2207                 // succeeds.
2208         }
2209
2210         MUTATOR_ONREMOVE
2211         {
2212                 print("This is a game type and it cannot be removed at runtime.");
2213                 return -1;
2214         }
2215
2216         return 0;
2217 }