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