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