4ŞU   Ź   nut scripts/vscripts interneted_relentlesswitch 5p—  ˙¨  ¨I  ˙˙director_base_addon —ę2`  ˙l  <   ˙˙  txt   addoninfo o÷\y  ˙    l  ˙˙    "AddonInfo"
{
     addonSteamAppID         550 //L4D2 Don't Change
     addontitle              "Relentless Witch" //Name for Addon Menu
     addonversion            1
     addonauthor             "Interneted"
     addonauthorSteamID      "AUTHOT_STEAM_ID" //Optional. the number of your steam profile URL
     addonDescription        "Relentless Witch"
}IncludeScript("interneted_relentlesswitch", getroottable());printl("========================================");
printl("Executing Relentless Witch by Interneted");
printl("========================================");

/**
 * I love crashing people's game, that's my specialty.
 * But seriously I only have 1 final resort.
 * It's to comment out TakeDamage function when the witch starts to attack on RWAttack function.
 * When the witch starts retargetting, it called infected_hurt like 30 times or more.
 * That's most likely what causes the crash, if it is, sorry for causing trouble lol.
 * Special thanks to the nightmare enthusiast and vscripters who gave me some insights.
 */

Interneted_RelentlessWitch <-
{
    Settings =
    {
        // if witch cannot find any target within this range, nothing will happen, -1 for infinite range.
        maxRange = 1200.0,
        // set it to true if the witch needs to kill its victim first before retargetting.
        killIncap = false,
        // how times can the witch retarget, -1 for infinite times.
        retargetCount = -1,
        // disabling rng will make the witch retarget the closest survivor.
        rng = true
    }

    function ParseConfigFile()
	{
		local tData;
        local path = "interneted_relentlesswitch/settings.cfg";
        // thanks tomaz.
        local key_order = ["maxRange", "killIncap", "retargetCount", "rng"];
		local function SerializeSettings()
		{
            local sData = "{";
			foreach (i, val in key_order)
			{
                if(val in Settings)
                {
                    sData = sData + "\n\t" + val + " = " + Settings[val];
                }
			}
			sData = sData + "\n}";
			StringToFile(path, sData);
		}

        // Take the user's input and update the Settings table.
		if(tData = FileToString(path))
		{
			try
            {
				tData = compilestring("return " + tData)();
				local invalid = false;

				foreach(key, val in Settings)
				{
					if(key in tData)
					{
                        local input_type = typeof(tData[key])
                        local type = typeof(Settings[key])
                        if(type == "bool")
                        {
                            if(input_type == "bool") { Settings[key] = tData[key]; }
                            else if(!invalid) { invalid = true; }
                        }
                        else if(type == "integer")
                        {
                            if(input_type == "integer") { Settings[key] = tData[key]; }
                            else if(input_type == "float") { Settings[key] = tData[key].tointeger(); invalid = true; }
                            else if(!invalid) { invalid = true; }
                        }
                        else if(type == "float")
                        {
                            if(input_type == "integer" || input_type == "float") { Settings[key] = tData[key]; }
                            else if(!invalid) { invalid = true; }
                        }
					}
					else if (!invalid) { invalid = true; }
				}
				if(invalid) { SerializeSettings(); }
			}
			catch (error) { SerializeSettings(); }
		} else { SerializeSettings(); }
	}

    function NavCheck(witch, survivor)
    {
        // Witch can always target survivors when they are in checkpoint.
        local witch_area = witch.GetLastKnownArea();
        if(witch_area != null)
        {
            if(witch_area.HasSpawnAttributes(1 << 11)) { return true; }
        }
        // If player_left_checkpoint haven't fired for that particular player,
        // no matter the nav they're in, it will always have CHECKPOINT spawn attribute.
        // ? what the hell.
        // This mostly happen when you try to warp survivors using admin system.
        local survivor_area = survivor.GetLastKnownArea();
        if(survivor_area != null)
        {
            if(survivor_area.HasSpawnAttributes(1 << 11)) { return false; }
        }
        return true;
    }

    function GetTargetSurvivor(witch, condition, s = null)
    {
        local array = []
        for(local survivor; survivor = Entities.FindByClassname(survivor, "player");)
        {
            if(survivor == s) { continue; }
            if(!condition(survivor)) { continue; }
            // ! Settings.maxRange
            if(Settings.maxRange >= 0)
            {
                if((witch.GetOrigin() - survivor.GetOrigin()).Length() > Settings.maxRange) { continue; }
            }
            if(NavCheck(witch, survivor))
            {
                array.append(survivor);
            }
        }

        if(array.len() <= 0) { return null; }
        // ! Settings.rng
        if(Settings.rng) { return array[RandomInt(0, array.len() - 1)]; }
        else
        {
            local witch_pos = witch.GetOrigin();

            local target = array[0];
            local distance = (array[0].GetOrigin() - witch_pos).Length();
            foreach(i, survivor in array)
            {
                local length = (survivor.GetOrigin()- witch_pos).Length();
                if(length < distance)
                {
                    distance = length;
                    target = survivor;
                }
            }
            return target;
        }
        return null;
    }

    function RWFindTarget(witch, s = null)
    {
        if(witch.ValidateScriptScope())
        {
            local scope = witch.GetScriptScope();
            local condition = @(survivor) (survivor.IsValid() && survivor.IsSurvivor() && !survivor.IsIncapacitated() && !survivor.IsDying() && !survivor.IsDead());
            local survivor = GetTargetSurvivor(witch, condition, s)
            if(survivor != null)
            {
                scope.interneted_relentwitch_retarget <- survivor;
                RWReset(witch); return;
            }
            // Cannot find target, find another random alive survivor.
            condition = @(survivor) (survivor.IsValid() && survivor.IsSurvivor() && !survivor.IsDying() && !survivor.IsDead())
            survivor = GetTargetSurvivor(witch, condition);
            if(survivor != null)
            {
                scope.interneted_relentwitch_retarget <- survivor;
                RWReset(witch); return;
            }
            scope.interneted_relentwitch_retarget <- null;
        }
    }

    function RWReset(witch)
    {
        // I don't like using TakeDamage, fake-bile and fire damage.
        // I want the scoreboard to display accurate information.
        if(witch != null)
        {
            if(witch.ValidateScriptScope())
            {
                NetProps.SetPropFloat(witch, "m_rage", 0.0);
                NetProps.SetPropFloat(witch, "m_wanderrage", 0.0);
                CommandABot({ bot = witch, cmd = 3 })
                witch.GetScriptScope().interneted_relentwitch_target <- null;
            }
        }
    }

    function RWAttack(witch)
    {
        if(witch != null)
        {
            if(witch.ValidateScriptScope())
            {
                local scope = witch.GetScriptScope();
                if("interneted_relentwitch_retarget" in scope)
                {
                    local survivor = scope.interneted_relentwitch_retarget;
                    if(survivor != null)
                    {
                        NetProps.SetPropFloat(witch, "m_rage", 1.0);
                        NetProps.SetPropFloat(witch, "m_wanderrage", 1.0);
                        // * JUST IN CASE
                        // witch.TakeDamage(0.0, 0, survivor);
                        CommandABot({ bot = witch, cmd = 0, target = survivor });
                        scope.interneted_relentwitch_retarget <- null;
                        scope.interneted_relentwitch_target <- survivor;
                        return;
                    }
                }
                // Cannot retarget to a survivor, find another random alive survivor, 2nd try to make sure the witch is always aggro.
                local condition = @(survivor) (survivor.IsValid() && survivor.IsSurvivor() && !survivor.IsDying() && !survivor.IsDead())
                local survivor = GetTargetSurvivor(witch, condition);
                if(survivor != null)
                {
                    NetProps.SetPropFloat(witch, "m_rage", 1.0);
                    NetProps.SetPropFloat(witch, "m_wanderrage", 1.0);
                    CommandABot({ bot = witch, cmd = 0, target = survivor });
                    scope.interneted_relentwitch_retarget <- null;
                    scope.interneted_relentwitch_target <- survivor;
                }
            }
        }
    }

    function ValidateEventParams(params)
    {
        if(!("userid" in params && "attackerentid" in params)) { return null; }

        local survivor = GetPlayerFromUserID(params.userid);
        local witch = EntIndexToHScript(params.attackerentid);
        if(survivor == null || witch == null) { return null; }
        if(!survivor.IsValid() || !witch.IsValid()) { return null; }
        if(survivor.IsSurvivor() && witch.GetClassname() == "witch" && witch.ValidateScriptScope())
        {
            local scope = witch.GetScriptScope()
            scope.interneted_relentwitch_target <- survivor;
            // You must add a delay for whatever reason, otherwise the game will crash, goddammit.
            // ! Settings.retargetCount
            if("interneted_relentwitch_count" in scope)
            {
                if(scope.interneted_relentwitch_count < 1)
                {
                    scope.interneted_relentwitch_target <- null;
                    return null;
                }
                scope.interneted_relentwitch_count -= 1;
            }
            DoEntFire("!self", "RunScriptCode", "::Interneted_RelentlessWitch.RWFindTarget(self)", 0.0, null, witch);
            DoEntFire("!self", "RunScriptCode", "::Interneted_RelentlessWitch.RWAttack(self)", 0.01, null, witch);
            return witch;
        }
        return null;
    }

    function ReTargetLostWitch(s, w = null)
    {
        for(local witch; witch = Entities.FindByClassname(witch, "witch");)
        {
            if(!witch.IsValid()) { continue; }
            if(witch == w) { continue; }
            if(witch.ValidateScriptScope())
            {
                local scope = witch.GetScriptScope();
                // ! Settings.retargetCount
                if("interneted_relentwitch_count" in scope)
                {
                    if(scope.interneted_relentwitch_count < 1)
                    {
                        scope.interneted_relentwitch_target <- null;
                        continue;
                    }
                    scope.interneted_relentwitch_count -= 1;
                }
                if("interneted_relentwitch_target" in scope && "interneted_relentwitch_startled" in scope)
                {
                    if(scope.interneted_relentwitch_target == s && scope.interneted_relentwitch_startled)
                    {
                        RWFindTarget(witch, s);
                        // Add more delay than just 0.0, for some reason, sometimes, the witch doesn't attack.
                        // Probably because the witch is still resetting her AI.
                        DoEntFire("!self", "RunScriptCode", "::Interneted_RelentlessWitch.RWAttack(self)", 0.01, null, witch);
                    }
                }
            }
        }
    }

    function OnGameEvent_witch_spawn(params)
    {
        if("witchid" in params)
        {
            local witch = EntIndexToHScript(params.witchid);
            if(witch == null) { return; }
            if(!witch.IsValid()) { return; }
            if(witch.GetClassname() == "witch" && witch.ValidateScriptScope())
            {
                local scope = witch.GetScriptScope();
                scope.interneted_relentwitch_startled <- false;
                scope.interneted_relentwitch_target <- null;
                scope.interneted_relentwitch_retarget <- null;
                // ! Settings.retargetCount
                if(Settings.retargetCount >= 0)
                {
                    scope.interneted_relentwitch_count <- Settings.retargetCount;
                }
            }
        }
    }

    // Subsequent startles will not fire this event, after a lot of tests.
    function OnGameEvent_witch_harasser_set(params)
    {
        if("witchid" in params && "userid" in params)
        {
            local witch = EntIndexToHScript(params.witchid);
            local survivor = GetPlayerFromUserID(params.userid);
            if(witch == null || survivor == null) { return; }
            if(!witch.IsValid() || !survivor.IsValid()) { return; }
            if(witch.GetClassname() == "witch" && witch.ValidateScriptScope() && survivor.IsSurvivor())
            {
                local scope = witch.GetScriptScope();
                scope.interneted_relentwitch_startled <- true;
                scope.interneted_relentwitch_target <- survivor;
            }
        }
    }

    // Prevent the witch from attacking a survivor when her AI is currently reset
    // and we are trying to command her to attack another survivor.
    // * This mostly happen when the witch is burning.
    // ? I didn't include shoving the witch, since incaps cannot shove anyway, I think.
    function OnGameEvent_infected_hurt(params)
    {
        if(!("entityid" in params && "amount" in params)) { return; }
        local witch = EntIndexToHScript(params.entityid);
        if(witch == null) { return; }
        if(!witch.IsValid()) { return; }
        if(witch.GetClassname() == "witch" && witch.ValidateScriptScope())
        {
            local scope = witch.GetScriptScope();
            if("interneted_relentwitch_startled" in scope && "interneted_relentwitch_retarget" in scope)
            {
                if(scope.interneted_relentwitch_startled && scope.interneted_relentwitch_retarget != null)
                {
                    // If we're not checking this, the witch will become invincible for some reason
                    // when we are resetting her AI lel.
                    if(params.amount >= witch.GetHealth()) { return; }
                    RWAttack(witch);
                }
            }
        }
    }

    function OnGameEvent_entity_shoved(params)
    {
        if(!("entityid" in params && "attacker" in params)) { return; }
        local witch = EntIndexToHScript(params.entityid);
        local survivor = GetPlayerFromUserID(params.attacker);
        if(witch == null || survivor == null) { return; }
        if(!witch.IsValid() || !survivor.IsValid()) { return; }
        if(witch.GetClassname() == "witch" && survivor.IsSurvivor() && witch.ValidateScriptScope())
        {
            local scope = witch.GetScriptScope();
            if("interneted_relentwitch_startled" in scope && "interneted_relentwitch_retarget" in scope)
            {
                if(scope.interneted_relentwitch_startled && scope.interneted_relentwitch_retarget != null)
                {
                    RWAttack(witch);
                }
            }
        }
    }

    function OnGameEvent_player_incapacitated(params)
    {
        local witch = ValidateEventParams(params)
        ReTargetLostWitch(("userid" in params) ? GetPlayerFromUserID(params.userid) : null, witch);
    }

    function OnGameEvent_player_death(params)
    {
        local witch = ValidateEventParams(params)
        ReTargetLostWitch(("userid" in params) ? GetPlayerFromUserID(params.userid) : null, witch);
    }

    function OnGameEvent_player_ledge_grab(params)
    {
        ReTargetLostWitch(("userid" in params) ? GetPlayerFromUserID(params.userid) : null);
    }

    // * ENTERED CHECKPOINT

    function OnGameEvent_player_entered_checkpoint(params)
    {
        if("userid" in params)
        {
            local survivor = GetPlayerFromUserID(params.userid);
            if(survivor == null) { return; }
            if(!survivor.IsValid()) { return; }
            if(survivor.IsSurvivor())
            {
                ReTargetLostWitch(survivor);
            }
        }
    }

    function OnGameEvent_player_entered_start_area(params)
    {
        if("userid" in params)
        {
            local survivor = GetPlayerFromUserID(params.userid);
            if(survivor == null) { return; }
            if(!survivor.IsValid()) { return; }
            if(survivor.IsSurvivor())
            {
                ReTargetLostWitch(survivor);
            }
        }
    }

    // * IDLE
    function SwitchTargetVar(old, new)
    {
        if(old == null || new == null) { return; }
        if(!old.IsValid() || !new.IsValid()) { return; }
        if(old.IsSurvivor())
        {
            for(local witch; witch = Entities.FindByClassname(witch, "witch");)
            {
                if(!witch.IsValid()) { continue; }
                if(witch.ValidateScriptScope())
                {
                    local scope = witch.GetScriptScope();
                    if("interneted_relentwitch_target" in scope)
                    {
                        if(scope.interneted_relentwitch_target == old)
                        {
                            scope.interneted_relentwitch_target <- new;
                        }
                    }
					// Update 27/05/2025
					if("interneted_relentwitch_retarget" in scope)
					{
						if(scope.interneted_relentwitch_retarget == old)
						{
							scope.interneted_relentwitch_retarget <- new;
						}
					}
                }
            }
        }
    }

    function OnGameEvent_bot_player_replace(params)
    {
        if("player" in params && "bot" in params)
        {
            SwitchTargetVar(GetPlayerFromUserID(params.bot), GetPlayerFromUserID(params.player));
        }
    }

    function OnGameEvent_player_bot_replace(params)
    {
        if("player" in params && "bot" in params)
        {
            SwitchTargetVar(GetPlayerFromUserID(params.player), GetPlayerFromUserID(params.bot));
        }
    }
}

::Interneted_RelentlessWitch.ParseConfigFile();

if(::Interneted_RelentlessWitch.Settings.killIncap)
{
    delete ::Interneted_RelentlessWitch.OnGameEvent_player_incapacitated;
    delete ::Interneted_RelentlessWitch.OnGameEvent_player_ledge_grab;
}

__CollectEventCallbacks(::Interneted_RelentlessWitch, "OnGameEvent_", "GameEventCallbacks", RegisterScriptGameEventListener);