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