├── stream ├── p_dock_crane_cabl_s.ydr └── p_dock_crane_cabl_s+hidr.ytd ├── fxmanifest.lua ├── server └── server.js ├── README.md └── client ├── objects.js └── main.js /stream/p_dock_crane_cabl_s.ydr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonassvensson4/harbor-crane/HEAD/stream/p_dock_crane_cabl_s.ydr -------------------------------------------------------------------------------- /stream/p_dock_crane_cabl_s+hidr.ytd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonassvensson4/harbor-crane/HEAD/stream/p_dock_crane_cabl_s+hidr.ytd -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'adamant' 2 | 3 | game 'gta5' 4 | 5 | client_scripts { 6 | 'client/objects.js', 7 | 'client/main.js' 8 | } 9 | 10 | server_script 'server/server.js'; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | let hasSpawnedObjects = false; 2 | 3 | onNet('harbor-crane:spawn', () => { 4 | if ( !hasSpawnedObjects ) { 5 | hasSpawnedObjects = true; 6 | emitNet('harbor-crane:createObjects', source); 7 | } 8 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # harbor-crane 2 | A functioning harbor crane from the GTA story mission Scouting the Port. 3 | 4 | Do note that this is a work in progress. There are some features that will hopefully get updated with the help of the FiveM community. 5 | 6 | FiveM forum post: https://forum.cfx.re/t/harbor-crane/1169287 7 | 8 | # Controls 9 | **/usecrane** - Enter/Exit the crane, use the command when standing outside the cabin near the ladder.
10 | **W** - Moves the lifter/spreader up.
11 | **S** - Moves the lifter/spreader down.
12 | **A** - Moves the whole crane to the left.
13 | **D** - Moves the whole crane to the right.
14 | **Arrow up** - Moves the cabin forward.
15 | **Arrow down** - Moves the cabin backwards.
16 | **Enter** - Attach/Detach container.
17 | **1** - Reset camera
18 | **2** - Camera from above
19 | **3** - Camera from the left side
20 | **4** - Camera from the right side
21 | -------------------------------------------------------------------------------- /client/objects.js: -------------------------------------------------------------------------------- 1 | // All the objects that the crane uses, including the containers which it can pick up 2 | const objects = { 3 | 'frame': { 4 | model: 'prop_dock_rtg_ld', 5 | coords: { x: -47.290, y: -2415.690, z: 5.18 } 6 | }, 7 | 'cabin': { 8 | model: 'p_dock_rtg_ld_cab', 9 | position: { x: -0.1, y: 0.0, z: 18.0 }, 10 | attachTo: 'frame' 11 | }, 12 | 'lifter-cables1': { 13 | model: 'p_dock_crane_cabl_s', 14 | position: { x: 0.1, y: 1.0, z: -5.5 }, 15 | rotation: { x: 0.0, y: 0.0, z: 90.0 }, 16 | attachTo: 'cabin' 17 | }, 18 | 'lifter-cables2': { 19 | model: 'p_dock_crane_cabl_s', 20 | position: { x: 0.0, y: 0.0, z: 0.0 }, 21 | rotation: { x: 0.0, y: 0.0, z: 90.0 }, 22 | attachTo: 'lifter-cables1' 23 | }, 24 | 'lifter-cables3': { 25 | model: 'p_dock_crane_cabl_s', 26 | position: { x: 0.0, y: 0.0, z: 0.0 }, 27 | rotation: { x: 0.0, y: 0.0, z: 90.0 }, 28 | attachTo: 'lifter-cables2' 29 | }, 30 | 'lifter-cables4': { 31 | model: 'p_dock_crane_cabl_s', 32 | position: { x: 0.0, y: 0.0, z: 0.0 }, 33 | rotation: { x: 0.0, y: 0.0, z: 90.0 }, 34 | attachTo: 'lifter-cables3' 35 | }, 36 | 'lifter-cables5': { 37 | model: 'p_dock_crane_cabl_s', 38 | position: { x: 0.0, y: 0.0, z: 0.0 }, 39 | rotation: { x: 0.0, y: 0.0, z: 90.0 }, 40 | attachTo: 'lifter-cables4' 41 | }, 42 | 'lifter-cables6': { 43 | model: 'p_dock_crane_cabl_s', 44 | position: { x: 0.0, y: 0.0, z: 0.0 }, 45 | rotation: { x: 0.0, y: 0.0, z: 90.0 }, 46 | attachTo: 'lifter-cables5' 47 | }, 48 | 'lifter': { 49 | model: 'p_dock_crane_sld_s', 50 | position: { x: 0.0, y: 0.0, z: 1.3 }, 51 | rotation: { x: 0.0, y: 0.0, z: 90.0 }, 52 | attachTo: 'lifter-cables6' 53 | }, 54 | 'wheel': { 55 | model: 'p_dock_rtg_ld_wheel', 56 | positions: { 57 | leftFront: { 58 | 2: { x: -5, y: -9.02, z: 0.65 }, 59 | 1: { x: -3.58, y: -9.02, z: 0.65 } 60 | }, 61 | leftBack: { 62 | 2: { x: 3.3, y: -9.02, z: 0.65 }, 63 | 1: { x: 4.7, y: -9.02, z: 0.65 } 64 | }, 65 | rightFront: { 66 | 2: { x: -5, y: 9.46, z: 0.65 }, 67 | 1: { x: -3.58, y: 9.46, z: 0.65 } 68 | }, 69 | rightBack: { 70 | 2: { x: 3.3, y: 9.46, z: 0.65 }, 71 | 1: { x: 4.7, y: 9.46, z: 0.65 } 72 | } 73 | }, 74 | attachTo: 'frame' 75 | }, 76 | 'containers': { 77 | 'container-red': { 78 | model: 'prop_container_01a' 79 | }, 80 | 'container-bilgeco-blue': { 81 | model: 'prop_container_01c' 82 | }, 83 | 'container-jetsam': { 84 | model: 'prop_container_01d' 85 | }, 86 | 'container-bilgeco-green': { 87 | model: 'prop_container_01e' 88 | }, 89 | 'container-krapea': { 90 | model: 'prop_container_01b' 91 | }, 92 | 'container-postop': { 93 | model: 'prop_container_01h' 94 | }, 95 | 'container-gopostal': { 96 | model: 'prop_container_01g' 97 | }, 98 | 'container-landocorp': { 99 | model: 'prop_container_01f' 100 | } 101 | } 102 | } 103 | 104 | // All the created objects will be stored in this object 105 | let createdObjects = {}; 106 | 107 | // Starting position of the lifter cables, need this since there are 6 cables 108 | let currentCable = 1; 109 | 110 | // Tick to be able to check if the container hits the specific Z value, #NoCollisionWorkAround 111 | let containerFallTick; 112 | 113 | // Make sure every model has been requested 114 | Object.keys( objects ).forEach(type => { 115 | if ( type === 'containers' ) { 116 | Object.keys( objects[type] ).forEach(container => { 117 | let model = objects[type][container].model; 118 | 119 | if ( !HasModelLoaded(model) && IsModelInCdimage(model) ) { 120 | RequestModel(model); 121 | } 122 | }); 123 | } else { 124 | let model = objects[type].model; 125 | 126 | if ( !HasModelLoaded(model) && IsModelInCdimage(model) ) { 127 | RequestModel(model); 128 | } 129 | } 130 | }); 131 | 132 | // Place and attach objects to the crane 133 | function placeCrane() { 134 | Object.keys( objects ).forEach(type => { 135 | let model = GetHashKey(objects[type].model); 136 | 137 | if ( objects[type].coords ) { 138 | // Object has coords set, should just be placed with the CreateObject native 139 | createdObjects[type] = CreateObject(model, objects[type].coords.x, objects[type].coords.y, objects[type].coords.z, true, true, true); 140 | 141 | if ( objects[type].heading ) { 142 | SetEntityHeading(createdObjects[type], objects[type].heading); 143 | } 144 | } else if ( objects[type].positions ) { 145 | // Object has multiple positions set, it should then be attached to the frame object 146 | const positions = objects[type].positions; 147 | 148 | Object.keys( positions ).forEach(group => { 149 | Object.keys( positions[group] ).forEach(num => { 150 | createdObjects[`${ type }-${ group }-${ num }`] = CreateObject( 151 | model, 152 | objects.frame.coords.x, 153 | objects.frame.coords.y, 154 | objects.frame.coords.z, 155 | 1.0, 156 | true, 157 | false 158 | ); 159 | AttachEntityToEntity( 160 | createdObjects[`${ type }-${ group }-${ num }`], 161 | createdObjects[objects[type].attachTo], 162 | 0, 163 | positions[group][num].x, 164 | positions[group][num].y, 165 | positions[group][num].z, 166 | 0.0, 167 | 0.0, 168 | 0.0, 169 | false, 170 | false, 171 | true, 172 | false, 173 | 0, 174 | false 175 | ); 176 | }); 177 | }); 178 | } else if ( objects[type].position ) { 179 | let collision = true; 180 | 181 | if ( type === 'cabin' ) { 182 | collision = true; 183 | } 184 | 185 | // Object has just one position set, attach it 186 | createdObjects[type] = CreateObject( 187 | model, 188 | objects.frame.coords.x, 189 | objects.frame.coords.y, 190 | objects.frame.coords.z + 15.5, 191 | 1.0, 192 | true, 193 | false 194 | ); 195 | 196 | AttachEntityToEntity( 197 | createdObjects[type], 198 | createdObjects[objects[type].attachTo], 199 | 0, 200 | objects[type].position.x, 201 | objects[type].position.y, 202 | objects[type].position.z, 203 | objects[type].rotation ? objects[type].rotation.x : 0.0, 204 | objects[type].rotation ? objects[type].rotation.y : 0.0, 205 | objects[type].rotation ? objects[type].rotation.z : 0.0, 206 | false, 207 | false, 208 | collision, 209 | false, 210 | 0, 211 | false 212 | ); 213 | // AttachEntityToEntityPhysically( 214 | // createdObjects[type], 215 | // createdObjects[objects[type].attachTo], 216 | // 0, 217 | // 0, 218 | // objects[type].position.x, 219 | // objects[type].position.y, 220 | // objects[type].position.z, 221 | // objects[type].position.x, 222 | // objects[type].position.y, 223 | // objects[type].position.z, 224 | // objects[type].rotation ? objects[type].rotation.x : 0.0, 225 | // objects[type].rotation ? objects[type].rotation.y : 0.0, 226 | // objects[type].rotation ? objects[type].rotation.z : 0.0, 227 | // 10.0, 228 | // false, 229 | // false, 230 | // true, 231 | // false, 232 | // 0 233 | // ); 234 | } 235 | }); 236 | } 237 | 238 | // Place random containers 239 | function placeContainers() { 240 | // Coordinates where the containers can spawn at 241 | const containerSpawn = { 242 | x: [ -52.990, -66.80324, -80.80324, -94.80324 ], 243 | y: [ -2420.99, -2418.29, -2415.59, -2412.92, -2410.22, ], 244 | z: [ 5, 7.82, 10.64 ], 245 | heading: 90.0 246 | } 247 | 248 | // Array of all the containers, to make sure it doesn't spawn two containers at the same location 249 | let containerPlacements = [ 250 | [ 251 | [], 252 | [], 253 | [] 254 | ], 255 | [ 256 | [], 257 | [], 258 | [] 259 | ], 260 | [ 261 | [], 262 | [], 263 | [] 264 | ], 265 | [ 266 | [], 267 | [], 268 | [] 269 | ], 270 | ]; 271 | 272 | let containerAmount = Math.floor(Math.random() * 60) + 3; 273 | let xPlacement = 0; 274 | let zPlacement = 0; 275 | 276 | function SpawnContainer( data ) { 277 | let name = `${ data.color }-${ data.x }-${ data.index }-${ data.z }`; 278 | 279 | createdObjects[name] = CreateObject( 280 | GetHashKey(objects.containers[data.color].model), 281 | containerSpawn.x[data.x], 282 | containerSpawn.y[data.index], 283 | containerSpawn.z[data.z], 284 | true, 285 | true, 286 | false 287 | ); 288 | 289 | SetEntityHeading(createdObjects[name], containerSpawn.heading); 290 | FreezeEntityPosition(createdObjects[name], true); 291 | 292 | containerPlacements[data.x][data.z].push({ 293 | color: data.color, 294 | coords: [ containerSpawn.x[data.x], containerSpawn.y[data.index], containerSpawn.z[data.z] ], 295 | entity: createdObjects[name] 296 | }); 297 | } 298 | 299 | for ( let i = 0; i < containerAmount; i++ ) { 300 | for ( let x = 0; x < containerPlacements.length; x++ ) { 301 | for ( let z = 0; z < containerPlacements[x].length; z++ ) { 302 | let containerColor = Object.keys( objects.containers )[Math.floor(Math.random() * Math.floor(Object.keys( objects.containers ).length))]; 303 | let index = containerPlacements[xPlacement][zPlacement].length; 304 | 305 | if ( !containerPlacements[xPlacement][zPlacement].length || containerPlacements[xPlacement][zPlacement].length < 5 ) { 306 | SpawnContainer({ 307 | x: xPlacement, 308 | z: zPlacement, 309 | index: index, 310 | color: containerColor 311 | }); 312 | break; 313 | } else { 314 | if ( zPlacement != 2 ) { 315 | zPlacement = zPlacement + 1; 316 | 317 | SpawnContainer({ 318 | x: xPlacement, 319 | z: zPlacement, 320 | index: index, 321 | color: containerColor 322 | }); 323 | } else { 324 | if ( xPlacement < containerPlacements.length - 1 ) { 325 | zPlacement = 0; 326 | xPlacement = xPlacement + 1; 327 | 328 | SpawnContainer({ 329 | x: xPlacement, 330 | z: zPlacement, 331 | index: index, 332 | color: containerColor 333 | }); 334 | } 335 | } 336 | break; 337 | } 338 | } 339 | break; 340 | } 341 | } 342 | } 343 | 344 | emitNet('harbor-crane:spawn'); 345 | onNet('harbor-crane:createObjects', () => { 346 | placeCrane(); 347 | // Wait a couple of seconds before spawning in the containers, a ladder on the crane seems to break if it's spawning in the containers too quickly 348 | setTimeout(() => { 349 | placeContainers(); 350 | }, 4000); 351 | }); -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | // Tick to be able to use keys, storing it in a variable to be able to remove it later 2 | let useCraneTick; 3 | 4 | // Using crane? False or true, I have this to be able to use the command as an toggle 5 | let usingCrane = false; 6 | 7 | // The camera 8 | let cam; 9 | 10 | let soundID = {}; 11 | 12 | // Request sounds and animations 13 | RequestAmbientAudioBank('Crane', false, -1); 14 | RequestAmbientAudioBank('Crane_Stress', false, -1); 15 | RequestAmbientAudioBank('Crane_Impact_Sweeteners', false, -1); 16 | RequestScriptAudioBank('Container_Lifter', false, -1); 17 | RequestAnimDict('map_objects'); 18 | RequestAnimDict('missheistdockssetup1trevor_crane'); 19 | 20 | // Crane movements, camera positioning and attaching/detaching containers 21 | class Crane { 22 | constructor() { 23 | this.container; 24 | this.cameraAngle; 25 | } 26 | 27 | SetContainer( container ) { 28 | return this.container = container; 29 | } 30 | 31 | GetPos( object ) { 32 | return GetEntityCoords(createdObjects[object]); 33 | } 34 | 35 | Down() { 36 | let cable = `lifter-cables${ currentCable }`; 37 | 38 | // Change max Z value if there's a container attached so it won't clip through the ground 39 | let maxZ = this.container ? -2.16 : -2.5; 40 | 41 | if ( (Math.round((objects[cable].position.z - 0.06 + Number.EPSILON) * 100) / 100) >= maxZ ) { 42 | if ( !soundID['down'] ) { 43 | soundID['down'] = { 44 | id: GetSoundId(), 45 | played: true 46 | } 47 | 48 | PlaySoundFromEntity( 49 | soundID['down'].id, 50 | 'Move_U_D', 51 | createdObjects['cabin'], 52 | 'CRANE_SOUNDS', 53 | 0, 54 | false, 55 | 0 56 | ); 57 | } 58 | 59 | objects[cable].position.z = objects[cable].position.z - 0.06; 60 | AttachEntityToEntity(createdObjects[cable], createdObjects[`lifter-cables${ currentCable - 1 }`], 0, 0.0, 0.0, objects[cable].position.z, 0.0, 0.0, 90.0, false, false, true, false, 0, false); 61 | } else { 62 | if ( currentCable != 6 ) { 63 | currentCable = currentCable + 1; 64 | } 65 | } 66 | } 67 | 68 | Up() { 69 | let cable = `lifter-cables${ currentCable }`; 70 | 71 | if ( (Math.round((objects[cable].position.z + 0.06 + Number.EPSILON) * 100) / 100) < 0.0 ) { 72 | if ( !soundID['up'] ) { 73 | soundID['up'] = { 74 | id: GetSoundId(), 75 | played: true 76 | } 77 | 78 | PlaySoundFromEntity( 79 | soundID['up'].id, 80 | 'Move_U_D', 81 | createdObjects['cabin'], 82 | 'CRANE_SOUNDS', 83 | 0, 84 | false, 85 | 0 86 | ); 87 | } 88 | 89 | objects[cable].position.z = objects[cable].position.z + 0.06; 90 | AttachEntityToEntity(createdObjects[cable], createdObjects[`lifter-cables${ currentCable - 1 }`], 0, 0.0, 0.0, objects[cable].position.z, 0.0, 0.0, 90.0, false, false, true, false, 0, false); 91 | } else { 92 | if ( currentCable != 2 ) { 93 | currentCable = currentCable - 1; 94 | } 95 | } 96 | } 97 | 98 | Left() { 99 | if ( (Math.round((objects['cabin'].position.y - 0.02 + Number.EPSILON) * 100) / 100) > -7.0 ) { 100 | if ( !soundID['left'] ) { 101 | soundID['left'] = { 102 | id: GetSoundId(), 103 | played: true 104 | } 105 | 106 | PlaySoundFromEntity( 107 | soundID['left'].id, 108 | 'Move_L_R', 109 | createdObjects['cabin'], 110 | 'CRANE_SOUNDS', 111 | 0, 112 | false, 113 | 0 114 | ); 115 | } 116 | 117 | objects['cabin'].position.y = objects['cabin'].position.y - 0.02; 118 | AttachEntityToEntity(createdObjects['cabin'], createdObjects['frame'], 0, -0.1, objects['cabin'].position.y, 18.0, 0.0, 0.0, 0.0, false, false, true, false, 0, false); 119 | this.Camera(this.cameraAngle, [ 1, objects['cabin'].position.y ]); 120 | } 121 | } 122 | 123 | Right() { 124 | if ( (Math.round((objects['cabin'].position.y - 0.02 + Number.EPSILON) * 100) / 100) < 5.66 ) { 125 | if ( !soundID['right'] ) { 126 | soundID['right'] = { 127 | id: GetSoundId(), 128 | played: true 129 | } 130 | 131 | PlaySoundFromEntity( 132 | soundID['right'].id, 133 | 'Move_L_R', 134 | createdObjects['cabin'], 135 | 'CRANE_SOUNDS', 136 | 0, 137 | false, 138 | 0 139 | ); 140 | } 141 | 142 | objects['cabin'].position.y = objects['cabin'].position.y + 0.02; 143 | AttachEntityToEntity(createdObjects['cabin'], createdObjects['frame'], 0, -0.1, objects['cabin'].position.y, 18.0, 0.0, 0.0, 0.0, false, false, true, false, 0, false); 144 | this.Camera(this.cameraAngle, [ 1, objects['cabin'].position.y ]); 145 | } 146 | } 147 | 148 | Forward() { 149 | if ( (Math.round((objects['frame'].coords.x - 0.05 + Number.EPSILON) * 100) / 100) > -109.34 ) { 150 | if ( !soundID['forward'] ) { 151 | soundID['forward'] = { 152 | id: GetSoundId(), 153 | played: true 154 | } 155 | 156 | PlaySoundFromEntity( 157 | soundID['forward'].id, 158 | 'Move_Base', 159 | createdObjects['cabin'], 160 | 'CRANE_SOUNDS', 161 | 0, 162 | false, 163 | 0 164 | ); 165 | } 166 | 167 | objects['frame'].coords.x = objects['frame'].coords.x - 0.05; 168 | SetEntityCoords(createdObjects['frame'], objects['frame'].coords.x, objects['frame'].coords.y, objects['frame'].coords.z - 0.12); 169 | this.Camera(this.cameraAngle, [ 0, objects['cabin'].position.x ]); 170 | } 171 | } 172 | 173 | Backwards() { 174 | if ( (Math.round((objects['frame'].coords.x + 0.05 + Number.EPSILON) * 100) / 100) < -47.29 ) { 175 | if ( !soundID['backwards'] ) { 176 | soundID['backwards'] = { 177 | id: GetSoundId(), 178 | played: true 179 | } 180 | 181 | PlaySoundFromEntity( 182 | soundID['backwards'].id, 183 | 'Move_Base', 184 | createdObjects['cabin'], 185 | 'CRANE_SOUNDS', 186 | 0, 187 | false, 188 | 0 189 | ); 190 | } 191 | 192 | objects['frame'].coords.x = objects['frame'].coords.x + 0.05; 193 | SetEntityCoords(createdObjects['frame'], objects['frame'].coords.x, objects['frame'].coords.y, objects['frame'].coords.z - 0.12); 194 | this.Camera(this.cameraAngle, [ 0, objects['cabin'].position.x ]); 195 | } 196 | } 197 | 198 | Attach() { 199 | Object.keys( objects.containers ).forEach(container => { 200 | let model = GetHashKey(objects.containers[container].model); 201 | let coords = this.GetPos('lifter'); 202 | let closestContainer = GetClosestObjectOfType(coords[0], coords[1], coords[2] - 2.8, 1.0, model); 203 | 204 | if ( closestContainer && !this.container ) { 205 | this.container = closestContainer; 206 | 207 | FreezeEntityPosition(this.container, true); 208 | AttachEntityToEntity( 209 | this.container, 210 | createdObjects['lifter'], 211 | 0, 212 | 0.0, 213 | 0.0, 214 | -3.2, 215 | 0.0, 216 | 0.0, 217 | 90.0, 218 | false, 219 | false, 220 | true, 221 | false, 222 | 0, 223 | false 224 | ); 225 | 226 | PlaySoundFromEntity( 227 | -1, 228 | 'Attach_Container', 229 | createdObjects['frame'], 230 | 'CRANE_SOUNDS', 231 | false, 232 | false 233 | ); 234 | 235 | PlayEntityAnim(createdObjects['lifter'], 'Dock_crane_SLD_load', 'map_objects', 8.0, false, true, 0, 0.0, 0); 236 | } 237 | }); 238 | } 239 | 240 | Detach() { 241 | let zValue = 5; 242 | 243 | Object.keys( objects.containers ).forEach(( container, i ) => { 244 | let model = GetHashKey(objects.containers[container].model); 245 | let coords = this.GetPos('lifter'); 246 | let closestContainer = GetClosestObjectOfType(coords[0], coords[1], zValue, 1.5, model); 247 | 248 | if ( closestContainer && this.container != closestContainer ) { 249 | zValue = GetEntityCoords(closestContainer)[2] + 2.8; 250 | } 251 | }); 252 | 253 | DetachEntity(this.container); 254 | SetEntityCollision(this.container, false, true); 255 | FreezeEntityPosition(this.container, false); 256 | SetEntityDynamic(this.container, true); 257 | 258 | PlaySoundFromEntity( 259 | -1, 260 | 'Detach_Container', 261 | createdObjects['frame'], 262 | 'CRANE_SOUNDS', 263 | false, 264 | false 265 | ); 266 | 267 | PlayEntityAnim(createdObjects['lifter'], 'Dock_crane_SLD_unload', 'map_objects', 8.0, false, true, 0, 0.0, 0); 268 | 269 | containerFallTick = setTick(() => { 270 | if ( GetEntityCoords(this.container)[2] <= zValue ) { 271 | FreezeEntityPosition(this.container, true); 272 | SetEntityCollision(this.container, true); 273 | clearTick(containerFallTick); 274 | this.container = false; 275 | } 276 | }); 277 | } 278 | 279 | AttachOrDetach() { 280 | if ( !this.container ) { 281 | this.Attach(); 282 | } else { 283 | this.Detach(); 284 | } 285 | } 286 | 287 | Camera( angle, coords ) { 288 | let frameCoords = this.GetPos('frame'); 289 | let cabinCoords = this.GetPos('cabin'); 290 | 291 | if ( !DoesCamExist(cam) ) { 292 | cam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true); 293 | } 294 | 295 | switch ( angle ) { 296 | case 0: 297 | // Reset camera 298 | RenderScriptCams(false, false, 0, true, true); 299 | DestroyCam(cam); 300 | this.cameraAngle = 0; 301 | break; 302 | case 1: 303 | // Camera from above 304 | if ( coords ) { 305 | if ( cabinCoords[coords[0]] < coords[1] ) { 306 | cabinCoords[coords[0]] = cabinCoords[coords[0]] + coords[1]; 307 | } else { 308 | cabinCoords[coords[0]] = cabinCoords[coords[0]] - coords[1]; 309 | } 310 | } 311 | 312 | this.cameraAngle = 1; 313 | 314 | SetCamCoord(cam, cabinCoords[0], cabinCoords[1] + 1, cabinCoords[2] - 2.8); 315 | PointCamAtCoord(cam, cabinCoords[0], cabinCoords[1] + 1, cabinCoords[2] - 10); 316 | RenderScriptCams(true, true, 0, true, true); 317 | break; 318 | case 2: 319 | // Camera from the right side 320 | this.cameraAngle = 2; 321 | 322 | SetCamCoord(cam, frameCoords[0] - 5, frameCoords[1] + 8, frameCoords[2] + 10); 323 | PointCamAtCoord(cam, cabinCoords[0], cabinCoords[1], cabinCoords[2] - 20); 324 | RenderScriptCams(true, true, 0, true, true); 325 | break; 326 | case 3: 327 | // Camera from the right side 328 | this.cameraAngle = 3; 329 | 330 | SetCamCoord(cam, frameCoords[0] + 5, frameCoords[1] - 8, frameCoords[2] + 10); 331 | PointCamAtCoord(cam, cabinCoords[0], cabinCoords[1], cabinCoords[2] - 20); 332 | RenderScriptCams(true, true, 0, true, true); 333 | break; 334 | } 335 | } 336 | } 337 | 338 | let crane = new Crane(); 339 | 340 | // Stops and removes the sound 341 | function releaseSound( sound ) { 342 | if ( soundID[sound] ) { 343 | StopSound(soundID[sound].id); 344 | ReleaseSoundId(soundID[sound].id); 345 | delete soundID[sound]; 346 | } 347 | } 348 | 349 | // Crane controls 350 | function useCrane() { 351 | useCraneTick = setTick(() => { 352 | // Hide the weapon wheel since I'm using some of the buttons that opens it 353 | HideHudComponentThisFrame(19); 354 | 355 | // Key: Arrow down 356 | if ( IsControlPressed(1, 173) ) { 357 | crane.Left(); 358 | } 359 | 360 | if ( IsControlJustReleased(1, 173) ) { 361 | releaseSound('left'); 362 | } 363 | 364 | // Key: Arrow up 365 | if ( IsControlPressed(1, 172) ) { 366 | crane.Right(); 367 | } 368 | 369 | if ( IsControlJustReleased(1, 172) ) { 370 | releaseSound('right'); 371 | } 372 | 373 | // Key: W 374 | if ( IsControlPressed(1, 32) ) { 375 | crane.Up(); 376 | } 377 | 378 | if ( IsControlJustReleased(1, 32) ) { 379 | releaseSound('up'); 380 | } 381 | 382 | // Key: S 383 | if ( IsControlPressed(1, 33) ) { 384 | crane.Down(); 385 | } 386 | 387 | if ( IsControlJustReleased(1, 33) ) { 388 | releaseSound('down'); 389 | } 390 | 391 | // Key: A 392 | if ( IsControlPressed(1, 34) ) { 393 | crane.Forward(); 394 | } 395 | 396 | if ( IsControlJustReleased(1, 34) ) { 397 | releaseSound('forward'); 398 | } 399 | 400 | // Key: D 401 | if ( IsControlPressed(1, 35) ) { 402 | crane.Backwards(); 403 | } 404 | 405 | if ( IsControlJustReleased(1, 35) ) { 406 | releaseSound('backwards'); 407 | } 408 | 409 | // Key: Enter 410 | if ( IsControlJustReleased(1, 191) ) { 411 | crane.AttachOrDetach(); 412 | } 413 | 414 | // Key: 1 415 | if ( IsControlJustReleased(1, 157) ) { 416 | crane.Camera(0); 417 | } 418 | 419 | // Key: 2 420 | if ( IsControlJustReleased(1, 158) ) { 421 | crane.Camera(1); 422 | } 423 | 424 | // Key: 3 425 | if ( IsControlJustReleased(1, 160) ) { 426 | crane.Camera(2); 427 | } 428 | 429 | // Key: 4 430 | if ( IsControlJustReleased(1, 164) ) { 431 | crane.Camera(3); 432 | } 433 | }); 434 | } 435 | 436 | // usecrane command, first use will enter the crane, second use will exit the crane 437 | RegisterCommand('usecrane', () => { 438 | let playerPed = PlayerPedId(); 439 | let coordsPlayer = GetEntityCoords(playerPed); 440 | let coordsCabin = crane.GetPos('cabin'); 441 | let distance = GetDistanceBetweenCoords(coordsPlayer[0], coordsPlayer[1], coordsPlayer[2] + 1, coordsCabin[0], coordsCabin[1] - 2, coordsCabin[2], true); 442 | 443 | if ( !usingCrane ) { 444 | // Enter the crane 445 | usingCrane = true; 446 | 447 | if ( distance < 2.2 ) { 448 | // Enter animation scene 449 | scene = CreateSynchronizedScene(-0.1, -0.1, -0.35, 0, 0, 0, 2); 450 | AttachSynchronizedSceneToEntity(scene, createdObjects['cabin'], -1); 451 | TaskSynchronizedScene(PlayerPedId(), scene, 'missheistdockssetup1trevor_crane', 'get_in', 1000.0, -8.0, 0, 0, 1148846080, 0); 452 | SetSynchronizedSceneOcclusionPortal(scene, true); 453 | 454 | // Let the animation run for a couple of seconds 455 | setTimeout(() => { 456 | // Change to first person 457 | SetFollowPedCamViewMode(4); 458 | // Enable crane controls 459 | useCrane(); 460 | }, 2000); 461 | } else { 462 | // Player is standing too far away 463 | emit('chat:addMessage', { 464 | color: [255, 0, 0], 465 | multiline: true, 466 | args: ['Crane', `You're standing too far away!`] 467 | }); 468 | } 469 | } else { 470 | // Exit the crane, reset everything 471 | usingCrane = false; 472 | 473 | // Remove the useCrane tick, crane controls will be disabled 474 | clearTick(useCraneTick); 475 | 476 | // Exit animation scene 477 | scene = CreateSynchronizedScene(-0.1, -0.1, -0.35, 0, 0, 0, 2); 478 | SetSynchronizedSceneOcclusionPortal(scene, true); 479 | SetSynchronizedSceneLooped(scene, false) 480 | AttachSynchronizedSceneToEntity(scene, createdObjects['cabin'], -1); 481 | TaskSynchronizedScene(PlayerPedId(), scene, 'missheistdockssetup1trevor_crane', 'get_out', 1000.0, -8.0, 0, 0, 1148846080, 0); 482 | 483 | 484 | // Give the animation some time to finish 485 | setTimeout(() => { 486 | SetFollowPedCamViewMode(1); // Reset to third person 487 | SetEntityCollision(playerPed, true, true); // Set ped collision since it seems to be gone without it(?) 488 | ClearPedTasks(playerPed); // Clear the ped task 489 | DetachEntity(playerPed, false, true); // Detach the player from the cabin 490 | }, 8000); 491 | } 492 | }); 493 | 494 | // Used in development 495 | // setTick(() => { 496 | // // Remove all the created objects by pressing E 497 | // if ( IsControlJustReleased(1, 38) ) { 498 | // Object.keys( createdObjects ).forEach(type => { 499 | // DeleteObject(createdObjects[type]); 500 | // }); 501 | // RopeUnloadTextures() 502 | // DeleteRope(createdObjects['rope']); 503 | // } 504 | // }); --------------------------------------------------------------------------------