Get a pet cat!
Key RequiredUniversal

Get a pet cat!

Universal ScriptHellogoovy727Hellogoovy727 2K views 4.2K executesUpdated Jun 12, 2026
3.3/ 5(3 votes)
2167% Positive
Lua Scriptโ€ข2209 lines (45586 chars)
--// ๐Ÿฑ SMART CAT COMPANION AI
--// FULL LIFE + BEHAVIOR + RAGDOLL + REAL JUMP + WALL SCRATCHING + SIT/SLEEP/LOAF
--// Model: 18314616360
--// Client-side LocalScript
--// Put in StarterPlayer > StarterPlayerScripts
--//
--// UPDATED:
--// โฑ๏ธ Total lifetime = 1 hour
--// ๐ŸŒฑ Growth stops at 30 minutes
--// โค๏ธ Pet by TOUCHING the cat model (no ProximityPrompt)
--// ๐Ÿพ Improved petting animation
--// ๐Ÿ›ก๏ธ Animation offsets are isolated so limbs don't twist the whole body

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

local player = Players.LocalPlayer
local rng = Random.new()

--------------------------------------------------
-- SETTINGS
--------------------------------------------------

local MODEL_ID = "rbxassetid://18314616360"

-- LIFE:
-- The cat lives for 60 minutes.
-- Growth reaches FINAL_SIZE at 30 minutes and stays there
-- for the remaining 30 minutes.
local LIFE_TIME = 60 * 60
local GROWTH_TIME = 30 * 60

local START_SIZE = 0.70
local FINAL_SIZE = 1.15

local SPAWN_DISTANCE = 6
local STOP_DISTANCE = 5
local FOLLOW_RESUME_DISTANCE = 18
local WALK_SPEED = 8
local RUN_SPEED = 12
local RUN_DISTANCE = 30
local TOO_FAR_DISTANCE = 45

-- Jump
local JUMP_POWER = 45
local RECOVERY_JUMP_POWER = 55
local JUMP_COOLDOWN = 1.25
local MAX_JUMP_TIME = 1.25
local LANDING_BUFFER = 0.12
local JUMP_ANIMATION_TIME = 0.62

-- Wall detection
local WALL_CHECK_DISTANCE = 4.5
local WALL_SCRATCH_DISTANCE = 4
local WALL_SCRATCH_CHANCE = 0.20
local WALL_SCRATCH_MIN_TIME = 2
local WALL_SCRATCH_MAX_TIME = 4
local WALL_SCRATCH_COOLDOWN = 10

-- Other behavior
local SMELL_CHANCE = 0.10
local GROOM_CHANCE = 0.08
local PAW_CHANCE = 0.05
local STRETCH_CHANCE = 0.06
local WATCH_CHANCE = 0.06
local PLAY_CHANCE = 0.07
local ZOOMIE_CHANCE = 0.06
local EXPLORE_CHANCE = 0.25
local FOOD_SEARCH_CHANCE = 0.16
local WATER_CHANCE = 0.07
local CHASE_CHANCE = 0.05

-- Custom resting behaviors
local SIT_CHANCE = 0.08
local SLEEP_CHANCE = 0.045
local LOAF_CHANCE = 0.075

local SIT_MIN_TIME = 3
local SIT_MAX_TIME = 7
local SLEEP_MIN_TIME = 8
local SLEEP_MAX_TIME = 16
local LOAF_MIN_TIME = 4
local LOAF_MAX_TIME = 9

local SLEEP_HEAD_DROP = math.rad(22)
local LOAF_FRONT_TUCK = math.rad(48)
local LOAF_BACK_TUCK = math.rad(62)

local FOOD_SEARCH_RADIUS = 45
local WATER_SEARCH_RADIUS = 40
local CHASE_RADIUS = 25

-- Touch petting
local PET_TIME = 2.8
local PET_COOLDOWN = 0.35
local PET_TOUCH_DISTANCE = 9

local RANDOM_SOUND_ID = "rbxassetid://133371725828981"
local MIN_SOUND_DELAY = 5
local MAX_SOUND_DELAY = 15

local RAGDOLL_ON_DEATH = true
local HIGH_SPEED_IMPACT = 70
local HIGH_FALL_DISTANCE = 35

--------------------------------------------------
-- PLAYER
--------------------------------------------------

local character = player.Character or player.CharacterAdded:Wait()
local playerRoot = character:WaitForChild("HumanoidRootPart")

--------------------------------------------------
-- LOAD CAT
--------------------------------------------------

local objects = game:GetObjects(MODEL_ID)
if not objects or not objects[1] then
	warn("๐Ÿฑ Could not load cat model.")
	return
end

local rig = objects[1]
rig.Parent = workspace

local humanoid = rig:FindFirstChildOfClass("Humanoid")
local rigRoot = rig:FindFirstChild("HumanoidRootPart")

if not humanoid or not rigRoot then
	warn("๐Ÿฑ Cat needs a Humanoid and HumanoidRootPart.")
	rig:Destroy()
	return
end

--------------------------------------------------
-- LIFE
--------------------------------------------------

local birthTime = time()
local currentScale = START_SIZE
local catIsDead = false
local deathProcessed = false
local deathCause = "Unknown"

--------------------------------------------------
-- PHYSICS
--------------------------------------------------

humanoid.BreakJointsOnDeath = false
humanoid.RequiresNeck = false
humanoid.UseJumpPower = true
humanoid.JumpPower = JUMP_POWER
humanoid.WalkSpeed = WALK_SPEED
humanoid.AutoRotate = true

pcall(function()
	humanoid:SetStateEnabled(Enum.HumanoidStateType.Seated, false)
end)

local function configureCollision()
	for _, obj in ipairs(rig:GetDescendants()) do
		if obj:IsA("BasePart") then
			obj.Anchored = false
			obj.CanCollide = true
			obj.CanTouch = true
			obj.CanQuery = true
			obj.Massless = false
			pcall(function()
				obj.CollisionGroup = "Default"
			end)
		end
	end
end

configureCollision()

rig:PivotTo(playerRoot.CFrame * CFrame.new(0, 0, -SPAWN_DISTANCE))

pcall(function()
	rig:ScaleTo(START_SIZE)
end)

--------------------------------------------------
-- ANIMATOR
--------------------------------------------------

local animator = humanoid:FindFirstChildOfClass("Animator")
if not animator then
	animator = Instance.new("Animator")
	animator.Parent = humanoid
end

local walkAnimation = Instance.new("Animation")
walkAnimation.AnimationId =
	humanoid.RigType == Enum.HumanoidRigType.R15
	and "rbxassetid://913402848"
	or "rbxassetid://180426354"

local walkTrack
pcall(function()
	walkTrack = animator:LoadAnimation(walkAnimation)
end)

