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