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