if walkTrack then
	walkTrack.Looped = true
	walkTrack.Priority = Enum.AnimationPriority.Movement
end

--------------------------------------------------
-- SOUND
--------------------------------------------------

local sound = Instance.new("Sound")
sound.Name = "RandomCatSound"
sound.SoundId = RANDOM_SOUND_ID
sound.Volume = 1
sound.RollOffMinDistance = 5
sound.RollOffMaxDistance = 60
sound.Parent = rigRoot

local scratchSound = Instance.new("Sound")
scratchSound.Name = "WallScratchSound"
scratchSound.SoundId = RANDOM_SOUND_ID
scratchSound.Volume = 0.35
scratchSound.PlaybackSpeed = 1.35
scratchSound.RollOffMinDistance = 4
scratchSound.RollOffMaxDistance = 35
scratchSound.Parent = rigRoot

--------------------------------------------------
-- TOUCH PETTING
--------------------------------------------------

local petting = false
local petUntil = 0
local lastPetTouch = -math.huge
local petConnectionList = {}

local function disconnectPetTouches()
	for _, connection in ipairs(petConnectionList) do
		if connection then
			connection:Disconnect()
		end
	end
	table.clear(petConnectionList)
end

--------------------------------------------------
-- RAYCAST
--------------------------------------------------

local rayParams = RaycastParams.new()
rayParams.FilterType = Enum.RaycastFilterType.Exclude
rayParams.FilterDescendantsInstances = {rig, character}
rayParams.IgnoreWater = true

--------------------------------------------------
-- FIND MOTOR6DS
--------------------------------------------------

local function findJoint(...)
	local names = {...}

	for _, obj in ipairs(rig:GetDescendants()) do
		if obj:IsA("Motor6D") then
			for _, wanted in ipairs(names) do
				if obj.Name:lower() == wanted:lower() then
					return obj
				end
			end
		end
	end

	for _, obj in ipairs(rig:GetDescendants()) do
		if obj:IsA("Motor6D") then
			local lower = obj.Name:lower()
			for _, wanted in ipairs(names) do
				if string.find(lower, wanted:lower(), 1, true) then
					return obj
				end
			end
		end
	end

	return nil
end

local leftShoulder = findJoint("Left Shoulder", "LeftShoulder", "Left Arm")
local rightShoulder = findJoint("Right Shoulder", "RightShoulder", "Right Arm")
local leftHip = findJoint("Left Hip", "LeftHip", "Left Leg")
local rightHip = findJoint("Right Hip", "RightHip", "Right Leg")
local neck = findJoint("Neck")

local joints = {leftShoulder, rightShoulder, leftHip, rightHip, neck}
local originalTransforms = {}

for _, joint in ipairs(joints) do
	if joint then
		originalTransforms[joint] = joint.Transform
	end
end

--------------------------------------------------
-- CUSTOM ANIMATION SYSTEM
--------------------------------------------------

local function setJoint(joint, offset, weight)
	if not joint then return end

	local original = originalTransforms[joint] or CFrame.identity

	-- Every animation is calculated from the original transform.
	-- This prevents one behavior from accumulating rotations into another.
	joint.Transform = original:Lerp(original * offset, weight or 1)
end

local function resetCustomAnimation()
	for joint, original in pairs(originalTransforms) do
		if joint and joint.Parent then
			joint.Transform = original
		end
	end
end

--------------------------------------------------
-- STATES
--------------------------------------------------

local state = "FOLLOWING"
local stateUntil = 0
local targetPosition = nil
local behaviorTarget = nil
local randomDirection = nil

local jumping = false
local jumpStartedAt = 0
local jumpEndTime = 0
local lastJumpTime = -math.huge

local scratching = false
local scratchUntil = 0
local scratchNormal = nil
local scratchWallPosition = nil
local lastScratchTime = -math.huge

local resting = false
local restUntil = 0

local recovering = false
local recoveryUntil = 0

local lastPosition = rigRoot.Position
local stuckTimer = 0

--------------------------------------------------
-- WALK ANIMATION
--------------------------------------------------

local function stopWalk()
	if walkTrack and walkTrack.IsPlaying then
		walkTrack:Stop(0.12)
	end
end

local function playWalk()
	if walkTrack and not walkTrack.IsPlaying then
		walkTrack:Play(0.12)
	end
end

--------------------------------------------------
-- STAND
--------------------------------------------------

local function forceStand()
	if humanoid.Sit then
		humanoid.Sit = false
	end

	if humanoid:GetState() == Enum.HumanoidStateType.Seated then
		humanoid:ChangeState(Enum.HumanoidStateType.GettingUp)
	end
end

--------------------------------------------------
-- GROUND
--------------------------------------------------

local function isGrounded()
	if humanoid.FloorMaterial ~= Enum.Material.Air then
		return true
	end

	local result = workspace:Raycast(
		rigRoot.Position + Vector3.new(0, 0.5, 0),
		Vector3.new(0, -4.5, 0),
		rayParams
	)

	return result ~= nil
end

--------------------------------------------------
-- WALL RAY
--------------------------------------------------

local function rayHit(direction, distance)
	local flat = Vector3.new(direction.X, 0, direction.Z)

	if flat.Magnitude < 0.05 then
		return nil
	end

	return workspace:Raycast(
		rigRoot.Position + Vector3.new(0, 1.4, 0),
		flat.Unit * distance,
		rayParams
	)
end

--------------------------------------------------
-- REAL JUMP ANIMATION
--------------------------------------------------

local function updateJumpAnimation()
	if not jumping or catIsDead then return end

	local elapsed = time() - jumpStartedAt
	local progress = math.clamp(elapsed / JUMP_ANIMATION_TIME, 0, 1)
	local tuck = math.sin(progress * math.pi)

	local frontAngle = math.rad(-18) - math.rad(42) * tuck
	local backAngle = math.rad(8) + math.rad(40) * tuck

	setJoint(leftShoulder,
		CFrame.Angles(frontAngle, 0, math.rad(-10)), 1)

	setJoint(rightShoulder,
		CFrame.Angles(frontAngle, 0, math.rad(10)), 1)

	setJoint(leftHip,
		CFrame.Angles(backAngle, 0, 0), 1)

	setJoint(rightHip,
		CFrame.Angles(backAngle, 0, 0), 1)

	if neck then
		setJoint(neck,
			CFrame.Angles(math.rad(-8) - math.rad(8) * tuck, 0, 0), 1)
	end
end

--------------------------------------------------
-- LANDING
--------------------------------------------------

