Sistema de grúas para FiveM: programa trabajos profesionales de remolque
Gestiona un trabajo de grúa en FiveM. Solicitudes de NPC, gestión del depósito, animaciones de plataforma y scripts con integración para mecánicos y policía.
Agency Scripts
Fundador y desarrollador principal en Agency Scripts
Descripción general del sistema de grúa
Un sistema de grúas y depósito de vehículos crea todo un ecosistema de trabajos en tu servidor de FiveM y ofrece a los jugadores una trayectoria profesional civil que se relaciona de forma natural con la policía, los mecánicos y otros conductores. El sistema incluye varios componentes interconectados: una capa de gestión del trabajo que controla el inicio y el fin de turno, un sistema de despacho que asigna solicitudes de grúa a conductores disponibles, mecánicas para enganchar vehículos que simulan un remolque realista, un depósito que almacena vehículos incautados y abandonados, y una estructura de tarifas que determina cuánto cuesta recuperar los vehículos depositados. Si está bien diseñado, el trabajo de grúa se convierte en uno de los roles más sociales del servidor, porque cada llamada implica interactuar directamente con otro jugador que necesita ayuda con su vehículo averiado o mal estacionado.
Configuración del trabajo y sistema de turnos
Empieza por definir el trabajo de grúa en el sistema de trabajos de tu framework. Los jugadores deben poder fichar en un depósito de grúas, recibir una grúa de empresa y empezar a aceptar avisos de despacho. El sistema de turnos debe llevar un registro de los gruistas activos para que el despacho sepa quién está disponible. Cuando un jugador fiche, genera una grúa de plataforma de una lista predefinida de modelos y asígnasela. Cuando termine el turno, el vehículo de empresa debe devolverse o eliminarse. Guarda el estado de servicio del jugador en el servidor para que persista correctamente y el sistema de despacho pueda consultarlo:
Config.TowJob = {
jobName = 'tow',
clockInLocation = vector3(409.09, -1622.57, 29.29),
vehicleSpawn = vector4(405.87, -1631.14, 29.29, 228.35),
towVehicles = {
{model = 'flatbed', label = 'Flatbed Truck', rank = 0},
{model = 'towtruck', label = 'Tow Truck (Hook)', rank = 1},
{model = 'towtruck2', label = 'Heavy Tow Truck', rank = 2},
},
impoundLot = vector3(409.59, -1637.87, 29.29),
impoundReturn = vector4(403.12, -1640.23, 29.29, 138.92),
payPerTow = {min = 350, max = 750},
payPerImpound = 500,
}
RegisterNetEvent('tow:server:clockIn', function()
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
if Player.PlayerData.job.name ~= Config.TowJob.jobName then
TriggerClientEvent('QBCore:Notify', src, 'You are not a tow driver', 'error')
return
end
Player.Functions.SetJobDuty(true)
TowDrivers[src] = {
onDuty = true,
currentCall = nil,
vehicleNet = nil,
towedVehicle = nil,
}
TriggerClientEvent('QBCore:Notify', src, 'You are now on duty', 'success')
TriggerClientEvent('tow:client:spawnVehicle', src)
end)
Mecánicas para enganchar vehículos
El principal reto técnico de un sistema de grúas es enganchar un vehículo a otro de forma realista y que se comporte correctamente al conducir. FiveM ofrece la función AttachEntityToEntity nativa para este propósito, pero ajustar bien las posiciones requiere calibrar con cuidado cada modelo de grúa. El método de plataforma funciona mejor con la mayoría de los vehículos: el coche remolcado se coloca sobre la plataforma con los desfases adecuados para que quede asentado de forma natural. En las grúas con gancho, se levantan las ruedas delanteras del vehículo remolcado mientras las traseras permanecen en el suelo. Cada método de enganche requiere distintos vectores de desfase y valores de rotación. Prueba estos valores en el juego y haz pequeños ajustes hasta que el vehículo quede bien colocado sin atravesar el camión:
function AttachVehicleToFlatbed(towTruck, targetVehicle)
local towModel = GetEntityModel(towTruck)
-- Offsets calibrated for the flatbed model
local offsets = {
['flatbed'] = {x = 0.0, y = -0.5, z = 1.1, rx = 0.0, ry = 0.0, rz = 0.0},
}
local offset = offsets['flatbed']
if not offset then return false end
-- Freeze target vehicle physics
SetEntityCollision(targetVehicle, false, false)
FreezeEntityPosition(targetVehicle, true)
-- Attach to flatbed
AttachEntityToEntity(
targetVehicle, towTruck,
0, -- bone index
offset.x, offset.y, offset.z,
offset.rx, offset.ry, offset.rz,
false, false, false, false, 0, true
)
-- Store attachment state
TriggerServerEvent('tow:server:vehicleAttached', VehToNet(towTruck), VehToNet(targetVehicle))
return true
end
function DetachVehicleFromFlatbed(towTruck, targetVehicle)
DetachEntity(targetVehicle, true, true)
SetEntityCollision(targetVehicle, true, true)
FreezeEntityPosition(targetVehicle, false)
-- Place vehicle on ground behind the truck
local truckCoords = GetEntityCoords(towTruck)
local truckHeading = GetEntityHeading(towTruck)
local behind = GetOffsetFromEntityInWorldCoords(towTruck, 0.0, -12.0, 0.0)
SetEntityCoords(targetVehicle, behind.x, behind.y, behind.z)
SetEntityHeading(targetVehicle, truckHeading)
SetVehicleOnGroundProperly(targetVehicle)
TriggerServerEvent('tow:server:vehicleDetached', VehToNet(towTruck))
end
Un detalle importante es desactivar la colisión del vehículo enganchado para evitar fallos de físicas. Cuando dos vehículos tienen la colisión activa y están enganchados, el motor del juego puede provocar sacudidas violentas o lanzarlos por los aires. Congelar la posición del vehículo objetivo y desactivar su colisión mientras está enganchado elimina por completo estos problemas.
Sistema de despacho y solicitudes de grúa
El sistema de despacho conecta a los jugadores que necesitan una grúa con los gruistas disponibles. Los jugadores pueden solicitar una grúa desde el teléfono, a través de un NPC de un taller mecánico o mediante postes de emergencia ubicados por el mapa. Cuando llega una solicitud, el servidor comprueba qué gruistas están disponibles y se la asigna al más cercano. Si no hay gruistas de servicio, ofrece un servicio alternativo de grúa NPC que cobre una tarifa más alta y teletransporte el vehículo al depósito tras un tiempo de espera. La notificación del despacho debe incluir la ubicación del solicitante, el modelo del vehículo y el motivo de la solicitud, como avería, accidente o estacionamiento ilegal. Permite que el gruista acepte o rechace el aviso. Si lo rechaza, asígnalo al siguiente gruista más cercano:
RegisterNetEvent('tow:server:requestTow', function(reason)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local playerCoords = GetEntityCoords(GetPlayerPed(src))
local vehicle = GetVehiclePedIsIn(GetPlayerPed(src), true)
local vehicleModel = vehicle ~= 0 and GetDisplayNameFromVehicleModel(GetEntityModel(vehicle)) or 'Unknown'
-- Find nearest available tow driver
local nearestDriver = nil
local nearestDist = math.huge
for driverId, data in pairs(TowDrivers) do
if data.onDuty and not data.currentCall then
local driverCoords = GetEntityCoords(GetPlayerPed(driverId))
local dist = #(playerCoords - driverCoords)
if dist < nearestDist then
nearestDist = dist
nearestDriver = driverId
end
end
end
if nearestDriver then
TowDrivers[nearestDriver].currentCall = {
requesterId = src,
coords = playerCoords,
vehicle = vehicleModel,
reason = reason,
timestamp = os.time(),
}
TriggerClientEvent('tow:client:incomingCall', nearestDriver, {
coords = playerCoords,
vehicle = vehicleModel,
reason = reason,
playerName = Player.PlayerData.charinfo.firstname,
})
TriggerClientEvent('QBCore:Notify', src, 'Tow truck dispatched. ETA based on driver location.', 'success')
else
TriggerClientEvent('QBCore:Notify', src, 'No tow drivers available. NPC service will arrive shortly.', 'info')
StartNPCTowService(src, playerCoords, vehicle)
end
end)
Gestión del depósito de vehículos
El depósito es donde se guardan los vehículos remolcados e incautados hasta que sus propietarios los recuperen. Diseña la base de datos del depósito para registrar qué vehículos están depositados, quién los depositó, cuándo lo hizo y cuál es la tarifa de liberación. La policía debe poder depositar vehículos directamente durante controles de tráfico o tras persecuciones. Estos depósitos pueden conllevar tarifas más altas o periodos mínimos de retención más prolongados. La interfaz del depósito debe mostrar una lista de los vehículos del jugador que están allí, con datos como el modelo, la matrícula, la fecha de depósito, el motivo y la tarifa de liberación. Los jugadores pagan la tarifa en la ventanilla del depósito y el vehículo aparece en un punto de entrega cercano:
CREATE TABLE IF NOT EXISTS impounded_vehicles (
id INT AUTO_INCREMENT PRIMARY KEY,
plate VARCHAR(10) NOT NULL,
citizenid VARCHAR(50) NOT NULL,
vehicle_data LONGTEXT NOT NULL,
impound_reason ENUM('tow', 'police', 'abandoned', 'parking') DEFAULT 'tow',
impound_fee INT DEFAULT 250,
impounded_by VARCHAR(50) DEFAULT NULL,
impounded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
hold_until TIMESTAMP DEFAULT NULL,
released_at TIMESTAMP DEFAULT NULL,
INDEX idx_plate (plate),
INDEX idx_citizen (citizenid),
INDEX idx_released (released_at)
);
-- Server-side: Impound a vehicle
RegisterNetEvent('tow:server:impoundVehicle', function(vehicleNet, reason)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local vehicle = NetworkGetEntityFromNetworkId(vehicleNet)
if not DoesEntityExist(vehicle) then return end
local plate = GetVehicleNumberPlateText(vehicle)
local vehicleData = QBCore.Functions.GetVehicleProperties(vehicle)
local fee = Config.ImpoundFees[reason] or 250
MySQL.insert('INSERT INTO impounded_vehicles (plate, citizenid, vehicle_data, impound_reason, impound_fee, impounded_by) VALUES (?, ?, ?, ?, ?, ?)', {
plate:trim(),
GetVehicleOwner(plate),
json.encode(vehicleData),
reason,
fee,
Player.PlayerData.citizenid,
})
DeleteEntity(vehicle)
local pay = math.random(Config.TowJob.payPerImpound - 100, Config.TowJob.payPerImpound + 100)
Player.Functions.AddMoney('bank', pay, 'tow-impound')
TriggerClientEvent('QBCore:Notify', src, 'Vehicle impounded. Earned $' .. pay, 'success')
end)
Control automatizado de estacionamiento
Añade un sistema automatizado que detecte vehículos estacionados en zonas restringidas y solicite una grúa cuando termine un periodo de gracia configurable. Define zonas de estacionamiento restringido alrededor de hidrantes, paradas de autobús, comisarías y entradas de urgencias de hospitales. Cuando un jugador estacione en una zona restringida y se aleje, inicia un temporizador. Si el vehículo sigue desatendido cuando termine el periodo de gracia, genera una solicitud de grúa y avisa a los gruistas disponibles. Así creas trabajo de forma orgánica para los gruistas sin exigir que los jugadores soliciten el servicio manualmente. El sistema solo debe marcar vehículos propiedad de jugadores, ya que el motor del juego gestiona los vehículos NPC y los elimina de forma natural. Notifica al propietario del vehículo que su coche ha sido marcado para remolque, dándole la oportunidad de regresar y moverlo antes de que llegue la grúa.
Ingresos y progresión
Estructura el trabajo de grúa con un sistema de progresión que recompense el trabajo constante. Los nuevos conductores de grúa empiezan en el rango cero con acceso a la plataforma básica y ganan una tarifa base por cada remolque completado. A medida que acumulan remolques completados, suben de rango y desbloquean grúas más pesadas capaces de transportar vehículos más grandes, como camiones y autobuses, acceso a contratos de depósito policial que pagan más y una grúa personal que pueden personalizar. Registra estadísticas como el total de remolques completados, la distancia recorrida mientras remolcan y el tiempo medio de respuesta a las llamadas de servicio. Muestra estas estadísticas en un panel de trabajo al que se pueda acceder mediante un NPC o una app del teléfono. Considera implementar un sistema de reputación en el que los conductores de grúa que respondan con rapidez de forma constante y completen trabajos sin dañar vehículos obtengan multiplicadores de bonificación en su paga, mientras que quienes ignoren llamadas o dañen vehículos al remolcarlos reciban penalizaciones en la paga. Esto crea un incentivo natural para ofrecer un servicio de calidad que beneficia a toda la comunidad del servidor, al garantizar que los servicios de grúa sean fiables y profesionales.
Gestión de casos límite y red
Los sistemas de grúa tienen varios casos límite complejos que requieren una gestión cuidadosa. Si un conductor de grúa se desconecta mientras remolca un vehículo, el servidor debe detectar la desconexión y desenganchar el vehículo remolcado de forma segura, dejándolo en el suelo para que no quede flotando en el aire ni caiga a través del mapa. Si el propietario de un vehículo inicia sesión mientras su coche está enganchado a una grúa, debería ver el vehículo marcado como remolcado en su lista de vehículos, junto con información sobre el conductor de la grúa. Gestiona con cuidado la propiedad de las entidades de los vehículos, ya que el native de enganche solo funciona de forma fiable cuando el cliente que realiza el enganche tiene la propiedad de ambas entidades. Usa NetworkRequestControlOfEntity para garantizar que el cliente del conductor de la grúa controle ambos vehículos antes de intentar engancharlos. Para la sincronización multijugador, transmite el estado del enganche a todos los jugadores cercanos para que vean el vehículo remolcado en la posición correcta en sus pantallas. Las bolsas de estado funcionan bien para esto porque replican automáticamente los datos a todos los clientes que estén dentro del alcance de la entidad.