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