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