]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_ctf.qc
More cleanup
[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, 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                         centerprint(tmp_player, strcat("You got the ", flag.netname, "!"));
506                 else if(!IsDifferentTeam(tmp_player, player))
507                         centerprint(tmp_player, strcat("Your ", Team_ColorCode(player.team), "team mate ", verbosename, "^7got the flag! Protect them!"));
508                 else if(!IsDifferentTeam(tmp_player, flag))
509                         centerprint(tmp_player, strcat("The ", Team_ColorCode(player.team), "enemy ", verbosename, "^7got your flag! Retrieve it!"));
510         }
511         
512         // scoring
513         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
514         switch(pickuptype)
515         {               
516                 case PICKUP_BASE:
517                 {
518                         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
519                         ctf_EventLog("steal", flag.team, player);
520                         break;
521                 }
522                 
523                 case PICKUP_DROPPED:
524                 {
525                         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);
526                         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);
527                         dprint("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
528                         PlayerTeamScore_AddScore(player, pickup_dropped_score);
529                         ctf_EventLog("pickup", flag.team, player);
530                         break;
531                 }
532                 
533                 default: break;
534         }
535         
536         // speedrunning
537         if(pickuptype == PICKUP_BASE)
538         {
539                 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
540                 if((player.speedrunning) && (ctf_captimerecord))
541                         ctf_FakeTimeLimit(player, time + ctf_captimerecord);
542         }
543                 
544         // effects
545         pointparticles(particleeffectnum(flag.toucheffect), player.origin, '0 0 0', 1);
546         
547         // waypoints 
548         if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
549         ctf_FlagcarrierWaypoints(player);
550         WaypointSprite_Ping(player.wps_flagcarrier);
551 }
552
553
554 // ===================
555 // Main Flag Functions
556 // ===================
557
558 void ctf_CheckFlagReturn(entity flag, float returntype)
559 {
560         if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
561         {
562                 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
563                 
564                 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
565                 {
566                         switch(returntype)
567                         {
568                                 case RETURN_DROPPED: bprint("The ", flag.netname, " was dropped in the base and returned itself\n"); break;
569                                 case RETURN_DAMAGE: bprint("The ", flag.netname, " was destroyed and returned to base\n"); break;
570                                 case RETURN_SPEEDRUN: bprint("The ", flag.netname, " became impatient after ", ftos_decimals(ctf_captimerecord, 2), " seconds and returned itself\n"); break;
571                                 case RETURN_NEEDKILL: bprint("The ", flag.netname, " fell somewhere it couldn't be reached and returned to base\n"); break;
572                                 
573                                 default:
574                                 case RETURN_TIMEOUT:
575                                         { bprint("The ", flag.netname, " has returned to base\n"); break; }
576                         }
577                         sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTN_NONE);
578                         ctf_EventLog("returned", flag.team, world);
579                         ctf_RespawnFlag(flag);
580                 }
581         }
582 }
583
584 void ctf_CheckStalemate(void)
585 {
586         // declarations
587         float stale_red_flags, stale_blue_flags;
588         entity tmp_entity;
589
590         entity ctf_staleflaglist; // reset the list, we need to build the list each time this function runs
591
592         // build list of stale flags
593         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
594         {
595                 if(autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
596                 if(tmp_entity.ctf_status != FLAG_BASE)
597                 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
598                 {
599                         tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
600                         ctf_staleflaglist = tmp_entity;
601                         
602                         switch(tmp_entity.team)
603                         {
604                                 case COLOR_TEAM1: ++stale_red_flags; break;
605                                 case COLOR_TEAM2: ++stale_blue_flags; break;
606                         }
607                 }
608         }
609
610         if(stale_red_flags && stale_blue_flags)
611                 ctf_stalemate = TRUE;
612         else if(!stale_red_flags && !stale_blue_flags)
613                 ctf_stalemate = FALSE;
614         
615         // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
616         if(ctf_stalemate)
617         {
618                 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
619                 {
620                         if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
621                                 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));
622                 }
623                 
624                 if not(wpforenemy_announced)
625                 {
626                         FOR_EACH_REALPLAYER(tmp_entity)
627                                 if(tmp_entity.flagcarried)
628                                         centerprint(tmp_entity, "Stalemate! Enemies can now see you on radar!");
629                                 else
630                                         centerprint(tmp_entity, "Stalemate! Flag carriers can now be seen by enemies on radar!");
631                         
632                         wpforenemy_announced = TRUE;
633                 }
634         }
635 }
636
637 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
638 {
639         if(ITEM_DAMAGE_NEEDKILL(deathtype))
640         {
641                 // automatically kill the flag and return it
642                 self.health = 0;
643                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
644                 return;
645         }
646         if(autocvar_g_ctf_flag_return_damage) 
647         {
648                 // reduce health and check if it should be returned
649                 self.health = self.health - damage;
650                 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
651                 return;
652         }
653 }
654
655 void ctf_FlagThink()
656 {
657         // declarations
658         entity tmp_entity;
659
660         self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
661
662         // captureshield
663         if(self == ctf_worldflaglist) // only for the first flag
664                 FOR_EACH_CLIENT(tmp_entity)
665                         ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
666
667         // sanity checks
668         if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
669                 dprint("wtf the flag got squashed?\n");
670                 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
671                 if(!trace_startsolid) // can we resize it without getting stuck?
672                         setsize(self, FLAG_MIN, FLAG_MAX); }
673                         
674         switch(self.ctf_status) // reset flag angles in case warpzones adjust it
675         {
676                 case FLAG_DROPPED:
677                 {
678                         self.angles = '0 0 0';
679                         break;
680                 }
681                 
682                 default: break;
683         }
684
685         // main think method
686         switch(self.ctf_status)
687         {       
688                 case FLAG_BASE:
689                 {
690                         if(autocvar_g_ctf_dropped_capture_radius)
691                         {
692                                 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
693                                         if(tmp_entity.ctf_status == FLAG_DROPPED)
694                                                 if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
695                                                         ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
696                         }
697                         return;
698                 }
699                 
700                 case FLAG_DROPPED:
701                 {
702                         if(autocvar_g_ctf_flag_dropped_floatinwater)
703                         {
704                                 vector midpoint = ((self.absmin + self.absmax) * 0.5);
705                                 if(pointcontents(midpoint) == CONTENT_WATER)
706                                 {
707                                         self.velocity = self.velocity * 0.5;
708                                         
709                                         if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
710                                                 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
711                                         else
712                                                 { self.movetype = MOVETYPE_FLY; }
713                                 }
714                                 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
715                         }
716                         if(autocvar_g_ctf_flag_return_dropped)
717                         {
718                                 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
719                                 {
720                                         self.health = 0;
721                                         ctf_CheckFlagReturn(self, RETURN_DROPPED);
722                                         return;
723                                 }
724                         }
725                         if(autocvar_g_ctf_flag_return_time)
726                         {
727                                 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
728                                 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
729                                 return;
730                         } 
731                         return;
732                 }
733                         
734                 case FLAG_CARRY:
735                 {
736                         if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord)) 
737                         {
738                                 self.health = 0;
739                                 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
740
741                                 tmp_entity = self;
742                                 self = self.owner;
743                                 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
744                                 ImpulseCommands();
745                                 self = tmp_entity;
746                         }
747                         if(autocvar_g_ctf_flagcarrier_waypointforenemy_stalemate)
748                         {
749                                 if(time >= wpforenemy_nextthink)
750                                 {
751                                         ctf_CheckStalemate();
752                                         wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
753                                 }
754                         }
755                         return;
756                 }
757                 
758                 case FLAG_PASSING:
759                 {
760                         vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
761                         targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
762                         WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
763                         
764                         if((self.pass_target == world)
765                                 || (self.pass_target.deadflag != DEAD_NO)
766                                 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
767                                 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
768                                 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
769                         {
770                                 // give up, pass failed
771                                 ctf_Handle_Drop(self, world, DROP_PASS);
772                         }
773                         else
774                         {
775                                 // still a viable target, go for it
776                                 ctf_CalculatePassVelocity(self, targ_origin, self.origin, TRUE);
777                         }
778                         return;
779                 }
780
781                 default: // this should never happen
782                 {
783                         dprint("ctf_FlagThink(): Flag exists with no status?\n");
784                         return;
785                 }
786         }
787 }
788
789 void ctf_FlagTouch()
790 {
791         if(gameover) { return; }
792         
793         entity toucher = other;
794         
795         // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
796         if(ITEM_TOUCH_NEEDKILL())
797         {
798                 self.health = 0;
799                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
800                 return;
801         }
802         
803         // special touch behaviors
804         if(toucher.vehicle_flags & VHF_ISVEHICLE)
805         {
806                 if(autocvar_g_ctf_allow_vehicle_touch)
807                         toucher = toucher.owner; // the player is actually the vehicle owner, not other
808                 else
809                         return; // do nothing
810         }
811         else if(toucher.classname != "player") // The flag just touched an object, most likely the world
812         {
813                 if(time > self.wait) // if we haven't in a while, play a sound/effect
814                 {
815                         pointparticles(particleeffectnum(self.toucheffect), self.origin, '0 0 0', 1);
816                         sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTN_NORM);
817                         self.wait = time + FLAG_TOUCHRATE;
818                 }
819                 return;
820         }
821         else if(toucher.deadflag != DEAD_NO) { return; }
822
823         switch(self.ctf_status) 
824         {       
825                 case FLAG_BASE:
826                 {
827                         if(!IsDifferentTeam(toucher, self) && (toucher.flagcarried) && IsDifferentTeam(toucher.flagcarried, self))
828                                 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
829                         else if(IsDifferentTeam(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time))
830                                 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
831                         break;
832                 }
833                 
834                 case FLAG_DROPPED:
835                 {
836                         if(!IsDifferentTeam(toucher, self))
837                                 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
838                         else if((!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
839                                 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
840                         break;
841                 }
842                         
843                 case FLAG_CARRY:
844                 {
845                         dprint("Someone touched a flag even though it was being carried?\n");
846                         break;
847                 }
848                 
849                 case FLAG_PASSING:
850                 {
851                         if((toucher.classname == "player") && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
852                         {
853                                 if(IsDifferentTeam(toucher, self.pass_sender))
854                                         ctf_Handle_Return(self, toucher);
855                                 else
856                                         ctf_Handle_Retrieve(self, toucher);
857                         }
858                         break;
859                 }
860         }
861 }
862
863 .float last_respawn;
864 void ctf_RespawnFlag(entity flag)
865 {
866         // check for flag respawn being called twice in a row
867         if(flag.last_respawn > time - 0.5)
868                 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
869
870         flag.last_respawn = time;
871         
872         // reset the player (if there is one)
873         if((flag.owner) && (flag.owner.flagcarried == flag))
874         {
875                 if(flag.owner.wps_enemyflagcarrier)
876                         WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
877                         
878                 WaypointSprite_Kill(flag.wps_flagcarrier);
879                 
880                 flag.owner.flagcarried = world;
881
882                 if(flag.speedrunning)
883                         ctf_FakeTimeLimit(flag.owner, -1);
884         }
885
886         if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
887                 { WaypointSprite_Kill(flag.wps_flagdropped); }
888
889         // reset the flag
890         setattachment(flag, world, "");
891         setorigin(flag, flag.ctf_spawnorigin);
892         
893         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
894         flag.takedamage = DAMAGE_NO;
895         flag.health = flag.max_flag_health;
896         flag.solid = SOLID_TRIGGER;
897         flag.velocity = '0 0 0';
898         flag.angles = flag.mangle;
899         flag.flags = FL_ITEM | FL_NOTARGET;
900         
901         flag.ctf_status = FLAG_BASE;
902         flag.owner = world;
903         flag.pass_distance = 0;
904         flag.pass_sender = world;
905         flag.pass_target = world;
906         flag.ctf_dropper = world;
907         flag.ctf_pickuptime = 0;
908         flag.ctf_droptime = 0;
909
910         wpforenemy_announced = FALSE;
911 }
912
913 void ctf_Reset()
914 {
915         if(self.owner)
916                 if(self.owner.classname == "player")
917                         ctf_Handle_Throw(self.owner, world, DROP_RESET);
918                         
919         ctf_RespawnFlag(self);
920 }
921
922 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
923 {
924         // bot waypoints
925         waypoint_spawnforitem_force(self, self.origin);
926         self.nearestwaypointtimeout = 0; // activate waypointing again
927         self.bot_basewaypoint = self.nearestwaypoint;
928
929         // waypointsprites
930         WaypointSprite_SpawnFixed(((self.team == COLOR_TEAM1) ? "redbase" : "bluebase"), self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
931         WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
932
933         // captureshield setup
934         ctf_CaptureShield_Spawn(self);
935 }
936
937 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc 
938 {       
939         // declarations
940         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. 
941         self = flag; // for later usage with droptofloor()
942         
943         // main setup
944         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
945         ctf_worldflaglist = flag;
946
947         setattachment(flag, world, ""); 
948
949         flag.netname = ((teamnumber) ? "^1RED^7 flag" : "^4BLUE^7 flag");
950         flag.team = ((teamnumber) ? COLOR_TEAM1 : COLOR_TEAM2); // COLOR_TEAM1: color 4 team (red) - COLOR_TEAM2: color 13 team (blue)
951         flag.items = ((teamnumber) ? IT_KEY2 : IT_KEY1); // IT_KEY2: gold key (redish enough) - IT_KEY1: silver key (bluish enough)
952         flag.classname = "item_flag_team";
953         flag.target = "###item###"; // wut?
954         flag.flags = FL_ITEM | FL_NOTARGET;
955         flag.solid = SOLID_TRIGGER;
956         flag.takedamage = DAMAGE_NO;
957         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;   
958         flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
959         flag.health = flag.max_flag_health;
960         flag.event_damage = ctf_FlagDamage;
961         flag.pushable = TRUE;
962         flag.teleportable = TELEPORT_NORMAL;
963         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
964         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
965         flag.velocity = '0 0 0';
966         flag.mangle = flag.angles;
967         flag.reset = ctf_Reset;
968         flag.touch = ctf_FlagTouch;
969         flag.think = ctf_FlagThink;
970         flag.nextthink = time + FLAG_THINKRATE;
971         flag.ctf_status = FLAG_BASE;
972         
973         if(!flag.model) { flag.model = ((teamnumber) ? autocvar_g_ctf_flag_red_model : autocvar_g_ctf_flag_blue_model); }
974         if(!flag.scale) { flag.scale = FLAG_SCALE; }
975         if(!flag.skin) { flag.skin = ((teamnumber) ? autocvar_g_ctf_flag_red_skin : autocvar_g_ctf_flag_blue_skin); }
976         if(!flag.toucheffect) { flag.toucheffect = ((teamnumber) ? "redflag_touch" : "blueflag_touch"); }
977         if(!flag.passeffect) { flag.passeffect = ((teamnumber) ? "red_pass" : "blue_pass"); }
978         if(!flag.capeffect) { flag.capeffect = ((teamnumber) ? "red_cap" : "blue_cap"); }
979         
980         // sound 
981         if(!flag.snd_flag_taken) { flag.snd_flag_taken  = ((teamnumber) ? "ctf/red_taken.wav" : "ctf/blue_taken.wav"); }
982         if(!flag.snd_flag_returned) { flag.snd_flag_returned = ((teamnumber) ? "ctf/red_returned.wav" : "ctf/blue_returned.wav"); }
983         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
984         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.
985         if(!flag.snd_flag_dropped) { flag.snd_flag_dropped = ((teamnumber) ? "ctf/red_dropped.wav" : "ctf/blue_dropped.wav"); }
986         if(!flag.snd_flag_touch) { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
987         if(!flag.snd_flag_pass) { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
988         
989         // precache
990         precache_sound(flag.snd_flag_taken);
991         precache_sound(flag.snd_flag_returned);
992         precache_sound(flag.snd_flag_capture);
993         precache_sound(flag.snd_flag_respawn);
994         precache_sound(flag.snd_flag_dropped);
995         precache_sound(flag.snd_flag_touch);
996         precache_sound(flag.snd_flag_pass);
997         precache_model(flag.model);
998         precache_model("models/ctf/shield.md3");
999         precache_model("models/ctf/shockwavetransring.md3");
1000
1001         // appearence
1002         setmodel(flag, flag.model); // precision set below
1003         setsize(flag, FLAG_MIN, FLAG_MAX);
1004         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1005         
1006         if(autocvar_g_ctf_flag_glowtrails)
1007         {
1008                 flag.glow_color = ((teamnumber) ? 251 : 210); // 251: red - 210: blue
1009                 flag.glow_size = 25;
1010                 flag.glow_trail = 1;
1011         }
1012         
1013         flag.effects |= EF_LOWPRECISION;
1014         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1015         if(autocvar_g_ctf_dynamiclights)   { flag.effects |= ((teamnumber) ? EF_RED : EF_BLUE); }
1016         
1017         // flag placement
1018         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1019         {       
1020                 flag.dropped_origin = flag.origin; 
1021                 flag.noalign = TRUE;
1022                 flag.movetype = MOVETYPE_NONE;
1023         }
1024         else // drop to floor, automatically find a platform and set that as spawn origin
1025         { 
1026                 flag.noalign = FALSE;
1027                 self = flag;
1028                 droptofloor();
1029                 flag.movetype = MOVETYPE_TOSS; 
1030         }       
1031         
1032         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1033 }
1034
1035
1036 // ================
1037 // Bot player logic
1038 // ================
1039
1040 // NOTE: LEGACY CODE, needs to be re-written!
1041
1042 void havocbot_calculate_middlepoint()
1043 {
1044         entity f;
1045         vector s = '0 0 0';
1046         vector fo = '0 0 0';
1047         float n = 0;
1048
1049         f = ctf_worldflaglist;
1050         while (f)
1051         {
1052                 fo = f.origin;
1053                 s = s + fo;
1054                 f = f.ctf_worldflagnext;
1055         }
1056         if(!n)
1057                 return;
1058         havocbot_ctf_middlepoint = s * (1.0 / n);
1059         havocbot_ctf_middlepoint_radius  = vlen(fo - havocbot_ctf_middlepoint);
1060 }
1061
1062
1063 entity havocbot_ctf_find_flag(entity bot)
1064 {
1065         entity f;
1066         f = ctf_worldflaglist;
1067         while (f)
1068         {
1069                 if (bot.team == f.team)
1070                         return f;
1071                 f = f.ctf_worldflagnext;
1072         }
1073         return world;
1074 }
1075
1076 entity havocbot_ctf_find_enemy_flag(entity bot)
1077 {
1078         entity f;
1079         f = ctf_worldflaglist;
1080         while (f)
1081         {
1082                 if (bot.team != f.team)
1083                         return f;
1084                 f = f.ctf_worldflagnext;
1085         }
1086         return world;
1087 }
1088
1089 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1090 {
1091         if not(teamplay)
1092                 return 0;
1093
1094         float c = 0;
1095         entity head;
1096
1097         FOR_EACH_PLAYER(head)
1098         {
1099                 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1100                         continue;
1101
1102                 if(vlen(head.origin - org) < tc_radius)
1103                         ++c;
1104         }
1105
1106         return c;
1107 }
1108
1109 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1110 {
1111         entity head;
1112         head = ctf_worldflaglist;
1113         while (head)
1114         {
1115                 if (self.team == head.team)
1116                         break;
1117                 head = head.ctf_worldflagnext;
1118         }
1119         if (head)
1120                 navigation_routerating(head, ratingscale, 10000);
1121 }
1122
1123 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1124 {
1125         entity head;
1126         head = ctf_worldflaglist;
1127         while (head)
1128         {
1129                 if (self.team == head.team)
1130                         break;
1131                 head = head.ctf_worldflagnext;
1132         }
1133         if not(head)
1134                 return;
1135
1136         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1137 }
1138
1139 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1140 {
1141         entity head;
1142         head = ctf_worldflaglist;
1143         while (head)
1144         {
1145                 if (self.team != head.team)
1146                         break;
1147                 head = head.ctf_worldflagnext;
1148         }
1149         if (head)
1150                 navigation_routerating(head, ratingscale, 10000);
1151 }
1152
1153 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1154 {
1155         if not(bot_waypoints_for_items)
1156         {
1157                 havocbot_goalrating_ctf_enemyflag(ratingscale);
1158                 return;
1159         }
1160
1161         entity head;
1162
1163         head = havocbot_ctf_find_enemy_flag(self);
1164
1165         if not(head)
1166                 return;
1167
1168         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1169 }
1170
1171 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1172 {
1173         entity mf;
1174
1175         mf = havocbot_ctf_find_flag(self);
1176
1177         if(mf.ctf_status == FLAG_BASE)
1178                 return;
1179
1180         if(mf.tag_entity)
1181                 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1182 }
1183
1184 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1185 {
1186         entity head;
1187         head = ctf_worldflaglist;
1188         while (head)
1189         {
1190                 // flag is out in the field
1191                 if(head.ctf_status != FLAG_BASE)
1192                 if(head.tag_entity==world)      // dropped
1193                 {
1194                         if(df_radius)
1195                         {
1196                                 if(vlen(org-head.origin)<df_radius)
1197                                         navigation_routerating(head, ratingscale, 10000);
1198                         }
1199                         else
1200                                 navigation_routerating(head, ratingscale, 10000);
1201                 }
1202
1203                 head = head.ctf_worldflagnext;
1204         }
1205 }
1206
1207 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1208 {
1209         entity head;
1210         float t;
1211         head = findchainfloat(bot_pickup, TRUE);
1212         while (head)
1213         {
1214                 // gather health and armor only
1215                 if (head.solid)
1216                 if (head.health || head.armorvalue)
1217                 if (vlen(head.origin - org) < sradius)
1218                 {
1219                         // get the value of the item
1220                         t = head.bot_pickupevalfunc(self, head) * 0.0001;
1221                         if (t > 0)
1222                                 navigation_routerating(head, t * ratingscale, 500);
1223                 }
1224                 head = head.chain;
1225         }
1226 }
1227
1228 void havocbot_ctf_reset_role(entity bot)
1229 {
1230         float cdefense, cmiddle, coffense;
1231         entity mf, ef, head;
1232         float c;
1233
1234         if(bot.deadflag != DEAD_NO)
1235                 return;
1236
1237         if(vlen(havocbot_ctf_middlepoint)==0)
1238                 havocbot_calculate_middlepoint();
1239
1240         // Check ctf flags
1241         if (bot.flagcarried)
1242         {
1243                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1244                 return;
1245         }
1246
1247         mf = havocbot_ctf_find_flag(bot);
1248         ef = havocbot_ctf_find_enemy_flag(bot);
1249
1250         // Retrieve stolen flag
1251         if(mf.ctf_status!=FLAG_BASE)
1252         {
1253                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1254                 return;
1255         }
1256
1257         // If enemy flag is taken go to the middle to intercept pursuers
1258         if(ef.ctf_status!=FLAG_BASE)
1259         {
1260                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1261                 return;
1262         }
1263
1264         // if there is only me on the team switch to offense
1265         c = 0;
1266         FOR_EACH_PLAYER(head)
1267         if(head.team==bot.team)
1268                 ++c;
1269
1270         if(c==1)
1271         {
1272                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1273                 return;
1274         }
1275
1276         // Evaluate best position to take
1277         // Count mates on middle position
1278         cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1279
1280         // Count mates on defense position
1281         cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1282
1283         // Count mates on offense position
1284         coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1285
1286         if(cdefense<=coffense)
1287                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1288         else if(coffense<=cmiddle)
1289                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1290         else
1291                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1292 }
1293
1294 void havocbot_role_ctf_carrier()
1295 {
1296         if(self.deadflag != DEAD_NO)
1297         {
1298                 havocbot_ctf_reset_role(self);
1299                 return;
1300         }
1301
1302         if (self.flagcarried == world)
1303         {
1304                 havocbot_ctf_reset_role(self);
1305                 return;
1306         }
1307
1308         if (self.bot_strategytime < time)
1309         {
1310                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1311
1312                 navigation_goalrating_start();
1313                 havocbot_goalrating_ctf_ourbase(50000);
1314
1315                 if(self.health<100)
1316                         havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1317
1318                 navigation_goalrating_end();
1319
1320                 if (self.navigation_hasgoals)
1321                         self.havocbot_cantfindflag = time + 10;
1322                 else if (time > self.havocbot_cantfindflag)
1323                 {
1324                         // Can't navigate to my own base, suicide!
1325                         // TODO: drop it and wander around
1326                         Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1327                         return;
1328                 }
1329         }
1330 }
1331
1332 void havocbot_role_ctf_escort()
1333 {
1334         entity mf, ef;
1335
1336         if(self.deadflag != DEAD_NO)
1337         {
1338                 havocbot_ctf_reset_role(self);
1339                 return;
1340         }
1341
1342         if (self.flagcarried)
1343         {
1344                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1345                 return;
1346         }
1347
1348         // If enemy flag is back on the base switch to previous role
1349         ef = havocbot_ctf_find_enemy_flag(self);
1350         if(ef.ctf_status==FLAG_BASE)
1351         {
1352                 self.havocbot_role = self.havocbot_previous_role;
1353                 self.havocbot_role_timeout = 0;
1354                 return;
1355         }
1356
1357         // If the flag carrier reached the base switch to defense
1358         mf = havocbot_ctf_find_flag(self);
1359         if(mf.ctf_status!=FLAG_BASE)
1360         if(vlen(ef.origin - mf.dropped_origin) < 300)
1361         {
1362                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1363                 return;
1364         }
1365
1366         // Set the role timeout if necessary
1367         if (!self.havocbot_role_timeout)
1368         {
1369                 self.havocbot_role_timeout = time + random() * 30 + 60;
1370         }
1371
1372         // If nothing happened just switch to previous role
1373         if (time > self.havocbot_role_timeout)
1374         {
1375                 self.havocbot_role = self.havocbot_previous_role;
1376                 self.havocbot_role_timeout = 0;
1377                 return;
1378         }
1379
1380         // Chase the flag carrier
1381         if (self.bot_strategytime < time)
1382         {
1383                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1384                 navigation_goalrating_start();
1385                 havocbot_goalrating_ctf_enemyflag(30000);
1386                 havocbot_goalrating_ctf_ourstolenflag(40000);
1387                 havocbot_goalrating_items(10000, self.origin, 10000);
1388                 navigation_goalrating_end();
1389         }
1390 }
1391
1392 void havocbot_role_ctf_offense()
1393 {
1394         entity mf, ef;
1395         vector pos;
1396
1397         if(self.deadflag != DEAD_NO)
1398         {
1399                 havocbot_ctf_reset_role(self);
1400                 return;
1401         }
1402
1403         if (self.flagcarried)
1404         {
1405                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1406                 return;
1407         }
1408
1409         // Check flags
1410         mf = havocbot_ctf_find_flag(self);
1411         ef = havocbot_ctf_find_enemy_flag(self);
1412
1413         // Own flag stolen
1414         if(mf.ctf_status!=FLAG_BASE)
1415         {
1416                 if(mf.tag_entity)
1417                         pos = mf.tag_entity.origin;
1418                 else
1419                         pos = mf.origin;
1420
1421                 // Try to get it if closer than the enemy base
1422                 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1423                 {
1424                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1425                         return;
1426                 }
1427         }
1428
1429         // Escort flag carrier
1430         if(ef.ctf_status!=FLAG_BASE)
1431         {
1432                 if(ef.tag_entity)
1433                         pos = ef.tag_entity.origin;
1434                 else
1435                         pos = ef.origin;
1436
1437                 if(vlen(pos-mf.dropped_origin)>700)
1438                 {
1439                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1440                         return;
1441                 }
1442         }
1443
1444         // About to fail, switch to middlefield
1445         if(self.health<50)
1446         {
1447                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1448                 return;
1449         }
1450
1451         // Set the role timeout if necessary
1452         if (!self.havocbot_role_timeout)
1453                 self.havocbot_role_timeout = time + 120;
1454
1455         if (time > self.havocbot_role_timeout)
1456         {
1457                 havocbot_ctf_reset_role(self);
1458                 return;
1459         }
1460
1461         if (self.bot_strategytime < time)
1462         {
1463                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1464                 navigation_goalrating_start();
1465                 havocbot_goalrating_ctf_ourstolenflag(50000);
1466                 havocbot_goalrating_ctf_enemybase(20000);
1467                 havocbot_goalrating_items(5000, self.origin, 1000);
1468                 havocbot_goalrating_items(1000, self.origin, 10000);
1469                 navigation_goalrating_end();
1470         }
1471 }
1472
1473 // Retriever (temporary role):
1474 void havocbot_role_ctf_retriever()
1475 {
1476         entity mf;
1477
1478         if(self.deadflag != DEAD_NO)
1479         {
1480                 havocbot_ctf_reset_role(self);
1481                 return;
1482         }
1483
1484         if (self.flagcarried)
1485         {
1486                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1487                 return;
1488         }
1489
1490         // If flag is back on the base switch to previous role
1491         mf = havocbot_ctf_find_flag(self);
1492         if(mf.ctf_status==FLAG_BASE)
1493         {
1494                 havocbot_ctf_reset_role(self);
1495                 return;
1496         }
1497
1498         if (!self.havocbot_role_timeout)
1499                 self.havocbot_role_timeout = time + 20;
1500
1501         if (time > self.havocbot_role_timeout)
1502         {
1503                 havocbot_ctf_reset_role(self);
1504                 return;
1505         }
1506
1507         if (self.bot_strategytime < time)
1508         {
1509                 float rt_radius;
1510                 rt_radius = 10000;
1511
1512                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1513                 navigation_goalrating_start();
1514                 havocbot_goalrating_ctf_ourstolenflag(50000);
1515                 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1516                 havocbot_goalrating_ctf_enemybase(30000);
1517                 havocbot_goalrating_items(500, self.origin, rt_radius);
1518                 navigation_goalrating_end();
1519         }
1520 }
1521
1522 void havocbot_role_ctf_middle()
1523 {
1524         entity mf;
1525
1526         if(self.deadflag != DEAD_NO)
1527         {
1528                 havocbot_ctf_reset_role(self);
1529                 return;
1530         }
1531
1532         if (self.flagcarried)
1533         {
1534                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1535                 return;
1536         }
1537
1538         mf = havocbot_ctf_find_flag(self);
1539         if(mf.ctf_status!=FLAG_BASE)
1540         {
1541                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1542                 return;
1543         }
1544
1545         if (!self.havocbot_role_timeout)
1546                 self.havocbot_role_timeout = time + 10;
1547
1548         if (time > self.havocbot_role_timeout)
1549         {
1550                 havocbot_ctf_reset_role(self);
1551                 return;
1552         }
1553
1554         if (self.bot_strategytime < time)
1555         {
1556                 vector org;
1557
1558                 org = havocbot_ctf_middlepoint;
1559                 org_z = self.origin_z;
1560
1561                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1562                 navigation_goalrating_start();
1563                 havocbot_goalrating_ctf_ourstolenflag(50000);
1564                 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1565                 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1566                 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1567                 havocbot_goalrating_items(2500, self.origin, 10000);
1568                 havocbot_goalrating_ctf_enemybase(2500);
1569                 navigation_goalrating_end();
1570         }
1571 }
1572
1573 void havocbot_role_ctf_defense()
1574 {
1575         entity mf;
1576
1577         if(self.deadflag != DEAD_NO)
1578         {
1579                 havocbot_ctf_reset_role(self);
1580                 return;
1581         }
1582
1583         if (self.flagcarried)
1584         {
1585                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1586                 return;
1587         }
1588
1589         // If own flag was captured
1590         mf = havocbot_ctf_find_flag(self);
1591         if(mf.ctf_status!=FLAG_BASE)
1592         {
1593                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1594                 return;
1595         }
1596
1597         if (!self.havocbot_role_timeout)
1598                 self.havocbot_role_timeout = time + 30;
1599
1600         if (time > self.havocbot_role_timeout)
1601         {
1602                 havocbot_ctf_reset_role(self);
1603                 return;
1604         }
1605         if (self.bot_strategytime < time)
1606         {
1607                 float mp_radius;
1608                 vector org;
1609
1610                 org = mf.dropped_origin;
1611                 mp_radius = havocbot_ctf_middlepoint_radius;
1612
1613                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1614                 navigation_goalrating_start();
1615
1616                 // if enemies are closer to our base, go there
1617                 entity head, closestplayer = world;
1618                 float distance, bestdistance = 10000;
1619                 FOR_EACH_PLAYER(head)
1620                 {
1621                         if(head.deadflag!=DEAD_NO)
1622                                 continue;
1623
1624                         distance = vlen(org - head.origin);
1625                         if(distance<bestdistance)
1626                         {
1627                                 closestplayer = head;
1628                                 bestdistance = distance;
1629                         }
1630                 }
1631
1632                 if(closestplayer)
1633                 if(closestplayer.team!=self.team)
1634                 if(vlen(org - self.origin)>1000)
1635                 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1636                         havocbot_goalrating_ctf_ourbase(30000);
1637
1638                 havocbot_goalrating_ctf_ourstolenflag(20000);
1639                 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1640                 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1641                 havocbot_goalrating_items(10000, org, mp_radius);
1642                 havocbot_goalrating_items(5000, self.origin, 10000);
1643                 navigation_goalrating_end();
1644         }
1645 }
1646
1647 void havocbot_role_ctf_setrole(entity bot, float role)
1648 {
1649         dprint(strcat(bot.netname," switched to "));
1650         switch(role)
1651         {
1652                 case HAVOCBOT_CTF_ROLE_CARRIER:
1653                         dprint("carrier");
1654                         bot.havocbot_role = havocbot_role_ctf_carrier;
1655                         bot.havocbot_role_timeout = 0;
1656                         bot.havocbot_cantfindflag = time + 10;
1657                         bot.bot_strategytime = 0;
1658                         break;
1659                 case HAVOCBOT_CTF_ROLE_DEFENSE:
1660                         dprint("defense");
1661                         bot.havocbot_role = havocbot_role_ctf_defense;
1662                         bot.havocbot_role_timeout = 0;
1663                         break;
1664                 case HAVOCBOT_CTF_ROLE_MIDDLE:
1665                         dprint("middle");
1666                         bot.havocbot_role = havocbot_role_ctf_middle;
1667                         bot.havocbot_role_timeout = 0;
1668                         break;
1669                 case HAVOCBOT_CTF_ROLE_OFFENSE:
1670                         dprint("offense");
1671                         bot.havocbot_role = havocbot_role_ctf_offense;
1672                         bot.havocbot_role_timeout = 0;
1673                         break;
1674                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1675                         dprint("retriever");
1676                         bot.havocbot_previous_role = bot.havocbot_role;
1677                         bot.havocbot_role = havocbot_role_ctf_retriever;
1678                         bot.havocbot_role_timeout = time + 10;
1679                         bot.bot_strategytime = 0;
1680                         break;
1681                 case HAVOCBOT_CTF_ROLE_ESCORT:
1682                         dprint("escort");
1683                         bot.havocbot_previous_role = bot.havocbot_role;
1684                         bot.havocbot_role = havocbot_role_ctf_escort;
1685                         bot.havocbot_role_timeout = time + 30;
1686                         bot.bot_strategytime = 0;
1687                         break;
1688         }
1689         dprint("\n");
1690 }
1691
1692
1693 // ==============
1694 // Hook Functions
1695 // ==============
1696
1697 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1698 {
1699         entity flag;
1700         
1701         // initially clear items so they can be set as necessary later.
1702         self.items &~= (IT_RED_FLAG_CARRYING | IT_RED_FLAG_TAKEN | IT_RED_FLAG_LOST 
1703                 | IT_BLUE_FLAG_CARRYING | IT_BLUE_FLAG_TAKEN | IT_BLUE_FLAG_LOST | IT_CTF_SHIELDED);
1704
1705         // scan through all the flags and notify the client about them 
1706         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1707         {
1708                 switch(flag.ctf_status)
1709                 {
1710                         case FLAG_PASSING:
1711                         case FLAG_CARRY:
1712                         {
1713                                 if((flag.owner == self) || (flag.pass_sender == self))
1714                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_CARRYING : IT_BLUE_FLAG_CARRYING); // carrying: self is currently carrying the flag
1715                                 else 
1716                                         self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_TAKEN : IT_BLUE_FLAG_TAKEN); // taken: someone on self's team is carrying the flag
1717                                 break;
1718                         }
1719                         case FLAG_DROPPED:
1720                         {
1721                                 self.items |= ((flag.items & IT_KEY2) ? IT_RED_FLAG_LOST : IT_BLUE_FLAG_LOST); // lost: the flag is dropped somewhere on the map
1722                                 break;
1723                         }
1724                 }
1725         }
1726         
1727         // item for stopping players from capturing the flag too often
1728         if(self.ctf_captureshielded)
1729                 self.items |= IT_CTF_SHIELDED;
1730         
1731         // update the health of the flag carrier waypointsprite
1732         if(self.wps_flagcarrier) 
1733                 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1734         
1735         return FALSE;
1736 }
1737
1738 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1739 {
1740         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1741         {
1742                 if(frag_target == frag_attacker) // damage done to yourself
1743                 {
1744                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1745                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1746                 }
1747                 else // damage done to everyone else
1748                 {
1749                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1750                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1751                 }
1752         }
1753         else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1754         {
1755                 if(autocvar_g_ctf_flagcarrier_auto_helpme_when_damaged > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent)))
1756                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1757         }
1758         return FALSE;
1759 }
1760
1761 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1762 {
1763         if((frag_attacker != frag_target) && (frag_attacker.classname == "player") && (frag_target.flagcarried))
1764         {
1765                 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1766                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1767         }
1768                                 
1769         if(frag_target.flagcarried)
1770                 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1771                 
1772         return FALSE;
1773 }
1774
1775 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1776 {
1777         frag_score = 0;
1778         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1779 }
1780
1781 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1782 {
1783         entity flag; // temporary entity for the search method
1784         
1785         if(self.flagcarried)
1786                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1787         
1788         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1789         {
1790                 if(flag.pass_sender == self) { flag.pass_sender = world; }
1791                 if(flag.pass_target == self) { flag.pass_target = world; }
1792                 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1793         }
1794                 
1795         return FALSE;
1796 }
1797
1798 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1799 {
1800         if(self.flagcarried) 
1801         if(!autocvar_g_ctf_portalteleport)
1802                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1803
1804         return FALSE;
1805 }
1806
1807 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1808 {
1809         if(MUTATOR_RETURNVALUE || gameover) { return FALSE; }
1810         
1811         entity player = self;
1812
1813         if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1814         {
1815                 // pass the flag to a team mate
1816                 if(autocvar_g_ctf_pass)
1817                 {
1818                         entity head, closest_target;
1819                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1820                         
1821                         while(head) // find the closest acceptable target to pass to
1822                         {
1823                                 if(head.classname == "player" && head.deadflag == DEAD_NO)
1824                                 if(head != player && !IsDifferentTeam(head, player))
1825                                 if(!head.speedrunning && !head.vehicle)
1826                                 {
1827                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc) 
1828                                         vector head_center = WarpZone_UnTransformOrigin(head, PLAYER_CENTER(head));
1829                                         vector passer_center = PLAYER_CENTER(player);
1830                                         
1831                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1832                                         {
1833                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried) 
1834                                                 { 
1835                                                         if(clienttype(head) == CLIENTTYPE_BOT)
1836                                                         {
1837                                                                 centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1838                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
1839                                                         }
1840                                                         else
1841                                                         {
1842                                                                 centerprint(head, strcat(player.netname, " requests you to pass the ", head.flagcarried.netname)); 
1843                                                                 centerprint(player, strcat("Requesting ", head.netname, " to pass you the ", head.flagcarried.netname)); 
1844                                                         }
1845                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait; 
1846                                                         return TRUE; 
1847                                                 }
1848                                                 else if(player.flagcarried)
1849                                                 {
1850                                                         if(closest_target)
1851                                                         {
1852                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, PLAYER_CENTER(closest_target));
1853                                                                 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1854                                                                         { closest_target = head; }
1855                                                         }
1856                                                         else { closest_target = head; }
1857                                                 }
1858                                         }
1859                                 }
1860                                 head = head.chain;
1861                         }
1862                         
1863                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1864                 }
1865                 
1866                 // throw the flag in front of you
1867                 if(autocvar_g_ctf_throw && player.flagcarried)
1868                         { ctf_Handle_Throw(player, world, DROP_THROW); return TRUE; }
1869         }
1870                 
1871         return FALSE;
1872 }
1873
1874 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
1875 {
1876         if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
1877         {
1878                 WaypointSprite_HelpMePing(self.wps_flagcarrier);
1879         } 
1880         else // create a normal help me waypointsprite
1881         {
1882                 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');
1883                 WaypointSprite_Ping(self.wps_helpme);
1884         }
1885
1886         return TRUE;
1887 }
1888
1889 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
1890 {
1891         if(vh_player.flagcarried)
1892         {
1893                 if(!autocvar_g_ctf_allow_vehicle_carry)
1894                 {
1895                         ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
1896                 }
1897                 else
1898                 {            
1899                         setattachment(vh_player.flagcarried, vh_vehicle, ""); 
1900                         setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
1901                         vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
1902                         //vh_player.flagcarried.angles = '0 0 0';       
1903                 }
1904                 return TRUE;
1905         }
1906                 
1907         return FALSE;
1908 }
1909
1910 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
1911 {
1912         if(vh_player.flagcarried)
1913         {
1914                 setattachment(vh_player.flagcarried, vh_player, ""); 
1915                 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
1916                 vh_player.flagcarried.scale = FLAG_SCALE;
1917                 vh_player.flagcarried.angles = '0 0 0';
1918                 return TRUE;
1919         }
1920
1921         return FALSE;
1922 }
1923
1924 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
1925 {
1926         if(self.flagcarried)
1927         {
1928                 bprint("The ", self.flagcarried.netname, " was returned to base by its carrier\n");
1929                 ctf_RespawnFlag(self);
1930                 return TRUE;
1931         }
1932         
1933         return FALSE;
1934 }
1935
1936 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
1937 {
1938         entity flag; // temporary entity for the search method
1939         
1940         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1941         {
1942                 switch(flag.ctf_status)
1943                 {
1944                         case FLAG_DROPPED:
1945                         case FLAG_PASSING:
1946                         {
1947                                 // lock the flag, game is over
1948                                 flag.movetype = MOVETYPE_NONE;
1949                                 flag.takedamage = DAMAGE_NO;
1950                                 flag.solid = SOLID_NOT;
1951                                 flag.nextthink = FALSE; // stop thinking
1952                                 
1953                                 print("stopping the ", flag.netname, " from moving.\n");
1954                                 break;
1955                         }
1956                         
1957                         default:
1958                         case FLAG_BASE:
1959                         case FLAG_CARRY:
1960                         {
1961                                 // do nothing for these flags
1962                                 break;
1963                         }
1964                 }
1965         }
1966         
1967         return FALSE;
1968 }
1969
1970 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
1971 {
1972         havocbot_ctf_reset_role(self);
1973         return TRUE;
1974 }
1975
1976
1977 // ==========
1978 // Spawnfuncs
1979 // ==========
1980
1981 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
1982 CTF Starting point for a player in team one (Red).
1983 Keys: "angle" viewing angle when spawning. */
1984 void spawnfunc_info_player_team1()
1985 {
1986         if(g_assault) { remove(self); return; }
1987         
1988         self.team = COLOR_TEAM1; // red
1989         spawnfunc_info_player_deathmatch();
1990 }
1991
1992
1993 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
1994 CTF Starting point for a player in team two (Blue).
1995 Keys: "angle" viewing angle when spawning. */
1996 void spawnfunc_info_player_team2()
1997 {
1998         if(g_assault) { remove(self); return; }
1999         
2000         self.team = COLOR_TEAM2; // blue
2001         spawnfunc_info_player_deathmatch();
2002 }
2003
2004 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2005 CTF Starting point for a player in team three (Yellow).
2006 Keys: "angle" viewing angle when spawning. */
2007 void spawnfunc_info_player_team3()
2008 {
2009         if(g_assault) { remove(self); return; }
2010         
2011         self.team = COLOR_TEAM3; // yellow
2012         spawnfunc_info_player_deathmatch();
2013 }
2014
2015
2016 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2017 CTF Starting point for a player in team four (Purple).
2018 Keys: "angle" viewing angle when spawning. */
2019 void spawnfunc_info_player_team4()
2020 {
2021         if(g_assault) { remove(self); return; }
2022         
2023         self.team = COLOR_TEAM4; // purple
2024         spawnfunc_info_player_deathmatch();
2025 }
2026
2027 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2028 CTF flag for team one (Red).
2029 Keys: 
2030 "angle" Angle the flag will point (minus 90 degrees)... 
2031 "model" model to use, note this needs red and blue as skins 0 and 1...
2032 "noise" sound played when flag is picked up...
2033 "noise1" sound played when flag is returned by a teammate...
2034 "noise2" sound played when flag is captured...
2035 "noise3" sound played when flag is lost in the field and respawns itself... 
2036 "noise4" sound played when flag is dropped by a player...
2037 "noise5" sound played when flag touches the ground... */
2038 void spawnfunc_item_flag_team1()
2039 {
2040         if(!g_ctf) { remove(self); return; }
2041
2042         ctf_FlagSetup(1, self); // 1 = red
2043 }
2044
2045 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2046 CTF flag for team two (Blue).
2047 Keys: 
2048 "angle" Angle the flag will point (minus 90 degrees)... 
2049 "model" model to use, note this needs red and blue as skins 0 and 1...
2050 "noise" sound played when flag is picked up...
2051 "noise1" sound played when flag is returned by a teammate...
2052 "noise2" sound played when flag is captured...
2053 "noise3" sound played when flag is lost in the field and respawns itself... 
2054 "noise4" sound played when flag is dropped by a player...
2055 "noise5" sound played when flag touches the ground... */
2056 void spawnfunc_item_flag_team2()
2057 {
2058         if(!g_ctf) { remove(self); return; }
2059
2060         ctf_FlagSetup(0, self); // the 0 is misleading, but -- 0 = blue.
2061 }
2062
2063 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2064 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2065 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.
2066 Keys:
2067 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2068 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2069 void spawnfunc_ctf_team()
2070 {
2071         if(!g_ctf) { remove(self); return; }
2072         
2073         self.classname = "ctf_team";
2074         self.team = self.cnt + 1;
2075 }
2076
2077 // compatibility for quake maps
2078 void spawnfunc_team_CTF_redflag()    { spawnfunc_item_flag_team1();    }
2079 void spawnfunc_team_CTF_blueflag()   { spawnfunc_item_flag_team2();    }
2080 void spawnfunc_team_CTF_redplayer()  { spawnfunc_info_player_team1();  }
2081 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2();  }
2082 void spawnfunc_team_CTF_redspawn()   { spawnfunc_info_player_team1();  }
2083 void spawnfunc_team_CTF_bluespawn()  { spawnfunc_info_player_team2();  }
2084
2085
2086 // ==============
2087 // Initialization
2088 // ==============
2089
2090 // scoreboard setup
2091 void ctf_ScoreRules()
2092 {
2093         ScoreRules_basics(2, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
2094         ScoreInfo_SetLabel_TeamScore  (ST_CTF_CAPS,     "caps",      SFL_SORT_PRIO_PRIMARY);
2095         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS,     "caps",      SFL_SORT_PRIO_SECONDARY);
2096         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME,  "captime",   SFL_LOWER_IS_BETTER | SFL_TIME);
2097         ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS,  "pickups",   0);
2098         ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS,  "fckills",   0);
2099         ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS,  "returns",   0);
2100         ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS,    "drops",     SFL_LOWER_IS_BETTER);
2101         ScoreRules_basics_end();
2102 }
2103
2104 // code from here on is just to support maps that don't have flag and team entities
2105 void ctf_SpawnTeam (string teamname, float teamcolor)
2106 {
2107         entity oldself;
2108         oldself = self;
2109         self = spawn();
2110         self.classname = "ctf_team";
2111         self.netname = teamname;
2112         self.cnt = teamcolor;
2113
2114         spawnfunc_ctf_team();
2115
2116         self = oldself;
2117 }
2118
2119 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2120 {
2121         // if no teams are found, spawn defaults
2122         if(find(world, classname, "ctf_team") == world)
2123         {
2124                 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2125                 ctf_SpawnTeam("Red", COLOR_TEAM1 - 1);
2126                 ctf_SpawnTeam("Blue", COLOR_TEAM2 - 1);
2127         }
2128         
2129         ctf_ScoreRules();
2130 }
2131
2132 void ctf_Initialize()
2133 {
2134         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2135
2136         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2137         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2138         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2139         
2140         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2141 }
2142
2143
2144 MUTATOR_DEFINITION(gamemode_ctf)
2145 {
2146         MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2147         MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2148         MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2149         MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2150         MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2151         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2152         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2153         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2154         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2155         MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2156         MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2157         MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2158         MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2159         MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY);
2160         
2161         MUTATOR_ONADD
2162         {
2163                 if(time > 1) // game loads at time 1
2164                         error("This is a game type and it cannot be added at runtime.");
2165                 g_ctf = 1;
2166                 ctf_Initialize();
2167         }
2168
2169         MUTATOR_ONREMOVE
2170         {
2171                 g_ctf = 0;
2172                 error("This is a game type and it cannot be removed at runtime.");
2173         }
2174
2175         return 0;
2176 }