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