local function landingAnimation()
	if catIsDead then return end

	local started = time()

	task.spawn(function()
		while rig.Parent and not catIsDead and time() - started < 0.18 do
			local a = math.clamp((time() - started) / 0.18, 0, 1)
			local squash = math.sin(a * math.pi)

			setJoint(leftShoulder,
				CFrame.Angles(math.rad(12) * squash, 0, math.rad(-5) * squash), 0.9)

			setJoint(rightShoulder,
				CFrame.Angles(math.rad(12) * squash, 0, math.rad(5) * squash), 0.9)

			setJoint(leftHip,
				CFrame.Angles(math.rad(-10) * squash, 0, 0), 0.9)

			setJoint(rightHip,
				CFrame.Angles(math.rad(-10) * squash, 0, 0), 0.9)

			RunService.Heartbeat:Wait()
		end

		resetCustomAnimation()
	end)
end

--------------------------------------------------
-- JUMP
--------------------------------------------------

local function tryJump(force)
	if catIsDead or petting or jumping or humanoid.Health <= 0 then
		return false
	end

	if time() - lastJumpTime < JUMP_COOLDOWN then
		return false
	end

	if not force and not isGrounded() then
		return false
	end

	forceStand()

	jumping = true
	jumpStartedAt = time()
	jumpEndTime = time() + MAX_JUMP_TIME
	lastJumpTime = time()

	stopWalk()

	humanoid.JumpPower = force and RECOVERY_JUMP_POWER or JUMP_POWER
	humanoid.Jump = true
	humanoid:ChangeState(Enum.HumanoidStateType.Jumping)

	updateJumpAnimation()

	return true
end

local function updateJump()
	if not jumping then return false end

	updateJumpAnimation()

	local now = time()

	if isGrounded() and now - jumpStartedAt > LANDING_BUFFER then
		jumping = false
		humanoid.JumpPower = JUMP_POWER
		landingAnimation()
		return false
	end

	if now >= jumpEndTime then
		jumping = false
		humanoid.JumpPower = JUMP_POWER
		landingAnimation()
		return false
	end

	humanoid:Move(humanoid.MoveDirection, false)
	return true
end

--------------------------------------------------
-- WALL SCRATCHING
--------------------------------------------------

local function isScratchable(part)
	if not part or not part:IsA("BasePart") then
		return false
	end

	if part:IsDescendantOf(rig) or part:IsDescendantOf(character) then
		return false
	end

	if part.Transparency >= 0.95 or not part.CanCollide then
		return false
	end

	if CollectionService:HasTag(part, "Scratchable") then
		return true
	end

	local n = part.Name:lower()

	return
		string.find(n, "wall", 1, true) ~= nil
		or string.find(n, "wood", 1, true) ~= nil
		or string.find(n, "tree", 1, true) ~= nil
		or string.find(n, "door", 1, true) ~= nil
		or string.find(n, "box", 1, true) ~= nil
		or string.find(n, "scratch", 1, true) ~= nil
end

local function findScratchWall()
	local forward = rigRoot.CFrame.LookVector
	local hit = rayHit(forward, WALL_SCRATCH_DISTANCE)

	if hit and isScratchable(hit.Instance) then
		return hit
	end

	local right = rigRoot.CFrame.RightVector

	for _, dir in ipairs({
		(forward + right * 0.35).Unit,
		(forward - right * 0.35).Unit
	}) do
		local sideHit = rayHit(dir, WALL_SCRATCH_DISTANCE)

		if sideHit and isScratchable(sideHit.Instance) then
			return sideHit
		end
	end

	return nil
end

local function startWallScratch(hit)
	if scratching or jumping or petting or catIsDead then
		return false
	end

	if time() - lastScratchTime < WALL_SCRATCH_COOLDOWN then
		return false
	end

	if not hit or not hit.Instance then
		return false
	end

	scratching = true
	lastScratchTime = time()
	scratchUntil = time() + rng:NextNumber(WALL_SCRATCH_MIN_TIME, WALL_SCRATCH_MAX_TIME)
	scratchWallPosition = hit.Position
	scratchNormal = hit.Normal

	state = "SCRATCHING"

	stopWalk()
	humanoid:Move(Vector3.zero, false)
	humanoid.WalkSpeed = 0
	humanoid.AutoRotate = false

	local p = rigRoot.Position
	local target = Vector3.new(hit.Position.X, p.Y, hit.Position.Z)

	if (target - p).Magnitude > 0.05 then
		rigRoot.CFrame = CFrame.lookAt(p, target)
	end

	return true
end

local function updateScratch()
	if not scratching then return end

	forceStand()
	humanoid:Move(Vector3.zero, false)

	if scratchWallPosition then
		local p = rigRoot.Position
		local target = Vector3.new(scratchWallPosition.X, p.Y, scratchWallPosition.Z)

		if (target - p).Magnitude > 0.05 then
			rigRoot.CFrame = CFrame.lookAt(p, target)
		end
	end

	local now = time()
	local phase = math.sin(now * 15)
	local paw = math.rad(48) * phase

	setJoint(leftShoulder,
		CFrame.Angles(math.rad(-38) + paw, 0, math.rad(-8)), 1)

	setJoint(rightShoulder,
		CFrame.Angles(math.rad(-38) - paw, 0, math.rad(8)), 1)

	if neck then
		setJoint(neck,
			CFrame.Angles(math.rad(8), math.sin(now * 4) * math.rad(4), 0), 0.8)
	end

	if not scratchSound.IsPlaying and math.abs(phase) > 0.95 then
		scratchSound:Play()
	end

	if now >= scratchUntil then
		scratching = false
		scratchWallPosition = nil
		scratchNormal = nil

		humanoid.AutoRotate = true
		humanoid.WalkSpeed = WALK_SPEED

		resetCustomAnimation()

		state = "IDLE"
		stateUntil = now + 2
	end
end

--------------------------------------------------
-- RESTING
--------------------------------------------------

