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