// Script work by fellen (https://steamcommunity.com/profiles/76561198055313095).

const MASK_BLOCKLOS = 0x4041

class SubterraneanAnimosity.BotAI
{
	static VisionMask = MASK_BLOCKLOS|CONTENTS_IGNORE_NODRAW_OPAQUE
	static CosHalfFOV2 = cos(90.0 / 2.0)
	static UpdateFrequency = Convars.GetFloat("nb_update_frequency")

	me = null
	scope = null
	LocomotionInterface = null
	visionrange2 = -1.0
	team = TEAM_UNASSIGNED

	function constructor(handle)
	{
		me = handle
		LocomotionInterface = me.GetLocomotionInterface()
		scope = me.GetScriptScope()
		scope.BotAI <- this

		local visionrange = me.GetMaxVisionRangeOverride()
		if (visionrange > 0.0)
			visionrange2 = visionrange * visionrange
		else
			visionrange2 = 36000000.0
		team = me.GetTeam()
	}

	// Returns true if there is any threat in bot's LOS.
	function ThreatInLOS()
	{
		foreach (player in SubterraneanAnimosity.HUMANS)
		{
			if (!IsThreat(player))
				continue

			if (IsTargetVisible(player))
				return true
		}
		return false
	}

	// Partial replication of IVision::IsAbleToSee().
	function IsTargetVisible(target)
	{
		local delta = target.GetCenter() - me.EyePosition()
		local delta2 = delta.LengthSqr()

		if (visionrange2 < delta2)
			return false

		local cosdiff = delta.Dot(me.EyeAngles().Forward())

		if (cosdiff < 0.0)
			return false

		if (cosdiff * cosdiff > delta2 * CosHalfFOV2)
		{
			local trace =
			{
				start = me.EyePosition(),
				end = target.GetOrigin(),
				mask = VisionMask,
				ignore = me
			}

			TraceLineEx(trace)
			return !trace.hit
		}
		return false
	}

	// Partial replication of CTFBotVision::IsIgnored().
	function IsThreat(target)
	{
		if (!target.IsAlive())
			return false

		if (target.GetTeam() == team)
			return false

		// This is not every reveal cond, but these are the only ones bots care about.
		if (target.IsPlayer())
		{
			if (target.InCond(TF_COND_BURNING) ||
				target.InCond(TF_COND_URINE) ||
				target.InCond(TF_COND_STEALTHED_BLINK) ||
				target.InCond(TF_COND_BLEEDING))
				return true

			// m_Shared.GetPercentInvisible() is not exposed to us.
			if (target.IsStealthed())
				return false

			// This condition is separated as it only reveals if the player is not cloaked.
			if (target.InCond(TF_COND_DISGUISING))
				return true

			if (target.GetDisguiseTeam() == team)
				return false
		}
		else if (startswith(target.GetClassname(), "obj_"))
		{
			if (NetProps.GetPropBool(target, "m_bCarried") ||
				NetProps.GetPropBool(target, "m_bPlacing"))
				return false
		}

		return true
	}
}

SubterraneanAnimosity.Bots <-
{
	TagStore = null

	PlayerClassNames = ["", "Scout", "Sniper", "Soldier", "Demoman", "Medic", "Heavy", "Pyro", "Spy", "Engineer"]
	LaughEvilCount = [0, 3, 3, 3, 5, 5, 4, 4, 2, 6]
	LaughEvilGiantCount = [0, 3, 0, 3, 5, 0, 4, 4, 0, 0]
}

function SubterraneanAnimosity::Bots::LaughEvil(bot)
{
	local pclass = bot.GetPlayerClass()
	local variant = ""
	if (bot.IsMiniBoss())
	{
		if (LaughEvilGiantCount[pclass])
			variant = ".M_MVM_LaughEvil0" + RandomInt(1, LaughEvilGiantCount[pclass]).tostring()
		else
			variant = ".MVM_LaughEvil0" + RandomInt(1, LaughEvilCount[pclass]).tostring()
	}
	else
		variant = ".MVM_LaughEvil0" + RandomInt(1, LaughEvilCount[pclass]).tostring()

	local replacementline = PlayerClassNames[pclass] + variant
	PrecacheScriptSound(replacementline)
	bot.EmitSound(replacementline)
}

function SubterraneanAnimosity::Bots::FillTagDefaults(tagtable, defaults)
{
	foreach (k, v in defaults)
	{
		if (k in tagtable)
			continue
		tagtable[k] <- v
	}
}

function SubterraneanAnimosity::Bots::ReplaceBackticks(string)
{
	local arr = split(string, "`" )
	local end = arr.len() - 1
	if (end <= 1)
		return string

	local ret = ""
	foreach (i, sub in arr)
	{
		if (i == end)
		{
			ret += sub
			break
		}
		ret += sub + "\""
	}
	return ret
}

function SubterraneanAnimosity::Bots::ParseWeaponSwitch(bot, tag)
{
	compilestring("TagStore=" + ReplaceBackticks(tag.slice(13)) )()

	FillTagDefaults(TagStore,
	{
		Slot = LOADOUT_POSITION_PRIMARY,
		AtHealth = 0
	})

	local weapon = null
	local nobreak = true
	for (local i = 0; i < MAX_WEAPONS; ++i)
	{
		weapon = NetProps.GetPropEntityArray(bot, "m_hMyWeapons", i)
		if (!weapon || weapon.GetSlot() != TagStore.Slot)
			continue

		nobreak = false
		break
	}

	if (nobreak)
	{
		printl("Could not find weapon for WeaponSwitch tag.")
		return
	}

	local scope = bot.GetScriptScope()
	scope.WeaponSwitchWeapon <- weapon
	scope.WeaponSwitchHealth <- TagStore.AtHealth
	scope.WeaponSwitch <- function()
	{
		if (self.GetHealth() > WeaponSwitchHealth)
			return -1.0

		self.Weapon_Switch(WeaponSwitchWeapon)
		self.AddCustomAttribute("disable weapon switch", 1, -1.0)
		self.FlagForUpdate(true)
		SubterraneanAnimosity.AddContextThink(self, "WeaponSwitch", true)
	}
	SubterraneanAnimosity.AddContextThink(bot, "WeaponSwitch")
}

