--// ๐Ÿฑ 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")