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