Enemy Fireball Script
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Quire = require(ReplicatedStorage:WaitForChild("Shared").Quire)
local ProjectileReplication_Server = Quire("ProjectileReplication_Server")
local tool = script.Parent
local handle = tool.Handle
local character = tool.Parent
local humanoid = character.Humanoid
local rightHand = character.RightHand
local workspaceProjectiles = workspace.Projectiles
local statsFolder = tool.Stats
local SPEED = statsFolder.Speed.Value
local RANGE = statsFolder.Range.Value
local DAMAGE = statsFolder.Damage.Value
local BULLET_TYPE = statsFolder.BulletType.Value
local FIREBALL_OFFSET = statsFolder.FireballOffset.Value
local BULLET_ACCELERATION = statsFolder.BulletAcceleration.Value
local COOLDOWN_TIME = statsFolder.CooldownTime.Value
local stateFolder = character.State
local isAttackingValue = stateFolder.IsAttacking
local targetObjectValue = stateFolder.Target
local shooting = false
local function shoot()
if shooting then return end
shooting = true
while targetObjectValue.Value and shooting do
local targetPrimary = targetObjectValue.Value.PrimaryPart
local origin = handle.Position + FIREBALL_OFFSET
local direction = (targetPrimary.Position - origin).Unit
local speed = (direction * SPEED) + targetPrimary.Velocity
local projectileData = {
bulletType = BULLET_TYPE,
owner = nil,
enemyOwned = true,
damage = DAMAGE
}
local castArgs = {
origin = origin,
directionWithMagnitude = direction * RANGE,
velocity = speed,
blacklist = {character, workspaceProjectiles},
cosmeticBulletObject = nil,
ignoreWater = true,
bulletAcceleration = BULLET_ACCELERATION,
canPierceFunction = function(hitPart, hitPoint, normal, material)
if Players:GetPlayerFromCharacter(hitPart.Parent) then
return true
end
return false
end
}
ProjectileReplication_Server.fireProjectile(projectileData, castArgs)
wait(COOLDOWN_TIME)
end
end
isAttackingValue.Changed:Connect(function()
if not shooting and isAttackingValue.Value then
shoot()
elseif shooting and not isAttackingValue.Value then
shooting = false
end
end)
ProjectileReplication_Client Module
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")
local DEBUG_RAYS_ENABLED = true
local Quire = require(ReplicatedStorage:WaitForChild("Shared").Quire)
local NetworkUtil = Quire("NetworkUtil")
local Caster = Quire("FastCast").new()
local projectileReplicationEvent = NetworkUtil:GetRemoteEvent("ProjectileReplicationEvent")
local projectileFolder = ReplicatedStorage:WaitForChild("Projectiles")
local workspaceProjectiles = workspace:WaitForChild("Projectiles")
local projectileList = {
Fireball = {
cosmeticBulletObject = projectileFolder:WaitForChild("FireballPart"),
fadeTweenInfo = TweenInfo.new(.2),
fadeTweenProperties = {Transparency = 1}
}
}
local ProjectileReplication_Client = {}
function ProjectileReplication_Client.fireProjectile(projectileData, castArgs, startTime)
local latency = tick() - startTime
print("latency", latency)
if projectileData.bulletType == "Fireball" then
local newBullet = projectileList.Fireball.cosmeticBulletObject:Clone()
newBullet.Parent = workspaceProjectiles
castArgs.cosmeticBulletObject = newBullet
end
Caster:FireWithBlacklist(
projectileData, castArgs.origin, castArgs.directionWithMagnitude, castArgs.velocity, castArgs.blacklist,
castArgs.cosmeticBulletObject, castArgs.ignoreWater, castArgs.bulletAcceleration, castArgs.canPierceFunction
)
end
Caster.RayHit:Connect(function(projectileData, hitPart, hitPoint, normal, material, cosmeticBulletObject)
if projectileData.bulletType == "Fireball" then
local projectileInfo = projectileList.Fireball
local fadeTween = TweenService:Create(cosmeticBulletObject, projectileInfo.fadeTweenInfo, projectileInfo.fadeTweenProperties)
fadeTween:Play()
fadeTween.Completed:Wait()
end
cosmeticBulletObject:Destroy()
end)
Caster.LengthChanged:Connect(function(projectileData, origin, segmentOrigin, segmentDirection, length, cosmeticBulletObject)
local baseCF = CFrame.new(segmentOrigin, segmentOrigin + segmentDirection)
local projectHalfSize = cosmeticBulletObject.Size.Z/2
cosmeticBulletObject.CFrame = baseCF * CFrame.new(0, 0, -(length - projectHalfSize))
if DEBUG_RAYS_ENABLED then
local part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.BrickColor = BrickColor.Green()
part.Size = Vector3.new(1.2, .8, length)
part.CFrame = baseCF
part.Parent = workspaceProjectiles
Debris:AddItem(part, 5)
end
end)
projectileReplicationEvent.OnClientEvent:Connect(ProjectileReplication_Client.fireProjectile)
return ProjectileReplication_Client
ProjectileReplication_Server Module
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")
local DEBUG_RAYS_ENABLED = true
local Quire = require(ReplicatedStorage:WaitForChild("Shared").Quire)
local NetworkUtil = Quire("NetworkUtil")
local Caster = Quire("FastCast").new()
local projectileReplicationEvent = NetworkUtil:GetRemoteEvent("ProjectileReplicationEvent")
local projectileFolder = ReplicatedStorage:WaitForChild("Projectiles")
local workspaceProjectiles = workspace:WaitForChild("Projectiles")
local function fireAllClientsExcept(clientToIgnore, ...)
for _, client in ipairs(Players:GetPlayers()) do
if client ~= clientToIgnore then
projectileReplicationEvent:FireClient(client, ...)
end
end
end
local ProjectileReplication_Server = {}
function ProjectileReplication_Server.fireProjectile(projectileData, castArgs)
local projectileOwner = projectileData.owner
local now = tick()
fireAllClientsExcept(projectileOwner, projectileData, castArgs, now)
Caster:FireWithBlacklist(
projectileData, castArgs.origin, castArgs.directionWithMagnitude, castArgs.velocity, castArgs.blacklist,
castArgs.cosmeticBulletObject, castArgs.ignoreWater, castArgs.bulletAcceleration, castArgs.canPierceFunction
)
end
Caster.RayHit:Connect(function(projectileData, hitPart, hitPoint, normal, material, projectile)
if projectileData.bulletType == "Fireball" then
local humanoid = hitPart and hitPart.Parent:FindFirstChild("Humanoid")
if not humanoid then return end
humanoid.Health = humanoid.Health - projectileData.damage
end
end)
Caster.LengthChanged:Connect(function(projectileData, origin, segmentOrigin, segmentDirection, length, cosmeticBulletObject)
local baseCF = CFrame.new(segmentOrigin, segmentOrigin + segmentDirection)
if DEBUG_RAYS_ENABLED then
local part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.BrickColor = BrickColor.Blue()
part.Size = Vector3.new(1, 1, length)
part.CFrame = baseCF
part.Parent = workspaceProjectiles
Debris:AddItem(part, 5)
end
end)
return ProjectileReplication_Server
Enemy AI shoots a projectile calling ProjectileReplication_Server.fireProjectile(projectileData, castArgs) and then fires a remote event to all players telling them to call ProjectileReplication_Client.fireProjectile(projectileData, castArgs, startTime) so the client can make their own projectile for visuals
note: the enemy shoots where the target is moving depending on the velocity of the target
The server does hit detection for dealing damage, and doesn’t render a projectile object, the client renders a projectile for visuals, and does hit detection to remove the fireball object on hit
doing it on client allows for smooth projectiles, but I’m having issues with delay, the server hits a character, but the client slightly usually misses
blue is the server ray and green is the client, the server ray hits, but the client ray does not
it works as expected if you’re not moving, or not change directions
I have two ideas of my own so far, the first one is passing the time the attack was fired with the remote event, and adjusting the client projectile based on that, but I’m not sure how to go about doing that
local latency = tick() - startTime
print("latency", latency)
my second idea is to do all the hit detection on the client, but that isn’t very secure
any help is appreciated, the only scripts worth looking at are the 3 at the top of this post, but I’ll include a place file for anyone who wants to test themselves
ProjectileReplicationRepro.rbxl (88.0 KB)
Edit: updated images with gifs, and made the debug rays straighter