Skip to content
Working

Tail Wag (server)

POPooPoo324 viewsUniversalJul 22, 2026
Share
Tail Wag (server)

Script Code

Lua
--tungsten88474 (hat tool) and mxsynry (fix) on discord

local env = (getgenv and getgenv()) or _G
env.__TailAnim = env.__TailAnim or {}

local TA = env.__TailAnim

if type(TA._cleanup) == "function" then
	pcall(TA._cleanup)
end

TA.Enabled = true
TA.Speed = 10.0
TA.Amplitude = 35

-- Optional:
-- Set this to the exact accessory name when automatic detection fails.
-- Example:
-- TA.AccessoryName = "White Fox Tail"
TA.AccessoryName = TA.AccessoryName or nil

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local localPlayer = Players.LocalPlayer

if not localPlayer then
	error("TailAnim: LocalPlayer is unavailable")
end

local characterConnection = nil
local childConnection = nil
local renderConnection = nil

local currentCharacter = nil
local currentAccessory = nil
local realHandle = nil

local fakeAccessory = nil
local fakeHandle = nil
local fakeJoint = nil
local fakeBaseC0 = nil

local savedJoint = nil
local savedJointParent = nil

local animationTime = 0
local currentYaw = 0
local lastSearchTime = 0
local bindGeneration = 0

local function disconnect(connection)
	if connection then
		pcall(function()
			connection:Disconnect()
		end)
	end
end

local function lower(value)
	return string.lower(tostring(value or ""))
end

local function containsPlain(value, searchText)
	return string.find(
		lower(value),
		lower(searchText),
		1,
		true
	) ~= nil
end

local function isHairRelatedName(value)
	local name = lower(value)

	return string.find(name, "ponytail", 1, true) ~= nil
		or string.find(name, "pony tail", 1, true) ~= nil
		or string.find(name, "pigtail", 1, true) ~= nil
		or string.find(name, "pig tail", 1, true) ~= nil
		or string.find(name, "hair", 1, true) ~= nil
		or string.find(name, "braid", 1, true) ~= nil
		or string.find(name, "bangs", 1, true) ~= nil
		or string.find(name, "hairstyle", 1, true) ~= nil
end

local function hasTailWord(value)
	local text = lower(value)

	if text == "" or isHairRelatedName(text) then
		return false
	end

	-- Match "tail" as a separate word.
	if string.match(text, "%f[%a]tail%f[%A]") then
		return true
	end

	-- Common separators used in accessory names.
	if string.find(text, "_tail", 1, true)
		or string.find(text, "tail_", 1, true)
		or string.find(text, "-tail", 1, true)
		or string.find(text, "tail-", 1, true)
	then
		return true
	end

	return false
end

local function getHandle(accessory)
	if not accessory or not accessory:IsA("Accessory") then
		return nil
	end

	local handle = accessory:FindFirstChild("Handle")

	if handle and handle:IsA("BasePart") then
		return handle
	end

	return nil
end

local function getAccessoryJoint(accessory)
	local handle = getHandle(accessory)

	if not handle then
		return nil
	end

	local accessoryWeld = handle:FindFirstChild("AccessoryWeld")

	if accessoryWeld and (
		accessoryWeld:IsA("Weld")
		or accessoryWeld:IsA("Motor6D")
	) then
		return accessoryWeld
	end

	for _, object in ipairs(handle:GetChildren()) do
		if object:IsA("Motor6D") or object:IsA("Weld") then
			return object
		end
	end

	return nil
end

local function getAccessoryType(accessory)
	local success, accessoryType = pcall(function()
		return accessory.AccessoryType
	end)

	if success then
		return accessoryType
	end

	return nil
end

local function hasHairAttachment(accessory)
	local handle = getHandle(accessory)

	if not handle then
		return false
	end

	for _, object in ipairs(handle:GetDescendants()) do
		if object:IsA("Attachment") then
			local attachmentName = lower(object.Name)

			if string.find(
				attachmentName,
				"hairattachment",
				1,
				true
			) then
				return true
			end
		end
	end

	return false
end

local function hasTailAttachment(accessory)
	local handle = getHandle(accessory)

	if not handle then
		return false
	end

	for _, object in ipairs(handle:GetDescendants()) do
		if object:IsA("Attachment") then
			if hasTailWord(object.Name) then
				return true
			end
		end
	end

	return false
