let roping = false; let carriedRope = null; let hookProp = null; setTick(async () => { let ms = 1000; // Ensure correct job if (Config.RequiredJobs && Config.RequiredJobs.length > 0) { const playerJob = GetPlayerJob(); if (!Config.RequiredJobs.includes(playerJob)) { await Delay(ms); return; } } // Ensure player is not in a vehicle const ped = PlayerPedId(); if (IsPedInAnyVehicle(ped, true)) { await Delay(ms); return; } // Ensure player is not dead if (Functions.IsPlayerDead()) { await Delay(ms); return; } // Check for a vehicle const pedCoords = GetEntityCoords(ped, true); let vehicle = Functions.GetClosestVehicle({ coords: pedCoords, range: 5, }); if (!DoesEntityExist(vehicle)) { await Delay(ms); return; } // Ensure vehicle is not broken down if (GetVehicleEngineHealth(vehicle) <= 100.0) { await Delay(ms); return; } // Check the vehicle class const vehicleModel = GetEntityModel(vehicle); if (Config.TowVehicles[vehicleModel] == null) { await Delay(ms); return; } // Define the pickup coords let TowVehicleCoords = Config.TowVehicles[vehicleModel]; if (TowVehicleCoords == Config.TowVehicles[Config.FlatbedModel] && Entity(vehicle).state.bedLowered) TowVehicleCoords = Config.FlatbedLowerd; // Ensure the player is close to the attach point const offsetCoords = GetOffsetFromEntityInWorldCoords(vehicle, ...TowVehicleCoords); const dist = Functions.CalcDist(pedCoords, offsetCoords); if (dist > 1.5) { await Delay(ms); return; } // Slow down tick ms = 0; // Display the correct text const ropeAttachedVehicle = Entity(vehicle).state.RopeAttachedVehicle != null; let text = roping ? Config.Locales['cancel'] : Config.Locales['rope']; text = ropeAttachedVehicle ? Config.Locales['remove'] : text; text = dist < 1 ? Config.Locales['keyIndicator'] + text : text; DrawText3D(offsetCoords, text); // Click handler if (dist < 1 && IsControlJustReleased(0, Config.Keys.interact)) { if (ropeAttachedVehicle) { emitNet('gs_towrope:DetachRope', NetworkGetNetworkIdFromEntity(vehicle)); Functions.PlayAnim(ped, 'anim@amb@clubhouse@tutorial@bkr_tut_ig3@', 'machinic_loop_mechandplayer', { infinite: true, blend: 2.0, flag: 1 }); await Delay(2500) ClearPedTasks(ped) } else { await HandleRope(vehicle, offsetCoords); } } if (ms > 0) await Delay(ms); }); const HandleRope = async (originVehicle, originCoords) => { const ped = PlayerPedId(); // Handle the case of an existing rope if (roping) { ClearPedTasks(ped); DeleteRope(carriedRope); DeleteEntity(hookProp); roping = false; return; } // Play animation to pickup the the rope. Functions.PlayAnim(ped, 'anim@amb@clubhouse@tutorial@bkr_tut_ig3@', 'machinic_loop_mechandplayer', { infinite: true, blend: 2.0, flag: 1 }); await Delay(2500) ClearPedTasks(ped) // Spawn the hook and play the animation, give some time for the prop to get to the right position in the animation Functions.PlayAnim(ped, 'move_p_m_zero_rucksack', 'idle', { infinite: true, blend: 2.0, flag: 35 }); hookProp = await AddPropToPlayer('prop_rope_hook_01', 6286, 0.05, 0.02, -0.02, 84.0, -191.0, 29.0); await Delay(750); // Create the rope and attach the entities carriedRope = await CreateRope(...originCoords, 0.0, 0.0, 0.0, 0.5, 0.5, 0.1); const boneCoords = GetWorldPositionOfEntityBone(ped, GetPedBoneIndex(ped, 40269)); AttachEntitiesToRope(carriedRope, ped, originVehicle, ...boneCoords, ...originCoords, Config.MaxRopeLength, 0, 0); roping = true; const carryLoop = setTick(async () => { // Disable entering vehicles DisableControlAction(0, 23, true); // If canceled, delete the current rope attached to the player and reset the state and stop this tick. if (IsControlPressed(0, Config.Keys.cancel) || IsPedInAnyVehicle(ped, true) || IsPedRagdoll(ped) || !roping) { ClearPedTasks(ped); RemoveRope(carriedRope); DeleteEntity(hookProp); clearTick(carryLoop); roping = false; } // Find the closest vehicle and display text to attach rope const pedCoords = GetEntityCoords(ped); let closestVehicle = Functions.GetClosestVehicle({ coords: pedCoords, range: 10, }); // Only continue if a vehicle is found if (closestVehicle != 0 && closestVehicle != originVehicle) { let text = Config.Locales['attachCable']; // Get the vehicle model dimensions and text positions const [maximum, minimum] = GetModelDimensions(GetEntityModel(closestVehicle)); const yOffset = (maximum[1] - minimum[1]) / 2; const vehicleOffsetBack = GetOffsetFromEntityInWorldCoords(closestVehicle, 0.0, yOffset, 0.0); const vehicleOffsetFront = GetOffsetFromEntityInWorldCoords(closestVehicle, 0.0, -yOffset, 0.0); // Check if front or back is closest to the ped let vehicleOffset = vehicleOffsetBack; let yOffsetSign = 1; if (Functions.CalcDist(pedCoords, vehicleOffsetBack) > Functions.CalcDist(pedCoords, vehicleOffsetFront)) { vehicleOffset = vehicleOffsetFront; yOffsetSign = -1; } // Display the correct text const dist = Functions.CalcDist(pedCoords, vehicleOffset); text = dist < 2.0 ? Config.Locales['keyIndicator'] + text : text; DrawText3D(vehicleOffset, text); // If the ped is close enough, allow attachment of the rope if (dist < 2.0) { if (IsControlPressed(0, Config.Keys.interact)) { // Check if the found vehicle is not attached to anything. if (IsEntityAttached(closestVehicle)) { Functions.ShowNotification({ message: Config.Locales['vehicleIsAttached'] }); return; } // Animation attaching the rope. const vehicleHeading = GetEntityHeading(closestVehicle); const pedHeading = yOffsetSign == -1 ? vehicleHeading - 180.0 : vehicleHeading; SetEntityHeading(ped, pedHeading); Functions.PlayAnim(ped, 'anim@amb@clubhouse@tutorial@bkr_tut_ig3@', 'machinic_loop_mechandplayer', { infinite: true, blend: 2.0, flag: 51 }); await Delay(2500) ClearPedTasks(ped) // If vehicle is still close, attach the rope if (Functions.CalcDist(pedCoords, GetOffsetFromEntityInWorldCoords(closestVehicle, 0.0, yOffsetSign * yOffset, 0.0)) < 3.0) { // Update the server with the rope start coords and the to be attached vehicle emitNet( 'gs_towrope:AttachRope', NetworkGetNetworkIdFromEntity(originVehicle), NetworkGetNetworkIdFromEntity(closestVehicle), yOffsetSign ); } ClearPedTasks(ped); RemoveRope(carriedRope); DeleteEntity(hookProp); clearTick(carryLoop); roping = false; } } } }); }; const CreateRope = async (x1, y1, z1, x2, y2, z2, maxLength, initLength, minLength) => { RopeLoadTextures(); while (!RopeAreTexturesLoaded()) { await Delay(10); } retval = AddRope(x1, y1, z1, x2, y2, z2, maxLength, 4, initLength, minLength, 1.0, false, false, true, 5.0, false, 0); retval = retval[0]; return retval; }; const RemoveRope = async (rope) => { if (DoesRopeExist(rope)) { DeleteRope(rope); RopeUnloadTextures(); } }; const AddPropToPlayer = async (propName, bone, off1, off2, off3, rot1, rot2, rot3) => { const Player = PlayerPedId(); const [x, y, z] = GetEntityCoords(Player); await Functions.LoadModel(propName); animProp = CreateObject(GetHashKey(propName), x, y, z + 0.2, true, true, true); while (!DoesEntityExist(animProp)) { await Delay(50); } SetEntityCollision(animProp, false, false); if (typeof bone == 'string') { bone = GetEntityBoneIndexByName(Player, bone); } else { bone = GetPedBoneIndex(Player, bone); } AttachEntityToEntity(animProp, Player, bone, off1, off2, off3, rot1, rot2, rot3, true, true, false, true, 1, true); SetModelAsNoLongerNeeded(propName); return animProp; }; const GetAttachedVehicleRopePosition = (attachedVehicle, yOffsetSign) => { const [maximum, minimum] = GetModelDimensions(GetEntityModel(attachedVehicle)); const yOffset = (maximum[1] - minimum[1]) / 2; const attachedVehicleRopePosition = GetOffsetFromEntityInWorldCoords(attachedVehicle, 0.0, yOffsetSign * yOffset + 0.5 * yOffsetSign, 0.0); return attachedVehicleRopePosition; }; const ropeList = {}; AddStateBagChangeHandler('RopeAttachedVehicle', null, async (bagName, key, RopeAttachedVehicleInfo, reserved, replicated) => { // Only continue if a vehicle is attached with a rope if (RopeAttachedVehicleInfo == null) return; // Ensure the main vehicle is loaded let startTime = GetGameTimer(); let vehicle = 0; while (vehicle == 0 && GetGameTimer() - startTime < 1000) { vehicle = GetEntityFromStateBagName(bagName); await Delay(0); } if (vehicle == 0) return; // Check the vehicle class for the rope attach position const vehicleModel = GetEntityModel(vehicle); if (Config.TowVehicles[vehicleModel] == null) return; // Define the pickup coords let TowVehicleCoords = Config.TowVehicles[vehicleModel]; if (TowVehicleCoords == Config.TowVehicles[Config.FlatbedModel] && Entity(vehicle).state.bedLowered) TowVehicleCoords = Config.FlatbedLowerd; const vehicleRopePosition = GetOffsetFromEntityInWorldCoords(vehicle, ...TowVehicleCoords); // Ensure the attachedVehicle is loaded const attachedVehicleNetId = RopeAttachedVehicleInfo[0]; startTime = GetGameTimer(); while (!NetworkDoesNetworkIdExist(attachedVehicleNetId) && GetGameTimer() - startTime < 1000) { await Delay(0); } if (!NetworkDoesNetworkIdExist(attachedVehicleNetId)) return; const attachedVehicle = NetworkGetEntityFromNetworkId(attachedVehicleNetId); if (!DoesEntityExist(attachedVehicle)) { return; } // Get the sign of the y-offset and the attached vehicle rope position const yOffsetSign = RopeAttachedVehicleInfo[1]; const attachedVehicleRopePosition = GetAttachedVehicleRopePosition(attachedVehicle, yOffsetSign); // Ensure the distance between the vehicles is not to large const vehicleDist = Functions.CalcDist(vehicleRopePosition, attachedVehicleRopePosition); if (vehicleDist > Config.MaxRopeLength + 2.5) return; // +2.5 to take into account the [Press E] distance and some extra (desync) margin // Create the rope const rope = await CreateRope(...vehicleRopePosition, 0.0, 0.0, 0.0, 0.5, vehicleDist, 0.1); // Max dist at 0.5 to fix the rope texture going through the endpoint () AttachEntitiesToRope(rope, vehicle, attachedVehicle, ...vehicleRopePosition, ...attachedVehicleRopePosition, vehicleDist + 0.5, 0, 0); // Some extra margin with +0.5 ropeList[vehicle] = rope; // Create a loop to check if the rope needs to be deleted let winding = false; const ropeLoop = setTick(async () => { let ms = 1000; if (!DoesEntityExist(vehicle) || !DoesEntityExist(attachedVehicle) || Entity(vehicle).state.RopeAttachedVehicle == null) { DeleteRope(rope); delete ropeList[vehicle]; clearTick(ropeLoop); return; } const vehicleSpeed = GetEntitySpeed(vehicle); const attachedVehicleSpeed = GetEntitySpeed(attachedVehicle); if (vehicleSpeed > 10 || attachedVehicleSpeed > 10) { emitNet('gs_towrope:DetachRope', NetworkGetNetworkIdFromEntity(vehicle)); await Delay(500); return; } // In case the distance is short and the vehicle is frozen, it should be unfrozen (seperate from if-statement below due to the flag != 131) const dist = RopeGetDistanceBetweenEnds(rope); const playerId = PlayerId(); if (dist < Config.MinimumRopeLength && IsEntityPositionFrozen(vehicle)) { const vehicleEntityOwner = NetworkGetEntityOwner(vehicle); if (playerId == vehicleEntityOwner) { Functions.ShowNotification({ message: Config.Locales['ropeTurnedOff'] }); FreezeEntityPosition(vehicle, false); } } // In case the rope distance is short or the vehicle is moving, dont allow any interaction and stop the rope from moving const ropeFlags = GetRopeFlags(rope); const attachedVehicleEntityOwner = NetworkGetEntityOwner(attachedVehicle); if ((dist < Config.MinimumRopeLength && ropeFlags != 131) || (GetEntitySpeed(vehicle) > 0.5 && ropeFlags != 131)) { // Flag 131 is standard, 163 is winding if (playerId == attachedVehicleEntityOwner) { StopRopeWinding(rope); emitNet( 'gs_towrope:RopeResetLength', NetworkGetNetworkIdFromEntity(vehicle), NetworkGetNetworkIdFromEntity(attachedVehicle), yOffsetSign ); } else { emitNet( 'gs_towrope:StopWindRope', NetworkGetNetworkIdFromEntity(vehicle), NetworkGetNetworkIdFromEntity(attachedVehicle), yOffsetSign ); } await Delay(ms); return; } // Check if the ped is the driver of the main vehicle to allow interaction const ped = PlayerPedId(); const driver = GetPedInVehicleSeat(vehicle, -1); if (ped == driver && dist >= Config.MinimumRopeLength) { ms = 0; if (IsControlJustReleased(0, Config.Keys.wind)) { if (!winding) { // Stop winding winding = true; FreezeEntityPosition(vehicle, true); Functions.ShowNotification({ message: Config.Locales['ropeTurnedOn'] }); if (playerId == attachedVehicleEntityOwner) { StartRopeWinding(rope); } else { emitNet('gs_towrope:StartWindRope', NetworkGetNetworkIdFromEntity(vehicle), NetworkGetNetworkIdFromEntity(attachedVehicle)); } } else { // Start winding if (playerId == attachedVehicleEntityOwner) { StopRopeWinding(rope); emitNet( 'gs_towrope:RopeResetLength', NetworkGetNetworkIdFromEntity(vehicle), NetworkGetNetworkIdFromEntity(attachedVehicle), yOffsetSign ); } else { emitNet( 'gs_towrope:StopWindRope', NetworkGetNetworkIdFromEntity(vehicle), NetworkGetNetworkIdFromEntity(attachedVehicle), yOffsetSign ); } winding = false; FreezeEntityPosition(vehicle, false); Functions.ShowNotification({ message: Config.Locales['ropeTurnedOff'] }); } await Delay(500); } } if (ms > 0) await Delay(ms); }); }); onNet('gs_towrope:StartWindRopeClient', async (vehicleNetId) => { // Check if the net id and entity exist if (!NetworkDoesNetworkIdExist(vehicleNetId)) return; const vehicle = NetworkGetEntityFromNetworkId(vehicleNetId); if (!DoesEntityExist(vehicle)) return; // Avoid the start of winding in case the distance is not synced correctly const dist = RopeGetDistanceBetweenEnds(ropeList[vehicle]); if (dist < Config.MinimumRopeLength) return; if (ropeList[vehicle] != null) { StartRopeWinding(ropeList[vehicle]); } }); onNet('gs_towrope:StopWindRopeClient', async (vehicleNetId) => { // Check if the net id and entity exist if (!NetworkDoesNetworkIdExist(vehicleNetId)) return; const vehicle = NetworkGetEntityFromNetworkId(vehicleNetId); if (!DoesEntityExist(vehicle)) return; if (ropeList[vehicle] != null) { StopRopeWinding(ropeList[vehicle]); } }); onNet('gs_towrope:RopeResetLenghtClient', async (vehicleNetId, attachedVehicleNetId, yOffsetSign) => { // Check if the net id and entity exist if (!NetworkDoesNetworkIdExist(vehicleNetId)) return; const vehicle = NetworkGetEntityFromNetworkId(vehicleNetId); if (!DoesEntityExist(vehicle)) return; // If the player is the entity owner of the flatbed, and the flatbed is frozen, it should be unfrozen // This is done as extra, incase the flatbed is somehow still frozen if (PlayerId() == NetworkGetEntityOwner(vehicle)) { if (IsEntityPositionFrozen(vehicle)); FreezeEntityPosition(vehicle, false); } // Check if the net id and entity exist if (!NetworkDoesNetworkIdExist(attachedVehicleNetId)) return; const attachedVehicle = NetworkGetEntityFromNetworkId(attachedVehicleNetId); if (!DoesEntityExist(attachedVehicle)) return; // Only continue if the rope already exists if (ropeList[vehicle] != null) { const dist = RopeGetDistanceBetweenEnds(ropeList[vehicle]); if (dist > Config.MaxRopeLength + 2.5) return; // Check the vehicle class for the rope attach position const vehicleModel = GetEntityModel(vehicle); if (Config.TowVehicles[vehicleModel] == null) return; let TowVehicleCoords = Config.TowVehicles[vehicleModel]; if (TowVehicleCoords == Config.TowVehicles[Config.FlatbedModel] && Entity(vehicle).state.bedLowered) TowVehicleCoords = Config.FlatbedLowerd; const vehicleRopePosition = GetOffsetFromEntityInWorldCoords(vehicle, ...TowVehicleCoords); // Get the rope position for the attachedVehicle const attachedVehicleRopePosition = GetAttachedVehicleRopePosition(attachedVehicle, yOffsetSign); AttachEntitiesToRope(ropeList[vehicle], vehicle, attachedVehicle, ...vehicleRopePosition, ...attachedVehicleRopePosition, dist, 0, 0); } });