Cat companion
Key RequiredUniversal

Cat companion

Universal ScriptHellogoovy727Hellogoovy727 800 views 1.5K executesUpdated Sep 28, 2026
5.0/ 5(1 votes)
10100% Positive
Lua Scriptโ€ข3922 lines (57047 chars)
--// ๐Ÿฑ SMART CAT COMPANION AI
--// FULL LIFE + BEHAVIOR + RAGDOLL SYSTEM
--// Model: 18314616360
--// Client-side LocalScript
--//
--// ๐Ÿšซ NO SITTING
--// ๐Ÿšซ NO SLEEPING
--// โค๏ธ PETTING
--// ๐Ÿ– FOOD
--// ๐Ÿ’ง DRINKING
--// ๐Ÿ‘ƒ SNIFFING
--// ๐Ÿงผ GROOMING
--// ๐Ÿพ PAWING
--// ๐Ÿง˜ STRETCHING
--// ๐Ÿ‘€ LOOKING
--// ๐ŸŽพ PLAYING
--// ๐Ÿƒ ZOOMIES
--// ๐Ÿพ EXPLORATION
--// ๐Ÿฆ WATCHING / CHASING
--// ๐Ÿ˜บ RANDOM MEOWS
--// ๐Ÿงฑ WALL AVOIDANCE
--// ๐Ÿง  STUCK RECOVERY
--// ๐Ÿธ SMART JUMPING
--// ๐ŸŒฑ GROWTH
--// โฑ๏ธ 30-MINUTE LIFESPAN
--// โ˜ ๏ธ NON-GRAPHIC DEATH CAUSES
--// ๐Ÿฆด PHYSICAL RAGDOLL
--//
--// Put this LocalScript in:
--// StarterPlayer > StarterPlayerScripts

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

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

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

local MODEL_ID = "rbxassetid://18314616360"

--------------------------------------------------
-- ๐ŸŒฑ LIFE
--------------------------------------------------

local LIFE_TIME = 30 * 60

local START_SIZE = 0.70
local FINAL_SIZE = 1.15

local birthTime = time()
local currentScale = START_SIZE

local catIsDead = false
local deathCause = "Unknown"

--------------------------------------------------
-- ๐Ÿฑ FOLLOWING
--------------------------------------------------

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

--------------------------------------------------
-- ๐Ÿง  STUCK
--------------------------------------------------

local STUCK_CHECK_TIME = 1
local STUCK_DISTANCE = 1.2

local lastStuckPosition
local stuckTimer = 0

local RECOVERY_TIME = 1.6
local recovering = false
local recoveryUntil = 0

--------------------------------------------------
-- ๐Ÿงฑ WALLS
--------------------------------------------------

local OBSTACLE_CHECK_DISTANCE = 5
local SIDE_CHECK_DISTANCE = 5
local HARD_BLOCK_DISTANCE = 3.5

local BLOCKCAST_SIZE =
	Vector3.new(3.5, 4, 3.5)

local turningAroundWall = false
local wallTurnDirection
local wallTurnUntil = 0

--------------------------------------------------
-- ๐Ÿธ JUMPING
--------------------------------------------------

local NORMAL_JUMP_POWER = 45
local RECOVERY_JUMP_POWER = 55

local JUMP_COOLDOWN = 1.25
local JUMP_CONTROL_TIME = 0.28
local MAX_JUMP_TIME = 1.25
local LANDING_BUFFER = 0.12

local jumping = false
local jumpStartedAt = 0
local jumpEndTime = 0
local lastJumpTime = 0

--------------------------------------------------
-- ๐Ÿ‘ƒ SNIFFING
--------------------------------------------------

local SMELL_ENABLED = true
local SMELL_CHANCE = 0.13

local SMELL_MIN_TIME = 2
local SMELL_MAX_TIME = 4

--------------------------------------------------
-- ๐Ÿงผ GROOMING
--------------------------------------------------

local GROOM_ENABLED = true
local GROOM_CHANCE = 0.10

local GROOM_MIN_TIME = 2
local GROOM_MAX_TIME = 5

--------------------------------------------------
-- ๐Ÿพ PAWING
--------------------------------------------------

local PAW_ENABLED = true
local PAW_CHANCE = 0.08

local PAW_MIN_TIME = 1
local PAW_MAX_TIME = 2.5

--------------------------------------------------
-- ๐Ÿง˜ STRETCHING
--------------------------------------------------

local STRETCH_ENABLED = true
local STRETCH_CHANCE = 0.07

local STRETCH_MIN_TIME = 1.5
local STRETCH_MAX_TIME = 3

--------------------------------------------------
-- ๐Ÿ‘€ WATCHING
--------------------------------------------------

local WATCH_ENABLED = true
local WATCH_CHANCE = 0.08

local WATCH_MIN_TIME = 1.5
local WATCH_MAX_TIME = 4

--------------------------------------------------
-- ๐ŸŽพ PLAY
--------------------------------------------------

local PLAY_ENABLED = true
local PLAY_CHANCE = 0.08

local PLAY_MIN_TIME = 1.5
local PLAY_MAX_TIME = 3

--------------------------------------------------
-- ๐Ÿƒ ZOOMIES
--------------------------------------------------

local RANDOM_RUN_ENABLED = true
local RANDOM_RUN_CHANCE = 0.08

local RANDOM_RUN_MIN_TIME = 2
local RANDOM_RUN_MAX_TIME = 5

--------------------------------------------------
-- ๐Ÿพ EXPLORE
--------------------------------------------------

local EXPLORE_ENABLED = true
local EXPLORE_CHANCE = 0.30

local EXPLORE_MIN_DISTANCE = 8
local EXPLORE_MAX_DISTANCE = 35

local EXPLORE_WAIT_MIN = 2
local EXPLORE_WAIT_MAX = 7

local EXPLORE_TIMEOUT = 12

--------------------------------------------------
-- ๐Ÿ– FOOD
--------------------------------------------------

local FOOD_ENABLED = true

local FOOD_SEARCH_CHANCE = 0.16

local FOOD_EAT_TIME_MIN = 3
local FOOD_EAT_TIME_MAX = 6

local FOOD_SEARCH_RADIUS = 45
local FOOD_DETECT_DISTANCE = 3

--------------------------------------------------
-- ๐Ÿ’ง WATER
--------------------------------------------------

local WATER_ENABLED = true

local WATER_CHANCE = 0.07

local WATER_DRINK_TIME_MIN = 2
local WATER_DRINK_TIME_MAX = 5

local WATER_SEARCH_RADIUS = 40

--------------------------------------------------
-- ๐Ÿฆ CHASING
--------------------------------------------------

local CHASE_ENABLED = true

local CHASE_CHANCE = 0.05

local CHASE_TIME_MIN = 2
local CHASE_TIME_MAX = 5

local CHASE_RADIUS = 25

--------------------------------------------------
-- โค๏ธ PET
--------------------------------------------------

local PET_ENABLED = true

local PET_TIME = 2.5
local PET_MAX_DISTANCE = 8

