Загрузка данных
local RunService = game:GetService("RunService")
local Toad = {}
Toad.__index = Toad
local FOLLOW_RADIUS = 8
local AGGRO_RADIUS = 15
local LEASH_RADIUS = 20
local JUMP_HEIGHT = 5
local JUMP_TIME = 0.45
local ATTACK_DAMAGE = 10
local ATTACK_COOLDOWN = 0.15 -- пауза между атаками одной жабы, чтоб не спамило на приземлении
function Toad.new(model, owner, slotIndex)
local self = setmetatable({}, Toad)
self.model = model
self.owner = owner
self.state = "Follow"
self.target = nil
self.slotIndex = slotIndex or 0 -- нужно для расстановки жаб по кругу вокруг игрока
self.lastAttackTime = 0
local root = model.PrimaryPart
if not root then
warn("У модели жабы не задан PrimaryPart! Жаба не будет двигаться:", model:GetFullName())
return self
end
-- Сварка всех частей к торсу, чтобы двигалась вся модель целиком
for _, part in ipairs(model:GetDescendants()) do
if part:IsA("BasePart") and part ~= root then
part.Anchored = false
local weld = Instance.new("WeldConstraint")
weld.Part0 = root
weld.Part1 = part
weld.Parent = root
end
end
root.Anchored = false
-- Запоминаем высоту, на которой модель должна стоять над полом
self.groundY = root.Position.Y
return self
end
function Toad:FindEnemy()
local enemiesFolder = workspace:FindFirstChild("Enemies")
if not enemiesFolder then return nil, nil end
local myPos = self.model.PrimaryPart.Position
local best, bestDist
for _, enemy in ipairs(enemiesFolder:GetChildren()) do
if enemy.PrimaryPart then
local hum = enemy:FindFirstChild("Humanoid")
if hum and hum.Health > 0 then
local d = (enemy.PrimaryPart.Position - myPos).Magnitude
if not bestDist or d < bestDist then
best, bestDist = enemy, d
end
end
end
end
return best, bestDist
end
-- Точка вокруг игрока, куда жаба должна стремиться в режиме Follow.
-- Разные жабы получают разные точки по кругу, чтобы не толпиться в одном месте.
function Toad:GetFollowSpot()
local ownerPos = self.owner.PrimaryPart.Position
local angle = (self.slotIndex * 47) % 360 -- 47 — просто "некруглое" число, чтобы точки не совпадали визуально
local rad = math.rad(angle)
local radius = 4 + (self.slotIndex % 3) -- небольшой разброс по радиусу тоже
local offset = Vector3.new(math.cos(rad) * radius, 0, math.sin(rad) * radius)
return ownerPos + offset
end
function Toad:Update()
if self.state == "Jumping" then return end
if not self.model.PrimaryPart then return end
if not self.model.Parent then return end -- модель могла быть удалена
local ownerPos = self.owner.PrimaryPart.Position
local myPos = self.model.PrimaryPart.Position
-- потеряли цель?
if self.target then
if not self.target.Parent
or (self.target.PrimaryPart.Position - ownerPos).Magnitude > LEASH_RADIUS then
self.target = nil
else
local hum = self.target:FindFirstChild("Humanoid")
if hum and hum.Health <= 0 then
self.target = nil
end
end
end
-- ищем новую цель
if not self.target then
local enemy, dist = self:FindEnemy()
if enemy and dist <= AGGRO_RADIUS then
self.target = enemy
end
end
if self.target then
self:JumpTo(self.target.PrimaryPart.Position, true)
else
local followSpot = self:GetFollowSpot()
if (myPos - followSpot).Magnitude > 3 then -- небольшой люфт, чтобы не дёргалась на месте
self:JumpTo(followSpot, false)
end
end
end
function Toad:JumpTo(targetPosRaw, isAttack)
self.state = "Jumping"
local root = self.model.PrimaryPart
-- всегда приземляемся на свою "родную" высоту, не на высоту цели
local targetPos = Vector3.new(targetPosRaw.X, self.groundY, targetPosRaw.Z)
local start = root.Position
local t0 = tick()
local conn
conn = RunService.Heartbeat:Connect(function()
if not self.model.Parent then
conn:Disconnect()
return
end
local a = (tick() - t0) / JUMP_TIME
if a >= 1 then
conn:Disconnect()
self.model:PivotTo(CFrame.new(targetPos) * self:GetFacingCFrame(start, targetPos))
if isAttack and self.target and self.target.Parent then
self:Hit(self.target)
end
self.state = "Follow"
return
end
local flat = start:Lerp(targetPos, a)
local height = math.sin(a * math.pi) * JUMP_HEIGHT
local pos = flat + Vector3.new(0, height, 0)
local facing = self:GetFacingCFrame(start, targetPos)
local flip = CFrame.Angles(a * math.pi * 2, 0, 0)
self.model:PivotTo(CFrame.new(pos) * facing * flip)
end)
end
-- Считает разворот "лицом к цели", с защитой от нулевого вектора направления
function Toad:GetFacingCFrame(fromPos, toPos)
local dir = toPos - fromPos
local flatDir = Vector3.new(dir.X, 0, dir.Z)
if flatDir.Magnitude < 0.1 then
return CFrame.new() -- цель прямо над/под нами — не поворачиваем
end
return CFrame.lookAt(Vector3.new(0,0,0), flatDir.Unit)
end
function Toad:Hit(enemy)
local now = tick()
if now - self.lastAttackTime < ATTACK_COOLDOWN then return end
self.lastAttackTime = now
local hum = enemy:FindFirstChild("Humanoid")
if hum then
hum:TakeDamage(ATTACK_DAMAGE)
end
end
function Toad:Destroy()
if self.model then
self.model:Destroy()
end
end
return Toad