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