]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_ctf.qc
Merge branch 'master' into divVerent/4team_ctf
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_ctf.qc
1 // ================================================================
2 //  Official capture the flag game mode coding, reworked by Samual
3 //  Last updated: September, 2012
4 // ================================================================
5
6 void ctf_FakeTimeLimit(entity e, float t)
7 {
8         msg_entity = e;
9         WriteByte(MSG_ONE, 3); // svc_updatestat
10         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
11         if(t < 0)
12                 WriteCoord(MSG_ONE, autocvar_timelimit);
13         else
14                 WriteCoord(MSG_ONE, (t + 1) / 60);
15 }
16
17 void ctf_EventLog(string mode, float flagteam, entity actor) // use an alias for easy changing and quick editing later
18 {
19         if(autocvar_sv_eventlog)
20                 GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
21 }
22
23 void ctf_CaptureRecord(entity flag, entity player)
24 {
25         entity tmp_entity;
26         float cap_record = ctf_captimerecord;
27         float cap_time = (time - flag.ctf_pickuptime);
28         string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
29         
30         // notify about shit
31         FOR_EACH_REALCLIENT(tmp_entity)
32         {
33                 if(tmp_entity.CAPTURE_VERBOSE)
34                 {
35                         if(!ctf_captimerecord) { Send_Notification(NOTIF_ONE_ONLY, tmp_entity, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
36                         else if(cap_time < cap_record) { Send_Notification(NOTIF_ONE_ONLY, tmp_entity, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
37                         else { Send_Notification(NOTIF_ONE_ONLY, tmp_entity, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
38                 }
39                 else { Send_Notification(NOTIF_ONE_ONLY, tmp_entity, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_), player.netname); }
40         }
41
42         // the previous notification broadcast is only sent to real clients, this will notify server log too
43         if(server_is_dedicated)
44         {
45                 if(autocvar_notification_ctf_capture_verbose)
46                 {
47                         if(!ctf_captimerecord) { Local_Notification(MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
48                         else if(cap_time < cap_record) { Local_Notification(MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
49                         else { Local_Notification(MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
50                 }
51                 else { Local_Notification(MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_CAPTURE_), player.netname); }
52         }
53         
54         // write that shit in the database
55         if((!ctf_captimerecord) || (cap_time < cap_record))
56         {
57                 ctf_captimerecord = cap_time;
58                 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
59                 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
60                 write_recordmarker(player, (time - cap_time), cap_time); 
61         } 
62 }
63
64 void ctf_FlagcarrierWaypoints(entity player)
65 {
66         WaypointSprite_Spawn("flagcarrier", 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, TRUE, RADARICON_FLAG, WPCOLOR_FLAGCARRIER(player.team));
67         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent) * 2);
68         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent));
69         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
70 }
71
72 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
73 {
74         float current_distance = vlen((('1 0 0' * to_x) + ('0 1 0' * to_y)) - (('1 0 0' * from_x) + ('0 1 0' * from_y))); // for the sake of this check, exclude Z axis
75         float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
76         float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
77         //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
78
79         vector targpos;
80         if(current_height) // make sure we can actually do this arcing path
81         {
82                 targpos = (to + ('0 0 1' * current_height));
83                 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
84                 if(trace_fraction < 1)
85                 {
86                         //print("normal arc line failed, trying to find new pos...");
87                         WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
88                         targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
89                         WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
90                         if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
91                         /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
92                 }
93         }
94         else { targpos = to; }
95
96         //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
97
98         vector desired_direction = normalize(targpos - from);
99         if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
100         else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
101 }
102
103 float ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
104 {
105         if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
106         {
107                 // directional tracing only
108                 float spreadlimit;
109                 makevectors(passer_angle);
110
111                 // find the closest point on the enemy to the center of the attack
112                 float ang; // angle between shotdir and h
113                 float h; // hypotenuse, which is the distance between attacker to head
114                 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
115                 
116                 h = vlen(head_center - passer_center);
117                 ang = acos(dotproduct(normalize(head_center - passer_center), v_forward));
118                 a = h * cos(ang);
119
120                 vector nearest_on_line = (passer_center + a * v_forward);
121                 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
122
123                 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
124                 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
125
126                 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
127                         { return TRUE; }
128                 else
129                         { return FALSE; }
130         }
131         else { return TRUE; }
132 }
133
134 float ctf_IsDifferentTeam(entity a, entity b)
135 {
136         float f = IsDifferentTeam(a, b);
137         return (autocvar_g_ctf_reverse) ? !f : f;
138 }
139
140 float ctf_Stalemate_waypointsprite_visible_for_player(entity e)
141 {
142         // personal waypoints
143         if(self.enemy)
144                 if(self.enemy != e)
145                         return FALSE;
146                         
147         // team waypoints
148         if(self.rule == SPRITERULE_DEFAULT)
149         if(ctf_IsDifferentTeam(self.owner.flagcarried, self.owner))
150         if(ctf_IsDifferentTeam(self.owner.flagcarried, e))
151         if(!IsDifferentTeam(self.owner, e) || !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 > 0) + (stale_blue_flags > 0) + (stale_yellow_flags > 0) + (stale_pink_flags > 0);
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 && stale_flags > 0 && 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.waypointsprite_visible_for_player = ctf_Stalemate_waypointsprite_visible_for_player;
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)
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                 if(flag.owner.wps_enemyflagcarrier)
942                         WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
943                         
944                 WaypointSprite_Kill(flag.wps_flagcarrier);
945                 
946                 flag.owner.flagcarried = world;
947
948                 if(flag.speedrunning)
949                         ctf_FakeTimeLimit(flag.owner, -1);
950         }
951
952         if((flag.ctf_status == FLAG_DROPPED) && (flag.wps_flagdropped))
953                 { WaypointSprite_Kill(flag.wps_flagdropped); }
954
955         // reset the flag
956         setattachment(flag, world, "");
957         setorigin(flag, flag.ctf_spawnorigin);
958         
959         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
960         flag.takedamage = DAMAGE_NO;
961         flag.health = flag.max_flag_health;
962         flag.solid = SOLID_TRIGGER;
963         flag.velocity = '0 0 0';
964         flag.angles = flag.mangle;
965         flag.flags = FL_ITEM | FL_NOTARGET;
966         
967         flag.ctf_status = FLAG_BASE;
968         flag.owner = world;
969         flag.pass_distance = 0;
970         flag.pass_sender = world;
971         flag.pass_target = world;
972         flag.ctf_dropper = world;
973         flag.ctf_pickuptime = 0;
974         flag.ctf_droptime = 0;
975 }
976
977 void ctf_Reset()
978 {
979         if(self.owner)
980                 if(IS_PLAYER(self.owner))
981                         ctf_Handle_Throw(self.owner, world, DROP_RESET);
982                         
983         ctf_RespawnFlag(self);
984 }
985
986 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
987 {
988         // bot waypoints
989         waypoint_spawnforitem_force(self, self.origin);
990         self.nearestwaypointtimeout = 0; // activate waypointing again
991         self.bot_basewaypoint = self.nearestwaypoint;
992
993         // waypointsprites
994         string basename = "base";
995         
996         switch(self.team)
997         {
998                 case NUM_TEAM_1: basename = "redbase"; break;
999                 case NUM_TEAM_2: basename = "bluebase"; break;
1000                 case NUM_TEAM_3: basename = "yellowbase"; break;
1001                 case NUM_TEAM_4: basename = "pinkbase"; break;
1002         }
1003         
1004         WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
1005         WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, colormapPaletteColor(self.team - 1, FALSE));
1006
1007         // captureshield setup
1008         ctf_CaptureShield_Spawn(self);
1009 }
1010
1011 void ctf_FlagSetup(float teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc 
1012 {
1013         // declarations
1014         self = flag; // for later usage with droptofloor()
1015         
1016         // main setup
1017         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1018         ctf_worldflaglist = flag;
1019
1020         setattachment(flag, world, "");
1021
1022         flag.netname = strcat(Static_Team_ColorName_Upper(teamnumber), "^7 flag");
1023         flag.team = teamnumber;
1024         flag.classname = "item_flag_team";
1025         flag.target = "###item###"; // wut?
1026         flag.flags = FL_ITEM | FL_NOTARGET;
1027         flag.solid = SOLID_TRIGGER;
1028         flag.takedamage = DAMAGE_NO;
1029         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;   
1030         flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1031         flag.health = flag.max_flag_health;
1032         flag.event_damage = ctf_FlagDamage;
1033         flag.pushable = TRUE;
1034         flag.teleportable = TELEPORT_NORMAL;
1035         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1036         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1037         flag.velocity = '0 0 0';
1038         flag.mangle = flag.angles;
1039         flag.reset = ctf_Reset;
1040         flag.touch = ctf_FlagTouch;
1041         flag.think = ctf_FlagThink;
1042         flag.nextthink = time + FLAG_THINKRATE;
1043         flag.ctf_status = FLAG_BASE;
1044
1045         // appearence
1046         if(flag.model == "")       { flag.model = strzone(cvar_string(strcat("g_ctf_flag_", Static_Team_ColorName_Lower(teamnumber), "_model"))); }
1047         if(!flag.scale)            { flag.scale = FLAG_SCALE; }
1048         if(!flag.skin)             { flag.skin = cvar(strcat("g_ctf_flag_", Static_Team_ColorName_Lower(teamnumber), "_skin")); }
1049         if(flag.toucheffect == "") { flag.toucheffect = strzone(strcat(Static_Team_ColorName_Lower(teamnumber), "_touch")); }
1050         if(flag.passeffect == "")  { flag.passeffect = strzone(strcat(Static_Team_ColorName_Lower(teamnumber), "_pass")); }
1051         if(flag.capeffect == "")   { flag.capeffect = strzone(strcat(Static_Team_ColorName_Lower(teamnumber), "_cap")); }
1052         
1053         // sound 
1054         if(flag.snd_flag_taken == "")    { flag.snd_flag_taken  = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_taken.wav")); }
1055         if(flag.snd_flag_returned == "") { flag.snd_flag_returned = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_returned.wav")); }
1056         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
1057         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.
1058         if(flag.snd_flag_dropped == "")  { flag.snd_flag_dropped = strzone(strcat("ctf/", Static_Team_ColorName_Lower(teamnumber), "_dropped.wav")); }
1059         if(flag.snd_flag_touch == "")    { flag.snd_flag_touch = "ctf/touch.wav"; } // again has no team-based sound
1060         if(flag.snd_flag_pass == "")     { flag.snd_flag_pass = "ctf/pass.wav"; } // same story here
1061         
1062         // precache
1063         precache_sound(flag.snd_flag_taken);
1064         precache_sound(flag.snd_flag_returned);
1065         precache_sound(flag.snd_flag_capture);
1066         precache_sound(flag.snd_flag_respawn);
1067         precache_sound(flag.snd_flag_dropped);
1068         precache_sound(flag.snd_flag_touch);
1069         precache_sound(flag.snd_flag_pass);
1070         precache_model(flag.model);
1071         precache_model("models/ctf/shield.md3");
1072         precache_model("models/ctf/shockwavetransring.md3");
1073
1074         // appearence
1075         setmodel(flag, flag.model); // precision set below
1076         setsize(flag, FLAG_MIN, FLAG_MAX);
1077         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1078         
1079         if(autocvar_g_ctf_flag_glowtrails)
1080         {
1081                 switch(teamnumber)
1082                 {
1083                         case NUM_TEAM_1: flag.glow_color = 251; break; // red
1084                         case NUM_TEAM_2: flag.glow_color = 210; break; // blue
1085                         case NUM_TEAM_3: flag.glow_color = 110; break; // yellow
1086                         case NUM_TEAM_4: flag.glow_color = 145; break; // pink
1087                 }
1088                 flag.glow_size = 25;
1089                 flag.glow_trail = 1;
1090         }
1091         
1092         flag.effects |= EF_LOWPRECISION;
1093         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1094         if(autocvar_g_ctf_dynamiclights)
1095         {
1096                 switch(teamnumber)
1097                 {
1098                         case NUM_TEAM_1: flag.effects |= EF_RED; break;
1099                         case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1100                         case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1101                         case NUM_TEAM_4: flag.effects |= EF_RED; break;
1102                 }
1103         }
1104         
1105         // flag placement
1106         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1107         {       
1108                 flag.dropped_origin = flag.origin; 
1109                 flag.noalign = TRUE;
1110                 flag.movetype = MOVETYPE_NONE;
1111         }
1112         else // drop to floor, automatically find a platform and set that as spawn origin
1113         { 
1114                 flag.noalign = FALSE;
1115                 self = flag;
1116                 droptofloor();
1117                 flag.movetype = MOVETYPE_TOSS; 
1118         }       
1119         
1120         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1121 }
1122
1123
1124 // ================
1125 // Bot player logic
1126 // ================
1127
1128 // NOTE: LEGACY CODE, needs to be re-written!
1129
1130 void havocbot_calculate_middlepoint()
1131 {
1132         entity f;
1133         vector s = '0 0 0';
1134         vector fo = '0 0 0';
1135         float n = 0;
1136
1137         f = ctf_worldflaglist;
1138         while (f)
1139         {
1140                 fo = f.origin;
1141                 s = s + fo;
1142                 f = f.ctf_worldflagnext;
1143         }
1144         if(!n)
1145                 return;
1146         havocbot_ctf_middlepoint = s * (1.0 / n);
1147         havocbot_ctf_middlepoint_radius  = vlen(fo - havocbot_ctf_middlepoint);
1148 }
1149
1150
1151 entity havocbot_ctf_find_flag(entity bot)
1152 {
1153         entity f;
1154         f = ctf_worldflaglist;
1155         while (f)
1156         {
1157                 if (!ctf_IsDifferentTeam(bot, f))
1158                         return f;
1159                 f = f.ctf_worldflagnext;
1160         }
1161         return world;
1162 }
1163
1164 entity havocbot_ctf_find_enemy_flag(entity bot)
1165 {
1166         entity f;
1167         f = ctf_worldflaglist;
1168         while (f)
1169         {
1170                 if (ctf_IsDifferentTeam(bot, f))
1171                         return f;
1172                 f = f.ctf_worldflagnext;
1173         }
1174         return world;
1175 }
1176
1177 float havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1178 {
1179         if not(teamplay)
1180                 return 0;
1181
1182         float c = 0;
1183         entity head;
1184
1185         FOR_EACH_PLAYER(head)
1186         {
1187                 if(head.team!=bot.team || head.deadflag != DEAD_NO || head == bot)
1188                         continue;
1189
1190                 if(vlen(head.origin - org) < tc_radius)
1191                         ++c;
1192         }
1193
1194         return c;
1195 }
1196
1197 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1198 {
1199         entity head;
1200         head = ctf_worldflaglist;
1201         while (head)
1202         {
1203                 if (!ctf_IsDifferentTeam(self, head))
1204                         break;
1205                 head = head.ctf_worldflagnext;
1206         }
1207         if (head)
1208                 navigation_routerating(head, ratingscale, 10000);
1209 }
1210
1211 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1212 {
1213         entity head;
1214         head = ctf_worldflaglist;
1215         while (head)
1216         {
1217                 if (!ctf_IsDifferentTeam(self, head))
1218                         break;
1219                 head = head.ctf_worldflagnext;
1220         }
1221         if not(head)
1222                 return;
1223
1224         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1225 }
1226
1227 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1228 {
1229         entity head;
1230         head = ctf_worldflaglist;
1231         while (head)
1232         {
1233                 if (ctf_IsDifferentTeam(self, head))
1234                         break;
1235                 head = head.ctf_worldflagnext;
1236         }
1237         if (head)
1238                 navigation_routerating(head, ratingscale, 10000);
1239 }
1240
1241 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1242 {
1243         if not(bot_waypoints_for_items)
1244         {
1245                 havocbot_goalrating_ctf_enemyflag(ratingscale);
1246                 return;
1247         }
1248
1249         entity head;
1250
1251         head = havocbot_ctf_find_enemy_flag(self);
1252
1253         if not(head)
1254                 return;
1255
1256         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1257 }
1258
1259 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1260 {
1261         entity mf;
1262
1263         mf = havocbot_ctf_find_flag(self);
1264
1265         if(mf.ctf_status == FLAG_BASE)
1266                 return;
1267
1268         if(mf.tag_entity)
1269                 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1270 }
1271
1272 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1273 {
1274         entity head;
1275         head = ctf_worldflaglist;
1276         while (head)
1277         {
1278                 // flag is out in the field
1279                 if(head.ctf_status != FLAG_BASE)
1280                 if(head.tag_entity==world)      // dropped
1281                 {
1282                         if(df_radius)
1283                         {
1284                                 if(vlen(org-head.origin)<df_radius)
1285                                         navigation_routerating(head, ratingscale, 10000);
1286                         }
1287                         else
1288                                 navigation_routerating(head, ratingscale, 10000);
1289                 }
1290
1291                 head = head.ctf_worldflagnext;
1292         }
1293 }
1294
1295 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1296 {
1297         entity head;
1298         float t;
1299         head = findchainfloat(bot_pickup, TRUE);
1300         while (head)
1301         {
1302                 // gather health and armor only
1303                 if (head.solid)
1304                 if (head.health || head.armorvalue)
1305                 if (vlen(head.origin - org) < sradius)
1306                 {
1307                         // get the value of the item
1308                         t = head.bot_pickupevalfunc(self, head) * 0.0001;
1309                         if (t > 0)
1310                                 navigation_routerating(head, t * ratingscale, 500);
1311                 }
1312                 head = head.chain;
1313         }
1314 }
1315
1316 void havocbot_ctf_reset_role(entity bot)
1317 {
1318         float cdefense, cmiddle, coffense;
1319         entity mf, ef, head;
1320         float c;
1321
1322         if(bot.deadflag != DEAD_NO)
1323                 return;
1324
1325         if(vlen(havocbot_ctf_middlepoint)==0)
1326                 havocbot_calculate_middlepoint();
1327
1328         // Check ctf flags
1329         if (bot.flagcarried)
1330         {
1331                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1332                 return;
1333         }
1334
1335         mf = havocbot_ctf_find_flag(bot);
1336         ef = havocbot_ctf_find_enemy_flag(bot);
1337
1338         // Retrieve stolen flag
1339         if(mf.ctf_status!=FLAG_BASE)
1340         {
1341                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1342                 return;
1343         }
1344
1345         // If enemy flag is taken go to the middle to intercept pursuers
1346         if(ef.ctf_status!=FLAG_BASE)
1347         {
1348                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1349                 return;
1350         }
1351
1352         // if there is only me on the team switch to offense
1353         c = 0;
1354         FOR_EACH_PLAYER(head)
1355         if(head.team==bot.team)
1356                 ++c;
1357
1358         if(c==1)
1359         {
1360                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1361                 return;
1362         }
1363
1364         // Evaluate best position to take
1365         // Count mates on middle position
1366         cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1367
1368         // Count mates on defense position
1369         cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1370
1371         // Count mates on offense position
1372         coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1373
1374         if(cdefense<=coffense)
1375                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1376         else if(coffense<=cmiddle)
1377                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1378         else
1379                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1380 }
1381
1382 void havocbot_role_ctf_carrier()
1383 {
1384         if(self.deadflag != DEAD_NO)
1385         {
1386                 havocbot_ctf_reset_role(self);
1387                 return;
1388         }
1389
1390         if (self.flagcarried == world)
1391         {
1392                 havocbot_ctf_reset_role(self);
1393                 return;
1394         }
1395
1396         if (self.bot_strategytime < time)
1397         {
1398                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1399
1400                 navigation_goalrating_start();
1401                 havocbot_goalrating_ctf_ourbase(50000);
1402
1403                 if(self.health<100)
1404                         havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1405
1406                 navigation_goalrating_end();
1407
1408                 if (self.navigation_hasgoals)
1409                         self.havocbot_cantfindflag = time + 10;
1410                 else if (time > self.havocbot_cantfindflag)
1411                 {
1412                         // Can't navigate to my own base, suicide!
1413                         // TODO: drop it and wander around
1414                         Damage(self, self, self, 100000, DEATH_KILL, self.origin, '0 0 0');
1415                         return;
1416                 }
1417         }
1418 }
1419
1420 void havocbot_role_ctf_escort()
1421 {
1422         entity mf, ef;
1423
1424         if(self.deadflag != DEAD_NO)
1425         {
1426                 havocbot_ctf_reset_role(self);
1427                 return;
1428         }
1429
1430         if (self.flagcarried)
1431         {
1432                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1433                 return;
1434         }
1435
1436         // If enemy flag is back on the base switch to previous role
1437         ef = havocbot_ctf_find_enemy_flag(self);
1438         if(ef.ctf_status==FLAG_BASE)
1439         {
1440                 self.havocbot_role = self.havocbot_previous_role;
1441                 self.havocbot_role_timeout = 0;
1442                 return;
1443         }
1444
1445         // If the flag carrier reached the base switch to defense
1446         mf = havocbot_ctf_find_flag(self);
1447         if(mf.ctf_status!=FLAG_BASE)
1448         if(vlen(ef.origin - mf.dropped_origin) < 300)
1449         {
1450                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1451                 return;
1452         }
1453
1454         // Set the role timeout if necessary
1455         if (!self.havocbot_role_timeout)
1456         {
1457                 self.havocbot_role_timeout = time + random() * 30 + 60;
1458         }
1459
1460         // If nothing happened just switch to previous role
1461         if (time > self.havocbot_role_timeout)
1462         {
1463                 self.havocbot_role = self.havocbot_previous_role;
1464                 self.havocbot_role_timeout = 0;
1465                 return;
1466         }
1467
1468         // Chase the flag carrier
1469         if (self.bot_strategytime < time)
1470         {
1471                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1472                 navigation_goalrating_start();
1473                 havocbot_goalrating_ctf_enemyflag(30000);
1474                 havocbot_goalrating_ctf_ourstolenflag(40000);
1475                 havocbot_goalrating_items(10000, self.origin, 10000);
1476                 navigation_goalrating_end();
1477         }
1478 }
1479
1480 void havocbot_role_ctf_offense()
1481 {
1482         entity mf, ef;
1483         vector pos;
1484
1485         if(self.deadflag != DEAD_NO)
1486         {
1487                 havocbot_ctf_reset_role(self);
1488                 return;
1489         }
1490
1491         if (self.flagcarried)
1492         {
1493                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1494                 return;
1495         }
1496
1497         // Check flags
1498         mf = havocbot_ctf_find_flag(self);
1499         ef = havocbot_ctf_find_enemy_flag(self);
1500
1501         // Own flag stolen
1502         if(mf.ctf_status!=FLAG_BASE)
1503         {
1504                 if(mf.tag_entity)
1505                         pos = mf.tag_entity.origin;
1506                 else
1507                         pos = mf.origin;
1508
1509                 // Try to get it if closer than the enemy base
1510                 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1511                 {
1512                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1513                         return;
1514                 }
1515         }
1516
1517         // Escort flag carrier
1518         if(ef.ctf_status!=FLAG_BASE)
1519         {
1520                 if(ef.tag_entity)
1521                         pos = ef.tag_entity.origin;
1522                 else
1523                         pos = ef.origin;
1524
1525                 if(vlen(pos-mf.dropped_origin)>700)
1526                 {
1527                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1528                         return;
1529                 }
1530         }
1531
1532         // About to fail, switch to middlefield
1533         if(self.health<50)
1534         {
1535                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1536                 return;
1537         }
1538
1539         // Set the role timeout if necessary
1540         if (!self.havocbot_role_timeout)
1541                 self.havocbot_role_timeout = time + 120;
1542
1543         if (time > self.havocbot_role_timeout)
1544         {
1545                 havocbot_ctf_reset_role(self);
1546                 return;
1547         }
1548
1549         if (self.bot_strategytime < time)
1550         {
1551                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1552                 navigation_goalrating_start();
1553                 havocbot_goalrating_ctf_ourstolenflag(50000);
1554                 havocbot_goalrating_ctf_enemybase(20000);
1555                 havocbot_goalrating_items(5000, self.origin, 1000);
1556                 havocbot_goalrating_items(1000, self.origin, 10000);
1557                 navigation_goalrating_end();
1558         }
1559 }
1560
1561 // Retriever (temporary role):
1562 void havocbot_role_ctf_retriever()
1563 {
1564         entity mf;
1565
1566         if(self.deadflag != DEAD_NO)
1567         {
1568                 havocbot_ctf_reset_role(self);
1569                 return;
1570         }
1571
1572         if (self.flagcarried)
1573         {
1574                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1575                 return;
1576         }
1577
1578         // If flag is back on the base switch to previous role
1579         mf = havocbot_ctf_find_flag(self);
1580         if(mf.ctf_status==FLAG_BASE)
1581         {
1582                 havocbot_ctf_reset_role(self);
1583                 return;
1584         }
1585
1586         if (!self.havocbot_role_timeout)
1587                 self.havocbot_role_timeout = time + 20;
1588
1589         if (time > self.havocbot_role_timeout)
1590         {
1591                 havocbot_ctf_reset_role(self);
1592                 return;
1593         }
1594
1595         if (self.bot_strategytime < time)
1596         {
1597                 float rt_radius;
1598                 rt_radius = 10000;
1599
1600                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1601                 navigation_goalrating_start();
1602                 havocbot_goalrating_ctf_ourstolenflag(50000);
1603                 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1604                 havocbot_goalrating_ctf_enemybase(30000);
1605                 havocbot_goalrating_items(500, self.origin, rt_radius);
1606                 navigation_goalrating_end();
1607         }
1608 }
1609
1610 void havocbot_role_ctf_middle()
1611 {
1612         entity mf;
1613
1614         if(self.deadflag != DEAD_NO)
1615         {
1616                 havocbot_ctf_reset_role(self);
1617                 return;
1618         }
1619
1620         if (self.flagcarried)
1621         {
1622                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1623                 return;
1624         }
1625
1626         mf = havocbot_ctf_find_flag(self);
1627         if(mf.ctf_status!=FLAG_BASE)
1628         {
1629                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1630                 return;
1631         }
1632
1633         if (!self.havocbot_role_timeout)
1634                 self.havocbot_role_timeout = time + 10;
1635
1636         if (time > self.havocbot_role_timeout)
1637         {
1638                 havocbot_ctf_reset_role(self);
1639                 return;
1640         }
1641
1642         if (self.bot_strategytime < time)
1643         {
1644                 vector org;
1645
1646                 org = havocbot_ctf_middlepoint;
1647                 org_z = self.origin_z;
1648
1649                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1650                 navigation_goalrating_start();
1651                 havocbot_goalrating_ctf_ourstolenflag(50000);
1652                 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
1653                 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1654                 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1655                 havocbot_goalrating_items(2500, self.origin, 10000);
1656                 havocbot_goalrating_ctf_enemybase(2500);
1657                 navigation_goalrating_end();
1658         }
1659 }
1660
1661 void havocbot_role_ctf_defense()
1662 {
1663         entity mf;
1664
1665         if(self.deadflag != DEAD_NO)
1666         {
1667                 havocbot_ctf_reset_role(self);
1668                 return;
1669         }
1670
1671         if (self.flagcarried)
1672         {
1673                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1674                 return;
1675         }
1676
1677         // If own flag was captured
1678         mf = havocbot_ctf_find_flag(self);
1679         if(mf.ctf_status!=FLAG_BASE)
1680         {
1681                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1682                 return;
1683         }
1684
1685         if (!self.havocbot_role_timeout)
1686                 self.havocbot_role_timeout = time + 30;
1687
1688         if (time > self.havocbot_role_timeout)
1689         {
1690                 havocbot_ctf_reset_role(self);
1691                 return;
1692         }
1693         if (self.bot_strategytime < time)
1694         {
1695                 float mp_radius;
1696                 vector org;
1697
1698                 org = mf.dropped_origin;
1699                 mp_radius = havocbot_ctf_middlepoint_radius;
1700
1701                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1702                 navigation_goalrating_start();
1703
1704                 // if enemies are closer to our base, go there
1705                 entity head, closestplayer = world;
1706                 float distance, bestdistance = 10000;
1707                 FOR_EACH_PLAYER(head)
1708                 {
1709                         if(head.deadflag!=DEAD_NO)
1710                                 continue;
1711
1712                         distance = vlen(org - head.origin);
1713                         if(distance<bestdistance)
1714                         {
1715                                 closestplayer = head;
1716                                 bestdistance = distance;
1717                         }
1718                 }
1719
1720                 if(closestplayer)
1721                 if(closestplayer.team!=self.team)
1722                 if(vlen(org - self.origin)>1000)
1723                 if(checkpvs(self.origin,closestplayer)||random()<0.5)
1724                         havocbot_goalrating_ctf_ourbase(30000);
1725
1726                 havocbot_goalrating_ctf_ourstolenflag(20000);
1727                 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
1728                 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
1729                 havocbot_goalrating_items(10000, org, mp_radius);
1730                 havocbot_goalrating_items(5000, self.origin, 10000);
1731                 navigation_goalrating_end();
1732         }
1733 }
1734
1735 void havocbot_role_ctf_setrole(entity bot, float role)
1736 {
1737         dprint(strcat(bot.netname," switched to "));
1738         switch(role)
1739         {
1740                 case HAVOCBOT_CTF_ROLE_CARRIER:
1741                         dprint("carrier");
1742                         bot.havocbot_role = havocbot_role_ctf_carrier;
1743                         bot.havocbot_role_timeout = 0;
1744                         bot.havocbot_cantfindflag = time + 10;
1745                         bot.bot_strategytime = 0;
1746                         break;
1747                 case HAVOCBOT_CTF_ROLE_DEFENSE:
1748                         dprint("defense");
1749                         bot.havocbot_role = havocbot_role_ctf_defense;
1750                         bot.havocbot_role_timeout = 0;
1751                         break;
1752                 case HAVOCBOT_CTF_ROLE_MIDDLE:
1753                         dprint("middle");
1754                         bot.havocbot_role = havocbot_role_ctf_middle;
1755                         bot.havocbot_role_timeout = 0;
1756                         break;
1757                 case HAVOCBOT_CTF_ROLE_OFFENSE:
1758                         dprint("offense");
1759                         bot.havocbot_role = havocbot_role_ctf_offense;
1760                         bot.havocbot_role_timeout = 0;
1761                         break;
1762                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1763                         dprint("retriever");
1764                         bot.havocbot_previous_role = bot.havocbot_role;
1765                         bot.havocbot_role = havocbot_role_ctf_retriever;
1766                         bot.havocbot_role_timeout = time + 10;
1767                         bot.bot_strategytime = 0;
1768                         break;
1769                 case HAVOCBOT_CTF_ROLE_ESCORT:
1770                         dprint("escort");
1771                         bot.havocbot_previous_role = bot.havocbot_role;
1772                         bot.havocbot_role = havocbot_role_ctf_escort;
1773                         bot.havocbot_role_timeout = time + 30;
1774                         bot.bot_strategytime = 0;
1775                         break;
1776         }
1777         dprint("\n");
1778 }
1779
1780
1781 // ==============
1782 // Hook Functions
1783 // ==============
1784
1785 MUTATOR_HOOKFUNCTION(ctf_PlayerPreThink)
1786 {
1787         entity flag;
1788         
1789         float t = 0, t2 = 0, t3 = 0;
1790         
1791         // initially clear items so they can be set as necessary later.
1792         self.items &= ~(IT_RED_FLAG_CARRYING    | IT_RED_FLAG_TAKEN     | IT_RED_FLAG_LOST 
1793                                   | IT_BLUE_FLAG_CARRYING       | IT_BLUE_FLAG_TAKEN    | IT_BLUE_FLAG_LOST
1794                                   | IT_YELLOW_FLAG_CARRYING | IT_YELLOW_FLAG_TAKEN      | IT_YELLOW_FLAG_LOST
1795                                   | IT_PINK_FLAG_CARRYING       | IT_PINK_FLAG_TAKEN    | IT_PINK_FLAG_LOST
1796                                   | IT_CTF_SHIELDED);
1797
1798         // scan through all the flags and notify the client about them 
1799         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1800         {
1801                 if(flag.team == NUM_TEAM_1) { t = IT_RED_FLAG_CARRYING;         t2 = IT_RED_FLAG_TAKEN;         t3 = IT_RED_FLAG_LOST; }
1802                 if(flag.team == NUM_TEAM_2) { t = IT_BLUE_FLAG_CARRYING;        t2 = IT_BLUE_FLAG_TAKEN;        t3 = IT_BLUE_FLAG_LOST; }
1803                 if(flag.team == NUM_TEAM_3) { t = IT_YELLOW_FLAG_CARRYING;      t2 = IT_YELLOW_FLAG_TAKEN;      t3 = IT_YELLOW_FLAG_LOST; }
1804                 if(flag.team == NUM_TEAM_4) { t = IT_PINK_FLAG_CARRYING;        t2 = IT_PINK_FLAG_TAKEN;        t3 = IT_PINK_FLAG_LOST; }
1805         
1806                 switch(flag.ctf_status)
1807                 {
1808                         case FLAG_PASSING:
1809                         case FLAG_CARRY:
1810                         {
1811                                 if((flag.owner == self) || (flag.pass_sender == self))
1812                                         self.items |= t; // carrying: self is currently carrying the flag
1813                                 else 
1814                                         self.items |= t2; // taken: someone else is carrying the flag
1815                                 break;
1816                         }
1817                         case FLAG_DROPPED:
1818                         {
1819                                 self.items |= t3; // lost: the flag is dropped somewhere on the map
1820                                 break;
1821                         }
1822                 }
1823         }
1824         
1825         // item for stopping players from capturing the flag too often
1826         if(self.ctf_captureshielded)
1827                 self.items |= IT_CTF_SHIELDED;
1828         
1829         // update the health of the flag carrier waypointsprite
1830         if(self.wps_flagcarrier) 
1831                 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent));
1832         
1833         return FALSE;
1834 }
1835
1836 MUTATOR_HOOKFUNCTION(ctf_PlayerDamage) // for changing damage and force values that are applied to players in g_damage.qc
1837 {
1838         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
1839         {
1840                 if(frag_target == frag_attacker) // damage done to yourself
1841                 {
1842                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
1843                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
1844                 }
1845                 else // damage done to everyone else
1846                 {
1847                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
1848                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
1849                 }
1850         }
1851         else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && ctf_IsDifferentTeam(frag_target, frag_attacker)) // if the target is a flagcarrier
1852         {
1853                 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent)))
1854                 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
1855                 {
1856                         frag_target.wps_helpme_time = time;
1857                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
1858                 }
1859                 // todo: add notification for when flag carrier needs help?
1860         }
1861         return FALSE;
1862 }
1863
1864 MUTATOR_HOOKFUNCTION(ctf_PlayerDies)
1865 {
1866         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
1867         {
1868                 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
1869                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
1870         }
1871                                 
1872         if(frag_target.flagcarried)
1873                 { ctf_Handle_Throw(frag_target, world, DROP_NORMAL); }
1874                 
1875         return FALSE;
1876 }
1877
1878 MUTATOR_HOOKFUNCTION(ctf_GiveFragsForKill)
1879 {
1880         frag_score = 0;
1881         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
1882 }
1883
1884 MUTATOR_HOOKFUNCTION(ctf_RemovePlayer)
1885 {
1886         entity flag; // temporary entity for the search method
1887         
1888         if(self.flagcarried)
1889                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1890         
1891         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1892         {
1893                 if(flag.pass_sender == self) { flag.pass_sender = world; }
1894                 if(flag.pass_target == self) { flag.pass_target = world; }
1895                 if(flag.ctf_dropper == self) { flag.ctf_dropper = world; }
1896         }
1897                 
1898         return FALSE;
1899 }
1900
1901 MUTATOR_HOOKFUNCTION(ctf_PortalTeleport)
1902 {
1903         if(self.flagcarried) 
1904         if(!autocvar_g_ctf_portalteleport)
1905                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
1906
1907         return FALSE;
1908 }
1909
1910 MUTATOR_HOOKFUNCTION(ctf_PlayerUseKey)
1911 {
1912         if(MUTATOR_RETURNVALUE || gameover) { return FALSE; }
1913         
1914         entity player = self;
1915
1916         if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
1917         {
1918                 // pass the flag to a team mate
1919                 if(autocvar_g_ctf_pass)
1920                 {
1921                         entity head, closest_target = world;
1922                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, TRUE);
1923                         
1924                         while(head) // find the closest acceptable target to pass to
1925                         {
1926                                 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
1927                                 if(head != player && !IsDifferentTeam(head, player))
1928                                 if(!head.speedrunning && !head.vehicle)
1929                                 {
1930                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc) 
1931                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
1932                                         vector passer_center = CENTER_OR_VIEWOFS(player);
1933                                         
1934                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
1935                                         {
1936                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried) 
1937                                                 { 
1938                                                         if(IS_BOT_CLIENT(head))
1939                                                         {
1940                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1941                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
1942                                                         }
1943                                                         else
1944                                                         {
1945                                                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
1946                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
1947                                                         }
1948                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait; 
1949                                                         return TRUE; 
1950                                                 }
1951                                                 else if(player.flagcarried)
1952                                                 {
1953                                                         if(closest_target)
1954                                                         {
1955                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
1956                                                                 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
1957                                                                         { closest_target = head; }
1958                                                         }
1959                                                         else { closest_target = head; }
1960                                                 }
1961                                         }
1962                                 }
1963                                 head = head.chain;
1964                         }
1965                         
1966                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return TRUE; }
1967                 }
1968                 
1969                 // throw the flag in front of you
1970                 if(autocvar_g_ctf_throw && player.flagcarried)
1971                 {
1972                         if(player.throw_count == -1)
1973                         {
1974                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
1975                                 {
1976                                         player.throw_prevtime = time;
1977                                         player.throw_count = 1;
1978                                         ctf_Handle_Throw(player, world, DROP_THROW);
1979                                         return TRUE;
1980                                 }
1981                                 else
1982                                 {
1983                                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
1984                                         return FALSE;
1985                                 }
1986                         }
1987                         else
1988                         {
1989                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
1990                                 else { player.throw_count += 1; }
1991                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
1992                                         
1993                                 player.throw_prevtime = time;
1994                                 ctf_Handle_Throw(player, world, DROP_THROW);
1995                                 return TRUE;
1996                         }
1997                 }
1998         }
1999                 
2000         return FALSE;
2001 }
2002
2003 MUTATOR_HOOKFUNCTION(ctf_HelpMePing)
2004 {
2005         if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2006         {
2007                 self.wps_helpme_time = time;
2008                 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2009         } 
2010         else // create a normal help me waypointsprite
2011         {
2012                 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');
2013                 WaypointSprite_Ping(self.wps_helpme);
2014         }
2015
2016         return TRUE;
2017 }
2018
2019 MUTATOR_HOOKFUNCTION(ctf_VehicleEnter)
2020 {
2021         if(vh_player.flagcarried)
2022         {
2023                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2024                 {
2025                         ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2026                 }
2027                 else
2028                 {            
2029                         setattachment(vh_player.flagcarried, vh_vehicle, ""); 
2030                         setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2031                         vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2032                         //vh_player.flagcarried.angles = '0 0 0';       
2033                 }
2034                 return TRUE;
2035         }
2036                 
2037         return FALSE;
2038 }
2039
2040 MUTATOR_HOOKFUNCTION(ctf_VehicleExit)
2041 {
2042         if(vh_player.flagcarried)
2043         {
2044                 setattachment(vh_player.flagcarried, vh_player, ""); 
2045                 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2046                 vh_player.flagcarried.scale = FLAG_SCALE;
2047                 vh_player.flagcarried.angles = '0 0 0';
2048                 return TRUE;
2049         }
2050
2051         return FALSE;
2052 }
2053
2054 MUTATOR_HOOKFUNCTION(ctf_AbortSpeedrun)
2055 {
2056         if(self.flagcarried)
2057         {
2058                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_));
2059                 ctf_RespawnFlag(self.flagcarried);
2060                 return TRUE;
2061         }
2062         
2063         return FALSE;
2064 }
2065
2066 MUTATOR_HOOKFUNCTION(ctf_MatchEnd)
2067 {
2068         entity flag; // temporary entity for the search method
2069         
2070         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2071         {
2072                 switch(flag.ctf_status)
2073                 {
2074                         case FLAG_DROPPED:
2075                         case FLAG_PASSING:
2076                         {
2077                                 // lock the flag, game is over
2078                                 flag.movetype = MOVETYPE_NONE;
2079                                 flag.takedamage = DAMAGE_NO;
2080                                 flag.solid = SOLID_NOT;
2081                                 flag.nextthink = FALSE; // stop thinking
2082                                 
2083                                 //dprint("stopping the ", flag.netname, " from moving.\n");
2084                                 break;
2085                         }
2086                         
2087                         default:
2088                         case FLAG_BASE:
2089                         case FLAG_CARRY:
2090                         {
2091                                 // do nothing for these flags
2092                                 break;
2093                         }
2094                 }
2095         }
2096         
2097         return FALSE;
2098 }
2099
2100 MUTATOR_HOOKFUNCTION(ctf_BotRoles)
2101 {
2102         havocbot_ctf_reset_role(self);
2103         return TRUE;
2104 }
2105
2106 MUTATOR_HOOKFUNCTION(ctf_GetCvars)
2107 {
2108         GetCvars_handleFloat(get_cvars_s, get_cvars_f, CAPTURE_VERBOSE, "notification_ctf_capture_verbose");
2109         GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_TEAM_VERBOSE, "notification_ctf_pickup_team_verbose");
2110         GetCvars_handleFloat(get_cvars_s, get_cvars_f, PICKUP_ENEMY_VERBOSE, "notification_ctf_pickup_enemy_verbose");
2111         return TRUE;
2112 }
2113
2114 MUTATOR_HOOKFUNCTION(ctf_GetTeamCount)
2115 {
2116         ret_float = ctf_teams;
2117         return 0;
2118 }
2119
2120
2121 // ==========
2122 // Spawnfuncs
2123 // ==========
2124
2125 /*QUAKED spawnfunc_info_player_team1 (1 0 0) (-16 -16 -24) (16 16 24)
2126 CTF Starting point for a player in team one (Red).
2127 Keys: "angle" viewing angle when spawning. */
2128 void spawnfunc_info_player_team1()
2129 {
2130         if(g_assault) { remove(self); return; }
2131         
2132         self.team = NUM_TEAM_1; // red
2133         spawnfunc_info_player_deathmatch();
2134 }
2135
2136
2137 /*QUAKED spawnfunc_info_player_team2 (1 0 0) (-16 -16 -24) (16 16 24)
2138 CTF Starting point for a player in team two (Blue).
2139 Keys: "angle" viewing angle when spawning. */
2140 void spawnfunc_info_player_team2()
2141 {
2142         if(g_assault) { remove(self); return; }
2143         
2144         self.team = NUM_TEAM_2; // blue
2145         spawnfunc_info_player_deathmatch();
2146 }
2147
2148 /*QUAKED spawnfunc_info_player_team3 (1 0 0) (-16 -16 -24) (16 16 24)
2149 CTF Starting point for a player in team three (Yellow).
2150 Keys: "angle" viewing angle when spawning. */
2151 void spawnfunc_info_player_team3()
2152 {
2153         if(g_assault) { remove(self); return; }
2154         
2155         self.team = NUM_TEAM_3; // yellow
2156         spawnfunc_info_player_deathmatch();
2157 }
2158
2159
2160 /*QUAKED spawnfunc_info_player_team4 (1 0 0) (-16 -16 -24) (16 16 24)
2161 CTF Starting point for a player in team four (Purple).
2162 Keys: "angle" viewing angle when spawning. */
2163 void spawnfunc_info_player_team4()
2164 {
2165         if(g_assault) { remove(self); return; }
2166         
2167         self.team = NUM_TEAM_4; // purple
2168         spawnfunc_info_player_deathmatch();
2169 }
2170
2171 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2172 CTF flag for team one (Red).
2173 Keys: 
2174 "angle" Angle the flag will point (minus 90 degrees)... 
2175 "model" model to use, note this needs red and blue as skins 0 and 1...
2176 "noise" sound played when flag is picked up...
2177 "noise1" sound played when flag is returned by a teammate...
2178 "noise2" sound played when flag is captured...
2179 "noise3" sound played when flag is lost in the field and respawns itself... 
2180 "noise4" sound played when flag is dropped by a player...
2181 "noise5" sound played when flag touches the ground... */
2182 void spawnfunc_item_flag_team1()
2183 {
2184         if(!g_ctf) { remove(self); return; }
2185
2186         ctf_FlagSetup(NUM_TEAM_1, self);
2187 }
2188
2189 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2190 CTF flag for team two (Blue).
2191 Keys: 
2192 "angle" Angle the flag will point (minus 90 degrees)... 
2193 "model" model to use, note this needs red and blue as skins 0 and 1...
2194 "noise" sound played when flag is picked up...
2195 "noise1" sound played when flag is returned by a teammate...
2196 "noise2" sound played when flag is captured...
2197 "noise3" sound played when flag is lost in the field and respawns itself... 
2198 "noise4" sound played when flag is dropped by a player...
2199 "noise5" sound played when flag touches the ground... */
2200 void spawnfunc_item_flag_team2()
2201 {
2202         if(!g_ctf) { remove(self); return; }
2203
2204         ctf_FlagSetup(NUM_TEAM_2, self);
2205 }
2206
2207 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2208 CTF flag for team three (Yellow).
2209 Keys: 
2210 "angle" Angle the flag will point (minus 90 degrees)... 
2211 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2212 "noise" sound played when flag is picked up...
2213 "noise1" sound played when flag is returned by a teammate...
2214 "noise2" sound played when flag is captured...
2215 "noise3" sound played when flag is lost in the field and respawns itself... 
2216 "noise4" sound played when flag is dropped by a player...
2217 "noise5" sound played when flag touches the ground... */
2218 void spawnfunc_item_flag_team3()
2219 {
2220         if(!g_ctf) { remove(self); return; }
2221
2222         ctf_FlagSetup(NUM_TEAM_3, self);
2223 }
2224
2225 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2226 CTF flag for team two (Pink).
2227 Keys: 
2228 "angle" Angle the flag will point (minus 90 degrees)... 
2229 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2230 "noise" sound played when flag is picked up...
2231 "noise1" sound played when flag is returned by a teammate...
2232 "noise2" sound played when flag is captured...
2233 "noise3" sound played when flag is lost in the field and respawns itself... 
2234 "noise4" sound played when flag is dropped by a player...
2235 "noise5" sound played when flag touches the ground... */
2236 void spawnfunc_item_flag_team4()
2237 {
2238         if(!g_ctf) { remove(self); return; }
2239
2240         ctf_FlagSetup(NUM_TEAM_4, self);
2241 }
2242
2243 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2244 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2245 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.
2246 Keys:
2247 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2248 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2249 void spawnfunc_ctf_team()
2250 {
2251         if(!g_ctf) { remove(self); return; }
2252         
2253         self.classname = "ctf_team";
2254         self.team = self.cnt + 1;
2255 }
2256
2257 // compatibility for quake maps
2258 void spawnfunc_team_CTF_redflag()    { spawnfunc_item_flag_team1();    }
2259 void spawnfunc_team_CTF_blueflag()   { spawnfunc_item_flag_team2();    }
2260 void spawnfunc_team_CTF_redplayer()  { spawnfunc_info_player_team1();  }
2261 void spawnfunc_team_CTF_blueplayer() { spawnfunc_info_player_team2();  }
2262 void spawnfunc_team_CTF_redspawn()   { spawnfunc_info_player_team1();  }
2263 void spawnfunc_team_CTF_bluespawn()  { spawnfunc_info_player_team2();  }
2264
2265
2266 // ==============
2267 // Initialization
2268 // ==============
2269
2270 // scoreboard setup
2271 void ctf_ScoreRules(float teams)
2272 {
2273         CheckAllowedTeams(world);
2274         ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
2275         ScoreInfo_SetLabel_TeamScore  (ST_CTF_CAPS,     "caps",      SFL_SORT_PRIO_PRIMARY);
2276         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS,     "caps",      SFL_SORT_PRIO_SECONDARY);
2277         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME,  "captime",   SFL_LOWER_IS_BETTER | SFL_TIME);
2278         ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS,  "pickups",   0);
2279         ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS,  "fckills",   0);
2280         ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS,  "returns",   0);
2281         ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS,    "drops",     SFL_LOWER_IS_BETTER);
2282         ScoreRules_basics_end();
2283 }
2284
2285 // code from here on is just to support maps that don't have flag and team entities
2286 void ctf_SpawnTeam (string teamname, float teamcolor)
2287 {
2288         entity oldself;
2289         oldself = self;
2290         self = spawn();
2291         self.classname = "ctf_team";
2292         self.netname = teamname;
2293         self.cnt = teamcolor;
2294
2295         spawnfunc_ctf_team();
2296
2297         self = oldself;
2298 }
2299
2300 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2301 {
2302         ctf_teams = 2;
2303         
2304         entity tmp_entity;
2305         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2306         if(tmp_entity.team == NUM_TEAM_3)
2307         {
2308                 ++ctf_teams;
2309                 break; // found 1 flag for this team
2310         }
2311         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2312         if(tmp_entity.team == NUM_TEAM_4)
2313         {
2314                 ++ctf_teams;
2315                 break; // found 1 flag for this team
2316         }
2317         
2318         ctf_teams = bound(2, ctf_teams, 4);
2319
2320         // if no teams are found, spawn defaults
2321         if(find(world, classname, "ctf_team") == world)
2322         {
2323                 print("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2324                 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2325                 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2326                 if(ctf_teams >= 3)
2327                         ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2328                 if(ctf_teams >= 4)
2329                         ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2330         }
2331         
2332         ctf_ScoreRules(ctf_teams);
2333 }
2334
2335 void ctf_Initialize()
2336 {
2337         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2338
2339         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2340         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2341         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2342         
2343         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2344 }
2345
2346
2347 MUTATOR_DEFINITION(gamemode_ctf)
2348 {
2349         MUTATOR_HOOK(MakePlayerObserver, ctf_RemovePlayer, CBC_ORDER_ANY);
2350         MUTATOR_HOOK(ClientDisconnect, ctf_RemovePlayer, CBC_ORDER_ANY);
2351         MUTATOR_HOOK(PlayerDies, ctf_PlayerDies, CBC_ORDER_ANY);
2352         MUTATOR_HOOK(MatchEnd, ctf_MatchEnd, CBC_ORDER_ANY);
2353         MUTATOR_HOOK(PortalTeleport, ctf_PortalTeleport, CBC_ORDER_ANY);
2354         MUTATOR_HOOK(GiveFragsForKill, ctf_GiveFragsForKill, CBC_ORDER_ANY);
2355         MUTATOR_HOOK(PlayerPreThink, ctf_PlayerPreThink, CBC_ORDER_ANY);
2356         MUTATOR_HOOK(PlayerDamage_Calculate, ctf_PlayerDamage, CBC_ORDER_ANY);
2357         MUTATOR_HOOK(PlayerUseKey, ctf_PlayerUseKey, CBC_ORDER_ANY);
2358         MUTATOR_HOOK(HelpMePing, ctf_HelpMePing, CBC_ORDER_ANY);
2359         MUTATOR_HOOK(VehicleEnter, ctf_VehicleEnter, CBC_ORDER_ANY);
2360         MUTATOR_HOOK(VehicleExit, ctf_VehicleExit, CBC_ORDER_ANY);
2361         MUTATOR_HOOK(AbortSpeedrun, ctf_AbortSpeedrun, CBC_ORDER_ANY);
2362         MUTATOR_HOOK(HavocBot_ChooseRule, ctf_BotRoles, CBC_ORDER_ANY);
2363         MUTATOR_HOOK(GetCvars, ctf_GetCvars, CBC_ORDER_ANY);
2364         MUTATOR_HOOK(GetTeamCount, ctf_GetTeamCount, CBC_ORDER_ANY);
2365         
2366         MUTATOR_ONADD
2367         {
2368                 if(time > 1) // game loads at time 1
2369                         error("This is a game type and it cannot be added at runtime.");
2370                 ctf_Initialize();
2371         }
2372
2373         MUTATOR_ONROLLBACK_OR_REMOVE
2374         {
2375                 // we actually cannot roll back ctf_Initialize here
2376                 // BUT: we don't need to! If this gets called, adding always
2377                 // succeeds.
2378         }
2379
2380         MUTATOR_ONREMOVE
2381         {
2382                 print("This is a game type and it cannot be removed at runtime.");
2383                 return -1;
2384         }
2385
2386         return 0;
2387 }