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