local petting = false
local petUntil = 0

--------------------------------------------------
-- ๐Ÿ”Š SOUND
--------------------------------------------------

local RANDOM_SOUND_ID =
	"rbxassetid://133371725828981"

local MIN_SOUND_DELAY = 5
local MAX_SOUND_DELAY = 15

local SOUND_VOLUME = 1

--------------------------------------------------
-- ๐ŸŽฌ WALK
--------------------------------------------------

local R6_WALK =
	"rbxassetid://180426354"

local R15_WALK =
	"rbxassetid://913402848"

--------------------------------------------------
-- โ˜ ๏ธ DEATH
--------------------------------------------------

local RAGDOLL_ON_DEATH = true

local HIGH_SPEED_IMPACT = 70
local HIGH_FALL_DISTANCE = 35

local deathProcessed = false

--------------------------------------------------
-- CHARACTER
--------------------------------------------------

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

--------------------------------------------------
-- HUMANOID
--------------------------------------------------

local humanoid =
	rig:FindFirstChildOfClass("Humanoid")

if not humanoid then
	warn("๐Ÿฑ Cat has no Humanoid.")
	return
end

local rigRoot =
	rig:FindFirstChild("HumanoidRootPart")

if not rigRoot then
	warn("๐Ÿฑ Cat has no HumanoidRootPart.")
	return
end

--------------------------------------------------
-- ๐Ÿ›ก๏ธ HUMANOID SAFETY
--------------------------------------------------

humanoid.BreakJointsOnDeath = false
humanoid.RequiresNeck = false

--------------------------------------------------
-- ๐Ÿงฑ COLLISION
--------------------------------------------------

for _, object in ipairs(rig:GetDescendants()) do

	if object:IsA("BasePart") then

		object.Anchored = false
		object.CanCollide = true
		object.CanTouch = true
		object.CanQuery = true
		object.Massless = false

		pcall(function()
			object.CollisionGroup = "Default"
		end)

	end

end

rigRoot.CanCollide = true
rigRoot.CanTouch = true
rigRoot.CanQuery = true

--------------------------------------------------
-- SPAWN
--------------------------------------------------

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

--------------------------------------------------
-- SIZE
--------------------------------------------------

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

currentScale = START_SIZE

--------------------------------------------------
-- HUMANOID SETTINGS
--------------------------------------------------

humanoid.WalkSpeed = WALK_SPEED
humanoid.AutoRotate = true
humanoid.UseJumpPower = true
humanoid.JumpPower = NORMAL_JUMP_POWER

humanoid.Sit = false

pcall(function()

	humanoid:SetStateEnabled(
		Enum.HumanoidStateType.Seated,
		false
	)

end)

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

local animator =
	humanoid:FindFirstChildOfClass("Animator")

if not animator then

	animator = Instance.new("Animator")
	animator.Parent = humanoid

end

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

local animation =
	Instance.new("Animation")

if humanoid.RigType ==
	Enum.HumanoidRigType.R15 then

	animation.AnimationId = R15_WALK

else

	animation.AnimationId = R6_WALK

end

local walkTrack

