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