]> git.rm.cloudns.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'master' into Lyberta/GamemodesSplit
authorLyberta <lyberta@lyberta.net>
Tue, 21 Aug 2018 07:35:05 +0000 (10:35 +0300)
committerLyberta <lyberta@lyberta.net>
Tue, 21 Aug 2018 07:35:05 +0000 (10:35 +0300)
1  2 
qcsrc/common/gamemodes/gamemode/ctf/sv_ctf.qc
qcsrc/common/gamemodes/gamemode/domination/sv_domination.qc
qcsrc/common/gamemodes/gamemode/freezetag/sv_freezetag.qc
qcsrc/common/gamemodes/gamemode/keepaway/sv_keepaway.qc
qcsrc/common/gamemodes/gamemode/keepaway/sv_keepaway.qh

index d77b7208759c699beb5f5c1fe820798a05b4048f,0000000000000000000000000000000000000000..7f0843e8ef745e6e3367c6590e487098c0c8dffc
mode 100644,000000..100644
--- /dev/null
@@@ -1,2773 -1,0 +1,2773 @@@
-       flag.target = "###item###"; // wut?
 +#include "sv_ctf.qh"
 +
 +#include <common/effects/all.qh>
 +#include <common/vehicles/all.qh>
 +#include <server/teamplay.qh>
 +
 +#include <lib/warpzone/common.qh>
 +
 +bool autocvar_g_ctf_allow_vehicle_carry;
 +bool autocvar_g_ctf_allow_vehicle_touch;
 +bool autocvar_g_ctf_allow_monster_touch;
 +bool autocvar_g_ctf_throw;
 +float autocvar_g_ctf_throw_angle_max;
 +float autocvar_g_ctf_throw_angle_min;
 +int autocvar_g_ctf_throw_punish_count;
 +float autocvar_g_ctf_throw_punish_delay;
 +float autocvar_g_ctf_throw_punish_time;
 +float autocvar_g_ctf_throw_strengthmultiplier;
 +float autocvar_g_ctf_throw_velocity_forward;
 +float autocvar_g_ctf_throw_velocity_up;
 +float autocvar_g_ctf_drop_velocity_up;
 +float autocvar_g_ctf_drop_velocity_side;
 +bool autocvar_g_ctf_oneflag_reverse;
 +bool autocvar_g_ctf_portalteleport;
 +bool autocvar_g_ctf_pass;
 +float autocvar_g_ctf_pass_arc;
 +float autocvar_g_ctf_pass_arc_max;
 +float autocvar_g_ctf_pass_directional_max;
 +float autocvar_g_ctf_pass_directional_min;
 +float autocvar_g_ctf_pass_radius;
 +float autocvar_g_ctf_pass_wait;
 +bool autocvar_g_ctf_pass_request;
 +float autocvar_g_ctf_pass_turnrate;
 +float autocvar_g_ctf_pass_timelimit;
 +float autocvar_g_ctf_pass_velocity;
 +bool autocvar_g_ctf_dynamiclights;
 +float autocvar_g_ctf_flag_collect_delay;
 +float autocvar_g_ctf_flag_damageforcescale;
 +bool autocvar_g_ctf_flag_dropped_waypoint;
 +bool autocvar_g_ctf_flag_dropped_floatinwater;
 +bool autocvar_g_ctf_flag_glowtrails;
 +int autocvar_g_ctf_flag_health;
 +bool autocvar_g_ctf_flag_return;
 +bool autocvar_g_ctf_flag_return_carrying;
 +float autocvar_g_ctf_flag_return_carried_radius;
 +float autocvar_g_ctf_flag_return_time;
 +bool autocvar_g_ctf_flag_return_when_unreachable;
 +float autocvar_g_ctf_flag_return_damage;
 +float autocvar_g_ctf_flag_return_damage_delay;
 +float autocvar_g_ctf_flag_return_dropped;
 +float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
 +float autocvar_g_ctf_flagcarrier_auto_helpme_time;
 +float autocvar_g_ctf_flagcarrier_selfdamagefactor;
 +float autocvar_g_ctf_flagcarrier_selfforcefactor;
 +float autocvar_g_ctf_flagcarrier_damagefactor;
 +float autocvar_g_ctf_flagcarrier_forcefactor;
 +//float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
 +bool autocvar_g_ctf_fullbrightflags;
 +bool autocvar_g_ctf_ignore_frags;
 +bool autocvar_g_ctf_score_ignore_fields;
 +int autocvar_g_ctf_score_capture;
 +int autocvar_g_ctf_score_capture_assist;
 +int autocvar_g_ctf_score_kill;
 +int autocvar_g_ctf_score_penalty_drop;
 +int autocvar_g_ctf_score_penalty_returned;
 +int autocvar_g_ctf_score_pickup_base;
 +int autocvar_g_ctf_score_pickup_dropped_early;
 +int autocvar_g_ctf_score_pickup_dropped_late;
 +int autocvar_g_ctf_score_return;
 +float autocvar_g_ctf_shield_force;
 +float autocvar_g_ctf_shield_max_ratio;
 +int autocvar_g_ctf_shield_min_negscore;
 +bool autocvar_g_ctf_stalemate;
 +int autocvar_g_ctf_stalemate_endcondition;
 +float autocvar_g_ctf_stalemate_time;
 +bool autocvar_g_ctf_reverse;
 +float autocvar_g_ctf_dropped_capture_delay;
 +float autocvar_g_ctf_dropped_capture_radius;
 +
 +void ctf_FakeTimeLimit(entity e, float t)
 +{
 +      msg_entity = e;
 +      WriteByte(MSG_ONE, 3); // svc_updatestat
 +      WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
 +      if(t < 0)
 +              WriteCoord(MSG_ONE, autocvar_timelimit);
 +      else
 +              WriteCoord(MSG_ONE, (t + 1) / 60);
 +}
 +
 +void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
 +{
 +      if(autocvar_sv_eventlog)
 +              GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != NULL) ? ftos(actor.playerid) : "")));
 +              //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
 +}
 +
 +void ctf_CaptureRecord(entity flag, entity player)
 +{
 +      float cap_record = ctf_captimerecord;
 +      float cap_time = (time - flag.ctf_pickuptime);
 +      string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
 +
 +      // notify about shit
 +      if(ctf_oneflag)
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname);
 +      else if(!ctf_captimerecord)
 +              Send_Notification(NOTIF_ALL, NULL, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_CAPTURE_TIME), player.netname, TIME_ENCODE(cap_time));
 +      else if(cap_time < cap_record)
 +              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));
 +      else
 +              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));
 +
 +      // write that shit in the database
 +      if(!ctf_oneflag) // but not in 1-flag mode
 +      if((!ctf_captimerecord) || (cap_time < cap_record))
 +      {
 +              ctf_captimerecord = cap_time;
 +              db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
 +              db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
 +              write_recordmarker(player, flag.ctf_pickuptime, cap_time);
 +      }
 +
 +      if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
 +              race_setTime(GetMapname(), TIME_ENCODE(cap_time), player.crypto_idfp, player.netname, player, false);
 +}
 +
 +bool ctf_Immediate_Return_Allowed(entity flag, entity toucher)
 +{
 +      int num_perteam = 0;
 +      FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), { ++num_perteam; });
 +
 +      // automatically return if there's only 1 player on the team
 +      return ((autocvar_g_ctf_flag_return || num_perteam <= 1 || (autocvar_g_ctf_flag_return_carrying && toucher.flagcarried))
 +              && flag.team);
 +}
 +
 +bool ctf_Return_Customize(entity this, entity client)
 +{
 +      // only to the carrier
 +      return boolean(client == this.owner);
 +}
 +
 +void ctf_FlagcarrierWaypoints(entity player)
 +{
 +      WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
 +      WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
 +      WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(GetResourceAmount(player, RESOURCE_HEALTH), GetResourceAmount(player, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
 +      WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
 +
 +      if(player.flagcarried && CTF_SAMETEAM(player, player.flagcarried))
 +      {
 +              if(!player.wps_enemyflagcarrier)
 +              {
 +                      entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, player, FLAG_WAYPOINT_OFFSET, NULL, 0, player, wps_enemyflagcarrier, true, RADARICON_FLAG);
 +                      wp.colormod = WPCOLOR_ENEMYFC(player.team);
 +                      setcefc(wp, ctf_Stalemate_Customize);
 +
 +                      if(IS_REAL_CLIENT(player) && !ctf_stalemate)
 +                              Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_VISIBLE);
 +              }
 +
 +              if(!player.wps_flagreturn)
 +              {
 +                      entity owp = WaypointSprite_SpawnFixed(WP_FlagReturn, player.flagcarried.ctf_spawnorigin + FLAG_WAYPOINT_OFFSET, player, wps_flagreturn, RADARICON_FLAG);
 +                      owp.colormod = '0 0.8 0.8';
 +                      //WaypointSprite_UpdateTeamRadar(player.wps_flagreturn, RADARICON_FLAG, ((player.team) ? colormapPaletteColor(player.team - 1, false) : '1 1 1'));
 +                      setcefc(owp, ctf_Return_Customize);
 +              }
 +      }
 +}
 +
 +void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
 +{
 +      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
 +      float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
 +      float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
 +      //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
 +
 +      vector targpos;
 +      if(current_height) // make sure we can actually do this arcing path
 +      {
 +              targpos = (to + ('0 0 1' * current_height));
 +              WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
 +              if(trace_fraction < 1)
 +              {
 +                      //print("normal arc line failed, trying to find new pos...");
 +                      WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
 +                      targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
 +                      WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
 +                      if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
 +                      /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
 +              }
 +      }
 +      else { targpos = to; }
 +
 +      //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
 +
 +      vector desired_direction = normalize(targpos - from);
 +      if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
 +      else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
 +}
 +
 +bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
 +{
 +      if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
 +      {
 +              // directional tracing only
 +              float spreadlimit;
 +              makevectors(passer_angle);
 +
 +              // find the closest point on the enemy to the center of the attack
 +              float h; // hypotenuse, which is the distance between attacker to head
 +              float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
 +
 +              h = vlen(head_center - passer_center);
 +              a = h * (normalize(head_center - passer_center) * v_forward);
 +
 +              vector nearest_on_line = (passer_center + a * v_forward);
 +              float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
 +
 +              spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
 +              spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
 +
 +              if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
 +                      { return true; }
 +              else
 +                      { return false; }
 +      }
 +      else { return true; }
 +}
 +
 +
 +// =======================
 +// CaptureShield Functions
 +// =======================
 +
 +bool ctf_CaptureShield_CheckStatus(entity p)
 +{
 +      int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
 +      int players_worseeq, players_total;
 +
 +      if(ctf_captureshield_max_ratio <= 0)
 +              return false;
 +
 +      s  = GameRules_scoring_add(p, CTF_CAPS, 0);
 +      s2 = GameRules_scoring_add(p, CTF_PICKUPS, 0);
 +      s3 = GameRules_scoring_add(p, CTF_RETURNS, 0);
 +      s4 = GameRules_scoring_add(p, CTF_FCKILLS, 0);
 +
 +      sr = ((s - s2) + (s3 + s4));
 +
 +      if(sr >= -ctf_captureshield_min_negscore)
 +              return false;
 +
 +      players_total = players_worseeq = 0;
 +      FOREACH_CLIENT(IS_PLAYER(it), {
 +              if(DIFF_TEAM(it, p))
 +                      continue;
 +              se  = GameRules_scoring_add(it, CTF_CAPS, 0);
 +              se2 = GameRules_scoring_add(it, CTF_PICKUPS, 0);
 +              se3 = GameRules_scoring_add(it, CTF_RETURNS, 0);
 +              se4 = GameRules_scoring_add(it, CTF_FCKILLS, 0);
 +
 +              ser = ((se - se2) + (se3 + se4));
 +
 +              if(ser <= sr)
 +                      ++players_worseeq;
 +              ++players_total;
 +      });
 +
 +      // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
 +      // use this rule here
 +
 +      if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
 +              return false;
 +
 +      return true;
 +}
 +
 +void ctf_CaptureShield_Update(entity player, bool wanted_status)
 +{
 +      bool updated_status = ctf_CaptureShield_CheckStatus(player);
 +      if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
 +      {
 +              Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
 +              player.ctf_captureshielded = updated_status;
 +      }
 +}
 +
 +bool ctf_CaptureShield_Customize(entity this, entity client)
 +{
 +      if(!client.ctf_captureshielded) { return false; }
 +      if(CTF_SAMETEAM(this, client)) { return false; }
 +
 +      return true;
 +}
 +
 +void ctf_CaptureShield_Touch(entity this, entity toucher)
 +{
 +      if(!toucher.ctf_captureshielded) { return; }
 +      if(CTF_SAMETEAM(this, toucher)) { return; }
 +
 +      vector mymid = (this.absmin + this.absmax) * 0.5;
 +      vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
 +
 +      Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
 +      if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
 +}
 +
 +void ctf_CaptureShield_Spawn(entity flag)
 +{
 +      entity shield = new(ctf_captureshield);
 +
 +      shield.enemy = flag;
 +      shield.team = flag.team;
 +      settouch(shield, ctf_CaptureShield_Touch);
 +      setcefc(shield, ctf_CaptureShield_Customize);
 +      shield.effects = EF_ADDITIVE;
 +      set_movetype(shield, MOVETYPE_NOCLIP);
 +      shield.solid = SOLID_TRIGGER;
 +      shield.avelocity = '7 0 11';
 +      shield.scale = 0.5;
 +
 +      setorigin(shield, flag.origin);
 +      setmodel(shield, MDL_CTF_SHIELD);
 +      setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
 +}
 +
 +
 +// ====================
 +// Drop/Pass/Throw Code
 +// ====================
 +
 +void ctf_Handle_Drop(entity flag, entity player, int droptype)
 +{
 +      // declarations
 +      player = (player ? player : flag.pass_sender);
 +
 +      // main
 +      set_movetype(flag, MOVETYPE_TOSS);
 +      flag.takedamage = DAMAGE_YES;
 +      flag.angles = '0 0 0';
 +      SetResourceAmountExplicit(flag, RESOURCE_HEALTH, flag.max_flag_health);
 +      flag.ctf_droptime = time;
 +      flag.ctf_dropper = player;
 +      flag.ctf_status = FLAG_DROPPED;
 +
 +      // messages and sounds
 +      Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_LOST), player.netname);
 +      _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
 +      ctf_EventLog("dropped", player.team, player);
 +
 +      // scoring
 +      GameRules_scoring_add_team(player, SCORE, -((flag.score_drop) ? flag.score_drop : autocvar_g_ctf_score_penalty_drop));
 +      GameRules_scoring_add(player, CTF_DROPS, 1);
 +
 +      // waypoints
 +      if(autocvar_g_ctf_flag_dropped_waypoint) {
 +              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);
 +              wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
 +      }
 +
 +      if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
 +      {
 +              WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
 +              WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResourceAmount(flag, RESOURCE_HEALTH));
 +      }
 +
 +      player.throw_antispam = time + autocvar_g_ctf_pass_wait;
 +
 +      if(droptype == DROP_PASS)
 +      {
 +              flag.pass_distance = 0;
 +              flag.pass_sender = NULL;
 +              flag.pass_target = NULL;
 +      }
 +}
 +
 +void ctf_Handle_Retrieve(entity flag, entity player)
 +{
 +      entity sender = flag.pass_sender;
 +
 +      // transfer flag to player
 +      flag.owner = player;
 +      flag.owner.flagcarried = flag;
 +      GameRules_scoring_vip(player, true);
 +
 +      // reset flag
 +      if(player.vehicle)
 +      {
 +              setattachment(flag, player.vehicle, "");
 +              setorigin(flag, VEHICLE_FLAG_OFFSET);
 +              flag.scale = VEHICLE_FLAG_SCALE;
 +      }
 +      else
 +      {
 +              setattachment(flag, player, "");
 +              setorigin(flag, FLAG_CARRY_OFFSET);
 +      }
 +      set_movetype(flag, MOVETYPE_NONE);
 +      flag.takedamage = DAMAGE_NO;
 +      flag.solid = SOLID_NOT;
 +      flag.angles = '0 0 0';
 +      flag.ctf_status = FLAG_CARRY;
 +
 +      // messages and sounds
 +      _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
 +      ctf_EventLog("receive", flag.team, player);
 +
 +      FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
 +              if(it == sender)
 +                      Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_SENT), player.netname);
 +              else if(it == player)
 +                      Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_RECEIVED), sender.netname);
 +              else if(SAME_TEAM(it, sender))
 +                      Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_NUM(flag.team, CENTER_CTF_PASS_OTHER), sender.netname, player.netname);
 +      });
 +
 +      // create new waypoint
 +      ctf_FlagcarrierWaypoints(player);
 +
 +      sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
 +      player.throw_antispam = sender.throw_antispam;
 +
 +      flag.pass_distance = 0;
 +      flag.pass_sender = NULL;
 +      flag.pass_target = NULL;
 +}
 +
 +void ctf_Handle_Throw(entity player, entity receiver, int droptype)
 +{
 +      entity flag = player.flagcarried;
 +      vector targ_origin, flag_velocity;
 +
 +      if(!flag) { return; }
 +      if((droptype == DROP_PASS) && !receiver) { return; }
 +
 +      if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
 +
 +      // reset the flag
 +      setattachment(flag, NULL, "");
 +      setorigin(flag, player.origin + FLAG_DROP_OFFSET);
 +      flag.owner.flagcarried = NULL;
 +      GameRules_scoring_vip(flag.owner, false);
 +      flag.owner = NULL;
 +      flag.solid = SOLID_TRIGGER;
 +      flag.ctf_dropper = player;
 +      flag.ctf_droptime = time;
 +      navigation_dynamicgoal_set(flag);
 +
 +      flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
 +
 +      switch(droptype)
 +      {
 +              case DROP_PASS:
 +              {
 +                      // warpzone support:
 +                      // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
 +                      // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
 +                      WarpZone_RefSys_Copy(flag, receiver);
 +                      WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
 +                      targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
 +
 +                      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
 +                      ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
 +
 +                      // main
 +                      set_movetype(flag, MOVETYPE_FLY);
 +                      flag.takedamage = DAMAGE_NO;
 +                      flag.pass_sender = player;
 +                      flag.pass_target = receiver;
 +                      flag.ctf_status = FLAG_PASSING;
 +
 +                      // other
 +                      _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
 +                      WarpZone_TrailParticles(NULL, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
 +                      ctf_EventLog("pass", flag.team, player);
 +                      break;
 +              }
 +
 +              case DROP_THROW:
 +              {
 +                      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'));
 +
 +                      flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & ITEM_Strength.m_itemid) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
 +                      flag.velocity = W_CalculateProjectileVelocity(player, player.velocity, flag_velocity, false);
 +                      ctf_Handle_Drop(flag, player, droptype);
 +                      break;
 +              }
 +
 +              case DROP_RESET:
 +              {
 +                      flag.velocity = '0 0 0'; // do nothing
 +                      break;
 +              }
 +
 +              default:
 +              case DROP_NORMAL:
 +              {
 +                      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);
 +                      ctf_Handle_Drop(flag, player, droptype);
 +                      break;
 +              }
 +      }
 +
 +      // kill old waypointsprite
 +      WaypointSprite_Ping(player.wps_flagcarrier);
 +      WaypointSprite_Kill(player.wps_flagcarrier);
 +
 +      if(player.wps_enemyflagcarrier)
 +              WaypointSprite_Kill(player.wps_enemyflagcarrier);
 +
 +      if(player.wps_flagreturn)
 +              WaypointSprite_Kill(player.wps_flagreturn);
 +
 +      // captureshield
 +      ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
 +}
 +
 +void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
 +{
 +      return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
 +}
 +
 +// ==============
 +// Event Handlers
 +// ==============
 +
 +void nades_GiveBonus(entity player, float score);
 +
 +void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
 +{
 +      entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
 +      entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
 +      entity player_team_flag = NULL, tmp_entity;
 +      float old_time, new_time;
 +
 +      if(!player) { return; } // without someone to give the reward to, we can't possibly cap
 +      if(CTF_DIFFTEAM(player, flag)) { return; }
 +      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)
 +
 +      if (toucher.goalentity == flag.bot_basewaypoint)
 +              toucher.goalentity_lock_timeout = 0;
 +
 +      if(ctf_oneflag)
 +      for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
 +      if(SAME_TEAM(tmp_entity, player))
 +      {
 +              player_team_flag = tmp_entity;
 +              break;
 +      }
 +
 +      nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
 +
 +      player.throw_prevtime = time;
 +      player.throw_count = 0;
 +
 +      // messages and sounds
 +      Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_NUM(enemy_flag.team, CENTER_CTF_CAPTURE));
 +      ctf_CaptureRecord(enemy_flag, player);
 +      _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);
 +
 +      switch(capturetype)
 +      {
 +              case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
 +              case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
 +              default: break;
 +      }
 +
 +      // scoring
 +      float pscore = 0;
 +      if(enemy_flag.score_capture || flag.score_capture)
 +              pscore = floor((max(1, enemy_flag.score_capture) + max(1, flag.score_capture)) * 0.5);
 +      GameRules_scoring_add_team(player, SCORE, ((pscore) ? pscore : autocvar_g_ctf_score_capture));
 +      float capscore = 0;
 +      if(enemy_flag.score_team_capture || flag.score_team_capture)
 +              capscore = floor((max(1, enemy_flag.score_team_capture) + max(1, flag.score_team_capture)) * 0.5);
 +      GameRules_scoring_add_team(player, CTF_CAPS, ((capscore) ? capscore : 1));
 +
 +      old_time = GameRules_scoring_add(player, CTF_CAPTIME, 0);
 +      new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
 +      if(!old_time || new_time < old_time)
 +              GameRules_scoring_add(player, CTF_CAPTIME, new_time - old_time);
 +
 +      // effects
 +      Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
 +      //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
 +
 +      // other
 +      if(capturetype == CAPTURE_NORMAL)
 +      {
 +              WaypointSprite_Kill(player.wps_flagcarrier);
 +              if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
 +
 +              if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
 +                      { GameRules_scoring_add_team(enemy_flag.ctf_dropper, SCORE, ((enemy_flag.score_assist) ? enemy_flag.score_assist : autocvar_g_ctf_score_capture_assist)); }
 +      }
 +
 +      flag.enemy = toucher;
 +
 +      // reset the flag
 +      player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
 +      ctf_RespawnFlag(enemy_flag);
 +}
 +
 +void ctf_Handle_Return(entity flag, entity player)
 +{
 +      // messages and sounds
 +      if(IS_MONSTER(player))
 +      {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN_MONSTER), player.monster_name);
 +      }
 +      else if(flag.team)
 +      {
 +              Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_RETURN));
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(flag.team, INFO_CTF_RETURN), player.netname);
 +      }
 +      _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
 +      ctf_EventLog("return", flag.team, player);
 +
 +      // scoring
 +      if(IS_PLAYER(player))
 +      {
 +              GameRules_scoring_add_team(player, SCORE, ((flag.score_return) ? flag.score_return : autocvar_g_ctf_score_return)); // reward for return
 +              GameRules_scoring_add(player, CTF_RETURNS, 1); // add to count of returns
 +
 +              nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
 +      }
 +
 +      TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
 +
 +      if(flag.ctf_dropper)
 +      {
 +              GameRules_scoring_add(flag.ctf_dropper, SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
 +              ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
 +              flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
 +      }
 +
 +      // other
 +      if(player.flagcarried == flag)
 +              WaypointSprite_Kill(player.wps_flagcarrier);
 +
 +      flag.enemy = player;
 +
 +      // reset the flag
 +      ctf_RespawnFlag(flag);
 +}
 +
 +void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
 +{
 +      // declarations
 +      float pickup_dropped_score; // used to calculate dropped pickup score
 +
 +      // attach the flag to the player
 +      flag.owner = player;
 +      player.flagcarried = flag;
 +      GameRules_scoring_vip(player, true);
 +      if(player.vehicle)
 +      {
 +              setattachment(flag, player.vehicle, "");
 +              setorigin(flag, VEHICLE_FLAG_OFFSET);
 +              flag.scale = VEHICLE_FLAG_SCALE;
 +      }
 +      else
 +      {
 +              setattachment(flag, player, "");
 +              setorigin(flag, FLAG_CARRY_OFFSET);
 +      }
 +
 +      // flag setup
 +      set_movetype(flag, MOVETYPE_NONE);
 +      flag.takedamage = DAMAGE_NO;
 +      flag.solid = SOLID_NOT;
 +      flag.angles = '0 0 0';
 +      flag.ctf_status = FLAG_CARRY;
 +
 +      switch(pickuptype)
 +      {
 +              case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
 +              case PICKUP_DROPPED: SetResourceAmountExplicit(flag, RESOURCE_HEALTH, flag.max_flag_health); break; // reset health/return timelimit
 +              default: break;
 +      }
 +
 +      // messages and sounds
 +      Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_PICKUP), player.netname);
 +      if(ctf_stalemate)
 +              Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER);
 +      if(!flag.team)
 +              Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL);
 +      else if(CTF_DIFFTEAM(player, flag))
 +              Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(flag.team, CENTER_CTF_PICKUP));
 +      else
 +              Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_RETURN : CENTER_CTF_PICKUP_RETURN_ENEMY), Team_ColorCode(flag.team));
 +
 +      Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, APP_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
 +
 +      if(!flag.team)
 +              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); });
 +
 +      if(flag.team)
 +              FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
 +                      if(CTF_SAMETEAM(flag, it))
 +                      if(SAME_TEAM(player, it))
 +                              Send_Notification(NOTIF_ONE, it, MSG_CHOICE, APP_TEAM_NUM(flag.team, CHOICE_CTF_PICKUP_TEAM), Team_ColorCode(player.team), player.netname);
 +                      else
 +                              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);
 +              });
 +
 +      _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
 +
 +      // scoring
 +      GameRules_scoring_add(player, CTF_PICKUPS, 1);
 +      nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
 +      switch(pickuptype)
 +      {
 +              case PICKUP_BASE:
 +              {
 +                      GameRules_scoring_add_team(player, SCORE, ((flag.score_pickup) ? flag.score_pickup : autocvar_g_ctf_score_pickup_base));
 +                      ctf_EventLog("steal", flag.team, player);
 +                      break;
 +              }
 +
 +              case PICKUP_DROPPED:
 +              {
 +                      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);
 +                      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);
 +                      LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score));
 +                      GameRules_scoring_add_team(player, SCORE, pickup_dropped_score);
 +                      ctf_EventLog("pickup", flag.team, player);
 +                      break;
 +              }
 +
 +              default: break;
 +      }
 +
 +      // speedrunning
 +      if(pickuptype == PICKUP_BASE)
 +      {
 +              flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
 +              if((player.speedrunning) && (ctf_captimerecord))
 +                      ctf_FakeTimeLimit(player, time + ctf_captimerecord);
 +      }
 +
 +      // effects
 +      Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
 +
 +      // waypoints
 +      if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
 +      ctf_FlagcarrierWaypoints(player);
 +      WaypointSprite_Ping(player.wps_flagcarrier);
 +}
 +
 +
 +// ===================
 +// Main Flag Functions
 +// ===================
 +
 +void ctf_CheckFlagReturn(entity flag, int returntype)
 +{
 +      if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
 +      {
 +              if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, GetResourceAmount(flag, RESOURCE_HEALTH)); }
 +
 +              if((GetResourceAmount(flag, RESOURCE_HEALTH) <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
 +              {
 +                      switch(returntype)
 +                      {
 +                              case RETURN_DROPPED:
 +                                      Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DROPPED)); break;
 +                              case RETURN_DAMAGE:
 +                                      Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_DAMAGED)); break;
 +                              case RETURN_SPEEDRUN:
 +                                      Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_SPEEDRUN), TIME_ENCODE(ctf_captimerecord)); break;
 +                              case RETURN_NEEDKILL:
 +                                      Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_NEEDKILL)); break;
 +                              default:
 +                              case RETURN_TIMEOUT:
 +                                      Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(flag.team, INFO_CTF_FLAGRETURN_TIMEOUT)); break;
 +                      }
 +                      _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
 +                      ctf_EventLog("returned", flag.team, NULL);
 +                      flag.enemy = NULL;
 +                      ctf_RespawnFlag(flag);
 +              }
 +      }
 +}
 +
 +bool ctf_Stalemate_Customize(entity this, entity client)
 +{
 +      // make spectators see what the player would see
 +      entity e = WaypointSprite_getviewentity(client);
 +      entity wp_owner = this.owner;
 +
 +      // team waypoints
 +      //if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
 +      if(SAME_TEAM(wp_owner, e)) { return false; }
 +      if(!IS_PLAYER(e)) { return false; }
 +
 +      return true;
 +}
 +
 +void ctf_CheckStalemate()
 +{
 +      // declarations
 +      int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
 +      entity tmp_entity;
 +
 +      entity ctf_staleflaglist = NULL; // reset the list, we need to build the list each time this function runs
 +
 +      // build list of stale flags
 +      for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
 +      {
 +              if(autocvar_g_ctf_stalemate)
 +              if(tmp_entity.ctf_status != FLAG_BASE)
 +              if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
 +              {
 +                      tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
 +                      ctf_staleflaglist = tmp_entity;
 +
 +                      switch(tmp_entity.team)
 +                      {
 +                              case NUM_TEAM_1: ++stale_red_flags; break;
 +                              case NUM_TEAM_2: ++stale_blue_flags; break;
 +                              case NUM_TEAM_3: ++stale_yellow_flags; break;
 +                              case NUM_TEAM_4: ++stale_pink_flags; break;
 +                              default: ++stale_neutral_flags; break;
 +                      }
 +              }
 +      }
 +
 +      if(ctf_oneflag)
 +              stale_flags = (stale_neutral_flags >= 1);
 +      else
 +              stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
 +
 +      if(ctf_oneflag && stale_flags == 1)
 +              ctf_stalemate = true;
 +      else if(stale_flags >= 2)
 +              ctf_stalemate = true;
 +      else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
 +              { ctf_stalemate = false; wpforenemy_announced = false; }
 +      else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
 +              { ctf_stalemate = false; wpforenemy_announced = false; }
 +
 +      // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
 +      if(ctf_stalemate)
 +      {
 +              for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
 +              {
 +                      if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
 +                      {
 +                              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);
 +                              wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
 +                              setcefc(tmp_entity.owner.wps_enemyflagcarrier, ctf_Stalemate_Customize);
 +                      }
 +              }
 +
 +              if (!wpforenemy_announced)
 +              {
 +                      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)); });
 +
 +                      wpforenemy_announced = true;
 +              }
 +      }
 +}
 +
 +void ctf_FlagDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
 +{
 +      if(ITEM_DAMAGE_NEEDKILL(deathtype))
 +      {
 +              if(autocvar_g_ctf_flag_return_damage_delay)
 +                      this.ctf_flagdamaged_byworld = true;
 +              else
 +              {
 +                      SetResourceAmountExplicit(this, RESOURCE_HEALTH, 0);
 +                      ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
 +              }
 +              return;
 +      }
 +      if(autocvar_g_ctf_flag_return_damage)
 +      {
 +              // reduce health and check if it should be returned
 +              TakeResource(this, RESOURCE_HEALTH, damage);
 +              ctf_CheckFlagReturn(this, RETURN_DAMAGE);
 +              return;
 +      }
 +}
 +
 +void ctf_FlagThink(entity this)
 +{
 +      // declarations
 +      entity tmp_entity;
 +
 +      this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
 +
 +      // captureshield
 +      if(this == ctf_worldflaglist) // only for the first flag
 +              FOREACH_CLIENT(true, { ctf_CaptureShield_Update(it, 1); }); // release shield only
 +
 +      // sanity checks
 +      if(this.mins != this.m_mins || this.maxs != this.m_maxs) { // reset the flag boundaries in case it got squished
 +              LOG_TRACE("wtf the flag got squashed?");
 +              tracebox(this.origin, this.m_mins, this.m_maxs, this.origin, MOVE_NOMONSTERS, this);
 +              if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
 +                      setsize(this, this.m_mins, this.m_maxs);
 +      }
 +
 +      // main think method
 +      switch(this.ctf_status)
 +      {
 +              case FLAG_BASE:
 +              {
 +                      if(autocvar_g_ctf_dropped_capture_radius)
 +                      {
 +                              for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
 +                                      if(tmp_entity.ctf_status == FLAG_DROPPED)
 +                                      if(vdist(this.origin - tmp_entity.origin, <, autocvar_g_ctf_dropped_capture_radius))
 +                                      if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
 +                                              ctf_Handle_Capture(this, tmp_entity, CAPTURE_DROPPED);
 +                      }
 +                      return;
 +              }
 +
 +              case FLAG_DROPPED:
 +              {
 +                      this.angles = '0 0 0'; // reset flag angles in case warpzones adjust it
 +
 +                      if(autocvar_g_ctf_flag_dropped_floatinwater)
 +                      {
 +                              vector midpoint = ((this.absmin + this.absmax) * 0.5);
 +                              if(pointcontents(midpoint) == CONTENT_WATER)
 +                              {
 +                                      this.velocity = this.velocity * 0.5;
 +
 +                                      if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
 +                                              { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
 +                                      else
 +                                              { set_movetype(this, MOVETYPE_FLY); }
 +                              }
 +                              else if(this.move_movetype == MOVETYPE_FLY) { set_movetype(this, MOVETYPE_TOSS); }
 +                      }
 +                      if(autocvar_g_ctf_flag_return_dropped)
 +                      {
 +                              if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
 +                              {
 +                                      SetResourceAmountExplicit(this, RESOURCE_HEALTH, 0);
 +                                      ctf_CheckFlagReturn(this, RETURN_DROPPED);
 +                                      return;
 +                              }
 +                      }
 +                      if(this.ctf_flagdamaged_byworld)
 +                      {
 +                              TakeResource(this, RESOURCE_HEALTH, ((this.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE));
 +                              ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
 +                              return;
 +                      }
 +                      else if(autocvar_g_ctf_flag_return_time)
 +                      {
 +                              TakeResource(this, RESOURCE_HEALTH, ((this.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE));
 +                              ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
 +                              return;
 +                      }
 +                      return;
 +              }
 +
 +              case FLAG_CARRY:
 +              {
 +                      if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
 +                      {
 +                              SetResourceAmountExplicit(this, RESOURCE_HEALTH, 0);
 +                              ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
 +
 +                              CS(this.owner).impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
 +                              ImpulseCommands(this.owner);
 +                      }
 +                      if(autocvar_g_ctf_stalemate)
 +                      {
 +                              if(time >= wpforenemy_nextthink)
 +                              {
 +                                      ctf_CheckStalemate();
 +                                      wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
 +                              }
 +                      }
 +                      if(CTF_SAMETEAM(this, this.owner) && this.team)
 +                      {
 +                              if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
 +                                      ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
 +                              else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
 +                                      ctf_Handle_Return(this, this.owner);
 +                      }
 +                      return;
 +              }
 +
 +              case FLAG_PASSING:
 +              {
 +                      vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
 +                      targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
 +                      WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
 +
 +                      if((this.pass_target == NULL)
 +                              || (IS_DEAD(this.pass_target))
 +                              || (this.pass_target.flagcarried)
 +                              || (vdist(this.origin - targ_origin, >, autocvar_g_ctf_pass_radius))
 +                              || ((trace_fraction < 1) && (trace_ent != this.pass_target))
 +                              || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
 +                      {
 +                              // give up, pass failed
 +                              ctf_Handle_Drop(this, NULL, DROP_PASS);
 +                      }
 +                      else
 +                      {
 +                              // still a viable target, go for it
 +                              ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
 +                      }
 +                      return;
 +              }
 +
 +              default: // this should never happen
 +              {
 +                      LOG_TRACE("ctf_FlagThink(): Flag exists with no status?");
 +                      return;
 +              }
 +      }
 +}
 +
 +METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
 +{
 +      return = false;
 +      if(game_stopped) return;
 +      if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
 +
 +      bool is_not_monster = (!IS_MONSTER(toucher));
 +
 +      // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
 +      if(ITEM_TOUCH_NEEDKILL())
 +      {
 +              if(!autocvar_g_ctf_flag_return_damage_delay)
 +              {
 +                      SetResourceAmountExplicit(flag, RESOURCE_HEALTH, 0);
 +                      ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
 +              }
 +              if(!flag.ctf_flagdamaged_byworld) { return; }
 +      }
 +
 +      // special touch behaviors
 +      if(STAT(FROZEN, toucher)) { return; }
 +      else if(IS_VEHICLE(toucher))
 +      {
 +              if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
 +                      toucher = toucher.owner; // the player is actually the vehicle owner, not other
 +              else
 +                      return; // do nothing
 +      }
 +      else if(IS_MONSTER(toucher))
 +      {
 +              if(!autocvar_g_ctf_allow_monster_touch)
 +                      return; // do nothing
 +      }
 +      else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
 +      {
 +              if(time > flag.wait) // if we haven't in a while, play a sound/effect
 +              {
 +                      Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
 +                      _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
 +                      flag.wait = time + FLAG_TOUCHRATE;
 +              }
 +              return;
 +      }
 +      else if(IS_DEAD(toucher)) { return; }
 +
 +      switch(flag.ctf_status)
 +      {
 +              case FLAG_BASE:
 +              {
 +                      if(ctf_oneflag)
 +                      {
 +                              if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
 +                                      ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
 +                              else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
 +                                      ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
 +                      }
 +                      else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
 +                              ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
 +                      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)
 +                      {
 +                              ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
 +                              ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
 +                      }
 +                      else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
 +                              ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
 +                      break;
 +              }
 +
 +              case FLAG_DROPPED:
 +              {
 +                      if(CTF_SAMETEAM(toucher, flag) && ctf_Immediate_Return_Allowed(flag, toucher))
 +                              ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
 +                      else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
 +                              ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
 +                      break;
 +              }
 +
 +              case FLAG_CARRY:
 +              {
 +                      LOG_TRACE("Someone touched a flag even though it was being carried?");
 +                      break;
 +              }
 +
 +              case FLAG_PASSING:
 +              {
 +                      if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
 +                      {
 +                              if(DIFF_TEAM(toucher, flag.pass_sender))
 +                              {
 +                                      if(ctf_Immediate_Return_Allowed(flag, toucher))
 +                                              ctf_Handle_Return(flag, toucher);
 +                                      else if(is_not_monster && (!toucher.flagcarried))
 +                                              ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED);
 +                              }
 +                              else if(!toucher.flagcarried)
 +                                      ctf_Handle_Retrieve(flag, toucher);
 +                      }
 +                      break;
 +              }
 +      }
 +}
 +
 +.float last_respawn;
 +void ctf_RespawnFlag(entity flag)
 +{
 +      // check for flag respawn being called twice in a row
 +      if(flag.last_respawn > time - 0.5)
 +              { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
 +
 +      flag.last_respawn = time;
 +
 +      // reset the player (if there is one)
 +      if((flag.owner) && (flag.owner.flagcarried == flag))
 +      {
 +              WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
 +              WaypointSprite_Kill(flag.owner.wps_flagreturn);
 +              WaypointSprite_Kill(flag.wps_flagcarrier);
 +
 +              flag.owner.flagcarried = NULL;
 +              GameRules_scoring_vip(flag.owner, false);
 +
 +              if(flag.speedrunning)
 +                      ctf_FakeTimeLimit(flag.owner, -1);
 +      }
 +
 +      if((flag.owner) && (flag.owner.vehicle))
 +              flag.scale = FLAG_SCALE;
 +
 +      if(flag.ctf_status == FLAG_DROPPED)
 +              { WaypointSprite_Kill(flag.wps_flagdropped); }
 +
 +      // reset the flag
 +      setattachment(flag, NULL, "");
 +      setorigin(flag, flag.ctf_spawnorigin);
 +
 +      set_movetype(flag, ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS));
 +      flag.takedamage = DAMAGE_NO;
 +      SetResourceAmountExplicit(flag, RESOURCE_HEALTH, flag.max_flag_health);
 +      flag.solid = SOLID_TRIGGER;
 +      flag.velocity = '0 0 0';
 +      flag.angles = flag.mangle;
 +      flag.flags = FL_ITEM | FL_NOTARGET;
 +
 +      flag.ctf_status = FLAG_BASE;
 +      flag.owner = NULL;
 +      flag.pass_distance = 0;
 +      flag.pass_sender = NULL;
 +      flag.pass_target = NULL;
 +      flag.ctf_dropper = NULL;
 +      flag.ctf_pickuptime = 0;
 +      flag.ctf_droptime = 0;
 +      flag.ctf_flagdamaged_byworld = false;
 +      navigation_dynamicgoal_unset(flag);
 +
 +      ctf_CheckStalemate();
 +}
 +
 +void ctf_Reset(entity this)
 +{
 +      if(this.owner && IS_PLAYER(this.owner))
 +              ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
 +
 +      this.enemy = NULL;
 +      ctf_RespawnFlag(this);
 +}
 +
 +bool ctf_FlagBase_Customize(entity this, entity client)
 +{
 +      entity e = WaypointSprite_getviewentity(client);
 +      entity wp_owner = this.owner;
 +      entity flag = e.flagcarried;
 +      if(flag && CTF_SAMETEAM(e, flag))
 +              return false;
 +      if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
 +              return false;
 +      return true;
 +}
 +
 +void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
 +{
 +      // bot waypoints
 +      waypoint_spawnforitem_force(this, this.origin);
 +      navigation_dynamicgoal_init(this, true);
 +
 +      // waypointsprites
 +      entity basename;
 +      switch (this.team)
 +      {
 +              case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
 +              case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
 +              case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
 +              case NUM_TEAM_4: basename = WP_FlagBasePink; break;
 +              default: basename = WP_FlagBaseNeutral; break;
 +      }
 +
 +      entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
 +      wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
 +      WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
 +      setcefc(wp, ctf_FlagBase_Customize);
 +
 +      // captureshield setup
 +      ctf_CaptureShield_Spawn(this);
 +}
 +
 +.bool pushable;
 +
 +void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
 +{
 +      // main setup
 +      flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
 +      ctf_worldflaglist = flag;
 +
 +      setattachment(flag, NULL, "");
 +
 +      flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
 +      flag.team = teamnumber;
 +      flag.classname = "item_flag_team";
++      flag.target = "###item###"; // for finding the nearest item using findnearest
 +      flag.flags = FL_ITEM | FL_NOTARGET;
 +      IL_PUSH(g_items, flag);
 +      flag.solid = SOLID_TRIGGER;
 +      flag.takedamage = DAMAGE_NO;
 +      flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
 +      flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
 +      SetResourceAmountExplicit(flag, RESOURCE_HEALTH, flag.max_flag_health);
 +      flag.event_damage = ctf_FlagDamage;
 +      flag.pushable = true;
 +      flag.teleportable = TELEPORT_NORMAL;
 +      flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
 +      flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
 +      flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
 +      if(flag.damagedbycontents)
 +              IL_PUSH(g_damagedbycontents, flag);
 +      flag.velocity = '0 0 0';
 +      flag.mangle = flag.angles;
 +      flag.reset = ctf_Reset;
 +      settouch(flag, ctf_FlagTouch);
 +      setthink(flag, ctf_FlagThink);
 +      flag.nextthink = time + FLAG_THINKRATE;
 +      flag.ctf_status = FLAG_BASE;
 +
 +      // crudely force them all to 0
 +      if(autocvar_g_ctf_score_ignore_fields)
 +              flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
 +
 +      string teamname = Static_Team_ColorName_Lower(teamnumber);
 +      // appearence
 +      if(!flag.scale)                         { flag.scale = FLAG_SCALE; }
 +      if(flag.skin == 0)                      { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
 +      if(flag.model == "")            { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
 +      if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
 +      if (flag.passeffect == "")      { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
 +      if (flag.capeffect == "")       { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
 +
 +      // sounds
 +#define X(s,b) \
 +              if(flag.s == "") flag.s = b; \
 +              precache_sound(flag.s);
 +
 +      X(snd_flag_taken,               strzone(SND(CTF_TAKEN(teamnumber))))
 +      X(snd_flag_returned,    strzone(SND(CTF_RETURNED(teamnumber))))
 +      X(snd_flag_capture,     strzone(SND(CTF_CAPTURE(teamnumber))))
 +      X(snd_flag_dropped,     strzone(SND(CTF_DROPPED(teamnumber))))
 +      X(snd_flag_respawn,     strzone(SND(CTF_RESPAWN)))
 +      X(snd_flag_touch,               strzone(SND(CTF_TOUCH)))
 +      X(snd_flag_pass,                strzone(SND(CTF_PASS)))
 +#undef X
 +
 +      // precache
 +      precache_model(flag.model);
 +
 +      // appearence
 +      _setmodel(flag, flag.model); // precision set below
 +      setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
 +      flag.m_mins = flag.mins; // store these for squash checks
 +      flag.m_maxs = flag.maxs;
 +      setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
 +
 +      if(autocvar_g_ctf_flag_glowtrails)
 +      {
 +              switch(teamnumber)
 +              {
 +                      case NUM_TEAM_1: flag.glow_color = 251; break;
 +                      case NUM_TEAM_2: flag.glow_color = 210; break;
 +                      case NUM_TEAM_3: flag.glow_color = 110; break;
 +                      case NUM_TEAM_4: flag.glow_color = 145; break;
 +                      default:                 flag.glow_color = 254; break;
 +              }
 +              flag.glow_size = 25;
 +              flag.glow_trail = 1;
 +      }
 +
 +      flag.effects |= EF_LOWPRECISION;
 +      if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
 +      if(autocvar_g_ctf_dynamiclights)
 +      {
 +              switch(teamnumber)
 +              {
 +                      case NUM_TEAM_1: flag.effects |= EF_RED; break;
 +                      case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
 +                      case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
 +                      case NUM_TEAM_4: flag.effects |= EF_RED; break;
 +                      default:                 flag.effects |= EF_DIMLIGHT; break;
 +              }
 +      }
 +
 +      // flag placement
 +      if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
 +      {
 +              flag.dropped_origin = flag.origin;
 +              flag.noalign = true;
 +              set_movetype(flag, MOVETYPE_NONE);
 +      }
 +      else // drop to floor, automatically find a platform and set that as spawn origin
 +      {
 +              flag.noalign = false;
 +              droptofloor(flag);
 +              set_movetype(flag, MOVETYPE_NONE);
 +      }
 +
 +      InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
 +}
 +
 +
 +// ================
 +// Bot player logic
 +// ================
 +
 +// NOTE: LEGACY CODE, needs to be re-written!
 +
 +void havocbot_ctf_calculate_middlepoint()
 +{
 +      entity f;
 +      vector s = '0 0 0';
 +      vector fo = '0 0 0';
 +      int n = 0;
 +
 +      f = ctf_worldflaglist;
 +      while (f)
 +      {
 +              fo = f.origin;
 +              s = s + fo;
 +              f = f.ctf_worldflagnext;
 +              n++;
 +      }
 +      if(!n)
 +              return;
 +
 +      havocbot_middlepoint = s / n;
 +      havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
 +
 +      havocbot_symmetry_axis_m = 0;
 +      havocbot_symmetry_axis_q = 0;
 +      if(n == 2)
 +      {
 +              // for symmetrical editing of waypoints
 +              entity f1 = ctf_worldflaglist;
 +              entity f2 = f1.ctf_worldflagnext;
 +              float m = -(f1.origin.y - f2.origin.y) / (f1.origin.x - f2.origin.x);
 +              float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
 +              havocbot_symmetry_axis_m = m;
 +              havocbot_symmetry_axis_q = q;
 +      }
 +      havocbot_symmetry_origin_order = n;
 +}
 +
 +
 +entity havocbot_ctf_find_flag(entity bot)
 +{
 +      entity f;
 +      f = ctf_worldflaglist;
 +      while (f)
 +      {
 +              if (CTF_SAMETEAM(bot, f))
 +                      return f;
 +              f = f.ctf_worldflagnext;
 +      }
 +      return NULL;
 +}
 +
 +entity havocbot_ctf_find_enemy_flag(entity bot)
 +{
 +      entity f;
 +      f = ctf_worldflaglist;
 +      while (f)
 +      {
 +              if(ctf_oneflag)
 +              {
 +                      if(CTF_DIFFTEAM(bot, f))
 +                      {
 +                              if(f.team)
 +                              {
 +                                      if(bot.flagcarried)
 +                                              return f;
 +                              }
 +                              else if(!bot.flagcarried)
 +                                      return f;
 +                      }
 +              }
 +              else if (CTF_DIFFTEAM(bot, f))
 +                      return f;
 +              f = f.ctf_worldflagnext;
 +      }
 +      return NULL;
 +}
 +
 +int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
 +{
 +      if (!teamplay)
 +              return 0;
 +
 +      int c = 0;
 +
 +      FOREACH_CLIENT(IS_PLAYER(it), {
 +              if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
 +                      continue;
 +
 +              if(vdist(it.origin - org, <, tc_radius))
 +                      ++c;
 +      });
 +
 +      return c;
 +}
 +
 +// unused
 +#if 0
 +void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
 +{
 +      entity head;
 +      head = ctf_worldflaglist;
 +      while (head)
 +      {
 +              if (CTF_SAMETEAM(this, head))
 +                      break;
 +              head = head.ctf_worldflagnext;
 +      }
 +      if (head)
 +              navigation_routerating(this, head, ratingscale, 10000);
 +}
 +#endif
 +
 +void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
 +{
 +      entity head;
 +      head = ctf_worldflaglist;
 +      while (head)
 +      {
 +              if (CTF_SAMETEAM(this, head))
 +              {
 +                      if (this.flagcarried)
 +                      if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
 +                      {
 +                              head = head.ctf_worldflagnext; // skip base if it has a different group
 +                              continue;
 +                      }
 +                      break;
 +              }
 +              head = head.ctf_worldflagnext;
 +      }
 +      if (!head)
 +              return;
 +
 +      navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
 +}
 +
 +void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
 +{
 +      entity head;
 +      head = ctf_worldflaglist;
 +      while (head)
 +      {
 +              if(ctf_oneflag)
 +              {
 +                      if(CTF_DIFFTEAM(this, head))
 +                      {
 +                              if(head.team)
 +                              {
 +                                      if(this.flagcarried)
 +                                              break;
 +                              }
 +                              else if(!this.flagcarried)
 +                                      break;
 +                      }
 +              }
 +              else if(CTF_DIFFTEAM(this, head))
 +                      break;
 +              head = head.ctf_worldflagnext;
 +      }
 +      if (head)
 +              navigation_routerating(this, head, ratingscale, 10000);
 +}
 +
 +void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
 +{
 +      if (!bot_waypoints_for_items)
 +      {
 +              havocbot_goalrating_ctf_enemyflag(this, ratingscale);
 +              return;
 +      }
 +
 +      entity head;
 +
 +      head = havocbot_ctf_find_enemy_flag(this);
 +
 +      if (!head)
 +              return;
 +
 +      navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
 +}
 +
 +void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
 +{
 +      entity mf;
 +
 +      mf = havocbot_ctf_find_flag(this);
 +
 +      if(mf.ctf_status == FLAG_BASE)
 +              return;
 +
 +      if(mf.tag_entity)
 +              navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
 +}
 +
 +void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
 +{
 +      entity head;
 +      head = ctf_worldflaglist;
 +      while (head)
 +      {
 +              // flag is out in the field
 +              if(head.ctf_status != FLAG_BASE)
 +              if(head.tag_entity==NULL)       // dropped
 +              {
 +                      if(df_radius)
 +                      {
 +                              if(vdist(org - head.origin, <, df_radius))
 +                                      navigation_routerating(this, head, ratingscale, 10000);
 +                      }
 +                      else
 +                              navigation_routerating(this, head, ratingscale, 10000);
 +              }
 +
 +              head = head.ctf_worldflagnext;
 +      }
 +}
 +
 +void havocbot_goalrating_ctf_carrieritems(entity this, float ratingscale, vector org, float sradius)
 +{
 +      IL_EACH(g_items, it.bot_pickup,
 +      {
 +              // gather health and armor only
 +              if (it.solid)
 +              if (GetResourceAmount(it, RESOURCE_HEALTH) || GetResourceAmount(it, RESOURCE_ARMOR))
 +              if (vdist(it.origin - org, <, sradius))
 +              {
 +                      // get the value of the item
 +                      float t = it.bot_pickupevalfunc(this, it) * 0.0001;
 +                      if (t > 0)
 +                              navigation_routerating(this, it, t * ratingscale, 500);
 +              }
 +      });
 +}
 +
 +void havocbot_ctf_reset_role(entity this)
 +{
 +      float cdefense, cmiddle, coffense;
 +      entity mf, ef;
 +      float c;
 +
 +      if(IS_DEAD(this))
 +              return;
 +
 +      // Check ctf flags
 +      if (this.flagcarried)
 +      {
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
 +              return;
 +      }
 +
 +      mf = havocbot_ctf_find_flag(this);
 +      ef = havocbot_ctf_find_enemy_flag(this);
 +
 +      // Retrieve stolen flag
 +      if(mf.ctf_status!=FLAG_BASE)
 +      {
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
 +              return;
 +      }
 +
 +      // If enemy flag is taken go to the middle to intercept pursuers
 +      if(ef.ctf_status!=FLAG_BASE)
 +      {
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
 +              return;
 +      }
 +
 +      // if there is only me on the team switch to offense
 +      c = 0;
 +      FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(it, this), { ++c; });
 +
 +      if(c==1)
 +      {
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
 +              return;
 +      }
 +
 +      // Evaluate best position to take
 +      // Count mates on middle position
 +      cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
 +
 +      // Count mates on defense position
 +      cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
 +
 +      // Count mates on offense position
 +      coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
 +
 +      if(cdefense<=coffense)
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
 +      else if(coffense<=cmiddle)
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
 +      else
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
 +}
 +
 +void havocbot_role_ctf_carrier(entity this)
 +{
 +      if(IS_DEAD(this))
 +      {
 +              havocbot_ctf_reset_role(this);
 +              return;
 +      }
 +
 +      if (this.flagcarried == NULL)
 +      {
 +              havocbot_ctf_reset_role(this);
 +              return;
 +      }
 +
 +      if (navigation_goalrating_timeout(this))
 +      {
 +              navigation_goalrating_start(this);
 +
 +              if(ctf_oneflag)
 +                      havocbot_goalrating_ctf_enemybase(this, 50000);
 +              else
 +                      havocbot_goalrating_ctf_ourbase(this, 50000);
 +
 +              if(GetResourceAmount(this, RESOURCE_HEALTH) < 100)
 +                      havocbot_goalrating_ctf_carrieritems(this, 1000, this.origin, 1000);
 +
 +              navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
 +
 +              entity head = ctf_worldflaglist;
 +              while (head)
 +              {
 +                      if (this.goalentity == head.bot_basewaypoint)
 +                      {
 +                              this.goalentity_lock_timeout = time + 5;
 +                              break;
 +                      }
 +                      head = head.ctf_worldflagnext;
 +              }
 +
 +              if (this.goalentity)
 +                      this.havocbot_cantfindflag = time + 10;
 +              else if (time > this.havocbot_cantfindflag)
 +              {
 +                      // Can't navigate to my own base, suicide!
 +                      // TODO: drop it and wander around
 +                      Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
 +                      return;
 +              }
 +      }
 +}
 +
 +void havocbot_role_ctf_escort(entity this)
 +{
 +      entity mf, ef;
 +
 +      if(IS_DEAD(this))
 +      {
 +              havocbot_ctf_reset_role(this);
 +              return;
 +      }
 +
 +      if (this.flagcarried)
 +      {
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
 +              return;
 +      }
 +
 +      // If enemy flag is back on the base switch to previous role
 +      ef = havocbot_ctf_find_enemy_flag(this);
 +      if(ef.ctf_status==FLAG_BASE)
 +      {
 +              this.havocbot_role = this.havocbot_previous_role;
 +              this.havocbot_role_timeout = 0;
 +              return;
 +      }
 +
 +      // If the flag carrier reached the base switch to defense
 +      mf = havocbot_ctf_find_flag(this);
 +      if(mf.ctf_status!=FLAG_BASE)
 +      if(vdist(ef.origin - mf.dropped_origin, <, 300))
 +      {
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
 +              return;
 +      }
 +
 +      // Set the role timeout if necessary
 +      if (!this.havocbot_role_timeout)
 +      {
 +              this.havocbot_role_timeout = time + random() * 30 + 60;
 +      }
 +
 +      // If nothing happened just switch to previous role
 +      if (time > this.havocbot_role_timeout)
 +      {
 +              this.havocbot_role = this.havocbot_previous_role;
 +              this.havocbot_role_timeout = 0;
 +              return;
 +      }
 +
 +      // Chase the flag carrier
 +      if (navigation_goalrating_timeout(this))
 +      {
 +              navigation_goalrating_start(this);
 +
 +              havocbot_goalrating_ctf_enemyflag(this, 30000);
 +              havocbot_goalrating_ctf_ourstolenflag(this, 40000);
 +              havocbot_goalrating_items(this, 10000, this.origin, 10000);
 +
 +              navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
 +      }
 +}
 +
 +void havocbot_role_ctf_offense(entity this)
 +{
 +      entity mf, ef;
 +      vector pos;
 +
 +      if(IS_DEAD(this))
 +      {
 +              havocbot_ctf_reset_role(this);
 +              return;
 +      }
 +
 +      if (this.flagcarried)
 +      {
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
 +              return;
 +      }
 +
 +      // Check flags
 +      mf = havocbot_ctf_find_flag(this);
 +      ef = havocbot_ctf_find_enemy_flag(this);
 +
 +      // Own flag stolen
 +      if(mf.ctf_status!=FLAG_BASE)
 +      {
 +              if(mf.tag_entity)
 +                      pos = mf.tag_entity.origin;
 +              else
 +                      pos = mf.origin;
 +
 +              // Try to get it if closer than the enemy base
 +              if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
 +              {
 +                      havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
 +                      return;
 +              }
 +      }
 +
 +      // Escort flag carrier
 +      if(ef.ctf_status!=FLAG_BASE)
 +      {
 +              if(ef.tag_entity)
 +                      pos = ef.tag_entity.origin;
 +              else
 +                      pos = ef.origin;
 +
 +              if(vdist(pos - mf.dropped_origin, >, 700))
 +              {
 +                      havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
 +                      return;
 +              }
 +      }
 +
 +      // About to fail, switch to middlefield
 +      if(GetResourceAmount(this, RESOURCE_HEALTH) < 50)
 +      {
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
 +              return;
 +      }
 +
 +      // Set the role timeout if necessary
 +      if (!this.havocbot_role_timeout)
 +              this.havocbot_role_timeout = time + 120;
 +
 +      if (time > this.havocbot_role_timeout)
 +      {
 +              havocbot_ctf_reset_role(this);
 +              return;
 +      }
 +
 +      if (navigation_goalrating_timeout(this))
 +      {
 +              navigation_goalrating_start(this);
 +
 +              havocbot_goalrating_ctf_ourstolenflag(this, 50000);
 +              havocbot_goalrating_ctf_enemybase(this, 20000);
 +              havocbot_goalrating_items(this, 5000, this.origin, 1000);
 +              havocbot_goalrating_items(this, 1000, this.origin, 10000);
 +
 +              navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
 +      }
 +}
 +
 +// Retriever (temporary role):
 +void havocbot_role_ctf_retriever(entity this)
 +{
 +      entity mf;
 +
 +      if(IS_DEAD(this))
 +      {
 +              havocbot_ctf_reset_role(this);
 +              return;
 +      }
 +
 +      if (this.flagcarried)
 +      {
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
 +              return;
 +      }
 +
 +      // If flag is back on the base switch to previous role
 +      mf = havocbot_ctf_find_flag(this);
 +      if(mf.ctf_status==FLAG_BASE)
 +      {
 +              if (mf.enemy == this) // did this bot return the flag?
 +                      navigation_goalrating_timeout_force(this);
 +              havocbot_ctf_reset_role(this);
 +              return;
 +      }
 +
 +      if (!this.havocbot_role_timeout)
 +              this.havocbot_role_timeout = time + 20;
 +
 +      if (time > this.havocbot_role_timeout)
 +      {
 +              havocbot_ctf_reset_role(this);
 +              return;
 +      }
 +
 +      if (navigation_goalrating_timeout(this))
 +      {
 +              float rt_radius;
 +              rt_radius = 10000;
 +
 +              navigation_goalrating_start(this);
 +
 +              havocbot_goalrating_ctf_ourstolenflag(this, 50000);
 +              havocbot_goalrating_ctf_droppedflags(this, 40000, this.origin, rt_radius);
 +              havocbot_goalrating_ctf_enemybase(this, 30000);
 +              havocbot_goalrating_items(this, 500, this.origin, rt_radius);
 +
 +              navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
 +      }
 +}
 +
 +void havocbot_role_ctf_middle(entity this)
 +{
 +      entity mf;
 +
 +      if(IS_DEAD(this))
 +      {
 +              havocbot_ctf_reset_role(this);
 +              return;
 +      }
 +
 +      if (this.flagcarried)
 +      {
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
 +              return;
 +      }
 +
 +      mf = havocbot_ctf_find_flag(this);
 +      if(mf.ctf_status!=FLAG_BASE)
 +      {
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
 +              return;
 +      }
 +
 +      if (!this.havocbot_role_timeout)
 +              this.havocbot_role_timeout = time + 10;
 +
 +      if (time > this.havocbot_role_timeout)
 +      {
 +              havocbot_ctf_reset_role(this);
 +              return;
 +      }
 +
 +      if (navigation_goalrating_timeout(this))
 +      {
 +              vector org;
 +
 +              org = havocbot_middlepoint;
 +              org.z = this.origin.z;
 +
 +              navigation_goalrating_start(this);
 +
 +              havocbot_goalrating_ctf_ourstolenflag(this, 50000);
 +              havocbot_goalrating_ctf_droppedflags(this, 30000, this.origin, 10000);
 +              havocbot_goalrating_enemyplayers(this, 10000, org, havocbot_middlepoint_radius * 0.5);
 +              havocbot_goalrating_items(this, 5000, org, havocbot_middlepoint_radius * 0.5);
 +              havocbot_goalrating_items(this, 2500, this.origin, 10000);
 +              havocbot_goalrating_ctf_enemybase(this, 2500);
 +
 +              navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
 +      }
 +}
 +
 +void havocbot_role_ctf_defense(entity this)
 +{
 +      entity mf;
 +
 +      if(IS_DEAD(this))
 +      {
 +              havocbot_ctf_reset_role(this);
 +              return;
 +      }
 +
 +      if (this.flagcarried)
 +      {
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
 +              return;
 +      }
 +
 +      // If own flag was captured
 +      mf = havocbot_ctf_find_flag(this);
 +      if(mf.ctf_status!=FLAG_BASE)
 +      {
 +              havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
 +              return;
 +      }
 +
 +      if (!this.havocbot_role_timeout)
 +              this.havocbot_role_timeout = time + 30;
 +
 +      if (time > this.havocbot_role_timeout)
 +      {
 +              havocbot_ctf_reset_role(this);
 +              return;
 +      }
 +      if (navigation_goalrating_timeout(this))
 +      {
 +              vector org = mf.dropped_origin;
 +
 +              navigation_goalrating_start(this);
 +
 +              // if enemies are closer to our base, go there
 +              entity closestplayer = NULL;
 +              float distance, bestdistance = 10000;
 +              FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
 +                      distance = vlen(org - it.origin);
 +                      if(distance<bestdistance)
 +                      {
 +                              closestplayer = it;
 +                              bestdistance = distance;
 +                      }
 +              });
 +
 +              if(closestplayer)
 +              if(DIFF_TEAM(closestplayer, this))
 +              if(vdist(org - this.origin, >, 1000))
 +              if(checkpvs(this.origin,closestplayer)||random()<0.5)
 +                      havocbot_goalrating_ctf_ourbase(this, 30000);
 +
 +              havocbot_goalrating_ctf_ourstolenflag(this, 20000);
 +              havocbot_goalrating_ctf_droppedflags(this, 20000, org, havocbot_middlepoint_radius);
 +              havocbot_goalrating_enemyplayers(this, 15000, org, havocbot_middlepoint_radius);
 +              havocbot_goalrating_items(this, 10000, org, havocbot_middlepoint_radius);
 +              havocbot_goalrating_items(this, 5000, this.origin, 10000);
 +
 +              navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
 +      }
 +}
 +
 +void havocbot_role_ctf_setrole(entity bot, int role)
 +{
 +      string s = "(null)";
 +      switch(role)
 +      {
 +              case HAVOCBOT_CTF_ROLE_CARRIER:
 +                      s = "carrier";
 +                      bot.havocbot_role = havocbot_role_ctf_carrier;
 +                      bot.havocbot_role_timeout = 0;
 +                      bot.havocbot_cantfindflag = time + 10;
 +                      if (bot.havocbot_previous_role != bot.havocbot_role)
 +                              navigation_goalrating_timeout_force(bot);
 +                      break;
 +              case HAVOCBOT_CTF_ROLE_DEFENSE:
 +                      s = "defense";
 +                      bot.havocbot_role = havocbot_role_ctf_defense;
 +                      bot.havocbot_role_timeout = 0;
 +                      break;
 +              case HAVOCBOT_CTF_ROLE_MIDDLE:
 +                      s = "middle";
 +                      bot.havocbot_role = havocbot_role_ctf_middle;
 +                      bot.havocbot_role_timeout = 0;
 +                      break;
 +              case HAVOCBOT_CTF_ROLE_OFFENSE:
 +                      s = "offense";
 +                      bot.havocbot_role = havocbot_role_ctf_offense;
 +                      bot.havocbot_role_timeout = 0;
 +                      break;
 +              case HAVOCBOT_CTF_ROLE_RETRIEVER:
 +                      s = "retriever";
 +                      bot.havocbot_previous_role = bot.havocbot_role;
 +                      bot.havocbot_role = havocbot_role_ctf_retriever;
 +                      bot.havocbot_role_timeout = time + 10;
 +                      if (bot.havocbot_previous_role != bot.havocbot_role)
 +                              navigation_goalrating_timeout_expire(bot, 2);
 +                      break;
 +              case HAVOCBOT_CTF_ROLE_ESCORT:
 +                      s = "escort";
 +                      bot.havocbot_previous_role = bot.havocbot_role;
 +                      bot.havocbot_role = havocbot_role_ctf_escort;
 +                      bot.havocbot_role_timeout = time + 30;
 +                      if (bot.havocbot_previous_role != bot.havocbot_role)
 +                              navigation_goalrating_timeout_expire(bot, 2);
 +                      break;
 +      }
 +      LOG_TRACE(bot.netname, " switched to ", s);
 +}
 +
 +
 +// ==============
 +// Hook Functions
 +// ==============
 +
 +MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      int t = 0, t2 = 0, t3 = 0;
 +      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)
 +
 +      // initially clear items so they can be set as necessary later.
 +      STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING         | CTF_RED_FLAG_TAKEN            | CTF_RED_FLAG_LOST
 +                                                 | CTF_BLUE_FLAG_CARRYING             | CTF_BLUE_FLAG_TAKEN           | CTF_BLUE_FLAG_LOST
 +                                                 | CTF_YELLOW_FLAG_CARRYING   | CTF_YELLOW_FLAG_TAKEN         | CTF_YELLOW_FLAG_LOST
 +                                                 | CTF_PINK_FLAG_CARRYING     | CTF_PINK_FLAG_TAKEN           | CTF_PINK_FLAG_LOST
 +                                                 | CTF_NEUTRAL_FLAG_CARRYING  | CTF_NEUTRAL_FLAG_TAKEN        | CTF_NEUTRAL_FLAG_LOST
 +                                                 | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
 +
 +      // scan through all the flags and notify the client about them
 +      for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
 +      {
 +              if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING;              t2 = CTF_RED_FLAG_TAKEN;                t3 = CTF_RED_FLAG_LOST; }
 +              if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING;             t2 = CTF_BLUE_FLAG_TAKEN;               t3 = CTF_BLUE_FLAG_LOST; }
 +              if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING;   t2 = CTF_YELLOW_FLAG_TAKEN;             t3 = CTF_YELLOW_FLAG_LOST; }
 +              if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING;             t2 = CTF_PINK_FLAG_TAKEN;               t3 = CTF_PINK_FLAG_LOST; }
 +              if(flag.team == 0 && !b5)                  { b5 = true; t = CTF_NEUTRAL_FLAG_CARRYING;  t2 = CTF_NEUTRAL_FLAG_TAKEN;    t3 = CTF_NEUTRAL_FLAG_LOST; STAT(CTF_FLAGSTATUS, player) |= CTF_FLAG_NEUTRAL; }
 +
 +              switch(flag.ctf_status)
 +              {
 +                      case FLAG_PASSING:
 +                      case FLAG_CARRY:
 +                      {
 +                              if((flag.owner == player) || (flag.pass_sender == player))
 +                                      STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
 +                              else
 +                                      STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
 +                              break;
 +                      }
 +                      case FLAG_DROPPED:
 +                      {
 +                              STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
 +                              break;
 +                      }
 +              }
 +      }
 +
 +      // item for stopping players from capturing the flag too often
 +      if(player.ctf_captureshielded)
 +              STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
 +
 +      if(ctf_stalemate)
 +              STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
 +
 +      // update the health of the flag carrier waypointsprite
 +      if(player.wps_flagcarrier)
 +              WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(GetResourceAmount(player, RESOURCE_HEALTH), GetResourceAmount(player, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
 +{
 +      entity frag_attacker = M_ARGV(1, entity);
 +      entity frag_target = M_ARGV(2, entity);
 +      float frag_damage = M_ARGV(4, float);
 +      vector frag_force = M_ARGV(6, vector);
 +
 +      if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
 +      {
 +              if(frag_target == frag_attacker) // damage done to yourself
 +              {
 +                      frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
 +                      frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
 +              }
 +              else // damage done to everyone else
 +              {
 +                      frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
 +                      frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
 +              }
 +
 +              M_ARGV(4, float) = frag_damage;
 +              M_ARGV(6, vector) = frag_force;
 +      }
 +      else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
 +      {
 +              if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(GetResourceAmount(frag_target, RESOURCE_HEALTH), GetResourceAmount(frag_target, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id)))
 +              if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
 +              {
 +                      frag_target.wps_helpme_time = time;
 +                      WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
 +              }
 +              // todo: add notification for when flag carrier needs help?
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
 +{
 +      entity frag_attacker = M_ARGV(1, entity);
 +      entity frag_target = M_ARGV(2, entity);
 +
 +      if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
 +      {
 +              GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
 +              GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
 +      }
 +
 +      if(frag_target.flagcarried)
 +      {
 +              entity tmp_entity = frag_target.flagcarried;
 +              ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
 +              tmp_entity.ctf_dropper = NULL;
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
 +{
 +      M_ARGV(2, float) = 0; // frag score
 +      return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
 +}
 +
 +void ctf_RemovePlayer(entity player)
 +{
 +      if(player.flagcarried)
 +              { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
 +
 +      for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
 +      {
 +              if(flag.pass_sender == player) { flag.pass_sender = NULL; }
 +              if(flag.pass_target == player) { flag.pass_target = NULL; }
 +              if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      ctf_RemovePlayer(player);
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      ctf_RemovePlayer(player);
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
 +{
 +      if(!autocvar_g_ctf_leaderboard)
 +              return;
 +
 +      entity player = M_ARGV(0, entity);
 +
 +      if(IS_REAL_CLIENT(player))
 +      {
 +              int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
 +              race_send_rankings_cnt(MSG_ONE);
 +              for (int i = 1; i <= m; ++i)
 +              {
 +                      race_SendRankings(i, 0, 0, MSG_ONE);
 +              }
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
 +{
 +      if(!autocvar_g_ctf_leaderboard)
 +              return;
 +
 +      entity player = M_ARGV(0, entity);
 +
 +      if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
 +      {
 +              if (!player.stored_netname)
 +                      player.stored_netname = strzone(uid2name(player.crypto_idfp));
 +              if(player.stored_netname != player.netname)
 +              {
 +                      db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
 +                      strcpy(player.stored_netname, player.netname);
 +              }
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      if(player.flagcarried)
 +      if(!autocvar_g_ctf_portalteleport)
 +              { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
 +{
 +      if(MUTATOR_RETURNVALUE || game_stopped) return;
 +
 +      entity player = M_ARGV(0, entity);
 +
 +      if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
 +      {
 +              // pass the flag to a team mate
 +              if(autocvar_g_ctf_pass)
 +              {
 +                      entity head, closest_target = NULL;
 +                      head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
 +
 +                      while(head) // find the closest acceptable target to pass to
 +                      {
 +                              if(IS_PLAYER(head) && !IS_DEAD(head))
 +                              if(head != player && SAME_TEAM(head, player))
 +                              if(!head.speedrunning && !head.vehicle)
 +                              {
 +                                      // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
 +                                      vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
 +                                      vector passer_center = CENTER_OR_VIEWOFS(player);
 +
 +                                      if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
 +                                      {
 +                                              if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
 +                                              {
 +                                                      if(IS_BOT_CLIENT(head))
 +                                                      {
 +                                                              Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
 +                                                              ctf_Handle_Throw(head, player, DROP_PASS);
 +                                                      }
 +                                                      else
 +                                                      {
 +                                                              Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
 +                                                              Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
 +                                                      }
 +                                                      player.throw_antispam = time + autocvar_g_ctf_pass_wait;
 +                                                      return true;
 +                                              }
 +                                              else if(player.flagcarried && !head.flagcarried)
 +                                              {
 +                                                      if(closest_target)
 +                                                      {
 +                                                              vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
 +                                                              if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
 +                                                                      { closest_target = head; }
 +                                                      }
 +                                                      else { closest_target = head; }
 +                                              }
 +                                      }
 +                              }
 +                              head = head.chain;
 +                      }
 +
 +                      if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
 +              }
 +
 +              // throw the flag in front of you
 +              if(autocvar_g_ctf_throw && player.flagcarried)
 +              {
 +                      if(player.throw_count == -1)
 +                      {
 +                              if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
 +                              {
 +                                      player.throw_prevtime = time;
 +                                      player.throw_count = 1;
 +                                      ctf_Handle_Throw(player, NULL, DROP_THROW);
 +                                      return true;
 +                              }
 +                              else
 +                              {
 +                                      Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
 +                                      return false;
 +                              }
 +                      }
 +                      else
 +                      {
 +                              if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
 +                              else { player.throw_count += 1; }
 +                              if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
 +
 +                              player.throw_prevtime = time;
 +                              ctf_Handle_Throw(player, NULL, DROP_THROW);
 +                              return true;
 +                      }
 +              }
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
 +      {
 +              player.wps_helpme_time = time;
 +              WaypointSprite_HelpMePing(player.wps_flagcarrier);
 +      }
 +      else // create a normal help me waypointsprite
 +      {
 +              WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
 +              WaypointSprite_Ping(player.wps_helpme);
 +      }
 +
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
 +{
 +      entity player = M_ARGV(0, entity);
 +      entity veh = M_ARGV(1, entity);
 +
 +      if(player.flagcarried)
 +      {
 +              if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
 +              {
 +                      ctf_Handle_Throw(player, NULL, DROP_NORMAL);
 +              }
 +              else
 +              {
 +                      player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
 +                      setattachment(player.flagcarried, veh, "");
 +                      setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
 +                      player.flagcarried.scale = VEHICLE_FLAG_SCALE;
 +                      //player.flagcarried.angles = '0 0 0';
 +              }
 +              return true;
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      if(player.flagcarried)
 +      {
 +              setattachment(player.flagcarried, player, "");
 +              setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
 +              player.flagcarried.scale = FLAG_SCALE;
 +              player.flagcarried.angles = '0 0 0';
 +              player.flagcarried.nodrawtoclient = NULL;
 +              return true;
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      if(player.flagcarried)
 +      {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
 +              ctf_RespawnFlag(player.flagcarried);
 +              return true;
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
 +{
 +      entity flag; // temporary entity for the search method
 +
 +      for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
 +      {
 +              switch(flag.ctf_status)
 +              {
 +                      case FLAG_DROPPED:
 +                      case FLAG_PASSING:
 +                      {
 +                              // lock the flag, game is over
 +                              set_movetype(flag, MOVETYPE_NONE);
 +                              flag.takedamage = DAMAGE_NO;
 +                              flag.solid = SOLID_NOT;
 +                              flag.nextthink = false; // stop thinking
 +
 +                              //dprint("stopping the ", flag.netname, " from moving.\n");
 +                              break;
 +                      }
 +
 +                      default:
 +                      case FLAG_BASE:
 +                      case FLAG_CARRY:
 +                      {
 +                              // do nothing for these flags
 +                              break;
 +                      }
 +              }
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
 +{
 +      entity bot = M_ARGV(0, entity);
 +
 +      havocbot_ctf_reset_role(bot);
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
 +{
 +      M_ARGV(1, string) = "ctf_team";
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
 +{
 +      entity spectatee = M_ARGV(0, entity);
 +      entity client = M_ARGV(1, entity);
 +
 +      STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, GetRecords)
 +{
 +      int record_page = M_ARGV(0, int);
 +      string ret_string = M_ARGV(1, string);
 +
 +      for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
 +      {
 +              if (MapInfo_Get_ByID(i))
 +              {
 +                      float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
 +
 +                      if(!r)
 +                              continue;
 +
 +                      // TODO: uid2name
 +                      string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
 +                      ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
 +              }
 +      }
 +
 +      M_ARGV(1, string) = ret_string;
 +}
 +
 +bool superspec_Spectate(entity this, entity targ); // TODO
 +void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
 +MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
 +{
 +      entity player = M_ARGV(0, entity);
 +      string cmd_name = M_ARGV(1, string);
 +      int cmd_argc = M_ARGV(2, int);
 +
 +      if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
 +
 +      if(cmd_name == "followfc")
 +      {
 +              if(!g_ctf)
 +                      return true;
 +
 +              int _team = 0;
 +              bool found = false;
 +
 +              if(cmd_argc == 2)
 +              {
 +                      switch(argv(1))
 +                      {
 +                              case "red":    if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
 +                              case "blue":   if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
 +                              case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
 +                              case "pink":   if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
 +                      }
 +              }
 +
 +              FOREACH_CLIENT(IS_PLAYER(it), {
 +                      if(it.flagcarried && (it.team == _team || _team == 0))
 +                      {
 +                              found = true;
 +                              if(_team == 0 && IS_SPEC(player) && player.enemy == it)
 +                                      continue; // already spectating this fc, try another
 +                              return superspec_Spectate(player, it);
 +                      }
 +              });
 +
 +              if(!found)
 +                      superspec_msg("", "", player, "No active flag carrier\n", 1);
 +              return true;
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
 +{
 +      entity frag_target = M_ARGV(0, entity);
 +
 +      if(frag_target.flagcarried)
 +              ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
 +}
 +
 +
 +// ==========
 +// Spawnfuncs
 +// ==========
 +
 +/*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
 +CTF flag for team one (Red).
 +Keys:
 +"angle" Angle the flag will point (minus 90 degrees)...
 +"model" model to use, note this needs red and blue as skins 0 and 1...
 +"noise" sound played when flag is picked up...
 +"noise1" sound played when flag is returned by a teammate...
 +"noise2" sound played when flag is captured...
 +"noise3" sound played when flag is lost in the field and respawns itself...
 +"noise4" sound played when flag is dropped by a player...
 +"noise5" sound played when flag touches the ground... */
 +spawnfunc(item_flag_team1)
 +{
 +      if(!g_ctf) { delete(this); return; }
 +
 +      ctf_FlagSetup(NUM_TEAM_1, this);
 +}
 +
 +/*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
 +CTF flag for team two (Blue).
 +Keys:
 +"angle" Angle the flag will point (minus 90 degrees)...
 +"model" model to use, note this needs red and blue as skins 0 and 1...
 +"noise" sound played when flag is picked up...
 +"noise1" sound played when flag is returned by a teammate...
 +"noise2" sound played when flag is captured...
 +"noise3" sound played when flag is lost in the field and respawns itself...
 +"noise4" sound played when flag is dropped by a player...
 +"noise5" sound played when flag touches the ground... */
 +spawnfunc(item_flag_team2)
 +{
 +      if(!g_ctf) { delete(this); return; }
 +
 +      ctf_FlagSetup(NUM_TEAM_2, this);
 +}
 +
 +/*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
 +CTF flag for team three (Yellow).
 +Keys:
 +"angle" Angle the flag will point (minus 90 degrees)...
 +"model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
 +"noise" sound played when flag is picked up...
 +"noise1" sound played when flag is returned by a teammate...
 +"noise2" sound played when flag is captured...
 +"noise3" sound played when flag is lost in the field and respawns itself...
 +"noise4" sound played when flag is dropped by a player...
 +"noise5" sound played when flag touches the ground... */
 +spawnfunc(item_flag_team3)
 +{
 +      if(!g_ctf) { delete(this); return; }
 +
 +      ctf_FlagSetup(NUM_TEAM_3, this);
 +}
 +
 +/*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
 +CTF flag for team four (Pink).
 +Keys:
 +"angle" Angle the flag will point (minus 90 degrees)...
 +"model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
 +"noise" sound played when flag is picked up...
 +"noise1" sound played when flag is returned by a teammate...
 +"noise2" sound played when flag is captured...
 +"noise3" sound played when flag is lost in the field and respawns itself...
 +"noise4" sound played when flag is dropped by a player...
 +"noise5" sound played when flag touches the ground... */
 +spawnfunc(item_flag_team4)
 +{
 +      if(!g_ctf) { delete(this); return; }
 +
 +      ctf_FlagSetup(NUM_TEAM_4, this);
 +}
 +
 +/*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
 +CTF flag (Neutral).
 +Keys:
 +"angle" Angle the flag will point (minus 90 degrees)...
 +"model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
 +"noise" sound played when flag is picked up...
 +"noise1" sound played when flag is returned by a teammate...
 +"noise2" sound played when flag is captured...
 +"noise3" sound played when flag is lost in the field and respawns itself...
 +"noise4" sound played when flag is dropped by a player...
 +"noise5" sound played when flag touches the ground... */
 +spawnfunc(item_flag_neutral)
 +{
 +      if(!g_ctf) { delete(this); return; }
 +      if(!cvar("g_ctf_oneflag")) { delete(this); return; }
 +
 +      ctf_FlagSetup(0, this);
 +}
 +
 +/*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
 +Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
 +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.
 +Keys:
 +"netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
 +"cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
 +spawnfunc(ctf_team)
 +{
 +      if(!g_ctf) { delete(this); return; }
 +
 +      this.classname = "ctf_team";
 +      this.team = this.cnt + 1;
 +}
 +
 +// compatibility for quake maps
 +spawnfunc(team_CTF_redflag)    { spawnfunc_item_flag_team1(this);    }
 +spawnfunc(team_CTF_blueflag)   { spawnfunc_item_flag_team2(this);    }
 +spawnfunc(info_player_team1);
 +spawnfunc(team_CTF_redplayer)  { spawnfunc_info_player_team1(this);  }
 +spawnfunc(team_CTF_redspawn)   { spawnfunc_info_player_team1(this);  }
 +spawnfunc(info_player_team2);
 +spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this);  }
 +spawnfunc(team_CTF_bluespawn)  { spawnfunc_info_player_team2(this);  }
 +
 +spawnfunc(team_CTF_neutralflag)       { spawnfunc_item_flag_neutral(this);  }
 +spawnfunc(team_neutralobelisk)        { spawnfunc_item_flag_neutral(this);  }
 +
 +// compatibility for wop maps
 +spawnfunc(team_redplayer)      { spawnfunc_info_player_team1(this);  }
 +spawnfunc(team_blueplayer)     { spawnfunc_info_player_team2(this);  }
 +spawnfunc(team_ctl_redlolly)   { spawnfunc_item_flag_team1(this);    }
 +spawnfunc(team_CTL_redlolly)   { spawnfunc_item_flag_team1(this);    }
 +spawnfunc(team_ctl_bluelolly)  { spawnfunc_item_flag_team2(this);    }
 +spawnfunc(team_CTL_bluelolly)  { spawnfunc_item_flag_team2(this);    }
 +
 +
 +// ==============
 +// Initialization
 +// ==============
 +
 +// scoreboard setup
 +void ctf_ScoreRules(int teams)
 +{
 +      GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
 +        field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
 +        field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
 +        field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
 +        field(SP_CTF_PICKUPS, "pickups", 0);
 +        field(SP_CTF_FCKILLS, "fckills", 0);
 +        field(SP_CTF_RETURNS, "returns", 0);
 +        field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
 +      });
 +}
 +
 +// code from here on is just to support maps that don't have flag and team entities
 +void ctf_SpawnTeam (string teamname, int teamcolor)
 +{
 +      entity this = new_pure(ctf_team);
 +      this.netname = teamname;
 +      this.cnt = teamcolor - 1;
 +      this.spawnfunc_checked = true;
 +      this.team = teamcolor;
 +}
 +
 +void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
 +{
 +      ctf_teams = 0;
 +
 +      entity tmp_entity;
 +      for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
 +      {
 +              //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
 +              //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
 +
 +              switch(tmp_entity.team)
 +              {
 +                      case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
 +                      case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
 +                      case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
 +                      case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
 +              }
 +              if(tmp_entity.team == 0) { ctf_oneflag = true; }
 +      }
 +
 +      havocbot_ctf_calculate_middlepoint();
 +
 +      if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
 +      {
 +              ctf_teams = 0; // so set the default red and blue teams
 +              BITSET_ASSIGN(ctf_teams, BIT(0));
 +              BITSET_ASSIGN(ctf_teams, BIT(1));
 +      }
 +
 +      //ctf_teams = bound(2, ctf_teams, 4);
 +
 +      // if no teams are found, spawn defaults
 +      if(find(NULL, classname, "ctf_team") == NULL)
 +      {
 +              LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
 +              if(ctf_teams & BIT(0))
 +                      ctf_SpawnTeam("Red", NUM_TEAM_1);
 +              if(ctf_teams & BIT(1))
 +                      ctf_SpawnTeam("Blue", NUM_TEAM_2);
 +              if(ctf_teams & BIT(2))
 +                      ctf_SpawnTeam("Yellow", NUM_TEAM_3);
 +              if(ctf_teams & BIT(3))
 +                      ctf_SpawnTeam("Pink", NUM_TEAM_4);
 +      }
 +
 +      ctf_ScoreRules(ctf_teams);
 +}
 +
 +void ctf_Initialize()
 +{
 +      ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
 +
 +      ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
 +      ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
 +      ctf_captureshield_force = autocvar_g_ctf_shield_force;
 +
 +      InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);
 +}
index 751d23aa10639fc4956dfa4fb75136e7445f9233,0000000000000000000000000000000000000000..37eb3b89ac24387d8875708af50a995c4e8dd5a2
mode 100644,000000..100644
--- /dev/null
@@@ -1,670 -1,0 +1,670 @@@
- float Domination_CheckWinner()
 +#include "sv_domination.qh"
 +
 +#include <server/teamplay.qh>
 +
 +bool g_domination;
 +
 +int autocvar_g_domination_default_teams;
 +bool autocvar_g_domination_disable_frags;
 +int autocvar_g_domination_point_amt;
 +bool autocvar_g_domination_point_fullbright;
 +float autocvar_g_domination_round_timelimit;
 +float autocvar_g_domination_warmup;
 +float autocvar_g_domination_point_rate;
 +int autocvar_g_domination_teams_override;
 +
 +void dom_EventLog(string mode, float team_before, entity actor) // use an alias for easy changing and quick editing later
 +{
 +      if(autocvar_sv_eventlog)
 +              GameLogEcho(strcat(":dom:", mode, ":", ftos(team_before), ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
 +}
 +
 +void set_dom_state(entity e)
 +{
 +      STAT(DOM_TOTAL_PPS, e) = total_pps;
 +      STAT(DOM_PPS_RED, e) = pps_red;
 +      STAT(DOM_PPS_BLUE, e) = pps_blue;
 +      if(domination_teams >= 3)
 +              STAT(DOM_PPS_YELLOW, e) = pps_yellow;
 +      if(domination_teams >= 4)
 +              STAT(DOM_PPS_PINK, e) = pps_pink;
 +}
 +
 +void dompoint_captured(entity this)
 +{
 +      float old_delay, old_team, real_team;
 +
 +      // now that the delay has expired, switch to the latest team to lay claim to this point
 +      entity head = this.owner;
 +
 +      real_team = this.cnt;
 +      this.cnt = -1;
 +
 +      dom_EventLog("taken", this.team, this.dmg_inflictor);
 +      this.dmg_inflictor = NULL;
 +
 +      this.goalentity = head;
 +      this.model = head.mdl;
 +      this.modelindex = head.dmg;
 +      this.skin = head.skin;
 +
 +      float points, wait_time;
 +      if (autocvar_g_domination_point_amt)
 +              points = autocvar_g_domination_point_amt;
 +      else
 +              points = this.frags;
 +      if (autocvar_g_domination_point_rate)
 +              wait_time = autocvar_g_domination_point_rate;
 +      else
 +              wait_time = this.wait;
 +
 +      if(domination_roundbased)
 +              bprint(sprintf("^3%s^3%s\n", head.netname, this.message));
 +      else
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_DOMINATION_CAPTURE_TIME, head.netname, this.message, points, wait_time);
 +
 +      if(this.enemy.playerid == this.enemy_playerid)
 +              GameRules_scoring_add(this.enemy, DOM_TAKES, 1);
 +      else
 +              this.enemy = NULL;
 +
 +      if (head.noise != "")
 +              if(this.enemy)
 +                      _sound(this.enemy, CH_TRIGGER, head.noise, VOL_BASE, ATTEN_NORM);
 +              else
 +                      _sound(this, CH_TRIGGER, head.noise, VOL_BASE, ATTEN_NORM);
 +      if (head.noise1 != "")
 +              play2all(head.noise1);
 +
 +      this.delay = time + wait_time;
 +
 +      // do trigger work
 +      old_delay = this.delay;
 +      old_team = this.team;
 +      this.team = real_team;
 +      this.delay = 0;
 +      SUB_UseTargets (this, this, NULL);
 +      this.delay = old_delay;
 +      this.team = old_team;
 +
 +      entity msg = WP_DomNeut;
 +      switch(real_team)
 +      {
 +              case NUM_TEAM_1: msg = WP_DomRed; break;
 +              case NUM_TEAM_2: msg = WP_DomBlue; break;
 +              case NUM_TEAM_3: msg = WP_DomYellow; break;
 +              case NUM_TEAM_4: msg = WP_DomPink; break;
 +      }
 +
 +      WaypointSprite_UpdateSprites(this.sprite, msg, WP_Null, WP_Null);
 +
 +      total_pps = 0, pps_red = 0, pps_blue = 0, pps_yellow = 0, pps_pink = 0;
 +      IL_EACH(g_dompoints, true,
 +      {
 +              if (autocvar_g_domination_point_amt)
 +                      points = autocvar_g_domination_point_amt;
 +              else
 +                      points = it.frags;
 +              if (autocvar_g_domination_point_rate)
 +                      wait_time = autocvar_g_domination_point_rate;
 +              else
 +                      wait_time = it.wait;
 +              switch(it.goalentity.team)
 +              {
 +                      case NUM_TEAM_1: pps_red += points/wait_time; break;
 +                      case NUM_TEAM_2: pps_blue += points/wait_time; break;
 +                      case NUM_TEAM_3: pps_yellow += points/wait_time; break;
 +                      case NUM_TEAM_4: pps_pink += points/wait_time; break;
 +              }
 +              total_pps += points/wait_time;
 +      });
 +
 +      WaypointSprite_UpdateTeamRadar(this.sprite, RADARICON_DOMPOINT, colormapPaletteColor(this.goalentity.team - 1, 0));
 +      WaypointSprite_Ping(this.sprite);
 +
 +      this.captime = time;
 +
 +      FOREACH_CLIENT(IS_REAL_CLIENT(it), { set_dom_state(it); });
 +}
 +
 +void AnimateDomPoint(entity this)
 +{
 +      if(this.pain_finished > time)
 +              return;
 +      this.pain_finished = time + this.t_width;
 +      if(this.nextthink > this.pain_finished)
 +              this.nextthink = this.pain_finished;
 +
 +      this.frame = this.frame + 1;
 +      if(this.frame > this.t_length)
 +              this.frame = 0;
 +}
 +
 +void dompointthink(entity this)
 +{
 +      float fragamt;
 +
 +      this.nextthink = time + 0.1;
 +
 +      //this.frame = this.frame + 1;
 +      //if(this.frame > 119)
 +      //      this.frame = 0;
 +      AnimateDomPoint(this);
 +
 +      // give points
 +
 +      if (game_stopped || this.delay > time || time < game_starttime) // game has ended, don't keep giving points
 +              return;
 +
 +      if(autocvar_g_domination_point_rate)
 +              this.delay = time + autocvar_g_domination_point_rate;
 +      else
 +              this.delay = time + this.wait;
 +
 +      // give credit to the team
 +      // NOTE: this defaults to 0
 +      if (!domination_roundbased)
 +      if (this.goalentity.netname != "")
 +      {
 +              if(autocvar_g_domination_point_amt)
 +                      fragamt = autocvar_g_domination_point_amt;
 +              else
 +                      fragamt = this.frags;
 +              TeamScore_AddToTeam(this.goalentity.team, ST_SCORE, fragamt);
 +              TeamScore_AddToTeam(this.goalentity.team, ST_DOM_TICKS, fragamt);
 +
 +              // give credit to the individual player, if he is still there
 +              if (this.enemy.playerid == this.enemy_playerid)
 +              {
 +                      GameRules_scoring_add(this.enemy, SCORE, fragamt);
 +                      GameRules_scoring_add(this.enemy, DOM_TICKS, fragamt);
 +              }
 +              else
 +                      this.enemy = NULL;
 +      }
 +}
 +
 +void dompointtouch(entity this, entity toucher)
 +{
 +      if(!IS_PLAYER(toucher))
 +              return;
 +      if(GetResourceAmount(toucher, RESOURCE_HEALTH) < 1)
 +              return;
 +
 +      if(round_handler_IsActive() && !round_handler_IsRoundStarted())
 +              return;
 +
 +      if(time < this.captime + 0.3)
 +              return;
 +
 +      // only valid teams can claim it
 +      entity head = find(NULL, classname, "dom_team");
 +      while (head && head.team != toucher.team)
 +              head = find(head, classname, "dom_team");
 +      if (!head || head.netname == "" || head == this.goalentity)
 +              return;
 +
 +      // delay capture
 +
 +      this.team = this.goalentity.team; // this stores the PREVIOUS team!
 +
 +      this.cnt = toucher.team;
 +      this.owner = head; // team to switch to after the delay
 +      this.dmg_inflictor = toucher;
 +
 +      // this.state = 1;
 +      // this.delay = time + cvar("g_domination_point_capturetime");
 +      //this.nextthink = time + cvar("g_domination_point_capturetime");
 +      //this.think = dompoint_captured;
 +
 +      // go to neutral team in the mean time
 +      head = find(NULL, classname, "dom_team");
 +      while (head && head.netname != "")
 +              head = find(head, classname, "dom_team");
 +      if(head == NULL)
 +              return;
 +
 +      WaypointSprite_UpdateSprites(this.sprite, WP_DomNeut, WP_Null, WP_Null);
 +      WaypointSprite_UpdateTeamRadar(this.sprite, RADARICON_DOMPOINT, '0 1 1');
 +      WaypointSprite_Ping(this.sprite);
 +
 +      this.goalentity = head;
 +      this.model = head.mdl;
 +      this.modelindex = head.dmg;
 +      this.skin = head.skin;
 +
 +      this.enemy = toucher; // individual player scoring
 +      this.enemy_playerid = toucher.playerid;
 +      dompoint_captured(this);
 +}
 +
 +void dom_controlpoint_setup(entity this)
 +{
 +      entity head;
 +      // find the spawnfunc_dom_team representing unclaimed points
 +      head = find(NULL, classname, "dom_team");
 +      while(head && head.netname != "")
 +              head = find(head, classname, "dom_team");
 +      if (!head)
 +              objerror(this, "no spawnfunc_dom_team with netname \"\" found\n");
 +
 +      // copy important properties from spawnfunc_dom_team entity
 +      this.goalentity = head;
 +      _setmodel(this, head.mdl); // precision already set
 +      this.skin = head.skin;
 +
 +      this.cnt = -1;
 +
 +      if(this.message == "")
 +              this.message = " has captured a control point";
 +
 +      if(this.frags <= 0)
 +              this.frags = 1;
 +      if(this.wait <= 0)
 +              this.wait = 5;
 +
 +      float points, waittime;
 +      if (autocvar_g_domination_point_amt)
 +              points = autocvar_g_domination_point_amt;
 +      else
 +              points = this.frags;
 +      if (autocvar_g_domination_point_rate)
 +              waittime = autocvar_g_domination_point_rate;
 +      else
 +              waittime = this.wait;
 +
 +      total_pps += points/waittime;
 +
 +      if(!this.t_width)
 +              this.t_width = 0.02; // frame animation rate
 +      if(!this.t_length)
 +              this.t_length = 239; // maximum frame
 +
 +      setthink(this, dompointthink);
 +      this.nextthink = time;
 +      settouch(this, dompointtouch);
 +      this.solid = SOLID_TRIGGER;
 +      if(!this.flags & FL_ITEM)
 +              IL_PUSH(g_items, this);
 +      this.flags = FL_ITEM;
 +      setsize(this, '-32 -32 -32', '32 32 32');
 +      setorigin(this, this.origin + '0 0 20');
 +      droptofloor(this);
 +
 +      waypoint_spawnforitem(this);
 +      WaypointSprite_SpawnFixed(WP_DomNeut, this.origin + '0 0 32', this, sprite, RADARICON_DOMPOINT);
 +}
 +
 +int total_control_points;
 +void Domination_count_controlpoints()
 +{
 +      total_control_points = 0;
 +      for (int i = 1; i <= NUM_TEAMS; ++i)
 +      {
 +              Team_SetNumberOfControlPoints(Team_GetTeamFromIndex(i), 0);
 +      }
 +      IL_EACH(g_dompoints, true,
 +      {
 +              ++total_control_points;
 +              if (!Entity_HasValidTeam(it.goalentity))
 +              {
 +                      continue;
 +              }
 +              entity team_ = Entity_GetTeam(it.goalentity);
 +              int num_control_points = Team_GetNumberOfControlPoints(team_);
 +              ++num_control_points;
 +              Team_SetNumberOfControlPoints(team_, num_control_points);
 +      });
 +}
 +
 +int Domination_GetWinnerTeam()
 +{
 +      int winner_team = 0;
 +      if (Team_GetNumberOfControlPoints(Team_GetTeamFromIndex(1)) ==
 +              total_control_points)
 +      {
 +              winner_team = NUM_TEAM_1;
 +      }
 +      for (int i = 2; i <= NUM_TEAMS; ++i)
 +      {
 +              if (Team_GetNumberOfControlPoints(Team_GetTeamFromIndex(i)) ==
 +                      total_control_points)
 +              {
 +                      if (winner_team != 0)
 +                      {
 +                              return 0;
 +                      }
 +                      winner_team = Team_IndexToTeam(i);
 +              }
 +      }
 +      if (winner_team)
 +      {
 +              return winner_team;
 +      }
 +      return -1; // no control points left?
 +}
 +
-               return 1;
++bool Domination_CheckWinner()
 +{
 +      if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
 +      {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_OVER);
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_OVER);
 +
 +              game_stopped = true;
 +              round_handler_Init(5, autocvar_g_domination_warmup, autocvar_g_domination_round_timelimit);
-               return 0;
++              return true;
 +      }
 +
 +      Domination_count_controlpoints();
 +
 +      float winner_team = Domination_GetWinnerTeam();
 +
 +      if(winner_team == -1)
-       return 1;
++              return false;
 +
 +      if(winner_team > 0)
 +      {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, APP_TEAM_NUM(winner_team, CENTER_ROUND_TEAM_WIN));
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(winner_team, INFO_ROUND_TEAM_WIN));
 +              TeamScore_AddToTeam(winner_team, ST_DOM_CAPS, +1);
 +      }
 +      else if(winner_team == -1)
 +      {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED);
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED);
 +      }
 +
 +      game_stopped = true;
 +      round_handler_Init(5, autocvar_g_domination_warmup, autocvar_g_domination_round_timelimit);
 +
- float Domination_CheckPlayers()
++      return true;
 +}
 +
-       return 1;
++bool Domination_CheckPlayers()
 +{
++      return true;
 +}
 +
 +void Domination_RoundStart()
 +{
 +      FOREACH_CLIENT(IS_PLAYER(it), { it.player_blocked = false; });
 +}
 +
 +//go to best items, or control points you don't own
 +void havocbot_goalrating_controlpoints(entity this, float ratingscale, vector org, float sradius)
 +{
 +      IL_EACH(g_dompoints, vdist((((it.absmin + it.absmax) * 0.5) - org), <, sradius),
 +      {
 +              if(it.cnt > -1) // this is just being fought
 +                      navigation_routerating(this, it, ratingscale, 5000);
 +              else if(it.goalentity.cnt == 0) // unclaimed
 +                      navigation_routerating(this, it, ratingscale * 0.5, 5000);
 +              else if(it.goalentity.team != this.team) // other team's point
 +                      navigation_routerating(this, it, ratingscale * 0.2, 5000);
 +      });
 +}
 +
 +void havocbot_role_dom(entity this)
 +{
 +      if(IS_DEAD(this))
 +              return;
 +
 +      if (navigation_goalrating_timeout(this))
 +      {
 +              navigation_goalrating_start(this);
 +              havocbot_goalrating_controlpoints(this, 10000, this.origin, 15000);
 +              havocbot_goalrating_items(this, 8000, this.origin, 8000);
 +              //havocbot_goalrating_enemyplayers(this, 3000, this.origin, 2000);
 +              havocbot_goalrating_waypoints(this, 1, this.origin, 3000);
 +              navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(dom, TeamBalance_CheckAllowedTeams)
 +{
 +      // fallback?
 +      M_ARGV(0, float) = domination_teams;
 +      string ret_string = "dom_team";
 +
 +      entity head = find(NULL, classname, ret_string);
 +      while(head)
 +      {
 +              if(head.netname != "")
 +              {
 +                      if (Team_IsValidTeam(head.team))
 +                      {
 +                              M_ARGV(0, float) |= Team_TeamToBit(head.team);
 +                      }
 +              }
 +
 +              head = find(head, classname, ret_string);
 +      }
 +
 +      M_ARGV(1, string) = string_null;
 +
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(dom, reset_map_players)
 +{
 +      total_pps = 0, pps_red = 0, pps_blue = 0, pps_yellow = 0, pps_pink = 0;
 +      FOREACH_CLIENT(IS_PLAYER(it), {
 +              PutClientInServer(it);
 +              if(domination_roundbased)
 +                      it.player_blocked = 1;
 +              if(IS_REAL_CLIENT(it))
 +                      set_dom_state(it);
 +      });
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(dom, PlayerSpawn)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      if(domination_roundbased)
 +      if(!round_handler_IsRoundStarted())
 +              player.player_blocked = 1;
 +      else
 +              player.player_blocked = 0;
 +}
 +
 +MUTATOR_HOOKFUNCTION(dom, ClientConnect)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      set_dom_state(player);
 +}
 +
 +MUTATOR_HOOKFUNCTION(dom, HavocBot_ChooseRole)
 +{
 +      entity bot = M_ARGV(0, entity);
 +
 +      bot.havocbot_role = havocbot_role_dom;
 +      return true;
 +}
 +
 +/*QUAKED spawnfunc_dom_controlpoint (0 .5 .8) (-16 -16 -24) (16 16 32)
 +Control point for Domination gameplay.
 +*/
 +spawnfunc(dom_controlpoint)
 +{
 +      if(!g_domination)
 +      {
 +              delete(this);
 +              return;
 +      }
 +      setthink(this, dom_controlpoint_setup);
 +      this.nextthink = time + 0.1;
 +      this.reset = dom_controlpoint_setup;
 +
 +      if(!this.scale)
 +              this.scale = 0.6;
 +
 +      this.effects = this.effects | EF_LOWPRECISION;
 +      if (autocvar_g_domination_point_fullbright)
 +              this.effects |= EF_FULLBRIGHT;
 +
 +      IL_PUSH(g_dompoints, this);
 +}
 +
 +/*QUAKED spawnfunc_dom_team (0 .5 .8) (-32 -32 -24) (32 32 32)
 +Team declaration for Domination gameplay, this allows you to decide what team
 +names and control point models are used in your map.
 +
 +Note: If you use spawnfunc_dom_team entities you must define at least 3 and only two
 +can have netname set!  The nameless team owns all control points at start.
 +
 +Keys:
 +"netname"
 + Name of the team (for example Red Team, Blue Team, Green Team, Yellow Team, Life, Death, etc)
 +"cnt"
 + Scoreboard color of the team (for example 4 is red and 13 is blue)
 +"model"
 + Model to use for control points owned by this team (for example
 + "progs/b_g_key.mdl" is a gold keycard, and "progs/b_s_key.mdl" is a silver
 + keycard)
 +"skin"
 + Skin of the model to use (for team skins on a single model)
 +"noise"
 + Sound to play when this team captures a point.
 + (this is a localized sound, like a small alarm or other effect)
 +"noise1"
 + Narrator speech to play when this team captures a point.
 + (this is a global sound, like "Red team has captured a control point")
 +*/
 +
 +spawnfunc(dom_team)
 +{
 +      if(!g_domination || autocvar_g_domination_teams_override >= 2)
 +      {
 +              delete(this);
 +              return;
 +      }
 +      precache_model(this.model);
 +      if (this.noise != "")
 +              precache_sound(this.noise);
 +      if (this.noise1 != "")
 +              precache_sound(this.noise1);
 +      this.classname = "dom_team";
 +      _setmodel(this, this.model); // precision not needed
 +      this.mdl = this.model;
 +      this.dmg = this.modelindex;
 +      this.model = "";
 +      this.modelindex = 0;
 +      // this would have to be changed if used in quakeworld
 +      if(this.cnt)
 +              this.team = this.cnt + 1; // WHY are these different anyway?
 +}
 +
 +// scoreboard setup
 +void ScoreRules_dom(int teams)
 +{
 +      if(domination_roundbased)
 +      {
 +          GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
 +            field_team(ST_DOM_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
 +            field(SP_DOM_TAKES, "takes", 0);
 +          });
 +      }
 +      else
 +      {
 +              float sp_domticks, sp_score;
 +              sp_score = sp_domticks = 0;
 +              if(autocvar_g_domination_disable_frags)
 +                      sp_domticks = SFL_SORT_PRIO_PRIMARY;
 +              else
 +                      sp_score = SFL_SORT_PRIO_PRIMARY;
 +              GameRules_scoring(teams, sp_score, sp_score, {
 +            field_team(ST_DOM_TICKS, "ticks", sp_domticks);
 +            field(SP_DOM_TICKS, "ticks", sp_domticks);
 +            field(SP_DOM_TAKES, "takes", 0);
 +              });
 +      }
 +}
 +
 +// code from here on is just to support maps that don't have control point and team entities
 +void dom_spawnteam(string teamname, float teamcolor, string pointmodel, float pointskin, Sound capsound, string capnarration, string capmessage)
 +{
 +      TC(Sound, capsound);
 +      entity e = new_pure(dom_team);
 +      e.netname = strzone(teamname);
 +      e.cnt = teamcolor;
 +      e.model = pointmodel;
 +      e.skin = pointskin;
 +      e.noise = strzone(Sound_fixpath(capsound));
 +      e.noise1 = strzone(capnarration);
 +      e.message = strzone(capmessage);
 +
 +      // this code is identical to spawnfunc_dom_team
 +      _setmodel(e, e.model); // precision not needed
 +      e.mdl = e.model;
 +      e.dmg = e.modelindex;
 +      e.model = "";
 +      e.modelindex = 0;
 +      // this would have to be changed if used in quakeworld
 +      e.team = e.cnt + 1;
 +
 +      //eprint(e);
 +}
 +
 +void dom_spawnpoint(vector org)
 +{
 +      entity e = spawn();
 +      e.classname = "dom_controlpoint";
 +      setthink(e, spawnfunc_dom_controlpoint);
 +      e.nextthink = time;
 +      setorigin(e, org);
 +      spawnfunc_dom_controlpoint(e);
 +}
 +
 +// spawn some default teams if the map is not set up for domination
 +void dom_spawnteams(int teams)
 +{
 +      TC(int, teams);
 +      dom_spawnteam(Team_ColoredFullName(NUM_TEAM_1), NUM_TEAM_1-1, "models/domination/dom_red.md3", 0, SND_DOM_CLAIM, "", "Red team has captured a control point");
 +      dom_spawnteam(Team_ColoredFullName(NUM_TEAM_2), NUM_TEAM_2-1, "models/domination/dom_blue.md3", 0, SND_DOM_CLAIM, "", "Blue team has captured a control point");
 +      if(teams >= 3)
 +              dom_spawnteam(Team_ColoredFullName(NUM_TEAM_3), NUM_TEAM_3-1, "models/domination/dom_yellow.md3", 0, SND_DOM_CLAIM, "", "Yellow team has captured a control point");
 +      if(teams >= 4)
 +              dom_spawnteam(Team_ColoredFullName(NUM_TEAM_4), NUM_TEAM_4-1, "models/domination/dom_pink.md3", 0, SND_DOM_CLAIM, "", "Pink team has captured a control point");
 +      dom_spawnteam("", 0, "models/domination/dom_unclaimed.md3", 0, SND_Null, "", "");
 +}
 +
 +void dom_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
 +{
 +      // if no teams are found, spawn defaults
 +      if(find(NULL, classname, "dom_team") == NULL || autocvar_g_domination_teams_override >= 2)
 +      {
 +              LOG_TRACE("No \"dom_team\" entities found on this map, creating them anyway.");
 +              domination_teams = autocvar_g_domination_teams_override;
 +              if (domination_teams < 2)
 +                      domination_teams = autocvar_g_domination_default_teams;
 +              domination_teams = bound(2, domination_teams, 4);
 +              dom_spawnteams(domination_teams);
 +      }
 +
 +      entity balance = TeamBalance_CheckAllowedTeams(NULL);
 +      int teams = TeamBalance_GetAllowedTeams(balance);
 +      TeamBalance_Destroy(balance);
 +      domination_teams = teams;
 +
 +      domination_roundbased = autocvar_g_domination_roundbased;
 +
 +      ScoreRules_dom(domination_teams);
 +
 +      if(domination_roundbased)
 +      {
 +              round_handler_Spawn(Domination_CheckPlayers, Domination_CheckWinner, Domination_RoundStart);
 +              round_handler_Init(5, autocvar_g_domination_warmup, autocvar_g_domination_round_timelimit);
 +      }
 +}
 +
 +void dom_Initialize()
 +{
 +      g_domination = true;
 +      InitializeEntity(NULL, dom_DelayedInit, INITPRIO_GAMETYPE);
 +}
index 804b39d67d17a5648934735faeff1c492c6d7433,0000000000000000000000000000000000000000..5c85c3b1be0d4754327ba5ce163ff213b2377efe
mode 100644,000000..100644
--- /dev/null
@@@ -1,606 -1,0 +1,606 @@@
- float freezetag_CheckTeams()
 +#include "sv_freezetag.qh"
 +
 +#include <server/resources.qh>
 +
 +float autocvar_g_freezetag_frozen_maxtime;
 +float autocvar_g_freezetag_revive_clearspeed;
 +float autocvar_g_freezetag_round_timelimit;
 +//int autocvar_g_freezetag_teams;
 +int autocvar_g_freezetag_teams_override;
 +float autocvar_g_freezetag_warmup;
 +
 +void freezetag_count_alive_players()
 +{
 +      total_players = 0;
 +      for (int i = 1; i <= NUM_TEAMS; ++i)
 +      {
 +              Team_SetNumberOfAlivePlayers(Team_GetTeamFromIndex(i), 0);
 +      }
 +      FOREACH_CLIENT(IS_PLAYER(it) && Entity_HasValidTeam(it),
 +      {
 +              ++total_players;
 +              if ((GetResourceAmount(it, RESOURCE_HEALTH) < 1) ||
 +                      (STAT(FROZEN, it) == 1))
 +              {
 +                      continue;
 +              }
 +              entity team_ = Entity_GetTeam(it);
 +              int num_alive = Team_GetNumberOfAlivePlayers(team_);
 +              ++num_alive;
 +              Team_SetNumberOfAlivePlayers(team_, num_alive);
 +      });
 +      FOREACH_CLIENT(IS_REAL_CLIENT(it),
 +      {
 +              STAT(REDALIVE, it) = Team_GetNumberOfAlivePlayers(Team_GetTeamFromIndex(
 +                      1));
 +              STAT(BLUEALIVE, it) = Team_GetNumberOfAlivePlayers(
 +                      Team_GetTeamFromIndex(2));
 +              STAT(YELLOWALIVE, it) = Team_GetNumberOfAlivePlayers(
 +                      Team_GetTeamFromIndex(3));
 +              STAT(PINKALIVE, it) = Team_GetNumberOfAlivePlayers(
 +                      Team_GetTeamFromIndex(4));
 +      });
 +
 +      eliminatedPlayers.SendFlags |= 1;
 +}
 +
 +#define FREEZETAG_ALIVE_TEAMS_OK() (Team_GetNumberOfAliveTeams() == NumTeams(freezetag_teams))
 +
-               return 1;
++bool freezetag_CheckTeams()
 +{
 +      static float prev_missing_teams_mask;
 +      if(FREEZETAG_ALIVE_TEAMS_OK())
 +      {
 +              if(prev_missing_teams_mask > 0)
 +                      Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_TEAMS);
 +              prev_missing_teams_mask = -1;
-               return 0;
++              return true;
 +      }
 +      if(total_players == 0)
 +      {
 +              if(prev_missing_teams_mask > 0)
 +                      Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_TEAMS);
 +              prev_missing_teams_mask = -1;
-       return 0;
++              return false;
 +      }
 +      int missing_teams_mask = 0;
 +      for (int i = 1; i <= NUM_TEAMS; ++i)
 +      {
 +              if ((freezetag_teams & Team_IndexToBit(i)) &&
 +                      (Team_GetNumberOfAlivePlayers(Team_GetTeamFromIndex(i)) == 0))
 +              {
 +                      missing_teams_mask |= Team_IndexToBit(i);
 +              }
 +      }
 +      if(prev_missing_teams_mask != missing_teams_mask)
 +      {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MISSING_TEAMS, missing_teams_mask);
 +              prev_missing_teams_mask = missing_teams_mask;
 +      }
- float freezetag_CheckWinner()
++      return false;
 +}
 +
 +int freezetag_getWinnerTeam()
 +{
 +      int winner_team = 0;
 +      if (Team_GetNumberOfAlivePlayers(Team_GetTeamFromIndex(1)) >= 1)
 +      {
 +              winner_team = NUM_TEAM_1;
 +      }
 +      for (int i = 2; i <= NUM_TEAMS; ++i)
 +      {
 +              if (Team_GetNumberOfAlivePlayers(Team_GetTeamFromIndex(i)) >= 1)
 +              {
 +                      if (winner_team != 0)
 +                      {
 +                              return 0;
 +                      }
 +                      winner_team = Team_IndexToTeam(i);
 +              }
 +      }
 +      if (winner_team)
 +      {
 +              return winner_team;
 +      }
 +      return -1; // no player left
 +}
 +
 +void nades_Clear(entity);
 +void nades_GiveBonus(entity player, float score);
 +
-               return 1;
++bool freezetag_CheckWinner()
 +{
 +      if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
 +      {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_OVER);
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_OVER);
 +              FOREACH_CLIENT(IS_PLAYER(it), {
 +                      it.freezetag_frozen_timeout = 0;
 +                      nades_Clear(it);
 +              });
 +              game_stopped = true;
 +              round_handler_Init(5, autocvar_g_freezetag_warmup, autocvar_g_freezetag_round_timelimit);
-               return 0;
++              return true;
 +      }
 +
 +      if (Team_GetNumberOfAliveTeams() > 1)
 +      {
-       return 1;
++              return false;
 +      }
 +
 +      int winner_team = freezetag_getWinnerTeam();
 +      if(winner_team > 0)
 +      {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, APP_TEAM_NUM(winner_team, CENTER_ROUND_TEAM_WIN));
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(winner_team, INFO_ROUND_TEAM_WIN));
 +              TeamScore_AddToTeam(winner_team, ST_SCORE, +1);
 +      }
 +      else if(winner_team == -1)
 +      {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED);
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED);
 +      }
 +
 +      FOREACH_CLIENT(IS_PLAYER(it), {
 +              it.freezetag_frozen_timeout = 0;
 +              nades_Clear(it);
 +      });
 +
 +      game_stopped = true;
 +      round_handler_Init(5, autocvar_g_freezetag_warmup, autocvar_g_freezetag_round_timelimit);
- float freezetag_isEliminated(entity e)
++      return true;
 +}
 +
 +entity freezetag_LastPlayerForTeam(entity this)
 +{
 +      entity last_pl = NULL;
 +      FOREACH_CLIENT(IS_PLAYER(it) && it != this && SAME_TEAM(it, this), {
 +              if (!STAT(FROZEN, it) && GetResourceAmount(it, RESOURCE_HEALTH) >= 1)
 +              {
 +                      if (!last_pl)
 +                              last_pl = it;
 +                      else
 +                              return NULL;
 +              }
 +      });
 +      return last_pl;
 +}
 +
 +void freezetag_LastPlayerForTeam_Notify(entity this)
 +{
 +      if(round_handler_IsActive())
 +      if(round_handler_IsRoundStarted())
 +      {
 +              entity pl = freezetag_LastPlayerForTeam(this);
 +              if(pl)
 +                      Send_Notification(NOTIF_ONE, pl, MSG_CENTER, CENTER_ALONE);
 +      }
 +}
 +
 +void freezetag_Add_Score(entity targ, entity attacker)
 +{
 +      if(attacker == targ)
 +      {
 +              // you froze your own dumb targ
 +              // counted as "suicide" already
 +              GameRules_scoring_add(targ, SCORE, -1);
 +      }
 +      else if(IS_PLAYER(attacker))
 +      {
 +              // got frozen by an enemy
 +              // counted as "kill" and "death" already
 +              GameRules_scoring_add(targ, SCORE, -1);
 +              GameRules_scoring_add(attacker, SCORE, +1);
 +      }
 +      // else nothing - got frozen by the game type rules themselves
 +}
 +
 +// to be called when the player is frozen by freezetag (on death, spectator join etc), gives the score
 +void freezetag_Freeze(entity targ, entity attacker)
 +{
 +      if(STAT(FROZEN, targ))
 +              return;
 +
 +      if(autocvar_g_freezetag_frozen_maxtime > 0)
 +              targ.freezetag_frozen_timeout = time + autocvar_g_freezetag_frozen_maxtime;
 +
 +      Freeze(targ, 0, 1, true);
 +
 +      freezetag_count_alive_players();
 +
 +      freezetag_Add_Score(targ, attacker);
 +}
 +
++bool freezetag_isEliminated(entity e)
 +{
 +      if(IS_PLAYER(e) && (STAT(FROZEN, e) == 1 || IS_DEAD(e)))
 +              return true;
 +      return false;
 +}
 +
 +
 +// ================
 +// Bot player logic
 +// ================
 +
 +void(entity this) havocbot_role_ft_freeing;
 +void(entity this) havocbot_role_ft_offense;
 +
 +void havocbot_goalrating_freeplayers(entity this, float ratingscale, vector org, float sradius)
 +{
 +      float t;
 +      FOREACH_CLIENT(IS_PLAYER(it) && it != this && SAME_TEAM(it, this), {
 +              if (STAT(FROZEN, it) == 1)
 +              {
 +                      if(vdist(it.origin - org, >, sradius))
 +                              continue;
 +                      navigation_routerating(this, it, ratingscale, 2000);
 +              }
 +              else if(vdist(it.origin - org, >, 400)) // avoid gathering all teammates in one place
 +              {
 +                      // If teamate is not frozen still seek them out as fight better
 +                      // in a group.
 +                      t = 0.2 * 150 / (GetResourceAmount(this, RESOURCE_HEALTH) + GetResourceAmount(this, RESOURCE_ARMOR));
 +                      navigation_routerating(this, it, t * ratingscale, 2000);
 +              }
 +      });
 +}
 +
 +void havocbot_role_ft_offense(entity this)
 +{
 +      if(IS_DEAD(this))
 +              return;
 +
 +      if (!this.havocbot_role_timeout)
 +              this.havocbot_role_timeout = time + random() * 10 + 20;
 +
 +      // Count how many players on team are unfrozen.
 +      int unfrozen = 0;
 +      FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(it, this) && !(STAT(FROZEN, it) != 1), { unfrozen++; });
 +
 +      // If only one left on team or if role has timed out then start trying to free players.
 +      if (((unfrozen == 0) && (!STAT(FROZEN, this))) || (time > this.havocbot_role_timeout))
 +      {
 +              LOG_TRACE("changing role to freeing");
 +              this.havocbot_role = havocbot_role_ft_freeing;
 +              this.havocbot_role_timeout = 0;
 +              return;
 +      }
 +
 +      if (navigation_goalrating_timeout(this))
 +      {
 +              navigation_goalrating_start(this);
 +              havocbot_goalrating_items(this, 10000, this.origin, 10000);
 +              havocbot_goalrating_enemyplayers(this, 20000, this.origin, 10000);
 +              havocbot_goalrating_freeplayers(this, 9000, this.origin, 10000);
 +              havocbot_goalrating_waypoints(this, 1, this.origin, 3000);
 +              navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
 +      }
 +}
 +
 +void havocbot_role_ft_freeing(entity this)
 +{
 +      if(IS_DEAD(this))
 +              return;
 +
 +      if (!this.havocbot_role_timeout)
 +              this.havocbot_role_timeout = time + random() * 10 + 20;
 +
 +      if (time > this.havocbot_role_timeout)
 +      {
 +              LOG_TRACE("changing role to offense");
 +              this.havocbot_role = havocbot_role_ft_offense;
 +              this.havocbot_role_timeout = 0;
 +              return;
 +      }
 +
 +      if (navigation_goalrating_timeout(this))
 +      {
 +              navigation_goalrating_start(this);
 +              havocbot_goalrating_items(this, 8000, this.origin, 10000);
 +              havocbot_goalrating_enemyplayers(this, 10000, this.origin, 10000);
 +              havocbot_goalrating_freeplayers(this, 20000, this.origin, 10000);
 +              havocbot_goalrating_waypoints(this, 1, this.origin, 3000);
 +              navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
 +      }
 +}
 +
 +
 +// ==============
 +// Hook Functions
 +// ==============
 +
 +void ft_RemovePlayer(entity this)
 +{
 +      SetResourceAmountExplicit(this, RESOURCE_HEALTH, 0); // neccessary to update correctly alive stats
 +      if(!STAT(FROZEN, this))
 +              freezetag_LastPlayerForTeam_Notify(this);
 +      Unfreeze(this);
 +      freezetag_count_alive_players();
 +}
 +
 +MUTATOR_HOOKFUNCTION(ft, ClientDisconnect)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      ft_RemovePlayer(player);
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ft, MakePlayerObserver)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      ft_RemovePlayer(player);
 +}
 +
 +MUTATOR_HOOKFUNCTION(ft, PlayerDies)
 +{
 +      entity frag_attacker = M_ARGV(1, entity);
 +      entity frag_target = M_ARGV(2, entity);
 +      float frag_deathtype = M_ARGV(3, float);
 +
 +      if(round_handler_IsActive())
 +      if(round_handler_CountdownRunning())
 +      {
 +              if(STAT(FROZEN, frag_target))
 +                      Unfreeze(frag_target);
 +              freezetag_count_alive_players();
 +              return true; // let the player die so that he can respawn whenever he wants
 +      }
 +
 +      // Cases DEATH_TEAMCHANGE and DEATH_AUTOTEAMCHANGE are needed to fix a bug whe
 +      // you succeed changing team through the menu: you both really die (gibbing) and get frozen
 +      if(ITEM_DAMAGE_NEEDKILL(frag_deathtype)
 +              || frag_deathtype == DEATH_TEAMCHANGE.m_id || frag_deathtype == DEATH_AUTOTEAMCHANGE.m_id)
 +      {
 +              // let the player die, he will be automatically frozen when he respawns
 +              if(STAT(FROZEN, frag_target) != 1)
 +              {
 +                      freezetag_Add_Score(frag_target, frag_attacker);
 +                      freezetag_count_alive_players();
 +                      freezetag_LastPlayerForTeam_Notify(frag_target);
 +              }
 +              else
 +                      Unfreeze(frag_target); // remove ice
 +              SetResourceAmountExplicit(frag_target, RESOURCE_HEALTH, 0); // Unfreeze resets health
 +              frag_target.freezetag_frozen_timeout = -2; // freeze on respawn
 +              return true;
 +      }
 +
 +      if(STAT(FROZEN, frag_target))
 +              return true;
 +
 +      freezetag_Freeze(frag_target, frag_attacker);
 +      freezetag_LastPlayerForTeam_Notify(frag_target);
 +
 +      if(frag_attacker == frag_target || frag_attacker == NULL)
 +      {
 +              if(IS_PLAYER(frag_target))
 +                      Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_FREEZETAG_SELF);
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_SELF, frag_target.netname);
 +      }
 +      else
 +      {
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_FREEZE, frag_target.netname, frag_attacker.netname);
 +      }
 +
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ft, PlayerSpawn)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      if(player.freezetag_frozen_timeout == -1) // if PlayerSpawn is called by reset_map_players
 +              return true; // do nothing, round is starting right now
 +
 +      if(player.freezetag_frozen_timeout == -2) // player was dead
 +      {
 +              freezetag_Freeze(player, NULL);
 +              return true;
 +      }
 +
 +      freezetag_count_alive_players();
 +
 +      if(round_handler_IsActive())
 +      if(round_handler_IsRoundStarted())
 +      {
 +              Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_SPAWN_LATE);
 +              freezetag_Freeze(player, NULL);
 +      }
 +
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ft, reset_map_players)
 +{
 +      FOREACH_CLIENT(IS_PLAYER(it), {
 +              CS(it).killcount = 0;
 +              it.freezetag_frozen_timeout = -1;
 +              PutClientInServer(it);
 +              it.freezetag_frozen_timeout = 0;
 +      });
 +      freezetag_count_alive_players();
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ft, GiveFragsForKill, CBC_ORDER_FIRST)
 +{
 +      M_ARGV(2, float) = 0; // no frags counted in Freeze Tag
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ft, Unfreeze)
 +{
 +      entity targ = M_ARGV(0, entity);
 +      targ.freezetag_frozen_time = 0;
 +      targ.freezetag_frozen_timeout = 0;
 +
 +      freezetag_count_alive_players();
 +}
 +
 +MUTATOR_HOOKFUNCTION(ft, PlayerPreThink, CBC_ORDER_FIRST)
 +{
 +      if(game_stopped)
 +              return true;
 +
 +      if(round_handler_IsActive())
 +      if(!round_handler_IsRoundStarted())
 +              return true;
 +
 +      int n;
 +      entity o = NULL;
 +      entity player = M_ARGV(0, entity);
 +      //if(STAT(FROZEN, player))
 +      //if(player.freezetag_frozen_timeout > 0 && time < player.freezetag_frozen_timeout)
 +              //player.iceblock.alpha = ICE_MIN_ALPHA + (ICE_MAX_ALPHA - ICE_MIN_ALPHA) * (player.freezetag_frozen_timeout - time) / (player.freezetag_frozen_timeout - player.freezetag_frozen_time);
 +
 +      if(player.freezetag_frozen_timeout > 0 && time >= player.freezetag_frozen_timeout)
 +              n = -1;
 +      else
 +      {
 +              vector revive_extra_size = '1 1 1' * autocvar_g_freezetag_revive_extra_size;
 +              n = 0;
 +              FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
 +                      if(STAT(FROZEN, it) == 0)
 +                      if(!IS_DEAD(it))
 +                      if(SAME_TEAM(it, player))
 +                      if(boxesoverlap(player.absmin - revive_extra_size, player.absmax + revive_extra_size, it.absmin, it.absmax))
 +                      {
 +                              if(!o)
 +                                      o = it;
 +                              if(STAT(FROZEN, player) == 1)
 +                                      it.reviving = true;
 +                              ++n;
 +                      }
 +              });
 +
 +      }
 +
 +      if(n && STAT(FROZEN, player) == 1) // OK, there is at least one teammate reviving us
 +      {
 +              STAT(REVIVE_PROGRESS, player) = bound(0, STAT(REVIVE_PROGRESS, player) + frametime * max(1/60, autocvar_g_freezetag_revive_speed), 1);
 +              SetResourceAmountExplicit(player, RESOURCE_HEALTH, max(1, STAT(REVIVE_PROGRESS, player) * ((warmup_stage) ? warmup_start_health : start_health)));
 +
 +              if(STAT(REVIVE_PROGRESS, player) >= 1)
 +              {
 +                      Unfreeze(player);
 +                      freezetag_count_alive_players();
 +
 +                      if(n == -1)
 +                      {
 +                              Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_AUTO_REVIVED, autocvar_g_freezetag_frozen_maxtime);
 +                              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_AUTO_REVIVED, player.netname, autocvar_g_freezetag_frozen_maxtime);
 +                              return true;
 +                      }
 +
 +                      // EVERY team mate nearby gets a point (even if multiple!)
 +                      FOREACH_CLIENT(IS_PLAYER(it) && it.reviving, {
 +                              GameRules_scoring_add(it, FREEZETAG_REVIVALS, +1);
 +                              GameRules_scoring_add(it, SCORE, +1);
 +                              nades_GiveBonus(it,autocvar_g_nades_bonus_score_low);
 +                      });
 +
 +                      Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_REVIVED, o.netname);
 +                      Send_Notification(NOTIF_ONE, o, MSG_CENTER, CENTER_FREEZETAG_REVIVE, player.netname);
 +                      Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED, player.netname, o.netname);
 +              }
 +
 +              FOREACH_CLIENT(IS_PLAYER(it) && it.reviving, {
 +                      STAT(REVIVE_PROGRESS, it) = STAT(REVIVE_PROGRESS, player);
 +                      it.reviving = false;
 +              });
 +      }
 +      else if(!n && STAT(FROZEN, player) == 1) // only if no teammate is nearby will we reset
 +      {
 +              STAT(REVIVE_PROGRESS, player) = bound(0, STAT(REVIVE_PROGRESS, player) - frametime * autocvar_g_freezetag_revive_clearspeed, 1);
 +              SetResourceAmountExplicit(player, RESOURCE_HEALTH, max(1, STAT(REVIVE_PROGRESS, player) * ((warmup_stage) ? warmup_start_health : start_health)));
 +      }
 +      else if(!n && !STAT(FROZEN, player))
 +      {
 +              STAT(REVIVE_PROGRESS, player) = 0; // thawing nobody
 +      }
 +
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ft, SetStartItems)
 +{
 +      start_items &= ~IT_UNLIMITED_AMMO;
 +      //start_health       = warmup_start_health       = cvar("g_lms_start_health");
 +      //start_armorvalue   = warmup_start_armorvalue   = cvar("g_lms_start_armor");
 +      start_ammo_shells  = warmup_start_ammo_shells  = cvar("g_lms_start_ammo_shells");
 +      start_ammo_nails   = warmup_start_ammo_nails   = cvar("g_lms_start_ammo_nails");
 +      start_ammo_rockets = warmup_start_ammo_rockets = cvar("g_lms_start_ammo_rockets");
 +      start_ammo_cells   = warmup_start_ammo_cells   = cvar("g_lms_start_ammo_cells");
 +      start_ammo_plasma  = warmup_start_ammo_plasma  = cvar("g_lms_start_ammo_plasma");
 +      start_ammo_fuel    = warmup_start_ammo_fuel    = cvar("g_lms_start_ammo_fuel");
 +}
 +
 +MUTATOR_HOOKFUNCTION(ft, HavocBot_ChooseRole)
 +{
 +      entity bot = M_ARGV(0, entity);
 +
 +      if (!IS_DEAD(bot))
 +      {
 +              if (random() < 0.5)
 +                      bot.havocbot_role = havocbot_role_ft_freeing;
 +              else
 +                      bot.havocbot_role = havocbot_role_ft_offense;
 +      }
 +
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ft, TeamBalance_CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
 +{
 +      M_ARGV(0, float) = freezetag_teams;
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ft, SetWeaponArena)
 +{
 +      // most weapons arena
 +      if(M_ARGV(0, string) == "0" || M_ARGV(0, string) == "")
 +              M_ARGV(0, string) = "most";
 +}
 +
 +MUTATOR_HOOKFUNCTION(ft, FragCenterMessage)
 +{
 +      entity frag_attacker = M_ARGV(0, entity);
 +      entity frag_target = M_ARGV(1, entity);
 +      //float frag_deathtype = M_ARGV(2, float);
 +      int kill_count_to_attacker = M_ARGV(3, int);
 +      int kill_count_to_target = M_ARGV(4, int);
 +
 +      if(STAT(FROZEN, frag_target))
 +              return; // target was already frozen, so this is just pushing them off the cliff
 +
 +      Send_Notification(NOTIF_ONE, frag_attacker, MSG_CHOICE, CHOICE_FRAG_FREEZE, frag_target.netname, kill_count_to_attacker, (IS_BOT_CLIENT(frag_target) ? -1 : CS(frag_target).ping));
 +      Send_Notification(NOTIF_ONE, frag_target, MSG_CHOICE, CHOICE_FRAGGED_FREEZE, frag_attacker.netname, kill_count_to_target,
 +              GetResourceAmount(frag_attacker, RESOURCE_HEALTH), GetResourceAmount(frag_attacker, RESOURCE_ARMOR), (IS_BOT_CLIENT(frag_attacker) ? -1 : CS(frag_attacker).ping));
 +
 +      return true;
 +}
 +
 +void freezetag_Initialize()
 +{
 +      freezetag_teams = autocvar_g_freezetag_teams_override;
 +      if(freezetag_teams < 2)
 +              freezetag_teams = cvar("g_freezetag_teams"); // read the cvar directly as it gets written earlier in the same frame
 +
 +      freezetag_teams = BITS(bound(2, freezetag_teams, 4));
 +      GameRules_scoring(freezetag_teams, SFL_SORT_PRIO_PRIMARY, SFL_SORT_PRIO_PRIMARY, {
 +              field(SP_FREEZETAG_REVIVALS, "revivals", 0);
 +      });
 +
 +      round_handler_Spawn(freezetag_CheckTeams, freezetag_CheckWinner, func_null);
 +      round_handler_Init(5, autocvar_g_freezetag_warmup, autocvar_g_freezetag_round_timelimit);
 +
 +      EliminatedPlayers_Init(freezetag_isEliminated);
 +}
index 133050b0005a483caefb27bc281dd9e55a84d9f3,0000000000000000000000000000000000000000..88ef162f6173dd356d047449a63e1709ce4facf9
mode 100644,000000..100644
--- /dev/null
@@@ -1,472 -1,0 +1,484 @@@
-       if(game_stopped) return;
-       if(!this) return;
 +#include "sv_keepaway.qh"
 +
 +#include <common/effects/all.qh>
 +
 +.entity ballcarried;
 +
 +int autocvar_g_keepaway_ballcarrier_effects;
 +float autocvar_g_keepaway_ballcarrier_damage;
 +float autocvar_g_keepaway_ballcarrier_force;
 +float autocvar_g_keepaway_ballcarrier_highspeed;
 +float autocvar_g_keepaway_ballcarrier_selfdamage;
 +float autocvar_g_keepaway_ballcarrier_selfforce;
 +float autocvar_g_keepaway_noncarrier_damage;
 +float autocvar_g_keepaway_noncarrier_force;
 +float autocvar_g_keepaway_noncarrier_selfdamage;
 +float autocvar_g_keepaway_noncarrier_selfforce;
 +bool autocvar_g_keepaway_noncarrier_warn;
 +int autocvar_g_keepaway_score_bckill;
 +int autocvar_g_keepaway_score_killac;
 +int autocvar_g_keepaway_score_timepoints;
 +float autocvar_g_keepaway_score_timeinterval;
 +float autocvar_g_keepawayball_damageforcescale;
 +int autocvar_g_keepawayball_effects;
 +float autocvar_g_keepawayball_respawntime;
 +int autocvar_g_keepawayball_trail_color;
 +
 +bool ka_ballcarrier_waypointsprite_visible_for_player(entity this, entity player, entity view) // runs on waypoints which are attached to ballcarriers, updates once per frame
 +{
 +      if(view.ballcarried)
 +              if(IS_SPEC(player))
 +                      return false; // we don't want spectators of the ballcarrier to see the attached waypoint on the top of their screen
 +
 +      // TODO: Make the ballcarrier lack a waypointsprite whenever they have the invisibility powerup
 +
 +      return true;
 +}
 +
 +void ka_EventLog(string mode, entity actor) // use an alias for easy changing and quick editing later
 +{
 +      if(autocvar_sv_eventlog)
 +              GameLogEcho(strcat(":ka:", mode, ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
 +}
 +
 +void ka_TouchEvent(entity this, entity toucher);
 +void ka_RespawnBall(entity this) // runs whenever the ball needs to be relocated
 +{
 +      if(game_stopped) return;
 +      vector oldballorigin = this.origin;
 +
 +      if(!MoveToRandomMapLocation(this, DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_PLAYERCLIP, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_BODY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 10, 1024, 256))
 +      {
 +              entity spot = SelectSpawnPoint(this, true);
 +              setorigin(this, spot.origin);
 +              this.angles = spot.angles;
 +      }
 +
 +      makevectors(this.angles);
 +      set_movetype(this, MOVETYPE_BOUNCE);
 +      this.velocity = '0 0 200';
 +      this.angles = '0 0 0';
 +      this.effects = autocvar_g_keepawayball_effects;
 +      settouch(this, ka_TouchEvent);
 +      setthink(this, ka_RespawnBall);
 +      this.nextthink = time + autocvar_g_keepawayball_respawntime;
 +      navigation_dynamicgoal_set(this);
 +
 +      Send_Effect(EFFECT_ELECTRO_COMBO, oldballorigin, '0 0 0', 1);
 +      Send_Effect(EFFECT_ELECTRO_COMBO, this.origin, '0 0 0', 1);
 +
 +      WaypointSprite_Spawn(WP_KaBall, 0, 0, this, '0 0 64', NULL, this.team, this, waypointsprite_attachedforcarrier, false, RADARICON_FLAGCARRIER);
 +      WaypointSprite_Ping(this.waypointsprite_attachedforcarrier);
 +
 +      sound(this, CH_TRIGGER, SND_KA_RESPAWN, VOL_BASE, ATTEN_NONE); // ATTEN_NONE (it's a sound intended to be heard anywhere)
 +}
 +
 +void ka_TimeScoring(entity this)
 +{
 +      if(this.owner.ballcarried)
 +      { // add points for holding the ball after a certain amount of time
 +              if(autocvar_g_keepaway_score_timepoints)
 +                      GameRules_scoring_add(this.owner, SCORE, autocvar_g_keepaway_score_timepoints);
 +
 +              GameRules_scoring_add(this.owner, KEEPAWAY_BCTIME, (autocvar_g_keepaway_score_timeinterval / 1)); // interval is divided by 1 so that time always shows "seconds"
 +              this.nextthink = time + autocvar_g_keepaway_score_timeinterval;
 +      }
 +}
 +
 +void ka_TouchEvent(entity this, entity toucher) // runs any time that the ball comes in contact with something
 +{
-       entity e = ball.owner; ball.owner = NULL;
-       e.ballcarried = NULL;
-       GameRules_scoring_vip(e, false);
++      if (!this || game_stopped)
++              return;
++
 +      if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_NOIMPACT)
 +      { // The ball fell off the map, respawn it since players can't get to it
 +              ka_RespawnBall(this);
 +              return;
 +      }
 +      if(IS_DEAD(toucher)) { return; }
 +      if(STAT(FROZEN, toucher)) { return; }
 +      if (!IS_PLAYER(toucher))
 +      {  // The ball just touched an object, most likely the world
 +              Send_Effect(EFFECT_BALL_SPARKS, this.origin, '0 0 0', 1);
 +              sound(this, CH_TRIGGER, SND_KA_TOUCH, VOL_BASE, ATTEN_NORM);
 +              return;
 +      }
 +      else if(this.wait > time) { return; }
 +
 +      // attach the ball to the player
 +      this.owner = toucher;
 +      toucher.ballcarried = this;
 +      GameRules_scoring_vip(toucher, true);
 +      setattachment(this, toucher, "");
 +      setorigin(this, '0 0 0');
 +
 +      // make the ball invisible/unable to do anything/set up time scoring
 +      this.velocity = '0 0 0';
 +      set_movetype(this, MOVETYPE_NONE);
 +      this.effects |= EF_NODRAW;
 +      settouch(this, func_null);
 +      setthink(this, ka_TimeScoring);
 +      this.nextthink = time + autocvar_g_keepaway_score_timeinterval;
 +      this.takedamage = DAMAGE_NO;
 +      navigation_dynamicgoal_unset(this);
 +
 +      // apply effects to player
 +      toucher.glow_color = autocvar_g_keepawayball_trail_color;
 +      toucher.glow_trail = true;
 +      toucher.effects |= autocvar_g_keepaway_ballcarrier_effects;
 +
 +      // messages and sounds
 +      ka_EventLog("pickup", toucher);
 +      Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_KEEPAWAY_PICKUP, toucher.netname);
 +      Send_Notification(NOTIF_ALL_EXCEPT, toucher, MSG_CENTER, CENTER_KEEPAWAY_PICKUP, toucher.netname);
 +      Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_KEEPAWAY_PICKUP_SELF);
 +      sound(this.owner, CH_TRIGGER, SND_KA_PICKEDUP, VOL_BASE, ATTEN_NONE); // ATTEN_NONE (it's a sound intended to be heard anywhere)
 +
 +      // scoring
 +      GameRules_scoring_add(toucher, KEEPAWAY_PICKUPS, 1);
 +
 +      // waypoints
 +      WaypointSprite_AttachCarrier(WP_KaBallCarrier, toucher, RADARICON_FLAGCARRIER);
 +      toucher.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = ka_ballcarrier_waypointsprite_visible_for_player;
 +      WaypointSprite_UpdateRule(toucher.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
 +      WaypointSprite_Ping(toucher.waypointsprite_attachedforcarrier);
 +      WaypointSprite_Kill(this.waypointsprite_attachedforcarrier);
 +}
 +
++void ka_PlayerReset(entity plyr)
++{
++      plyr.ballcarried = NULL;
++      GameRules_scoring_vip(plyr, false);
++      WaypointSprite_Kill(plyr.waypointsprite_attachedforcarrier);
++
++      // reset the player effects
++      plyr.glow_trail = false;
++      plyr.effects &= ~autocvar_g_keepaway_ballcarrier_effects;
++}
++
 +void ka_DropEvent(entity plyr) // runs any time that a player is supposed to lose the ball
 +{
 +      entity ball;
 +      ball = plyr.ballcarried;
 +
 +      if(!ball) { return; }
 +
 +      // reset the ball
 +      setattachment(ball, NULL, "");
 +      set_movetype(ball, MOVETYPE_BOUNCE);
 +      ball.wait = time + 1;
 +      settouch(ball, ka_TouchEvent);
 +      setthink(ball, ka_RespawnBall);
 +      ball.nextthink = time + autocvar_g_keepawayball_respawntime;
 +      ball.takedamage = DAMAGE_YES;
 +      ball.effects &= ~EF_NODRAW;
 +      setorigin(ball, plyr.origin + '0 0 10');
 +      ball.velocity = '0 0 200' + '0 100 0'*crandom() + '100 0 0'*crandom();
-       // reset the player effects
-       plyr.glow_trail = false;
-       plyr.effects &= ~autocvar_g_keepaway_ballcarrier_effects;
++      ball.owner = NULL;
 +      navigation_dynamicgoal_set(ball);
 +
-       // scoring
-       // GameRules_scoring_add(plyr, KEEPAWAY_DROPS, 1); Not anymore, this is 100% the same as pickups and is useless.
 +      // messages and sounds
 +      ka_EventLog("dropped", plyr);
 +      Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_KEEPAWAY_DROPPED, plyr.netname);
 +      Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_KEEPAWAY_DROPPED, plyr.netname);
 +      sound(NULL, CH_TRIGGER, SND_KA_DROPPED, VOL_BASE, ATTEN_NONE); // ATTEN_NONE (it's a sound intended to be heard anywhere)
 +
-       WaypointSprite_Kill(plyr.waypointsprite_attachedforcarrier);
 +      // waypoints
 +      WaypointSprite_Spawn(WP_KaBall, 0, 0, ball, '0 0 64', NULL, ball.team, ball, waypointsprite_attachedforcarrier, false, RADARICON_FLAGCARRIER);
 +      WaypointSprite_UpdateRule(ball.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
 +      WaypointSprite_Ping(ball.waypointsprite_attachedforcarrier);
- /** used to clear the ballcarrier whenever the match switches from warmup to normal */
- void ka_Reset(entity this)
++
++      ka_PlayerReset(plyr);
++}
++
++.bool pushable;
++
++MODEL(KA_BALL, "models/orbs/orbblue.md3");
++
++void ka_RemoveBall()
++{
++      entity plyr = ka_ball.owner;
++      if (plyr) // it was attached
++              ka_PlayerReset(plyr);
++      else
++              WaypointSprite_DetachCarrier(ka_ball);
++      delete(ka_ball);
++      ka_ball = NULL;
 +}
 +
-       if((this.owner) && (IS_PLAYER(this.owner)))
-               ka_DropEvent(this.owner);
++void ka_SpawnBall()
 +{
-               setthink(this, ka_RespawnBall);
-               settouch(this, func_null);
-               this.nextthink = game_starttime;
++      entity e = new(keepawayball);
++      setmodel(e, MDL_KA_BALL);
++      setsize(e, '-16 -16 -20', '16 16 20'); // 20 20 20 was too big, player is only 16 16 24... gotta cheat with the Z (20) axis so that the particle isn't cut off
++      e.damageforcescale = autocvar_g_keepawayball_damageforcescale;
++      e.takedamage = DAMAGE_YES;
++      e.solid = SOLID_TRIGGER;
++      set_movetype(e, MOVETYPE_BOUNCE);
++      e.glow_color = autocvar_g_keepawayball_trail_color;
++      e.glow_trail = true;
++      e.flags = FL_ITEM;
++      IL_PUSH(g_items, e);
++      e.pushable = true;
++      settouch(e, ka_TouchEvent);
++      e.owner = NULL;
++      ka_ball = e;
++      navigation_dynamicgoal_init(ka_ball, false);
 +
++      InitializeEntity(e, ka_RespawnBall, INITPRIO_SETLOCATION); // is this the right priority? Neh, I have no idea.. Well-- it works! So.
++}
++
++void ka_Handler_CheckBall(entity this)
++{
 +      if(time < game_starttime)
 +      {
-               ka_RespawnBall(this);
++              if (ka_ball)
++                      ka_RemoveBall();
 +      }
 +      else
- .bool pushable;
- // ==============
- // Initialization
- // ==============
- MODEL(KA_BALL, "models/orbs/orbblue.md3");
- void ka_SpawnBall() // loads various values for the ball, runs only once at start of match
- {
-       entity e = new(keepawayball);
-       setmodel(e, MDL_KA_BALL);
-       setsize(e, '-16 -16 -20', '16 16 20'); // 20 20 20 was too big, player is only 16 16 24... gotta cheat with the Z (20) axis so that the particle isn't cut off
-       e.damageforcescale = autocvar_g_keepawayball_damageforcescale;
-       e.takedamage = DAMAGE_YES;
-       e.solid = SOLID_TRIGGER;
-       set_movetype(e, MOVETYPE_BOUNCE);
-       e.glow_color = autocvar_g_keepawayball_trail_color;
-       e.glow_trail = true;
-       e.flags = FL_ITEM;
-       IL_PUSH(g_items, e);
-       e.pushable = true;
-       e.reset = ka_Reset;
-       settouch(e, ka_TouchEvent);
-       e.owner = NULL;
-       ka_ball = e;
-       navigation_dynamicgoal_init(ka_ball, false);
-       InitializeEntity(e, ka_RespawnBall, INITPRIO_SETLOCATION); // is this the right priority? Neh, I have no idea.. Well-- it works! So.
- }
- void ka_Initialize() // run at the start of a match, initiates game mode
- {
-       ka_SpawnBall();
- }
++      {
++              if (!ka_ball)
++                      ka_SpawnBall();
++      }
++
++      this.nextthink = time;
++}
++
++void ka_Initialize() // run at the start of a match, initiates game mode
++{
++      ka_Handler = new(ka_Handler);
++      setthink(ka_Handler, ka_Handler_CheckBall);
++      ka_Handler.nextthink = time;
 +}
 +
 +
 +// ================
 +// Bot player logic
 +// ================
 +
 +void havocbot_goalrating_ball(entity this, float ratingscale, vector org)
 +{
 +      float t;
 +      entity ball_owner;
 +      ball_owner = ka_ball.owner;
 +
 +      if (ball_owner == this)
 +              return;
 +
 +      // If ball is carried by player then hunt them down.
 +      if (ball_owner)
 +      {
 +              t = (GetResourceAmount(this, RESOURCE_HEALTH) + GetResourceAmount(this, RESOURCE_ARMOR)) / (GetResourceAmount(ball_owner, RESOURCE_HEALTH) + GetResourceAmount(ball_owner, RESOURCE_ARMOR));
 +              navigation_routerating(this, ball_owner, t * ratingscale, 2000);
 +      }
 +      else // Ball has been dropped so collect.
 +              navigation_routerating(this, ka_ball, ratingscale, 2000);
 +}
 +
 +void havocbot_role_ka_carrier(entity this)
 +{
 +      if (IS_DEAD(this))
 +              return;
 +
 +      if (navigation_goalrating_timeout(this))
 +      {
 +              navigation_goalrating_start(this);
 +              havocbot_goalrating_items(this, 10000, this.origin, 10000);
 +              havocbot_goalrating_enemyplayers(this, 20000, this.origin, 10000);
 +              havocbot_goalrating_waypoints(this, 1, this.origin, 3000);
 +              navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
 +      }
 +
 +      if (!this.ballcarried)
 +      {
 +              this.havocbot_role = havocbot_role_ka_collector;
 +              navigation_goalrating_timeout_expire(this, 2);
 +      }
 +}
 +
 +void havocbot_role_ka_collector(entity this)
 +{
 +      if (IS_DEAD(this))
 +              return;
 +
 +      if (navigation_goalrating_timeout(this))
 +      {
 +              navigation_goalrating_start(this);
 +              havocbot_goalrating_items(this, 10000, this.origin, 10000);
 +              havocbot_goalrating_enemyplayers(this, 1000, this.origin, 10000);
 +              havocbot_goalrating_ball(this, 20000, this.origin);
 +              navigation_goalrating_end(this);
 +
 +              navigation_goalrating_timeout_set(this);
 +      }
 +
 +      if (this.ballcarried)
 +      {
 +              this.havocbot_role = havocbot_role_ka_carrier;
 +              navigation_goalrating_timeout_expire(this, 2);
 +      }
 +}
 +
 +
 +// ==============
 +// Hook Functions
 +// ==============
 +
 +MUTATOR_HOOKFUNCTION(ka, PlayerDies)
 +{
 +      entity frag_attacker = M_ARGV(1, entity);
 +      entity frag_target = M_ARGV(2, entity);
 +
 +      if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)))
 +      {
 +              if(frag_target.ballcarried) { // add to amount of times killing carrier
 +                      GameRules_scoring_add(frag_attacker, KEEPAWAY_CARRIERKILLS, 1);
 +                      if(autocvar_g_keepaway_score_bckill) // add bckills to the score
 +                              GameRules_scoring_add(frag_attacker, SCORE, autocvar_g_keepaway_score_bckill);
 +              }
 +              else if(!frag_attacker.ballcarried)
 +                      if(autocvar_g_keepaway_noncarrier_warn)
 +                              Send_Notification(NOTIF_ONE_ONLY, frag_attacker, MSG_CENTER, CENTER_KEEPAWAY_WARN);
 +
 +              if(frag_attacker.ballcarried) // add to amount of kills while ballcarrier
 +                      GameRules_scoring_add(frag_attacker, SCORE, autocvar_g_keepaway_score_killac);
 +      }
 +
 +      if(frag_target.ballcarried) { ka_DropEvent(frag_target); } // a player with the ball has died, drop it
 +}
 +
 +MUTATOR_HOOKFUNCTION(ka, GiveFragsForKill)
 +{
 +      M_ARGV(2, float) = 0; // no frags counted in keepaway
 +      return true; // you deceptive little bugger ;3 This needs to be true in order for this function to even count.
 +}
 +
 +MUTATOR_HOOKFUNCTION(ka, PlayerPreThink)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      // clear the item used for the ball in keepaway
 +      player.items &= ~IT_KEY1;
 +
 +      // if the player has the ball, make sure they have the item for it (Used for HUD primarily)
 +      if(player.ballcarried)
 +              player.items |= IT_KEY1;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ka, PlayerUseKey)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      if(MUTATOR_RETURNVALUE == 0)
 +      if(player.ballcarried)
 +      {
 +              ka_DropEvent(player);
 +              return true;
 +      }
 +}
 +
 +MUTATOR_HOOKFUNCTION(ka, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
 +{
 +      entity frag_attacker = M_ARGV(1, entity);
 +      entity frag_target = M_ARGV(2, entity);
 +      float frag_damage = M_ARGV(4, float);
 +      vector frag_force = M_ARGV(6, vector);
 +
 +      if(frag_attacker.ballcarried) // if the attacker is a ballcarrier
 +      {
 +              if(frag_target == frag_attacker) // damage done to yourself
 +              {
 +                      frag_damage *= autocvar_g_keepaway_ballcarrier_selfdamage;
 +                      frag_force *= autocvar_g_keepaway_ballcarrier_selfforce;
 +              }
 +              else // damage done to noncarriers
 +              {
 +                      frag_damage *= autocvar_g_keepaway_ballcarrier_damage;
 +                      frag_force *= autocvar_g_keepaway_ballcarrier_force;
 +              }
 +      }
 +      else if (!frag_target.ballcarried) // if the target is a noncarrier
 +      {
 +              if(frag_target == frag_attacker) // damage done to yourself
 +              {
 +                      frag_damage *= autocvar_g_keepaway_noncarrier_selfdamage;
 +                      frag_force *= autocvar_g_keepaway_noncarrier_selfforce;
 +              }
 +              else // damage done to other noncarriers
 +              {
 +                      frag_damage *= autocvar_g_keepaway_noncarrier_damage;
 +                      frag_force *= autocvar_g_keepaway_noncarrier_force;
 +              }
 +      }
 +
 +      M_ARGV(4, float) = frag_damage;
 +      M_ARGV(6, vector) = frag_force;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ka, ClientDisconnect)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      if(player.ballcarried) { ka_DropEvent(player); } // a player with the ball has left the match, drop it
 +}
 +
 +MUTATOR_HOOKFUNCTION(ka, MakePlayerObserver)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      if(player.ballcarried) { ka_DropEvent(player); } // a player with the ball has left the match, drop it
 +}
 +
 +MUTATOR_HOOKFUNCTION(ka, PlayerPowerups)
 +{
 +      entity player = M_ARGV(0, entity);
 +
 +      // In the future this hook is supposed to allow me to do some extra stuff with waypointsprites and invisibility powerup
 +      // So bare with me until I can fix a certain bug with ka_ballcarrier_waypointsprite_visible_for_player()
 +
 +      player.effects &= ~autocvar_g_keepaway_ballcarrier_effects;
 +
 +      if(player.ballcarried)
 +              player.effects |= autocvar_g_keepaway_ballcarrier_effects;
 +}
 +
 +
 +MUTATOR_HOOKFUNCTION(ka, PlayerPhysics_UpdateStats)
 +{
 +      entity player = M_ARGV(0, entity);
 +      // these automatically reset, no need to worry
 +
 +      if(player.ballcarried)
 +              STAT(MOVEVARS_HIGHSPEED, player) *= autocvar_g_keepaway_ballcarrier_highspeed;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ka, BotShouldAttack)
 +{
 +      entity bot = M_ARGV(0, entity);
 +      entity targ = M_ARGV(1, entity);
 +
 +      // if neither player has ball then don't attack unless the ball is on the ground
 +      if(!targ.ballcarried && !bot.ballcarried && ka_ball.owner)
 +              return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ka, HavocBot_ChooseRole)
 +{
 +      entity bot = M_ARGV(0, entity);
 +
 +      if (bot.ballcarried)
 +              bot.havocbot_role = havocbot_role_ka_carrier;
 +      else
 +              bot.havocbot_role = havocbot_role_ka_collector;
 +      return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(ka, DropSpecialItems)
 +{
 +      entity frag_target = M_ARGV(0, entity);
 +
 +      if(frag_target.ballcarried)
 +              ka_DropEvent(frag_target);
 +}
index 83208bf37ce8d945e9726a0e9d53508e25d382ba,0000000000000000000000000000000000000000..3c14c89af180942f3d506d10471050521912c496
mode 100644,000000..100644
--- /dev/null
@@@ -1,29 -1,0 +1,30 @@@
 +#pragma once
 +
 +#include <common/mutators/base.qh>
 +#include <common/scores.qh>
 +void ka_Initialize();
 +
 +REGISTER_MUTATOR(ka, false)
 +{
 +    MUTATOR_STATIC();
 +      MUTATOR_ONADD
 +      {
 +          GameRules_scoring(0, SFL_SORT_PRIO_PRIMARY, 0, {
 +            field(SP_KEEPAWAY_PICKUPS, "pickups", 0);
 +            field(SP_KEEPAWAY_CARRIERKILLS, "bckills", 0);
 +            field(SP_KEEPAWAY_BCTIME, "bctime", SFL_SORT_PRIO_SECONDARY);
 +        });
 +
 +              ka_Initialize();
 +      }
 +      return false;
 +}
 +
 +
 +entity ka_ball;
++entity ka_Handler;
 +
 +void(entity this) havocbot_role_ka_carrier;
 +void(entity this) havocbot_role_ka_collector;
 +
 +void ka_DropEvent(entity plyr);