pcall(function()
	walkTrack =
		animator:LoadAnimation(animation)
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 = SOUND_VOLUME

sound.RollOffMaxDistance = 60
sound.RollOffMinDistance = 5

sound.Parent = rigRoot

--------------------------------------------------
-- โค๏ธ PET PROMPT
--------------------------------------------------

local petPrompt =
	Instance.new("ProximityPrompt")

petPrompt.Name = "PetCatPrompt"
petPrompt.ActionText = "Pet Cat"
petPrompt.ObjectText = "๐Ÿฑ Cat"

petPrompt.KeyboardKeyCode = Enum.KeyCode.E
petPrompt.HoldDuration = 0.5
petPrompt.MaxActivationDistance = PET_MAX_DISTANCE

petPrompt.RequiresLineOfSight = false
petPrompt.Style =
	Enum.ProximityPromptStyle.Default

petPrompt.Parent = rigRoot

--------------------------------------------------
-- STATE
--------------------------------------------------

local state = "FOLLOWING"

local targetPosition
local stateUntil = 0

local randomRunDirection
local randomRunUntil = 0

local watchPosition

local behaviorTarget

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

local rayParams =
	RaycastParams.new()

rayParams.FilterType =
	Enum.RaycastFilterType.Exclude

rayParams.FilterDescendantsInstances = {
	rig,
	character
}

rayParams.IgnoreWater = true

--------------------------------------------------
-- INITIAL
--------------------------------------------------

lastStuckPosition =
	rigRoot.Position

--------------------------------------------------
-- ๐ŸŽฌ JOINTS
--------------------------------------------------

local joints = {}

for _, object in ipairs(rig:GetDescendants()) do

	if object:IsA("Motor6D") then
		joints[object.Name] = object
	end

end

local function findJoint(...)

	for _, name in ipairs({...}) do

		if joints[name] then
			return joints[name]
		end

	end

	for _, object in ipairs(rig:GetDescendants()) do

		if object:IsA("Motor6D") then

			local lower =
				object.Name:lower()

			for _, wanted in ipairs({...}) do

				if string.find(
					lower,
					wanted:lower(),
					1,
					true
				) then

					return object

				end

			end

		end

	end

	return nil

end

local leftShoulder =
	findJoint(
		"Left Shoulder",
		"LeftShoulder"
	)

local rightShoulder =
	findJoint(
		"Right Shoulder",
		"RightShoulder"
	)

local leftHip =
	findJoint(
		"Left Hip",
		"LeftHip"
	)

local rightHip =
	findJoint(
		"Right Hip",
		"RightHip"
	)

local neck =
	findJoint("Neck")

--------------------------------------------------
-- SAVE ORIGINAL JOINTS
--------------------------------------------------

local originalTransforms = {}

for _, object in ipairs(rig:GetDescendants()) do

	if object:IsA("Motor6D") then

		originalTransforms[object] =
			object.Transform

	end

end

--------------------------------------------------
-- ANIMATION
--------------------------------------------------

local function animateJoint(
	joint,
	cframe,
	weight
)

	if not joint then
		return
	end

	local original =
		originalTransforms[joint]

	if not original then
		original = CFrame.identity
	end

	joint.Transform =
		original:Lerp(
			original * cframe,
			weight or 1
		)

end

local function updateLimbAnimation()

	if catIsDead then
		return
	end

	local now = time()

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

	if state == "IDLE" then

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

		animateJoint(
			leftShoulder,
			CFrame.Angles(breathe, 0, 0),
			0.8
		)

		animateJoint(
			rightShoulder,
			CFrame.Angles(-breathe, 0, 0),
			0.8
		)

		return

	end

	--------------------------------------------------
	-- SNIFFING
	--------------------------------------------------

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

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

		local head =
			math.sin(now * 7) *
			math.rad(7)

		animateJoint(
			neck,
			CFrame.Angles(
				math.rad(-18) + head,
				0,
				0
			),
			0.9
		)

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

		animateJoint(
			rightShoulder,
			CFrame.Angles(
				-sniff,
				0,
				math.rad(4)
			),
			0.8
		)

		return

	end

	--------------------------------------------------
	-- EATING
	--------------------------------------------------

	if state == "EATING" then

		local chew =
			math.sin(now * 8) *
			math.rad(4)

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

		animateJoint(
			neck,
			CFrame.Angles(
				math.rad(20) + head,
				0,
				0
			),
			1
		)

		animateJoint(
			leftShoulder,
			CFrame.Angles(
				chew,
				0,
				0
			),
			0.6
		)

		animateJoint(
			rightShoulder,
			CFrame.Angles(
				-chew,
				0,
				0
			),
			0.6
		)

		return

	end

	--------------------------------------------------
	-- DRINKING
	--------------------------------------------------

	if state == "DRINKING" then

		local drink =
			math.sin(now * 5) *
			math.rad(5)

		animateJoint(
			neck,
			CFrame.Angles(
				math.rad(30) + drink,
				0,
				0
			),
			1
		)

		return

	end

	--------------------------------------------------
	-- GROOMING
	--------------------------------------------------

	if state == "GROOMING" then

		local paw =
			math.sin(now * 9) *
			math.rad(25)

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

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

		return

	end

	--------------------------------------------------
	-- PAWING
	--------------------------------------------------

	if state == "PAWING" then

		local paw =
			math.sin(now * 12) *
			math.rad(35)

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

		animateJoint(
			rightShoulder,
			CFrame.Angles(
				math.rad(-25) - paw,
				0,
				math.rad(8)
			),
			1
		)

		return

	end

	--------------------------------------------------
	-- STRETCHING
	--------------------------------------------------

	if state == "STRETCHING" then

		local stretch =
			math.sin(
				math.clamp(
					(now % 2) * 2,
					0,
					1
				) * math.pi
			)

		animateJoint(
			leftShoulder,
			CFrame.Angles(
				math.rad(-25) * stretch,
				0,
				0
			),
			1
		)

		animateJoint(
			rightShoulder,
			CFrame.Angles(
				math.rad(-25) * stretch,
				0,
				0
			),
			1
		)

		animateJoint(
			leftHip,
			CFrame.Angles(
				math.rad(18) * stretch,
				0,
				0
			),
			1
		)

		animateJoint(
			rightHip,
			CFrame.Angles(
				math.rad(18) * stretch,
				0,
				0
			),
			1
		)

		return

	end

	--------------------------------------------------
	-- WATCHING
	--------------------------------------------------

	if state == "WATCHING"
		or state == "CHASING" then

		local head =
			math.sin(now * 2) *
			math.rad(4)

		animateJoint(
			neck,
			CFrame.Angles(
				math.rad(-3),
				head,
				0
			),
			0.8
		)

		return

	end

	--------------------------------------------------
	-- PLAYING
	--------------------------------------------------

	if state == "PLAYING" then

		local bounce =
			math.sin(now * 9) *
			math.rad(18)

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

		animateJoint(
			rightShoulder,
			CFrame.Angles(
				-bounce,
				0,
				math.rad(10)
			),
			0.9
		)

		return

	end

	--------------------------------------------------
	-- RUNNING
	--------------------------------------------------

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

		local run =
			math.sin(now * 13) *
			math.rad(35)

		animateJoint(
			leftShoulder,
			CFrame.Angles(run, 0, 0),
			0.8
		)

		animateJoint(
			rightShoulder,
			CFrame.Angles(-run, 0, 0),
			0.8
		)

		animateJoint(
			leftHip,
			CFrame.Angles(-run, 0, 0),
			0.8
		)

		animateJoint(
			rightHip,
			CFrame.Angles(run, 0, 0),
			0.8
		)

		return

	end

	--------------------------------------------------
	-- WALKING
	--------------------------------------------------

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

		local walk =
			math.sin(now * 10) *
			math.rad(15)

		animateJoint(
			leftShoulder,
			CFrame.Angles(walk, 0, 0),
			0.65
		)

		animateJoint(
			rightShoulder,
			CFrame.Angles(-walk, 0, 0),
			0.65
		)

		animateJoint(
			leftHip,
			CFrame.Angles(-walk, 0, 0),
			0.65
		)

		animateJoint(
			rightHip,
			CFrame.Angles(walk, 0, 0),
			0.65
		)

		return

	end

	--------------------------------------------------
	-- PETTING
	--------------------------------------------------

	if state == "PETTING" then

		local happy =
			math.sin(now * 5) *
			math.rad(6)

		animateJoint(
			leftShoulder,
			CFrame.Angles(
				happy,
				0,
				math.rad(-8)
			),
			0.8
		)

		animateJoint(
			rightShoulder,
			CFrame.Angles(
				-happy,
				0,
				math.rad(8)
			),
			0.8
		)

		return

	end

end

--------------------------------------------------
-- WALK HELPERS
--------------------------------------------------

local function stopWalk()

	if walkTrack
		and walkTrack.IsPlaying then

		walkTrack:Stop(0.15)

	end

end

local function playWalk()

	if walkTrack
		and not walkTrack.IsPlaying then

		walkTrack:Play(0.15)

	end

end

--------------------------------------------------
-- FORCE 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

--------------------------------------------------
-- GROUNDED
--------------------------------------------------

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, 0),
			rayParams
		)

	return result ~= nil

end

--------------------------------------------------
-- RAY
--------------------------------------------------

local function rayHit(direction, distance)

	if direction.Magnitude < 0.05 then
		return nil
	end

	local origin =
		rigRoot.Position +
		Vector3.new(0, 1.5, 0)

	return workspace:Raycast(
		origin,
		direction.Unit * distance,
		rayParams
	)

end

--------------------------------------------------
-- BLOCKCAST
--------------------------------------------------

local function getHardObstacle(direction)

	if direction.Magnitude < 0.05 then
		return nil
	end

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

	if flat.Magnitude < 0.05 then
		return nil
	end

	flat = flat.Unit

	local origin =
		rigRoot.Position +
		Vector3.new(0, 1.5, 0)

	return workspace:Blockcast(
		CFrame.lookAt(
			origin,
			origin + flat
		),
		BLOCKCAST_SIZE,
		flat * HARD_BLOCK_DISTANCE,
		rayParams
	)

end

--------------------------------------------------
-- WALL AVOIDANCE
--------------------------------------------------

