Skip to content

Instantly share code, notes, and snippets.

@ThangCZ
Last active April 23, 2022 12:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ThangCZ/52ebf7cbea9f18549ac6854688295386 to your computer and use it in GitHub Desktop.
Save ThangCZ/52ebf7cbea9f18549ac6854688295386 to your computer and use it in GitHub Desktop.
Helicam
const fovMax = 80.0;
const fovMin = 10.0; // max zoom level (smaller fov is more zoom)
const zoomspeed = 2.0; // camera zoom speed
const speedLr = 3.0; // speed by which the camera pans left-right
const speedUd = 3.0; // speed by which the camera pans up-down
// control id to toggle nightvision mode. Default: INPUT_AIM (Right mouse btn)
const toggleNightvision = 25;
// control id to lock onto a vehicle with the camera. Default (spacebar)
const toggleLockOn = 22;
helicam = false;
// default FOV when the helicam is enabled.
const fov = (fovMax + fovMin) * 0.5;
let nightvision = false;
let cam;
let scaleform;
let lockedOnVehicle = null;
let lastLockedOn = 0;
const spotlightButton = 101; // Numpad 5
const spotlightSyncRate = 5;
const serverSyncRate = 5;
let lerpValue = 0;
let spotlightTimer = null;
let spotlightRemoteId = null;
let spotlights = {};
let spotlightBrightness = 5;
let spotlightRoundness = 20;
let spotlightRadius = 4;
let spotlightFalloff = 2;
mp.events.add('switchhelicam', () => {
if (helicam) {
destroyHelicam();
} else {
enableHelicam();
}
});
async function enableHelicam() {
mp.game.graphics.setTimecycleModifier('heliGunCam');
mp.game.graphics.setTimecycleModifierStrength(0.3);
scaleform = mp.game.graphics.requestScaleformMovie('HELI_CAM');
while (!mp.game.graphics.hasScaleformMovieLoaded(scaleform)) {
await mp.game.waitAsync(0);
}
const lPed = mp.players.local;
const heli = lPed.vehicle;
cam = mp.cameras.new(
'DEFAULT_SCRIPTED_FLY_CAMERA',
lPed.position,
new mp.Vector3(0, 0, mp.players.local.getHeading()),
60,
);
cam.setActive(true);
cam.setRot(0.0, 0.0, heli.getHeading(), 2);
cam.setFov(fov);
cam.setFarClip(5000);
mp.game.cam.renderScriptCams(true, false, 0, true, false);
// _ATTACH_CAM_TO_VEHICLE_BONE
mp.game.invoke('0x8DB3F12A02CAEF72', cam.handle, heli.handle,
getSpotlightBoneIndex(heli), false, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5, false);
mp.game.graphics.pushScaleformMovieFunction(scaleform, 'SET_CAM_LOGO');
mp.game.graphics.pushScaleformMovieFunctionParameterInt(0);
mp.game.graphics.popScaleformMovieFunctionVoid();
helicam = true;
}
function destroyHelicam() {
// CLEAR_TIMECYCLE_MODIFIER
mp.game.invoke('0x0F07E7745A236711');
// CLEAR_FOCUS
mp.game.invoke('0x31B73D1EA9F01DA2');
mp.game.cam.renderScriptCams(false, false, 0, true, false);
if (scaleform != null || scaleform != 0) {
mp.game.graphics.setScaleformMovieAsNoLongerNeeded(scaleform);
}
if (cam != null) {
cam.destroy(true);
cam = null;
}
helicam = false;
mp.game.graphics.setSeethrough(false);
mp.game.graphics.setNightvision(false);
nightvision = false;
lockedOnVehicle = null;
destroySpotlight();
}
function handleHelicamZoom() {
mp.game.controls.disableAllControlActions(2);
const x = mp.game.controls.getDisabledControlNormal(7, 1) * speedLr;
const y = mp.game.controls.getDisabledControlNormal(7, 2) * speedUd;
const zoomIn = mp.game.controls.getDisabledControlNormal(2, 40) * zoomspeed;
const zoomOut =
mp.game.controls.getDisabledControlNormal(2, 41) * zoomspeed;
let currentRot = cam.getRot(2);
currentRot = new mp.Vector3(currentRot.x - y, 0, currentRot.z - x);
cam.setRot(currentRot.x, currentRot.y, currentRot.z, 2);
if (zoomIn > 0) {
let currentFov = cam.getFov();
currentFov -= zoomIn;
if (currentFov < fovMin) currentFov = fovMin;
cam.setFov(currentFov);
} else if (zoomOut > 0) {
let currentFov = cam.getFov();
currentFov += zoomOut;
if (currentFov > fovMax) currentFov = fovMax;
cam.setFov(currentFov);
}
}
function drawAltFovScaleform() {
mp.game.graphics.pushScaleformMovieFunction(
scaleform,
'SET_ALT_FOV_HEADING',
);
mp.game.graphics.pushScaleformMovieFunctionParameterFloat(
mp.players.local.vehicle.position.z,
);
const zoomValue = (1.0/(fovMax-fovMin))*(cam.getFov()-fovMin);
mp.game.graphics.pushScaleformMovieFunctionParameterFloat(zoomValue);
mp.game.graphics.pushScaleformMovieFunctionParameterFloat(cam.getRot(2).z);
mp.game.graphics.popScaleformMovieFunctionVoid();
mp.game.graphics.drawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255,
true);
}
mp.events.add('render', () => {
if (!helicam) {
return;
}
if (!mp.players.local.vehicle || !mp.players.local.vehicle.doesExist()) {
destroyHelicam();
return;
}
if (cam !== null && cam.isActive() && cam.isRendering()) {
handleHelicamZoom();
}
if (mp.game.controls.isDisabledControlJustPressed(0, toggleNightvision)) {
changeVision();
}
if (lockedOnVehicle) {
if (lockedOnVehicle.handle != 0 &&
(isLockOnVisible(cam, lockedOnVehicle) ||
Date.now() - lastLockedOn < 1000 ) &&
!mp.game.controls.isDisabledControlJustPressed(0, toggleLockOn)) {
cam.pointAt(lockedOnVehicle.handle, 0, 0, 0, true);
renderVehicleInfo(lockedOnVehicle);
} else {
mp.game.audio.playSoundFrontend(
-1, 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', false);
lockedOnVehicle = null;
cam.stopPointing();
}
} else if (mp.game.controls.isDisabledControlJustPressed(0, toggleLockOn)) {
const vehicleDetected = pointingAtVehicle(cam);
if (vehicleDetected != null && vehicleDetected.handle != 0) {
mp.game.audio.playSoundFrontend(
-1, 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', false);
lockedOnVehicle = vehicleDetected;
lastLockedOn = Date.now();
}
}
drawAltFovScaleform();
});
function changeVision() {
mp.game.audio.playSoundFrontend(
-1, 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', false);
nightvision = !nightvision;
mp.game.graphics.setNightvision(nightvision);
}
function renderVehicleInfo(vehicle) {
const vehname = mp.game.ui.getLabelText(
mp.game.vehicle.getDisplayNameFromVehicleModel(vehicle.model),
);
const licenseplate = vehicle.getNumberPlateText();
const speed = (vehicle.getSpeed() * 3.6) | 0;
mp.game.graphics.drawText(
`Model: ${vehname}\n Plate: ${licenseplate}\n Speed: ${speed} km/h`,
[0.5, 0.9],
{
font: 0,
color: [255, 255, 255, 185],
scale: [0.0, 0.45],
outline: true,
},
);
}
function getPointinigAtEntity(camera, distance = 1000) {
const position = camera.getCoord();
const direction = camera.getDirection();
const farAway = new mp.Vector3(
direction.x * distance + position.x,
direction.y * distance + position.y,
direction.z * distance + position.z,
);
const result = mp.raycasting.testPointToPoint(position, farAway,
mp.players.local.vehicle.handle, 19);
if (result && result.entity.handle !== mp.players.local.handle &&
result.entity.handle !== mp.players.local.vehicle.handle) {
return result.entity;
}
return null;
}
function pointingAtVehicle(camera, distance = 1000) {
const entity = getPointinigAtEntity(camera, distance);
if (entity && entity.type === 'vehicle') {
return entity;
}
return null;
}
// TODO: The whole lock on thing is a bit fiddly and needs some love.
function isLockOnVisible(camera, entity, distance = 1000) {
return true;
const remoteEntity = getPointinigAtEntity(camera, distance);
if (remoteEntity === entity) {
lastLockedOn = Date.now();
return true;
}
return false;
}
function destroySpotlight() {
if (spotlightTimer) {
clearInterval(spotlightTimer);
spotlightTimer = null;
mp.events.callRemote('deletespotlight', spotlightRemoteId);
}
}
function syncSpotlight() {
const direction = cam.getDirection();
mp.events.callRemote('updatespotlight', spotlightRemoteId,
direction.x, direction.y, direction.z,
lockedOnVehicle ? lockedOnVehicle.remoteId : -1);
}
mp.events.add('render', () => {
// float TIMESTEP();
const timeDelta = mp.game.invokeFloat('0x0000000050597EE2');
lerpValue += timeDelta / (1.0 / serverSyncRate);
for (const [sHelicopter, direction] of Object.entries(spotlights)) {
const helicopter = mp.vehicles.atRemoteId(sHelicopter);
if (!helicopter || !helicopter.doesExist()) {
continue;
}
const matrix = helicopter.getMatrix(null, null, null, null);
const spotlightPos = helicopter.getWorldPositionOfBone(
getSpotlightBoneIndex(helicopter));
let pos = new mp.Vector3(spotlightPos.x, spotlightPos.y, spotlightPos.z);
pos = pos.subtract(matrix.upVector.multiply(0.5));
let drawPos = new mp.Vector3(direction.x, direction.y, direction.z);
if (direction.vehicle != -1) {
const lockedVehicle = mp.vehicles.atRemoteId(direction.vehicle);
if (lockedVehicle && lockedVehicle.doesExist()) {
drawPos = lockedVehicle.position.subtract(pos);
}
} else {
drawPos = lerpVec(direction.oldX, direction.oldY, direction.oldZ,
direction.x, direction.y, direction.z, lerpValue);
}
mp.game.graphics.drawSpotLightWithShadow(pos.x, pos.y, pos.z,
drawPos.x, drawPos.y, drawPos.z, 255, 255, 255, 1000,
spotlightBrightness, spotlightRoundness,
spotlightRadius, spotlightFalloff, parseInt(sHelicopter) + 1);
}
});
mp.events.add('updatespotlights', (spotlightsJson) => {
const newSpotlights = {};
for (const [sHelicopter, direction] of Object.entries(JSON.parse(
spotlightsJson))) {
direction.oldX = direction.x;
direction.oldY = direction.y;
direction.oldZ = direction.z;
if (sHelicopter in spotlights && direction.vehicle == -1 &&
spotlights[sHelicopter].vehicle == -1) {
const lerpedDirection = lerpVec(spotlights[sHelicopter].oldX,
spotlights[sHelicopter].oldY, spotlights[sHelicopter].oldZ,
spotlights[sHelicopter].x, spotlights[sHelicopter].y,
spotlights[sHelicopter].z, lerpValue);
direction.oldX = lerpedDirection.x;
direction.oldY = lerpedDirection.y;
direction.oldZ = lerpedDirection.z;
}
newSpotlights[sHelicopter] = direction;
}
spotlights = newSpotlights;
lerpValue = 0;
});
mp.keys.bind(spotlightButton, false, () => {
if (!helicam) {
return;
}
if (spotlightTimer) {
destroySpotlight();
return;
}
spotlightRemoteId = mp.players.local.vehicle.remoteId;
spotlightTimer = setInterval(syncSpotlight, 1000 / spotlightSyncRate);
});
function getSpotlightBoneIndex(vehicle) {
for (const bone of ['searchlight_base', 'turret_1base']) {
const boneIndex = vehicle.getBoneIndexByName(bone);
if (boneIndex !== -1) {
return boneIndex;
}
}
return -1;
}
function lerpVec(oldX, oldY, oldZ, newX, newY, newZ, t) {
return new mp.Vector3(
oldX + (newX - oldX) * t,
oldY + (newY - oldY) * t,
oldZ + (newZ - oldZ) * t,
);
}
// Debug stuff
// Bind to H
mp.keys.bind(0x48, true, function() {
if (mp.players.local.vehicle && mp.players.local.vehicle.doesExist()&&
mp.players.local.vehicle.getClass() == 15 &&
getSpotlightBoneIndex(mp.players.local.vehicle) !== -1) {
mp.events.call('switchhelicam');
}
});
// Change properties of the spotlight rendering
mp.events.add('playerCommand', (command) => {
const args = command.split(/[ ]+/);
const commandName = args[0];
args.shift();
if (commandName == 'brightness') {
spotlightBrightness = Number(args[0]);
}
if (commandName == 'roundness') {
spotlightRoundness = Number(args[0]);
}
if (commandName == 'radius') {
spotlightRadius = Number(args[0]);
}
if (commandName == 'falloff') {
spotlightFalloff = Number(args[0]);
}
});
const syncRate = 5;
const spotlights = {};
mp.events.add('updatespotlight', (_, remoteId, dirx, diry, dirz, veh) => {
spotlights[remoteId] = {x: dirx, y: diry, z: dirz, vehicle: veh};
});
mp.events.add('deletespotlight', (_, remoteId) => {
delete spotlights[remoteId];
});
setInterval(() => {
const result = JSON.stringify(spotlights);
mp.players.call('updatespotlights', [result]);
}, 1000 / syncRate);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment