]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/ctf/sv_ctf.qc
Merge branch 'master' into z411/bai-server
[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         Give_Medal(player, CAPTURE);
616         
617         // announcer
618         AnnounceScores(player.team);
619
620         // effects
621         Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
622 #if 0
623         shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
624 #endif
625
626         // other
627         if(capturetype == CAPTURE_NORMAL)
628         {
629                 WaypointSprite_Kill(player.wps_flagcarrier);
630                 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
631
632                 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
633                 {
634                         GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist));
635                         Give_Medal(enemy_flag.ctf_dropper, ASSIST);
636                 }
637         }
638
639         flag.enemy = toucher;
640
641         // reset the flag
642         player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
643         ctf_RespawnFlag(enemy_flag);
644 }
645
646 void ctf_Handle_Return(entity flag, entity player)
647 {
648         // messages and sounds
649         if(IS_MONSTER(player))
650         {
651                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
652         }
653         else if(flag.team)
654         {
655                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
656                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
657                 
658                 FOREACH_CLIENT(IS_PLAYER(it), {
659                         if(it.team == flag.team)
660                                 Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_CTF_RETURN_TEAM);
661                         else
662                                 Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_CTF_RETURN_ENEMY);
663                 });
664                 Send_Notification(NOTIF_ALL_SPEC, NULL, MSG_ANNCE, APP_TEAM_NUM(flag.team, ANNCE_CTF_RETURN));
665         }
666         _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
667         ctf_EventLog("return", flag.team, player);
668
669         // scoring
670         if(IS_PLAYER(player))
671         {
672                 GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
673                 GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
674
675                 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
676         }
677
678         TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
679
680         if(flag.ctf_dropper)
681         {
682                 GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
683                 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
684                 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
685         }
686
687         // other
688         if(player.flagcarried == flag)
689                 WaypointSprite_Kill(player.wps_flagcarrier);
690
691         flag.enemy = player;
692
693         // reset the flag
694         ctf_RespawnFlag(flag);
695 }
696
697 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
698 {
699         // declarations
700         float pickup_dropped_score; // used to calculate dropped pickup score
701
702         // attach the flag to the player
703         flag.owner = player;
704         player.flagcarried = flag;
705         GameRules_scoring_vip(player, true);
706         if(player.vehicle)
707         {
708                 setattachment(flag, player.vehicle, "");
709                 setorigin(flag, VEHICLE_FLAG_OFFSET);
710                 flag.scale = VEHICLE_FLAG_SCALE;
711         }
712         else
713         {
714                 setattachment(flag, player, "");
715                 setorigin(flag, FLAG_CARRY_OFFSET);
716         }
717
718         // flag setup
719         set_movetype(flag, MOVETYPE_NONE);
720         flag.takedamage = DAMAGE_NO;
721         flag.solid = SOLID_NOT;
722         flag.angles = '0 0 0';
723         flag.ctf_status = FLAG_CARRY;
724
725         switch(pickuptype)
726         {
727                 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
728                 case PICKUP_DROPPED: SetResourceExplicit(flag, RES_HEALTH, flag.max_health); break; // reset health/return timelimit
729                 default: break;
730         }
731
732         // messages and sounds
733         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
734         Send_Notification(NOTIF_ALL_SPEC, NULL, MSG_ANNCE, APP_TEAM_NUM(flag.team, ANNCE_CTF_PICKUP));
735         
736         if(ctf_stalemate)
737                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
738         if(!flag.team)
739                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
740         else if(CTF_DIFFTEAM(player, flag))
741         {
742                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
743                 Send_Notification(NOTIF_ONE_ONLY, player, MSG_ANNCE, ANNCE_CTF_PICKUP_YOU);
744         }
745         else
746                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
747
748         Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
749         Send_Notification(NOTIF_TEAM_ONLY_EXCEPT, player, MSG_ANNCE, ANNCE_CTF_PICKUP_TEAM);
750
751         if(!flag.team)
752                 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); });
753
754         if(flag.team)
755                 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
756                         if(CTF_SAMETEAM(flag, it))
757                         {
758                                 if(SAME_TEAM(player, it)) {
759                                         Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
760                                         Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_CTF_PICKUP_TEAM);
761                                 } else {
762                                         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);
763                                         Send_Notification(NOTIF_ONE_ONLY, it, MSG_ANNCE, ANNCE_CTF_PICKUP_ENEMY);
764                                 }
765                         }
766                 });
767
768         _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
769
770         // scoring
771         GameRules_scoring_add(player, CTF_PICKUPS, 1);
772         nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
773         switch(pickuptype)
774         {
775                 case PICKUP_BASE:
776                 {
777                         GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
778                         ctf_EventLog("steal", flag.team, player);
779                         break;
780                 }
781
782                 case PICKUP_DROPPED:
783                 {
784                         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);
785                         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);
786                         LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
787                         GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
788                         ctf_EventLog("pickup", flag.team, player);
789                         break;
790                 }
791
792                 default: break;
793         }
794
795         // speedrunning
796         if(pickuptype == PICKUP_BASE)
797         {
798                 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
799                 if((player.speedrunning) && (ctf_captimerecord))
800                         ctf_FakeTimeLimit(player, time + ctf_captimerecord);
801         }
802
803         // effects
804         Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
805
806         // waypoints
807         if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
808         ctf_FlagcarrierWaypoints(player);
809         WaypointSprite_Ping(player.wps_flagcarrier);
810 }
811
812
813 // ===================
814 // Main Flag Functions
815 // ===================
816
817 void ctf_CheckFlagReturn(entity flag, int returntype)
818 {
819         if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
820         {
821                 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResource(flag, RES_HEALTH)); }
822
823                 if((GetResource(flag, RES_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
824                 {
825                         switch(returntype)
826                         {
827                                 case RETURN_DROPPED:
828                                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
829                                 case RETURN_DAMAGE:
830                                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
831                                 case RETURN_SPEEDRUN:
832                                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
833                                 case RETURN_NEEDKILL:
834                                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
835                                 default:
836                                 case RETURN_TIMEOUT:
837                                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
838                         }
839                         _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
840                         ctf_EventLog("returned", flag.team, NULL);
841                         flag.enemy = NULL;
842                         ctf_RespawnFlag(flag);
843                 }
844         }
845 }
846
847 bool ctf_Stalemate_Customize(entity this, entity client)
848 {
849         // make spectators see what the player would see
850         entity e = WaypointSprite_getviewentity(client);
851         entity wp_owner = this.owner;
852
853         // team waypoints
854         //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
855         if(SAME_TEAM(wp_owner, e)) { return false; }
856         if(!IS_PLAYER(e)) { return false; }
857
858         return true;
859 }
860
861 void ctf_CheckStalemate()
862 {
863         // declarations
864         int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
865         entity tmp_entity;
866
867         entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
868
869         // build list of stale flags
870         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
871         {
872                 if(autocvar_g_ctf_stalemate)
873                 if(tmp_entity.ctf_status != FLAG_BASE)
874                 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
875                 {
876                         tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
877                         ctf_staleflaglist = tmp_entity;
878
879                         switch(tmp_entity.team)
880                         {
881                                 case NUM_TEAM_1: ++stale_red_flags; break;
882                                 case NUM_TEAM_2: ++stale_blue_flags; break;
883                                 case NUM_TEAM_3: ++stale_yellow_flags; break;
884                                 case NUM_TEAM_4: ++stale_pink_flags; break;
885                                 default: ++stale_neutral_flags; break;
886                         }
887                 }
888         }
889
890         if(ctf_oneflag)
891                 stale_flags = (stale_neutral_flags >= 1);
892         else
893                 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
894
895         if(ctf_oneflag && stale_flags == 1)
896                 ctf_stalemate = true;
897         else if(stale_flags >= 2)
898                 ctf_stalemate = true;
899         else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
900                 { ctf_stalemate = false; wpforenemy_announced = false; }
901         else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
902                 { ctf_stalemate = false; wpforenemy_announced = false; }
903
904         // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
905         if(ctf_stalemate)
906         {
907                 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
908                 {
909                         if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
910                         {
911                                 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);
912                                 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
913                                 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
914                         }
915                 }
916
917                 if (!wpforenemy_announced)
918                 {
919                         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)); });
920
921                         wpforenemy_announced = true;
922                 }
923         }
924 }
925
926 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
927 {
928         if(ITEM_DAMAGE_NEEDKILL(deathtype))
929         {
930                 if(autocvar_g_ctf_flag_return_damage_delay)
931                         this.ctf_flagdamaged_byworld = true;
932                 else
933                 {
934                         SetResourceExplicit(this, RES_HEALTH, 0);
935                         ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
936                 }
937                 return;
938         }
939         if(autocvar_g_ctf_flag_return_damage)
940         {
941                 // reduce health and check if it should be returned
942                 TakeResource(this, RES_HEALTH, damage);
943                 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
944                 return;
945         }
946 }
947
948 void ctf_FlagThink(entity this)
949 {
950         // declarations
951         entity tmp_entity;
952
953         this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
954
955         // captureshield
956         if(this == ctf_worldflaglist) // only for the first flag
957                 FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
958
959         // sanity checks
960         if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
961                 LOG_TRACE("wtf the flag got squashed?");
962                 tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
963                 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
964                         setsize(this, this.m_mins, this.m_maxs);
965         }
966
967         // main think method
968         switch(this.ctf_status)
969         {
970                 case FLAG_BASE:
971                 {
972                         if(autocvar_g_ctf_dropped_capture_radius)
973                         {
974                                 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
975                                         if(tmp_entity.ctf_status == FLAG_DROPPED)
976                                         if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
977                                         if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
978                                                 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
979                         }
980                         return;
981                 }
982
983                 case FLAG_DROPPED:
984                 {
985                         this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
986
987                         if(autocvar_g_ctf_flag_dropped_floatinwater)
988                         {
989                                 vector midpoint = ((this.absmin + this.absmax) * 0.5);
990                                 if(pointcontents(midpoint) == CONTENT_WATER)
991                                 {
992                                         this.velocity = this.velocity * 0.5;
993
994                                         if (pointcontents(midpoint + eZ * FLAG_FLOAT_OFFSET_Z) == CONTENT_WATER)
995                                                 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
996                                         else
997                                                 { set_movetype(this, MOVETYPE_FLY); }
998                                 }
999                                 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
1000                         }
1001                         if(autocvar_g_ctf_flag_return_dropped)
1002                         {
1003                                 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
1004                                 {
1005                                         SetResourceExplicit(this, RES_HEALTH, 0);
1006                                         ctf_CheckFlagReturn(this, RETURN_DROPPED);
1007                                         return;
1008                                 }
1009                         }
1010                         if(this.ctf_flagdamaged_byworld)
1011                         {
1012                                 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
1013                                 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
1014                                 return;
1015                         }
1016                         else if(autocvar_g_ctf_flag_return_time)
1017                         {
1018                                 TakeResource(this, RES_HEALTH, (this.max_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
1019                                 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
1020                                 return;
1021                         }
1022                         return;
1023                 }
1024
1025                 case FLAG_CARRY:
1026                 {
1027                         if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
1028                         {
1029                                 SetResourceExplicit(this, RES_HEALTH, 0);
1030                                 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
1031
1032                                 CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
1033                                 ImpulseCommands(this.owner);
1034                         }
1035                         if(autocvar_g_ctf_stalemate)
1036                         {
1037                                 if(time >= wpforenemy_nextthink)
1038                                 {
1039                                         ctf_CheckStalemate();
1040                                         wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1041                                 }
1042                         }
1043                         if(CTF_SAMETEAM(this, this.owner) && this.team)
1044                         {
1045                                 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1046                                         ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
1047                                 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
1048                                         ctf_Handle_Return(this, this.owner);
1049                         }
1050                         return;
1051                 }
1052
1053                 case FLAG_PASSING:
1054                 {
1055                         vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
1056                         targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1057                         WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1058
1059                         if((this.pass_target == NULL)
1060                                 || (IS_DEAD(this.pass_target))
1061                                 || (this.pass_target.flagcarried)
1062                                 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1063                                 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1064                                 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1065                         {
1066                                 // give up, pass failed
1067                                 ctf_Handle_Drop(this, NULL, DROP_PASS);
1068                         }
1069                         else
1070                         {
1071                                 // still a viable target, go for it
1072                                 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1073                         }
1074                         return;
1075                 }
1076
1077                 default: // this should never happen
1078                 {
1079                         LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1080                         return;
1081                 }
1082         }
1083 }
1084
1085 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1086 {
1087         return = false;
1088         if(game_stopped) return;
1089         if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1090
1091         bool is_not_monster = (!IS_MONSTER(toucher));
1092
1093         // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1094         if(ITEM_TOUCH_NEEDKILL())
1095         {
1096                 if(!autocvar_g_ctf_flag_return_damage_delay)
1097                 {
1098                         SetResourceExplicit(flag, RES_HEALTH, 0);
1099                         ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1100                 }
1101                 if(!flag.ctf_flagdamaged_byworld) { return; }
1102         }
1103
1104         // special touch behaviors
1105         if(STAT(FROZEN, toucher)) { return; }
1106         else if(IS_VEHICLE(toucher))
1107         {
1108                 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1109                         toucher = toucher.owner; // the player is actually the vehicle owner, not other
1110                 else
1111                         return; // do nothing
1112         }
1113         else if(IS_MONSTER(toucher))
1114         {
1115                 if(!autocvar_g_ctf_allow_monster_touch)
1116                         return; // do nothing
1117         }
1118         else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1119         {
1120                 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1121                 {
1122                         Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1123                         _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1124                         flag.wait = time + FLAG_TOUCHRATE;
1125                 }
1126                 return;
1127         }
1128         else if(IS_DEAD(toucher)) { return; }
1129
1130         switch(flag.ctf_status)
1131         {
1132                 case FLAG_BASE:
1133                 {
1134                         if(ctf_oneflag)
1135                         {
1136                                 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1137                                         ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1138                                 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1139                                         ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1140                         }
1141                         else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1142                                 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1143                         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)
1144                         {
1145                                 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1146                                 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1147                         }
1148                         else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1149                                 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1150                         break;
1151                 }
1152
1153                 case FLAG_DROPPED:
1154                 {
1155                         if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
1156                                 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1157                         else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1158                                 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1159                         break;
1160                 }
1161
1162                 case FLAG_CARRY:
1163                 {
1164                         LOG_TRACE("Someone touched a flag even though it was being carried?");
1165                         break;
1166                 }
1167
1168                 case FLAG_PASSING:
1169                 {
1170                         if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1171                         {
1172                                 if(DIFF_TEAM(toucher, flag.pass_sender))
1173                                 {
1174                                         if(ctf_Immediate_Return_Allowed(flag, toucher))
1175                                                 ctf_Handle_Return(flag, toucher);
1176                                         else if(is_not_monster && (!toucher.flagcarried))
1177                                                 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
1178                                 }
1179                                 else if(!toucher.flagcarried)
1180                                         ctf_Handle_Retrieve(flag, toucher);
1181                         }
1182                         break;
1183                 }
1184         }
1185 }
1186
1187 .float last_respawn;
1188 void ctf_RespawnFlag(entity flag)
1189 {
1190         flag.watertype = CONTENT_EMPTY; // TODO: it is unclear why this workaround is needed, likely many other potential breakage points!!
1191         // check for flag respawn being called twice in a row
1192         if(flag.last_respawn > time - 0.5)
1193                 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1194
1195         flag.last_respawn = time;
1196
1197         // reset the player (if there is one)
1198         if((flag.owner) && (flag.owner.flagcarried == flag))
1199         {
1200                 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1201                 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1202                 WaypointSprite_Kill(flag.wps_flagcarrier);
1203
1204                 flag.owner.flagcarried = NULL;
1205                 GameRules_scoring_vip(flag.owner, false);
1206
1207                 if(flag.speedrunning)
1208                         ctf_FakeTimeLimit(flag.owner, -1);
1209         }
1210
1211         if((flag.owner) && (flag.owner.vehicle))
1212                 flag.scale = FLAG_SCALE;
1213
1214         if(flag.ctf_status == FLAG_DROPPED)
1215                 { WaypointSprite_Kill(flag.wps_flagdropped); }
1216
1217         // reset the flag
1218         setattachment(flag, NULL, "");
1219         setorigin(flag, flag.ctf_spawnorigin);
1220
1221         //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1222         set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1223         flag.takedamage = DAMAGE_NO;
1224         SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1225         flag.solid = SOLID_TRIGGER;
1226         flag.velocity = '0 0 0';
1227         flag.angles = flag.mangle;
1228         flag.flags = FL_ITEM | FL_NOTARGET;
1229
1230         flag.ctf_status = FLAG_BASE;
1231         flag.owner = NULL;
1232         flag.pass_distance = 0;
1233         flag.pass_sender = NULL;
1234         flag.pass_target = NULL;
1235         flag.ctf_dropper = NULL;
1236         flag.ctf_pickuptime = 0;
1237         flag.ctf_droptime = 0;
1238         flag.ctf_flagdamaged_byworld = false;
1239         navigation_dynamicgoal_unset(flag);
1240
1241         ctf_CheckStalemate();
1242 }
1243
1244 void ctf_Reset(entity this)
1245 {
1246         if(this.owner && IS_PLAYER(this.owner))
1247                 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1248
1249         this.enemy = NULL;
1250         ctf_RespawnFlag(this);
1251 }
1252
1253 bool ctf_FlagBase_Customize(entity this, entity client)
1254 {
1255         entity e = WaypointSprite_getviewentity(client);
1256         entity wp_owner = this.owner;
1257         entity flag = e.flagcarried;
1258         if(flag && CTF_SAMETEAM(e, flag))
1259                 return false;
1260         if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1261                 return false;
1262         return true;
1263 }
1264
1265 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1266 {
1267         // bot waypoints
1268         waypoint_spawnforitem_force(this, this.origin);
1269         navigation_dynamicgoal_init(this, true);
1270
1271         // waypointsprites
1272         entity basename;
1273         switch (this.team)
1274         {
1275                 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1276                 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1277                 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1278                 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1279                 default: basename = WP_FlagBaseNeutral; break;
1280         }
1281
1282         if(autocvar_g_ctf_flag_waypoint)
1283         {
1284                 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1285                 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1286                 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1287                 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1288                 setcefc(wp, ctf_FlagBase_Customize);
1289         }
1290
1291         // captureshield setup
1292         ctf_CaptureShield_Spawn(this);
1293 }
1294
1295 .bool pushable;
1296
1297 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1298 {
1299         // main setup
1300         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1301         ctf_worldflaglist = flag;
1302
1303         setattachment(flag, NULL, "");
1304
1305         flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1306         flag.team = teamnum;
1307         flag.classname = "item_flag_team";
1308         flag.target = "###item###"; // for finding the nearest item using findnearest
1309         flag.flags = FL_ITEM | FL_NOTARGET;
1310         IL_PUSH(g_items, flag);
1311         flag.solid = SOLID_TRIGGER;
1312         flag.takedamage = DAMAGE_NO;
1313         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1314         flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1315         SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1316         flag.event_damage = ctf_FlagDamage;
1317         flag.pushable = true;
1318         flag.teleportable = TELEPORT_NORMAL;
1319         flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1320         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1321         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1322         if(flag.damagedbycontents)
1323                 IL_PUSH(g_damagedbycontents, flag);
1324         flag.velocity = '0 0 0';
1325         flag.mangle = flag.angles;
1326         flag.reset = ctf_Reset;
1327         settouch(flag, ctf_FlagTouch);
1328         setthink(flag, ctf_FlagThink);
1329         flag.nextthink = time + FLAG_THINKRATE;
1330         flag.ctf_status = FLAG_BASE;
1331
1332         // crudely force them all to 0
1333         if(autocvar_g_ctf_score_ignore_fields)
1334                 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1335
1336         string teamname = Static_Team_ColorName_Lower(teamnum);
1337         // appearence
1338         if(!flag.scale)                         { flag.scale = FLAG_SCALE; }
1339         if(flag.skin == 0)                      { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1340         if(flag.model == "")            { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1341         if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1342         if (flag.passeffect == "")      { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1343         if (flag.capeffect == "")       { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1344
1345         // sounds
1346 #define X(s,b) \
1347                 if(flag.s == "") flag.s = b; \
1348                 precache_sound(flag.s);
1349
1350         X(snd_flag_taken,               strzone(SND(CTF_TAKEN(teamnum))))
1351         X(snd_flag_returned,    strzone(SND(CTF_RETURNED(teamnum))))
1352         X(snd_flag_capture,     strzone(SND(CTF_CAPTURE(teamnum))))
1353         X(snd_flag_dropped,     strzone(SND(CTF_DROPPED(teamnum))))
1354         X(snd_flag_respawn,     strzone(SND(CTF_RESPAWN)))
1355         X(snd_flag_touch,               strzone(SND(CTF_TOUCH)))
1356         X(snd_flag_pass,                strzone(SND(CTF_PASS)))
1357 #undef X
1358
1359         // precache
1360         precache_model(flag.model);
1361
1362         // appearence
1363         _setmodel(flag, flag.model); // precision set below
1364         setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1365         flag.m_mins = flag.mins; // store these for squash checks
1366         flag.m_maxs = flag.maxs;
1367         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1368
1369         if(autocvar_g_ctf_flag_glowtrails)
1370         {
1371                 switch(teamnum)
1372                 {
1373                         case NUM_TEAM_1: flag.glow_color = 251; break;
1374                         case NUM_TEAM_2: flag.glow_color = 210; break;
1375                         case NUM_TEAM_3: flag.glow_color = 110; break;
1376                         case NUM_TEAM_4: flag.glow_color = 145; break;
1377                         default:                 flag.glow_color = 254; break;
1378                 }
1379                 flag.glow_size = 25;
1380                 flag.glow_trail = 1;
1381         }
1382
1383         flag.effects |= EF_LOWPRECISION;
1384         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1385         if(autocvar_g_ctf_dynamiclights)
1386         {
1387                 switch(teamnum)
1388                 {
1389                         case NUM_TEAM_1: flag.effects |= EF_RED; break;
1390                         case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1391                         case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1392                         case NUM_TEAM_4: flag.effects |= EF_RED; break;
1393                         default:                 flag.effects |= EF_DIMLIGHT; break;
1394                 }
1395         }
1396
1397         // flag placement
1398         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1399         {
1400                 flag.dropped_origin = flag.origin;
1401                 flag.noalign = true;
1402                 set_movetype(flag, MOVETYPE_NONE);
1403         }
1404         else // drop to floor, automatically find a platform and set that as spawn origin
1405         {
1406                 flag.noalign = false;
1407                 droptofloor(flag);
1408                 set_movetype(flag, MOVETYPE_NONE);
1409         }
1410
1411         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1412 }
1413
1414
1415 // ================
1416 // Bot player logic
1417 // ================
1418
1419 // NOTE: LEGACY CODE, needs to be re-written!
1420
1421 void havocbot_ctf_calculate_middlepoint()
1422 {
1423         entity f;
1424         vector s = '0 0 0';
1425         vector fo = '0 0 0';
1426         int n = 0;
1427
1428         f = ctf_worldflaglist;
1429         while (f)
1430         {
1431                 fo = f.origin;
1432                 s = s + fo;
1433                 f = f.ctf_worldflagnext;
1434                 n++;
1435         }
1436         if(!n)
1437                 return;
1438
1439         havocbot_middlepoint = s / n;
1440         havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1441
1442         havocbot_symmetry_axis_m = 0;
1443         havocbot_symmetry_axis_q = 0;
1444         if(n == 2)
1445         {
1446                 // for symmetrical editing of waypoints
1447                 entity f1 = ctf_worldflaglist;
1448                 entity f2 = f1.ctf_worldflagnext;
1449                 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1450                 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1451                 havocbot_symmetry_axis_m = m;
1452                 havocbot_symmetry_axis_q = q;
1453         }
1454         havocbot_symmetry_origin_order = n;
1455 }
1456
1457
1458 entity havocbot_ctf_find_flag(entity bot)
1459 {
1460         entity f;
1461         f = ctf_worldflaglist;
1462         while (f)
1463         {
1464                 if (CTF_SAMETEAM(bot, f))
1465                         return f;
1466                 f = f.ctf_worldflagnext;
1467         }
1468         return NULL;
1469 }
1470
1471 entity havocbot_ctf_find_enemy_flag(entity bot)
1472 {
1473         entity f;
1474         f = ctf_worldflaglist;
1475         while (f)
1476         {
1477                 if(ctf_oneflag)
1478                 {
1479                         if(CTF_DIFFTEAM(bot, f))
1480                         {
1481                                 if(f.team)
1482                                 {
1483                                         if(bot.flagcarried)
1484                                                 return f;
1485                                 }
1486                                 else if(!bot.flagcarried)
1487                                         return f;
1488                         }
1489                 }
1490                 else if (CTF_DIFFTEAM(bot, f))
1491                         return f;
1492                 f = f.ctf_worldflagnext;
1493         }
1494         return NULL;
1495 }
1496
1497 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1498 {
1499         if (!teamplay)
1500                 return 0;
1501
1502         int c = 0;
1503
1504         FOREACH_CLIENT(IS_PLAYER(it), {
1505                 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1506                         continue;
1507
1508                 if(vdist(it.origin - org, <, tc_radius))
1509                         ++c;
1510         });
1511
1512         return c;
1513 }
1514
1515 // unused
1516 #if 0
1517 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1518 {
1519         entity head;
1520         head = ctf_worldflaglist;
1521         while (head)
1522         {
1523                 if (CTF_SAMETEAM(this, head))
1524                         break;
1525                 head = head.ctf_worldflagnext;
1526         }
1527         if (head)
1528                 navigation_routerating(this, head, ratingscale, 10000);
1529 }
1530 #endif
1531
1532 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1533 {
1534         entity head;
1535         head = ctf_worldflaglist;
1536         while (head)
1537         {
1538                 if (CTF_SAMETEAM(this, head))
1539                 {
1540                         if (this.flagcarried)
1541                         if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1542                         {
1543                                 head = head.ctf_worldflagnext; // skip base if it has a different group
1544                                 continue;
1545                         }
1546                         break;
1547                 }
1548                 head = head.ctf_worldflagnext;
1549         }
1550         if (!head)
1551                 return;
1552
1553         navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1554 }
1555
1556 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1557 {
1558         entity head;
1559         head = ctf_worldflaglist;
1560         while (head)
1561         {
1562                 if(ctf_oneflag)
1563                 {
1564                         if(CTF_DIFFTEAM(this, head))
1565                         {
1566                                 if(head.team)
1567                                 {
1568                                         if(this.flagcarried)
1569                                                 break;
1570                                 }
1571                                 else if(!this.flagcarried)
1572                                         break;
1573                         }
1574                 }
1575                 else if(CTF_DIFFTEAM(this, head))
1576                         break;
1577                 head = head.ctf_worldflagnext;
1578         }
1579         if (head)
1580         {
1581                 if (head.ctf_status == FLAG_CARRY)
1582                 {
1583                         // adjust rating of our flag carrier depending on his health
1584                         head = head.tag_entity;
1585                         float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1586                         ratingscale += ratingscale * f * 0.1;
1587                 }
1588                 navigation_routerating(this, head, ratingscale, 10000);
1589         }
1590 }
1591
1592 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1593 {
1594         // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1595         /*
1596         if (!bot_waypoints_for_items)
1597         {
1598                 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1599                 return;
1600         }
1601         */
1602         entity head;
1603
1604         head = havocbot_ctf_find_enemy_flag(this);
1605
1606         if (!head)
1607                 return;
1608
1609         navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1610 }
1611
1612 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1613 {
1614         entity mf;
1615
1616         mf = havocbot_ctf_find_flag(this);
1617
1618         if(mf.ctf_status == FLAG_BASE)
1619                 return;
1620
1621         if(mf.tag_entity)
1622                 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1623 }
1624
1625 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1626 {
1627         entity head;
1628         head = ctf_worldflaglist;
1629         while (head)
1630         {
1631                 // flag is out in the field
1632                 if(head.ctf_status != FLAG_BASE)
1633                 if(head.tag_entity==NULL)       // dropped
1634                 {
1635                         if(df_radius)
1636                         {
1637                                 if(vdist(org - head.origin, <, df_radius))
1638                                         navigation_routerating(this, head, ratingscale, 10000);
1639                         }
1640                         else
1641                                 navigation_routerating(this, head, ratingscale, 10000);
1642                 }
1643
1644                 head = head.ctf_worldflagnext;
1645         }
1646 }
1647
1648 void havocbot_ctf_reset_role(entity this)
1649 {
1650         float cdefense, cmiddle, coffense;
1651         entity mf, ef;
1652
1653         if(IS_DEAD(this))
1654                 return;
1655
1656         // Check ctf flags
1657         if (this.flagcarried)
1658         {
1659                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1660                 return;
1661         }
1662
1663         mf = havocbot_ctf_find_flag(this);
1664         ef = havocbot_ctf_find_enemy_flag(this);
1665
1666         // Retrieve stolen flag
1667         if(mf.ctf_status!=FLAG_BASE)
1668         {
1669                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1670                 return;
1671         }
1672
1673         // If enemy flag is taken go to the middle to intercept pursuers
1674         if(ef.ctf_status!=FLAG_BASE)
1675         {
1676                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1677                 return;
1678         }
1679
1680         // if there is no one else on the team switch to offense
1681         int count = 0;
1682         // don't check if this bot is a player since it isn't true when the bot is added to the server
1683         FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1684
1685         if (count == 0)
1686         {
1687                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1688                 return;
1689         }
1690         else if (time < CS(this).jointime + 1)
1691         {
1692                 // if bots spawn all at once set good default roles
1693                 if (count == 1)
1694                 {
1695                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1696                         return;
1697                 }
1698                 else if (count == 2)
1699                 {
1700                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1701                         return;
1702                 }
1703         }
1704
1705         // Evaluate best position to take
1706         // Count mates on middle position
1707         cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1708
1709         // Count mates on defense position
1710         cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1711
1712         // Count mates on offense position
1713         coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1714
1715         if(cdefense<=coffense)
1716                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1717         else if(coffense<=cmiddle)
1718                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1719         else
1720                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1721
1722         // if bots spawn all at once assign them a more appropriated role after a while
1723         if (time < CS(this).jointime + 1 && count > 2)
1724                 this.havocbot_role_timeout = time + 10 + random() * 10;
1725 }
1726
1727 bool havocbot_ctf_is_basewaypoint(entity item)
1728 {
1729         if (item.classname != "waypoint")
1730                 return false;
1731
1732         entity head = ctf_worldflaglist;
1733         while (head)
1734         {
1735                 if (item == head.bot_basewaypoint)
1736                         return true;
1737                 head = head.ctf_worldflagnext;
1738         }
1739         return false;
1740 }
1741
1742 void havocbot_role_ctf_carrier(entity this)
1743 {
1744         if(IS_DEAD(this))
1745         {
1746                 havocbot_ctf_reset_role(this);
1747                 return;
1748         }
1749
1750         if (this.flagcarried == NULL)
1751         {
1752                 havocbot_ctf_reset_role(this);
1753                 return;
1754         }
1755
1756         if (navigation_goalrating_timeout(this))
1757         {
1758                 navigation_goalrating_start(this);
1759
1760                 // role: carrier
1761                 entity mf = havocbot_ctf_find_flag(this);
1762                 vector base_org = mf.dropped_origin;
1763                 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1764                 if(ctf_oneflag)
1765                         havocbot_goalrating_ctf_enemybase(this, base_rating);
1766                 else
1767                         havocbot_goalrating_ctf_ourbase(this, base_rating);
1768
1769                 // start collecting items very close to the bot but only inside of own base radius
1770                 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1771                         havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1772
1773                 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1774
1775                 navigation_goalrating_end(this);
1776
1777                 navigation_goalrating_timeout_set(this);
1778
1779                 entity goal = this.goalentity;
1780                 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1781                         this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1782
1783                 if (goal)
1784                         this.havocbot_cantfindflag = time + 10;
1785                 else if (time > this.havocbot_cantfindflag)
1786                 {
1787                         // Can't navigate to my own base, suicide!
1788                         // TODO: drop it and wander around
1789                         Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1790                         return;
1791                 }
1792         }
1793 }
1794
1795 void havocbot_role_ctf_escort(entity this)
1796 {
1797         entity mf, ef;
1798
1799         if(IS_DEAD(this))
1800         {
1801                 havocbot_ctf_reset_role(this);
1802                 return;
1803         }
1804
1805         if (this.flagcarried)
1806         {
1807                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1808                 return;
1809         }
1810
1811         // If enemy flag is back on the base switch to previous role
1812         ef = havocbot_ctf_find_enemy_flag(this);
1813         if(ef.ctf_status==FLAG_BASE)
1814         {
1815                 this.havocbot_role = this.havocbot_previous_role;
1816                 this.havocbot_role_timeout = 0;
1817                 return;
1818         }
1819         if (ef.ctf_status == FLAG_DROPPED)
1820         {
1821                 navigation_goalrating_timeout_expire(this, 1);
1822                 return;
1823         }
1824
1825         // If the flag carrier reached the base switch to defense
1826         mf = havocbot_ctf_find_flag(this);
1827         if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1828         {
1829                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1830                 return;
1831         }
1832
1833         // Set the role timeout if necessary
1834         if (!this.havocbot_role_timeout)
1835         {
1836                 this.havocbot_role_timeout = time + random() * 30 + 60;
1837         }
1838
1839         // If nothing happened just switch to previous role
1840         if (time > this.havocbot_role_timeout)
1841         {
1842                 this.havocbot_role = this.havocbot_previous_role;
1843                 this.havocbot_role_timeout = 0;
1844                 return;
1845         }
1846
1847         // Chase the flag carrier
1848         if (navigation_goalrating_timeout(this))
1849         {
1850                 navigation_goalrating_start(this);
1851
1852                 // role: escort
1853                 havocbot_goalrating_ctf_enemyflag(this, 10000);
1854                 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1855                 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1856
1857                 navigation_goalrating_end(this);
1858
1859                 navigation_goalrating_timeout_set(this);
1860         }
1861 }
1862
1863 void havocbot_role_ctf_offense(entity this)
1864 {
1865         entity mf, ef;
1866         vector pos;
1867
1868         if(IS_DEAD(this))
1869         {
1870                 havocbot_ctf_reset_role(this);
1871                 return;
1872         }
1873
1874         if (this.flagcarried)
1875         {
1876                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1877                 return;
1878         }
1879
1880         // Check flags
1881         mf = havocbot_ctf_find_flag(this);
1882         ef = havocbot_ctf_find_enemy_flag(this);
1883
1884         // Own flag stolen
1885         if(mf.ctf_status!=FLAG_BASE)
1886         {
1887                 if(mf.tag_entity)
1888                         pos = mf.tag_entity.origin;
1889                 else
1890                         pos = mf.origin;
1891
1892                 // Try to get it if closer than the enemy base
1893                 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1894                 {
1895                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1896                         return;
1897                 }
1898         }
1899
1900         // Escort flag carrier
1901         if(ef.ctf_status!=FLAG_BASE)
1902         {
1903                 if(ef.tag_entity)
1904                         pos = ef.tag_entity.origin;
1905                 else
1906                         pos = ef.origin;
1907
1908                 if(vdist(pos - mf.dropped_origin, >, 700))
1909                 {
1910                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1911                         return;
1912                 }
1913         }
1914
1915         // Set the role timeout if necessary
1916         if (!this.havocbot_role_timeout)
1917                 this.havocbot_role_timeout = time + 120;
1918
1919         if (time > this.havocbot_role_timeout)
1920         {
1921                 havocbot_ctf_reset_role(this);
1922                 return;
1923         }
1924
1925         if (navigation_goalrating_timeout(this))
1926         {
1927                 navigation_goalrating_start(this);
1928
1929                 // role: offense
1930                 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1931                 havocbot_goalrating_ctf_enemybase(this, 10000);
1932                 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1933
1934                 navigation_goalrating_end(this);
1935
1936                 navigation_goalrating_timeout_set(this);
1937         }
1938 }
1939
1940 // Retriever (temporary role):
1941 void havocbot_role_ctf_retriever(entity this)
1942 {
1943         entity mf;
1944
1945         if(IS_DEAD(this))
1946         {
1947                 havocbot_ctf_reset_role(this);
1948                 return;
1949         }
1950
1951         if (this.flagcarried)
1952         {
1953                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1954                 return;
1955         }
1956
1957         // If flag is back on the base switch to previous role
1958         mf = havocbot_ctf_find_flag(this);
1959         if(mf.ctf_status==FLAG_BASE)
1960         {
1961                 if (mf.enemy == this) // did this bot return the flag?
1962                         navigation_goalrating_timeout_force(this);
1963                 havocbot_ctf_reset_role(this);
1964                 return;
1965         }
1966
1967         if (!this.havocbot_role_timeout)
1968                 this.havocbot_role_timeout = time + 20;
1969
1970         if (time > this.havocbot_role_timeout)
1971         {
1972                 havocbot_ctf_reset_role(this);
1973                 return;
1974         }
1975
1976         if (navigation_goalrating_timeout(this))
1977         {
1978                 const float RT_RADIUS = 10000;
1979
1980                 navigation_goalrating_start(this);
1981
1982                 // role: retriever
1983                 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1984                 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1985                 havocbot_goalrating_ctf_enemybase(this, 8000);
1986                 entity ef = havocbot_ctf_find_enemy_flag(this);
1987                 vector enemy_base_org = ef.dropped_origin;
1988                 // start collecting items very close to the bot but only inside of enemy base radius
1989                 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1990                         havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1991                 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1992
1993                 navigation_goalrating_end(this);
1994
1995                 navigation_goalrating_timeout_set(this);
1996         }
1997 }
1998
1999 void havocbot_role_ctf_middle(entity this)
2000 {
2001         entity mf;
2002
2003         if(IS_DEAD(this))
2004         {
2005                 havocbot_ctf_reset_role(this);
2006                 return;
2007         }
2008
2009         if (this.flagcarried)
2010         {
2011                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2012                 return;
2013         }
2014
2015         mf = havocbot_ctf_find_flag(this);
2016         if(mf.ctf_status!=FLAG_BASE)
2017         {
2018                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2019                 return;
2020         }
2021
2022         if (!this.havocbot_role_timeout)
2023                 this.havocbot_role_timeout = time + 10;
2024
2025         if (time > this.havocbot_role_timeout)
2026         {
2027                 havocbot_ctf_reset_role(this);
2028                 return;
2029         }
2030
2031         if (navigation_goalrating_timeout(this))
2032         {
2033                 vector org;
2034
2035                 org = havocbot_middlepoint;
2036                 org.z = this.origin.z;
2037
2038                 navigation_goalrating_start(this);
2039
2040                 // role: middle
2041                 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2042                 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2043                 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2044                 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2045                 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2046                 havocbot_goalrating_ctf_enemybase(this, 3000);
2047
2048                 navigation_goalrating_end(this);
2049
2050                 entity goal = this.goalentity;
2051                 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2052                         this.goalentity_lock_timeout = time + 2;
2053
2054                 navigation_goalrating_timeout_set(this);
2055         }
2056 }
2057
2058 void havocbot_role_ctf_defense(entity this)
2059 {
2060         entity mf;
2061
2062         if(IS_DEAD(this))
2063         {
2064                 havocbot_ctf_reset_role(this);
2065                 return;
2066         }
2067
2068         if (this.flagcarried)
2069         {
2070                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2071                 return;
2072         }
2073
2074         // If own flag was captured
2075         mf = havocbot_ctf_find_flag(this);
2076         if(mf.ctf_status!=FLAG_BASE)
2077         {
2078                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2079                 return;
2080         }
2081
2082         if (!this.havocbot_role_timeout)
2083                 this.havocbot_role_timeout = time + 30;
2084
2085         if (time > this.havocbot_role_timeout)
2086         {
2087                 havocbot_ctf_reset_role(this);
2088                 return;
2089         }
2090         if (navigation_goalrating_timeout(this))
2091         {
2092                 vector org = mf.dropped_origin;
2093
2094                 navigation_goalrating_start(this);
2095
2096                 // if enemies are closer to our base, go there
2097                 entity closestplayer = NULL;
2098                 float distance, bestdistance = 10000;
2099                 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2100                         distance = vlen(org - it.origin);
2101                         if(distance<bestdistance)
2102                         {
2103                                 closestplayer = it;
2104                                 bestdistance = distance;
2105                         }
2106                 });
2107
2108                 // role: defense
2109                 if(closestplayer)
2110                 if(DIFF_TEAM(closestplayer, this))
2111                 if(vdist(org - this.origin, >, 1000))
2112                 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2113                         havocbot_goalrating_ctf_ourbase(this, 10000);
2114
2115                 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2116                 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2117                 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2118                 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2119                 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2120
2121                 navigation_goalrating_end(this);
2122
2123                 navigation_goalrating_timeout_set(this);
2124         }
2125 }
2126
2127 void havocbot_role_ctf_setrole(entity bot, int role)
2128 {
2129         string s = "(null)";
2130         switch(role)
2131         {
2132                 case HAVOCBOT_CTF_ROLE_CARRIER:
2133                         s = "carrier";
2134                         bot.havocbot_role = havocbot_role_ctf_carrier;
2135                         bot.havocbot_role_timeout = 0;
2136                         bot.havocbot_cantfindflag = time + 10;
2137                         if (bot.havocbot_previous_role != bot.havocbot_role)
2138                                 navigation_goalrating_timeout_force(bot);
2139                         break;
2140                 case HAVOCBOT_CTF_ROLE_DEFENSE:
2141                         s = "defense";
2142                         bot.havocbot_role = havocbot_role_ctf_defense;
2143                         bot.havocbot_role_timeout = 0;
2144                         break;
2145                 case HAVOCBOT_CTF_ROLE_MIDDLE:
2146                         s = "middle";
2147                         bot.havocbot_role = havocbot_role_ctf_middle;
2148                         bot.havocbot_role_timeout = 0;
2149                         break;
2150                 case HAVOCBOT_CTF_ROLE_OFFENSE:
2151                         s = "offense";
2152                         bot.havocbot_role = havocbot_role_ctf_offense;
2153                         bot.havocbot_role_timeout = 0;
2154                         break;
2155                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2156                         s = "retriever";
2157                         bot.havocbot_previous_role = bot.havocbot_role;
2158                         bot.havocbot_role = havocbot_role_ctf_retriever;
2159                         bot.havocbot_role_timeout = time + 10;
2160                         if (bot.havocbot_previous_role != bot.havocbot_role)
2161                                 navigation_goalrating_timeout_expire(bot, 2);
2162                         break;
2163                 case HAVOCBOT_CTF_ROLE_ESCORT:
2164                         s = "escort";
2165                         bot.havocbot_previous_role = bot.havocbot_role;
2166                         bot.havocbot_role = havocbot_role_ctf_escort;
2167                         bot.havocbot_role_timeout = time + 30;
2168                         if (bot.havocbot_previous_role != bot.havocbot_role)
2169                                 navigation_goalrating_timeout_expire(bot, 2);
2170                         break;
2171         }
2172         LOG_TRACE(bot.netname, " switched to ", s);
2173 }
2174
2175
2176 // ==============
2177 // Hook Functions
2178 // ==============
2179
2180 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2181 {
2182         entity player = M_ARGV(0, entity);
2183
2184         int t = 0, t2 = 0, t3 = 0;
2185         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)
2186
2187         // initially clear items so they can be set as necessary later.
2188         STAT(OBJECTIVE_STATUS, player) &= ~(CTF_RED_FLAG_CARRYING               | CTF_RED_FLAG_TAKEN            | CTF_RED_FLAG_LOST
2189                                                    | CTF_BLUE_FLAG_CARRYING             | CTF_BLUE_FLAG_TAKEN           | CTF_BLUE_FLAG_LOST
2190                                                    | CTF_YELLOW_FLAG_CARRYING   | CTF_YELLOW_FLAG_TAKEN         | CTF_YELLOW_FLAG_LOST
2191                                                    | CTF_PINK_FLAG_CARRYING     | CTF_PINK_FLAG_TAKEN           | CTF_PINK_FLAG_LOST
2192                                                    | CTF_NEUTRAL_FLAG_CARRYING  | CTF_NEUTRAL_FLAG_TAKEN        | CTF_NEUTRAL_FLAG_LOST
2193                                                    | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2194
2195         // scan through all the flags and notify the client about them
2196         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2197         {
2198                 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING;              t2 = CTF_RED_FLAG_TAKEN;                t3 = CTF_RED_FLAG_LOST; }
2199                 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING;             t2 = CTF_BLUE_FLAG_TAKEN;               t3 = CTF_BLUE_FLAG_LOST; }
2200                 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING;   t2 = CTF_YELLOW_FLAG_TAKEN;             t3 = CTF_YELLOW_FLAG_LOST; }
2201                 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING;             t2 = CTF_PINK_FLAG_TAKEN;               t3 = CTF_PINK_FLAG_LOST; }
2202                 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; }
2203
2204                 switch(flag.ctf_status)
2205                 {
2206                         case FLAG_PASSING:
2207                         case FLAG_CARRY:
2208                         {
2209                                 if((flag.owner == player) || (flag.pass_sender == player))
2210                                         STAT(OBJECTIVE_STATUS, player) |= t; // carrying: player is currently carrying the flag
2211                                 else
2212                                         STAT(OBJECTIVE_STATUS, player) |= t2; // taken: someone else is carrying the flag
2213                                 break;
2214                         }
2215                         case FLAG_DROPPED:
2216                         {
2217                                 STAT(OBJECTIVE_STATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2218                                 break;
2219                         }
2220                 }
2221         }
2222
2223         // item for stopping players from capturing the flag too often
2224         if(player.ctf_captureshielded)
2225                 STAT(OBJECTIVE_STATUS, player) |= CTF_SHIELDED;
2226
2227         if(ctf_stalemate)
2228                 STAT(OBJECTIVE_STATUS, player) |= CTF_STALEMATE;
2229
2230         // update the health of the flag carrier waypointsprite
2231         if(player.wps_flagcarrier)
2232                 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);
2233 }
2234
2235 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in damage.qc
2236 {
2237         entity frag_attacker = M_ARGV(1, entity);
2238         entity frag_target = M_ARGV(2, entity);
2239         float frag_damage = M_ARGV(4, float);
2240         vector frag_force = M_ARGV(6, vector);
2241
2242         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2243         {
2244                 if(frag_target == frag_attacker) // damage done to yourself
2245                 {
2246                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2247                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2248                 }
2249                 else // damage done to everyone else
2250                 {
2251                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2252                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2253                 }
2254
2255                 M_ARGV(4, float) = frag_damage;
2256                 M_ARGV(6, vector) = frag_force;
2257         }
2258         else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2259         {
2260                 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
2261                         && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2262                 {
2263                         frag_target.wps_helpme_time = time;
2264                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2265                 }
2266                 // todo: add notification for when flag carrier needs help?
2267         }
2268 }
2269
2270 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2271 {
2272         entity frag_attacker = M_ARGV(1, entity);
2273         entity frag_target = M_ARGV(2, entity);
2274
2275         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)))
2276         {
2277                 if(frag_target.flagcarried) {
2278                         // Killing an enemy flag carrier
2279                         GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2280                         GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2281                 } else if(frag_attacker.flagcarried) {
2282                         Give_Medal(frag_attacker, DEFENSE);
2283                 } else {
2284                         entity tmp_entity;
2285                         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2286                         if(tmp_entity.ctf_status == FLAG_BASE && CTF_SAMETEAM(tmp_entity, frag_attacker))
2287                         {
2288                                 if(CTF_IS_NEAR(frag_target, tmp_entity, '1 1 1' * 1500))
2289                                 {
2290                                         Give_Medal(frag_attacker, DEFENSE);
2291                                 }
2292                                 break;
2293                         }
2294                 }
2295         }
2296
2297         if(frag_target.flagcarried)
2298         {
2299                 entity tmp_entity = frag_target.flagcarried;
2300                 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2301                 tmp_entity.ctf_dropper = NULL;
2302         }
2303         
2304 }
2305
2306 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2307 {
2308         M_ARGV(2, float) = 0; // frag score
2309         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2310 }
2311
2312 void ctf_RemovePlayer(entity player)
2313 {
2314         if(player.flagcarried)
2315                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2316
2317         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2318         {
2319                 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2320                 if(flag.pass_target == player) { flag.pass_target = NULL; }
2321                 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2322         }
2323 }
2324
2325 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2326 {
2327         entity player = M_ARGV(0, entity);
2328
2329         ctf_RemovePlayer(player);
2330 }
2331
2332 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2333 {
2334         entity player = M_ARGV(0, entity);
2335
2336         ctf_RemovePlayer(player);
2337 }
2338
2339 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2340 {
2341         if(!autocvar_g_ctf_leaderboard)
2342                 return;
2343
2344         entity player = M_ARGV(0, entity);
2345
2346         race_SendAll(player, true);
2347 }
2348
2349 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2350 {
2351         if(!autocvar_g_ctf_leaderboard)
2352                 return;
2353
2354         entity player = M_ARGV(0, entity);
2355
2356         race_checkAndWriteName(player);
2357 }
2358
2359 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2360 {
2361         entity player = M_ARGV(0, entity);
2362
2363         if(player.flagcarried)
2364         if(!autocvar_g_ctf_portalteleport)
2365                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2366 }
2367
2368 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2369 {
2370         if(MUTATOR_RETURNVALUE || game_stopped) return;
2371
2372         entity player = M_ARGV(0, entity);
2373
2374         if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2375         {
2376                 // pass the flag to a team mate
2377                 if(autocvar_g_ctf_pass)
2378                 {
2379                         entity head, closest_target = NULL;
2380                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2381
2382                         while(head) // find the closest acceptable target to pass to
2383                         {
2384                                 if(IS_PLAYER(head) && !IS_DEAD(head))
2385                                 if(head != player && SAME_TEAM(head, player))
2386                                 if(!head.speedrunning && !head.vehicle)
2387                                 {
2388                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in damage.qc)
2389                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2390                                         vector passer_center = CENTER_OR_VIEWOFS(player);
2391
2392                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2393                                         {
2394                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2395                                                 {
2396                                                         if(IS_BOT_CLIENT(head))
2397                                                         {
2398                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2399                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
2400                                                         }
2401                                                         else
2402                                                         {
2403                                                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2404                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2405                                                         }
2406                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2407                                                         return true;
2408                                                 }
2409                                                 else if(player.flagcarried && !head.flagcarried)
2410                                                 {
2411                                                         if(closest_target)
2412                                                         {
2413                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2414                                                                 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2415                                                                         { closest_target = head; }
2416                                                         }
2417                                                         else { closest_target = head; }
2418                                                 }
2419                                         }
2420                                 }
2421                                 head = head.chain;
2422                         }
2423
2424                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2425                 }
2426
2427                 // throw the flag in front of you
2428                 if(autocvar_g_ctf_throw && player.flagcarried)
2429                 {
2430                         if(player.throw_count == -1)
2431                         {
2432                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2433                                 {
2434                                         player.throw_prevtime = time;
2435                                         player.throw_count = 1;
2436                                         ctf_Handle_Throw(player, NULL, DROP_THROW);
2437                                         return true;
2438                                 }
2439                                 else
2440                                 {
2441                                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2442                                         return false;
2443                                 }
2444                         }
2445                         else
2446                         {
2447                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2448                                 else { player.throw_count += 1; }
2449                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2450
2451                                 player.throw_prevtime = time;
2452                                 ctf_Handle_Throw(player, NULL, DROP_THROW);
2453                                 return true;
2454                         }
2455                 }
2456         }
2457 }
2458
2459 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2460 {
2461         entity player = M_ARGV(0, entity);
2462
2463         if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2464         {
2465                 player.wps_helpme_time = time;
2466                 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2467         }
2468         else // create a normal help me waypointsprite
2469         {
2470                 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2471                 WaypointSprite_Ping(player.wps_helpme);
2472         }
2473
2474         return true;
2475 }
2476
2477 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2478 {
2479         entity player = M_ARGV(0, entity);
2480         entity veh = M_ARGV(1, entity);
2481
2482         if(player.flagcarried)
2483         {
2484                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2485                 {
2486                         ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2487                 }
2488                 else
2489                 {
2490                         player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2491                         setattachment(player.flagcarried, veh, "");
2492                         setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2493                         player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2494                         //player.flagcarried.angles = '0 0 0';
2495                 }
2496                 return true;
2497         }
2498 }
2499
2500 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2501 {
2502         entity player = M_ARGV(0, entity);
2503
2504         if(player.flagcarried)
2505         {
2506                 setattachment(player.flagcarried, player, "");
2507                 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2508                 player.flagcarried.scale = FLAG_SCALE;
2509                 player.flagcarried.angles = '0 0 0';
2510                 player.flagcarried.nodrawtoclient = NULL;
2511                 return true;
2512         }
2513 }
2514
2515 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2516 {
2517         entity player = M_ARGV(0, entity);
2518
2519         if(player.flagcarried)
2520         {
2521                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2522                 ctf_RespawnFlag(player.flagcarried);
2523                 return true;
2524         }
2525 }
2526
2527 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2528 {
2529         entity flag; // temporary entity for the search method
2530
2531         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2532         {
2533                 switch(flag.ctf_status)
2534                 {
2535                         case FLAG_DROPPED:
2536                         case FLAG_PASSING:
2537                         {
2538                                 // lock the flag, game is over
2539                                 set_movetype(flag, MOVETYPE_NONE);
2540                                 flag.takedamage = DAMAGE_NO;
2541                                 flag.solid = SOLID_NOT;
2542                                 flag.nextthink = false; // stop thinking
2543
2544                                 //dprint("stopping the ", flag.netname, " from moving.\n");
2545                                 break;
2546                         }
2547
2548                         default:
2549                         case FLAG_BASE:
2550                         case FLAG_CARRY:
2551                         {
2552                                 // do nothing for these flags
2553                                 break;
2554                         }
2555                 }
2556         }
2557 }
2558
2559 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2560 {
2561         entity bot = M_ARGV(0, entity);
2562
2563         havocbot_ctf_reset_role(bot);
2564         return true;
2565 }
2566
2567 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2568 {
2569         M_ARGV(1, string) = "ctf_team";
2570 }
2571
2572 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2573 {
2574         int record_page = M_ARGV(0, int);
2575         string ret_string = M_ARGV(1, string);
2576
2577         for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2578         {
2579                 if (MapInfo_Get_ByID(i))
2580                 {
2581                         float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2582
2583                         if(!r)
2584                                 continue;
2585
2586                         // TODO: uid2name
2587                         string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2588                         ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2589                 }
2590         }
2591
2592         M_ARGV(1, string) = ret_string;
2593 }
2594
2595 bool superspec_Spectate(entity this, entity targ); // TODO
2596 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2597 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2598 {
2599         entity player = M_ARGV(0, entity);
2600         string cmd_name = M_ARGV(1, string);
2601         int cmd_argc = M_ARGV(2, int);
2602
2603         if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2604
2605         if(cmd_name == "followfc")
2606         {
2607                 if(!g_ctf)
2608                         return true;
2609
2610                 int _team = 0;
2611                 bool found = false;
2612
2613                 if(cmd_argc == 2)
2614                 {
2615                         switch(argv(1))
2616                         {
2617                                 case "red":    if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2618                                 case "blue":   if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2619                                 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2620                                 case "pink":   if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2621                         }
2622                 }
2623
2624                 FOREACH_CLIENT(IS_PLAYER(it), {
2625                         if(it.flagcarried && (it.team == _team || _team == 0))
2626                         {
2627                                 found = true;
2628                                 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2629                                         continue; // already spectating this fc, try another
2630                                 return superspec_Spectate(player, it);
2631                         }
2632                 });
2633
2634                 if(!found)
2635                         superspec_msg("", "", player, "No active flag carrier\n", 1);
2636                 return true;
2637         }
2638 }
2639
2640 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2641 {
2642         entity frag_target = M_ARGV(0, entity);
2643
2644         if(frag_target.flagcarried)
2645                 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2646 }
2647
2648 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2649 {
2650         entity player = M_ARGV(0, entity);
2651         if(player.flagcarried)
2652                 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2653 }
2654
2655
2656 // ==========
2657 // Spawnfuncs
2658 // ==========
2659
2660 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2661 CTF flag for team one (Red).
2662 Keys:
2663 "angle" Angle the flag will point (minus 90 degrees)...
2664 "model" model to use, note this needs red and blue as skins 0 and 1...
2665 "noise" sound played when flag is picked up...
2666 "noise1" sound played when flag is returned by a teammate...
2667 "noise2" sound played when flag is captured...
2668 "noise3" sound played when flag is lost in the field and respawns itself...
2669 "noise4" sound played when flag is dropped by a player...
2670 "noise5" sound played when flag touches the ground... */
2671 spawnfunc(item_flag_team1)
2672 {
2673         if(!g_ctf) { delete(this); return; }
2674
2675         ctf_FlagSetup(NUM_TEAM_1, this);
2676 }
2677
2678 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2679 CTF flag for team two (Blue).
2680 Keys:
2681 "angle" Angle the flag will point (minus 90 degrees)...
2682 "model" model to use, note this needs red and blue as skins 0 and 1...
2683 "noise" sound played when flag is picked up...
2684 "noise1" sound played when flag is returned by a teammate...
2685 "noise2" sound played when flag is captured...
2686 "noise3" sound played when flag is lost in the field and respawns itself...
2687 "noise4" sound played when flag is dropped by a player...
2688 "noise5" sound played when flag touches the ground... */
2689 spawnfunc(item_flag_team2)
2690 {
2691         if(!g_ctf) { delete(this); return; }
2692
2693         ctf_FlagSetup(NUM_TEAM_2, this);
2694 }
2695
2696 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2697 CTF flag for team three (Yellow).
2698 Keys:
2699 "angle" Angle the flag will point (minus 90 degrees)...
2700 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2701 "noise" sound played when flag is picked up...
2702 "noise1" sound played when flag is returned by a teammate...
2703 "noise2" sound played when flag is captured...
2704 "noise3" sound played when flag is lost in the field and respawns itself...
2705 "noise4" sound played when flag is dropped by a player...
2706 "noise5" sound played when flag touches the ground... */
2707 spawnfunc(item_flag_team3)
2708 {
2709         if(!g_ctf) { delete(this); return; }
2710
2711         ctf_FlagSetup(NUM_TEAM_3, this);
2712 }
2713
2714 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2715 CTF flag for team four (Pink).
2716 Keys:
2717 "angle" Angle the flag will point (minus 90 degrees)...
2718 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2719 "noise" sound played when flag is picked up...
2720 "noise1" sound played when flag is returned by a teammate...
2721 "noise2" sound played when flag is captured...
2722 "noise3" sound played when flag is lost in the field and respawns itself...
2723 "noise4" sound played when flag is dropped by a player...
2724 "noise5" sound played when flag touches the ground... */
2725 spawnfunc(item_flag_team4)
2726 {
2727         if(!g_ctf) { delete(this); return; }
2728
2729         ctf_FlagSetup(NUM_TEAM_4, this);
2730 }
2731
2732 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2733 CTF flag (Neutral).
2734 Keys:
2735 "angle" Angle the flag will point (minus 90 degrees)...
2736 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2737 "noise" sound played when flag is picked up...
2738 "noise1" sound played when flag is returned by a teammate...
2739 "noise2" sound played when flag is captured...
2740 "noise3" sound played when flag is lost in the field and respawns itself...
2741 "noise4" sound played when flag is dropped by a player...
2742 "noise5" sound played when flag touches the ground... */
2743 spawnfunc(item_flag_neutral)
2744 {
2745         if(!g_ctf) { delete(this); return; }
2746         if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2747
2748         ctf_FlagSetup(0, this);
2749 }
2750
2751 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2752 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2753 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.
2754 Keys:
2755 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2756 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2757 spawnfunc(ctf_team)
2758 {
2759         if(!g_ctf) { delete(this); return; }
2760
2761         this.team = this.cnt + 1;
2762 }
2763
2764 // compatibility for quake maps
2765 spawnfunc(team_CTF_redflag)    { spawnfunc_item_flag_team1(this);    }
2766 spawnfunc(team_CTF_blueflag)   { spawnfunc_item_flag_team2(this);    }
2767 spawnfunc(info_player_team1);
2768 spawnfunc(team_CTF_redplayer)  { spawnfunc_info_player_team1(this);  }
2769 spawnfunc(team_CTF_redspawn)   { spawnfunc_info_player_team1(this);  }
2770 spawnfunc(info_player_team2);
2771 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this);  }
2772 spawnfunc(team_CTF_bluespawn)  { spawnfunc_info_player_team2(this);  }
2773
2774 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this);  }
2775 spawnfunc(team_neutralobelisk)  { spawnfunc_item_flag_neutral(this);  }
2776
2777 // compatibility for wop maps
2778 spawnfunc(team_redplayer)      { spawnfunc_info_player_team1(this);  }
2779 spawnfunc(team_blueplayer)     { spawnfunc_info_player_team2(this);  }
2780 spawnfunc(team_ctl_redlolly)   { spawnfunc_item_flag_team1(this);    }
2781 spawnfunc(team_CTL_redlolly)   { spawnfunc_item_flag_team1(this);    }
2782 spawnfunc(team_ctl_bluelolly)  { spawnfunc_item_flag_team2(this);    }
2783 spawnfunc(team_CTL_bluelolly)  { spawnfunc_item_flag_team2(this);    }
2784
2785
2786 // ==============
2787 // Initialization
2788 // ==============
2789
2790 // scoreboard setup
2791 void ctf_ScoreRules(int teams)
2792 {
2793         GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2794         field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2795         field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2796         field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2797         field(SP_CTF_PICKUPS, "pickups", 0);
2798         field(SP_CTF_FCKILLS, "fckills", 0);
2799         field(SP_CTF_RETURNS, "returns", 0);
2800         field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2801         });
2802 }
2803
2804 // code from here on is just to support maps that don't have flag and team entities
2805 void ctf_SpawnTeam (string teamname, int teamcolor)
2806 {
2807         entity this = new_pure(ctf_team);
2808         this.netname = teamname;
2809         this.cnt = teamcolor - 1;
2810         this.spawnfunc_checked = true;
2811         this.team = teamcolor;
2812 }
2813
2814 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2815 {
2816         ctf_teams = 0;
2817
2818         entity tmp_entity;
2819         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2820         {
2821                 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2822                 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2823
2824                 switch(tmp_entity.team)
2825                 {
2826                         case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2827                         case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2828                         case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2829                         case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2830                 }
2831                 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2832         }
2833
2834         havocbot_ctf_calculate_middlepoint();
2835
2836         if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2837         {
2838                 ctf_teams = 0; // so set the default red and blue teams
2839                 BITSET_ASSIGN(ctf_teams, BIT(0));
2840                 BITSET_ASSIGN(ctf_teams, BIT(1));
2841         }
2842
2843         //ctf_teams = bound(2, ctf_teams, 4);
2844
2845         // if no teams are found, spawn defaults
2846         if(find(NULL, classname, "ctf_team") == NULL)
2847         {
2848                 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2849                 if(ctf_teams & BIT(0))
2850                         ctf_SpawnTeam("Red", NUM_TEAM_1);
2851                 if(ctf_teams & BIT(1))
2852                         ctf_SpawnTeam("Blue", NUM_TEAM_2);
2853                 if(ctf_teams & BIT(2))
2854                         ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2855                 if(ctf_teams & BIT(3))
2856                         ctf_SpawnTeam("Pink", NUM_TEAM_4);
2857         }
2858
2859         ctf_ScoreRules(ctf_teams);
2860 }
2861
2862 void ctf_Initialize()
2863 {
2864         CTF_FLAG = NEW(Flag);
2865         record_type = CTF_RECORD;
2866         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2867
2868         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2869         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2870         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2871
2872         InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);
2873 }