
KeylessUniversal

--[[
tail physics
version 4.2
developed by @zephyrr skidded off @r3dtuxedo
]]--
local config = {
stiffness = 96,
damping = 9,
linearamp = Vector3.new(6.4, 2.5333333, 6.4),
angularamp = Vector3.new(0, 18, 0),
wagenabled = true,
wagdrop = 0.2,
wagsway = 0.4,
wagroll = 0.5,
wagblendin = 0.008,
wagblendout = 0.02,
wagspeed = 1,
timescale = 1,
windenabled = false,
gravityenabled = false,
customwind = Vector3.zero,
customforce = Vector3.zero,
maxdistance = 900,
maxtails = 5,
movethreshold = 15,
includednames = {} :: { string },
}
local run = game:GetService("RunService")
local players = game:GetService("Players")
local player = players.LocalPlayer
local char = player.Character or player.CharacterAdded:Wait()
local cam = workspace.CurrentCamera
local sin = math.sin
local cos = math.cos
local min = math.min
local max = math.max
local insert = table.insert
local find = table.find
local remove = table.remove
local sort = table.sort
local timestep = 1 / 120
local invtimestep = 120
local tails = {} :: { {
root: BasePart,
prevroot: CFrame,
prevdelta: CFrame,
weld: Weld,
origcf: CFrame,
prevweld: CFrame,
curweld: CFrame,
angle: Vector3,
angvel: Vector3,
wagblend: number,
seed: number,
pivot: CFrame,
invpivot: CFrame,
char: Model,
invscale: number,
dist: number,
rendering: boolean,
} }
local tracked = {} :: { [Model]: boolean }
local wagtime = 0
local windtime = 0
local accum = 0
local winddir = Vector3.zero
local windalpha = 0
local windalpha04 = 0
local windalpha006 = 0
local haswind = false
local gravforce = Vector3.zero
local linearforce = Vector3.zero
local haslinear = false
local function springcalc(pos: Vector3, vel: Vector3, stiff: number, damp: number): (Vector3, Vector3)
local accel = stiff * pos + damp * vel
local newvel = vel - accel * timestep
return pos + newvel * timestep, newvel
end
local function vectoangles(v: Vector3): CFrame
return CFrame.fromEulerAngles(v.X, v.Y, v.Z)
end
local function lerp(a: number, b: number, t: number): number
return a + (b - a) * t
end
local function updatewind()
local windforce = config.windenabled and (workspace.GlobalWind + config.customwind) or config.customwind
if windforce.Magnitude > 0 then
winddir = windforce.Unit
else
winddir = Vector3.zero
end
windalpha = windforce.Magnitude * 0.5
windalpha04 = windalpha * 0.4
windalpha006 = windalpha * windalpha * 0.06
haswind = windalpha > 0
end
local function updatelinear()
linearforce = config.gravityenabled and (gravforce + config.customforce) or config.customforce
haslinear = linearforce.Magnitude > 0
end
local function updategravity()
gravforce = Vector3.new(0, max((196.2 - workspace.Gravity) * 0.004, -1))
updatelinear()
end
updatewind()
updategravity()
workspace:GetPropertyChangedSignal("GlobalWind"):Connect(updatewind)
workspace:GetPropertyChangedSignal("Gravity"):Connect(updategravity)
local function getwagoffset(tail): Vector3
local blend = tail.wagblend
if tail.angvel.Magnitude > 0.3 then
blend = blend < 0.01 and 0 or blend * (1 - config.wagblendout)
else
blend = blend > 0.99 and 1 or lerp(blend, 1, config.wagblendin)
end
tail.wagblend = blend
if blend <= 0 then
return Vector3.zero
end
local wt = wagtime + tail.seed
local ft = wt + sin(wt) * sin(wt * 0.22727272727272727) * 0.9
return Vector3.new(
(sin(ft * 2) + 0.8) * config.wagdrop * blend,
cos(ft) * config.wagsway * blend,
sin(ft) * -config.wagroll * blend
)
end
local function updateinterp(tail, alpha: number)
local scale = tail.invscale * tail.char:GetScale()
local cf = tail.prevweld:Lerp(tail.curweld, alpha)
if config.wagenabled then
cf = cf * (cf:ToObjectSpace(tail.pivot * vectoangles(getwagoffset(tail)) * tail.invpivot)):Inverse()
end
tail.weld.C0 = cf.Rotation + cf.Position * scale
end
local function resettail(tail)
tail.prevroot = tail.root.CFrame
tail.prevdelta = CFrame.identity
tail.prevweld = tail.origcf
tail.curweld = tail.origcf
tail.angle = Vector3.zero
tail.angvel = Vector3.zero
tail.wagblend = 0
tail.rendering = true
end
local function restoretail(tail)
tail.rendering = false
local scale = tail.invscale * tail.char:GetScale()
tail.weld.C0 = tail.origcf.Rotation + tail.origcf.Position * scale
end
local function updatetail(tail, doweld: boolean, alpha: number)
local rootcf = tail.root.CFrame
local delta = tail.prevroot:ToObjectSpace(rootcf)
if delta.Position.Magnitude > config.movethreshold then
delta = delta.Rotation
end
local accel = tail.prevdelta:ToObjectSpace(delta)
local force = accel.Position * config.linearamp
if haswind then
local s4 = sin(windtime * 4)
local unstable = sin(windtime * 2 * s4) * windalpha04
local stable = sin(windtime * 2) * s4
local windforce = winddir * windalpha006 * (1 + (stable + unstable) * 0.4)
force -= rootcf:VectorToObjectSpace(windforce)
end
if haslinear then
force -= rootcf:VectorToObjectSpace(linearforce)
end
local pivot = tail.pivot
local disp = pivot.Position - tail.prevweld.Position
local torque = disp:Cross(force)
local x, y, z = accel:ToEulerAnglesXYZ()
local angaccel = Vector3.new(x, y, z) * config.angularamp
local newangle, newangvel = springcalc(tail.angle, tail.angvel, config.stiffness, config.damping)
tail.angle = newangle
local scale = tail.invscale * tail.char:GetScale()
tail.angvel = newangvel + torque / scale - angaccel
local newcur = pivot * vectoangles(newangle) * tail.invpivot
local newprev = tail.curweld
if doweld then
local cf = newprev:Lerp(newcur, alpha)
if config.wagenabled then
cf = cf * (cf:ToObjectSpace(pivot * vectoangles(getwagoffset(tail)) * tail.invpivot)):Inverse()
end
tail.weld.C0 = cf.Rotation + cf.Position * scale
end
tail.prevroot = rootcf
tail.prevdelta = delta
tail.prevweld = newprev
tail.curweld = newcur
end
local function getpivot(root: BasePart): CFrame?
local waistrig = root:FindFirstChild("WaistRigAttachment")
if waistrig and waistrig:IsA("Attachment") then
return waistrig.CFrame * CFrame.new(0, 0, root.Size.Z * 0.5)
end
local waistback = root:FindFirstChild("WaistBackAttachment")
if waistback and waistback:IsA("Attachment") then
return waistback.CFrame * CFrame.new(0, 0.3, 0)
end
return nil
end
local function nametoseed(name: string): number
local seed = 0
for i = 1, #name do
seed += string.byte(name, i) ^ 2
end
return seed
end
local function removetail(tail)
local idx = find(tails, tail)
if idx then
remove(tails, idx)
end
end
local function setupweld(characterModel: Model, weld: Weld, tailpart: BasePart, custompivot: CFrame | BasePart?)
local flipped = false
local root = weld.Part0
if not root then
warn("weld missing part0")
return
end
if root == tailpart then
root = weld.Part1
if not root then
warn("weld missing part1")
return
end
flipped = true
end
local pivot: CFrame?
if custompivot then
pivot = typeof(custompivot) == "CFrame" and custompivot or custompivot.CFrame
else
pivot = getpivot(root)
end
if not pivot then
warn("couldnt find attachment")
return
end
local origcf: CFrame
if flipped then
origcf = weld.C1 * weld.C0:Inverse()
weld.Part0, weld.Part1 = root, tailpart
else
origcf = weld.C0 * weld.C1:Inverse()
end
weld.C0 = origcf
weld.C1 = CFrame.identity
local localpivot = origcf:ToObjectSpace(CFrame.new(pivot.Position))
local seed = nametoseed(characterModel.Name)
local plr = players:GetPlayerFromCharacter(characterModel)
if plr then
seed += plr.UserId
end
local tail = {
root = root,
prevroot = root.CFrame,
prevdelta = CFrame.identity,
weld = weld,
origcf = origcf,
prevweld = origcf,
curweld = origcf,
angle = Vector3.zero,
angvel = Vector3.zero,
wagblend = 0,
seed = seed,
pivot = origcf * localpivot,
invpivot = localpivot:Inverse(),
char = characterModel,
invscale = 1 / characterModel:GetScale(),
dist = 0,
rendering = false,
}
insert(tails, tail)
weld.AncestryChanged:Connect(function()
if not weld.Parent then
removetail(tail)
end
end)
end
local function setupaccessory(characterModel: Model, accessory: Accessory)
local handle = accessory:FindFirstChild("Handle")
if not handle or not handle:IsA("BasePart") then
accessory.ChildAdded:Once(function()
setupaccessory(characterModel, accessory)
end)
return
end
local weld = handle:FindFirstChild("AccessoryWeld")
if weld and weld:IsA("Weld") then
setupweld(characterModel, weld, handle, nil)
end
end
local function istail(accessory: Accessory): boolean
local acctype = accessory.AccessoryType
local accname = accessory.Name:lower()
if acctype == Enum.AccessoryType.Back or acctype == Enum.AccessoryType.Waist then
if accname:find("tail", 1, true) then
return true
end
for i = 1, #config.includednames do
if accessory.Name == config.includednames[i] then
return true
end
end
end
return false
end
local function setupchar(characterModel: Model)
if tracked[characterModel] then
return
end
tracked[characterModel] = true
print("setting up " .. characterModel.Name)
characterModel.ChildAdded:Connect(function(child)
if child:IsA("Accessory") and istail(child) then
print("found new tail " .. child.Name)
setupaccessory(characterModel, child)
end
end)
local count = 0
for _, child in characterModel:GetChildren() do
if child:IsA("Accessory") and istail(child) then
print("found tail " .. child.Name)
setupaccessory(characterModel, child)
count += 1
end
end
if count > 0 then
print("setup " .. count .. " tails")
else
print("no tails found")
end
characterModel:GetPropertyChangedSignal("Parent"):Connect(function()
if not characterModel.Parent then
tracked[characterModel] = nil
end
end)
end
setupchar(char)
player.CharacterAdded:Connect(function(newchar)
char = newchar
tails = {}
tracked = {}
wagtime = 0
windtime = 0
accum = 0
print("resetting physics")
task.wait(0.5)
setupchar(newchar)
end)
local function sortbydist(a, b): boolean
return a.dist < b.dist
end
run.PreRender:Connect(function(dt: number)
dt = min(dt, 0.1) * config.timescale
wagtime += dt * config.wagspeed
windtime += dt
accum += dt
local steps = 0
while accum >= timestep do
accum -= timestep
steps += 1
end
local campos = cam.CFrame.Position
local rendering = {}
for _, tail in tails do
local dist = (tail.root.CFrame.Position - campos).Magnitude
if dist <= config.maxdistance then
tail.dist = dist
insert(rendering, tail)
elseif tail.rendering then
restoretail(tail)
end
end
sort(rendering, sortbydist)
local interp = steps == 0
local alpha = accum * invtimestep
local updated = 0
for _, tail in rendering do
if updated >= config.maxtails then
if tail.rendering then
restoretail(tail)
end
continue
end
if interp then
if tail.rendering then
updateinterp(tail, alpha)
end
else
if not tail.rendering then
resettail(tail)
end
for i = 1, steps do
updatetail(tail, i == steps, alpha)
end
end
updated += 1
end
end)
print("tracking " .. #tails .. " tails")



fe or not?
how does this work
@leeis its visual and it uses spring physics and movement detection
now this, this is epic
Ay question, does this work on uhh hat reanim or
@Enotfromrainworld12 no this is purely visual
can you make the script restart when i die or res*? (btw, peak script)
@coolguytoken i just pushed an update that fixes the res* bs. lmk if theres any other bugs
@zephyrr thank you ^_^
You should make one that loads an animation from the catalog that makes the waist part move if a tail is attached to the waist part on the players avatar, that way it can be FE. R15 only tho
woaaahhhh
as a furry (protogen) this has to be one of the best scripts ever [im aware its client sided]
❤️
What the actual *
Can I make one for ear or head accessory??
U*
É visual só?
can others see it aswell?
Is this client sided or server sided?
@joeyspenner cilentside. I wish I had bobux so I can make avatars, but I'm stuck with a Greenman avatar from wtd