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