
KeylessUniversal

--// ๐ฑ 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("----------------------------------")
![[KEYLESS] Moondiety Script | 17+ supported Games](https://scriptblox.com/images/sb/0-1753093706828.png)


ACTULLY THIS IS SO GOOD AND I SAY GOOD ITS GOOD ITS CAT CAT CUTE AND FUNNY
Works amazingly, client sided btw
Ai slop