local function updateRestAnimation()
	if catIsDead or not resting then
		return
	end

	local now = time()

	if state == "SITTING" then
		local breathe = math.sin(now * 1.8) * math.rad(1.5)

		setJoint(leftHip,
			CFrame.Angles(math.rad(-58), 0, math.rad(-3)), 1)

		setJoint(rightHip,
			CFrame.Angles(math.rad(-58), 0, math.rad(3)), 1)

		setJoint(leftShoulder,
			CFrame.Angles(math.rad(8) + breathe, 0, math.rad(-3)), 1)

		setJoint(rightShoulder,
			CFrame.Angles(math.rad(8) - breathe, 0, math.rad(3)), 1)

		setJoint(neck,
			CFrame.Angles(math.rad(-4), math.sin(now * 1.4) * math.rad(2), 0), 0.8)

	elseif state == "LOAFING" then
		local breathe = math.sin(now * 1.7) * math.rad(1.2)

		setJoint(leftShoulder,
			CFrame.Angles(LOAF_FRONT_TUCK + breathe, 0, math.rad(-2)), 1)

		setJoint(rightShoulder,
			CFrame.Angles(LOAF_FRONT_TUCK - breathe, 0, math.rad(2)), 1)

		setJoint(leftHip,
			CFrame.Angles(LOAF_BACK_TUCK, 0, math.rad(-2)), 1)

		setJoint(rightHip,
			CFrame.Angles(LOAF_BACK_TUCK, 0, math.rad(2)), 1)

		setJoint(neck,
			CFrame.Angles(math.rad(8), 0, 0), 0.9)

	elseif state == "SLEEPING" then
		local breathe = math.sin(now * 1.15) * math.rad(1.8)
		local tinyHead = math.sin(now * 0.8) * math.rad(1.5)

		setJoint(leftShoulder,
			CFrame.Angles(math.rad(50) + breathe, 0, math.rad(-5)), 1)

		setJoint(rightShoulder,
			CFrame.Angles(math.rad(50) - breathe, 0, math.rad(5)), 1)

		setJoint(leftHip,
			CFrame.Angles(math.rad(68), 0, math.rad(-4)), 1)

		setJoint(rightHip,
			CFrame.Angles(math.rad(68), 0, math.rad(4)), 1)

		setJoint(neck,
			CFrame.Angles(SLEEP_HEAD_DROP + tinyHead, 0, 0), 1)
	end
end

--------------------------------------------------
-- GENERAL LIMB ANIMATION
--------------------------------------------------

local function updateLimbAnimation()
	if catIsDead or scratching then return end

	if resting and not jumping then
		updateRestAnimation()
		return
	end

	if jumping then
		updateJumpAnimation()
		return
	end

	local now = time()

	if state == "FOLLOWING"
		or state == "WANDERING"
		or state == "RUNNING"
		or state == "RUNNING_RANDOM" then

		local speed = (state == "RUNNING" or state == "RUNNING_RANDOM") and 13 or 10
		local amount = (state == "RUNNING" or state == "RUNNING_RANDOM")
			and math.rad(30)
			or math.rad(15)

		local swing = math.sin(now * speed) * amount

		setJoint(leftShoulder, CFrame.Angles(swing, 0, 0), 0.7)
		setJoint(rightShoulder, CFrame.Angles(-swing, 0, 0), 0.7)
		setJoint(leftHip, CFrame.Angles(-swing, 0, 0), 0.7)
		setJoint(rightHip, CFrame.Angles(swing, 0, 0), 0.7)
		return
	end

	if state == "GROOMING" then
		local paw = math.sin(now * 9) * math.rad(25)

		setJoint(leftShoulder,
			CFrame.Angles(math.rad(-35) + paw, math.rad(5), math.rad(-10)), 0.9)

		setJoint(neck,
			CFrame.Angles(math.rad(12), math.sin(now * 4) * math.rad(8), 0), 0.8)
		return
	end

	if state == "PAWING" then
		local paw = math.sin(now * 12) * math.rad(35)

		setJoint(leftShoulder,
			CFrame.Angles(math.rad(-25) + paw, 0, math.rad(-8)), 1)

		setJoint(rightShoulder,
			CFrame.Angles(math.rad(-25) - paw, 0, math.rad(8)), 1)
		return
	end

	if state == "STRETCHING" then
		local s = math.sin((now % 2) / 2 * math.pi)

		setJoint(leftShoulder,
			CFrame.Angles(math.rad(-25) * s, 0, 0), 1)

		setJoint(rightShoulder,
			CFrame.Angles(math.rad(-25) * s, 0, 0), 1)

		setJoint(leftHip,
			CFrame.Angles(math.rad(18) * s, 0, 0), 1)

		setJoint(rightHip,
			CFrame.Angles(math.rad(18) * s, 0, 0), 1)
		return
	end

	if state == "SMELLING"
		or state == "SEARCHING_FOOD"
		or state == "SEARCHING_WATER" then

		local sniff = math.sin(now * 10) * math.rad(5)

		setJoint(neck,
			CFrame.Angles(math.rad(-18), 0, 0), 0.9)

		setJoint(leftShoulder,
			CFrame.Angles(sniff, 0, math.rad(-4)), 0.8)

		setJoint(rightShoulder,
			CFrame.Angles(-sniff, 0, math.rad(4)), 0.8)
		return
	end

	if state == "EATING" then
		setJoint(neck,
			CFrame.Angles(math.rad(20) + math.sin(now * 5) * math.rad(8), 0, 0), 1)
		return
	end

	if state == "DRINKING" then
		setJoint(neck,
			CFrame.Angles(math.rad(30) + math.sin(now * 5) * math.rad(5), 0, 0), 1)
		return
	end

	if state == "WATCHING" or state == "CHASING" then
		setJoint(neck,
			CFrame.Angles(math.rad(-3), math.sin(now * 2) * math.rad(4), 0), 0.8)
		return
	end

	if state == "PLAYING" then
		local bounce = math.sin(now * 9) * math.rad(18)

		setJoint(leftShoulder,
			CFrame.Angles(bounce, 0, math.rad(-10)), 0.9)

		setJoint(rightShoulder,
			CFrame.Angles(-bounce, 0, math.rad(10)), 0.9)
		return
	end

	-- Improved petting animation.
	if state == "PETTING" then
		local t = math.clamp(PET_TIME - math.max(petUntil - now, 0), 0, PET_TIME)
		local cycle = t * 2.6

		-- Gentle happy body movement.
		local sway = math.sin(cycle) * math.rad(3)
		local headNod = math.sin(cycle * 0.72) * math.rad(4)

		-- Alternating little paw lifts, rather than rotating the entire body.
		local leftPet = math.max(0, math.sin(cycle * 1.15))
		local rightPet = math.max(0, math.sin(cycle * 1.15 + math.pi))

		local leftPawAngle = math.rad(-16) - leftPet * math.rad(24)
		local rightPawAngle = math.rad(-16) - rightPet * math.rad(24)

		setJoint(leftShoulder,
			CFrame.Angles(leftPawAngle, sway * 0.15, math.rad(-5)), 0.9)

		setJoint(rightShoulder,
			CFrame.Angles(rightPawAngle, -sway * 0.15, math.rad(5)), 0.9)

		-- Small, safe head tilt/nod.
		setJoint(neck,
			CFrame.Angles(headNod, sway, 0), 0.75)

		return
	end

	-- Idle breathing.
	local breathe = math.sin(now * 2) * math.rad(2)

	setJoint(leftShoulder,
		CFrame.Angles(breathe, 0, 0), 0.5)

	setJoint(rightShoulder,
		CFrame.Angles(-breathe, 0, 0), 0.5)
end

--------------------------------------------------
-- MOVEMENT
--------------------------------------------------

