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