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