]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/ctf/sv_ctf.qc
Merge branch 'martin-t/keybinder' into 'master'
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / gamemodes / gamemode / ctf / sv_ctf.qc
1 #include "sv_ctf.qh"
2
3 #include <common/effects/all.qh>
4 #include <common/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         // check for flag respawn being called twice in a row
1151         if(flag.last_respawn > time - 0.5)
1152                 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1153
1154         flag.last_respawn = time;
1155
1156         // reset the player (if there is one)
1157         if((flag.owner) && (flag.owner.flagcarried == flag))
1158         {
1159                 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1160                 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1161                 WaypointSprite_Kill(flag.wps_flagcarrier);
1162
1163                 flag.owner.flagcarried = NULL;
1164                 GameRules_scoring_vip(flag.owner, false);
1165
1166                 if(flag.speedrunning)
1167                         ctf_FakeTimeLimit(flag.owner, -1);
1168         }
1169
1170         if((flag.owner) && (flag.owner.vehicle))
1171                 flag.scale = FLAG_SCALE;
1172
1173         if(flag.ctf_status == FLAG_DROPPED)
1174                 { WaypointSprite_Kill(flag.wps_flagdropped); }
1175
1176         // reset the flag
1177         setattachment(flag, NULL, "");
1178         setorigin(flag, flag.ctf_spawnorigin);
1179
1180         //set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS)); // would be desired, except maps that want floating flags have it set to fall!
1181         set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1182         flag.takedamage = DAMAGE_NO;
1183         SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1184         flag.solid = SOLID_TRIGGER;
1185         flag.velocity = '0 0 0';
1186         flag.angles = flag.mangle;
1187         flag.flags = FL_ITEM | FL_NOTARGET;
1188
1189         flag.ctf_status = FLAG_BASE;
1190         flag.owner = NULL;
1191         flag.pass_distance = 0;
1192         flag.pass_sender = NULL;
1193         flag.pass_target = NULL;
1194         flag.ctf_dropper = NULL;
1195         flag.ctf_pickuptime = 0;
1196         flag.ctf_droptime = 0;
1197         flag.ctf_flagdamaged_byworld = false;
1198         navigation_dynamicgoal_unset(flag);
1199
1200         ctf_CheckStalemate();
1201 }
1202
1203 void ctf_Reset(entity this)
1204 {
1205         if(this.owner && IS_PLAYER(this.owner))
1206                 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1207
1208         this.enemy = NULL;
1209         ctf_RespawnFlag(this);
1210 }
1211
1212 bool ctf_FlagBase_Customize(entity this, entity client)
1213 {
1214         entity e = WaypointSprite_getviewentity(client);
1215         entity wp_owner = this.owner;
1216         entity flag = e.flagcarried;
1217         if(flag && CTF_SAMETEAM(e, flag))
1218                 return false;
1219         if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1220                 return false;
1221         return true;
1222 }
1223
1224 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1225 {
1226         // bot waypoints
1227         waypoint_spawnforitem_force(this, this.origin);
1228         navigation_dynamicgoal_init(this, true);
1229
1230         // waypointsprites
1231         entity basename;
1232         switch (this.team)
1233         {
1234                 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1235                 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1236                 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1237                 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1238                 default: basename = WP_FlagBaseNeutral; break;
1239         }
1240
1241         if(autocvar_g_ctf_flag_waypoint)
1242         {
1243                 entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1244                 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1245                 wp.fade_rate = autocvar_g_ctf_flag_waypoint_maxdistance;
1246                 WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1247                 setcefc(wp, ctf_FlagBase_Customize);
1248         }
1249
1250         // captureshield setup
1251         ctf_CaptureShield_Spawn(this);
1252 }
1253
1254 .bool pushable;
1255
1256 void ctf_FlagSetup(int teamnum, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1257 {
1258         // main setup
1259         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1260         ctf_worldflaglist = flag;
1261
1262         setattachment(flag, NULL, "");
1263
1264         flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnum), Team_ColorName_Upper(teamnum)));
1265         flag.team = teamnum;
1266         flag.classname = "item_flag_team";
1267         flag.target = "###item###"; // for finding the nearest item using findnearest
1268         flag.flags = FL_ITEM | FL_NOTARGET;
1269         IL_PUSH(g_items, flag);
1270         flag.solid = SOLID_TRIGGER;
1271         flag.takedamage = DAMAGE_NO;
1272         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1273         flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1274         SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1275         flag.event_damage = ctf_FlagDamage;
1276         flag.pushable = true;
1277         flag.teleportable = TELEPORT_NORMAL;
1278         flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1279         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1280         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1281         if(flag.damagedbycontents)
1282                 IL_PUSH(g_damagedbycontents, flag);
1283         flag.velocity = '0 0 0';
1284         flag.mangle = flag.angles;
1285         flag.reset = ctf_Reset;
1286         settouch(flag, ctf_FlagTouch);
1287         setthink(flag, ctf_FlagThink);
1288         flag.nextthink = time + FLAG_THINKRATE;
1289         flag.ctf_status = FLAG_BASE;
1290
1291         // crudely force them all to 0
1292         if(autocvar_g_ctf_score_ignore_fields)
1293                 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1294
1295         string teamname = Static_Team_ColorName_Lower(teamnum);
1296         // appearence
1297         if(!flag.scale)                         { flag.scale = FLAG_SCALE; }
1298         if(flag.skin == 0)                      { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1299         if(flag.model == "")            { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1300         if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnum).eent_eff_name; }
1301         if (flag.passeffect == "")      { flag.passeffect = EFFECT_PASS(teamnum).eent_eff_name; }
1302         if (flag.capeffect == "")       { flag.capeffect = EFFECT_CAP(teamnum).eent_eff_name; }
1303
1304         // sounds
1305 #define X(s,b) \
1306                 if(flag.s == "") flag.s = b; \
1307                 precache_sound(flag.s);
1308
1309         X(snd_flag_taken,               strzone(SND(CTF_TAKEN(teamnum))))
1310         X(snd_flag_returned,    strzone(SND(CTF_RETURNED(teamnum))))
1311         X(snd_flag_capture,     strzone(SND(CTF_CAPTURE(teamnum))))
1312         X(snd_flag_dropped,     strzone(SND(CTF_DROPPED(teamnum))))
1313         X(snd_flag_respawn,     strzone(SND(CTF_RESPAWN)))
1314         X(snd_flag_touch,               strzone(SND(CTF_TOUCH)))
1315         X(snd_flag_pass,                strzone(SND(CTF_PASS)))
1316 #undef X
1317
1318         // precache
1319         precache_model(flag.model);
1320
1321         // appearence
1322         _setmodel(flag, flag.model); // precision set below
1323         setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1324         flag.m_mins = flag.mins; // store these for squash checks
1325         flag.m_maxs = flag.maxs;
1326         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1327
1328         if(autocvar_g_ctf_flag_glowtrails)
1329         {
1330                 switch(teamnum)
1331                 {
1332                         case NUM_TEAM_1: flag.glow_color = 251; break;
1333                         case NUM_TEAM_2: flag.glow_color = 210; break;
1334                         case NUM_TEAM_3: flag.glow_color = 110; break;
1335                         case NUM_TEAM_4: flag.glow_color = 145; break;
1336                         default:                 flag.glow_color = 254; break;
1337                 }
1338                 flag.glow_size = 25;
1339                 flag.glow_trail = 1;
1340         }
1341
1342         flag.effects |= EF_LOWPRECISION;
1343         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1344         if(autocvar_g_ctf_dynamiclights)
1345         {
1346                 switch(teamnum)
1347                 {
1348                         case NUM_TEAM_1: flag.effects |= EF_RED; break;
1349                         case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1350                         case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1351                         case NUM_TEAM_4: flag.effects |= EF_RED; break;
1352                         default:                 flag.effects |= EF_DIMLIGHT; break;
1353                 }
1354         }
1355
1356         // flag placement
1357         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1358         {
1359                 flag.dropped_origin = flag.origin;
1360                 flag.noalign = true;
1361                 set_movetype(flag, MOVETYPE_NONE);
1362         }
1363         else // drop to floor, automatically find a platform and set that as spawn origin
1364         {
1365                 flag.noalign = false;
1366                 droptofloor(flag);
1367                 set_movetype(flag, MOVETYPE_NONE);
1368         }
1369
1370         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1371 }
1372
1373
1374 // ================
1375 // Bot player logic
1376 // ================
1377
1378 // NOTE: LEGACY CODE, needs to be re-written!
1379
1380 void havocbot_ctf_calculate_middlepoint()
1381 {
1382         entity f;
1383         vector s = '0 0 0';
1384         vector fo = '0 0 0';
1385         int n = 0;
1386
1387         f = ctf_worldflaglist;
1388         while (f)
1389         {
1390                 fo = f.origin;
1391                 s = s + fo;
1392                 f = f.ctf_worldflagnext;
1393                 n++;
1394         }
1395         if(!n)
1396                 return;
1397
1398         havocbot_middlepoint = s / n;
1399         havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1400
1401         havocbot_symmetry_axis_m = 0;
1402         havocbot_symmetry_axis_q = 0;
1403         if(n == 2)
1404         {
1405                 // for symmetrical editing of waypoints
1406                 entity f1 = ctf_worldflaglist;
1407                 entity f2 = f1.ctf_worldflagnext;
1408                 float m = -(f1.origin.y - f2.origin.y) / (max(f1.origin.x - f2.origin.x, FLOAT_EPSILON));
1409                 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1410                 havocbot_symmetry_axis_m = m;
1411                 havocbot_symmetry_axis_q = q;
1412         }
1413         havocbot_symmetry_origin_order = n;
1414 }
1415
1416
1417 entity havocbot_ctf_find_flag(entity bot)
1418 {
1419         entity f;
1420         f = ctf_worldflaglist;
1421         while (f)
1422         {
1423                 if (CTF_SAMETEAM(bot, f))
1424                         return f;
1425                 f = f.ctf_worldflagnext;
1426         }
1427         return NULL;
1428 }
1429
1430 entity havocbot_ctf_find_enemy_flag(entity bot)
1431 {
1432         entity f;
1433         f = ctf_worldflaglist;
1434         while (f)
1435         {
1436                 if(ctf_oneflag)
1437                 {
1438                         if(CTF_DIFFTEAM(bot, f))
1439                         {
1440                                 if(f.team)
1441                                 {
1442                                         if(bot.flagcarried)
1443                                                 return f;
1444                                 }
1445                                 else if(!bot.flagcarried)
1446                                         return f;
1447                         }
1448                 }
1449                 else if (CTF_DIFFTEAM(bot, f))
1450                         return f;
1451                 f = f.ctf_worldflagnext;
1452         }
1453         return NULL;
1454 }
1455
1456 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1457 {
1458         if (!teamplay)
1459                 return 0;
1460
1461         int c = 0;
1462
1463         FOREACH_CLIENT(IS_PLAYER(it), {
1464                 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1465                         continue;
1466
1467                 if(vdist(it.origin - org, <, tc_radius))
1468                         ++c;
1469         });
1470
1471         return c;
1472 }
1473
1474 // unused
1475 #if 0
1476 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1477 {
1478         entity head;
1479         head = ctf_worldflaglist;
1480         while (head)
1481         {
1482                 if (CTF_SAMETEAM(this, head))
1483                         break;
1484                 head = head.ctf_worldflagnext;
1485         }
1486         if (head)
1487                 navigation_routerating(this, head, ratingscale, 10000);
1488 }
1489 #endif
1490
1491 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1492 {
1493         entity head;
1494         head = ctf_worldflaglist;
1495         while (head)
1496         {
1497                 if (CTF_SAMETEAM(this, head))
1498                 {
1499                         if (this.flagcarried)
1500                         if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1501                         {
1502                                 head = head.ctf_worldflagnext; // skip base if it has a different group
1503                                 continue;
1504                         }
1505                         break;
1506                 }
1507                 head = head.ctf_worldflagnext;
1508         }
1509         if (!head)
1510                 return;
1511
1512         navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1513 }
1514
1515 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1516 {
1517         entity head;
1518         head = ctf_worldflaglist;
1519         while (head)
1520         {
1521                 if(ctf_oneflag)
1522                 {
1523                         if(CTF_DIFFTEAM(this, head))
1524                         {
1525                                 if(head.team)
1526                                 {
1527                                         if(this.flagcarried)
1528                                                 break;
1529                                 }
1530                                 else if(!this.flagcarried)
1531                                         break;
1532                         }
1533                 }
1534                 else if(CTF_DIFFTEAM(this, head))
1535                         break;
1536                 head = head.ctf_worldflagnext;
1537         }
1538         if (head)
1539         {
1540                 if (head.ctf_status == FLAG_CARRY)
1541                 {
1542                         // adjust rating of our flag carrier depending on his health
1543                         head = head.tag_entity;
1544                         float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1545                         ratingscale += ratingscale * f * 0.1;
1546                 }
1547                 navigation_routerating(this, head, ratingscale, 10000);
1548         }
1549 }
1550
1551 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1552 {
1553         // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1554         /*
1555         if (!bot_waypoints_for_items)
1556         {
1557                 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1558                 return;
1559         }
1560         */
1561         entity head;
1562
1563         head = havocbot_ctf_find_enemy_flag(this);
1564
1565         if (!head)
1566                 return;
1567
1568         navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1569 }
1570
1571 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1572 {
1573         entity mf;
1574
1575         mf = havocbot_ctf_find_flag(this);
1576
1577         if(mf.ctf_status == FLAG_BASE)
1578                 return;
1579
1580         if(mf.tag_entity)
1581                 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1582 }
1583
1584 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1585 {
1586         entity head;
1587         head = ctf_worldflaglist;
1588         while (head)
1589         {
1590                 // flag is out in the field
1591                 if(head.ctf_status != FLAG_BASE)
1592                 if(head.tag_entity==NULL)       // dropped
1593                 {
1594                         if(df_radius)
1595                         {
1596                                 if(vdist(org - head.origin, <, df_radius))
1597                                         navigation_routerating(this, head, ratingscale, 10000);
1598                         }
1599                         else
1600                                 navigation_routerating(this, head, ratingscale, 10000);
1601                 }
1602
1603                 head = head.ctf_worldflagnext;
1604         }
1605 }
1606
1607 void havocbot_ctf_reset_role(entity this)
1608 {
1609         float cdefense, cmiddle, coffense;
1610         entity mf, ef;
1611
1612         if(IS_DEAD(this))
1613                 return;
1614
1615         // Check ctf flags
1616         if (this.flagcarried)
1617         {
1618                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1619                 return;
1620         }
1621
1622         mf = havocbot_ctf_find_flag(this);
1623         ef = havocbot_ctf_find_enemy_flag(this);
1624
1625         // Retrieve stolen flag
1626         if(mf.ctf_status!=FLAG_BASE)
1627         {
1628                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1629                 return;
1630         }
1631
1632         // If enemy flag is taken go to the middle to intercept pursuers
1633         if(ef.ctf_status!=FLAG_BASE)
1634         {
1635                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1636                 return;
1637         }
1638
1639         // if there is no one else on the team switch to offense
1640         int count = 0;
1641         // don't check if this bot is a player since it isn't true when the bot is added to the server
1642         FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1643
1644         if (count == 0)
1645         {
1646                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1647                 return;
1648         }
1649         else if (time < CS(this).jointime + 1)
1650         {
1651                 // if bots spawn all at once set good default roles
1652                 if (count == 1)
1653                 {
1654                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1655                         return;
1656                 }
1657                 else if (count == 2)
1658                 {
1659                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1660                         return;
1661                 }
1662         }
1663
1664         // Evaluate best position to take
1665         // Count mates on middle position
1666         cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1667
1668         // Count mates on defense position
1669         cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1670
1671         // Count mates on offense position
1672         coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1673
1674         if(cdefense<=coffense)
1675                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1676         else if(coffense<=cmiddle)
1677                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1678         else
1679                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1680
1681         // if bots spawn all at once assign them a more appropriated role after a while
1682         if (time < CS(this).jointime + 1 && count > 2)
1683                 this.havocbot_role_timeout = time + 10 + random() * 10;
1684 }
1685
1686 bool havocbot_ctf_is_basewaypoint(entity item)
1687 {
1688         if (item.classname != "waypoint")
1689                 return false;
1690
1691         entity head = ctf_worldflaglist;
1692         while (head)
1693         {
1694                 if (item == head.bot_basewaypoint)
1695                         return true;
1696                 head = head.ctf_worldflagnext;
1697         }
1698         return false;
1699 }
1700
1701 void havocbot_role_ctf_carrier(entity this)
1702 {
1703         if(IS_DEAD(this))
1704         {
1705                 havocbot_ctf_reset_role(this);
1706                 return;
1707         }
1708
1709         if (this.flagcarried == NULL)
1710         {
1711                 havocbot_ctf_reset_role(this);
1712                 return;
1713         }
1714
1715         if (navigation_goalrating_timeout(this))
1716         {
1717                 navigation_goalrating_start(this);
1718
1719                 // role: carrier
1720                 entity mf = havocbot_ctf_find_flag(this);
1721                 vector base_org = mf.dropped_origin;
1722                 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1723                 if(ctf_oneflag)
1724                         havocbot_goalrating_ctf_enemybase(this, base_rating);
1725                 else
1726                         havocbot_goalrating_ctf_ourbase(this, base_rating);
1727
1728                 // start collecting items very close to the bot but only inside of own base radius
1729                 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1730                         havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1731
1732                 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1733
1734                 navigation_goalrating_end(this);
1735
1736                 navigation_goalrating_timeout_set(this);
1737
1738                 entity goal = this.goalentity;
1739                 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1740                         this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1741
1742                 if (goal)
1743                         this.havocbot_cantfindflag = time + 10;
1744                 else if (time > this.havocbot_cantfindflag)
1745                 {
1746                         // Can't navigate to my own base, suicide!
1747                         // TODO: drop it and wander around
1748                         Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1749                         return;
1750                 }
1751         }
1752 }
1753
1754 void havocbot_role_ctf_escort(entity this)
1755 {
1756         entity mf, ef;
1757
1758         if(IS_DEAD(this))
1759         {
1760                 havocbot_ctf_reset_role(this);
1761                 return;
1762         }
1763
1764         if (this.flagcarried)
1765         {
1766                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1767                 return;
1768         }
1769
1770         // If enemy flag is back on the base switch to previous role
1771         ef = havocbot_ctf_find_enemy_flag(this);
1772         if(ef.ctf_status==FLAG_BASE)
1773         {
1774                 this.havocbot_role = this.havocbot_previous_role;
1775                 this.havocbot_role_timeout = 0;
1776                 return;
1777         }
1778         if (ef.ctf_status == FLAG_DROPPED)
1779         {
1780                 navigation_goalrating_timeout_expire(this, 1);
1781                 return;
1782         }
1783
1784         // If the flag carrier reached the base switch to defense
1785         mf = havocbot_ctf_find_flag(this);
1786         if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1787         {
1788                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1789                 return;
1790         }
1791
1792         // Set the role timeout if necessary
1793         if (!this.havocbot_role_timeout)
1794         {
1795                 this.havocbot_role_timeout = time + random() * 30 + 60;
1796         }
1797
1798         // If nothing happened just switch to previous role
1799         if (time > this.havocbot_role_timeout)
1800         {
1801                 this.havocbot_role = this.havocbot_previous_role;
1802                 this.havocbot_role_timeout = 0;
1803                 return;
1804         }
1805
1806         // Chase the flag carrier
1807         if (navigation_goalrating_timeout(this))
1808         {
1809                 navigation_goalrating_start(this);
1810
1811                 // role: escort
1812                 havocbot_goalrating_ctf_enemyflag(this, 10000);
1813                 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1814                 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1815
1816                 navigation_goalrating_end(this);
1817
1818                 navigation_goalrating_timeout_set(this);
1819         }
1820 }
1821
1822 void havocbot_role_ctf_offense(entity this)
1823 {
1824         entity mf, ef;
1825         vector pos;
1826
1827         if(IS_DEAD(this))
1828         {
1829                 havocbot_ctf_reset_role(this);
1830                 return;
1831         }
1832
1833         if (this.flagcarried)
1834         {
1835                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1836                 return;
1837         }
1838
1839         // Check flags
1840         mf = havocbot_ctf_find_flag(this);
1841         ef = havocbot_ctf_find_enemy_flag(this);
1842
1843         // Own flag stolen
1844         if(mf.ctf_status!=FLAG_BASE)
1845         {
1846                 if(mf.tag_entity)
1847                         pos = mf.tag_entity.origin;
1848                 else
1849                         pos = mf.origin;
1850
1851                 // Try to get it if closer than the enemy base
1852                 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1853                 {
1854                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1855                         return;
1856                 }
1857         }
1858
1859         // Escort flag carrier
1860         if(ef.ctf_status!=FLAG_BASE)
1861         {
1862                 if(ef.tag_entity)
1863                         pos = ef.tag_entity.origin;
1864                 else
1865                         pos = ef.origin;
1866
1867                 if(vdist(pos - mf.dropped_origin, >, 700))
1868                 {
1869                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1870                         return;
1871                 }
1872         }
1873
1874         // Set the role timeout if necessary
1875         if (!this.havocbot_role_timeout)
1876                 this.havocbot_role_timeout = time + 120;
1877
1878         if (time > this.havocbot_role_timeout)
1879         {
1880                 havocbot_ctf_reset_role(this);
1881                 return;
1882         }
1883
1884         if (navigation_goalrating_timeout(this))
1885         {
1886                 navigation_goalrating_start(this);
1887
1888                 // role: offense
1889                 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1890                 havocbot_goalrating_ctf_enemybase(this, 10000);
1891                 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1892
1893                 navigation_goalrating_end(this);
1894
1895                 navigation_goalrating_timeout_set(this);
1896         }
1897 }
1898
1899 // Retriever (temporary role):
1900 void havocbot_role_ctf_retriever(entity this)
1901 {
1902         entity mf;
1903
1904         if(IS_DEAD(this))
1905         {
1906                 havocbot_ctf_reset_role(this);
1907                 return;
1908         }
1909
1910         if (this.flagcarried)
1911         {
1912                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1913                 return;
1914         }
1915
1916         // If flag is back on the base switch to previous role
1917         mf = havocbot_ctf_find_flag(this);
1918         if(mf.ctf_status==FLAG_BASE)
1919         {
1920                 if (mf.enemy == this) // did this bot return the flag?
1921                         navigation_goalrating_timeout_force(this);
1922                 havocbot_ctf_reset_role(this);
1923                 return;
1924         }
1925
1926         if (!this.havocbot_role_timeout)
1927                 this.havocbot_role_timeout = time + 20;
1928
1929         if (time > this.havocbot_role_timeout)
1930         {
1931                 havocbot_ctf_reset_role(this);
1932                 return;
1933         }
1934
1935         if (navigation_goalrating_timeout(this))
1936         {
1937                 const float RT_RADIUS = 10000;
1938
1939                 navigation_goalrating_start(this);
1940
1941                 // role: retriever
1942                 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1943                 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1944                 havocbot_goalrating_ctf_enemybase(this, 8000);
1945                 entity ef = havocbot_ctf_find_enemy_flag(this);
1946                 vector enemy_base_org = ef.dropped_origin;
1947                 // start collecting items very close to the bot but only inside of enemy base radius
1948                 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1949                         havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1950                 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1951
1952                 navigation_goalrating_end(this);
1953
1954                 navigation_goalrating_timeout_set(this);
1955         }
1956 }
1957
1958 void havocbot_role_ctf_middle(entity this)
1959 {
1960         entity mf;
1961
1962         if(IS_DEAD(this))
1963         {
1964                 havocbot_ctf_reset_role(this);
1965                 return;
1966         }
1967
1968         if (this.flagcarried)
1969         {
1970                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1971                 return;
1972         }
1973
1974         mf = havocbot_ctf_find_flag(this);
1975         if(mf.ctf_status!=FLAG_BASE)
1976         {
1977                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1978                 return;
1979         }
1980
1981         if (!this.havocbot_role_timeout)
1982                 this.havocbot_role_timeout = time + 10;
1983
1984         if (time > this.havocbot_role_timeout)
1985         {
1986                 havocbot_ctf_reset_role(this);
1987                 return;
1988         }
1989
1990         if (navigation_goalrating_timeout(this))
1991         {
1992                 vector org;
1993
1994                 org = havocbot_middlepoint;
1995                 org.z = this.origin.z;
1996
1997                 navigation_goalrating_start(this);
1998
1999                 // role: middle
2000                 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
2001                 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
2002                 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2003                 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
2004                 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2005                 havocbot_goalrating_ctf_enemybase(this, 3000);
2006
2007                 navigation_goalrating_end(this);
2008
2009                 entity goal = this.goalentity;
2010                 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
2011                         this.goalentity_lock_timeout = time + 2;
2012
2013                 navigation_goalrating_timeout_set(this);
2014         }
2015 }
2016
2017 void havocbot_role_ctf_defense(entity this)
2018 {
2019         entity mf;
2020
2021         if(IS_DEAD(this))
2022         {
2023                 havocbot_ctf_reset_role(this);
2024                 return;
2025         }
2026
2027         if (this.flagcarried)
2028         {
2029                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2030                 return;
2031         }
2032
2033         // If own flag was captured
2034         mf = havocbot_ctf_find_flag(this);
2035         if(mf.ctf_status!=FLAG_BASE)
2036         {
2037                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2038                 return;
2039         }
2040
2041         if (!this.havocbot_role_timeout)
2042                 this.havocbot_role_timeout = time + 30;
2043
2044         if (time > this.havocbot_role_timeout)
2045         {
2046                 havocbot_ctf_reset_role(this);
2047                 return;
2048         }
2049         if (navigation_goalrating_timeout(this))
2050         {
2051                 vector org = mf.dropped_origin;
2052
2053                 navigation_goalrating_start(this);
2054
2055                 // if enemies are closer to our base, go there
2056                 entity closestplayer = NULL;
2057                 float distance, bestdistance = 10000;
2058                 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2059                         distance = vlen(org - it.origin);
2060                         if(distance<bestdistance)
2061                         {
2062                                 closestplayer = it;
2063                                 bestdistance = distance;
2064                         }
2065                 });
2066
2067                 // role: defense
2068                 if(closestplayer)
2069                 if(DIFF_TEAM(closestplayer, this))
2070                 if(vdist(org - this.origin, >, 1000))
2071                 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2072                         havocbot_goalrating_ctf_ourbase(this, 10000);
2073
2074                 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2075                 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2076                 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2077                 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2078                 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2079
2080                 navigation_goalrating_end(this);
2081
2082                 navigation_goalrating_timeout_set(this);
2083         }
2084 }
2085
2086 void havocbot_role_ctf_setrole(entity bot, int role)
2087 {
2088         string s = "(null)";
2089         switch(role)
2090         {
2091                 case HAVOCBOT_CTF_ROLE_CARRIER:
2092                         s = "carrier";
2093                         bot.havocbot_role = havocbot_role_ctf_carrier;
2094                         bot.havocbot_role_timeout = 0;
2095                         bot.havocbot_cantfindflag = time + 10;
2096                         if (bot.havocbot_previous_role != bot.havocbot_role)
2097                                 navigation_goalrating_timeout_force(bot);
2098                         break;
2099                 case HAVOCBOT_CTF_ROLE_DEFENSE:
2100                         s = "defense";
2101                         bot.havocbot_role = havocbot_role_ctf_defense;
2102                         bot.havocbot_role_timeout = 0;
2103                         break;
2104                 case HAVOCBOT_CTF_ROLE_MIDDLE:
2105                         s = "middle";
2106                         bot.havocbot_role = havocbot_role_ctf_middle;
2107                         bot.havocbot_role_timeout = 0;
2108                         break;
2109                 case HAVOCBOT_CTF_ROLE_OFFENSE:
2110                         s = "offense";
2111                         bot.havocbot_role = havocbot_role_ctf_offense;
2112                         bot.havocbot_role_timeout = 0;
2113                         break;
2114                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2115                         s = "retriever";
2116                         bot.havocbot_previous_role = bot.havocbot_role;
2117                         bot.havocbot_role = havocbot_role_ctf_retriever;
2118                         bot.havocbot_role_timeout = time + 10;
2119                         if (bot.havocbot_previous_role != bot.havocbot_role)
2120                                 navigation_goalrating_timeout_expire(bot, 2);
2121                         break;
2122                 case HAVOCBOT_CTF_ROLE_ESCORT:
2123                         s = "escort";
2124                         bot.havocbot_previous_role = bot.havocbot_role;
2125                         bot.havocbot_role = havocbot_role_ctf_escort;
2126                         bot.havocbot_role_timeout = time + 30;
2127                         if (bot.havocbot_previous_role != bot.havocbot_role)
2128                                 navigation_goalrating_timeout_expire(bot, 2);
2129                         break;
2130         }
2131         LOG_TRACE(bot.netname, " switched to ", s);
2132 }
2133
2134
2135 // ==============
2136 // Hook Functions
2137 // ==============
2138
2139 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2140 {
2141         entity player = M_ARGV(0, entity);
2142
2143         int t = 0, t2 = 0, t3 = 0;
2144         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)
2145
2146         // initially clear items so they can be set as necessary later.
2147         STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING         | CTF_RED_FLAG_TAKEN            | CTF_RED_FLAG_LOST
2148                                                    | CTF_BLUE_FLAG_CARRYING             | CTF_BLUE_FLAG_TAKEN           | CTF_BLUE_FLAG_LOST
2149                                                    | CTF_YELLOW_FLAG_CARRYING   | CTF_YELLOW_FLAG_TAKEN         | CTF_YELLOW_FLAG_LOST
2150                                                    | CTF_PINK_FLAG_CARRYING     | CTF_PINK_FLAG_TAKEN           | CTF_PINK_FLAG_LOST
2151                                                    | CTF_NEUTRAL_FLAG_CARRYING  | CTF_NEUTRAL_FLAG_TAKEN        | CTF_NEUTRAL_FLAG_LOST
2152                                                    | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2153
2154         // scan through all the flags and notify the client about them
2155         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2156         {
2157                 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING;              t2 = CTF_RED_FLAG_TAKEN;                t3 = CTF_RED_FLAG_LOST; }
2158                 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING;             t2 = CTF_BLUE_FLAG_TAKEN;               t3 = CTF_BLUE_FLAG_LOST; }
2159                 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING;   t2 = CTF_YELLOW_FLAG_TAKEN;             t3 = CTF_YELLOW_FLAG_LOST; }
2160                 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING;             t2 = CTF_PINK_FLAG_TAKEN;               t3 = CTF_PINK_FLAG_LOST; }
2161                 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; }
2162
2163                 switch(flag.ctf_status)
2164                 {
2165                         case FLAG_PASSING:
2166                         case FLAG_CARRY:
2167                         {
2168                                 if((flag.owner == player) || (flag.pass_sender == player))
2169                                         STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2170                                 else
2171                                         STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2172                                 break;
2173                         }
2174                         case FLAG_DROPPED:
2175                         {
2176                                 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2177                                 break;
2178                         }
2179                 }
2180         }
2181
2182         // item for stopping players from capturing the flag too often
2183         if(player.ctf_captureshielded)
2184                 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2185
2186         if(ctf_stalemate)
2187                 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2188
2189         // update the health of the flag carrier waypointsprite
2190         if(player.wps_flagcarrier)
2191                 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);
2192 }
2193
2194 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2195 {
2196         entity frag_attacker = M_ARGV(1, entity);
2197         entity frag_target = M_ARGV(2, entity);
2198         float frag_damage = M_ARGV(4, float);
2199         vector frag_force = M_ARGV(6, vector);
2200
2201         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2202         {
2203                 if(frag_target == frag_attacker) // damage done to yourself
2204                 {
2205                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2206                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2207                 }
2208                 else // damage done to everyone else
2209                 {
2210                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2211                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2212                 }
2213
2214                 M_ARGV(4, float) = frag_damage;
2215                 M_ARGV(6, vector) = frag_force;
2216         }
2217         else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2218         {
2219                 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
2220                         && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2221                 {
2222                         frag_target.wps_helpme_time = time;
2223                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2224                 }
2225                 // todo: add notification for when flag carrier needs help?
2226         }
2227 }
2228
2229 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2230 {
2231         entity frag_attacker = M_ARGV(1, entity);
2232         entity frag_target = M_ARGV(2, entity);
2233
2234         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2235         {
2236                 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2237                 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2238         }
2239
2240         if(frag_target.flagcarried)
2241         {
2242                 entity tmp_entity = frag_target.flagcarried;
2243                 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2244                 tmp_entity.ctf_dropper = NULL;
2245         }
2246 }
2247
2248 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2249 {
2250         M_ARGV(2, float) = 0; // frag score
2251         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2252 }
2253
2254 void ctf_RemovePlayer(entity player)
2255 {
2256         if(player.flagcarried)
2257                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2258
2259         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2260         {
2261                 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2262                 if(flag.pass_target == player) { flag.pass_target = NULL; }
2263                 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2264         }
2265 }
2266
2267 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2268 {
2269         entity player = M_ARGV(0, entity);
2270
2271         ctf_RemovePlayer(player);
2272 }
2273
2274 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2275 {
2276         entity player = M_ARGV(0, entity);
2277
2278         ctf_RemovePlayer(player);
2279 }
2280
2281 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2282 {
2283         if(!autocvar_g_ctf_leaderboard)
2284                 return;
2285
2286         entity player = M_ARGV(0, entity);
2287
2288         if(IS_REAL_CLIENT(player))
2289         {
2290                 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2291                 race_send_rankings_cnt(MSG_ONE);
2292                 for (int i = 1; i <= m; ++i)
2293                 {
2294                         race_SendRankings(i, 0, 0, MSG_ONE);
2295                 }
2296         }
2297 }
2298
2299 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2300 {
2301         if(!autocvar_g_ctf_leaderboard)
2302                 return;
2303
2304         entity player = M_ARGV(0, entity);
2305
2306         if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2307         {
2308                 if (!player.stored_netname)
2309                         player.stored_netname = strzone(uid2name(player.crypto_idfp));
2310                 if(player.stored_netname != player.netname)
2311                 {
2312                         db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2313                         strcpy(player.stored_netname, player.netname);
2314                 }
2315         }
2316 }
2317
2318 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2319 {
2320         entity player = M_ARGV(0, entity);
2321
2322         if(player.flagcarried)
2323         if(!autocvar_g_ctf_portalteleport)
2324                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2325 }
2326
2327 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2328 {
2329         if(MUTATOR_RETURNVALUE || game_stopped) return;
2330
2331         entity player = M_ARGV(0, entity);
2332
2333         if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2334         {
2335                 // pass the flag to a team mate
2336                 if(autocvar_g_ctf_pass)
2337                 {
2338                         entity head, closest_target = NULL;
2339                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2340
2341                         while(head) // find the closest acceptable target to pass to
2342                         {
2343                                 if(IS_PLAYER(head) && !IS_DEAD(head))
2344                                 if(head != player && SAME_TEAM(head, player))
2345                                 if(!head.speedrunning && !head.vehicle)
2346                                 {
2347                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2348                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2349                                         vector passer_center = CENTER_OR_VIEWOFS(player);
2350
2351                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2352                                         {
2353                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2354                                                 {
2355                                                         if(IS_BOT_CLIENT(head))
2356                                                         {
2357                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2358                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
2359                                                         }
2360                                                         else
2361                                                         {
2362                                                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2363                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2364                                                         }
2365                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2366                                                         return true;
2367                                                 }
2368                                                 else if(player.flagcarried && !head.flagcarried)
2369                                                 {
2370                                                         if(closest_target)
2371                                                         {
2372                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2373                                                                 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2374                                                                         { closest_target = head; }
2375                                                         }
2376                                                         else { closest_target = head; }
2377                                                 }
2378                                         }
2379                                 }
2380                                 head = head.chain;
2381                         }
2382
2383                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2384                 }
2385
2386                 // throw the flag in front of you
2387                 if(autocvar_g_ctf_throw && player.flagcarried)
2388                 {
2389                         if(player.throw_count == -1)
2390                         {
2391                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2392                                 {
2393                                         player.throw_prevtime = time;
2394                                         player.throw_count = 1;
2395                                         ctf_Handle_Throw(player, NULL, DROP_THROW);
2396                                         return true;
2397                                 }
2398                                 else
2399                                 {
2400                                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2401                                         return false;
2402                                 }
2403                         }
2404                         else
2405                         {
2406                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2407                                 else { player.throw_count += 1; }
2408                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2409
2410                                 player.throw_prevtime = time;
2411                                 ctf_Handle_Throw(player, NULL, DROP_THROW);
2412                                 return true;
2413                         }
2414                 }
2415         }
2416 }
2417
2418 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2419 {
2420         entity player = M_ARGV(0, entity);
2421
2422         if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2423         {
2424                 player.wps_helpme_time = time;
2425                 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2426         }
2427         else // create a normal help me waypointsprite
2428         {
2429                 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2430                 WaypointSprite_Ping(player.wps_helpme);
2431         }
2432
2433         return true;
2434 }
2435
2436 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2437 {
2438         entity player = M_ARGV(0, entity);
2439         entity veh = M_ARGV(1, entity);
2440
2441         if(player.flagcarried)
2442         {
2443                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2444                 {
2445                         ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2446                 }
2447                 else
2448                 {
2449                         player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2450                         setattachment(player.flagcarried, veh, "");
2451                         setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2452                         player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2453                         //player.flagcarried.angles = '0 0 0';
2454                 }
2455                 return true;
2456         }
2457 }
2458
2459 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2460 {
2461         entity player = M_ARGV(0, entity);
2462
2463         if(player.flagcarried)
2464         {
2465                 setattachment(player.flagcarried, player, "");
2466                 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2467                 player.flagcarried.scale = FLAG_SCALE;
2468                 player.flagcarried.angles = '0 0 0';
2469                 player.flagcarried.nodrawtoclient = NULL;
2470                 return true;
2471         }
2472 }
2473
2474 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2475 {
2476         entity player = M_ARGV(0, entity);
2477
2478         if(player.flagcarried)
2479         {
2480                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2481                 ctf_RespawnFlag(player.flagcarried);
2482                 return true;
2483         }
2484 }
2485
2486 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2487 {
2488         entity flag; // temporary entity for the search method
2489
2490         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2491         {
2492                 switch(flag.ctf_status)
2493                 {
2494                         case FLAG_DROPPED:
2495                         case FLAG_PASSING:
2496                         {
2497                                 // lock the flag, game is over
2498                                 set_movetype(flag, MOVETYPE_NONE);
2499                                 flag.takedamage = DAMAGE_NO;
2500                                 flag.solid = SOLID_NOT;
2501                                 flag.nextthink = false; // stop thinking
2502
2503                                 //dprint("stopping the ", flag.netname, " from moving.\n");
2504                                 break;
2505                         }
2506
2507                         default:
2508                         case FLAG_BASE:
2509                         case FLAG_CARRY:
2510                         {
2511                                 // do nothing for these flags
2512                                 break;
2513                         }
2514                 }
2515         }
2516 }
2517
2518 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2519 {
2520         entity bot = M_ARGV(0, entity);
2521
2522         havocbot_ctf_reset_role(bot);
2523         return true;
2524 }
2525
2526 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2527 {
2528         M_ARGV(1, string) = "ctf_team";
2529 }
2530
2531 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2532 {
2533         entity spectatee = M_ARGV(0, entity);
2534         entity client = M_ARGV(1, entity);
2535
2536         STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2537 }
2538
2539 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2540 {
2541         int record_page = M_ARGV(0, int);
2542         string ret_string = M_ARGV(1, string);
2543
2544         for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2545         {
2546                 if (MapInfo_Get_ByID(i))
2547                 {
2548                         float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2549
2550                         if(!r)
2551                                 continue;
2552
2553                         // TODO: uid2name
2554                         string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2555                         ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2556                 }
2557         }
2558
2559         M_ARGV(1, string) = ret_string;
2560 }
2561
2562 bool superspec_Spectate(entity this, entity targ); // TODO
2563 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2564 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2565 {
2566         entity player = M_ARGV(0, entity);
2567         string cmd_name = M_ARGV(1, string);
2568         int cmd_argc = M_ARGV(2, int);
2569
2570         if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2571
2572         if(cmd_name == "followfc")
2573         {
2574                 if(!g_ctf)
2575                         return true;
2576
2577                 int _team = 0;
2578                 bool found = false;
2579
2580                 if(cmd_argc == 2)
2581                 {
2582                         switch(argv(1))
2583                         {
2584                                 case "red":    if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2585                                 case "blue":   if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2586                                 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2587                                 case "pink":   if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2588                         }
2589                 }
2590
2591                 FOREACH_CLIENT(IS_PLAYER(it), {
2592                         if(it.flagcarried && (it.team == _team || _team == 0))
2593                         {
2594                                 found = true;
2595                                 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2596                                         continue; // already spectating this fc, try another
2597                                 return superspec_Spectate(player, it);
2598                         }
2599                 });
2600
2601                 if(!found)
2602                         superspec_msg("", "", player, "No active flag carrier\n", 1);
2603                 return true;
2604         }
2605 }
2606
2607 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2608 {
2609         entity frag_target = M_ARGV(0, entity);
2610
2611         if(frag_target.flagcarried)
2612                 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2613 }
2614
2615 MUTATOR_HOOKFUNCTION(ctf, LogDeath_AppendItemCodes)
2616 {
2617         entity player = M_ARGV(0, entity);
2618         if(player.flagcarried)
2619                 M_ARGV(1, string) = strcat(M_ARGV(1, string), "F"); // item codes
2620 }
2621
2622
2623 // ==========
2624 // Spawnfuncs
2625 // ==========
2626
2627 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2628 CTF flag for team one (Red).
2629 Keys:
2630 "angle" Angle the flag will point (minus 90 degrees)...
2631 "model" model to use, note this needs red and blue as skins 0 and 1...
2632 "noise" sound played when flag is picked up...
2633 "noise1" sound played when flag is returned by a teammate...
2634 "noise2" sound played when flag is captured...
2635 "noise3" sound played when flag is lost in the field and respawns itself...
2636 "noise4" sound played when flag is dropped by a player...
2637 "noise5" sound played when flag touches the ground... */
2638 spawnfunc(item_flag_team1)
2639 {
2640         if(!g_ctf) { delete(this); return; }
2641
2642         ctf_FlagSetup(NUM_TEAM_1, this);
2643 }
2644
2645 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2646 CTF flag for team two (Blue).
2647 Keys:
2648 "angle" Angle the flag will point (minus 90 degrees)...
2649 "model" model to use, note this needs red and blue as skins 0 and 1...
2650 "noise" sound played when flag is picked up...
2651 "noise1" sound played when flag is returned by a teammate...
2652 "noise2" sound played when flag is captured...
2653 "noise3" sound played when flag is lost in the field and respawns itself...
2654 "noise4" sound played when flag is dropped by a player...
2655 "noise5" sound played when flag touches the ground... */
2656 spawnfunc(item_flag_team2)
2657 {
2658         if(!g_ctf) { delete(this); return; }
2659
2660         ctf_FlagSetup(NUM_TEAM_2, this);
2661 }
2662
2663 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2664 CTF flag for team three (Yellow).
2665 Keys:
2666 "angle" Angle the flag will point (minus 90 degrees)...
2667 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2668 "noise" sound played when flag is picked up...
2669 "noise1" sound played when flag is returned by a teammate...
2670 "noise2" sound played when flag is captured...
2671 "noise3" sound played when flag is lost in the field and respawns itself...
2672 "noise4" sound played when flag is dropped by a player...
2673 "noise5" sound played when flag touches the ground... */
2674 spawnfunc(item_flag_team3)
2675 {
2676         if(!g_ctf) { delete(this); return; }
2677
2678         ctf_FlagSetup(NUM_TEAM_3, this);
2679 }
2680
2681 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2682 CTF flag for team four (Pink).
2683 Keys:
2684 "angle" Angle the flag will point (minus 90 degrees)...
2685 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2686 "noise" sound played when flag is picked up...
2687 "noise1" sound played when flag is returned by a teammate...
2688 "noise2" sound played when flag is captured...
2689 "noise3" sound played when flag is lost in the field and respawns itself...
2690 "noise4" sound played when flag is dropped by a player...
2691 "noise5" sound played when flag touches the ground... */
2692 spawnfunc(item_flag_team4)
2693 {
2694         if(!g_ctf) { delete(this); return; }
2695
2696         ctf_FlagSetup(NUM_TEAM_4, this);
2697 }
2698
2699 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2700 CTF flag (Neutral).
2701 Keys:
2702 "angle" Angle the flag will point (minus 90 degrees)...
2703 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2704 "noise" sound played when flag is picked up...
2705 "noise1" sound played when flag is returned by a teammate...
2706 "noise2" sound played when flag is captured...
2707 "noise3" sound played when flag is lost in the field and respawns itself...
2708 "noise4" sound played when flag is dropped by a player...
2709 "noise5" sound played when flag touches the ground... */
2710 spawnfunc(item_flag_neutral)
2711 {
2712         if(!g_ctf) { delete(this); return; }
2713         if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2714
2715         ctf_FlagSetup(0, this);
2716 }
2717
2718 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2719 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2720 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.
2721 Keys:
2722 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2723 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2724 spawnfunc(ctf_team)
2725 {
2726         if(!g_ctf) { delete(this); return; }
2727
2728         this.classname = "ctf_team";
2729         this.team = this.cnt + 1;
2730 }
2731
2732 // compatibility for quake maps
2733 spawnfunc(team_CTF_redflag)    { spawnfunc_item_flag_team1(this);    }
2734 spawnfunc(team_CTF_blueflag)   { spawnfunc_item_flag_team2(this);    }
2735 spawnfunc(info_player_team1);
2736 spawnfunc(team_CTF_redplayer)  { spawnfunc_info_player_team1(this);  }
2737 spawnfunc(team_CTF_redspawn)   { spawnfunc_info_player_team1(this);  }
2738 spawnfunc(info_player_team2);
2739 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this);  }
2740 spawnfunc(team_CTF_bluespawn)  { spawnfunc_info_player_team2(this);  }
2741
2742 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this);  }
2743 spawnfunc(team_neutralobelisk)  { spawnfunc_item_flag_neutral(this);  }
2744
2745 // compatibility for wop maps
2746 spawnfunc(team_redplayer)      { spawnfunc_info_player_team1(this);  }
2747 spawnfunc(team_blueplayer)     { spawnfunc_info_player_team2(this);  }
2748 spawnfunc(team_ctl_redlolly)   { spawnfunc_item_flag_team1(this);    }
2749 spawnfunc(team_CTL_redlolly)   { spawnfunc_item_flag_team1(this);    }
2750 spawnfunc(team_ctl_bluelolly)  { spawnfunc_item_flag_team2(this);    }
2751 spawnfunc(team_CTL_bluelolly)  { spawnfunc_item_flag_team2(this);    }
2752
2753
2754 // ==============
2755 // Initialization
2756 // ==============
2757
2758 // scoreboard setup
2759 void ctf_ScoreRules(int teams)
2760 {
2761         GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2762         field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2763         field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2764         field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2765         field(SP_CTF_PICKUPS, "pickups", 0);
2766         field(SP_CTF_FCKILLS, "fckills", 0);
2767         field(SP_CTF_RETURNS, "returns", 0);
2768         field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2769         });
2770 }
2771
2772 // code from here on is just to support maps that don't have flag and team entities
2773 void ctf_SpawnTeam (string teamname, int teamcolor)
2774 {
2775         entity this = new_pure(ctf_team);
2776         this.netname = teamname;
2777         this.cnt = teamcolor - 1;
2778         this.spawnfunc_checked = true;
2779         this.team = teamcolor;
2780 }
2781
2782 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2783 {
2784         ctf_teams = 0;
2785
2786         entity tmp_entity;
2787         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2788         {
2789                 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2790                 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2791
2792                 switch(tmp_entity.team)
2793                 {
2794                         case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2795                         case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2796                         case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2797                         case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2798                 }
2799                 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2800         }
2801
2802         havocbot_ctf_calculate_middlepoint();
2803
2804         if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2805         {
2806                 ctf_teams = 0; // so set the default red and blue teams
2807                 BITSET_ASSIGN(ctf_teams, BIT(0));
2808                 BITSET_ASSIGN(ctf_teams, BIT(1));
2809         }
2810
2811         //ctf_teams = bound(2, ctf_teams, 4);
2812
2813         // if no teams are found, spawn defaults
2814         if(find(NULL, classname, "ctf_team") == NULL)
2815         {
2816                 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2817                 if(ctf_teams & BIT(0))
2818                         ctf_SpawnTeam("Red", NUM_TEAM_1);
2819                 if(ctf_teams & BIT(1))
2820                         ctf_SpawnTeam("Blue", NUM_TEAM_2);
2821                 if(ctf_teams & BIT(2))
2822                         ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2823                 if(ctf_teams & BIT(3))
2824                         ctf_SpawnTeam("Pink", NUM_TEAM_4);
2825         }
2826
2827         ctf_ScoreRules(ctf_teams);
2828 }
2829
2830 void ctf_Initialize()
2831 {
2832         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2833
2834         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2835         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2836         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2837
2838         InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);
2839 }