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