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