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