local function getWallAvoidance(direction)

	if direction.Magnitude < 0.05 then
		return direction
	end

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

	if forward.Magnitude < 0.05 then
		return direction
	end

	forward = forward.Unit

	local right =
		Vector3.new(
			-forward.Z,
			0,
			forward.X
		)

	local left = -right

	local forwardHit =
		rayHit(
			forward,
			OBSTACLE_CHECK_DISTANCE
		)

	if not forwardHit then

		if turningAroundWall
			and wallTurnDirection
			and time() < wallTurnUntil then

			if not rayHit(
				wallTurnDirection,
				SIDE_CHECK_DISTANCE
			) then

				return (
					forward * 0.35 +
					wallTurnDirection * 0.65
				).Unit

			end

		end

		turningAroundWall = false
		wallTurnDirection = nil

		return forward

	end

	local leftHit =
		rayHit(
			left,
			SIDE_CHECK_DISTANCE
		)

	local rightHit =
		rayHit(
			right,
			SIDE_CHECK_DISTANCE
		)

	if not leftHit and not rightHit then

		wallTurnDirection =
			random:NextNumber() < 0.5
			and left
			or right

		turningAroundWall = true
		wallTurnUntil = time() + 1.5

		return (
			forward * 0.2 +
			wallTurnDirection * 0.8
		).Unit

	end

	if not leftHit then

		wallTurnDirection = left
		turningAroundWall = true
		wallTurnUntil = time() + 1.5

		return (
			forward * 0.2 +
			left * 0.8
		).Unit

	end

	if not rightHit then

		wallTurnDirection = right
		turningAroundWall = true
		wallTurnUntil = time() + 1.5

		return (
			forward * 0.2 +
			right * 0.8
		).Unit

	end

	local side =
		random:NextNumber() < 0.5
		and left
		or right

	return side.Unit

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()

	lastJumpTime = time()
	jumping = true

	jumpStartedAt = time()
	jumpEndTime =
		time() + MAX_JUMP_TIME

	stopWalk()

	humanoid.JumpPower =
		RECOVERY_JUMP_POWER

	humanoid.Jump = true

	humanoid:ChangeState(
		Enum.HumanoidStateType.Jumping
	)

	task.delay(
		JUMP_CONTROL_TIME,
		function()

			if rig.Parent
				and not catIsDead
				and humanoid.Health > 0 then

				humanoid.JumpPower =
					NORMAL_JUMP_POWER

			end

		end
	)

	return true

end

--------------------------------------------------
-- UPDATE JUMP
--------------------------------------------------

local function updateJump()

	if not jumping then
		return false
	end

	local now = time()

	if isGrounded()
		and now - jumpStartedAt >
		LANDING_BUFFER then

		jumping = false

		humanoid.JumpPower =
			NORMAL_JUMP_POWER

		return false

	end

	if now >= jumpEndTime then

		jumping = false

		humanoid.JumpPower =
			NORMAL_JUMP_POWER

		return false

	end

	humanoid:Move(
		Vector3.zero,
		false
	)

	return true

end

--------------------------------------------------
-- MOVE
--------------------------------------------------

local function moveDirection(direction, speed)

	if catIsDead
		or jumping
		or petting then

		return

	end

	if direction.Magnitude < 0.05 then
		return
	end

	forceStand()

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

	if flat.Magnitude < 0.05 then
		return
	end

	flat = flat.Unit

	local hardHit =
		getHardObstacle(flat)

	if hardHit then

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

		local left = -right

		local leftBlocked =
			getHardObstacle(left) ~= nil

		local rightBlocked =
			getHardObstacle(right) ~= nil

		if leftBlocked
			and rightBlocked then

			if isGrounded() then

				if tryJump(false) then
					return
				end

			end

			humanoid:Move(
				Vector3.zero,
				false
			)

			stopWalk()

			return

		end

		if not leftBlocked then

			flat =
				(
					flat * 0.15 +
					left * 0.85
				).Unit

		elseif not rightBlocked then

			flat =
				(
					flat * 0.15 +
					right * 0.85
				).Unit

		end

	end

	local movement =
		getWallAvoidance(flat)

	if movement.Magnitude < 0.05 then

		humanoid:Move(
			Vector3.zero,
			false
		)

		stopWalk()

		return

	end

	humanoid.WalkSpeed = speed

	humanoid:Move(
		movement,
		false
	)

	playWalk()

end

--------------------------------------------------
-- STOP
--------------------------------------------------

local function stopMoving()

	if jumping then
		return
	end

	humanoid:Move(
		Vector3.zero,
		false
	)

	stopWalk()

	forceStand()

	stuckTimer = 0
	lastStuckPosition =
		rigRoot.Position

end

--------------------------------------------------
-- LOOK
--------------------------------------------------

local function lookAt(position)

	if jumping or petting then
		return
	end

	local current =
		rigRoot.Position

	local target =
		Vector3.new(
			position.X,
			current.Y,
			position.Z
		)

	local difference =
		target - current

	if difference.Magnitude < 0.05 then
		return
	end

	rigRoot.CFrame =
		CFrame.lookAt(
			current,
			target
		)

end

--------------------------------------------------
-- PET
--------------------------------------------------

local function startPetting()

	if not PET_ENABLED
		or catIsDead
		or petting
		or humanoid.Health <= 0 then

		return

	end

	petting = true

	petUntil =
		time() + PET_TIME

	state = "PETTING"

	targetPosition = nil

	recovering = false
	jumping = false

	humanoid:Move(
		Vector3.zero,
		false
	)

	humanoid.WalkSpeed = 0
	humanoid.AutoRotate = false

	stopWalk()
	forceStand()

end

petPrompt.Triggered:Connect(
	function(triggeringPlayer)

		if triggeringPlayer ~= player then
			return
		end

		startPetting()

	end
)

--------------------------------------------------
-- PET UPDATE
--------------------------------------------------

local function updatePetting()

	if not petting then
		return
	end

	forceStand()

	humanoid:Move(
		Vector3.zero,
		false
	)

	humanoid.WalkSpeed = 0

	if time() >= petUntil then

		petting = false
		state = "IDLE"

		humanoid.AutoRotate = true

		humanoid.WalkSpeed =
			WALK_SPEED

		stateUntil =
			time() + 3

	end

end

--------------------------------------------------
-- GENERIC IDLE
--------------------------------------------------

local function startIdle()

	if jumping or petting then
		return
	end

	state = "IDLE"

	stopMoving()

	humanoid.WalkSpeed = 0

	stateUntil =
		time() +
		random:NextNumber(2, 5)

end

--------------------------------------------------
-- SNIFF
--------------------------------------------------

local function startSmelling()

	if jumping or petting then
		return
	end

	state = "SMELLING"

	stopMoving()

	humanoid.WalkSpeed = 0

	stateUntil =
		time() +
		random:NextNumber(
			SMELL_MIN_TIME,
			SMELL_MAX_TIME
		)

end

local function updateSmelling()

	forceStand()

	humanoid:Move(
		Vector3.zero,
		false
	)

	if time() >= stateUntil then

		startIdle()

	end

end

--------------------------------------------------
-- GROOM
--------------------------------------------------

local function startGrooming()

	if jumping or petting then
		return
	end

	state = "GROOMING"

	stopMoving()

	humanoid.WalkSpeed = 0

	stateUntil =
		time() +
		random:NextNumber(
			GROOM_MIN_TIME,
			GROOM_MAX_TIME
		)

end

local function updateGrooming()

	forceStand()

	humanoid:Move(
		Vector3.zero,
		false
	)

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