local function moveDirection(direction, speed)
	if catIsDead or jumping or petting or scratching or resting then
		return
	end

	local flat = Vector3.new(direction.X, 0, direction.Z)

	if flat.Magnitude < 0.05 then
		return
	end

	flat = flat.Unit

	local wallHit = rayHit(flat, WALL_CHECK_DISTANCE)

	if wallHit
		and isScratchable(wallHit.Instance)
		and rng:NextNumber() < WALL_SCRATCH_CHANCE
		and time() - lastScratchTime >= WALL_SCRATCH_COOLDOWN then

		startWallScratch(wallHit)
		return
	end

	local forwardHit = rayHit(flat, WALL_CHECK_DISTANCE)

	if forwardHit then
		local right = Vector3.new(-flat.Z, 0, flat.X)
		local left = -right

		local leftHit = rayHit(left, 3.5)
		local rightHit = rayHit(right, 3.5)

		if not leftHit then
			flat = (flat * 0.25 + left * 0.75).Unit
		elseif not rightHit then
			flat = (flat * 0.25 + right * 0.75).Unit
		elseif isGrounded() then
			tryJump(false)
			return
		end
	end

	humanoid.WalkSpeed = speed
	humanoid:Move(flat, false)
	playWalk()
end

local function stopMoving()
	if jumping then return end

	humanoid:Move(Vector3.zero, false)
	stopWalk()
	forceStand()
end

local function lookAt(position)
	if jumping or petting or scratching or resting then
		return
	end

	local p = rigRoot.Position
	local target = Vector3.new(position.X, p.Y, position.Z)

	if (target - p).Magnitude > 0.05 then
		rigRoot.CFrame = CFrame.lookAt(p, target)
	end
end

--------------------------------------------------
-- BASIC BEHAVIORS
--------------------------------------------------

local function startIdle()
	if jumping or petting or scratching or resting then return end

	state = "IDLE"
	stopMoving()

	humanoid.WalkSpeed = 0
	stateUntil = time() + rng:NextNumber(2, 5)
end

local function startTimedBehavior(name, minTime, maxTime)
	if jumping or petting or scratching or resting then return end

	state = name
	stopMoving()

	humanoid.WalkSpeed = 0
	stateUntil = time() + rng:NextNumber(minTime, maxTime)
end

local function startSmelling()
	startTimedBehavior("SMELLING", 2, 4)
end

local function startGrooming()
	startTimedBehavior("GROOMING", 2, 5)
end

local function startPawing()
	startTimedBehavior("PAWING", 1, 2.5)
end

local function startStretching()
	startTimedBehavior("STRETCHING", 1.5, 3)
end

local function startWatching()
	startTimedBehavior("WATCHING", 1.5, 4)
end

--------------------------------------------------
-- FOOD / WATER
--------------------------------------------------

local function findNamedPart(radius, names)
	local closest
	local closestDistance = radius

	for _, obj in ipairs(workspace:GetDescendants()) do
		if obj:IsA("BasePart")
			and not obj:IsDescendantOf(rig)
			and not obj:IsDescendantOf(character) then

			local n = obj.Name:lower()

			for _, wanted in ipairs(names) do
				if string.find(n, wanted, 1, true) then
					local d = (obj.Position - rigRoot.Position).Magnitude

					if d < closestDistance then
						closest = obj
						closestDistance = d
					end

					break
				end
			end
		end
	end

	return closest
end

local function startFoodSearch()
	local food = findNamedPart(
		FOOD_SEARCH_RADIUS,
		{"food", "fish", "meat", "catfood"}
	)

	if not food then
		return false
	end

	behaviorTarget = food
	state = "SEARCHING_FOOD"
	stateUntil = time() + 10

	return true
end

local function startWaterSearch()
	local water = findNamedPart(
		WATER_SEARCH_RADIUS,
		{"water", "bowl", "drink"}
	)

	if not water then
		return false
	end

	behaviorTarget = water
	state = "SEARCHING_WATER"
	stateUntil = time() + 10

	return true
end

local function updateFood()
	if not behaviorTarget or not behaviorTarget.Parent then
		startIdle()
		return
	end

	local d = (behaviorTarget.Position - rigRoot.Position).Magnitude

	if d <= 3 then
		stopMoving()

		state = "EATING"
		stateUntil = time() + rng:NextNumber(3, 6)

		return
	end

	if time() >= stateUntil then
		startIdle()
		return
	end

	moveDirection(
		behaviorTarget.Position - rigRoot.Position,
		WALK_SPEED
	)
end

local function updateEating()
	if behaviorTarget and behaviorTarget.Parent then
		lookAt(behaviorTarget.Position)
	end

	stopMoving()

	if time() >= stateUntil then
		behaviorTarget = nil
		startIdle()
	end
end

local function updateWater()
	if not behaviorTarget or not behaviorTarget.Parent then
		startIdle()
		return
	end

	local d = (behaviorTarget.Position - rigRoot.Position).Magnitude

	if d <= 3 then
		stopMoving()

		state = "DRINKING"
		stateUntil = time() + rng:NextNumber(2, 5)

		return
	end

	if time() >= stateUntil then
		startIdle()
		return
	end

	moveDirection(
		behaviorTarget.Position - rigRoot.Position,
		WALK_SPEED * 0.75
	)
end

local function updateDrinking()
	if behaviorTarget and behaviorTarget.Parent then
		lookAt(behaviorTarget.Position)
	end

	stopMoving()

	if time() >= stateUntil then
		behaviorTarget = nil
		startIdle()
	end
end

--------------------------------------------------
-- PLAY / ZOOMIES / EXPLORE
--------------------------------------------------

local function randomFlatDirection()
	local d = Vector3.new(
		rng:NextNumber(-1, 1),
		0,
		rng:NextNumber(-1, 1)
	)

	if d.Magnitude < 0.05 then
		d = rigRoot.CFrame.LookVector
	end

	return Vector3.new(d.X, 0, d.Z).Unit
end

local function startPlay()
	state = "PLAYING"
	randomDirection = randomFlatDirection()
	stateUntil = time() + rng:NextNumber(1.5, 3)
end

local function startZoomies()
	state = "RUNNING_RANDOM"
	randomDirection = randomFlatDirection()
	stateUntil = time() + rng:NextNumber(2, 5)
end

local function getExplorePosition()
	local angle = rng:NextNumber(0, math.pi * 2)
	local distance = rng:NextNumber(8, 35)

	local desired =
		playerRoot.Position
		+ Vector3.new(
			math.cos(angle) * distance,
			0,
			math.sin(angle) * distance
		)

	local ground = workspace:Raycast(
		desired + Vector3.new(0, 25, 0),
		Vector3.new(0, -50, 0),
		rayParams
	)

	return ground
		and ground.Position + Vector3.new(0, 2, 0)
		or desired
end

