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