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