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