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


local RunService = game:GetService("RunService")

local Toad = {}
Toad.__index = Toad

local FOLLOW_RADIUS    = 8
local FOLLOW_TOLERANCE = 5
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.5
local DECISION_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
    self.lastLandTime = 0
    self.jumpId = 0

    local root = model.PrimaryPart
    if not root then
        warn("У модели жабы не задан PrimaryPart:", model:GetFullName())
        return self
    end

    -- Убираем лишние объекты, которые могут конфликтовать с ручным управлением моделью
    local humanoid = model:FindFirstChildOfClass("Humanoid")
    if humanoid then humanoid:Destroy() end
    local animateScript = model:FindFirstChild("Animate")
    if animateScript then animateScript:Destroy() end

    -- Сварка всех частей к торсу + убираем физические коллизии/массу
    for _, part in ipairs(model:GetDescendants()) do
        if part:IsA("BasePart") then
            part.CanCollide = false
            part.Massless = true
            part.Anchored = false
            if part ~= root then
                local weld = Instance.new("WeldConstraint")
                weld.Part0 = root
                weld.Part1 = part
                weld.Parent = root
            end
        end
    end

    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

function Toad:GetFollowSpot()
    local ownerPos = self.owner.PrimaryPart.Position
    local angle = (self.slotIndex * 47) % 360
    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.Parent then return end
    if not self.model.PrimaryPart then return end
    if not self.owner or not self.owner.Parent or not self.owner.PrimaryPart 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
        if tick() - self.lastLandTime < DECISION_COOLDOWN then return end
        local followSpot = self:GetFollowSpot()
        if (myPos - followSpot).Magnitude > FOLLOW_TOLERANCE then
            self:JumpTo(followSpot, false)
        end
    end
end

function Toad:JumpTo(targetPosRaw, isAttack)
    self.state = "Jumping"
    self.jumpId = self.jumpId + 1
    local myJumpId = self.jumpId

    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 self.jumpId ~= myJumpId or 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"
            self.lastLandTime = tick()
            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()
    self.jumpId = self.jumpId + 1 -- останавливает активный Heartbeat-цикл прыжка
    if self.model then
        self.model:Destroy()
    end
end

return Toad