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