]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/mutator/gamemode_ctf.qc
CTF: rename an entity field; remove some redundant code
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / mutator / gamemode_ctf.qc
1 #include "gamemode_ctf.qh"
2
3 #ifndef CSQC
4 void ctf_Initialize();
5
6 REGISTER_MUTATOR(ctf, false)
7 {
8         MUTATOR_ONADD
9         {
10                 if (time > 1) // game loads at time 1
11                         error("This is a game type and it cannot be added at runtime.");
12                 ctf_Initialize();
13
14                 ActivateTeamplay();
15                 SetLimits(autocvar_capturelimit_override, autocvar_captureleadlimit_override, autocvar_timelimit_override, -1);
16                 have_team_spawns = -1; // request team spawns
17         }
18
19         MUTATOR_ONROLLBACK_OR_REMOVE
20         {
21                 // we actually cannot roll back ctf_Initialize here
22                 // BUT: we don't need to! If this gets called, adding always
23                 // succeeds.
24         }
25
26         MUTATOR_ONREMOVE
27         {
28                 LOG_INFO("This is a game type and it cannot be removed at runtime.");
29                 return -1;
30         }
31
32         return 0;
33 }
34 #endif
35
36 #ifdef SVQC
37 #include <common/vehicles/all.qh>
38 #include <server/teamplay.qh>
39 #endif
40
41 #include <lib/warpzone/common.qh>
42
43 bool autocvar_g_ctf_allow_vehicle_carry;
44 bool autocvar_g_ctf_allow_vehicle_touch;
45 bool autocvar_g_ctf_allow_monster_touch;
46 bool autocvar_g_ctf_throw;
47 float autocvar_g_ctf_throw_angle_max;
48 float autocvar_g_ctf_throw_angle_min;
49 int autocvar_g_ctf_throw_punish_count;
50 float autocvar_g_ctf_throw_punish_delay;
51 float autocvar_g_ctf_throw_punish_time;
52 float autocvar_g_ctf_throw_strengthmultiplier;
53 float autocvar_g_ctf_throw_velocity_forward;
54 float autocvar_g_ctf_throw_velocity_up;
55 float autocvar_g_ctf_drop_velocity_up;
56 float autocvar_g_ctf_drop_velocity_side;
57 bool autocvar_g_ctf_oneflag_reverse;
58 bool autocvar_g_ctf_portalteleport;
59 bool autocvar_g_ctf_pass;
60 float autocvar_g_ctf_pass_arc;
61 float autocvar_g_ctf_pass_arc_max;
62 float autocvar_g_ctf_pass_directional_max;
63 float autocvar_g_ctf_pass_directional_min;
64 float autocvar_g_ctf_pass_radius;
65 float autocvar_g_ctf_pass_wait;
66 bool autocvar_g_ctf_pass_request;
67 float autocvar_g_ctf_pass_turnrate;
68 float autocvar_g_ctf_pass_timelimit;
69 float autocvar_g_ctf_pass_velocity;
70 bool autocvar_g_ctf_dynamiclights;
71 float autocvar_g_ctf_flag_collect_delay;
72 float autocvar_g_ctf_flag_damageforcescale;
73 bool autocvar_g_ctf_flag_dropped_waypoint;
74 bool autocvar_g_ctf_flag_dropped_floatinwater;
75 bool autocvar_g_ctf_flag_glowtrails;
76 int autocvar_g_ctf_flag_health;
77 bool autocvar_g_ctf_flag_return;
78 bool autocvar_g_ctf_flag_return_carrying;
79 float autocvar_g_ctf_flag_return_carried_radius;
80 float autocvar_g_ctf_flag_return_time;
81 bool autocvar_g_ctf_flag_return_when_unreachable;
82 float autocvar_g_ctf_flag_return_damage;
83 float autocvar_g_ctf_flag_return_damage_delay;
84 float autocvar_g_ctf_flag_return_dropped;
85 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
86 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
87 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
88 float autocvar_g_ctf_flagcarrier_selfforcefactor;
89 float autocvar_g_ctf_flagcarrier_damagefactor;
90 float autocvar_g_ctf_flagcarrier_forcefactor;
91 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
92 bool autocvar_g_ctf_fullbrightflags;
93 bool autocvar_g_ctf_ignore_frags;
94 int autocvar_g_ctf_score_capture;
95 int autocvar_g_ctf_score_capture_assist;
96 int autocvar_g_ctf_score_kill;
97 int autocvar_g_ctf_score_penalty_drop;
98 int autocvar_g_ctf_score_penalty_returned;
99 int autocvar_g_ctf_score_pickup_base;
100 int autocvar_g_ctf_score_pickup_dropped_early;
101 int autocvar_g_ctf_score_pickup_dropped_late;
102 int autocvar_g_ctf_score_return;
103 float autocvar_g_ctf_shield_force;
104 float autocvar_g_ctf_shield_max_ratio;
105 int autocvar_g_ctf_shield_min_negscore;
106 bool autocvar_g_ctf_stalemate;
107 int autocvar_g_ctf_stalemate_endcondition;
108 float autocvar_g_ctf_stalemate_time;
109 bool autocvar_g_ctf_reverse;
110 float autocvar_g_ctf_dropped_capture_delay;
111 float autocvar_g_ctf_dropped_capture_radius;
112
113 void ctf_FakeTimeLimit(entity e, float t)
114 {
115         msg_entity = e;
116         WriteByte(MSG_ONE, 3); // svc_updatestat
117         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
118         if(t < 0)
119                 WriteCoord(MSG_ONE, autocvar_timelimit);
120         else
121                 WriteCoord(MSG_ONE, (t + 1) / 60);
122 }
123
124 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
125 {
126         if(autocvar_sv_eventlog)
127                 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
128                 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
129 }
130
131 void ctf_CaptureRecord(entity flag, entity player)
132 {
133         float cap_record = ctf_captimerecord;
134         float cap_time = (time - flag.ctf_pickuptime);
135         string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
136
137         // notify about shit
138         if(ctf_oneflag)
139                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
140         else if(!ctf_captimerecord)
141                 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, (cap_time * 100));
142         else if(cap_time < cap_record)
143                 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_BROKEN), player.netname, refername, (cap_time * 100), (cap_record * 100));
144         else
145                 Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_UNBROKEN), player.netname, refername, (cap_time * 100), (cap_record * 100));
146
147         // write that shit in the database
148         if(!ctf_oneflag) // but not in 1-flag mode
149         if((!ctf_captimerecord) || (cap_time < cap_record))
150         {
151                 ctf_captimerecord = cap_time;
152                 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
153                 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
154                 write_recordmarker(player, (time - cap_time), cap_time);
155         }
156 }
157
158 bool ctf_Return_Customize(entity this, entity client)
159 {
160         // only to the carrier
161         return boolean(client == this.owner);
162 }
163
164 void ctf_FlagcarrierWaypoints(entity player)
165 {
166         WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
167         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
168         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
169         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
170
171         if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
172         {
173                 if(!player.wps_enemyflagcarrier)
174                 {
175                         entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
176                         wp.colormod = WPCOLOR_ENEMYFC(player.team);
177                         setcefc(wp, ctf_Stalemate_Customize);
178
179                         if(IS_REAL_CLIENT(player) && !ctf_stalemate)
180                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
181                 }
182
183                 if(!player.wps_flagreturn)
184                 {
185                         entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
186                         owp.colormod = '0 0.8 0.8';
187                         //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
188                         setcefc(owp, ctf_Return_Customize);
189                 }
190         }
191 }
192
193 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
194 {
195         float current_distance = vlen((('1 0 0' * to.x) + ('0 1 0' * to.y)) - (('1 0 0' * from.x) + ('0 1 0' * from.y))); // for the sake of this check, exclude Z axis
196         float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
197         float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
198         //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
199
200         vector targpos;
201         if(current_height) // make sure we can actually do this arcing path
202         {
203                 targpos = (to + ('0 0 1' * current_height));
204                 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
205                 if(trace_fraction < 1)
206                 {
207                         //print("normal arc line failed, trying to find new pos...");
208                         WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
209                         targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
210                         WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
211                         if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
212                         /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
213                 }
214         }
215         else { targpos = to; }
216
217         //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
218
219         vector desired_direction = normalize(targpos - from);
220         if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
221         else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
222 }
223
224 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
225 {
226         if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
227         {
228                 // directional tracing only
229                 float spreadlimit;
230                 makevectors(passer_angle);
231
232                 // find the closest point on the enemy to the center of the attack
233                 float h; // hypotenuse, which is the distance between attacker to head
234                 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
235
236                 h = vlen(head_center - passer_center);
237                 a = h * (normalize(head_center - passer_center) * v_forward);
238
239                 vector nearest_on_line = (passer_center + a * v_forward);
240                 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
241
242                 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
243                 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
244
245                 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
246                         { return true; }
247                 else
248                         { return false; }
249         }
250         else { return true; }
251 }
252
253
254 // =======================
255 // CaptureShield Functions
256 // =======================
257
258 bool ctf_CaptureShield_CheckStatus(entity p)
259 {
260         int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
261         int players_worseeq, players_total;
262
263         if(ctf_captureshield_max_ratio <= 0)
264                 return false;
265
266         s  = PlayerScore_Add(p, SP_CTF_CAPS,    0);
267         s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
268         s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
269         s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
270
271         sr = ((s - s2) + (s3 + s4));
272
273         if(sr >= -ctf_captureshield_min_negscore)
274                 return false;
275
276         players_total = players_worseeq = 0;
277         FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
278                 if(DIFF_TEAM(it, p))
279                         continue;
280                 se  = PlayerScore_Add(it, SP_CTF_CAPS,    0);
281                 se2 = PlayerScore_Add(it, SP_CTF_PICKUPS, 0);
282                 se3 = PlayerScore_Add(it, SP_CTF_RETURNS, 0);
283                 se4 = PlayerScore_Add(it, SP_CTF_FCKILLS, 0);
284
285                 ser = ((se - se2) + (se3 + se4));
286
287                 if(ser <= sr)
288                         ++players_worseeq;
289                 ++players_total;
290         ));
291
292         // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
293         // use this rule here
294
295         if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
296                 return false;
297
298         return true;
299 }
300
301 void ctf_CaptureShield_Update(entity player, bool wanted_status)
302 {
303         bool updated_status = ctf_CaptureShield_CheckStatus(player);
304         if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
305         {
306                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
307                 player.ctf_captureshielded = updated_status;
308         }
309 }
310
311 bool ctf_CaptureShield_Customize(entity this, entity client)
312 {
313         if(!client.ctf_captureshielded) { return false; }
314         if(CTF_SAMETEAM(this, client)) { return false; }
315
316         return true;
317 }
318
319 void ctf_CaptureShield_Touch(entity this, entity toucher)
320 {
321         if(!toucher.ctf_captureshielded) { return; }
322         if(CTF_SAMETEAM(this, toucher)) { return; }
323
324         vector mymid = (this.absmin + this.absmax) * 0.5;
325         vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
326
327         Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
328         if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
329 }
330
331 void ctf_CaptureShield_Spawn(entity flag)
332 {
333         entity shield = new(ctf_captureshield);
334
335         shield.enemy = flag;
336         shield.team = flag.team;
337         settouch(shield, ctf_CaptureShield_Touch);
338         setcefc(shield, ctf_CaptureShield_Customize);
339         shield.effects = EF_ADDITIVE;
340         set_movetype(shield, MOVETYPE_NOCLIP);
341         shield.solid = SOLID_TRIGGER;
342         shield.avelocity = '7 0 11';
343         shield.scale = 0.5;
344
345         setorigin(shield, flag.origin);
346         setmodel(shield, MDL_CTF_SHIELD);
347         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
348 }
349
350
351 // ====================
352 // Drop/Pass/Throw Code
353 // ====================
354
355 void ctf_Handle_Drop(entity flag, entity player, int droptype)
356 {
357         // declarations
358         player = (player ? player : flag.pass_sender);
359
360         // main
361         set_movetype(flag, MOVETYPE_TOSS);
362         flag.takedamage = DAMAGE_YES;
363         flag.angles = '0 0 0';
364         flag.health = flag.max_flag_health;
365         flag.ctf_droptime = time;
366         flag.ctf_dropper = player;
367         flag.ctf_status = FLAG_DROPPED;
368
369         // messages and sounds
370         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
371         _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
372         ctf_EventLog("dropped", player.team, player);
373
374         // scoring
375         PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
376         PlayerScore_Add(player, SP_CTF_DROPS, 1);
377
378         // waypoints
379         if(autocvar_g_ctf_flag_dropped_waypoint) {
380                 entity wp = WaypointSprite_Spawn(WP_FlagDropped, 0, 0, flag, FLAG_WAYPOINT_OFFSET, NULL, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG);
381                 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
382         }
383
384         if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
385         {
386                 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
387                 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
388         }
389
390         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
391
392         if(droptype == DROP_PASS)
393         {
394                 flag.pass_distance = 0;
395                 flag.pass_sender = NULL;
396                 flag.pass_target = NULL;
397         }
398 }
399
400 void ctf_Handle_Retrieve(entity flag, entity player)
401 {
402         entity sender = flag.pass_sender;
403
404         // transfer flag to player
405         flag.owner = player;
406         flag.owner.flagcarried = flag;
407
408         // reset flag
409         if(player.vehicle)
410         {
411                 setattachment(flag, player.vehicle, "");
412                 setorigin(flag, VEHICLE_FLAG_OFFSET);
413                 flag.scale = VEHICLE_FLAG_SCALE;
414         }
415         else
416         {
417                 setattachment(flag, player, "");
418                 setorigin(flag, FLAG_CARRY_OFFSET);
419         }
420         set_movetype(flag, MOVETYPE_NONE);
421         flag.takedamage = DAMAGE_NO;
422         flag.solid = SOLID_NOT;
423         flag.angles = '0 0 0';
424         flag.ctf_status = FLAG_CARRY;
425
426         // messages and sounds
427         _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
428         ctf_EventLog("receive", flag.team, player);
429
430         FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), LAMBDA(
431                 if(it == sender)
432                         Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
433                 else if(it == player)
434                         Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
435                 else if(SAME_TEAM(it, sender))
436                         Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
437         ));
438
439         // create new waypoint
440         ctf_FlagcarrierWaypoints(player);
441
442         sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
443         player.throw_antispam = sender.throw_antispam;
444
445         flag.pass_distance = 0;
446         flag.pass_sender = NULL;
447         flag.pass_target = NULL;
448 }
449
450 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
451 {
452         entity flag = player.flagcarried;
453         vector targ_origin, flag_velocity;
454
455         if(!flag) { return; }
456         if((droptype == DROP_PASS) && !receiver) { return; }
457
458         if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
459
460         // reset the flag
461         setattachment(flag, NULL, "");
462         setorigin(flag, player.origin + FLAG_DROP_OFFSET);
463         flag.owner.flagcarried = NULL;
464         flag.owner = NULL;
465         flag.solid = SOLID_TRIGGER;
466         flag.ctf_dropper = player;
467         flag.ctf_droptime = time;
468
469         flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
470
471         switch(droptype)
472         {
473                 case DROP_PASS:
474                 {
475                         // warpzone support:
476                         // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
477                         // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
478                         WarpZone_RefSys_Copy(flag, receiver);
479                         WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
480                         targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
481
482                         flag.pass_distance = vlen((('1 0 0' * targ_origin.x) + ('0 1 0' * targ_origin.y)) - (('1 0 0' *  player.origin.x) + ('0 1 0' *  player.origin.y))); // for the sake of this check, exclude Z axis
483                         ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
484
485                         // main
486                         set_movetype(flag, MOVETYPE_FLY);
487                         flag.takedamage = DAMAGE_NO;
488                         flag.pass_sender = player;
489                         flag.pass_target = receiver;
490                         flag.ctf_status = FLAG_PASSING;
491
492                         // other
493                         _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
494                         WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
495                         ctf_EventLog("pass", flag.team, player);
496                         break;
497                 }
498
499                 case DROP_THROW:
500                 {
501                         makevectors((player.v_angle.y * '0 1 0') + (bound(autocvar_g_ctf_throw_angle_min, player.v_angle.x, autocvar_g_ctf_throw_angle_max) * '1 0 0'));
502
503                         flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & ITEM_Strength.m_itemid) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
504                         flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
505                         ctf_Handle_Drop(flag, player, droptype);
506                         break;
507                 }
508
509                 case DROP_RESET:
510                 {
511                         flag.velocity = '0 0 0'; // do nothing
512                         break;
513                 }
514
515                 default:
516                 case DROP_NORMAL:
517                 {
518                         flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, (('0 0 1' * autocvar_g_ctf_drop_velocity_up) + ((('0 1 0' * crandom()) + ('1 0 0' * crandom())) * autocvar_g_ctf_drop_velocity_side)), false);
519                         ctf_Handle_Drop(flag, player, droptype);
520                         break;
521                 }
522         }
523
524         // kill old waypointsprite
525         WaypointSprite_Ping(player.wps_flagcarrier);
526         WaypointSprite_Kill(player.wps_flagcarrier);
527
528         if(player.wps_enemyflagcarrier)
529                 WaypointSprite_Kill(player.wps_enemyflagcarrier);
530
531         if(player.wps_flagreturn)
532                 WaypointSprite_Kill(player.wps_flagreturn);
533
534         // captureshield
535         ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
536 }
537
538 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
539 {
540         return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
541 }
542
543 // ==============
544 // Event Handlers
545 // ==============
546
547 void nades_GiveBonus(entity player, float score);
548
549 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
550 {
551         entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
552         entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
553         entity player_team_flag = NULL, tmp_entity;
554         float old_time, new_time;
555
556         if(!player) { return; } // without someone to give the reward to, we can't possibly cap
557         if(CTF_DIFFTEAM(player, flag)) { return; }
558
559         if(ctf_oneflag)
560         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
561         if(SAME_TEAM(tmp_entity, player))
562         {
563                 player_team_flag = tmp_entity;
564                 break;
565         }
566
567         nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
568
569         player.throw_prevtime = time;
570         player.throw_count = 0;
571
572         // messages and sounds
573         Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
574         ctf_CaptureRecord(enemy_flag, player);
575         _sound(player, CH_TRIGGER, ((ctf_oneflag) ? player_team_flag.snd_flag_capture : ((DIFF_TEAM(player, flag)) ? enemy_flag.snd_flag_capture : flag.snd_flag_capture)), VOL_BASE, ATTEN_NONE);
576
577         switch(capturetype)
578         {
579                 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
580                 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
581                 default: break;
582         }
583
584         // scoring
585         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
586         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
587
588         old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
589         new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
590         if(!old_time || new_time < old_time)
591                 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
592
593         // effects
594         Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
595         //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
596
597         // other
598         if(capturetype == CAPTURE_NORMAL)
599         {
600                 WaypointSprite_Kill(player.wps_flagcarrier);
601                 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
602
603                 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
604                         { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
605         }
606
607         // reset the flag
608         player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
609         ctf_RespawnFlag(enemy_flag);
610 }
611
612 void ctf_Handle_Return(entity flag, entity player)
613 {
614         // messages and sounds
615         if(IS_MONSTER(player))
616         {
617                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
618         }
619         else if(flag.team)
620         {
621                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
622                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
623         }
624         _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
625         ctf_EventLog("return", flag.team, player);
626
627         // scoring
628         if(IS_PLAYER(player))
629         {
630                 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
631                 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
632
633                 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
634         }
635
636         TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
637
638         if(flag.ctf_dropper)
639         {
640                 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
641                 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
642                 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
643         }
644
645         // other
646         if(player.flagcarried == flag)
647                 WaypointSprite_Kill(player.wps_flagcarrier);
648
649         // reset the flag
650         ctf_RespawnFlag(flag);
651 }
652
653 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
654 {
655         // declarations
656         float pickup_dropped_score; // used to calculate dropped pickup score
657
658         // attach the flag to the player
659         flag.owner = player;
660         player.flagcarried = flag;
661         if(player.vehicle)
662         {
663                 setattachment(flag, player.vehicle, "");
664                 setorigin(flag, VEHICLE_FLAG_OFFSET);
665                 flag.scale = VEHICLE_FLAG_SCALE;
666         }
667         else
668         {
669                 setattachment(flag, player, "");
670                 setorigin(flag, FLAG_CARRY_OFFSET);
671         }
672
673         // flag setup
674         set_movetype(flag, MOVETYPE_NONE);
675         flag.takedamage = DAMAGE_NO;
676         flag.solid = SOLID_NOT;
677         flag.angles = '0 0 0';
678         flag.ctf_status = FLAG_CARRY;
679
680         switch(pickuptype)
681         {
682                 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
683                 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
684                 default: break;
685         }
686
687         // messages and sounds
688         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
689         if(ctf_stalemate)
690                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
691         if(!flag.team)
692                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
693         else if(CTF_DIFFTEAM(player, flag))
694                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
695         else
696                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_TEAM : CENTER_CTF_PICKUP_TEAM_ENEMY), Team_ColorCode(flag.team));
697
698         Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
699
700         if(!flag.team)
701                 FOREACH_CLIENT(IS_PLAYER(it) && it != player && DIFF_TEAM(it, player), LAMBDA(Send_Notification(NOTIF_ONE, it, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname)));
702
703         if(flag.team)
704                 FOREACH_CLIENT(IS_PLAYER(it) && it != player, LAMBDA(
705                         if(CTF_SAMETEAM(flag, it))
706                         if(SAME_TEAM(player, it))
707                                 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
708                         else
709                                 Send_Notification(NOTIF_ONE, it, MSG_CHOICE, ((SAME_TEAM(flag, player)) ? CHOICE_CTF_PICKUP_ENEMY_TEAM : CHOICE_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), player.netname);
710                 ));
711
712         _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
713
714         // scoring
715         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
716         nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
717         switch(pickuptype)
718         {
719                 case PICKUP_BASE:
720                 {
721                         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
722                         ctf_EventLog("steal", flag.team, player);
723                         break;
724                 }
725
726                 case PICKUP_DROPPED:
727                 {
728                         pickup_dropped_score = (autocvar_g_ctf_flag_return_time ? bound(0, ((flag.ctf_droptime + autocvar_g_ctf_flag_return_time) - time) / autocvar_g_ctf_flag_return_time, 1) : 1);
729                         pickup_dropped_score = floor((autocvar_g_ctf_score_pickup_dropped_late * (1 - pickup_dropped_score) + autocvar_g_ctf_score_pickup_dropped_early * pickup_dropped_score) + 0.5);
730                         LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
731                         PlayerTeamScore_AddScore(player, pickup_dropped_score);
732                         ctf_EventLog("pickup", flag.team, player);
733                         break;
734                 }
735
736                 default: break;
737         }
738
739         // speedrunning
740         if(pickuptype == PICKUP_BASE)
741         {
742                 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
743                 if((player.speedrunning) && (ctf_captimerecord))
744                         ctf_FakeTimeLimit(player, time + ctf_captimerecord);
745         }
746
747         // effects
748         Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
749
750         // waypoints
751         if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
752         ctf_FlagcarrierWaypoints(player);
753         WaypointSprite_Ping(player.wps_flagcarrier);
754 }
755
756
757 // ===================
758 // Main Flag Functions
759 // ===================
760
761 void ctf_CheckFlagReturn(entity flag, int returntype)
762 {
763         if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
764         {
765                 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
766
767                 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
768                 {
769                         switch(returntype)
770                         {
771                                 case RETURN_DROPPED:
772                                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
773                                 case RETURN_DAMAGE:
774                                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
775                                 case RETURN_SPEEDRUN:
776                                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), ctf_captimerecord); break;
777                                 case RETURN_NEEDKILL:
778                                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
779                                 default:
780                                 case RETURN_TIMEOUT:
781                                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
782                         }
783                         _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
784                         ctf_EventLog("returned", flag.team, NULL);
785                         ctf_RespawnFlag(flag);
786                 }
787         }
788 }
789
790 bool ctf_Stalemate_Customize(entity this, entity client)
791 {
792         // make spectators see what the player would see
793         entity e = WaypointSprite_getviewentity(client);
794         entity wp_owner = this.owner;
795
796         // team waypoints
797         //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
798         if(SAME_TEAM(wp_owner, e)) { return false; }
799         if(!IS_PLAYER(e)) { return false; }
800
801         return true;
802 }
803
804 void ctf_CheckStalemate()
805 {
806         // declarations
807         int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
808         entity tmp_entity;
809
810         entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
811
812         // build list of stale flags
813         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
814         {
815                 if(autocvar_g_ctf_stalemate)
816                 if(tmp_entity.ctf_status != FLAG_BASE)
817                 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
818                 {
819                         tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
820                         ctf_staleflaglist = tmp_entity;
821
822                         switch(tmp_entity.team)
823                         {
824                                 case NUM_TEAM_1: ++stale_red_flags; break;
825                                 case NUM_TEAM_2: ++stale_blue_flags; break;
826                                 case NUM_TEAM_3: ++stale_yellow_flags; break;
827                                 case NUM_TEAM_4: ++stale_pink_flags; break;
828                                 default: ++stale_neutral_flags; break;
829                         }
830                 }
831         }
832
833         if(ctf_oneflag)
834                 stale_flags = (stale_neutral_flags >= 1);
835         else
836                 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
837
838         if(ctf_oneflag && stale_flags == 1)
839                 ctf_stalemate = true;
840         else if(stale_flags >= 2)
841                 ctf_stalemate = true;
842         else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
843                 { ctf_stalemate = false; wpforenemy_announced = false; }
844         else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
845                 { ctf_stalemate = false; wpforenemy_announced = false; }
846
847         // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
848         if(ctf_stalemate)
849         {
850                 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
851                 {
852                         if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
853                         {
854                                 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, NULL, 0, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG);
855                                 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
856                                 setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
857                         }
858                 }
859
860                 if (!wpforenemy_announced)
861                 {
862                         FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), LAMBDA(Send_Notification(NOTIF_ONE, it, MSG_CENTER, ((it.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER))));
863
864                         wpforenemy_announced = true;
865                 }
866         }
867 }
868
869 void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
870 {
871         if(ITEM_DAMAGE_NEEDKILL(deathtype))
872         {
873                 if(autocvar_g_ctf_flag_return_damage_delay)
874                         this.ctf_flagdamaged_byworld = true;
875                 else
876                 {
877                         this.health = 0;
878                         ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
879                 }
880                 return;
881         }
882         if(autocvar_g_ctf_flag_return_damage)
883         {
884                 // reduce health and check if it should be returned
885                 this.health = this.health - damage;
886                 ctf_CheckFlagReturn(this, RETURN_DAMAGE);
887                 return;
888         }
889 }
890
891 void ctf_FlagThink(entity this)
892 {
893         // declarations
894         entity tmp_entity;
895
896         this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
897
898         // captureshield
899         if(this == ctf_worldflaglist) // only for the first flag
900                 FOREACH_CLIENT(true, LAMBDA(ctf_CaptureShield_Update(it, 1))); // release shield only
901
902         // sanity checks
903         if(this.mins != CTF_FLAG.m_mins || this.maxs != CTF_FLAG.m_maxs) { // reset the flag boundaries in case it got squished
904                 LOG_TRACE("wtf the flag got squashed?");
905                 tracebox(this.origin, CTF_FLAG.m_mins, CTF_FLAG.m_maxs, this.origin, MOVE_NOMONSTERS, this);
906                 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
907                         setsize(this, CTF_FLAG.m_mins, CTF_FLAG.m_maxs);
908         }
909
910         // main think method
911         switch(this.ctf_status)
912         {
913                 case FLAG_BASE:
914                 {
915                         if(autocvar_g_ctf_dropped_capture_radius)
916                         {
917                                 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
918                                         if(tmp_entity.ctf_status == FLAG_DROPPED)
919                                         if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
920                                         if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
921                                                 ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
922                         }
923                         return;
924                 }
925
926                 case FLAG_DROPPED:
927                 {
928                         this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
929
930                         if(autocvar_g_ctf_flag_dropped_floatinwater)
931                         {
932                                 vector midpoint = ((this.absmin + this.absmax) * 0.5);
933                                 if(pointcontents(midpoint) == CONTENT_WATER)
934                                 {
935                                         this.velocity = this.velocity * 0.5;
936
937                                         if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
938                                                 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
939                                         else
940                                                 { set_movetype(this, MOVETYPE_FLY); }
941                                 }
942                                 else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
943                         }
944                         if(autocvar_g_ctf_flag_return_dropped)
945                         {
946                                 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
947                                 {
948                                         this.health = 0;
949                                         ctf_CheckFlagReturn(this, RETURN_DROPPED);
950                                         return;
951                                 }
952                         }
953                         if(this.ctf_flagdamaged_byworld)
954                         {
955                                 this.health -= ((this.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
956                                 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
957                                 return;
958                         }
959                         else if(autocvar_g_ctf_flag_return_time)
960                         {
961                                 this.health -= ((this.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
962                                 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
963                                 return;
964                         }
965                         return;
966                 }
967
968                 case FLAG_CARRY:
969                 {
970                         if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
971                         {
972                                 this.health = 0;
973                                 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
974
975                                 this.owner.impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
976                                 ImpulseCommands(this.owner);
977                         }
978                         if(autocvar_g_ctf_stalemate)
979                         {
980                                 if(time >= wpforenemy_nextthink)
981                                 {
982                                         ctf_CheckStalemate();
983                                         wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
984                                 }
985                         }
986                         if(CTF_SAMETEAM(this, this.owner) && this.team)
987                         {
988                                 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
989                                         ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
990                                 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
991                                         ctf_Handle_Return(this, this.owner);
992                         }
993                         return;
994                 }
995
996                 case FLAG_PASSING:
997                 {
998                         vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
999                         targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
1000                         WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
1001
1002                         if((this.pass_target == NULL)
1003                                 || (IS_DEAD(this.pass_target))
1004                                 || (this.pass_target.flagcarried)
1005                                 || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
1006                                 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
1007                                 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1008                         {
1009                                 // give up, pass failed
1010                                 ctf_Handle_Drop(this, NULL, DROP_PASS);
1011                         }
1012                         else
1013                         {
1014                                 // still a viable target, go for it
1015                                 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
1016                         }
1017                         return;
1018                 }
1019
1020                 default: // this should never happen
1021                 {
1022                         LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
1023                         return;
1024                 }
1025         }
1026 }
1027
1028 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1029 {
1030         return = false;
1031         if(gameover) { return; }
1032         if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1033
1034         bool is_not_monster = (!IS_MONSTER(toucher));
1035
1036         // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1037         if(ITEM_TOUCH_NEEDKILL())
1038         {
1039                 if(!autocvar_g_ctf_flag_return_damage_delay)
1040                 {
1041                         flag.health = 0;
1042                         ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1043                 }
1044                 if(!flag.ctf_flagdamaged_byworld) { return; }
1045         }
1046
1047         int num_perteam = 0;
1048         FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), LAMBDA(++num_perteam));
1049
1050         // special touch behaviors
1051         if(STAT(FROZEN, toucher)) { return; }
1052         else if(IS_VEHICLE(toucher))
1053         {
1054                 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1055                         toucher = toucher.owner; // the player is actually the vehicle owner, not other
1056                 else
1057                         return; // do nothing
1058         }
1059         else if(IS_MONSTER(toucher))
1060         {
1061                 if(!autocvar_g_ctf_allow_monster_touch)
1062                         return; // do nothing
1063         }
1064         else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1065         {
1066                 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1067                 {
1068                         Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1069                         _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1070                         flag.wait = time + FLAG_TOUCHRATE;
1071                 }
1072                 return;
1073         }
1074         else if(IS_DEAD(toucher)) { return; }
1075
1076         switch(flag.ctf_status)
1077         {
1078                 case FLAG_BASE:
1079                 {
1080                         if(ctf_oneflag)
1081                         {
1082                                 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1083                                         ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1084                                 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1085                                         ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1086                         }
1087                         else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1088                                 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1089                         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)
1090                         {
1091                                 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1092                                 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1093                         }
1094                         else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1095                                 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1096                         break;
1097                 }
1098
1099                 case FLAG_DROPPED:
1100                 {
1101                         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
1102                                 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1103                         else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1104                                 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1105                         break;
1106                 }
1107
1108                 case FLAG_CARRY:
1109                 {
1110                         LOG_TRACE("Someone touched a flag even though it was being carried?");
1111                         break;
1112                 }
1113
1114                 case FLAG_PASSING:
1115                 {
1116                         if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1117                         {
1118                                 if(DIFF_TEAM(toucher, flag.pass_sender))
1119                                         ctf_Handle_Return(flag, toucher);
1120                                 else
1121                                         ctf_Handle_Retrieve(flag, toucher);
1122                         }
1123                         break;
1124                 }
1125         }
1126 }
1127
1128 .float last_respawn;
1129 void ctf_RespawnFlag(entity flag)
1130 {
1131         // check for flag respawn being called twice in a row
1132         if(flag.last_respawn > time - 0.5)
1133                 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1134
1135         flag.last_respawn = time;
1136
1137         // reset the player (if there is one)
1138         if((flag.owner) && (flag.owner.flagcarried == flag))
1139         {
1140                 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1141                 WaypointSprite_Kill(flag.owner.wps_flagreturn);
1142                 WaypointSprite_Kill(flag.wps_flagcarrier);
1143
1144                 flag.owner.flagcarried = NULL;
1145
1146                 if(flag.speedrunning)
1147                         ctf_FakeTimeLimit(flag.owner, -1);
1148         }
1149
1150         if((flag.owner) && (flag.owner.vehicle))
1151                 flag.scale = FLAG_SCALE;
1152
1153         if(flag.ctf_status == FLAG_DROPPED)
1154                 { WaypointSprite_Kill(flag.wps_flagdropped); }
1155
1156         // reset the flag
1157         setattachment(flag, NULL, "");
1158         setorigin(flag, flag.ctf_spawnorigin);
1159
1160         set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS));
1161         flag.takedamage = DAMAGE_NO;
1162         flag.health = flag.max_flag_health;
1163         flag.solid = SOLID_TRIGGER;
1164         flag.velocity = '0 0 0';
1165         flag.angles = flag.mangle;
1166         flag.flags = FL_ITEM | FL_NOTARGET;
1167
1168         flag.ctf_status = FLAG_BASE;
1169         flag.owner = NULL;
1170         flag.pass_distance = 0;
1171         flag.pass_sender = NULL;
1172         flag.pass_target = NULL;
1173         flag.ctf_dropper = NULL;
1174         flag.ctf_pickuptime = 0;
1175         flag.ctf_droptime = 0;
1176         flag.ctf_flagdamaged_byworld = false;
1177
1178         ctf_CheckStalemate();
1179 }
1180
1181 void ctf_Reset(entity this)
1182 {
1183         if(this.owner && IS_PLAYER(this.owner))
1184         ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1185
1186         ctf_RespawnFlag(this);
1187 }
1188
1189 bool ctf_FlagBase_Customize(entity this, entity client)
1190 {
1191         if(client.flagcarried && CTF_SAMETEAM(client, client.flagcarried))
1192                 return false;
1193         return true;
1194 }
1195
1196 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1197 {
1198         // bot waypoints
1199         waypoint_spawnforitem_force(this, this.origin);
1200         this.nearestwaypointtimeout = 0; // activate waypointing again
1201         this.bot_basewaypoint = this.nearestwaypoint;
1202
1203         // waypointsprites
1204         entity basename;
1205         switch (this.team)
1206         {
1207                 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1208                 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1209                 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1210                 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1211                 default: basename = WP_FlagBaseNeutral; break;
1212         }
1213
1214         entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1215         wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1216         WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1217         setcefc(wp, ctf_FlagBase_Customize);
1218
1219         // captureshield setup
1220         ctf_CaptureShield_Spawn(this);
1221 }
1222
1223 .bool pushable;
1224
1225 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1226 {
1227         // main setup
1228         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1229         ctf_worldflaglist = flag;
1230
1231         setattachment(flag, NULL, "");
1232
1233         flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1234         flag.team = teamnumber;
1235         flag.classname = "item_flag_team";
1236         flag.target = "###item###"; // wut?
1237         flag.flags = FL_ITEM | FL_NOTARGET;
1238         IL_PUSH(g_items, flag);
1239         flag.solid = SOLID_TRIGGER;
1240         flag.takedamage = DAMAGE_NO;
1241         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1242         flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1243         flag.health = flag.max_flag_health;
1244         flag.event_damage = ctf_FlagDamage;
1245         flag.pushable = true;
1246         flag.teleportable = TELEPORT_NORMAL;
1247         flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1248         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1249         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1250         if(flag.damagedbycontents)
1251                 IL_PUSH(g_damagedbycontents, flag);
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, CheckAllowedTeams)
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 }