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