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