end

--------------------------------------------------
-- PAW
--------------------------------------------------

local function startPawing()

	if jumping or petting then
		return
	end

	state = "PAWING"

	stopMoving()

	humanoid.WalkSpeed = 0

	stateUntil =
		time() +
		random:NextNumber(
			PAW_MIN_TIME,
			PAW_MAX_TIME
		)

end

local function updatePawing()

	forceStand()

	humanoid:Move(
		Vector3.zero,
		false
	)

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

end

--------------------------------------------------
-- STRETCH
--------------------------------------------------

local function startStretching()

	if jumping or petting then
		return
	end

	state = "STRETCHING"

	stopMoving()

	humanoid.WalkSpeed = 0

	stateUntil =
		time() +
		random:NextNumber(
			STRETCH_MIN_TIME,
			STRETCH_MAX_TIME
		)

end

local function updateStretching()

	forceStand()

	humanoid:Move(
		Vector3.zero,
		false
	)

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

end

--------------------------------------------------
-- PLAY
--------------------------------------------------

local function startPlay()

	if jumping or petting then
		return
	end

	state = "PLAYING"

	local direction =
		Vector3.new(
			random:NextNumber(-1, 1),
			0,
			random:NextNumber(-1, 1)
		)

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

	randomRunDirection =
		Vector3.new(
			direction.X,
			0,
			direction.Z
		).Unit

	stateUntil =
		time() +
		random:NextNumber(
			PLAY_MIN_TIME,
			PLAY_MAX_TIME
		)

end

local function updatePlaying()

	if time() >= stateUntil then

		startIdle()
		return

	end

	moveDirection(
		randomRunDirection,
		RUN_SPEED
	)

end

--------------------------------------------------
-- RANDOM RUN
--------------------------------------------------

local function startRandomRun()

	if jumping or petting then
		return
	end

	state = "RUNNING_RANDOM"

	local direction =
		Vector3.new(
			random:NextNumber(-1, 1),
			0,
			random:NextNumber(-1, 1)
		)

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

	randomRunDirection =
		Vector3.new(
			direction.X,
			0,
			direction.Z
		).Unit

	randomRunUntil =
		time() +
		random:NextNumber(
			RANDOM_RUN_MIN_TIME,
			RANDOM_RUN_MAX_TIME
		)

end

local function updateRandomRun(
	playerPosition
)

	if time() >= randomRunUntil then

		startIdle()
		return

	end

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

	if distance > 20 then

		state = "FOLLOWING"
		return

	end

	moveDirection(
		randomRunDirection,
		RUN_SPEED
	)

end

--------------------------------------------------
-- EXPLORE POSITION
--------------------------------------------------

local function getRandomExplorePosition()

	for _ = 1, 15 do

		local angle =
			random:NextNumber(
				0,
				math.pi * 2
			)

		local distance =
			random:NextNumber(
				EXPLORE_MIN_DISTANCE,
				EXPLORE_MAX_DISTANCE
			)

		local offset =
			Vector3.new(
				math.cos(angle) * distance,
				0,
				math.sin(angle) * distance
			)

		local desired =
			playerRoot.Position +
			offset

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

		if ground then

			return ground.Position +
				Vector3.new(0, 2, 0)

		end

	end

	return playerRoot.Position +
		Vector3.new(
			random:NextNumber(-10, 10),
			0,
			random:NextNumber(-10, 10)
		)

end

--------------------------------------------------
-- EXPLORE
--------------------------------------------------

local function startExplore()

	if petting then
		return
	end

	state = "WANDERING"

	targetPosition =
		getRandomExplorePosition()

	stateUntil =
		time() + EXPLORE_TIMEOUT

	humanoid.WalkSpeed =
		WALK_SPEED

end

local function updateExplore(
	playerPosition
)

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

	if distance >
		FOLLOW_RESUME_DISTANCE then

		state = "FOLLOWING"
		targetPosition = nil
		return

	end

	if not targetPosition then

		targetPosition =
			getRandomExplorePosition()

	end

	local difference =
		targetPosition -
		rigRoot.Position

	difference =
		Vector3.new(
			difference.X,
			0,
			difference.Z
		)

	if difference.Magnitude < 2
		or time() >= stateUntil then

		stopMoving()

		targetPosition = nil

		state = "IDLE"

		stateUntil =
			time() +
			random:NextNumber(
				EXPLORE_WAIT_MIN,
				EXPLORE_WAIT_MAX
			)

		return

	end

	moveDirection(
		difference,
		WALK_SPEED * 0.65
	)

end

--------------------------------------------------
-- WATCH
--------------------------------------------------

local function startWatching()

	if jumping or petting then
		return
	end

	state = "WATCHING"

	stopMoving()

	humanoid.WalkSpeed = 0

	watchPosition =
		rigRoot.Position +
		Vector3.new(
			random:NextNumber(-15, 15),
			0,
			random:NextNumber(-15, 15)
		)

	stateUntil =
		time() +
		random:NextNumber(
			WATCH_MIN_TIME,
			WATCH_MAX_TIME
		)

end

local function updateWatching()

	forceStand()

	humanoid:Move(
		Vector3.zero,
		false
	)

	if watchPosition then
		lookAt(watchPosition)
	end

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

end

--------------------------------------------------
-- ๐Ÿ– FIND FOOD
--------------------------------------------------

local function findFood()

	local closest
	local closestDistance =
		FOOD_SEARCH_RADIUS

	for _, object in ipairs(
		workspace:GetDescendants()
	) do

		if object:IsA("BasePart") then

			local name =
				object.Name:lower()

			if string.find(name, "food", 1, true)
				or string.find(name, "fish", 1, true)
				or string.find(name, "meat", 1, true)
				or string.find(name, "catfood", 1, true) then

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

				if distance <
					closestDistance then

					closest =
						object

					closestDistance =
						distance

				end

			end

		end

	end

	return closest

end

--------------------------------------------------
-- ๐Ÿ– START EATING
--------------------------------------------------

local function startFoodSearch()

	if not FOOD_ENABLED
		or jumping
		or petting then

		return false

	end

	local food =
		findFood()

	if not food then
		return false
	end

	behaviorTarget = food

	state = "SEARCHING_FOOD"

	stateUntil =
		time() + 10

	return true

end

local function updateFoodSearch()

	if not behaviorTarget
		or not behaviorTarget.Parent then

		startIdle()
		return

	end

	local difference =
		behaviorTarget.Position -
		rigRoot.Position

	local distance =
		difference.Magnitude

	if distance <= FOOD_DETECT_DISTANCE then

		stopMoving()

		state = "EATING"

		stateUntil =
			time() +
			random:NextNumber(
				FOOD_EAT_TIME_MIN,
				FOOD_EAT_TIME_MAX
			)

		return

	end

	if time() >= stateUntil then

		startIdle()
		return

	end

	moveDirection(
		difference,
		WALK_SPEED
	)

end

