├── 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 | // });
--------------------------------------------------------------------------------