local rs = game:GetService("ReplicatedStorage")
local ss = game:GetService("ServerStorage")

local Get = require(rs.Get)

local NPCsConfig = require(Get.SSConfigs.NPCsConfig)
local RaritiesConfig = require(Get.SSConfigs.RaritiesConfig)
local VariantsConfig = require(Get.SSConfigs.VariantsConfig)

local npcFolder = ss.NPCs
local spawnPointsFolder = workspace.Map.SpawnPoints

local lastSpawnTime = os.time()
local MIN_DISTANCE = 28

local NPC_SPEED = 12
local NPC_SCALE = 1.3
local COOLDOWN = 3

local npcList = {}

local function GetRarityFromChance(chance)
	local thresholds = {
		{ name = "Common",    min = 10     },
		{ name = "Uncommon",  min = 6      },
		{ name = "Rare",      min = 2.5    },
		{ name = "Epic",      min = 1      },
		{ name = "Legendary", min = 0.15   },
		{ name = "Mythic",    min = 0.05   },
		{ name = "Ultimate",  min = 0.001  },
	}

	table.sort(thresholds, function(a, b) return a.min > b.min end)

	for _, r in ipairs(thresholds) do
		if chance >= r.min then
			local config = RaritiesConfig[r.name]
			if config then
				return r.name, config.Multiplier
			end
		end
	end

	return "Common", RaritiesConfig.Common.Multiplier
end

local function PickWeightedVariant(variants)
	local total, weighted = 0, {}

	for _, name in ipairs(variants) do
		local data = VariantsConfig[name]
		if data then
			total += data.Chance
			table.insert(weighted, {name = name, chance = data.Chance, multiplier = data.Multiplier})
		end
	end
	if total == 0 then return variants[1] end

	local roll, accum = math.random() * total, 0

	for _, v in ipairs(weighted) do
		accum += v.chance
		if roll <= accum then return v.name, v.multiplier end
	end

	return weighted[1].name, weighted[1].multiplier
end

for _, category in pairs(NPCsConfig) do
	for name, data in pairs(category) do
		if data.Spawns and data.Chance and data.BaseValue and data.Variants then
			local rarityName, rarityMultiplier = GetRarityFromChance(data.Chance)
			local variantName, variantMultiplier = PickWeightedVariant(data.Variants)
			local combinedChance = data.Chance * rarityMultiplier * variantMultiplier
			local combinedValue = math.floor(data.BaseValue * variantMultiplier)

			table.insert(npcList, {
				name = name,
				rarity = rarityName,
				variant = variantName,
				chance = combinedChance,
				value = combinedValue
			})
		end
	end
end

local totalChance = 0

for _, npc in ipairs(npcList) do
	totalChance += npc.chance
end

local lastPickedName = nil
local lastPickedCount = 0

