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