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