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