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