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