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