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