local function startExplore()
	state = "WANDERING"
	targetPosition = getExplorePosition()
	stateUntil = time() + 12
end

local function updateExplore(playerPosition)
	if (rigRoot.Position - playerPosition).Magnitude > FOLLOW_RESUME_DISTANCE then
		state = "FOLLOWING"
		targetPosition = nil
		return
	end

	if not targetPosition then
		startExplore()
		return
	end

	local d = targetPosition - rigRoot.Position
	d = Vector3.new(d.X, 0, d.Z)

	if d.Magnitude < 2 or time() >= stateUntil then
		targetPosition = nil
		startIdle()
		return
	end

	moveDirection(d, WALK_SPEED * 0.65)
end

--------------------------------------------------
-- CHASING
--------------------------------------------------

local function findMovingPart()
	local closest
	local best = CHASE_RADIUS

	for _, obj in ipairs(workspace:GetDescendants()) do
		if obj:IsA("BasePart") and not obj:IsDescendantOf(rig) then
			local v = obj.AssemblyLinearVelocity

			if v.Magnitude > 5 then
				local d = (obj.Position - rigRoot.Position).Magnitude

				if d < best then
					best = d
					closest = obj
				end
			end
		end
	end

	return closest
end

local function startChasing()
	local target = findMovingPart()

	if not target then
		return false
	end

	behaviorTarget = target
	state = "CHASING"
	stateUntil = time() + rng:NextNumber(2, 5)

	return true
end

local function updateChasing()
	if not behaviorTarget
		or not behaviorTarget.Parent
		or time() >= stateUntil then

		behaviorTarget = nil
		startIdle()
		return
	end

	moveDirection(
		behaviorTarget.Position - rigRoot.Position,
		RUN_SPEED
	)
end

--------------------------------------------------
-- SIT / SLEEP / LOAF
--------------------------------------------------

local function stopResting()
	resting = false
	restUntil = 0

	resetCustomAnimation()

	if not catIsDead then
		humanoid.AutoRotate = true
		humanoid.WalkSpeed = WALK_SPEED
	end
end

local function startSitting()
	if catIsDead or petting or scratching or jumping then return end

	resting = true
	state = "SITTING"
	restUntil = time() + rng:NextNumber(SIT_MIN_TIME, SIT_MAX_TIME)

	stopMoving()

	humanoid.WalkSpeed = 0
	humanoid.AutoRotate = false

	resetCustomAnimation()
end

local function startLoafing()
	if catIsDead or petting or scratching or jumping then return end

	resting = true
	state = "LOAFING"
	restUntil = time() + rng:NextNumber(LOAF_MIN_TIME, LOAF_MAX_TIME)

	stopMoving()

	humanoid.WalkSpeed = 0
	humanoid.AutoRotate = false

	resetCustomAnimation()
end

local function startSleeping()
	if catIsDead or petting or scratching or jumping then return end

	resting = true
	state = "SLEEPING"
	restUntil = time() + rng:NextNumber(SLEEP_MIN_TIME, SLEEP_MAX_TIME)

	stopMoving()

	humanoid.WalkSpeed = 0
	humanoid.AutoRotate = false

	resetCustomAnimation()
end

local function updateResting(playerPosition)
	if not resting then
		return false
	end

	humanoid:Move(Vector3.zero, false)
	humanoid.WalkSpeed = 0

	local distance = (rigRoot.Position - playerPosition).Magnitude

	if distance > FOLLOW_RESUME_DISTANCE * 1.35 then
		stopResting()
		state = "FOLLOWING"
		return false
	end

	if time() >= restUntil then
		stopResting()
		state = "IDLE"
		stateUntil = time() + rng:NextNumber(2, 4)
		return false
	end

	return true
end

--------------------------------------------------
-- IMPROVED TOUCH PETTING
--------------------------------------------------

local function startPetting()
	if catIsDead or petting or scratching or jumping then
		return
	end

	if (playerRoot.Position - rigRoot.Position).Magnitude > PET_TOUCH_DISTANCE then
		return
	end

	if resting then
		stopResting()
	end

	petting = true
	petUntil = time() + PET_TIME
	state = "PETTING"

	stopMoving()

	humanoid.WalkSpeed = 0
	humanoid.AutoRotate = false

	resetCustomAnimation()
end

local function onCatTouched(hit)
	if catIsDead or not hit then
		return
	end

	local hitCharacter = hit:FindFirstAncestorOfClass("Model")

	if hitCharacter ~= character then
		return
	end

	if not playerRoot or not playerRoot.Parent then
		return
	end

	if (playerRoot.Position - rigRoot.Position).Magnitude > PET_TOUCH_DISTANCE then
		return
	end

	local now = time()

	if now - lastPetTouch < PET_COOLDOWN then
		return
	end

	lastPetTouch = now

	startPetting()
end

local function connectPetTouch(part)
	if not part:IsA("BasePart") then
		return
	end

	table.insert(
		petConnectionList,
		part.Touched:Connect(onCatTouched)
	)
end

for _, obj in ipairs(rig:GetDescendants()) do
	connectPetTouch(obj)
end

rig.DescendantAdded:Connect(function(obj)
	connectPetTouch(obj)
end)

local function updatePetting()
	if not petting then
		return
	end

	humanoid:Move(Vector3.zero, false)
	humanoid.WalkSpeed = 0

	if playerRoot and playerRoot.Parent then
		local distance = (playerRoot.Position - rigRoot.Position).Magnitude

		-- Let the animation finish even if the player steps away,
		-- but don't allow a new touch to keep it running forever.
		if distance > PET_TOUCH_DISTANCE * 1.6
			and time() < petUntil then

			petUntil = math.min(
				petUntil,
				time() + 0.35
			)
		end
	end

	if time() >= petUntil then
		petting = false

		humanoid.AutoRotate = true
		humanoid.WalkSpeed = WALK_SPEED

		resetCustomAnimation()

		state = "IDLE"
		stateUntil = time() + 3
	end
end

--------------------------------------------------
-- BEHAVIOR CHOICE
--------------------------------------------------

local function chooseBehavior()
	if catIsDead or petting or scratching or resting then
		return
	end

	if rng:NextNumber() < FOOD_SEARCH_CHANCE and startFoodSearch() then
		return
	end

	if rng:NextNumber() < WATER_CHANCE and startWaterSearch() then
		return
	end

	if rng:NextNumber() < CHASE_CHANCE and startChasing() then
		return
	end

	local restRoll = rng:NextNumber()

	if restRoll < SLEEP_CHANCE then
		startSleeping()
		return
	elseif restRoll < SLEEP_CHANCE + LOAF_CHANCE then
		startLoafing()
		return
	elseif restRoll < SLEEP_CHANCE + LOAF_CHANCE + SIT_CHANCE then
		startSitting()
		return
	end

	local wall = findScratchWall()

	if wall
		and rng:NextNumber() < WALL_SCRATCH_CHANCE then

		if startWallScratch(wall) then
			return
		end
	end

	local roll = rng:NextNumber()

	if roll < 0.10 then
		startSmelling()
	elseif roll < 0.18 then
		startGrooming()
	elseif roll < 0.23 then
		startPawing()
	elseif roll < 0.29 then
		startStretching()
	elseif roll < 0.35 then
		startWatching()
	elseif roll < 0.42 then
		startPlay()
	elseif roll < 0.48 then
		startZoomies()
	elseif roll < 0.73 then
		startExplore()
	else
		startIdle()
	end
