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