if not TowBarMod then TowBarMod = {} end TowBarMod.Wrecker = TowBarMod.Wrecker or {} local Wrecker = TowBarMod.Wrecker Wrecker.MaxAttachDistance = 0.6 Wrecker.HeightStep = 0.30 Wrecker.MinHeightLevel = 0 Wrecker.MaxHeightLevel = 2 Wrecker.ScanRadius = 8 local SupportedWreckers = { -- KI5 '76 Chevrolet K Series (Workshop 3161951724) ["Base.76chevyC30CCwrecker"] = true, ["Base.76chevyC30SCwrecker"] = true, ["Base.76chevyK30CCwrecker"] = true, ["Base.76chevyK30SCwrecker"] = true, -- KI5 '93 Chevrolet Suburban / Silverado (Workshop 3152529790). -- The mechanic variant inherits the K3500 wrecker template. ["Base.93chevySilveradoK3500wrecker"] = true, ["Base.93chevySilveradoK3500mechanic"] = true, -- KI5 '78 AM General M35 Series Trucks (Workshop 2799152995). ["Base.78amgeneralM62"] = true } local HeightAttachmentIds = { [0] = "towbarWreckerHookLow", [1] = "towbarWreckerHookMid", [2] = "towbarWreckerHookHigh" } local function getScriptFullName(script) if not script then return nil end local ok, fullName = pcall(function() return script:getFullName() end) if ok and fullName then return tostring(fullName) end local name = script:getName() return name and ("Base." .. tostring(name)) or nil end function Wrecker.isSupportedWrecker(vehicle) if not vehicle then return false end local script = vehicle:getScript() local fullName = getScriptFullName(script) return SupportedWreckers[fullName] == true and vehicle:attachmentExist("hook") end function Wrecker.getHeightAttachmentId(level) return HeightAttachmentIds[Wrecker.normalizeHeightLevel(level)] end function Wrecker.normalizeHeightLevel(level) local numeric = tonumber(level) or Wrecker.MinHeightLevel numeric = math.floor(numeric + 0.5) return math.max(Wrecker.MinHeightLevel, math.min(Wrecker.MaxHeightLevel, numeric)) end function Wrecker.nextHeightLevel(currentLevel, direction) local current = Wrecker.normalizeHeightLevel(currentLevel) local delta = tonumber(direction) if delta ~= -1 and delta ~= 1 then return nil end return math.max(Wrecker.MinHeightLevel, math.min(Wrecker.MaxHeightLevel, current + delta)) end local function distanceSquared(pointA, pointB) if not pointA or not pointB then return nil end local dx = pointA:x() - pointB:x() local dy = pointA:y() - pointB:y() return dx * dx + dy * dy end local function forEachVehicle(collection, callback) if not collection then return end if type(collection) == "table" then for _, vehicle in ipairs(collection) do callback(vehicle) end return end local ok, iterator = pcall(function() return collection:iterator() end) if ok and iterator then while iterator:hasNext() do callback(iterator:next()) end return end local sizeOk, size = pcall(function() return collection:size() end) if not sizeOk then return end for index = 0, size - 1 do callback(collection:get(index)) end end local function isTargetEligible(wrecker, vehicle) if not vehicle or vehicle == wrecker then return false end if vehicle:getVehicleTowing() or vehicle:getVehicleTowedBy() then return false end if wrecker:getVehicleTowing() or wrecker:getVehicleTowedBy() then return false end local script = vehicle:getScript() if not script then return false end local name = string.lower(tostring(script:getName() or "")) return not string.find(name, "trailer", 1, true) end function Wrecker.resolveNearestTarget(wrecker, candidates) if not Wrecker.isSupportedWrecker(wrecker) then return nil end local hookPoint = wrecker:getAttachmentWorldPos("hook", Vector3f.new()) if not hookPoint then return nil end local best local maxDistanceSquared = Wrecker.MaxAttachDistance * Wrecker.MaxAttachDistance forEachVehicle(candidates, function(vehicle) if not isTargetEligible(wrecker, vehicle) then return end for _, attachmentId in ipairs({ "trailerfront", "trailer" }) do if vehicle:attachmentExist(attachmentId) then local targetPoint = vehicle:getAttachmentWorldPos(attachmentId, Vector3f.new()) local score = distanceSquared(hookPoint, targetPoint) if score and score <= maxDistanceSquared then local vehicleId = vehicle:getId() if not best or score < best.distanceSquared or (score == best.distanceSquared and vehicleId < best.vehicle:getId()) then best = { vehicle = vehicle, attachment = attachmentId, distanceSquared = score } end end end end end) return best end function Wrecker.getWorldVehicles() local cell = getCell() return cell and cell:getVehicles() or nil end local function registerHookAttachments() local manager = getScriptManager() if not manager then return end for fullName in pairs(SupportedWreckers) do local script = manager:getVehicle(fullName) local baseAttachment = script and script:getAttachmentById("hook") or nil local baseOffset = baseAttachment and baseAttachment:getOffset() or nil if baseOffset then for level = Wrecker.MinHeightLevel, Wrecker.MaxHeightLevel do local attachmentId = HeightAttachmentIds[level] if script:getAttachmentById(attachmentId) == nil then local attachment = ModelAttachment.new(attachmentId) attachment:getOffset():set( baseOffset:x(), baseOffset:y() + level * Wrecker.HeightStep, baseOffset:z() ) attachment:setUpdateConstraint(false) script:addAttachment(attachment) end end end end end Wrecker.registerHookAttachments = registerHookAttachments Events.OnGameBoot.Add(registerHookAttachments)