Загрузка данных


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