end

--------------------------------------------------
-- STUCK RECOVERY
--------------------------------------------------

local function updateStuck(dt, moving)
	if jumping or petting or scratching or not moving then
		stuckTimer = 0
		lastPosition = rigRoot.Position
		return
	end

	local moved = (rigRoot.Position - lastPosition).Magnitude

	if moved < 1.2 then
		stuckTimer += dt
	else
		stuckTimer = 0
		lastPosition = rigRoot.Position
	end

	if stuckTimer >= 1 then
		stuckTimer = 0
		recovering = true
		recoveryUntil = time() + 1.6

		if isGrounded() then
			tryJump(true)
		end
	end
end

--------------------------------------------------
-- DEATH / RAGDOLL
--------------------------------------------------

local highestFallY = rigRoot.Position.Y
local wasAirborne = false

local function updateDeathCause()
	if catIsDead then
		return
	end

	local velocity = rigRoot.AssemblyLinearVelocity

	if velocity.Magnitude >= HIGH_SPEED_IMPACT then
		deathCause = "High-speed impact"
	end

	if not isGrounded() then
		if not wasAirborne then
			wasAirborne = true
			highestFallY = rigRoot.Position.Y
		else
			highestFallY = math.max(
				highestFallY,
				rigRoot.Position.Y
			)
		end
	else
		if wasAirborne then
			if highestFallY - rigRoot.Position.Y >= HIGH_FALL_DISTANCE then
				deathCause = "Fall from height"
			end
		end

		wasAirborne = false
		highestFallY = rigRoot.Position.Y
	end
end

local function ragdollCat()
	if deathProcessed or not RAGDOLL_ON_DEATH then
		return
	end

	deathProcessed = true

	humanoid.AutoRotate = false
	humanoid.WalkSpeed = 0
	humanoid.JumpPower = 0
	humanoid.PlatformStand = true

	pcall(function()
		humanoid:ChangeState(Enum.HumanoidStateType.Physics)
	end)

	for _, joint in ipairs(rig:GetDescendants()) do
		if joint:IsA("Motor6D")
			and joint.Part0
			and joint.Part1 then

			local a0 = Instance.new("Attachment")
			a0.CFrame = joint.C0
			a0.Parent = joint.Part0

			local a1 = Instance.new("Attachment")
			a1.CFrame = joint.C1
			a1.Parent = joint.Part1

			local socket = Instance.new("BallSocketConstraint")
			socket.Attachment0 = a0
			socket.Attachment1 = a1
			socket.LimitsEnabled = true
			socket.UpperAngle = 45
			socket.TwistLimitsEnabled = true
			socket.TwistLowerAngle = -35
			socket.TwistUpperAngle = 35
			socket.Parent = joint.Part0

			joint.Enabled = false
		end
	end

	configureCollision()

	print("โ˜ ๏ธ Cat ragdolled. Cause: " .. tostring(deathCause))
end

--------------------------------------------------
-- AGE + GROWTH
--------------------------------------------------

local function updateAge()
	if catIsDead then
		return
	end

	local age = time() - birthTime

	-- Full lifetime is 60 minutes.
	if age >= LIFE_TIME then
		deathCause = "Old age"
		catIsDead = true
		state = "DEAD"

		petting = false
		jumping = false
		scratching = false
		recovering = false
		resting = false

		stopWalk()
		humanoid:Move(Vector3.zero, false)
		humanoid.WalkSpeed = 0
		humanoid.AutoRotate = false

		sound:Stop()

		task.spawn(function()
			local started = time()

			while rig.Parent and time() - started < 1.5 do
				local a = math.clamp(
					(time() - started) / 1.5,
					0,
					1
				)

				setJoint(
					leftShoulder,
					CFrame.Angles(
						math.rad(30) * a,
						0,
						math.rad(-7) * a
					),
					1
				)

				setJoint(
					rightShoulder,
					CFrame.Angles(
						math.rad(30) * a,
						0,
						math.rad(7) * a
					),
					1
				)

				setJoint(
					neck,
					CFrame.Angles(math.rad(12) * a, 0, 0),
					1
				)

				RunService.Heartbeat:Wait()
			end

			if rig.Parent then
				ragdollCat()
			end
		end)

		return
	end

	-- Growth ONLY happens during the first 30 minutes.
	local growthProgress = math.clamp(
		age / GROWTH_TIME,
		0,
		1
	)

	local smooth =
		growthProgress
		* growthProgress
		* (3 - 2 * growthProgress)

	local newScale =
		START_SIZE
		+ (FINAL_SIZE - START_SIZE) * smooth

	if math.abs(newScale - currentScale) > 0.001 then
		currentScale = newScale

		pcall(function()
			rig:ScaleTo(currentScale)
		end)
	end
end

--------------------------------------------------
-- RANDOM SOUNDS
--------------------------------------------------

task.spawn(function()
	while rig.Parent and not catIsDead do
		task.wait(
			rng:NextNumber(
				MIN_SOUND_DELAY,
				MAX_SOUND_DELAY
			)
		)

		if rig.Parent and not catIsDead then
			sound:Stop()
			sound:Play()
		end
	end
end)

--------------------------------------------------
-- MAIN AI
--------------------------------------------------