local function updateEating()

	if behaviorTarget
		and behaviorTarget.Parent then

		lookAt(
			behaviorTarget.Position
		)

	end

	humanoid:Move(
		Vector3.zero,
		false
	)

	if time() >= stateUntil then

		behaviorTarget = nil
		startIdle()

	end

end

--------------------------------------------------
-- ๐Ÿ’ง FIND WATER
--------------------------------------------------

local function findWater()

	local closest
	local closestDistance =
		WATER_SEARCH_RADIUS

	for _, object in ipairs(
		workspace:GetDescendants()
	) do

		if object:IsA("BasePart") then

			local name =
				object.Name:lower()

			if string.find(name, "water", 1, true)
				or string.find(name, "bowl", 1, true)
				or string.find(name, "drink", 1, true) then

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

				if distance <
					closestDistance then

					closest =
						object

					closestDistance =
						distance

				end

			end

		end

	end

	return closest

end

--------------------------------------------------
-- ๐Ÿ’ง START DRINKING
--------------------------------------------------

local function startWaterSearch()

	if not WATER_ENABLED
		or jumping
		or petting then

		return false

	end

	local water =
		findWater()

	if not water then
		return false
	end

	behaviorTarget = water

	state = "SEARCHING_WATER"

	stateUntil =
		time() + 10

	return true

end

local function updateWaterSearch()

	if not behaviorTarget
		or not behaviorTarget.Parent then

		startIdle()
		return

	end

	local difference =
		behaviorTarget.Position -
		rigRoot.Position

	local distance =
		difference.Magnitude

	if distance <= 3 then

		stopMoving()

		state = "DRINKING"

		stateUntil =
			time() +
			random:NextNumber(
				WATER_DRINK_TIME_MIN,
				WATER_DRINK_TIME_MAX
			)

		return

	end

	if time() >= stateUntil then

		startIdle()
		return

	end

	moveDirection(
		difference,
		WALK_SPEED * 0.75
	)

end

local function updateDrinking()

	if behaviorTarget
		and behaviorTarget.Parent then

		lookAt(
			behaviorTarget.Position
		)

	end

	humanoid:Move(
		Vector3.zero,
		false
	)

	if time() >= stateUntil then

		behaviorTarget = nil
		startIdle()

	end

end

--------------------------------------------------
-- ๐Ÿฆ CHASE
--------------------------------------------------

local function findMovingObject()

	local closest
	local closestDistance =
		CHASE_RADIUS

	for _, object in ipairs(
		workspace:GetDescendants()
	) do

		if object:IsA("BasePart")
			and object ~= rigRoot then

			local velocity =
				object.AssemblyLinearVelocity

			if velocity.Magnitude > 5 then

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

				if distance <
					closestDistance then

					closest =
						object

					closestDistance =
						distance

				end

			end

		end

	end

	return closest

end

local function startChasing()

	if not CHASE_ENABLED
		or jumping
		or petting then

		return false

	end

	local target =
		findMovingObject()

	if not target then
		return false
	end

	behaviorTarget = target

	state = "CHASING"

	stateUntil =
		time() +
		random:NextNumber(
			CHASE_TIME_MIN,
			CHASE_TIME_MAX
		)

	return true

end

local function updateChasing()

	if not behaviorTarget
		or not behaviorTarget.Parent then

		startIdle()
		return

	end

	if time() >= stateUntil then

		behaviorTarget = nil
		startIdle()
		return

	end

	local difference =
		behaviorTarget.Position -
		rigRoot.Position

	if difference.Magnitude < 2 then

		startIdle()
		return

	end

	moveDirection(
		difference,
		RUN_SPEED
	)

end

--------------------------------------------------
-- ๐Ÿง  STUCK
--------------------------------------------------

local function updateStuck(moving, dt)

	if jumping or petting then

		stuckTimer = 0
		lastStuckPosition =
			rigRoot.Position

		return

	end

	if not moving then

		stuckTimer = 0
		lastStuckPosition =
			rigRoot.Position

		return

	end

	local movement =
		(
			rigRoot.Position -
			lastStuckPosition
		).Magnitude

	if movement < STUCK_DISTANCE then

		stuckTimer += dt

	else

		stuckTimer = 0

		lastStuckPosition =
			rigRoot.Position

	end

	if stuckTimer >=
		STUCK_CHECK_TIME then

		stuckTimer = 0

		recovering = true

		recoveryUntil =
			time() + RECOVERY_TIME

		if isGrounded() then
			tryJump(true)
		end

	end

end

--------------------------------------------------
-- โ˜ ๏ธ DEATH CAUSE DETECTION
--------------------------------------------------

local highestFallY =
	rigRoot.Position.Y

local wasAirborne = false

local function updateDeathCause()

	if catIsDead then
		return
	end

	local velocity =
		rigRoot.AssemblyLinearVelocity

	local speed =
		velocity.Magnitude

	local grounded =
		isGrounded()

	if not grounded then

		if not wasAirborne then

			wasAirborne = true
			highestFallY =
				rigRoot.Position.Y

		else

			highestFallY =
				math.max(
					highestFallY,
					rigRoot.Position.Y
				)

		end

	else

		if wasAirborne then

			local fallDistance =
				highestFallY -
				rigRoot.Position.Y

			if fallDistance >=
				HIGH_FALL_DISTANCE then

				deathCause =
					"Fall from height"

			end

		end

		wasAirborne = false

		highestFallY =
			rigRoot.Position.Y

	end

	if speed >=
		HIGH_SPEED_IMPACT then

		deathCause =
			"High-speed impact"

	end

end

--------------------------------------------------
-- โ˜ ๏ธ RAGDOLL
--------------------------------------------------

local function ragdollCat()

	if not RAGDOLL_ON_DEATH
		or deathProcessed then

		return

	end

	deathProcessed = true

	if not rig.Parent then
		return
	end

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

	pcall(function()

		humanoid:ChangeState(
			Enum.HumanoidStateType.Physics
		)

	end)

	--------------------------------------------------
	-- Create physical constraints
	--------------------------------------------------

	for _, joint in ipairs(
		rig:GetDescendants()
	) do

		if joint:IsA("Motor6D")
			and joint.Part0
			and joint.Part1 then

			local part0 =
				joint.Part0

			local part1 =
				joint.Part1

			local attachment0 =
				Instance.new("Attachment")

			attachment0.Name =
				"CatRagdollA0"

			attachment0.CFrame =
				joint.C0

			attachment0.Parent =
				part0

			local attachment1 =
				Instance.new("Attachment")

			attachment1.Name =
				"CatRagdollA1"

			attachment1.CFrame =
				joint.C1

			attachment1.Parent =
				part1

			local constraint =
				Instance.new(
					"BallSocketConstraint"
				)

			constraint.Name =
				"CatRagdollConstraint"

			constraint.Attachment0 =
				attachment0

			constraint.Attachment1 =
				attachment1

			constraint.LimitsEnabled = true
			constraint.UpperAngle = 45

			constraint.TwistLimitsEnabled = true
			constraint.TwistLowerAngle = -35
			constraint.TwistUpperAngle = 35

			constraint.Parent =
				part0

			joint.Enabled = false

		end

	end

	--------------------------------------------------
	-- Physical parts
	--------------------------------------------------

	for _, object in ipairs(
		rig:GetDescendants()
	) do

		if object:IsA("BasePart") then

			object.Anchored = false
			object.CanCollide = true
			object.CanTouch = true
			object.CanQuery = true
			object.Massless = false

			pcall(function()
				object.CollisionGroup = "Default"
			end)

		end

	end

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

