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