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