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