function SubterraneanAnimosity::Bots::ParseCustomWeaponModel(bot, tag)
{
	compilestring("TagStore=" + ReplaceBackticks(tag.slice(18)) )()

	FillTagDefaults(TagStore,
	{
		Slot = LOADOUT_POSITION_PRIMARY,
		Model = "models/empty.mdl"
	})

	local targetwep = null
	for (local i = 0; i < MAX_WEAPONS; ++i)
	{
		local weapon = NetProps.GetPropEntityArray(bot, "m_hMyWeapons", i)
		if (!weapon)
			continue

		if (weapon.GetSlot() != TagStore.Slot)
			continue

		targetwep = weapon
		break
	}

	// Have to do it this way because weapons will unhide themselves,
	//  and making them transparent will still outline themselves
	//   when carrying the bomb.
	targetwep.ValidateScriptScope()
	targetwep.GetScriptScope().HideMe <- function()
	{
		self.DisableDraw()
		return -1.0
	}
	SubterraneanAnimosity.AddContextThink(targetwep, "HideMe")
	bot.GetScriptScope().OnDeathEnts.push(targetwep)

	SubterraneanAnimosity.GivePlayerCosmetic(bot, TagStore.Model)
}

function SubterraneanAnimosity::Bots::ParseWeaponProjStyle(bot, tag)
{
	compilestring("TagStore=" + ReplaceBackticks(tag.slice(16)))()

	FillTagDefaults(TagStore,
	{
		Slot = LOADOUT_POSITION_PRIMARY,
		Trail = null,
		Model = null,
		KillIcon = null,
		Scale = null
	})

	if (TagStore.Model)
		PrecacheModel(TagStore.Model)

	local targetwep = null
	for (local i = 0; i < MAX_WEAPONS; ++i)
	{
		local weapon = NetProps.GetPropEntityArray(bot, "m_hMyWeapons", i)
		if (!weapon)
			continue

		if (weapon.GetSlot() != TagStore.Slot)
			continue

		targetwep = weapon
		break
	}

	NetProps.SetPropBool(targetwep, "m_bForcePurgeFixedupStrings", true)
	targetwep.ValidateScriptScope()
	local scope = targetwep.GetScriptScope()

	scope.WeaponProjStyleInfo <- TagStore
	scope.WeaponProjStyleInfo.ShooterProp <- "m_hLauncher"

	switch (targetwep.GetClassname())
	{
		case "tf_weapon_flaregun":
			scope.WeaponProjStyleInfo.ShooterProp = "m_hOriginalLauncher"
			break
	}

	scope.WeaponProjStyleShoot <- function()
	{
		for (local proj; proj = Entities.FindByClassname(proj, "tf_projectile_*");)
		{
			if (NetProps.GetPropEntity(proj, WeaponProjStyleInfo.ShooterProp) != self)
				continue

			if (proj.GetScriptScope())
				continue

			proj.ValidateScriptScope()
			NetProps.SetPropBool(proj, "m_bForcePurgeFixedupStrings", true)

			if (WeaponProjStyleInfo.Trail)
				SubterraneanAnimosity.ProjectileTrail(proj, WeaponProjStyleInfo.Trail)

			if (WeaponProjStyleInfo.Model)
				proj.SetModel(WeaponProjStyleInfo.Model)

			if (WeaponProjStyleInfo.Scale)
				proj.SetModelScale(WeaponProjStyleInfo.Scale, -1.0)

			if (WeaponProjStyleInfo.KillIcon)
				proj.KeyValueFromString("classname", WeaponProjStyleInfo.KillIcon)

			break
		}
	}
	SubterraneanAnimosity.HookWeaponFire(targetwep, "WeaponProjStyleShoot", 0.05)
}

function SubterraneanAnimosity::Bots::ParseFireWeapon(bot, tag)
{
	local scope = bot.GetScriptScope()

	compilestring("TagStore=" + ReplaceBackticks(tag.slice(10)))()
	FillTagDefaults(TagStore,
	{
		Type = "Primary",
		IfSeeTarget = true,
		Duration = 0.0,
		Cooldown = 0.0
	})
	scope.FireWeaponInfo <- TagStore

	switch (TagStore.Type)
	{
		case "Primary":
			scope.FireWeaponFuncName <- "PressFireButton"
			break
		case "Secondary":
			scope.FireWeaponFuncName <- "PressAltFireButton"
			break
		case "Special":
			scope.FireWeaponFuncName <- "PressSpecialFireButton"
			break
	}

	SubterraneanAnimosity.BotAI(bot)

	scope.FireWeaponThink <- function()
	{
		local lastarea = self.GetLastKnownArea()
		if (lastarea && lastarea.HasAttributeTF(TF_NAV_SPAWN_ROOM_BLUE))
			return BotAI.UpdateFrequency

		if (FireWeaponInfo.IfSeeTarget && !BotAI.ThreatInLOS())
			return BotAI.UpdateFrequency

		self[FireWeaponFuncName](FireWeaponInfo.Duration)
			return FireWeaponInfo.Duration + FireWeaponInfo.Cooldown
	}
	SubterraneanAnimosity.AddContextThink(bot, "FireWeaponThink")
}

IncludeScript("subterranean_animosity/spells.nut", SubterraneanAnimosity.Bots)
