--[[ Dynamic Gameplay Camera v1.0.29 - Organic Idle Orbit CLIENT-SIDE - Multi Theft Auto: San Andreas Objetivo desta versão: - Gameplay camera estilo GTA V / Forza, estável atrás do veículo. - Q/E laterais e Mouse3/Q+E para visão de ré enquanto pressionado. - Idle integrada, mas SEM animações/keyframes/perfis. - Quando o veículo fica parado, entra uma câmera orgânica contínua: angle, raio, altura e foco variam por seno/cosseno. - Não existe troca de animação, ponto zero, fromCam -> target interno ou reset para chase. ]] local Config = { enabled = true, debug = false, activation = { minSpeedForGameplay = 1.0, }, camera = { -- Chase normal forwardCamOffset = { x = 0.0, y = -7.35, z = 2.28 }, forwardLookOffset = { x = 0.0, y = 2.25, z = 0.86 }, -- Visão de ré / ré real reverseCamOffset = { x = 0.0, y = 6.35, z = 2.18 }, reverseLookOffset = { x = 0.0, y = -2.65, z = 0.82 }, positionSmooth = 10.5, lookSmooth = 12.0, reverseSmooth = 7.2, -- Inércia bem sutil accelPullMax = 0.18, brakePushMax = 0.12, accelSmooth = 7.0, }, fov = { enabled = true, default = 70, max = 84, speedForMax = 220, smooth = 6.5, }, turnVision = { enabled = true, minSpeed = 12.0, fullSpeed = 95.0, inputStrength = 0.22, yawRateStrength = 0.18, cameraSideShift = 0.12, lookSideShift = 0.38, frontLookBoost = 0.55, maxCameraSide = 0.20, maxLookSide = 0.52, maxFrontBoost = 0.70, smooth = 10.0, invert = false, }, manualViews = { enabled = true, sideSmooth = 11.0, rearSmooth = 10.0, -- Q/E lateral levemente angulada sideCam = { x = 4.25, y = -4.55, z = 2.10 }, sideLook = { x = 0.78, y = 1.55, z = 0.88 }, -- Mouse3 ou Q+E rearCamOffset = { x = 0.0, y = 6.15, z = 2.12 }, rearLookOffset = { x = 0.0, y = -3.15, z = 0.84 }, }, safety = { maxTiltForEffects = 50.0, resetFovOnStopResource = true, }, } local IdleConfig = { enabled = true, idleTimeToStart = 5000, minSpeedToStart = 1.5, minSpeedToExit = 2.2, closeCooldown = 1200, cancelWhenCursorVisible = true, forceVehicleStreamable = true, -- Organic Idle Orbit: não há perfis, keyframes nem transições internas. orbitSpeed = 0.095, -- rad/s. Menor = mais lento. radiusSideBase = 5.35, radiusFrontRearBase = 4.85, radiusVariation = 0.72, detailVariation = 0.26, heightBase = 0.96, heightVariation = 0.33, minHeight = 0.48, maxHeight = 1.48, lookSideVariation = 0.42, lookFrontRearVariation = 0.88, lookZBase = 0.70, lookZVariation = 0.13, -- A câmera viva segue o alvo orgânico como mola. followSmooth = 4.9, lookSmooth = 5.7, firstEntrySmooth = 2.3, firstEntryTimeMs = 2400, microShake = 0.0012, enableTestCommands = true, } local state = { active = false, vehicle = nil, cameraMode = "off", -- off | gameplay | idle lastTick = getTickCount(), currentCam = nil, currentLook = nil, currentFov = Config.fov.default, lastSpeed = 0, lastYaw = nil, yawRate = 0, accelEffect = 0, turnAmount = 0, manualSide = 0, manualSideBlend = 0, manualRearBlend = 0, mouse3Held = false, qHeld = false, eHeld = false, } local idle = { active = false, idleStartTick = false, lastCloseTick = 0, entryTick = 0, phase = 0, followCam = nil, followLook = nil, } -- ========================================================= -- UTIL -- ========================================================= local function clamp(v, mn, mx) return math.max(mn, math.min(mx, v)) end local function lerp(a, b, t) return a + (b - a) * t end local function smoothAlpha(speed, dt) return 1 - math.exp(-(speed or 8) * (dt or 0.016)) end local function normalizeAngle(a) while a > 180 do a = a - 360 end while a < -180 do a = a + 360 end return a end local function getSpeedKMH(vehicle) if not isElement(vehicle) then return 0 end local vx, vy, vz = getElementVelocity(vehicle) return ((vx * vx + vy * vy + vz * vz) ^ 0.5) * 180 end local function getDriverVehicle() local vehicle = getPedOccupiedVehicle(localPlayer) if not vehicle or not isElement(vehicle) then return nil end if getVehicleOccupant(vehicle, 0) ~= localPlayer then return nil end if isPedDead(localPlayer) then return nil end return vehicle end local function getVehicleYaw(vehicle) local _, _, rz = getElementRotation(vehicle) return rz or 0 end local function getVehicleTilt(vehicle) local rx, ry = getElementRotation(vehicle) rx = math.abs(normalizeAngle(rx or 0)) ry = math.abs(normalizeAngle(ry or 0)) return math.max(rx, ry) end -- Posição relativa usando yaw estável, não pitch/roll. -- Isso evita que a câmera vire junto em capotamento ou pancada. local function getVehicleRelativeYawPosition(vehicle, ox, oy, oz) if not isElement(vehicle) then return nil end local px, py, pz = getElementPosition(vehicle) local yaw = math.rad(getVehicleYaw(vehicle)) local cy, sy = math.cos(yaw), math.sin(yaw) local x = px + cy * ox + (-sy) * oy local y = py + sy * ox + cy * oy local z = pz + oz return x, y, z end local function worldToVehicleYawOffset(vehicle, wx, wy, wz) if not isElement(vehicle) or not wx then return 0, -6, 2 end local px, py, pz = getElementPosition(vehicle) local yaw = math.rad(getVehicleYaw(vehicle)) local cy, sy = math.cos(yaw), math.sin(yaw) local dx, dy = wx - px, wy - py local ox = dx * cy + dy * sy local oy = dx * (-sy) + dy * cy local oz = wz - pz return ox, oy, oz end local function getSignedForwardSpeedKMH(vehicle) if not isElement(vehicle) then return 0 end local vx, vy = getElementVelocity(vehicle) local yaw = math.rad(getVehicleYaw(vehicle)) local fx, fy = -math.sin(yaw), math.cos(yaw) local dot = vx * fx + vy * fy return dot * 180 end local function applyFov(value) state.currentFov = value if setCameraFieldOfView then setCameraFieldOfView("vehicle", value) setCameraFieldOfView("player", value) end end local function resetCameraNative() applyFov(Config.fov.default) setCameraTarget(localPlayer) end local function getLookLeft() return state.qHeld or getControlState("look_left") end local function getLookRight() return state.eHeld or getControlState("look_right") end local function getLookBehind() return state.mouse3Held or getControlState("look_behind") or (getLookLeft() and getLookRight()) end -- ========================================================= -- LIFECYCLE -- ========================================================= local function stopDynamicCamera(reason) if not state.active and state.cameraMode == "off" then return end state.active = false state.vehicle = nil state.cameraMode = "off" idle.active = false idle.idleStartTick = false idle.followCam = nil idle.followLook = nil state.currentCam = nil state.currentLook = nil state.lastYaw = nil state.accelEffect = 0 state.turnAmount = 0 state.manualSideBlend = 0 state.manualRearBlend = 0 resetCameraNative() if Config.debug then outputDebugString("[DynamicCamera] stop: " .. tostring(reason)) end end local function startDynamicCamera(vehicle) if state.active then return end if not isElement(vehicle) then return end state.active = true state.vehicle = vehicle state.cameraMode = "gameplay" state.lastTick = getTickCount() state.lastSpeed = getSpeedKMH(vehicle) state.lastYaw = getVehicleYaw(vehicle) local cx, cy, cz, lx, ly, lz = getCameraMatrix() state.currentCam = { cx, cy, cz } state.currentLook = { lx, ly, lz } if Config.debug then outputDebugString("[DynamicCamera] start") end end -- ========================================================= -- IDLE ORGANIC ORBIT -- ========================================================= local function calculateIdlePhaseFromCurrentCamera(vehicle) local cx, cy, cz if state.currentCam then cx, cy, cz = state.currentCam[1], state.currentCam[2], state.currentCam[3] else cx, cy, cz = getCameraMatrix() end local ox, oy = worldToVehicleYawOffset(vehicle, cx, cy, cz) -- O alvo orgânico usa x = cos(phase), y = sin(phase). return math.atan2(oy / math.max(IdleConfig.radiusFrontRearBase, 0.01), ox / math.max(IdleConfig.radiusSideBase, 0.01)) end local function idleStart() if idle.active then return end local vehicle = state.vehicle if not IdleConfig.enabled or not isElement(vehicle) then return end idle.active = true idle.entryTick = getTickCount() idle.phase = calculateIdlePhaseFromCurrentCamera(vehicle) state.cameraMode = "idle" if state.currentCam then idle.followCam = { state.currentCam[1], state.currentCam[2], state.currentCam[3] } else local cx, cy, cz = getCameraMatrix() idle.followCam = { cx, cy, cz } end if state.currentLook then idle.followLook = { state.currentLook[1], state.currentLook[2], state.currentLook[3] } else local _, _, _, lx, ly, lz = getCameraMatrix() idle.followLook = { lx, ly, lz } end if IdleConfig.forceVehicleStreamable and setElementStreamable then setElementStreamable(vehicle, true) end end local function idleExit(reason) if not idle.active then return end idle.active = false idle.lastCloseTick = getTickCount() idle.idleStartTick = false state.cameraMode = "gameplay" -- A chase assume a posição atual, sem setCameraTarget e sem corte. if idle.followCam then state.currentCam = { idle.followCam[1], idle.followCam[2], idle.followCam[3] } end if idle.followLook then state.currentLook = { idle.followLook[1], idle.followLook[2], idle.followLook[3] } end idle.followCam = nil idle.followLook = nil end local function updateIdleTrigger(vehicle, speed) if not IdleConfig.enabled then return end if idle.active then return end if getTickCount() - (idle.lastCloseTick or 0) < IdleConfig.closeCooldown then idle.idleStartTick = false return end if IdleConfig.cancelWhenCursorVisible and isCursorShowing() then idle.idleStartTick = false return end if getLookBehind() or getLookLeft() or getLookRight() then idle.idleStartTick = false return end if speed <= IdleConfig.minSpeedToStart then if not idle.idleStartTick then idle.idleStartTick = getTickCount() return end if getTickCount() - idle.idleStartTick >= IdleConfig.idleTimeToStart then idleStart() end else idle.idleStartTick = false end end local function renderOrganicIdle(vehicle, dt) if not idle.active then return nil end if not isElement(vehicle) then stopDynamicCamera("idle_invalid_vehicle"); return nil end local speed = getSpeedKMH(vehicle) if speed > IdleConfig.minSpeedToExit or getLookBehind() or getLookLeft() or getLookRight() then idleExit("moved_or_manual_view") return nil end idle.phase = idle.phase + (IdleConfig.orbitSpeed or 0.09) * (dt or 0.016) local p = idle.phase local t = getTickCount() / 1000 -- Tudo é contínuo. Não existe próximo perfil, próximo target fixo ou ponto zero. local sideRadius = IdleConfig.radiusSideBase + math.sin(p * 0.73 + 0.8) * IdleConfig.radiusVariation + math.sin(p * 2.15) * IdleConfig.detailVariation local frontRadius = IdleConfig.radiusFrontRearBase + math.cos(p * 0.61 - 0.4) * (IdleConfig.radiusVariation * 0.65) + math.cos(p * 2.45) * (IdleConfig.detailVariation * 0.7) local ox = math.cos(p) * sideRadius local oy = math.sin(p) * frontRadius local oz = IdleConfig.heightBase + math.sin(p * 0.82 + 1.25) * IdleConfig.heightVariation + math.sin(p * 2.20) * 0.07 oz = clamp(oz, IdleConfig.minHeight, IdleConfig.maxHeight) local lx = math.sin(p * 1.35 + 0.2) * IdleConfig.lookSideVariation local ly = math.cos(p * 1.08 - 0.35) * IdleConfig.lookFrontRearVariation local lz = IdleConfig.lookZBase + math.sin(p * 1.70 + 0.6) * IdleConfig.lookZVariation lz = clamp(lz, 0.46, IdleConfig.maxHeight - 0.18) local shake = IdleConfig.microShake or 0 if shake > 0 then ox = ox + math.sin(t * 0.67) * shake oy = oy + math.cos(t * 0.53) * shake oz = oz + math.sin(t * 0.47) * shake * 0.5 end local camX, camY, camZ = getVehicleRelativeYawPosition(vehicle, ox, oy, oz) local lookX, lookY, lookZ = getVehicleRelativeYawPosition(vehicle, lx, ly, lz) if not camX or not lookX then return nil end if not idle.followCam then idle.followCam = { camX, camY, camZ } end if not idle.followLook then idle.followLook = { lookX, lookY, lookZ } end local sinceEntry = getTickCount() - (idle.entryTick or getTickCount()) local camSmooth = IdleConfig.followSmooth local lookSmooth = IdleConfig.lookSmooth if sinceEntry < IdleConfig.firstEntryTimeMs then camSmooth = IdleConfig.firstEntrySmooth lookSmooth = math.max(IdleConfig.firstEntrySmooth, IdleConfig.lookSmooth * 0.72) end local ac = smoothAlpha(camSmooth, dt) local al = smoothAlpha(lookSmooth, dt) idle.followCam[1] = lerp(idle.followCam[1], camX, ac) idle.followCam[2] = lerp(idle.followCam[2], camY, ac) idle.followCam[3] = lerp(idle.followCam[3], camZ, ac) idle.followLook[1] = lerp(idle.followLook[1], lookX, al) idle.followLook[2] = lerp(idle.followLook[2], lookY, al) idle.followLook[3] = lerp(idle.followLook[3], lookZ, al) state.currentCam = { idle.followCam[1], idle.followCam[2], idle.followCam[3] } state.currentLook = { idle.followLook[1], idle.followLook[2], idle.followLook[3] } return { cam = state.currentCam, look = state.currentLook, } end -- ========================================================= -- GAMEPLAY CHASE -- ========================================================= local function buildGameplayTarget(vehicle, dt) local speed = getSpeedKMH(vehicle) local signedSpeed = getSignedForwardSpeedKMH(vehicle) local yaw = getVehicleYaw(vehicle) local tilt = getVehicleTilt(vehicle) local yawDelta = 0 if state.lastYaw then yawDelta = normalizeAngle(yaw - state.lastYaw) end state.lastYaw = yaw state.yawRate = lerp(state.yawRate or 0, yawDelta / math.max(dt, 0.001), smoothAlpha(6.0, dt)) local accel = (speed - (state.lastSpeed or speed)) / math.max(dt, 0.001) state.lastSpeed = speed local targetAccelEffect = 0 if accel > 4 then targetAccelEffect = -clamp(accel / 80, 0, 1) * Config.camera.accelPullMax elseif accel < -8 then targetAccelEffect = clamp((-accel) / 120, 0, 1) * Config.camera.brakePushMax end state.accelEffect = lerp(state.accelEffect, targetAccelEffect, smoothAlpha(Config.camera.accelSmooth, dt)) -- Manual views local left, right = getLookLeft(), getLookRight() local rear = getLookBehind() local sideTarget = 0 if left and not right then sideTarget = -1 end if right and not left then sideTarget = 1 end if rear then sideTarget = 0 end state.manualSide = sideTarget state.manualSideBlend = lerp(state.manualSideBlend, math.abs(sideTarget), smoothAlpha(Config.manualViews.sideSmooth, dt)) state.manualRearBlend = lerp(state.manualRearBlend, rear and 1 or 0, smoothAlpha(Config.manualViews.rearSmooth, dt)) local reverseByMotion = signedSpeed < -2.0 and clamp((-signedSpeed) / 18, 0, 1) or 0 local reverseBlend = math.max(state.manualRearBlend, reverseByMotion) reverseBlend = clamp(reverseBlend, 0, 1) local baseCam = Config.camera.forwardCamOffset local baseLook = Config.camera.forwardLookOffset local revCam = Config.camera.reverseCamOffset local revLook = Config.camera.reverseLookOffset local camOffset = { x = lerp(baseCam.x, revCam.x, reverseBlend), y = lerp(baseCam.y, revCam.y, reverseBlend), z = lerp(baseCam.z, revCam.z, reverseBlend), } local lookOffset = { x = lerp(baseLook.x, revLook.x, reverseBlend), y = lerp(baseLook.y, revLook.y, reverseBlend), z = lerp(baseLook.z, revLook.z, reverseBlend), } -- Q/E lateral, levemente angulado. if state.manualSideBlend > 0.001 and sideTarget ~= 0 then local s = sideTarget local sideCam = Config.manualViews.sideCam local sideLook = Config.manualViews.sideLook camOffset.x = lerp(camOffset.x, sideCam.x * s, state.manualSideBlend) camOffset.y = lerp(camOffset.y, sideCam.y, state.manualSideBlend) camOffset.z = lerp(camOffset.z, sideCam.z, state.manualSideBlend) lookOffset.x = lerp(lookOffset.x, sideLook.x * s, state.manualSideBlend) lookOffset.y = lerp(lookOffset.y, sideLook.y, state.manualSideBlend) lookOffset.z = lerp(lookOffset.z, sideLook.z, state.manualSideBlend) end -- Visão de curva extremamente discreta, só em gameplay e sem manual view. local turnTarget = 0 if Config.turnVision.enabled and reverseBlend < 0.1 and state.manualSideBlend < 0.05 and tilt <= Config.safety.maxTiltForEffects then local steer = 0 if getControlState("vehicle_left") then steer = steer - 1 end if getControlState("vehicle_right") then steer = steer + 1 end if Config.turnVision.invert then steer = -steer end local speedFactor = clamp((speed - Config.turnVision.minSpeed) / (Config.turnVision.fullSpeed - Config.turnVision.minSpeed), 0, 1) local yawContribution = clamp((state.yawRate or 0) * Config.turnVision.yawRateStrength, -1, 1) turnTarget = clamp((steer * Config.turnVision.inputStrength + yawContribution) * speedFactor, -1, 1) end state.turnAmount = lerp(state.turnAmount, turnTarget, smoothAlpha(Config.turnVision.smooth, dt)) camOffset.x = camOffset.x + clamp(state.turnAmount * Config.turnVision.cameraSideShift, -Config.turnVision.maxCameraSide, Config.turnVision.maxCameraSide) lookOffset.x = lookOffset.x + clamp(state.turnAmount * Config.turnVision.lookSideShift, -Config.turnVision.maxLookSide, Config.turnVision.maxLookSide) lookOffset.y = lookOffset.y + math.abs(state.turnAmount) * Config.turnVision.frontLookBoost -- Recuo/avanço físico sutil. camOffset.y = camOffset.y + state.accelEffect local camX, camY, camZ = getVehicleRelativeYawPosition(vehicle, camOffset.x, camOffset.y, camOffset.z) local lookX, lookY, lookZ = getVehicleRelativeYawPosition(vehicle, lookOffset.x, lookOffset.y, lookOffset.z) return { cam = { camX, camY, camZ }, look = { lookX, lookY, lookZ } } end local function renderGameplay(vehicle, dt) local target = buildGameplayTarget(vehicle, dt) if not target then return nil end if not state.currentCam then state.currentCam = { target.cam[1], target.cam[2], target.cam[3] } end if not state.currentLook then state.currentLook = { target.look[1], target.look[2], target.look[3] } end local ac = smoothAlpha(Config.camera.positionSmooth, dt) local al = smoothAlpha(Config.camera.lookSmooth, dt) state.currentCam[1] = lerp(state.currentCam[1], target.cam[1], ac) state.currentCam[2] = lerp(state.currentCam[2], target.cam[2], ac) state.currentCam[3] = lerp(state.currentCam[3], target.cam[3], ac) state.currentLook[1] = lerp(state.currentLook[1], target.look[1], al) state.currentLook[2] = lerp(state.currentLook[2], target.look[2], al) state.currentLook[3] = lerp(state.currentLook[3], target.look[3], al) return { cam = state.currentCam, look = state.currentLook } end -- ========================================================= -- MAIN RENDER -- ========================================================= local function updateFov(vehicle, speed, dt) if not Config.fov.enabled then return end local targetFov = Config.fov.default if state.cameraMode == "gameplay" then local p = clamp(speed / Config.fov.speedForMax, 0, 1) targetFov = Config.fov.default + (Config.fov.max - Config.fov.default) * p end local a = smoothAlpha(Config.fov.smooth, dt) local fov = lerp(state.currentFov or Config.fov.default, targetFov, a) applyFov(fov) end local function onCameraPreRender(timeSlice) if not Config.enabled then if state.active then stopDynamicCamera("disabled") end return end local now = getTickCount() local dt = (timeSlice and timeSlice > 0) and (timeSlice / 1000) or ((now - (state.lastTick or now)) / 1000) dt = clamp(dt, 0.001, 0.05) state.lastTick = now local vehicle = getDriverVehicle() if not vehicle then stopDynamicCamera("no_driver_vehicle") return end if not state.active or state.vehicle ~= vehicle then startDynamicCamera(vehicle) end local speed = getSpeedKMH(vehicle) updateIdleTrigger(vehicle, speed) updateFov(vehicle, speed, dt) local result if idle.active then state.cameraMode = "idle" result = renderOrganicIdle(vehicle, dt) if result then setCameraMatrix(result.cam[1], result.cam[2], result.cam[3], result.look[1], result.look[2], result.look[3]) return end end -- Enquanto idle estiver ativa, gameplay não roda no mesmo frame. if idle.active then return end state.cameraMode = "gameplay" result = renderGameplay(vehicle, dt) if result then setCameraMatrix(result.cam[1], result.cam[2], result.cam[3], result.look[1], result.look[2], result.look[3]) end end addEventHandler("onClientPreRender", root, onCameraPreRender) -- ========================================================= -- INPUT / EVENTS -- ========================================================= bindKey("mouse3", "down", function() state.mouse3Held = true end) bindKey("mouse3", "up", function() state.mouse3Held = false end) bindKey("q", "down", function() state.qHeld = true end) bindKey("q", "up", function() state.qHeld = false end) bindKey("e", "down", function() state.eHeld = true end) bindKey("e", "up", function() state.eHeld = false end) addEventHandler("onClientVehicleExit", root, function(player) if player == localPlayer then stopDynamicCamera("vehicle_exit") end end) addEventHandler("onClientPlayerWasted", localPlayer, function() stopDynamicCamera("player_wasted") end) addEventHandler("onClientElementDestroy", root, function() if source == state.vehicle then stopDynamicCamera("vehicle_destroyed") end end) addEventHandler("onClientResourceStop", resourceRoot, function() resetCameraNative() end) -- ========================================================= -- COMMANDS -- ========================================================= addCommandHandler("dyncam", function() Config.enabled = not Config.enabled if not Config.enabled then stopDynamicCamera("command_disabled") end outputChatBox("#ff0048[DynamicCamera] #ffffffSistema: " .. (Config.enabled and "ligado" or "desligado"), 255,255,255,true) end) addCommandHandler("dyncamreset", function() stopDynamicCamera("manual_reset") outputChatBox("#ff0048[DynamicCamera] #ffffffCâmera resetada.", 255,255,255,true) end) addCommandHandler("dyncaminfo", function() local speed = state.vehicle and isElement(state.vehicle) and getSpeedKMH(state.vehicle) or 0 outputChatBox("#ff0048[DynamicCamera] #ffffffv1.0.29 | modo: " .. tostring(state.cameraMode) .. " | idle: " .. tostring(idle.active) .. " | speed: " .. string.format("%.1f", speed) .. " km/h", 255,255,255,true) end) if IdleConfig.enableTestCommands then addCommandHandler("testcine", function() local vehicle = getDriverVehicle() if not vehicle then outputChatBox("#ff0048[DynamicCamera] #ffffffVocê precisa estar dirigindo um veículo.", 255,255,255,true) return end if not state.active then startDynamicCamera(vehicle) end idleStart() outputChatBox("#ff0048[DynamicCamera] #ffffffIdle Organic ativada para teste.", 255,255,255,true) end) addCommandHandler("stopcine", function() if idle.active then idleExit("manual_stopcine") outputChatBox("#ff0048[DynamicCamera] #ffffffIdle Organic desligada.", 255,255,255,true) else outputChatBox("#ff0048[DynamicCamera] #ffffffIdle já está desligada.", 255,255,255,true) end end) end