--[[ 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")