local function GetRandomNPC()
	local tries = 0

	while true do
		tries += 1
		local roll, accum = math.random() * totalChance, 0
		local chosenNPC = nil

		for _, npc in ipairs(npcList) do
			accum += npc.chance
			if roll <= accum then
				chosenNPC = npc
				break
			end
		end

		if not chosenNPC then
			chosenNPC = npcList[#npcList]
		end

		if chosenNPC.name == lastPickedName then
			if lastPickedCount < 2 then
				lastPickedCount += 1
				return chosenNPC
			else
				if tries >= 10 then
					lastPickedCount = 1
					return chosenNPC
				end
			end
		else
			lastPickedName = chosenNPC.name
			lastPickedCount = 1
			return chosenNPC
		end
	end
end

local function Walk(npc, spawnCFrame)
	local hrp = npc:FindFirstChild("HumanoidRootPart")
	local humanoid = npc:FindFirstChildOfClass("Humanoid")

	if not (hrp and humanoid) then return end

	local targetPosition = spawnCFrame.Position + (spawnCFrame.LookVector * 475)

	task.spawn(function()
		while npc and npc.Parent do
			humanoid:MoveTo(targetPosition)
			local reached = humanoid.MoveToFinished:Wait()

			if reached then
				if npc and npc.Parent then
					npc:Destroy()
				end
				break
			end
		end
	end)
end

local function SetNetworkOwnership(npc)
	local hrp = npc:FindFirstChild("HumanoidRootPart")
	if not hrp then return end

	local closestPlayer, closestDist = nil, math.huge
	for _, player in pairs(game.Players:GetPlayers()) do
		local char = player.Character
		if char and char:FindFirstChild("HumanoidRootPart") then
			local dist = (char.HumanoidRootPart.Position - hrp.Position).Magnitude
			if dist < closestDist then
				closestPlayer = player
				closestDist = dist
			end
		end
	end

	if closestPlayer then
		hrp:SetNetworkOwner(closestPlayer)
	end
end

local function SpawnNPCAt(spawnCFrame)
	for _, existingNPC in pairs(workspace.Map.NPCs.Roaming:GetChildren()) do
		if not existingNPC:HasTag("NPC") then continue end
		local hrp = existingNPC:FindFirstChild("HumanoidRootPart")
		if hrp and (hrp.Position - spawnCFrame.Position).Magnitude < MIN_DISTANCE then
			return
		end
	end

	for _ = 1, 10 do
		local picked = GetRandomNPC()
		local model = npcFolder:FindFirstChild(picked.name)

		if model and model:IsA("Model") and model.PrimaryPart then
			local npc = model:Clone()
			local torso = npc:WaitForChild("Torso")
			local hum = npc:FindFirstChildOfClass("Humanoid")

			local animateClone = script.Animate:Clone()
			animateClone.Parent = npc
			animateClone.Enabled = true

			local highlightClone = script.Highlight:Clone()
			highlightClone.Adornee = npc
			highlightClone.Parent = npc

			npc:AddTag("NPC")

			npc:SetPrimaryPartCFrame(spawnCFrame)

			npc:SetAttribute("Walking", true)

			npc:ScaleTo(NPC_SCALE)

			torso.Neck.C0 = CFrame.new(Vector3.new(0, NPC_SCALE, 0)) * CFrame.Angles(0,0,0)
			torso.Neck.C1 = CFrame.new(Vector3.new(0, -0.5 * NPC_SCALE, 0)) * CFrame.Angles(0,0,0)

			npc.Parent = workspace.Map.NPCs.Roaming

			SetNetworkOwnership(npc)
			
			model.PrimaryPart.Anchored = true

			task.delay(0.1, function()
				for _, v in pairs(npc:GetDescendants()) do
					if v:IsA("BasePart") then
						v.CollisionGroup = "Non-Collidable"
						v.Anchored = false
						v.CanCollide = false
						v.CanQuery = false
						v.CanTouch = false
						v.AudioCanCollide = false
						v.EnableFluidForces = false
						v.Massless = true
					end
				end

				hum.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
				hum.DisplayName = ""
				hum.HealthDisplayDistance = 0
				hum.HealthDisplayType = Enum.HumanoidHealthDisplayType.AlwaysOff
				hum.NameOcclusion = Enum.NameOcclusion.NoOcclusion
				hum.BreakJointsOnDeath = false
				hum.EvaluateStateMachine = true
				hum.RequiresNeck = true
				hum.AutoRotate = false
				hum.AutomaticScalingEnabled = true
				hum.MaxHealth = math.huge
				hum.Health = math.huge
				hum.MaxSlopeAngle = 0
				hum.AutoJumpEnabled = false
				hum.WalkSpeed = NPC_SPEED * Get.SS:GetAttribute("SpeedMultiplier")
				hum.UseJumpPower = false
				hum.JumpHeight = 0
			end)

			task.delay(0.1, Walk, npc, spawnCFrame)

			print(string.format("Spawned: %s | Rarity: %s | Variant: %s | Value: %d", picked.name, picked.rarity, picked.variant, picked.value))
			return
		end
	end
end

local function InitializeSpawn()
	while Get.SS:GetAttribute("ShouldSpawn") do
		task.wait()

		if os.time() - lastSpawnTime >= COOLDOWN then
			lastSpawnTime = os.time()

			for _, point in pairs(spawnPointsFolder:GetChildren()) do
				if point:IsA("BasePart") then
					task.spawn(SpawnNPCAt, point.CFrame)
				end
			end
		end
	end
end

Get.SS:GetAttributeChangedSignal("SpeedMultiplier"):Connect(function()
	for i, npc in pairs(workspace.Map.NPCs.Roaming:GetChildren()) do
		if not npc:HasTag("NPC") then continue end

		local hum = npc:FindFirstChildOfClass("Humanoid")
		hum.WalkSpeed = NPC_SPEED * Get.SS:GetAttribute("SpeedMultiplier")
	end
end)

task.spawn(InitializeSpawn)

fix the network ownership and issues with spawning, managing distance between npcs before spawning