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