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