Server script never works when moved to the client

i have these scripts here that toggle the visibility of models that are tagged, using CollectionService and TweenService. it works fine, except when i place the content of these scripts inside of a couple of LocalScripts and change the RunService.Heartbeat to RunService.RenderStepped, it fails to update (or even toggle) the transparencies of the children of the tagged models.

i want it to be client-sided because i do not want the script’s responsiveness to be tied to the player’s connection.

script that handles the tagging
-- handles tags & stuff

local run_service = game:GetService("RunService")
local collection_service = game:GetService("CollectionService")
local main_part = script.Parent
local decal = main_part:WaitForChild("normal")

local can_see_tx = 16569627204
local cannot_see_tx = 10198213112
local enemy_tag = "invisible_enemy"
local visibility_tag = "is_visible"
local visible_enemies = {} -- table to store visible enemies

local vision_distance = 60
local field_of_view = 90
local vision_in_front_only = true

local function target_in_vision(target, main_part)
	if target and target:IsA("Model") and target.PrimaryPart then
		local origin = main_part.Position
		local targetPosition = target.PrimaryPart.Position
		local direction = (targetPosition - origin).unit * vision_distance
		local ray = Ray.new(origin, direction)
		local ignore_list = {main_part}

		local hit, _ = workspace:FindPartOnRayWithIgnoreList(ray, ignore_list)

		if hit and target.PrimaryPart then
			if hit:IsDescendantOf(target) then
				if vision_in_front_only then
					local unit = (targetPosition - origin).Unit
					local is_looking = main_part.CFrame.LookVector
					local dot = unit:Dot(is_looking)

					local half_fov = math.rad(field_of_view) / 2
					local max_dot = math.cos(half_fov)

					if dot > max_dot then
						return true
					end
				else
					return true
				end
			end
		end
	end

	return false
end

local function update_visible_enemies()
	for _, enemy in ipairs(collection_service:GetTagged(enemy_tag)) do
		local is_visible = target_in_vision(enemy, main_part)
		local was_visible = visible_enemies[enemy]

		if is_visible and not was_visible then
			visible_enemies[enemy] = true
			collection_service:AddTag(enemy, visibility_tag)
		elseif not is_visible and was_visible then
			visible_enemies[enemy] = nil
			collection_service:RemoveTag(enemy, visibility_tag)
		end
	end
end
	
run_service.Heartbeat:Connect(function()    
	update_visible_enemies()

	-- check if there are any visible enemies
	if next(visible_enemies) then
		decal.Texture = ("rbxassetid://" .. tostring(can_see_tx)) -- can see target
	else
		decal.Texture = ("rbxassetid://" .. tostring(cannot_see_tx)) -- cannot see target
	end
end)

script that handles the visibility
local tween_service = game:GetService("TweenService")
local collection_service = game:GetService("CollectionService")

local enemy_tag = "invisible_enemy"
local visibility_tag = "is_visible"
local visible_enemies = {}

-- attempt #9 million, true = visible, false = invisible
local original_transparency = {}

local function set_transparency(instance, transparency)
	if instance:IsA("BasePart") or instance:IsA("MeshPart") or instance:IsA("UnionOperation") or instance:IsA("Part") then
		local tween = tween_service:Create(instance, TweenInfo.new(0.2, Enum.EasingStyle.Circular, Enum.EasingDirection.InOut), {Transparency = transparency})
		tween:Play()
	end
end

local function initialize_transparency(obj: Instance)
	if obj:IsA("Model") then
		for _, child in ipairs(obj:GetDescendants()) do
			if not original_transparency[child] then
				if child:IsA("BasePart") or child:IsA("MeshPart") or child:IsA("UnionOperation") or child:IsA("Part") then
					original_transparency[child] = child.Transparency
				end
			end
			set_transparency(child, 1)
		end
	else
		if not original_transparency[obj] then
			original_transparency[obj] = obj.Transparency
		end
		set_transparency(obj, 1)
	end
end

local function toggle_visibility(obj: Instance, is_visible: boolean)
	if obj:IsA("Model") then
		for _, child in ipairs(obj:GetDescendants()) do
			if original_transparency[child] then
				set_transparency(child, is_visible and original_transparency[child] or 1)
			end
		end
	elseif original_transparency[obj] then
		set_transparency(obj, is_visible and original_transparency[obj] or 1)
	end
end

for i, enemy in ipairs(collection_service:GetTagged(enemy_tag)) do
	initialize_transparency(enemy)
end

collection_service:GetInstanceAddedSignal(visibility_tag):Connect(function(obj)
	toggle_visibility(obj, true)
end)

collection_service:GetInstanceRemovedSignal(visibility_tag):Connect(function(obj)
	toggle_visibility(obj, false)
end)

RenderStepped only fires on the client. A server script can never listen to it.

Why not use local scripts?

i tried that, but as i said, when i place the content of the scripts inside of a couple of LocalScripts, it never updates the visibility of the tagged models.

Where are you placing these local scripts?

Local scripts only work when parented under a player or their character

i put the script that handles visibility inside of StarterGui, while i put the script that checks the field of view inside of a tool, specifically, the tool’s handle