end

local function hasWaistOrBackAttachment(accessory)
	local handle = getHandle(accessory)

	if not handle then
		return false
	end

	for _, object in ipairs(handle:GetDescendants()) do
		if object:IsA("Attachment") then
			local attachmentName = lower(object.Name)

			if string.find(
				attachmentName,
				"waistbackattachment",
				1,
				true
			)
				or string.find(
					attachmentName,
					"waistcenterattachment",
					1,
					true
				)
				or string.find(
					attachmentName,
					"bodybackattachment",
					1,
					true
				)
			then
				return true
			end
		end
	end

	return false
end

local function hasTailMarkerInDescendants(accessory)
	local handle = getHandle(accessory)

	if not handle then
		return false
	end

	for _, object in ipairs(handle:GetDescendants()) do
		if hasTailWord(object.Name) then
			return true
		end

		if object:IsA("SpecialMesh") then
			local meshId = ""
			local textureId = ""

			pcall(function()
				meshId = object.MeshId
			end)

			pcall(function()
				textureId = object.TextureId
			end)

			if hasTailWord(meshId) or hasTailWord(textureId) then
				return true
			end
		end
	end

	return false
end

local function matchesConfiguredAccessory(accessory)
	if type(TA.AccessoryName) ~= "string"
		or TA.AccessoryName == ""
	then
		return false
	end

	return lower(accessory.Name) == lower(TA.AccessoryName)
end

local function isTailAccessory(accessory)
	if not accessory or not accessory:IsA("Accessory") then
		return false
	end

	if accessory:GetAttribute("__TailAnimFake") == true then
		return false
	end

	if not getHandle(accessory) then
		return false
	end

	-- An explicitly configured accessory name takes priority.
	if matchesConfiguredAccessory(accessory) then
		return true
	end

	local accessoryType = getAccessoryType(accessory)

	-- Never select an accessory Roblox identifies as hair.
	if accessoryType == Enum.AccessoryType.Hair then
		return false
	end

	-- Reject hair attachments even if the accessory type is incorrect.
	if hasHairAttachment(accessory) then
		return false
	end

	-- Reject common hairstyle names containing the word "tail."
	if isHairRelatedName(accessory.Name) then
		return false
	end

	local nameHasTail = hasTailWord(accessory.Name)
	local attachmentHasTail = hasTailAttachment(accessory)
	local descendantsHaveTail =
		hasTailMarkerInDescendants(accessory)

	if not nameHasTail
		and not attachmentHasTail
		and not descendantsHaveTail
	then
		return false
	end

	-- A tail normally uses a waist or back attachment.
	if hasWaistOrBackAttachment(accessory) then
		return true
	end

	-- Accept clearly named tail accessories unless they are hair.
	if nameHasTail then
		return true
	end

	return attachmentHasTail or descendantsHaveTail
end

local function configureInvisibleAccessory(accessory)
	for _, object in ipairs(accessory:GetDescendants()) do
		if object:IsA("BasePart") then
			object.Transparency = 1
			object.CanCollide = false
			object.CanTouch = false
			object.CanQuery = false
			object.Massless = true

			pcall(function()
				object.CastShadow = false
			end)
		end
	end
end

local function configureRealHandle(handle)
	handle.Anchored = false
	handle.CanCollide = false
	handle.CanTouch = false
	handle.CanQuery = false
	handle.Massless = true
end

local function destroyFakeAccessory()
	if fakeAccessory then
		pcall(function()
			fakeAccessory:Destroy()
		end)
	end

	fakeAccessory = nil
	fakeHandle = nil
	fakeJoint = nil
	fakeBaseC0 = nil
end

local function restoreOriginalJoint()
	if not savedJoint then
		return
	end

	local joint = savedJoint

	savedJoint = nil

	if currentAccessory
		and currentAccessory.Parent
		and realHandle
		and realHandle.Parent
		and not getAccessoryJoint(currentAccessory)
	then
		local restored = pcall(function()
			joint.Parent = savedJointParent or realHandle
		end)

		if restored then
			savedJointParent = nil
			return
		end
	end

	pcall(function()
		joint:Destroy()
	end)

	savedJointParent = nil
end

