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