end

--------------------------------------------------
-- ๐ŸŒฑ AGE
--------------------------------------------------

local function updateAge()

	if catIsDead then
		return
	end

	local age =
		time() - birthTime

	if age >= LIFE_TIME then

		deathCause = "Old age"

		catIsDead = true
		state = "DEAD"

		targetPosition = nil
		behaviorTarget = nil

		jumping = false
		petting = false
		recovering = false

		humanoid:Move(
			Vector3.zero,
			false
		)

		humanoid.WalkSpeed = 0
		humanoid.AutoRotate = false

		stopWalk()

		if petPrompt then
			petPrompt.Enabled = false
		end

		if sound then
			sound:Stop()
		end

		--------------------------------------------------
		-- Gentle non-graphic death transition
		--------------------------------------------------

		local started =
			time()

		local duration = 2.5

		task.spawn(function()

			while rig.Parent
				and time() - started <
				duration do

				local progress =
					math.clamp(
						(time() - started) /
							duration,
						0,
						1
					)

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

				animateJoint(
					leftShoulder,
					CFrame.Angles(
						math.rad(35) *
							smooth,
						0,
						math.rad(-8) *
							smooth
					),
					1
				)

				animateJoint(
					rightShoulder,
					CFrame.Angles(
						math.rad(35) *
							smooth,
						0,
						math.rad(8) *
							smooth
					),
					1
				)

				animateJoint(
					neck,
					CFrame.Angles(
						math.rad(15) *
							smooth,
						0,
						0
					),
					1
				)

				RunService.Heartbeat:Wait()

			end

			if rig.Parent then

				ragdollCat()

			end

		end)

		return

	end

	--------------------------------------------------
	-- GROWTH
	--------------------------------------------------

	local progress =
		math.clamp(
			age / LIFE_TIME,
			0,
			1
		)

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

	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(
			random:NextNumber(
				MIN_SOUND_DELAY,
				MAX_SOUND_DELAY
			)
		)

		if rig.Parent
			and not catIsDead
			and state ~= "DEAD" then

			sound:Stop()
			sound:Play()

		end

	end

end)

--------------------------------------------------
-- ๐Ÿง  CHOOSE BEHAVIOR
--------------------------------------------------

local function chooseIdleBehavior()

	if petting
		or catIsDead then

		return

	end

	--------------------------------------------------
	-- Food gets special priority if nearby
	--------------------------------------------------

	if FOOD_ENABLED
		and random:NextNumber() <
		FOOD_SEARCH_CHANCE then

		if startFoodSearch() then
			return
		end

	end

	--------------------------------------------------
	-- Water
	--------------------------------------------------

	if WATER_ENABLED
		and random:NextNumber() <
		WATER_CHANCE then

		if startWaterSearch() then
			return
		end

	end

	--------------------------------------------------
	-- Chase
	--------------------------------------------------

	if CHASE_ENABLED
		and random:NextNumber() <
		CHASE_CHANCE then

		if startChasing() then
			return
		end

	end

	--------------------------------------------------
	-- Other behaviors
	--------------------------------------------------

	local roll =
		random:NextNumber()

	if roll < 0.13
		and SMELL_ENABLED then

		startSmelling()

	elseif roll < 0.23
		and GROOM_ENABLED then

		startGrooming()

	elseif roll < 0.31
		and PAW_ENABLED then

		startPawing()

	elseif roll < 0.38
		and STRETCH_ENABLED then

		startStretching()

	elseif roll < 0.46
		and WATCH_ENABLED then

		startWatching()

	elseif roll < 0.54
		and PLAY_ENABLED then

		startPlay()

	elseif roll < 0.62
		and RANDOM_RUN_ENABLED then

		startRandomRun()

	elseif roll < 0.88
		and EXPLORE_ENABLED then

		startExplore()

	else

		startIdle()

	end

end

--------------------------------------------------
-- ๐Ÿง  MAIN AI
--------------------------------------------------

RunService.Heartbeat:Connect(
	function(dt)

		if not rig.Parent
			or catIsDead
			or not character.Parent
			or not playerRoot.Parent
			or humanoid.Health <= 0 then

			return

		end

		--------------------------------------------------
		-- AGE
		--------------------------------------------------

		updateAge()

		if catIsDead then
			return
		end

		--------------------------------------------------
		-- DEATH DETECTION
		--------------------------------------------------

		updateDeathCause()

		--------------------------------------------------
		-- ANIMATION
		--------------------------------------------------

		updateLimbAnimation()

		--------------------------------------------------
		-- NEVER SIT
		--------------------------------------------------

		forceStand()

		--------------------------------------------------
		-- PET
		--------------------------------------------------

		if petting then

			updatePetting()
			return

		end

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

		if updateJump() then
			return
		end

		local playerPosition =
			playerRoot.Position

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

		--------------------------------------------------
		-- FLOOR SAFETY
		--------------------------------------------------

		if not isGrounded()
			and not jumping then

			tryJump(true)
			return

		end

		--------------------------------------------------
		-- FOOD SEARCH
		--------------------------------------------------

		if state == "SEARCHING_FOOD" then

			updateFoodSearch()
			return

		end

		--------------------------------------------------
		-- EATING
		--------------------------------------------------

		if state == "EATING" then

			updateEating()
			return

		end

		--------------------------------------------------
		-- WATER SEARCH
		--------------------------------------------------

		if state == "SEARCHING_WATER" then

			updateWaterSearch()
			return

		end

		--------------------------------------------------
		-- DRINKING
		--------------------------------------------------

		if state == "DRINKING" then

			updateDrinking()
			return

		end

		--------------------------------------------------
		-- SNIFF
		--------------------------------------------------

		if state == "SMELLING" then

			updateSmelling()

			if distance >
				FOLLOW_RESUME_DISTANCE then

				state = "FOLLOWING"
			end

			return

		end

		--------------------------------------------------
		-- GROOM
		--------------------------------------------------

		if state == "GROOMING" then

			updateGrooming()

			if distance >
				FOLLOW_RESUME_DISTANCE then

				state = "FOLLOWING"
			end

			return

		end

		--------------------------------------------------
		-- PAW
		--------------------------------------------------

		if state == "PAWING" then

			updatePawing()
			return

		end

		--------------------------------------------------
		-- STRETCH
		--------------------------------------------------

		if state == "STRETCHING" then

			updateStretching()
			return

		end

		--------------------------------------------------
		-- WATCH
		--------------------------------------------------

		if state == "WATCHING" then

			updateWatching()
			return

		end

		--------------------------------------------------
		-- PLAY
		--------------------------------------------------

		if state == "PLAYING" then

			updatePlaying()

			if distance > 25 then
				state = "FOLLOWING"
			end

			return

		end

		--------------------------------------------------
		-- CHASE
		--------------------------------------------------

		if state == "CHASING" then

			updateChasing()
			return

		end

		--------------------------------------------------
		-- RANDOM RUN
		--------------------------------------------------

		if state == "RUNNING_RANDOM" then

			updateRandomRun(
				playerPosition
			)

			return

		end

		--------------------------------------------------
		-- EXPLORE
		--------------------------------------------------

		if state == "WANDERING" then

			updateExplore(
				playerPosition
			)

			return

		end

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

		if recovering then

			if time() <
				recoveryUntil then

				moveDirection(
					playerPosition -
						rigRoot.Position,
					WALK_SPEED
				)

				updateStuck(
					true,
					dt
				)

				return

			else

				recovering = false

			end

		end

		--------------------------------------------------
		-- TOO FAR
		--------------------------------------------------

		if distance >=
			TOO_FAR_DISTANCE then

			state = "RUNNING"

			moveDirection(
				playerPosition -
					rigRoot.Position,
				RUN_SPEED
			)

			updateStuck(true, dt)

			return

		end

		--------------------------------------------------
		-- RUNNING
		--------------------------------------------------

		if state == "RUNNING" then

			if distance <=
				RUN_DISTANCE then

				state = "FOLLOWING"

				humanoid.WalkSpeed =
					WALK_SPEED

				return

			end

			moveDirection(
				playerPosition -
					rigRoot.Position,
				RUN_SPEED
			)

			updateStuck(true, dt)

			return

		end

		--------------------------------------------------
		-- FOLLOWING
		--------------------------------------------------

		if state == "FOLLOWING" then

			if distance <=
				STOP_DISTANCE then

				stopMoving()

				humanoid.WalkSpeed = 0

				lookAt(playerPosition)

				if random:NextNumber()
					< 0.012 then

					chooseIdleBehavior()

				end

				return

			end

			moveDirection(
				playerPosition -
					rigRoot.Position,
				WALK_SPEED
			)

			updateStuck(true, dt)

			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

				chooseIdleBehavior()

			end

			return

		end

	end
)

