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