local function releaseCurrentAccessory(restoreOriginal)
	destroyFakeAccessory()

	if restoreOriginal then
		restoreOriginalJoint()
	elseif savedJoint then
		pcall(function()
			savedJoint:Destroy()
		end)

		savedJoint = nil
		savedJointParent = nil
	end

	currentAccessory = nil
	realHandle = nil

	animationTime = 0
	currentYaw = 0
end

local function removeOldFakeAccessories(character)
	if not character then
		return
	end

	for _, child in ipairs(character:GetChildren()) do
		if child:IsA("Accessory")
			and child:GetAttribute("__TailAnimFake") == true
		then
			pcall(function()
				child:Destroy()
			end)
		end
	end
end

local function attachTail(accessory)
	if not accessory
		or not accessory.Parent
		or not accessory:IsA("Accessory")
	then
		return false
	end

	if not isTailAccessory(accessory) then
		return false
	end

	local handle = getHandle(accessory)
	local originalJoint = getAccessoryJoint(accessory)

	if not handle or not originalJoint then
		return false
	end

	releaseCurrentAccessory(true)

	local previousArchivable = accessory.Archivable
	accessory.Archivable = true

	local cloneSuccess, clonedAccessory = pcall(function()
		return accessory:Clone()
	end)

	accessory.Archivable = previousArchivable

	if not cloneSuccess or not clonedAccessory then
		warn(
			"TailAnim: unable to clone accessory "
				.. tostring(accessory.Name)
		)

		return false
	end

	clonedAccessory.Name =
		"__TailAnimFake_" .. tostring(accessory.Name)

	clonedAccessory:SetAttribute("__TailAnimFake", true)

	configureInvisibleAccessory(clonedAccessory)

	clonedAccessory.Parent = accessory.Parent

	local clonedHandle = getHandle(clonedAccessory)
	local clonedJoint = getAccessoryJoint(clonedAccessory)

	if not clonedHandle or not clonedJoint then
		clonedAccessory:Destroy()

		warn(
			"TailAnim: cloned tail has no valid handle or joint"
		)

		return false
	end

	local jointClone = nil

	local jointCloneSuccess = pcall(function()
		originalJoint.Archivable = true
		jointClone = originalJoint:Clone()
	end)

	if not jointCloneSuccess or not jointClone then
		clonedAccessory:Destroy()

		warn(
			"TailAnim: unable to preserve the original joint"
		)

		return false
	end

	currentAccessory = accessory
	realHandle = handle

	fakeAccessory = clonedAccessory
	fakeHandle = clonedHandle
	fakeJoint = clonedJoint
	fakeBaseC0 = clonedJoint.C0

	savedJoint = jointClone
	savedJointParent = originalJoint.Parent

	configureRealHandle(realHandle)

	local destroyed = pcall(function()
		originalJoint:Destroy()
	end)

	if not destroyed then
		releaseCurrentAccessory(false)
		return false
	end

	animationTime = 0
	currentYaw = 0

	return true
end

local function findBestTail(character)
	if not character or not character.Parent then
		return nil
	end

	local configuredMatch = nil
	local namedMatch = nil
	local otherMatch = nil

	for _, child in ipairs(character:GetChildren()) do
		if child:IsA("Accessory")
			and child:GetAttribute("__TailAnimFake") ~= true
		then
			if matchesConfiguredAccessory(child) then
				configuredMatch = child
				break
			end

			if isTailAccessory(child) then
				if hasTailWord(child.Name) then
					namedMatch = namedMatch or child
				else
					otherMatch = otherMatch or child
				end
			end
		end
	end

	return configuredMatch or namedMatch or otherMatch
end

local function findAndAttachTail(character)
	local accessory = findBestTail(character)

	if not accessory then
		return false
	end

	return attachTail(accessory)
end

local function bindCharacter(character)
	bindGeneration += 1

	local generation = bindGeneration

	disconnect(childConnection)

	childConnection = nil
	TA._childConn = nil

	releaseCurrentAccessory(true)
	removeOldFakeAccessories(character)

	currentCharacter = character
	lastSearchTime = 0

	-- Give Roblox a moment to add all character accessories.
	task.delay(0.35, function()
		if generation ~= bindGeneration then
			return
		end

		if character ~= localPlayer.Character then
			return
		end

		findAndAttachTail(character)
	end)

	childConnection = character.ChildAdded:Connect(function(child)
		if not child:IsA("Accessory") then
			return
		end

		if child:GetAttribute("__TailAnimFake") == true then
			return
		end

		task.delay(0.2, function()
			if generation ~= bindGeneration then
				return
			end

			if child.Parent ~= character then
				return
			end

			if isTailAccessory(child) then
				attachTail(child)
			end
		end)
	end)

	TA._childConn = childConnection
