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