
KeylessUniversal

--// ๐ฑ 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")No comments yet for this script.