--------------------------------------------------
-- ๐Ÿ”„ PLAYER RESPAWN
--------------------------------------------------

player.CharacterAdded:Connect(
	function(newCharacter)

		character = newCharacter

		local newRoot =
			newCharacter:WaitForChild(
				"HumanoidRootPart"
			)

		playerRoot = newRoot

		rayParams.FilterDescendantsInstances = {
			rig,
			character
		}

		--------------------------------------------------
		-- CAT DOES NOT RESET
		--------------------------------------------------

		if rig.Parent
			and not catIsDead then

			jumping = false
			recovering = false
			petting = false

			behaviorTarget = nil

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

			state = "FOLLOWING"

			targetPosition = nil

			humanoid.JumpPower =
				NORMAL_JUMP_POWER

			humanoid.WalkSpeed =
				WALK_SPEED

			humanoid.AutoRotate = true

			forceStand()

			if petPrompt then
				petPrompt.Enabled = true
			end

		end

	end
)

--------------------------------------------------
-- โ˜ ๏ธ HUMANOID DEATH
--------------------------------------------------

humanoid.Died:Connect(
	function()

		if catIsDead then
			return
		end

		--------------------------------------------------
		-- Determine cause
		--------------------------------------------------

		if deathCause == "Unknown" then

			local velocity =
				rigRoot.AssemblyLinearVelocity

			if velocity.Magnitude >=
				HIGH_SPEED_IMPACT then

				deathCause =
					"High-speed impact"

			else

				deathCause =
					"Unexpected accident"

			end

		end

		catIsDead = true
		state = "DEAD"

		jumping = false
		petting = false
		recovering = false

		targetPosition = nil
		behaviorTarget = nil

		humanoid:Move(
			Vector3.zero,
			false
		)

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

		stopWalk()

		if petPrompt then
			petPrompt.Enabled = false
		end

		if sound then
			sound:Stop()
		end

		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()

		for _, object in ipairs(
			rig:GetDescendants()
		) do

			if object:IsA("BasePart") then

				object.Anchored = false
				object.CanCollide = true
				object.CanTouch = true
				object.CanQuery = true
				object.Massless = false

				pcall(function()
					object.CollisionGroup =
						"Default"
				end)

			end

		end

		if not jumping
			and not petting
			and state ~= "IDLE"
			and state ~= "SMELLING"
			and state ~= "GROOMING"
			and state ~= "PAWING"
			and state ~= "WATCHING"
			and state ~= "EATING"
			and state ~= "DRINKING"
			and state ~= "STRETCHING" then

			local velocity =
				rigRoot.AssemblyLinearVelocity

			local moving =
				humanoid.MoveDirection.Magnitude
				> 0.2

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

				tryJump(true)

			end

		end

	end

end)

--------------------------------------------------
-- ๐Ÿฑ DONE
--------------------------------------------------

print("๐Ÿฑ Smart Cat Companion AI loaded!")
print("----------------------------------")
print("๐Ÿšซ Sitting: OFF")
print("๐Ÿšซ Sleeping: OFF")
print("โค๏ธ Pet Cat: ON")
print("๐Ÿ– Food system: ON")
print("๐Ÿ’ง Water system: ON")
print("๐Ÿ‘ƒ Sniffing: ON")
print("๐Ÿงผ Grooming: ON")
print("๐Ÿพ Pawing: ON")
print("๐Ÿง˜ Stretching: ON")
print("๐Ÿ‘€ Watching: ON")
print("๐Ÿฆ Chasing: ON")
print("๐ŸŽพ Playing: ON")
print("๐Ÿƒ Zoomies: ON")
print("๐Ÿพ Exploration: ON")
print("๐Ÿ˜บ Random sounds: ON")
print("๐Ÿงฑ Wall avoidance: ON")
print("๐Ÿ›ก๏ธ Hard collision: ON")
print("๐Ÿง  Stuck recovery: ON")
print("๐Ÿธ Smart jumping: ON")
print("๐ŸŒฑ Growth: ON")
print("โฑ๏ธ Lifespan: 30 minutes")
print("โ˜ ๏ธ Death causes: ON")
print("๐Ÿฆด Ragdoll death: ON")
print("๐Ÿ”„ Player respawn protection: ON")
print("----------------------------------")

Features & Information

Get a smart cat companion!

Community Comments (3)

TornadoIntercepterTornadoIntercepterSep 30, 2026

ACTULLY THIS IS SO GOOD AND I SAY GOOD ITS GOOD ITS CAT CAT CUTE AND FUNNY

WinnerWinnerOct 1, 2026

Works amazingly, client sided btw

reneeekyttlereneeekyttleOct 13, 2026

Ai slop

More Scripts for Universal Scripts