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