]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_ctf.qc
Merge branch 'master' into Mario/ctf_updates
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_ctf.qc
1 // ================================================================
2 //  Official capture the flag game mode coding, reworked by Samual
3 //  Last updated: September, 2012
4 // ================================================================
5
6 void ctf_FakeTimeLimit(entity e, float t)
7 {
8         msg_entity = e;
9         WriteByte(MSG_ONE, 3); // svc_updatestat
10         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
11         if(t < 0)
12                 WriteCoord(MSG_ONE, autocvar_timelimit);
13         else
14                 WriteCoord(MSG_ONE, (t + 1) / 60);
15 }
16
17 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
18 {
19         if(autocvar_sv_eventlog)
20                 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : "")));
21                 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
22 }
23
24 void ctf_CaptureRecord(entity flag, entity player)
25 {
26         float cap_record = ctf_captimerecord;
27         float cap_time = (time - flag.ctf_pickuptime);
28         string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
29
30         // notify about shit
31         if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
32         else if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
33         else if(cap_time < cap_record) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
34         else { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
35
36         // write that shit in the database
37         if(!ctf_oneflag) // but not in 1-flag mode
38         if((!ctf_captimerecord) || (cap_time < cap_record))
39         {
40                 ctf_captimerecord = cap_time;
41                 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
42                 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
43                 write_recordmarker(player, (time - cap_time), cap_time);
44         }
45 }
46
47 void ctf_FlagcarrierWaypoints(entity player)
48 {
49         WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
50         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON) * 2);
51         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
52         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
53 }
54
55 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
56 {
57         float current_distance = vlen((('1 0 0' * to.x) + ('0 1 0' * to.y)) - (('1 0 0' * from.x) + ('0 1 0' * from.y))); // for the sake of this check, exclude Z axis
58         float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
59         float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
60         //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
61
62         vector targpos;
63         if(current_height) // make sure we can actually do this arcing path
64         {
65                 targpos = (to + ('0 0 1' * current_height));
66                 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
67                 if(trace_fraction < 1)
68                 {
69                         //print("normal arc line failed, trying to find new pos...");
70                         WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
71                         targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
72                         WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
73                         if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
74                         /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
75                 }
76         }
77         else { targpos = to; }
78
79         //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
80
81         vector desired_direction = normalize(targpos - from);
82         if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
83         else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
84 }
85
86 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
87 {
88         if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
89         {
90                 // directional tracing only
91                 float spreadlimit;
92                 makevectors(passer_angle);
93
94                 // find the closest point on the enemy to the center of the attack
95                 float 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                 if(ctf_oneflag)
1504                         havocbot_goalrating_ctf_enemybase(50000);
1505                 else
1506                         havocbot_goalrating_ctf_ourbase(50000);
1507
1508                 if(self.health<100)
1509                         havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1510
1511                 navigation_goalrating_end();
1512
1513                 if (self.navigation_hasgoals)
1514                         self.havocbot_cantfindflag = time + 10;
1515                 else if (time > self.havocbot_cantfindflag)
1516                 {
1517                         // Can't navigate to my own base, suicide!
1518                         // TODO: drop it and wander around
1519                         Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1520                         return;
1521                 }
1522         }
1523 }
1524
1525 void havocbot_role_ctf_escort()
1526 {
1527         entity mf, ef;
1528
1529         if(self.deadflag != DEAD_NO)
1530         {
1531                 havocbot_ctf_reset_role(self);
1532                 return;
1533         }
1534
1535         if (self.flagcarried)
1536         {
1537                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1538                 return;
1539         }
1540
1541         // If enemy flag is back on the base switch to previous role
1542         ef = havocbot_ctf_find_enemy_flag(self);
1543         if(ef.ctf_status==FLAG_BASE)
1544         {
1545                 self.havocbot_role = self.havocbot_previous_role;
1546                 self.havocbot_role_timeout = 0;
1547                 return;
1548         }
1549
1550         // If the flag carrier reached the base switch to defense
1551         mf = havocbot_ctf_find_flag(self);
1552         if(mf.ctf_status!=FLAG_BASE)
1553         if(vlen(ef.origin - mf.dropped_origin) < 300)
1554         {
1555                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1556                 return;
1557         }
1558
1559         // Set the role timeout if necessary
1560         if (!self.havocbot_role_timeout)
1561         {
1562                 self.havocbot_role_timeout = time + random() * 30 + 60;
1563         }
1564
1565         // If nothing happened just switch to previous role
1566         if (time > self.havocbot_role_timeout)
1567         {
1568                 self.havocbot_role = self.havocbot_previous_role;
1569                 self.havocbot_role_timeout = 0;
1570                 return;
1571         }
1572
1573         // Chase the flag carrier
1574         if (self.bot_strategytime < time)
1575         {
1576                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1577                 navigation_goalrating_start();
1578                 havocbot_goalrating_ctf_enemyflag(30000);
1579                 havocbot_goalrating_ctf_ourstolenflag(40000);
1580                 havocbot_goalrating_items(10000, self.origin, 10000);
1581                 navigation_goalrating_end();
1582         }
1583 }
1584
1585 void havocbot_role_ctf_offense()
1586 {
1587         entity mf, ef;
1588         vector pos;
1589
1590         if(self.deadflag != DEAD_NO)
1591         {
1592                 havocbot_ctf_reset_role(self);
1593                 return;
1594         }
1595
1596         if (self.flagcarried)
1597         {
1598                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1599                 return;
1600         }
1601
1602         // Check flags
1603         mf = havocbot_ctf_find_flag(self);
1604         ef = havocbot_ctf_find_enemy_flag(self);
1605
1606         // Own flag stolen
1607         if(mf.ctf_status!=FLAG_BASE)
1608         {
1609                 if(mf.tag_entity)
1610                         pos = mf.tag_entity.origin;
1611                 else
1612                         pos = mf.origin;
1613
1614                 // Try to get it if closer than the enemy base
1615                 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1616                 {
1617                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1618                         return;
1619                 }
1620         }
1621
1622         // Escort flag carrier
1623         if(ef.ctf_status!=FLAG_BASE)
1624         {
1625                 if(ef.tag_entity)
1626                         pos = ef.tag_entity.origin;
1627                 else
1628                         pos = ef.origin;
1629
1630                 if(vlen(pos-mf.dropped_origin)>700)
1631                 {
1632                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1633                         return;
1634                 }
1635         }
1636
1637         // About to fail, switch to middlefield
1638         if(self.health<50)
1639         {
1640                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1641                 return;
1642         }
1643
1644         // Set the role timeout if necessary
1645         if (!self.havocbot_role_timeout)
1646                 self.havocbot_role_timeout = time + 120;
1647
1648         if (time > self.havocbot_role_timeout)
1649         {
1650                 havocbot_ctf_reset_role(self);
1651                 return;
1652         }
1653
1654         if (self.bot_strategytime < time)
1655         {
1656                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1657                 navigation_goalrating_start();
1658                 havocbot_goalrating_ctf_ourstolenflag(50000);
1659                 havocbot_goalrating_ctf_enemybase(20000);
1660                 havocbot_goalrating_items(5000, self.origin, 1000);
1661                 havocbot_goalrating_items(1000, self.origin, 10000);
1662                 navigation_goalrating_end();
1663         }
1664 }
1665
1666 // Retriever (temporary role):
1667 void havocbot_role_ctf_retriever()
1668 {
1669         entity mf;
1670
1671         if(self.deadflag != DEAD_NO)
1672         {
1673                 havocbot_ctf_reset_role(self);
1674                 return;
1675         }
1676
1677         if (self.flagcarried)
1678         {
1679                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1680                 return;
1681         }
1682
1683         // If flag is back on the base switch to previous role
1684         mf = havocbot_ctf_find_flag(self);
1685         if(mf.ctf_status==FLAG_BASE)
1686         {
1687                 havocbot_ctf_reset_role(self);
1688                 return;
1689         }
1690
1691         if (!self.havocbot_role_timeout)
1692                 self.havocbot_role_timeout = time + 20;
1693
1694         if (time > self.havocbot_role_timeout)
1695         {
1696                 havocbot_ctf_reset_role(self);
1697                 return;
1698         }
1699
1700         if (self.bot_strategytime < time)
1701         {
1702                 float rt_radius;
1703                 rt_radius = 10000;
1704
1705                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1706                 navigation_goalrating_start();
1707                 havocbot_goalrating_ctf_ourstolenflag(50000);
1708                 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1709                 havocbot_goalrating_ctf_enemybase(30000);
1710                 havocbot_goalrating_items(500, self.origin, rt_radius);
1711                 navigation_goalrating_end();
1712         }
1713 }
1714
1715 void havocbot_role_ctf_middle()
1716 {
1717         entity mf;
1718
1719         if(self.deadflag != DEAD_NO)
1720         {
1721                 havocbot_ctf_reset_role(self);
1722                 return;
1723         }
1724
1725         if (self.flagcarried)
1726         {
1727                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1728                 return;
1729         }
1730
1731         mf = havocbot_ctf_find_flag(self);
1732         if(mf.ctf_status!=FLAG_BASE)
1733         {
1734                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1735                 return;
1736         }
1737
1738         if (!self.havocbot_role_timeout)
1739                 self.havocbot_role_timeout = time + 10;
1740
1741         if (time > self.havocbot_role_timeout)
1742         {
1743                 havocbot_ctf_reset_role(self);
1744                 return;
1745         }
1746
1747         if (self.bot_strategytime < time)
1748         {
1749                 vector org;
1750
1751                 org = havocbot_ctf_middlepoint;
1752                 org.z = self.origin.z;
1753
1754                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1755                 navigation_goalrating_start();
1756                 havocbot_goalrating_ctf_ourstolenflag(50000);
1757                 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1758                 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1759                 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1760                 havocbot_goalrating_items(2500, self.origin, 10000);
1761                 havocbot_goalrating_ctf_enemybase(2500);
1762                 navigation_goalrating_end();
1763         }
1764 }
1765
1766 void havocbot_role_ctf_defense()
1767 {
1768         entity mf;
1769
1770         if(self.deadflag != DEAD_NO)
1771         {
1772                 havocbot_ctf_reset_role(self);
1773                 return;
1774         }
1775
1776         if (self.flagcarried)
1777         {
1778                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1779                 return;
1780         }
1781
1782         // If own flag was captured
1783         mf = havocbot_ctf_find_flag(self);
1784         if(mf.ctf_status!=FLAG_BASE)
1785         {
1786                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1787                 return;
1788         }
1789
1790         if (!self.havocbot_role_timeout)
1791                 self.havocbot_role_timeout = time + 30;
1792
1793         if (time > self.havocbot_role_timeout)
1794         {
1795                 havocbot_ctf_reset_role(self);
1796                 return;
1797         }
1798         if (self.bot_strategytime < time)
1799         {
1800                 float mp_radius;
1801                 vector org;
1802
1803                 org = mf.dropped_origin;
1804                 mp_radius = havocbot_ctf_middlepoint_radius;
1805
1806                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1807                 navigation_goalrating_start();
1808
1809                 // if enemies are closer to our base, go there
1810                 entity head, closestplayer = world;
1811                 float distance, bestdistance = 10000;
1812                 FOR_EACH_PLAYER(head)
1813                 {
1814                         if(head.deadflag!=DEAD_NO)
1815                                 continue;
1816
1817                         distance = vlen(org - head.origin);
1818                         if(distance<bestdistance)
1819                         {
1820                                 closestplayer = head;
1821                                 bestdistance = distance;
1822                         }
1823                 }
1824
1825                 if(closestplayer)
1826                 if(DIFF_TEAM(closestplayer, self))
1827                 if(vlen(org - self.origin)>1000)
1828                 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1829                         havocbot_goalrating_ctf_ourbase(30000);
1830
1831                 havocbot_goalrating_ctf_ourstolenflag(20000);
1832                 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1833                 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1834                 havocbot_goalrating_items(10000, org, mp_radius);
1835                 havocbot_goalrating_items(5000, self.origin, 10000);
1836                 navigation_goalrating_end();
1837         }
1838 }
1839
1840 void havocbot_role_ctf_setrole(entity bot, int role)
1841 {
1842         dprint(strcat(bot.netname," switched to "));
1843         switch(role)
1844         {
1845                 case HAVOCBOT_CTF_ROLE_CARRIER:
1846                         dprint("carrier");
1847                         bot.havocbot_role = havocbot_role_ctf_carrier;
1848                         bot.havocbot_role_timeout = 0;
1849                         bot.havocbot_cantfindflag = time + 10;
1850                         bot.bot_strategytime = 0;
1851                         break;
1852                 case HAVOCBOT_CTF_ROLE_DEFENSE:
1853                         dprint("defense");
1854                         bot.havocbot_role = havocbot_role_ctf_defense;
1855                         bot.havocbot_role_timeout = 0;
1856                         break;
1857                 case HAVOCBOT_CTF_ROLE_MIDDLE:
1858                         dprint("middle");
1859                         bot.havocbot_role = havocbot_role_ctf_middle;
1860                         bot.havocbot_role_timeout = 0;
1861                         break;
1862                 case HAVOCBOT_CTF_ROLE_OFFENSE:
1863                         dprint("offense");
1864                         bot.havocbot_role = havocbot_role_ctf_offense;
1865                         bot.havocbot_role_timeout = 0;
1866                         break;
1867                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1868                         dprint("retriever");
1869                         bot.havocbot_previous_role = bot.havocbot_role;
1870                         bot.havocbot_role = havocbot_role_ctf_retriever;
1871                         bot.havocbot_role_timeout = time + 10;
1872                         bot.bot_strategytime = 0;
1873                         break;
1874                 case HAVOCBOT_CTF_ROLE_ESCORT:
1875                         dprint("escort");
1876                         bot.havocbot_previous_role = bot.havocbot_role;
1877                         bot.havocbot_role = havocbot_role_ctf_escort;
1878                         bot.havocbot_role_timeout = time + 30;
1879                         bot.bot_strategytime = 0;
1880                         break;
1881         }
1882         dprint("\n");
1883 }
1884
1885
1886 // ==============
1887 // Hook Functions
1888 // ==============
1889
1890 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1891 {
1892         entity flag;
1893         int t = 0, t2 = 0, t3 = 0;
1894
1895         // initially clear items so they can be set as necessary later.
1896         self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING          | CTF_RED_FLAG_TAKEN            | CTF_RED_FLAG_LOST 
1897                                                    | CTF_BLUE_FLAG_CARRYING             | CTF_BLUE_FLAG_TAKEN           | CTF_BLUE_FLAG_LOST
1898                                                    | CTF_YELLOW_FLAG_CARRYING   | CTF_YELLOW_FLAG_TAKEN         | CTF_YELLOW_FLAG_LOST
1899                                                    | CTF_PINK_FLAG_CARRYING     | CTF_PINK_FLAG_TAKEN           | CTF_PINK_FLAG_LOST
1900                                                    | CTF_NEUTRAL_FLAG_CARRYING  | CTF_NEUTRAL_FLAG_TAKEN        | CTF_NEUTRAL_FLAG_LOST
1901                                                    | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
1902
1903         // scan through all the flags and notify the client about them
1904         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1905         {
1906                 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING;                t2 = CTF_RED_FLAG_TAKEN;                t3 = CTF_RED_FLAG_LOST; }
1907                 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING;               t2 = CTF_BLUE_FLAG_TAKEN;               t3 = CTF_BLUE_FLAG_LOST; }
1908                 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING;     t2 = CTF_YELLOW_FLAG_TAKEN;             t3 = CTF_YELLOW_FLAG_LOST; }
1909                 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING;               t2 = CTF_PINK_FLAG_TAKEN;               t3 = CTF_PINK_FLAG_LOST; }
1910                 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; }
1911
1912                 switch(flag.ctf_status)
1913                 {
1914                         case FLAG_PASSING:
1915                         case FLAG_CARRY:
1916                         {
1917                                 if((flag.owner == self) || (flag.pass_sender == self))
1918                                         self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
1919                                 else 
1920                                         self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
1921                                 break;
1922                         }
1923                         case FLAG_DROPPED:
1924                         {
1925                                 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
1926                                 break;
1927                         }
1928                 }
1929         }
1930
1931         // item for stopping players from capturing the flag too often
1932         if(self.ctf_captureshielded)
1933                 self.ctf_flagstatus |= CTF_SHIELDED;
1934
1935         // update the health of the flag carrier waypointsprite
1936         if(self.wps_flagcarrier)
1937                 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
1938
1939         return false;
1940 }
1941
1942 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1943 {
1944         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1945         {
1946                 if(frag_target == frag_attacker) // damage done to yourself
1947                 {
1948                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1949                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1950                 }
1951                 else // damage done to everyone else
1952                 {
1953                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1954                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1955                 }
1956         }
1957         else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
1958         {
1959                 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)))
1960                 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1961                 {
1962                         frag_target.wps_helpme_time = time;
1963                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1964                 }
1965                 // todo: add notification for when flag carrier needs help?
1966         }
1967         return false;
1968 }
1969
1970 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1971 {
1972         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1973         {
1974                 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1975                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1976         }
1977
1978         if(frag_target.flagcarried)
1979         {
1980                 entity tmp_entity = frag_target.flagcarried;
1981                 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
1982                 tmp_entity.ctf_dropper = world;
1983         }
1984
1985         return false;
1986 }
1987
1988 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1989 {
1990         frag_score = 0;
1991         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1992 }
1993
1994 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1995 {
1996         entity flag; // temporary entity for the search method
1997
1998         if(self.flagcarried)
1999                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2000
2001         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2002         {
2003                 if(flag.pass_sender == self) { flag.pass_sender = world; }
2004                 if(flag.pass_target == self) { flag.pass_target = world; }
2005                 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
2006         }
2007
2008         return false;
2009 }
2010
2011 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
2012 {
2013         if(self.flagcarried)
2014         if(!autocvar_g_ctf_portalteleport)
2015                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2016
2017         return false;
2018 }
2019
2020 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
2021 {
2022         if(MUTATOR_RETURNVALUE || gameover) { return false; }
2023
2024         entity player = self;
2025
2026         if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2027         {
2028                 // pass the flag to a team mate
2029                 if(autocvar_g_ctf_pass)
2030                 {
2031                         entity head, closest_target = world;
2032                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2033
2034                         while(head) // find the closest acceptable target to pass to
2035                         {
2036                                 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
2037                                 if(head != player && SAME_TEAM(head, player))
2038                                 if(!head.speedrunning && !head.vehicle)
2039                                 {
2040                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2041                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2042                                         vector passer_center = CENTER_OR_VIEWOFS(player);
2043
2044                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2045                                         {
2046                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2047                                                 {
2048                                                         if(IS_BOT_CLIENT(head))
2049                                                         {
2050                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2051                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
2052                                                         }
2053                                                         else
2054                                                         {
2055                                                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2056                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2057                                                         }
2058                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2059                                                         return true;
2060                                                 }
2061                                                 else if(player.flagcarried)
2062                                                 {
2063                                                         if(closest_target)
2064                                                         {
2065                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2066                                                                 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2067                                                                         { closest_target = head; }
2068                                                         }
2069                                                         else { closest_target = head; }
2070                                                 }
2071                                         }
2072                                 }
2073                                 head = head.chain;
2074                         }
2075
2076                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2077                 }
2078
2079                 // throw the flag in front of you
2080                 if(autocvar_g_ctf_throw && player.flagcarried)
2081                 {
2082                         if(player.throw_count == -1)
2083                         {
2084                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2085                                 {
2086                                         player.throw_prevtime = time;
2087                                         player.throw_count = 1;
2088                                         ctf_Handle_Throw(player, world, DROP_THROW);
2089                                         return true;
2090                                 }
2091                                 else
2092                                 {
2093                                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2094                                         return false;
2095                                 }
2096                         }
2097                         else
2098                         {
2099                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2100                                 else { player.throw_count += 1; }
2101                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2102
2103                                 player.throw_prevtime = time;
2104                                 ctf_Handle_Throw(player, world, DROP_THROW);
2105                                 return true;
2106                         }
2107                 }
2108         }
2109
2110         return false;
2111 }
2112
2113 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
2114 {
2115         if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2116         {
2117                 self.wps_helpme_time = time;
2118                 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2119         }
2120         else // create a normal help me waypointsprite
2121         {
2122                 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');
2123                 WaypointSprite_Ping(self.wps_helpme);
2124         }
2125
2126         return true;
2127 }
2128
2129 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
2130 {
2131         if(vh_player.flagcarried)
2132         {
2133                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2134                 {
2135                         ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2136                 }
2137                 else
2138                 {
2139                         setattachment(vh_player.flagcarried, vh_vehicle, "");
2140                         setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2141                         vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2142                         //vh_player.flagcarried.angles = '0 0 0';
2143                 }
2144                 return true;
2145         }
2146
2147         return false;
2148 }
2149
2150 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
2151 {
2152         if(vh_player.flagcarried)
2153         {
2154                 setattachment(vh_player.flagcarried, vh_player, "");
2155                 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2156                 vh_player.flagcarried.scale = FLAG_SCALE;
2157                 vh_player.flagcarried.angles = '0 0 0';
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
2394 // ==============
2395 // Initialization
2396 // ==============
2397
2398 // scoreboard setup
2399 void ctf_ScoreRules(int teams)
2400 {
2401         CheckAllowedTeams(world);
2402         ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2403         ScoreInfo_SetLabel_TeamScore  (ST_CTF_CAPS,     "caps",      SFL_SORT_PRIO_PRIMARY);
2404         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS,     "caps",      SFL_SORT_PRIO_SECONDARY);
2405         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME,  "captime",   SFL_LOWER_IS_BETTER | SFL_TIME);
2406         ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS,  "pickups",   0);
2407         ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS,  "fckills",   0);
2408         ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS,  "returns",   0);
2409         ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS,    "drops",     SFL_LOWER_IS_BETTER);
2410         ScoreRules_basics_end();
2411 }
2412
2413 // code from here on is just to support maps that don't have flag and team entities
2414 void ctf_SpawnTeam (string teamname, int teamcolor)
2415 {
2416         entity oldself;
2417         oldself = self;
2418         self = spawn();
2419         self.classname = "ctf_team";
2420         self.netname = teamname;
2421         self.cnt = teamcolor;
2422
2423         spawnfunc_ctf_team();
2424
2425         self = oldself;
2426 }
2427
2428 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2429 {
2430         ctf_teams = 2;
2431
2432         entity tmp_entity;
2433         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2434         {
2435                 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2436                 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2437                 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2438         }
2439
2440         ctf_teams = bound(2, ctf_teams, 4);
2441
2442         // if no teams are found, spawn defaults
2443         if(find(world, classname, "ctf_team") == world)
2444         {
2445                 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2446                 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2447                 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2448                 if(ctf_teams >= 3)
2449                         ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2450                 if(ctf_teams >= 4)
2451                         ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2452         }
2453
2454         ctf_ScoreRules(ctf_teams);
2455 }
2456
2457 void ctf_Initialize()
2458 {
2459         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2460
2461         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2462         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2463         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2464
2465         addstat(STAT_CTF_FLAGSTATUS, AS_INT, ctf_flagstatus);
2466
2467         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2468 }
2469
2470
2471 MUTATOR_DEFINITION(gamemode_ctf)
2472 {
2473         MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2474         MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2475         MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2476         MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2477         MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2478         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2479         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2480         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2481         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2482         MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2483         MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2484         MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2485         MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2486         MUTATOR_HOOK(HavocBot_ChooseRole, ctf_BotRoles, CBC_ORDER_ANY);
2487         MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2488         MUTATOR_HOOK(SpectateCopy, ctf_SpectateCopy, CBC_ORDER_ANY);
2489
2490         MUTATOR_ONADD
2491         {
2492                 if(time > 1) // game loads at time 1
2493                         error("This is a game type and it cannot be added at runtime.");
2494                 ctf_Initialize();
2495         }
2496
2497         MUTATOR_ONROLLBACK_OR_REMOVE
2498         {
2499                 // we actually cannot roll back ctf_Initialize here
2500                 // BUT: we don't need to! If this gets called, adding always
2501                 // succeeds.
2502         }
2503
2504         MUTATOR_ONREMOVE
2505         {
2506                 print("This is a game type and it cannot be removed at runtime.");
2507                 return -1;
2508         }
2509
2510         return 0;
2511 }