]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/mutator/gamemode_ctf.qc
Bot AI: avoid wasting a lot of CPU time when bot is playing middlefield role and...
[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_goalrating_ctf_carrieritems(entity this, float ratingscale, vector org, float sradius)
1576 {
1577         IL_EACH(g_items, it.bot_pickup,
1578         {
1579                 // gather health and armor only
1580                 if (it.solid)
1581                 if (it.health || it.armorvalue)
1582                 if (vdist(it.origin - org, <, sradius))
1583                 {
1584                         float t = it.bot_pickupevalfunc(this, it) * 0.0001;
1585                         if (t > 0)
1586                                 navigation_routerating(this, it, t * ratingscale, 500);
1587                 }
1588         });
1589 }
1590
1591 void havocbot_ctf_reset_role(entity this)
1592 {
1593         float cdefense, cmiddle, coffense;
1594         entity mf, ef;
1595         float c;
1596
1597         if(IS_DEAD(this))
1598                 return;
1599
1600         // Check ctf flags
1601         if (this.flagcarried)
1602         {
1603                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1604                 return;
1605         }
1606
1607         mf = havocbot_ctf_find_flag(this);
1608         ef = havocbot_ctf_find_enemy_flag(this);
1609
1610         // Retrieve stolen flag
1611         if(mf.ctf_status!=FLAG_BASE)
1612         {
1613                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1614                 return;
1615         }
1616
1617         // If enemy flag is taken go to the middle to intercept pursuers
1618         if(ef.ctf_status!=FLAG_BASE)
1619         {
1620                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1621                 return;
1622         }
1623
1624         // if there is only me on the team switch to offense
1625         c = 0;
1626         FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(it, this), { ++c; });
1627
1628         if(c==1)
1629         {
1630                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1631                 return;
1632         }
1633
1634         // Evaluate best position to take
1635         // Count mates on middle position
1636         cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1637
1638         // Count mates on defense position
1639         cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1640
1641         // Count mates on offense position
1642         coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1643
1644         if(cdefense<=coffense)
1645                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1646         else if(coffense<=cmiddle)
1647                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1648         else
1649                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1650 }
1651
1652 bool havocbot_ctf_is_basewaypoint(entity item)
1653 {
1654         if (item.classname != "waypoint")
1655                 return false;
1656
1657         entity head = ctf_worldflaglist;
1658         while (head)
1659         {
1660                 if (item == head.bot_basewaypoint)
1661                         return true;
1662                 head = head.ctf_worldflagnext;
1663         }
1664         return false;
1665 }
1666
1667 void havocbot_role_ctf_carrier(entity this)
1668 {
1669         if(IS_DEAD(this))
1670         {
1671                 havocbot_ctf_reset_role(this);
1672                 return;
1673         }
1674
1675         if (this.flagcarried == NULL)
1676         {
1677                 havocbot_ctf_reset_role(this);
1678                 return;
1679         }
1680
1681         if (navigation_goalrating_timeout(this))
1682         {
1683                 navigation_goalrating_start(this);
1684
1685                 if(ctf_oneflag)
1686                         havocbot_goalrating_ctf_enemybase(this, 50000);
1687                 else
1688                         havocbot_goalrating_ctf_ourbase(this, 50000);
1689
1690                 if(this.health<100)
1691                         havocbot_goalrating_ctf_carrieritems(this, 1000, this.origin, 1000);
1692
1693                 navigation_goalrating_end(this);
1694
1695                 navigation_goalrating_timeout_set(this);
1696
1697                 entity goal = this.goalentity;
1698                 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1699                         this.goalentity_lock_timeout = time + 5;
1700
1701                 if (goal)
1702                         this.havocbot_cantfindflag = time + 10;
1703                 else if (time > this.havocbot_cantfindflag)
1704                 {
1705                         // Can't navigate to my own base, suicide!
1706                         // TODO: drop it and wander around
1707                         Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1708                         return;
1709                 }
1710         }
1711 }
1712
1713 void havocbot_role_ctf_escort(entity this)
1714 {
1715         entity mf, ef;
1716
1717         if(IS_DEAD(this))
1718         {
1719                 havocbot_ctf_reset_role(this);
1720                 return;
1721         }
1722
1723         if (this.flagcarried)
1724         {
1725                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1726                 return;
1727         }
1728
1729         // If enemy flag is back on the base switch to previous role
1730         ef = havocbot_ctf_find_enemy_flag(this);
1731         if(ef.ctf_status==FLAG_BASE)
1732         {
1733                 this.havocbot_role = this.havocbot_previous_role;
1734                 this.havocbot_role_timeout = 0;
1735                 return;
1736         }
1737         if (ef.ctf_status == FLAG_DROPPED)
1738         {
1739                 navigation_goalrating_timeout_expire(this, 1);
1740                 return;
1741         }
1742
1743         // If the flag carrier reached the base switch to defense
1744         mf = havocbot_ctf_find_flag(this);
1745         if(mf.ctf_status!=FLAG_BASE)
1746         if(vdist(ef.origin - mf.dropped_origin, <, 300))
1747         {
1748                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1749                 return;
1750         }
1751
1752         // Set the role timeout if necessary
1753         if (!this.havocbot_role_timeout)
1754         {
1755                 this.havocbot_role_timeout = time + random() * 30 + 60;
1756         }
1757
1758         // If nothing happened just switch to previous role
1759         if (time > this.havocbot_role_timeout)
1760         {
1761                 this.havocbot_role = this.havocbot_previous_role;
1762                 this.havocbot_role_timeout = 0;
1763                 return;
1764         }
1765
1766         // Chase the flag carrier
1767         if (navigation_goalrating_timeout(this))
1768         {
1769                 navigation_goalrating_start(this);
1770
1771                 havocbot_goalrating_ctf_enemyflag(this, 30000);
1772                 havocbot_goalrating_ctf_ourstolenflag(this, 40000);
1773                 havocbot_goalrating_items(this, 10000, this.origin, 10000);
1774
1775                 navigation_goalrating_end(this);
1776
1777                 navigation_goalrating_timeout_set(this);
1778         }
1779 }
1780
1781 void havocbot_role_ctf_offense(entity this)
1782 {
1783         entity mf, ef;
1784         vector pos;
1785
1786         if(IS_DEAD(this))
1787         {
1788                 havocbot_ctf_reset_role(this);
1789                 return;
1790         }
1791
1792         if (this.flagcarried)
1793         {
1794                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1795                 return;
1796         }
1797
1798         // Check flags
1799         mf = havocbot_ctf_find_flag(this);
1800         ef = havocbot_ctf_find_enemy_flag(this);
1801
1802         // Own flag stolen
1803         if(mf.ctf_status!=FLAG_BASE)
1804         {
1805                 if(mf.tag_entity)
1806                         pos = mf.tag_entity.origin;
1807                 else
1808                         pos = mf.origin;
1809
1810                 // Try to get it if closer than the enemy base
1811                 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1812                 {
1813                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1814                         return;
1815                 }
1816         }
1817
1818         // Escort flag carrier
1819         if(ef.ctf_status!=FLAG_BASE)
1820         {
1821                 if(ef.tag_entity)
1822                         pos = ef.tag_entity.origin;
1823                 else
1824                         pos = ef.origin;
1825
1826                 if(vdist(pos - mf.dropped_origin, >, 700))
1827                 {
1828                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1829                         return;
1830                 }
1831         }
1832
1833         // About to fail, switch to middlefield
1834         if(this.health<50)
1835         {
1836                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1837                 return;
1838         }
1839
1840         // Set the role timeout if necessary
1841         if (!this.havocbot_role_timeout)
1842                 this.havocbot_role_timeout = time + 120;
1843
1844         if (time > this.havocbot_role_timeout)
1845         {
1846                 havocbot_ctf_reset_role(this);
1847                 return;
1848         }
1849
1850         if (navigation_goalrating_timeout(this))
1851         {
1852                 navigation_goalrating_start(this);
1853
1854                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1855                 havocbot_goalrating_ctf_enemybase(this, 20000);
1856                 havocbot_goalrating_items(this, 5000, this.origin, 1000);
1857                 havocbot_goalrating_items(this, 1000, this.origin, 10000);
1858
1859                 navigation_goalrating_end(this);
1860
1861                 navigation_goalrating_timeout_set(this);
1862         }
1863 }
1864
1865 // Retriever (temporary role):
1866 void havocbot_role_ctf_retriever(entity this)
1867 {
1868         entity mf;
1869
1870         if(IS_DEAD(this))
1871         {
1872                 havocbot_ctf_reset_role(this);
1873                 return;
1874         }
1875
1876         if (this.flagcarried)
1877         {
1878                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1879                 return;
1880         }
1881
1882         // If flag is back on the base switch to previous role
1883         mf = havocbot_ctf_find_flag(this);
1884         if(mf.ctf_status==FLAG_BASE)
1885         {
1886                 if (mf.enemy == this) // did this bot return the flag?
1887                         navigation_goalrating_timeout_force(this);
1888                 havocbot_ctf_reset_role(this);
1889                 return;
1890         }
1891
1892         if (!this.havocbot_role_timeout)
1893                 this.havocbot_role_timeout = time + 20;
1894
1895         if (time > this.havocbot_role_timeout)
1896         {
1897                 havocbot_ctf_reset_role(this);
1898                 return;
1899         }
1900
1901         if (navigation_goalrating_timeout(this))
1902         {
1903                 float rt_radius;
1904                 rt_radius = 10000;
1905
1906                 navigation_goalrating_start(this);
1907
1908                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1909                 havocbot_goalrating_ctf_droppedflags(this, 40000, this.origin, rt_radius);
1910                 havocbot_goalrating_ctf_enemybase(this, 30000);
1911                 havocbot_goalrating_items(this, 500, this.origin, rt_radius);
1912
1913                 navigation_goalrating_end(this);
1914
1915                 navigation_goalrating_timeout_set(this);
1916         }
1917 }
1918
1919 void havocbot_role_ctf_middle(entity this)
1920 {
1921         entity mf;
1922
1923         if(IS_DEAD(this))
1924         {
1925                 havocbot_ctf_reset_role(this);
1926                 return;
1927         }
1928
1929         if (this.flagcarried)
1930         {
1931                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1932                 return;
1933         }
1934
1935         mf = havocbot_ctf_find_flag(this);
1936         if(mf.ctf_status!=FLAG_BASE)
1937         {
1938                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1939                 return;
1940         }
1941
1942         if (!this.havocbot_role_timeout)
1943                 this.havocbot_role_timeout = time + 10;
1944
1945         if (time > this.havocbot_role_timeout)
1946         {
1947                 havocbot_ctf_reset_role(this);
1948                 return;
1949         }
1950
1951         if (navigation_goalrating_timeout(this))
1952         {
1953                 vector org;
1954
1955                 org = havocbot_middlepoint;
1956                 org.z = this.origin.z;
1957
1958                 navigation_goalrating_start(this);
1959
1960                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1961                 havocbot_goalrating_ctf_droppedflags(this, 30000, this.origin, 10000);
1962                 havocbot_goalrating_enemyplayers(this, 10000, org, havocbot_middlepoint_radius * 0.5);
1963                 havocbot_goalrating_items(this, 5000, org, havocbot_middlepoint_radius * 0.5);
1964                 havocbot_goalrating_items(this, 2500, this.origin, 10000);
1965                 havocbot_goalrating_ctf_enemybase(this, 2500);
1966
1967                 navigation_goalrating_end(this);
1968
1969                 entity goal = this.goalentity;
1970                 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1971                         this.goalentity_lock_timeout = time + 2;
1972
1973                 navigation_goalrating_timeout_set(this);
1974         }
1975 }
1976
1977 void havocbot_role_ctf_defense(entity this)
1978 {
1979         entity mf;
1980
1981         if(IS_DEAD(this))
1982         {
1983                 havocbot_ctf_reset_role(this);
1984                 return;
1985         }
1986
1987         if (this.flagcarried)
1988         {
1989                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1990                 return;
1991         }
1992
1993         // If own flag was captured
1994         mf = havocbot_ctf_find_flag(this);
1995         if(mf.ctf_status!=FLAG_BASE)
1996         {
1997                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1998                 return;
1999         }
2000
2001         if (!this.havocbot_role_timeout)
2002                 this.havocbot_role_timeout = time + 30;
2003
2004         if (time > this.havocbot_role_timeout)
2005         {
2006                 havocbot_ctf_reset_role(this);
2007                 return;
2008         }
2009         if (navigation_goalrating_timeout(this))
2010         {
2011                 vector org = mf.dropped_origin;
2012
2013                 navigation_goalrating_start(this);
2014
2015                 // if enemies are closer to our base, go there
2016                 entity closestplayer = NULL;
2017                 float distance, bestdistance = 10000;
2018                 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2019                         distance = vlen(org - it.origin);
2020                         if(distance<bestdistance)
2021                         {
2022                                 closestplayer = it;
2023                                 bestdistance = distance;
2024                         }
2025                 });
2026
2027                 if(closestplayer)
2028                 if(DIFF_TEAM(closestplayer, this))
2029                 if(vdist(org - this.origin, >, 1000))
2030                 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2031                         havocbot_goalrating_ctf_ourbase(this, 30000);
2032
2033                 havocbot_goalrating_ctf_ourstolenflag(this, 20000);
2034                 havocbot_goalrating_ctf_droppedflags(this, 20000, org, havocbot_middlepoint_radius);
2035                 havocbot_goalrating_enemyplayers(this, 15000, org, havocbot_middlepoint_radius);
2036                 havocbot_goalrating_items(this, 10000, org, havocbot_middlepoint_radius);
2037                 havocbot_goalrating_items(this, 5000, this.origin, 10000);
2038
2039                 navigation_goalrating_end(this);
2040
2041                 navigation_goalrating_timeout_set(this);
2042         }
2043 }
2044
2045 void havocbot_role_ctf_setrole(entity bot, int role)
2046 {
2047         string s = "(null)";
2048         switch(role)
2049         {
2050                 case HAVOCBOT_CTF_ROLE_CARRIER:
2051                         s = "carrier";
2052                         bot.havocbot_role = havocbot_role_ctf_carrier;
2053                         bot.havocbot_role_timeout = 0;
2054                         bot.havocbot_cantfindflag = time + 10;
2055                         if (bot.havocbot_previous_role != bot.havocbot_role)
2056                                 navigation_goalrating_timeout_force(bot);
2057                         break;
2058                 case HAVOCBOT_CTF_ROLE_DEFENSE:
2059                         s = "defense";
2060                         bot.havocbot_role = havocbot_role_ctf_defense;
2061                         bot.havocbot_role_timeout = 0;
2062                         break;
2063                 case HAVOCBOT_CTF_ROLE_MIDDLE:
2064                         s = "middle";
2065                         bot.havocbot_role = havocbot_role_ctf_middle;
2066                         bot.havocbot_role_timeout = 0;
2067                         break;
2068                 case HAVOCBOT_CTF_ROLE_OFFENSE:
2069                         s = "offense";
2070                         bot.havocbot_role = havocbot_role_ctf_offense;
2071                         bot.havocbot_role_timeout = 0;
2072                         break;
2073                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2074                         s = "retriever";
2075                         bot.havocbot_previous_role = bot.havocbot_role;
2076                         bot.havocbot_role = havocbot_role_ctf_retriever;
2077                         bot.havocbot_role_timeout = time + 10;
2078                         if (bot.havocbot_previous_role != bot.havocbot_role)
2079                                 navigation_goalrating_timeout_expire(bot, 2);
2080                         break;
2081                 case HAVOCBOT_CTF_ROLE_ESCORT:
2082                         s = "escort";
2083                         bot.havocbot_previous_role = bot.havocbot_role;
2084                         bot.havocbot_role = havocbot_role_ctf_escort;
2085                         bot.havocbot_role_timeout = time + 30;
2086                         if (bot.havocbot_previous_role != bot.havocbot_role)
2087                                 navigation_goalrating_timeout_expire(bot, 2);
2088                         break;
2089         }
2090         LOG_TRACE(bot.netname, " switched to ", s);
2091 }
2092
2093
2094 // ==============
2095 // Hook Functions
2096 // ==============
2097
2098 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2099 {
2100         entity player = M_ARGV(0, entity);
2101
2102         int t = 0, t2 = 0, t3 = 0;
2103         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)
2104
2105         // initially clear items so they can be set as necessary later.
2106         STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING         | CTF_RED_FLAG_TAKEN            | CTF_RED_FLAG_LOST
2107                                                    | CTF_BLUE_FLAG_CARRYING             | CTF_BLUE_FLAG_TAKEN           | CTF_BLUE_FLAG_LOST
2108                                                    | CTF_YELLOW_FLAG_CARRYING   | CTF_YELLOW_FLAG_TAKEN         | CTF_YELLOW_FLAG_LOST
2109                                                    | CTF_PINK_FLAG_CARRYING     | CTF_PINK_FLAG_TAKEN           | CTF_PINK_FLAG_LOST
2110                                                    | CTF_NEUTRAL_FLAG_CARRYING  | CTF_NEUTRAL_FLAG_TAKEN        | CTF_NEUTRAL_FLAG_LOST
2111                                                    | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2112
2113         // scan through all the flags and notify the client about them
2114         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2115         {
2116                 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING;              t2 = CTF_RED_FLAG_TAKEN;                t3 = CTF_RED_FLAG_LOST; }
2117                 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING;             t2 = CTF_BLUE_FLAG_TAKEN;               t3 = CTF_BLUE_FLAG_LOST; }
2118                 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING;   t2 = CTF_YELLOW_FLAG_TAKEN;             t3 = CTF_YELLOW_FLAG_LOST; }
2119                 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING;             t2 = CTF_PINK_FLAG_TAKEN;               t3 = CTF_PINK_FLAG_LOST; }
2120                 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; }
2121
2122                 switch(flag.ctf_status)
2123                 {
2124                         case FLAG_PASSING:
2125                         case FLAG_CARRY:
2126                         {
2127                                 if((flag.owner == player) || (flag.pass_sender == player))
2128                                         STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2129                                 else
2130                                         STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2131                                 break;
2132                         }
2133                         case FLAG_DROPPED:
2134                         {
2135                                 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2136                                 break;
2137                         }
2138                 }
2139         }
2140
2141         // item for stopping players from capturing the flag too often
2142         if(player.ctf_captureshielded)
2143                 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2144
2145         if(ctf_stalemate)
2146                 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2147
2148         // update the health of the flag carrier waypointsprite
2149         if(player.wps_flagcarrier)
2150                 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2151 }
2152
2153 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2154 {
2155         entity frag_attacker = M_ARGV(1, entity);
2156         entity frag_target = M_ARGV(2, entity);
2157         float frag_damage = M_ARGV(4, float);
2158         vector frag_force = M_ARGV(6, vector);
2159
2160         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2161         {
2162                 if(frag_target == frag_attacker) // damage done to yourself
2163                 {
2164                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2165                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2166                 }
2167                 else // damage done to everyone else
2168                 {
2169                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2170                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2171                 }
2172
2173                 M_ARGV(4, float) = frag_damage;
2174                 M_ARGV(6, vector) = frag_force;
2175         }
2176         else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2177         {
2178                 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)))
2179                 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2180                 {
2181                         frag_target.wps_helpme_time = time;
2182                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2183                 }
2184                 // todo: add notification for when flag carrier needs help?
2185         }
2186 }
2187
2188 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2189 {
2190         entity frag_attacker = M_ARGV(1, entity);
2191         entity frag_target = M_ARGV(2, entity);
2192
2193         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2194         {
2195                 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2196                 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2197         }
2198
2199         if(frag_target.flagcarried)
2200         {
2201                 entity tmp_entity = frag_target.flagcarried;
2202                 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2203                 tmp_entity.ctf_dropper = NULL;
2204         }
2205 }
2206
2207 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2208 {
2209         M_ARGV(2, float) = 0; // frag score
2210         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2211 }
2212
2213 void ctf_RemovePlayer(entity player)
2214 {
2215         if(player.flagcarried)
2216                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2217
2218         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2219         {
2220                 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2221                 if(flag.pass_target == player) { flag.pass_target = NULL; }
2222                 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2223         }
2224 }
2225
2226 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2227 {
2228         entity player = M_ARGV(0, entity);
2229
2230         ctf_RemovePlayer(player);
2231 }
2232
2233 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2234 {
2235         entity player = M_ARGV(0, entity);
2236
2237         ctf_RemovePlayer(player);
2238 }
2239
2240 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2241 {
2242         if(!autocvar_g_ctf_leaderboard)
2243                 return;
2244
2245         entity player = M_ARGV(0, entity);
2246
2247         if(IS_REAL_CLIENT(player))
2248         {
2249                 for(int i = 1; i <= RANKINGS_CNT; ++i)
2250                 {
2251                         race_SendRankings(i, 0, 0, MSG_ONE);
2252                 }
2253         }
2254 }
2255
2256 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2257 {
2258         if(!autocvar_g_ctf_leaderboard)
2259                 return;
2260
2261         entity player = M_ARGV(0, entity);
2262
2263         if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2264         {
2265                 if (!player.stored_netname)
2266                         player.stored_netname = strzone(uid2name(player.crypto_idfp));
2267                 if(player.stored_netname != player.netname)
2268                 {
2269                         db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2270                         strcpy(player.stored_netname, player.netname);
2271                 }
2272         }
2273 }
2274
2275 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2276 {
2277         entity player = M_ARGV(0, entity);
2278
2279         if(player.flagcarried)
2280         if(!autocvar_g_ctf_portalteleport)
2281                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2282 }
2283
2284 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2285 {
2286         if(MUTATOR_RETURNVALUE || game_stopped) return;
2287
2288         entity player = M_ARGV(0, entity);
2289
2290         if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2291         {
2292                 // pass the flag to a team mate
2293                 if(autocvar_g_ctf_pass)
2294                 {
2295                         entity head, closest_target = NULL;
2296                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2297
2298                         while(head) // find the closest acceptable target to pass to
2299                         {
2300                                 if(IS_PLAYER(head) && !IS_DEAD(head))
2301                                 if(head != player && SAME_TEAM(head, player))
2302                                 if(!head.speedrunning && !head.vehicle)
2303                                 {
2304                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2305                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2306                                         vector passer_center = CENTER_OR_VIEWOFS(player);
2307
2308                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2309                                         {
2310                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2311                                                 {
2312                                                         if(IS_BOT_CLIENT(head))
2313                                                         {
2314                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2315                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
2316                                                         }
2317                                                         else
2318                                                         {
2319                                                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2320                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2321                                                         }
2322                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2323                                                         return true;
2324                                                 }
2325                                                 else if(player.flagcarried && !head.flagcarried)
2326                                                 {
2327                                                         if(closest_target)
2328                                                         {
2329                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2330                                                                 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2331                                                                         { closest_target = head; }
2332                                                         }
2333                                                         else { closest_target = head; }
2334                                                 }
2335                                         }
2336                                 }
2337                                 head = head.chain;
2338                         }
2339
2340                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2341                 }
2342
2343                 // throw the flag in front of you
2344                 if(autocvar_g_ctf_throw && player.flagcarried)
2345                 {
2346                         if(player.throw_count == -1)
2347                         {
2348                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2349                                 {
2350                                         player.throw_prevtime = time;
2351                                         player.throw_count = 1;
2352                                         ctf_Handle_Throw(player, NULL, DROP_THROW);
2353                                         return true;
2354                                 }
2355                                 else
2356                                 {
2357                                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2358                                         return false;
2359                                 }
2360                         }
2361                         else
2362                         {
2363                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2364                                 else { player.throw_count += 1; }
2365                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2366
2367                                 player.throw_prevtime = time;
2368                                 ctf_Handle_Throw(player, NULL, DROP_THROW);
2369                                 return true;
2370                         }
2371                 }
2372         }
2373 }
2374
2375 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2376 {
2377         entity player = M_ARGV(0, entity);
2378
2379         if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2380         {
2381                 player.wps_helpme_time = time;
2382                 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2383         }
2384         else // create a normal help me waypointsprite
2385         {
2386                 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2387                 WaypointSprite_Ping(player.wps_helpme);
2388         }
2389
2390         return true;
2391 }
2392
2393 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2394 {
2395         entity player = M_ARGV(0, entity);
2396         entity veh = M_ARGV(1, entity);
2397
2398         if(player.flagcarried)
2399         {
2400                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2401                 {
2402                         ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2403                 }
2404                 else
2405                 {
2406                         player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2407                         setattachment(player.flagcarried, veh, "");
2408                         setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2409                         player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2410                         //player.flagcarried.angles = '0 0 0';
2411                 }
2412                 return true;
2413         }
2414 }
2415
2416 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2417 {
2418         entity player = M_ARGV(0, entity);
2419
2420         if(player.flagcarried)
2421         {
2422                 setattachment(player.flagcarried, player, "");
2423                 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2424                 player.flagcarried.scale = FLAG_SCALE;
2425                 player.flagcarried.angles = '0 0 0';
2426                 player.flagcarried.nodrawtoclient = NULL;
2427                 return true;
2428         }
2429 }
2430
2431 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2432 {
2433         entity player = M_ARGV(0, entity);
2434
2435         if(player.flagcarried)
2436         {
2437                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2438                 ctf_RespawnFlag(player.flagcarried);
2439                 return true;
2440         }
2441 }
2442
2443 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2444 {
2445         entity flag; // temporary entity for the search method
2446
2447         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2448         {
2449                 switch(flag.ctf_status)
2450                 {
2451                         case FLAG_DROPPED:
2452                         case FLAG_PASSING:
2453                         {
2454                                 // lock the flag, game is over
2455                                 set_movetype(flag, MOVETYPE_NONE);
2456                                 flag.takedamage = DAMAGE_NO;
2457                                 flag.solid = SOLID_NOT;
2458                                 flag.nextthink = false; // stop thinking
2459
2460                                 //dprint("stopping the ", flag.netname, " from moving.\n");
2461                                 break;
2462                         }
2463
2464                         default:
2465                         case FLAG_BASE:
2466                         case FLAG_CARRY:
2467                         {
2468                                 // do nothing for these flags
2469                                 break;
2470                         }
2471                 }
2472         }
2473 }
2474
2475 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2476 {
2477         entity bot = M_ARGV(0, entity);
2478
2479         havocbot_ctf_reset_role(bot);
2480         return true;
2481 }
2482
2483 MUTATOR_HOOKFUNCTION(ctf, CheckAllowedTeams)
2484 {
2485         //M_ARGV(0, float) = ctf_teams;
2486         M_ARGV(1, string) = "ctf_team";
2487         return true;
2488 }
2489
2490 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2491 {
2492         entity spectatee = M_ARGV(0, entity);
2493         entity client = M_ARGV(1, entity);
2494
2495         STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2496 }
2497
2498 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2499 {
2500         int record_page = M_ARGV(0, int);
2501         string ret_string = M_ARGV(1, string);
2502
2503         for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2504         {
2505                 if (MapInfo_Get_ByID(i))
2506                 {
2507                         float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2508
2509                         if(!r)
2510                                 continue;
2511
2512                         // TODO: uid2name
2513                         string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2514                         ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2515                 }
2516         }
2517
2518         M_ARGV(1, string) = ret_string;
2519 }
2520
2521 bool superspec_Spectate(entity this, entity targ); // TODO
2522 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2523 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2524 {
2525         entity player = M_ARGV(0, entity);
2526         string cmd_name = M_ARGV(1, string);
2527         int cmd_argc = M_ARGV(2, int);
2528
2529         if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2530
2531         if(cmd_name == "followfc")
2532         {
2533                 if(!g_ctf)
2534                         return true;
2535
2536                 int _team = 0;
2537                 bool found = false;
2538
2539                 if(cmd_argc == 2)
2540                 {
2541                         switch(argv(1))
2542                         {
2543                                 case "red":    if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2544                                 case "blue":   if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2545                                 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2546                                 case "pink":   if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2547                         }
2548                 }
2549
2550                 FOREACH_CLIENT(IS_PLAYER(it), {
2551                         if(it.flagcarried && (it.team == _team || _team == 0))
2552                         {
2553                                 found = true;
2554                                 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2555                                         continue; // already spectating this fc, try another
2556                                 return superspec_Spectate(player, it);
2557                         }
2558                 });
2559
2560                 if(!found)
2561                         superspec_msg("", "", player, "No active flag carrier\n", 1);
2562                 return true;
2563         }
2564 }
2565
2566 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2567 {
2568         entity frag_target = M_ARGV(0, entity);
2569
2570         if(frag_target.flagcarried)
2571                 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2572 }
2573
2574
2575 // ==========
2576 // Spawnfuncs
2577 // ==========
2578
2579 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2580 CTF flag for team one (Red).
2581 Keys:
2582 "angle" Angle the flag will point (minus 90 degrees)...
2583 "model" model to use, note this needs red and blue as skins 0 and 1...
2584 "noise" sound played when flag is picked up...
2585 "noise1" sound played when flag is returned by a teammate...
2586 "noise2" sound played when flag is captured...
2587 "noise3" sound played when flag is lost in the field and respawns itself...
2588 "noise4" sound played when flag is dropped by a player...
2589 "noise5" sound played when flag touches the ground... */
2590 spawnfunc(item_flag_team1)
2591 {
2592         if(!g_ctf) { delete(this); return; }
2593
2594         ctf_FlagSetup(NUM_TEAM_1, this);
2595 }
2596
2597 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2598 CTF flag for team two (Blue).
2599 Keys:
2600 "angle" Angle the flag will point (minus 90 degrees)...
2601 "model" model to use, note this needs red and blue as skins 0 and 1...
2602 "noise" sound played when flag is picked up...
2603 "noise1" sound played when flag is returned by a teammate...
2604 "noise2" sound played when flag is captured...
2605 "noise3" sound played when flag is lost in the field and respawns itself...
2606 "noise4" sound played when flag is dropped by a player...
2607 "noise5" sound played when flag touches the ground... */
2608 spawnfunc(item_flag_team2)
2609 {
2610         if(!g_ctf) { delete(this); return; }
2611
2612         ctf_FlagSetup(NUM_TEAM_2, this);
2613 }
2614
2615 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2616 CTF flag for team three (Yellow).
2617 Keys:
2618 "angle" Angle the flag will point (minus 90 degrees)...
2619 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2620 "noise" sound played when flag is picked up...
2621 "noise1" sound played when flag is returned by a teammate...
2622 "noise2" sound played when flag is captured...
2623 "noise3" sound played when flag is lost in the field and respawns itself...
2624 "noise4" sound played when flag is dropped by a player...
2625 "noise5" sound played when flag touches the ground... */
2626 spawnfunc(item_flag_team3)
2627 {
2628         if(!g_ctf) { delete(this); return; }
2629
2630         ctf_FlagSetup(NUM_TEAM_3, this);
2631 }
2632
2633 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2634 CTF flag for team four (Pink).
2635 Keys:
2636 "angle" Angle the flag will point (minus 90 degrees)...
2637 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2638 "noise" sound played when flag is picked up...
2639 "noise1" sound played when flag is returned by a teammate...
2640 "noise2" sound played when flag is captured...
2641 "noise3" sound played when flag is lost in the field and respawns itself...
2642 "noise4" sound played when flag is dropped by a player...
2643 "noise5" sound played when flag touches the ground... */
2644 spawnfunc(item_flag_team4)
2645 {
2646         if(!g_ctf) { delete(this); return; }
2647
2648         ctf_FlagSetup(NUM_TEAM_4, this);
2649 }
2650
2651 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2652 CTF flag (Neutral).
2653 Keys:
2654 "angle" Angle the flag will point (minus 90 degrees)...
2655 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2656 "noise" sound played when flag is picked up...
2657 "noise1" sound played when flag is returned by a teammate...
2658 "noise2" sound played when flag is captured...
2659 "noise3" sound played when flag is lost in the field and respawns itself...
2660 "noise4" sound played when flag is dropped by a player...
2661 "noise5" sound played when flag touches the ground... */
2662 spawnfunc(item_flag_neutral)
2663 {
2664         if(!g_ctf) { delete(this); return; }
2665         if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2666
2667         ctf_FlagSetup(0, this);
2668 }
2669
2670 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2671 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2672 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.
2673 Keys:
2674 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2675 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2676 spawnfunc(ctf_team)
2677 {
2678         if(!g_ctf) { delete(this); return; }
2679
2680         this.classname = "ctf_team";
2681         this.team = this.cnt + 1;
2682 }
2683
2684 // compatibility for quake maps
2685 spawnfunc(team_CTF_redflag)    { spawnfunc_item_flag_team1(this);    }
2686 spawnfunc(team_CTF_blueflag)   { spawnfunc_item_flag_team2(this);    }
2687 spawnfunc(info_player_team1);
2688 spawnfunc(team_CTF_redplayer)  { spawnfunc_info_player_team1(this);  }
2689 spawnfunc(team_CTF_redspawn)   { spawnfunc_info_player_team1(this);  }
2690 spawnfunc(info_player_team2);
2691 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this);  }
2692 spawnfunc(team_CTF_bluespawn)  { spawnfunc_info_player_team2(this);  }
2693
2694 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this);  }
2695 spawnfunc(team_neutralobelisk)  { spawnfunc_item_flag_neutral(this);  }
2696
2697 // compatibility for wop maps
2698 spawnfunc(team_redplayer)      { spawnfunc_info_player_team1(this);  }
2699 spawnfunc(team_blueplayer)     { spawnfunc_info_player_team2(this);  }
2700 spawnfunc(team_ctl_redlolly)   { spawnfunc_item_flag_team1(this);    }
2701 spawnfunc(team_CTL_redlolly)   { spawnfunc_item_flag_team1(this);    }
2702 spawnfunc(team_ctl_bluelolly)  { spawnfunc_item_flag_team2(this);    }
2703 spawnfunc(team_CTL_bluelolly)  { spawnfunc_item_flag_team2(this);    }
2704
2705
2706 // ==============
2707 // Initialization
2708 // ==============
2709
2710 // scoreboard setup
2711 void ctf_ScoreRules(int teams)
2712 {
2713         CheckAllowedTeams(NULL);
2714         GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2715         field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2716         field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2717         field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2718         field(SP_CTF_PICKUPS, "pickups", 0);
2719         field(SP_CTF_FCKILLS, "fckills", 0);
2720         field(SP_CTF_RETURNS, "returns", 0);
2721         field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2722         });
2723 }
2724
2725 // code from here on is just to support maps that don't have flag and team entities
2726 void ctf_SpawnTeam (string teamname, int teamcolor)
2727 {
2728         entity this = new_pure(ctf_team);
2729         this.netname = teamname;
2730         this.cnt = teamcolor - 1;
2731         this.spawnfunc_checked = true;
2732         this.team = teamcolor;
2733 }
2734
2735 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2736 {
2737         ctf_teams = 0;
2738
2739         entity tmp_entity;
2740         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2741         {
2742                 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2743                 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2744
2745                 switch(tmp_entity.team)
2746                 {
2747                         case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2748                         case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2749                         case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2750                         case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2751                 }
2752                 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2753         }
2754
2755         havocbot_ctf_calculate_middlepoint();
2756
2757         if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2758         {
2759                 ctf_teams = 0; // so set the default red and blue teams
2760                 BITSET_ASSIGN(ctf_teams, BIT(0));
2761                 BITSET_ASSIGN(ctf_teams, BIT(1));
2762         }
2763
2764         //ctf_teams = bound(2, ctf_teams, 4);
2765
2766         // if no teams are found, spawn defaults
2767         if(find(NULL, classname, "ctf_team") == NULL)
2768         {
2769                 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2770                 if(ctf_teams & BIT(0))
2771                         ctf_SpawnTeam("Red", NUM_TEAM_1);
2772                 if(ctf_teams & BIT(1))
2773                         ctf_SpawnTeam("Blue", NUM_TEAM_2);
2774                 if(ctf_teams & BIT(2))
2775                         ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2776                 if(ctf_teams & BIT(3))
2777                         ctf_SpawnTeam("Pink", NUM_TEAM_4);
2778         }
2779
2780         ctf_ScoreRules(ctf_teams);
2781 }
2782
2783 void ctf_Initialize()
2784 {
2785         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2786
2787         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2788         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2789         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2790
2791         InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);
2792 }