RunService.Heartbeat:Connect(function(dt)
	if not rig.Parent or catIsDead then
		return
	end

	if not character.Parent
		or not playerRoot.Parent then
		return
	end

	if humanoid.Health <= 0 then
		return
	end

	updateAge()

	if catIsDead then
		return
	end

	updateDeathCause()

	if jumping then
		updateJumpAnimation()
	end

	updateLimbAnimation()
	forceStand()

	if petting then
		updatePetting()
		return
	end

	if scratching then
		updateScratch()
		return
	end

	if updateJump() then
		return
	end

	local playerPosition = playerRoot.Position
	local distance =
		(rigRoot.Position - playerPosition).Magnitude

	--------------------------------------------------
	-- RESTING
	--------------------------------------------------

	if resting then
		updateResting(playerPosition)
		return
	end

	--------------------------------------------------
	-- SPECIAL STATES
	--------------------------------------------------

	if state == "SEARCHING_FOOD" then
		updateFood()
		return
	end

	if state == "EATING" then
		updateEating()
		return
	end

	if state == "SEARCHING_WATER" then
		updateWater()
		return
	end

	if state == "DRINKING" then
		updateDrinking()
		return
	end

	if state == "SMELLING"
		or state == "GROOMING"
		or state == "PAWING"
		or state == "STRETCHING"
		or state == "WATCHING" then

		stopMoving()

		if state == "WATCHING" then
			lookAt(playerPosition)
		end

		if time() >= stateUntil then
			startIdle()
		end

		if distance > FOLLOW_RESUME_DISTANCE then
			state = "FOLLOWING"
		end

		return
	end

	if state == "PLAYING" then
		if time() >= stateUntil or distance > 25 then
			startIdle()
			return
		end

		moveDirection(randomDirection, RUN_SPEED)
		updateStuck(dt, true)

		return
	end

	if state == "RUNNING_RANDOM" then
		if time() >= stateUntil or distance > 20 then
			startIdle()
			return
		end

		moveDirection(randomDirection, RUN_SPEED)
		updateStuck(dt, true)

		return
	end

	if state == "WANDERING" then
		updateExplore(playerPosition)
		updateStuck(dt, true)
		return
	end

	if state == "CHASING" then
		updateChasing()
		updateStuck(dt, true)
		return
	end

	--------------------------------------------------
	-- RECOVERY
	--------------------------------------------------

	if recovering then
		if time() < recoveryUntil then
			moveDirection(
				playerPosition - rigRoot.Position,
				WALK_SPEED
			)

			updateStuck(dt, true)
			return
		else
			recovering = false
		end
	end

	--------------------------------------------------
	-- FOLLOW
	--------------------------------------------------

	if distance >= TOO_FAR_DISTANCE then
		state = "RUNNING"

		moveDirection(
			playerPosition - rigRoot.Position,
			RUN_SPEED
		)

		updateStuck(dt, true)

		return
	end

	if state == "RUNNING" then
		if distance <= RUN_DISTANCE then
			state = "FOLLOWING"
			humanoid.WalkSpeed = WALK_SPEED
			return
		end

		moveDirection(
			playerPosition - rigRoot.Position,
			RUN_SPEED
		)

		updateStuck(dt, true)

		return
	end

	if state == "FOLLOWING" then
		if distance <= STOP_DISTANCE then
			stopMoving()
			humanoid.WalkSpeed = 0
			lookAt(playerPosition)

			if rng:NextNumber() < 0.012 then
				chooseBehavior()
			end

			return
		end

		moveDirection(
			playerPosition - rigRoot.Position,
			WALK_SPEED
		)

		updateStuck(dt, true)

		return
	end

	--------------------------------------------------
	-- IDLE
	--------------------------------------------------

	if state == "IDLE" then
		stopMoving()
		humanoid.WalkSpeed = 0
		lookAt(playerPosition)

		if distance > FOLLOW_RESUME_DISTANCE then
			state = "FOLLOWING"
			humanoid.WalkSpeed = WALK_SPEED
			return
		end

		if time() >= stateUntil then
			chooseBehavior()
		end
	end
end)

--------------------------------------------------
-- PLAYER RESPAWN
--------------------------------------------------

player.CharacterAdded:Connect(function(newCharacter)
	character = newCharacter
	playerRoot = newCharacter:WaitForChild("HumanoidRootPart")

	rayParams.FilterDescendantsInstances = {rig, character}

	if rig.Parent and not catIsDead then
		jumping = false
		petting = false
		scratching = false
		recovering = false
		resting = false

		behaviorTarget = nil
		targetPosition = nil

		rig:PivotTo(
			playerRoot.CFrame
			* CFrame.new(0, 0, -SPAWN_DISTANCE)
		)

		state = "FOLLOWING"

		humanoid.JumpPower = JUMP_POWER
		humanoid.WalkSpeed = WALK_SPEED
		humanoid.AutoRotate = true

		resetCustomAnimation()
		forceStand()
	end
end)

--------------------------------------------------
-- HUMANOID DEATH
--------------------------------------------------

humanoid.Died:Connect(function()
	if catIsDead then
		return
	end

	if deathCause == "Unknown" then
		if rigRoot.AssemblyLinearVelocity.Magnitude >= HIGH_SPEED_IMPACT then
			deathCause = "High-speed impact"
		else
			deathCause = "Unexpected accident"
		end
	end

	catIsDead = true
	state = "DEAD"

	jumping = false
	petting = false
	scratching = false
	recovering = false
	resting = false

	stopWalk()

	humanoid.WalkSpeed = 0
	humanoid.JumpPower = 0
	humanoid.AutoRotate = false

	sound:Stop()

	task.defer(function()
		if rig.Parent then
			ragdollCat()
		end
	end)

	print("โ˜ ๏ธ Cat died from: " .. tostring(deathCause))
end)

--------------------------------------------------
-- ANTI-STUCK / COLLISION
--------------------------------------------------

task.spawn(function()
	while rig.Parent and not catIsDead do
		task.wait(0.15)

		if humanoid.Health <= 0 then
			break
		end

		forceStand()
		configureCollision()

		if not jumping
			and not petting
			and not scratching
			and not resting then

			local moving =
				humanoid.MoveDirection.Magnitude > 0.2

			local velocity =
				rigRoot.AssemblyLinearVelocity

			if moving
				and velocity.Magnitude < 0.15
				and isGrounded() then

				tryJump(true)
			end
		end
	end
end)

--------------------------------------------------
-- DONE
--------------------------------------------------

print("๐Ÿฑ Smart Cat Companion AI loaded!")
print("----------------------------------")
print("โค๏ธ Petting: TOUCH CAT")
print("๐Ÿพ Improved petting animation: ON")
print("๐Ÿ– Food: ON")
print("๐Ÿ’ง Water: ON")
print("๐Ÿ‘ƒ Sniffing: ON")
print("๐Ÿงผ Grooming: ON")
print("๐Ÿพ Pawing: ON")
print("๐Ÿงฑ Wall scratching: ON")
print("๐Ÿธ Real jump animation: ON")
print("๐ŸŒฑ Growth: ON")
print("๐Ÿ“ˆ Growth duration: 30 minutes")
print("โฑ๏ธ Total lifespan: 1 hour")
print("๐Ÿฆด Ragdoll: ON")
print("๐Ÿ”„ Respawn protection: ON")
print("----------------------------------")
print("๐Ÿฑ Custom sitting: ON")
print("๐Ÿ˜ด Custom sleeping: ON")
print("๐Ÿž Custom loafing: ON")

Features & Information

Get a pet cat!

Community Comments (0)

No comments yet for this script.

More Scripts for Universal Scripts