Hello, I am having an issue with my slingshot script.
I am sure that there is a workaround to it (hopefully) but these are the scripts.
The main issue here is probably not the scripts, but rather the server to client replication beind.. odd.
This is the Client Side script that sends info to the server:
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")
local button = script.Parent
local frame = script.Parent.Parent
local event = game.ReplicatedStorage.RemoteEvents.AnimationEvent
local cooldown = frame:WaitForChild("Cooldown")
local globalCooldown = frame.Parent:WaitForChild("GlobalCooldown")
local mouse = game.Players.LocalPlayer:GetMouse()
local function setOverlayVisible(targetFrame, state)
local overlay = targetFrame:FindFirstChild("OverlayFrame")
if overlay then
overlay.Visible = state
end
end
local function startGlobalCooldown()
globalCooldown.Value = true
for _, child in ipairs(frame.Parent:GetChildren()) do
if child:IsA("Frame") then
setOverlayVisible(child, true)
end
end
local globalCooldownTime = frame.GlobalCooldownTime.Value
task.delay(globalCooldownTime, function()
globalCooldown.Value = false
for _, child in ipairs(frame.Parent:GetChildren()) do
if child:IsA("Frame") then
setOverlayVisible(child, false)
end
end
end)
end
local function playCooldown(cooldownTime, doGlobal)
cooldown.Value = true
if doGlobal then
for _, child in ipairs(frame.Parent:GetChildren()) do
if child:IsA("Frame") then
setOverlayVisible(child, true)
end
end
else
setOverlayVisible(frame, true)
end
local cooldownFrame = frame.CooldownFrame
local bar = cooldownFrame.Bar
cooldownFrame.Visible = true
bar.AnchorPoint = Vector2.new(0, 0.5)
bar.Position = UDim2.fromScale(0, 0.5)
bar.Size = UDim2.fromScale(0, 1)
local tween = TweenService:Create(
bar,
TweenInfo.new(cooldownTime, Enum.EasingStyle.Linear),
{ Size = UDim2.fromScale(1, 1) }
)
tween:Play()
tween.Completed:Wait()
cooldown.Value = false
cooldownFrame.Visible = false
if doGlobal then
for _, child in ipairs(frame.Parent:GetChildren()) do
if child:IsA("Frame") then
setOverlayVisible(child, false)
end
end
else
setOverlayVisible(frame, false)
end
end
local function activateAnimation(input, isGameProcessedEvent)
if cooldown.Value or globalCooldown.Value then return end
local isValidInput = false
local inputTypeValue = frame.InputType.Value
if input and not isGameProcessedEvent then
local successKey, keyCode = pcall(function()
return Enum.KeyCode[inputTypeValue]
end)
if successKey and keyCode and input.KeyCode == keyCode then
isValidInput = true
else
local successInput, userInputType = pcall(function()
return Enum.UserInputType[inputTypeValue]
end)
if successInput and userInputType and input.UserInputType == userInputType then
isValidInput = true
end
end
end
if input then
local successMouse, mouseButtonType = pcall(function()
return Enum.UserInputType[inputTypeValue]
end)
if successMouse and mouseButtonType and input.UserInputType == mouseButtonType then
isValidInput = true
end
end
if not isValidInput then return end
startGlobalCooldown()
local animation = "rbxassetid://" .. frame.Animation.Value
local attackName = frame.AttackName.Value
local character = frame.Parent.Character.Value
local cooldownTime = frame.CooldownTime.Value
local doGlobal = frame.DoGlobal.Value
task.spawn(function()
playCooldown(cooldownTime, doGlobal)
end)
event:FireServer({
Animation = animation,
AttackName = attackName,
Character = character,
Frame = frame,
DoGlobal = doGlobal,
CooldownTime = cooldownTime,
HitPosition = mouse.Hit.Position
})
end
button.MouseButton1Click:Connect(activateAnimation)
UserInputService.InputBegan:Connect(activateAnimation)
this is the script that recieves that information and sends it to the proper module script:
local animationEvent = game.ReplicatedStorage.RemoteEvents.AnimationEvent
local moduleFolder = game.ServerStorage.AttackModules
local cooldowns = {}
-- cooldowns[player][key] = lastUseTime
local function isOnCooldown(player, key, cooldownTime)
cooldowns[player] = cooldowns[player] or {}
local last = cooldowns[player][key]
if last and (time() - last < cooldownTime) then
return true
end
cooldowns[player][key] = time()
return false
end
local function handleAnimation(player, data)
if typeof(data) ~= "table" then return end
if typeof(data.AttackName) ~= "string" then return end
if typeof(data.Animation) ~= "string" then return end
if typeof(data.CooldownTime) ~= "number" then return end
local characterModel = player.Character
if not characterModel then return end
local humanoid = characterModel:FindFirstChildOfClass("Humanoid")
if not humanoid then return end
local attackName = data.AttackName
local cooldownTime = data.CooldownTime
local doGlobal = data.DoGlobal
local animFrame = data.Frame
local mousePos = data.HitPosition
local character = data.Character
local animationId = data.Animation
-- per-attack cooldown (server authoritative)
if isOnCooldown(player, attackName, cooldownTime) then
return
end
-- optional global cooldown
if doGlobal and animFrame and animFrame.Parent then
local globalCooldownValue = animFrame.Parent:FindFirstChild("GlobalCooldownTime")
if globalCooldownValue then
if isOnCooldown(player, "GLOBAL", globalCooldownValue.Value) then
return
end
end
end
-- play animation
local anim = Instance.new("Animation")
anim.AnimationId = animationId
anim.Parent = characterModel
local track = humanoid:LoadAnimation(anim)
track:Play()
-- load attack module
local moduleScript = moduleFolder:FindFirstChild(character)
if not moduleScript then return end
local attacks = require(moduleScript)
local attackFunc = attacks[attackName]
if not attackFunc then
warn("Attack not found:", attackName)
return
end
-- ✅ EXECUTE ATTACK (CONFIG STYLE)
attackFunc({
Player = player,
AnimTrack = track,
AnimFrame = animFrame,
MousePosition = mousePos
})
end
animationEvent.OnServerEvent:Connect(handleAnimation)
And the real root of our problems here is the module script.
local module = {}
--// SERVICES
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
------------------------------------------------------------
-- FOLDER
------------------------------------------------------------
local bulletsFolder = workspace:FindFirstChild("Bullets")
if not bulletsFolder then
bulletsFolder = Instance.new("Folder")
bulletsFolder.Name = "Bullets"
bulletsFolder.Parent = workspace
end
module.Bullets = {}
------------------------------------------------------------
-- DAMAGE CALCULATION
------------------------------------------------------------
local function calculateDamage(dmg, model)
if not model then return dmg end
local stats = model:FindFirstChild("Stats")
if not stats then return dmg end
local armor = stats:FindFirstChild("Armor") and stats.Armor.Value or 0
local resistance = stats:FindFirstChild("Resistance") and stats.Resistance.Value or 0
dmg = math.max(dmg - armor, 0)
return dmg * (1 - math.clamp(resistance, 0, 1))
end
------------------------------------------------------------
-- FADE BULLET
------------------------------------------------------------
local function fadeBullet(bullet)
local tween = TweenService:Create(bullet, TweenInfo.new(0.4), {Transparency = 1})
tween:Play()
tween.Completed:Once(function()
if bullet then bullet:Destroy() end
end)
end
------------------------------------------------------------
-- FIRE BULLET
------------------------------------------------------------
function module.FireBullet(data)
if not data then return end
local origin = data.origin
local target = data.MousePos
local speed = data.speed or 50
local player = data.player
-- Direction and distance
local displacement = target - origin
local flatDir = Vector3.new(displacement.X, 0, displacement.Z)
local distance = flatDir.Magnitude
if distance < 0.1 then return end
-- Physics-based arc
local g = workspace.Gravity
local heightDiff = target.Y - origin.Y
local angle = math.rad(45)
local v_squared = (g * distance^2) / (2 * math.cos(angle)^2 * (distance * math.tan(angle) - heightDiff))
local velocity
if v_squared <= 0 or v_squared ~= v_squared then
-- fallback
angle = math.rad(30)
local time = distance / (speed * math.cos(angle))
local verticalSpeed = (heightDiff + 0.5 * g * time^2) / time
local horizontalSpeed = speed * math.cos(angle)
velocity = flatDir.Unit * horizontalSpeed + Vector3.new(0, verticalSpeed, 0)
else
local v = math.sqrt(v_squared)
local horizontalSpeed = v * math.cos(angle)
local verticalSpeed = v * math.sin(angle)
velocity = flatDir.Unit * horizontalSpeed + Vector3.new(0, verticalSpeed, 0)
end
-- Clamp horizontal speed
local horizontalVel = Vector3.new(velocity.X, 0, velocity.Z)
local horizontalSpeed = horizontalVel.Magnitude
if horizontalSpeed > 40 then horizontalVel = horizontalVel.Unit * 40
elseif horizontalSpeed < 1 and horizontalSpeed > 0 then horizontalVel = horizontalVel.Unit * 1 end
velocity = horizontalVel + Vector3.new(0, velocity.Y, 0)
------------------------------------------------------------
-- CREATE SERVER BULLET
------------------------------------------------------------
local bullet = Instance.new("Part")
bullet.Size = Vector3.new(0.4,0.4,0.4)
bullet.Shape = Enum.PartType.Ball
bullet.Material = Enum.Material.Plastic
bullet.Color = Color3.fromRGB(200,200,200)
bullet.Anchored = false
bullet.CanCollide = false
bullet.Massless = false
bullet.CFrame = CFrame.new(origin)
bullet.Parent = bulletsFolder
bullet.AssemblyLinearVelocity = velocity
bullet:SetNetworkOwner(nil)
module.Bullets[bullet] = {
damage = data.damage,
player = player,
hasHit = false
}
------------------------------------------------------------
-- CLIENT VISUAL BULLET SPAWN
------------------------------------------------------------
local visualEvent = ReplicatedStorage:WaitForChild("RemoteEvents"):WaitForChild("SpawnVisualBullet")
if player and player:IsA("Player") then
local bulletData = {
Position = bullet.Position,
Size = bullet.Size,
Color = bullet.Color,
}
visualEvent:FireClient(player, bulletData, bullet)
end
------------------------------------------------------------
-- SERVER SIMULATION LOOP
------------------------------------------------------------
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = {bulletsFolder, player.Character, bullet}
local lifetime = data.lifetime or 6
local alive = 0
local conn
conn = RunService.Heartbeat:Connect(function(dt)
local info = module.Bullets[bullet]
if not info then conn:Disconnect() return end
alive += dt
if alive >= lifetime then
module.Bullets[bullet] = nil
fadeBullet(bullet)
conn:Disconnect()
return
end
local currentPos = bullet.Position
local nextPos = currentPos + bullet.AssemblyLinearVelocity * dt
local travel = nextPos - currentPos
local hit = workspace:Spherecast(currentPos, bullet.Size.X/2, travel, params)
if hit and not info.hasHit then
info.hasHit = true
local part = hit.Instance
local model = part:FindFirstAncestorOfClass("Model")
local hum = model and model:FindFirstChildOfClass("Humanoid")
if hum then
hum:TakeDamage(calculateDamage(info.damage, model))
end
bullet.CanCollide = true
module.Bullets[bullet] = nil
task.delay(3, function() fadeBullet(bullet) end)
conn:Disconnect()
end
end)
end
return module
I can say with 100% certainty that there is some dumb mistakes, as I did use some AI to program this.
But here is a video showcasing the delay with the bullet.
As you can see, the NPC takes damage before the bullet hits the noob visually.
I am wondering if there is a workaround for this. I’ve heard of replicating a part on the client for a fake bullet, but how would you make a physics version of that?
Wouldn’t you have to send info like the position, what’s touching it, from the client to the server? Would doing that make it easily exploitable for players?
Anyways, you don’t HAVE to completely correct the code, you could just give me some logic on HOW i could exactly fix this issue.
example: using local scripts and scripts, use the server to replicate the exact bullet position to the client bullet visual (which i’ve tried, the delay makes it just as bad as not having it)
I’ve also tried fastCast, the same issue occurs.
I created a bullet that deletes once it touches something, on the server it deletes when it is supposed to, but on the client it visually deletes before it touches the part.
Thanks to anyone who tries helping me with this big issue,
Hopefully i’m not asking for too much (i know i am:sob:)