end

function TA:SetEnabled(value)
	self.Enabled = not not value
end

function TA:SetSpeed(value)
	local numericValue = tonumber(value)

	if numericValue then
		self.Speed = math.max(numericValue, 0)
	end
end

function TA:SetAmplitude(value)
	local numericValue = tonumber(value)

	if numericValue then
		self.Amplitude = math.clamp(
			numericValue,
			0,
			180
		)
	end
end

function TA:SetAccessoryName(name)
	if name == nil or tostring(name) == "" then
		self.AccessoryName = nil
	else
		self.AccessoryName = tostring(name)
	end

	local character = localPlayer.Character

	if character then
		releaseCurrentAccessory(true)

		task.defer(function()
			findAndAttachTail(character)
		end)
	end
end

function TA:GetSelectedAccessory()
	return currentAccessory
end

function TA:Stop()
	self.Enabled = false

	if type(self._cleanup) == "function" then
		self._cleanup()
	end
end

characterConnection =
	localPlayer.CharacterAdded:Connect(function(character)
		bindCharacter(character)
	end)

TA._charConn = characterConnection

if localPlayer.Character then
	bindCharacter(localPlayer.Character)
end

renderConnection =
	RunService.RenderStepped:Connect(function(deltaTime)
		local character = localPlayer.Character

		if not character then
			return
		end

		local tailIsValid =
			currentAccessory
			and currentAccessory.Parent == character
			and realHandle
			and realHandle.Parent
			and fakeAccessory
			and fakeAccessory.Parent == character
			and fakeHandle
			and fakeHandle.Parent
			and fakeJoint
			and fakeJoint.Parent
			and fakeBaseC0

		if not tailIsValid then
			local now = os.clock()

			if now - lastSearchTime >= 1 then
				lastSearchTime = now
				findAndAttachTail(character)
			end

			return
		end

		local targetYaw = 0

		if TA.Enabled then
			animationTime +=
				deltaTime * (tonumber(TA.Speed) or 10)

			targetYaw =
				math.sin(animationTime)
				* math.rad(
					tonumber(TA.Amplitude) or 35
				)
		end

		local smoothing =
			1 - math.exp(-deltaTime * 18)

		currentYaw +=
			(targetYaw - currentYaw) * smoothing

		fakeJoint.C0 =
			fakeBaseC0
			* CFrame.Angles(0, currentYaw, 0)

		realHandle.CFrame = fakeHandle.CFrame

		pcall(function()
			realHandle.AssemblyLinearVelocity =
				Vector3.new(0, 0, 30)

			realHandle.AssemblyAngularVelocity =
				Vector3.new(0, 0, 0)
		end)
	end)

TA._conn = renderConnection

TA._cleanup = function()
	bindGeneration += 1

	disconnect(renderConnection)
	disconnect(characterConnection)
	disconnect(childConnection)

	renderConnection = nil
	characterConnection = nil
	childConnection = nil

	TA._conn = nil
	TA._charConn = nil
	TA._childConn = nil

	releaseCurrentAccessory(true)

	currentCharacter = nil
end

Description

AI is used to make guide for you in the script ---------------------------------------------- **Moony2HP (@tungsten88474)** i found it on scriptblox and i just made it server **XII (@syniyrz)** how did you even make it server 😭 **Moony2HP (@tungsten88474)** skidding, i look at a tool hat hold script or whatever that let you hold ur hats as tools saw that it destroyed the weld and used cframe and i kinda ported that over **XII (@syniyrz)** ouuuu creative **Moony2HP (@tungsten88474)** yeah

Comments (0)

Log in to join the conversation

  • Be the first to comment.

Frequently asked questions

How do I use the Tail Wag (server) script?
Copy the script code above, join Universal on Roblox, then paste and execute the code in your executor. The features activate as soon as the script runs.
Is Tail Wag (server) still working?
Yes — Tail Wag (server) is currently marked as working (last updated Sep 1, 2026). If it breaks after a Roblox update, check back soon for a refreshed version.
Which executor works with Tail Wag (server)?
This script works with most popular Roblox executors. Check our executors page to find a trusted free or paid executor for your device.