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