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