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